diff --git a/.travis.yml b/.travis.yml index 505c78a7f..d10faa0fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ branches: - master - /^v\d+\.\d+\.\d+$/ +compiler: clang + os: - linux - osx @@ -26,11 +28,17 @@ addons: sources: - ubuntu-toolchain-r-test packages: + - build-essential + - libssl-dev - gcc-4.9 - g++-4.9 - lcov before_install: + - export CC=clang + - export CXX=clang++ + - export npm_config_clang=1 + - if [ $TRAVIS_OS_NAME != "linux" ]; then git clone https://github.com/creationix/nvm.git ./.nvm; source ./.nvm/nvm.sh; @@ -38,17 +46,17 @@ before_install: - nvm install $NODE_VERSION - - if [ $TRAVIS_OS_NAME == "linux" ]; then - export GYP_DEFINES="use_obsolete_asm=true"; + - if [ -z "$TRAVIS_TAG" ] && [ $TRAVIS_OS_NAME == "linux" ] && [ $NODE_VERSION == "0.12" ]; then + export GYP_DEFINES="coverage=1 use_obsolete_asm=true"; export JOBS=4; export CC=/usr/bin/gcc-4.9; export CXX=/usr/bin/g++-4.9; - fi - - - if [ $TRAVIS_OS_NAME == "linux" ] && [ $NODE_VERSION == "0.12" ]; then - export GYP_DEFINES="coverage=1 use_obsolete_asm=true"; + export npm_config_clang=0; wget http://downloads.sourceforge.net/ltp/lcov-1.10.tar.gz; tar xvfz lcov-1.10.tar.gz; + else + export GYP_DEFINES="use_obsolete_asm=true"; + export JOBS=4; fi install: @@ -66,7 +74,7 @@ before_script: - git config --global user.email johndoe@example.com script: - - if [ $TRAVIS_OS_NAME == "linux" ] && [ $NODE_VERSION == "0.12" ]; then + - if [ -z "$TRAVIS_TAG" ] && [ $TRAVIS_OS_NAME == "linux" ] && [ $NODE_VERSION == "0.12" ]; then npm test && npm run cov && npm run coveralls; else npm test; diff --git a/CHANGELOG.md b/CHANGELOG.md index f954d31e8..5746adfc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [0.11.0](https://github.com/nodegit/nodegit/releases/tag/v0.11.0) (2016-02-04) + +[Full Changelog](https://github.com/nodegit/nodegit/compare/v0.10.0...v0.11.0) + +- Change `Revert.commit` and `Revert.revert` to by async. [PR #887](https://github.com/nodegit/nodegit/pull/887 +- Added `RevWalk#fileHistoryWalk` for a faster way to retrieve history for a specific file. [PR #889](https://github.com/nodegit/nodegit/pull/889 + ## [0.10.0](https://github.com/nodegit/nodegit/releases/tag/v0.10.0) (2016-02-01) [Full Changelog](https://github.com/nodegit/nodegit/compare/v0.9.0...v0.10.0) @@ -13,18 +20,18 @@ - `ConvenientPatch#hunks` returns a promise with an array of `ConvenientHunks`. - `ConvenientHunk` - `ConvenientHunk` does not have an exposed diffHunk associated with it, but does have the same members as diffHunk: - - `size()` : number of lines in the hunk - - `oldStart()` : old starting position - - `oldLines()` : number of lines in old file - - `newStart()` : new starting position - - `newLines()` : number of lines in new file - - `headerLen()` : length of header - - `header()` : returns the header of the hunk - - `lines()` : returns a promise containing `DiffLines`, not `ConvenientLines`. + - `size()` : number of lines in the hunk + - `oldStart()` : old starting position + - `oldLines()` : number of lines in old file + - `newStart()` : new starting position + - `newLines()` : number of lines in new file + - `headerLen()` : length of header + - `header()` : returns the header of the hunk + - `lines()` : returns a promise containing `DiffLines`, not `ConvenientLines`. - `DiffLine` - `DiffLine` now contains the members `rawContent()` and `content()`. - - `rawContent()` contains the unformatted content of the line. This is no longer a string from the line to the end of the file. - - `content()` contains the utf8 formatted content of the line. + - `rawContent()` contains the unformatted content of the line. This is no longer a string from the line to the end of the file. + - `content()` contains the utf8 formatted content of the line. ## [0.9.0](https://github.com/nodegit/nodegit/releases/tag/v0.9.0) (2016-01-21) diff --git a/README.md b/README.md index f8206d211..189cb4dcd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ NodeGit -**Stable: 0.10.0** +**Stable: 0.11.0** ## Have a problem? Come chat with us! ## diff --git a/examples/walk-history-for-file.js b/examples/walk-history-for-file.js index 340308abe..f09415455 100644 --- a/examples/walk-history-for-file.js +++ b/examples/walk-history-for-file.js @@ -1,49 +1,62 @@ var nodegit = require("../"), - path = require("path"); + path = require("path"), + historyFile = "generate/input/descriptor.json", + walker, + historyCommits = [], + commit, + repo; // This code walks the history of the master branch and prints results // that look very similar to calling `git log` from the command line +function compileHistory(resultingArrayOfCommits) { + var lastSha; + if (historyCommits.length > 0) { + lastSha = historyCommits[historyCommits.length - 1].commit.sha(); + if ( + resultingArrayOfCommits.length == 1 && + resultingArrayOfCommits[0].commit.sha() == lastSha + ) { + return; + } + } + + resultingArrayOfCommits.forEach(function(entry) { + historyCommits.push(entry); + }); + + lastSha = historyCommits[historyCommits.length - 1].commit.sha(); + + walker = repo.createRevWalk(); + walker.push(lastSha); + walker.sorting(nodegit.Revwalk.SORT.TIME); + + return walker.fileHistoryWalk(historyFile, 500) + .then(compileHistory); +} + nodegit.Repository.open(path.resolve(__dirname, "../.git")) - .then(function(repo) { + .then(function(r) { + repo = r; return repo.getMasterCommit(); }) .then(function(firstCommitOnMaster){ // History returns an event. - var history = firstCommitOnMaster.history(nodegit.Revwalk.SORT.Time); - var commits = []; - - // History emits "commit" event for each commit in the branch's history - history.on("commit", function(commit) { - return commit.getDiff() - .then(function(diffList) { - diffList.map(function(diff) { - diff.patches().then(function(patches) { - patches.map(function(patch) { - var result = - !!~patch.oldFile().path().indexOf("descriptor.json") || - !!~patch.newFile().path().indexOf("descriptor.json"); - - if(result && !~commits.indexOf(commit)) { - commits.push(commit); - } - }); - }); - }); - }); - }); + walker = repo.createRevWalk(); + walker.push(firstCommitOnMaster.sha()); + walker.sorting(nodegit.Revwalk.SORT.Time); - history.on("end", function() { - commits.forEach(function(commit) { - console.log("commit " + commit.sha()); - console.log("Author:", commit.author().name() + - " <" + commit.author().email() + ">"); - console.log("Date:", commit.date()); - console.log("\n " + commit.message()); - }); + return walker.fileHistoryWalk(historyFile, 500); + }) + .then(compileHistory) + .then(function() { + historyCommits.forEach(function(entry) { + commit = entry.commit; + console.log("commit " + commit.sha()); + console.log("Author:", commit.author().name() + + " <" + commit.author().email() + ">"); + console.log("Date:", commit.date()); + console.log("\n " + commit.message()); }); - - // Don't forget to call `start()`! - history.start(); }) .done(); diff --git a/generate/input/descriptor.json b/generate/input/descriptor.json index ffd28f344..f7aef8c27 100644 --- a/generate/input/descriptor.json +++ b/generate/input/descriptor.json @@ -1795,6 +1795,20 @@ }, "revert": { "functions": { + "git_revert": { + "isAsync": true, + "return": { + "isErrorCode": true + } + }, + "git_revert_commit": { + "isAsync": true, + "args": { + "merge_options": { + "isOptional": true + } + } + }, "git_revert_init_options": { "ignore": true } @@ -1843,6 +1857,10 @@ "ignore": true }, "revwalk": { + "dependencies": [ + "../include/commit.h", + "../include/functions/copy.h" + ], "functions": { "git_revwalk_add_hide_cb": { "ignore": true diff --git a/generate/input/libgit2-supplement.json b/generate/input/libgit2-supplement.json index 708b78982..f525a6390 100644 --- a/generate/input/libgit2-supplement.json +++ b/generate/input/libgit2-supplement.json @@ -200,6 +200,36 @@ "isErrorCode": true } }, + "git_revwalk_file_history_walk": { + "args": [ + { + "name": "file_path", + "type": "const char *" + }, + { + "name": "max_count", + "type": "int" + }, + { + "name": "out", + "type": "std::vector< std::pair > *> *" + }, + { + "name": "walk", + "type": "git_revwalk *" + } + ], + "type": "function", + "isManual": true, + "cFile": "generate/templates/manual/revwalk/file_history_walk.cc", + "isAsync": true, + "isPrototypeMethod": true, + "group": "revwalk", + "return": { + "type": "int", + "isErrorCode": true + } + }, "git_stash_save": { "type": "function", "file": "stash.h", @@ -263,7 +293,8 @@ [ "revwalk", [ - "git_revwalk_fast_walk" + "git_revwalk_fast_walk", + "git_revwalk_file_history_walk" ] ], [ diff --git a/generate/templates/manual/revwalk/file_history_walk.cc b/generate/templates/manual/revwalk/file_history_walk.cc new file mode 100644 index 000000000..2ad9a24ed --- /dev/null +++ b/generate/templates/manual/revwalk/file_history_walk.cc @@ -0,0 +1,266 @@ +NAN_METHOD(GitRevwalk::FileHistoryWalk) +{ + if (info.Length() == 0 || !info[0]->IsString()) { + return Nan::ThrowError("File path to get the history is required."); + } + + if (info.Length() == 1 || !info[1]->IsNumber()) { + return Nan::ThrowError("Max count is required and must be a number."); + } + + if (info.Length() == 2 || !info[2]->IsFunction()) { + return Nan::ThrowError("Callback is required and must be a Function."); + } + + FileHistoryWalkBaton* baton = new FileHistoryWalkBaton; + + baton->error_code = GIT_OK; + baton->error = NULL; + String::Utf8Value from_js_file_path(info[0]->ToString()); + baton->file_path = strdup(*from_js_file_path); + baton->max_count = (unsigned int)info[1]->ToNumber()->Value(); + baton->out = new std::vector< std::pair > *>; + baton->out->reserve(baton->max_count); + baton->walk = Nan::ObjectWrap::Unwrap(info.This())->GetValue(); + + Nan::Callback *callback = new Nan::Callback(Local::Cast(info[2])); + FileHistoryWalkWorker *worker = new FileHistoryWalkWorker(baton, callback); + worker->SaveToPersistent("fileHistoryWalk", info.This()); + + Nan::AsyncQueueWorker(worker); + return; +} + +void GitRevwalk::FileHistoryWalkWorker::Execute() +{ + git_repository *repo = git_revwalk_repository(baton->walk); + git_oid *nextOid = (git_oid *)malloc(sizeof(git_oid)); + giterr_clear(); + for ( + unsigned int i = 0; + i < baton->max_count && (baton->error_code = git_revwalk_next(nextOid, baton->walk)) == GIT_OK; + ++i + ) { + // check if this commit has the file + git_commit *nextCommit; + + if ((baton->error_code = git_commit_lookup(&nextCommit, repo, nextOid)) != GIT_OK) { + break; + } + + git_tree *thisTree, *parentTree; + if ((baton->error_code = git_commit_tree(&thisTree, nextCommit)) != GIT_OK) { + git_commit_free(nextCommit); + break; + } + + git_diff *diffs; + git_commit *parent; + unsigned int parents = git_commit_parentcount(nextCommit); + if (parents > 1) { + git_commit_free(nextCommit); + continue; + } else if (parents == 1) { + if ((baton->error_code = git_commit_parent(&parent, nextCommit, 0)) != GIT_OK) { + git_commit_free(nextCommit); + break; + } + if ( + (baton->error_code = git_commit_tree(&parentTree, parent)) != GIT_OK || + (baton->error_code = git_diff_tree_to_tree(&diffs, repo, parentTree, thisTree, NULL)) != GIT_OK + ) { + git_commit_free(nextCommit); + git_commit_free(parent); + break; + } + } else { + if ((baton->error_code = git_diff_tree_to_tree(&diffs, repo, NULL, thisTree, NULL)) != GIT_OK) { + git_commit_free(nextCommit); + break; + } + } + + bool flag = false; + bool doRenamedPass = false; + unsigned int numDeltas = git_diff_num_deltas(diffs); + for (unsigned int j = 0; j < numDeltas; ++j) { + git_patch *nextPatch; + baton->error_code = git_patch_from_diff(&nextPatch, diffs, j); + + if (baton->error_code < GIT_OK) { + break; + } + + if (nextPatch == NULL) { + continue; + } + + const git_diff_delta *delta = git_patch_get_delta(nextPatch); + bool isEqualOldFile = !strcmp(delta->old_file.path, baton->file_path); + bool isEqualNewFile = !strcmp(delta->new_file.path, baton->file_path); + + if (isEqualNewFile) { + if (delta->status == GIT_DELTA_ADDED || delta->status == GIT_DELTA_DELETED) { + doRenamedPass = true; + break; + } + std::pair > *historyEntry; + if (!isEqualOldFile) { + historyEntry = new std::pair >( + nextCommit, + std::pair(strdup(delta->old_file.path), delta->status) + ); + } else { + historyEntry = new std::pair >( + nextCommit, + std::pair(strdup(delta->new_file.path), delta->status) + ); + } + baton->out->push_back(historyEntry); + flag = true; + } + + git_patch_free(nextPatch); + + if (flag) { + break; + } + } + + if ( + doRenamedPass && + (baton->error_code = git_diff_find_similar(diffs, NULL)) == GIT_OK + ) { + flag = false; + numDeltas = git_diff_num_deltas(diffs); + for (unsigned int j = 0; j < numDeltas; ++j) { + git_patch *nextPatch; + baton->error_code = git_patch_from_diff(&nextPatch, diffs, j); + + if (baton->error_code < GIT_OK) { + break; + } + + if (nextPatch == NULL) { + continue; + } + + const git_diff_delta *delta = git_patch_get_delta(nextPatch); + bool isEqualOldFile = !strcmp(delta->old_file.path, baton->file_path); + bool isEqualNewFile = !strcmp(delta->new_file.path, baton->file_path); + + if (isEqualNewFile) { + std::pair > *historyEntry; + if (!isEqualOldFile) { + historyEntry = new std::pair >( + nextCommit, + std::pair(strdup(delta->old_file.path), delta->status) + ); + } else { + historyEntry = new std::pair >( + nextCommit, + std::pair(strdup(delta->new_file.path), delta->status) + ); + } + baton->out->push_back(historyEntry); + flag = true; + } + + git_patch_free(nextPatch); + + if (flag) { + break; + } + } + } + + git_diff_free(diffs); + + if (!flag && nextCommit != NULL) { + git_commit_free(nextCommit); + } + + if (baton->error_code != GIT_OK) { + break; + } + } + + free(nextOid); + + if (baton->error_code != GIT_OK) { + if (baton->error_code != GIT_ITEROVER) { + baton->error = git_error_dup(giterr_last()); + + while(!baton->out->empty()) + { + std::pair > *pairToFree = baton->out->back(); + baton->out->pop_back(); + git_commit_free(pairToFree->first); + free(pairToFree->second.first); + free(pairToFree); + } + + delete baton->out; + + baton->out = NULL; + } + } else { + baton->error_code = GIT_OK; + } +} + +void GitRevwalk::FileHistoryWalkWorker::HandleOKCallback() +{ + if (baton->out != NULL) { + unsigned int size = baton->out->size(); + Local result = Nan::New(size); + for (unsigned int i = 0; i < size; i++) { + Local historyEntry = Nan::New(); + std::pair > *batonResult = baton->out->at(i); + Nan::Set(historyEntry, Nan::New("commit").ToLocalChecked(), GitCommit::New(batonResult->first, true)); + Nan::Set(historyEntry, Nan::New("status").ToLocalChecked(), Nan::New(batonResult->second.second)); + if (batonResult->second.second == GIT_DELTA_RENAMED) { + Nan::Set(historyEntry, Nan::New("oldName").ToLocalChecked(), Nan::New(batonResult->second.first).ToLocalChecked()); + } + Nan::Set(result, Nan::New(i), historyEntry); + + free(batonResult->second.first); + free(batonResult); + } + + Local argv[2] = { + Nan::Null(), + result + }; + callback->Call(2, argv); + + delete baton->out; + return; + } + + if (baton->error) { + Local argv[1] = { + Nan::Error(baton->error->message) + }; + callback->Call(1, argv); + if (baton->error->message) + { + free((void *)baton->error->message); + } + + free((void *)baton->error); + return; + } + + if (baton->error_code < 0) { + Local err = Nan::Error("Method next has thrown an error.")->ToObject(); + err->Set(Nan::New("errno").ToLocalChecked(), Nan::New(baton->error_code)); + Local argv[1] = { + err + }; + callback->Call(1, argv); + return; + } + + callback->Call(0, NULL); +} diff --git a/generate/templates/templates/class_header.h b/generate/templates/templates/class_header.h index 74ec20094..a378f11a5 100644 --- a/generate/templates/templates/class_header.h +++ b/generate/templates/templates/class_header.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "async_baton.h" #include "promise_completion.h" diff --git a/generate/templates/templates/struct_header.h b/generate/templates/templates/struct_header.h index e4a3b98d4..c1f30cc4e 100644 --- a/generate/templates/templates/struct_header.h +++ b/generate/templates/templates/struct_header.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "async_baton.h" diff --git a/guides/repositories/README.md b/guides/repositories/README.md index 65cb063c2..a436a24c9 100644 --- a/guides/repositories/README.md +++ b/guides/repositories/README.md @@ -85,7 +85,7 @@ You can pass a second function parameter to the `.then` method that will have the reason why a promise failed in it's first argument. ``` javascript -NodeGit.Repository.open(pathToRepo).then(function (sucessfulResult) { +NodeGit.Repository.open(pathToRepo).then(function (successfulResult) { // This is the first function of the then which contains the successfully // calculated result of the promise }, function (reasonForFailure) { @@ -100,7 +100,7 @@ You can also append a `.catch` to the end of a promise chain which will receive any promise failure that isn't previously caught ``` javascript -NodeGit.Repository.open(pathToRepo).then(function (sucessfulResult) { +NodeGit.Repository.open(pathToRepo).then(function (successfulResult) { // This is the first function of the then which contains the successfully // calculated result of the promise }) @@ -115,7 +115,7 @@ If you append a `.done` at the end of your chain, you will have any error that wasn't previously handled by the above 2 methods thrown. ``` javascript -NodeGit.Repository.open(pathToRepo).then(function (sucessfulResult) { +NodeGit.Repository.open(pathToRepo).then(function (successfulResult) { // This is the first function of the then which contains the successfully // calculated result of the promise }) diff --git a/lib/convenient_hunks.js b/lib/convenient_hunks.js new file mode 100644 index 000000000..7c8edb17a --- /dev/null +++ b/lib/convenient_hunks.js @@ -0,0 +1,63 @@ +var NodeGit = require("../"); + +var ConvenientHunk = NodeGit.ConvenientHunk; + +var header = ConvenientHunk.prototype.header; + /** + * Diff header string that represents the context of this hunk + * of the diff. Something like `@@ -169,14 +167,12 @@ ...` + * @return {String} + */ +ConvenientHunk.prototype.header = header; + +var size = ConvenientHunk.prototype.size; +/** + * Number of lines in this hunk + * @return {Number} + */ +ConvenientHunk.prototype.size = size; + +var lines = ConvenientHunk.prototype.lines; +/** + * The lines in this hunk + * @async + * @return {Array} + */ +ConvenientHunk.prototype.lines = lines; + +var headerLen = ConvenientHunk.prototype.headerLen; +/** + * The length of the header + * @return {Number} + */ +ConvenientHunk.prototype.headerLen = headerLen; + +var newLines = ConvenientHunk.prototype.newLines; +/** + * The number of new lines in the hunk + * @return {Number} + */ +ConvenientHunk.prototype.newLines = newLines; + +var newStart = ConvenientHunk.prototype.newStart; +/** + * The starting offset of the first new line in the file + * @return {Number} + */ +ConvenientHunk.prototype.newStart = newStart; + +var oldLines = ConvenientHunk.prototype.oldLines; +/** + * The number of old lines in the hunk + * @return {Number} + */ +ConvenientHunk.prototype.oldLines = oldLines; + +var oldStart = ConvenientHunk.prototype.oldStart; +/** + * The starting offset of the first old line in the file + * @return {Number} + */ +ConvenientHunk.prototype.oldStart = oldStart; + + exports.module = ConvenientHunk; diff --git a/lib/convenient_patch.js b/lib/convenient_patch.js new file mode 100644 index 000000000..b8f3d86e5 --- /dev/null +++ b/lib/convenient_patch.js @@ -0,0 +1,134 @@ +var NodeGit = require("../"); + +var ConvenientPatch = NodeGit.ConvenientPatch; + +var oldFile = ConvenientPatch.prototype.oldFile; +/** + * Old name of the file + * @return {String} + */ +ConvenientPatch.prototype.oldFile = oldFile; + +var newFile = ConvenientPatch.prototype.newFile; +/** + * New name of the file + * @return {String} + */ +ConvenientPatch.prototype.newFile = newFile; + +var size = ConvenientPatch.prototype.size; +/** + * The number of hunks in this patch + * @return {Number} + */ +ConvenientPatch.prototype.size = size; + +var hunks = ConvenientPatch.prototype.hunks; +/** + * The hunks in this patch + * @async + * @return {Array} a promise that resolves to an array of + * ConvenientHunks + */ +ConvenientPatch.prototype.hunks = hunks; + +var status = ConvenientPatch.prototype.status; +/** + * The status of this patch (unmodified, added, deleted) + * @return {Number} + */ +ConvenientPatch.prototype.status = status; + +/** + * @typedef lineStats + * @type {Object} + * @property {number} total_context # of contexts in the patch + * @property {number} total_additions # of lines added in the patch + * @property {number} total_deletions # of lines deleted in the patch + */ + +var lineStats = ConvenientPatch.prototype.lineStats; +/** + * The line statistics of this patch (#contexts, #added, #deleted) + * @return {lineStats} + */ +ConvenientPatch.prototype.lineStats = lineStats; + +var isUnmodified = ConvenientPatch.prototype.isUnmodified; +/** + * Is this an unmodified patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isUnmodified = isUnmodified; + +var isAdded = ConvenientPatch.prototype.isAdded; +/** + * Is this an added patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isAdded = isAdded; + +var isDeleted = ConvenientPatch.prototype.isDeleted; +/** + * Is this a deleted patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isDeleted = isDeleted; + +var isModified = ConvenientPatch.prototype.isModified; +/** + * Is this an modified patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isModified = isModified; + +var isRenamed = ConvenientPatch.prototype.isRenamed; +/** + * Is this a renamed patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isRenamed = isRenamed; + +var isCopied = ConvenientPatch.prototype.isCopied; +/** + * Is this a copied patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isCopied = isCopied; + +var isIgnored = ConvenientPatch.prototype.isIgnored; +/** + * Is this an ignored patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isIgnored = isIgnored; + +var isUntracked = ConvenientPatch.prototype.isUntracked; +/** + * Is this an untracked patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isUntracked = isUntracked; + +var isTypeChange = ConvenientPatch.prototype.isTypeChange; +/** + * Is this a type change? + * @return {Boolean} + */ +ConvenientPatch.prototype.isTypeChange = isTypeChange; + +var isUnreadable = ConvenientPatch.prototype.isUnreadable; +/** + * Is this an undreadable patch? + * @return {Boolean} + */ +ConvenientPatch.prototype.isUnreadable = isUnreadable; + +var isConflicted = ConvenientPatch.prototype.isConflicted; +/** + * Is this a conflicted patch? + * @return {Boolean} + */ + ConvenientPatch.prototype.isConflicted = isConflicted; + + exports.module = ConvenientPatch; diff --git a/lib/revert.js b/lib/revert.js new file mode 100644 index 000000000..90022508d --- /dev/null +++ b/lib/revert.js @@ -0,0 +1,47 @@ +var NodeGit = require("../"); +var normalizeOptions = NodeGit.Utils.normalizeOptions; + +var Revert = NodeGit.Revert; + +/** + * Reverts the given commit against the given "our" commit, producing an index + * that reflects the result of the revert. + * + * @async + * @param {Repository} repo the repository that contains the given commits. + * @param {Commit} revert_commit the commit to revert + * @param {Commit} our_commit the commit to revert against (e.g. HEAD) + * @param {Number} mainline the parent of the revert commit, if it is a merge + * @param {MergeOptions} merge_options the merge options (or null for defaults) + * + * @return {Index} the index result + */ +var commit = Revert.commit; +Revert.commit = function( + repo, + revert_commit, + our_commit, + mainline, + merge_options, + callback +) { + merge_options = normalizeOptions(merge_options, NodeGit.MergeOptions); + + return commit.call( + this, + repo, + revert_commit, + our_commit, + mainline, + merge_options + ) + .then(function(result) { + if (typeof callback === "function") { + callback(null, result); + } + + return result; + }, callback); +}; + +module.exports = Revert; diff --git a/lib/revwalk.js b/lib/revwalk.js index 4d9844cce..d3cfa0456 100644 --- a/lib/revwalk.js +++ b/lib/revwalk.js @@ -123,3 +123,21 @@ Revwalk.prototype.getCommits = function(count) { return Promise.all(promises); }); }; + +/** + * @typedef historyEntry + * @type {Object} + * @property {Commit} commit the commit for this entry + * @property {Number} status the status of the file in the commit + * @property {String} oldName the old name that is provided when status is + * renamed + */ + +var fileHistoryWalk = Revwalk.prototype.fileHistoryWalk; +/** + * @param {String} filePath + * @param {Number} max_count + * @async + * @return {Array} + */ +Revwalk.prototype.fileHistoryWalk = fileHistoryWalk; diff --git a/package.json b/package.json index 76063d750..b41500a0c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nodegit", "description": "Node.js libgit2 asynchronous native bindings", - "version": "0.10.0", + "version": "0.11.0", "homepage": "http://nodegit.org", "keywords": [ "libgit2", @@ -75,8 +75,8 @@ "cppcov": "mkdir -p test/coverage/cpp && ./lcov-1.10/bin/lcov --gcov-tool /usr/bin/gcov-4.9 --capture --directory build/Release/obj.target/nodegit/src --output-file test/coverage/cpp/lcov_full.info", "mergecov": "lcov-result-merger 'test/**/*.info' 'test/coverage/merged.lcov' && ./lcov-1.10/bin/genhtml test/coverage/merged.lcov --output-directory test/coverage/report", "cov": "npm run cppcov && npm run filtercov && npm run mergecov", - "mocha": "mocha test/runner test/tests", - "mochaDebug": "mocha --debug-brk test/runner test/tests", + "mocha": "mocha test/runner test/tests --timeout 15000", + "mochaDebug": "mocha --debug-brk test/runner test/tests --timeout 15000", "test": "npm run lint && node --expose-gc test", "generateJson": "node generate/scripts/generateJson", "generateNativeCode": "node generate/scripts/generateNativeCode", diff --git a/test/index.js b/test/index.js index ee64e4484..52cbff50e 100644 --- a/test/index.js +++ b/test/index.js @@ -12,7 +12,9 @@ if (process.platform === 'win32') { var args = cov.concat([ "test/runner", "test/tests", - "--expose-gc" + "--expose-gc", + "--timeout", + "15000" ]); if (!process.env.APPVEYOR && !process.env.TRAVIS) { diff --git a/test/tests/remote.js b/test/tests/remote.js index 747bf2eba..ff7948cad 100644 --- a/test/tests/remote.js +++ b/test/tests/remote.js @@ -180,8 +180,6 @@ describe("Remote", function() { }); it("can fetch from a private repository", function() { - this.timeout(15000); - var repo = this.repository; var remote = Remote.create(repo, "private", privateUrl); var fetchOptions = { @@ -208,8 +206,6 @@ describe("Remote", function() { it("can reject fetching from private repository without valid credentials", function() { - this.timeout(15000); - var repo = this.repository; var remote = Remote.create(repo, "private", privateUrl); var firstPass = true; @@ -241,9 +237,6 @@ describe("Remote", function() { }); it("can fetch from all remotes", function() { - // Set a reasonable timeout here for the fetchAll test - this.timeout(15000); - var repository = this.repository; Remote.create(repository, "test1", url); Remote.create(repository, "test2", url2); @@ -261,7 +254,6 @@ describe("Remote", function() { }); it("will reject if credentials promise rejects", function() { - this.timeout(5000); var repo = this.repository; var branch = "should-not-exist"; return Remote.lookup(repo, "origin") @@ -335,7 +327,6 @@ describe("Remote", function() { }); it("cannot push to a repository with invalid credentials", function() { - this.timeout(5000); var repo = this.repository; var branch = "should-not-exist"; return Remote.lookup(repo, "origin") diff --git a/test/tests/revert.js b/test/tests/revert.js new file mode 100644 index 000000000..b3f151831 --- /dev/null +++ b/test/tests/revert.js @@ -0,0 +1,63 @@ +var _ = require("lodash"); +var assert = require("assert"); +var RepoUtils = require("../utils/repository_setup"); +var path = require("path"); +var fs = require("fs"); +var local = path.join.bind(path, __dirname); + +describe("Revert", function() { + var NodeGit = require("../../"); + + var Revert = NodeGit.Revert; + var RevertOptions = NodeGit.RevertOptions; + + var test; + var fileName = "foobar.js"; + var repoPath = local("../repos/revertRepo"); + + beforeEach(function() { + test = this; + + return RepoUtils.createRepository(repoPath) + .then(function(repository) { + test.repository = repository; + + return RepoUtils.commitFileToRepo( + repository, + fileName, + "line1\nline2\nline3" + ); + }) + .then(function(firstCommit) { + test.firstCommit = firstCommit; + }); + }); + + it("revert modifies the working directoy", function() { + var fileStats = fs.statSync(path.join(repoPath, fileName)); + assert.ok(fileStats.isFile()); + + Revert.revert(test.repository, test.firstCommit, new RevertOptions()) + .then(function() { + try { + fs.statSync(path.join(repoPath, fileName)); + assert.fail("Working directory was not reverted"); + } + catch (error) { + // pass + } + }); + }); + + it("revert modifies the index", function() { + Revert.revert(test.repository, test.firstCommit, new RevertOptions()) + .then(function() { + return test.repository.index(); + }) + .then(function(index) { + var entries = index.entries; + assert.equal(1, entries.length); + assert.ok(_.endsWith(fileName, entries[0].path)); + }); + }); +}); diff --git a/test/tests/revwalk.js b/test/tests/revwalk.js index 79fc58e01..518dd3cd3 100644 --- a/test/tests/revwalk.js +++ b/test/tests/revwalk.js @@ -1,4 +1,7 @@ var assert = require("assert"); +var RepoUtils = require("../utils/repository_setup"); +var promisify = require("promisify-node"); +var fse = promisify(require("fs-extra")); var path = require("path"); var local = path.join.bind(path, __dirname); @@ -27,6 +30,7 @@ describe("Revwalk", function() { beforeEach(function() { this.walker = this.repository.createRevWalk(); + this.walker.sorting(NodeGit.Revwalk.SORT.TIME); this.walker.push(this.commit.id()); }); @@ -141,7 +145,7 @@ describe("Revwalk", function() { }); }); - it("can get do a fast walk", function() { + it("can do a fast walk", function() { var test = this; var magicSha = "b8a94aefb22d0534cc0e5acf533989c13d8725dc"; @@ -152,6 +156,140 @@ describe("Revwalk", function() { }); }); + it("can get the history of a file", function() { + var test = this; + var magicShas = [ + "6ed3027eda383d417457b99b38c73f88f601c368", + "95cefff6aabd3c1f6138ec289f42fec0921ff610", + "7ad92a7e4d26a1af93f3450aea8b9d9b8069ea8c", + "96f077977eb1ffcb63f9ce766cdf110e9392fdf5", + "694adc5369687c47e02642941906cfc5cb21e6c2", + "eebd0ead15d62eaf0ba276da53af43bbc3ce43ab", + "1273fff13b3c28cfdb13ba7f575d696d2a8902e1" + ]; + + return test.walker.fileHistoryWalk("include/functions/copy.h", 1000) + .then(function(results) { + var shas = results.map(function(result) { + return result.commit.sha(); + }); + assert.equal(magicShas.length, shas.length); + magicShas.forEach(function(sha, i) { + assert.equal(sha, shas[i]); + }); + }); + }); + + it("can get the history of a file while ignoring parallel branches", + function() { + var test = this; + var magicShas = [ + "f80e085e3118bbd6aad49dad7c53bdc37088bf9b", + "907b29d8a3b765570435c922a59cd849836a7b51" + ]; + var shas; + var walker = test.repository.createRevWalk(); + walker.sorting(NodeGit.Revwalk.SORT.TIME); + walker.push("115d114e2c4d5028c7a78428f16a4528c51be7dd"); + + return walker.fileHistoryWalk("README.md", 15) + .then(function(results) { + shas = results.map(function(result) { + return result.commit.sha(); + }); + assert.equal(magicShas.length, shas.length); + magicShas.forEach(function(sha, i) { + assert.equal(sha, shas[i]); + }); + + magicShas = [ + "4a34168b80fe706f52417106821c9cbfec630e47", + "f80e085e3118bbd6aad49dad7c53bdc37088bf9b", + "694b2d703a02501f288269bea7d1a5d643a83cc8", + "907b29d8a3b765570435c922a59cd849836a7b51" + ]; + + walker = test.repository.createRevWalk(); + walker.sorting(NodeGit.Revwalk.SORT.TIME); + walker.push("d46f7da82969ca6620864d79a55b951be0540bda"); + + return walker.fileHistoryWalk("README.md", 50); + }) + .then(function(results) { + shas = results.map(function(result) { + return result.commit.sha(); + }); + assert.equal(magicShas.length, shas.length); + magicShas.forEach(function(sha, i) { + assert.equal(sha, shas[i]); + }); + }); + }); + + it("can yield information about renames in a file history walk", + function() { + var treeOid; + var repo; + var fileNameA = "a.txt"; + var fileNameB = "b.txt"; + var repoPath = local("../repos/renamedFileRepo"); + var signature = NodeGit.Signature.create("Foo bar", + "foo@bar.com", 123456789, 60); + return RepoUtils.createRepository(repoPath) + .then(function(r) { + repo = r; + return RepoUtils.commitFileToRepo( + repo, + fileNameA, + "line1\nline2\nline3\n" + ); + }) + .then(function() { + return fse.move( + path.join(repoPath, fileNameA), + path.join(repoPath, fileNameB) + ); + }) + .then(function() { + return repo.openIndex() + .then(function(index) { + index.read(1); + index.addByPath(fileNameB); + index.removeByPath(fileNameA); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oidResult) { + treeOid = oidResult; + return NodeGit.Reference.nameToId(repo, "HEAD"); + }) + .then(function(head) { + return repo.getCommit(head); + }) + .then(function(head) { + return repo.createCommit("HEAD", signature, signature, + "renamed commit", treeOid, [head]); + }) + .then(function() { + return NodeGit.Reference.nameToId(repo, "HEAD"); + }) + .then(function(commitOid) { + var walker = repo.createRevWalk(); + walker.sorting(NodeGit.Revwalk.SORT.TIME); + walker.push(commitOid.tostrS()); + return walker.fileHistoryWalk(fileNameB, 5); + }) + .then(function(results) { + assert.equal(results[0].status, NodeGit.Diff.DELTA.RENAMED); + assert.equal(results[0].oldName, fileNameA); + }) + .then(function() { + return fse.remove(repoPath); + }); + }); + // This test requires forcing garbage collection, so mocha needs to be run // via node rather than npm, with a la `node --expose-gc [pathtohmoca] // [testglob]`