TL;DR
You want git rebase
, which you must follow up with a force-push.
Long, using the updated question:
I clone a repo master with a v2 tag to my pc [using git clone <url>
]. I create a new test branch.
Let's assume you did this with:
git checkout -b test v2
where v2
is the tag in question. There's a difference, in Git, between a branch name like master
, and a tag name like v2
. The difference is actually very small and comes in two parts that are strongly related.
A branch name identifies one particular commit. But which commit, changes over time. The name identifies the latest or last commit that we'd like to say is "on the branch". In fact, it changes automatically for you, after you run git checkout <branch-name>
and start making new commits.
A tag name identifies one particular commit. It should never identify any other commit—it should stay a name for that commit, forever. (It is possible to move a tag—anyone can do this—but for purposes of keeping one's sanity, just don't do it.) And, after git checkout <tag-name>
, you are in a slightly odd state that Git calls a detached HEAD.
The reason for all of this is that Git is really not about branches at all, but rather all about commits. A commit is sort of the fundamental unit in Git. Every commit has a big ugly hash ID, unique to that one particular commit. These hash IDs are, in effect, the "true names" of the commits. No commit can ever change—not a single bit—because the hash ID is actually a cryptographic checksum of the contents of the commit. If you could somehow change the contents, that would change the hash ID, which would mean you have a new, different commit.
This cryptographic-checksum thing is also why commit IDs seem so nonsensical, and are pretty much useless to humans. The fact that they are useless to humans is why we need names for them, and hence why we have branch and tag names.
Each commit stores a bunch of things, such as the name and email address of the person who made the commit (and the time-stamp), a log message for git log
to show, and a full and complete snapshot of every source file. One of the things each commit stores is the raw hash ID of its parent commit. These form a backwards-looking chain:
... <-F <-G <-H
(where the letters stand in for the actual hash IDs).
Given the hash ID of commit H
, Git can find the actual commit itself. That commit contains the hash ID of commit G
, so Git can find G
. Having found G
, Git gets the hash ID of F
, so that it can find F
, and so on. So Git is constantly working backwards, starting with the latest and going back in time.
Since nothing inside a commit can ever change, we can just draw this as a linear sequence:
...--F--G--H
as long as we remember that it's easy to go backwards but not to go forwards.
Making a new commit is just a matter of:
Selecting a branch name, which also selects its last commit:
...--F--G--H <-- branch-name (HEAD)
Selecting the branch attaches the special name HEAD
to the name, so now HEAD
is simultaneously synonymous with both branch-name
and commit H
.
Doing some work and using git add
to copy files back into Git's index (also called the staging area, or sometimes the cache, depending on who is doing this calling).
Note that if you use git add
on existing files, they've been copied into the index over top of the files that were already in the index, replacing some of the old versions. If you use git add
on new files, they've been copied into the index, but they're just new files—they didn't overwrite any previous index copy.
Running git commit
. This freezes the index copies into committed versions. It also gathers from you your log message, and puts you in as the author and committer of this new commit. The parent of this new commit is the current commit H
:
...--F--G--H <-- branch-name (HEAD)
\
I
And, now that the new commit exists (and therefore has acquired its own new unique hash ID), Git simply writes its hash ID into the branch-name, so that branch-name
now points to commit I
:
...--F--G--H
\
I <-- branch-name (HEAD)
So, if you did git checkout -b test v2
, then you have something like this in your repository. Note that the name v1
identifies some other (different, probably earlier) commit, so let's draw them all:
...--F <-- tag: v1
\
G--H <-- tag: v2, test (HEAD)
Note that git checkout -b name commit-ID
sets things up so that the new name
points to the selected commit commit-ID
, then makes it current by filling in your index and work-tree from that commit, and attaches the special name HEAD
to the new name
, all in one command.
I add new files and commit
OK, so you created a new file in the work-tree (where you do your work) and ran git add newfile
. This copied newfile
into your index—there's one index that just goes with this one work-tree—and then you ran git commit
to make a new commit I
:
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I <-- test (HEAD)
then push to test branch.
So at this point you ran:
git push -u origin test
This sends commit I
itself to the other Git at the URL your Git is remembering under the name origin
. Note that there's a whole separate Git repository there! It has its own branches and tags, though because tags never move—or should never move—your tags and their tags should all agree as to which commit hash ID v1
and v2
point-to.
Having sent commit I
, your Git then asked their Git to set—or in this case create—their own branch-name test
, pointing to commit I
. Presumably this was all fine with their Git, over at origin
, so it did that.
Now my senior dev told me that master with v2 tag has a problem and ask me not to use. So my test branch needs to fallback to master with v1 tag.
No part of any existing commit can ever change. This means commit I
is stuck where it is. If you made a few more commits, they're all stuck:
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I--J--K <-- test (HEAD)
What you need is a new series of commits that are like I-J-K
, but different in a few ways. In particular, you want your new files to be added to the snapshot that is in F
, as pointed-to by tag v1
. Then Git should commit that snapshot, re-using your commit message from commit I
, to make a shiny new commits with a new and different hash ID that we'll call I'
to indicate that it's a copy of I
:
I' <-- [somehow remembered as in-progress]
/
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I--J--K <-- test
Having successfully copied I
to I'
, you now want your Git to copy J
to J'
in the same way, then copy K
to K'
:
I'-J'-K' <-- [somehow remembered as in-progress]
/
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I--J--K <-- test
Last, you'd like your Git to peel your test
label off commit K
and paste it onto commit K'
instead, and get back on your branch test
as usual, except now this means commit K'
:
I'-J'-K' <-- test (HEAD)
/
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I--J--K [abandoned]
Once all that has happened, you need to send new commits I'-J'-K'
to the other Git at origin
and tell origin
's Git to move their test
to point to K'
, too.
git rebase
does the first part for you
First, you should run:
git checkout test
git status # and make sure everything is committed!
If all looks good, then you just need:
git rebase --onto v1 v2
This command tells Git: Copy some commits. The commits to copy are the ones that are "on"—technically, reachable from—my current branch, minus any commits that are also "on" the name v2
. The place to put them is after the commit identified by the name v1
.
- The name
v2
names commit H
, which names commit G
and so on backwards. So these commits won't be copied.
- The current branch name,
test
, names commit K
, which names J
which names I
which names H
, so therefore I-J-K
are the ones to copy.
- The
--onto v1
tells Git where to put the copies: after commit F
, as named by v1
.
At the end of the copying process, Git yanks the label test
(your current branch) off commit K
and makes it point to the copy K'
instead.
git push
now requires --force
Since you already sent commit I
to origin
, they have:
...--F <-- tag: v1
\
G--H <-- tag: v2
\
I <-- test
as their set of commits and their branch and tag names. Though, of course, if you sent J
and K
since then, their test
points to commit K
. For now we can just assume theirs points to I
and that they don't have J
and K
, because if they do have them and their test
points to their copy of K
Note that the hash IDs—and the underlying commits themselves—are the same in every Git repository. This is why the hash IDs are cryptographic checksums of the contents! As long as you and they have the same contents, you have the same commit, and it therefore has the same hash ID. If you have different hash IDs, you have different contents, and different commits. All we need to know is: do you have this hash ID?
If you have not abused the tag names, those are also the same in all Git repos: the same names identify the same commits. But the branch names differ, on purpose, because Git is built to keep adding new commits.
So now that you have run git rebase
and abandoned three old commits I-J-K
, now you will run git push origin test
again. This will send them I'-J'-K'
—your new commits, that you have that they don't, which your Git and their Git can tell by hash ID alone—and then ask them to move their test from wherever it points now in their repository—to I
or maybe to K
. You're asking them to move it to point to K'
.
They will say no. The reason they'll say no is that their Git will see that moving their test
from I
or K
, whichever it is set to now, to K'
is going to abandon commit I
(and J
and Kif they have those). So instead of politely asking the other Git, the one at
origin, to please if it's OK update
testto point to
K'`, you need to do:
git push --force origin test
to tell them: Move your test
to point to K'
, even if that abandons some commits!
(They can still say "no", but generally, if it's your branch that you're telling them to force, they should allow it.)