Reputation: 31912
I have this repo, origin/master
I had a JIRA ticket, X-123. I created a branch named X-123, did some commits against it, which have been pushed up to origin/X-123
. And then a pull request made from origin/X-123 to the master branch, that got merged in.
The work's all complete except now in hindsight, it turns out the work I did should really have been against ticket Y-789. Ideally I want to:
Right now I'd settle for just the last of those; any ideas? Since all the work's already been merged into master, it's maybe shutting the door after the horse has bolted. I'm tempted to revert all the work I did against X-123, then repeat it against Y-789, but it seems as if there should be some easy step here (this can't be that unusual surely?).
Upvotes: 1
Views: 1166
Reputation: 487735
There's probably a better, Jira-specific, way to deal with this, but here's how you could do it all in Git.
Renaming a branch—a local name—is trivial:
git branch -m <newname>
while you're on it, or:
git branch -m <oldname> <newname>
Making changes to existing commits, however, is impossible; and renaming the branch does no good. But despair not! :-)
First, a small aside:
I have this repo,
origin/master
origin/master
is not a repository. It's a typical name for your Git's memory: "This is what I saw in another Git, which I'm calling origin
, the last time I called up that other Git on the Internet-phone and asked it about its branches. It said it had a master
that was commit a234b67...
or whatever, so I wrote down origin/master
in my repository to remember that their master = a234b67...
."
Your Git does this with every branch it sees in that other Git: if they have a branch named X-123
, you get a copy named origin/X-123
. That includes when your Git asks their Git to create an X-123
. As soon as they say "OK", your Git knows that their Git now has this X-123
(and that its hash ID is whatever your Git told them to use to create it!).
Second, let's just note here that a branch name, in your repository, like master
or X-123
is a shortened form of a full name that Git calls a reference. Any full name that starts with refs/heads/
is a branch, so your master
is really refs/heads/master
and your X-123
is really refs/heads/X-123
.
These so-called remote-tracking branches like origin/master
and origin/X-123
are references whose full name starts with refs/remotes/
and goes on to include the origin/
part. Your Git just renames a foreign Git's branch names (refs/heads/master
) to make your remote-tracking names (refs/remotes/origin/master
).
(For that matter, tag names are just references in refs/tags/
.)
Later, your Git strips away the refs/heads/
or refs/remotes/
part for display purposes. That's why you don't normally see these prefixes. Sometimes, for whatever reason, Git only strips away refs/
and you see remotes/origin/master
. In a few cases, knowing the full name is important. They probably won't come up here, but it's worth knowing.
Whether or not you rename your local branch from X-123
(refs/heads/X-123) to Y-789
(refs/heads/Y-789), the name itself simply maps to some commit hash: ac0ffee4deadcafe...
or whatever. Changing the name leaves the same commit hash in the new name. You must now make copies of all the commits except the merge.
The main tool you have for copying commits one at a time is git cherry-pick
. Let's say you make a new branch Y-789
that has no copies of the old commits yet, nor the merge. (I'm assuming your development branched off from master
based on your description, but if not, just use the other name here.) Let's draw this as a commit graph fragment:
...--o--*-----m------M <-- master, origin/master
\ /
A--B--C--D <-- X-123, origin/X-123
A round o
represents an ordinary commit. One of them, *
, is filled in to mark it: that's the merge base between what master
was earlier, back when it pointed to commit m
, and what is still the tip of X-123
and origin/X-123
. Another one, m
, has a letter to indicate that it is where master
was before X-123
was merged in, and the last one, M
, is marked to show that it's the merge commit, to which master
and origin/master
point. Finally, four of them have labels A-B-C-D
so that we can refer to them as the originals that we plan to copy.
What we want now is to make the Y-789
series of commits, which will be copies of the A-B-C-D
series. So we make a new label, Y-789, pointing to the merge base:
...--o--* <-- Y-789 (HEAD)
|\
| ----m------M <-- master, origin/master
\ /
A--B--C--D <-- X-123, origin/X-123
As indicated by the (HEAD)
, we want to be on this new branch now.
Next, we need to copy A
but, in the copy, change its commit message slightly, so that instead of X-123 blah
it says Y-789 blah
. To do that:
git cherry-pick --edit <hash ID of A>
This updates our index and work-tree to copy whatever got changed in A
and set up a commit message template, without actually making the commit yet, then invoke git commit
in a way that lets us edit the message. (Without --edit
, this makes the commit. We could then git commit --amend
it to fix its message, but that makes one "throwaway" commit so it's not quite as efficient—though we may not really care; "junk" commits are cheap in Git; read on for ways to automate this).
Then we just repeat for the remaining commits. This builds up a new chain:
...--o--*--A'-B'-C'-D' <-- Y-789 (HEAD)
|\
| ----m------M <-- master, origin/master
\ /
A--B--C--D <-- X-123, origin/X-123
We can now make a new pull request asking to merge D'
(Y-789, which we push so that there is now an origin/Y-789
) into master
. For someone who knows how (see below for details), the merge will be trivial to do, as master
already has the same changes in it, courtesy of merging D
; but if this merge gets done as a strict "add merge commit", the result will look like this:
...--o--*--A'-B'-C'---D' <-- Y-789, origin/Y-789
|\ \
| ----m------M--N <-- master, origin/master
\ /
A--B--C--D <-- X-123, origin/X-123
where N
is the new merge commit.
You can now safely delete the X-123
label (and do it in the other repository as well so that git fetch --prune
strips out your refs/remotes/origin/X-123
), but all those original commits will remain in the graph, and you will have the A-B-C-D
commits forever.
To get rid of the originals is harder, because merge commit M
exists. It can be done, but everyone who has this merge M
—that's you, and the other Git at your origin
, and any other Git user who has picked up merge commit M
—has to strip this commit out of each of their repositories that has it.
If you do this, though, and then merge D'
into master
, you get this graph:
...--o--*--A'-B'-C'-D' <-- Y-789, origin/Y-789
|\ \
| ----m-------N <-- master, origin/master
\
A--B--C--D <-- X-123, origin/X-123
This graph can be re-drawn to be much less messy, and in fact, it will look like the original graph except for using Y-789
.
If everyone now deletes their names X-123
and origin/X-123
, Git will eventually garbage collect commits A-B-C-D
. Until then, no one will see them normally either: they will just be zombie commits, lurking in each repository copy in case someone wants to resurrect them.
git cherry-pick
copyingTo automate the fancy copying, you can simply use git rebase
. What git rebase
does is identify some set of commits—in our case, A-B-C-D
—and copy them. When you run it as git rebase -i
, it first brings up an editor session on a series of pick
commands, each representing a single git cherry-pick
. You can change each pick
to edit
—this is the Big Hammer that lets you run git commit --amend
—or reword
, which just lets you edit the commit message, just as we did above.
After doing all the cherry picking, git rebase
moves the branch label. That is, instead of making a new branch Y-789
to start, Git makes all the new commits on a temporary (unnamed) branch, then moves the existing current branch name to point to the copies.
In other words, we would just run:
git checkout X-123
git rebase -i --onto $(git merge-base X-123 master) master
to tell Git to start in X-123
, find (for copying purposes) commits on X-123
that are not on master
, and then copy those commits to come after merge base *
, while also letting us change pick
to reword
. Once it's all done, Git will move the name X-123
to point to the final copy:
...--o--*--A'-B'-C'-D' <-- X-123 (HEAD)
|\
| ----m------M <-- master, origin/master
\ /
A--B--C--D <-- origin/X-123
Note that X-123
has moved. We now want to rename it Y-789
and then git push -u origin Y-789
to create Y-789
on the other Git (and hence origin/Y-789
in ours, and set that as our new upstream). We won't have to delete our existing X-123
, nor our origin/X-123
, we'll just need to convince the upstream repository to delete his X-123
and then use git fetch --prune
as usual.
Then we'll make the pull request and let whoever controls merging deal with it all.
D'
Whether merging D'
is easy or hard depends on these things:
D
into master
?M
? (Note that this has consequences for everyone "downstream", including you: you must all repeat this stripping-out. That could be very easy, or it could be hard.)git merge -s ours
?M
and there were conflicts, does whoever is doing this know how to simulate git merge -s ours
?If there were no merge conflicts and no hand-tweaking required, everything is really easy. Just keep or strip M
as desired (to strip it, we need git reset --hard
and we need to be sure there are no commits after M
as well, and then there are all those downstream problems, so be very sure this is what you want). Then merge, and you're done.
If there were merge conflicts, but you can git merge -s ours
without stripping M
, it's easy: just git merge -s ours
to say "we already did the merge correctly, so completely ignore all the commits on the A'-B'-C'-D'
chain when making the merge result.
If there were merge conflicts and the person doing the merge is stripping away M
, it's a bit harder. The general idea is to strip out M
from master
but keep the commit and its tree (e.g., make a name pointing to it, or rely on reflogs and just copy-paste the hash, or whatever). Then run git merge
, which will see the same merge conflicts; but then remove the merge result and replace it with the tree from the saved commit. Or, equivalently, one can use the git commit-tree
plumbing command to do this with less muss-and-fuss, but that's getting pretty deep into Git arcana.
Note that all this assumes you make no changes to the tree, i.e., that git diff
of the original D
commit and the new D'
commit would be empty: only the messages (and hash IDs) have changed, not the source trees associated with the commits.
Upvotes: 7