1 | const gitLogParser = require('git-log-parser');
|
2 | const getStream = require('get-stream');
|
3 | const execa = require('execa');
|
4 | const debug = require('debug')('semantic-release:git');
|
5 | const {GIT_NOTE_REF} = require('./definitions/constants');
|
6 |
|
7 | Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
|
8 |
|
9 | /**
|
10 | * Get the commit sha for a given tag.
|
11 | *
|
12 | * @param {String} tagName Tag name for which to retrieve the commit sha.
|
13 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
14 | *
|
15 | * @return {String} The commit sha of the tag in parameter or `null`.
|
16 | */
|
17 | async function getTagHead(tagName, execaOptions) {
|
18 | return (await execa('git', ['rev-list', '-1', tagName], execaOptions)).stdout;
|
19 | }
|
20 |
|
21 | /**
|
22 | * Get all the tags for a given branch.
|
23 | *
|
24 | * @param {String} branch The branch for which to retrieve the tags.
|
25 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
26 | *
|
27 | * @return {Array<String>} List of git tags.
|
28 | * @throws {Error} If the `git` command fails.
|
29 | */
|
30 | async function getTags(branch, execaOptions) {
|
31 | return (await execa('git', ['tag', '--merged', branch], execaOptions)).stdout
|
32 | .split('\n')
|
33 | .map((tag) => tag.trim())
|
34 | .filter(Boolean);
|
35 | }
|
36 |
|
37 | /**
|
38 | * Retrieve a range of commits.
|
39 | *
|
40 | * @param {String} from to includes all commits made after this sha (does not include this sha).
|
41 | * @param {String} to to includes all commits made before this sha (also include this sha).
|
42 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
43 | * @return {Promise<Array<Object>>} The list of commits between `from` and `to`.
|
44 | */
|
45 | async function getCommits(from, to, execaOptions) {
|
46 | return (
|
47 | await getStream.array(
|
48 | gitLogParser.parse(
|
49 | {_: `${from ? from + '..' : ''}${to}`},
|
50 | {cwd: execaOptions.cwd, env: {...process.env, ...execaOptions.env}}
|
51 | )
|
52 | )
|
53 | ).map(({message, gitTags, ...commit}) => ({...commit, message: message.trim(), gitTags: gitTags.trim()}));
|
54 | }
|
55 |
|
56 | /**
|
57 | * Get all the repository branches.
|
58 | *
|
59 | * @param {String} repositoryUrl The remote repository URL.
|
60 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
61 | *
|
62 | * @return {Array<String>} List of git branches.
|
63 | * @throws {Error} If the `git` command fails.
|
64 | */
|
65 | async function getBranches(repositoryUrl, execaOptions) {
|
66 | return (await execa('git', ['ls-remote', '--heads', repositoryUrl], execaOptions)).stdout
|
67 | .split('\n')
|
68 | .filter(Boolean)
|
69 | .map((branch) => branch.match(/^.+refs\/heads\/(?<branch>.+)$/)[1]);
|
70 | }
|
71 |
|
72 | /**
|
73 | * Verify if the `ref` exits
|
74 | *
|
75 | * @param {String} ref The reference to verify.
|
76 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
77 | *
|
78 | * @return {Boolean} `true` if the reference exists, falsy otherwise.
|
79 | */
|
80 | async function isRefExists(ref, execaOptions) {
|
81 | try {
|
82 | return (await execa('git', ['rev-parse', '--verify', ref], execaOptions)).exitCode === 0;
|
83 | } catch (error) {
|
84 | debug(error);
|
85 | }
|
86 | }
|
87 |
|
88 | /**
|
89 | * Fetch all the tags from a branch. Unshallow if necessary.
|
90 | * This will update the local branch from the latest on the remote if:
|
91 | * - The branch is not the one that triggered the CI
|
92 | * - The CI created a detached head
|
93 | *
|
94 | * Otherwise it just calls `git fetch` without specifying the `refspec` option to avoid overwritting the head commit set by the CI.
|
95 | *
|
96 | * The goal is to retrieve the informations on all the release branches without "disturbing" the CI, leaving the trigger branch or the detached head intact.
|
97 | *
|
98 | * @param {String} repositoryUrl The remote repository URL.
|
99 | * @param {String} branch The repository branch to fetch.
|
100 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
101 | */
|
102 | async function fetch(repositoryUrl, branch, ciBranch, execaOptions) {
|
103 | const isDetachedHead =
|
104 | (await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {...execaOptions, reject: false})).stdout === 'HEAD';
|
105 |
|
106 | try {
|
107 | await execa(
|
108 | 'git',
|
109 | [
|
110 | 'fetch',
|
111 | '--unshallow',
|
112 | '--tags',
|
113 | ...(branch === ciBranch && !isDetachedHead
|
114 | ? [repositoryUrl]
|
115 | : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
116 | ],
|
117 | execaOptions
|
118 | );
|
119 | } catch (_) {
|
120 | await execa(
|
121 | 'git',
|
122 | [
|
123 | 'fetch',
|
124 | '--tags',
|
125 | ...(branch === ciBranch && !isDetachedHead
|
126 | ? [repositoryUrl]
|
127 | : ['--update-head-ok', repositoryUrl, `+refs/heads/${branch}:refs/heads/${branch}`]),
|
128 | ],
|
129 | execaOptions
|
130 | );
|
131 | }
|
132 | }
|
133 |
|
134 | /**
|
135 | * Unshallow the git repository if necessary and fetch all the notes.
|
136 | *
|
137 | * @param {String} repositoryUrl The remote repository URL.
|
138 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
139 | */
|
140 | async function fetchNotes(repositoryUrl, execaOptions) {
|
141 | try {
|
142 | await execa(
|
143 | 'git',
|
144 | ['fetch', '--unshallow', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`],
|
145 | execaOptions
|
146 | );
|
147 | } catch (_) {
|
148 | await execa('git', ['fetch', repositoryUrl, `+refs/notes/${GIT_NOTE_REF}:refs/notes/${GIT_NOTE_REF}`], {
|
149 | ...execaOptions,
|
150 | reject: false,
|
151 | });
|
152 | }
|
153 | }
|
154 |
|
155 | /**
|
156 | * Get the HEAD sha.
|
157 | *
|
158 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
159 | *
|
160 | * @return {String} the sha of the HEAD commit.
|
161 | */
|
162 | async function getGitHead(execaOptions) {
|
163 | return (await execa('git', ['rev-parse', 'HEAD'], execaOptions)).stdout;
|
164 | }
|
165 |
|
166 | /**
|
167 | * Get the repository remote URL.
|
168 | *
|
169 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
170 | *
|
171 | * @return {string} The value of the remote git URL.
|
172 | */
|
173 | async function repoUrl(execaOptions) {
|
174 | try {
|
175 | return (await execa('git', ['config', '--get', 'remote.origin.url'], execaOptions)).stdout;
|
176 | } catch (error) {
|
177 | debug(error);
|
178 | }
|
179 | }
|
180 |
|
181 | /**
|
182 | * Test if the current working directory is a Git repository.
|
183 | *
|
184 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
185 | *
|
186 | * @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise.
|
187 | */
|
188 | async function isGitRepo(execaOptions) {
|
189 | try {
|
190 | return (await execa('git', ['rev-parse', '--git-dir'], execaOptions)).exitCode === 0;
|
191 | } catch (error) {
|
192 | debug(error);
|
193 | }
|
194 | }
|
195 |
|
196 | /**
|
197 | * Verify the write access authorization to remote repository with push dry-run.
|
198 | *
|
199 | * @param {String} repositoryUrl The remote repository URL.
|
200 | * @param {String} branch The repository branch for which to verify write access.
|
201 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
202 | *
|
203 | * @throws {Error} if not authorized to push.
|
204 | */
|
205 | async function verifyAuth(repositoryUrl, branch, execaOptions) {
|
206 | try {
|
207 | await execa('git', ['push', '--dry-run', '--no-verify', repositoryUrl, `HEAD:${branch}`], execaOptions);
|
208 | } catch (error) {
|
209 | debug(error);
|
210 | throw error;
|
211 | }
|
212 | }
|
213 |
|
214 | /**
|
215 | * Tag the commit head on the local repository.
|
216 | *
|
217 | * @param {String} tagName The name of the tag.
|
218 | * @param {String} ref The Git reference to tag.
|
219 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
220 | *
|
221 | * @throws {Error} if the tag creation failed.
|
222 | */
|
223 | async function tag(tagName, ref, execaOptions) {
|
224 | await execa('git', ['tag', tagName, ref], execaOptions);
|
225 | }
|
226 |
|
227 | /**
|
228 | * Push to the remote repository.
|
229 | *
|
230 | * @param {String} repositoryUrl The remote repository URL.
|
231 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
232 | *
|
233 | * @throws {Error} if the push failed.
|
234 | */
|
235 | async function push(repositoryUrl, execaOptions) {
|
236 | await execa('git', ['push', '--tags', repositoryUrl], execaOptions);
|
237 | }
|
238 |
|
239 | /**
|
240 | * Push notes to the remote repository.
|
241 | *
|
242 | * @param {String} repositoryUrl The remote repository URL.
|
243 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
244 | *
|
245 | * @throws {Error} if the push failed.
|
246 | */
|
247 | async function pushNotes(repositoryUrl, execaOptions) {
|
248 | await execa('git', ['push', repositoryUrl, `refs/notes/${GIT_NOTE_REF}`], execaOptions);
|
249 | }
|
250 |
|
251 | /**
|
252 | * Verify a tag name is a valid Git reference.
|
253 | *
|
254 | * @param {String} tagName the tag name to verify.
|
255 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
256 | *
|
257 | * @return {Boolean} `true` if valid, falsy otherwise.
|
258 | */
|
259 | async function verifyTagName(tagName, execaOptions) {
|
260 | try {
|
261 | return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOptions)).exitCode === 0;
|
262 | } catch (error) {
|
263 | debug(error);
|
264 | }
|
265 | }
|
266 |
|
267 | /**
|
268 | * Verify a branch name is a valid Git reference.
|
269 | *
|
270 | * @param {String} branch the branch name to verify.
|
271 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
272 | *
|
273 | * @return {Boolean} `true` if valid, falsy otherwise.
|
274 | */
|
275 | async function verifyBranchName(branch, execaOptions) {
|
276 | try {
|
277 | return (await execa('git', ['check-ref-format', `refs/heads/${branch}`], execaOptions)).exitCode === 0;
|
278 | } catch (error) {
|
279 | debug(error);
|
280 | }
|
281 | }
|
282 |
|
283 | /**
|
284 | * Verify the local branch is up to date with the remote one.
|
285 | *
|
286 | * @param {String} repositoryUrl The remote repository URL.
|
287 | * @param {String} branch The repository branch for which to verify status.
|
288 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
289 | *
|
290 | * @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise.
|
291 | */
|
292 | async function isBranchUpToDate(repositoryUrl, branch, execaOptions) {
|
293 | return (
|
294 | (await getGitHead(execaOptions)) ===
|
295 | (await execa('git', ['ls-remote', '--heads', repositoryUrl, branch], execaOptions)).stdout.match(/^(?<ref>\w+)?/)[1]
|
296 | );
|
297 | }
|
298 |
|
299 | /**
|
300 | * Get and parse the JSON note of a given reference.
|
301 | *
|
302 | * @param {String} ref The Git reference for which to retrieve the note.
|
303 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
304 | *
|
305 | * @return {Object} the parsed JSON note if there is one, an empty object otherwise.
|
306 | */
|
307 | async function getNote(ref, execaOptions) {
|
308 | try {
|
309 | return JSON.parse((await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'show', ref], execaOptions)).stdout);
|
310 | } catch (error) {
|
311 | if (error.exitCode === 1) {
|
312 | return {};
|
313 | }
|
314 |
|
315 | debug(error);
|
316 | throw error;
|
317 | }
|
318 | }
|
319 |
|
320 | /**
|
321 | * Get and parse the JSON note of a given reference.
|
322 | *
|
323 | * @param {Object} note The object to save in the reference note.
|
324 | * @param {String} ref The Git reference to add the note to.
|
325 | * @param {Object} [execaOpts] Options to pass to `execa`.
|
326 | */
|
327 | async function addNote(note, ref, execaOptions) {
|
328 | await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-f', '-m', JSON.stringify(note), ref], execaOptions);
|
329 | }
|
330 |
|
331 | module.exports = {
|
332 | getTagHead,
|
333 | getTags,
|
334 | getCommits,
|
335 | getBranches,
|
336 | isRefExists,
|
337 | fetch,
|
338 | fetchNotes,
|
339 | getGitHead,
|
340 | repoUrl,
|
341 | isGitRepo,
|
342 | verifyAuth,
|
343 | tag,
|
344 | push,
|
345 | pushNotes,
|
346 | verifyTagName,
|
347 | isBranchUpToDate,
|
348 | verifyBranchName,
|
349 | getNote,
|
350 | addNote,
|
351 | };
|