@procedures/git 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +77 -12
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/module.d.ts.map +1 -1
  5. package/dist/module.js +312 -2
  6. package/dist/module.js.map +1 -1
  7. package/dist/procedures/git/blame.d.ts +17 -0
  8. package/dist/procedures/git/blame.d.ts.map +1 -0
  9. package/dist/procedures/git/blame.js +69 -0
  10. package/dist/procedures/git/blame.js.map +1 -0
  11. package/dist/procedures/git/branch/add.d.ts +12 -0
  12. package/dist/procedures/git/branch/add.d.ts.map +1 -0
  13. package/dist/procedures/git/branch/add.js +21 -0
  14. package/dist/procedures/git/branch/add.js.map +1 -0
  15. package/dist/procedures/git/branch/delete.d.ts +13 -0
  16. package/dist/procedures/git/branch/delete.d.ts.map +1 -0
  17. package/dist/procedures/git/branch/delete.js +24 -0
  18. package/dist/procedures/git/branch/delete.js.map +1 -0
  19. package/dist/procedures/git/branch/list.d.ts +13 -0
  20. package/dist/procedures/git/branch/list.d.ts.map +1 -0
  21. package/dist/procedures/git/branch/list.js +37 -0
  22. package/dist/procedures/git/branch/list.js.map +1 -0
  23. package/dist/procedures/git/branch.d.ts +8 -2
  24. package/dist/procedures/git/branch.d.ts.map +1 -1
  25. package/dist/procedures/git/branch.js +18 -37
  26. package/dist/procedures/git/branch.js.map +1 -1
  27. package/dist/procedures/git/commit.d.ts.map +1 -1
  28. package/dist/procedures/git/commit.js +9 -11
  29. package/dist/procedures/git/commit.js.map +1 -1
  30. package/dist/procedures/git/index.d.ts +19 -0
  31. package/dist/procedures/git/index.d.ts.map +1 -1
  32. package/dist/procedures/git/index.js +19 -0
  33. package/dist/procedures/git/index.js.map +1 -1
  34. package/dist/procedures/git/init.d.ts +6 -2
  35. package/dist/procedures/git/init.d.ts.map +1 -1
  36. package/dist/procedures/git/init.js +8 -4
  37. package/dist/procedures/git/init.js.map +1 -1
  38. package/dist/procedures/git/merge/abort.d.ts +14 -0
  39. package/dist/procedures/git/merge/abort.d.ts.map +1 -0
  40. package/dist/procedures/git/merge/abort.js +18 -0
  41. package/dist/procedures/git/merge/abort.js.map +1 -0
  42. package/dist/procedures/git/merge.d.ts +16 -0
  43. package/dist/procedures/git/merge.d.ts.map +1 -0
  44. package/dist/procedures/git/merge.js +41 -0
  45. package/dist/procedures/git/merge.js.map +1 -0
  46. package/dist/procedures/git/push.d.ts.map +1 -1
  47. package/dist/procedures/git/push.js +4 -2
  48. package/dist/procedures/git/push.js.map +1 -1
  49. package/dist/procedures/git/rebase/abort.d.ts +13 -0
  50. package/dist/procedures/git/rebase/abort.d.ts.map +1 -0
  51. package/dist/procedures/git/rebase/abort.js +17 -0
  52. package/dist/procedures/git/rebase/abort.js.map +1 -0
  53. package/dist/procedures/git/rebase/continue.d.ts +13 -0
  54. package/dist/procedures/git/rebase/continue.d.ts.map +1 -0
  55. package/dist/procedures/git/rebase/continue.js +23 -0
  56. package/dist/procedures/git/rebase/continue.js.map +1 -0
  57. package/dist/procedures/git/rebase.d.ts +22 -0
  58. package/dist/procedures/git/rebase.d.ts.map +1 -0
  59. package/dist/procedures/git/rebase.js +46 -0
  60. package/dist/procedures/git/rebase.js.map +1 -0
  61. package/dist/procedures/git/refs.d.ts +31 -0
  62. package/dist/procedures/git/refs.d.ts.map +1 -0
  63. package/dist/procedures/git/refs.js +52 -0
  64. package/dist/procedures/git/refs.js.map +1 -0
  65. package/dist/procedures/git/reset.d.ts +13 -0
  66. package/dist/procedures/git/reset.d.ts.map +1 -0
  67. package/dist/procedures/git/reset.js +31 -0
  68. package/dist/procedures/git/reset.js.map +1 -0
  69. package/dist/procedures/git/restore.d.ts +13 -0
  70. package/dist/procedures/git/restore.d.ts.map +1 -0
  71. package/dist/procedures/git/restore.js +26 -0
  72. package/dist/procedures/git/restore.js.map +1 -0
  73. package/dist/procedures/git/revParse.d.ts +13 -0
  74. package/dist/procedures/git/revParse.d.ts.map +1 -0
  75. package/dist/procedures/git/revParse.js +23 -0
  76. package/dist/procedures/git/revParse.js.map +1 -0
  77. package/dist/procedures/git/run.d.ts +2 -0
  78. package/dist/procedures/git/run.d.ts.map +1 -1
  79. package/dist/procedures/git/run.js +1 -0
  80. package/dist/procedures/git/run.js.map +1 -1
  81. package/dist/procedures/git/show.d.ts +15 -0
  82. package/dist/procedures/git/show.d.ts.map +1 -0
  83. package/dist/procedures/git/show.js +49 -0
  84. package/dist/procedures/git/show.js.map +1 -0
  85. package/dist/procedures/git/tag/add.d.ts +12 -0
  86. package/dist/procedures/git/tag/add.d.ts.map +1 -0
  87. package/dist/procedures/git/tag/add.js +25 -0
  88. package/dist/procedures/git/tag/add.js.map +1 -0
  89. package/dist/procedures/git/tag/delete.d.ts +12 -0
  90. package/dist/procedures/git/tag/delete.d.ts.map +1 -0
  91. package/dist/procedures/git/tag/delete.js +19 -0
  92. package/dist/procedures/git/tag/delete.js.map +1 -0
  93. package/dist/procedures/git/tag/list.d.ts +12 -0
  94. package/dist/procedures/git/tag/list.d.ts.map +1 -0
  95. package/dist/procedures/git/tag/list.js +34 -0
  96. package/dist/procedures/git/tag/list.js.map +1 -0
  97. package/dist/procedures/git/worktree/add.d.ts +13 -0
  98. package/dist/procedures/git/worktree/add.d.ts.map +1 -0
  99. package/dist/procedures/git/worktree/add.js +35 -0
  100. package/dist/procedures/git/worktree/add.js.map +1 -0
  101. package/dist/procedures/git/worktree/list.d.ts +17 -0
  102. package/dist/procedures/git/worktree/list.d.ts.map +1 -0
  103. package/dist/procedures/git/worktree/list.js +52 -0
  104. package/dist/procedures/git/worktree/list.js.map +1 -0
  105. package/dist/procedures/git/worktree/remove.d.ts +13 -0
  106. package/dist/procedures/git/worktree/remove.d.ts.map +1 -0
  107. package/dist/procedures/git/worktree/remove.js +20 -0
  108. package/dist/procedures/git/worktree/remove.js.map +1 -0
  109. package/dist/types.d.ts +308 -0
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +265 -0
  112. package/dist/types.js.map +1 -1
  113. package/package.json +4 -4
  114. package/src/module.ts +411 -2
  115. package/src/procedures/git/blame.ts +68 -0
  116. package/src/procedures/git/branch/add.ts +24 -0
  117. package/src/procedures/git/branch/delete.ts +29 -0
  118. package/src/procedures/git/branch/list.ts +40 -0
  119. package/src/procedures/git/branch.ts +19 -40
  120. package/src/procedures/git/commit.ts +9 -11
  121. package/src/procedures/git/index.ts +19 -0
  122. package/src/procedures/git/init.ts +11 -10
  123. package/src/procedures/git/merge/abort.ts +20 -0
  124. package/src/procedures/git/merge.ts +42 -0
  125. package/src/procedures/git/push.ts +4 -2
  126. package/src/procedures/git/rebase/abort.ts +19 -0
  127. package/src/procedures/git/rebase/continue.ts +25 -0
  128. package/src/procedures/git/rebase.ts +48 -0
  129. package/src/procedures/git/refs.ts +55 -0
  130. package/src/procedures/git/reset.ts +34 -0
  131. package/src/procedures/git/restore.ts +27 -0
  132. package/src/procedures/git/revParse.ts +24 -0
  133. package/src/procedures/git/run.ts +3 -0
  134. package/src/procedures/git/show.ts +57 -0
  135. package/src/procedures/git/tag/add.ts +27 -0
  136. package/src/procedures/git/tag/delete.ts +22 -0
  137. package/src/procedures/git/tag/list.ts +36 -0
  138. package/src/procedures/git/worktree/add.ts +35 -0
  139. package/src/procedures/git/worktree/list.ts +48 -0
  140. package/src/procedures/git/worktree/remove.ts +21 -0
  141. package/src/types.ts +623 -0
