6

I have two copies of a repository, each of them with a few tens of stashed states (stash@{0}, stash@{1},...).

I need to get rid of one of the copies, and I want to transfer all stashed changes from the copy I am deleting to the copy I will keep. I want to keep all the information on parents and dates of all stashed states, as well as the state of the index.

I have been reading the docs, and I cannot find any simple/direct way to do this. Is it possible at all?


Update 1: One of the reasons I have for wanting to keep changes as stashed states, and not as commits, is that by using the --index flag I can retrieve both changes staged for commit and changes in the work directory. If I create a commit, I will also be destroying information of the stashed states where parent, index and work copy are all different one from another. My stashed states usually correspond to preliminary testing work that is very far from being compilable, not ready to be committed, and at the moment I don't have the time to go through tens of them sorting them out.


Update 2: I think I know how to view the information I want to back up, for example,

$ git show stash{5}
commit eb5731e828f467dbe9214d0e6a350f33898c1363
Merge: c9608582 1d6cb78d
Author: Author <author@example.com>
Date:   Wed Sep 20 18:54:51 2017 +0100

clearly produces the the id of the work directory state (commit line) and the date, and the ids in the Merge: line are the parent commit id and the id of the index.

What I don't know is how to transfer all this information to the second copy of the repository, as a new stashed state of it.


Update 3: Clarification: both copies of the repository already have stashed states.

