UNPKG

10.6 kBJavaScriptView Raw
1/*
2 * Copyright 2018 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13'use strict';
14
15const { resolve: resolvePath } = require('path');
16const { PassThrough } = require('stream');
17
18const fse = require('fs-extra');
19const git = require('isomorphic-git');
20git.plugins.set('fs', require('fs'));
21
22const { pathExists } = require('./utils');
23
24/**
25 * Various helper functions for reading git meta-data and content
26 */
27
28/**
29 * Returns the name (abbreviated form) of the currently checked out branch.
30 *
31 * @param {string} dir git repo path
32 * @returns {Promise<string>} name of the currently checked out branch
33 */
34async function currentBranch(dir) {
35 return git.currentBranch({ dir, fullname: false });
36}
37
38/**
39 * Parses Github url path subsegment `<ref>/<filePath>` (e.g. `master/some/file.txt`
40 * or `some/branch/some/file.txt`) and returns an `{ ref, fpath }` object.
41 *
42 * Issue #53: Handle branch names containing '/' (e.g. 'foo/bar')
43 *
44 * @param {string} dir git repo path
45 * @param {string} refPathName path including reference (branch or tag) and file path
46 * (e.g. `master/some/file.txt` or `some/branch/some/file.txt`)
47 * @returns {Promise<object>} an `{ ref, pathName }` object or `undefined` if the ref cannot
48 * be resolved to an existing branch or tag.
49 */
50async function determineRefPathName(dir, refPathName) {
51 const branches = await git.listBranches({ dir });
52 const tags = await git.listTags({ dir });
53 const refs = branches.concat(tags);
54 // find matching refs
55 const matchingRefs = refs.filter((ref) => refPathName.startsWith(`${ref}/`));
56 if (!matchingRefs.length) {
57 return undefined;
58 }
59 // find longest matching ref
60 const matchingRef = matchingRefs.reduce((a, b) => ((b.length > a.length) ? b : a));
61 return { ref: matchingRef, pathName: refPathName.substr(matchingRef.length) };
62}
63
64/**
65 * Determines whether the specified reference is currently checked out in the working dir.
66 *
67 * @param {string} dir git repo path
68 * @param {string} ref reference (branch or tag)
69 * @returns {Promise<boolean>} `true` if the specified reference is checked out
70 */
71async function isCheckedOut(dir, ref) {
72 let oidCurrent;
73 return git.resolveRef({ dir, ref: 'HEAD' })
74 .then((oid) => {
75 oidCurrent = oid;
76 return git.resolveRef({ dir, ref });
77 })
78 .then((oid) => oidCurrent === oid)
79 .catch(() => false);
80}
81
82/**
83 * Returns the commit oid of the curent commit referenced by `ref`
84 *
85 * @param {string} dir git repo path
86 * @param {string} ref reference (branch, tag or commit sha)
87 * @returns {Promise<string>} commit oid of the curent commit referenced by `ref`
88 * @throws {GitError} `err.code === 'ResolveRefError'`: invalid reference
89 */
90async function resolveCommit(dir, ref) {
91 return git.resolveRef({ dir, ref })
92 .catch(async (err) => {
93 if (err.code === 'ResolveRefError') {
94 // fallback: is ref a shortened oid prefix?
95 const oid = await git.expandOid({ dir, oid: ref }).catch(() => { throw err; });
96 return git.resolveRef({ dir, ref: oid });
97 }
98 // re-throw
99 throw err;
100 });
101}
102
103/**
104 * Returns the blob oid of the file at revision `ref` and `pathName`
105 *
106 * @param {string} dir git repo path
107 * @param {string} ref reference (branch, tag or commit sha)
108 * @param {string} filePath relative path to file
109 * @param {boolean} includeUncommitted include uncommitted changes in working dir
110 * @returns {Promise<string>} blob oid of specified file
111 * @throws {GitError} `err.code === 'TreeOrBlobNotFoundError'`: resource not found
112 * `err.code === 'ResolveRefError'`: invalid reference
113 */
114async function resolveBlob(dir, ref, pathName, includeUncommitted) {
115 const commitSha = await resolveCommit(dir, ref);
116
117 // project-helix/#150: check for uncommitted local changes
118 // project-helix/#183: serve newly created uncommitted files
119 // project-helix/#187: only serve uncommitted content if currently
120 // checked-out and requested refs match
121
122 if (!includeUncommitted) {
123 return (await git.readObject({ dir, oid: commitSha, filepath: pathName })).oid;
124 }
125 // check working dir status
126 const status = await git.status({ dir, filepath: pathName });
127 if (status.endsWith('unmodified')) {
128 return (await git.readObject({ dir, oid: commitSha, filepath: pathName })).oid;
129 }
130 if (status.endsWith('absent') || status.endsWith('deleted')) {
131 const err = new Error(`Not found: ${pathName}`);
132 err.code = git.E.TreeOrBlobNotFoundError;
133 throw err;
134 }
135 // temporary workaround for https://github.com/isomorphic-git/isomorphic-git/issues/752
136 // => remove once isomorphic-git #252 is fixed
137 if (status.endsWith('added') && !await pathExists(dir, pathName)) {
138 const err = new Error(`Not found: ${pathName}`);
139 err.code = git.E.TreeOrBlobNotFoundError;
140 throw err;
141 }
142 // return blob id representing working dir file
143 const content = await fse.readFile(resolvePath(dir, pathName));
144 return git.writeObject({
145 dir,
146 object: content,
147 type: 'blob',
148 format: 'content',
149 });
150}
151
152/**
153 * Returns the contents of the file at revision `ref` and `pathName`
154 *
155 * @param {string} dir git repo path
156 * @param {string} ref reference (branch, tag or commit sha)
157 * @param {string} filePath relative path to file
158 * @param {boolean} includeUncommitted include uncommitted changes in working dir
159 * @returns {Promise<Buffer>} content of specified file
160 * @throws {GitError} `err.code === 'TreeOrBlobNotFoundError'`: resource not found
161 * `err.code === 'ResolveRefError'`: invalid reference
162 */
163async function getRawContent(dir, ref, pathName, includeUncommitted) {
164 return resolveBlob(dir, ref, pathName, includeUncommitted)
165 .then((oid) => git.readObject({ dir, oid, format: 'content' }).object);
166}
167
168/**
169 * Returns a stream for reading the specified blob.
170 *
171 * @param {string} dir git repo path
172 * @param {string} oid blob sha1
173 * @returns {Promise<Stream>} readable Stream instance
174 */
175async function createBlobReadStream(dir, oid) {
176 const { object: content } = await git.readObject({ dir, oid });
177 const stream = new PassThrough();
178 stream.end(content);
179 return stream;
180}
181
182/**
183 * Retrieves the specified object from the loose object store.
184 *
185 * @param {string} dir git repo path
186 * @param {string} oid object id
187 * @returns {Promise<Object>} object identified by `oid`
188 */
189async function getObject(dir, oid) {
190 return git.readObject({ dir, oid });
191}
192
193/**
194 * Checks if the specified string is a valid SHA-1 value.
195 *
196 * @param {string} str
197 * @returns {boolean} `true` if `str` represents a valid SHA-1, otherwise `false`
198 */
199function isValidSha(str) {
200 if (typeof str === 'string' && str.length === 40) {
201 const res = str.match(/[0-9a-f]/g);
202 return res && res.length === 40;
203 }
204 return false;
205}
206
207/**
208 * Returns the tree object identified directly by its sha
209 * or indirectly via reference (branch, tag or commit sha)
210 *
211 * @param {string} dir git repo path
212 * @param {string} refOrSha either tree sha or reference (branch, tag or commit sha)
213 * @returns {Promise<string>} commit oid of the curent commit referenced by `ref`
214 * @throws {GitError} `err.code === 'ResolveRefError'`: invalid reference
215 * `err.code === 'ReadObjectFail'`: not found
216 */
217async function resolveTree(dir, refOrSha) {
218 if (isValidSha(refOrSha)) {
219 // full commit or tree sha
220 return git.readObject({ dir, oid: refOrSha })
221 .then((obj) => {
222 if (obj.type === 'tree') {
223 return obj;
224 }
225 if (obj.type === 'commit') {
226 return git.readObject({ dir, oid: obj.object.tree });
227 }
228 throw new Error(`unexpected object: ${obj}`);
229 });
230 }
231 // reference (branch, tag, shorthand commit sha)
232 return resolveCommit(dir, refOrSha)
233 .then((oid) => git.readObject({ dir, oid }))
234 .then((obj) => git.readObject({ dir, oid: obj.object.tree }));
235}
236
237/**
238 * Returns a commit log, i.e. an array of commits in reverse chronological order.
239 *
240 * @param {string} dir git repo path
241 * @param {string} ref reference (branch, tag or commit sha)
242 * @param {string} path only commits containing this file path will be returned
243 * @throws {GitError} `err.code === 'ResolveRefError'`: invalid reference
244 * `err.code === 'ReadObjectFail'`: not found
245 */
246async function commitLog(dir, ref, path) {
247 return git.log({ dir, ref, path })
248 .catch(async (err) => {
249 if (err.code === 'ResolveRefError') {
250 // fallback: is ref a shortened oid prefix?
251 const oid = await git.expandOid({ dir, oid: ref });
252 return git.log({ dir, ref: oid, path });
253 }
254 // re-throw
255 throw err;
256 })
257 .then(async (commits) => {
258 if (typeof path === 'string' && path.length) {
259 // filter by path
260 let lastSHA = null;
261 let lastCommit = null;
262 const filteredCommits = [];
263 for (let i = 0; i < commits.length; i += 1) {
264 const commit = commits[i];
265 /* eslint-disable no-await-in-loop */
266 try {
267 const o = await git.readObject({ dir, oid: commit.oid, filepath: path });
268 if (i === commits.length - 1) {
269 // file already existed in first commit
270 filteredCommits.push(commit);
271 break;
272 }
273 if (o.oid !== lastSHA) {
274 if (lastSHA !== null) {
275 filteredCommits.push(lastCommit);
276 }
277 lastSHA = o.oid;
278 }
279 } catch (err) {
280 // file no longer there
281 filteredCommits.push(lastCommit);
282 break;
283 }
284 lastCommit = commit;
285 }
286 // unfiltered commits
287 return filteredCommits;
288 }
289 // unfiltered commits
290 return commits;
291 });
292}
293
294module.exports = {
295 currentBranch,
296 getRawContent,
297 resolveTree,
298 resolveCommit,
299 resolveBlob,
300 isCheckedOut,
301 createBlobReadStream,
302 getObject,
303 isValidSha,
304 commitLog,
305 determineRefPathName,
306};