| 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 | 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 | 2 | import { withUniwind } from "uniwind"; |
| 3 | 3 | |
| 4 | 4 | const ThemedSvg = withUniwind(Svg); |
| 5 | 5 | |
| 6 | export type SourceControlIconKind = "github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops"; | |
| 7 | ||
| 6 | /** Draws a host's glyph by its client definition's `icon` key; null for a key it has no art for. */ | |
| 8 | 7 | export function SourceControlIcon(props: { |
| 9 | readonly kind: SourceControlIconKind; | |
| 8 | readonly kind: string; | |
| 10 | 9 | readonly size?: number; |
| 11 | 10 | readonly color?: string; |
| 12 | 11 | readonly colorClassName?: string; |
| ... | ... | @@ -252,5 +251,14 @@ export function SourceControlIcon(props: { |
| 252 | 251 | /> |
| 253 | 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 | 1 | import type { StaticScreenProps } from "@react-navigation/native"; |
| 2 | 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 | 8 | import { AddProjectRepositoryScreen } from "./AddProjectScreen"; |
| 6 | 9 | |
| ... | ... | @@ -14,14 +17,7 @@ export function AddProjectRepositoryRoute({ |
| 14 | 17 | }: StaticScreenProps<AddProjectRepositoryRouteParams>) { |
| 15 | 18 | const params = route.params ?? {}; |
| 16 | 19 | const source = Array.isArray(params.source) ? params.source[0] : params.source; |
| 17 | const title = | |
| 18 | source === "github" || | |
| 19 | source === "gitlab" || | |
| 20 | source === "forgejo" || | |
| 21 | source === "bitbucket" || | |
| 22 | source === "azure-devops" | |
| 23 | ? addProjectRemoteSourceLabel(source) | |
| 24 | : "Git URL"; | |
| 20 | const title = addProjectRemoteSourceLabel(parseAddProjectRemoteSource(source)); | |
| 25 | 21 | |
| 26 | 22 | return ( |
| 27 | 23 | <> |
apps/mobile/src/features/projects/AddProjectScreen.tsx+10-19| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | import { SourceControlProviderKind } from "@t3tools/contracts"; | |
| 2 | 1 | import { MaterialListRow } from "../../components/MaterialListRow"; |
| 3 | 2 | import { SettingsScreen } from "../settings/components/SettingsScreen"; |
| 4 | 3 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| ... | ... | @@ -20,6 +19,7 @@ import { |
| 20 | 19 | getNewProjectGitHubTarget, |
| 21 | 20 | getNewProjectPathPreview, |
| 22 | 21 | normalizePastedCloneUrl, |
| 22 | parseAddProjectRemoteSource, | |
| 23 | 23 | resolveAddProjectPath, |
| 24 | 24 | sortAddProjectProviderSources, |
| 25 | 25 | type AddProjectRemoteSource, |
| ... | ... | @@ -49,6 +49,7 @@ import { |
| 49 | 49 | type EnvironmentMachineKind, |
| 50 | 50 | ProjectId, |
| 51 | 51 | resolveEnvironmentMachineKind, |
| 52 | SourceControlProviderKind, | |
| 52 | 53 | } from "@t3tools/contracts"; |
| 53 | 54 | import { CommonActions, StackActions, useNavigation } from "@react-navigation/native"; |
| 54 | 55 | import { SymbolView } from "../../components/AppSymbol"; |
| ... | ... | @@ -71,6 +72,7 @@ import { AppText as Text, AppTextInput as TextInput } from "../../components/App |
| 71 | 72 | import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol"; |
| 72 | 73 | import { ErrorBanner } from "../../components/ErrorBanner"; |
| 73 | 74 | import { SourceControlIcon } from "../../components/SourceControlIcon"; |
| 75 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 74 | 76 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 75 | 77 | import { uuidv4 } from "../../lib/uuid"; |
| 76 | 78 | import { useAtomCommand } from "../../state/use-atom-command"; |
| ... | ... | @@ -123,18 +125,7 @@ function stringParam(value: string | string[] | undefined): string | null { |
| 123 | 125 | } |
| 124 | 126 | |
| 125 | 127 | function sourceFromParam(value: string | string[] | undefined): AddProjectRemoteSource { |
| 126 | const source = 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"; | |
| 128 | return parseAddProjectRemoteSource(stringParam(value)); | |
| 138 | 129 | } |
| 139 | 130 | |
| 140 | 131 | function SectionTitle(props: { readonly children: string }) { |
| ... | ... | @@ -499,7 +490,7 @@ function SourceControlRow(props: { |
| 499 | 490 | /> |
| 500 | 491 | ) : ( |
| 501 | 492 | <SourceControlIcon |
| 502 | kind={props.source} | |
| 493 | kind={sourceControlClients.get(props.source).icon} | |
| 503 | 494 | size={Platform.OS === "android" ? 24 : 18} |
| 504 | 495 | colorClassName="accent-icon" |
| 505 | 496 | /> |
| ... | ... | @@ -659,13 +650,13 @@ export function AddProjectSourceScreen() { |
| 659 | 650 | key={candidate} |
| 660 | 651 | source={candidate} |
| 661 | 652 | selectedEnvironmentId={selectedEnvironment.environmentId} |
| 662 | ready={canCloneProject && readiness[candidate].ready} | |
| 653 | ready={canCloneProject && readiness(candidate).ready} | |
| 663 | 654 | hint={ |
| 664 | 655 | !canCloneProject |
| 665 | 656 | ? "This connection cannot clone projects." |
| 666 | : readiness[candidate].ready | |
| 657 | : readiness(candidate).ready | |
| 667 | 658 | ? addProjectRemoteSourcePathHint(candidate) |
| 668 | : (readiness[candidate].hint ?? "") | |
| 659 | : (readiness(candidate).hint ?? "") | |
| 669 | 660 | } |
| 670 | 661 | isFirst={false} |
| 671 | 662 | /> |
| ... | ... | @@ -813,7 +804,7 @@ export function AddProjectRepositoryScreen(props: { |
| 813 | 804 | const result = await lookupRepositoryQuery({ |
| 814 | 805 | environmentId: environment.environmentId, |
| 815 | 806 | input: { |
| 816 | provider: SourceControlProviderKind.make(provider), | |
| 807 | provider, | |
| 817 | 808 | repository: repositoryInput.trim(), |
| 818 | 809 | }, |
| 819 | 810 | }); |
| ... | ... | @@ -1153,7 +1144,7 @@ export function AddProjectNewScreen(props: { readonly environmentId?: string | s |
| 1153 | 1144 | } |
| 1154 | 1145 | icon={ |
| 1155 | 1146 | <SourceControlIcon |
| 1156 | kind="github" | |
| 1147 | kind={sourceControlClients.get("github").icon} | |
| 1157 | 1148 | size={Platform.OS === "android" ? 24 : 18} |
| 1158 | 1149 | colorClassName="accent-icon" |
| 1159 | 1150 | /> |
apps/mobile/src/state/thread-pr-presentation.ts+3-3| ... | ... | @@ -4,7 +4,7 @@ import type { |
| 4 | 4 | ThreadPullRequestLink, |
| 5 | 5 | VcsStatusResult, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl"; | |
| 7 | import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; | |
| 8 | 8 | |
| 9 | 9 | import { |
| 10 | 10 | resolveThreadCurrentPullRequestLink, |
| ... | ... | @@ -39,7 +39,7 @@ export function presentThreadPr( |
| 39 | 39 | pr: ThreadPr, |
| 40 | 40 | provider: VcsStatusResult["sourceControlProvider"] | null | undefined, |
| 41 | 41 | ): ThreadPrPresentation { |
| 42 | const presentation = resolveChangeRequestPresentation(provider); | |
| 42 | const { changeRequest } = sourceControlClients.get(provider?.kind); | |
| 43 | 43 | const isDraft = pr.state === "open" && pr.isDraft === true; |
| 44 | 44 | return { |
| 45 | 45 | kind: "pull-request", |
| ... | ... | @@ -50,7 +50,7 @@ export function presentThreadPr( |
| 50 | 50 | updatedAt: pr.updatedAt ?? null, |
| 51 | 51 | url: pr.url, |
| 52 | 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 | 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 | 219 | }; |
| 220 | 220 | |
| 221 | 221 | const readiness = buildAddProjectRemoteSourceReadiness(discovery); |
| 222 | expect(readiness.url.ready).toBe(true); | |
| 223 | expect(readiness.github.ready).toBe(true); | |
| 224 | expect(readiness.gitlab).toEqual({ ready: false, hint: "Run glab auth login" }); | |
| 222 | expect(readiness("url").ready).toBe(true); | |
| 223 | expect(readiness(SourceControlProviderKind.make("github")).ready).toBe(true); | |
| 224 | expect(readiness(SourceControlProviderKind.make("gitlab"))).toEqual({ | |
| 225 | ready: false, | |
| 226 | hint: "Run glab auth login", | |
| 227 | }); | |
| 225 | 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 | 24 | resolveProjectPathForDispatch, |
| 25 | 25 | } from "../state/projects.ts"; |
| 26 | 26 | import type { EnvironmentProject } from "../state/models.ts"; |
| 27 | import { sourceControlClients } from "../sourceControlClients.ts"; | |
| 27 | 28 | |
| 28 | export type AddProjectRemoteProviderKind = | |
| 29 | | "github" | |
| 30 | | "gitlab" | |
| 31 | | "forgejo" | |
| 32 | | "bitbucket" | |
| 33 | | "azure-devops"; | |
| 34 | export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; | |
| 29 | /** A host to clone from, or `url` for a pasted clone URL. */ | |
| 30 | export type AddProjectRemoteSource = SourceControlProviderKind | "url"; | |
| 35 | 31 | |
| 36 | 32 | export function canCreateProjectInEnvironment( |
| 37 | 33 | connectionPhase: EnvironmentConnectionPhase | null | undefined, |
| ... | ... | @@ -52,10 +48,15 @@ export function availableScratchWorkspaceRoot( |
| 52 | 48 | : null; |
| 53 | 49 | } |
| 54 | 50 | |
| 55 | export type AddProjectRemoteSourceReadiness = Record< | |
| 56 | AddProjectRemoteSource, | |
| 57 | { readonly ready: boolean; readonly hint: string | null } | |
| 58 | >; | |
| 51 | export interface AddProjectRemoteSourceReadinessEntry { | |
| 52 | readonly ready: boolean; | |
| 53 | readonly hint: string | null; | |
| 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 | 61 | export type AddProjectCloneFlow = |
| 61 | 62 | | { |
| ... | ... | @@ -72,62 +73,31 @@ export type AddProjectCloneFlow = |
| 72 | 73 | readonly remoteUrl: string; |
| 73 | 74 | }; |
| 74 | 75 | |
| 75 | const ADD_PROJECT_REMOTE_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [ | |
| 76 | "url", | |
| 77 | "github", | |
| 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 | ]; | |
| 76 | /** The hosts the clone picker offers, in the order the built-in definitions list them. */ | |
| 77 | const ADD_PROJECT_REMOTE_PROVIDER_SOURCES: ReadonlyArray<SourceControlProviderKind> = | |
| 78 | sourceControlClients.definitions.map((definition) => definition.kind); | |
| 91 | 79 | |
| 92 | 80 | export function addProjectRemoteSourceLabel(source: AddProjectRemoteSource): string { |
| 93 | switch (source) { | |
| 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 | } | |
| 81 | return source === "url" ? "Git URL" : sourceControlClients.get(source).pickerLabel; | |
| 107 | 82 | } |
| 108 | 83 | |
| 109 | 84 | export function addProjectRemoteSourcePathHint(source: AddProjectRemoteSource): string { |
| 110 | switch (source) { | |
| 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 | } | |
| 85 | return source === "url" ? "URL" : sourceControlClients.get(source).repositoryPathHint; | |
| 123 | 86 | } |
| 124 | 87 | |
| 125 | 88 | export function addProjectRemoteSourceProvider( |
| 126 | 89 | source: AddProjectRemoteSource, |
| 127 | ): AddProjectRemoteProviderKind | null { | |
| 90 | ): SourceControlProviderKind | null { | |
| 128 | 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 | 101 | const GITHUB_REPOSITORY_SHORTHAND = |
| 132 | 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 | 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 | 113 | export function getDefaultCloneUrl( |
| 144 | 114 | repository: Pick<SourceControlRepositoryInfo, "provider" | "url" | "sshUrl">, |
| 145 | 115 | ): string { |
| 146 | return repository.provider === "github" || repository.provider === "forgejo" | |
| 116 | return sourceControlClients.get(repository.provider).defaultCloneTransport === "https" | |
| 147 | 117 | ? repository.url |
| 148 | 118 | : repository.sshUrl; |
| 149 | 119 | } |
| 150 | 120 | |
| 151 | 121 | export function sortAddProjectProviderSources( |
| 152 | readinessBySource: AddProjectRemoteSourceReadiness, | |
| 153 | ): ReadonlyArray<AddProjectRemoteProviderKind> { | |
| 122 | readiness: AddProjectRemoteSourceReadiness, | |
| 123 | ): ReadonlyArray<SourceControlProviderKind> { | |
| 154 | 124 | return Arr.sort( |
| 155 | 125 | ADD_PROJECT_REMOTE_PROVIDER_SOURCES, |
| 156 | 126 | Order.mapInput( |
| ... | ... | @@ -158,61 +128,41 @@ export function sortAddProjectProviderSources( |
| 158 | 128 | ready: Order.flip(Order.Boolean), |
| 159 | 129 | label: Order.String, |
| 160 | 130 | }), |
| 161 | (source: AddProjectRemoteProviderKind) => ({ | |
| 162 | ready: readinessBySource[source].ready, | |
| 131 | (source: SourceControlProviderKind) => ({ | |
| 132 | ready: readiness(source).ready, | |
| 163 | 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 | 145 | export function buildAddProjectRemoteSourceReadiness( |
| 170 | 146 | discovery: SourceControlDiscoveryResult | null, |
| 171 | 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 | 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) { | |
| 193 | const kind = addProjectRemoteSourceProvider(source); | |
| 194 | if (!kind) continue; | |
| 195 | const provider = providerByKind.get(SourceControlProviderKind.make(kind)); | |
| 196 | if (!provider) { | |
| 197 | readiness[source] = unavailable; | |
| 198 | continue; | |
| 199 | } | |
| 200 | if (provider.status !== "available") { | |
| 201 | readiness[source] = { ready: false, hint: provider.installHint }; | |
| 202 | continue; | |
| 203 | } | |
| 151 | return (source) => { | |
| 152 | if (source === "url") return READY; | |
| 153 | const provider = providerByKind.get(source); | |
| 154 | if (!provider) return UNAVAILABLE; | |
| 155 | if (provider.status !== "available") return { ready: false, hint: provider.installHint }; | |
| 204 | 156 | if (provider.auth.status === "unauthenticated") { |
| 205 | readiness[source] = { | |
| 157 | return { | |
| 206 | 158 | ready: false, |
| 207 | 159 | hint: |
| 208 | 160 | Option.getOrNull(provider.auth.detail) ?? |
| 209 | 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 }; | |
| 214 | } | |
| 215 | return readiness; | |
| 164 | return READY; | |
| 165 | }; | |
| 216 | 166 | } |
| 217 | 167 | |
| 218 | 168 | export function getAddProjectInitialQuery(baseDirectory: string | null | undefined): string { |
| ... | ... | @@ -283,8 +233,9 @@ export function getNewProjectPathPreview(newProjectsRoot: string, name: string): |
| 283 | 233 | export function getNewProjectGitHubTarget( |
| 284 | 234 | discovery: SourceControlDiscoveryResult | null, |
| 285 | 235 | ): { readonly account: string | null } | null { |
| 286 | if (!buildAddProjectRemoteSourceReadiness(discovery).github.ready) return null; | |
| 287 | const github = discovery?.sourceControlProviders.find((provider) => provider.kind === "github"); | |
| 236 | const kind = SourceControlProviderKind.make("github"); | |
| 237 | if (!buildAddProjectRemoteSourceReadiness(discovery)(kind).ready) return null; | |
| 238 | const github = discovery?.sourceControlProviders.find((provider) => provider.kind === kind); | |
| 288 | 239 | return { account: github ? Option.getOrNull(github.auth.account) : null }; |
| 289 | 240 | } |
| 290 | 241 |