Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
33cf1dc
feat(git): add guided repository configuration
knocking4thcylinder Sep 27, 2026
06e1822
fix(git): handle configuration defaults and worktree overrides
knocking4thcylinder Sep 27, 2026
89f0e5e
test(git): account for inherited configuration
knocking4thcylinder Sep 27, 2026
e57278a
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 27, 2026
083e77f
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 27, 2026
4c0052a
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 28, 2026
14b3886
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 28, 2026
67b8dc0
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 29, 2026
bea1b7c
fix(git): normalize configuration thresholds to MiB
knocking4thcylinder Sep 29, 2026
069e9dd
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 29, 2026
3d9392e
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 29, 2026
e7a6013
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 29, 2026
befdf5a
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 30, 2026
263edbe
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 30, 2026
52cd422
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Sep 30, 2026
dad81c3
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Oct 1, 2026
ac6bf1c
Merge branch 'main' into feat/git-guided-config
knocking4thcylinder Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 171 additions & 6 deletions apps/mobile/src/features/threads/git/GitOverviewSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { gitLargeFileThresholdMib } from "@t3tools/client-runtime/vcs-configuration";
import {
type GitActionRequestInput,
buildMenuItems,
getGitActionDisabledReason,
requiresDefaultBranchConfirmation,
} from "@t3tools/client-runtime/state/vcs";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import {
resolveThreadPullRequestChains,
threadPullRequestKeyOf,
} from "@t3tools/shared/threadPullRequests";
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
import {
EnvironmentId,
ThreadId,
type VcsConfigurationResult,
type VcsConfigurationWriteInput,
} from "@t3tools/contracts";
import {
CommonActions,
StackActions,
Expand All @@ -17,7 +27,15 @@ import {
} from "@react-navigation/native";
import { SymbolView } from "../../../components/AppSymbol";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "react-native";
import {
Alert,
Platform,
Pressable,
RefreshControl,
ScrollView,
TextInput,
View,
} from "react-native";

import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";
import { useSafeAreaInsets } from "react-native-safe-area-context";
Expand All @@ -36,6 +54,7 @@ import {
} from "../../../native/StackHeader";
import { tryOpenExternalUrl } from "../../../lib/openExternalUrl";
import { useEnvironmentQuery } from "../../../state/query";
import { useAtomCommand } from "../../../state/use-atom-command";
import { useThreadSelection } from "../../../state/use-thread-selection";
import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";
import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";
Expand All @@ -46,6 +65,63 @@ import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSh

const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);

function ConfigurationField({
label,
detail,
entry,
setting,
disabled,
onWrite,
}: {
label: string;
detail: string;
entry: VcsConfigurationResult["userName"];
setting: VcsConfigurationWriteInput["setting"];
disabled: boolean;
onWrite: (setting: VcsConfigurationWriteInput["setting"], value: string | null) => void;
}) {
const [value, setValue] = useState(() => {
const initial = entry.repository ?? entry.effective;
return setting === "largeFile" ? gitLargeFileThresholdMib(initial) : (initial ?? "");
});
return (
<View className="gap-2 border-t border-border px-4 py-3">
<Text className="text-sm font-t3-bold">{label}</Text>
<Text className="text-xs text-foreground-muted">{detail}</Text>
<TextInput
className="rounded-xl border border-border bg-screen px-3 py-2 text-foreground"
accessibilityLabel={label}
value={value}
onChangeText={setValue}
editable={!disabled}
autoCapitalize="none"
/>
<Text className="text-xs text-foreground-muted">
{entry.repository !== null
? entry.scope === "worktree"
? "Worktree override"
: "Repository override"
: entry.effective !== null
? `Inherited: ${entry.effective}`
: "Unset"}
</Text>
<View className="flex-row gap-3">
<Pressable
disabled={disabled || value.trim() === ""}
onPress={() => onWrite(setting, value)}
>
<Text className="font-t3-bold text-primary">Save</Text>
</Pressable>
{entry.repository !== null ? (
<Pressable disabled={disabled} onPress={() => onWrite(setting, null)}>
<Text className="font-t3-bold text-foreground-muted">Reset</Text>
</Pressable>
) : null}
</View>
</View>
);
}

