Repository navigation
Expand file tree
/
Copy pathdiff.ts
More file actions
executable file
·117 lines (102 loc) · 4.65 KB
/
Copy pathdiff.ts
File metadata and controls
executable file
·117 lines (102 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env bun
import { parseArgs } from "util";
import { resolveComparison } from "./diff/comparison.ts";
import { runDiffTui } from "./diff/tui.ts";
const { values, positionals } = parseArgs({
args: Bun.argv.slice(2),
options: {
base: { type: "string" },
direct: { type: "boolean", default: false },
help: { type: "boolean", short: "h", default: false },
},
strict: true,
allowPositionals: true,
});
if (values.help) {
console.log(`Usage: td diff [--inline] [branch]
Arguments:
branch Base branch to compare against (defaults to the remote default branch)
Options:
--inline Use the current terminal instead of an attached window
--base <branch> Alias for the branch argument
--direct Compare directly against the base branch tip instead of the merge base
The worktree comparison includes staged, unstaged, and untracked changes.
During a merge, the merge base includes the pending merge parents.
Normal mode:
c / f / d Toggle commits, files, or diff pane
h / l Focus the pane to the left or right
j / k Navigate the focused pane
mouse wheel Move the pointer in the commit or file pane
left click Focus the commit or file pane and select a row
/ Start an fzf query for the focused list
alt-a/m/d/r/c/t/u Filter files by change status and edit the query
alt-x Clear the file query
tab / shift-tab Toggle selection and move
enter Collapse or expand a folder
ctrl-c Copy selected diff text, otherwise clear selection
ctrl-y Copy selected file paths
q Exit
Query mode:
^A pattern Filter by status and path pattern (^D, ^M, etc.)
alt-a/m/d/r/c/t/u Replace the query with a change-status selector
alt-x Clear the file query
enter Apply query
escape Cancel query and restore the previous query
tab / shift-tab Toggle selection and move
ctrl-c Copy selected diff text, otherwise clear selection
`);
process.exit(0);
}
function runGit(args: string[], quiet = false) {
const proc = Bun.spawnSync(["git", ...args], { stdout: "pipe", stderr: "pipe" });
const stdout = new TextDecoder().decode(proc.stdout);
const stderr = new TextDecoder().decode(proc.stderr).trimEnd();
if (proc.exitCode !== 0 && !quiet) throw new Error(stderr || `git ${args[0]} failed`);
return { code: proc.exitCode, stdout, stderr };
}
function remoteNames() {
return runGit(["remote"], true).stdout.split("\n").filter(Boolean);
}
function remoteDefaultBranch(remote: string) {
const ref = runGit(["symbolic-ref", "--quiet", "--short", `refs/remotes/${remote}/HEAD`], true).stdout.trim();
const prefix = `${remote}/`;
return ref.startsWith(prefix) ? ref.slice(prefix.length) : undefined;
}
function remoteBranchCandidates(requested?: string) {
const remotes = remoteNames();
if (remotes.length === 0) throw new Error("No git remotes are configured.");
const parts = requested?.split("/") ?? [];
const explicitRemote = Boolean(requested) && remotes.includes(parts[0] ?? "") && parts.length > 1;
const fallbackRemote = remotes[0];
if (!fallbackRemote) throw new Error("No git remotes are configured.");
const remote = explicitRemote ? parts[0]! : remotes.includes("origin") ? "origin" : fallbackRemote;
const branch = requested
? explicitRemote
? parts.slice(1).join("/")
: requested
: remoteDefaultBranch(remote);
const branches = branch ? (branch === "main" ? ["main", "master"] : [branch]) : ["main", "master"];
return branches.map((candidate) => ({ remote, branch: candidate }));
}
function resolveBaseRef(requested?: string) {
for (const candidate of remoteBranchCandidates(requested)) {
const ref = `${candidate.remote}/${candidate.branch}`;
const fetched = runGit(
["fetch", "--quiet", candidate.remote, `+refs/heads/${candidate.branch}:refs/remotes/${ref}`],
true,
);
if (fetched.code === 0 && runGit(["rev-parse", "--verify", `${ref}^{commit}`], true).code === 0) return ref;
}
throw new Error(`Remote branch${requested ? ` '${requested}'` : ""} was not found or could not be fetched.`);
}
async function main() {
if (runGit(["rev-parse", "--is-inside-work-tree"], true).code !== 0) {
throw new Error("td diff: not inside a git repository.");
}
const baseRef = resolveBaseRef(values.base ?? positionals[0]);
await runDiffTui(resolveComparison(baseRef, values.direct));
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});