Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to a more modern code. #8

Merged
merged 1 commit into from Feb 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 57 additions & 58 deletions README.md
Expand Up @@ -11,120 +11,119 @@ npm install git-promise --save
Once installed, you can use it in your JavaScript files like so:

```js
var git = require("git-promise");
const git = require("git-promise");

git("rev-parse --abbrev-ref HEAD").then(function (branch) {
console.log(branch); // This is your current branch
});
const branch = await git("rev-parse --abbrev-ref HEAD");
console.log(branch); // This is your current branch
```

The module will handle exit code automatically, so
The module will handle git exit code automatically, so

```js
var git = require("git-promise");
const git = require("git-promise");

git("merge origin/master").then(function () {
try {
await git("merge origin/master");
// Everything was fine
}).fail(function (err) {
} catch (err) {
// Something went bad, maybe merge conflict?
console.error(err);
});
}
```

`err` is an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) object augmented with `stdout` property. The following code:
`err` is an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) object augmented with `code` property. The following code:

```js
git('clone http://example.org/notExistingExample.git').fail(function (err) {
try {
await git('clone http://example.org/notExistingExample.git');
} catch (err) {
console.log("MESSAGE");
console.log(err.message);
console.log("STDOUT");
console.log(err.stdout);
});
console.log("ERROR CODE");
console.log(err.code);
}
```

will log:

```
MESSAGE
'git clone http://example.org/notExistingExample.git' exited with error code 128
STDOUT
Cloning into 'notExistingExample'...
fatal: remote error: Repository does not exist
The requested repository does not exist, or you do not have permission to
access it.
}
ERROR CODE
128
```

## Advanced usage

The `git` command accepts a second parameter that can be used to parse the output or to deal with non 0 exit code.

```js
var git = require("git-promise");
const git = require("git-promise");

git("status -sb", function (stdout) {
return stdout.match(/## (.*)/)[1];
}).then(function (branch) {
console.log(branch); // This is your current branch
});
const branch = await git("status -sb",
(stdout) => stdout.match(/## (.*)/)[1]);
console.log(branch); // This is your current branch
```

The callback accepts 2 parameters, `(stdout, code)`, where `stdout` is the output of the git command and `code` is the exit code.
The callback accepts 2 parameters, `(stdout, error)`, where `stdout` is the output of the git command and `error` is either `null` or an `Error` in case the git command fails.

The return value of this function will be the resolved value of the promise.

If the `code` parameter is not specified, it'll be handled automatically and the promise will be rejected in case of non 0 code.
If the `error` parameter is not specified, it'll be handled automatically and the promise will be rejected in case of non 0 error codes.

```js
var git = require("git-promise");
const git = require("git-promise");

git("merge-base --is-ancestor master HEAD", function (stdout, code) {
if (code === 0) {
git("merge-base --is-ancestor master HEAD", function (stdout, error) {
if (!error) {
// the branch we are on is fast forward to master
return true;
} else if (code === 1) {
} else if (error.code === 1) {
// no, it's not
return false;
} else {
// some other error happened
throw new Error("Something bad happened: " + stdout);
throw error;
}
}).then(function (isFastForward) {
console.log(isFastForward);
}).fail(function (err) {
}).catch(function (err) {
// deal with the error
});
```

### Argument parsing

Version 1.0 changes the way the input command is parsed, so instead of executing anything that gets passed as the first parameter, it makes sure that `git` is the only executable used.

`git("status | grep hello")` won't be executed as a shell command, but everything will be passed as arguments to `git`, likely resulting in an error in this specific case.

If your `git` command stops working after upgrading to version 1.0
1. Make sure you're only executing git commands.
1. Try passing an array of arguments instead of a string. For instance: `git(["merge-base", "--is-ancestor", "master", "HEAD"]);`.

### Chaining commands

Imagine to be on a local branch which is not fast forward with master and you want to know which commit were pushed on master after the forking point:

```js
var git = require("git-promise");
const git = require("git-promise");

function findForkCommit () {
return git("merge-base master HEAD", function (output) {
return output.trim();
});
return git("merge-base master HEAD", output => output.trim());
}

function findChanges (forkCommit) {
return git("log " + forkCommit + "..master --format=oneline", function (output) {
return output.trim().split("\n");
});
return git("log " + forkCommit + "..master --format=oneline",
output => output.trim().split("\n"));
}

// synchronization can be done in many ways, for instance with Q
var Q = require("q");
[findForkCommit, findChanges].reduce(Q.when, Q({})).then(function (commits) {
console.log(commits);
});

// or simply using promises, simple cases only?
findForkCommit().then(findChanges).then(function (commits) {
console.log(commits);
});
const forkCommit = await findForkCommit();
const commits = await findChanges(forkCommit);
```

