new-branch 0.3.1 → 0.5.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 +72 -2
- package/dist/cli.js +32 -4
- package/dist/git/gitBuiltins.js +79 -0
- package/dist/git/gitConfig.js +11 -0
- package/dist/pattern/transforms/index.js +18 -12
- package/dist/pattern/transforms/registry.js +10 -0
- package/dist/runtime/builtins.js +44 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
<img src="./logo.svg" width="180" alt="new-branch logo" />
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
+
> “Explicit is better than implicit.”
|
|
8
|
+
> — The Zen of Python (PEP 20)
|
|
9
|
+
|
|
7
10
|
A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
|
|
8
11
|
|
|
9
12
|

|
|
10
13
|
|
|
14
|
+
[](https://github.com/teles/new-branch/actions/workflows/ci.yml)
|
|
15
|
+
[](https://codecov.io/gh/teles/new-branch)
|
|
16
|
+
|
|
11
17
|
---
|
|
12
18
|
|
|
13
19
|
## Why
|
|
@@ -82,10 +88,49 @@ Example:
|
|
|
82
88
|
|
|
83
89
|
## Built-in Variables
|
|
84
90
|
|
|
91
|
+
### Core Variables
|
|
92
|
+
|
|
85
93
|
- `type`
|
|
86
94
|
- `title`
|
|
87
95
|
- `id`
|
|
88
96
|
|
|
97
|
+
### Date Built-ins (derived from local system time)
|
|
98
|
+
|
|
99
|
+
- `year` → YYYY
|
|
100
|
+
- `month` → MM (zero padded)
|
|
101
|
+
- `day` → DD (zero padded)
|
|
102
|
+
- `date` → YYYY-MM-DD
|
|
103
|
+
- `dateCompact` → YYYYMMDD
|
|
104
|
+
|
|
105
|
+
### Git Built-ins (derived from current Git repository)
|
|
106
|
+
|
|
107
|
+
- `currentBranch` → Current Git branch name (e.g. `main`, `feature/PROJ-123`)
|
|
108
|
+
- `shortSha` → Short SHA of `HEAD` (e.g. `a1b2c3d`)
|
|
109
|
+
- `repoName` → Repository directory name
|
|
110
|
+
- `userName` → Git user name (`git config user.name`)
|
|
111
|
+
- `lastTag` → Most recent Git tag (`git describe --tags --abbrev=0`)
|
|
112
|
+
|
|
113
|
+
> Note:
|
|
114
|
+
>
|
|
115
|
+
> - Git built-ins are resolved lazily and only when referenced in the pattern.
|
|
116
|
+
> - They are never prompted interactively.
|
|
117
|
+
> - When unavailable (e.g. outside a Git repository), they resolve to an empty string.
|
|
118
|
+
|
|
119
|
+
#### Example with Git built-ins
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
new-branch \
|
|
123
|
+
--pattern "{currentBranch}-{shortSha}-{type}-{title:slugify}" \
|
|
124
|
+
--type feat \
|
|
125
|
+
--title "Improve logging"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Example output:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
main-a1b2c3d-feat-improve-logging
|
|
132
|
+
```
|
|
133
|
+
|
|
89
134
|
---
|
|
90
135
|
|
|
91
136
|
## Built-in Transforms
|
|
@@ -144,11 +189,36 @@ You can define a default pattern in `package.json`:
|
|
|
144
189
|
}
|
|
145
190
|
```
|
|
146
191
|
|
|
147
|
-
|
|
192
|
+
### Git Configuration
|
|
193
|
+
|
|
194
|
+
You can also define a default pattern using Git config:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git config --local new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or globally:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
git config --global new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
To remove the pattern from Git config:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Remove from local repository
|
|
210
|
+
git config --unset --local new-branch.pattern
|
|
211
|
+
|
|
212
|
+
# Remove from global config
|
|
213
|
+
git config --unset --global new-branch.pattern
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
When using Git config, the resolution order becomes:
|
|
148
217
|
|
|
149
218
|
1. CLI flags
|
|
150
219
|
2. `package.json` configuration
|
|
151
|
-
3.
|
|
220
|
+
3. Git config (`new-branch.pattern`)
|
|
221
|
+
4. Interactive prompt (if enabled)
|
|
152
222
|
|
|
153
223
|
---
|
|
154
224
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,10 @@ import { parsePattern } from "./pattern/parsePattern.js";
|
|
|
4
4
|
import { defaultTransforms } from "./pattern/transforms/index.js";
|
|
5
5
|
import { renderPattern } from "./pattern/transforms/renderPattern.js";
|
|
6
6
|
import { resolveMissingValues } from "./runtime/resolveMissingValues.js";
|
|
7
|
+
import { getBuiltinValues } from "./runtime/builtins.js";
|
|
7
8
|
import { loadProjectConfig } from "./config/loadProjectConfig.js";
|
|
9
|
+
import { getGitConfig } from "./git/gitConfig.js";
|
|
10
|
+
import { extractGitBuiltinKeysFromPattern, getGitBuiltins, patternNeedsGitBuiltins, } from "./git/gitBuiltins.js";
|
|
8
11
|
import { sanitizeGitRef } from "./git/sanitizeGitRef.js";
|
|
9
12
|
import { validateBranchName } from "./git/validateBranchName.js";
|
|
10
13
|
import { createBranch } from "./git/createBranch.js";
|
|
@@ -46,7 +49,7 @@ function toInitialValues(args) {
|
|
|
46
49
|
type: args.options.type,
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
|
-
async function run() {
|
|
52
|
+
export async function run() {
|
|
50
53
|
// Step 0: args/options
|
|
51
54
|
// Note: CAC prints help, but depending on our parseArgs wrapper we might not
|
|
52
55
|
// expose `help` in `args.options`. We still want to exit early and never
|
|
@@ -62,14 +65,35 @@ async function run() {
|
|
|
62
65
|
const prompt = args.options.prompt !== false;
|
|
63
66
|
// Pipeline: pattern -> AST -> resolve values -> render -> sanitize -> validate -> (optional) git -> output
|
|
64
67
|
const projectConfig = await loadProjectConfig();
|
|
65
|
-
|
|
68
|
+
// Git config (respects local -> global precedence automatically)
|
|
69
|
+
let gitPattern;
|
|
70
|
+
if (!args.options.pattern && !projectConfig.pattern) {
|
|
71
|
+
gitPattern = await getGitConfig("new-branch.pattern");
|
|
72
|
+
}
|
|
73
|
+
const resolvedPattern = args.options.pattern ?? projectConfig.pattern ?? gitPattern;
|
|
66
74
|
const patternRes = requirePattern(resolvedPattern);
|
|
67
75
|
if (!isOk(patternRes))
|
|
68
76
|
fail("Invalid CLI arguments.", patternRes.error);
|
|
69
77
|
const astRes = safe(() => parsePattern(patternRes.value));
|
|
70
78
|
if (!isOk(astRes))
|
|
71
79
|
fail("Invalid pattern.", astRes.error);
|
|
72
|
-
|
|
80
|
+
// Runtime built-ins (date, etc.)
|
|
81
|
+
const builtinValues = getBuiltinValues();
|
|
82
|
+
// Git built-ins (only if the pattern references them)
|
|
83
|
+
let gitValues = {};
|
|
84
|
+
if (patternNeedsGitBuiltins(patternRes.value)) {
|
|
85
|
+
const gitKeys = extractGitBuiltinKeysFromPattern(patternRes.value);
|
|
86
|
+
const gitRes = await safeAsync(() => getGitBuiltins(gitKeys));
|
|
87
|
+
if (!isOk(gitRes))
|
|
88
|
+
fail("Failed to resolve git builtins.", gitRes.error);
|
|
89
|
+
// GitBuiltins is compatible with RenderValues (string | undefined)
|
|
90
|
+
gitValues = gitRes.value;
|
|
91
|
+
}
|
|
92
|
+
const initialValues = {
|
|
93
|
+
...builtinValues,
|
|
94
|
+
...gitValues,
|
|
95
|
+
...toInitialValues(args),
|
|
96
|
+
};
|
|
73
97
|
const valuesRes = await safeAsync(() => resolveMissingValues(astRes.value, initialValues, {
|
|
74
98
|
prompt,
|
|
75
99
|
}));
|
|
@@ -106,4 +130,8 @@ async function run() {
|
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
|
-
|
|
133
|
+
// Only execute the CLI automatically when not running tests.
|
|
134
|
+
// In tests, `run` can be imported and called directly.
|
|
135
|
+
if (process.env.NODE_ENV !== "test") {
|
|
136
|
+
run().catch((e) => fail("Unexpected error in CLI.", e));
|
|
137
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { getGitConfig } from "../git/gitConfig.js";
|
|
3
|
+
export const GIT_BUILTIN_KEYS = [
|
|
4
|
+
"shortSha",
|
|
5
|
+
"currentBranch",
|
|
6
|
+
"userName",
|
|
7
|
+
"repoName",
|
|
8
|
+
"lastTag",
|
|
9
|
+
];
|
|
10
|
+
function uniqueKeys(keys) {
|
|
11
|
+
return Array.from(new Set(keys));
|
|
12
|
+
}
|
|
13
|
+
function pickAllKeysIfUndefined(keys) {
|
|
14
|
+
return keys?.length ? uniqueKeys(keys) : GIT_BUILTIN_KEYS;
|
|
15
|
+
}
|
|
16
|
+
async function safeExec(args) {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = await execa("git", args);
|
|
19
|
+
const value = stdout.trim();
|
|
20
|
+
return value.length ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function deriveRepoName(repoRoot) {
|
|
27
|
+
const parts = repoRoot.split(/[\\/]/).filter(Boolean);
|
|
28
|
+
return parts.length ? parts[parts.length - 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
const RESOLVERS = {
|
|
31
|
+
shortSha: () => safeExec(["rev-parse", "--short", "HEAD"]),
|
|
32
|
+
currentBranch: async () => {
|
|
33
|
+
const value = await safeExec(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
34
|
+
return value === "HEAD" ? undefined : value;
|
|
35
|
+
},
|
|
36
|
+
repoName: async () => {
|
|
37
|
+
const repoRoot = await safeExec(["rev-parse", "--show-toplevel"]);
|
|
38
|
+
return repoRoot ? deriveRepoName(repoRoot) : undefined;
|
|
39
|
+
},
|
|
40
|
+
lastTag: () => safeExec(["describe", "--tags", "--abbrev=0"]),
|
|
41
|
+
userName: async () => {
|
|
42
|
+
const value = await getGitConfig("user.name");
|
|
43
|
+
return value ?? process.env.USER ?? process.env.USERNAME ?? undefined;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
async function resolveGitBuiltins(keys) {
|
|
47
|
+
const wanted = pickAllKeysIfUndefined(keys);
|
|
48
|
+
const entries = await Promise.all(wanted.map(async (key) => {
|
|
49
|
+
const value = await RESOLVERS[key]();
|
|
50
|
+
return [key, value];
|
|
51
|
+
}));
|
|
52
|
+
return entries.reduce((acc, [key, value]) => {
|
|
53
|
+
acc[key] = value;
|
|
54
|
+
return acc;
|
|
55
|
+
}, {});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolves git-based built-in variables.
|
|
59
|
+
*
|
|
60
|
+
* - If `keys` is omitted, resolves all supported git builtins.
|
|
61
|
+
* - If `keys` is provided, resolves only those keys.
|
|
62
|
+
*/
|
|
63
|
+
export async function getGitBuiltins(keys) {
|
|
64
|
+
return resolveGitBuiltins(keys);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns true if the pattern contains at least one git builtin key.
|
|
68
|
+
* (Call this before `getGitBuiltins()` if you want to avoid running git at all.)
|
|
69
|
+
*/
|
|
70
|
+
export function patternNeedsGitBuiltins(pattern) {
|
|
71
|
+
return GIT_BUILTIN_KEYS.some((key) => pattern.includes(key));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extracts which git builtin keys are present in the pattern.
|
|
75
|
+
* (Use the returned keys to resolve only what you need.)
|
|
76
|
+
*/
|
|
77
|
+
export function extractGitBuiltinKeysFromPattern(pattern) {
|
|
78
|
+
return GIT_BUILTIN_KEYS.filter((key) => pattern.includes(key));
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
export async function getGitConfig(key) {
|
|
3
|
+
try {
|
|
4
|
+
const { stdout } = await execa("git", ["config", "--get", key]);
|
|
5
|
+
const value = stdout.trim();
|
|
6
|
+
return value.length ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { buildRegistry } from "../../pattern/transforms/registry.js";
|
|
1
2
|
import { lower } from "../../pattern/transforms/lower.js";
|
|
2
3
|
import { upper } from "../../pattern/transforms/upper.js";
|
|
3
4
|
import { max } from "../../pattern/transforms/max.js";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { slugify } from "../../pattern/transforms/slugify.js";
|
|
6
|
+
import { camel } from "../../pattern/transforms/camel.js";
|
|
7
|
+
import { kebab } from "../../pattern/transforms/kebab.js";
|
|
8
|
+
import { snake } from "../../pattern/transforms/snake.js";
|
|
9
|
+
import { title } from "../../pattern/transforms/title.js";
|
|
10
|
+
import { words } from "../../pattern/transforms/words.js";
|
|
11
|
+
export const allTransforms = [
|
|
12
|
+
lower,
|
|
13
|
+
upper,
|
|
14
|
+
max,
|
|
15
|
+
slugify,
|
|
16
|
+
camel,
|
|
17
|
+
kebab,
|
|
18
|
+
snake,
|
|
19
|
+
title,
|
|
20
|
+
words,
|
|
21
|
+
];
|
|
16
22
|
export const defaultTransforms = buildRegistry(allTransforms);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pads a number with a leading zero when necessary.
|
|
3
|
+
*
|
|
4
|
+
* @param n - Number to pad.
|
|
5
|
+
* @returns A zero-padded string (e.g., 2 → "02").
|
|
6
|
+
*/
|
|
7
|
+
function pad(n) {
|
|
8
|
+
return String(n).padStart(2, "0");
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns built-in date-based variables derived from
|
|
12
|
+
* the local system time.
|
|
13
|
+
*
|
|
14
|
+
* This function is intentionally synchronous and pure.
|
|
15
|
+
* The optional `now` parameter allows deterministic testing.
|
|
16
|
+
*
|
|
17
|
+
* Provided variables:
|
|
18
|
+
*
|
|
19
|
+
* - `year` → YYYY
|
|
20
|
+
* - `month` → MM (zero padded)
|
|
21
|
+
* - `day` → DD (zero padded)
|
|
22
|
+
* - `date` → YYYY-MM-DD
|
|
23
|
+
* - `dateCompact` → YYYYMMDD
|
|
24
|
+
*
|
|
25
|
+
* @param now - Optional Date instance used to generate values.
|
|
26
|
+
* Defaults to the current system date/time.
|
|
27
|
+
* @returns An object containing built-in date variables.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const builtins = getBuiltinValues(new Date("2026-02-19"));
|
|
31
|
+
* builtins.date; // "2026-02-19"
|
|
32
|
+
*/
|
|
33
|
+
export function getBuiltinValues(now = new Date()) {
|
|
34
|
+
const year = String(now.getFullYear());
|
|
35
|
+
const month = pad(now.getMonth() + 1);
|
|
36
|
+
const day = pad(now.getDate());
|
|
37
|
+
return {
|
|
38
|
+
year,
|
|
39
|
+
month,
|
|
40
|
+
day,
|
|
41
|
+
date: `${year}-${month}-${day}`,
|
|
42
|
+
dateCompact: `${year}${month}${day}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "new-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Generate and create standardized git branch names from a pattern.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"lint": "eslint src --ext .ts"
|
|
27
27
|
},
|
|
28
28
|
"bin": {
|
|
29
|
-
"new-branch": "
|
|
30
|
-
"git-
|
|
29
|
+
"new-branch": "dist/cli.js",
|
|
30
|
+
"git-new-branch": "dist/cli.js",
|
|
31
|
+
"git-nb": "dist/cli.js"
|
|
31
32
|
},
|
|
32
33
|
"author": "Teles <github.com/teles>",
|
|
33
34
|
"license": "MIT",
|
|
@@ -48,10 +49,17 @@
|
|
|
48
49
|
"packageManager": "pnpm@10.22.0",
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@eslint/js": "10.0.1",
|
|
52
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
53
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
54
|
+
"@semantic-release/git": "^10.0.1",
|
|
55
|
+
"@semantic-release/github": "^12.0.6",
|
|
56
|
+
"@semantic-release/npm": "^13.1.4",
|
|
57
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
51
58
|
"@types/node": "25.2.3",
|
|
52
59
|
"@vitest/coverage-v8": "4.0.18",
|
|
53
60
|
"eslint": "10.0.0",
|
|
54
61
|
"prettier": "3.8.1",
|
|
62
|
+
"semantic-release": "^25.0.3",
|
|
55
63
|
"tsc-alias": "1.8.16",
|
|
56
64
|
"tsx": "4.21.0",
|
|
57
65
|
"typescript": "5.9.3",
|