UNPKG

6.9 kBJavaScriptView Raw
1const execa = require('execa');
2const { pipeP, split } = require('ramda');
3const fse = require('fs-extra');
4const path = require('path');
5const tempy = require('tempy');
6const fileUrl = require('file-url');
7const gitLogParser = require('git-log-parser');
8const pEachSeries = require('p-each-series');
9const getStream = require('get-stream');
10
11const git = async (args, options = {}) => {
12 const { stdout } = await execa('git', args, options);
13 return stdout;
14};
15
16/**
17 * // https://stackoverflow.com/questions/424071/how-to-list-all-the-files-in-a-commit
18 * @async
19 * @param hash Git commit hash.
20 * @return {Promise<Array>} List of modified files in a commit.
21 */
22const getCommitFiles = pipeP(
23 hash =>
24 git(['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', hash]),
25 split('\n')
26);
27
28/**
29 * https://stackoverflow.com/a/957978/89594
30 * @async
31 * @return {Promise<String>} System path of the git repository.
32 */
33const getRoot = () => git(['rev-parse', '--show-toplevel']);
34
35/**
36 * Create commits on the current git repository.
37 *
38 * @param {Array<string>} messages Commit messages.
39 * @param {Object} [execaOpts] Options to pass to `execa`.
40 *
41 * @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
42 */
43const gitCommitsWithFiles = async commits => {
44 for (const commit of commits) {
45 for (const file of commit.files) {
46 let filePath = path.join(process.cwd(), file.name);
47 await fse.outputFile(
48 filePath,
49 (file.body = !'undefined' ? file.body : commit.message)
50 );
51 await execa('git', ['add', filePath]);
52 }
53 await execa('git', [
54 'commit',
55 '-m',
56 commit.message,
57 '--allow-empty',
58 '--no-gpg-sign',
59 ]);
60 }
61 return (await gitGetCommits(undefined)).slice(0, commits.length);
62};
63
64/**
65 * Initialize git repository
66 * If `withRemote` is `true`, creates a bare repository and initialize it.
67 * If `withRemote` is `false`, creates a regular repository and initialize it.
68 *
69 * @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
70 * @return {{cwd: string, repositoryUrl: string}} The path of the repository
71 */
72const initGit = async withRemote => {
73 const cwd = tempy.directory();
74 const args = withRemote
75 ? ['--bare', '--initial-branch=master']
76 : ['--initial-branch=master'];
77
78 await execa('git', ['init', ...args], { cwd }).catch(async () => {
79 const args = withRemote ? ['--bare'] : [];
80 return await execa('git', ['init', ...args], { cwd });
81 });
82 const repositoryUrl = fileUrl(cwd);
83 return { cwd, repositoryUrl };
84};
85
86/**
87 * Create commits on the current git repository.
88 *
89 * @param {Array<string>} messages Commit messages.
90 * @param {Object} [execaOpts] Options to pass to `execa`.
91 *
92 * @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
93 */
94const gitCommits = async (messages, execaOptions) => {
95 await pEachSeries(
96 messages,
97 async message =>
98 (
99 await execa(
100 'git',
101 ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'],
102 execaOptions
103 )
104 ).stdout
105 );
106 return (await gitGetCommits(undefined, execaOptions)).slice(
107 0,
108 messages.length
109 );
110};
111
112/**
113 * Get the list of parsed commits since a git reference.
114 *
115 * @param {String} [from] Git reference from which to seach commits.
116 * @param {Object} [execaOpts] Options to pass to `execa`.
117 *
118 * @return {Array<Commit>} The list of parsed commits.
119 */
120gitGetCommits = async from => {
121 Object.assign(gitLogParser.fields, {
122 hash: 'H',
123 message: 'B',
124 gitTags: 'd',
125 committerDate: { key: 'ci', type: Date },
126 });
127 return (
128 await getStream.array(
129 gitLogParser.parse(
130 { _: `${from ? from + '..' : ''}HEAD` },
131 { env: { ...process.env } }
132 )
133 )
134 ).map(commit => {
135 commit.message = commit.message.trim();
136 commit.gitTags = commit.gitTags.trim();
137 return commit;
138 });
139};
140
141/**
142 * Initialize an existing bare repository:
143 * - Clone the repository
144 * - Change the current working directory to the clone root
145 * - Create a default branch
146 * - Create an initial commits
147 * - Push to origin
148 *
149 * @param {String} repositoryUrl The URL of the bare repository.
150 * @param {String} [branch='master'] the branch to initialize.
151 */
152const initBareRepo = async (repositoryUrl, branch = 'master') => {
153 const cwd = tempy.directory();
154 await execa('git', ['clone', '--no-hardlinks', repositoryUrl, cwd], { cwd });
155 await gitCheckout(branch, true, { cwd });
156 gitCommits(['Initial commit'], { cwd });
157 await execa('git', ['push', repositoryUrl, branch], { cwd });
158};
159
160/**
161 * Create a temporary git repository.
162 * If `withRemote` is `true`, creates a shallow clone. Change the current working directory to the clone root.
163 * If `withRemote` is `false`, just change the current working directory to the repository root.
164 *
165 *
166 * @param {Boolean} withRemote `true` to create a shallow clone of a bare repository.
167 * @param {String} [branch='master'] The branch to initialize.
168 * @return {String} The path of the clone if `withRemote` is `true`, the path of the repository otherwise.
169 */
170const initGitRepo = async (withRemote, branch = 'master') => {
171 let { cwd, repositoryUrl } = await initGit(withRemote);
172 if (withRemote) {
173 await initBareRepo(repositoryUrl, branch);
174 cwd = gitShallowClone(repositoryUrl, branch);
175 } else {
176 await gitCheckout(branch, true, { cwd });
177 }
178
179 await execa('git', ['config', 'commit.gpgsign', false], { cwd });
180
181 return { cwd, repositoryUrl };
182};
183
184/**
185 * Create a shallow clone of a git repository and change the current working directory to the cloned repository root.
186 * The shallow will contain a limited number of commit and no tags.
187 *
188 * @param {String} repositoryUrl The path of the repository to clone.
189 * @param {String} [branch='master'] the branch to clone.
190 * @param {Number} [depth=1] The number of commit to clone.
191 * @return {String} The path of the cloned repository.
192 */
193const gitShallowClone = (repositoryUrl, branch = 'master', depth = 1) => {
194 const cwd = tempy.directory();
195
196 execa(
197 'git',
198 [
199 'clone',
200 '--no-hardlinks',
201 '--no-tags',
202 '-b',
203 branch,
204 '--depth',
205 depth,
206 repositoryUrl,
207 cwd,
208 ],
209 {
210 cwd,
211 }
212 );
213 return cwd;
214};
215
216/**
217 * Checkout a branch on the current git repository.
218 *
219 * @param {String} branch Branch name.
220 * @param {Boolean} create to create the branch, `false` to checkout an existing branch.
221 * @param {Object} [execaOptions] Options to pass to `execa`.
222 */
223const gitCheckout = async (branch, create, execaOptions) => {
224 await execa(
225 'git',
226 create ? ['checkout', '-b', branch] : ['checkout', branch],
227 execaOptions
228 );
229};
230
231module.exports = {
232 getCommitFiles,
233 getRoot,
234 gitCommitsWithFiles,
235 initGitRepo,
236 initGit,
237 initBareRepo,
238};