type GitOverviewSheetProps = StaticScreenProps<{
readonly environmentId: string;
readonly threadId: string;
Expand Down Expand Up @@ -75,10 +151,6 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
);
const gitState = useSelectedThreadGitState();
const gitActions = useSelectedThreadGitActions();
const theme = useUniwindTheme();
const foregroundColor = theme["--color-foreground"];
const sheetColor = theme["--color-sheet"];

const gitStatus = useEnvironmentQuery(
selectedThread !== null && selectedThreadCwd !== null
? vcsEnvironment.status({
Expand All @@ -87,12 +159,53 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
})
: null,
);
const [configurationOpen, setConfigurationOpen] = useState(false);
const [configurationSaving, setConfigurationSaving] = useState(false);
const configuration = useEnvironmentQuery(
configurationOpen &&
selectedThread !== null &&
selectedThreadCwd !== null &&
gitStatus.data?.isRepo === true
? vcsEnvironment.configuration({
environmentId: selectedThread.environmentId,
input: { cwd: selectedThreadCwd },
})
: null,
);
const writeConfiguration = useAtomCommand(vcsEnvironment.writeConfiguration, {
reportFailure: false,
});
const theme = useUniwindTheme();
const foregroundColor = theme["--color-foreground"];
const sheetColor = theme["--color-sheet"];

const currentBranchLabel = gitStatus.data?.refName ?? selectedThread?.branch ?? "Detached HEAD";
const currentStatusSummary = statusSummary(gitStatus.data);
const currentWorktreePath = selectedThreadWorktreePath;
const gitOperationLabel = gitState.gitOperationLabel;
const busy = gitOperationLabel !== null;
const onWriteConfiguration = useCallback(
(setting: VcsConfigurationWriteInput["setting"], value: string | null) => {
if (selectedThread === null || selectedThreadCwd === null) return;
setConfigurationSaving(true);
void writeConfiguration({
environmentId: selectedThread.environmentId,
input: { cwd: selectedThreadCwd, setting, value },
}).then((result) => {
setConfigurationSaving(false);
if (result._tag === "Success") {
configuration.refresh();
} else if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Could not update repository configuration",
error instanceof Error ? error.message : "An error occurred.",
);
}
});
},
[configuration, selectedThread, selectedThreadCwd, writeConfiguration],
);
const isRepo = gitStatus.data?.isRepo ?? true;
const hasPrimaryRemote = gitStatus.data?.hasPrimaryRemote ?? false;
const isDefaultRef = gitStatus.data?.isDefaultRef ?? false;
Expand Down Expand Up @@ -306,6 +419,58 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
})
}
/>
{isRepo ? (
<>
{Platform.OS !== "android" ? <View className="ml-12 h-px bg-border" /> : null}
<SheetListRow
icon="slider.horizontal.3"
title="Repository configuration"
subtitle="Author identity and large-file settings"
onPress={() => setConfigurationOpen((open) => !open)}
/>
{configurationOpen ? (
gitStatus.error ? (
<Text className="px-4 py-3 text-sm text-destructive">{gitStatus.error}</Text>
) : configuration.error ? (
<Text className="px-4 py-3 text-sm text-destructive">{configuration.error}</Text>
) : configuration.data ? (
<>
<ConfigurationField
key={`${selectedThread?.environmentId}:${selectedThreadCwd}:name:${configuration.data.userName.repository}:${configuration.data.userName.effective}`}
label="Commit author name"
detail="Used for new commits in this repository"
entry={configuration.data.userName}
setting="userName"
disabled={configurationSaving}
onWrite={onWriteConfiguration}
/>
<ConfigurationField
key={`${selectedThread?.environmentId}:${selectedThreadCwd}:email:${configuration.data.userEmail.repository}:${configuration.data.userEmail.effective}`}
label="Commit author email"
detail="Used for new commits in this repository"
entry={configuration.data.userEmail}
setting="userEmail"
disabled={configurationSaving}
onWrite={onWriteConfiguration}
/>
<ConfigurationField
key={`${selectedThread?.environmentId}:${selectedThreadCwd}:large:${configuration.data.largeFile.repository}:${configuration.data.largeFile.effective}`}
label="Large file diff threshold"
detail="Git treats files above this size as binary in diffs. Enter MiB."
entry={configuration.data.largeFile}
setting="largeFile"
disabled={configurationSaving}
onWrite={onWriteConfiguration}
/>
</>
) : (
<Text className="px-4 py-3 text-sm text-foreground-muted">
Loading configuration...
</Text>
)
) : null}
</>
) : null}
</View>

