1 | 'use strict';
|
2 |
|
3 | const { execSync } = require('child_process');
|
4 |
|
5 | const defaults = {
|
6 | authorEmailCommand: 'git log --format="%ae" -n 1',
|
7 | blameCommand: 'git blame --porcelain',
|
8 | blameEmailClean: /[<>]/g,
|
9 | branchChangesCommand: 'git diff --numstat $(git merge-base master HEAD)',
|
10 | branchCommand: 'git branch --show-current',
|
11 | changeSetCommand: 'git log --first-parent --pretty="format:%H, %aE, %cN, %s"',
|
12 | changeSetRegExp: /^(\w{40}),\s(.*?),\s(.*?),\s(.*)$/,
|
13 | configCommand: 'git config',
|
14 | emailRegExp: /(@.*)$/,
|
15 | grepCommand: 'git grep',
|
16 | mergeBaseCommand: 'git merge-base master HEAD',
|
17 | notesCommands: 'git notes',
|
18 | origin: /^origin\//,
|
19 | shaCommand: 'git rev-parse HEAD',
|
20 | shortSHACommand: 'git rev-parse --short HEAD',
|
21 | statusCommand: 'git status --branch --porcelain --untracked-files=all',
|
22 | };
|
23 |
|
24 | function gitAuthorEmail () {
|
25 | return execSync(defaults.authorEmailCommand, { cwd: process.cwd() }).toString().
|
26 | trim();
|
27 | }
|
28 |
|
29 | function gitBlame (file, lineNumber) {
|
30 | const summary = execSync(
|
31 | `${ defaults.blameCommand } -L${ lineNumber },${ lineNumber } ${ file }`,
|
32 | { cwd: process.cwd() }).toString().
|
33 | trim().
|
34 | split(/\n/);
|
35 |
|
36 | const hashInformation = summary[0].split(/\s+/);
|
37 | const blame = {
|
38 | hash: hashInformation[0],
|
39 | additions: hashInformation[1],
|
40 | deletions: hashInformation[2],
|
41 | author: { },
|
42 | committer: {},
|
43 | };
|
44 |
|
45 | for (let i = 1; i < summary.length - 1; i++) {
|
46 | const [ , type, value ] = summary[i].match(/^(.*?)\s(.*)$/);
|
47 | let previousInformation;
|
48 |
|
49 | switch (type) {
|
50 | case 'author':
|
51 | blame.author.name = value;
|
52 | break;
|
53 | case 'author-mail':
|
54 | blame.author.email = value.replace(defaults.blameEmailClean, '');
|
55 | break;
|
56 | case 'author-time':
|
57 | blame.author.date = new Date(Number(value) * 1000);
|
58 | break;
|
59 | case 'committer':
|
60 | blame.committer.name = value;
|
61 | break;
|
62 | case 'committer-mail':
|
63 | blame.committer.email = value.replace(defaults.blameEmailClean, '');
|
64 | break;
|
65 | case 'committer-time':
|
66 | blame.committer.date = new Date(Number(value) * 1000);
|
67 | break;
|
68 | case 'summary':
|
69 | blame.summary = value;
|
70 | break;
|
71 | case 'previous':
|
72 | previousInformation = value.split(/\s+/);
|
73 | blame.previous = {
|
74 | hash: previousInformation[0],
|
75 | file: previousInformation[1],
|
76 | };
|
77 | break;
|
78 | case 'filename':
|
79 | blame.file = value;
|
80 | break;
|
81 | default:
|
82 | break;
|
83 | }
|
84 | }
|
85 |
|
86 | blame.line = summary[summary.length - 1].slice(1);
|
87 |
|
88 | return blame;
|
89 | }
|
90 |
|
91 | function gitBranch () {
|
92 | let branch = process.env.GIT_BRANCH || process.env.BRANCH_NAME;
|
93 | if (!branch) {
|
94 | branch = execSync(defaults.branchCommand, { cwd: process.cwd() }).toString();
|
95 | }
|
96 | branch = branch || 'detached HEAD';
|
97 | return branch.trim().replace(defaults.origin, '');
|
98 | }
|
99 |
|
100 | function gitBranchChanges () {
|
101 | return execSync(defaults.branchChangesCommand, { cwd: process.cwd() }).toString().
|
102 | trim().
|
103 | split('\n').
|
104 | map((line) => {
|
105 | const [ additions, deletions, file ] = line.split(/\s+/);
|
106 | return {
|
107 | file,
|
108 | additions,
|
109 | deletions,
|
110 | };
|
111 | });
|
112 | }
|
113 |
|
114 | function gitChangeSet (initialCommit) {
|
115 | let command = defaults.changeSetCommand;
|
116 | if (initialCommit) {
|
117 | command += ` "${ initialCommit }..HEAD"`;
|
118 | } else if (process.env.LAST_SUCCESSFUL_COMMIT) {
|
119 | command += ` "${ process.env.LAST_SUCCESSFUL_COMMIT }..HEAD"`;
|
120 | } else {
|
121 | return null;
|
122 | }
|
123 |
|
124 | const changes = execSync(command, { cwd: process.cwd() }).toString().
|
125 | trim().
|
126 | split('\n');
|
127 |
|
128 | const changeSet = {};
|
129 |
|
130 | changes.forEach((change) => {
|
131 | if (defaults.changeSetRegExp.test(change)) {
|
132 | const [ , hash, email, name, title ] = change.match(defaults.changeSetRegExp);
|
133 |
|
134 | if (defaults.emailRegExp.test(email)) {
|
135 | const id = email.replace(defaults.emailRegExp, '').toLowerCase();
|
136 |
|
137 | changeSet[id] = changeSet[id] || {
|
138 | id,
|
139 | name,
|
140 | email,
|
141 | changes: [],
|
142 | };
|
143 |
|
144 | changeSet[id].changes.push({
|
145 | short: hash.substring(0, 12),
|
146 | hash,
|
147 | title,
|
148 | });
|
149 | }
|
150 | }
|
151 | });
|
152 |
|
153 | return changeSet;
|
154 | }
|
155 |
|
156 | function gitConfig (name, value) {
|
157 | if (name) {
|
158 | let command = `${ defaults.configCommand } ${ name }`;
|
159 | if (value) {
|
160 | command += ` ${ value }`;
|
161 | }
|
162 | return execSync(command, { cwd: process.cwd() }).toString().
|
163 | trim();
|
164 | }
|
165 | return '';
|
166 | }
|
167 |
|
168 | function gitGrep (pattern) {
|
169 | return execSync(`${ defaults.grepCommand } ${ pattern }`, { cwd: process.cwd() }).toString().
|
170 | trim().
|
171 | split('\n').
|
172 | map((line) => line.match(/^([^:]+):(.*?)$/).slice(1, 3));
|
173 | }
|
174 |
|
175 | function gitMergeBase () {
|
176 | try {
|
177 | return execSync(defaults.mergeBaseCommand, {
|
178 | cwd: process.cwd(),
|
179 | stdio: [ 'pipe', 'pipe', 'ignore' ],
|
180 | }).toString().
|
181 | trim();
|
182 | } catch (error) {
|
183 | return null;
|
184 | }
|
185 | }
|
186 |
|
187 | function gitNotesAdd (message, prefix, force) {
|
188 | try {
|
189 | let command = defaults.notesCommand;
|
190 | if (prefix) {
|
191 | command += ` --ref=${ prefix }`;
|
192 | }
|
193 |
|
194 | command += ` add -m "${ message }"`;
|
195 |
|
196 | if (force) {
|
197 | command += ' -f';
|
198 | }
|
199 |
|
200 | execSync(command, {
|
201 | cwd: process.cwd(),
|
202 | stdio: 'ignore',
|
203 | });
|
204 |
|
205 | return true;
|
206 | } catch (error) {
|
207 | return false;
|
208 | }
|
209 | }
|
210 |
|
211 | function gitNotesRemove (prefix) {
|
212 | try {
|
213 | let command = defaults.notesCommand;
|
214 | if (prefix) {
|
215 | command += ` --ref=${ prefix }`;
|
216 | }
|
217 | command += ' remove';
|
218 |
|
219 | execSync(command, {
|
220 | cwd: process.cwd(),
|
221 | stdio: 'ignore',
|
222 | });
|
223 |
|
224 | return true;
|
225 | } catch (error) {
|
226 | return false;
|
227 | }
|
228 | }
|
229 |
|
230 | function gitNotesShow (prefix) {
|
231 | try {
|
232 | let command = defaults.notesCommand;
|
233 | if (prefix) {
|
234 | command += ` --ref=${ prefix }`;
|
235 | }
|
236 | command += ' show';
|
237 |
|
238 | const notes = execSync(command, {
|
239 | cwd: process.cwd(),
|
240 | stdio: [ 'pipe', 'pipe', 'ignore' ],
|
241 | }).toString().
|
242 | trim();
|
243 |
|
244 | if (notes.startsWith('error: No note found')) {
|
245 | return null;
|
246 | }
|
247 | return notes;
|
248 | } catch (error) {
|
249 | return null;
|
250 | }
|
251 | }
|
252 |
|
253 | function gitSHA () {
|
254 | return execSync(defaults.shaCommand, { cwd: process.cwd() }).toString().
|
255 | trim();
|
256 | }
|
257 |
|
258 | function gitShortSHA () {
|
259 | return execSync(defaults.shortSHACommand, { cwd: process.cwd() }).toString().
|
260 | trim();
|
261 | }
|
262 |
|
263 | function gitStatus () {
|
264 | const status = execSync(defaults.statusCommand, { cwd: process.cwd() }).toString().
|
265 | trim().
|
266 | split('\n');
|
267 |
|
268 | let [ branch, ...changes ] = status;
|
269 |
|
270 | branch = branch.replace('## ', '');
|
271 | changes = changes.map((item) => item.replace(/^[ ][MD]\s+(.*)$/, '$1 (modified)').
|
272 | replace(/^M[ MD]\s+(.*)$/, '$1 (modified in index)').
|
273 | replace(/^A[ MD]\s+(.*)$/, '$1 (added)').
|
274 | replace(/^D[ M]\s+(.*)$/, '$1 (deleted)').
|
275 | replace(/^R[ MD]\s+(.*)$/, '$1 (renamed)').
|
276 | replace(/^C[ MD]\s+(.*)$/, '$1 (copied)').
|
277 | replace(/^[MARC][ ]\s+(.*)$/, '$1 (index and work tree matches)').
|
278 | replace(/^[ MARC]M\s+(.*)$/, '$1 (work tree changed since index)').
|
279 | replace(/^[ MARC]D\s+(.*)$/, '$1 (deleted in work tree)').
|
280 | replace(/^DD\s+(.*)$/, '$1 (unmerged, both deleted)').
|
281 | replace(/^AU\s+(.*)$/, '$1 (unmerged, added by us)').
|
282 | replace(/^UD\s+(.*)$/, '$1 (unmerged, deleted by them)').
|
283 | replace(/^UA\s+(.*)$/, '$1 (unmerged, added by them)').
|
284 | replace(/^DU\s+(.*)$/, '$1 (unmerged, deleted by us)').
|
285 | replace(/^AA\s+(.*)$/, '$1 (unmerged, both added)').
|
286 | replace(/^UU\s+(.*)$/, '$1 (unmerged, both modified)').
|
287 | replace(/^\?\?\s+(.*)$/, '$1 (untracked)').
|
288 | replace(/^!!\s+(.*)$/, '$1 (ignored)')).sort();
|
289 |
|
290 | return {
|
291 | branch,
|
292 | changes,
|
293 | clean: !changes.length,
|
294 | };
|
295 | }
|
296 |
|
297 | module.exports = {
|
298 | authorEmail: gitAuthorEmail,
|
299 | blame: gitBlame,
|
300 | branch: gitBranch,
|
301 | branchChanges: gitBranchChanges,
|
302 | changeSet: gitChangeSet,
|
303 | config: Object.assign(gitConfig, {
|
304 | alias: (name, value) => gitConfig(`alias.${ name }`, value),
|
305 | editor: (value) => gitConfig('editor', value),
|
306 | email: (value) => gitConfig('user.email', value),
|
307 | user: (value) => gitConfig('user.name', value),
|
308 | }),
|
309 | defaults,
|
310 | grep: gitGrep,
|
311 | mergeBase: gitMergeBase,
|
312 | notes: {
|
313 | add: gitNotesAdd,
|
314 | remove: gitNotesRemove,
|
315 | show: gitNotesShow,
|
316 | },
|
317 | sha: gitSHA,
|
318 | shortSHA: gitShortSHA,
|
319 | status: gitStatus,
|
320 | };
|