Fix git operations from repository subdirectories (#1344)

* Fix git operations from repository subdirectories

* fix bot comments

---------

Co-authored-by: Konstantin Zolin <zolin_ka@vk.com>
This commit is contained in:
kostazol
2026-05-23 21:27:43 +03:00
committed by GitHub
co-authored by Konstantin Zolin
parent 47f72c2e09
commit 3dfefb8ffc
+205 -95
View File
@@ -307,6 +307,63 @@ const normalizeDirectoryPath = (value) => {
return trimmed;
};
const toGitPath = (value) => value.replace(/\\/g, '/');
const isInsideOrSameDirectory = (root, target) => {
const relative = path.relative(root, target);
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
};
const resolveGitRepositoryRoot = async (directoryPath, git) => {
const topLevel = await git.raw(['rev-parse', '--show-toplevel']);
const normalizedTopLevel = topLevel.trim();
return path.isAbsolute(normalizedTopLevel)
? path.resolve(normalizedTopLevel)
: path.resolve(directoryPath, normalizedTopLevel);
};
const createRepositoryGitContext = async (directory) => {
const directoryPath = normalizeDirectoryPath(directory);
const directoryGit = await createGit(directoryPath);
const repoRoot = await resolveGitRepositoryRoot(directoryPath, directoryGit);
const git = path.resolve(directoryPath) === repoRoot ? directoryGit : await createGit(repoRoot);
return { directoryPath, directoryGit, repoRoot, git };
};
const resolveGitInternalPath = async (repoRoot, git, gitPath) => {
const resolved = await git.raw(['rev-parse', '--git-path', gitPath]);
return path.resolve(repoRoot, resolved.trim());
};
const resolveGitFileContext = async (directoryPath, git, filePath, repoRootOverride = null) => {
const repoRoot = repoRootOverride || await resolveGitRepositoryRoot(directoryPath, git);
const candidates = Array.from(new Set([
path.resolve(repoRoot, filePath),
path.resolve(directoryPath, filePath),
]));
for (const absolutePath of candidates) {
if (!isInsideOrSameDirectory(repoRoot, absolutePath)) {
continue;
}
const repoPath = toGitPath(path.relative(repoRoot, absolutePath));
const existsInWorktree = await fsp.stat(absolutePath).then((stat) => stat.isFile()).catch(() => false);
const existsInIndex = await git.raw(['cat-file', '-e', `:${repoPath}`]).then(() => true).catch(() => false);
const existsInHead = await git.raw(['cat-file', '-e', `HEAD:${repoPath}`]).then(() => true).catch(() => false);
if (existsInWorktree || existsInIndex || existsInHead) {
return {
absolutePath,
repoPath,
repoRoot,
};
}
}
throw new Error('Invalid file path');
};
const cleanBranchName = (branch) => {
if (!branch) {
return branch;
@@ -595,6 +652,21 @@ const runGitCommand = async (cwd, args) => {
}
};
const resolveGitCommitFilePath = async (repoRoot, hash, candidates) => {
for (const candidate of candidates) {
const [originalTreeResult, modifiedTreeResult] = await Promise.all([
runGitCommand(repoRoot, ['ls-tree', '--name-only', `${hash}^`, '--', candidate]),
runGitCommand(repoRoot, ['ls-tree', '--name-only', hash, '--', candidate]),
]);
if ((originalTreeResult.success && originalTreeResult.stdout.trim()) || (modifiedTreeResult.success && modifiedTreeResult.stdout.trim())) {
return candidate;
}
}
throw new Error('Invalid file path');
};
const runGitCommandOrThrow = async (cwd, args, fallbackMessage) => {
const result = await runGitCommand(cwd, args);
if (!result.success) {
@@ -1212,11 +1284,11 @@ export async function setLocalIdentity(directory, profile) {
}
export async function getStatus(directory, options = {}) {
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const lightMode = options.mode === 'light';
try {
const { repoRoot, git } = await createRepositoryGitContext(directory);
// Use -uall to show all untracked files individually, not just directories
const status = await git.status(['-uall']);
@@ -1285,7 +1357,7 @@ export async function getStatus(directory, options = {}) {
continue;
}
const absolutePath = path.join(directoryPath, file.path);
const absolutePath = path.join(repoRoot, file.path);
try {
const stat = await fsp.stat(absolutePath);
@@ -1403,7 +1475,8 @@ export async function getStatus(directory, options = {}) {
const headSha = mergeHead.trim().slice(0, 7);
// Only set mergeInProgress if we actually have a valid head SHA
if (headSha) {
const mergeMsg = await fsp.readFile(path.join(directoryPath, '.git', 'MERGE_MSG'), 'utf8').catch(() => '');
const mergeMsgPath = await resolveGitInternalPath(repoRoot, git, 'MERGE_MSG').catch(() => '');
const mergeMsg = mergeMsgPath ? await fsp.readFile(mergeMsgPath, 'utf8').catch(() => '') : '';
mergeInProgress = {
head: headSha,
message: mergeMsg.split('\n')[0] || '',
@@ -1416,13 +1489,15 @@ export async function getStatus(directory, options = {}) {
try {
// Check for rebase in progress (.git/rebase-merge or .git/rebase-apply)
const rebaseMergeExists = await fsp.stat(path.join(directoryPath, '.git', 'rebase-merge')).then(() => true).catch(() => false);
const rebaseApplyExists = await fsp.stat(path.join(directoryPath, '.git', 'rebase-apply')).then(() => true).catch(() => false);
const rebaseMergePath = await resolveGitInternalPath(repoRoot, git, 'rebase-merge').catch(() => '');
const rebaseApplyPath = await resolveGitInternalPath(repoRoot, git, 'rebase-apply').catch(() => '');
const rebaseMergeExists = rebaseMergePath ? await fsp.stat(rebaseMergePath).then(() => true).catch(() => false) : false;
const rebaseApplyExists = rebaseApplyPath ? await fsp.stat(rebaseApplyPath).then(() => true).catch(() => false) : false;
if (rebaseMergeExists || rebaseApplyExists) {
const rebaseDir = rebaseMergeExists ? 'rebase-merge' : 'rebase-apply';
const headName = await fsp.readFile(path.join(directoryPath, '.git', rebaseDir, 'head-name'), 'utf8').catch(() => '');
const onto = await fsp.readFile(path.join(directoryPath, '.git', rebaseDir, 'onto'), 'utf8').catch(() => '');
const rebasePath = rebaseMergeExists ? rebaseMergePath : rebaseApplyPath;
const headName = await fsp.readFile(path.join(rebasePath, 'head-name'), 'utf8').catch(() => '');
const onto = await fsp.readFile(path.join(rebasePath, 'onto'), 'utf8').catch(() => '');
const headNameTrimmed = headName.trim().replace('refs/heads/', '');
const ontoTrimmed = onto.trim().slice(0, 7);
@@ -1462,11 +1537,12 @@ export async function getStatus(directory, options = {}) {
}
}
export async function getDiff(directory, { path, staged = false, contextLines = 3 } = {}) {
const git = await createGit(directory);
export async function getDiff(directory, { path: filePath, staged = false, contextLines = 3 } = {}) {
const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
try {
const args = ['diff', '--no-color'];
const fileContext = filePath ? await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot) : null;
if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
args.push(`-U${Math.max(0, contextLines)}`);
@@ -1476,8 +1552,8 @@ export async function getDiff(directory, { path, staged = false, contextLines =
args.push('--cached');
}
if (path) {
args.push('--', path);
if (fileContext) {
args.push('--', fileContext.repoPath);
}
const diff = await git.raw(args);
@@ -1489,15 +1565,19 @@ export async function getDiff(directory, { path, staged = false, contextLines =
return diff;
}
if (!fileContext) {
return diff;
}
try {
await git.raw(['ls-files', '--error-unmatch', path]);
await git.raw(['ls-files', '--error-unmatch', '--', fileContext.repoPath]);
return diff;
} catch {
const noIndexArgs = ['diff', '--no-color'];
if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
noIndexArgs.push(`-U${Math.max(0, contextLines)}`);
}
noIndexArgs.push('--no-index', '--', '/dev/null', path);
noIndexArgs.push('--no-index', '--', '/dev/null', fileContext.repoPath);
try {
const noIndexDiff = await git.raw(noIndexArgs);
return noIndexDiff;
@@ -1515,8 +1595,8 @@ export async function getDiff(directory, { path, staged = false, contextLines =
}
}
export async function getRangeDiff(directory, { base, head, path, contextLines = 3 } = {}) {
const git = await createGit(directory);
export async function getRangeDiff(directory, { base, head, path: filePath, contextLines = 3 } = {}) {
const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
const baseRef = typeof base === 'string' ? base.trim() : '';
const headRef = typeof head === 'string' ? head.trim() : '';
if (!baseRef || !headRef) {
@@ -1541,15 +1621,16 @@ export async function getRangeDiff(directory, { base, head, path, contextLines =
args.push(`-U${Math.max(0, contextLines)}`);
}
args.push(`${resolvedBase}...${headRef}`);
if (path) {
args.push('--', path);
if (filePath) {
const fileContext = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
args.push('--', fileContext.repoPath);
}
const diff = await git.raw(args);
return diff;
}
export async function getRangeFiles(directory, { base, head } = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const baseRef = typeof base === 'string' ? base.trim() : '';
const headRef = typeof head === 'string' ? head.trim() : '';
if (!baseRef || !headRef) {
@@ -1686,15 +1767,14 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
throw new Error('directory and path are required for getFileDiff');
}
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
const isImage = isImageFile(filePath);
const mimeType = isImage ? getImageMimeType(filePath) : null;
const { absolutePath, repoPath } = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
if (!isImage) {
const absolutePath = path.join(directoryPath, filePath);
const isBinaryBySniff = await looksBinaryBySniff(absolutePath);
const isBinary = isBinaryBySniff || (await isBinaryDiff(directoryPath, filePath, staged));
const isBinary = isBinaryBySniff || (await isBinaryDiff(repoRoot, repoPath, staged));
if (isBinary) {
return {
original: '',
@@ -1710,8 +1790,8 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
if (isImage) {
// For images, use git show with raw output and convert to base64
try {
const { stdout } = await execFileAsync(getGitBinary(), ['show', `HEAD:${filePath}`], {
cwd: directoryPath,
const { stdout } = await execFileAsync(getGitBinary(), ['show', `HEAD:${repoPath}`], {
cwd: repoRoot,
encoding: 'buffer',
windowsHide: true,
maxBuffer: 50 * 1024 * 1024, // 50MB max
@@ -1723,23 +1803,22 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
original = '';
}
} else {
original = await git.show([`HEAD:${filePath}`]);
original = await git.show([`HEAD:${repoPath}`]);
}
} catch {
original = '';
}
const fullPath = path.join(directoryPath, filePath);
let modified = '';
try {
const stat = await fsp.stat(fullPath);
const stat = await fsp.stat(absolutePath);
if (stat.isFile()) {
if (isImage) {
// For images, read as binary and convert to data URL
const buffer = await fsp.readFile(fullPath);
const buffer = await fsp.readFile(absolutePath);
modified = `data:${mimeType};base64,${buffer.toString('base64')}`;
} else {
modified = await fsp.readFile(fullPath, 'utf8');
modified = await fsp.readFile(absolutePath, 'utf8');
}
}
} catch (error) {
@@ -1761,26 +1840,23 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
export async function revertFile(directory, filePath) {
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const repoRoot = path.resolve(directoryPath);
const absoluteTarget = path.resolve(repoRoot, filePath);
if (!absoluteTarget.startsWith(repoRoot + path.sep) && absoluteTarget !== repoRoot) {
throw new Error('Invalid file path');
}
const directoryGit = await createGit(directoryPath);
const repoRoot = await resolveGitRepositoryRoot(directoryPath, directoryGit);
const { absolutePath, repoPath } = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
const git = await createGit(repoRoot);
const isTracked = await git
.raw(['ls-files', '--error-unmatch', filePath])
.raw(['ls-files', '--error-unmatch', '--', repoPath])
.then(() => true)
.catch(() => false);
if (!isTracked) {
try {
await git.raw(['clean', '-f', '-d', '--', filePath]);
await git.raw(['clean', '-f', '-d', '--', repoPath]);
return;
} catch (cleanError) {
try {
await fsp.rm(absoluteTarget, { recursive: true, force: true });
await fsp.rm(absolutePath, { recursive: true, force: true });
return;
} catch (fsError) {
if (fsError && typeof fsError === 'object' && fsError.code === 'ENOENT') {
@@ -1793,16 +1869,16 @@ export async function revertFile(directory, filePath) {
}
try {
await git.raw(['restore', '--staged', filePath]);
await git.raw(['restore', '--staged', '--', repoPath]);
} catch (error) {
await git.raw(['reset', 'HEAD', '--', filePath]).catch(() => {});
await git.raw(['reset', 'HEAD', '--', repoPath]).catch(() => {});
}
try {
await git.raw(['restore', filePath]);
await git.raw(['restore', '--', repoPath]);
} catch (error) {
try {
await git.raw(['checkout', '--', filePath]);
await git.raw(['checkout', '--', repoPath]);
} catch (fallbackError) {
console.error('Failed to revert git file:', fallbackError);
throw fallbackError;
@@ -1826,7 +1902,7 @@ export async function collectDiffs(directory, files = []) {
}
export async function pull(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const pullOptions = options.rebase === true
? { ...(options.options && typeof options.options === 'object' && !Array.isArray(options.options) ? options.options : {}), '--rebase': null }
: options.options || {};
@@ -1852,7 +1928,7 @@ export async function pull(directory, options = {}) {
}
export async function listStashes(directory) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const output = await git.raw(['stash', 'list', '--format=%gd%x1f%gs%x1f%cr%x1f%H']);
return String(output || '')
.split('\n')
@@ -1866,7 +1942,7 @@ export async function listStashes(directory) {
}
export async function countStashFiles(directory, refs = []) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const uniqueRefs = Array.from(new Set((Array.isArray(refs) ? refs : []).map((ref) => String(ref || '').trim()).filter(Boolean)));
const counts = {};
const concurrency = 4;
@@ -1889,7 +1965,7 @@ export async function countStashFiles(directory, refs = []) {
return counts;
}
export async function stashPush(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const message = typeof options.message === 'string' && options.message.trim()
? options.message.trim()
: `OpenChamber stash ${new Date().toISOString()}`;
@@ -1903,14 +1979,14 @@ export async function stashPush(directory, options = {}) {
}
export async function stashApply(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const ref = typeof options.ref === 'string' && options.ref.trim() ? options.ref.trim() : 'stash@{0}';
await git.raw(['stash', 'apply', ref]);
return { success: true, ref };
}
export async function stashDrop(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const ref = typeof options.ref === 'string' && options.ref.trim() ? options.ref.trim() : 'stash@{0}';
await git.raw(['stash', 'drop', ref]);
return { success: true, ref };
@@ -1924,7 +2000,7 @@ export async function stashPop(directory, options = {}) {
}
export async function push(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const describePushError = (error) => {
const fromNestedGit = error?.git && typeof error.git === 'object'
@@ -2064,7 +2140,7 @@ export async function deleteRemoteBranch(directory, options = {}) {
throw new Error('branch is required to delete remote branch');
}
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
const targetBranch = branch.startsWith('refs/heads/')
? branch.substring('refs/heads/'.length)
: branch;
@@ -2080,7 +2156,7 @@ export async function deleteRemoteBranch(directory, options = {}) {
}
export async function fetch(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.fetch(
@@ -2097,7 +2173,7 @@ export async function fetch(directory, options = {}) {
}
export async function commit(directory, message, options = {}) {
const git = await createGit(directory);
const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
try {
const requestedFiles = Array.isArray(options.files)
@@ -2105,14 +2181,19 @@ export async function commit(directory, message, options = {}) {
.map((value) => String(value || '').trim())
.filter(Boolean)
: [];
let filesToCommit = requestedFiles;
let filesToCommit = [];
if (options.addAll) {
await git.add('.');
} else if (requestedFiles.length > 0) {
filesToCommit = Array.from(new Set(await Promise.all(requestedFiles.map(async (filePath) => {
const fileContext = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
return fileContext.repoPath;
}))));
const status = await git.status();
const fileStatusByPath = new Map(status.files.map((file) => [file.path, file]));
filesToCommit = requestedFiles.filter((filePath) => fileStatusByPath.has(filePath));
filesToCommit = filesToCommit.filter((filePath) => fileStatusByPath.has(filePath));
if (filesToCommit.length === 0) {
throw new Error('No selected files are available to commit. Refresh git status and try again.');
@@ -2165,7 +2246,7 @@ export async function commit(directory, message, options = {}) {
}
export async function getBranches(directory) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
const result = await git.branch();
@@ -2226,7 +2307,7 @@ async function filterActiveRemoteBranches(git, remoteBranches) {
}
export async function createBranch(directory, branchName, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.checkoutBranch(branchName, options.startPoint || 'HEAD');
@@ -2238,7 +2319,7 @@ export async function createBranch(directory, branchName, options = {}) {
}
export async function checkoutBranch(directory, branchName) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.checkout(branchName);
@@ -2251,12 +2332,14 @@ export async function checkoutBranch(directory, branchName) {
export async function getWorktrees(directory) {
const directoryPath = normalizeDirectoryPath(directory);
if (!directoryPath || !fs.existsSync(directoryPath) || !fs.existsSync(path.join(directoryPath, '.git'))) {
if (!directoryPath || !fs.existsSync(directoryPath)) {
return [];
}
try {
const directoryGit = await createGit(directoryPath);
const repoRoot = await resolveGitRepositoryRoot(directoryPath, directoryGit);
const result = await runGitCommandOrThrow(
directoryPath,
repoRoot,
['worktree', 'list', '--porcelain'],
'Failed to list git worktrees'
);
@@ -2689,7 +2772,7 @@ export async function removeWorktree(directory, input = {}) {
}
export async function deleteBranch(directory, branch, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
const branchName = branch.startsWith('refs/heads/')
@@ -2730,10 +2813,13 @@ export async function resolveBaseRefForLog(from, checkRef) {
}
export async function getLog(directory, options = {}) {
const git = await createGit(directory);
const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
try {
const maxCount = options.maxCount || 50;
const filePath = options.file
? (await resolveGitFileContext(directoryPath, directoryGit, options.file, repoRoot)).repoPath
: undefined;
// Prefer the local ref; fall back to origin/<from> only when the local ref
// cannot be resolved (e.g. user has never checked out the base branch).
@@ -2751,7 +2837,7 @@ export async function getLog(directory, options = {}) {
maxCount,
from: resolvedFrom,
to: options.to,
file: options.file
file: filePath
});
const logArgs = [
@@ -2770,8 +2856,8 @@ export async function getLog(directory, options = {}) {
logArgs.push(options.to);
}
if (options.file) {
logArgs.push('--', options.file);
if (filePath) {
logArgs.push('--', filePath);
}
const rawLog = await git.raw(logArgs);
@@ -2922,6 +3008,7 @@ export async function canonicalizeWorktreeState(directory) {
const cwd = await canonicalPath(directoryPath);
const git = await createGit(directoryPath);
const repoRoot = await resolveGitRepositoryRoot(directoryPath, git).catch(() => directoryPath);
let worktreeRoot = null;
let worktreeStatus = 'ready';
@@ -2962,13 +3049,17 @@ export async function canonicalizeWorktreeState(directory) {
if (status.current && (await git.raw(['rev-parse', '--verify', 'MERGE_HEAD']).then(() => true).catch(() => false))) {
attentionReason = 'merge';
} else {
const rebaseMerge = await fsp.stat(path.join(directoryPath, '.git', 'rebase-merge')).then(() => true).catch(() => false);
const rebaseApply = await fsp.stat(path.join(directoryPath, '.git', 'rebase-apply')).then(() => true).catch(() => false);
const rebaseMergePath = await resolveGitInternalPath(repoRoot, git, 'rebase-merge').catch(() => '');
const rebaseApplyPath = await resolveGitInternalPath(repoRoot, git, 'rebase-apply').catch(() => '');
const rebaseMerge = rebaseMergePath ? await fsp.stat(rebaseMergePath).then(() => true).catch(() => false) : false;
const rebaseApply = rebaseApplyPath ? await fsp.stat(rebaseApplyPath).then(() => true).catch(() => false) : false;
if (rebaseMerge || rebaseApply) {
attentionReason = 'rebase';
} else if (status.conflicted && status.conflicted.length > 0) {
const cherryPickHead = await fsp.stat(path.join(directoryPath, '.git', 'CHERRY_PICK_HEAD')).then(() => true).catch(() => false);
const revertHead = await fsp.stat(path.join(directoryPath, '.git', 'REVERT_HEAD')).then(() => true).catch(() => false);
const cherryPickHeadPath = await resolveGitInternalPath(repoRoot, git, 'CHERRY_PICK_HEAD').catch(() => '');
const revertHeadPath = await resolveGitInternalPath(repoRoot, git, 'REVERT_HEAD').catch(() => '');
const cherryPickHead = cherryPickHeadPath ? await fsp.stat(cherryPickHeadPath).then(() => true).catch(() => false) : false;
const revertHead = revertHeadPath ? await fsp.stat(revertHeadPath).then(() => true).catch(() => false) : false;
if (cherryPickHead) attentionReason = 'cherry-pick';
else if (revertHead) attentionReason = 'revert';
}
@@ -2990,7 +3081,7 @@ export async function canonicalizeWorktreeState(directory) {
}
export async function getCommitFiles(directory, commitHash) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
@@ -3071,7 +3162,7 @@ export async function getCommitFiles(directory, commitHash) {
}
export async function renameBranch(directory, oldName, newName) {
const git = await createGit(directory);
const { git, repoRoot } = await createRepositoryGitContext(directory);
try {
const normalizedOldName = cleanBranchName(String(oldName || '').trim());
@@ -3100,12 +3191,12 @@ export async function renameBranch(directory, oldName, newName) {
if (upstream) {
try {
await runGitCommandOrThrow(
directory,
repoRoot,
['branch', `--set-upstream-to=${upstream.full}`, normalizedNewName],
`Failed to set upstream to ${upstream.full}`
);
} catch {
await setBranchTrackingFallback(directory, normalizedNewName, upstream);
await setBranchTrackingFallback(repoRoot, normalizedNewName, upstream);
}
}
}
@@ -3118,7 +3209,7 @@ export async function renameBranch(directory, oldName, newName) {
}
export async function getRemotes(directory) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
const remotes = await git.getRemotes(true);
@@ -3146,7 +3237,7 @@ export async function removeRemote(directory, options = {}) {
throw new Error('Cannot remove origin remote');
}
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.removeRemote(remoteName);
@@ -3158,7 +3249,7 @@ export async function removeRemote(directory, options = {}) {
}
export async function rebase(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
const { onto } = options;
@@ -3194,7 +3285,7 @@ export async function rebase(directory, options = {}) {
}
export async function abortRebase(directory) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.rebase(['--abort']);
@@ -3206,7 +3297,7 @@ export async function abortRebase(directory) {
}
export async function merge(directory, options = {}) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
const { branch } = options;
@@ -3242,7 +3333,7 @@ export async function merge(directory, options = {}) {
}
export async function abortMerge(directory) {
const git = await createGit(directory);
const { git } = await createRepositoryGitContext(directory);
try {
await git.merge(['--abort']);
@@ -3254,8 +3345,7 @@ export async function abortMerge(directory) {
}
export async function continueRebase(directory) {
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const { git } = await createRepositoryGitContext(directory);
try {
// Set GIT_EDITOR to prevent editor prompts
@@ -3295,8 +3385,7 @@ export async function continueRebase(directory) {
}
export async function continueMerge(directory) {
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const { git } = await createRepositoryGitContext(directory);
try {
// Check if there are still unmerged files
@@ -3341,8 +3430,7 @@ export async function continueMerge(directory) {
}
export async function getConflictDetails(directory) {
const directoryPath = normalizeDirectoryPath(directory);
const git = await createGit(directoryPath);
const { repoRoot, git } = await createRepositoryGitContext(directory);
try {
// Get git status --porcelain
@@ -3371,9 +3459,8 @@ export async function getConflictDetails(directory) {
if (mergeHeadExists) {
operation = 'merge';
const mergeHead = await git.raw(['rev-parse', 'MERGE_HEAD']).catch(() => '');
const mergeMsg = await fsp
.readFile(path.join(directoryPath, '.git', 'MERGE_MSG'), 'utf8')
.catch(() => '');
const mergeMsgPath = await resolveGitInternalPath(repoRoot, git, 'MERGE_MSG').catch(() => '');
const mergeMsg = mergeMsgPath ? await fsp.readFile(mergeMsgPath, 'utf8').catch(() => '') : '';
headInfo = `MERGE_HEAD: ${mergeHead.trim()}\n${mergeMsg}`;
} else {
// Check for REBASE_HEAD (rebase in progress)
@@ -3411,12 +3498,35 @@ export async function getCommitFileDiff(directory, hash, filePath, isBinary) {
return { original: '', modified: '', isBinary: true };
}
const directoryPath = normalizeDirectoryPath(directory);
const { directoryPath, repoRoot } = await createRepositoryGitContext(directory);
const candidates = Array.from(new Set([
toGitPath(path.relative(repoRoot, path.resolve(repoRoot, filePath))),
toGitPath(path.relative(repoRoot, path.resolve(directoryPath, filePath))),
])).filter((candidate) => candidate && !candidate.startsWith('..') && !path.isAbsolute(candidate));
const [originalResult, modifiedResult] = await Promise.all([
runGitCommand(directoryPath, ['show', `${hash}^:${filePath}`]),
runGitCommand(directoryPath, ['show', `${hash}:${filePath}`]),
]);
let originalResult = null;
let modifiedResult = null;
for (const candidate of candidates) {
const [candidateOriginalResult, candidateModifiedResult] = await Promise.all([
runGitCommand(repoRoot, ['show', `${hash}^:${candidate}`]),
runGitCommand(repoRoot, ['show', `${hash}:${candidate}`]),
]);
if (candidateOriginalResult.success || candidateModifiedResult.success) {
originalResult = candidateOriginalResult;
modifiedResult = candidateModifiedResult;
break;
}
}
if (!originalResult || !modifiedResult) {
const resolvedPath = await resolveGitCommitFilePath(repoRoot, hash, candidates);
[originalResult, modifiedResult] = await Promise.all([
runGitCommand(repoRoot, ['show', `${hash}^:${resolvedPath}`]),
runGitCommand(repoRoot, ['show', `${hash}:${resolvedPath}`]),
]);
}
const original = originalResult.success ? originalResult.stdout : '';
const modified = modifiedResult.success ? modifiedResult.stdout : '';