| author | |
| committer | |
| log | 579159d6c6e96f9c18358e492fbf0b8af740e80c |
| tree | 7a1a8b34277431238c5a09ee1044d3f86e0010ff |
| parent | c8a603307c2d50f8fb9d302f4b51f20fd9f91b33 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>19 files changed, 228 insertions(+), 516 deletions(-)
apps/web/src/components/CommandPalette.tsx+42-178| ... | ... | @@ -14,7 +14,15 @@ import { |
| 14 | 14 | getNewProjectGitHubTarget, |
| 15 | 15 | getNewProjectPathPreview, |
| 16 | 16 | normalizePastedCloneUrl, |
| 17 | addProjectRemoteSourceLabel, | |
| 18 | addProjectRemoteSourcePathHint, | |
| 19 | addProjectRemoteSourceProvider, | |
| 20 | buildAddProjectRemoteSourceReadiness, | |
| 21 | sortAddProjectProviderSources, | |
| 22 | type AddProjectRemoteSource, | |
| 23 | type AddProjectRemoteSourceReadiness, | |
| 17 | 24 | } from "@t3tools/client-runtime/operations/projects"; |
| 25 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 18 | 26 | import { connectionStatusText } from "@t3tools/client-runtime/connection"; |
| 19 | 27 | import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; |
| 20 | 28 | import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference"; |
| ... | ... | @@ -187,14 +195,8 @@ import { |
| 187 | 195 | CommandPaletteVirtualizedResults, |
| 188 | 196 | scrollCommandPaletteRowIntoView, |
| 189 | 197 | } from "./CommandPaletteResults"; |
| 190 | import { | |
| 191 | AzureDevOpsIcon, | |
| 192 | BitbucketIcon, | |
| 193 | GitCafeIcon, | |
| 194 | GitHubIcon, | |
| 195 | GitLabIcon, | |
| 196 | ForgejoIcon, | |
| 197 | } from "./Icons"; | |
| 198 | import { GitHubIcon } from "./Icons"; | |
| 199 | import { sourceControlIcon } from "~/sourceControlPresentation"; | |
| 198 | 200 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| 199 | 201 | import { Checkbox } from "./ui/checkbox"; |
| 200 | 202 | import { ProjectFavicon } from "./ProjectFavicon"; |
| ... | ... | @@ -257,15 +259,6 @@ interface AddProjectEnvironmentOption { |
| 257 | 259 | readonly status: string; |
| 258 | 260 | } |
| 259 | 261 | |
| 260 | type AddProjectRemoteProviderKind = | |
| 261 | | "github" | |
| 262 | | "gitlab" | |
| 263 | | "forgejo" | |
| 264 | | "bitbucket" | |
| 265 | | "azure-devops" | |
| 266 | | "gitcafe"; | |
| 267 | type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; | |
| 268 | ||
| 269 | 262 | type AddProjectCloneFlow = |
| 270 | 263 | | { |
| 271 | 264 | readonly step: "repository"; |
| ... | ... | @@ -281,83 +274,10 @@ type AddProjectCloneFlow = |
| 281 | 274 | readonly remoteUrl: string; |
| 282 | 275 | }; |
| 283 | 276 | |
| 284 | const REMOTE_PROJECT_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [ | |
| 285 | "url", | |
| 286 | "github", | |
| 287 | "gitlab", | |
| 288 | "forgejo", | |
| 289 | "bitbucket", | |
| 290 | "azure-devops", | |
| 291 | "gitcafe", | |
| 292 | ]; | |
| 293 | const REMOTE_PROJECT_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [ | |
| 294 | "github", | |
| 295 | "gitlab", | |
| 296 | "forgejo", | |
| 297 | "bitbucket", | |
| 298 | "azure-devops", | |
| 299 | "gitcafe", | |
| 300 | ]; | |
| 301 | ||
| 302 | function remoteProjectSourceLabel(source: AddProjectRemoteSource): string { | |
| 303 | switch (source) { | |
| 304 | case "github": | |
| 305 | return "GitHub"; | |
| 306 | case "forgejo": | |
| 307 | return "Forgejo / Gitea"; | |
| 308 | case "gitlab": | |
| 309 | return "GitLab"; | |
| 310 | case "bitbucket": | |
| 311 | return "Bitbucket"; | |
| 312 | case "azure-devops": | |
| 313 | return "Azure DevOps"; | |
| 314 | case "gitcafe": | |
| 315 | return "GitCafe"; | |
| 316 | case "url": | |
| 317 | return "Git URL"; | |
| 318 | } | |
| 319 | } | |
| 320 | ||
| 321 | function remoteProjectSourcePathHint(source: AddProjectRemoteSource): string { | |
| 322 | switch (source) { | |
| 323 | case "forgejo": | |
| 324 | case "github": | |
| 325 | case "gitcafe": | |
| 326 | return "owner/repo"; | |
| 327 | case "gitlab": | |
| 328 | return "group/project"; | |
| 329 | case "bitbucket": | |
| 330 | return "workspace/repository"; | |
| 331 | case "azure-devops": | |
| 332 | return "project/repository"; | |
| 333 | case "url": | |
| 334 | return "URL"; | |
| 335 | } | |
| 336 | } | |
| 337 | ||
| 338 | function remoteProjectSourceProvider( | |
| 339 | source: AddProjectRemoteSource, | |
| 340 | ): AddProjectRemoteProviderKind | null { | |
| 341 | return source === "url" ? null : source; | |
| 342 | } | |
| 343 | ||
| 344 | 277 | function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: string): ReactNode { |
| 345 | switch (source) { | |
| 346 | case "github": | |
| 347 | return <GitHubIcon className={className} />; | |
| 348 | case "forgejo": | |
| 349 | return <ForgejoIcon className={className} />; | |
| 350 | case "gitlab": | |
| 351 | return <GitLabIcon className={className} />; | |
| 352 | case "bitbucket": | |
| 353 | return <BitbucketIcon className={className} />; | |
| 354 | case "azure-devops": | |
| 355 | return <AzureDevOpsIcon className={className} />; | |
| 356 | case "gitcafe": | |
| 357 | return <GitCafeIcon className={className} />; | |
| 358 | case "url": | |
| 359 | return <LinkIcon className={className} />; | |
| 360 | } | |
| 278 | if (source === "url") return <LinkIcon className={className} />; | |
| 279 | const Icon = sourceControlIcon(sourceControlClients.get(source)); | |
| 280 | return <Icon className={className} />; | |
| 361 | 281 | } |
| 362 | 282 | |
| 363 | 283 | function projectFaviconIcon(project: Project): ReactNode { |
| ... | ... | @@ -370,82 +290,26 @@ function remoteProjectInputPlaceholder(flow: AddProjectCloneFlow | null): string |
| 370 | 290 | if (flow.source === "url") { |
| 371 | 291 | return "Enter Git clone URL"; |
| 372 | 292 | } |
| 373 | return `Enter ${remoteProjectSourceLabel(flow.source)} repository (${remoteProjectSourcePathHint(flow.source)})`; | |
| 374 | } | |
| 375 | ||
| 376 | function sourceProviderKind(source: AddProjectRemoteSource): AddProjectRemoteProviderKind | null { | |
| 377 | return source === "url" ? null : source; | |
| 378 | } | |
| 379 | ||
| 380 | function sortAddProjectProviderSources( | |
| 381 | readinessBySource: AddProjectRemoteSourceReadiness, | |
| 382 | ): ReadonlyArray<AddProjectRemoteProviderKind> { | |
| 383 | return REMOTE_PROJECT_PROVIDER_SOURCES.toSorted((left, right) => { | |
| 384 | const leftReady = readinessBySource[left].ready; | |
| 385 | const rightReady = readinessBySource[right].ready; | |
| 386 | if (leftReady !== rightReady) { | |
| 387 | return leftReady ? -1 : 1; | |
| 388 | } | |
| 389 | return remoteProjectSourceLabel(left).localeCompare(remoteProjectSourceLabel(right)); | |
| 390 | }); | |
| 293 | return `Enter ${addProjectRemoteSourceLabel(flow.source)} repository (${addProjectRemoteSourcePathHint(flow.source)})`; | |
| 391 | 294 | } |
| 392 | 295 | |
| 393 | type AddProjectRemoteSourceReadiness = Record< | |
| 394 | AddProjectRemoteSource, | |
| 395 | { readonly ready: boolean; readonly hint: string | null } | |
| 396 | >; | |
| 397 | ||
| 398 | function buildAddProjectRemoteSourceReadiness( | |
| 296 | /** The palette's readiness, pointing at Settings by its menu path. */ | |
| 297 | function buildPaletteRemoteSourceReadiness( | |
| 399 | 298 | discovery: SourceControlDiscoveryResult | null, |
| 400 | 299 | ): AddProjectRemoteSourceReadiness { |
| 401 | const unavailable = { | |
| 402 | ready: false, | |
| 403 | hint: "Provider status unavailable. Open Settings -> Source Control and rescan.", | |
| 404 | } as const; | |
| 405 | const defaultReadiness: AddProjectRemoteSourceReadiness = { | |
| 406 | url: { ready: true, hint: null }, | |
| 407 | github: unavailable, | |
| 408 | gitlab: unavailable, | |
| 409 | forgejo: unavailable, | |
| 410 | bitbucket: unavailable, | |
| 411 | "azure-devops": unavailable, | |
| 412 | gitcafe: unavailable, | |
| 300 | const readiness = buildAddProjectRemoteSourceReadiness(discovery); | |
| 301 | return (source) => { | |
| 302 | const entry = readiness(source); | |
| 303 | return entry.hint === null | |
| 304 | ? entry | |
| 305 | : { | |
| 306 | ...entry, | |
| 307 | hint: entry.hint.replace( | |
| 308 | "Open Source Control settings", | |
| 309 | "Open Settings -> Source Control", | |
| 310 | ), | |
| 311 | }; | |
| 413 | 312 | }; |
| 414 | ||
| 415 | if (!discovery) { | |
| 416 | return defaultReadiness; | |
| 417 | } | |
| 418 | ||
| 419 | const providerByKind = new Map( | |
| 420 | discovery.sourceControlProviders.map((provider) => [provider.kind, provider]), | |
| 421 | ); | |
| 422 | const readiness = { ...defaultReadiness }; | |
| 423 | ||
| 424 | for (const source of REMOTE_PROJECT_SOURCES) { | |
| 425 | const kind = sourceProviderKind(source); | |
| 426 | if (!kind) continue; | |
| 427 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); | |
| 428 | if (!provider) { | |
| 429 | readiness[source] = unavailable; | |
| 430 | continue; | |
| 431 | } | |
| 432 | if (provider.status !== "available") { | |
| 433 | readiness[source] = { ready: false, hint: provider.installHint }; | |
| 434 | continue; | |
| 435 | } | |
| 436 | if (provider.auth.status === "unauthenticated") { | |
| 437 | readiness[source] = { | |
| 438 | ready: false, | |
| 439 | hint: | |
| 440 | Option.getOrNull(provider.auth.detail) ?? | |
| 441 | `${provider.label} is not authenticated. Open Settings -> Source Control for setup guidance.`, | |
| 442 | }; | |
| 443 | continue; | |
| 444 | } | |
| 445 | readiness[source] = { ready: true, hint: null }; | |
| 446 | } | |
| 447 | ||
| 448 | return readiness; | |
| 449 | 313 | } |
| 450 | 314 | |
| 451 | 315 | function errorMessage(error: unknown): string { |
| ... | ... | @@ -1673,13 +1537,13 @@ function OpenCommandPaletteDialog(props: { |
| 1673 | 1537 | ]; |
| 1674 | 1538 | |
| 1675 | 1539 | for (const source of orderedSources) { |
| 1676 | const label = remoteProjectSourceLabel(source); | |
| 1540 | const label = addProjectRemoteSourceLabel(source); | |
| 1677 | 1541 | const title = source === "url" ? "Git URL" : `${label} repository`; |
| 1678 | 1542 | const description = |
| 1679 | 1543 | source === "url" |
| 1680 | 1544 | ? "Clone from a remote URL" |
| 1681 | : `Clone ${label} ${remoteProjectSourcePathHint(source)}`; | |
| 1682 | const readiness = readinessBySource[source]; | |
| 1545 | : `Clone ${label} ${addProjectRemoteSourcePathHint(source)}`; | |
| 1546 | const readiness = readinessBySource(source); | |
| 1683 | 1547 | const disabledHint = readiness.hint; |
| 1684 | 1548 | |
| 1685 | 1549 | const titleTrailingContent = readiness.ready ? undefined : ( |
| ... | ... | @@ -1767,7 +1631,7 @@ function OpenCommandPaletteDialog(props: { |
| 1767 | 1631 | addonIcon: <FolderPlusIcon className={ADDON_ICON_CLASS} />, |
| 1768 | 1632 | groups: buildAddProjectSourceGroups( |
| 1769 | 1633 | environmentId, |
| 1770 | buildAddProjectRemoteSourceReadiness( | |
| 1634 | buildPaletteRemoteSourceReadiness( | |
| 1771 | 1635 | browseEnvironmentId === environmentId ? sourceControlDiscovery.data : null, |
| 1772 | 1636 | ), |
| 1773 | 1637 | ), |
| ... | ... | @@ -2105,13 +1969,13 @@ function OpenCommandPaletteDialog(props: { |
| 2105 | 1969 | "repository", |
| 2106 | 1970 | "repo", |
| 2107 | 1971 | "git", |
| 2108 | "github", | |
| 2109 | "gitlab", | |
| 2110 | "forgejo", | |
| 2111 | "bitbucket", | |
| 2112 | "azure", | |
| 2113 | "devops", | |
| 2114 | "gitcafe", | |
| 1972 | // Every host's name, spelled as a word: "azure devops" searches as `azure` and `devops`. | |
| 1973 | ...sourceControlClients.definitions.flatMap((definition) => | |
| 1974 | definition.pickerLabel | |
| 1975 | .toLowerCase() | |
| 1976 | .split(/[^a-z0-9]+/u) | |
| 1977 | .filter(Boolean), | |
| 1978 | ), | |
| 2115 | 1979 | "url", |
| 2116 | 1980 | "environment", |
| 2117 | 1981 | ], |
| ... | ... | @@ -2356,7 +2220,7 @@ function OpenCommandPaletteDialog(props: { |
| 2356 | 2220 | currentView.groups[0]?.value === sourceSelectionViewValue |
| 2357 | 2221 | ? buildAddProjectSourceGroups( |
| 2358 | 2222 | addProjectEnvironmentId, |
| 2359 | buildAddProjectRemoteSourceReadiness(sourceControlDiscovery.data), | |
| 2223 | buildPaletteRemoteSourceReadiness(sourceControlDiscovery.data), | |
| 2360 | 2224 | ) |
| 2361 | 2225 | : currentView?.groups[0]?.value === "themes" |
| 2362 | 2226 | ? changeThemeItem.groups |
| ... | ... | @@ -2606,7 +2470,7 @@ function OpenCommandPaletteDialog(props: { |
| 2606 | 2470 | return; |
| 2607 | 2471 | } |
| 2608 | 2472 | |
| 2609 | const provider = remoteProjectSourceProvider(addProjectCloneFlow.source); | |
| 2473 | const provider = addProjectRemoteSourceProvider(addProjectCloneFlow.source); | |
| 2610 | 2474 | if (!provider) { |
| 2611 | 2475 | const destinationPath = getCloneDestinationPath( |
| 2612 | 2476 | getDefaultCloneParentPath(addProjectCloneFlow.environmentId), |
| ... | ... | @@ -2631,7 +2495,7 @@ function OpenCommandPaletteDialog(props: { |
| 2631 | 2495 | const lookupResult = await lookupRepository({ |
| 2632 | 2496 | environmentId: addProjectCloneFlow.environmentId, |
| 2633 | 2497 | input: { |
| 2634 | provider: SourceControlProviderKind.make(provider), | |
| 2498 | provider, | |
| 2635 | 2499 | repository: rawRepository, |
| 2636 | 2500 | }, |
| 2637 | 2501 | }); |
apps/web/src/components/GitActionsControl.logic.ts+5-10| ... | ... | @@ -4,11 +4,8 @@ import type { |
| 4 | 4 | VcsStatusResult, |
| 5 | 5 | } from "@t3tools/contracts"; |
| 6 | 6 | import { isTemporaryWorktreeBranch } from "@t3tools/shared/git"; |
| 7 | import { | |
| 8 | DEFAULT_CHANGE_REQUEST_TERMINOLOGY, | |
| 9 | getChangeRequestTerminology, | |
| 10 | type ChangeRequestTerminology, | |
| 11 | } from "../sourceControlPresentation"; | |
| 7 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 8 | import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients"; | |
| 12 | 9 | |
| 13 | 10 | export type GitActionIconName = "commit" | "push" | "pr"; |
| 14 | 11 | |
| ... | ... | @@ -68,9 +65,7 @@ export function resolveGitActionResultToastTiming( |
| 68 | 65 | function resolveChangeRequestTerminology( |
| 69 | 66 | gitStatus: VcsStatusResult | null, |
| 70 | 67 | ): ChangeRequestTerminology { |
| 71 | return gitStatus?.sourceControlProvider | |
| 72 | ? getChangeRequestTerminology(gitStatus.sourceControlProvider) | |
| 73 | : DEFAULT_CHANGE_REQUEST_TERMINOLOGY; | |
| 68 | return sourceControlClients.get(gitStatus?.sourceControlProvider?.kind).changeRequest; | |
| 74 | 69 | } |
| 75 | 70 | |
| 76 | 71 | export function resolveGitActionProgressPresentation(input: { |
| ... | ... | @@ -129,7 +124,7 @@ export function buildGitActionProgressStages(input: { |
| 129 | 124 | shouldPushBeforePr?: boolean; |
| 130 | 125 | terminology?: ChangeRequestTerminology; |
| 131 | 126 | }): string[] { |
| 132 | const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY; | |
| 127 | const terminology = input.terminology ?? sourceControlClients.get(undefined).changeRequest; | |
| 133 | 128 | const branchStages = input.featureBranch ? ["Preparing feature ref..."] : []; |
| 134 | 129 | const pushStage = input.pushTarget ? `Pushing to ${input.pushTarget}...` : "Pushing..."; |
| 135 | 130 | const prStages = [ |
| ... | ... | @@ -406,7 +401,7 @@ export function resolveDefaultBranchActionDialogCopy(input: { |
| 406 | 401 | }): DefaultBranchActionDialogCopy { |
| 407 | 402 | const branchLabel = input.branchName; |
| 408 | 403 | const suffix = ` on "${branchLabel}". You can continue on this ref or create a feature ref and run the same action there.`; |
| 409 | const terminology = input.terminology ?? DEFAULT_CHANGE_REQUEST_TERMINOLOGY; | |
| 404 | const terminology = input.terminology ?? sourceControlClients.get(undefined).changeRequest; | |
| 410 | 405 | |
| 411 | 406 | if (input.action === "push" || input.action === "commit_push") { |
| 412 | 407 | if (input.includesCommit) { |
apps/web/src/components/GitActionsControl.tsx+44-122| ... | ... | @@ -44,14 +44,6 @@ import { |
| 44 | 44 | GlobeIcon, |
| 45 | 45 | } from "lucide-react"; |
| 46 | 46 | import { Radio as RadioPrimitive } from "@base-ui/react/radio"; |
| 47 | import { | |
| 48 | AzureDevOpsIcon, | |
| 49 | BitbucketIcon, | |
| 50 | GitHubIcon, | |
| 51 | GitLabIcon, | |
| 52 | ForgejoIcon, | |
| 53 | GitCafeIcon, | |
| 54 | } from "~/components/Icons"; | |
| 55 | 47 | import { RadioGroup } from "~/components/ui/radio-group"; |
| 56 | 48 | import { Spinner } from "~/components/ui/spinner"; |
| 57 | 49 | import { Toggle, ToggleGroup } from "~/components/ui/toggle-group"; |
| ... | ... | @@ -131,7 +123,8 @@ import { |
| 131 | 123 | THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS, |
| 132 | 124 | THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS, |
| 133 | 125 | } from "./chat/threadDetailsPanelStyles"; |
| 134 | import { getSourceControlPresentation } from "~/sourceControlPresentation"; | |
| 126 | import { getSourceControlPresentation, sourceControlIcon } from "~/sourceControlPresentation"; | |
| 127 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 135 | 128 | import { useOpenLink } from "~/browser/useOpenLink"; |
| 136 | 129 | |
| 137 | 130 | interface GitActionsControlProps { |
| ... | ... | @@ -158,14 +151,6 @@ interface PendingDefaultBranchAction { |
| 158 | 151 | filePaths?: string[]; |
| 159 | 152 | } |
| 160 | 153 | |
| 161 | type PublishProviderKind = | |
| 162 | | "github" | |
| 163 | | "gitlab" | |
| 164 | | "forgejo" | |
| 165 | | "bitbucket" | |
| 166 | | "azure-devops" | |
| 167 | | "gitcafe"; | |
| 168 | ||
| 169 | 154 | type GitActionToastId = ReturnType<typeof toastManager.add>; |
| 170 | 155 | |
| 171 | 156 | interface RunGitActionWithToastInput { |
| ... | ... | @@ -203,77 +188,25 @@ function requestVcsStatusRefresh( |
| 203 | 188 | } |
| 204 | 189 | const RUNNING_SOURCE_CONTROL_ACTIONS = ["runStackedAction", "pull", "publishRepository"] as const; |
| 205 | 190 | |
| 206 | const PUBLISH_PROVIDER_OPTIONS = [ | |
| 207 | { | |
| 208 | value: "forgejo", | |
| 209 | label: "Forgejo / Gitea", | |
| 210 | description: "Your signed-in server", | |
| 211 | host: "your server", | |
| 212 | pathPlaceholder: "owner/repo", | |
| 213 | Icon: ForgejoIcon, | |
| 214 | }, | |
| 215 | { | |
| 216 | value: "github", | |
| 217 | label: "GitHub", | |
| 218 | description: "github.com", | |
| 219 | host: "github.com", | |
| 220 | pathPlaceholder: "owner/repo", | |
| 221 | Icon: GitHubIcon, | |
| 222 | }, | |
| 223 | { | |
| 224 | value: "gitlab", | |
| 225 | label: "GitLab", | |
| 226 | description: "gitlab.com", | |
| 227 | host: "gitlab.com", | |
| 228 | pathPlaceholder: "group/project", | |
| 229 | Icon: GitLabIcon, | |
| 230 | }, | |
| 231 | { | |
| 232 | value: "bitbucket", | |
| 233 | label: "Bitbucket", | |
| 234 | description: "bitbucket.org", | |
| 235 | host: "bitbucket.org", | |
| 236 | pathPlaceholder: "workspace/repository", | |
| 237 | Icon: BitbucketIcon, | |
| 238 | }, | |
| 239 | { | |
| 240 | value: "azure-devops", | |
| 241 | label: "Azure DevOps", | |
| 242 | description: "dev.azure.com", | |
| 243 | host: "dev.azure.com", | |
| 244 | pathPlaceholder: "project/repository", | |
| 245 | Icon: AzureDevOpsIcon, | |
| 246 | }, | |
| 247 | { | |
| 248 | value: "gitcafe", | |
| 249 | label: "GitCafe", | |
| 250 | description: "git.cafe", | |
| 251 | host: "git.cafe", | |
| 252 | pathPlaceholder: "owner/repo", | |
| 253 | Icon: GitCafeIcon, | |
| 254 | }, | |
| 255 | ] as const satisfies ReadonlyArray<{ | |
| 256 | readonly value: PublishProviderKind; | |
| 257 | readonly label: string; | |
| 258 | readonly description: string; | |
| 259 | readonly host: string; | |
| 260 | readonly pathPlaceholder: string; | |
| 261 | readonly Icon: typeof GitHubIcon; | |
| 262 | }>; | |
| 263 | ||
| 264 | function publishProviderOption(provider: PublishProviderKind) { | |
| 191 | /** Every built-in host, in the order the built-in definitions list them. */ | |
| 192 | const PUBLISH_PROVIDER_OPTIONS = sourceControlClients.definitions.map((definition) => ({ | |
| 193 | value: definition.kind, | |
| 194 | label: definition.pickerLabel, | |
| 195 | description: definition.publishDescription, | |
| 196 | pathPlaceholder: definition.repositoryPathHint, | |
| 197 | definition, | |
| 198 | Icon: sourceControlIcon(definition), | |
| 199 | })); | |
| 200 | ||
| 201 | function publishProviderOption(provider: SourceControlProviderKind) { | |
| 265 | 202 | return ( |
| 266 | 203 | PUBLISH_PROVIDER_OPTIONS.find((option) => option.value === provider) ?? |
| 267 | PUBLISH_PROVIDER_OPTIONS[0] | |
| 204 | PUBLISH_PROVIDER_OPTIONS[0]! | |
| 268 | 205 | ); |
| 269 | 206 | } |
| 270 | 207 | |
| 271 | function isPublishProviderKind(provider: string): provider is PublishProviderKind { | |
| 272 | return PUBLISH_PROVIDER_OPTIONS.some((option) => option.value === provider); | |
| 273 | } | |
| 274 | ||
| 275 | 208 | function getPublishProviderReadiness(input: { |
| 276 | provider: PublishProviderKind; | |
| 209 | provider: SourceControlProviderKind; | |
| 277 | 210 | sourceControlProviders: ReadonlyArray<SourceControlProviderDiscoveryItem>; |
| 278 | 211 | }): { readonly ready: boolean; readonly hint: string | null } { |
| 279 | 212 | const discovered = input.sourceControlProviders.find( |
| ... | ... | @@ -553,7 +486,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 553 | 486 | }), |
| 554 | 487 | ); |
| 555 | 488 | const [selectedPublishProvider, setSelectedPublishProvider] = |
| 556 | useState<PublishProviderKind | null>(null); | |
| 489 | useState<SourceControlProviderKind | null>(null); | |
| 557 | 490 | const [publishRepositoryOverride, setPublishRepositoryOverride] = useState<string | null>(null); |
| 558 | 491 | const [publishVisibility, setPublishVisibility] = |
| 559 | 492 | useState<SourceControlRepositoryVisibility>("private"); |
| ... | ... | @@ -574,43 +507,32 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 574 | 507 | ); |
| 575 | 508 | const publishRepositoryAction = useSourceControlPublishRepositoryAction(sourceControlScope); |
| 576 | 509 | const publishAccountByProvider = useMemo(() => { |
| 577 | const accounts: Record<PublishProviderKind, string | null> = { | |
| 578 | github: null, | |
| 579 | gitlab: null, | |
| 580 | forgejo: null, | |
| 581 | bitbucket: null, | |
| 582 | "azure-devops": null, | |
| 583 | gitcafe: null, | |
| 584 | }; | |
| 510 | const accounts = new Map<string, string | null>(); | |
| 585 | 511 | for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) { |
| 586 | const kind: string = provider.kind; | |
| 587 | if (isPublishProviderKind(kind)) { | |
| 588 | accounts[kind] = Option.getOrNull(provider.auth.account); | |
| 589 | } | |
| 512 | accounts.set(provider.kind, Option.getOrNull(provider.auth.account)); | |
| 590 | 513 | } |
| 591 | 514 | return accounts; |
| 592 | 515 | }, [sourceControlDiscovery.data]); |
| 593 | 516 | const publishProviderReadiness = useMemo(() => { |
| 594 | 517 | const sourceControlProviders = sourceControlDiscovery.data?.sourceControlProviders ?? []; |
| 595 | return Object.fromEntries( | |
| 518 | const readiness = new Map( | |
| 596 | 519 | PUBLISH_PROVIDER_OPTIONS.map((option) => [ |
| 597 | 520 | option.value, |
| 598 | getPublishProviderReadiness({ | |
| 599 | provider: option.value, | |
| 600 | sourceControlProviders, | |
| 601 | }), | |
| 521 | getPublishProviderReadiness({ provider: option.value, sourceControlProviders }), | |
| 602 | 522 | ]), |
| 603 | ) as Record<PublishProviderKind, { readonly ready: boolean; readonly hint: string | null }>; | |
| 523 | ); | |
| 524 | return (provider: SourceControlProviderKind) => | |
| 525 | readiness.get(provider) ?? { ready: false, hint: null }; | |
| 604 | 526 | }, [sourceControlDiscovery.data]); |
| 605 | 527 | const hasReadyPublishProvider = useMemo( |
| 606 | () => PUBLISH_PROVIDER_OPTIONS.some((option) => publishProviderReadiness[option.value].ready), | |
| 528 | () => PUBLISH_PROVIDER_OPTIONS.some((option) => publishProviderReadiness(option.value).ready), | |
| 607 | 529 | [publishProviderReadiness], |
| 608 | 530 | ); |
| 609 | 531 | const sortedPublishProviderOptions = useMemo( |
| 610 | 532 | () => |
| 611 | 533 | PUBLISH_PROVIDER_OPTIONS.toSorted((left, right) => { |
| 612 | const leftReady = publishProviderReadiness[left.value].ready; | |
| 613 | const rightReady = publishProviderReadiness[right.value].ready; | |
| 534 | const leftReady = publishProviderReadiness(left.value).ready; | |
| 535 | const rightReady = publishProviderReadiness(right.value).ready; | |
| 614 | 536 | if (leftReady !== rightReady) { |
| 615 | 537 | return leftReady ? -1 : 1; |
| 616 | 538 | } |
| ... | ... | @@ -619,26 +541,26 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 619 | 541 | [publishProviderReadiness], |
| 620 | 542 | ); |
| 621 | 543 | const firstReadyPublishProvider = sortedPublishProviderOptions.find( |
| 622 | (option) => publishProviderReadiness[option.value].ready, | |
| 544 | (option) => publishProviderReadiness(option.value).ready, | |
| 623 | 545 | )?.value; |
| 624 | 546 | const publishProvider = |
| 625 | selectedPublishProvider !== null && publishProviderReadiness[selectedPublishProvider].ready | |
| 547 | selectedPublishProvider !== null && publishProviderReadiness(selectedPublishProvider).ready | |
| 626 | 548 | ? selectedPublishProvider |
| 627 | : (firstReadyPublishProvider ?? selectedPublishProvider ?? "github"); | |
| 628 | const selectedPublishProviderReadiness = publishProviderReadiness[publishProvider]; | |
| 629 | const publishRepositoryPrefill = publishAccountByProvider[publishProvider] | |
| 630 | ? `${publishAccountByProvider[publishProvider]}/` | |
| 631 | : ""; | |
| 549 | : (firstReadyPublishProvider ?? | |
| 550 | selectedPublishProvider ?? | |
| 551 | sourceControlClients.get(undefined).kind); | |
| 552 | const selectedPublishProviderReadiness = publishProviderReadiness(publishProvider); | |
| 553 | const publishAccount = publishAccountByProvider.get(publishProvider) ?? null; | |
| 554 | const publishRepositoryPrefill = publishAccount ? `${publishAccount}/` : ""; | |
| 632 | 555 | const publishRepository = publishRepositoryOverride ?? publishRepositoryPrefill; |
| 633 | 556 | const currentPublishProvider = publishProviderOption(publishProvider); |
| 634 | const publishHost = | |
| 635 | publishProvider === "forgejo" | |
| 636 | ? (Option.getOrNull( | |
| 637 | sourceControlDiscovery.data?.sourceControlProviders.find( | |
| 638 | (provider) => provider.kind === "forgejo", | |
| 639 | )?.auth.host ?? Option.none(), | |
| 640 | ) ?? currentPublishProvider.host) | |
| 641 | : currentPublishProvider.host; | |
| 557 | const publishHost = currentPublishProvider.definition.publishHost( | |
| 558 | Option.getOrNull( | |
| 559 | sourceControlDiscovery.data?.sourceControlProviders.find( | |
| 560 | (provider) => provider.kind === publishProvider, | |
| 561 | )?.auth.host ?? Option.none(), | |
| 562 | ), | |
| 563 | ); | |
| 642 | 564 | const publishPathPlaceholder = currentPublishProvider.pathPlaceholder; |
| 643 | 565 | const publishProviderLabel = currentPublishProvider.label; |
| 644 | 566 | const publishWizardSteps = ["Provider", "Repository", "Summary"] as const; |
| ... | ... | @@ -672,7 +594,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 672 | 594 | |
| 673 | 595 | void (async () => { |
| 674 | 596 | const result = await publishRepositoryAction.run({ |
| 675 | provider: SourceControlProviderKind.make(publishProvider), | |
| 597 | provider: publishProvider, | |
| 676 | 598 | repository: publishRepository.trim(), |
| 677 | 599 | visibility: publishVisibility, |
| 678 | 600 | remoteName: publishRemoteName.trim() || "origin", |
| ... | ... | @@ -757,14 +679,14 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) { |
| 757 | 679 | <RadioGroup |
| 758 | 680 | value={publishProvider} |
| 759 | 681 | onValueChange={(value) => { |
| 760 | setSelectedPublishProvider(value as PublishProviderKind); | |
| 682 | setSelectedPublishProvider(value as SourceControlProviderKind); | |
| 761 | 683 | setPublishRepositoryOverride(null); |
| 762 | 684 | }} |
| 763 | 685 | aria-labelledby="publish-provider-cards-label" |
| 764 | 686 | className="grid grid-cols-2" |
| 765 | 687 | > |
| 766 | 688 | {sortedPublishProviderOptions.map((option) => { |
| 767 | const readiness = publishProviderReadiness[option.value]; | |
| 689 | const readiness = publishProviderReadiness(option.value); | |
| 768 | 690 | const isSelected = publishProvider === option.value && readiness.ready; |
| 769 | 691 | if (!readiness.ready) { |
| 770 | 692 | return ( |
apps/web/src/components/ThreadStatusIndicators.tsx+9-20| ... | ... | @@ -6,14 +6,12 @@ import { |
| 6 | 6 | import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests"; |
| 7 | 7 | |
| 8 | 8 | import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests"; |
| 9 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; | |
| 10 | 9 | import { |
| 11 | 10 | resolveEnvironmentMachineKind, |
| 12 | 11 | type EnvironmentId, |
| 13 | 12 | type ThreadLinkedPullRequest, |
| 14 | 13 | type ThreadPullRequestLink, |
| 15 | 14 | type VcsStatusResult, |
| 16 | SourceControlProviderKind, | |
| 17 | 15 | } from "@t3tools/contracts"; |
| 18 | 16 | import { Atom } from "effect/reactivity"; |
| 19 | 17 | import { FolderGit2Icon, TerminalIcon } from "lucide-react"; |
| ... | ... | @@ -38,7 +36,7 @@ import { linkedPullRequestDetailAtom, useSharedPullRequestSummary } from "../sta |
| 38 | 36 | import { useThreadRunningTerminalIds } from "../state/terminalSessions"; |
| 39 | 37 | import { vcsEnvironment } from "../state/vcs"; |
| 40 | 38 | import { useUiStateStore } from "../uiStateStore"; |
| 41 | import { resolveChangeRequestPresentation } from "../sourceControlPresentation"; | |
| 39 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 42 | 40 | import { |
| 43 | 41 | resolveThreadLastVisitedAt, |
| 44 | 42 | resolveThreadStatusPill, |
| ... | ... | @@ -131,19 +129,10 @@ export function linkedPullRequestSnapshotStatus( |
| 131 | 129 | ): LinkedThreadPullRequestStatus | null { |
| 132 | 130 | const snapshot = link.snapshot; |
| 133 | 131 | if (snapshot === null) return null; |
| 134 | // GitCafe's `/pulls/` routes look like Forgejo's, so its two hosts are told apart first. | |
| 135 | const kind = | |
| 136 | detectSourceControlProviderFromRemoteUrl(link.url)?.kind === "gitcafe" | |
| 137 | ? "gitcafe" | |
| 138 | : link.url.includes("/-/merge_requests/") | |
| 139 | ? "gitlab" | |
| 140 | : link.url.includes("/pullrequest/") | |
| 141 | ? "azure-devops" | |
| 142 | : link.url.includes("/pull-requests/") | |
| 143 | ? "bitbucket" | |
| 144 | : link.url.includes("/pulls/") | |
| 145 | ? "forgejo" | |
| 146 | : "github"; | |
| 132 | // A link records no host kind, so read it from the URL's change request path. | |
| 133 | const kind = ( | |
| 134 | sourceControlClients.findByChangeRequestUrl(link.url) ?? sourceControlClients.get(undefined) | |
| 135 | ).kind; | |
| 147 | 136 | return { |
| 148 | 137 | pr: { |
| 149 | 138 | number: link.number, |
| ... | ... | @@ -155,7 +144,7 @@ export function linkedPullRequestSnapshotStatus( |
| 155 | 144 | baseRef: snapshot.baseBranch, |
| 156 | 145 | ...(snapshot.updatedAt === null ? {} : { updatedAt: snapshot.updatedAt }), |
| 157 | 146 | }, |
| 158 | sourceControlProvider: { kind: SourceControlProviderKind.make(kind), name: kind, baseUrl: "" }, | |
| 147 | sourceControlProvider: { kind, name: kind, baseUrl: "" }, | |
| 159 | 148 | }; |
| 160 | 149 | } |
| 161 | 150 | |
| ... | ... | @@ -442,12 +431,12 @@ export function prStatusIndicator( |
| 442 | 431 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, |
| 443 | 432 | ): PrStatusIndicator | null { |
| 444 | 433 | if (!pr) return null; |
| 445 | const presentation = resolveChangeRequestPresentation(provider); | |
| 434 | const { shortLabel } = sourceControlClients.get(provider?.kind).changeRequest; | |
| 446 | 435 | const state = resolvePullRequestState({ state: pr.state, isDraft: pr.isDraft === true }); |
| 447 | 436 | |
| 448 | const tooltipLead = `${presentation.shortName} #${pr.number} - ${state.label}`; | |
| 437 | const tooltipLead = `${shortLabel} #${pr.number} - ${state.label}`; | |
| 449 | 438 | return { |
| 450 | label: `${presentation.shortName} ${state.label.toLowerCase()}`, | |
| 439 | label: `${shortLabel} ${state.label.toLowerCase()}`, | |
| 451 | 440 | colorClass: state.toneClassName, |
| 452 | 441 | Icon: state.Icon, |
| 453 | 442 | tooltip: `${tooltipLead}: ${pr.title}`, |
apps/web/src/components/pullRequest/LinkPullRequestDialog.logic.test.ts+6-1| ... | ... | @@ -1,6 +1,11 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { changeRequestWebUrl, resolveLinkPullRequestInput } from "./LinkPullRequestDialog"; | |
| 3 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 4 | ||
| 5 | import { resolveLinkPullRequestInput } from "./LinkPullRequestDialog"; | |
| 6 | ||
| 7 | const changeRequestWebUrl = (kind: string, host: string, repository: string, number: number) => | |
| 8 | sourceControlClients.get(kind).changeRequestUrl({ host, repository, number }); | |
| 4 | 9 | |
| 5 | 10 | const project = { |
| 6 | 11 | host: "github.com", |
apps/web/src/components/pullRequest/LinkPullRequestDialog.tsx+11-12| ... | ... | @@ -1,10 +1,5 @@ |
| 1 | import { changeRequestUrlFor as changeRequestWebUrl } from "@t3tools/shared/changeRequestUrl"; | |
| 2 | export { changeRequestUrlFor as changeRequestWebUrl } from "@t3tools/shared/changeRequestUrl"; | |
| 3 | import { | |
| 4 | pullRequestHostOf, | |
| 5 | type ScopedThreadRef, | |
| 6 | type SourceControlProviderKind, | |
| 7 | } from "@t3tools/contracts"; | |
| 1 | import { pullRequestHostOf, type ScopedThreadRef } from "@t3tools/contracts"; | |
| 2 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 8 | 3 | import { useAtomValue } from "@effect/atom-react"; |
| 9 | 4 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 10 | 5 | |
| ... | ... | @@ -138,15 +133,19 @@ function LinkPullRequestDialog({ |
| 138 | 133 | identity.displayName ?? |
| 139 | 134 | (identity.owner && identity.name ? `${identity.owner}/${identity.name}` : null); |
| 140 | 135 | if (repository === null) return null; |
| 141 | const kind = identity.provider as SourceControlProviderKind; | |
| 142 | const host = pullRequestHostOf(identity, kind); | |
| 136 | const definition = sourceControlClients.get(identity.provider ?? "unknown"); | |
| 137 | const host = pullRequestHostOf(identity, definition.kind); | |
| 143 | 138 | return { |
| 144 | 139 | host, |
| 145 | 140 | repository, |
| 146 | 141 | webUrl: (number: number) => |
| 147 | kind === "forgejo" && identity.webUrl | |
| 148 | ? `${identity.webUrl.replace(/\/+$/, "")}/pulls/${number}` | |
| 149 | : changeRequestWebUrl(kind, host, repository, number, identity.locator.remoteUrl), | |
| 142 | definition.changeRequestUrl({ | |
| 143 | host, | |
| 144 | repository, | |
| 145 | number, | |
| 146 | remoteUrl: identity.locator.remoteUrl, | |
| 147 | webUrl: identity.webUrl, | |
| 148 | }), | |
| 150 | 149 | }; |
| 151 | 150 | }, [environmentProjects, projectId]); |
| 152 | 151 | const linking = usePullRequestLinking(threadRef.environmentId); |
apps/web/src/components/pullRequest/PullRequestComposer.tsx+4-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 1 | 2 | /** |
| 2 | 3 | * The single floating control over a pull request. Commenting on the change and submitting the |
| 3 | 4 | * review that carries the Code tab's line comments used to float as two buttons that crowded |
| ... | ... | @@ -158,7 +159,9 @@ export function PullRequestComposer({ |
| 158 | 159 | environmentId={environmentId} |
| 159 | 160 | reference={reference} |
| 160 | 161 | verdicts={verdicts} |
| 161 | requestChangesSummaryRequired={detail.provider === "forgejo"} | |
| 162 | requestChangesSummaryRequired={sourceControlClients | |
| 163 | .get(detail.provider) | |
| 164 | .reviewSummaryRequired("request-changes")} | |
| 162 | 165 | textareaRef={reviewRef} |
| 163 | 166 | pending={reviewPending} |
| 164 | 167 | onPendingChange={setReviewPending} |
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+15-11| ... | ... | @@ -83,6 +83,7 @@ import { |
| 83 | 83 | } from "~/state/pullRequests"; |
| 84 | 84 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 85 | 85 | import { PullRequestStackMenu } from "./PullRequestStackMenu"; |
| 86 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 86 | 87 | import { PullRequestThreadLinks } from "./PullRequestThreadLinks"; |
| 87 | 88 | import { vcsEnvironment } from "~/state/vcs"; |
| 88 | 89 | import { formatRelativeTimeLabel } from "~/timestampFormat"; |
| ... | ... | @@ -302,7 +303,7 @@ const openNumberContextMenu = ( |
| 302 | 303 | event.stopPropagation(); |
| 303 | 304 | void showPullRequestLinkContextMenu({ |
| 304 | 305 | url: detail.url, |
| 305 | openLabel: openOnHostLabel(detail.provider), | |
| 306 | openLabel: openOnHostLabel(sourceControlClients.get(detail.provider)), | |
| 306 | 307 | position: { x: event.clientX, y: event.clientY }, |
| 307 | 308 | }); |
| 308 | 309 | }; |
| ... | ... | @@ -732,16 +733,18 @@ export function PullRequestDetailPanel({ |
| 732 | 733 | if (detail?.autoMergeMethod !== undefined) setMergeMethod(detail.autoMergeMethod); |
| 733 | 734 | }, [detail?.autoMergeMethod, pullRequestKey]); |
| 734 | 735 | const repositoryUrl = detail === null ? null : changeRequestRepositoryUrl(detail.url); |
| 736 | const hostClient = sourceControlClients.get(detail?.provider); | |
| 735 | 737 | const markdownContext = useMemo( |
| 736 | () => ({ repositoryUrl: detail?.provider === "github" ? repositoryUrl : null, threadRef }), | |
| 737 | [detail?.provider, repositoryUrl, threadRef], | |
| 738 | () => ({ | |
| 739 | repositoryUrl: | |
| 740 | repositoryUrl === null ? null : hostClient.referenceAutolinkRepositoryUrl(repositoryUrl), | |
| 741 | threadRef, | |
| 742 | }), | |
| 743 | [hostClient, repositoryUrl, threadRef], | |
| 738 | 744 | ); |
| 739 | 745 | const authorProfileUrl = |
| 740 | detail?.provider === "github" && | |
| 741 | detail.author !== null && | |
| 742 | !detail.author.login.endsWith("[bot]") && | |
| 743 | repositoryUrl !== null | |
| 744 | ? new URL(`/${encodeURIComponent(detail.author.login)}`, repositoryUrl).toString() | |
| 746 | detail?.author && repositoryUrl !== null | |
| 747 | ? hostClient.authorProfileUrl(detail.author.login, repositoryUrl) | |
| 745 | 748 | : null; |
| 746 | 749 | const checkoutCommand = handoffSummary |
| 747 | 750 | ? pullRequestCheckoutCommand( |
| ... | ... | @@ -1725,7 +1728,7 @@ export function PullRequestDetailPanel({ |
| 1725 | 1728 | </button> |
| 1726 | 1729 | } |
| 1727 | 1730 | /> |
| 1728 | <TooltipPopup side="top">{openOnHostLabel(detail.provider)}</TooltipPopup> | |
| 1731 | <TooltipPopup side="top">{openOnHostLabel(hostClient)}</TooltipPopup> | |
| 1729 | 1732 | </Tooltip> |
| 1730 | 1733 | </> |
| 1731 | 1734 | ) : null} |
| ... | ... | @@ -1780,7 +1783,7 @@ export function PullRequestDetailPanel({ |
| 1780 | 1783 | </button> |
| 1781 | 1784 | } |
| 1782 | 1785 | /> |
| 1783 | <TooltipPopup side="top">{openOnHostLabel(detail.provider)}</TooltipPopup> | |
| 1786 | <TooltipPopup side="top">{openOnHostLabel(hostClient)}</TooltipPopup> | |
| 1784 | 1787 | </Tooltip> |
| 1785 | 1788 | <Tooltip> |
| 1786 | 1789 | <TooltipTrigger |
| ... | ... | @@ -1806,6 +1809,7 @@ export function PullRequestDetailPanel({ |
| 1806 | 1809 | ) : null} |
| 1807 | 1810 | {nativeStack ? ( |
| 1808 | 1811 | <PullRequestStackMenu |
| 1812 | hostLabel={hostClient.label} | |
| 1809 | 1813 | stack={nativeStack} |
| 1810 | 1814 | notice={nativeStackQuery.notice} |
| 1811 | 1815 | onRetry={nativeStackQuery.error ? nativeStackQuery.refresh : undefined} |
| ... | ... | @@ -2211,7 +2215,7 @@ export function PullRequestDetailPanel({ |
| 2211 | 2215 | ) : null} |
| 2212 | 2216 | <MenuItem onClick={() => void readLocalApi()?.shell.openExternal(detail.url)}> |
| 2213 | 2217 | <ArrowUpRightIcon className="size-3.5" /> |
| 2214 | {openOnHostLabel(detail.provider)} | |
| 2218 | {openOnHostLabel(hostClient)} | |
| 2215 | 2219 | </MenuItem> |
| 2216 | 2220 | <MenuItem onClick={() => copyReference(detail.url, "PR link")}> |
| 2217 | 2221 | <LinkIcon className="size-3.5" /> |
apps/web/src/components/pullRequest/PullRequestRow.tsx+2-1| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 1 | 2 | import { SearchIcon } from "lucide-react"; |
| 2 | 3 | import { PullRequestStackPopover } from "./PullRequestStackPopover"; |
| 3 | 4 | import { |
| ... | ... | @@ -158,7 +159,7 @@ function PullRequestRowImpl({ |
| 158 | 159 | event.stopPropagation(); |
| 159 | 160 | void showPullRequestLinkContextMenu({ |
| 160 | 161 | url: entry.url, |
| 161 | openLabel: openOnHostLabel(entry.provider), | |
| 162 | openLabel: openOnHostLabel(sourceControlClients.get(entry.provider)), | |
| 162 | 163 | position: { x: event.clientX, y: event.clientY }, |
| 163 | 164 | }); |
| 164 | 165 | }} |
apps/web/src/components/pullRequest/PullRequestStackMenu.tsx+5-2| ... | ... | @@ -28,6 +28,7 @@ import { PullRequestStackLayerContent } from "./PullRequestStackLayerContent"; |
| 28 | 28 | import { PullRequestGlyph } from "./pullRequestIcons"; |
| 29 | 29 | |
| 30 | 30 | export function PullRequestStackMenu({ |
| 31 | hostLabel, | |
| 31 | 32 | stack, |
| 32 | 33 | reference, |
| 33 | 34 | environmentId, |
| ... | ... | @@ -39,6 +40,8 @@ export function PullRequestStackMenu({ |
| 39 | 40 | notice, |
| 40 | 41 | onRetry, |
| 41 | 42 | }: { |
| 43 | /** The host's name, which reports what it did with the stack. */ | |
| 44 | hostLabel: string; | |
| 42 | 45 | notice?: string | null; |
| 43 | 46 | onRetry?: (() => void) | undefined; |
| 44 | 47 | stack: PullRequestStack; |
| ... | ... | @@ -117,7 +120,7 @@ export function PullRequestStackMenu({ |
| 117 | 120 | title: action === "merge" ? "Stack merge request completed" : "Stack rebased", |
| 118 | 121 | description: |
| 119 | 122 | action === "merge" |
| 120 | ? "GitHub merged the stack or added it to its merge queue." | |
| 123 | ? `${hostLabel} merged the stack or added it to its merge queue.` | |
| 121 | 124 | : undefined, |
| 122 | 125 | }); |
| 123 | 126 | } |
| ... | ... | @@ -232,7 +235,7 @@ export function PullRequestStackMenu({ |
| 232 | 235 | </DialogTitle> |
| 233 | 236 | <DialogDescription> |
| 234 | 237 | {confirmation === "merge" |
| 235 | ? `Merge #${reference.number} and its unmerged layers below into ${stack.base} using ${mergeMethod}. GitHub checks their rules before merging or queueing them and rebases the remaining stack after merging.` | |
| 238 | ? `Merge #${reference.number} and its unmerged layers below into ${stack.base} using ${mergeMethod}. ${hostLabel} checks their rules before merging or queueing them and rebases the remaining stack after merging.` | |
| 236 | 239 | : `Rebase the remote branches from bottom to top onto ${stack.base}. This rewrites branch history and may restart checks. If a layer fails, earlier updates remain.`} |
| 237 | 240 | </DialogDescription> |
| 238 | 241 | </DialogHeader> |
apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx+4-4| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 1 | 2 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 2 | 3 | import type { |
| 3 | 4 | EnvironmentId, |
| ... | ... | @@ -83,10 +84,9 @@ function CommentIdentity({ |
| 83 | 84 | detail: PullRequestDetailView; |
| 84 | 85 | }) { |
| 85 | 86 | const actor = comment.author; |
| 86 | const profileUrl = | |
| 87 | detail.provider === "github" && actor && !actor.login.endsWith("[bot]") | |
| 88 | ? new URL(`/${encodeURIComponent(actor.login)}`, detail.url).toString() | |
| 89 | : null; | |
| 87 | const profileUrl = actor | |
| 88 | ? sourceControlClients.get(detail.provider).authorProfileUrl(actor.login, detail.url) | |
| 89 | : null; | |
| 90 | 90 | return ( |
| 91 | 91 | <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1 text-xs"> |
| 92 | 92 | <PullRequestActorLabel actor={actor} profileUrl={profileUrl} className="max-w-full" /> |
apps/web/src/components/pullRequest/ThreadPullRequestsPanel.tsx+2-1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | import type { ProjectId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts"; |
| 2 | 2 | import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl"; |
| 3 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 3 | 4 | import { |
| 4 | 5 | resolveThreadPullRequestChains, |
| 5 | 6 | visibleThreadPullRequests, |
| ... | ... | @@ -210,7 +211,7 @@ function LinkRow({ |
| 210 | 211 | </TooltipTrigger> |
| 211 | 212 | <TooltipPopup> |
| 212 | 213 | {stack.kind === "native" |
| 213 | ? `GitHub stack of ${stack.size}: merging a layer lands the ones below it.` | |
| 214 | ? `${sourceControlClients.hostLabelForChangeRequestUrl(link.url)} stack of ${stack.size}: merging a layer lands the ones below it.` | |
| 214 | 215 | : `${stack.size} pull requests chained by base branch.`} |
| 215 | 216 | </TooltipPopup> |
| 216 | 217 | </Tooltip> |
apps/web/src/components/pullRequest/pullRequestDetail.logic.ts+22-37| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 1 | 2 | import * as Schema from "effect/Schema"; |
| 2 | 3 | |
| 3 | 4 | import { |
| ... | ... | @@ -68,9 +69,6 @@ export function resolvePullRequestMergeMethod( |
| 68 | 69 | return allowed[0] ?? "merge"; |
| 69 | 70 | } |
| 70 | 71 | |
| 71 | const safeShellArgument = /^[A-Za-z0-9._/@+=,-]+$/; | |
| 72 | const bitbucketRepositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; | |
| 73 | ||
| 74 | 72 | export type PullRequestPrimaryControl = |
| 75 | 73 | | "resolve" |
| 76 | 74 | | "ready" |
| ... | ... | @@ -117,32 +115,9 @@ export function pullRequestCheckoutCommand( |
| 117 | 115 | headRepositoryNameWithOwner?: string | null, |
| 118 | 116 | repositoryUrl?: string | null, |
| 119 | 117 | ): string | null { |
| 120 | switch (provider) { | |
| 121 | case "github": | |
| 122 | return `gh pr checkout ${number}`; | |
| 123 | case "gitlab": | |
| 124 | return `glab mr checkout ${number}`; | |
| 125 | case "forgejo": | |
| 126 | return repositoryUrl | |
| 127 | ? `git fetch '${repositoryUrl.replaceAll("'", "'\\''")}' refs/pull/${number}/head && git checkout -B pulls/${number} FETCH_HEAD` | |
| 128 | : null; | |
| 129 | case "azure-devops": | |
| 130 | return `az repos pr checkout --id ${number}`; | |
| 131 | case "gitcafe": | |
| 132 | return `cafe pr checkout ${number}`; | |
| 133 | case "bitbucket": { | |
| 134 | if ( | |
| 135 | !headRepositoryNameWithOwner || | |
| 136 | !bitbucketRepositoryName.test(headRepositoryNameWithOwner) || | |
| 137 | !safeShellArgument.test(headBranch) | |
| 138 | ) { | |
| 139 | return null; | |
| 140 | } | |
| 141 | return `git clone --single-branch --branch ${headBranch} https://bitbucket.org/${headRepositoryNameWithOwner}.git t3code-pr-${number}`; | |
| 142 | } | |
| 143 | default: | |
| 144 | return null; | |
| 145 | } | |
| 118 | return sourceControlClients | |
| 119 | .get(provider) | |
| 120 | .checkoutCommand({ number, headBranch, headRepositoryNameWithOwner, repositoryUrl }); | |
| 146 | 121 | } |
| 147 | 122 | |
| 148 | 123 | /** Build a checkout command from identity metadata while the detail request is still pending. */ |
| ... | ... | @@ -151,15 +126,23 @@ export function loadingPullRequestCheckoutCommand( |
| 151 | 126 | identity: RepositoryIdentity | null | undefined, |
| 152 | 127 | ): string | null { |
| 153 | 128 | const host = reference.host?.trim().toLowerCase(); |
| 154 | const provider = | |
| 155 | identity?.provider ?? | |
| 156 | (host === "github.com" ? "github" : host === "gitlab.com" ? "gitlab" : null); | |
| 157 | if (provider !== "github" && provider !== "gitlab" && provider !== "azure-devops") return null; | |
| 158 | const kind = SourceControlProviderKind.make(provider); | |
| 159 | if (identity?.provider !== undefined && host && pullRequestHostOf(identity, kind) !== host) { | |
| 129 | // Before the detail loads, only the number is known, so only hosts whose command needs nothing | |
| 130 | // else qualify. Without an identity, only a public hostname names the host. | |
| 131 | const definition = identity?.provider | |
| 132 | ? sourceControlClients.find(identity.provider) | |
| 133 | : host | |
| 134 | ? sourceControlClients.findByPublicHost(host) | |
| 135 | : undefined; | |
| 136 | const command = definition?.checkoutCommand({ number: reference.number, headBranch: "" }); | |
| 137 | if (!definition || !command) return null; | |
| 138 | if ( | |
| 139 | identity?.provider !== undefined && | |
| 140 | host && | |
| 141 | pullRequestHostOf(identity, definition.kind) !== host | |
| 142 | ) { | |
| 160 | 143 | return null; |
| 161 | 144 | } |
| 162 | return pullRequestCheckoutCommand(kind, reference.number, ""); | |
| 145 | return command; | |
| 163 | 146 | } |
| 164 | 147 | |
| 165 | 148 | /** Activity changes only when the same host resource reports a newer revision. */ |
| ... | ... | @@ -967,7 +950,9 @@ export function buildFixFindingsHandoff(input: { |
| 967 | 950 | ? ["Failing checks:", ...includedChecks.map((check) => `> ${check}`)] |
| 968 | 951 | : []), |
| 969 | 952 | ...(input.commentsTruncated |
| 970 | ? ["The conversation was truncated; more review comments may exist on GitHub."] | |
| 953 | ? [ | |
| 954 | `The conversation was truncated; more review comments may exist on ${sourceControlClients.hostLabelForChangeRequestUrl(input.url)}.`, | |
| 955 | ] | |
| 971 | 956 | : []), |
| 972 | 957 | ...(omitted > 0 ? [`${omitted} further findings were omitted.`] : []), |
| 973 | 958 | ...(includedThreads.length === 0 && |
apps/web/src/components/pullRequest/pullRequestLinkContextMenu.ts+6-11| ... | ... | @@ -1,4 +1,8 @@ |
| 1 | 1 | import type { ContextMenuItem } from "@t3tools/contracts"; |
| 2 | import { | |
| 3 | type SourceControlClientDefinition, | |
| 4 | UNKNOWN_SOURCE_CONTROL_CLIENT, | |
| 5 | } from "@t3tools/client-runtime/source-control-clients"; | |
| 2 | 6 | |
| 3 | 7 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| 4 | 8 | import { readLocalApi } from "~/localApi"; |
| ... | ... | @@ -8,17 +12,8 @@ import { toastManager } from "../ui/toast"; |
| 8 | 12 | export type PullRequestLinkContextMenuAction = "copy-link" | "open-external"; |
| 9 | 13 | |
| 10 | 14 | /** Named for the host rather than "externally": the point is where you will land. */ |
| 11 | const OPEN_ON_HOST_LABELS: Partial<Record<string, string>> = { | |
| 12 | github: "Open on GitHub", | |
| 13 | gitlab: "Open on GitLab", | |
| 14 | forgejo: "Open on Forgejo", | |
| 15 | bitbucket: "Open on Bitbucket", | |
| 16 | gitcafe: "Open on GitCafe", | |
| 17 | "azure-devops": "Open on Azure DevOps", | |
| 18 | }; | |
| 19 | ||
| 20 | export const openOnHostLabel = (provider: string): string => | |
| 21 | OPEN_ON_HOST_LABELS[provider] ?? "Open on host"; | |
| 15 | export const openOnHostLabel = (host: SourceControlClientDefinition): string => | |
| 16 | host === UNKNOWN_SOURCE_CONTROL_CLIENT ? "Open on host" : `Open on ${host.label}`; | |
| 22 | 17 | |
| 23 | 18 | /** Copy first: it is the reason to right-click a number rather than click it. */ |
| 24 | 19 | function pullRequestLinkContextMenuItems( |
apps/web/src/components/settings/SourceControlSettings.tsx+5-22| ... | ... | @@ -5,7 +5,6 @@ import * as Option from "effect/Option"; |
| 5 | 5 | import { useEffect, useState, type ReactNode } from "react"; |
| 6 | 6 | import type { |
| 7 | 7 | BackgroundActivitySettings, |
| 8 | SourceControlProviderKind, | |
| 9 | 8 | SourceControlDiscoveryResult, |
| 10 | 9 | SourceControlProviderAuth, |
| 11 | 10 | SourceControlProviderDiscoveryItem, |
| ... | ... | @@ -46,17 +45,7 @@ import { |
| 46 | 45 | import { Switch } from "../ui/switch"; |
| 47 | 46 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 48 | 47 | |
| 49 | import { | |
| 50 | AzureDevOpsIcon, | |
| 51 | BitbucketIcon, | |
| 52 | GitCafeIcon, | |
| 53 | GitHubIcon, | |
| 54 | GitIcon, | |
| 55 | GitLabIcon, | |
| 56 | ForgejoIcon, | |
| 57 | JujutsuIcon, | |
| 58 | type Icon, | |
| 59 | } from "../Icons"; | |
| 48 | import { GitIcon, JujutsuIcon, type Icon } from "../Icons"; | |
| 60 | 49 | import { BitbucketCredentialsSettings } from "./BitbucketCredentialsSettings"; |
| 61 | 50 | import { GitHubAccountSettings } from "./GitHubAccountSettings"; |
| 62 | 51 | import { GitHubTokenSettings } from "./GitHubTokenSettings"; |
| ... | ... | @@ -72,21 +61,14 @@ import { |
| 72 | 61 | } from "./settingsLayout"; |
| 73 | 62 | import { searchableSetting } from "./settingsSearch"; |
| 74 | 63 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; |
| 64 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 65 | import { sourceControlIcon } from "~/sourceControlPresentation"; | |
| 75 | 66 | |
| 76 | 67 | const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { |
| 77 | 68 | versionControlSystems: [], |
| 78 | 69 | sourceControlProviders: [], |
| 79 | 70 | }; |
| 80 | 71 | |
| 81 | const SOURCE_CONTROL_PROVIDER_ICONS: Partial<Record<string, Icon>> = { | |
| 82 | github: GitHubIcon, | |
| 83 | gitlab: GitLabIcon, | |
| 84 | forgejo: ForgejoIcon, | |
| 85 | "azure-devops": AzureDevOpsIcon, | |
| 86 | bitbucket: BitbucketIcon, | |
| 87 | gitcafe: GitCafeIcon, | |
| 88 | }; | |
| 89 | ||
| 90 | 72 | const VCS_ICONS: Partial<Record<VcsDriverKind, Icon>> = { |
| 91 | 73 | git: GitIcon, |
| 92 | 74 | jj: JujutsuIcon, |
| ... | ... | @@ -185,8 +167,9 @@ function SourceControlItemMark({ |
| 185 | 167 | readonly item: VcsDiscoveryItem | SourceControlProviderDiscoveryItem; |
| 186 | 168 | }) { |
| 187 | 169 | const dotClassName = itemStatusDot(item); |
| 170 | const host = isProviderDiscoveryItem(item) ? sourceControlClients.find(item.kind) : undefined; | |
| 188 | 171 | const Icon = isProviderDiscoveryItem(item) |
| 189 | ? SOURCE_CONTROL_PROVIDER_ICONS[item.kind] | |
| 172 | ? host && sourceControlIcon(host) | |
| 190 | 173 | : VCS_ICONS[item.kind]; |
| 191 | 174 | |
| 192 | 175 | if (!Icon) { |
apps/web/src/sourceControlPresentation.ts+44-63| ... | ... | @@ -1,17 +1,10 @@ |
| 1 | 1 | import type { ElementType } from "react"; |
| 2 | import type { SourceControlProviderInfo, SourceControlProviderKind } from "@t3tools/contracts"; | |
| 3 | export { | |
| 4 | DEFAULT_CHANGE_REQUEST_TERMINOLOGY, | |
| 5 | getChangeRequestTerminology, | |
| 6 | resolveChangeRequestPresentation, | |
| 7 | type ChangeRequestPresentation, | |
| 8 | type ChangeRequestTerminology, | |
| 9 | } from "@t3tools/shared/sourceControl"; | |
| 10 | import { | |
| 11 | getChangeRequestTerminology, | |
| 12 | resolveChangeRequestPresentation, | |
| 13 | type ChangeRequestTerminology, | |
| 14 | } from "@t3tools/shared/sourceControl"; | |
| 2 | import type { SourceControlProviderInfo } from "@t3tools/contracts"; | |
| 3 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 4 | import type { | |
| 5 | ChangeRequestTerminology, | |
| 6 | SourceControlClientDefinition, | |
| 7 | } from "@t3tools/client-runtime/source-control-clients"; | |
| 15 | 8 | import { |
| 16 | 9 | AzureDevOpsIcon, |
| 17 | 10 | BitbucketIcon, |
| ... | ... | @@ -22,65 +15,53 @@ import { |
| 22 | 15 | } from "./components/Icons"; |
| 23 | 16 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; |
| 24 | 17 | |
| 18 | export type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients"; | |
| 19 | ||
| 20 | type SourceControlIcon = ElementType<{ className?: string }>; | |
| 21 | ||
| 22 | /** Web art for each definition `icon` key; a key without art draws the change request glyph. */ | |
| 23 | const SOURCE_CONTROL_ICONS: Partial<Record<string, SourceControlIcon>> = { | |
| 24 | github: GitHubIcon, | |
| 25 | gitlab: GitLabIcon, | |
| 26 | forgejo: ForgejoIcon, | |
| 27 | bitbucket: BitbucketIcon, | |
| 28 | "azure-devops": AzureDevOpsIcon, | |
| 29 | gitcafe: GitCafeIcon, | |
| 30 | }; | |
| 31 | ||
| 32 | export function sourceControlIcon(definition: SourceControlClientDefinition): SourceControlIcon { | |
| 33 | return SOURCE_CONTROL_ICONS[definition.icon] ?? PullRequestGlyph.pullRequest; | |
| 34 | } | |
| 35 | ||
| 25 | 36 | export interface SourceControlPresentation { |
| 37 | readonly definition: SourceControlClientDefinition; | |
| 26 | 38 | readonly providerName: string; |
| 27 | 39 | readonly terminology: ChangeRequestTerminology; |
| 28 | readonly Icon: ElementType<{ className?: string }>; | |
| 40 | readonly Icon: SourceControlIcon; | |
| 29 | 41 | } |
| 30 | 42 | |
| 43 | function presentDefinition( | |
| 44 | definition: SourceControlClientDefinition, | |
| 45 | providerName: string, | |
| 46 | ): SourceControlPresentation { | |
| 47 | return { | |
| 48 | definition, | |
| 49 | providerName, | |
| 50 | terminology: definition.changeRequest, | |
| 51 | Icon: sourceControlIcon(definition), | |
| 52 | }; | |
| 53 | } | |
| 54 | ||
| 55 | /** Named as the server reported the host, e.g. "GitHub Self-Hosted", when it reported one. */ | |
| 31 | 56 | export function getSourceControlPresentation( |
| 32 | 57 | provider: SourceControlProviderInfo | null | undefined, |
| 33 | 58 | ): SourceControlPresentation { |
| 34 | const presentation = resolveChangeRequestPresentation(provider); | |
| 35 | switch (presentation.icon) { | |
| 36 | case "github": | |
| 37 | return { | |
| 38 | providerName: provider?.name || presentation.providerName, | |
| 39 | terminology: getChangeRequestTerminology(provider), | |
| 40 | Icon: GitHubIcon, | |
| 41 | }; | |
| 42 | case "forgejo": | |
| 43 | return { | |
| 44 | providerName: provider?.name || presentation.providerName, | |
| 45 | terminology: getChangeRequestTerminology(provider), | |
| 46 | Icon: ForgejoIcon, | |
| 47 | }; | |
| 48 | case "gitlab": | |
| 49 | return { | |
| 50 | providerName: provider?.name || presentation.providerName, | |
| 51 | terminology: getChangeRequestTerminology(provider), | |
| 52 | Icon: GitLabIcon, | |
| 53 | }; | |
| 54 | case "azure-devops": | |
| 55 | return { | |
| 56 | providerName: provider?.name || presentation.providerName, | |
| 57 | terminology: getChangeRequestTerminology(provider), | |
| 58 | Icon: AzureDevOpsIcon, | |
| 59 | }; | |
| 60 | case "bitbucket": | |
| 61 | return { | |
| 62 | providerName: provider?.name || presentation.providerName, | |
| 63 | terminology: getChangeRequestTerminology(provider), | |
| 64 | Icon: BitbucketIcon, | |
| 65 | }; | |
| 66 | case "gitcafe": | |
| 67 | return { | |
| 68 | providerName: provider?.name || presentation.providerName, | |
| 69 | terminology: getChangeRequestTerminology(provider), | |
| 70 | Icon: GitCafeIcon, | |
| 71 | }; | |
| 72 | case "change-request": | |
| 73 | return { | |
| 74 | providerName: provider?.name || presentation.providerName, | |
| 75 | terminology: getChangeRequestTerminology(provider), | |
| 76 | Icon: PullRequestGlyph.pullRequest, | |
| 77 | }; | |
| 78 | } | |
| 59 | const definition = sourceControlClients.get(provider?.kind); | |
| 60 | return presentDefinition(definition, provider?.name || definition.label); | |
| 79 | 61 | } |
| 80 | 62 | |
| 81 | 63 | /** For surfaces that know only the host kind, such as a change request row or filter. */ |
| 82 | export function getSourceControlPresentationForKind( | |
| 83 | kind: SourceControlProviderKind, | |
| 84 | ): SourceControlPresentation { | |
| 85 | return getSourceControlPresentation({ kind, name: "", baseUrl: "" }); | |
| 64 | export function getSourceControlPresentationForKind(kind: string): SourceControlPresentation { | |
| 65 | const definition = sourceControlClients.get(kind); | |
| 66 | return presentDefinition(definition, definition.label); | |
| 86 | 67 | } |
packages/client-runtime/src/sourceControlClients.ts+1| ... | ... | @@ -12,6 +12,7 @@ import * as Bitbucket from "@t3tools/source-control-bitbucket/client/definition" |
| 12 | 12 | import { makeSourceControlClientRegistry } from "@t3tools/source-control-core/client/definition"; |
| 13 | 13 | |
| 14 | 14 | export { |
| 15 | UNKNOWN_SOURCE_CONTROL_CLIENT, | |
| 15 | 16 | type ChangeRequestTerminology, |
| 16 | 17 | type SourceControlClientDefinition, |
| 17 | 18 | } from "@t3tools/source-control-core/client/definition"; |
packages/shared/src/sourceControl.ts-19| ... | ... | @@ -24,11 +24,6 @@ export interface ChangeRequestTerminology { |
| 24 | 24 | readonly singular: string; |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | export const DEFAULT_CHANGE_REQUEST_TERMINOLOGY: ChangeRequestTerminology = { | |
| 28 | shortLabel: "PR", | |
| 29 | singular: "pull request", | |
| 30 | }; | |
| 31 | ||
| 32 | 27 | const GITHUB_CHANGE_REQUEST_PRESENTATION: ChangeRequestPresentation = { |
| 33 | 28 | icon: "github", |
| 34 | 29 | providerName: "GitHub", |
| ... | ... | @@ -132,20 +127,6 @@ function resolveChangeRequestPresentationForKind( |
| 132 | 127 | return resolveChangeRequestPresentation({ kind, name: "", baseUrl: "" }); |
| 133 | 128 | } |
| 134 | 129 | |
| 135 | export function getChangeRequestTerminology( | |
| 136 | provider: SourceControlProviderInfo | null | undefined, | |
| 137 | ): ChangeRequestTerminology { | |
| 138 | if (!provider) { | |
| 139 | return DEFAULT_CHANGE_REQUEST_TERMINOLOGY; | |
| 140 | } | |
| 141 | ||
| 142 | const presentation = resolveChangeRequestPresentation(provider); | |
| 143 | return { | |
| 144 | shortLabel: presentation.shortName, | |
| 145 | singular: presentation.longName, | |
| 146 | }; | |
| 147 | } | |
| 148 | ||
| 149 | 130 | export function getChangeRequestTerminologyForKind( |
| 150 | 131 | kind: SourceControlProviderKind, |
| 151 | 132 | ): ChangeRequestTerminology { |
packages/source-control-core/src/client/definition.ts+1-1| ... | ... | @@ -101,7 +101,7 @@ export function defineSourceControlClient<const Definition extends SourceControl |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | 103 | /** What clients show for a host they ship no definition for, including `unknown`. */ |
| 104 | const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlClientDefinition = { | |
| 104 | export const UNKNOWN_SOURCE_CONTROL_CLIENT: SourceControlClientDefinition = { | |
| 105 | 105 | kind: SourceControlProviderKind.make("unknown"), |
| 106 | 106 | label: "source control", |
| 107 | 107 | pickerLabel: "source control", |