fix: Show completed phase diffs in review tab
Completed phases showed "No phases pending review" because: 1. Frontend filtered only pending_review phases 2. Server rejected non-pending_review phases 3. After merge, three-dot diff returned empty (merge base moved) Fix: store pre-merge merge base hash on phase, use it to reconstruct diffs for completed phases. Frontend now shows both pending_review and completed phases with read-only mode (Merged badge) for completed ones.
This commit is contained in:
@@ -68,6 +68,7 @@ describe('writeInputFiles', () => {
|
|||||||
name: 'Phase One',
|
name: 'Phase One',
|
||||||
content: 'First phase',
|
content: 'First phase',
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
|
mergeBase: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as Phase;
|
} as Phase;
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const phases = sqliteTable('phases', {
|
|||||||
status: text('status', { enum: ['pending', 'approved', 'in_progress', 'completed', 'blocked', 'pending_review'] })
|
status: text('status', { enum: ['pending', 'approved', 'in_progress', 'completed', 'blocked', 'pending_review'] })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default('pending'),
|
.default('pending'),
|
||||||
|
mergeBase: text('merge_base'),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
});
|
});
|
||||||
|
|||||||
1
apps/server/drizzle/0031_add_phase_merge_base.sql
Normal file
1
apps/server/drizzle/0031_add_phase_merge_base.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE phases ADD COLUMN merge_base TEXT;
|
||||||
@@ -218,6 +218,13 @@
|
|||||||
"when": 1772150400000,
|
"when": 1772150400000,
|
||||||
"tag": "0030_remove_task_approval",
|
"tag": "0030_remove_task_approval",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1772236800000,
|
||||||
|
"tag": "0031_add_phase_merge_base",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -278,6 +278,18 @@ export class ExecutionOrchestrator {
|
|||||||
|
|
||||||
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
const projects = await this.projectRepository.findProjectsByInitiativeId(phase.initiativeId);
|
||||||
|
|
||||||
|
// Store merge base before merging so we can reconstruct diffs for completed phases
|
||||||
|
for (const project of projects) {
|
||||||
|
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
||||||
|
try {
|
||||||
|
const mergeBase = await this.branchManager.getMergeBase(clonePath, initBranch, phBranch);
|
||||||
|
await this.phaseRepository.update(phaseId, { mergeBase });
|
||||||
|
break; // Only need one merge base (first project)
|
||||||
|
} catch {
|
||||||
|
// Phase branch may not exist in this project clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
const clonePath = await ensureProjectClone(project, this.workspaceRoot);
|
||||||
const result = await this.branchManager.mergeBranch(clonePath, phBranch, initBranch);
|
const result = await this.branchManager.mergeBranch(clonePath, phBranch, initBranch);
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ export interface BranchManager {
|
|||||||
*/
|
*/
|
||||||
diffCommit(repoPath: string, commitHash: string): Promise<string>;
|
diffCommit(repoPath: string, commitHash: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the merge base (common ancestor) of two branches.
|
||||||
|
* Returns the commit hash of the merge base.
|
||||||
|
*/
|
||||||
|
getMergeBase(repoPath: string, branch1: string, branch2: string): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push a branch to a remote.
|
* Push a branch to a remote.
|
||||||
* Defaults to 'origin' if no remote specified.
|
* Defaults to 'origin' if no remote specified.
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ export class SimpleGitBranchManager implements BranchManager {
|
|||||||
return git.diff([`${commitHash}~1`, commitHash]);
|
return git.diff([`${commitHash}~1`, commitHash]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMergeBase(repoPath: string, branch1: string, branch2: string): Promise<string> {
|
||||||
|
const git = simpleGit(repoPath);
|
||||||
|
const result = await git.raw(['merge-base', branch1, branch2]);
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async pushBranch(repoPath: string, branch: string, remote = 'origin'): Promise<void> {
|
async pushBranch(repoPath: string, branch: string, remote = 'origin'): Promise<void> {
|
||||||
const git = simpleGit(repoPath);
|
const git = simpleGit(repoPath);
|
||||||
await git.push(remote, branch);
|
await git.push(remote, branch);
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
if (!phase) {
|
if (!phase) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
||||||
}
|
}
|
||||||
if (phase.status !== 'pending_review') {
|
if (phase.status !== 'pending_review' && phase.status !== 'completed') {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not pending review (status: ${phase.status})` });
|
throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not reviewable (status: ${phase.status})` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiative = await initiativeRepo.findById(phase.initiativeId);
|
const initiative = await initiativeRepo.findById(phase.initiativeId);
|
||||||
@@ -230,13 +230,15 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
|
|
||||||
const initBranch = initiative.branch;
|
const initBranch = initiative.branch;
|
||||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||||
|
// For completed phases, use stored merge base; for pending_review, use initiative branch
|
||||||
|
const diffBase = (phase.status === 'completed' && phase.mergeBase) ? phase.mergeBase : initBranch;
|
||||||
|
|
||||||
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
||||||
let rawDiff = '';
|
let rawDiff = '';
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
||||||
const diff = await branchManager.diffBranches(clonePath, initBranch, phBranch);
|
const diff = await branchManager.diffBranches(clonePath, diffBase, phBranch);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
rawDiff += diff + '\n';
|
rawDiff += diff + '\n';
|
||||||
}
|
}
|
||||||
@@ -270,8 +272,8 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
if (!phase) {
|
if (!phase) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
throw new TRPCError({ code: 'NOT_FOUND', message: `Phase '${input.phaseId}' not found` });
|
||||||
}
|
}
|
||||||
if (phase.status !== 'pending_review') {
|
if (phase.status !== 'pending_review' && phase.status !== 'completed') {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not pending review (status: ${phase.status})` });
|
throw new TRPCError({ code: 'BAD_REQUEST', message: `Phase is not reviewable (status: ${phase.status})` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const initiative = await initiativeRepo.findById(phase.initiativeId);
|
const initiative = await initiativeRepo.findById(phase.initiativeId);
|
||||||
@@ -281,13 +283,14 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
|||||||
|
|
||||||
const initBranch = initiative.branch;
|
const initBranch = initiative.branch;
|
||||||
const phBranch = phaseBranchName(initBranch, phase.name);
|
const phBranch = phaseBranchName(initBranch, phase.name);
|
||||||
|
const diffBase = (phase.status === 'completed' && phase.mergeBase) ? phase.mergeBase : initBranch;
|
||||||
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
const projects = await projectRepo.findProjectsByInitiativeId(phase.initiativeId);
|
||||||
|
|
||||||
const allCommits: Array<{ hash: string; shortHash: string; message: string; author: string; date: string; filesChanged: number; insertions: number; deletions: number }> = [];
|
const allCommits: Array<{ hash: string; shortHash: string; message: string; author: string; date: string; filesChanged: number; insertions: number; deletions: number }> = [];
|
||||||
|
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
const clonePath = await ensureProjectClone(project, ctx.workspaceRoot!);
|
||||||
const commits = await branchManager.listCommits(clonePath, initBranch, phBranch);
|
const commits = await branchManager.listCommits(clonePath, diffBase, phBranch);
|
||||||
allCommits.push(...commits);
|
allCommits.push(...commits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ import type { FileDiff, ReviewStatus } from "./types";
|
|||||||
interface PhaseOption {
|
interface PhaseOption {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReviewHeaderProps {
|
interface ReviewHeaderProps {
|
||||||
phases: PhaseOption[];
|
phases: PhaseOption[];
|
||||||
activePhaseId: string | null;
|
activePhaseId: string | null;
|
||||||
|
isReadOnly?: boolean;
|
||||||
onPhaseSelect: (id: string) => void;
|
onPhaseSelect: (id: string) => void;
|
||||||
phaseName: string;
|
phaseName: string;
|
||||||
sourceBranch: string;
|
sourceBranch: string;
|
||||||
@@ -44,6 +46,7 @@ interface ReviewHeaderProps {
|
|||||||
export function ReviewHeader({
|
export function ReviewHeader({
|
||||||
phases,
|
phases,
|
||||||
activePhaseId,
|
activePhaseId,
|
||||||
|
isReadOnly,
|
||||||
onPhaseSelect,
|
onPhaseSelect,
|
||||||
phaseName,
|
phaseName,
|
||||||
sourceBranch,
|
sourceBranch,
|
||||||
@@ -92,6 +95,12 @@ export function ReviewHeader({
|
|||||||
<div className="flex gap-1 overflow-x-auto">
|
<div className="flex gap-1 overflow-x-auto">
|
||||||
{phases.map((phase) => {
|
{phases.map((phase) => {
|
||||||
const isActive = phase.id === activePhaseId;
|
const isActive = phase.id === activePhaseId;
|
||||||
|
const isCompleted = phase.status === "completed";
|
||||||
|
const dotColor = isActive
|
||||||
|
? "bg-primary"
|
||||||
|
: isCompleted
|
||||||
|
? "bg-status-success-dot"
|
||||||
|
: "bg-status-warning-dot";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={phase.id}
|
key={phase.id}
|
||||||
@@ -106,9 +115,7 @@ export function ReviewHeader({
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`h-1.5 w-1.5 rounded-full shrink-0 ${
|
className={`h-1.5 w-1.5 rounded-full shrink-0 ${dotColor}`}
|
||||||
isActive ? "bg-primary" : "bg-status-warning-dot"
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
{phase.name}
|
{phase.name}
|
||||||
</button>
|
</button>
|
||||||
@@ -171,102 +178,111 @@ export function ReviewHeader({
|
|||||||
{preview && <PreviewControls preview={preview} />}
|
{preview && <PreviewControls preview={preview} />}
|
||||||
|
|
||||||
{/* Review status / actions */}
|
{/* Review status / actions */}
|
||||||
{status === "pending" && (
|
{isReadOnly ? (
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onRequestChanges}
|
|
||||||
disabled={isRequestingChanges}
|
|
||||||
className="h-8 text-xs px-3 border-status-error-border/50 text-status-error-fg hover:bg-status-error-bg/50 hover:border-status-error-border"
|
|
||||||
>
|
|
||||||
{isRequestingChanges ? (
|
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
)}
|
|
||||||
Request Changes
|
|
||||||
</Button>
|
|
||||||
<div className="relative" ref={confirmRef}>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (unresolvedCount > 0) return;
|
|
||||||
setShowConfirmation(true);
|
|
||||||
}}
|
|
||||||
disabled={unresolvedCount > 0}
|
|
||||||
className="h-9 px-5 text-sm font-semibold shadow-sm"
|
|
||||||
>
|
|
||||||
{unresolvedCount > 0 ? (
|
|
||||||
<>
|
|
||||||
<AlertCircle className="h-3.5 w-3.5" />
|
|
||||||
{unresolvedCount} unresolved
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<GitMerge className="h-3.5 w-3.5" />
|
|
||||||
Approve & Merge
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Merge confirmation dropdown */}
|
|
||||||
{showConfirmation && (
|
|
||||||
<div className="absolute right-0 top-full mt-1 z-30 w-64 rounded-lg border border-border bg-card shadow-lg p-4">
|
|
||||||
<p className="text-sm font-semibold mb-3">
|
|
||||||
Ready to merge?
|
|
||||||
</p>
|
|
||||||
<div className="space-y-1.5 mb-4">
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<Check className="h-3.5 w-3.5 text-status-success-fg" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
0 unresolved comments
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
|
||||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{viewed}/{total} files viewed
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowConfirmation(false)}
|
|
||||||
className="h-8 text-xs"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setShowConfirmation(false);
|
|
||||||
onApprove();
|
|
||||||
}}
|
|
||||||
className="h-8 px-4 text-xs font-semibold shadow-sm"
|
|
||||||
>
|
|
||||||
<GitMerge className="h-3.5 w-3.5" />
|
|
||||||
Merge Now
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{status === "approved" && (
|
|
||||||
<Badge variant="success" size="xs">
|
<Badge variant="success" size="xs">
|
||||||
<Check className="h-3 w-3" />
|
<Check className="h-3 w-3" />
|
||||||
Approved
|
Merged
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{status === "changes_requested" && (
|
|
||||||
<Badge variant="warning" size="xs">
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
Changes Requested
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{status === "pending" && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onRequestChanges}
|
||||||
|
disabled={isRequestingChanges}
|
||||||
|
className="h-8 text-xs px-3 border-status-error-border/50 text-status-error-fg hover:bg-status-error-bg/50 hover:border-status-error-border"
|
||||||
|
>
|
||||||
|
{isRequestingChanges ? (
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
Request Changes
|
||||||
|
</Button>
|
||||||
|
<div className="relative" ref={confirmRef}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (unresolvedCount > 0) return;
|
||||||
|
setShowConfirmation(true);
|
||||||
|
}}
|
||||||
|
disabled={unresolvedCount > 0}
|
||||||
|
className="h-9 px-5 text-sm font-semibold shadow-sm"
|
||||||
|
>
|
||||||
|
{unresolvedCount > 0 ? (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="h-3.5 w-3.5" />
|
||||||
|
{unresolvedCount} unresolved
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<GitMerge className="h-3.5 w-3.5" />
|
||||||
|
Approve & Merge
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Merge confirmation dropdown */}
|
||||||
|
{showConfirmation && (
|
||||||
|
<div className="absolute right-0 top-full mt-1 z-30 w-64 rounded-lg border border-border bg-card shadow-lg p-4">
|
||||||
|
<p className="text-sm font-semibold mb-3">
|
||||||
|
Ready to merge?
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1.5 mb-4">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<Check className="h-3.5 w-3.5 text-status-success-fg" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
0 unresolved comments
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{viewed}/{total} files viewed
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowConfirmation(false)}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setShowConfirmation(false);
|
||||||
|
onApprove();
|
||||||
|
}}
|
||||||
|
className="h-8 px-4 text-xs font-semibold shadow-sm"
|
||||||
|
>
|
||||||
|
<GitMerge className="h-3.5 w-3.5" />
|
||||||
|
Merge Now
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === "approved" && (
|
||||||
|
<Badge variant="success" size="xs">
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
Approved
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{status === "changes_requested" && (
|
||||||
|
<Badge variant="warning" size="xs">
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
Changes Requested
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.r
|
|||||||
| initiativeId | text FK → initiatives (cascade) | |
|
| initiativeId | text FK → initiatives (cascade) | |
|
||||||
| name | text NOT NULL | |
|
| name | text NOT NULL | |
|
||||||
| content | text nullable | Tiptap JSON |
|
| content | text nullable | Tiptap JSON |
|
||||||
| status | text enum | 'pending' \| 'approved' \| 'in_progress' \| 'completed' \| 'blocked' |
|
| status | text enum | 'pending' \| 'approved' \| 'in_progress' \| 'completed' \| 'blocked' \| 'pending_review' |
|
||||||
|
| mergeBase | text nullable | git merge-base hash stored before phase merge, enables diff reconstruction for completed phases |
|
||||||
| createdAt, updatedAt | integer/timestamp | |
|
| createdAt, updatedAt | integer/timestamp | |
|
||||||
|
|
||||||
### phase_dependencies
|
### phase_dependencies
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Worktrees stored in `.cw-worktrees/` subdirectory of the repo. Each agent gets a
|
|||||||
| `remoteBranchExists(repoPath, branch)` | Check remote tracking branches (`origin/<branch>`) |
|
| `remoteBranchExists(repoPath, branch)` | Check remote tracking branches (`origin/<branch>`) |
|
||||||
| `listCommits(repoPath, base, head)` | List commits head has that base doesn't (with stats) |
|
| `listCommits(repoPath, base, head)` | List commits head has that base doesn't (with stats) |
|
||||||
| `diffCommit(repoPath, commitHash)` | Get unified diff for a single commit |
|
| `diffCommit(repoPath, commitHash)` | Get unified diff for a single commit |
|
||||||
|
| `getMergeBase(repoPath, branch1, branch2)` | Get common ancestor commit hash |
|
||||||
|
|
||||||
`remoteBranchExists` is used by `registerProject` and `updateProject` to validate that a project's default branch actually exists in the cloned repository before saving.
|
`remoteBranchExists` is used by `registerProject` and `updateProject` to validate that a project's default branch actually exists in the cloned repository before saving.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user