feat: Add git log parsing and rebase endpoint with input validation

This commit is contained in:
gsxdsm
2026-02-18 00:31:31 -08:00
parent e6e04d57bc
commit d30296d559
42 changed files with 2826 additions and 376 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import {
Dialog,
DialogContent,
@@ -149,6 +149,9 @@ export function CherryPickDialog({
const [commitLimit, setCommitLimit] = useState(30);
const [hasMoreCommits, setHasMoreCommits] = useState(false);
// Ref to track the latest fetchCommits request and ignore stale responses
const fetchCommitsRequestRef = useRef(0);
// Cherry-pick state
const [isCherryPicking, setIsCherryPicking] = useState(false);
@@ -182,6 +185,8 @@ export function CherryPickDialog({
useEffect(() => {
if (!open || !worktree) return;
let mounted = true;
const fetchBranchData = async () => {
setLoadingBranches(true);
try {
@@ -193,6 +198,8 @@ export function CherryPickDialog({
api.worktree.listBranches(worktree.path, false),
]);
if (!mounted) return;
if (remotesResult.success && remotesResult.result) {
setRemotes(remotesResult.result.remotes);
// Default to first remote if available, otherwise local
@@ -212,13 +219,20 @@ export function CherryPickDialog({
setLocalBranches(branches);
}
} catch (err) {
if (!mounted) return;
console.error('Failed to fetch branch data:', err);
} finally {
setLoadingBranches(false);
if (mounted) {
setLoadingBranches(false);
}
}
};
fetchBranchData();
return () => {
mounted = false;
};
}, [open, worktree]);
// Fetch commits when branch is selected
@@ -226,6 +240,9 @@ export function CherryPickDialog({
async (limit: number = 30, append: boolean = false) => {
if (!worktree || !selectedBranch) return;
// Increment the request counter and capture the current request ID
const requestId = ++fetchCommitsRequestRef.current;
if (append) {
setLoadingMoreCommits(true);
} else {
@@ -239,18 +256,28 @@ export function CherryPickDialog({
const api = getHttpApiClient();
const result = await api.worktree.getBranchCommitLog(worktree.path, selectedBranch, limit);
// Ignore stale responses from superseded requests
if (requestId !== fetchCommitsRequestRef.current) return;
if (result.success && result.result) {
setCommits(result.result.commits);
// If we got exactly the limit, there may be more commits
setHasMoreCommits(result.result.commits.length >= limit);
} else {
} else if (!append) {
setCommitsError(result.error || 'Failed to load commits');
}
} catch (err) {
setCommitsError(err instanceof Error ? err.message : 'Failed to load commits');
// Ignore stale responses from superseded requests
if (requestId !== fetchCommitsRequestRef.current) return;
if (!append) {
setCommitsError(err instanceof Error ? err.message : 'Failed to load commits');
}
} finally {
setLoadingCommits(false);
setLoadingMoreCommits(false);
// Only update loading state if this is still the current request
if (requestId === fetchCommitsRequestRef.current) {
setLoadingCommits(false);
setLoadingMoreCommits(false);
}
}
},
[worktree, selectedBranch]
@@ -384,6 +411,7 @@ export function CherryPickDialog({
sourceBranch: selectedBranch,
targetBranch: conflictInfo.targetBranch,
targetWorktreePath: conflictInfo.targetWorktreePath,
operationType: 'merge',
});
onOpenChange(false);
}
@@ -703,7 +731,7 @@ export function CherryPickDialog({
<SelectTrigger className="w-full">
<SelectValue placeholder="Select source..." />
</SelectTrigger>
<SelectContent className="text-black dark:text-black">
<SelectContent className="text-foreground">
<SelectItem value="__local__">Local Branches</SelectItem>
{remotes.map((remote) => (
<SelectItem key={remote.name} value={remote.name}>
@@ -725,7 +753,7 @@ export function CherryPickDialog({
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a branch..." />
</SelectTrigger>
<SelectContent className="text-black dark:text-black">
<SelectContent className="text-foreground">
{branchOptions.map((branch) => (
<SelectItem key={branch} value={branch}>
{branch}