mergelog 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Morten Daniel Fornes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # mergelog
2
+
3
+ > Format git commits as Markdown for merge and pull request descriptions
4
+
5
+ ## Why
6
+
7
+ - Generate clean Markdown directly from `git log`
8
+ - Optimized for GitLab and GitHub merge request descriptions
9
+ - Automatically formats code-like terms with backticks
10
+ - Detects issue references like `#1234`
11
+ - Detects the repository main branch automatically
12
+ - Supports clipboard output
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install mergelog -g
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```sh
23
+ $ mergelog --help
24
+
25
+ mergelog
26
+
27
+ Format git commits as Markdown for merge/pull request descriptions.
28
+
29
+ Usage
30
+ mergelog
31
+ mergelog 3
32
+ mergelog origin/main..HEAD
33
+ mergelog 3 --no-code
34
+ mergelog 3 --clipboard
35
+
36
+ Options
37
+ -h, --help Show this help screen
38
+ -C, --no-code Disable automatic backticks
39
+ -c, --clipboard Copy Markdown to the clipboard
40
+
41
+ Defaults
42
+ No range: detected upstream/default branch .. HEAD
43
+ Number argument: last N commits, e.g. 3 -> -3
44
+
45
+ Output
46
+ ### Commit subject
47
+
48
+ Commit body
49
+
50
+ Close #1234
51
+ ```
52
+
53
+ ## Example output
54
+
55
+ ```md
56
+ ### Tools: Improve startup logging
57
+
58
+ Show clearer restart and shutdown messages during local development.
59
+
60
+ ### API: Deprecate timezone fields
61
+
62
+ Mark legacy timezone fields as deprecated in the API schema.
63
+
64
+ Close #1234
65
+
66
+ ### Calendar: Fix week rendering for January 2027
67
+
68
+ Render calendar weeks using actual dates instead of ISO week numbers.
69
+ ```
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ const colors = {
4
+ bold: "\x1b[1m",
5
+ dim: "\x1b[2m",
6
+ cyan: "\x1b[36m",
7
+ green: "\x1b[32m",
8
+ yellow: "\x1b[33m",
9
+ reset: "\x1b[0m",
10
+ };
11
+ const args = process.argv.slice(2);
12
+ if (args.includes("-h") || args.includes("--help")) {
13
+ help();
14
+ process.exit(0);
15
+ }
16
+ const autoCode = !args.includes("-C") && !args.includes("--no-code");
17
+ const clipboard = args.includes("-c") || args.includes("--clipboard");
18
+ const arg = args.find(x => !x.startsWith("-"));
19
+ main();
20
+ /** Format git commits as Markdown for merge/pull request descriptions. */
21
+ function main() {
22
+ const range = resolveRange(arg);
23
+ const log = git(["log", range, "--reverse", "--format=%x1e%B%x1f"], { showErrors: true });
24
+ const markdown = formatCommits(log);
25
+ if (clipboard) {
26
+ copyToClipboard(markdown);
27
+ }
28
+ else {
29
+ console.log(markdown);
30
+ }
31
+ }
32
+ /** Resolves a user argument to a git log range. */
33
+ function resolveRange(arg) {
34
+ if (arg === undefined)
35
+ return `${getDefaultBaseRef()}..HEAD`;
36
+ if (/^\d+$/.test(arg))
37
+ return `-${arg}`;
38
+ return arg;
39
+ }
40
+ /** Formats raw git log output as Markdown sections. */
41
+ function formatCommits(log) {
42
+ return log
43
+ .split("\x1f")
44
+ .map(x => x.replace(/\x1e/g, "").trim())
45
+ .filter(Boolean)
46
+ .map(formatCommit)
47
+ .join("\n\n");
48
+ }
49
+ /** Formats one commit message as a Markdown heading and body. */
50
+ function formatCommit(commit) {
51
+ const [subject = "", ...bodyLines] = commit.split("\n");
52
+ const body = codeify(bodyLines.join("\n").trim());
53
+ return [`### ${subject}`, body].filter(Boolean).join("\n\n");
54
+ }
55
+ /** Adds inline-code formatting to likely code identifiers. */
56
+ function codeify(text) {
57
+ if (!autoCode)
58
+ return text;
59
+ return text
60
+ .split(/(`[^`]*`)/g)
61
+ .map(codeifyNonCodeSpan)
62
+ .join("");
63
+ }
64
+ /** Adds inline-code formatting outside existing backtick spans. */
65
+ function codeifyNonCodeSpan(part) {
66
+ if (part.startsWith("`") && part.endsWith("`"))
67
+ return part;
68
+ return (part
69
+ // Files and paths with a generic alphanumeric extension.
70
+ .replace(/(?<!`)\b([\w./-]*[\w-]+\.[A-Za-z0-9][A-Za-z0-9-]*)\b(?!`)/g, "`$1`")
71
+ // Dotfiles such as .env.
72
+ .replace(/(?<!`)(^|\s)(\.[A-Za-z0-9_-]+)(?!`)/g, "$1`$2`")
73
+ // Environment variables, optionally including simple assignments.
74
+ .replace(/(?<!`)\b([A-Z][A-Z0-9]+(?:_[A-Z0-9]+)+(?:=[^\s`]+)?)\b(?!`)/g, "`$1`")
75
+ // Package scripts or namespaced identifiers such as help:plain.
76
+ .replace(/(?<!`)\b([A-Za-z0-9_-]+:[A-Za-z0-9_:-]+)\b(?!`)/g, "`$1`")
77
+ // Short and long CLI flags such as -r and --rebuild.
78
+ .replace(/(^|\s)(?<!`)(-{1,2}[a-zA-Z0-9][a-zA-Z0-9-]*)(?!`)/g, "$1`$2`")
79
+ // Simple function or method-call expressions without nested parentheses.
80
+ .replace(/(?<!`)\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\([^`()\n]*\))(?!`)/g, "`$1`")
81
+ // Dotted identifiers such as yEditor.saveChanges.
82
+ .replace(/(?<!`)\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)\b(?!`)/g, "`$1`")
83
+ // camelCase identifiers, while avoiding all-uppercase acronyms.
84
+ .replace(/(?<!`)\b([a-z][A-Za-z0-9_$]*[A-Z][\w$]*)\b(?!`)/g, "`$1`"));
85
+ }
86
+ /** Finds the best default branch to compare the current branch against. */
87
+ function getDefaultBaseRef() {
88
+ const upstream = tryGit(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
89
+ if (upstream)
90
+ return upstream;
91
+ const originHead = tryGit(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
92
+ if (originHead)
93
+ return originHead;
94
+ for (const ref of ["origin/main", "origin/master", "main", "master"]) {
95
+ if (gitRefExists(ref))
96
+ return ref;
97
+ }
98
+ throw new Error("Could not detect base branch. Pass a range or number explicitly.");
99
+ }
100
+ /** Returns true if the given git ref exists locally. */
101
+ function gitRefExists(ref) {
102
+ return tryGit(["rev-parse", "--verify", "--quiet", ref]) !== undefined;
103
+ }
104
+ /** Runs git and returns undefined if the command fails. */
105
+ function tryGit(args) {
106
+ try {
107
+ return git(args);
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ /** Runs a git command and returns trimmed stdout. */
114
+ function git(args, options = {}) {
115
+ return execFileSync("git", args, {
116
+ encoding: "utf8",
117
+ stdio: ["ignore", "pipe", options.showErrors ? "inherit" : "ignore"],
118
+ }).trim();
119
+ }
120
+ /** Copies text to the system clipboard. */
121
+ function copyToClipboard(text) {
122
+ if (process.platform === "darwin") {
123
+ execFileSync("pbcopy", { input: text });
124
+ return;
125
+ }
126
+ if (process.platform === "win32") {
127
+ execFileSync("clip", { input: text });
128
+ return;
129
+ }
130
+ try {
131
+ execFileSync("wl-copy", { input: text });
132
+ return;
133
+ }
134
+ catch { }
135
+ try {
136
+ execFileSync("xclip", ["-selection", "clipboard"], { input: text });
137
+ return;
138
+ }
139
+ catch { }
140
+ throw new Error("Could not find a clipboard command. Install wl-copy or xclip, or pipe the output manually.");
141
+ }
142
+ /** Prints command usage and options. */
143
+ function help() {
144
+ console.log(`${colors.bold}${colors.cyan}mergelog${colors.reset}
145
+
146
+ ${colors.dim}Format git log output as Markdown for merge and pull request descriptions.${colors.reset}
147
+
148
+ ${colors.bold}Usage${colors.reset}
149
+ ${colors.green}mergelog${colors.reset}
150
+ ${colors.green}mergelog${colors.reset} ${colors.yellow}3${colors.reset}
151
+ ${colors.green}mergelog${colors.reset} ${colors.yellow}origin/main..HEAD${colors.reset}
152
+ ${colors.green}mergelog${colors.reset} ${colors.yellow}3 --no-code${colors.reset}
153
+ ${colors.green}mergelog${colors.reset} ${colors.yellow}3 --clipboard${colors.reset}
154
+
155
+ ${colors.bold}Options${colors.reset}
156
+ ${colors.yellow}-h, --help${colors.reset} Show this help screen
157
+ ${colors.yellow}-C, --no-code${colors.reset} Disable automatic backticks
158
+ ${colors.yellow}-c, --clipboard${colors.reset} Copy Markdown to the clipboard
159
+
160
+ ${colors.bold}Defaults${colors.reset}
161
+ No range: detected upstream/default branch .. HEAD
162
+ Number argument: last N commits, e.g. ${colors.yellow}3${colors.reset} -> ${colors.yellow}-3${colors.reset}
163
+
164
+ ${colors.bold}Output${colors.reset}
165
+ ### Commit subject
166
+
167
+ Commit body
168
+
169
+ Close #1234
170
+ `);
171
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mergelog",
3
+ "version": "1.0.0",
4
+ "description": "Format git log output as Markdown for merge and pull request descriptions",
5
+ "keywords": [
6
+ "git",
7
+ "git-log",
8
+ "markdown",
9
+ "merge-request",
10
+ "pull-request",
11
+ "changelog",
12
+ "cli",
13
+ "developer-tools",
14
+ "productivity"
15
+ ],
16
+ "homepage": "https://github.com/mortend/tstools/mergelog",
17
+ "bugs": {
18
+ "url": "https://github.com/mortend/tstools/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mortend/tstools.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "mortend.co",
26
+ "type": "module",
27
+ "bin": {
28
+ "mergelog": "dist/mergelog.js"
29
+ },
30
+ "scripts": {
31
+ "prepack": "cp ../LICENSE . && tsc",
32
+ "postpack": "rm -f LICENSE",
33
+ "test": "tsx mergelog.ts"
34
+ },
35
+ "files": [
36
+ "dist/*",
37
+ "LICENSE",
38
+ "README.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22"
42
+ }
43
+ }