3

I'm on branch a. I want to make a commit on branch b, so that someone who clones branch b has the same working directory as I have now.

Stashing the current changes does not work because in some cases this results in a conflict.

I'm looking for the equivalent of making a temporary copy of the working directory, calling git checkout -f b, deleting all files, copying the temporary directory to the project directory and making a commit.

Toast
  • 596
  • 2
  • 19
  • 39
  • Commit your changes on branch a; checkout branch b; git pull origin/a --force; git reset HEAD~1; do your commit; git push --force; checkout branch a again; git reset HEAD~1; and that's it – ovimunt Mar 29 '19 at 00:48

3 Answers3

2

git reset --soft if your friend. If you want the revision after B to be the way you have on the working tree (uncommitted as of now), you can do this:

git checkout --detach # disconnect from A
git reset --soft b # set the branch pointer to whatever revision B is pointing to.... Your working tree will be unaffected
git add . # add everything to index
git commit -m "revision after B that made it look the way I had it where I was working"
# if you like everything, move b branch to this revision and push
git branch -f b
git checkout b
git push some-remote b

That should do.

eftshift0
  • 26,375
  • 3
  • 36
  • 60
1

Any commit essentially is "a temporary copy of the working directory".

The "changes between" you refer to are in fact produced for you from the snapshots when you git show <commit> for example.

That why it's a bit difficult to gie a straightforward answer your question. Let's try these two possible paths :


With history rewrite of branch b

If you want a commit on branch b reflecting the exact state on branch a at this very moment, why not just point directly branch b where it should. Like this :

# get the uncommited changes on the branch
git commit -am "Useful message"

# point b where a is now
git branch -f b a

# instead of forcing the branch we could merge with a strategy ours
# but it's hard to tell what you need from your description alone

# reset a to its previous state
git reset --hard HEAD^

Initial state :

C1---C2 <<< a <<< HEAD # with a dirty tree, uncommited changes

      ? <<< b # (you didn't mention where b was pointing)

Result afterwards

C1---C2 <<< a <<< HEAD
     \
      C3 <<< b # where your last (previously uncommited) changes are

As it rewrites history of the branch, it should be considered carefully, and probably ruled out if the branch is shared. Still, it does what you asked for : "someone who clones branch b has the same working directory as I have now".


Without rewriting history of branch b

To avoid rewriting the history of branch b, an alternative is to merge a into b with a strategy which will take everything from a side, not only conflicting parts. It would go like this :

# we work on a copy of branch a
git checkout -b a-copy a

# get the uncommited changes on the branch
git commit -am "Useful message"

# proceed with the merge, taking nothing from b
git merge -s ours b

# we now reflect the merge on b, and this is a fast-forward
git checkout b
git merge a-copy

# no further need for the working copy of a
git branch -D a-copy

And a doesn't need to be reset because it didn't move at any point.

After the first commit :

C1---C2 <<< a
     \
      C3 <<< a-copy <<< HEAD

      o <<< b (anywhere in the tree)

After the first merge :

C1---C2 <<< a
     \
      C3 (same state as a, plus the previously uncommited changes)
       \
        C4 <<< a-copy <<< HEAD (the merge taking only C3 side)
       /
      o <<< b (anywhere in the tree)

End state :

C1---C2 <<< a
     \
      C3 (same state as a, plus the previously uncommited changes)
       \
        C4 <<< b <<< HEAD
       /
      o
Romain Valeri
  • 19,645
  • 3
  • 36
  • 61
  • Can you suggest a way that keeps the history of the branch `b`? So that just one commit is added. – Toast Mar 29 '19 at 01:24
1

Actually, git stash does save the current work-tree. The problem is that the way it saves it is not directly suitable to your needs. There is also a secondary problem, which may be a very nasty problem but may instead be quite minor. See the caveat below. But in the end, you may be able to use git stash to do what you want.

What to know

First, remember that Git does not make commits from your work-tree (your working directory) at all—and that commits are complete snapshots of all files. They are snapshots that are made from the index, not from the work-tree. The files in a new commit are those that are in the index right now.

(Remember also that the index is that thing that other parts of Git call the staging area, or sometimes the cache. It holds one copy of every file that will go into the next commit. That copy is initially the copy taken from the current commit, except for some edge cases noted in Checkout another branch when there are uncommitted changes on the current branch.)

