@atomist/automation-client
Version:
Atomist API for software low-level client
369 lines • 16.3 kB
JavaScript
;
/*
* 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