jme52
  • 1,123
  • 9
  • 18
  • You should probably convert the stashes into proper commits on separate branches. Stashes are not meant to be shared or tranferred. – poke Aug 28 '18 at 15:16
  • I understand stashes are not easy / meant to be shared or transferred, but given the flexibility of git tools I would expect that it would be possible to do so. – jme52 Aug 28 '18 at 15:57
  • local branches should be used to store work that is experimental, not ready, etc. you don't have to push these local branches to anywhere until they are finished and merged to your dev or another "officical" branch. – Murilo Cruz Aug 28 '18 at 16:02
  • Possible duplicate of [Is it possible to push a git stash to a remote repository?](https://stackoverflow.com/questions/1550378/is-it-possible-to-push-a-git-stash-to-a-remote-repository) – phd Aug 28 '18 at 16:07
  • 1
    @phd: Thank you for the reference, but as far as I can see that question does not discuss the possibility of keeping the corresponding index statuses, and none of the answers seems to preserve them. – jme52 Aug 28 '18 at 16:25
  • Note that, as Mark Adelsberger's answer says, each stash is just a clump of commits (I like to call them "stash bags"—see https://stackoverflow.com/a/20589663/1256452) with a funny name. Most transfer operations work by name, so it's the funny name—the reflog hackery—that introduces all the problems. – torek Aug 28 '18 at 16:54

3 Answers3

6

Update - I do want to go ahead and agree with others that a workflow that leads to this probably isn't the best workflow.[1] But everyone's so busy harping on that, not a lot of people are providing practical answers about how you get from where you are to where you want to be. So:


The stash information is stored in a collection of commits, plus a ref with a heavily manipulated reflog. Dealing with the reflog will be the hardest part of what you're asking.

Not only is the reflog considered a local data structure (so no built-in behavior exists to share it), but each repo presumably has a conflicting reflog representing the local stack of stashed states, and the question of how to combine them is not straightforward.

One approach might look something like this. I'll call the repo you're dropping source, and the one you're keeping target.

First, create easily-shareable refs at each stash state in source.

$ cd /path/to/source
$ git tag stash-s0 stash
$ git tag stash-s1 stash@{1} 
$ git tag stash-s2 stash@{2}
// etc.

You also need to make notes of all of the stash messages. They're in the stash reflog.

$ git reflog stash
1111111 stash@{0}: Custom Stash Message Here
2222222 stash@{1}: WIP on master: 1234567 2

(You could store those as annotations on the tags, but IMO that's really no more convenient than anything else...)

Now you need to copy those tags (and their history) to target; that will ensure that all stashed data is present

$ cd /path/to/target
$ git fetch --tags file://localhost/path/to/source

(file://localhost/path/to/source is one possible URL for source, assuming it's locally accessible from target; you could use any git URL, or if source is already a configured remote of target, you can use the remote name instead of the url.)

Now comes the tricky part; you need to rebuild the stash reflog on target.

First you need to keep track of any entries already in targets stash reflog. You can do this with tags, the same as the stashes from source.

$ git tag stash-t0 stash
$ git tag stash-t1 stash@{1}
// etc.

And, again, make note of existing stash entry messages

$ git reflog stash
3333333 stash@{0}: WIP on master: 7654321 2

Then you can remove the stash ref. Normally I wouldn't circumvent the git interfaces but in this case, it's not like there's a "safe" way to do it.

$ rm .git/refs/stash
$ rm .git/logs/refs/stash

And finally you can build the new stash stack. Your first command will be

$ git update-ref --create-reflog -m "<stash-message-1>" refs/stash <tag-name-1>

or, on new enough versions of git

$ git stash store -m "<stash-message-1>" <tag-name-1>

where <stash-message-1> and <tag-name-1> are the stash message you recorded for what will now be the last (oldest/"bottom") stash on the stack, and the tag you used to preserve that stash state, respectively. Each subsequent command will be

$ git update-ref -m "<stash-message-n>" refs/stash <tag-name-n>

or

$ git stash store -m "<stash-message-n>" <tag-name-n>

progressing "forward through time" in the list of stashes.

And then you can do away with the tags you used.

$ git tag -d stash-s1
// ...

[1] It's feasible in git to create temporary branches then use interactive rebase as needed to clean the history up as you finally are ready to migrate it onto a "real" branch. And a stash is just as "heavy-weight" as real commits anyway, because a stash is real commits. A stash is great to push some changes out of the way for a minute so you can use your worktree for something else real quick, but a long-lived stash isn't the best thing.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • I will note that if I were to do this myself (I stopped using stash for anything long running so that I don't have to do this :-) ), I would not bother rebuilding the reflog in the target, I'd just leave the extra tags or other refs (I might use an alternate set of references) in place. But I'd probably actually start by using `git stash branch` in the original repo, to turn each stash into a real branch, after which everything is easy. – torek Aug 28 '18 at 16:29
  • @torek - I certainly agree with the sentiment that over-using stashes (or using a single stash for more than a short time) is not a preferred practice. As to whether those variations on the above procedure are acceptable to OP, I can't say. It seems the question is about how to end with the ability to keep using the stash command to access the changes, so I answered toward that goal. – Mark Adelsberger Aug 28 '18 at 16:36
  • One more thing worth mentioning: modern Git has `git stash store` to do the reflog manipulation, which at least insulates you from having to use `git update-ref` directly. – torek Aug 28 '18 at 16:48
  • This answer meets the OP's needs better than mine did. I will reiterate what others have said though. The need to do this indicates a likely workflow issue. – LightBender Aug 28 '18 at 17:34
  • @torek - That's true, and since it's a purpose-made command it makes sense to use it in place of `update-ref`. That said, the "hard part" of rebuilding the stash reflog is keeping the commit messages straight, and unfortunately `git stash store` doesn't help (or hinder) with that. – Mark Adelsberger Aug 28 '18 at 18:11
  • Thank you for your answer, I am giving it a try. Would it be possible for you to clarify if `git reflog stash` will do anything with old stashed states that have already been dropped, and why I should go through the stash store commands following time order? Also, `.git/logs/refs/stash` now contains dates and author information from when I ran the `git stash store` commands today, rather than the dates when I saved them, could you explain if that is OK, or if there is going to be any issues? (dates and authorship of the stashed commits and indexes have of course not changed) – jme52 Aug 29 '18 at 14:54
  • Finally, could you comment on how safe this is for all the actual commits of the repository, and if there is any way to check that everything has correctly been copied before removing the old repository? I like to err on the cautious side - e.g., I did `git fetch --no-tags /path/to/source.git +refs/tags/src_stashed*:refs/stags/src_stashed*` to make sure that with this I could not mess up the tags of the destination repository. – jme52 Aug 29 '18 at 14:58
  • @ripero: If you see all your stashes in the `git stash list` output, and you can run `git gc` without warnings (as I commented in point 6 of my answer below), then you can be pretty confident that all is properly transferred. However a backup to a cheap USB memory stick will not hurt anyone, will it? – rodrigo Aug 29 '18 at 15:44
  • @ripero - The git object store is pretty resistant to information loss. I wouldn't anticipate any risk toward existing commits. Of course in the event of an OS-level crash, anything is possible - so backups are never bad - but, for example, git will never try to "rewrite" an existing object, nor delete an object that anything might need; so the risks of corruption are limited. I do agree with your thinking that being selective about which refs you transfer is a good idea. It would even be reasonable to use branches instead of tags, so that by default they would map into a remotes/x/ namespace – Mark Adelsberger Aug 29 '18 at 17:09
3

Stashes are not meant to be moved that way, you should really do private branches, where you can do several commits without publishing them. But since stashes are actually implemented as hidden commits you can copy them if you are willing to do some hacking.

These instructions worked for me, but they may depend on the current git version or other details, YMMV:

  1. Copy the files .git/stash and .git/logs/refs/stash from the source repository to the destination. Warning! This will overwrite your whole stash list!. You can try concatenating both .git/logs/refs/stash files to merge both stashes (older goes first), but obviously you cannot merge the tip of the stash.
  2. Run this command in the source repository: git config uploadpack.allowAnySHA1InWant true.
  3. Run this command in the source repository and copy the output to a file/clipboard/whatever (I will call it $REFS): git stash list --format=%H.
  4. In the destination repository run: git fetch SOURCE $REFS (being $REFS the output of step 3).
  5. Check that all is well in the destination repository with git stash list.
  6. Run git gc in the destination repository and check for any warning: reflog of 'refs/stash' references pruned commits message that may reveal missing data.

The trick is that stashes in git are actually implemented as the reflog of a hidden ref named stash. So if you copy the ref, the reflog and fetch all the referenced commits, it will just work. And since you cannot fetch a commit by hash unless it is part of a public ref (and stash is not) you need to relax that condition, that is why you need uploadpack.allowAnySHA1InWant = true

NOTE: If there is no .git/stash file in the source repo, it means that it is packed. You can get the content that should be on that file with git rev-parse stash.

NOTE 2: If you have a stash in the destination directory and you don't think editing reflogs by hand is a good idea (it is not!) you can copy the original stash files with a different name, say stash2. Then you can see this alternative stash with git reflog stash2 and apply them with git apply stash2@{N}.

rodrigo
  • 94,151
  • 12
  • 143
  • 190
  • This is workable *if* the target repo doesn't already have stashes; as you note, if it does then this will not get the desired result. (And from the question, my assumption is that it does.) – Mark Adelsberger Aug 28 '18 at 16:16
  • @MarkAdelsberger: True, you could try concatenating the `.git/logs/refs/stash` files from both repositories. I think it should work. – rodrigo Aug 28 '18 at 16:18
  • 1
    I wouldn't recommend manipulating the logs manually in that way. – Mark Adelsberger Aug 28 '18 at 16:29
  • @MarkAdelsberger: Totally agreed. Ditto for using the stash in OP's way. But sometimes you have to do what you have to do... – rodrigo Aug 28 '18 at 16:38
  • @MarkAdelsberger: Or maybe they can copy the old stash with a different name `stash2`. It would work more or less the same way without editing the content of the log files. – rodrigo Aug 28 '18 at 16:49
  • Thank you, your explanation was useful in helping me understand what is going on. However, for now I am going to try to avoid editing manually the logs. – jme52 Aug 29 '18 at 14:44
  • Also, if I have understood your explanation correctly, I wonder if, in your last sentence, you actually mean "you can see this stash with `git reflog stash2`" - I would have expected `git reflog stash` to show the original stash. – jme52 Aug 29 '18 at 14:44
  • @ripero: Yes, it is a typo, fixed now. – rodrigo Aug 29 '18 at 15:41
0

Transfer A's stashes to B and B may have its own stashes.

  1. Push all of A's stashes to B. For the N-th stash, git push <path_to_B> stash@{N}:refs/tags/stashN. Run a loop to push them all.

  2. In A, run git rev-parse refs/stash and you get a hash. In B, run echo $hash > .git/refs/stash. Replace $hash with the real hash you get in A.

  3. Append the content of A's .git/logs/refs/stash to B's .git/logs/refs/stash. The last line of B's original .git/logs/refs/stash is hash1 hash2 name email timestamp message. The first line of A's is hash3 hash4 name email timestamp message. Change hash3 to hash2 in B's appended .git/logs/refs/stash.

  4. In B, delete all the tags from stash0 to stashN. git tag -d stash0 for example.

ElpieKay
  • 27,194
  • 6
  • 32
  • 53