Skip to content
Source
Sign inSign up
Open History to see the last commit

GitBranchesSheet.tsx

171 lines6.4 KB

1import { sanitizeFeatureBranchName } from "@t3tools/shared/git";
2import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
3import { useState } from "react";
4​
5import { SheetHeader } from "../../../components/ui/SheetHeader";
6import { FitSheet } from "../../../components/ui/FitSheet";
7import { FormField, FormSection } from "../../../components/ui/FormSection";
8import { ListChoiceRow } from "../../../components/ui/ListChoiceRow";
9import { ListSection } from "../../../components/ui/ListSection";
10import { useEnvironmentQuery } from "../../../state/query";
11import { useThreadSelection } from "../../../state/use-thread-selection";
12import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions";
13import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state";
14import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-worktree";
15import { vcsEnvironment } from "../../../state/vcs";
16import { SheetActionButton } from "./gitSheetComponents";
17​
18type GitBranchesSheetProps = StaticScreenProps<{
19 readonly environmentId: string;
20 readonly threadId: string;
21}>;
22​
23export function GitBranchesSheet(_props: GitBranchesSheetProps) {
24 const navigation = useNavigation();
25 const { selectedThread } = useThreadSelection();
26 const { selectedThreadCwd } = useSelectedThreadWorktree();
27 const gitState = useSelectedThreadGitState();
28 const gitActions = useSelectedThreadGitActions();
29 const { canChangeThreadBranch } = gitActions;
30​
31 const gitStatus = useEnvironmentQuery(
32 selectedThread !== null && selectedThreadCwd !== null
33 ? vcsEnvironment.branch({
34 environmentId: selectedThread.environmentId,
35 input: { cwd: selectedThreadCwd },
36 })
37 : null,
38 );
39​
40 const currentBranchLabel = gitStatus.data?.refName ?? selectedThread?.branch ?? "Detached HEAD";
41 const currentWorktreePath = selectedThreadCwd;
42 const availableBranches = gitState.selectedThreadBranches;
43 const branchesLoading = gitState.selectedThreadBranchesLoading;
44 const busy = gitState.gitOperationLabel !== null;
45​
46 const [newBranchName, setNewBranchName] = useState("");
47 const [worktreeBaseBranchOverride, setWorktreeBaseBranchOverride] = useState<string | null>(null);
48 const worktreeBaseBranch =
49 worktreeBaseBranchOverride ??
50 (currentBranchLabel === "Detached HEAD" ? "main" : currentBranchLabel);
51 const [worktreeBranchName, setWorktreeBranchName] = useState("");
52​
53 const disabledExistingBranchNames: Array<string> = [];
54 for (const branch of availableBranches) {
55 if (branch.worktreePath !== null && branch.worktreePath !== currentWorktreePath) {
56 disabledExistingBranchNames.push(branch.name);
57 }
58 }
59 const disabledExistingBranches = new Set(disabledExistingBranchNames);
60​
61 return (
62 <FitSheet
63 header={<SheetHeader title="Branches & worktrees" onClose={() => navigation.goBack()} />}
64 keyboardShouldPersistTaps="handled"
65 showsVerticalScrollIndicator={false}
66 >
67 <FormSection title="New branch">
68 <FormField
69 value={newBranchName}
70 onChangeText={setNewBranchName}
71 placeholder="feature/mobile-polish"
72 accessibilityLabel="New branch name"
73 />
74 <SheetActionButton
75 icon="plus"
76 label="Create & checkout"
77 tone="primary"
78 disabled={!canChangeThreadBranch || busy || newBranchName.trim().length === 0}
79 onPress={() => {
80 const branch = sanitizeFeatureBranchName(newBranchName.trim());
81 if (branch.length === 0) return;
82 void gitActions.onCreateSelectedThreadBranch(branch).then((result) => {
83 if (result === null) return;
84 setNewBranchName("");
85 navigation.goBack();
86 });
87 }}
88 />
89 </FormSection>
90​
91 <FormSection title="New worktree">
92 <FormField
93 label="Base branch"
94 value={worktreeBaseBranch}
95 onChangeText={setWorktreeBaseBranchOverride}
96 placeholder="main"
97 accessibilityLabel="Worktree base branch"
98 />
99 <FormField
100 label="New branch"
101 value={worktreeBranchName}
102 onChangeText={setWorktreeBranchName}
103 placeholder="feature/mobile-thread"
104 accessibilityLabel="Worktree branch name"
105 />
106 <SheetActionButton
107 icon="square.split.2x1"
108 label="Create worktree"
109 tone="primary"
110 disabled={
111 !canChangeThreadBranch ||
112 busy ||
113 worktreeBaseBranch.trim().length === 0 ||
114 worktreeBranchName.trim().length === 0
115 }
116 onPress={() => {
117 const baseBranch = worktreeBaseBranch.trim();
118 const newBranch = worktreeBranchName.trim();
119 if (baseBranch.length === 0 || newBranch.length === 0) return;
120 void gitActions
121 .onCreateSelectedThreadWorktree({ baseBranch, newBranch })
122 .then((result) => {
123 if (result === null) return;
124 setWorktreeBranchName("");
125 navigation.goBack();
126 });
127 }}
128 />
129 </FormSection>
130​
131 <ListSection
132 title="Existing branches"
133 footer={
134 branchesLoading
135 ? "Loading branches..."
136 : availableBranches.length === 0
137 ? "No local branches found."
138 : undefined
139 }
140 >
141 {availableBranches.map((branch, index) => {
142 const disabled = disabledExistingBranches.has(branch.name);
143 const subtitle = branch.worktreePath
144 ? branch.worktreePath === currentWorktreePath
145 ? "Checked out in this thread"
146 : "Checked out in another worktree"
147 : branch.isDefault
148 ? "Default branch"
149 : "Local branch";
150​
151 return (
152 <ListChoiceRow
153 key={branch.name}
154 label={branch.name}
155 description={subtitle}
156 selected={branch.current}
157 separated={index > 0}
158 disabled={!canChangeThreadBranch || busy || disabled}
159 onPress={() => {
160 void gitActions.onCheckoutSelectedThreadBranch(branch.name).then((result) => {
161 if (result === null) return;
162 navigation.goBack();
163 });
164 }}
165 />
166 );
167 })}
168 </ListSection>
169 </FitSheet>
170 );