diff --git a/spec/remote.spec.js b/spec/remote.spec.js index 752f19dd..9b5a0b1a 100644 --- a/spec/remote.spec.js +++ b/spec/remote.spec.js @@ -254,5 +254,19 @@ describe('Git Remotes', function() { ); }); + it('fetches with no args, explicit dest args, and with just one arg', function() { + expectTreeAsync( + 'git clone; git fakeTeamwork; git fetch origin master:o/master;git fakeTeamwork;git fetch;git fakeTeamwork;git fetch origin master', + '{"branches":{"master":{"target":"C1","id":"master","remoteTrackingBranchID":"o/master"},"o/master":{"target":"C4","id":"o/master","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"},"C4":{"parents":["C3"],"id":"C4"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"target":"C4","id":"master","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"},"C4":{"parents":["C3"],"id":"C4"}},"HEAD":{"target":"master","id":"HEAD"}}}' + ); + }); + + it('doesnt fetch if out of sync, but will update explicit dest if specified', function() { + expectTreeAsync( + 'git clone; git fakeTeamwork; git fetch origin master:master;gc;git fakeTeamwork;git fetch origin master:master', + '{"branches":{"master":{"target":"C3","id":"master","remoteTrackingBranchID":"o/master"},"o/master":{"target":"C1","id":"o/master","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C3":{"parents":["C2"],"id":"C3"}},"HEAD":{"target":"master","id":"HEAD"},"originTree":{"branches":{"master":{"target":"C4","id":"master","remoteTrackingBranchID":null}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"},"C4":{"parents":["C2"],"id":"C4"}},"HEAD":{"target":"master","id":"HEAD"}}}' + ); + }); + }); diff --git a/src/js/git/commands.js b/src/js/git/commands.js index 4e9b7bba..e72bd549 100644 --- a/src/js/git/commands.js +++ b/src/js/git/commands.js @@ -23,6 +23,13 @@ var validateBranchName = function(engine, name) { return engine.validateBranchName(name); }; +var validateBranchNameIfNeeded = function(engine, name) { + if (engine.refs[name]) { + return name; + } + return validateBranchName(engine, name); +}; + var assertIsBranch = function(engine, ref) { assertIsRef(engine, ref); var obj = engine.refs[ref]; @@ -297,7 +304,10 @@ var commandConfig = { if (firstArg && isColonRefspec(firstArg)) { var refspecParts = firstArg.split(':'); source = refspecParts[0]; - destination = validateBranchName(engine, refspecParts[1]); + destination = validateBranchNameIfNeeded( + engine, + crappyUnescape(refspecParts[1]) + ); } else if (firstArg) { // here is the deal -- its JUST like git push. the first arg // is used as both the destination and the source, so we need diff --git a/src/js/git/index.js b/src/js/git/index.js index 770a1b37..aec553f4 100644 --- a/src/js/git/index.js +++ b/src/js/git/index.js @@ -26,6 +26,12 @@ function catchShortCircuit(err) { } } +function invariant(truthy, reason) { + if (!truthy) { + throw new Error(reason); + } +} + function GitEngine(options) { this.rootCommit = null; this.refs = {}; @@ -371,6 +377,20 @@ GitEngine.prototype.makeOrigin = function(treeString) { }, this); }; +GitEngine.prototype.makeRemoteBranchIfNeeded = function(branchName) { + if (this.refs[ORIGIN_PREFIX + branchName]) { + return; + } + return this.makeRemoteBranchForRemote(branchName); +}; + +GitEngine.prototype.makeBranchIfNeeded = function(branchName) { + if (this.refs[branchName]) { + return; + } + return this.validateAndMakeBranch(branchName, this.getCommitFromRef('HEAD')); +}; + GitEngine.prototype.makeRemoteBranchForRemote = function(branchName) { var target = this.origin.refs[branchName].get('target'); var originTarget = this.findCommonAncestorWithRemote( @@ -1035,53 +1055,60 @@ GitEngine.prototype.pushDeleteRemoteBranch = function( GitEngine.prototype.fetch = function(options) { options = options || {}; - // ok this is just like git push -- if the destination does not exist, - // we need to make it - if (options.destination && !this.refs[options.destination]) { - // its just like creating a branch, we will merge (if pulling) later + // first check for super stupid case where we are just making + // a branch with fetch... + if (options.destination && options.source === '') { this.validateAndMakeBranch( options.destination, this.getCommitFromRef('HEAD') ); - } + return; + } else if (options.destination && options.source) { + this.makeRemoteBranchIfNeeded(options.source); + this.makeBranchIfNeeded(options.destination); - // now we need to know which remotes to fetch. lets do this by either checking our source - // option or just getting all of them - var branchesToFetch; - if (options.source) { - // gah -- first we have to check that we even have a remote branch - // for this source (we know its on the remote based on validation) - if (!this.refs[ORIGIN_PREFIX + options.source]) { - this.makeRemoteBranchForRemote(options.source); - } - // now just specify branches to fetch based on this source - branchesToFetch = [this.refs[ORIGIN_PREFIX + options.source]]; - } else { - branchesToFetch = this.branchCollection.filter(function(branch) { - return branch.getIsRemote(); - }); + return this.fetchCore([{ + destination: options.destination, + source: options.source + }], + options + ); } + // get all remote branches and specify the dest / source pairs + var allBranchesOnRemote = this.origin.branchCollection.toArray(); + var sourceDestPairs = _.map(allBranchesOnRemote, function(branch) { + var branchName = branch.get('id'); + this.makeRemoteBranchIfNeeded(branchName); + return { + destination: branch.getPrefixedID(), + source: branchName + }; + }, this); + return this.fetchCore(sourceDestPairs, options); +}; + +GitEngine.prototype.fetchCore = function(sourceDestPairs, options) { // first check if our local remote branch is upstream of the origin branch set. // this check essentially pretends the local remote branch is in origin and // could be fast forwarded (basic sanity check) - _.each(branchesToFetch, function(localRemoteBranch) { + _.each(sourceDestPairs, function(pair) { this.checkUpstreamOfSource( this, this.origin, - localRemoteBranch, - this.origin.refs[localRemoteBranch.getBaseID()] + pair.destination, + pair.source ); }, this); // then we get the difference in commits between these two graphs var commitsToMake = []; - _.each(branchesToFetch, function(localRemoteBranch) { + _.each(sourceDestPairs, function(pair) { commitsToMake = commitsToMake.concat(this.getTargetGraphDifference( this, this.origin, - localRemoteBranch, - this.origin.refs[localRemoteBranch.getBaseID()], + pair.destination, + pair.source, _.extend( {}, options, @@ -1159,14 +1186,15 @@ GitEngine.prototype.fetch = function(options) { }, this); chain = chain.then(_.bind(function() { - // update all the remote branches - _.each(branchesToFetch, function(localRemoteBranch) { - var remoteBranch = this.origin.refs[localRemoteBranch.getBaseID()]; - var remoteLocationID = remoteBranch.get('target').get('id'); + // update all the destinations + _.each(sourceDestPairs, function(pair) { + var ours = this.refs[pair.destination]; + var theirs = this.origin.refs[pair.source]; + var theirCommitID = theirs.get('target').get('id'); // by definition we just made the commit with this id, // so we can grab it now - var localCommit = this.refs[remoteLocationID]; - this.setTargetLocation(localRemoteBranch, localCommit); + var localCommit = this.refs[theirCommitID]; + this.setTargetLocation(ours, localCommit); }, this); // unhighlight origin by refreshing diff --git a/src/js/intl/strings.js b/src/js/intl/strings.js index 6eb98896..7a2595bb 100644 --- a/src/js/intl/strings.js +++ b/src/js/intl/strings.js @@ -92,7 +92,7 @@ exports.strings = { }, 'git-error-origin-fetch-no-ff': { '__desc__': 'One of the error messages for git', - 'en_US': 'Your origin branch is out of sync with the remote branch and fetch cannot be performed. try using --force', + 'en_US': 'Your origin branch is out of sync with the remote branch and fetch cannot be performed', 'fr_FR': 'Votre branche origin n\'est plus synchronisée avec la branche distante et fetch ne peut pas être appliqué. Essayez avec l\'option --force' }, 'git-error-origin-push-no-ff': { diff --git a/todo.txt b/todo.txt index e88b89be..98dd9475 100644 --- a/todo.txt +++ b/todo.txt @@ -22,12 +22,6 @@ aka fetch + merge, just like expected. ill probably still update o/master just f just pass options into fetch, then merge the source. -2) oh boy heres anothere data point. git fetch banana:banana will actually fast-forward banana to what remote has, but not update o/banana. weirdly it doesnt let you do this if you are checked out on banana - -3) furthermore, if banana has commits and its not a FF, it will reject the command. wth?? - -3.5) but of course git fetch banana:oBanana will make a new branch - 4) and then "git pull origin banana:origin/banana" works because it goes into the remote branch and then merges the fetch HEAD with current location [ ] FIX the level you came up with @@ -50,6 +44,8 @@ Ideas for cleaning Done things: (I only started this on Dec 17th 2012 to get a better sense of what was done) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +[x] oh boy heres anothere data point. git fetch banana:banana will actually fast-forward banana to what remote has, but not update o/banana. weirdly it doesnt let you do this if you are checked out on banana +[x] furthermore, if banana has commits and its not a FF, it will reject the command. wth?? [x] test is failing because we create banana when we should only really be creating o/banana [x] work on TABBED levels layout [x] EASY -- make colors the same between remote branches and their remote counterparts