If your work-tree differs from your index, and you want to snapshot your work-tree, you need to git add each file, overwriting the copy in the index, before you can commit this. This, of course, destroys any careful staging you have done in your index.

But that's why git stash really makes two commits:

  • One commit saves your current index state, as a new commit that is not on any branch. Now it's safe to destroy the index state.
  • The second commit saves your current work-tree, as a commit with two parents: the index commit, and the current commit. To get that commit made, Git replaces all of the files in the index with their work-tree variants (because Git makes commits from the index, not from the work-tree).1

(There's actually a third optional commit to hold untracked or untracked-plus-ignored files. If that commit exists, it is the third parent of the work-tree commit. Usually it just doesn't exist though.)

Having made these two commits, git stash updates refs/stash to remember the work-tree commit w's hash ID. That commit remembers the index commit i's hash ID, as well as the current commit's hash ID:

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A   <-- b

Then git stash runs git reset --hard, so that your index and work-tree go back to matching commit T. I've highlighted one other commit A, as pointed-to by some other branch b.


1Technically, git stash makes commit w using a second, auxiliary / temporary index, just in case something goes wrong. It can just abandon the temporary index in that case. Making index commit i is very easy though, as the plumbing command git write-tree does all the work.


Making use of Git's stash commit

Remember the caveat here: git stash essentially just does git add on all the files that are already in the index. Any untracked files, including any untracked-and-ignored files, aren't in commit w at all. They're just sitting in your work-tree. That's true even if, had you done git checkout A to get to commit A, some of those files would have been copied into your index. (Of course in this case you would generally have seen a complaint that Git needed to overwrite some untracked file, first.)

Anyway, except for this one big caveat, the stash commit w has, in its snapshot, exactly the snapshot you say you would like to be added just past commit A.

You can, now that this snapshot exists, tell Git to make a new commit B that has A as its parent and w's tree as its snapshot. This needs one Git plumbing command:

git commit-tree -p refs/heads/b refs/stash^{tree}

That is, we use the name refs/heads/b (branch b, pointing to commit A) to tell Git what the parent hash ID should be for our new commit. We use refs/stash^{tree} to tell Git what the tree (snapshot) should be for our new commit. Git reads standard input to collect a log message—if you like, add -m <message> or -F <file> to supply a message, or send one to the standard input:

echo some message | git commit-tree -p refs/heads/b refs/stash^{tree}

The result is:

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A   <-- b
        \
         B

where new commit B has the same snapshot as stash commit w.

The git commit-tree command prints out the hash ID of the new commit. You'll need to grab this—perhaps into a shell variable—and then most likely set some name, such as refs/heads/b, to remember this commit. For instance:

hash=$(git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash

giving:

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A--B   <-- b

That is, new commit b is now the tip of existing branch b. The snapshot in B is that in w; they're automatically shared. The log message in B is whatever you gave to git commit-tree. The hash ID of B is now stored in b, and B's parent is A, so that this new commit is on branch b, just as you wanted.

Now that all of that is done, you will want to restore your index and work-tree, which git stash threw out, but first saved in those two commits. To do that, use git stash pop --index. The --index is important: it compares your current index to i and uses the differences to restore your index.2 Then it compares your current work-tree to w and uses the differences to restore your work-tree from w. The pop part of this then discards the i-w commits and if there were other stashed commits, makes refs/stash remember the correct one.

Hence, ignoring all the places where things could go wrong and all the appropriate error checking, the following command sequence might do what you want, depending on just what is is you want:

git stash push   # and make sure it does something
hash=$(echo automatic commit of work-tree |
    git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash
git stash pop --index

This is entirely untested (and has some bad failure modes, especially the one where git stash push says that there is nothing to save and refuses to do anything at all).


2This is an inefficient way of just reading i directly into the index, but it achieves the same goal. The same holds for the w step.

torek
  • 448,244
  • 59
  • 642
  • 775
  • I'll also note that except for the way it totally ruins your current index, it's much more *efficient* to just add everything and commit (using `git write-tree` and `git commit-tree` and `git update-ref` to put the commit onto branch `b`). You can fix the former problem by using a temporary index, the way `git stash` does. But that wouldn't illustrate why you can use `git stash` to get what you want. :-) – torek Mar 29 '19 at 01:47