@@ -1,61 +1,40 @@
1
1
  /**
2
- * git.branch procedure
2
+ * git.branch procedure (deprecated)
3
3
  *
4
- * Branch operations
4
+ * Branch operations. Since roadmap R40, git.branch.list, git.branch.add and git.branch.delete do
5
+ * this, one procedure for each operation. This procedure stays for one release, and calls them.
6
+ * Its inputs and outputs do not change. The current branch comes from `git symbolic-ref`, so a
7
+ * repository without commits works now too (before, `git rev-parse --abbrev-ref HEAD` failed).
5
8
  */
6
9
 
7
- import { gitArg } from "./args.js";
8
- import { git, type GitContext } from "./run.js";
9
- import type { GitBranchInput, GitBranchOutput, GitBranchInfo } from "../../types.js";
10
+ import { type GitContext } from "./run.js";
11
+ import { currentBranch } from "./refs.js";
12
+ import { gitBranchList } from "./branch/list.js";
13
+ import { gitBranchAdd } from "./branch/add.js";
14
+ import { gitBranchDelete } from "./branch/delete.js";
15
+ import type { GitBranchInput, GitBranchOutput } from "../../types.js";
10
16
 
11
17
  /**
12
18
  * Branch operations
19
+ *
20
+ * @deprecated Use `gitBranchList`, `gitBranchAdd` or `gitBranchDelete` (git.branch.list,
21
+ * git.branch.add, git.branch.delete).
13
22
  */
