UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

369 lines 16.3 kB
"use strict"; /* * Copyright © 2018 Atomist, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const _ = require("lodash"); const promiseRetry = require("promise-retry"); const RepoId_1 = require("../../operations/common/RepoId"); const DirectoryManager_1 = require("../../spi/clone/DirectoryManager"); const tmpDirectoryManager_1 = require("../../spi/clone/tmpDirectoryManager"); const child_process_1 = require("../../util/child_process"); const logger_1 = require("../../util/logger"); const LocalProject_1 = require("../local/LocalProject"); const NodeFsLocalProject_1 = require("../local/NodeFsLocalProject"); const gitStatus_1 = require("./gitStatus"); exports.DefaultDirectoryManager = tmpDirectoryManager_1.TmpDirectoryManager; /** * Implements GitProject interface using the Git binary from the command line. * Works only if git is installed. */ class GitCommandGitProject extends NodeFsLocalProject_1.NodeFsLocalProject { constructor(id, baseDir, credentials, release, provenance, shouldCache = false) { super(id, baseDir, release, shouldCache); this.baseDir = baseDir; this.credentials = credentials; this.provenance = provenance; this.newRepo = false; this.branch = (id.branch || id.sha) || "master"; logger_1.logger.debug(`Created GitProject`); } static fromProject(p, credentials) { if (LocalProject_1.isLocalProject(p)) { return GitCommandGitProject.fromBaseDir(p.id, p.baseDir, credentials, () => Promise.resolve()); } throw new Error(`Project ${p.name} doesn't have a local directory`); } /** * Create a project from an existing git directory * @param {RepoRef} id * @param {string} baseDir * @param {ProjectOperationCredentials} credentials * @param release call this when you're done with the project. make its filesystem resources available to others. * @param provenance optional; for debugging, describe how this was constructed * @return {GitCommandGitProject} */ static fromBaseDir(id, baseDir, credentials, release, provenance) { return new GitCommandGitProject(id, baseDir, credentials, release, provenance); } /** * Create a new GitCommandGitProject by cloning the given remote project * @param {ProjectOperationCredentials} credentials * @param {RemoteRepoRef} id * @param {CloneOptions} opts * @param {DirectoryManager} directoryManager * @return {Promise<GitCommandGitProject>} */ static cloned(credentials, id, opts = DirectoryManager_1.DefaultCloneOptions, directoryManager = exports.DefaultDirectoryManager) { return __awaiter(this, void 0, void 0, function* () { const p = yield clone(credentials, id, opts, directoryManager); if (!!id.path) { // It's possible to request a clone but only work with part of it. const pathInsideRepo = id.path.startsWith("/") ? id.path : "/" + id.path; const gp = GitCommandGitProject.fromBaseDir(id, p.baseDir + pathInsideRepo, credentials, () => p.release(), p.provenance + "\ncopied into one with extra path " + id.path); return gp; } else { return p; } }); } init() { return __awaiter(this, void 0, void 0, function* () { this.newRepo = true; this.branch = "master"; return this.gitInProjectBaseDir(["init"]) .then(() => this); }); } isClean() { return this.gitInProjectBaseDir(["status", "--porcelain"]) .then(result => result.stdout === ""); } gitStatus() { return gitStatus_1.runStatusIn(this.baseDir); } /** * Remote is of form https://github.com/USERNAME/REPOSITORY.git * @param remote */ setRemote(remote) { this.remote = remote; return this.gitInProjectBaseDir(["remote", "add", "origin", remote]) .then(() => this); } setUserConfig(user, email) { return this.gitInProjectBaseDir(["config", "user.name", user]) .then(() => this.gitInProjectBaseDir(["config", "user.email", email])) .then(() => this); } createAndSetRemote(gid, description = gid.repo, visibility) { this.id = gid; return gid.createRemote(this.credentials, description, visibility) .then(res => { if (res.success) { logger_1.logger.debug(`Repo created ok`); return this.setRemote(gid.cloneUrl(this.credentials)); } else { return Promise.reject(res.error); } }); } configureFromRemote() { if (RepoId_1.isRemoteRepoRef(this.id)) { return this.id.setUserConfig(this.credentials, this) .then(() => this); } return Promise.resolve(this); } /** * Raise a PR after a push to this branch * @param title * @param body */ raisePullRequest(title, body = this.name, targetBranch = "master") { if (!this.branch) { throw new Error("Cannot create a PR: no branch has been created"); } if (!RepoId_1.isRemoteRepoRef(this.id)) { throw new Error("No remote in " + JSON.stringify(this.id)); } return this.id.raisePullRequest(this.credentials, title, body, this.branch, targetBranch) .then(() => this); } /** * `git add .` and `git commit -m MESSAGE` * @param {string} message Commit message * @returns {Promise<this>} */ commit(message) { return this.gitInProjectBaseDir(["add", "."]) .then(() => this.gitInProjectBaseDir(["commit", "-m", message])) .then(() => this); } /** * Check out a particular commit. We'll end in detached head state * @param ref branch or SHA * @return {any} */ checkout(ref) { return __awaiter(this, void 0, void 0, function* () { yield this.gitInProjectBaseDir(["checkout", ref, "--"]); if (!isValidSHA1(ref)) { this.branch = ref; } return this; }); } /** * Revert all changes since last commit * @return {any} */ revert() { return __awaiter(this, void 0, void 0, function* () { return clean(this.baseDir) .then(() => this); }); } push(options) { return __awaiter(this, void 0, void 0, function* () { const gitPushArgs = ["push"]; _.forOwn(options, (v, k) => { const opt = k.replace(/_/g, "-"); if (typeof v === "boolean") { if (v === false) { gitPushArgs.push(`--no-${opt}`); } else { gitPushArgs.push(`--${opt}`); } } else if (typeof v === "string") { gitPushArgs.push(`--${opt}=${v}`); } else { return Promise.reject(new Error(`Unknown option key type for ${k}: ${typeof v}`)); } }); if (!!this.branch && !!this.remote) { // We need to set the remote gitPushArgs.push(this.remote, this.branch); } else { gitPushArgs.push("--set-upstream", "origin", this.branch); } return this.gitInProjectBaseDir(gitPushArgs) .then(() => this) .catch(err => { err.message = `Unable to push 'git "${gitPushArgs.join('" "')}"': ${err.message}`; logger_1.logger.error(err.message); return Promise.reject(err); }); }); } /** * Create branch from current HEAD. * @param name Name of branch to create. * @return project object */ createBranch(name) { return __awaiter(this, void 0, void 0, function* () { return this.gitInProjectBaseDir(["branch", name]) .then(() => this.gitInProjectBaseDir(["checkout", name, "--"])) .then(() => { this.branch = name; return this; }); }); } hasBranch(name) { return __awaiter(this, void 0, void 0, function* () { return this.gitInProjectBaseDir(["branch", "--list", name]) .then(result => result.stdout.includes(name)); }); } gitInProjectBaseDir(args) { return __awaiter(this, void 0, void 0, function* () { return child_process_1.execPromise("git", args, { cwd: this.baseDir }); }); } } exports.GitCommandGitProject = GitCommandGitProject; /** * Clone the given repo from GitHub * @param credentials git provider credentials * @param id remote repo ref * @param opts options for clone * @param directoryManager strategy for cloning */ function clone(credentials, id, opts, directoryManager, secondTry = false) { return __awaiter(this, void 0, void 0, function* () { const cloneDirectoryInfo = yield directoryManager.directoryFor(id.owner, id.repo, id.branch, opts); logger_1.logger.debug("Directory info: %j", cloneDirectoryInfo); switch (cloneDirectoryInfo.type) { case "empty-directory": return cloneInto(credentials, cloneDirectoryInfo, opts, id); case "existing-directory": const repoDir = cloneDirectoryInfo.path; try { yield resetOrigin(repoDir, credentials, id); // sometimes the credentials are in the origin URL // Why do we not fetch? yield checkout(repoDir, id.branch || id.sha); yield clean(repoDir); return GitCommandGitProject.fromBaseDir(id, repoDir, credentials, cloneDirectoryInfo.release, cloneDirectoryInfo.provenance + "\nRe-using existing clone"); } catch (error) { yield cloneDirectoryInfo.invalidate(); if (secondTry) { throw error; } else { return clone(credentials, id, opts, directoryManager, true); } } default: throw new Error("What is this type: " + cloneDirectoryInfo.type); } }); } function cloneInto(credentials, targetDirectoryInfo, opts, id) { return __awaiter(this, void 0, void 0, function* () { logger_1.logger.debug(`Cloning repo with owner '${id.owner}', name '${id.repo}', branch '${id.branch}', sha '${id.sha}' and options '${JSON.stringify(opts)}'`); const sha = id.sha || "HEAD"; const repoDir = targetDirectoryInfo.path; const url = id.cloneUrl(credentials); const cloneBranch = id.branch; const cloneArgs = ["clone", url, repoDir]; // If we wanted a deep clone, just clone it if (!opts.alwaysDeep) { // If we didn't ask for a deep clone, then default to cloning only the tip of the default branch. // the cloneOptions let us ask for more commits than that cloneArgs.push("--depth", ((opts.depth && opts.depth > 0) ? opts.depth : 1).toString(10)); if (cloneBranch) { // if not cloning deeply, be sure we clone the right branch cloneArgs.push("--branch", cloneBranch); } if (opts.noSingleBranch) { cloneArgs.push("--no-single-branch"); } } // Note: branch takes preference for checkout because we might be about to commit to it. // If you want to be sure to land on your SHA, set opts.detachHead to true. // Or don't, but then call gitStatus() on the returned project to check whether the branch is still at the SHA you wanted. const checkoutRef = opts.detachHead ? sha : id.branch || sha; // TODO CD user:password should be replaced too const cleanUrl = url.replace(/\/\/.*:x-oauth-basic/, "//TOKEN:x-oauth-basic"); logger_1.logger.debug(`Cloning repo '${cleanUrl}' in '${repoDir}'`); const retryOptions = { retries: 4, factor: 2, minTimeout: 100, maxTimeout: 500, randomize: false, }; yield promiseRetry(retryOptions, (retry, count) => { return child_process_1.execPromise("git", cloneArgs) .catch(err => { logger_1.logger.debug(`Clone of ${id.owner}/${id.repo} attempt ${count} failed: ` + err.message); retry(err); }); }); try { yield child_process_1.execPromise("git", ["checkout", checkoutRef, "--"], { cwd: repoDir }); } catch (err) { // When the head moved on and we only cloned with depth; we might have to do a full clone to get to the commit we want logger_1.logger.debug(`Ref ${checkoutRef} not in cloned history. Attempting full clone`); yield child_process_1.execPromise("git", ["fetch", "--unshallow"], { cwd: repoDir }) .then(() => child_process_1.execPromise("git", ["checkout", checkoutRef, "--"], { cwd: repoDir })); } logger_1.logger.debug(`Clone succeeded with URL '${cleanUrl}'`); return GitCommandGitProject.fromBaseDir(id, repoDir, credentials, targetDirectoryInfo.release, targetDirectoryInfo.provenance + "\nfreshly cloned"); }); } function resetOrigin(repoDir, credentials, id) { return __awaiter(this, void 0, void 0, function* () { return child_process_1.execPromise("git", ["remote", "set-url", "origin", id.cloneUrl(credentials)], { cwd: repoDir }); }); } function checkout(repoDir, branch = "HEAD") { return __awaiter(this, void 0, void 0, function* () { return child_process_1.execPromise("git", ["fetch", "origin", branch], { cwd: repoDir }) .then(() => child_process_1.execPromise("git", ["checkout", branch, "--"], { cwd: repoDir })) .then(() => child_process_1.execPromise("git", ["reset", "--hard", `origin/${branch}`], { cwd: repoDir })); }); } function clean(repoDir) { return __awaiter(this, void 0, void 0, function* () { return child_process_1.execPromise("git", ["clean", "-dfx"], { cwd: repoDir }) // also removes ignored files .then(() => child_process_1.execPromise("git", ["checkout", "--", "."], { cwd: repoDir })); }); } function isValidSHA1(s) { return /^[a-fA-F0-9]{40}$/.test(s); } exports.isValidSHA1 = isValidSHA1; //# sourceMappingURL=GitCommandGitProject.js.map