### Working directory
Expand All @@ -134,31 +133,27 @@ By default all git commands run in the current working directory (i.e. `process.
You can use the following syntax to run a git command in different folder

```js
var git = require("git-promise");
const git = require("git-promise");

git("blame file1.js", {cwd: "src/"}).then(function () {
// Blame someone
});
await git("blame file1.js", {cwd: "src/"});
```

### Custom git executable

By default any command tries to use `git` in `$PATH`, if you have installed `git` in a funky location you can override this value using `gitExec`.

```js
var git = require("git-promise");
const git = require("git-promise");

git("status", {gitExec: "/usr/local/sbin/git"}).then(function () {
// All good, I guess
});
await git("status", {gitExec: "/usr/local/sbin/git"});
```

## Utility methods

This module comes with some utility methods to parse the output of some git commands

```js
var util = require("git-promise/util");
const util = require("git-promise/util");
```

* `util.extractStatus(output [, lineSeparator])`
Expand Down Expand Up @@ -190,8 +185,8 @@ The method works both with or without option `-z`.
Try to determine if there's a merge conflict from the output of `git merge-tree`

```js
var git = require("git-promise");
var util = require("git-promise/util");
const git = require("git-promise");
const util = require("git-promise/util");

git("merge-tree <root-commit> <branch1> <branch2>").then(function (stdout) {
console.log(util.hasConflict(stdout));
Expand All @@ -200,6 +195,10 @@ git("merge-tree <root-commit> <branch1> <branch2>").then(function (stdout) {

## Release History

* 1.0.0
BREAKING CHANGE: The returned value is now a standard JavaScript `Promise`, not anymore a `Q` promise.
BREAKING CHANGE: Internally the library switches from `shell` to `execFile` to avoid problems with non sanitized input commands.
BREAKING CHANGE: Callbacks using 2 parameters now receive an error as second parameter instead of an error code.
* 0.3.1 Fix current working directory not switching back when command exits with error
* 0.3.0 Custom git executable with `gitExec` option
* 0.2.0 Change current working directory
Expand Down
97 changes: 39 additions & 58 deletions index.js
@@ -1,64 +1,45 @@
var shell = require("shelljs");
var originalSilent = shell.config.silent;
var Q = require("q");
const util = require("util");
const execFile = util.promisify(require("child_process").execFile);

module.exports = function (command, options, callback) {
var deferred = Q.defer();
const defaultCallback = (stdout) => stdout;
const defaultOptions = {};
const isString = (_) => typeof _ === "string";
const isObject = (_) => typeof _ === "object";
const isFunction = (_) => typeof _ === "function";

if (command.substring(0, 4) !== "git ") {
command = "git " + command;
}
for (var i = 1; i < arguments.length; i += 1) {
var arg = arguments[i];
if (typeof arg === "function") {
callback = arg;
} else if (typeof arg === "object") {
options = arg;
}
}
if (!callback) {
// If we completely ignore the command, resolve with the command output
callback = function (stdout) {
return stdout;
};
}
if (options && options.gitExec) {
command = command.replace(/^git/, options.gitExec);
}
module.exports = function (commandOrArgs, optionsOrCallback, callbackMaybe) {
const callback = [
optionsOrCallback,
callbackMaybe,
defaultCallback,
].find(isFunction);
const options = [
optionsOrCallback,
callbackMaybe,
defaultOptions,
].find(isObject);

if (options && options.cwd) {
shell.config.silent = true;
shell.pushd(options.cwd);
shell.config.silent = originalSilent;
// Strip `git ` from the beginning since it's reduntant
if (isString(commandOrArgs) && commandOrArgs.startsWith("git ")) {
commandOrArgs = commandOrArgs.substring(4);
}
shell.exec(command, {silent: true}, function (code, output) {
var args;

// If cwd was changed earlier, then change it back to process' root directory
if (options && options.cwd) {
shell.config.silent = true;
shell.popd();
shell.config.silent = originalSilent;
}

if (callback.length === 1) {
// Automatically handle non 0 exit codes
if (code !== 0) {
var error = new Error("'" + command + "' exited with error code " + code);
error.stdout = output;
return deferred.reject(error);
const execBinary = options.gitExec || "git";
const execOptions = {
cwd: options.cwd,
windowsHide: true,
};
const execArguments = isString(commandOrArgs)
? commandOrArgs.split(" ")
: commandOrArgs;
return execFile(execBinary, execArguments, execOptions).then(
({stdout}) => callback(stdout, null),
(error) => {
if (callback.length === 1) {
throw error;
} else {
// The callback is interested in the error, try to catch it.
return callback("", error);
}
args = [output];
} else {
// This callback is interested in the exit code, don't handle exit code
args = [output, code];
}

try {
deferred.resolve(callback.apply(null, args));
} catch (ex) {
deferred.reject(ex);
}
});
return deferred.promise;
},
);
};
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions package.json
Expand Up @@ -34,10 +34,6 @@
"git",
"shell"
],
"dependencies": {
"q": "~1.4.1",
"shelljs": "~0.5.3"
},
"devDependencies": {
"baretest": "1.0.0",
"eslint": "6.8.0"
Expand Down