mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -7,8 +7,8 @@ import { secureFs } from '@automaker/platform';
|
||||
import path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { BINARY_EXTENSIONS, type FileStatus } from './types.js';
|
||||
import { isGitRepo, parseGitStatus } from './status.js';
|
||||
import { BINARY_EXTENSIONS, type FileStatus, type MergeStateInfo } from './types.js';
|
||||
import { isGitRepo, parseGitStatus, detectMergeState, detectMergeCommit } from './status.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const logger = createLogger('GitUtils');
|
||||
@@ -243,11 +243,15 @@ export async function generateDiffsForNonGitDirectory(
|
||||
|
||||
/**
|
||||
* Get git repository diffs for a given path
|
||||
* Handles both git repos and non-git directories
|
||||
* Handles both git repos and non-git directories.
|
||||
* Also detects merge state and annotates files accordingly.
|
||||
*/
|
||||
export async function getGitRepositoryDiffs(
|
||||
repoPath: string
|
||||
): Promise<{ diff: string; files: FileStatus[]; hasChanges: boolean }> {
|
||||
export async function getGitRepositoryDiffs(repoPath: string): Promise<{
|
||||
diff: string;
|
||||
files: FileStatus[];
|
||||
hasChanges: boolean;
|
||||
mergeState?: MergeStateInfo;
|
||||
}> {
|
||||
// Check if it's a git repository
|
||||
const isRepo = await isGitRepo(repoPath);
|
||||
|
||||
@@ -273,11 +277,133 @@ export async function getGitRepositoryDiffs(
|
||||
const files = parseGitStatus(status);
|
||||
|
||||
// Generate synthetic diffs for untracked (new) files
|
||||
const combinedDiff = await appendUntrackedFileDiffs(repoPath, diff, files);
|
||||
let combinedDiff = await appendUntrackedFileDiffs(repoPath, diff, files);
|
||||
|
||||
// Detect merge state (in-progress merge/rebase/cherry-pick)
|
||||
const mergeState = await detectMergeState(repoPath);
|
||||
|
||||
// If no in-progress merge, check if HEAD is a completed merge commit
|
||||
// and include merge commit changes in the diff and file list
|
||||
if (!mergeState.isMerging) {
|
||||
const mergeCommitInfo = await detectMergeCommit(repoPath);
|
||||
|
||||
if (mergeCommitInfo.isMergeCommit && mergeCommitInfo.mergeAffectedFiles.length > 0) {
|
||||
// Get the diff of the merge commit relative to first parent
|
||||
try {
|
||||
const { stdout: mergeDiff } = await execAsync('git diff HEAD~1 HEAD', {
|
||||
cwd: repoPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
|
||||
// Add merge-affected files to the file list (avoid duplicates with working tree changes)
|
||||
const fileByPath = new Map(files.map((f) => [f.path, f]));
|
||||
const existingPaths = new Set(fileByPath.keys());
|
||||
for (const filePath of mergeCommitInfo.mergeAffectedFiles) {
|
||||
if (!existingPaths.has(filePath)) {
|
||||
const newFile = {
|
||||
status: 'M',
|
||||
path: filePath,
|
||||
statusText: 'Merged',
|
||||
indexStatus: ' ',
|
||||
workTreeStatus: ' ',
|
||||
isMergeAffected: true,
|
||||
mergeType: 'merged',
|
||||
};
|
||||
files.push(newFile);
|
||||
fileByPath.set(filePath, newFile);
|
||||
existingPaths.add(filePath);
|
||||
} else {
|
||||
// Mark existing file as also merge-affected
|
||||
const existing = fileByPath.get(filePath);
|
||||
if (existing) {
|
||||
existing.isMergeAffected = true;
|
||||
existing.mergeType = 'merged';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend merge diff to the combined diff so merge changes appear
|
||||
// For files that only exist in the merge (not in working tree), we need their diffs
|
||||
if (mergeDiff.trim()) {
|
||||
// Parse the existing working tree diff to find which files it covers
|
||||
const workingTreeDiffPaths = new Set<string>();
|
||||
const diffLines = combinedDiff.split('\n');
|
||||
for (const line of diffLines) {
|
||||
if (line.startsWith('diff --git')) {
|
||||
const match = line.match(/diff --git a\/(.*?) b\/(.*)/);
|
||||
if (match) {
|
||||
workingTreeDiffPaths.add(match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only include merge diff entries for files NOT already in working tree diff
|
||||
const mergeDiffFiles = mergeDiff.split(/(?=diff --git)/);
|
||||
const newMergeDiffs: string[] = [];
|
||||
for (const fileDiff of mergeDiffFiles) {
|
||||
if (!fileDiff.trim()) continue;
|
||||
const match = fileDiff.match(/diff --git a\/(.*?) b\/(.*)/);
|
||||
if (match && !workingTreeDiffPaths.has(match[2])) {
|
||||
newMergeDiffs.push(fileDiff);
|
||||
}
|
||||
}
|
||||
|
||||
if (newMergeDiffs.length > 0) {
|
||||
combinedDiff = newMergeDiffs.join('') + combinedDiff;
|
||||
}
|
||||
}
|
||||
} catch (mergeError) {
|
||||
// Best-effort: log and continue without merge diff
|
||||
logger.error('Failed to get merge commit diff:', mergeError);
|
||||
|
||||
// Ensure files[] is consistent with mergeState.mergeAffectedFiles even when the
|
||||
// diff command failed. Without this, mergeAffectedFiles would list paths that have
|
||||
// no corresponding entry in the files array.
|
||||
const existingPathsAfterError = new Set(files.map((f) => f.path));
|
||||
for (const filePath of mergeCommitInfo.mergeAffectedFiles) {
|
||||
if (!existingPathsAfterError.has(filePath)) {
|
||||
files.push({
|
||||
status: 'M',
|
||||
path: filePath,
|
||||
statusText: 'Merged',
|
||||
indexStatus: ' ',
|
||||
workTreeStatus: ' ',
|
||||
isMergeAffected: true,
|
||||
mergeType: 'merged',
|
||||
});
|
||||
existingPathsAfterError.add(filePath);
|
||||
} else {
|
||||
// Mark existing file as also merge-affected
|
||||
const existing = files.find((f) => f.path === filePath);
|
||||
if (existing) {
|
||||
existing.isMergeAffected = true;
|
||||
existing.mergeType = 'merged';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return with merge commit info in the mergeState
|
||||
return {
|
||||
diff: combinedDiff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
mergeState: {
|
||||
isMerging: false,
|
||||
mergeOperationType: 'merge',
|
||||
isCleanMerge: true,
|
||||
mergeAffectedFiles: mergeCommitInfo.mergeAffectedFiles,
|
||||
conflictFiles: [],
|
||||
isMergeCommit: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
diff: combinedDiff,
|
||||
files,
|
||||
hasChanges: files.length > 0,
|
||||
...(mergeState.isMerging ? { mergeState } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,10 +7,15 @@
|
||||
export { execGitCommand } from './exec.js';
|
||||
|
||||
// Export types and constants
|
||||
export { BINARY_EXTENSIONS, GIT_STATUS_MAP, type FileStatus } from './types.js';
|
||||
export {
|
||||
BINARY_EXTENSIONS,
|
||||
GIT_STATUS_MAP,
|
||||
type FileStatus,
|
||||
type MergeStateInfo,
|
||||
} from './types.js';
|
||||
|
||||
// Export status utilities
|
||||
export { isGitRepo, parseGitStatus } from './status.js';
|
||||
export { isGitRepo, parseGitStatus, detectMergeState, detectMergeCommit } from './status.js';
|
||||
|
||||
// Export diff utilities
|
||||
export {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { GIT_STATUS_MAP, type FileStatus } from './types.js';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { GIT_STATUS_MAP, type FileStatus, type MergeStateInfo } from './types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -95,12 +97,161 @@ export function parseGitStatus(statusOutput: string): FileStatus[] {
|
||||
primaryStatus = workTreeStatus; // Working tree change
|
||||
}
|
||||
|
||||
// Detect merge-affected files: when both X and Y are 'U', or U appears in either position
|
||||
// In merge state, git uses 'U' (unmerged) to indicate merge-affected entries
|
||||
const isMergeAffected =
|
||||
indexStatus === 'U' ||
|
||||
workTreeStatus === 'U' ||
|
||||
(indexStatus === 'A' && workTreeStatus === 'A') || // both-added
|
||||
(indexStatus === 'D' && workTreeStatus === 'D'); // both-deleted (during merge)
|
||||
|
||||
let mergeType: string | undefined;
|
||||
if (isMergeAffected) {
|
||||
if (indexStatus === 'U' && workTreeStatus === 'U') mergeType = 'both-modified';
|
||||
else if (indexStatus === 'A' && workTreeStatus === 'U') mergeType = 'added-by-us';
|
||||
else if (indexStatus === 'U' && workTreeStatus === 'A') mergeType = 'added-by-them';
|
||||
else if (indexStatus === 'D' && workTreeStatus === 'U') mergeType = 'deleted-by-us';
|
||||
else if (indexStatus === 'U' && workTreeStatus === 'D') mergeType = 'deleted-by-them';
|
||||
else if (indexStatus === 'A' && workTreeStatus === 'A') mergeType = 'both-added';
|
||||
else if (indexStatus === 'D' && workTreeStatus === 'D') mergeType = 'both-deleted';
|
||||
else mergeType = 'unmerged';
|
||||
}
|
||||
|
||||
return {
|
||||
status: primaryStatus,
|
||||
path: filePath,
|
||||
statusText: getStatusText(indexStatus, workTreeStatus),
|
||||
indexStatus,
|
||||
workTreeStatus,
|
||||
...(isMergeAffected && { isMergeAffected: true }),
|
||||
...(mergeType && { mergeType }),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current HEAD commit is a merge commit (has more than one parent).
|
||||
* This is used to detect completed merge commits so we can show what the merge changed.
|
||||
*
|
||||
* @param repoPath - Path to the git repository or worktree
|
||||
* @returns Object with isMergeCommit flag and the list of files affected by the merge
|
||||
*/
|
||||
export async function detectMergeCommit(
|
||||
repoPath: string
|
||||
): Promise<{ isMergeCommit: boolean; mergeAffectedFiles: string[] }> {
|
||||
try {
|
||||
// Check how many parents HEAD has using rev-parse
|
||||
// For a merge commit, HEAD^2 exists (second parent); for non-merge commits it doesn't
|
||||
try {
|
||||
await execAsync('git rev-parse --verify "HEAD^2"', { cwd: repoPath });
|
||||
} catch {
|
||||
// HEAD^2 doesn't exist — not a merge commit
|
||||
return { isMergeCommit: false, mergeAffectedFiles: [] };
|
||||
}
|
||||
|
||||
// HEAD is a merge commit - get the files it changed relative to first parent
|
||||
let mergeAffectedFiles: string[] = [];
|
||||
try {
|
||||
const { stdout: diffOutput } = await execAsync('git diff --name-only "HEAD~1" "HEAD"', {
|
||||
cwd: repoPath,
|
||||
});
|
||||
mergeAffectedFiles = diffOutput
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((f) => f.trim().length > 0);
|
||||
} catch {
|
||||
// Ignore errors getting affected files
|
||||
}
|
||||
|
||||
return { isMergeCommit: true, mergeAffectedFiles };
|
||||
} catch {
|
||||
return { isMergeCommit: false, mergeAffectedFiles: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the current merge state of a git repository.
|
||||
* Checks for .git/MERGE_HEAD, .git/REBASE_HEAD, .git/CHERRY_PICK_HEAD
|
||||
* to determine if a merge/rebase/cherry-pick is in progress.
|
||||
*
|
||||
* @param repoPath - Path to the git repository or worktree
|
||||
* @returns MergeStateInfo describing the current merge state
|
||||
*/
|
||||
export async function detectMergeState(repoPath: string): Promise<MergeStateInfo> {
|
||||
const defaultState: MergeStateInfo = {
|
||||
isMerging: false,
|
||||
mergeOperationType: null,
|
||||
isCleanMerge: false,
|
||||
mergeAffectedFiles: [],
|
||||
conflictFiles: [],
|
||||
};
|
||||
|
||||
try {
|
||||
// Find the actual .git directory (handles worktrees with .git file pointing to main repo)
|
||||
const { stdout: gitDirRaw } = await execAsync('git rev-parse --git-dir', { cwd: repoPath });
|
||||
const gitDir = path.resolve(repoPath, gitDirRaw.trim());
|
||||
|
||||
// Check for merge/rebase/cherry-pick indicators
|
||||
let mergeOperationType: 'merge' | 'rebase' | 'cherry-pick' | null = null;
|
||||
|
||||
const checks = [
|
||||
{ file: 'MERGE_HEAD', type: 'merge' as const },
|
||||
{ file: 'REBASE_HEAD', type: 'rebase' as const },
|
||||
{ file: 'rebase-merge', type: 'rebase' as const },
|
||||
{ file: 'rebase-apply', type: 'rebase' as const },
|
||||
{ file: 'CHERRY_PICK_HEAD', type: 'cherry-pick' as const },
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
try {
|
||||
await fs.access(path.join(gitDir, check.file));
|
||||
mergeOperationType = check.type;
|
||||
break;
|
||||
} catch {
|
||||
// File doesn't exist, continue checking
|
||||
}
|
||||
}
|
||||
|
||||
if (!mergeOperationType) {
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
// Get unmerged files (files with conflicts)
|
||||
let conflictFiles: string[] = [];
|
||||
try {
|
||||
const { stdout: diffOutput } = await execAsync('git diff --name-only --diff-filter=U', {
|
||||
cwd: repoPath,
|
||||
});
|
||||
conflictFiles = diffOutput
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((f) => f.trim().length > 0);
|
||||
} catch {
|
||||
// Ignore errors getting conflict files
|
||||
}
|
||||
|
||||
// Get all files affected by the merge (staged files that came from the merge)
|
||||
let mergeAffectedFiles: string[] = [];
|
||||
try {
|
||||
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
|
||||
cwd: repoPath,
|
||||
});
|
||||
const files = parseGitStatus(statusOutput);
|
||||
mergeAffectedFiles = files
|
||||
.filter((f) => f.isMergeAffected || (f.indexStatus !== ' ' && f.indexStatus !== '?'))
|
||||
.map((f) => f.path);
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return {
|
||||
isMerging: true,
|
||||
mergeOperationType,
|
||||
isCleanMerge: conflictFiles.length === 0,
|
||||
mergeAffectedFiles,
|
||||
conflictFiles,
|
||||
};
|
||||
} catch {
|
||||
return defaultState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* Git utilities types and constants
|
||||
*/
|
||||
|
||||
// Re-export MergeStateInfo from the centralized @automaker/types package
|
||||
export type { MergeStateInfo } from '@automaker/types';
|
||||
|
||||
// Binary file extensions to skip
|
||||
export const BINARY_EXTENSIONS = new Set([
|
||||
'.png',
|
||||
@@ -74,4 +77,8 @@ export interface FileStatus {
|
||||
indexStatus?: string;
|
||||
/** Raw working tree status character from git porcelain format */
|
||||
workTreeStatus?: string;
|
||||
/** Whether this file is involved in a merge operation (both-modified, added-by-us, etc.) */
|
||||
isMergeAffected?: boolean;
|
||||
/** Type of merge involvement: 'both-modified' | 'added-by-us' | 'added-by-them' | 'deleted-by-us' | 'deleted-by-them' | 'both-added' | 'both-deleted' */
|
||||
mergeType?: string;
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ export {
|
||||
getThinkingTokenBudget,
|
||||
isAdaptiveThinkingModel,
|
||||
getThinkingLevelsForModel,
|
||||
getDefaultThinkingLevel,
|
||||
// Event hook constants
|
||||
EVENT_HOOK_TRIGGER_LABELS,
|
||||
// Claude-compatible provider templates (new)
|
||||
@@ -359,6 +360,7 @@ export type {
|
||||
AddRemoteResult,
|
||||
AddRemoteResponse,
|
||||
AddRemoteErrorResponse,
|
||||
MergeStateInfo,
|
||||
} from './worktree.js';
|
||||
export { PR_STATES, validatePRState } from './worktree.js';
|
||||
|
||||
|
||||
@@ -268,6 +268,16 @@ export function getThinkingLevelsForModel(model: string): ThinkingLevel[] {
|
||||
return ['none', 'low', 'medium', 'high', 'ultrathink'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default thinking level for a given model.
|
||||
* Used when selecting a model via the primary button in the two-stage selector.
|
||||
* Always returns 'none' — users can configure their preferred default
|
||||
* via the defaultThinkingLevel setting in the model defaults page.
|
||||
*/
|
||||
export function getDefaultThinkingLevel(_model: string): ThinkingLevel {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/** ModelProvider - AI model provider for credentials and API key management */
|
||||
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode' | 'gemini' | 'copilot';
|
||||
|
||||
@@ -1051,6 +1061,8 @@ export interface GlobalSettings {
|
||||
enableDependencyBlocking: boolean;
|
||||
/** Skip verification requirement in auto-mode (treat 'completed' same as 'verified') */
|
||||
skipVerificationInAutoMode: boolean;
|
||||
/** User's preferred action after a clean merge (null = ask every time) */
|
||||
mergePostAction: 'commit' | 'manual' | null;
|
||||
/** Default: use git worktrees for feature branches */
|
||||
useWorktrees: boolean;
|
||||
/** Default: planning approach (skip/lite/spec/full) */
|
||||
@@ -1086,6 +1098,15 @@ export interface GlobalSettings {
|
||||
/** Phase-specific AI model configuration */
|
||||
phaseModels: PhaseModelConfig;
|
||||
|
||||
/** Default thinking level applied when selecting a model via the primary button
|
||||
* in the two-stage model selector. Users can still adjust per-model via the expand arrow.
|
||||
* Defaults to 'none' (no extended thinking). */
|
||||
defaultThinkingLevel?: ThinkingLevel;
|
||||
|
||||
/** Default reasoning effort applied when selecting a Codex model via the primary button
|
||||
* in the two-stage model selector. Defaults to 'none'. */
|
||||
defaultReasoningEffort?: ReasoningEffort;
|
||||
|
||||
// Legacy AI Model Selection (deprecated - use phaseModels instead)
|
||||
/** @deprecated Use phaseModels.enhancementModel instead */
|
||||
enhancementModel: ModelAlias;
|
||||
@@ -1150,6 +1171,10 @@ export interface GlobalSettings {
|
||||
/** Maps project path -> last selected session ID in that project */
|
||||
lastSelectedSessionByProject: Record<string, string>;
|
||||
|
||||
// Worktree Selection Tracking
|
||||
/** Maps project path -> last selected worktree (path + branch) for restoring on PWA reload */
|
||||
currentWorktreeByProject?: Record<string, { path: string | null; branch: string }>;
|
||||
|
||||
// Window State (Electron only)
|
||||
/** Persisted window bounds for restoring position/size across sessions */
|
||||
windowBounds?: WindowBounds;
|
||||
@@ -1574,6 +1599,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
||||
defaultSkipTests: true,
|
||||
enableDependencyBlocking: true,
|
||||
skipVerificationInAutoMode: false,
|
||||
mergePostAction: null,
|
||||
useWorktrees: true,
|
||||
defaultPlanningMode: 'skip',
|
||||
defaultRequirePlanApproval: false,
|
||||
@@ -1585,6 +1611,8 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
||||
showQueryDevtools: true,
|
||||
enableAiCommitMessages: true,
|
||||
phaseModels: DEFAULT_PHASE_MODELS,
|
||||
defaultThinkingLevel: 'none',
|
||||
defaultReasoningEffort: 'none',
|
||||
enhancementModel: 'sonnet', // Legacy alias still supported
|
||||
validationModel: 'opus', // Legacy alias still supported
|
||||
enabledCursorModels: getAllCursorModelIds(), // Returns prefixed IDs
|
||||
@@ -1607,6 +1635,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
||||
recentFolders: [],
|
||||
worktreePanelCollapsed: false,
|
||||
lastSelectedSessionByProject: {},
|
||||
currentWorktreeByProject: {},
|
||||
autoLoadClaudeMd: true,
|
||||
skipSandboxWarning: false,
|
||||
codexAutoLoadAgents: DEFAULT_CODEX_AUTO_LOAD_AGENTS,
|
||||
|
||||
@@ -74,3 +74,21 @@ export interface AddRemoteErrorResponse {
|
||||
/** Optional error code for specific error types (e.g., 'REMOTE_EXISTS') */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge state information for a git repository
|
||||
*/
|
||||
export interface MergeStateInfo {
|
||||
/** Whether a merge is currently in progress */
|
||||
isMerging: boolean;
|
||||
/** Type of merge operation: 'merge' | 'rebase' | 'cherry-pick' | null */
|
||||
mergeOperationType: 'merge' | 'rebase' | 'cherry-pick' | null;
|
||||
/** Whether the merge completed cleanly (no conflicts) */
|
||||
isCleanMerge: boolean;
|
||||
/** Files affected by the merge */
|
||||
mergeAffectedFiles: string[];
|
||||
/** Files with unresolved conflicts */
|
||||
conflictFiles: string[];
|
||||
/** Whether the current HEAD is a completed merge commit (has multiple parents) */
|
||||
isMergeCommit?: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user