@samaksh/git-worker 0.1.0 → 0.2.1
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/dist/cli/index.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/services/git/reader.js +105 -34
- package/dist/services/workspace/manager.js +1 -1
- package/dist/stages/eligibility.js +38 -14
- package/dist/stages/select.js +46 -37
- package/package.json +11 -1
- package/GIT_WORKER_PROJECT_PLAN.md +0 -499
- package/demo/README.md +0 -58
- package/demo/index.ts +0 -526
- package/src/app.ts +0 -69
- package/src/cli/commands.ts +0 -142
- package/src/cli/index.ts +0 -123
- package/src/config/defaults.ts +0 -46
- package/src/config/load.ts +0 -70
- package/src/config/schema.ts +0 -48
- package/src/core/errors.ts +0 -45
- package/src/core/pipeline.ts +0 -17
- package/src/core/types.ts +0 -188
- package/src/services/editor/index.ts +0 -85
- package/src/services/git/ops.ts +0 -170
- package/src/services/git/reader.ts +0 -148
- package/src/services/report/index.ts +0 -104
- package/src/services/review/index.ts +0 -80
- package/src/services/state/index.ts +0 -77
- package/src/services/template/env.ts +0 -169
- package/src/services/template/interpolator.ts +0 -109
- package/src/services/template/transform.ts +0 -97
- package/src/services/template/validator.ts +0 -75
- package/src/services/workspace/cache.ts +0 -144
- package/src/services/workspace/manager.ts +0 -370
- package/src/stages/apply.ts +0 -194
- package/src/stages/edit-template.ts +0 -227
- package/src/stages/eligibility.ts +0 -67
- package/src/stages/load-template.ts +0 -188
- package/src/stages/plan.ts +0 -362
- package/src/stages/push.ts +0 -120
- package/src/stages/report.ts +0 -126
- package/src/stages/resolve-project.ts +0 -341
- package/src/stages/review.ts +0 -228
- package/src/stages/select.ts +0 -302
- package/src/stages/sync.ts +0 -63
- package/src/utils/hash.ts +0 -15
- package/src/utils/ui.ts +0 -137
- package/src/utils/which.ts +0 -18
- package/tsconfig.json +0 -18
package/dist/cli/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const program = new Command();
|
|
|
7
7
|
program
|
|
8
8
|
.name("git-worker")
|
|
9
9
|
.description("Safe, composable CLI/TUI for applying reviewed file changes across many Git branches")
|
|
10
|
-
.version("0.1
|
|
10
|
+
.version("0.2.1")
|
|
11
11
|
.option("-r, --repo <path>", "Repository path or remote URL (default: current directory)")
|
|
12
12
|
.option("-c, --cached", "Select from previously cloned and cached repositories")
|
|
13
13
|
.option("-b, --branch <pattern>", "Branch pattern — glob (uat/*) or regex (/uat.*/)")
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -23,56 +23,127 @@ export async function readFileFromBranch(repoPath, branch, filePath) {
|
|
|
23
23
|
export async function listBranches(repoPath, remote) {
|
|
24
24
|
try {
|
|
25
25
|
const git = simpleGit(repoPath);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
//
|
|
26
|
+
// Fast path: use git for-each-ref plumbing command.
|
|
27
|
+
// Retrieves all branch names, SHAs, and ref types in a single process execution (<25ms for 300+ branches)
|
|
28
|
+
// instead of running hundreds of sequential rev-parse child processes.
|
|
29
29
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const rawOutput = await git.raw([
|
|
31
|
+
"for-each-ref",
|
|
32
|
+
"--sort=-committerdate",
|
|
33
|
+
"--format=%(objectname)\t%(refname)",
|
|
34
|
+
"refs/remotes",
|
|
35
|
+
"refs/heads",
|
|
36
|
+
]);
|
|
37
|
+
const remoteBranches = [];
|
|
38
|
+
const localBranches = [];
|
|
39
|
+
for (const rawLine of rawOutput.split("\n")) {
|
|
40
|
+
const line = rawLine.trim();
|
|
41
|
+
if (!line)
|
|
33
42
|
continue;
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
43
|
+
const tabIdx = line.indexOf("\t");
|
|
44
|
+
if (tabIdx === -1)
|
|
36
45
|
continue;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
46
|
+
const sha = line.substring(0, tabIdx).trim();
|
|
47
|
+
const refname = line.substring(tabIdx + 1).trim();
|
|
48
|
+
if (!sha || !refname)
|
|
40
49
|
continue;
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
if (refname.startsWith("refs/remotes/")) {
|
|
51
|
+
const rest = refname.substring("refs/remotes/".length);
|
|
52
|
+
const slashIdx = rest.indexOf("/");
|
|
53
|
+
if (slashIdx === -1)
|
|
54
|
+
continue;
|
|
55
|
+
const remoteName = rest.substring(0, slashIdx);
|
|
56
|
+
const branchName = rest.substring(slashIdx + 1);
|
|
57
|
+
// Skip symbolic refs like origin/HEAD
|
|
58
|
+
if (branchName === "HEAD" || branchName.endsWith("/HEAD"))
|
|
59
|
+
continue;
|
|
60
|
+
if (remote && remoteName !== remote)
|
|
61
|
+
continue;
|
|
62
|
+
remoteBranches.push({
|
|
47
63
|
name: branchName,
|
|
48
64
|
remote: remoteName,
|
|
49
|
-
sha
|
|
65
|
+
sha,
|
|
50
66
|
isRemote: true,
|
|
51
67
|
});
|
|
52
68
|
}
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
else if (refname.startsWith("refs/heads/")) {
|
|
70
|
+
const branchName = refname.substring("refs/heads/".length);
|
|
71
|
+
localBranches.push({
|
|
72
|
+
name: branchName,
|
|
73
|
+
sha,
|
|
74
|
+
isRemote: false,
|
|
75
|
+
});
|
|
55
76
|
}
|
|
56
77
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (const name of local.all) {
|
|
65
|
-
if (!seen.has(name)) {
|
|
66
|
-
const sha = await git.revparse([name]);
|
|
67
|
-
seen.add(name);
|
|
68
|
-
branches.push({ name, sha: sha.trim(), isRemote: false });
|
|
78
|
+
const branches = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
// 1. Remote branches take priority (source of truth for managed clone)
|
|
81
|
+
for (const b of remoteBranches) {
|
|
82
|
+
if (!seen.has(b.name)) {
|
|
83
|
+
seen.add(b.name);
|
|
84
|
+
branches.push(b);
|
|
69
85
|
}
|
|
70
86
|
}
|
|
87
|
+
// 2. Local branches (only if not already discovered on remote)
|
|
88
|
+
for (const b of localBranches) {
|
|
89
|
+
if (!seen.has(b.name)) {
|
|
90
|
+
seen.add(b.name);
|
|
91
|
+
branches.push(b);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return ok(branches);
|
|
71
95
|
}
|
|
72
96
|
catch {
|
|
73
|
-
//
|
|
97
|
+
// Fallback path: use standard branch enumeration if for-each-ref fails
|
|
98
|
+
const branches = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
try {
|
|
101
|
+
const remoteBranches = await git.branch(["-r"]);
|
|
102
|
+
for (const fullName of remoteBranches.all) {
|
|
103
|
+
if (fullName.includes("HEAD"))
|
|
104
|
+
continue;
|
|
105
|
+
const parts = fullName.trim().split("/");
|
|
106
|
+
if (parts.length < 2)
|
|
107
|
+
continue;
|
|
108
|
+
const remoteName = parts[0];
|
|
109
|
+
const branchName = parts.slice(1).join("/");
|
|
110
|
+
if (remote && remoteName !== remote)
|
|
111
|
+
continue;
|
|
112
|
+
if (seen.has(branchName))
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
const sha = await git.revparse([fullName.trim()]);
|
|
116
|
+
seen.add(branchName);
|
|
117
|
+
branches.push({
|
|
118
|
+
name: branchName,
|
|
119
|
+
remote: remoteName,
|
|
120
|
+
sha: sha.trim(),
|
|
121
|
+
isRemote: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// skip unresolvable
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// no remotes
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const local = await git.branchLocal();
|
|
134
|
+
for (const name of local.all) {
|
|
135
|
+
if (!seen.has(name)) {
|
|
136
|
+
const sha = await git.revparse([name]);
|
|
137
|
+
seen.add(name);
|
|
138
|
+
branches.push({ name, sha: sha.trim(), isRemote: false });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// ignore
|
|
144
|
+
}
|
|
145
|
+
return ok(branches);
|
|
74
146
|
}
|
|
75
|
-
return ok(branches);
|
|
76
147
|
}
|
|
77
148
|
catch (e) {
|
|
78
149
|
return err(gitError("Failed to list branches", e));
|
|
@@ -6,7 +6,7 @@ import { join, resolve, normalize } from "path";
|
|
|
6
6
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { createHash } from "crypto";
|
|
9
|
-
const GIT_WORKER_VERSION = "0.1
|
|
9
|
+
const GIT_WORKER_VERSION = "0.2.1";
|
|
10
10
|
const LOCK_FILE = ".git-worker-lock";
|
|
11
11
|
/**
|
|
12
12
|
* Extract a human-readable repository name from a remote URL or file path.
|
|
@@ -9,24 +9,48 @@ export const checkEligibilityStage = async (context, deps) => {
|
|
|
9
9
|
const eligible = [];
|
|
10
10
|
const ineligible = [];
|
|
11
11
|
const isEnvTarget = context.target.path === ".env" || context.target.path.startsWith(".env.");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
const total = context.selectedBranches.length;
|
|
13
|
+
const isBatchMode = total > 8;
|
|
14
|
+
const s = clack.spinner();
|
|
15
|
+
if (isBatchMode) {
|
|
16
|
+
s.start(`Checking eligibility for ${total} branches…`);
|
|
17
|
+
}
|
|
18
|
+
// Concurrent execution pool (up to 16 workers in parallel)
|
|
19
|
+
const concurrency = Math.min(16, total);
|
|
20
|
+
let completed = 0;
|
|
21
|
+
let currentIndex = 0;
|
|
22
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
23
|
+
while (currentIndex < total) {
|
|
24
|
+
const idx = currentIndex++;
|
|
25
|
+
const branch = context.selectedBranches[idx];
|
|
26
|
+
// 1. Check if the target file exists in the branch
|
|
27
|
+
const targetRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, context.target.path);
|
|
28
|
+
const targetFound = targetRead.ok;
|
|
29
|
+
// 2. If target is a non-.env file, check if .env is available as variable source
|
|
30
|
+
let envNote = "";
|
|
31
|
+
if (!isEnvTarget) {
|
|
32
|
+
const envRead = await deps.services.git.readFileFromBranch(context.project.managedRepoPath, branch, ".env");
|
|
33
|
+
if (envRead.ok) {
|
|
34
|
+
envNote = " • .env present";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
envNote = " • .env missing (variables will require fallbacks)";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const fileStatus = targetFound ? "target file found" : "target file will be created";
|
|
41
|
+
eligible.push(branch);
|
|
42
|
+
completed++;
|
|
43
|
+
if (isBatchMode) {
|
|
44
|
+
s.message(`Checking eligibility (${completed}/${total})…`);
|
|
22
45
|
}
|
|
23
46
|
else {
|
|
24
|
-
|
|
47
|
+
statusLine(targetFound ? "ok" : "info", branch.name, `${branch.sha.slice(0, 8)} — ${fileStatus}${envNote}`);
|
|
25
48
|
}
|
|
26
49
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
});
|
|
51
|
+
await Promise.all(workers);
|
|
52
|
+
if (isBatchMode) {
|
|
53
|
+
s.stop(`Checked ${total} branches (${eligible.length} eligible)`);
|
|
30
54
|
}
|
|
31
55
|
if (ineligible.length > 0) {
|
|
32
56
|
console.log("");
|
package/dist/stages/select.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ok, err } from "../core/types.js";
|
|
2
2
|
import { workspaceError, inputCancelledError } from "../core/errors.js";
|
|
3
|
-
import { clack, sectionHeader, statusLine
|
|
3
|
+
import { clack, sectionHeader, statusLine } from "../utils/ui.js";
|
|
4
|
+
import inquirer from "inquirer";
|
|
4
5
|
import chalk from "chalk";
|
|
5
6
|
/**
|
|
6
7
|
* Matches a branch name against a pattern.
|
|
@@ -100,44 +101,43 @@ export const selectTargetsStage = async (context, deps) => {
|
|
|
100
101
|
hint: "proceed to target file selection",
|
|
101
102
|
});
|
|
102
103
|
options.push({
|
|
103
|
-
value: "
|
|
104
|
-
label:
|
|
105
|
-
hint: "
|
|
104
|
+
value: "list",
|
|
105
|
+
label: "📋 Select from branch list",
|
|
106
|
+
hint: "Space: toggle • a: select all • i: invert",
|
|
106
107
|
});
|
|
107
108
|
options.push({
|
|
108
|
-
value: "
|
|
109
|
-
label:
|
|
110
|
-
hint: "glob (uat/*) or regex (/uat.*/)",
|
|
109
|
+
value: "all",
|
|
110
|
+
label: `🌟 Select all (a) — all ${totalCount} branches`,
|
|
111
111
|
});
|
|
112
112
|
options.push({
|
|
113
|
-
value: "
|
|
114
|
-
label:
|
|
115
|
-
hint: "toggle individual checkboxes",
|
|
113
|
+
value: "invert",
|
|
114
|
+
label: `🔄 Invert selection (i) — select other ${totalCount - selectedCount} branch(es)`,
|
|
116
115
|
});
|
|
117
116
|
options.push({
|
|
118
|
-
value: "
|
|
119
|
-
label:
|
|
117
|
+
value: "pattern",
|
|
118
|
+
label: "🔍 Filter by pattern or regex",
|
|
119
|
+
hint: "glob (uat/*) or regex (/uat.*/)",
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
else {
|
|
123
123
|
options.push({
|
|
124
|
-
value: "
|
|
125
|
-
label: "
|
|
126
|
-
hint: "
|
|
124
|
+
value: "list",
|
|
125
|
+
label: "📋 Select from branch list",
|
|
126
|
+
hint: "Space: toggle • a: select all • i: invert",
|
|
127
127
|
});
|
|
128
128
|
options.push({
|
|
129
129
|
value: "all",
|
|
130
|
-
label: `🌟 Select
|
|
130
|
+
label: `🌟 Select all (a) — all ${totalCount} branches`,
|
|
131
131
|
hint: "select every branch in repository",
|
|
132
132
|
});
|
|
133
133
|
options.push({
|
|
134
|
-
value: "
|
|
135
|
-
label:
|
|
136
|
-
hint: "select branches with checkboxes",
|
|
134
|
+
value: "invert",
|
|
135
|
+
label: `🔄 Invert selection (i) — select all ${totalCount} branches`,
|
|
137
136
|
});
|
|
138
137
|
options.push({
|
|
139
|
-
value: "
|
|
140
|
-
label:
|
|
138
|
+
value: "pattern",
|
|
139
|
+
label: "🔍 Filter by pattern or regex",
|
|
140
|
+
hint: "glob (uat/*) or regex (/uat.*/)",
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
const previewText = selectedCount > 0
|
|
@@ -199,24 +199,33 @@ export const selectTargetsStage = async (context, deps) => {
|
|
|
199
199
|
continue;
|
|
200
200
|
}
|
|
201
201
|
if (choice === "list") {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
try {
|
|
203
|
+
const { picked } = await inquirer.prompt([
|
|
204
|
+
{
|
|
205
|
+
type: "checkbox",
|
|
206
|
+
name: "picked",
|
|
207
|
+
message: "Select branches to process:",
|
|
208
|
+
pageSize: 15,
|
|
209
|
+
choices: allBranches.map((b) => ({
|
|
210
|
+
name: `${b.name} (${b.isRemote ? `remote/${b.remote}` : "local"} ${b.sha.slice(0, 8)})`,
|
|
211
|
+
value: b.name,
|
|
212
|
+
checked: selectedNames.has(b.name),
|
|
213
|
+
})),
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
selectedNames = new Set(picked);
|
|
217
|
+
statusLine("ok", `Selection updated: ${selectedNames.size} branch(es) selected`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
if (e.isTtyError ||
|
|
222
|
+
e.name === "ExitPromptError" ||
|
|
223
|
+
e.message?.includes("force closed") ||
|
|
224
|
+
e.message?.includes("User force closed")) {
|
|
225
|
+
return err(inputCancelledError("Branch selection cancelled"));
|
|
226
|
+
}
|
|
215
227
|
continue;
|
|
216
228
|
}
|
|
217
|
-
selectedNames = new Set(picked);
|
|
218
|
-
statusLine("ok", `Selection updated: ${selectedNames.size} branch(es) selected`);
|
|
219
|
-
continue;
|
|
220
229
|
}
|
|
221
230
|
}
|
|
222
231
|
branches = allBranches.filter((b) => selectedNames.has(b.name));
|
package/package.json
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samaksh/git-worker",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Safe, composable TypeScript CLI/TUI for applying reviewed file changes across many Git branches",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"bin": {
|
|
8
9
|
"git-worker": "dist/cli/index.js",
|
|
9
10
|
"gitworker": "dist/cli/index.js"
|
|
10
11
|
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs"
|
|
19
|
+
],
|
|
11
20
|
"scripts": {
|
|
12
21
|
"build": "tsc",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
13
23
|
"dev": "tsx src/cli/index.ts",
|
|
14
24
|
"demo": "tsx demo/index.ts",
|
|
15
25
|
"demo:ci": "tsx demo/index.ts --ci",
|