UNPKG

git-command-helper

Version:
673 lines (666 loc) 19.9 kB
// git-command-helper 2.1.0 by Dimas Lanjaka <dimaslanjaka@gmail.com> (https://www.webmanajemen.com) 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Bluebird = require('bluebird'); var spawn = require('cross-spawn'); var fs = require('fs-extra'); var _ = require('lodash'); var sbgUtility = require('sbg-utility'); var path = require('upath'); var index$1 = require('./functions/index.js'); var dryHelper = require('./functions/dry-helper.js'); var gitignore = require('./functions/gitignore.js'); var isFileChanged = require('./functions/isFileChanged.js'); var latestCommit = require('./functions/latestCommit.js'); var originHelper = require('./functions/origin-helper.js'); var staged = require('./git/staged.js'); var helper = require('./helper.js'); var indexExports = require('./index-exports.js'); var instances = require('./instances.js'); var spawn$1 = require('./spawn.js'); var submodule = require('./submodule.js'); var index = require('./utils/index.js'); var extractSubmodule = require('./utils/extract-submodule.js'); var safeUrl = require('./utils/safe-url.js'); var getGithubBranches = require('./functions/getGithubBranches.js'); var getGithubCurrentBranch = require('./functions/getGithubCurrentBranch.js'); var getGithubRemote = require('./functions/getGithubRemote.js'); var getGithubRootDir = require('./functions/getGithubRootDir.js'); var getGithubRepoUrl = require('./functions/getGithubRepoUrl.js'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var spawn__namespace = /*#__PURE__*/_interopNamespaceDefault(spawn); /** * NodeJS GitHub Helper * @author Dimas Lanjaka <dimaslanjaka@gmail.com> */ /** * GitHub Command Helper For NodeJS */ class git { /** is current device is github actions */ static isGithubCI = typeof process.env["GITHUB_WORKFLOW"] === "string" && typeof process.env["GITHUB_WORKFLOW_SHA"] === "string"; /** is current device is github actions */ isGithubCI = typeof process.env["GITHUB_WORKFLOW"] === "string" && typeof process.env["GITHUB_WORKFLOW_SHA"] === "string"; submodules; user; email; remote; branch = "master"; submodule; cwd; token; // external funcs helper = helper; static helper = helper; ext = indexExports; static ext = indexExports; util = index; static util = index; crossSpawn = spawn__namespace; static crossSpawn = spawn__namespace; // exports infos infos = index$1; getGithubBranches = getGithubBranches.getGithubBranches; getGithubCurrentBranch = getGithubCurrentBranch.getGithubCurrentBranch; getGithubRemote = getGithubRemote.getGithubRemote; getGithubRootDir = getGithubRootDir.getGithubRootDir; constructor(gitInput, branch = "master") { let gitdir; if (typeof gitInput === "string") { gitdir = gitInput; if (branch) this.branch = branch; } else { gitdir = gitInput.cwd; if (gitInput.ref || gitInput.branch) this.branch = gitInput.ref || gitInput.branch || branch; this.remote = gitInput.url || gitInput.remote; this.email = gitInput.email; this.user = gitInput.user; } if (instances.hasInstance(gitdir)) return instances.getInstance(gitdir); this.cwd = gitdir; if (this.remote) ; // auto recreate git directory if (!fs.existsSync(gitdir)) { // create .git folder fs.mkdirSync(path.join(gitdir, ".git"), { recursive: true }); const self = this; this.spawn("git", ["init"]).then(function () { if (typeof self.remote === "function") this.setremote(self.remote); }); } if (fs.existsSync(path.join(gitdir, ".gitmodules"))) { this.submodules = extractSubmodule(path.join(gitdir, ".gitmodules")); this.submodule = new submodule.submodule(gitdir); } if (!instances.hasInstance(gitdir)) instances.setInstance(gitdir, this); } /** * Clone the repository * @returns */ async clone() { if (!this.remote) throw new Error("remote is not set!"); if (!this.cwd) throw new Error("cwd is not set!"); if (fs.existsSync(this.cwd)) { throw new Error("cwd already exists! " + this.cwd); } return await this.spawn("git", ["clone", this.remote, this.cwd], { stdio: "inherit" }); } setToken(token) { this.token = token; } getToken() { return this.token; } /** * get repository and raw file url * @param file relative to git root without leading `/` * @returns */ getGithubRepoUrl(file) { return getGithubRepoUrl.getGithubRepoUrl(file, { cwd: this.cwd }); } /** * check file is untracked * @param file relative to git root without leading `/` * @returns */ isUntracked(file) { return isFileChanged.isUntracked(file, { cwd: this.cwd }); } /** * get latest commit hash * @param customPath * @param options * @returns */ latestCommit(customPath, options) { return latestCommit.latestCommit(customPath, this.spawnOpt(options)); } async info() { const opt = this.spawnOpt({ stdio: "pipe" }); return { root: await this.getGithubRootDir(opt), remote: await this.getremote(["-v"]), branch: await this.getbranch(), status: await this.status() }; } /** * git config --global --add safe.directory PATH_FOLDER */ addSafe() { return spawn$1.spawnSilent("git", "config --global --add safe.directory".split(" ").concat([this.cwd]), this.spawnOpt({ stdio: "inherit" })).catch(_.noop).finally(() => console.log(this.cwd, "added to safe directory")); } /** * call spawn async * * default option is `{ cwd: this.cwd }` * @param cmd * @param args * @param spawnOpt * @returns */ spawn(cmd, args, spawnOpt) { return spawn$1.spawn(cmd, args, this.spawnOpt(spawnOpt || { stdio: "pipe" })); } /** * setup merge on pull strategy * @returns */ setAutoRebase() { return this.spawn("git", ["config", "pull.rebase", "false"]); } /** * setup end of line LF * @link https://stackoverflow.com/a/13154031 * @returns */ setForceLF() { return this.spawn("git", ["config", "core.autocrlf", "false"]); } /** * git fetch * @param arg argument git-fetch, ex ['--all'] * @param optionSpawn * @returns */ fetch(arg, optionSpawn = { stdio: "inherit" }) { let args = []; if (Array.isArray(arg)) args = args.concat(arg); if (args.length === 0) { args.push("origin", this.branch); } // return default git fetch when branch not set if (!this.branch) return spawn$1.spawn("git", ["fetch"], this.spawnOpt(optionSpawn)); // remove non-string paramters args = ["fetch"].concat(args).filter(str => typeof str === "string" && str.length > 0); return spawn$1.spawn("git", args, this.spawnOpt(optionSpawn)); } /** * git pull * @param arg example: `['--recurse-submodule']` * @param optionSpawn * @returns */ async pull(arg, optionSpawn = { stdio: "inherit" }) { let args = []; if (Array.isArray(arg)) args = args.concat(arg); if (args.length === 0) { args.push("origin", this.branch); } const opt = this.spawnOpt(optionSpawn || { stdio: "inherit" }); try { return await spawn$1.spawn("git", ["pull"].concat(args), opt); } catch (e) { if (e instanceof Error) { if (opt.stdio === "inherit") console.log(e.message); return e.message; } } } /** * git pull accept merge from remote (accept all incoming changes) * @see https://stackoverflow.com/a/21777677 * @see https://www.folkstalk.com/tech/git-accept-incoming-changes-for-all-with-code-examples/ */ async pullAcceptTheirs(optionSpawn = { stdio: "inherit" }) { await this.pull(["-X", "theirs"], optionSpawn); await this.spawn("git", ["checkout", "--theirs", "."], optionSpawn); } /** * git commit * @param mode -am, -m, etc * @param msg commit messages * @param optionSpawn * @returns */ commit(msg, mode = "m", optionSpawn = { stdio: "inherit" }) { if (!mode.startsWith("-")) mode = "-" + mode; return new Bluebird((resolve, reject) => { const opt = this.spawnOpt(optionSpawn); const child = spawn$1.spawn("git", ["commit", mode, msg], opt); if (opt.stdio !== "inherit") { child.then(str => { resolve(str); }); } else { resolve(); } child.catch(reject); }); } /** * add and commit file * @param gitFilePath * @param msg * @param mode am/m * @returns */ addAndCommit(gitFilePath, msg, mode = "m") { return new Bluebird((resolve, reject) => { this.add(gitFilePath, { stdio: "pipe" }).then(_ => this.commit(msg, mode, { stdio: "pipe" }).then(resolve).catch(reject)); }); } /** * bulk add and commit * @param options array of `path` and `msg` commit message * @returns */ commits(options) { const self = this; const errors = []; async function run() { if (options.length > 0) { try { try { await self.addAndCommit(options[0].path, options[0].msg || "update " + options[0].path + " " + new Date()); } catch (e) { errors.push(e); } } finally { options.shift(); await run(); } } } return new Bluebird(resolve => { run().then(() => resolve(errors)); }); } /** * git push * @param force * @param optionSpawn * @returns */ async push(force = false, optionSpawn = { stdio: "inherit" }) { let args = ["push"]; if (force) args = args.concat("-f"); const opt = this.spawnOpt(optionSpawn); try { return await spawn$1.spawn("git", args, opt); } catch (e) { if (e instanceof Error) { if (opt.stdio === "inherit") { console.log(e.message); } //console.log(e.message); if (/^error: failed to push some refs to/gim.test(e.message)) { if (/the tip of your current branch is behind/gim.test(e.message)) { return await this.push(true, opt); } } } } } /** * Determines whether the current branch can be pushed to the specified remote origin. * * @param originName - The name of the remote origin. Defaults to `"origin"`. * @param branchName - The name of the branch to check. If not provided, uses the current branch. * @returns A promise that resolves to a boolean indicating if the branch can be pushed. */ async canPush(originName = "origin", branchName) { return dryHelper.isCanPush({ cwd: this.cwd, origin: originName, branch: branchName || this.branch }); } /* async canPush(originName = 'origin', branchName = this.branch) { // git push --dry-run if (branchName) { await spawn( 'git', ['push', '-u', originName || 'origin', branchName || this.branch, '--dry-run'], this.spawnOpt({ stdio: 'pipe' }) ); } // repository is not up to date const changed = !(await this.isUpToDate()); // repostory file changes status const staged = await this.status(); // test git push --dry-run const dry = await spawnAsync('git', ['push', '--dry-run'], this.spawnOpt({ stdio: 'pipe' })); console.log({ staged, changed, dry: dry.output.join(EOL).trim() != 'Everything up-to-date' }); // return repository is not up to date return changed && staged.length === 0 && dry.output.join(EOL).trim() != 'Everything up-to-date'; } */ /** * Spawn option default stdio pipe * @param opt * @returns */ spawnOpt(opt = {}) { return Object.assign({ cwd: this.cwd, stdio: "pipe" }, opt); } /** * check has any file changed */ async hasChanged() { const status = await this.status(); return status.length > 0; } isIgnored = gitignore.isIgnored; static isIgnored = gitignore.isIgnored; /** * git add * @param gitFilePath specific path or argument -A * @param optionSpawn * @returns */ add(gitFilePath, optionSpawn = { stdio: "inherit" }) { return spawn$1.spawn("git", ["add", gitFilePath], this.spawnOpt(optionSpawn)); } /** * Check if a file is staged for commit. * @param gitFilePath Path to the file relative to the git root. * @returns Promise that resolves to a boolean indicating if the file is staged. */ isStaged(gitFilePath) { return staged.isStaged(gitFilePath, this.spawnOpt({ stdio: "pipe" })); } /** * git checkout * @param branchName * @param optionSpawn * @returns */ async checkout(branchName, optionSpawn = { stdio: "inherit" }) { return await spawn$1.spawn("git", ["checkout", branchName], this.spawnOpt(optionSpawn || {})); } /** * get current branch informations * @returns */ async getbranch() { return await spawn$1.spawn("git", ["branch"], this.spawnOpt({ stdio: "pipe" })).then(str => str.split(/\n/).map(str => str.split(/\s/).map(str => str.trim())).filter(str => str.length > 0).map(item => { return { active: item.length > 1, branch: item[1] }; }).filter(item => typeof item.branch === "string" && item.branch.trim().length > 0)); } /** * Check if current repository is up to date with origin/remote * @returns */ isUpToDate() { const rgUpToDate = /^your branch is up to date with/gim; return new Bluebird(resolve => { spawn$1.spawn("git", ["status"], this.spawnOpt({ stdio: "pipe" })).then(stdout => { resolve(rgUpToDate.test(stdout)); }); }); } /** * git status * @returns */ status() { const rgMod = /^\s*(modified|added|deleted):/gim; const rgChanged = /^\s*(changes not staged for commit|changes to be committed):/gim; const rgUntracked = /^untracked files:([\s\S]*?)\n\n/gim; return new Bluebird((resolve, reject) => { spawn$1.spawn("git", ["status"], this.spawnOpt({ stdio: "pipe" })).then(response => { // check changed if (rgChanged.test(response)) { // modded, added, deleted const result = response.split("\n").map(str => str.trim()).filter(str => rgMod.test(str)).map(str => { const split = str.split(/:\s+/); return { changes: split[0], path: (split[1] || "").replace(/\(.*\)$/, "").trim() }; }); resolve(result); } // untracked const result = (Array.from(response.match(rgUntracked) || [])[0] || "").split(/\n/).map(str => str.trim()).filter(str => { return !/^\(use/gim.test(str) && str.length > 0; }).map(str => { if (!str.includes(":")) return { changes: "untracked", path: str }; }).filter(str => typeof str === "object"); resolve(result); }).catch(reject); }); } /** * git init * @returns */ async init(spawnOpt = { stdio: "inherit" }) { if (!fs.existsSync(path.join(this.cwd, ".git"))) fs.mkdirSync(path.join(this.cwd, ".git"), { recursive: true }); return spawn$1.spawnSilent("git", ["init"], this.spawnOpt(spawnOpt)).catch(_.noop); } setcwd(v) { this.cwd = v; } setemail(v) { this.email = v; return spawn$1.spawn("git", ["config", "user.email", this.email], this.spawnOpt()); } setuser(v) { this.user = v; return spawn$1.spawn("git", ["config", "user.name", this.user], this.spawnOpt()); } /** * Apply `this.user` to the specified remote URL (inserts username into URL if possible) * @param originName remote name, defaults to "origin" * @returns object with error and message */ async applyUserToOriginUrl(originName = "origin") { const result = await originHelper.applyUserToOriginUrl(this.remote, this.user, originName, this.spawnOpt()); if (!result.error && this.user) { // update local remote string if successful const urlObj = new URL(this.remote); if (!urlObj.username) { urlObj.username = this.user; this.remote = urlObj.toString(); } } return result; } /** * set remote url * @param remoteURL repository url * @param name custom object name * @returns * @example * // default * git add remote origin https:// * // custom name * git add remote customName https:// */ async setremote(remoteURL, name, spawnOpt = {}) { const newremote = String(remoteURL); if (this.remote !== newremote) { this.remote = newremote; } const opt = this.spawnOpt(Object.assign({ stdio: "pipe" }, spawnOpt || {})); try { return await spawn$1.spawn("git", ["remote", "add", name || "origin", this.remote], opt); } catch { return await helper.suppress(() => spawn$1.spawn("git", ["remote", "set-url", name || "origin", this.remote], opt)); } } /** * get remote information. default `origin` * @param args * @returns */ async getremote(args) { if (typeof args === "string") return await getGithubRemote.getGithubRemote(args, { cwd: this.cwd }); try { const res = await spawn$1.spawn("git", ["remote"].concat(args || ["-v"]), this.spawnOpt({ stdio: "pipe" })); const result = { fetch: { origin: "", url: "" }, push: { origin: "", url: "" } }; const lines = res.split(/\n/gm).filter(split => split.length > 0); lines.map(splitted => { let key; const nameUrl = splitted.split(/\t/).map(str => { const rg = /\((.*)\)/gm; if (rg.test(str)) return str.replace(rg, (_whole, v1) => { key = v1; return ""; }).trim(); return str.trim(); }); // skip non-origin if (nameUrl[0] != "origin") return; if (key) { result[key] = { origin: nameUrl[0], url: safeUrl.safeURL(nameUrl[1]) }; } else { throw new Error("key never assigned"); } }); return result; } catch { // } } checkLock() { return fs.existsSync(path.join(this.cwd, ".git/index.lock")); } /** * set branch (git checkout branchName) * @param branchName * @returns */ async setbranch(branchName, force = false, spawnOpt) { this.branch = branchName; const args = ["checkout"]; if (force) args.push("-f"); args.push(this.branch); const _checkout = await spawn$1.spawn("git", args, this.spawnOpt(spawnOpt || { stdio: "pipe" })).catch(e => console.log("cannot checkout", this.branch, e.message)); // git branch --set-upstream-to=origin/<branch> gh-pages await spawn$1.spawn("git", ["branch", "--set-upstream-to=origin/" + this.branch, this.branch], this.spawnOpt(spawnOpt || { stdio: "pipe" })).catch(e => console.log("cannot set upstream", this.branch, e.message)); // return _checkout; } /** * Reset to latest commit of remote branch * @param branch */ reset(branch = this.branch) { return spawn$1.spawn("git", ["reset", "--hard", "origin/" + branch || this.branch], { stdio: "inherit", cwd: this.cwd }); } toString() { return sbgUtility.jsonStringifyWithCircularRefs(this); } } exports.default = git; exports.git = git;