@procedures/git 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +1130 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +514 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures/git/add.d.ts +12 -0
- package/dist/procedures/git/add.d.ts.map +1 -0
- package/dist/procedures/git/add.js +26 -0
- package/dist/procedures/git/add.js.map +1 -0
- package/dist/procedures/git/args.d.ts +15 -0
- package/dist/procedures/git/args.d.ts.map +1 -0
- package/dist/procedures/git/args.js +20 -0
- package/dist/procedures/git/args.js.map +1 -0
- package/dist/procedures/git/branch.d.ts +12 -0
- package/dist/procedures/git/branch.d.ts.map +1 -0
- package/dist/procedures/git/branch.js +54 -0
- package/dist/procedures/git/branch.js.map +1 -0
- package/dist/procedures/git/checkout.d.ts +12 -0
- package/dist/procedures/git/checkout.d.ts.map +1 -0
- package/dist/procedures/git/checkout.js +23 -0
- package/dist/procedures/git/checkout.js.map +1 -0
- package/dist/procedures/git/clone.d.ts +12 -0
- package/dist/procedures/git/clone.d.ts.map +1 -0
- package/dist/procedures/git/clone.js +28 -0
- package/dist/procedures/git/clone.js.map +1 -0
- package/dist/procedures/git/commit.d.ts +12 -0
- package/dist/procedures/git/commit.d.ts.map +1 -0
- package/dist/procedures/git/commit.js +41 -0
- package/dist/procedures/git/commit.js.map +1 -0
- package/dist/procedures/git/diff.d.ts +18 -0
- package/dist/procedures/git/diff.d.ts.map +1 -0
- package/dist/procedures/git/diff.js +58 -0
- package/dist/procedures/git/diff.js.map +1 -0
- package/dist/procedures/git/fetch.d.ts +12 -0
- package/dist/procedures/git/fetch.d.ts.map +1 -0
- package/dist/procedures/git/fetch.js +29 -0
- package/dist/procedures/git/fetch.js.map +1 -0
- package/dist/procedures/git/index.d.ts +15 -0
- package/dist/procedures/git/index.d.ts.map +1 -0
- package/dist/procedures/git/index.js +15 -0
- package/dist/procedures/git/index.js.map +1 -0
- package/dist/procedures/git/init.d.ts +12 -0
- package/dist/procedures/git/init.d.ts.map +1 -0
- package/dist/procedures/git/init.js +32 -0
- package/dist/procedures/git/init.js.map +1 -0
- package/dist/procedures/git/log.d.ts +12 -0
- package/dist/procedures/git/log.d.ts.map +1 -0
- package/dist/procedures/git/log.js +35 -0
- package/dist/procedures/git/log.js.map +1 -0
- package/dist/procedures/git/predicates.d.ts +40 -0
- package/dist/procedures/git/predicates.d.ts.map +1 -0
- package/dist/procedures/git/predicates.js +76 -0
- package/dist/procedures/git/predicates.js.map +1 -0
- package/dist/procedures/git/pull.d.ts +12 -0
- package/dist/procedures/git/pull.d.ts.map +1 -0
- package/dist/procedures/git/pull.js +36 -0
- package/dist/procedures/git/pull.js.map +1 -0
- package/dist/procedures/git/push.d.ts +12 -0
- package/dist/procedures/git/push.d.ts.map +1 -0
- package/dist/procedures/git/push.js +39 -0
- package/dist/procedures/git/push.js.map +1 -0
- package/dist/procedures/git/remote.d.ts +13 -0
- package/dist/procedures/git/remote.d.ts.map +1 -0
- package/dist/procedures/git/remote.js +30 -0
- package/dist/procedures/git/remote.js.map +1 -0
- package/dist/procedures/git/run.d.ts +32 -0
- package/dist/procedures/git/run.d.ts.map +1 -0
- package/dist/procedures/git/run.js +60 -0
- package/dist/procedures/git/run.js.map +1 -0
- package/dist/procedures/git/stash.d.ts +38 -0
- package/dist/procedures/git/stash.d.ts.map +1 -0
- package/dist/procedures/git/stash.js +211 -0
- package/dist/procedures/git/stash.js.map +1 -0
- package/dist/procedures/git/status.d.ts +17 -0
- package/dist/procedures/git/status.d.ts.map +1 -0
- package/dist/procedures/git/status.js +107 -0
- package/dist/procedures/git/status.js.map +1 -0
- package/dist/procedures/git.d.ts +5 -0
- package/dist/procedures/git.d.ts.map +1 -0
- package/dist/procedures/git.js +5 -0
- package/dist/procedures/git.js.map +1 -0
- package/dist/register.d.ts +7 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/types.d.ts +350 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +397 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -3
- package/src/index.ts +20 -0
- package/src/module.ts +656 -0
- package/src/procedures/git/add.ts +29 -0
- package/src/procedures/git/args.ts +21 -0
- package/src/procedures/git/branch.ts +61 -0
- package/src/procedures/git/checkout.ts +27 -0
- package/src/procedures/git/clone.ts +33 -0
- package/src/procedures/git/commit.ts +46 -0
- package/src/procedures/git/diff.ts +62 -0
- package/src/procedures/git/fetch.ts +32 -0
- package/src/procedures/git/index.ts +22 -0
- package/src/procedures/git/init.ts +43 -0
- package/src/procedures/git/log.ts +40 -0
- package/src/procedures/git/predicates.ts +90 -0
- package/src/procedures/git/pull.ts +45 -0
- package/src/procedures/git/push.ts +42 -0
- package/src/procedures/git/remote.ts +31 -0
- package/src/procedures/git/run.ts +70 -0
- package/src/procedures/git/stash.ts +255 -0
- package/src/procedures/git/status.ts +98 -0
- package/src/procedures/git.ts +5 -0
- package/src/register.ts +11 -0
- package/src/types.ts +815 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.branch procedure
|
|
3
|
+
*
|
|
4
|
+
* Branch operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitBranchInput, GitBranchOutput, GitBranchInfo } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Branch operations
|
|
13
|
+
*/
|
|
14
|
+
export async function gitBranch(input: GitBranchInput, ctx: GitContext = {}): Promise<GitBranchOutput> {
|
|
15
|
+
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
|
+
|
|
21
|
+
// Delete branch
|
|
22
|
+
if (del && name) {
|
|
23
|
+
await git(["branch", "-d", gitArg("name", name)], run);
|
|
24
|
+
return { deleted: name, current };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create branch
|
|
28
|
+
if (name && !list) {
|
|
29
|
+
await git(["branch", gitArg("name", name)], run);
|
|
30
|
+
return { created: name, current };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 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 };
|
|
61
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.checkout procedure
|
|
3
|
+
*
|
|
4
|
+
* Checkout branch or files
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitCheckoutInput, GitCheckoutOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checkout branch or files
|
|
13
|
+
*/
|
|
14
|
+
export async function gitCheckout(input: GitCheckoutInput, ctx: GitContext = {}): Promise<GitCheckoutOutput> {
|
|
15
|
+
const { ref, create, paths, cwd } = input;
|
|
16
|
+
|
|
17
|
+
const args: string[] = ["checkout"];
|
|
18
|
+
if (create) args.push("-b");
|
|
19
|
+
args.push(gitArg("ref", ref));
|
|
20
|
+
if (paths && paths.length > 0) {
|
|
21
|
+
args.push("--", ...paths);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await git(args, { cwd, signal: ctx.signal });
|
|
25
|
+
|
|
26
|
+
return { ref, created: create };
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.clone procedure
|
|
3
|
+
*
|
|
4
|
+
* Clone repository
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join, basename } from "node:path";
|
|
8
|
+
import { gitArg } from "./args.js";
|
|
9
|
+
import { git, type GitContext } from "./run.js";
|
|
10
|
+
import type { GitCloneInput, GitCloneOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clone repository
|
|
14
|
+
*/
|
|
15
|
+
export async function gitClone(input: GitCloneInput, ctx: GitContext = {}): Promise<GitCloneOutput> {
|
|
16
|
+
const { url, dest, branch, depth, cwd } = input;
|
|
17
|
+
|
|
18
|
+
// Determine destination directory
|
|
19
|
+
const destDir = dest || basename(url, ".git").replace(/\.git$/, "");
|
|
20
|
+
const fullPath = cwd ? join(cwd, destDir) : destDir;
|
|
21
|
+
|
|
22
|
+
const args: string[] = ["clone"];
|
|
23
|
+
if (branch) args.push("-b", branch);
|
|
24
|
+
if (depth) args.push("--depth", String(depth));
|
|
25
|
+
args.push("--", gitArg("url", url), gitArg("dest", destDir));
|
|
26
|
+
|
|
27
|
+
await git(args, { cwd, signal: ctx.signal });
|
|
28
|
+
|
|
29
|
+
// Get the branch that was checked out
|
|
30
|
+
const clonedBranch = (await git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: fullPath, signal: ctx.signal })).trim();
|
|
31
|
+
|
|
32
|
+
return { path: fullPath, branch: clonedBranch };
|
|
33
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.commit procedure
|
|
3
|
+
*
|
|
4
|
+
* Create commit
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { git, type GitContext } from "./run.js";
|
|
8
|
+
import type { GitCommitInput, GitCommitOutput } from "../../types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create commit
|
|
12
|
+
*/
|
|
13
|
+
export async function gitCommit(input: GitCommitInput, ctx: GitContext = {}): Promise<GitCommitOutput> {
|
|
14
|
+
const { message, all, amend, cwd } = input;
|
|
15
|
+
const run = { cwd, signal: ctx.signal };
|
|
16
|
+
|
|
17
|
+
// Check if there's anything to commit (unless amending)
|
|
18
|
+
if (!amend) {
|
|
19
|
+
const status = (await git(["status", "--porcelain"], run)).trim();
|
|
20
|
+
if (!status && !all) {
|
|
21
|
+
// Nothing staged and not using -a, skip commit
|
|
22
|
+
return { hash: "", message: "", author: "", date: "", skipped: true };
|
|
23
|
+
}
|
|
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
|
+
}
|
|
32
|
+
|
|
33
|
+
const args: string[] = ["commit"];
|
|
34
|
+
if (all) args.push("-a");
|
|
35
|
+
if (amend) args.push("--amend");
|
|
36
|
+
args.push("-m", message);
|
|
37
|
+
|
|
38
|
+
await git(args, run);
|
|
39
|
+
|
|
40
|
+
// Get commit info
|
|
41
|
+
const format = "--format=%H%n%s%n%an%n%ci";
|
|
42
|
+
const info = (await git(["log", "-1", format], run)).trim();
|
|
43
|
+
const [hash = "", msg = "", author = "", date = ""] = info.split("\n");
|
|
44
|
+
|
|
45
|
+
return { hash, message: msg, author, date };
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.diff procedure
|
|
3
|
+
*
|
|
4
|
+
* Show changes
|
|
5
|
+
*
|
|
6
|
+
* The counts come from `git diff --numstat -z`. With `-z`, each path is exact, and a rename gives
|
|
7
|
+
* the old and the new path in separate fields (deep dive WRP-7). Before, the procedure split each
|
|
8
|
+
* line at whitespace: a path with a space lost its end, and a rename gave its old path only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { gitArg } from "./args.js";
|
|
12
|
+
import { git, zFields, type GitContext } from "./run.js";
|
|
13
|
+
import type { GitDiffInput, GitDiffOutput, GitDiffFile } from "../../types.js";
|
|
14
|
+
|
|
15
|
+
/** This function reads the output of `git diff --numstat -z`. */
|
|
16
|
+
export function parseNumstat(output: string): GitDiffFile[] {
|
|
17
|
+
const files: GitDiffFile[] = [];
|
|
18
|
+
const fields = zFields(output);
|
|
19
|
+
for (let i = 0; i < fields.length; i++) {
|
|
20
|
+
const [add = "0", del = "0", ...rest] = fields[i]!.split("\t");
|
|
21
|
+
// A binary file has "-" for both counts
|
|
22
|
+
const additions = add === "-" ? 0 : parseInt(add, 10);
|
|
23
|
+
const deletions = del === "-" ? 0 : parseInt(del, 10);
|
|
24
|
+
const path = rest.join("\t");
|
|
25
|
+
if (path === "") {
|
|
26
|
+
// A rename or a copy: the old path, then the new path, in the next two fields
|
|
27
|
+
const from = fields[++i] ?? "";
|
|
28
|
+
const to = fields[++i] ?? "";
|
|
29
|
+
files.push({ path: to, from, additions, deletions });
|
|
30
|
+
} else {
|
|
31
|
+
files.push({ path, additions, deletions });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Show changes
|
|
39
|
+
*/
|
|
40
|
+
export async function gitDiff(input: GitDiffInput, ctx: GitContext = {}): Promise<GitDiffOutput> {
|
|
41
|
+
const { staged, ref, paths, stat, cwd } = input;
|
|
42
|
+
const run = { cwd, signal: ctx.signal };
|
|
43
|
+
|
|
44
|
+
const selection: string[] = [];
|
|
45
|
+
if (staged) selection.push("--cached");
|
|
46
|
+
if (ref) selection.push(gitArg("ref", ref));
|
|
47
|
+
const pathspec = paths && paths.length > 0 ? ["--", ...paths] : [];
|
|
48
|
+
|
|
49
|
+
const files = parseNumstat(await git(["diff", ...selection, "--numstat", "-z", ...pathspec], run));
|
|
50
|
+
const result: GitDiffOutput = {
|
|
51
|
+
files,
|
|
52
|
+
additions: files.reduce((sum, file) => sum + file.additions, 0),
|
|
53
|
+
deletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Include full diff if not stat-only
|
|
57
|
+
if (!stat) {
|
|
58
|
+
result.diff = await git(["diff", ...selection, ...pathspec], run);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.fetch procedure
|
|
3
|
+
*
|
|
4
|
+
* Fetch from remote without merging
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitFetchInput, GitFetchOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch from remote
|
|
13
|
+
*/
|
|
14
|
+
export async function gitFetch(input: GitFetchInput, ctx: GitContext = {}): Promise<GitFetchOutput> {
|
|
15
|
+
const { branch, all, prune, cwd } = input;
|
|
16
|
+
const remoteName = input.remote ?? "origin";
|
|
17
|
+
|
|
18
|
+
const args: string[] = ["fetch"];
|
|
19
|
+
if (all) {
|
|
20
|
+
args.push("--all");
|
|
21
|
+
} else {
|
|
22
|
+
args.push(gitArg("remote", remoteName));
|
|
23
|
+
if (branch) {
|
|
24
|
+
args.push(gitArg("branch", branch));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (prune) args.push("--prune");
|
|
28
|
+
|
|
29
|
+
await git(args, { cwd, signal: ctx.signal });
|
|
30
|
+
|
|
31
|
+
return { remote: remoteName, fetched: true };
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { gitStatus } from "./status.js";
|
|
2
|
+
export { gitAdd } from "./add.js";
|
|
3
|
+
export { gitCommit } from "./commit.js";
|
|
4
|
+
export { gitPush } from "./push.js";
|
|
5
|
+
export { gitPull } from "./pull.js";
|
|
6
|
+
export { gitClone } from "./clone.js";
|
|
7
|
+
export { gitCheckout } from "./checkout.js";
|
|
8
|
+
export { gitBranch } from "./branch.js";
|
|
9
|
+
export { gitLog } from "./log.js";
|
|
10
|
+
export { gitDiff } from "./diff.js";
|
|
11
|
+
export { gitInit } from "./init.js";
|
|
12
|
+
export { gitRemote, gitRemoteSet } from "./remote.js";
|
|
13
|
+
export { gitFetch } from "./fetch.js";
|
|
14
|
+
export {
|
|
15
|
+
gitStashList,
|
|
16
|
+
gitStashPush,
|
|
17
|
+
gitStashPop,
|
|
18
|
+
gitStashApply,
|
|
19
|
+
gitStashDrop,
|
|
20
|
+
gitStashExport,
|
|
21
|
+
gitStashImport,
|
|
22
|
+
} from "./stash.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.init procedure
|
|
3
|
+
*
|
|
4
|
+
* Initialize a git repository
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import type { ProcedureContext } from "@procedures/client";
|
|
9
|
+
import { git } from "./run.js";
|
|
10
|
+
import type { GitInitInput, GitInitOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
interface FsExistsOutput { exists: boolean; path: string; }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize a git repository
|
|
16
|
+
*/
|
|
17
|
+
export async function gitInit(input: GitInitInput, ctx: ProcedureContext): Promise<GitInitOutput> {
|
|
18
|
+
const cwd = input.cwd ? resolve(input.cwd) : process.cwd();
|
|
19
|
+
|
|
20
|
+
// Check if already a git repo
|
|
21
|
+
const gitDir = join(cwd, ".git");
|
|
22
|
+
const existsResult = await ctx.client.call<{ path: string }, FsExistsOutput>(
|
|
23
|
+
["fs", "exists"],
|
|
24
|
+
{ path: gitDir }
|
|
25
|
+
);
|
|
26
|
+
const alreadyExists = existsResult.exists;
|
|
27
|
+
|
|
28
|
+
if (!alreadyExists) {
|
|
29
|
+
const args: string[] = ["init"];
|
|
30
|
+
if (input.bare) {
|
|
31
|
+
args.push("--bare");
|
|
32
|
+
}
|
|
33
|
+
if (input.initialBranch) {
|
|
34
|
+
args.push(`--initial-branch=${input.initialBranch}`);
|
|
35
|
+
}
|
|
36
|
+
await git(args, { cwd, signal: ctx.signal });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
path: cwd,
|
|
41
|
+
created: !alreadyExists,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.log procedure
|
|
3
|
+
*
|
|
4
|
+
* Show commit log
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitLogInput, GitLogOutput, GitLogCommit } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
// Control characters that do not occur in a name, an email, a date or a subject
|
|
12
|
+
const FIELD = "\x1f";
|
|
13
|
+
const RECORD = "\x1e";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Show commit log
|
|
17
|
+
*/
|
|
18
|
+
export async function gitLog(input: GitLogInput, ctx: GitContext = {}): Promise<GitLogOutput> {
|
|
19
|
+
const { count, ref, cwd } = input;
|
|
20
|
+
|
|
21
|
+
const format = `--format=${["%H", "%h", "%an", "%ae", "%ci", "%s"].join("%x1f")}%x1e`;
|
|
22
|
+
const args = ["log", `-n${count}`, format];
|
|
23
|
+
if (ref) args.push(gitArg("ref", ref));
|
|
24
|
+
|
|
25
|
+
const output = await git(args, { cwd, signal: ctx.signal });
|
|
26
|
+
const commits: GitLogCommit[] = output
|
|
27
|
+
.split(RECORD)
|
|
28
|
+
.map((record) => record.replace(/^\n/, ""))
|
|
29
|
+
.filter((record) => record.length > 0)
|
|
30
|
+
.map((record) => {
|
|
31
|
+
const [hash = "", shortHash = "", author = "", email = "", date = "", message = ""] = record.split(FIELD);
|
|
32
|
+
return { hash, shortHash, author, email, date, message };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result: GitLogOutput = { commits };
|
|
36
|
+
if (input.oneline) {
|
|
37
|
+
result.lines = commits.map((commit) => `${commit.shortHash} ${commit.message}`);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git predicate procedures
|
|
3
|
+
*
|
|
4
|
+
* Simple boolean predicates for conditional execution.
|
|
5
|
+
* These return true/false based on git repository state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { git, GitError, type GitContext } from "./run.js";
|
|
9
|
+
|
|
10
|
+
export interface GitPredicateInput {
|
|
11
|
+
/** Working directory (default: process.cwd()) */
|
|
12
|
+
cwd?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GitPredicateOutput {
|
|
16
|
+
/** The predicate result */
|
|
17
|
+
value: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The output of a git command, or null when git fails (for example outside a repository).
|
|
22
|
+
* An aborted signal still throws.
|
|
23
|
+
*/
|
|
24
|
+
async function gitOrNull(args: string[], cwd: string | undefined, ctx: GitContext): Promise<string | null> {
|
|
25
|
+
try {
|
|
26
|
+
return (await git(args, { cwd, signal: ctx.signal })).trim();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof GitError) return null;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if there are any changes (unstaged, staged, or untracked)
|
|
35
|
+
*/
|
|
36
|
+
export async function gitHasChanges(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
37
|
+
const status = await gitOrNull(["status", "--porcelain"], input.cwd, ctx);
|
|
38
|
+
return { value: status !== null && status.length > 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if there are any staged changes ready to commit
|
|
43
|
+
*/
|
|
44
|
+
export async function gitHasStagedChanges(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
45
|
+
// git diff --cached shows only staged changes
|
|
46
|
+
const diff = await gitOrNull(["diff", "--cached", "--name-only"], input.cwd, ctx);
|
|
47
|
+
return { value: diff !== null && diff.length > 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if there are any unstaged changes (modified or deleted files)
|
|
52
|
+
*/
|
|
53
|
+
export async function gitHasUnstagedChanges(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
54
|
+
// git diff shows only unstaged changes to tracked files
|
|
55
|
+
const diff = await gitOrNull(["diff", "--name-only"], input.cwd, ctx);
|
|
56
|
+
return { value: diff !== null && diff.length > 0 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if there are any untracked files
|
|
61
|
+
*/
|
|
62
|
+
export async function gitHasUntrackedFiles(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
63
|
+
// List untracked files only
|
|
64
|
+
const untracked = await gitOrNull(["ls-files", "--others", "--exclude-standard"], input.cwd, ctx);
|
|
65
|
+
return { value: untracked !== null && untracked.length > 0 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if there are local commits that haven't been pushed
|
|
70
|
+
*/
|
|
71
|
+
export async function gitHasLocalCommits(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
72
|
+
// Get the upstream tracking branch
|
|
73
|
+
const upstream = await gitOrNull(["rev-parse", "--abbrev-ref", "@{upstream}"], input.cwd, ctx);
|
|
74
|
+
if (upstream !== null) {
|
|
75
|
+
// Count commits ahead of upstream
|
|
76
|
+
const count = await gitOrNull(["rev-list", "--count", `${upstream}..HEAD`], input.cwd, ctx);
|
|
77
|
+
if (count !== null) return { value: parseInt(count, 10) > 0 };
|
|
78
|
+
}
|
|
79
|
+
// No upstream or error - HEAD exists but no upstream: treat as having local commits
|
|
80
|
+
const head = await gitOrNull(["rev-parse", "HEAD"], input.cwd, ctx);
|
|
81
|
+
return { value: head !== null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the working directory is clean (no changes at all)
|
|
86
|
+
*/
|
|
87
|
+
export async function gitIsClean(input: GitPredicateInput, ctx: GitContext = {}): Promise<GitPredicateOutput> {
|
|
88
|
+
const result = await gitHasChanges(input, ctx);
|
|
89
|
+
return { value: !result.value };
|
|
90
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.pull procedure
|
|
3
|
+
*
|
|
4
|
+
* Pull from remote
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitPullInput, GitPullOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pull from remote
|
|
13
|
+
*/
|
|
14
|
+
export async function gitPull(input: GitPullInput, ctx: GitContext = {}): Promise<GitPullOutput> {
|
|
15
|
+
const { branch, rebase, cwd } = input;
|
|
16
|
+
const remoteName = input.remote ?? "origin";
|
|
17
|
+
const run = { cwd, signal: ctx.signal };
|
|
18
|
+
|
|
19
|
+
// Get current branch if not specified
|
|
20
|
+
const branchName = branch || (await git(["rev-parse", "--abbrev-ref", "HEAD"], run)).trim();
|
|
21
|
+
|
|
22
|
+
// Get current HEAD before pull
|
|
23
|
+
const beforeHead = (await git(["rev-parse", "HEAD"], run)).trim();
|
|
24
|
+
|
|
25
|
+
const args: string[] = ["pull"];
|
|
26
|
+
if (rebase) args.push("--rebase");
|
|
27
|
+
args.push(gitArg("remote", remoteName), gitArg("branch", branchName));
|
|
28
|
+
|
|
29
|
+
await git(args, run);
|
|
30
|
+
|
|
31
|
+
// Get new HEAD after pull
|
|
32
|
+
const afterHead = (await git(["rev-parse", "HEAD"], run)).trim();
|
|
33
|
+
|
|
34
|
+
// Count new commits
|
|
35
|
+
let commits = 0;
|
|
36
|
+
if (beforeHead !== afterHead) {
|
|
37
|
+
const count = (await git(["rev-list", "--count", `${beforeHead}..${afterHead}`], run)).trim();
|
|
38
|
+
commits = parseInt(count, 10) || 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if it was fast-forward
|
|
42
|
+
const fastForward = !rebase && beforeHead !== afterHead;
|
|
43
|
+
|
|
44
|
+
return { remote: remoteName, branch: branchName, commits, fastForward };
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.push procedure
|
|
3
|
+
*
|
|
4
|
+
* Push to remote
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { gitArg } from "./args.js";
|
|
8
|
+
import { git, GitError, type GitContext } from "./run.js";
|
|
9
|
+
import type { GitPushInput, GitPushOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Push to remote
|
|
13
|
+
*/
|
|
14
|
+
export async function gitPush(input: GitPushInput, ctx: GitContext = {}): Promise<GitPushOutput> {
|
|
15
|
+
const { branch, force, setUpstream, cwd } = input;
|
|
16
|
+
const remoteName = input.remote ?? "origin";
|
|
17
|
+
const run = { cwd, signal: ctx.signal };
|
|
18
|
+
|
|
19
|
+
// Get current branch if not specified
|
|
20
|
+
const branchName = branch || (await git(["rev-parse", "--abbrev-ref", "HEAD"], run)).trim();
|
|
21
|
+
|
|
22
|
+
// Count commits to push
|
|
23
|
+
let commits = 0;
|
|
24
|
+
try {
|
|
25
|
+
const count = (await git(["rev-list", "--count", `${remoteName}/${branchName}..HEAD`], run)).trim();
|
|
26
|
+
commits = parseInt(count, 10) || 0;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Remote branch may not exist yet
|
|
29
|
+
if (!(error instanceof GitError)) throw error;
|
|
30
|
+
const count = (await git(["rev-list", "--count", "HEAD"], run)).trim();
|
|
31
|
+
commits = parseInt(count, 10) || 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const args: string[] = ["push"];
|
|
35
|
+
if (setUpstream) args.push("-u");
|
|
36
|
+
if (force) args.push("--force");
|
|
37
|
+
args.push(gitArg("remote", remoteName), gitArg("branch", branchName));
|
|
38
|
+
|
|
39
|
+
await git(args, run);
|
|
40
|
+
|
|
41
|
+
return { remote: remoteName, branch: branchName, commits };
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.remote and git.remote.set
|
|
3
|
+
*
|
|
4
|
+
* git.remote reads the URL of a remote. git.remote.set replaces it, or adds the remote. Two
|
|
5
|
+
* procedures, so that a reader of the URL declares that it changes nothing (CAPABILITIES-2026-10).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { gitArg } from "./args.js";
|
|
9
|
+
import { git, GitError, type GitContext } from "./run.js";
|
|
10
|
+
import type { GitRemoteInput, GitRemoteOutput, GitRemoteSetInput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/** This function gives the URL of a remote. */
|
|
13
|
+
export async function gitRemote(input: GitRemoteInput, ctx: GitContext = {}): Promise<GitRemoteOutput> {
|
|
14
|
+
const { name, cwd } = input;
|
|
15
|
+
const url = (await git(["remote", "get-url", gitArg("name", name)], { cwd, signal: ctx.signal })).trim();
|
|
16
|
+
return { name, url };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** This function sets the URL of a remote, and adds the remote when it is missing. */
|
|
20
|
+
export async function gitRemoteSet(input: GitRemoteSetInput, ctx: GitContext = {}): Promise<GitRemoteOutput> {
|
|
21
|
+
const { name, url, cwd } = input;
|
|
22
|
+
const run = { cwd, signal: ctx.signal };
|
|
23
|
+
try {
|
|
24
|
+
await git(["remote", "set-url", gitArg("name", name), gitArg("url", url)], run);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// The remote is missing: add it
|
|
27
|
+
if (!(error instanceof GitError)) throw error;
|
|
28
|
+
await git(["remote", "add", gitArg("name", name), gitArg("url", url)], run);
|
|
29
|
+
}
|
|
30
|
+
return { name, url };
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the git procedures run git: through `runCommand` of client-shell (roadmap 2.2).
|
|
3
|
+
*
|
|
4
|
+
* Before, they called `execFileSync`: each git call blocked the event loop of the host (an MCP
|
|
5
|
+
* server or a warm `mark` server stopped all its other calls), and the signal of the call could
|
|
6
|
+
* not stop git. Now each call is async, the signal kills git, and the output has a limit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runCommand } from "@procedures/shell/command";
|
|
10
|
+
import { GIT_MAX_BUFFER } from "./args.js";
|
|
11
|
+
|
|
12
|
+
/** The part of the procedure context that the git procedures read. */
|
|
13
|
+
export interface GitContext {
|
|
14
|
+
signal?: AbortSignal | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GitRunOptions extends GitContext {
|
|
18
|
+
cwd?: string | undefined;
|
|
19
|
+
/** Text to write to the stdin of git (for example a patch for `git apply`). */
|
|
20
|
+
input?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A git command that ended with an exit code other than 0. */
|
|
24
|
+
export class GitError extends Error {
|
|
25
|
+
constructor(
|
|
26
|
+
readonly args: readonly string[],
|
|
27
|
+
readonly exitCode: number,
|
|
28
|
+
readonly stdout: string,
|
|
29
|
+
readonly stderr: string,
|
|
30
|
+
) {
|
|
31
|
+
super(`git ${args.join(" ")} failed (exit ${exitCode}): ${(stderr || stdout).trim()}`);
|
|
32
|
+
this.name = "GitError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* This function runs `git <args>` and gives its stdout. It throws a `GitError` when git fails,
|
|
38
|
+
* and an `AbortError` when the signal aborts.
|
|
39
|
+
*/
|
|
40
|
+
export async function git(args: string[], options: GitRunOptions = {}): Promise<string> {
|
|
41
|
+
const result = await runCommand("git", {
|
|
42
|
+
args,
|
|
43
|
+
cwd: options.cwd,
|
|
44
|
+
signal: options.signal,
|
|
45
|
+
input: options.input,
|
|
46
|
+
maxOutputBytes: GIT_MAX_BUFFER,
|
|
47
|
+
});
|
|
48
|
+
if (result.aborted) {
|
|
49
|
+
const error = new Error(`git ${args[0]} was aborted`);
|
|
50
|
+
error.name = "AbortError";
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (result.error) {
|
|
54
|
+
throw new Error(`git did not start: ${result.error}`);
|
|
55
|
+
}
|
|
56
|
+
if (result.truncated?.stdout) {
|
|
57
|
+
throw new Error(`The output of git ${args[0]} is longer than ${GIT_MAX_BUFFER} bytes`);
|
|
58
|
+
}
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
throw new GitError(args, result.exitCode, result.stdout, result.stderr);
|
|
61
|
+
}
|
|
62
|
+
return result.stdout;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** This function splits output that git wrote with `-z` into its fields. */
|
|
66
|
+
export function zFields(output: string): string[] {
|
|
67
|
+
const fields = output.split("\0");
|
|
68
|
+
if (fields.at(-1) === "") fields.pop();
|
|
69
|
+
return fields;
|
|
70
|
+
}
|