git-command-helper
Version:
github command helper for nodejs
673 lines (666 loc) • 19.9 kB
JavaScript
// 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;