Question

Sometimes I want to clean up my personal git repo by deleting all my locally made branches. However, I like to rebase before pushing changes. With a rebase by itself:

  1. I clean the commit history before pushing to others
  2. I can still access the previous tip of the branch via git reflog

But if I delete the branch, I delete the reflog too (thus loosing access to those commit objects). Is there a way to delete the branch, but keep the reflog?

Currently, I solve the problem by making a secondary repo on my machine called backup and I push there before rebasing and any other 'destructive' actions. That way the backup's reflog keeps all activity. But I would rather just keep the reflogs in my main repo if possible.

Was it helpful?

Solution

Edit: now that the question is edited to clarify it, most of this answer is irrelevant. The short version is "no, at least not yet: reflogs are deleted when you delete the branch." There are some proposals to change this in a future version of git, if some issues can be worked-around. (I think this would be nice, because you could then just do git branch --undelete foo, or however they decide to spell it, and get everything back, all restored from the saved reflog. The biggest issue is purely an implementation detail: if you have a branch x and delete it and then create a branch x/sub, the deleted-branch's reflog would occupy the name x that has to become a directory. There's also the question of what it means to create branch x, delete branch x, and create branch x again without using some sort of --undelete spelling: what does that do to the old x reflog?)


At first I thought you meant "delete a branch but keep its reflog" (for which the answer is currently "no", reflogs get deleted when you delete the branch, though there are some proposals to change this, if some issues can be worked-around). But now I think what you mean is, you have something like this (output from git log --oneline --graph --decorate):

* 9390606 (devel, origin/devel) latest tip
| * a45b0c4 (branch-X) some work, finished
| * a37b5ec some work, phase two
| * eef283e some work, phase one
|/  
* 1f0507b starting point

You then want to rebase your local branch-X onto the tip of the branch with the names devel and origin/devel, but also keep your original branch, branch-X, under some other name.

This is easy to do. Copy the branch label and then rebase:

$ git checkout branch-X
$ git branch branch-X-v1
$ git rebase -i devel     # with or without -i, really

Let's say in the rebase you combine all three phases and change the message. This gives you:

* 3afbcac (branch-X) add feature X
* 9390606 (devel, origin/devel) latest tip
| * a45b0c4 (branch-X-v1) some work, finished
| * a37b5ec some work, phase two
| * eef283e some work, phase one
|/  
* 1f0507b starting point

The other thing you may want to do here is to "hide" the label branch-X-v1 and not see these commits in normal views. There's one way to do this that is even easier: don't copy the label, just do the rebase.

Let's pause for a moment and describe git's label mechanism. Labels are human-readable strings that identify a commit (or other git object) by its big ugly SHA-1 (the full SHA-1, not just the 7-character abbreviations shown above).

All labels in git work fairly similarly: they are just names, living mostly under a top-level directory-like name, refs/. Branches and tags live in refs/heads/ and refs/tags/ respectively.1 There are a few other things "special" about branch and tag names: specifically, various git commands (like git branch and git tag) look for them automatically. Branches are even more special, in that git checkout will get "on" one, and then git knows to advance the branch reference automatically when new commits are made there, and move the label on rebase, and so on. But for the purpose of labeling the tip of a series of commits, like the three some work ones, any name will do. What you need is an alternative name.

Fortunately, there are names that are built-in to git that last for "a while", but don't show up in git log --all output. These are git's "reflogs", which you can view with git reflog and git reflog branch. You will see names like branch-X@{1}, which keeps track of where branch-X pointed before the rebase.

The drawbacks here are that these names are temporary2 and relative (branch-X@{1} becomes branch-X@{2}, {2} becomes {3}, and so on, each time the branch tip moves). You can use forms like @{yesterday} as well, which can help, but I like my names a bit more permanent and steady myself.

You can use a non-branch name. The obvious one is a tag. Instead of copying the branch label to another branch like branch-X-v1, copy it to a tag:

git tag tag-X-v1

but you may find these clutter up your views too.

If you like, you can copy it to a name outside the standard name-spaces, but even those will show in --all output, which looks at all refs, not just branches and tags. If your viewer—whether that's you running git log, or if it's gitk or some other GUI— uses --branches --tags --remotes rather than --all, those "outside names" will be hidden, along with the commits they label. But you will have to spell them out explicitly, using refs/mine/ or whatever space you put them in, every time; and you will have to use git update-ref to create and delete them. (You could of course put this in a script: in fact, git update-ref is meant for use in scripts.)

My personal preference is just to use extra branch names. I wind up with X-v1 through X-v5 or whatever, as I revise the commit-series.


1"Remote branches" live in refs/remotes/. Notes, which are sort of new-ish to git, live in refs/notes/. The filter-branch command uses refs/original/. I think this is a complete list, at least today, but it's hard to be sure.

2They expire after 30 days by default. Sometimes they last 90 days by default, but not for this particular case, as the pre-rebase branch-tip is not reachable from the post-rebase branch-tip.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top