{linkedPrChains.length > 0 ? (
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.vcsCreateRef]: AuthOrchestrationOperateScope,
[WS_METHODS.vcsSwitchRef]: AuthOrchestrationOperateScope,
[WS_METHODS.vcsInit]: AuthOrchestrationOperateScope,
[WS_METHODS.vcsConfigurationRead]: AuthOrchestrationReadScope,
[WS_METHODS.vcsConfigurationWrite]: AuthOrchestrationOperateScope,
[WS_METHODS.reviewGetDiffPreview]: AuthReviewWriteScope,
[WS_METHODS.reviewGetDiffFileContents]: AuthReviewWriteScope,
[WS_METHODS.terminalOpen]: AuthTerminalOperateScope,
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ import * as VcsDriver from "./vcs/VcsDriver.ts";
import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";
import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts";
import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts";
import * as VcsConfigurationService from "./vcs/VcsConfigurationService.ts";
import * as GitHubCli from "./sourceControl/GitHubCli.ts";
import * as VcsProcess from "./vcs/VcsProcess.ts";
import * as GitWorkflowService from "./git/GitWorkflowService.ts";
Expand Down Expand Up @@ -742,6 +743,9 @@ const buildAppUnderTest = (options?: {
const vcsProvisioningLayer = VcsProvisioningService.layer.pipe(
Layer.provide(vcsDriverRegistryLayer),
);
const vcsConfigurationLayer = VcsConfigurationService.layer.pipe(
Layer.provide(vcsDriverRegistryLayer),
);
const reviewLayer = options?.layers?.reviewService
? Layer.mock(ReviewService.ReviewService)({
...options.layers.reviewService,
Expand Down Expand Up @@ -944,7 +948,7 @@ const buildAppUnderTest = (options?: {
Layer.provide(gitVcsDriverLayer),
Layer.provide(gitWorkflowLayer),
Layer.provide(reviewLayer),
Layer.provide(vcsProvisioningLayer),
Layer.provide(Layer.merge(vcsProvisioningLayer, vcsConfigurationLayer)),
Layer.provide(
Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({
...options?.layers?.sourceControlRepositoryService,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts";
import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts";
import * as VcsProcess from "./vcs/VcsProcess.ts";
import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts";
import * as VcsConfigurationService from "./vcs/VcsConfigurationService.ts";
import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";
import * as ProjectCloneTracker from "./project/ProjectCloneTracker.ts";
import * as GitWorkflowService from "./git/GitWorkflowService.ts";
Expand Down Expand Up @@ -394,6 +395,7 @@ const VcsLayerLive = Layer.empty.pipe(
Layer.provideMerge(VcsProjectConfig.layer),
Layer.provideMerge(VcsDriverRegistryLayerLive),
Layer.provideMerge(VcsProvisioningService.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),
Layer.provideMerge(VcsConfigurationService.layer.pipe(Layer.provide(VcsDriverRegistryLayerLive))),
Layer.provideMerge(GitWorkflowLayerLive),
Layer.provideMerge(ReviewLayerLive),
Layer.provideMerge(SourceControlRepositoryServiceLayerLive),
Expand Down
Loading
Loading