UNPKG

@adobe/git-server

Version:

serve a git repository over http(s)

269 lines (251 loc) 8.86 kB
/* * Copyright 2018 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ 'use strict'; const { resolve: resolvePath } = require('path'); const { PassThrough } = require('stream'); const fse = require('fs-extra'); const git = require('isomorphic-git'); git.plugins.set('fs', require('fs')); /** * Various helper functions for reading git meta-data and content */ /** * Returns the name (abbreviated form) of the currently checked out branch. * * @param {string} dir git repo path * @returns {Promise<string>} name of the currently checked out branch */ async function currentBranch(dir) { return git.currentBranch({ dir, fullname: false }); } /** * Determines whether the specified reference is currently checked out in the working dir. * * @param {string} dir git repo path * @param {string} ref reference (branch or tag) * @returns {Promise<boolean>} `true` if the specified reference is checked out */ async function isCheckedOut(dir, ref) { let oidCurrent; return git.resolveRef({ dir, ref: 'HEAD' }) .then((oid) => { oidCurrent = oid; return git.resolveRef({ dir, ref }); }) .then(oid => oidCurrent === oid) .catch(() => false); } /** * Returns the commit oid of the curent commit referenced by `ref` * * @param {string} dir git repo path * @param {string} ref reference (branch, tag or commit sha) * @returns {Promise<string>} commit oid of the curent commit referenced by `ref` * @throws {GitError} `err.code === 'ResolveRefError'`: invalid reference */ async function resolveCommit(dir, ref) { return git.resolveRef({ dir, ref }) .catch(async (err) => { if (err.code === 'ResolveRefError') { // fallback: is ref a shortened oid prefix? const oid = await git.expandOid({ dir, oid: ref }); return git.resolveRef({ dir, ref: oid }); } // re-throw throw err; }); } /** * Returns the blob oid of the file at revision `ref` and `pathName` * * @param {string} dir git repo path * @param {string} ref reference (branch, tag or commit sha) * @param {string} filePath relative path to file * @param {boolean} includeUncommitted include uncommitted changes in working dir * @returns {Promise<string>} blob oid of specified file * @throws {GitError} `err.code === 'TreeOrBlobNotFoundError'`: resource not found */ async function resolveBlob(dir, ref, pathName, includeUncommitted) { const commitSha = await resolveCommit(dir, ref); // project-helix/#150: check for uncommitted local changes // project-helix/#183: serve newly created uncommitted files // project-helix/#187: only serve uncommitted content if currently // checked-out and requested refs match if (!includeUncommitted) { return (await git.readObject({ dir, oid: commitSha, filepath: pathName })).oid; } // check working dir status const status = await git.status({ dir, filepath: pathName }); if (status.endsWith('unmodified')) { return (await git.readObject({ dir, oid: commitSha, filepath: pathName })).oid; } if (status.endsWith('absent') || status.endsWith('deleted')) { const err = new Error(`Not found: ${pathName}`); err.code = git.E.TreeOrBlobNotFoundError; throw err; } // return blob id representing working dir file const content = await fse.readFile(resolvePath(dir, pathName)); return git.writeObject({ dir, object: content, type: 'blob', format: 'content', }); } /** * Returns the contents of the file at revision `ref` and `pathName` * * @param {string} dir git repo path * @param {string} ref reference (branch, tag or commit sha) * @param {string} filePath relative path to file * @param {boolean} includeUncommitted include uncommitted changes in working dir * @returns {Promise<Buffer>} content of specified file * @throws {GitError} `err.code === 'TreeOrBlobNotFoundError'`: resource not found */ async function getRawContent(dir, ref, pathName, includeUncommitted) { return resolveBlob(dir, ref, pathName, includeUncommitted) .then(oid => git.readObject({ dir, oid, format: 'content' }).object); } /** * Returns a stream for reading the specified blob. * * @param {string} dir git repo path * @param {string} oid blob sha1 * @returns {Promise<Stream>} readable Stream instance */ async function createBlobReadStream(dir, oid) { const { object: content } = await git.readObject({ dir, oid }); const stream = new PassThrough(); stream.end(content); return stream; } /** * Retrieves the specified object from the loose object store. * * @param {string} dir git repo path * @param {string} oid object id * @returns {Promise<Object>} object identified by `oid` */ async function getObject(dir, oid) { return git.readObject({ dir, oid }); } /** * Checks if the specified string is a valid SHA-1 value. * * @param {string} str * @returns {boolean} `true` if `str` represents a valid SHA-1, otherwise `false` */ function isValidSha(str) { if (typeof str === 'string' && str.length === 40) { const res = str.match(/[0-9a-f]/g); return res && res.length === 40; } return false; } /** * Returns the tree object identified directly by its sha * or indirectly via reference (branch, tag or commit sha) * * @param {string} dir git repo path * @param {string} refOrSha either tree sha or reference (branch, tag or commit sha) * @returns {Promise<string>} commit oid of the curent commit referenced by `ref` * @throws {GitError} `err.code === 'ShortOidNotFound'`: invalid reference * `err.code === 'ReadObjectFail'`: not found */ async function resolveTree(dir, refOrSha) { if (isValidSha(refOrSha)) { // full commit or tree sha return git.readObject({ dir, oid: refOrSha }) .then((obj) => { if (obj.type === 'tree') { return obj; } if (obj.type === 'commit') { return git.readObject({ dir, oid: obj.object.tree }); } throw new Error(`unexpected object: ${obj}`); }); } // reference (branch, tag, shorthand commit sha) return resolveCommit(dir, refOrSha) .then(oid => git.readObject({ dir, oid })) .then(obj => git.readObject({ dir, oid: obj.object.tree })); } /** * Returns a commit log, i.e. an array of commits in reverse chronological order. * * @param {string} dir git repo path * @param {string} ref reference (branch, tag or commit sha) * @param {string} path only commits containing this file path will be returned * @throws {GitError} `err.code === 'ResolveRefError'`: invalid reference * `err.code === 'ReadObjectFail'`: not found */ async function commitLog(dir, ref, path) { return git.log({ dir, ref, path }) .catch(async (err) => { if (err.code === 'ResolveRefError') { // fallback: is ref a shortened oid prefix? const oid = await git.expandOid({ dir, oid: ref }); return git.log({ dir, ref: oid, path }); } // re-throw throw err; }) .then(async (commits) => { if (typeof path === 'string' && path.length) { // filter by path let lastSHA = null; let lastCommit = null; const filteredCommits = []; for (let i = 0; i < commits.length; i += 1) { const commit = commits[i]; /* eslint-disable no-await-in-loop */ try { const o = await git.readObject({ dir, oid: commit.oid, filepath: path }); if (i === commits.length - 1) { // file already existed in first commit filteredCommits.push(commit); break; } if (o.oid !== lastSHA) { if (lastSHA !== null) { filteredCommits.push(lastCommit); } lastSHA = o.oid; } } catch (err) { // file no longer there filteredCommits.push(lastCommit); break; } lastCommit = commit; } // unfiltered commits return filteredCommits; } // unfiltered commits return commits; }); } module.exports = { currentBranch, getRawContent, resolveTree, resolveCommit, resolveBlob, isCheckedOut, createBlobReadStream, getObject, isValidSha, commitLog, };