wukong-gitlog-cli 0.0.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/.editorconfig +10 -0
- package/.eslintignore +8 -0
- package/.eslintrc +62 -0
- package/.prettierignore +13 -0
- package/.prettierrc +21 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/wukong-gitlog-cli +2 -0
- package/jsconfig.json +20 -0
- package/package.json +100 -0
- package/scripts/esbuild.config.mjs +1 -0
- package/src/cli.mjs +112 -0
- package/src/excel.mjs +52 -0
- package/src/git.mjs +48 -0
- package/src/text.mjs +56 -0
- package/src/utils/file.mjs +28 -0
- package/src/utils/index.mjs +2 -0
- package/src/utils/output.mjs +21 -0
package/.editorconfig
ADDED
package/.eslintignore
ADDED
package/.eslintrc
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"browser": true,
|
|
4
|
+
"es6": true,
|
|
5
|
+
"node": true
|
|
6
|
+
},
|
|
7
|
+
"extends": ["airbnb-base", "plugin:prettier/recommended"],
|
|
8
|
+
"parserOptions": {
|
|
9
|
+
"ecmaVersion": 2022,
|
|
10
|
+
"sourceType": "module"
|
|
11
|
+
},
|
|
12
|
+
"globals": {
|
|
13
|
+
"GLOBAL": "writable",
|
|
14
|
+
"$": "readonly",
|
|
15
|
+
"cd": "readonly",
|
|
16
|
+
"fetch": "readonly",
|
|
17
|
+
"question": "readonly",
|
|
18
|
+
"chalk": "readonly",
|
|
19
|
+
"argv": "readonly"
|
|
20
|
+
},
|
|
21
|
+
"plugins": ["prettier"],
|
|
22
|
+
"rules": {
|
|
23
|
+
"array-callback-return": "off",
|
|
24
|
+
"consistent-return": "off",
|
|
25
|
+
"no-await-in-loop": "warn",
|
|
26
|
+
"no-unused-vars": "warn",
|
|
27
|
+
"no-console": "off",
|
|
28
|
+
"no-plusplus": "off",
|
|
29
|
+
"no-unresolved": "off",
|
|
30
|
+
"import/no-unresolved": "off",
|
|
31
|
+
"import/no-named-as-default": "off",
|
|
32
|
+
"no-restricted-syntax": "off",
|
|
33
|
+
"no-underscore-dangle": "off",
|
|
34
|
+
"no-unused-expressions": "off",
|
|
35
|
+
"no-useless-constructor": "warn",
|
|
36
|
+
"no-useless-escape": "off",
|
|
37
|
+
"prettier/prettier": "off",
|
|
38
|
+
"import/prefer-default-export": "off",
|
|
39
|
+
"import/no-named-as-default-member": "off",
|
|
40
|
+
"import/extensions": "off",
|
|
41
|
+
"import/no-extraneous-dependencies": [
|
|
42
|
+
"off",
|
|
43
|
+
{
|
|
44
|
+
"devDependencies": true,
|
|
45
|
+
"peerDependencies": true
|
|
46
|
+
// optionalDependencies: true,
|
|
47
|
+
// bundledDependencies: true
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"settings": {
|
|
52
|
+
"import/resolver": {
|
|
53
|
+
"alias": [["@", "./src"]]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"overrides": [
|
|
57
|
+
{
|
|
58
|
+
"files": ["*.js", "*.mjs"]
|
|
59
|
+
// ... 其他规则配置
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bracketSpacing": true,
|
|
3
|
+
"importOrder": [
|
|
4
|
+
"^./conf(.*)(?<!.css)$",
|
|
5
|
+
"^(#|#company|#command|#ui|components|#utils|#constants||#lib|utils|config|vendored-lib)(/(.*)(?<!.css)$)",
|
|
6
|
+
"^(src|@)/(.*)(?<!.css)$",
|
|
7
|
+
"^[./]",
|
|
8
|
+
"^api/(.*)(?<!.css)$",
|
|
9
|
+
"^.+\\.s?css$"
|
|
10
|
+
],
|
|
11
|
+
"importOrderSeparation": true,
|
|
12
|
+
"importOrderSortSpecifiers": true,
|
|
13
|
+
"jsxBracketSameLine": true,
|
|
14
|
+
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
|
15
|
+
"printWidth": 80,
|
|
16
|
+
"semi": false,
|
|
17
|
+
"singleQuote": true,
|
|
18
|
+
"tabWidth": 2,
|
|
19
|
+
"trailingComma": "none",
|
|
20
|
+
"useTabs": false
|
|
21
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
|
+
|
|
5
|
+
### 0.0.1 (2025-11-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 🎸 文件结构优化 ([2bcedad](https://github.com/tomatobybike/wukong-gitlog-cli/commit/2bcedad8c8df7f41e0c8a03cb951c783343037b7))
|
|
11
|
+
* 🎸 增加output目录 ([c5bdf9d](https://github.com/tomatobybike/wukong-gitlog-cli/commit/c5bdf9d4f52f39bd7d580318bafc8ba4b6c129bc))
|
|
12
|
+
* 🎸 init ([ea82531](https://github.com/tomatobybike/wukong-gitlog-cli/commit/ea8253186a2fbf968c04b501ce180afc788e7b0f))
|
|
13
|
+
* 🎸 package.json ([5b6e1d4](https://github.com/tomatobybike/wukong-gitlog-cli/commit/5b6e1d49cf61a4254584a4e1e737099db2e4adba))
|
|
14
|
+
* 🎸 script ([94ea5e1](https://github.com/tomatobybike/wukong-gitlog-cli/commit/94ea5e1c5c6338f5da51d3e59aae059a98a333ac))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* 🐛 eslint ([9edf3cb](https://github.com/tomatobybike/wukong-gitlog-cli/commit/9edf3cb94bafe65c2eb94a22bb68f9b3fb9a9b9b))
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] [杨琼]
|
|
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,249 @@
|
|
|
1
|
+
# wukong-gitlog-cli
|
|
2
|
+
|
|
3
|
+
Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Export commit logs to JSON / text or Excel (XLSX)
|
|
10
|
+
- Group commits by date (day / month)
|
|
11
|
+
- Include a daily stats sheet in Excel
|
|
12
|
+
- Optional Gerrit links per commit (custom template or prefix)
|
|
13
|
+
- Small, dependency-friendly CLI using ZX and ExcelJS
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What's new
|
|
18
|
+
|
|
19
|
+
- `--gerrit` option to show Gerrit links in text/excel/json output
|
|
20
|
+
- `--out-dir` / `--out-parent` to control where output files are written (useful to avoid committing generated files)
|
|
21
|
+
- `npm` demo scripts for quickly running examples (`cli:text-demo`, `cli:excel-demo`, `cli:json-demo`, `cli:gerrit-demo` and parent variants)
|
|
22
|
+
- `src/utils` restructured with `src/utils/index.mjs` barrel to simplify imports
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Clone and install dependencies:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/tomatoboy/wukong-gitlog-cli.git
|
|
32
|
+
cd wukong-gitlog-cli
|
|
33
|
+
npm install
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run from the repo using Node:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node ./src/cli.mjs --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
(Optional) Install globally to run with a short command:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm link
|
|
46
|
+
# then you can run
|
|
47
|
+
wukong-gitlog-cli --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node ./src/cli.mjs [options]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Command-line options:
|
|
59
|
+
|
|
60
|
+
- `--author <name>` Filter commits by author name
|
|
61
|
+
- `--email <email>` Filter commits by author email
|
|
62
|
+
- `--since <date>` Start date (e.g., 2025-01-01)
|
|
63
|
+
- `--until <date>` End date
|
|
64
|
+
- `--limit <n>` Limit number of commits
|
|
65
|
+
- `--no-merges` Exclude merge commits
|
|
66
|
+
- `--json` Output JSON
|
|
67
|
+
- `--format <type>` Output format: `text` | `excel` | `json` (default: `text`)
|
|
68
|
+
- `--group-by <type>` Group commits by date: `day` | `month`
|
|
69
|
+
- `--stats` Include a `Stats` sheet in the Excel export
|
|
70
|
+
- `--gerrit <prefix>` Show Gerrit URL for each commit (supports templates `{{hash}}` and `{{changeId}}`; `{{changeId}}` falls back to `hash` when absent)
|
|
71
|
+
- `--out <file>` Output file name (without path). Defaults: `commits.json` / `commits.txt` / `commits.xlsx`
|
|
72
|
+
- `--out-dir <dir>` Output directory path — supports relative or absolute path, e.g., `--out-dir ../output`
|
|
73
|
+
- `--out-parent` Place output in the parent directory's `output/` folder (same as `--out-dir ../output`)
|
|
74
|
+
|
|
75
|
+
> Output files are written to an `output/` directory in the current working directory.
|
|
76
|
+
>
|
|
77
|
+
> Tip: Use `--out-parent` or `--out-dir ../output` to write outputs into the parent folder's `output/` to avoid accidentally committing generated files to your repository.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Gerrit support
|
|
82
|
+
|
|
83
|
+
Use the `--gerrit` option to include a Gerrit link for each commit. You can provide a template containing `{{hash}}` to place the full commit hash into the URL, for example:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node ./src/cli.mjs --gerrit "https://gerrit.example.com/c/project/+/{{hash}}" --limit 5 --format text
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If `{{hash}}` is not present, the CLI will append the commit hash to the prefix with a `/` separator.
|
|
90
|
+
|
|
91
|
+
You can also use `{{changeId}}` in the template to reference Gerrit change id. The tool will try to extract a `Change-Id: I...` value from the commit body and replace `{{changeId}}` with it. If it can't find a `Change-Id`, the CLI will fall back to using the commit `hash`.
|
|
92
|
+
|
|
93
|
+
The Gerrit link will show up in:
|
|
94
|
+
|
|
95
|
+
- The text output if `--format text` (as a new `Gerrit` column)
|
|
96
|
+
- The Excel export as a `Gerrit` column if `--format excel`
|
|
97
|
+
- JSON output will include a `gerrit` field for each record when `--gerrit` is used
|
|
98
|
+
- JSON output will include a `gerrit` field for each record when `--gerrit` is used
|
|
99
|
+
- When `--gerrit` uses `{{changeId}}`, the CLI will try to extract `Change-Id:` from the commit body and include `changeId` and `body` in the JSON record. If no `Change-Id` is present, the CLI falls back to `hash` when forming the Gerrit URL.
|
|
100
|
+
|
|
101
|
+
Note: `--out <file>` is the filename only and the directory used to store that file depends on:
|
|
102
|
+
|
|
103
|
+
- The default directory `./output/` in the current working directory
|
|
104
|
+
- `--out-dir <dir>` to override the target folder (relative or absolute)
|
|
105
|
+
- `--out-parent` to write to the parent repository folder `../output/` (same as `--out-dir ../output`)
|
|
106
|
+
|
|
107
|
+
For example:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
node ./src/cli.mjs --out parent.json --out-parent
|
|
111
|
+
node ./src/cli.mjs --out demo.txt --out-dir ../temp
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
Export as text, grouped by month, with Gerrit links:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node ./src/cli.mjs --format text --group-by month --gerrit "https://gerrit.example.com/c/project/+/{{hash}}"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Export to Excel with stats and Gerrit URLs:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node ./src/cli.mjs --format excel --stats --gerrit "https://gerrit.example.com/c/project/+/{{hash}}"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Export raw JSON:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
node ./src/cli.mjs --json --out commits.json
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Export text to a custom directory (parent output folder):
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
node ./src/cli.mjs --out-dir ../output --format text --limit 5 --out custom1.txt
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Quick demo (npm scripts)
|
|
145
|
+
|
|
146
|
+
We provided a few convenient npm scripts to quickly run common scenarios. Run them from the project root:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# show help
|
|
150
|
+
npm run cli:help
|
|
151
|
+
|
|
152
|
+
# simple text export (commits.txt in ./output)
|
|
153
|
+
npm run cli:text-demo
|
|
154
|
+
|
|
155
|
+
# Excel export with stats (commits.xlsx + commits.txt in ./output)
|
|
156
|
+
npm run cli:excel-demo
|
|
157
|
+
|
|
158
|
+
# JSON export (commits.json in ./output)
|
|
159
|
+
npm run cli:json-demo
|
|
160
|
+
|
|
161
|
+
# Gerrit text export demo
|
|
162
|
+
npm run cli:gerrit-demo
|
|
163
|
+
# Gerrit Change-Id demo (use commit Change-Id to build Gerrit URLs when present)
|
|
164
|
+
npm run cli:gerrit-changeid-demo
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If you prefer to write output outside the project (e.g., a parent `output/` folder), we also provide `npm` scripts that run with `--out-parent`:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# text export to parent `output/`
|
|
171
|
+
npm run cli:text-demo-parent
|
|
172
|
+
|
|
173
|
+
# excel export to parent `output/`
|
|
174
|
+
npm run cli:excel-demo-parent
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Example text output (from `npm run cli:text-demo`):
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
Hash | Author | Date | Message
|
|
181
|
+
---------------------------------------------------------------------------------------------------------------------
|
|
182
|
+
c5bdf9d4 | tom | 2025-11-25 | feat: 🎸 增加output目录
|
|
183
|
+
|
|
184
|
+
ea82531 | tom | 2025-11-25 | feat: 🎸 init
|
|
185
|
+
|
|
186
|
+
741de50 | tom | 2025-11-25 | first commit
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Example JSON output (from `npm run cli:json-demo`):
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
"hash": "c5bdf9d4f52f39bd7d580318bafc8ba4b6c129bc",
|
|
195
|
+
"author": "tom",
|
|
196
|
+
"email": "",
|
|
197
|
+
"date": "2025-11-25 17:24:32 +0800",
|
|
198
|
+
"message": "feat: 🎸 增加output目录"
|
|
199
|
+
}
|
|
200
|
+
/* truncated... */
|
|
201
|
+
]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Example JSON output including `changeId`/`gerrit` when `--gerrit` uses `{{changeId}}` (if present in commit):
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
"hash": "Iabc...",
|
|
210
|
+
"author": "tom",
|
|
211
|
+
"email": "",
|
|
212
|
+
"date": "2025-11-25 17:24:32 +0800",
|
|
213
|
+
"message": "feat: add feature",
|
|
214
|
+
"body": "feat: add feature\n\nChange-Id: Iabcd123456789",
|
|
215
|
+
"changeId": "Iabcd123456789",
|
|
216
|
+
"gerrit": "https://gerrit.example.com/c/project/+/Iabcd123456789"
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Notes & Developer Info
|
|
224
|
+
|
|
225
|
+
- The CLI prints helpful messages after exporting files and writes outputs to the `output/` folder in the repo root.
|
|
226
|
+
- Internally `src/utils/index.mjs` acts as a barrel that re-exports helper functions located in `src/utils/`.
|
|
227
|
+
- If you plan to reuse the helpers in other modules, import from `./src/utils/index.mjs` explicitly.
|
|
228
|
+
- The Excel export uses `exceljs` and adds an ``autoFilter`` to the sheet header.
|
|
229
|
+
|
|
230
|
+
Suggested `.gitignore` snippet (to avoid accidentally committing generated files):
|
|
231
|
+
|
|
232
|
+
```gitignore
|
|
233
|
+
# ignore commit exports
|
|
234
|
+
output/
|
|
235
|
+
custom-output/
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Contributing
|
|
241
|
+
|
|
242
|
+
PRs are welcome — add tests and keep changes modular. If you add new CLI flags or new fields in commit records, please update this README accordingly.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
249
|
+
|
package/jsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"checkJs": false,
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"target": "esnext",
|
|
8
|
+
"moduleResolution": "Node",
|
|
9
|
+
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"#src/*": ["src/*"],
|
|
13
|
+
"#utils/*": ["src/utils/*"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"include": ["src", "bin"],
|
|
18
|
+
|
|
19
|
+
"exclude": ["node_modules", "dist", "build", "**/*.test.js"]
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wukong-gitlog-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Advanced Git commit log exporter with Excel/JSON/TXT output, grouping, stats and CLI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git",
|
|
7
|
+
"gitlog",
|
|
8
|
+
"commit-log",
|
|
9
|
+
"commits",
|
|
10
|
+
"gerrit",
|
|
11
|
+
"excel",
|
|
12
|
+
"json",
|
|
13
|
+
"text",
|
|
14
|
+
"cli",
|
|
15
|
+
"export",
|
|
16
|
+
"commit-logs",
|
|
17
|
+
"wukong-gitlog",
|
|
18
|
+
"提交",
|
|
19
|
+
"提交记录",
|
|
20
|
+
"提交日志",
|
|
21
|
+
"导出",
|
|
22
|
+
"命令行工具",
|
|
23
|
+
"Excel导出",
|
|
24
|
+
"JSON导出",
|
|
25
|
+
"文本导出",
|
|
26
|
+
"Gerrit链接"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://tomatobybike.github.io/wukong-gitlog-cli/",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/tomatobybike/wukong-gitlog-cli"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"author": "Tom <tomatobybike@gmail.com>",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"bin": {
|
|
37
|
+
"wukong-gitlog-cli": "./bin/wukong-gitlog-cli"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"cli:excel-demo": "node ./src/cli.mjs --format excel --stats --limit 5 --out commits.xlsx",
|
|
41
|
+
"cli:excel-demo-parent": "node ./src/cli.mjs --out-parent --format excel --stats --limit 5 --out commits-parent.xlsx",
|
|
42
|
+
"cli:gerrit-changeid-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{changeId}}\" --limit 5 --out commits-gerrit-changeid.txt",
|
|
43
|
+
"cli:gerrit-demo": "node ./src/cli.mjs --format text --gerrit \"https://gerrit.example.com/c/project/+/{{hash}}\" --limit 5 --out commits-gerrit.txt",
|
|
44
|
+
"cli:help": "node ./src/cli.mjs --help",
|
|
45
|
+
"cli:json-demo": "node ./src/cli.mjs --json --limit 5 --out commits.json",
|
|
46
|
+
"cli:text-demo": "node ./src/cli.mjs --format text --limit 5 --out commits.txt",
|
|
47
|
+
"cli:text-demo-parent": "node ./src/cli.mjs --out-parent --format text --limit 5 --out commits-parent.txt",
|
|
48
|
+
"format": "prettier --write \"src/**/*.{js,mjs}\"",
|
|
49
|
+
"lint": "eslint src --ext .js,.mjs src",
|
|
50
|
+
"lint:fix": "eslint src --ext .js,.mjs --fix",
|
|
51
|
+
"pack": "npm pack",
|
|
52
|
+
"build": "node scripts/esbuild.config.mjs",
|
|
53
|
+
"prepare": "husky install",
|
|
54
|
+
"prettierall": "npx prettier --write 'src/**/*.{js,jsx}'",
|
|
55
|
+
"prepublishOnly": "yarn lint && yarn build",
|
|
56
|
+
"release": "yarn release:patch && yarn push:tags && npm publish",
|
|
57
|
+
"push:tags": "git push origin main --follow-tags",
|
|
58
|
+
"release:major": "yarn lint && standard-version --release-as major",
|
|
59
|
+
"release:minor": "yarn lint && standard-version --release-as minor",
|
|
60
|
+
"release:patch": "yarn lint && standard-version --release-as patch",
|
|
61
|
+
"sort": "sort-package-json"
|
|
62
|
+
},
|
|
63
|
+
"husky": {
|
|
64
|
+
"hooks": {
|
|
65
|
+
"pre-commit": "lint-staged"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"lint-staged": {
|
|
69
|
+
"package.json": [
|
|
70
|
+
"sort-package-json"
|
|
71
|
+
],
|
|
72
|
+
"src/**/*.js": [
|
|
73
|
+
"prettier --write",
|
|
74
|
+
"eslint --fix"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"chalk": "^5.3.0",
|
|
79
|
+
"commander": "^12.0.0",
|
|
80
|
+
"dayjs": "^1.11.10",
|
|
81
|
+
"exceljs": "^4.4.0",
|
|
82
|
+
"zx": "^7.2.3"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
|
86
|
+
"husky": "^9.1.7",
|
|
87
|
+
"lint-staged": "^16.2.7",
|
|
88
|
+
"eslint": "8.57.1",
|
|
89
|
+
"eslint-config-airbnb-base": "15.0.0",
|
|
90
|
+
"eslint-config-prettier": "10.1.8",
|
|
91
|
+
"eslint-import-resolver-alias": "1.1.2",
|
|
92
|
+
"eslint-plugin-import": "2.32.0",
|
|
93
|
+
"eslint-plugin-prettier": "5.5.4",
|
|
94
|
+
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
95
|
+
"prettier": "3.6.2",
|
|
96
|
+
"prettier-plugin-packagejson": "2.5.19",
|
|
97
|
+
"sort-package-json": "3.4.0",
|
|
98
|
+
"standard-version": "9.5.0"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log('✅ Build finished')
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getGitLogs } from './git.mjs';
|
|
5
|
+
import { renderText } from './text.mjs';
|
|
6
|
+
import { exportExcel } from './excel.mjs';
|
|
7
|
+
import { groupRecords, writeJSON, writeTextFile, outputFilePath } from './utils/index.mjs';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('git-commits')
|
|
13
|
+
.description('Advanced Git commit log exporter.')
|
|
14
|
+
.option('--author <name>', '指定 author 名')
|
|
15
|
+
.option('--email <email>', '指定 email')
|
|
16
|
+
.option('--since <date>', '起始日期')
|
|
17
|
+
.option('--until <date>', '结束日期')
|
|
18
|
+
.option('--limit <n>', '限制数量', parseInt)
|
|
19
|
+
.option('--no-merges', '不包含 merge commit')
|
|
20
|
+
.option('--json', '输出 JSON')
|
|
21
|
+
.option('--format <type>', '输出格式: text | excel | json', 'text')
|
|
22
|
+
.option('--group-by <type>', '按日期分组: day | month')
|
|
23
|
+
.option('--stats', '输出每日统计数据')
|
|
24
|
+
.option('--gerrit <prefix>', '显示 Gerrit 地址,支持在 prefix 中使用 {{hash}} 占位符')
|
|
25
|
+
.option('--out <file>', '输出文件名(不含路径)')
|
|
26
|
+
.option('--out-dir <dir>', '自定义输出目录,支持相对路径或绝对路径,例如 `--out-dir ../output`')
|
|
27
|
+
.option('--out-parent', '将输出目录放到当前工程的父目录的 `output/`(等同于 `--out-dir ../output`)')
|
|
28
|
+
.parse();
|
|
29
|
+
|
|
30
|
+
const opts = program.opts();
|
|
31
|
+
|
|
32
|
+
(async () => {
|
|
33
|
+
let records = await getGitLogs(opts);
|
|
34
|
+
|
|
35
|
+
// compute output directory root if user provided one or wants parent
|
|
36
|
+
const outDir = opts.outParent
|
|
37
|
+
? path.resolve(process.cwd(), '..', 'output')
|
|
38
|
+
: opts.outDir || undefined;
|
|
39
|
+
|
|
40
|
+
// --- Gerrit 地址处理(若提供) ---
|
|
41
|
+
if (opts.gerrit) {
|
|
42
|
+
const prefix = opts.gerrit;
|
|
43
|
+
// create new array to avoid mutating function parameters (eslint: no-param-reassign)
|
|
44
|
+
records = records.map(r => {
|
|
45
|
+
let gerritUrl;
|
|
46
|
+
if (prefix.includes('{{changeId}}')) {
|
|
47
|
+
const changeId = r.changeId || r.hash;
|
|
48
|
+
gerritUrl = prefix.replace('{{changeId}}', changeId);
|
|
49
|
+
} else if (prefix.includes('{{hash}}')) {
|
|
50
|
+
gerritUrl = prefix.replace('{{hash}}', r.hash);
|
|
51
|
+
} else {
|
|
52
|
+
// append hash to prefix, ensure slash handling
|
|
53
|
+
gerritUrl = prefix.endsWith('/') ? `${prefix}${r.hash}` : `${prefix}/${r.hash}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ...r, gerrit: gerritUrl };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- 分组 ---
|
|
61
|
+
const groups = opts.groupBy ? groupRecords(records, opts.groupBy) : null;
|
|
62
|
+
|
|
63
|
+
// --- JSON ---
|
|
64
|
+
if (opts.json || opts.format === 'json') {
|
|
65
|
+
const file = opts.out || 'commits.json';
|
|
66
|
+
const filepath = outputFilePath(file, outDir);
|
|
67
|
+
|
|
68
|
+
writeJSON(filepath, groups || records);
|
|
69
|
+
console.log(chalk.green(`JSON 已导出: ${filepath}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- TEXT ---
|
|
74
|
+
if (opts.format === 'text') {
|
|
75
|
+
const file = opts.out || 'commits.txt';
|
|
76
|
+
const filepath = outputFilePath(file, outDir);
|
|
77
|
+
|
|
78
|
+
const text = renderText(records, groups, { showGerrit: !!opts.gerrit });
|
|
79
|
+
writeTextFile(filepath, text);
|
|
80
|
+
|
|
81
|
+
console.log(text);
|
|
82
|
+
console.log(chalk.green(`文本已导出: ${filepath}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- EXCEL(强制同时输出 TXT) ---
|
|
87
|
+
if (opts.format === 'excel') {
|
|
88
|
+
// Excel
|
|
89
|
+
const excelFile = opts.out || 'commits.xlsx';
|
|
90
|
+
const excelPath = outputFilePath(excelFile, outDir);
|
|
91
|
+
|
|
92
|
+
// TXT(自动附带)
|
|
93
|
+
const txtFile = excelFile.replace(/\.xlsx$/, '.txt');
|
|
94
|
+
const txtPath = outputFilePath(txtFile, outDir);
|
|
95
|
+
|
|
96
|
+
// 导出 Excel 文件
|
|
97
|
+
await exportExcel(records, groups, {
|
|
98
|
+
file: excelPath,
|
|
99
|
+
stats: opts.stats,
|
|
100
|
+
gerrit: opts.gerrit
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 导出文本
|
|
104
|
+
const text = renderText(records, groups);
|
|
105
|
+
writeTextFile(txtPath, text);
|
|
106
|
+
|
|
107
|
+
console.log(chalk.green(`Excel 已导出: ${excelPath}`));
|
|
108
|
+
console.log(chalk.green(`文本已自动导出: ${txtPath}`));
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
})();
|
package/src/excel.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ExcelJS from 'exceljs';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
export async function exportExcel(records, groups, options = {}) {
|
|
5
|
+
const { file, stats, gerrit } = options;
|
|
6
|
+
|
|
7
|
+
const wb = new ExcelJS.Workbook();
|
|
8
|
+
const ws = wb.addWorksheet('Commits');
|
|
9
|
+
|
|
10
|
+
const cols = [
|
|
11
|
+
{ header: 'Hash', key: 'hash', width: 12 },
|
|
12
|
+
{ header: 'Author', key: 'author', width: 20 },
|
|
13
|
+
{ header: 'Email', key: 'email', width: 30 },
|
|
14
|
+
{ header: 'Date', key: 'date', width: 20 },
|
|
15
|
+
{ header: 'Message', key: 'message', width: 80 }
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
if (gerrit) {
|
|
19
|
+
cols.push({ header: 'Gerrit', key: 'gerrit', width: 50 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ws.columns = cols;
|
|
23
|
+
|
|
24
|
+
(groups ? Object.values(groups).flat() : records).forEach(r =>
|
|
25
|
+
ws.addRow(r)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
ws.autoFilter = { from: { row: 1, column: 1 }, to: { row: 1, column: cols.length } };
|
|
29
|
+
|
|
30
|
+
// --- stats sheet ---
|
|
31
|
+
if (stats) {
|
|
32
|
+
const statWs = wb.addWorksheet('Stats');
|
|
33
|
+
|
|
34
|
+
const map = {};
|
|
35
|
+
|
|
36
|
+
records.forEach(r => {
|
|
37
|
+
const d = dayjs(r.date).format('YYYY-MM-DD');
|
|
38
|
+
map[d] = (map[d] || 0) + 1;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
statWs.columns = [
|
|
42
|
+
{ header: 'Date', key: 'date', width: 15 },
|
|
43
|
+
{ header: 'Commits', key: 'count', width: 15 }
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
Object.entries(map).forEach(([d, cnt]) =>
|
|
47
|
+
statWs.addRow({ date: d, count: cnt })
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await wb.xlsx.writeFile(file);
|
|
52
|
+
}
|
package/src/git.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env zx
|
|
2
|
+
import 'zx/globals'
|
|
3
|
+
|
|
4
|
+
export async function getGitLogs(opts) {
|
|
5
|
+
const { author, email, since, until, limit, merges } = opts
|
|
6
|
+
|
|
7
|
+
// include subject and full body so we can extract Change-Id from commit message
|
|
8
|
+
const pretty = '%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%B%x1e'
|
|
9
|
+
|
|
10
|
+
const args = ['log', `--pretty=format:${pretty}`, '--date=iso']
|
|
11
|
+
|
|
12
|
+
if (author) args.push(`--author=${author}`)
|
|
13
|
+
if (email) args.push(`--author=${email}`)
|
|
14
|
+
if (since) args.push(`--since=${since}`)
|
|
15
|
+
if (until) args.push(`--until=${until}`)
|
|
16
|
+
if (merges === false) args.push(`--no-merges`)
|
|
17
|
+
if (limit) args.push(`-n`, `${limit}`)
|
|
18
|
+
|
|
19
|
+
// 使用 spread 形式传参,ZX 才会正确处理
|
|
20
|
+
const { stdout } = await $`git ${args}`.quiet()
|
|
21
|
+
|
|
22
|
+
return stdout
|
|
23
|
+
.split('\x1e')
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((r) => {
|
|
26
|
+
const f = r.split('\x1f').map((s) => (s || '').trim())
|
|
27
|
+
|
|
28
|
+
const hash = f[0]
|
|
29
|
+
const authorName = f[1]
|
|
30
|
+
const emailAddr = f[2]
|
|
31
|
+
const date = f[3]
|
|
32
|
+
const subject = f[4]
|
|
33
|
+
const body = f[5] || ''
|
|
34
|
+
|
|
35
|
+
// extract Change-Id from commit body (line like "Change-Id: Iabc123...")
|
|
36
|
+
const [, changeId] = body.match(/Change-Id:\s*(I[0-9a-fA-F]+)/) || []
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
hash,
|
|
40
|
+
author: authorName,
|
|
41
|
+
email: emailAddr,
|
|
42
|
+
date,
|
|
43
|
+
message: subject,
|
|
44
|
+
body,
|
|
45
|
+
changeId
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
package/src/text.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function renderText(records, groups = null, opts = {}) {
|
|
2
|
+
const { showGerrit = false } = opts;
|
|
3
|
+
const pad = (s, n) =>
|
|
4
|
+
s.length >= n ? `${s.slice(0, n - 1) }…` : s + ' '.repeat(n - s.length);
|
|
5
|
+
|
|
6
|
+
const baseHeader =
|
|
7
|
+
`${pad('Hash', 10)
|
|
8
|
+
} | ${
|
|
9
|
+
pad('Author', 18)
|
|
10
|
+
} | ${
|
|
11
|
+
pad('Date', 20)
|
|
12
|
+
} | ${
|
|
13
|
+
pad('Message', 60)}`;
|
|
14
|
+
|
|
15
|
+
const gerritHeader = showGerrit ? ` | ${ pad('Gerrit', 50)}` : '';
|
|
16
|
+
const header = baseHeader + gerritHeader;
|
|
17
|
+
|
|
18
|
+
const line = '-'.repeat(header.length);
|
|
19
|
+
|
|
20
|
+
const rows = [];
|
|
21
|
+
|
|
22
|
+
if (groups) {
|
|
23
|
+
for (const [g, list] of Object.entries(groups)) {
|
|
24
|
+
rows.push(`\n=== ${g} ===\n`);
|
|
25
|
+
list.forEach(r => {
|
|
26
|
+
rows.push(
|
|
27
|
+
(
|
|
28
|
+
[
|
|
29
|
+
pad(r.hash.slice(0, 8), 10),
|
|
30
|
+
pad(r.author, 18),
|
|
31
|
+
pad(r.date.replace(/ .+/, ''), 20),
|
|
32
|
+
pad(r.message, 60)
|
|
33
|
+
].join(' | ') +
|
|
34
|
+
(showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
records.forEach(r => {
|
|
41
|
+
rows.push(
|
|
42
|
+
(
|
|
43
|
+
[
|
|
44
|
+
pad(r.hash.slice(0, 8), 10),
|
|
45
|
+
pad(r.author, 18),
|
|
46
|
+
pad(r.date.replace(/ .+/, ''), 20),
|
|
47
|
+
pad(r.message, 60)
|
|
48
|
+
].join(' | ') +
|
|
49
|
+
(showGerrit ? ` | ${ pad(r.gerrit || '', 50)}` : '')
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [header, line, ...rows].join('\n');
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
export function writeJSON(file, data) {
|
|
5
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function writeTextFile(file, text) {
|
|
9
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function groupRecords(records, mode) {
|
|
13
|
+
const group = {};
|
|
14
|
+
|
|
15
|
+
records.forEach(r => {
|
|
16
|
+
const date = dayjs(r.date);
|
|
17
|
+
|
|
18
|
+
const key =
|
|
19
|
+
mode === 'day'
|
|
20
|
+
? date.format('YYYY-MM-DD')
|
|
21
|
+
: date.format('YYYY-MM');
|
|
22
|
+
|
|
23
|
+
if (!group[key]) group[key] = [];
|
|
24
|
+
group[key].push(r);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return group;
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function ensureOutputDir(customDir) {
|
|
5
|
+
// If a custom absolute/relative path is provided, resolve relative to cwd as-is
|
|
6
|
+
// Otherwise default to `output` inside current working directory.
|
|
7
|
+
const dir = customDir
|
|
8
|
+
? path.resolve(process.cwd(), customDir)
|
|
9
|
+
: path.resolve(process.cwd(), 'output');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(dir)) {
|
|
12
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function outputFilePath(filename, customDir) {
|
|
19
|
+
const dir = ensureOutputDir(customDir);
|
|
20
|
+
return path.join(dir, filename);
|
|
21
|
+
}
|