Skip to content

Commit 32c908e

Browse files
committed
Add branch checkout support to repo modal
1 parent 8893a2f commit 32c908e

1 file changed

Lines changed: 87 additions & 18 deletions

File tree

components/modals/RepoFormModal.tsx

Lines changed: 87 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,13 +1167,25 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
11671167
const [newBranchName, setNewBranchName] = useState('');
11681168
const [branchToMerge, setBranchToMerge] = useState('');
11691169
const [selectedBranch, setSelectedBranch] = useState<{ name: string; scope: 'local' | 'remote' } | null>(null);
1170+
const [isCheckoutLoading, setIsCheckoutLoading] = useState(false);
11701171
const [branchFilter, setBranchFilter] = useState('');
11711172
const [debouncedBranchFilter, setDebouncedBranchFilter] = useState('');
11721173
const branchItemRefs = useRef<{ local: Map<string, HTMLDivElement>; remote: Map<string, HTMLDivElement> }>({
11731174
local: new Map<string, HTMLDivElement>(),
11741175
remote: new Map<string, HTMLDivElement>(),
11751176
});
11761177

1178+
const normalizedSelectedBranchName = useMemo(() => {
1179+
if (!selectedBranch) {
1180+
return '';
1181+
}
1182+
if (selectedBranch.scope === 'remote') {
1183+
const segments = selectedBranch.name.split('/').slice(1);
1184+
return segments.join('/') || selectedBranch.name;
1185+
}
1186+
return selectedBranch.name;
1187+
}, [selectedBranch]);
1188+
11771189
// State for Releases Tab
11781190
const [releases, setReleases] = useState<ReleaseInfo[] | null>(null);
11791191
const [releasesLoading, setReleasesLoading] = useState(false);
@@ -1611,6 +1623,44 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
16111623
element?.focus();
16121624
}, []);
16131625

