UNPKG

8.29 kBJavaScriptView Raw
1'use strict';
2
3const { execSync } = require('child_process');
4
5const 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
24function gitAuthorEmail () {
25 return execSync(defaults.authorEmailCommand, { cwd: process.cwd() }).toString().
26 trim();
27}
28
29function 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
91function 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
100function 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
114function 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
156function 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
168function 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
175function gitMergeBase () {
176 try {
177 return execSync(defaults.mergeBaseCommand, {
178 cwd: process.cwd(),
179 stdio: [ 'pipe', 'pipe', 'ignore' ],
180 }).toString().
181 trim();
182 } catch (error) { // Likely a shallow clone
183 return null;
184 }
185}
186
187function 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
211function 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
230function 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
253function gitSHA () {
254 return execSync(defaults.shaCommand, { cwd: process.cwd() }).toString().
255 trim();
256}
257
258function gitShortSHA () {
259 return execSync(defaults.shortSHACommand, { cwd: process.cwd() }).toString().
260 trim();
261}
262
263function 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
297module.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};