pan-stack-cli 0.1.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/README.md +143 -0
- package/bin/pan.js +140 -0
- package/lib/discover.js +206 -0
- package/lib/git/git-clone.js +70 -0
- package/lib/git/git-fetch-all.js +62 -0
- package/lib/git/git-grv.js +98 -0
- package/lib/git/git-gst.js +98 -0
- package/lib/git/git-pull.js +76 -0
- package/lib/git/git-push.js +76 -0
- package/lib/git/helpers/cli.js +103 -0
- package/lib/git/helpers/git.js +61 -0
- package/lib/git/helpers/manifest.js +130 -0
- package/lib/manifest.js +312 -0
- package/lib/run-command.js +196 -0
- package/package.json +27 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Git status for PAN Stack project repositories.
|
|
4
|
+
*
|
|
5
|
+
* Alias tương đương gst trong oh-my-zsh — chạy git status trên workspace + repo con.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
|
|
10
|
+
const { ensureRepoExists, isGitRepo, runGitStatus } = require("./helpers/git");
|
|
11
|
+
|
|
12
|
+
function printRepoStatus({ label, destination, dryRun, gitArgs }) {
|
|
13
|
+
const statusArgs = gitArgs.length > 0 ? gitArgs.join(" ") : "";
|
|
14
|
+
if (dryRun) {
|
|
15
|
+
console.log(`[dry-run] (in ${destination}) git status ${statusArgs}`.trim());
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`== ${label} ==`);
|
|
20
|
+
console.log(destination);
|
|
21
|
+
runGitStatus(destination, gitArgs);
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function showWorkspaceRootStatus({ manifestPath, baseDir, dryRun, gitArgs }) {
|
|
26
|
+
const workspaceRoot = path.resolve(baseDir || path.dirname(manifestPath || ".project.yaml"));
|
|
27
|
+
if (!isGitRepo(workspaceRoot)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
printRepoStatus({
|
|
32
|
+
label: "workspace",
|
|
33
|
+
destination: workspaceRoot,
|
|
34
|
+
dryRun,
|
|
35
|
+
gitArgs,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printHelp() {
|
|
40
|
+
console.log(`usage: git-gst.js [--base-dir BASE_DIR] [--dry-run] [--no-workspace] [manifest] [git status args...]
|
|
41
|
+
|
|
42
|
+
Run git status for workspace root and each repository in the manifest.
|
|
43
|
+
Extra args are forwarded to git status (vd: -s, -sb).
|
|
44
|
+
|
|
45
|
+
${printCommonOptions()}
|
|
46
|
+
--no-workspace Skip workspace root (chỉ repo trong manifest)`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
const args = parseCommonArgs(process.argv.slice(2), {
|
|
51
|
+
defaults: { noWorkspace: false, gitArgs: [] },
|
|
52
|
+
passthrough: true,
|
|
53
|
+
handlers: {
|
|
54
|
+
"--no-workspace": (parsed) => {
|
|
55
|
+
parsed.noWorkspace = true;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (args.help) {
|
|
60
|
+
printHelp();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const gitArgs = args.gitArgs || [];
|
|
65
|
+
const manifestPath = path.resolve(args.manifest || ".project.yaml");
|
|
66
|
+
const repos = loadManifestRepos({
|
|
67
|
+
manifestPath,
|
|
68
|
+
baseDir: args.baseDir,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!args.noWorkspace) {
|
|
72
|
+
showWorkspaceRootStatus({
|
|
73
|
+
manifestPath,
|
|
74
|
+
baseDir: args.baseDir,
|
|
75
|
+
dryRun: args.dryRun,
|
|
76
|
+
gitArgs,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const repo of repos) {
|
|
81
|
+
if (!ensureRepoExists({ name: repo.name, destination: repo.destination })) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
printRepoStatus({
|
|
85
|
+
label: repo.name,
|
|
86
|
+
destination: repo.destination,
|
|
87
|
+
dryRun: args.dryRun,
|
|
88
|
+
gitArgs,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
main();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(error.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Pull latest changes for PAN Stack project repositories from a clone manifest.
|
|
4
|
+
*
|
|
5
|
+
* Skips repositories that do not exist or are not git checkouts. Failures warn
|
|
6
|
+
* and continue so one broken repo does not block the rest.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
|
|
10
|
+
const { ensureRepoExists, runGit } = require("./helpers/git");
|
|
11
|
+
|
|
12
|
+
function pullRepo({ name, destination, dryRun, rebase }) {
|
|
13
|
+
if (!ensureRepoExists({ name, destination })) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const gitArgs = ["pull"];
|
|
18
|
+
if (rebase) {
|
|
19
|
+
gitArgs.push("--rebase");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const status = runGit(gitArgs, {
|
|
23
|
+
cwd: destination,
|
|
24
|
+
dryRun,
|
|
25
|
+
action: "pull",
|
|
26
|
+
name,
|
|
27
|
+
});
|
|
28
|
+
if (status !== 0) {
|
|
29
|
+
console.log(`[warn] ${name}: pull failed, check branch state or remote access`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(`usage: git-pull.js [--base-dir BASE_DIR] [--dry-run] [--rebase] [manifest]
|
|
35
|
+
|
|
36
|
+
Pull latest changes for repositories listed in a PAN Stack clone manifest.
|
|
37
|
+
|
|
38
|
+
${printCommonOptions()}
|
|
39
|
+
--rebase Use git pull --rebase`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function main() {
|
|
43
|
+
const args = parseCommonArgs(process.argv.slice(2), {
|
|
44
|
+
defaults: { rebase: false },
|
|
45
|
+
handlers: {
|
|
46
|
+
"--rebase": (parsed) => {
|
|
47
|
+
parsed.rebase = true;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (args.help) {
|
|
52
|
+
printHelp();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const repos = loadManifestRepos({
|
|
57
|
+
manifestPath: args.manifest,
|
|
58
|
+
baseDir: args.baseDir,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const repo of repos) {
|
|
62
|
+
pullRepo({
|
|
63
|
+
name: repo.name,
|
|
64
|
+
destination: repo.destination,
|
|
65
|
+
dryRun: args.dryRun,
|
|
66
|
+
rebase: args.rebase,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
main();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Push local commits for PAN Stack project repositories from a clone manifest.
|
|
4
|
+
*
|
|
5
|
+
* Skips repositories that do not exist or are not git checkouts. Failures warn
|
|
6
|
+
* and continue so one broken repo does not block the rest.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { parseCommonArgs, loadManifestRepos, printCommonOptions } = require("./helpers/cli");
|
|
10
|
+
const { ensureRepoExists, runGit } = require("./helpers/git");
|
|
11
|
+
|
|
12
|
+
function pushRepo({ name, destination, dryRun, setUpstream }) {
|
|
13
|
+
if (!ensureRepoExists({ name, destination })) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const gitArgs = ["push"];
|
|
18
|
+
if (setUpstream) {
|
|
19
|
+
gitArgs.push("-u", "origin", "HEAD");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const status = runGit(gitArgs, {
|
|
23
|
+
cwd: destination,
|
|
24
|
+
dryRun,
|
|
25
|
+
action: "push",
|
|
26
|
+
name,
|
|
27
|
+
});
|
|
28
|
+
if (status !== 0) {
|
|
29
|
+
console.log(`[warn] ${name}: push failed, check branch state or remote access`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(`usage: git-push.js [--base-dir BASE_DIR] [--dry-run] [--set-upstream] [manifest]
|
|
35
|
+
|
|
36
|
+
Push local commits for repositories listed in a PAN Stack clone manifest.
|
|
37
|
+
|
|
38
|
+
${printCommonOptions()}
|
|
39
|
+
--set-upstream Use git push -u origin HEAD for new branches`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function main() {
|
|
43
|
+
const args = parseCommonArgs(process.argv.slice(2), {
|
|
44
|
+
defaults: { setUpstream: false },
|
|
45
|
+
handlers: {
|
|
46
|
+
"--set-upstream": (parsed) => {
|
|
47
|
+
parsed.setUpstream = true;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (args.help) {
|
|
52
|
+
printHelp();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const repos = loadManifestRepos({
|
|
57
|
+
manifestPath: args.manifest,
|
|
58
|
+
baseDir: args.baseDir,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const repo of repos) {
|
|
62
|
+
pushRepo({
|
|
63
|
+
name: repo.name,
|
|
64
|
+
destination: repo.destination,
|
|
65
|
+
dryRun: args.dryRun,
|
|
66
|
+
setUpstream: args.setUpstream,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
main();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const { parseManifest, manifestRepos, validateRepo } = require("./manifest");
|
|
4
|
+
|
|
5
|
+
function parseCommonArgs(argv, extraFlags = {}) {
|
|
6
|
+
const args = {
|
|
7
|
+
manifest: "",
|
|
8
|
+
baseDir: ".",
|
|
9
|
+
dryRun: false,
|
|
10
|
+
...extraFlags.defaults,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const arg = argv[index];
|
|
15
|
+
if (arg === "-h" || arg === "--help") {
|
|
16
|
+
args.help = true;
|
|
17
|
+
} else if (arg === "--base-dir") {
|
|
18
|
+
args.baseDir = argv[++index] || ".";
|
|
19
|
+
} else if (arg === "--dry-run") {
|
|
20
|
+
args.dryRun = true;
|
|
21
|
+
} else if (extraFlags.handlers && extraFlags.handlers[arg]) {
|
|
22
|
+
extraFlags.handlers[arg](args, argv, () => {
|
|
23
|
+
index += 1;
|
|
24
|
+
return argv[index];
|
|
25
|
+
});
|
|
26
|
+
} else if (extraFlags.passthrough) {
|
|
27
|
+
if (!args.manifest && looksLikeManifest(arg)) {
|
|
28
|
+
args.manifest = arg;
|
|
29
|
+
} else {
|
|
30
|
+
args.gitArgs = args.gitArgs || [];
|
|
31
|
+
args.gitArgs.push(arg);
|
|
32
|
+
}
|
|
33
|
+
} else if (!args.manifest) {
|
|
34
|
+
args.manifest = arg;
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function looksLikeManifest(arg) {
|
|
44
|
+
if (arg.endsWith(".yaml") || arg.endsWith(".yml")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return fs.existsSync(arg) && fs.statSync(arg).isFile();
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadManifestRepos({ manifestPath, baseDir }) {
|
|
55
|
+
const resolvedManifest = path.resolve(manifestPath || ".project.yaml");
|
|
56
|
+
if (!fs.existsSync(resolvedManifest)) {
|
|
57
|
+
throw new Error(`Manifest not found: ${manifestPath || ".project.yaml"}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = parseManifest(resolvedManifest);
|
|
61
|
+
const repos = manifestRepos(data);
|
|
62
|
+
if (repos.length === 0) {
|
|
63
|
+
throw new Error("No repositories found in manifest.");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolvedBaseDir = path.resolve(baseDir);
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const repo of repos) {
|
|
69
|
+
try {
|
|
70
|
+
const { name, url, repoPath } = validateRepo(repo);
|
|
71
|
+
results.push({
|
|
72
|
+
name,
|
|
73
|
+
url,
|
|
74
|
+
repoPath,
|
|
75
|
+
destination: path.resolve(resolvedBaseDir, repoPath),
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(`[warn] ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (results.length === 0) {
|
|
83
|
+
throw new Error("No valid repositories found in manifest.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printCommonOptions() {
|
|
90
|
+
return `positional arguments:
|
|
91
|
+
manifest Path to clone manifest YAML. Default: .project.yaml
|
|
92
|
+
|
|
93
|
+
options:
|
|
94
|
+
-h, --help show this help message and exit
|
|
95
|
+
--base-dir BASE_DIR Base directory for relative repo paths. Default: current directory.
|
|
96
|
+
--dry-run Print commands only`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
parseCommonArgs,
|
|
101
|
+
loadManifestRepos,
|
|
102
|
+
printCommonOptions,
|
|
103
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function isGitRepo(directory) {
|
|
6
|
+
return fs.existsSync(path.join(directory, ".git"));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function runGit(gitArgs, { cwd, dryRun, action, name }) {
|
|
10
|
+
const command = ["git", ...gitArgs];
|
|
11
|
+
if (dryRun) {
|
|
12
|
+
console.log(`[dry-run] (in ${cwd}) ${command.join(" ")}`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log(`[${action}] ${name}: ${cwd}`);
|
|
17
|
+
const result = spawnSync(command[0], command.slice(1), { stdio: "inherit", cwd });
|
|
18
|
+
return result.status ?? 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureRepoExists({ name, destination }) {
|
|
22
|
+
if (!fs.existsSync(destination)) {
|
|
23
|
+
console.log(`[skip] ${name}: ${destination} does not exist`);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (!isGitRepo(destination)) {
|
|
27
|
+
console.log(`[skip] ${name}: ${destination} is not a git repository`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listRemotesVerbose(directory) {
|
|
34
|
+
const result = spawnSync("git", ["remote", "-v"], {
|
|
35
|
+
cwd: directory,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return result.stdout
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map((line) => line.trimEnd())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runGitStatus(directory, gitArgs = []) {
|
|
48
|
+
const result = spawnSync("git", ["status", ...gitArgs], {
|
|
49
|
+
cwd: directory,
|
|
50
|
+
stdio: "inherit",
|
|
51
|
+
});
|
|
52
|
+
return result.status ?? 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
isGitRepo,
|
|
57
|
+
runGit,
|
|
58
|
+
ensureRepoExists,
|
|
59
|
+
listRemotesVerbose,
|
|
60
|
+
runGitStatus,
|
|
61
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
|
|
3
|
+
const PLACEHOLDER_VALUES = new Set(["", "repo_url_to_be_filled", "pan_stack_repo_url"]);
|
|
4
|
+
|
|
5
|
+
function cleanValue(value) {
|
|
6
|
+
let result = String(value || "").trim();
|
|
7
|
+
const commentIndex = result.indexOf(" #");
|
|
8
|
+
if (commentIndex >= 0) {
|
|
9
|
+
result = result.slice(0, commentIndex).trimEnd();
|
|
10
|
+
}
|
|
11
|
+
if (
|
|
12
|
+
(result.startsWith('"') && result.endsWith('"')) ||
|
|
13
|
+
(result.startsWith("'") && result.endsWith("'"))
|
|
14
|
+
) {
|
|
15
|
+
result = result.slice(1, -1);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function splitKeyValue(text) {
|
|
21
|
+
const index = text.indexOf(":");
|
|
22
|
+
if (index < 0) {
|
|
23
|
+
return [text.trim(), ""];
|
|
24
|
+
}
|
|
25
|
+
return [text.slice(0, index).trim(), cleanValue(text.slice(index + 1))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseManifest(manifestPath) {
|
|
29
|
+
const data = { repositories: [], stack: {} };
|
|
30
|
+
let section = "";
|
|
31
|
+
let currentRepo = null;
|
|
32
|
+
|
|
33
|
+
const lines = fs.readFileSync(manifestPath, "utf8").split(/\r?\n/);
|
|
34
|
+
for (const rawLine of lines) {
|
|
35
|
+
if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const indent = rawLine.length - rawLine.trimStart().length;
|
|
40
|
+
const text = rawLine.trim();
|
|
41
|
+
|
|
42
|
+
if (indent === 0) {
|
|
43
|
+
const [key, value] = splitKeyValue(text);
|
|
44
|
+
section = key;
|
|
45
|
+
currentRepo = null;
|
|
46
|
+
if (value) {
|
|
47
|
+
data[key] = value;
|
|
48
|
+
} else if (!(key in data)) {
|
|
49
|
+
data[key] = {};
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (section === "stack" && indent === 2) {
|
|
55
|
+
const [key, value] = splitKeyValue(text);
|
|
56
|
+
data.stack[key] = value;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (section === "repositories") {
|
|
61
|
+
if (indent === 2 && text.startsWith("- ")) {
|
|
62
|
+
currentRepo = {};
|
|
63
|
+
data.repositories.push(currentRepo);
|
|
64
|
+
const rest = text.slice(2).trim();
|
|
65
|
+
if (rest) {
|
|
66
|
+
const [key, value] = splitKeyValue(rest);
|
|
67
|
+
currentRepo[key] = value;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (currentRepo && indent === 4) {
|
|
72
|
+
const [key, value] = splitKeyValue(text);
|
|
73
|
+
currentRepo[key] = value;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uniqueRepos(repos) {
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
const result = [];
|
|
85
|
+
for (const repo of repos) {
|
|
86
|
+
const key = [
|
|
87
|
+
cleanValue(repo.name),
|
|
88
|
+
cleanValue(repo.url),
|
|
89
|
+
cleanValue(repo.path),
|
|
90
|
+
].join("\u0000");
|
|
91
|
+
if (seen.has(key)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(key);
|
|
95
|
+
result.push(repo);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function manifestRepos(data) {
|
|
101
|
+
const repositories = data.repositories || [];
|
|
102
|
+
return uniqueRepos(repositories);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateRepo(repo) {
|
|
106
|
+
const name = cleanValue(repo.name);
|
|
107
|
+
const url = cleanValue(repo.url);
|
|
108
|
+
const repoPath = cleanValue(repo.path) || name;
|
|
109
|
+
|
|
110
|
+
if (!name) {
|
|
111
|
+
throw new Error("Repository entry is missing 'name'.");
|
|
112
|
+
}
|
|
113
|
+
if (PLACEHOLDER_VALUES.has(url)) {
|
|
114
|
+
throw new Error(`Repository '${name}' is missing a real url.`);
|
|
115
|
+
}
|
|
116
|
+
if (!repoPath) {
|
|
117
|
+
throw new Error(`Repository '${name}' is missing 'path'.`);
|
|
118
|
+
}
|
|
119
|
+
return { name, url, repoPath };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
PLACEHOLDER_VALUES,
|
|
124
|
+
cleanValue,
|
|
125
|
+
splitKeyValue,
|
|
126
|
+
parseManifest,
|
|
127
|
+
uniqueRepos,
|
|
128
|
+
manifestRepos,
|
|
129
|
+
validateRepo,
|
|
130
|
+
};
|