| author | |
| committer | |
| log | c8a603307c2d50f8fb9d302f4b51f20fd9f91b33 |
| tree | 9fc9a44bcc54e0ae1240e52dd628e5560914d5bc |
| parent | 7bc732804222946c0c9b23af057484e4412698fc |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>6 files changed, 85 insertions(+), 136 deletions(-)
apps/mobile/src/components/SourceControlIcon.tsx+12-4| ... | @@ -1,12 +1,11 @@ | ... | @@ -1,12 +1,11 @@ |
| 1 | import Svg, { Circle, Defs, G, LinearGradient, Path, Stop } from "react-native-svg"; | 1 | import Svg, { Circle, Defs, G, LinearGradient, Path, Rect, Stop } from "react-native-svg"; |
| 2 | import { withUniwind } from "uniwind"; | 2 | import { withUniwind } from "uniwind"; |
| 3 | 3 | ||
| 4 | const ThemedSvg = withUniwind(Svg); | 4 | const ThemedSvg = withUniwind(Svg); |
| 5 | 5 | ||
| 6 | export type SourceControlIconKind = "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops"; | 6 | /** Draws a host's glyph by its client definition's `icon` key; null for a key it has no art for. */ |
| 7 | |||
| 8 | export function SourceControlIcon(props: { | 7 | export function SourceControlIcon(props: { |
| 9 | readonly kind: SourceControlIconKind; | 8 | readonly kind: string; |
| 10 | readonly size?: number; | 9 | readonly size?: number; |
| 11 | readonly color?: string; | 10 | readonly color?: string; |
| 12 | readonly colorClassName?: string; | 11 | readonly colorClassName?: string; |
| ... | @@ -252,5 +251,14 @@ export function SourceControlIcon(props: { | ... | @@ -252,5 +251,14 @@ export function SourceControlIcon(props: { |
| 252 | /> | 251 | /> |
| 253 | </Svg> | 252 | </Svg> |
| 254 | ); | 253 | ); |
| 254 | case "gitcafe": | ||
| 255 | // GitCafe's own mark from https://git.cafe/favicon.svg, inset like the web icon. | ||
| 256 | return ( | ||
| 257 | <Svg width={size} height={size} viewBox="0 0 64 64"> | ||
| 258 | <Rect x={4.8} y={4.8} width={54.4} height={54.4} fill="#a78bfa" /> | ||
| 259 | </Svg> | ||
| 260 | ); | ||
| 261 | default: | ||
| 262 | return null; | ||
| 255 | } | 263 | } |
| 256 | } | 264 | } |
apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx+5-9| ... | @@ -1,6 +1,9 @@ | ... | @@ -1,6 +1,9 @@ |
| 1 | import type { StaticScreenProps } from "@react-navigation/native"; | 1 | import type { StaticScreenProps } from "@react-navigation/native"; |
| 2 | import { NativeStackScreenOptions } from "../../native/StackHeader"; | 2 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 3 | import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects"; | 3 | import { |
| 4 | addProjectRemoteSourceLabel, | ||
| 5 | parseAddProjectRemoteSource, | ||
| 6 | } from "@t3tools/client-runtime/operations/projects"; | ||
| 4 | 7 | ||
| 5 | import { AddProjectRepositoryScreen } from "./AddProjectScreen"; | 8 | import { AddProjectRepositoryScreen } from "./AddProjectScreen"; |
| 6 | 9 | ||
| ... | @@ -14,14 +17,7 @@ export function AddProjectRepositoryRoute({ | ... | @@ -14,14 +17,7 @@ export function AddProjectRepositoryRoute({ |
| 14 | }: StaticScreenProps<AddProjectRepositoryRouteParams>) { | 17 | }: StaticScreenProps<AddProjectRepositoryRouteParams>) { |
| 15 | const params = route.params ?? {}; | 18 | const params = route.params ?? {}; |
| 16 | const source = Array.isArray(params.source) ? params.source[0] : params.source; | 19 | const source = Array.isArray(params.source) ? params.source[0] : params.source; |
| 17 | const title = | 20 | const title = addProjectRemoteSourceLabel(parseAddProjectRemoteSource(source)); |
| 18 | source === "github" || | ||
| 19 | source === "gitlab" || | ||
| 20 | source === "forgejo" || | ||
| 21 | source === "bitbucket" || | ||
| 22 | source === "azure-devops" | ||
| 23 | ? addProjectRemoteSourceLabel(source) | ||
| 24 | : "Git URL"; | ||
| 25 | 21 | ||
| 26 | return ( | 22 | return ( |
| 27 | <> | 23 | <> |
apps/mobile/src/features/projects/AddProjectScreen.tsx+10-19| ... | @@ -1,4 +1,3 @@ | ... | @@ -1,4 +1,3 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
| 2 | import { MaterialListRow } from "../../components/MaterialListRow"; | 1 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 3 | import { SettingsScreen } from "../settings/components/SettingsScreen"; | 2 | import { SettingsScreen } from "../settings/components/SettingsScreen"; |
| 4 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; | 3 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | @@ -20,6 +19,7 @@ import { | ... | @@ -20,6 +19,7 @@ import { |
| 20 | getNewProjectGitHubTarget, | 19 | getNewProjectGitHubTarget, |
| 21 | getNewProjectPathPreview, | 20 | getNewProjectPathPreview, |
| 22 | normalizePastedCloneUrl, | 21 | normalizePastedCloneUrl, |
| 22 | parseAddProjectRemoteSource, | ||
| 23 | resolveAddProjectPath, | 23 | resolveAddProjectPath, |
| 24 | sortAddProjectProviderSources, | 24 | sortAddProjectProviderSources, |
| 25 | type AddProjectRemoteSource, | 25 | type AddProjectRemoteSource, |
| ... | @@ -49,6 +49,7 @@ import { | ... | @@ -49,6 +49,7 @@ import { |
| 49 | type EnvironmentMachineKind, | 49 | type EnvironmentMachineKind, |
| 50 | ProjectId, | 50 | ProjectId, |
| 51 | resolveEnvironmentMachineKind, | 51 | resolveEnvironmentMachineKind, |
| 52 | SourceControlProviderKind, | ||
| 52 | } from "@t3tools/contracts"; | 53 | } from "@t3tools/contracts"; |
| 53 | import { CommonActions, StackActions, useNavigation } from "@react-navigation/native"; | 54 | import { CommonActions, StackActions, useNavigation } from "@react-navigation/native"; |
| 54 | import { SymbolView } from "../../components/AppSymbol"; | 55 | import { SymbolView } from "../../components/AppSymbol"; |
| ... | @@ -71,6 +72,7 @@ import { AppText as Text, AppTextInput as TextInput } from "../../components/App | ... | @@ -71,6 +72,7 @@ import { AppText as Text, AppTextInput as TextInput } from "../../components/App |
| 71 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; | 72 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; |
| 72 | import { ErrorBanner } from "../../components/ErrorBanner"; | 73 | import { ErrorBanner } from "../../components/ErrorBanner"; |
| 73 | import { SourceControlIcon } from "../../components/SourceControlIcon"; | 74 | import { SourceControlIcon } from "../../components/SourceControlIcon"; |
| 75 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | ||
| 74 | import { ThemedSwitch } from "../../components/ThemedSwitch"; | 76 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 75 | import { uuidv4 } from "../../lib/uuid"; | 77 | import { uuidv4 } from "../../lib/uuid"; |
| 76 | import { useAtomCommand } from "../../state/use-atom-command"; | 78 | import { useAtomCommand } from "../../state/use-atom-command"; |
| ... | @@ -123,18 +125,7 @@ function stringParam(value: string | string[] | undefined): string | null { | ... | @@ -123,18 +125,7 @@ function stringParam(value: string | string[] | undefined): string | null { |
| 123 | } | 125 | } |
| 124 | 126 | ||
| 125 | function sourceFromParam(value: string | string[] | undefined): AddProjectRemoteSource { | 127 | function sourceFromParam(value: string | string[] | undefined): AddProjectRemoteSource { |
| 126 | const source = stringParam(value); | 128 | return parseAddProjectRemoteSource(stringParam(value)); |
| 127 | if ( | ||
| 128 | source === "url" || | ||
| 129 | source === "github" || | ||
| 130 | source === "gitlab" || | ||
| 131 | source === "forgejo" || | ||
| 132 | source === "bitbucket" || | ||
| 133 | source === "azure-devops" | ||
| 134 | ) { | ||
| 135 | return source; | ||
| 136 | } | ||
| 137 | return "url"; | ||
| 138 | } | 129 | } |
| 139 | 130 | ||
| 140 | function SectionTitle(props: { readonly children: string }) { | 131 | function SectionTitle(props: { readonly children: string }) { |
| ... | @@ -499,7 +490,7 @@ function SourceControlRow(props: { | ... | @@ -499,7 +490,7 @@ function SourceControlRow(props: { |
| 499 | /> | 490 | /> |
| 500 | ) : ( | 491 | ) : ( |
| 501 | <SourceControlIcon | 492 | <SourceControlIcon |
| 502 | kind={props.source} | 493 | kind={sourceControlClients.get(props.source).icon} |
| 503 | size={Platform.OS === "android" ? 24 : 18} | 494 | size={Platform.OS === "android" ? 24 : 18} |
| 504 | colorClassName="accent-icon" | 495 | colorClassName="accent-icon" |
| 505 | /> | 496 | /> |
| ... | @@ -659,13 +650,13 @@ export function AddProjectSourceScreen() { | ... | @@ -659,13 +650,13 @@ export function AddProjectSourceScreen() { |
| 659 | key={candidate} | 650 | key={candidate} |
| 660 | source={candidate} | 651 | source={candidate} |
| 661 | selectedEnvironmentId={selectedEnvironment.environmentId} | 652 | selectedEnvironmentId={selectedEnvironment.environmentId} |
| 662 | ready={canCloneProject && readiness[candidate].ready} | 653 | ready={canCloneProject && readiness(candidate).ready} |
| 663 | hint={ | 654 | hint={ |
| 664 | !canCloneProject | 655 | !canCloneProject |
| 665 | ? "This connection cannot clone projects." | 656 | ? "This connection cannot clone projects." |
| 666 | : readiness[candidate].ready | 657 | : readiness(candidate).ready |
| 667 | ? addProjectRemoteSourcePathHint(candidate) | 658 | ? addProjectRemoteSourcePathHint(candidate) |
| 668 | : (readiness[candidate].hint ?? "") | 659 | : (readiness(candidate).hint ?? "") |
| 669 | } | 660 | } |
| 670 | isFirst={false} | 661 | isFirst={false} |
| 671 | /> | 662 | /> |
| ... | @@ -813,7 +804,7 @@ export function AddProjectRepositoryScreen(props: { | ... | @@ -813,7 +804,7 @@ export function AddProjectRepositoryScreen(props: { |
| 813 | const result = await lookupRepositoryQuery({ | 804 | const result = await lookupRepositoryQuery({ |
| 814 | environmentId: environment.environmentId, | 805 | environmentId: environment.environmentId, |
| 815 | input: { | 806 | input: { |
| 816 | provider: SourceControlProviderKind.make(provider), | 807 | provider, |
| 817 | repository: repositoryInput.trim(), | 808 | repository: repositoryInput.trim(), |
| 818 | }, | 809 | }, |
| 819 | }); | 810 | }); |
| ... | @@ -1153,7 +1144,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s | ... | @@ -1153,7 +1144,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1153 | } | 1144 | } |
| 1154 | icon={ | 1145 | icon={ |
| 1155 | <SourceControlIcon | 1146 | <SourceControlIcon |
| 1156 | kind="github" | 1147 | kind={sourceControlClients.get("github").icon} |
| 1157 | size={Platform.OS === "android" ? 24 : 18} | 1148 | size={Platform.OS === "android" ? 24 : 18} |
| 1158 | colorClassName="accent-icon" | 1149 | colorClassName="accent-icon" |
| 1159 | /> | 1150 | /> |
apps/mobile/src/state/thread-pr-presentation.ts+3-3| ... | @@ -4,7 +4,7 @@ import type { | ... | @@ -4,7 +4,7 @@ import type { |
| 4 | ThreadPullRequestLink, | 4 | ThreadPullRequestLink, |
| 5 | VcsStatusResult, | 5 | VcsStatusResult, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl"; | 7 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; |
| 8 | 8 | ||
| 9 | import { | 9 | import { |
| 10 | resolveThreadCurrentPullRequestLink, | 10 | resolveThreadCurrentPullRequestLink, |
| ... | @@ -39,7 +39,7 @@ export function presentThreadPr( | ... | @@ -39,7 +39,7 @@ export function presentThreadPr( |
| 39 | pr: ThreadPr, | 39 | pr: ThreadPr, |
| 40 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, | 40 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, |
| 41 | ): ThreadPrPresentation { | 41 | ): ThreadPrPresentation { |
| 42 | const presentation = resolveChangeRequestPresentation(provider); | 42 | const { changeRequest } = sourceControlClients.get(provider?.kind); |
| 43 | const isDraft = pr.state === "open" && pr.isDraft === true; | 43 | const isDraft = pr.state === "open" && pr.isDraft === true; |
| 44 | return { | 44 | return { |
| 45 | kind: "pull-request", | 45 | kind: "pull-request", |
| ... | @@ -50,7 +50,7 @@ export function presentThreadPr( | ... | @@ -50,7 +50,7 @@ export function presentThreadPr( |
| 50 | updatedAt: pr.updatedAt ?? null, | 50 | updatedAt: pr.updatedAt ?? null, |
| 51 | url: pr.url, | 51 | url: pr.url, |
| 52 | label: String(pr.number), | 52 | label: String(pr.number), |
| 53 | accessibilityLabel: `#${pr.number} ${presentation.longName} ${isDraft ? "draft" : pr.state}`, | 53 | accessibilityLabel: `#${pr.number} ${changeRequest.singular} ${isDraft ? "draft" : pr.state}`, |
| 54 | textClassName: isDraft ? "text-foreground-muted" : PR_STATE_TEXT_CLASS[pr.state], | 54 | textClassName: isDraft ? "text-foreground-muted" : PR_STATE_TEXT_CLASS[pr.state], |
| 55 | }; | 55 | }; |
| 56 | } | 56 | } |
packages/client-runtime/src/operations/projects.test.ts+6-3| ... | @@ -219,9 +219,12 @@ describe("add project shared logic", () => { | ... | @@ -219,9 +219,12 @@ describe("add project shared logic", () => { |
| 219 | }; | 219 | }; |
| 220 | 220 | ||
| 221 | const readiness = buildAddProjectRemoteSourceReadiness(discovery); | 221 | const readiness = buildAddProjectRemoteSourceReadiness(discovery); |
| 222 | expect(readiness.url.ready).toBe(true); | 222 | expect(readiness("url").ready).toBe(true); |
| 223 | expect(readiness.github.ready).toBe(true); | 223 | expect(readiness(SourceControlProviderKind.make("github")).ready).toBe(true); |
| 224 | expect(readiness.gitlab).toEqual({ ready: false, hint: "Run glab auth login" }); | 224 | expect(readiness(SourceControlProviderKind.make("gitlab"))).toEqual({ |
| 225 | ready: false, | ||
| 226 | hint: "Run glab auth login", | ||
| 227 | }); | ||
| 225 | expect(sortAddProjectProviderSources(readiness)[0]).toBe("github"); | 228 | expect(sortAddProjectProviderSources(readiness)[0]).toBe("github"); |
| 226 | }); | 229 | }); |
| 227 | 230 |
packages/client-runtime/src/operations/projects.ts+49-98| ... | @@ -24,14 +24,10 @@ import { | ... | @@ -24,14 +24,10 @@ import { |
| 24 | resolveProjectPathForDispatch, | 24 | resolveProjectPathForDispatch, |
| 25 | } from "../state/projects.ts"; | 25 | } from "../state/projects.ts"; |
| 26 | import type { EnvironmentProject } from "../state/models.ts"; | 26 | import type { EnvironmentProject } from "../state/models.ts"; |
| 27 | import { sourceControlClients } from "../sourceControlClients.ts"; | ||
| 27 | 28 | ||
| 28 | export type AddProjectRemoteProviderKind = | 29 | /** A host to clone from, or `url` for a pasted clone URL. */ |
| 29 | | "github" | 30 | export type AddProjectRemoteSource = SourceControlProviderKind | "url"; |
| 30 | | "gitlab" | ||
| 31 | | "forgejo" | ||
| 32 | | "bitbucket" | ||
| 33 | | "azure-devops"; | ||
| 34 | export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; | ||
| 35 | 31 | ||
| 36 | export function canCreateProjectInEnvironment( | 32 | export function canCreateProjectInEnvironment( |
| 37 | connectionPhase: EnvironmentConnectionPhase | null | undefined, | 33 | connectionPhase: EnvironmentConnectionPhase | null | undefined, |
| ... | @@ -52,10 +48,15 @@ export function availableScratchWorkspaceRoot( | ... | @@ -52,10 +48,15 @@ export function availableScratchWorkspaceRoot( |
| 52 | : null; | 48 | : null; |
| 53 | } | 49 | } |
| 54 | 50 | ||
| 55 | export type AddProjectRemoteSourceReadiness = Record< | 51 | export interface AddProjectRemoteSourceReadinessEntry { |
| 56 | AddProjectRemoteSource, | 52 | readonly ready: boolean; |
| 57 | { readonly ready: boolean; readonly hint: string | null } | 53 | readonly hint: string | null; |
| 58 | >; | 54 | } |
| 55 | |||
| 56 | /** Whether each clone source can be used on an environment, and why not. */ | ||
| 57 | export type AddProjectRemoteSourceReadiness = ( | ||
| 58 | source: AddProjectRemoteSource, | ||
| 59 | ) => AddProjectRemoteSourceReadinessEntry; | ||
| 59 | 60 | ||
| 60 | export type AddProjectCloneFlow = | 61 | export type AddProjectCloneFlow = |
| 61 | | { | 62 | | { |
| ... | @@ -72,62 +73,31 @@ export type AddProjectCloneFlow = | ... | @@ -72,62 +73,31 @@ export type AddProjectCloneFlow = |
| 72 | readonly remoteUrl: string; | 73 | readonly remoteUrl: string; |
| 73 | }; | 74 | }; |
| 74 | 75 | ||
| 75 | const ADD_PROJECT_REMOTE_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [ | 76 | /** The hosts the clone picker offers, in the order the built-in definitions list them. */ |
| 76 | "url", | 77 | const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<SourceControlProviderKind> = |
| 77 | "github", | 78 | sourceControlClients.definitions.map((definition) => definition.kind); |
| 78 | "gitlab", | ||
| 79 | "forgejo", | ||
| 80 | "bitbucket", | ||
| 81 | "azure-devops", | ||
| 82 | ]; | ||
| 83 | |||
| 84 | const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [ | ||
| 85 | "github", | ||
| 86 | "gitlab", | ||
| 87 | "forgejo", | ||
| 88 | "bitbucket", | ||
| 89 | "azure-devops", | ||
| 90 | ]; | ||
| 91 | 79 | ||
| 92 | export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string { | 80 | export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string { |
| 93 | switch (source) { | 81 | return source === "url" ? "Git URL" : sourceControlClients.get(source).pickerLabel; |
| 94 | case "github": | ||
| 95 | return "GitHub"; | ||
| 96 | case "forgejo": | ||
| 97 | return "Forgejo / Gitea"; | ||
| 98 | case "gitlab": | ||
| 99 | return "GitLab"; | ||
| 100 | case "bitbucket": | ||
| 101 | return "Bitbucket"; | ||
| 102 | case "azure-devops": | ||
| 103 | return "Azure DevOps"; | ||
| 104 | case "url": | ||
| 105 | return "Git URL"; | ||
| 106 | } | ||
| 107 | } | 82 | } |
| 108 | 83 | ||
| 109 | export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string { | 84 | export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string { |
| 110 | switch (source) { | 85 | return source === "url" ? "URL" : sourceControlClients.get(source).repositoryPathHint; |
| 111 | case "forgejo": | ||
| 112 | case "github": | ||
| 113 | return "owner/repo"; | ||
| 114 | case "gitlab": | ||
| 115 | return "group/project"; | ||
| 116 | case "bitbucket": | ||
| 117 | return "workspace/repository"; | ||
| 118 | case "azure-devops": | ||
| 119 | return "project/repository"; | ||
| 120 | case "url": | ||
| 121 | return "URL"; | ||
| 122 | } | ||
| 123 | } | 86 | } |
| 124 | 87 | ||
| 125 | export function addProjectRemoteSourceProvider( | 88 | export function addProjectRemoteSourceProvider( |
| 126 | source: AddProjectRemoteSource, | 89 | source: AddProjectRemoteSource, |
| 127 | ): AddProjectRemoteProviderKind | null { | 90 | ): SourceControlProviderKind | null { |
| 128 | return source === "url" ? null : source; | 91 | return source === "url" ? null : source; |
| 129 | } | 92 | } |
| 130 | 93 | ||
| 94 | /** A clone source named in a route or link, or `url` for anything this client does not ship. */ | ||
| 95 | export function parseAddProjectRemoteSource( | ||
| 96 | value: string | null | undefined, | ||
| 97 | ): AddProjectRemoteSource { | ||
| 98 | return (value ? sourceControlClients.find(value)?.kind : undefined) ?? "url"; | ||
| 99 | } | ||
| 100 | |||
| 131 | const GITHUB_REPOSITORY_SHORTHAND = | 101 | const GITHUB_REPOSITORY_SHORTHAND = |
| 132 | /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/; | 102 | /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]+(?:\.git)?$/; |
| 133 | 103 | ||
| ... | @@ -139,18 +109,18 @@ export function normalizePastedCloneUrl(input: string): string { | ... | @@ -139,18 +109,18 @@ export function normalizePastedCloneUrl(input: string): string { |
| 139 | return `https://github.com/${repository}`; | 109 | return `https://github.com/${repository}`; |
| 140 | } | 110 | } |
| 141 | 111 | ||
| 142 | /** GitHub and Forgejo default to HTTPS; other providers retain their existing SSH default. */ | 112 | /** The clone URL for the transport the repository's host defaults to. */ |
| 143 | export function getDefaultCloneUrl( | 113 | export function getDefaultCloneUrl( |
| 144 | repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">, | 114 | repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">, |
| 145 | ): string { | 115 | ): string { |
| 146 | return repository.provider === "github" || repository.provider === "forgejo" | 116 | return sourceControlClients.get(repository.provider).defaultCloneTransport === "https" |
| 147 | ? repository.url | 117 | ? repository.url |
| 148 | : repository.sshUrl; | 118 | : repository.sshUrl; |
| 149 | } | 119 | } |
| 150 | 120 | ||
| 151 | export function sortAddProjectProviderSources( | 121 | export function sortAddProjectProviderSources( |
| 152 | readinessBySource: AddProjectRemoteSourceReadiness, | 122 | readiness: AddProjectRemoteSourceReadiness, |
| 153 | ): ReadonlyArray<AddProjectRemoteProviderKind> { | 123 | ): ReadonlyArray<SourceControlProviderKind> { |
| 154 | return Arr.sort( | 124 | return Arr.sort( |
| 155 | ADD_PROJECT_REMOTE_PROVIDER_SOURCES, | 125 | ADD_PROJECT_REMOTE_PROVIDER_SOURCES, |
| 156 | Order.mapInput( | 126 | Order.mapInput( |
| ... | @@ -158,61 +128,41 @@ export function sortAddProjectProviderSources( | ... | @@ -158,61 +128,41 @@ export function sortAddProjectProviderSources( |
| 158 | ready: Order.flip(Order.Boolean), | 128 | ready: Order.flip(Order.Boolean), |
| 159 | label: Order.String, | 129 | label: Order.String, |
| 160 | }), | 130 | }), |
| 161 | (source: AddProjectRemoteProviderKind) => ({ | 131 | (source: SourceControlProviderKind) => ({ |
| 162 | ready: readinessBySource[source].ready, | 132 | ready: readiness(source).ready, |
| 163 | label: addProjectRemoteSourceLabel(source), | 133 | label: addProjectRemoteSourceLabel(source), |
| 164 | }), | 134 | }), |
| 165 | ), | 135 | ), |
| 166 | ); | 136 | ); |
| 167 | } | 137 | } |
| 168 | 138 | ||
| 139 | const READY: AddProjectRemoteSourceReadinessEntry = { ready: true, hint: null }; | ||
| 140 | const UNAVAILABLE: AddProjectRemoteSourceReadinessEntry = { | ||
| 141 | ready: false, | ||
| 142 | hint: "Provider status unavailable. Open Source Control settings and rescan.", | ||
| 143 | }; | ||
| 144 | |||
| 169 | export function buildAddProjectRemoteSourceReadiness( | 145 | export function buildAddProjectRemoteSourceReadiness( |
| 170 | discovery: SourceControlDiscoveryResult | null, | 146 | discovery: SourceControlDiscoveryResult | null, |
| 171 | ): AddProjectRemoteSourceReadiness { | 147 | ): AddProjectRemoteSourceReadiness { |
| 172 | const unavailable = { | ||
| 173 | ready: false, | ||
| 174 | hint: "Provider status unavailable. Open Source Control settings and rescan.", | ||
| 175 | } as const; | ||
| 176 | const readiness: AddProjectRemoteSourceReadiness = { | ||
| 177 | url: { ready: true, hint: null }, | ||
| 178 | github: unavailable, | ||
| 179 | gitlab: unavailable, | ||
| 180 | forgejo: unavailable, | ||
| 181 | bitbucket: unavailable, | ||
| 182 | "azure-devops": unavailable, | ||
| 183 | }; | ||
| 184 | |||
| 185 | if (!discovery) { | ||
| 186 | return readiness; | ||
| 187 | } | ||
| 188 | |||
| 189 | const providerByKind = new Map( | 148 | const providerByKind = new Map( |
| 190 | discovery.sourceControlProviders.map((provider) => [provider.kind, provider]), | 149 | (discovery?.sourceControlProviders ?? []).map((provider) => [provider.kind, provider]), |
| 191 | ); | 150 | ); |
| 192 | for (const source of ADD_PROJECT_REMOTE_SOURCES) { | 151 | return (source) => { |
| 193 | const kind = addProjectRemoteSourceProvider(source); | 152 | if (source === "url") return READY; |
| 194 | if (!kind) continue; | 153 | const provider = providerByKind.get(source); |
| 195 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); | 154 | if (!provider) return UNAVAILABLE; |
| 196 | if (!provider) { | 155 | if (provider.status !== "available") return { ready: false, hint: provider.installHint }; |
| 197 | readiness[source] = unavailable; | ||
| 198 | continue; | ||
| 199 | } | ||
| 200 | if (provider.status !== "available") { | ||
| 201 | readiness[source] = { ready: false, hint: provider.installHint }; | ||
| 202 | continue; | ||
| 203 | } | ||
| 204 | if (provider.auth.status === "unauthenticated") { | 156 | if (provider.auth.status === "unauthenticated") { |
| 205 | readiness[source] = { | 157 | return { |
| 206 | ready: false, | 158 | ready: false, |
| 207 | hint: | 159 | hint: |
| 208 | Option.getOrNull(provider.auth.detail) ?? | 160 | Option.getOrNull(provider.auth.detail) ?? |
| 209 | `${provider.label} is not authenticated. Open Source Control settings for setup guidance.`, | 161 | `${provider.label} is not authenticated. Open Source Control settings for setup guidance.`, |
| 210 | }; | 162 | }; |
| 211 | continue; | ||
| 212 | } | 163 | } |
| 213 | readiness[source] = { ready: true, hint: null }; | 164 | return READY; |
| 214 | } | 165 | }; |
| 215 | return readiness; | ||
| 216 | } | 166 | } |
| 217 | 167 | ||
| 218 | export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string { | 168 | export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string { |
| ... | @@ -283,8 +233,9 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string): | ... | @@ -283,8 +233,9 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string): |
| 283 | export function getNewProjectGitHubTarget( | 233 | export function getNewProjectGitHubTarget( |
| 284 | discovery: SourceControlDiscoveryResult | null, | 234 | discovery: SourceControlDiscoveryResult | null, |
| 285 | ): { readonly account: string | null } | null { | 235 | ): { readonly account: string | null } | null { |
| 286 | if (!buildAddProjectRemoteSourceReadiness(discovery).github.ready) return null; | 236 | const kind = SourceControlProviderKind.make("github"); |
| 287 | const github = discovery?.sourceControlProviders.find((provider) => provider.kind === "github"); | 237 | if (!buildAddProjectRemoteSourceReadiness(discovery)(kind).ready) return null; |
| 238 | const github = discovery?.sourceControlProviders.find((provider) => provider.kind === kind); | ||
| 288 | return { account: github ? Option.getOrNull(github.auth.account) : null }; | 239 | return { account: github ? Option.getOrNull(github.auth.account) : null }; |
| 289 | } | 240 | } |
| 290 | 241 |