14
23
  export async function gitBranch(input: GitBranchInput, ctx: GitContext = {}): Promise<GitBranchOutput> {
15
24
  const { name, delete: del, list, remote, cwd } = input;
16
- const run = { cwd, signal: ctx.signal };
17
-
18
- // Get current branch
19
- const current = (await git(["rev-parse", "--abbrev-ref", "HEAD"], run)).trim();
20
25
 
21
26
  // Delete branch
22
27
  if (del && name) {
23
- await git(["branch", "-d", gitArg("name", name)], run);
24
- return { deleted: name, current };
28
+ await gitBranchDelete({ name, cwd }, ctx);
29
+ return { deleted: name, current: await currentBranch({ cwd, signal: ctx.signal }) };
25
30
  }
26
31
 
27
32
  // Create branch
28
33
  if (name && !list) {
29
- await git(["branch", gitArg("name", name)], run);
30
- return { created: name, current };
34
+ await gitBranchAdd({ name, cwd }, ctx);
35
+ return { created: name, current: await currentBranch({ cwd, signal: ctx.signal }) };
31
36
  }
32
37
 
33
38
  // List branches
34
- const args = ["branch"];
35
- if (remote) args.push("-a");
36
- // The full ref name tells local from remote branches: a local "feature/x" also contains "/"
37
- args.push("--format=%(refname)|%(refname:short)|%(HEAD)|%(upstream:short)");
38
-
39
- const output = await git(args, run);
40
- const branches: GitBranchInfo[] = output
41
- .split("\n")
42
- .filter(Boolean)
43
- .map(line => {
44
- const parts = line.split("|");
45
- const fullName = parts[0] ?? "";
46
- const branchName = parts[1] ?? "";
47
- const head = parts[2] ?? "";
48
- const trackingVal = parts[3];
49
- const result: GitBranchInfo = {
50
- name: branchName.replace(/^remotes\//, ""),
51
- current: head === "*",
52
- remote: fullName.startsWith("refs/remotes/"),
53
- };
54
- if (trackingVal) {
55
- result.tracking = trackingVal;
56
- }
57
- return result;
58
- });
59
-
60
- return { branches, current };
39
+ return gitBranchList({ remote, cwd }, ctx);
61
40
  }
@@ -14,20 +14,18 @@ export async function gitCommit(input: GitCommitInput, ctx: GitContext = {}): Pr
14
14
  const { message, all, amend, cwd } = input;
15
15
  const run = { cwd, signal: ctx.signal };
16
16
 
17
- // Check if there's anything to commit (unless amending)
17
+ // Check if there's anything to commit (unless amending). Each line of `git status --porcelain`
18
+ // is "XY path": X is the index, Y the work tree, "??" an untracked file. Without -a, only a
19
+ // staged change counts. With -a, a change of a tracked file counts too. An untracked file is
20
+ // never a change to commit. (Before, any line counted, so an untracked file or an unstaged
21
+ // change made git commit fail with "nothing added to commit": roadmap R33.)
18
22
  if (!amend) {
19
- const status = (await git(["status", "--porcelain"], run)).trim();
20
- if (!status && !all) {
21
- // Nothing staged and not using -a, skip commit
23
+ const lines = (await git(["status", "--porcelain"], run)).split("\n").filter((line) => line.length > 0);
24
+ const tracked = lines.filter((line) => !line.startsWith("??") && !line.startsWith("!!"));
25
+ const staged = tracked.filter((line) => line[0] !== " ");
26
+ if ((all ? tracked : staged).length === 0) {
22
27
  return { hash: "", message: "", author: "", date: "", skipped: true };
23
28
  }
24
- // If using -a, check if there are any modified files
25
- if (all) {
26
- const hasChanges = status.length > 0;
27
- if (!hasChanges) {
28
- return { hash: "", message: "", author: "", date: "", skipped: true };
29
- }
30
- }
31
29
  }
32
30
 
33
31
  const args: string[] = ["commit"];
@@ -20,3 +20,22 @@ export {
20
20
  gitStashExport,
21
21
  gitStashImport,
22
22
  } from "./stash.js";
23
+ export { gitShow } from "./show.js";
24
+ export { gitBlame } from "./blame.js";
25
+ export { gitTagList } from "./tag/list.js";
26
+ export { gitTagAdd } from "./tag/add.js";
27
+ export { gitTagDelete } from "./tag/delete.js";
28
+ export { gitRevParse } from "./revParse.js";
29
+ export { gitReset } from "./reset.js";
30
+ export { gitRestore } from "./restore.js";
31
+ export { gitMerge } from "./merge.js";
32
+ export { gitMergeAbort } from "./merge/abort.js";
33
+ export { gitRebase } from "./rebase.js";
34
+ export { gitRebaseContinue } from "./rebase/continue.js";
35
+ export { gitRebaseAbort } from "./rebase/abort.js";
36
+ export { gitWorktreeList } from "./worktree/list.js";
37
+ export { gitWorktreeAdd } from "./worktree/add.js";
38
+ export { gitWorktreeRemove } from "./worktree/remove.js";
39
+ export { gitBranchList } from "./branch/list.js";
40
+ export { gitBranchAdd } from "./branch/add.js";
41
+ export { gitBranchDelete } from "./branch/delete.js";
@@ -2,28 +2,29 @@
2
2
  * git.init procedure
3
3
  *
4
4
  * Initialize a git repository
5
+ *
6
+ * The procedure looks for `.git` in the folder itself. Before, it called `fs.exists` through
7
+ * `ctx.client`, so a registry of `gitModule` alone (`createRegistry(gitModule)`) did not have the
8
+ * procedure that it called, and `git.init` always failed there (roadmap R33).
5
9
  */
6
10
 
11
+ import { access } from "node:fs/promises";
7
12
  import { join, resolve } from "node:path";
8
- import type { ProcedureContext } from "@procedures/client";
9
- import { git } from "./run.js";
13
+ import { git, type GitContext } from "./run.js";
10
14
  import type { GitInitInput, GitInitOutput } from "../../types.js";
11
15
 
12
- interface FsExistsOutput { exists: boolean; path: string; }
13
-
14
16
  /**
15
17
  * Initialize a git repository
16
18
  */
17
- export async function gitInit(input: GitInitInput, ctx: ProcedureContext): Promise<GitInitOutput> {
19
+ export async function gitInit(input: GitInitInput, ctx: GitContext = {}): Promise<GitInitOutput> {
18
20
  const cwd = input.cwd ? resolve(input.cwd) : process.cwd();
19
21
 
20
- // Check if already a git repo
22
+ // Check if already a git repo (as fs.exists did: an error of the check means "no")
21
23
  const gitDir = join(cwd, ".git");
22
- const existsResult = await ctx.client.call<{ path: string }, FsExistsOutput>(
23
- ["fs", "exists"],
24
- { path: gitDir }
24
+ const alreadyExists = await access(gitDir).then(
25
+ () => true,
26
+ () => false,
25
27
  );
26
- const alreadyExists = existsResult.exists;
27
28
 
28
29
  if (!alreadyExists) {
29
30
  const args: string[] = ["init"];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * git.merge.abort procedure
3
+ *
4
+ * Stop a merge that has conflicts (roadmap R40). Git puts back the branch and the files as they
5
+ * were before the merge. The resolutions of the conflicts are lost. Without a merge, the call
6
+ * fails.
7
+ */
8
+
9
+ import { git, type GitContext } from "../run.js";
10
+ import { headHash } from "../refs.js";
11
+ import type { GitMergeAbortInput, GitMergeAbortOutput } from "../../../types.js";
12
+
13
+ /**
14
+ * Stop a merge that has conflicts
15
+ */
16
+ export async function gitMergeAbort(input: GitMergeAbortInput, ctx: GitContext = {}): Promise<GitMergeAbortOutput> {
17
+ const run = { cwd: input.cwd, signal: ctx.signal };
18
+ await git(["merge", "--abort"], run);
19
+ return { aborted: true, hash: await headHash(run) };
20
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * git.merge procedure
3
+ *
4
+ * Merge a branch or a commit into the current branch (roadmap R40).
5
+ *
6
+ * A merge that stops on conflicts is a result, not an error: `conflict` is true, and `conflicts`
7
+ * names the files. Then resolve them and call git.commit, or call git.merge.abort. Git does not
8
+ * start an editor for the message of the merge commit.
9
+ */
10
+
11
+ import { gitArg } from "./args.js";
12
+ import { git, type GitContext } from "./run.js";
13
+ import { commitOf, conflictedFiles, headHash, NO_EDITOR, stoppedOnConflict } from "./refs.js";
14
+ import type { GitMergeInput, GitMergeOutput } from "../../types.js";
15
+
16
+ /**
17
+ * Merge a branch or a commit into the current branch
18
+ */
19
+ export async function gitMerge(input: GitMergeInput, ctx: GitContext = {}): Promise<GitMergeOutput> {
20
+ const { ref, message, cwd } = input;
21
+ const fastForward = input.fastForward ?? "auto";
22
+ const run = { cwd, signal: ctx.signal };
23
+
24
+ const target = await commitOf(ref, run);
25
+ const before = await headHash(run);
26
+
27
+ const args = ["merge", "--no-edit"];
28
+ if (fastForward === "only") args.push("--ff-only");
29
+ if (fastForward === "never") args.push("--no-ff");
30
+ if (message !== undefined) args.push("-m", message);
31
+ args.push(gitArg("ref", ref));
32
+
33
+ try {
34
+ await git(args, { ...run, env: NO_EDITOR });
35
+ } catch (error) {
36
+ if (!stoppedOnConflict(error)) throw error;
37
+ return { ref, hash: before, fastForward: false, upToDate: false, conflict: true, conflicts: await conflictedFiles(run) };
38
+ }
39
+
40
+ const after = await headHash(run);
41
+ return { ref, hash: after, fastForward: after !== before && after === target, upToDate: after === before, conflict: false };
42
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { gitArg } from "./args.js";
8
8
  import { git, GitError, type GitContext } from "./run.js";
9
+ import { currentBranch } from "./refs.js";
9
10
  import type { GitPushInput, GitPushOutput } from "../../types.js";
10
11
 
11
12
  /**
@@ -16,8 +17,9 @@ export async function gitPush(input: GitPushInput, ctx: GitContext = {}): Promis
16
17
  const remoteName = input.remote ?? "origin";
17
18
  const run = { cwd, signal: ctx.signal };
18
19
 
19
- // Get current branch if not specified
20
- const branchName = branch || (await git(["rev-parse", "--abbrev-ref", "HEAD"], run)).trim();
20
+ // The current branch if not specified. `git symbolic-ref` reads it in a repository without
21
+ // commits too, so git gives its own error there (roadmap R40).
22
+ const branchName = branch || (await currentBranch(run));
21
23
 
22
24
  // Count commits to push
23
25
  let commits = 0;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * git.rebase.abort procedure
3
+ *
4
+ * Stop a rebase, and go back to the branch as it was before the rebase (roadmap R40). The
5
+ * resolutions of the conflicts are lost. Without a rebase, the call fails.
6
+ */
7
+
8
+ import { git, type GitContext } from "../run.js";
9
+ import { headHash } from "../refs.js";
10
+ import type { GitRebaseAbortInput, GitRebaseAbortOutput } from "../../../types.js";
11
+
12
+ /**
13
+ * Stop a rebase, and go back to the branch before it
14
+ */
15
+ export async function gitRebaseAbort(input: GitRebaseAbortInput, ctx: GitContext = {}): Promise<GitRebaseAbortOutput> {
16
+ const run = { cwd: input.cwd, signal: ctx.signal };
17
+ await git(["rebase", "--abort"], run);
18
+ return { aborted: true, hash: await headHash(run) };
19
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * git.rebase.continue procedure
3
+ *
4
+ * Continue a rebase after the conflicts are resolved and staged (roadmap R40). Git keeps the
5
+ * message of each commit: it does not start an editor. A next commit can stop on conflicts again.
6
+ */
7
+
8
+ import { type GitContext } from "../run.js";
9
+ import { conflictedFiles, headHash } from "../refs.js";
10
+ import { rebaseStep } from "../rebase.js";
11
+ import type { GitRebaseContinueInput, GitRebaseContinueOutput } from "../../../types.js";
12
+
13
+ /**
14
+ * Continue a rebase after the conflicts are resolved
15
+ */
16
+ export async function gitRebaseContinue(input: GitRebaseContinueInput, ctx: GitContext = {}): Promise<GitRebaseContinueOutput> {
17
+ const run = { cwd: input.cwd, signal: ctx.signal };
18
+ // Files that still have conflicts: git refuses to continue, so the call gives them
19
+ const unresolved = await conflictedFiles(run);
20
+ if (unresolved.length > 0) {
21
+ return { hash: await headHash(run), completed: false, upToDate: false, conflict: true, conflicts: unresolved };
22
+ }
23
+ // A continued rebase changed the branch: upToDate is false
24
+ return rebaseStep(["rebase", "--continue"], "", run);
25
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * git.rebase procedure
3
+ *
4
+ * Rebase the current branch on another branch (roadmap R40). The rebase is never interactive,
5
+ * and git does not start an editor.
6
+ *
7
+ * A rebase that stops on conflicts is a result, not an error: `conflict` is true, and
8
+ * `conflicts` names the files. Then resolve them, stage them with git.add and call
9
+ * git.rebase.continue, or call git.rebase.abort.
10
+ */
11
+
12
+ import { gitArg } from "./args.js";
13
+ import { git, type GitContext, type GitRunOptions } from "./run.js";
14
+ import { commitOf, conflictedFiles, headHash, NO_EDITOR, stoppedOnConflict } from "./refs.js";
15
+ import type { GitRebaseInput, GitRebaseOutput } from "../../types.js";
16
+
17
+ /**
18
+ * This function runs a step of a rebase (the start or `--continue`), and gives its result. A
19
+ * stop on conflicts gives `conflict: true`. Any other failure throws.
20
+ */
21
+ export async function rebaseStep(args: string[], before: string, run: GitRunOptions): Promise<GitRebaseOutput> {
22
+ try {
23
+ await git(args, { ...run, env: NO_EDITOR });
24
+ } catch (error) {
25
+ if (!stoppedOnConflict(error)) throw error;
26
+ return { hash: await headHash(run), completed: false, upToDate: false, conflict: true, conflicts: await conflictedFiles(run) };
27
+ }
28
+ const hash = await headHash(run);
29
+ return { hash, completed: true, upToDate: hash === before, conflict: false };
30
+ }
31
+
32
+ /**
33
+ * Rebase the current branch on another branch
34
+ */
35
+ export async function gitRebase(input: GitRebaseInput, ctx: GitContext = {}): Promise<GitRebaseOutput> {
36
+ const { upstream, onto, branch, cwd } = input;
37
+ const run = { cwd, signal: ctx.signal };
38
+
39
+ // With a branch, git checks it out first: compare with that branch
40
+ const before = branch ? await commitOf(branch, run) : await headHash(run);
41
+
42
+ const args = ["rebase"];
43
+ if (onto) args.push("--onto", gitArg("onto", onto));
44
+ args.push(gitArg("upstream", upstream));
45
+ if (branch) args.push(gitArg("branch", branch));
46
+
47
+ return rebaseStep(args, before, run);
48
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Helpers of the git procedures of roadmap R40: the current branch, the hash of a ref, the
3
+ * files with conflicts, and the stop of a merge or a rebase on conflicts.
4
+ */
5
+
6
+ import { gitArg } from "./args.js";
7
+ import { git, GitError, zFields, type GitRunOptions } from "./run.js";
8
+
9
+ /**
10
+ * The current branch, or "HEAD" when HEAD is detached.
11
+ *
12
+ * `git rev-parse --abbrev-ref HEAD` fails in a repository without commits (roadmap R33), because
13
+ * HEAD names a branch that does not exist yet. `git symbolic-ref` reads the name that HEAD
14
+ * holds, so it works there too. It fails only for a detached HEAD.
15
+ */
16
+ export async function currentBranch(run: GitRunOptions): Promise<string> {
17
+ try {
18
+ return (await git(["symbolic-ref", "--quiet", "--short", "HEAD"], run)).trim();
19
+ } catch (error) {
20
+ if (!(error instanceof GitError)) throw error;
21
+ // A detached HEAD: exit 1 and no message. Outside a repository: exit 128, and that is an error.
22
+ if (error.exitCode === 1) return "HEAD";
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ /** The hash of the commit of a ref (a tag gives its commit). It throws a `GitError` for an unknown ref. */
28
+ export async function commitOf(ref: string, run: GitRunOptions): Promise<string> {
29
+ return (await git(["rev-parse", "--verify", `${gitArg("ref", ref)}^{commit}`], run)).trim();
30
+ }
31
+
32
+ /** The hash of HEAD. */
33
+ export async function headHash(run: GitRunOptions): Promise<string> {
34
+ return (await git(["rev-parse", "--verify", "HEAD"], run)).trim();
35
+ }
36
+
37
+ /** The files that have conflicts, as `git diff --diff-filter=U` gives them. */
38
+ export async function conflictedFiles(run: GitRunOptions): Promise<string[]> {
39
+ return zFields(await git(["diff", "--name-only", "--diff-filter=U", "-z"], run));
40
+ }
41
+
42
+ /**
43
+ * True when a failed merge or rebase stopped on conflicts. Git writes "CONFLICT (content): ..."
44
+ * to stdout, and "could not apply" to stderr. Any other failure (a working tree with changes, an
45
+ * unknown ref) is an error of the call.
46
+ */
47
+ export function stoppedOnConflict(error: unknown): boolean {
48
+ return error instanceof GitError && /^CONFLICT \(|could not apply/im.test(`${error.stdout}\n${error.stderr}`);
49
+ }
50
+
51
+ /**
52
+ * The environment of a merge or a rebase. Git does not start an editor for a message: it keeps
53
+ * the message that it made. `GIT_EDITOR` overrides `core.editor` and `EDITOR` of the machine.
54
+ */
55
+ export const NO_EDITOR: Record<string, string> = { GIT_EDITOR: "true" };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * git.reset procedure
3
+ *
4
+ * Move HEAD to a commit, and reset the index or the working tree (roadmap R40). The mode hard
5
+ * deletes the changes of the working tree that are not committed.
6
+ */
7
+
8
+ import { gitArg } from "./args.js";
9
+ import { git, type GitContext } from "./run.js";
10
+ import { headHash } from "./refs.js";
11
+ import type { GitResetInput, GitResetOutput } from "../../types.js";
12
+
13
+ /**
14
+ * Move HEAD to a commit, and reset the index or the working tree
15
+ */
16
+ export async function gitReset(input: GitResetInput, ctx: GitContext = {}): Promise<GitResetOutput> {
17
+ const ref = input.ref ?? "HEAD";
18
+ const mode = input.mode ?? "mixed";
19
+ const run = { cwd: input.cwd, signal: ctx.signal };
20
+
21
+ const args = ["reset", "--quiet"];
22
+ if (input.paths && input.paths.length > 0) {
23
+ // Git resets paths only in the index: HEAD and the working tree stay
24
+ if (mode !== "mixed") {
25
+ throw new Error(`git.reset: paths need the mode mixed, not ${mode}`);
26
+ }
27
+ args.push(gitArg("ref", ref), "--", ...input.paths);
28
+ } else {
29
+ args.push(`--${mode}`, gitArg("ref", ref));
30
+ }
31
+ await git(args, run);
32
+
33
+ return { ref, mode, hash: await headHash(run) };
34
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * git.restore procedure
3
+ *
4
+ * Restore files of the working tree or the index (roadmap R40). A restore of the working tree
5
+ * deletes the changes of the files that are not committed.
6
+ */
7
+
8
+ import { gitArg } from "./args.js";
9
+ import { git, type GitContext } from "./run.js";
10
+ import type { GitRestoreInput, GitRestoreOutput } from "../../types.js";
11
+
12
+ /**
13
+ * Restore files of the working tree or the index
14
+ */
15
+ export async function gitRestore(input: GitRestoreInput, ctx: GitContext = {}): Promise<GitRestoreOutput> {
16
+ const { paths, source, staged, worktree, cwd } = input;
17
+
18
+ const args = ["restore"];
19
+ if (source) args.push(`--source=${gitArg("source", source)}`);
20
+ if (staged) args.push("--staged");
21
+ if (worktree) args.push("--worktree");
22
+ // After "--", a path that starts with "-" stays a path
23
+ args.push("--", ...paths);
24
+ await git(args, { cwd, signal: ctx.signal });
25
+
26
+ return { restored: paths };
27
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * git.revParse procedure
3
+ *
4
+ * Give the hash or the name of a ref (roadmap R40). With `--verify`, git gives exactly one value,
5
+ * or fails for an unknown ref.
6
+ */
7
+
8
+ import { gitArg } from "./args.js";
9
+ import { git, type GitContext } from "./run.js";
10
+ import type { GitRevParseInput, GitRevParseOutput } from "../../types.js";
11
+
12
+ /**
13
+ * Give the hash or the name of a ref
14
+ */
15
+ export async function gitRevParse(input: GitRevParseInput, ctx: GitContext = {}): Promise<GitRevParseOutput> {
16
+ const ref = input.ref ?? "HEAD";
17
+ const args = ["rev-parse", "--verify"];
18
+ if (input.abbrevRef) args.push("--abbrev-ref");
19
+ else if (input.short) args.push("--short");
20
+ args.push(gitArg("ref", ref));
21
+
22
+ const value = (await git(args, { cwd: input.cwd, signal: ctx.signal })).trim();
23
+ return { ref, value };
24
+ }
@@ -18,6 +18,8 @@ export interface GitRunOptions extends GitContext {
18
18
  cwd?: string | undefined;
19
19
  /** Text to write to the stdin of git (for example a patch for `git apply`). */
20
20
  input?: string | undefined;
21
+ /** Environment variables to add for git (for example `GIT_EDITOR`). */
22
+ env?: Record<string, string> | undefined;
21
23
  }
22
24
 
23
25
  /** A git command that ended with an exit code other than 0. */
@@ -43,6 +45,7 @@ export async function git(args: string[], options: GitRunOptions = {}): Promise<
43
45
  cwd: options.cwd,
44
46
  signal: options.signal,
45
47
  input: options.input,
48
+ env: options.env,
46
49
  maxOutputBytes: GIT_MAX_BUFFER,
47
50
  });
48
51
  if (result.aborted) {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * git.show procedure
3
+ *
4
+ * Show a commit, or a file at a commit (roadmap R40).
5
+ *
6
+ * The procedure first resolves the ref to its commit, so a tag gives its commit and not the text
7
+ * of the tag object. A merge commit gives its changes against its first parent.
8
+ */
9
+
10
+ import { parseNumstat } from "./diff.js";
11
+ import { git, type GitContext } from "./run.js";
12
+ import { commitOf } from "./refs.js";
13
+ import type { GitShowInput, GitShowOutput } from "../../types.js";
14
+
15
+ // Control characters that do not occur in a name, an email, a date or a subject
16
+ const FIELD = "\x1f";
17
+
18
+ /**
19
+ * Show a commit, or a file at a commit
20
+ */
21
+ export async function gitShow(input: GitShowInput, ctx: GitContext = {}): Promise<GitShowOutput> {
22
+ const ref = input.ref ?? "HEAD";
23
+ const run = { cwd: input.cwd, signal: ctx.signal };
24
+ const hash = await commitOf(ref, run);
25
+
26
+ // A file at the commit: `<hash>:<path>` starts with the hash, so a path that starts with "-"
27
+ // stays a path
28
+ if (input.path !== undefined) {
29
+ const content = await git(["show", `${hash}:${input.path}`], run);
30
+ return { ref, hash, path: input.path, content };
31
+ }
32
+
33
+ const format = `--format=${["%H", "%h", "%an", "%ae", "%ci", "%s", "%b"].join("%x1f")}`;
34
+ const [, shortHash = "", author = "", email = "", date = "", message = "", body = ""] = (
35
+ await git(["show", "-s", format, hash], run)
36
+ ).split(FIELD);
37
+
38
+ const changes = ["show", "--format=", "--diff-merges=first-parent"];
39
+ const files = parseNumstat(await git([...changes, "--numstat", "-z", hash], run));
40
+ const result: GitShowOutput = {
41
+ ref,
42
+ hash,
43
+ shortHash,
44
+ author,
45
+ email,
46
+ date,
47
+ message,
48
+ body: body.replace(/\n+$/, ""),
49
+ files,
50
+ additions: files.reduce((sum, file) => sum + file.additions, 0),
51
+ deletions: files.reduce((sum, file) => sum + file.deletions, 0),
52
+ };
53
+ if (!input.stat) {
54
+ result.diff = (await git([...changes, "--patch", hash], run)).replace(/^\n/, "");
55
+ }
56
+ return result;
57
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * git.tag.add procedure
3
+ *
4
+ * Make a tag (roadmap R40). An existing tag is an error: the procedure never moves a tag.
5
+ */
6
+
7
+ import { gitArg } from "../args.js";
8
+ import { git, type GitContext } from "../run.js";
9
+ import { commitOf } from "../refs.js";
10
+ import type { GitTagAddInput, GitTagAddOutput } from "../../../types.js";
11
+
12
+ /**
13
+ * Make a tag
14
+ */
15
+ export async function gitTagAdd(input: GitTagAddInput, ctx: GitContext = {}): Promise<GitTagAddOutput> {
16
+ const { name, ref, message, cwd } = input;
17
+ const run = { cwd, signal: ctx.signal };
18
+
19
+ const args = ["tag"];
20
+ // With -m, git makes an annotated tag and does not start an editor
21
+ if (message !== undefined) args.push("-a", "-m", message);
22
+ args.push(gitArg("name", name));
23
+ if (ref) args.push(gitArg("ref", ref));
24
+ await git(args, run);
25
+
26
+ return { name, hash: await commitOf(`refs/tags/${name}`, run), annotated: message !== undefined };
27
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * git.tag.delete procedure
3
+ *
4
+ * Delete a tag (roadmap R40).
5
+ */
6
+
7
+ import { gitArg } from "../args.js";
8
+ import { git, type GitContext } from "../run.js";
9
+ import type { GitTagDeleteInput, GitTagDeleteOutput } from "../../../types.js";
10
+
11
+ /**
12
+ * Delete a tag
13
+ */
14
+ export async function gitTagDelete(input: GitTagDeleteInput, ctx: GitContext = {}): Promise<GitTagDeleteOutput> {
15
+ const name = gitArg("name", input.name);
16
+ const run = { cwd: input.cwd, signal: ctx.signal };
17
+
18
+ // The object of the tag before the deletion: a tag object or a commit
19
+ const hash = (await git(["rev-parse", "--verify", `refs/tags/${name}`], run)).trim();
20
+ await git(["tag", "-d", name], run);
21
+ return { name, hash };
22
+ }