1626+
const handleCheckoutBranch = useCallback(async () => {
1627+
if (!repository || !selectedBranch) {
1628+
return;
1629+
}
1630+
1631+
const currentBranch = branchInfo?.current;
1632+
const focusTarget = normalizedSelectedBranchName || selectedBranch.name;
1633+
const checkoutLabel = selectedBranch.name;
1634+
1635+
if (currentBranch && normalizedSelectedBranchName && normalizedSelectedBranchName === currentBranch) {
1636+
setToast({ message: `Already on '${currentBranch}'.`, type: 'info' });
1637+
return;
1638+
}
1639+
1640+
setIsCheckoutLoading(true);
1641+
const checkoutTarget = selectedBranch.name;
1642+
1643+
try {
1644+
const result = await window.electronAPI?.checkoutBranch(repository.localPath, checkoutTarget);
1645+
if (result?.success) {
1646+
setToast({ message: `Checked out '${checkoutLabel}'.`, type: 'success' });
1647+
await fetchBranches();
1648+
await onRefreshState(repository.id);
1649+
setBranchToMerge('');
1650+
setSelectedBranch(null);
1651+
if (focusTarget) {
1652+
requestAnimationFrame(() => focusBranchItem('local', focusTarget));
1653+
}
1654+
} else {
1655+
setToast({ message: `Checkout failed: ${result?.error || 'Electron API not available.'}`, type: 'error' });
1656+
}
1657+
} catch (error: any) {
1658+
setToast({ message: `Checkout failed: ${error.message}`, type: 'error' });
1659+
} finally {
1660+
setIsCheckoutLoading(false);
1661+
}
1662+
}, [repository, selectedBranch, branchInfo?.current, normalizedSelectedBranchName, setToast, fetchBranches, onRefreshState, focusBranchItem]);
1663+
16141664
const handleBranchKeyDown = useCallback((event: React.KeyboardEvent<HTMLDivElement>, branchName: string, scope: 'local' | 'remote') => {
16151665
if (event.key === 'Enter' || event.key === ' ') {
16161666
event.preventDefault();
@@ -1853,7 +1903,12 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
18531903
</div>
18541904
</div>
18551905
);
1856-
case 'branches':
1906+
case 'branches': {
1907+
const isCurrentSelection = Boolean(normalizedSelectedBranchName && branchInfo?.current && normalizedSelectedBranchName === branchInfo.current);
1908+
const checkoutDisabled = !selectedBranch || isCheckoutLoading || branchesLoading || isCurrentSelection;
1909+
const selectionDescription = selectedBranch
1910+
? `${selectedBranch.scope === 'remote' ? 'Remote' : 'Local'} branch: ${selectedBranch.name}`
1911+
: 'Select a branch to checkout.';
18571912
return (
18581913
<div className="flex-1 flex flex-col overflow-hidden">
18591914
<div className="flex-1 flex flex-col overflow-hidden p-4 gap-4">
@@ -1953,24 +2008,37 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
19532008
</ul>
19542009
</div>
19552010
</div>
1956-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 mt-2 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
1957-
<div>
1958-
<h4 className="font-semibold mb-2">Create New Branch</h4>
1959-
<div className="flex gap-2">
1960-
<input type="text" value={newBranchName} onChange={e => setNewBranchName(e.target.value)} placeholder="new-branch-name" className={formInputStyle}/>
1961-
<button type="button" onClick={handleCreateBranch} className="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-medium hover:bg-green-700">Create</button>
1962-
</div>
2011+
<div className="pt-4 mt-2 border-t border-gray-200 dark:border-gray-700 flex flex-col gap-4 flex-shrink-0">
2012+
<div className="flex flex-wrap items-center justify-between gap-3">
2013+
<p className="text-sm text-gray-600 dark:text-gray-300">{selectionDescription}</p>
2014+
<button
2015+
type="button"
2016+
onClick={handleCheckoutBranch}
2017+
disabled={checkoutDisabled}
2018+
className={`px-4 py-2 rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:bg-gray-400 disabled:cursor-not-allowed ${isCheckoutLoading ? 'cursor-wait' : ''}`}
2019+
>
2020+
{isCheckoutLoading ? 'Checking out...' : 'Checkout'}
2021+
</button>
19632022
</div>
1964-
<div>
1965-
<h4 className="font-semibold mb-2">Merge Branch into Current</h4>
1966-
<div className="flex gap-2">
1967-
<select value={branchToMerge || ''} onChange={e => setBranchToMerge(e.target.value)} className={formInputStyle}>
1968-
<option value="" disabled>Select a branch</option>
1969-
{(branchInfo?.local || []).filter(b => b !== branchInfo?.current).map(b => (
1970-
<option key={b} value={b}>{b}</option>
1971-
))}
1972-
</select>
1973-
<button type="button" onClick={handleMergeBranch} className="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">Merge</button>
2023+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
2024+
<div>
2025+
<h4 className="font-semibold mb-2">Create New Branch</h4>
2026+
<div className="flex gap-2">
2027+
<input type="text" value={newBranchName} onChange={e => setNewBranchName(e.target.value)} placeholder="new-branch-name" className={formInputStyle}/>
2028+
<button type="button" onClick={handleCreateBranch} className="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-medium hover:bg-green-700">Create</button>
2029+
</div>
2030+
</div>
2031+
<div>
2032+
<h4 className="font-semibold mb-2">Merge Branch into Current</h4>
2033+
<div className="flex gap-2">
2034+
<select value={branchToMerge || ''} onChange={e => setBranchToMerge(e.target.value)} className={formInputStyle}>
2035+
<option value="" disabled>Select a branch</option>
2036+
{(branchInfo?.local || []).filter(b => b !== branchInfo?.current).map(b => (
2037+
<option key={b} value={b}>{b}</option>
2038+
))}
2039+
</select>
2040+
<button type="button" onClick={handleMergeBranch} className="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">Merge</button>
2041+
</div>
19742042
</div>
19752043
</div>
19762044
</div>
@@ -1979,6 +2047,7 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
19792047
</div>
19802048
</div>
19812049
);
2050+
}
19822051
case 'releases':
19832052
if (!isGitHubRepo) return <div className="p-4 text-center text-gray-500">Release management is only available for repositories hosted on GitHub.</div>;
19842053
if (editingRelease) {

0 commit comments

Comments
 (0)