-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathGitUtils.js
132 lines (116 loc) · 4.87 KB
/
GitUtils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
const _ = require('underscore');
const {spawn, execSync} = require('child_process');
const CONST = require('./CONST');
const sanitizeStringForJSONParse = require('./sanitizeStringForJSONParse');
const {getPreviousVersion, SEMANTIC_VERSION_LEVELS} = require('../libs/versionUpdater');
/**
* @param {String} tag
*/
function fetchTag(tag) {
const previousPatchVersion = getPreviousVersion(tag, SEMANTIC_VERSION_LEVELS.PATCH);
try {
let command = `git fetch origin tag ${tag} --no-tags`;
// Exclude commits reachable from the previous patch version (i.e: previous checklist),
// so that we don't have to fetch the full history
// Note that this condition would only ever _not_ be true in the 1.0.0-0 edge case
if (previousPatchVersion !== tag) {
command += ` --shallow-exclude=${previousPatchVersion}`;
}
console.log(`Running command: ${command}`);
execSync(command);
} catch (e) {
// This can happen if the tag was only created locally but does not exist in the remote. In this case, we'll fetch history of the staging branch instead
const command = `git fetch origin staging --no-tags --shallow-exclude=${previousPatchVersion}`;
console.log(`Running command: ${command}`);
execSync(command);
}
}
/**
* Get merge logs between two tags (inclusive) as a JavaScript object.
*
* @param {String} fromTag
* @param {String} toTag
* @returns {Promise<Array<Object<{commit: String, subject: String, authorName: String}>>>}
*/
function getCommitHistoryAsJSON(fromTag, toTag) {
fetchTag(fromTag);
fetchTag(toTag);
console.log('Getting pull requests merged between the following tags:', fromTag, toTag);
return new Promise((resolve, reject) => {
let stdout = '';
let stderr = '';
const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`];
console.log(`Running command: git ${args.join(' ')}`);
const spawnedProcess = spawn('git', args);
spawnedProcess.on('message', console.log);
spawnedProcess.stdout.on('data', (chunk) => {
console.log(chunk.toString());
stdout += chunk.toString();
});
spawnedProcess.stderr.on('data', (chunk) => {
console.error(chunk.toString());
stderr += chunk.toString();
});
spawnedProcess.on('close', (code) => {
if (code !== 0) {
return reject(new Error(`${stderr}`));
}
resolve(stdout);
});
spawnedProcess.on('error', (err) => reject(err));
}).then((stdout) => {
// Sanitize just the text within commit subjects as that's the only potentially un-parseable text.
const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => sanitizeStringForJSONParse(subject));
// Then remove newlines, format as JSON and convert to a proper JS object
const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]');
return JSON.parse(json);
});
}
/**
* Parse merged PRs, excluding those from irrelevant branches.
*
* @param {Array<Object<{commit: String, subject: String, authorName: String}>>} commits
* @returns {Array<String>}
*/
function getValidMergedPRs(commits) {
const mergedPRs = new Set();
_.each(commits, (commit) => {
const author = commit.authorName;
if (author === CONST.OS_BOTIFY) {
return;
}
const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/);
if (!_.isArray(match) || match.length < 2) {
return;
}
const pr = match[1];
if (mergedPRs.has(pr)) {
// If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist.
// That also means that we don't want to include it in the current checklist, so we remove it now.
mergedPRs.delete(pr);
return;
}
mergedPRs.add(pr);
});
return Array.from(mergedPRs);
}
/**
* Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags
*
* @param {String} fromTag
* @param {String} toTag
* @returns {Promise<Array<String>>} – Pull request numbers
*/
function getPullRequestsMergedBetween(fromTag, toTag) {
return getCommitHistoryAsJSON(fromTag, toTag).then((commitList) => {
console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList);
// Find which commit messages correspond to merged PR's
const pullRequestNumbers = getValidMergedPRs(commitList);
console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers);
return pullRequestNumbers;
});
}
module.exports = {
getValidMergedPRs,
getPullRequestsMergedBetween,
};