diff --git a/example/merge-cleanly.js b/example/merge-cleanly.js new file mode 100644 index 000000000..6fabc672d --- /dev/null +++ b/example/merge-cleanly.js @@ -0,0 +1,118 @@ +var nodegit = require('../'); +var path = require('path'); +var Promise = require('nodegit-promise'); +var promisify = require('promisify-node'); +var fse = promisify(require('fs-extra')); +fse.ensureDir = promisify(fse.ensureDir); + +var ourFileName = 'ourNewFile.txt'; +var ourFileContent = 'I like Toll Roads. I have an EZ-Pass!'; +var ourBranchName = "ours"; + +var theirFileName = 'theirNewFile.txt'; +var theirFileContent = "I'm skeptical about Toll Roads"; +var theirBranchName = "theirs"; + +var repoDir = '../../newRepo'; + +var repository; +var ourCommit; +var theirCommit; +var ourBranch; +var theirBranch; + +var ourSignature = nodegit.Signature.create("Ron Paul", "RonPaul@TollRoadsRBest.info", 123456789, 60); +var theirSignature = nodegit.Signature.create("Greg Abbott", "Gregggg@IllTollYourFace.us", 123456789, 60); + +// Create a new repository in a clean directory, and add our first file +fse.remove(path.resolve(__dirname, repoDir)) +.then(function() { + return fse.ensureDir(path.resolve(__dirname, repoDir)); +}) +.then(function() { + return nodegit.Repository.init(path.resolve(__dirname, repoDir), 0); +}) +.then(function(repo) { + repository = repo; + return fse.writeFile(path.join(repository.workdir(), ourFileName), ourFileContent); +}) + +// Load up the repository index and make our initial commit to HEAD +.then(function() { + return repository.openIndex(); +}) +.then(function(index) { + index.read(1); + index.addByPath(ourFileName); + index.write() + + return index.writeTree(); +}) +.then(function(oid) { + return repository.createCommit('HEAD', ourSignature, ourSignature, 'we made a commit', oid, []); +}) + +// Get commit object from the oid, and create our new branches at that position +.then(function(commitOid) { + return repository.getCommit(commitOid).then(function(commit) { + ourCommit = commit; + }).then(function() { + return repository.createBranch(ourBranchName, commitOid).then(function(branch) { + ourBranch = branch; + return repository.createBranch(theirBranchName, commitOid); + }); + }); +}) + +// Create a new file, stage it and commit it to our second branch +.then(function(branch) { + theirBranch = branch; + return fse.writeFile(path.join(repository.workdir(), theirFileName), theirFileContent); +}) +.then(function() { + return repository.openIndex(); +}) +.then(function(index) { + index.read(1); + index.addByPath(theirFileName); + index.write() + + return index.writeTree(); +}) +.then(function(oid) { + // You don't have to change head to make a commit to a different branch. + return repository.createCommit(theirBranch.name(), theirSignature, theirSignature, 'they made a commit', oid, [ourCommit]); +}) +.then(function(commitOid) { + return repository.getCommit(commitOid).then(function(commit) { + theirCommit = commit; + }); +}) + + +// Merge the two commits +.then(function() { + return nodegit.Merge.commits(repository, ourCommit, theirCommit); +}) + + +// Merging returns an index that isn't backed by the repository. +// You have to manually check for merge conflicts. If there are none +// you just have to write the index. You do have to write it to +// the repository instead of just writing it. +.then(function(index) { + if (!index.hasConflicts()) { + index.write() + return index.writeTreeTo(repository); + } +}) + + +// Create our merge commit back on our branch +.then(function(oid) { + return repository.createCommit(ourBranch.name(), ourSignature, ourSignature, 'we merged their commit', oid, [ourCommit, theirCommit]); +}) +.done(function(commitId) { + // We never changed the HEAD after the initial commit; it should still be the same as master. + console.log('New Commit: ', commitId); +}); diff --git a/example/merge-with-conflicts.js b/example/merge-with-conflicts.js new file mode 100644 index 000000000..652198b77 --- /dev/null +++ b/example/merge-with-conflicts.js @@ -0,0 +1,169 @@ +var nodegit = require('../'); +var path = require('path'); +var Promise = require('nodegit-promise'); +var promisify = require('promisify-node'); +var fse = promisify(require('fs-extra')); +fse.ensureDir = promisify(fse.ensureDir); + +var repoDir = '../../newRepo'; +var fileName = 'newFile.txt'; + +var baseFileContent = 'All Bobs are created equal. ish.\n'; +var ourFileContent = "Big Bobs are best, IMHO.\n"; +var theirFileContent = "Nobody expects the small Bobquisition!\n"; +var finalFileContent = "Big Bobs are beautiful, and the small are unexpected!\n"; + +var baseSignature = nodegit.Signature.create("Peaceful Bob", "justchill@bob.net", 123456789, 60); +var ourSignature = nodegit.Signature.create("Big Bob", "impressive@bob.net", 123456789, 60); +var theirSignature = nodegit.Signature.create("Small Bob", "underestimated@bob.net", 123456789, 60); + +var ourBranchName = "ours"; +var theirBranchName = "theirs"; + +var repository; +var baseCommit; +var baseCommitOid; +var ourCommit; +var theirCommit; +var ourBranch; +var theirBranch; + +// Create a new repository in a clean directory, and add our first file +fse.remove(path.resolve(__dirname, repoDir)) +.then(function() { + return fse.ensureDir(path.resolve(__dirname, repoDir)); +}) +.then(function() { + return nodegit.Repository.init(path.resolve(__dirname, repoDir), 0); +}) +.then(function(repo) { + repository = repo; + return fse.writeFile(path.join(repository.workdir(), fileName), baseFileContent); +}) + + +// Load up the repository index and make our initial commit to HEAD +.then(function() { + return repository.openIndex(); +}) +.then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write() + + return index.writeTree(); +}) +.then(function(oid) { + return repository.createCommit('HEAD', baseSignature, baseSignature, 'bobs are all ok', oid, []); +}) +.then(function(commitOid) { + baseCommitOid = commitOid; + return repository.getCommit(commitOid).then(function(commit) { + baseCommit = commit; + }); +}) + + +// create our branches +.then(function() { + return repository.createBranch(ourBranchName, baseCommitOid).then(function(branch) { + ourBranch = branch; + }); +}) +.then(function() { + return repository.createBranch(theirBranchName, baseCommitOid).then(function(branch) { + theirBranch = branch; + }); +}) + + +// Write and commit our version of the file +.then(function() { + return fse.writeFile(path.join(repository.workdir(), fileName), ourFileContent); +}) +.then(function() { + return repository.openIndex().then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write() + + return index.writeTree(); + }); +}) +.then(function(oid) { + return repository.createCommit(ourBranch.name(), ourSignature, ourSignature, 'lol big bobs :yesway:', oid, [baseCommit]); +}) +.then(function(commitOid) { + return repository.getCommit(commitOid).then(function(commit) { + ourCommit = commit; + }); +}) + + +// Write and commit their version of the file +.then(function() { + return fse.writeFile(path.join(repository.workdir(), fileName), theirFileContent); +}) +.then(function() { + return repository.openIndex().then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write() + + return index.writeTree(); + }); +}) +.then(function(oid) { + return repository.createCommit(theirBranch.name(), theirSignature, theirSignature, 'lol big bobs :poop:', oid, [baseCommit]); +}) +.then(function(commitOid) { + return repository.getCommit(commitOid).then(function(commit) { + theirCommit = commit; + }); +}) + + +// move the head to our branch, just to keep things tidy +.then(function() { + return nodegit.Reference.lookup(repository, 'HEAD').then(function(head) { + return head.symbolicSetTarget(ourBranch.name(), ourSignature, ""); + }) +}) + + +// Merge their branch into our branch +.then(function() { + return nodegit.Merge.commits(repository, ourCommit, theirCommit, null); +}) + +// Merging returns an index that isn't backed by the repository. +// You have to write it to the repository instead of just writing it. +.then(function(index) { + if (index.hasConflicts()) { + console.log('Conflict time!'); + + // if the merge had comflicts, solve them + // (in this case, we simply overwrite the file) + fse.writeFileSync(path.join(repository.workdir(), fileName), finalFileContent); + } +}) + +// we need to get a new index as the other one isnt backed to +// the repository in the usual fashion, and just behaves weirdly +.then(function() { + return repository.openIndex().then(function(index) { + + index.read(1); + index.addByPath(fileName); + index.write(); + + return index.writeTree(); + }); +}) +.then(function(oid) { + // create the new merge commit on our branch + return repository.createCommit(ourBranch.name(), baseSignature, baseSignature, 'Stop this bob sized fued', oid, [ourCommit, theirCommit]); +}) +.done(function(commitId) { + console.log('New Commit: ', commitId); +}); diff --git a/generate/descriptor.json b/generate/descriptor.json index e52c66a9a..dd767bf55 100644 --- a/generate/descriptor.json +++ b/generate/descriptor.json @@ -630,9 +630,6 @@ "isOptional": true } } - }, - "git_index_write_tree_to": { - "ignore": true } } }, @@ -668,15 +665,19 @@ "git_merge_analysis": { "ignore": true }, - "git_merge_base": { - "ignore": true - }, "git_merge_base_many": { "ignore": true }, "git_merge_base_octopus": { "ignore": true }, + "git_merge_commits": { + "args": { + "opts": { + "isOptional": true + } + } + }, "git_merge_file": { "ignore": true }, @@ -1027,6 +1028,16 @@ }, "git_reference_next_name": { "ignore": true + }, + "git_reference_symbolic_set_target": { + "args": { + "signature": { + "isOptional": true + }, + "log_message": { + "isOptional": true + } + } } } }, diff --git a/generate/libgit2-supplement.json b/generate/libgit2-supplement.json index 72c36758d..693b2d865 100644 --- a/generate/libgit2-supplement.json +++ b/generate/libgit2-supplement.json @@ -261,6 +261,16 @@ "git_odb_object_size", "git_odb_object_type" ] + ], + [ + "merge_head", + [ + "git_merge_head_free", + "git_merge_head_from_fetchhead", + "git_merge_head_from_id", + "git_merge_head_from_ref", + "git_merge_head_id" + ] ] ] }, @@ -274,6 +284,15 @@ "git_odb_object_size", "git_odb_object_type" ] + }, + "merge": { + "functions": [ + "git_merge_head_free", + "git_merge_head_from_fetchhead", + "git_merge_head_from_id", + "git_merge_head_from_ref", + "git_merge_head_id" + ] } } } diff --git a/lib/merge.js b/lib/merge.js new file mode 100644 index 000000000..cd96c88ce --- /dev/null +++ b/lib/merge.js @@ -0,0 +1,22 @@ +var NodeGit = require("../"); +var normalizeOptions = require("./util/normalize_options"); + +var Merge = NodeGit.Merge; +var mergeCommits = Merge.commits; + +/** + * Merge 2 commits together and create an new index that can + * be used to create a merge commit. + * + * @param repo Repository that contains the given commits + * @param ourCommit The commit that reflects the destination tree + * @oaram theirCommit The commit to merge into ourCommit + * @param options The merge tree options (null for default) + */ +Merge.commits = function(repo, ourCommit, theirCommit, options) { + options = normalizeOptions(options, NodeGit.MergeOptions); + + return mergeCommits.call(this, repo, ourCommit, theirCommit, options); +}; + +module.exports = Merge; diff --git a/lib/nodegit.js b/lib/nodegit.js index 856f229e0..43760522c 100644 --- a/lib/nodegit.js +++ b/lib/nodegit.js @@ -50,6 +50,7 @@ require("./checkout"); require("./commit"); require("./diff"); require("./index"); +require("./merge"); require("./object"); require("./odb"); require("./odb_object"); diff --git a/test/tests/merge.js b/test/tests/merge.js new file mode 100644 index 000000000..72b3d9c74 --- /dev/null +++ b/test/tests/merge.js @@ -0,0 +1,287 @@ +var assert = require("assert"); +var path = require("path"); +var promisify = require("promisify-node"); +var fse = promisify(require("fs-extra")); +fse.ensureDir = promisify(fse.ensureDir); + +describe("Merge", function() { + var nodegit = require("../../"); + + var repoDir = path.resolve("test/repos/merge"); + var ourBranchName = "ours"; + var theirBranchName = "theirs"; + + beforeEach(function() { + var test = this; + return fse.remove(path.resolve(__dirname, repoDir)) + .then(function() { + return fse.ensureDir(path.resolve(__dirname, repoDir)); + }) + .then(function() { + return nodegit.Repository.init(path.resolve(__dirname, repoDir), 0); + }) + .then(function(repo) { + test.repository = repo; + }); + }); + + it("can cleanly merge 2 files", function() { + var ourFileName = "ourNewFile.txt"; + var theirFileName = "theirNewFile.txt"; + + var ourFileContent = "I like Toll Roads. I have an EZ-Pass!"; + var theirFileContent = "I'm skeptical about Toll Roads"; + + var ourSignature = nodegit.Signature.create + ("Ron Paul", "RonPaul@TollRoadsRBest.info", 123456789, 60); + var theirSignature = nodegit.Signature.create + ("Greg Abbott", "Gregggg@IllTollYourFace.us", 123456789, 60); + + var repository = this.repository; + var ourCommit; + var theirCommit; + var ourBranch; + var theirBranch; + + return fse.writeFile(path.join(repository.workdir(), ourFileName), + ourFileContent) + // Load up the repository index and make our initial commit to HEAD + .then(function() { + return repository.openIndex() + .then(function(index) { + index.read(1); + index.addByPath(ourFileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "11ead82b1135b8e240fb5d61e703312fb9cc3d6a"); + + return repository.createCommit("HEAD", ourSignature, + ourSignature, "we made a commit", oid, []); + }) + .then(function(commitOid) { + assert.equal(commitOid.toString(), + "91a183f87842ebb7a9b08dad8bc2473985796844"); + + return repository.getCommit(commitOid).then(function(commit) { + ourCommit = commit; + }).then(function() { + return repository.createBranch(ourBranchName, commitOid) + .then(function(branch) { + ourBranch = branch; + return repository.createBranch(theirBranchName, commitOid); + }); + }); + }) + .then(function(branch) { + theirBranch = branch; + return fse.writeFile(path.join(repository.workdir(), theirFileName), + theirFileContent); + }) + .then(function() { + return repository.openIndex() + .then(function(index) { + index.read(1); + index.addByPath(theirFileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "76631cb5a290dafe2959152626bb90f2a6d8ec94"); + + return repository.createCommit(theirBranch.name(), theirSignature, + theirSignature, "they made a commit", oid, [ourCommit]); + }) + .then(function(commitOid) { + assert.equal(commitOid.toString(), + "0e9231d489b3f4303635fc4b0397830da095e7e7"); + + return repository.getCommit(commitOid).then(function(commit) { + theirCommit = commit; + }); + }) + .then(function() { + return nodegit.Merge.commits(repository, ourCommit, theirCommit); + }) + .then(function(index) { + assert(!index.hasConflicts()); + index.write(); + return index.writeTreeTo(repository); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "76631cb5a290dafe2959152626bb90f2a6d8ec94"); + + return repository.createCommit(ourBranch.name(), ourSignature, + ourSignature, "we merged their commit", oid, + [ourCommit, theirCommit]); + }) + .then(function(commitId) { + assert.equal(commitId.toString(), + "eedee554af34dd4001d8abc799cb55bb7e56a58b"); + }); + }); + + it("can merge 2 branchs with conflicts on a single file", function () { + var baseFileContent = "All Bobs are created equal. ish.\n"; + var ourFileContent = "Big Bobs are best, IMHO.\n"; + var theirFileContent = "Nobody expects the small Bobquisition!\n"; + var finalFileContent = + "Big Bobs are beautiful, and the small are unexpected!\n"; + + var baseSignature = nodegit.Signature.create + ("Peaceful Bob", "justchill@bob.net", 123456789, 60); + var ourSignature = nodegit.Signature.create + ("Big Bob", "impressive@bob.net", 123456789, 60); + var theirSignature = nodegit.Signature.create + ("Small Bob", "underestimated@bob.net", 123456789, 60); + + var repository = this.repository; + var baseCommit; + var baseCommitOid; + var ourCommit; + var theirCommit; + var ourBranch; + var theirBranch; + var fileName = "newFile.txt"; + + return fse.writeFile(path.join(repository.workdir(), fileName), + baseFileContent) + .then(function() { + return repository.openIndex() + .then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "ea2f6521fb8097a881f43796946ac1603e1f4d75"); + + return repository.createCommit("HEAD", baseSignature, + baseSignature, "bobs are all ok", oid, []); + }) + .then(function(commitOid) { + assert.equal(commitOid.toString(), + "a9b202f7612273fb3a68f718304298704eaeb735"); + baseCommitOid = commitOid; + + return repository.getCommit(commitOid).then(function(commit) { + baseCommit = commit; + }); + }) + .then(function() { + return repository.createBranch(ourBranchName, baseCommitOid) + .then(function(branch) { + ourBranch = branch; + }); + }) + .then(function() { + return repository.createBranch(theirBranchName, baseCommitOid) + .then(function(branch) { + theirBranch = branch; + }); + }) + .then(function() { + return fse.writeFile(path.join(repository.workdir(), fileName), + ourFileContent); + }) + .then(function() { + return repository.openIndex().then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "c39b1e38b09085856cec7e7ff33e90f5a537d8a5"); + + return repository.createCommit(ourBranch.name(), ourSignature, + ourSignature, "lol big bobs :yesway:", oid, [baseCommit]); + }) + .then(function(commitOid) { + assert.equal(commitOid.toString(), + "935a89c09ad757a9dde2c0257f6f1e379f71816f"); + + return repository.getCommit(commitOid).then(function(commit) { + ourCommit = commit; + }); + }) + .then(function() { + return fse.writeFile(path.join(repository.workdir(), fileName), + theirFileContent); + }) + .then(function() { + return repository.openIndex().then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "d1a894a9a4a8c820eb66c82cdd7e6b76c8f713cb"); + + return repository.createCommit(theirBranch.name(), theirSignature, + theirSignature, "lol big bobs :poop:", oid, [baseCommit]); + }) + .then(function(commitOid) { + assert.equal(commitOid.toString(), + "bebb9ec2e0684c7cb7c1e1601c7d5a8f52b8b123"); + + return repository.getCommit(commitOid).then(function(commit) { + theirCommit = commit; + }); + }) + .then(function() { + return nodegit.Reference.lookup(repository, "HEAD") + .then(function(head) { + return head.symbolicSetTarget(ourBranch.name(), ourSignature, ""); + }); + }) + .then(function() { + return nodegit.Merge.commits(repository, ourCommit, theirCommit, null); + }) + .then(function(index) { + assert(index.hasConflicts()); + fse.writeFileSync(path.join(repository.workdir(), fileName), + finalFileContent); + }) + .then(function() { + return repository.openIndex().then(function(index) { + index.read(1); + index.addByPath(fileName); + index.write(); + + return index.writeTree(); + }); + }) + .then(function(oid) { + assert.equal(oid.toString(), + "b1cd49a27cd33b99ab6dad2fb82b3174812a8b47"); + + return repository.createCommit(ourBranch.name(), baseSignature, + baseSignature, "Stop this bob sized fued", oid, + [ourCommit, theirCommit]); + }) + .then(function(commitId) { + assert.equal(commitId.toString(), + "49014ccabf5125f9b69316acde36f891dfdb8b5c"); + }); + }); +});