{"thread":{"id":"32951","subject":"Merge with staged and unstaged changes","startedAt":"2013-02-20T19:17:52Z","lastAt":"2013-02-20T22:17:25Z","messageCount":4,"participants":["Edward Thomson","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"209916","messageId":"A54CE3E330039942B33B670D971F85740396B267@TK5EX14MBXC254.redmond.corp.microsoft.com","threadId":"32951","inReplyTo":null,"subject":"Merge with staged and unstaged changes","fromName":"Edward Thomson","fromEmail":"ethomson@microsoft.com","sentAt":"2013-02-20T19:17:52Z","receivedAt":"2013-02-20T19:17:52Z","isPatch":false,"sender":{"key":"ethomson@microsoft.com","avatar":"https://gravatar.com/avatar/c80dc8bacdf4afcf27334f21b70093a25ac5559d576357ea13592bf438113950?d=mp&s=160"},"body":"Hi-\n\nI've been investigating the cases where merge is allowed to proceed when\nthere are staged changes in the index or unstaged files in the working\ndirectory.  There are cases where I find the behavior surprising and I\nhope I can get clarification.  There are also two cases that I will report\nas bugs, where it appears that the unstaged file contents are deleted.\n\nFor these cases below, please consider the contents of a single path.\nIn the tables below, we will show the contents of a file across each input\nand output of the merge - consider that we're merging a single file in\nsome branch \"theirs\" into the current branch \"ours\" and that these two\nbranches have a common ancestor \"anc\".  The state of that file in our\nindex and workdir are represented by \"idx\" and \"wd\", respectively.\nUnless otherwise noted, these cases are true for both git-merge-resolve\nand git-merge-recursive.\n\n\nFor completeness and illustration purposes, I'll included the cases where\nthere are no changes staged or unstaged.  These succeed, as expected:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n1  A   A    B      A   A   take B        B   B\n2  A   B    A      B   B   take A        A   A\n\nMerge is also expected to proceed if the contents of our branch are the\nmerge result, and there are unstaged changes for that file in the workdir.\nIn this case, the file remains unstaged:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n3  A   B    A      B   C   take B        B   C\n\n\nWhat was surprising to me was that my merge can proceed if I stage a change\nthat is identical to the merge result.  That is, if my merge result would\nbe to take the contents from \"theirs\", then my merge can proceed if I've\nalready staged the same contents:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n4  A   A    B      B   B   take B        B   B\n5  A   A    B      B   C   take B        B   C\n\nThis seems unexpected - is there a use-case that this enables or is\nthis accidental?\n\n\nAnother surprising result was that if I have deleted a file (and staged\nthe deletion or not) then the merge will proceed and the file in question\nwill be recreated.  Consider \"X\" to be a missing file:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n6  A   A    B      A   X   take B        B   B\n7  A   A    B      X   X   take B        B   B\n\nI wouldn't have expected a file I deleted to be recreated with the other\nbranch's contents.  Is this behavior also intentional?\n\n\nFinally, there are cases when you have staged a deletion of the file and\nyou have unstaged changes in your workdir where the merge will silently\ndelete the unstaged data.  If there is a conflict, the xdiff output will\noverwrite the unstaged file:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n8  A   B    C      X   D   conflict      X   diff3_file\n\nAnd similarly, while git-merge-recursive (only) will also remove my\nuntracked file when there are no changes in our branch but the file was\ndeleted in their branch:\n\n   input                                 result\n   anc ours theirs idx wd  merge result  idx wd\n9  A   A    X      X   B   delete file   X   X\n\n\nI trust the last two cases, where data is lost, are bugs to report, but\ncould I get clarification on the other situations?\n\nThanks-\n\n-ed\n"},{"id":"209938","messageId":"7vobfeybwt.fsf@alter.siamese.dyndns.org","threadId":"32951","inReplyTo":"A54CE3E330039942B33B670D971F85740396B267@TK5EX14MBXC254.redmond.corp.microsoft.com","subject":"Re: Merge with staged and unstaged changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-20T20:21:06Z","receivedAt":"2013-02-20T20:21:06Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Edward Thomson <ethomson@microsoft.com> writes:\n\n> What was surprising to me was that my merge can proceed if I stage a change\n> that is identical to the merge result.  That is, if my merge result would\n> be to take the contents from \"theirs\", then my merge can proceed if I've\n> already staged the same contents:\n>\n>    input                                 result\n>    anc ours theirs idx wd  merge result  idx wd\n> 4  A   A    B      B   B   take B        B   B\n> 5  A   A    B      B   C   take B        B   C\n>\n> This seems unexpected - is there a use-case that this enables or is\n> this accidental?\n\nBoth are very much on purpose. The integrator may have seen the\npatch on the list, ran \"git apply [--index]\" on it to contemplate on\nit, and before commiting the result, saw a pull request for a branch\nthat contains the change.  The above two allow the pull from such a\nstate to succeed without losing any information.\n\nI think we have a similar table in Documentation/technical area that\nexplains these things, by the way.\n\n> Another surprising result was that if I have deleted a file (and staged\n> the deletion or not) then the merge will proceed and the file in question\n> will be recreated.  Consider \"X\" to be a missing file:\n>\n>    input                                 result\n>    anc ours theirs idx wd  merge result  idx wd\n> 6  A   A    B      A   X   take B        B   B\n> 7  A   A    B      X   X   take B        B   B\n\nI am not sure about #7, but #6 is done very much on purpose.  The\nlower level merge machinery deliberately equates \"in index but not\nchecked out to the working tree\" state and \"in index and not\nmodified in the working tree\" state; this is to support a merge done\nin a temporary working area that starts out empty.\n\nThis was designed really long time ago (read: during the first two\nweeks of Git development, back when there were no \"stash\" nor\n\"remote tracking branches\"), with a vision to make this scenario\nwork nicely:\n\n    You have checked out 'master' branch and you are working on it.\n    You see a more urgent pull request on 'maint' branch.  But your\n    working tree for 'master' is no shape to commit, yet.\n\nWithout disturbing the working area you are using to advance the\n'master' branch, you could do the merge \"only in the index\" by doing:\n\n * Fetch the requested commit:\n \n\t$ git fetch $over_there refs/heads/master\n\n * Populate a temporary index with the contents of your 'maint':\n\n\t$ GIT_INDEX_FILE=,,temp-index GIT_DIR=$(pwd)/.git\n        $ export GIT_INDEX_FILE GIT_DIR\n        $ git read-tree refs/heads/maint\n\n * Create an empty temporary working area and go there:\n\n\t$ mkdir ,,temp-merge\n\t$ cd ,,temp-merge\n\n * Run the three-way merge between your 'maint' and the requested\n   commit:\n\n\t$ MB=$(git merge-base FETCH_HEAD maint)\n\t$ git read-tree -m -u $MB maint FETCH_HEAD\n\nNotice that we start without _any_ file in that temporary working\narea (,,temp-merge directory).  In the last step, because the merge\nmachinery (read-tree -m -u) treats missing files as \"they are\nunmodified by us; we didn't even bother checking them out of the\nindex\", we will see _only_ the files that are different from our\n'maint' and files that needs our help to get conflicts resolved\nin the temporary working area after the command finishes.  You\ninspect them, resolve conflicts and run \"git update-index\" on them\n(remember, there weren't \"git add\" or \"git commit -a\"), write the\nresulting index as a tree with \"git write-tree\" and record the tree\nwith \"git commit-tree -p maint -p FETCH_HEAD\" (actually, back then\n\"commit-tree -p\" insisted on getting raw tree object names, so you\nhad to do the equivalent of \"git rev-parse maint^{tree}\" there) and\nthen update your 'maint' branch with the resulting commit.\n\nIf there is no conflict to be resolved, then you essentially can run\na script that does all of the above (and cleans up the temporary\ndirectory ,,temp-merge and the temporary index) to implement \"Can\nperform a merge into a branch that is not currently checked out,\nwithout disturbing my current work\" feature.  And this \"missing\nfiles are unmodified---I didn't even bother to check them out of the\nindex\" was done as an integral part of it.\n\nBack when we (IIRC, mostly between Linus and I) were discussing this\ndesign, it did come up that in the normal case of \"I am merging in\nmy working tree\", this behaviour would lose information, but \"I'm\nplanning to remove this\" is only a single bit and lossage of that\nwas judged to be trivially small and easily recoverable compared to\nthe benefit of being able to do a merge in an empty working tree,\nand that is where this behaviour comes from.\n\nWe could certainly revisit this design and make the behaviour\noptional.  When somebody wants to do the \"Can perform a merge into a\nbranch that is not currently checked out, without disturbing my\ncurrent work\" feature, its implementation needs to be able to turn\nit back on, but for doing everything else we do not have to treat a\nmissing file as unmodified.\n"},{"id":"209939","messageId":"A54CE3E330039942B33B670D971F85740396C4F8@TK5EX14MBXC254.redmond.corp.microsoft.com","threadId":"32951","inReplyTo":"7vobfeybwt.fsf@alter.siamese.dyndns.org","subject":"Re: Merge with staged and unstaged changes","fromName":"Edward Thomson","fromEmail":"ethomson@microsoft.com","sentAt":"2013-02-20T21:46:08Z","receivedAt":"2013-02-20T21:46:08Z","isPatch":false,"sender":{"key":"ethomson@microsoft.com","avatar":"https://gravatar.com/avatar/c80dc8bacdf4afcf27334f21b70093a25ac5559d576357ea13592bf438113950?d=mp&s=160"},"body":"On 2/20/13 2:21 PM, \"Junio C Hamano\" <gitster@pobox.com> wrote:\n\n>Both are very much on purpose. The integrator may have seen the\n>patch on the list, ran \"git apply [--index]\" on it to contemplate on\n>it, and before commiting the result, saw a pull request for a branch\n>that contains the change.  The above two allow the pull from such a\n>state to succeed without losing any information.\n\n>I think we have a similar table in Documentation/technical area that\n>explains these things, by the way.\n\nI believe you are referring to trivial-merge.txt which has been\nexceptionally helpful in understanding \"what\" unpack trees does.\nI appreciate this detailed explanation in providing the \"why\".\n\nI also appreciate your explanation of the affect of the workdir,\nand that makes sense.  I would have expected that the default was\nto presume the workdir files were existent, rather than the\nother way around, but we can agree that is an implementation detail.\n\nMy biggest concern, of course, was having the unstaged files in my\nworkdir overwritten or deleted.\n\nThanks again-\n\n-ed\n"},{"id":"209946","messageId":"7vk3q2y6iy.fsf@alter.siamese.dyndns.org","threadId":"32951","inReplyTo":"A54CE3E330039942B33B670D971F85740396C4F8@TK5EX14MBXC254.redmond.corp.microsoft.com","subject":"Re: Merge with staged and unstaged changes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2013-02-20T22:17:25Z","receivedAt":"2013-02-20T22:17:25Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Edward Thomson <ethomson@microsoft.com> writes:\n\n> I also appreciate your explanation of the affect of the workdir,\n> and that makes sense.  I would have expected that the default was\n> to presume the workdir files were existent, rather than the\n> other way around, but we can agree that is an implementation detail.\n>\n> My biggest concern, of course, was having the unstaged files in my\n> workdir overwritten or deleted.\n\nOh, no question about that part.  You concluded your original\nmessage with:\n\n>> I trust the last two cases, where data is lost, are bugs to\n>> report, but could I get clarification on the other situations?\n\nand I was responding to the part after the \"but could I get...\".\n\nI am fairly familiar with the \"read-tree -m -u O A B\" three-way\nmerge codepath (after all I designed that with Linus in the very\nearly days of Git), but I am not as familar with the merge-recursive\nbackend as merge-resolve, and I was hoping to see the \"bug\" part\ntriaged by other people.\n"}]}