mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 02:15:34 +08:00
fix(git): handle repositories without commits across status and remote flows
Improve git route behavior for repositories initialized with `git init` but with no commits yet. Previously, several routes called `git rev-parse --abbrev-ref HEAD`, which fails before the first commit and caused noisy console errors plus a broken Git panel state. What changed - add `getGitErrorDetails` helper to normalize git process failure text - add `isMissingHeadRevisionError` helper to detect no-HEAD/no-revision cases - add `getCurrentBranchName` helper: - uses `git symbolic-ref --short HEAD` first (works before first commit) - falls back to `git rev-parse --abbrev-ref HEAD` for detached HEAD and edge cases - add `repositoryHasCommits` helper using `git rev-parse --verify HEAD` Status route improvements - replace inline branch/HEAD error handling with shared helpers - keep returning valid branch + `hasCommits: false` for fresh repositories Remote status improvements - avoid hard failure when repository has no commits - return a safe, non-error payload with: - `hasUpstream: false` - `ahead: 0`, `behind: 0` - detected remote name when remotes exist - message: "Repository has no commits yet" - preserve existing upstream detection behavior for repositories with commits Consistency updates - switch fetch/pull/push/publish branch lookup to shared `getCurrentBranchName` to ensure the same branch-resolution behavior everywhere Result - `git init` repositories no longer trigger `rev-parse HEAD` ambiguity failures - Git panel remains usable before the first commit - backend branch detection is centralized and consistent across git operations
This commit is contained in:
@@ -139,6 +139,46 @@ async function validateGitRepository(projectPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGitErrorDetails(error) {
|
||||||
|
return `${error?.message || ''} ${error?.stderr || ''} ${error?.stdout || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMissingHeadRevisionError(error) {
|
||||||
|
const errorDetails = getGitErrorDetails(error).toLowerCase();
|
||||||
|
return errorDetails.includes('unknown revision')
|
||||||
|
|| errorDetails.includes('ambiguous argument')
|
||||||
|
|| errorDetails.includes('needed a single revision')
|
||||||
|
|| errorDetails.includes('bad revision');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCurrentBranchName(projectPath) {
|
||||||
|
try {
|
||||||
|
// symbolic-ref works even when the repository has no commits.
|
||||||
|
const { stdout } = await spawnAsync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: projectPath });
|
||||||
|
const branchName = stdout.trim();
|
||||||
|
if (branchName) {
|
||||||
|
return branchName;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Fall back to rev-parse for detached HEAD and older git edge cases.
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
||||||
|
return stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repositoryHasCommits(projectPath) {
|
||||||
|
try {
|
||||||
|
await spawnAsync('git', ['rev-parse', '--verify', 'HEAD'], { cwd: projectPath });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingHeadRevisionError(error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get git status for a project
|
// Get git status for a project
|
||||||
router.get('/status', async (req, res) => {
|
router.get('/status', async (req, res) => {
|
||||||
const { project } = req.query;
|
const { project } = req.query;
|
||||||
@@ -153,21 +193,8 @@ router.get('/status', async (req, res) => {
|
|||||||
// Validate git repository
|
// Validate git repository
|
||||||
await validateGitRepository(projectPath);
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
// Get current branch - handle case where there are no commits yet
|
const branch = await getCurrentBranchName(projectPath);
|
||||||
let branch = 'main';
|
const hasCommits = await repositoryHasCommits(projectPath);
|
||||||
let hasCommits = true;
|
|
||||||
try {
|
|
||||||
const { stdout: branchOutput } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
|
||||||
branch = branchOutput.trim();
|
|
||||||
} catch (error) {
|
|
||||||
// No HEAD exists - repository has no commits yet
|
|
||||||
if (error.message.includes('unknown revision') || error.message.includes('ambiguous argument')) {
|
|
||||||
hasCommits = false;
|
|
||||||
branch = 'main';
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get git status
|
// Get git status
|
||||||
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain'], { cwd: projectPath });
|
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain'], { cwd: projectPath });
|
||||||
@@ -851,9 +878,30 @@ router.get('/remote-status', async (req, res) => {
|
|||||||
const projectPath = await getActualProjectPath(project);
|
const projectPath = await getActualProjectPath(project);
|
||||||
await validateGitRepository(projectPath);
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
// Get current branch
|
const branch = await getCurrentBranchName(projectPath);
|
||||||
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
const hasCommits = await repositoryHasCommits(projectPath);
|
||||||
const branch = currentBranch.trim();
|
|
||||||
|
const { stdout: remoteOutput } = await spawnAsync('git', ['remote'], { cwd: projectPath });
|
||||||
|
const remotes = remoteOutput.trim().split('\n').filter(r => r.trim());
|
||||||
|
const hasRemote = remotes.length > 0;
|
||||||
|
const fallbackRemoteName = hasRemote
|
||||||
|
? (remotes.includes('origin') ? 'origin' : remotes[0])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Repositories initialized with `git init` can have a branch but no commits.
|
||||||
|
// Return a non-error state so the UI can show the initial-commit workflow.
|
||||||
|
if (!hasCommits) {
|
||||||
|
return res.json({
|
||||||
|
hasRemote,
|
||||||
|
hasUpstream: false,
|
||||||
|
branch,
|
||||||
|
remoteName: fallbackRemoteName,
|
||||||
|
ahead: 0,
|
||||||
|
behind: 0,
|
||||||
|
isUpToDate: false,
|
||||||
|
message: 'Repository has no commits yet'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there's a remote tracking branch (smart detection)
|
// Check if there's a remote tracking branch (smart detection)
|
||||||
let trackingBranch;
|
let trackingBranch;
|
||||||
@@ -863,25 +911,11 @@ router.get('/remote-status', async (req, res) => {
|
|||||||
trackingBranch = stdout.trim();
|
trackingBranch = stdout.trim();
|
||||||
remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
|
remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// No upstream branch configured - but check if we have remotes
|
|
||||||
let hasRemote = false;
|
|
||||||
let remoteName = null;
|
|
||||||
try {
|
|
||||||
const { stdout } = await spawnAsync('git', ['remote'], { cwd: projectPath });
|
|
||||||
const remotes = stdout.trim().split('\n').filter(r => r.trim());
|
|
||||||
if (remotes.length > 0) {
|
|
||||||
hasRemote = true;
|
|
||||||
remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
|
|
||||||
}
|
|
||||||
} catch (remoteError) {
|
|
||||||
// No remotes configured
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
hasRemote,
|
hasRemote,
|
||||||
hasUpstream: false,
|
hasUpstream: false,
|
||||||
branch,
|
branch,
|
||||||
remoteName,
|
remoteName: fallbackRemoteName,
|
||||||
message: 'No remote tracking branch configured'
|
message: 'No remote tracking branch configured'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -923,8 +957,7 @@ router.post('/fetch', async (req, res) => {
|
|||||||
await validateGitRepository(projectPath);
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
// Get current branch and its upstream remote
|
// Get current branch and its upstream remote
|
||||||
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
const branch = await getCurrentBranchName(projectPath);
|
||||||
const branch = currentBranch.trim();
|
|
||||||
|
|
||||||
let remoteName = 'origin'; // fallback
|
let remoteName = 'origin'; // fallback
|
||||||
try {
|
try {
|
||||||
@@ -965,8 +998,7 @@ router.post('/pull', async (req, res) => {
|
|||||||
await validateGitRepository(projectPath);
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
// Get current branch and its upstream remote
|
// Get current branch and its upstream remote
|
||||||
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
const branch = await getCurrentBranchName(projectPath);
|
||||||
const branch = currentBranch.trim();
|
|
||||||
|
|
||||||
let remoteName = 'origin'; // fallback
|
let remoteName = 'origin'; // fallback
|
||||||
let remoteBranch = branch; // fallback
|
let remoteBranch = branch; // fallback
|
||||||
@@ -1034,8 +1066,7 @@ router.post('/push', async (req, res) => {
|
|||||||
await validateGitRepository(projectPath);
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
// Get current branch and its upstream remote
|
// Get current branch and its upstream remote
|
||||||
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
const branch = await getCurrentBranchName(projectPath);
|
||||||
const branch = currentBranch.trim();
|
|
||||||
|
|
||||||
let remoteName = 'origin'; // fallback
|
let remoteName = 'origin'; // fallback
|
||||||
let remoteBranch = branch; // fallback
|
let remoteBranch = branch; // fallback
|
||||||
@@ -1109,8 +1140,7 @@ router.post('/publish', async (req, res) => {
|
|||||||
validateBranchName(branch);
|
validateBranchName(branch);
|
||||||
|
|
||||||
// Get current branch to verify it matches the requested branch
|
// Get current branch to verify it matches the requested branch
|
||||||
const { stdout: currentBranch } = await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: projectPath });
|
const currentBranchName = await getCurrentBranchName(projectPath);
|
||||||
const currentBranchName = currentBranch.trim();
|
|
||||||
|
|
||||||
if (currentBranchName !== branch) {
|
if (currentBranchName !== branch) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
|
|||||||
Reference in New Issue
Block a user