UNPKG

10.5 kBJavaScriptView Raw
1const gitLogParser = require('git-log-parser');
2const getStream = require('get-stream');
3const execa = require('execa');
4const debug = require('debug')('semantic-release:git');
5const {GIT_NOTE_REF} = require('./definitions/constants');
6
7Object.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 */
17async 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 */
30async 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 */
45async 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 */
65async 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 */
80async 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 */
102async 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 */
140async 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 */
162async 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 */
173async 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 */
188async 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 */
205async 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 */
223async 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 */
235async 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 */
247async 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 */
259async 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 */
275async 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 */
292async 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 */
307async 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 * Add JSON note to 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 */
327async function addNote(note, ref, execaOptions) {
328 await execa('git', ['notes', '--ref', GIT_NOTE_REF, 'add', '-f', '-m', JSON.stringify(note), ref], execaOptions);
329}
330
331module.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};