{"thread":{"id":"43530","subject":"Re: Working copy as a pseudo-head","startedAt":"2006-11-27T08:19:31Z","lastAt":"2006-11-27T09:20:39Z","messageCount":2,"participants":["Junio C Hamano","Steven Grimm"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"298388","messageId":"456A9F93.1060801@midwinter.com","threadId":"43530","inReplyTo":null,"subject":"Working copy as a pseudo-head","fromName":"Steven Grimm","fromEmail":"koreth@midwinter.com","sentAt":"2006-11-27T08:19:31Z","receivedAt":"2006-11-27T08:19:31Z","isPatch":false,"sender":{"key":"koreth@midwinter.com","avatar":"https://gravatar.com/avatar/71b4d2e8b62f168bdc9e9205341159e3567003b4f9e2127c617c5fa0a1f5bad2?d=mp&s=160"},"body":"I was trying to explain to someone today why \"git pull\" fails if you \nhave working-copy edits of one of the files that changed in the other \nbranch, and it occurred to me that there may be a way to model the world \nso it doesn't fail. I am still relatively new to git so I hope this is \nnot a totally idiotic idea.\n\nRight now there is a hard distinction between what's in the working copy \nand the revision history of the tree. Merge is an operation on the \nrevision history, and the working copy doesn't enter into it aside from \nthe sanity check in git-merge to prevent you from inadvertently \noverwriting local edits.\n\nWhat if instead, we had a notion of a temporary commit, call it WORK, \nthat is ahead of what we currently think of as HEAD? That is, where now \nwe have\n\nA----B----C----D(HEAD)\n\nwe instead allowed\n\nA----B----C----D(HEAD)----E(WORK)\n\nWhen you do some operation that currently would complain about \nworking-copy edits, it instead commits a WORK revision but does not \nadjust HEAD. Then it performs the operation against HEAD (advancing HEAD \nas needed) and rebases WORK under the newly merged HEAD, updating the \nworking copy as a normal rebase would. Then, with one exception I'll get \nto below, it removes the WORK revision from the version history but \nleaves the contents of the working copy alone.\n\nFor example, say you have a pristine working copy of a branch with some \nhistory:\n\nA----B----C(HEAD)\n\nand you make some edits to the working copy. Now you want to pull down \nthe latest changes from upstream, revision \"X\". So the system silently \ncommits your work in progress to a temporary revision, which (while the \nmerge is running) changes your history to:\n\nA----B----C(HEAD)----D(WORK)\n\nThen it merges the new changes with HEAD to form a new \"Y\" revision:\n\n            /--D(WORK)\n           /\nA----B----C----Y(HEAD)\n \\            /\n  \\-------X--/\n\nThen it rebases WORK:\n\nA----B----C----Y(HEAD)----D(WORK)\n \\            /\n  \\-------X--/\n\nAnd finally it removes WORK from the version history.\n\nA----B----C----Y(HEAD) + working-copy edits\n \\            /\n  \\-------X--/\n\nI believe something like the above is currently how Cogito does its \n\"update from the origin without losing working copy edits\" thing, though \nit has to stash the working-copy edits in a temporary branch. Having the \nunderlying functionality supported in the core git tools would be far \npreferable to having to hack around the lack of it in a frontend.\n\nThe one exception to the above flow is when you want to switch branches. \nHere, we create a WORK temporary commit on the current branch and move \nover to the new branch as normal, blowing away the WORK commit on the \n*new* branch, if any, and leaving its edits in the working copy. That \nallows you to freely switch back and forth between branches without \nworrying about whether you need to save your changes in progress, and \nwithout cluttering your repository with intermediate copies of files \nyou're editing.\n\nThis is a behavior change! But I think the proposed behavior makes more \nsense. It treats the working copy as an extension of whatever branch \nyou're on, rather than a separate entity that can flit back and forth \nbetween branches if there aren't conflicts or prevent you from changing \nbranches at all if there are. It has always seemed confusing and \ninconsistent to me that, in some sense, doing a commit makes my changes \nLESS sticky -- that is,\n\necho foo >> testfile\ngit checkout other-branch\ntail -1 testfile\n\nsays \"foo\", but\n\necho foo >> testfile\ngit commit -a\ngit checkout other-branch\ntail -1 testfile\n\ndoesn't. I know (or I think I do) why it's doing that, but from a user \nexperience point of view it's just weird, because it makes it look like \n\"commit\" reduces the importance of my change. And it also makes it a lot \nless convenient to pop over to another branch for a moment to check out \nthe old version of some code I'm in the middle of rewriting.\n\nAlso, I realize I can use StGIT to get something like the above. But \nagain, I think it'd be cleaner to support this in the underlying tool; \n\"pull down some updates while I'm still working\" is a very common usage \nmodel that's handled fine by other SCMs (Subversion, etc.) and right now \nwith Git it requires a bunch of awkward extra commands. And preserving \nworking-copy edits when switching branches seems like a huge convenience \nwin to me; I have a hard time imagining how the current behavior is a \ntime-saver for anyone (though I'm sure I'll hear all about it now!)\n\nComments? Is this just nuts?\n\n"},{"id":"295305","messageId":"7vlklxi6u0.fsf@assigned-by-dhcp.cox.net","threadId":"43530","inReplyTo":"456A9F93.1060801@midwinter.com","subject":"Re: Working copy as a pseudo-head","fromName":"Junio C Hamano","fromEmail":"junkio@cox.net","sentAt":"2006-11-27T09:20:39Z","receivedAt":"2006-11-27T09:20:39Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Steven Grimm <koreth@midwinter.com> writes:\n\n> Comments? Is this just nuts?\n\nIt is not \"nuts\", but we do not do it currently because it is a\nbit too combersome to make it recoverable when things go wrong\nwhile keeping the cost of making the necessary back-up for\nrecovery low.  When switching branches with local changes,\n\"git-checkout -m\" needs to deal with a similar issue, but that\ncase only deal with two trees and a working tree.  To do this\nfor three-way merge, the conflicts you need to deal with become\nmore complex.\n\nWhat needs to be done is very simple and straightforward.  You\nfirst stash away the working tree state in a \"virtual\" tree,\nthen perform the usual 3-way merge using the common ancestor,\nyour HEAD and the other head, and come up with the merge result\nwithout any of your changes.  Then you run another three-way\nmerge between the merge result and your previous working tree\nstate using the HEAD before the merge as the common ancestor.\n\n       o---o---X (other head)\n      /\n  ---o---o---H (your head)\n              \\  \n               W (your working tree)\n\n==>\n\n       o---o---X (other head)\n      /         \\\n  ---o---o---H---M (merge between the base trees)\n              \\  \n               W (your working tree)\n\n==>\n\n       o---o---X (other head)\n      /         \\\n  ---o---o---H---M (merge between the base trees)\n              \\   \\   \n               W---W'(your working tree, updated for the merge result)\n\nThe design goal here is that you do not want to get any of your\nlocal changes (i.e. \"diff H W\") to be included when you record\nthe result of the base merge 'M', and you would want the diff\nbetween H and W to be forward-ported to the diff between M and\nW'.\n\nThe first goal is already quite cumbersome when the merge\nbetween X and H involve conflicing merges.  We use the working\ntree to perform the file level merge (i.e. you edit them with\nyour favorite editor, and tell git when you are done), so while\nthat is happening we temporarily need to remove what was between\nH and W.  If the merge is too complex and the user decides to\nrevert the whole thing, we would need to rewind the index and\nworking tree to the state at W (that means we would at least\nneed to stash the diff between H and index, and index and\nworking tree before starting the merge).\n\nAfter the base merge is done, we would need to update the HEAD\nto point at M.  Then we would perform the second three-way merge\nbetween M and W using the original HEAD as their common\nancestor to come up with W'.  When this conflicts, there is no\neasy way for the user to recover, other than going back to the\noriginal state (that is, HEAD points at H and working tree is\nW), even after having spent effort to merge between X and H to\nproduce M.\n\nIn common usage (call that \"best current practice\" if you may),\nyou do not have local changes in the working tree that would\nconflict with the merge, so the current behaviour feels\ncumbersome only when the safety valve kicks in.  After seeing it\nfail due to the safety valve, you can do the same as what the\nabove pictures depict using existing tools.\n\n * preserve the local changes (i.e. come up with the virtual\n   \"W\"):\n\n\tgit commit -a -m WIP\n\tgit tag -f WIP\n        git reset --hard HEAD^\n\n * perform the merge in the now-clean tree:\n\n\tgit pull ;# again from the same place\n\tedit edit\n        test test\n        git commit -a ;# resolve conflicts, test and commit\n\n * recover what you have preserved, and discard the virtual W:\n\n\tgit pull --no-commit . WIP ;# merge in\n\tgit reset ;# revert index to HEAD aka base merge result.\n\tgit tag -d WIP\n"}]}