Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Stash apply #3018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
May 12, 2015
Merged

Stash apply #3018

merged 17 commits into from
May 12, 2015

Conversation

ethomson
Copy link
Member

A modification on #2705. I made some notes in that PR, though actually acting on those notes was quite a bit more challenging than I had hoped. This introduces two lower level ideas:

  • git_merge__iterators, to allow one to merge a tree and an index and get an index out as a result. (Interestingly git_merge had this broken out as a helper function at one point, and it was brought back in. So now it's a helper function again.) This allows us to merge the results of another merge, without having to write it out to a tree.
  • git_index_read_index, which behaves like git_index_read_tree except that it reads an index into the index. This maintains the stat cache of the existing items.

Sorry about the long delay in finishing this up. Stash application is completely and totally ridiculous (put these files on disk and these in the index unless this happens or this), as I'm sure @swisspol can attest to. I think that with the two basic functions mentioned above that the stash_apply can be a bit simpler and not necessarily have to write objects for the intermediate steps.

@swisspol
Copy link
Contributor

Thanks @ethomson, awesome work 👍

@swisspol
Copy link
Contributor

This allows us to merge the results of another merge, without having to write it out to a tree.

BTW is this for performance reasons or API impedance reasons?

@swisspol
Copy link
Contributor

@ethomson Any idea when this will land?

@ethomson
Copy link
Member Author

ethomson commented Apr 1, 2015

Thank you @swisspol for all your awesome work - I literally cannot express how painful it was to understand how stash application actually works in Git. Crazy. So thanks for digging into this.

And again, sorry to bikeshed all over this (and to take so long to do so) - this was mostly for perf (I work in a very large tree, a write/read of that is expensive.)

@Therzok
Copy link
Member

Therzok commented Apr 5, 2015

If any untracked or ignored file saved in the stash already exist in the
workdir, the function will return GIT_EEXISTS and both the workdir and index
will be left untouched.

This doesn't seem to return that, testing it under libgit2sharp as we speak.

@swisspol
Copy link
Contributor

swisspol commented Apr 5, 2015

Do you mean it returns a different error code or it succeeds?

@Therzok
Copy link
Member

Therzok commented Apr 6, 2015

It just returns GIT_EMERGECONFLICT. The tests that fail what the documentation states:

Test 1 - It flushes the contents to the working directory regardless.

Test 2 - Fails because it's not returning GIT_EEXISTS, but GIT_EMERGECONFLICT.

@swisspol
Copy link
Contributor

swisspol commented Apr 6, 2015

Any chance you could roll back libgit2 to c31a1ed and check if that behavior was in my original PR or a fallback of @ethomson changes?

@Therzok
Copy link
Member

Therzok commented Apr 6, 2015

I get different errors by following the docs with c31a1ed:

  • With REINSTATE_INDEX not set, I get the index reinstated on - Stashfixture.cs - 194, 224
touch file
put text
add
save
pop
  • With REINSTATE_INDEX set, I was expecting to get conflicts on - Stashfixture.cs - 253
touch file1, file2
put text in both
add file 1
stash
touch file1, file2
put other text in both
add file1
stash pop

But I got the stash applied successfully?

Probably I did something wrong, someone should look at the tests to see if I approached them correctly.

@swisspol
Copy link
Contributor

swisspol commented Apr 6, 2015

With REINSTATE_INDEX not set, I get the index reinstated on

This is the expected git stash behavior (don't ask, git stash is a mess):

polbookpro:test pol$ git init
Initialized empty Git repository in /private/tmp/test/.git/
polbookpro:test pol$ git commit --allow-empty -m "Initial commit"
[master (root-commit) 1ec5ac0] Initial commit

polbookpro:test pol$ date > file
polbookpro:test pol$ git add file
polbookpro:test pol$ git stash
Saved working directory and index state WIP on master: 1ec5ac0 Initial commit
HEAD is now at 1ec5ac0 Initial commit

polbookpro:test pol$ git stash pop
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   file

Dropped refs/stash@{0} (341999004335d04dbc782d2dce198b2ec3e56d03)

And again with the --index option:

polbookpro:test pol$ git stash
Saved working directory and index state WIP on master: 1ec5ac0 Initial commit
HEAD is now at 1ec5ac0 Initial commit
polbookpro:test pol$ git stash pop --index
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   file

Dropped refs/stash@{0} (d1c4bc6c9be9d331285b32d22876663aa69990f5)

Same end result, I know makes no sense. That's why I wrote stash API in libgit2 to mirror exactly how it's implemented in Git CLT (even if it was so convoluted for no good reason). You have all these peculiar behaviors you need to preserve.

With REINSTATE_INDEX set, I was expecting to get conflicts on

polbookpro:test pol$ git init
Initialized empty Git repository in /private/tmp/test/.git/
polbookpro:test pol$ git commit --allow-empty -m "Initial commit"
[master (root-commit) 5756c56] Initial commit

polbookpro:test pol$ date > file1
polbookpro:test pol$ date > file2
polbookpro:test pol$ git add file1
polbookpro:test pol$ git stash   <----- Note that in your test this won't stash "file2" since you don't include
Saved working directory and index state WIP on master: 5756c56 Initial commit
HEAD is now at 5756c56 Initial commit

polbookpro:test pol$ date > file1
polbookpro:test pol$ date > file2   <---- Doesn't affect stash
polbookpro:test pol$ git add file1
polbookpro:test pol$ git stash pop --index
error: file1: already exists in index
Conflicts in index. Try without --index.

Now if I try to apply that same stash using libgit2's API and with GIT_APPLY_REINSTATE_INDEX, it fails with the error Cannot create a tree from a not fully merged index.. So same as CLT.

If I use GIT_APPLY_DEFAULT instead, then it also works like CLT i.e. stash application succeeds but it generates an index with a conflict on file1.

@swisspol
Copy link
Contributor

swisspol commented Apr 6, 2015

(I updated my previous comment, so read it on the web not in the email notification.)

Anyway as far as I can tell c31a1ed is working correctly and like Git CLT. However, it seems @ethomson's changes might have broken something.

@Therzok
Copy link
Member

Therzok commented Apr 6, 2015

Okay, I'm starting to understand how it works. Thanks for the clarifications.

I'll study git.git behavior on stash and update the tests to match that behavior. Thanks a lot.

There is a possibility I'm wrong, and not something that @ethomson changed screwing stuff.

@swisspol
Copy link
Contributor

swisspol commented Apr 7, 2015

@ethomson I just found this tiny bug, you should pick it up if you haven't already fixed it:
git-up@913662b

if ((flags & GIT_APPLY_REINSTATE_INDEX) &&
git_oid_cmp(git_tree_id(stash_parent_tree), git_tree_id(index_tree))) {

if ((error = merge_index_and_tree(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the checkout options for notifying checkout paths isn't used here and below, right?

It's only reporting untracked files for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @ethomson

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the checkout options for notifying checkout paths isn't used here and below, right?

It's only reporting untracked files for me.

Can you provide a unit test or something to repro this with?

@ethomson
Copy link
Member Author

Yes, I dropped the GIT_EEXISTS since that's not quite what that error message means, I don't think. I meant to create a new error type here, but forgot when I pushed this up.

Otherwise, I'm having a bit of trouble following this thread. Can one of you two summarize the status here? Is something else broken? If so, what?

@Therzok
Copy link
Member

Therzok commented Apr 17, 2015

I was unaware of git stash's behavior in case of reinstate index or normal.

If EEXISTS was dropped, then it's fine by me.

Otherwise, there's no checkout notification called for the updated files.

@ethomson
Copy link
Member Author

So... I restored the GIT_EEXISTS behavior when an untracked file exists in the working directory that would be overwritten by the unstash operation. However I still feel like this is not quite the behavior that we want.

Do we really need two different exit codes here? Could we use the checkout notify callbacks to discern between this case and other checkout conflicts? I would much prefer to simply return GIT_EMERGECONFLICT here...

Otherwise, @Therzok - I think the behavior that you're seeing was because if you explicitly provide a checkout option, then you must also provide a checkout strategy that is not NONE (or else we won't do anything)... I added a test here, but:

I added a change to imply a checkout strategy of SAFE when you use a checkout strategy (eg, from GIT_CHECKOUT_STRATEGY_INIT). This better matches rebase, but it implies that you cannot have an unstash dry-run. Meh. I would like to think a little bit more about this because I think that we're trying to make it friendly for consumers (upgrading their checkout strategy) but cutting off the utility of GIT_CHECKOUT_NONE.)

@swisspol
Copy link
Contributor

Otherwise, I'm having a bit of trouble following this thread. Can one of you two summarize the status here? Is something else broken? If so, what?

Sorry @ethomson I missed some comments here. The point is AFAIK my original implementation is "canonical" since based on exactly the same heuristics as Git shell script. @Therzok noticed some suspect behavior which partially went away when switching back to my original implementation. So looks one of your change introduced a regression. Not sure why it wasn't captured by the existing unit tests though.

Looks like with your latest changes in, @Therzok would need to confirm his tests behave same with both my original implementation and yours.

I added a change to imply a checkout strategy of SAFE when you use a checkout strategy

It's like git_reset() too and seems reasonable to me.

@ethomson
Copy link
Member Author

My understanding is that @Therzok was testing to the documentation, which I had not updated to reflect my removal of the GIT_EEXISTS behavior.

I have since updated to replace that behavior that I had removed but I think this is the wrong way to signal that condition and we should find something superior.

@ethomson
Copy link
Member Author

ethomson commented May 1, 2015

Okay, well, there was a resounding silence there. So I've dropped the whole GIT_EEXISTS return code. We now return GIT_EMERGECONFLICT for all merge conflicts, just like we do with all the other checkout flows.

I've added a progress callback to the options structure that you can use if you want to be able to discern one checkout conflict from another.

Seem sensible?

@swisspol
Copy link
Contributor

swisspol commented May 1, 2015

Where are we standing unit test wise now? Are the unit tests I originally written all pass unmodified with your latest version? That's the most important thing to me. Then I'm curious to hear about @Therzok's functional tests and if the results are now the same for both my original implementation and yours.

I'm afraid I don't have feedback on the GIT_EEXISTS code as I'm not affected by it and I can't say I fully understand what's at stake, sorry.

@swisspol
Copy link
Contributor

swisspol commented May 1, 2015

I'm completely fine with the modified implementation BTW, I'd just like confirmation that the tests are passing all the way up the stack.

@ethomson
Copy link
Member Author

ethomson commented May 1, 2015

The only change that I made to your tests was to assert the return code to GIT_EMERGECONFLICT, then change it back to GIT_EEXISTS, and now back again. Your tests were very helpful and comprehensive, thank you.

@swisspol
Copy link
Contributor

swisspol commented May 1, 2015

Sounds like that if @Therzok's tests are now passing, this should most likely land finally :)

@Therzok
Copy link
Member

Therzok commented May 2, 2015

Yes, it's all working properly now. I'll update all of libgit2sharp's tests tomorrow.

I'm only curious as of why aren't the Checkout options themselves used for providing the notification callbacks. But this works for me.

@ethomson
Copy link
Member Author

Cool. I updated this with CHANGELOG information, and a little bit more documentation around the checkout_options change.

I also included @swisspol 's patch (above).

carlosmn added a commit that referenced this pull request May 12, 2015
@carlosmn carlosmn merged commit 77a15fe into libgit2:master May 12, 2015
@swisspol
Copy link
Contributor

I was under the impression the unit tests I added to libgit2 were covering all cases, but I must have missed something as there's a regression with this PR compared to my original implementation (some of my higher-level functional tests now fail):

Start with this state:

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   bar.txt
    modified:   hello_world.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    test.txt

Then do git stash -u and verify that git status shows no pending changes afterwards.

Using git stash apply results in this state:

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   bar.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt
    modified:   hello_world.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    test.txt

However using libgit2's git_stash_apply() results in this different state (which wasn't the case in my original implementation):

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt
    modified:   hello_world.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    bar.txt
    test.txt

Notice that bar.txt wasn't restored in the index.

@swisspol
Copy link
Contributor

And here's another regression (possibly the same underlying issue):

Start with this state:

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   bar.txt
    modified:   hello_world.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    test.txt

Then do git stash -u and verify that git status shows no pending changes afterwards.

Using git stash apply --index results in this state:

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   bar.txt
    modified:   hello_world.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    test.txt

However using libgit2's git_stash_apply() with the GIT_STASH_APPLY_REINSTATE_INDEX flag results in this different state (which wasn't the case in my original implementation):

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo.txt
    modified:   hello_world.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    bar.txt
    test.txt

Notice how the index wasn't restored at all.

@ethomson
Copy link
Member Author

I feel like there should be unit tests for this! (In the libgit2 tests, I mean.)

@swisspol
Copy link
Contributor

I agree. What do you make of the 2nd case though? It seems GIT_STASH_APPLY_REINSTATE_INDEX is not working at all?

swisspol added a commit to git-up/libgit2 that referenced this pull request May 13, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request May 14, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request May 14, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request May 22, 2015
@swisspol
Copy link
Contributor

Hey @ethomson so what do you wanna do about the above issues? Current stash apply in libgit2 is not working like Git core unfortunately.

@ethomson
Copy link
Member Author

I think that the first step would be to add failing unit tests. I'm heads-down right now, so if that's something you can do, that would be great.

swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 2, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 2, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 6, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 10, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 10, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 10, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 12, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 12, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 15, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 15, 2015
swisspol added a commit to git-up/libgit2 that referenced this pull request Jun 17, 2015
@swisspol
Copy link
Contributor

Sorry for the noise above... It's because I have to continuously revert this stash apply implementation in my fork of libgit2, since it doesn't behave like Git CLT.

@swisspol
Copy link
Contributor

I created a dedicated issue to track this regression: #3230.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants