Skip to content

Commit de84394

Browse files
committed
Add support for codiff <branch>.
1 parent 7171668 commit de84394

15 files changed

Lines changed: 569 additions & 38 deletions

File tree

bin/arguments.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
44
import { parseArgs } from 'node:util';
55

66
export const flagDefinitions = [
7+
{ argument: '<ref>', description: 'Open branch history.', name: 'branch', type: 'string' },
78
{ argument: '<ref>', description: 'Review a specific commit.', name: 'commit', type: 'string' },
89
{
910
argument: '<id>',
@@ -35,6 +36,7 @@ export const flagDefinitions = [
3536
export const usageExamples = [
3637
{ command: 'codiff', description: 'Review staged and unstaged changes.' },
3738
{ command: 'codiff /path/to/repo', description: 'Review changes in a specific repository.' },
39+
{ command: 'codiff branch-name', description: 'Review a branch history.' },
3840
{ command: 'codiff a1b2c3d', description: 'Review a specific commit.' },
3941
{ command: "codiff '#75'", description: 'Review pull request #75.' },
4042
{ command: 'codiff pr 75', description: 'Review pull request #75 (alternate syntax).' },
@@ -97,6 +99,36 @@ const isCommitRefArgument = (arg) =>
9799
headCommitRefPattern.test(arg) ||
98100
revisionSyntaxPattern.test(arg));
99101

102+
const isExplicitPathArgument = (arg) =>
103+
arg.startsWith('/') || arg.startsWith('./') || arg.startsWith('../');
104+
105+
const gitSucceeds = (repositoryPath, args) => {
106+
try {
107+
execFileSync('git', ['-C', repositoryPath, ...args], {
108+
stdio: ['ignore', 'ignore', 'ignore'],
109+
});
110+
return true;
111+
} catch {
112+
return false;
113+
}
114+
};
115+
116+
const isBranchRef = (repositoryPath, ref) =>
117+
gitSucceeds(repositoryPath, ['show-ref', '--verify', '--quiet', `refs/heads/${ref}`]) ||
118+
gitSucceeds(repositoryPath, ['show-ref', '--verify', '--quiet', `refs/remotes/${ref}`]);
119+
120+
const isCommitRef = (repositoryPath, ref) =>
121+
gitSucceeds(repositoryPath, ['rev-parse', '--verify', `${ref}^{commit}`]);
122+
123+
const resolveSourceCandidate = (repositoryPath, ref) =>
124+
isCommitRefArgument(ref) && isCommitRef(repositoryPath, ref)
125+
? { commitRef: ref }
126+
: isBranchRef(repositoryPath, ref)
127+
? { branchRef: ref }
128+
: isCommitRef(repositoryPath, ref) || isCommitRefArgument(ref)
129+
? { commitRef: ref }
130+
: null;
131+
100132
const parsePullRequestNumberArgument = (arg) => {
101133
const match = arg.match(pullRequestNumberPattern);
102134
return match ? Number(match[1]) : null;
@@ -211,11 +243,13 @@ export const parseArguments = (args) => {
211243
});
212244

213245
let commitRef = typeof values.commit === 'string' ? values.commit : null;
246+
let branchRef = typeof values.branch === 'string' ? values.branch : null;
214247
const codexSessionId =
215248
typeof values['codex-session'] === 'string' ? values['codex-session'] : null;
216249
let pullRequestNumber = null;
217250
let pullRequestUrl = null;
218251
let requestedPath = null;
252+
let sourceCandidate = null;
219253
const walkthroughContextPath =
220254
typeof values['walkthrough-context'] === 'string' ? values['walkthrough-context'] : null;
221255

@@ -243,15 +277,28 @@ export const parseArguments = (args) => {
243277
}
244278
}
245279

246-
if (!commitRef && isCommitRefArgument(arg)) {
247-
commitRef = arg;
280+
if (!commitRef && !branchRef && !sourceCandidate && !isExplicitPathArgument(arg)) {
281+
sourceCandidate = arg;
248282
} else if (requestedPath == null) {
249283
requestedPath = arg;
250284
}
251285
}
252286

287+
const repositoryPath = resolve(requestedPath ?? process.cwd());
288+
if (!commitRef && !branchRef && sourceCandidate) {
289+
const source = resolveSourceCandidate(repositoryPath, sourceCandidate);
290+
if (source?.branchRef) {
291+
branchRef = source.branchRef;
292+
} else if (source?.commitRef) {
293+
commitRef = source.commitRef;
294+
} else if (requestedPath == null) {
295+
requestedPath = sourceCandidate;
296+
}
297+
}
298+
253299
return {
254300
...(codexSessionId ? { codexSessionId } : {}),
301+
...(branchRef ? { branchRef } : {}),
255302
commitRef,
256303
help: values.help === true,
257304
pullRequestNumber,

bin/codiff-app

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ show_help() {
3131
printf '%s\n' "${blue_bold}Usage:${reset} ${gray}codiff [options] [<ref> | <pr> | <url>] [path]${reset}"
3232
printf '\n'
3333
printf '%s\n' "${blue_bold}Options:${reset}"
34+
printf ' %-22s%s%s%s\n' "--branch <ref>" "$gray" "Open branch history." "$reset"
3435
printf ' %-22s%s%s%s\n' "--commit <ref>" "$gray" "Review a specific commit." "$reset"
3536
printf ' %-22s%s%s%s\n' "--codex-session <id>" "$gray" "Attach Codex session metadata to a walkthrough." "$reset"
3637
printf ' %-22s%s%s%s\n' "--help, -h" "$gray" "Show this help message and exit." "$reset"
@@ -41,6 +42,7 @@ show_help() {
4142
printf '%s\n' "${blue_bold}Examples:${reset}"
4243
printf ' %-22s%s%s%s\n' "codiff" "$gray" "Review staged and unstaged changes." "$reset"
4344
printf ' %-22s%s%s%s\n' "codiff /path/to/repo" "$gray" "Review changes in a specific repository." "$reset"
45+
printf ' %-22s%s%s%s\n' "codiff branch-name" "$gray" "Review a branch history." "$reset"
4446
printf ' %-22s%s%s%s\n' "codiff a1b2c3d" "$gray" "Review a specific commit." "$reset"
4547
printf ' %-22s%s%s%s\n' "codiff '#75'" "$gray" "Review pull request #75." "$reset"
4648
printf ' %-22s%s%s%s\n' "codiff pr 75" "$gray" "Review pull request #75 (alternate syntax)." "$reset"
@@ -50,10 +52,13 @@ show_help() {
5052

5153
repository_path=""
5254
codex_session_id=""
55+
branch_ref=""
5356
commit_ref=""
5457
pull_request_source=""
58+
source_ref=""
5559
walkthrough_context_path=""
5660
expect_codex_session=0
61+
expect_branch_ref=0
5762
expect_commit_ref=0
5863
expect_pull_request_number=0
5964
expect_walkthrough_context=0
@@ -81,13 +86,36 @@ is_commit_ref_arg() {
8186
return 1
8287
}
8388

89+
is_explicit_path_arg() {
90+
case "$1" in
91+
/*|./*|../*) return 0 ;;
92+
esac
93+
94+
return 1
95+
}
96+
97+
is_git_branch_ref() {
98+
git -C "$1" show-ref --verify --quiet "refs/heads/$2" 2>/dev/null ||
99+
git -C "$1" show-ref --verify --quiet "refs/remotes/$2" 2>/dev/null
100+
}
101+
102+
is_git_commit_ref() {
103+
git -C "$1" rev-parse --verify "$2^{commit}" >/dev/null 2>&1
104+
}
105+
84106
for arg in "$@"; do
85107
if [ "$expect_codex_session" -eq 1 ]; then
86108
codex_session_id="$arg"
87109
expect_codex_session=0
88110
continue
89111
fi
90112

113+
if [ "$expect_branch_ref" -eq 1 ]; then
114+
branch_ref="$arg"
115+
expect_branch_ref=0
116+
continue
117+
fi
118+
91119
if [ "$expect_commit_ref" -eq 1 ]; then
92120
commit_ref="$arg"
93121
expect_commit_ref=0
@@ -127,6 +155,12 @@ for arg in "$@"; do
127155
--codex-session=*)
128156
codex_session_id="${arg#--codex-session=}"
129157
;;
158+
--branch)
159+
expect_branch_ref=1
160+
;;
161+
--branch=*)
162+
branch_ref="${arg#--branch=}"
163+
;;
130164
--commit)
131165
expect_commit_ref=1
132166
;;
@@ -148,15 +182,33 @@ for arg in "$@"; do
148182
pull_request_source="$arg"
149183
elif [ -z "$pull_request_source" ] && printf '%s' "$arg" | grep -Eq '^https://github\.com/[^/]+/[^/]+/pull/[0-9]+/?$'; then
150184
pull_request_source="$arg"
151-
elif [ -z "$commit_ref" ] && is_commit_ref_arg "$arg"; then
152-
commit_ref="$arg"
185+
elif [ -z "$commit_ref" ] && [ -z "$branch_ref" ] && [ -z "$source_ref" ] && ! is_explicit_path_arg "$arg"; then
186+
source_ref="$arg"
153187
elif [ -z "$repository_path" ]; then
154188
repository_path="$arg"
155189
fi
156190
;;
157191
esac
158192
done
159193

194+
if [ -n "$source_ref" ] && [ -z "$commit_ref" ] && [ -z "$branch_ref" ]; then
195+
repository_context="${repository_path:-$PWD}"
196+
case "$repository_context" in
197+
/*) ;;
198+
*) repository_context="$PWD/$repository_context" ;;
199+
esac
200+
201+
if is_commit_ref_arg "$source_ref" && is_git_commit_ref "$repository_context" "$source_ref"; then
202+
commit_ref="$source_ref"
203+
elif is_git_branch_ref "$repository_context" "$source_ref"; then
204+
branch_ref="$source_ref"
205+
elif is_git_commit_ref "$repository_context" "$source_ref" || is_commit_ref_arg "$source_ref"; then
206+
commit_ref="$source_ref"
207+
elif [ -z "$repository_path" ]; then
208+
repository_path="$source_ref"
209+
fi
210+
fi
211+
160212
repository_path="${repository_path:-$PWD}"
161213

162214
case "$repository_path" in
@@ -191,6 +243,10 @@ elif [ -n "$commit_ref" ] && [ "$walkthrough" -eq 1 ]; then
191243
open_codiff --walkthrough --commit "$commit_ref" "$repository_path"
192244
elif [ -n "$commit_ref" ]; then
193245
open_codiff --commit "$commit_ref" "$repository_path"
246+
elif [ -n "$branch_ref" ] && [ "$walkthrough" -eq 1 ]; then
247+
open_codiff --walkthrough --branch "$branch_ref" "$repository_path"
248+
elif [ -n "$branch_ref" ]; then
249+
open_codiff --branch "$branch_ref" "$repository_path"
194250
elif [ "$walkthrough" -eq 1 ]; then
195251
open_codiff --walkthrough "$repository_path"
196252
else

bin/codiff.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const run = () => {
2424
}
2525

2626
const {
27+
branchRef,
2728
codexSessionId,
2829
commitRef,
2930
pullRequestNumber,
@@ -51,6 +52,7 @@ const run = () => {
5152
detached: true,
5253
env: {
5354
...process.env,
55+
CODIFF_BRANCH_REF: branchRef ?? '',
5456
CODIFF_COMMIT_REF: commitRef ?? '',
5557
CODIFF_CODEX_SESSION_ID: codexSessionId ?? '',
5658
CODIFF_PULL_REQUEST_URL: pullRequestUrl ?? '',

electron/__tests__/command-line.test.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { execFile } from 'node:child_process';
12
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
23
import { createRequire } from 'node:module';
34
import { tmpdir } from 'node:os';
45
import { join } from 'node:path';
6+
import { promisify } from 'node:util';
57
import { expect, test } from 'vite-plus/test';
68

79
const require = createRequire(import.meta.url);
@@ -12,7 +14,10 @@ const { getInitialRepositoryPath, parseCommandLineArguments, parseGitHubRemoteUr
1214
launchOptions: {
1315
codexSessionId?: string;
1416
repositoryPathProvided: boolean;
15-
source?: { ref: string; type: 'commit' } | { type: 'pull-request'; url: string };
17+
source?:
18+
| { ref: string; type: 'branch' }
19+
| { ref: string; type: 'commit' }
20+
| { type: 'pull-request'; url: string };
1621
walkthrough: boolean;
1722
walkthroughContext?: unknown;
1823
},
@@ -23,7 +28,10 @@ const { getInitialRepositoryPath, parseCommandLineArguments, parseGitHubRemoteUr
2328
launchOptions: {
2429
codexSessionId?: string;
2530
repositoryPathProvided: boolean;
26-
source?: { ref: string; type: 'commit' } | { type: 'pull-request'; url: string };
31+
source?:
32+
| { ref: string; type: 'branch' }
33+
| { ref: string; type: 'commit' }
34+
| { type: 'pull-request'; url: string };
2735
walkthrough: boolean;
2836
walkthroughContext?: unknown;
2937
};
@@ -33,6 +41,12 @@ const { getInitialRepositoryPath, parseCommandLineArguments, parseGitHubRemoteUr
3341
parseGitHubRemoteUrl: (value: string) => { owner: string; repo: string } | null;
3442
};
3543

44+
const execFileAsync = promisify(execFile);
45+
46+
const git = async (repo: string, args: ReadonlyArray<string>) => {
47+
await execFileAsync('git', ['-C', repo, ...args], { encoding: 'utf8' });
48+
};
49+
3650
const defaultLaunchOptions = {
3751
repositoryPathProvided: false,
3852
walkthrough: false,
@@ -83,6 +97,82 @@ test('parses positional HEAD revisions as commit sources', () => {
8397
});
8498
});
8599

100+
test('parses plain refs as branch sources', async () => {
101+
const repositoryPath = await mkdtemp(join(tmpdir(), 'codiff-branch-ref-'));
102+
const previousCwd = process.cwd();
103+
104+
try {
105+
await git(repositoryPath, ['init']);
106+
await git(repositoryPath, ['config', 'user.email', 'codiff@example.com']);
107+
await git(repositoryPath, ['config', 'user.name', 'Codiff Test']);
108+
await git(repositoryPath, ['commit', '--allow-empty', '-m', 'initial commit']);
109+
await git(repositoryPath, ['checkout', '-b', 'feature']);
110+
process.chdir(repositoryPath);
111+
112+
expect(parseCommandLineArguments(['codiff', 'feature'])).toEqual({
113+
launchOptions: {
114+
repositoryPathProvided: false,
115+
source: {
116+
ref: 'feature',
117+
type: 'branch',
118+
},
119+
walkthrough: false,
120+
},
121+
pullRequestNumber: null,
122+
repositoryPath: null,
123+
});
124+
125+
expect(parseCommandLineArguments(['codiff', 'feature', repositoryPath])).toEqual({
126+
launchOptions: {
127+
repositoryPathProvided: true,
128+
source: {
129+
ref: 'feature',
130+
type: 'branch',
131+
},
132+
walkthrough: false,
133+
},
134+
pullRequestNumber: null,
135+
repositoryPath,
136+
});
137+
} finally {
138+
process.chdir(previousCwd);
139+
await rm(repositoryPath, { force: true, recursive: true });
140+
}
141+
});
142+
143+
test('parses hex-like refs as commits before branches', async () => {
144+
const repositoryPath = await mkdtemp(join(tmpdir(), 'codiff-hex-ref-'));
145+
const previousCwd = process.cwd();
146+
147+
try {
148+
await git(repositoryPath, ['init']);
149+
await git(repositoryPath, ['config', 'user.email', 'codiff@example.com']);
150+
await git(repositoryPath, ['config', 'user.name', 'Codiff Test']);
151+
await git(repositoryPath, ['commit', '--allow-empty', '-m', 'initial commit']);
152+
const { stdout } = await execFileAsync('git', ['-C', repositoryPath, 'rev-parse', 'HEAD'], {
153+
encoding: 'utf8',
154+
});
155+
const shortHash = stdout.trim().slice(0, 8);
156+
await git(repositoryPath, ['branch', shortHash]);
157+
process.chdir(repositoryPath);
158+
159+
expect(parseCommandLineArguments(['codiff', shortHash]).launchOptions.source).toEqual({
160+
ref: shortHash,
161+
type: 'commit',
162+
});
163+
164+
expect(
165+
parseCommandLineArguments(['codiff', '--branch', shortHash]).launchOptions.source,
166+
).toEqual({
167+
ref: shortHash,
168+
type: 'branch',
169+
});
170+
} finally {
171+
process.chdir(previousCwd);
172+
await rm(repositoryPath, { force: true, recursive: true });
173+
}
174+
});
175+
86176
test('parses Codex walkthrough seed command-line options', async () => {
87177
const directory = await mkdtemp(join(tmpdir(), 'codiff-context-'));
88178
const contextPath = join(directory, 'seed.json');

0 commit comments

Comments
 (0)