new-branch 0.6.0 → 0.8.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 +27 -211
- package/dist/cli.js +151 -7
- package/dist/config/loadConfig.js +28 -14
- package/dist/config/sources/git.loader.js +46 -2
- package/dist/config/sources/packageJson.loader.js +13 -0
- package/dist/config/sources/rc.loader.js +15 -0
- package/dist/config/types.js +6 -4
- package/dist/config/validate.js +76 -8
- package/dist/didactic/explain.js +94 -0
- package/dist/didactic/listTransforms.js +25 -0
- package/dist/didactic/printConfig.js +28 -0
- package/dist/git/createBranch.js +10 -3
- package/dist/git/gitBuiltins.js +73 -6
- package/dist/git/gitConfig.js +52 -0
- package/dist/git/sanitizeGitRef.js +23 -5
- package/dist/git/truncateEnd.js +31 -0
- package/dist/git/validateBranchName.js +12 -3
- package/dist/parseArgs.js +42 -0
- package/dist/pattern/parsePattern.js +14 -0
- package/dist/pattern/transforms/after.js +27 -0
- package/dist/pattern/transforms/before.js +27 -0
- package/dist/pattern/transforms/camel.js +13 -7
- package/dist/pattern/transforms/helpers/words.js +6 -0
- package/dist/pattern/transforms/ifEmpty.js +27 -0
- package/dist/pattern/transforms/index.js +24 -0
- package/dist/pattern/transforms/kebab.js +10 -4
- package/dist/pattern/transforms/lower.js +13 -0
- package/dist/pattern/transforms/max.js +15 -0
- package/dist/pattern/transforms/registry.js +13 -0
- package/dist/pattern/transforms/remove.js +25 -0
- package/dist/pattern/transforms/renderPattern.js +9 -0
- package/dist/pattern/transforms/replace.js +25 -0
- package/dist/pattern/transforms/replaceAll.js +25 -0
- package/dist/pattern/transforms/slugify.js +18 -0
- package/dist/pattern/transforms/snake.js +10 -4
- package/dist/pattern/transforms/stripAccents.js +24 -0
- package/dist/pattern/transforms/title.js +11 -4
- package/dist/pattern/transforms/types.js +5 -0
- package/dist/pattern/transforms/upper.js +13 -0
- package/dist/pattern/transforms/words.js +11 -7
- package/dist/pattern/types.js +5 -0
- package/dist/runtime/builtins.js +5 -0
- package/dist/runtime/enums.js +14 -0
- package/package.json +6 -2
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateProjectConfigSource, validateProjectConfigFinal } from "../validate.js";
|
|
4
|
+
/** Default filename for the RC configuration file. */
|
|
4
5
|
export const RC_FILENAME = ".newbranchrc.json";
|
|
6
|
+
/**
|
|
7
|
+
* Type guard for Node.js filesystem errors with a `code` property.
|
|
8
|
+
*
|
|
9
|
+
* @param e - The caught error value.
|
|
10
|
+
* @returns `true` if `e` has a `code` property.
|
|
11
|
+
*/
|
|
5
12
|
function isNodeFsError(e) {
|
|
6
13
|
return typeof e === "object" && e !== null && "code" in e;
|
|
7
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Config loader that reads from a `.newbranchrc.json` file in the
|
|
17
|
+
* current working directory.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Returns `found: false` when the RC file does not exist.
|
|
21
|
+
* Throws for any other filesystem or JSON parse error.
|
|
22
|
+
*/
|
|
8
23
|
export const rcLoader = {
|
|
9
24
|
source: "rc",
|
|
10
25
|
async load() {
|
package/dist/config/types.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
3
|
-
* Core types for `new-branch` configuration system.
|
|
2
|
+
* @module config/types
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Core types for the `new-branch` configuration system.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Naming consistency: always use `new-branch`
|
|
8
|
+
* (never `newBranch` or `newbranch`).
|
|
7
9
|
*/
|
|
8
10
|
export {};
|
package/dist/config/validate.js
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
3
|
-
* Validation + normalization logic for `new-branch` configuration.
|
|
2
|
+
* @module config/validate
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Validation and normalisation logic for `new-branch` configuration.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The validation pipeline consists of two stages:
|
|
8
|
+
* 1. {@link validateProjectConfigSource} — structural validation per source.
|
|
9
|
+
* 2. {@link validateProjectConfigFinal} — cross-field business rules.
|
|
8
10
|
*/
|
|
9
11
|
/**
|
|
10
|
-
* Throws a
|
|
12
|
+
* Throws a standardised configuration error when `condition` is falsy.
|
|
13
|
+
*
|
|
14
|
+
* @param condition - The value to assert.
|
|
15
|
+
* @param source - Label identifying the config source (for error messages).
|
|
16
|
+
* @param message - Human-readable description of the violated rule.
|
|
11
17
|
*/
|
|
12
18
|
function invariant(condition, source, message) {
|
|
13
19
|
if (!condition) {
|
|
14
20
|
throw new Error(`Invalid new-branch config from ${source}: ${message}`);
|
|
15
21
|
}
|
|
16
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether `v` is a non-null object.
|
|
25
|
+
*
|
|
26
|
+
* @param v - The value to check.
|
|
27
|
+
* @returns `true` if `v` is an object and not `null`.
|
|
28
|
+
*/
|
|
17
29
|
function isObject(v) {
|
|
18
30
|
return typeof v === "object" && v !== null;
|
|
19
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Type guard for strings.
|
|
34
|
+
*
|
|
35
|
+
* @param v - The value to check.
|
|
36
|
+
* @returns `true` if `v` is a `string`.
|
|
37
|
+
*/
|
|
20
38
|
function isString(v) {
|
|
21
39
|
return typeof v === "string";
|
|
22
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Trims a string and returns `undefined` when the result is empty.
|
|
43
|
+
*
|
|
44
|
+
* @param v - The string to trim.
|
|
45
|
+
* @returns The trimmed string, or `undefined` if blank.
|
|
46
|
+
*/
|
|
23
47
|
function trimOrUndefined(v) {
|
|
24
48
|
const t = v.trim();
|
|
25
49
|
return t.length > 0 ? t : undefined;
|
|
26
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalises a raw branch-type entry into a validated {@link BranchType}.
|
|
53
|
+
*
|
|
54
|
+
* @param raw - The unknown value to normalise.
|
|
55
|
+
* @param source - Config source label (for error messages).
|
|
56
|
+
* @returns A valid {@link BranchType}.
|
|
57
|
+
* @throws {@link Error} If `value` or `label` is missing/empty.
|
|
58
|
+
*/
|
|
27
59
|
function normalizeBranchType(raw, source) {
|
|
28
60
|
invariant(isObject(raw), source, "types[] must be an object");
|
|
29
61
|
const obj = raw;
|
|
@@ -34,7 +66,17 @@ function normalizeBranchType(raw, source) {
|
|
|
34
66
|
return { value, label };
|
|
35
67
|
}
|
|
36
68
|
/**
|
|
37
|
-
*
|
|
69
|
+
* Performs structural validation for a single configuration source.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* Validates shapes but does **not** check cross-field rules
|
|
73
|
+
* (e.g. `defaultType` existing in `types`). Use
|
|
74
|
+
* {@link validateProjectConfigFinal} for that.
|
|
75
|
+
*
|
|
76
|
+
* @param raw - The raw config object to validate.
|
|
77
|
+
* @param source - Config source label (for error messages).
|
|
78
|
+
* @returns A validated {@link ProjectConfig}.
|
|
79
|
+
* @throws {@link Error} On any structural violation.
|
|
38
80
|
*/
|
|
39
81
|
export function validateProjectConfigSource(raw, source) {
|
|
40
82
|
invariant(isObject(raw), source, "config must be an object");
|
|
@@ -48,6 +90,21 @@ export function validateProjectConfigSource(raw, source) {
|
|
|
48
90
|
invariant(isString(obj.defaultType), source, "defaultType must be a string");
|
|
49
91
|
cfg.defaultType = trimOrUndefined(obj.defaultType);
|
|
50
92
|
}
|
|
93
|
+
if ("patterns" in obj) {
|
|
94
|
+
const patternsVal = obj.patterns;
|
|
95
|
+
invariant(isObject(patternsVal), source, "patterns must be an object");
|
|
96
|
+
const entries = Object.entries(patternsVal);
|
|
97
|
+
const normalized = {};
|
|
98
|
+
for (const [key, val] of entries) {
|
|
99
|
+
invariant(isString(val), source, `patterns["${key}"] must be a string`);
|
|
100
|
+
const trimmed = trimOrUndefined(val);
|
|
101
|
+
invariant(trimmed, source, `patterns["${key}"] cannot be empty`);
|
|
102
|
+
normalized[key] = trimmed;
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(normalized).length > 0) {
|
|
105
|
+
cfg.patterns = normalized;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
51
108
|
if ("types" in obj) {
|
|
52
109
|
const typesVal = obj.types;
|
|
53
110
|
invariant(Array.isArray(typesVal), source, "types must be an array");
|
|
@@ -56,7 +113,18 @@ export function validateProjectConfigSource(raw, source) {
|
|
|
56
113
|
return cfg;
|
|
57
114
|
}
|
|
58
115
|
/**
|
|
59
|
-
*
|
|
116
|
+
* Performs cross-field (semantic) validation on an already-structurally-valid config.
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* Rules enforced:
|
|
120
|
+
* - `types`, when present, must not be empty.
|
|
121
|
+
* - `defaultType`, when present alongside `types`, must match one of
|
|
122
|
+
* the declared type values.
|
|
123
|
+
*
|
|
124
|
+
* @param cfg - The structurally-valid config to check.
|
|
125
|
+
* @param source - Config source label (for error messages).
|
|
126
|
+
* @returns The same config if all rules pass.
|
|
127
|
+
* @throws {@link Error} On any semantic violation.
|
|
60
128
|
*/
|
|
61
129
|
export function validateProjectConfigFinal(cfg, source) {
|
|
62
130
|
if (cfg.types) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Produces a structured, human-readable breakdown of the full
|
|
3
|
+
* branch-name pipeline.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* No branch is created — this is purely informational.
|
|
7
|
+
* Used by the `--explain` CLI flag.
|
|
8
|
+
*
|
|
9
|
+
* Sections printed:
|
|
10
|
+
* 1. Pattern and source
|
|
11
|
+
* 2. Variables used
|
|
12
|
+
* 3. Builtin values
|
|
13
|
+
* 4. Git values
|
|
14
|
+
* 5. CLI values
|
|
15
|
+
* 6. Transform chain with intermediate values
|
|
16
|
+
* 7. Rendered / Sanitized output
|
|
17
|
+
* 8. Max-length truncation (when applicable)
|
|
18
|
+
* 9. Final branch name
|
|
19
|
+
*
|
|
20
|
+
* @param input - The collected pipeline data.
|
|
21
|
+
* @returns A multi-line string ready to be printed to stdout.
|
|
22
|
+
*/
|
|
23
|
+
export function explain(input) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push("Pipeline explanation:\n");
|
|
26
|
+
// 1. Pattern
|
|
27
|
+
lines.push(` Pattern: ${input.pattern}`);
|
|
28
|
+
lines.push(` Pattern source: ${input.patternSource}`);
|
|
29
|
+
// 2. Variables used
|
|
30
|
+
const vars = input.ast.variablesUsed;
|
|
31
|
+
lines.push(`\n Variables used: ${vars.length > 0 ? vars.join(", ") : "(none)"}`);
|
|
32
|
+
// 3. Builtin values
|
|
33
|
+
const builtinEntries = Object.entries(input.builtinValues).filter(([, v]) => v !== undefined);
|
|
34
|
+
if (builtinEntries.length > 0) {
|
|
35
|
+
lines.push(`\n Builtin values:`);
|
|
36
|
+
for (const [k, v] of builtinEntries) {
|
|
37
|
+
lines.push(` ${k} = "${v}"`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 4. Git values
|
|
41
|
+
const gitEntries = Object.entries(input.gitValues).filter(([, v]) => v !== undefined);
|
|
42
|
+
if (gitEntries.length > 0) {
|
|
43
|
+
lines.push(`\n Git values:`);
|
|
44
|
+
for (const [k, v] of gitEntries) {
|
|
45
|
+
lines.push(` ${k} = "${v}"`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 5. CLI values
|
|
49
|
+
const cliEntries = Object.entries(input.cliValues).filter(([, v]) => v !== undefined);
|
|
50
|
+
if (cliEntries.length > 0) {
|
|
51
|
+
lines.push(`\n CLI values:`);
|
|
52
|
+
for (const [k, v] of cliEntries) {
|
|
53
|
+
lines.push(` ${k} = "${v}"`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 6. Transforms applied — with intermediate values
|
|
57
|
+
const transformNodes = input.ast.nodes.filter((n) => n.kind === "variable" && n.transforms.length > 0);
|
|
58
|
+
if (transformNodes.length > 0) {
|
|
59
|
+
lines.push(`\n Transforms applied:`);
|
|
60
|
+
for (const node of transformNodes) {
|
|
61
|
+
if (node.kind !== "variable")
|
|
62
|
+
continue;
|
|
63
|
+
const initial = input.resolvedValues[node.name] ?? "";
|
|
64
|
+
lines.push(` {${node.name}} = "${initial}"`);
|
|
65
|
+
let current = initial;
|
|
66
|
+
for (const t of node.transforms) {
|
|
67
|
+
const fn = input.transforms[t.name];
|
|
68
|
+
if (fn) {
|
|
69
|
+
current = fn(current, t.args);
|
|
70
|
+
}
|
|
71
|
+
const argsStr = t.args.length > 0 ? `:${t.args.join(":")}` : "";
|
|
72
|
+
lines.push(` → ${t.name}${argsStr} → "${current}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 7. Final result
|
|
77
|
+
lines.push(`\n Rendered: ${input.rendered}`);
|
|
78
|
+
if (input.rendered !== input.sanitized) {
|
|
79
|
+
lines.push(` Sanitized: ${input.sanitized}`);
|
|
80
|
+
}
|
|
81
|
+
// 8. Max-length truncation
|
|
82
|
+
if (input.maxLength !== undefined) {
|
|
83
|
+
lines.push(` Max length: ${input.maxLength}`);
|
|
84
|
+
if (input.truncated !== undefined && input.truncated !== input.sanitized) {
|
|
85
|
+
lines.push(` Truncated: ${input.truncated}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push(` Truncated: (no truncation needed)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const finalName = input.truncated ?? input.sanitized;
|
|
92
|
+
lines.push(`\n Final branch: ${finalName}`);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a human-readable table of all available transforms.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Each transform is listed with its name, summary, and an optional
|
|
6
|
+
* usage example. Used by the `--list-transforms` CLI flag.
|
|
7
|
+
*
|
|
8
|
+
* @param transforms - The ordered list of {@link TransformDef} definitions.
|
|
9
|
+
* @returns A multi-line formatted string ready to be printed to stdout.
|
|
10
|
+
*/
|
|
11
|
+
export function listTransforms(transforms) {
|
|
12
|
+
const lines = [];
|
|
13
|
+
lines.push("Available transforms:\n");
|
|
14
|
+
const maxName = Math.max(...transforms.map((t) => t.name.length));
|
|
15
|
+
for (const t of transforms) {
|
|
16
|
+
const name = t.name.padEnd(maxName + 2);
|
|
17
|
+
const summary = t.doc?.summary ?? "(no description)";
|
|
18
|
+
const usage = t.doc?.usage?.[0] ?? "";
|
|
19
|
+
lines.push(` ${name}${summary}`);
|
|
20
|
+
if (usage) {
|
|
21
|
+
lines.push(` ${"".padEnd(maxName + 2)}Example: ${usage}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats the resolved project configuration for display.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Shows the final resolved values from whichever source took
|
|
6
|
+
* precedence. Used by the `--print-config` CLI flag.
|
|
7
|
+
*
|
|
8
|
+
* @param config - The resolved {@link ProjectConfig}.
|
|
9
|
+
* @param source - Human-readable label identifying the winning source.
|
|
10
|
+
* @returns A multi-line formatted string ready to be printed to stdout.
|
|
11
|
+
*/
|
|
12
|
+
export function printConfig(config, source) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push("Resolved configuration:\n");
|
|
15
|
+
lines.push(` Source: ${source}`);
|
|
16
|
+
lines.push(` Pattern: ${config.pattern ?? "(not set)"}`);
|
|
17
|
+
lines.push(` Default type: ${config.defaultType ?? "(not set)"}`);
|
|
18
|
+
if (config.types && config.types.length > 0) {
|
|
19
|
+
lines.push(` Types:`);
|
|
20
|
+
for (const t of config.types) {
|
|
21
|
+
lines.push(` - ${t.value} (${t.label})`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
lines.push(` Types: (not configured)`);
|
|
26
|
+
}
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
package/dist/git/createBranch.js
CHANGED
|
@@ -2,11 +2,18 @@ import { execa } from "execa";
|
|
|
2
2
|
/**
|
|
3
3
|
* Creates and switches to a new Git branch.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Uses a two-step strategy for maximum compatibility:
|
|
6
7
|
* 1. Try `git switch -c <name>` (modern Git ≥ 2.23).
|
|
7
|
-
* 2. Fallback to `git checkout -b <name>` if switch is unavailable.
|
|
8
|
+
* 2. Fallback to `git checkout -b <name>` if `switch` is unavailable.
|
|
8
9
|
*
|
|
9
|
-
* @
|
|
10
|
+
* @param name - The name of the branch to create.
|
|
11
|
+
* @throws {@link Error} If both `git switch -c` and `git checkout -b` fail.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* await createBranch("feat/my-feature");
|
|
16
|
+
* ```
|
|
10
17
|
*/
|
|
11
18
|
export async function createBranch(name) {
|
|
12
19
|
try {
|
package/dist/git/gitBuiltins.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { getGitConfig } from "../git/gitConfig.js";
|
|
2
2
|
import { execa } from "execa";
|
|
3
|
+
/**
|
|
4
|
+
* Ordered list of all supported {@link GitBuiltinKey} values.
|
|
5
|
+
*/
|
|
3
6
|
export const GIT_BUILTIN_KEYS = [
|
|
4
7
|
"shortSha",
|
|
5
8
|
"currentBranch",
|
|
@@ -7,12 +10,30 @@ export const GIT_BUILTIN_KEYS = [
|
|
|
7
10
|
"repoName",
|
|
8
11
|
"lastTag",
|
|
9
12
|
];
|
|
13
|
+
/**
|
|
14
|
+
* Deduplicates an array of {@link GitBuiltinKey} values.
|
|
15
|
+
*
|
|
16
|
+
* @param keys - Array of keys that may contain duplicates.
|
|
17
|
+
* @returns A new array with unique keys only.
|
|
18
|
+
*/
|
|
10
19
|
function uniqueKeys(keys) {
|
|
11
20
|
return Array.from(new Set(keys));
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the requested keys, or all supported keys when none are specified.
|
|
24
|
+
*
|
|
25
|
+
* @param keys - Optional subset of keys to resolve.
|
|
26
|
+
* @returns The keys to resolve (deduplicated).
|
|
27
|
+
*/
|
|
13
28
|
function pickAllKeysIfUndefined(keys) {
|
|
14
29
|
return keys?.length ? uniqueKeys(keys) : GIT_BUILTIN_KEYS;
|
|
15
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Executes a git command and returns its trimmed stdout, or `undefined` on failure.
|
|
33
|
+
*
|
|
34
|
+
* @param args - Arguments to pass to the `git` command.
|
|
35
|
+
* @returns The trimmed stdout output, or `undefined` if the command fails or produces no output.
|
|
36
|
+
*/
|
|
16
37
|
async function safeExec(args) {
|
|
17
38
|
try {
|
|
18
39
|
const { stdout } = (await execa("git", args));
|
|
@@ -23,10 +44,23 @@ async function safeExec(args) {
|
|
|
23
44
|
return undefined;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Derives the repository name from the absolute path to the repo root.
|
|
49
|
+
*
|
|
50
|
+
* @param repoRoot - Absolute path returned by `git rev-parse --show-toplevel`.
|
|
51
|
+
* @returns The last segment of the path, or `undefined` if the path is empty.
|
|
52
|
+
*/
|
|
26
53
|
function deriveRepoName(repoRoot) {
|
|
27
54
|
const parts = repoRoot.split(/[\\/]/).filter(Boolean);
|
|
28
55
|
return parts.length ? parts[parts.length - 1] : undefined;
|
|
29
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolver map for each supported {@link GitBuiltinKey}.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Each resolver is an async function that returns the resolved value
|
|
62
|
+
* or `undefined` when unavailable.
|
|
63
|
+
*/
|
|
30
64
|
const RESOLVERS = {
|
|
31
65
|
shortSha: () => safeExec(["rev-parse", "--short", "HEAD"]),
|
|
32
66
|
currentBranch: async () => {
|
|
@@ -43,6 +77,12 @@ const RESOLVERS = {
|
|
|
43
77
|
return value ?? process.env.USER ?? process.env.USERNAME ?? undefined;
|
|
44
78
|
},
|
|
45
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* Internal resolver that resolves the requested git builtin keys in parallel.
|
|
82
|
+
*
|
|
83
|
+
* @param keys - Optional subset of keys to resolve. When omitted, all keys are resolved.
|
|
84
|
+
* @returns An object containing the resolved values.
|
|
85
|
+
*/
|
|
46
86
|
async function resolveGitBuiltins(keys) {
|
|
47
87
|
const wanted = pickAllKeysIfUndefined(keys);
|
|
48
88
|
const entries = await Promise.all(wanted.map(async (key) => {
|
|
@@ -57,22 +97,49 @@ async function resolveGitBuiltins(keys) {
|
|
|
57
97
|
/**
|
|
58
98
|
* Resolves git-based built-in variables.
|
|
59
99
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* Each variable maps to a git command executed in the current repository.
|
|
102
|
+
* Resolution is done in parallel for performance.
|
|
103
|
+
*
|
|
104
|
+
* @param keys - Optional subset of {@link GitBuiltinKey} values to resolve.
|
|
105
|
+
* When omitted, all supported git builtins are resolved.
|
|
106
|
+
* @returns A promise resolving to a {@link GitBuiltins} object.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* // Resolve only what is needed
|
|
111
|
+
* const builtins = await getGitBuiltins(["shortSha", "currentBranch"]);
|
|
112
|
+
* builtins.shortSha; // "abc1234"
|
|
113
|
+
* builtins.currentBranch; // "main"
|
|
114
|
+
* ```
|
|
62
115
|
*/
|
|
63
116
|
export async function getGitBuiltins(keys) {
|
|
64
117
|
return resolveGitBuiltins(keys);
|
|
65
118
|
}
|
|
66
119
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
120
|
+
* Checks whether a pattern string references at least one git builtin key.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* Use this before calling {@link getGitBuiltins} to avoid spawning
|
|
124
|
+
* unnecessary git subprocesses.
|
|
125
|
+
*
|
|
126
|
+
* @param pattern - The raw pattern string to check.
|
|
127
|
+
* @returns `true` if the pattern contains at least one {@link GitBuiltinKey}.
|
|
69
128
|
*/
|
|
70
129
|
export function patternNeedsGitBuiltins(pattern) {
|
|
71
130
|
return GIT_BUILTIN_KEYS.some((key) => pattern.includes(key));
|
|
72
131
|
}
|
|
73
132
|
/**
|
|
74
|
-
* Extracts which
|
|
75
|
-
*
|
|
133
|
+
* Extracts which {@link GitBuiltinKey} values are present in a pattern.
|
|
134
|
+
*
|
|
135
|
+
* @param pattern - The raw pattern string to inspect.
|
|
136
|
+
* @returns An array of {@link GitBuiltinKey} values found in the pattern.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* extractGitBuiltinKeysFromPattern("{currentBranch}-{shortSha}");
|
|
141
|
+
* // => ["shortSha", "currentBranch"]
|
|
142
|
+
* ```
|
|
76
143
|
*/
|
|
77
144
|
export function extractGitBuiltinKeysFromPattern(pattern) {
|
|
78
145
|
return GIT_BUILTIN_KEYS.filter((key) => pattern.includes(key));
|
package/dist/git/gitConfig.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* Retrieves a single Git config value by key.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Runs `git config --get <key>` as a subprocess. Returns `undefined`
|
|
7
|
+
* when the key is not set or the command fails (e.g. outside a repo).
|
|
8
|
+
*
|
|
9
|
+
* @param key - The Git config key to look up (e.g. `"user.name"`).
|
|
10
|
+
* @returns The trimmed config value, or `undefined` if not found.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const name = await getGitConfig("user.name"); // "Alice" | undefined
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
2
17
|
export async function getGitConfig(key) {
|
|
3
18
|
try {
|
|
4
19
|
const { stdout } = (await execa("git", ["config", "--get", key]));
|
|
@@ -9,3 +24,40 @@ export async function getGitConfig(key) {
|
|
|
9
24
|
return undefined;
|
|
10
25
|
}
|
|
11
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns all git config entries matching a key pattern using `--get-regexp`.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Each entry is returned as a `[key, value]` tuple. Returns an empty
|
|
32
|
+
* array if no entries match or if the command fails.
|
|
33
|
+
*
|
|
34
|
+
* @param pattern - A regular expression pattern passed to `git config --get-regexp`.
|
|
35
|
+
* @returns An array of `[key, value]` tuples.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const entries = await getGitConfigRegexp("^new-branch\\.patterns\\.");
|
|
40
|
+
* // => [["new-branch.patterns.hotfix", "hotfix/{id}"], ...]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export async function getGitConfigRegexp(pattern) {
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = (await execa("git", ["config", "--get-regexp", pattern]));
|
|
46
|
+
const raw = String(stdout ?? "").trim();
|
|
47
|
+
if (!raw.length)
|
|
48
|
+
return [];
|
|
49
|
+
return raw.split("\n").reduce((acc, line) => {
|
|
50
|
+
const idx = line.indexOf(" ");
|
|
51
|
+
if (idx === -1)
|
|
52
|
+
return acc;
|
|
53
|
+
const key = line.slice(0, idx);
|
|
54
|
+
const value = line.slice(idx + 1);
|
|
55
|
+
if (key && value)
|
|
56
|
+
acc.push([key, value]);
|
|
57
|
+
return acc;
|
|
58
|
+
}, []);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Performs a lightweight sanitization
|
|
3
|
-
* full validation to Git.
|
|
2
|
+
* Performs a lightweight sanitization of a Git ref name.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This function applies a set of heuristic cleanups to make the input
|
|
6
|
+
* more likely to pass `git check-ref-format --branch`. It does **not**
|
|
7
|
+
* fully reimplement all Git ref rules — final validation must still be
|
|
8
|
+
* delegated to Git itself via {@link validateBranchName}.
|
|
9
|
+
*
|
|
10
|
+
* Sanitization steps:
|
|
11
|
+
* 1. Trim whitespace and replace internal whitespace with `-`.
|
|
12
|
+
* 2. Remove characters forbidden by Git (`~ ^ : ? * [ ] \`).
|
|
13
|
+
* 3. Remove `@{` sequences.
|
|
14
|
+
* 4. Collapse multiple slashes and repeated dots.
|
|
15
|
+
* 5. Strip leading dashes/slashes and trailing slashes/dots.
|
|
16
|
+
* 6. Remove `.lock` suffix.
|
|
17
|
+
*
|
|
18
|
+
* @param input - The raw ref name to sanitize.
|
|
19
|
+
* @returns The sanitized ref name.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* sanitizeGitRef("feat/My Feature!!"); // => "feat/My-Feature"
|
|
24
|
+
* sanitizeGitRef("--leading/slash"); // => "leading/slash"
|
|
25
|
+
* ```
|
|
8
26
|
*/
|
|
9
27
|
export function sanitizeGitRef(input) {
|
|
10
28
|
let name = input.trim();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncates a string from the end to ensure it does not exceed `maxLength`.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* This is intentionally simple and deterministic:
|
|
6
|
+
* - If the string length is `<= maxLength`, return it unchanged.
|
|
7
|
+
* - Otherwise, cut from the end.
|
|
8
|
+
*
|
|
9
|
+
* Used by the `--max-length` CLI option after sanitization and before
|
|
10
|
+
* branch-name validation.
|
|
11
|
+
*
|
|
12
|
+
* @param value - The string to truncate.
|
|
13
|
+
* @param maxLength - The maximum allowed length. Must be a positive integer (`>= 1`).
|
|
14
|
+
* @returns The (possibly truncated) string.
|
|
15
|
+
* @throws {@link Error} If `maxLength` is not a positive integer.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* truncateEnd("feat/my-feature", 10); // => "feat/my-fe"
|
|
20
|
+
* truncateEnd("short", 100); // => "short"
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function truncateEnd(value, maxLength) {
|
|
24
|
+
if (!Number.isInteger(maxLength) || maxLength < 1) {
|
|
25
|
+
throw new Error(`--max-length must be a positive integer (>= 1), got "${maxLength}".`);
|
|
26
|
+
}
|
|
27
|
+
if (value.length <= maxLength) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return value.slice(0, maxLength);
|
|
31
|
+
}
|
|
@@ -2,10 +2,19 @@ import { execa } from "execa";
|
|
|
2
2
|
/**
|
|
3
3
|
* Validates a branch name by delegating to Git itself.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Invokes `git check-ref-format --branch <name>` as a subprocess.
|
|
7
|
+
* This ensures the name complies with all rules enforced by the
|
|
8
|
+
* installed version of Git.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
10
|
+
* @param name - The branch name to validate.
|
|
11
|
+
* @throws {@link Error} If the branch name does not pass `git check-ref-format`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* await validateBranchName("feat/my-branch"); // resolves
|
|
16
|
+
* await validateBranchName(".."); // throws
|
|
17
|
+
* ```
|
|
9
18
|
*/
|
|
10
19
|
export async function validateBranchName(name) {
|
|
11
20
|
try {
|