Reputation: 8490
I have code myscript.py
that is on main
and I am trying to move it to an existing branch branchA
.
% git branch
branchA
* main
myscript.py
is on main
, how can I move it to branchA
so that it no longer appears on main
?
Upvotes: 0
Views: 623
Reputation: 489818
Code—or more precisely, files—reside(s) in commits.
Branches—or more precisely, branch names—select commits. Even more precisely, a branch name holds the raw hash ID of one particular commit. All other commits that are in the branch, are in the branch because of that particular commit.
No commit, once made, can ever be changed. But the raw hash ID stored in a branch name can always be changed. In fact, that's the whole point of branch names: to store an ever-changing hash ID.
As we make new commits, Git:
Because one piece of the metadata that Git adds in the second step is the hash ID of the current commit, as stored in the branch name at the time we run git commit
, this makes the new commit link back to what used to be the current commit. If we start out with a chain of commits with hash IDs, where we represent the raw hash IDs with single uppercase letters to maintain our personal sanity, we might have something like this:
... <-F <-G <-H
Here H
stands for the hash ID of the current (and so far, last!) commit. The branch name main
stores this raw hash ID, so the name main
points to commit H
:
... <-F <-G <-H <-- main
Meanwhile the metadata stored in commit H
contains the raw hash ID of earlier commit G
, so we say that H
points to G
. That's the arrow coming out of H
. Commit G
, of course, is a commit, so it has a stored hash ID, which in this case points to still-earlier commit F
, and so on.
Now, again, no existing commit can ever change. Our normal use of Git is to add new commits, like this:
...--G--H <-- main, somebranch
Note how we have two names—main
and somebranch
—that *both point to commit H
.
We run git checkout main
or git switch main
, edit code, run git add
, and run git commit
. Git packages up a new snapshot and makes a new commit that we'll call I
. Because we have main
checked out, Git writes the new hash ID into the name main
. To remember which one we have checked out, let's attach the name HEAD
to main
, and draw in the new commit:
...--G--H <-- somebranch
\
I <-- main (HEAD)
Note how main
moved, while somebranch
didn't. If we now git checkout somebranch
or git switch somebranch
, we get:
...--G--H <-- somebranch (HEAD)
\
I <-- main
The files from commit I
vanish, and instead, we have the files from commit H
. Git has removed those that go with commit I
—they're safely stored in the I
snapshot—and replaced them with the files from H
.
We can now address one way to answer your question:
myscript.py
is onmain
, how can I move it tobranchA
so that it no longer appears onmain
?
We should draw what you have. I'm not sure what commit your branchA
selects, nor what commit your main
selects, so I have to guess: your own drawing would be better, or you could run git log --all --decorate --oneline --graph
or any of the other fancy commands from Pretty Git branch graphs. But let's say we have:
G--H <-- branchA
/
...--F
\
I <-- main (HEAD)
Let's further assume that all files are committed (because if not, you have more options).
You can simply run:
git rm myscript.py
git commit
to make a new commit on main
that lacks the file:
G--H <-- branchA
/
...--F
\
I--J <-- main (HEAD)
Commit J
no longer has the file. Now you can git switch branchA
to switch to your existing branch branchA
and commit H
:
G--H <-- branchA (HEAD)
/
...--F
\
I--J <-- main
You now see all the files from commit H
, which of course means you don't see myscript.py
. But we know it's saved permanently in commit I
, so we need to tell Git: go get this one saved file from this existing commit.
There are multiple commands to do this; the one I generally recommend is git restore
, with the --source
and -SW
options:
git restore --source main~1 -SW -- myscript.py
which is a bit longwinded, and does the same thing as the older command:
git checkout main~1 -- myscript.py
That is, it uses the name main
with the suffix ~1
to find commit J
first (main
), then step back once (~1
) to commit I
. Then it finds the file named myscript.py
that is in that commit and copies that file to two places:
-W
copies the file to your working tree, where you can see and edit it.-S
copies the file to Git's staging area, where it is now ready to be committed.The git checkout
command doesn't have -S
and -W
flags: it just always copies to both places.
Now that you have this file and it's already git add
-ed, you simply need to run git commit
to make a new commit that will update the current (HEAD
) branch name:
G--H--K <-- branchA (HEAD)
/
...--F
\
I--J <-- main
Commit K
is exactly like existing commit H
except that it has this one new file added.
Note that these commits, in these branches, are the history. Commits up through commit F
are in both branches. The way that Git finds these commits is to use the branch names to find the last commit and then work backwards.
If you haven't committed the file yet, it is currently only in your working tree and maybe also in Git's staging area. See joshmeranda's answer, but note that git switch -c
tries to create a new branch; you want git switch
, which uses the existing branch.
Here, we can count on the fact that the commit identified by the name branchA
does not have a file named myscript.py
in it. This gets extremely complicated in practice, though for this case it's simple. For all the gory details, see Checkout another branch when there are uncommitted changes on the current branch.
All of the above is about adding more commits. In some cases, we have some commits and we don't like them for some reason. In such cases, we can—within limits—tell Git to stop using these commits.
Consider the case where we have:
G--H <-- branchA
/
...--F
\
I <-- main (HEAD)
and we decide that we don't like commit I
. What we do is leave it alone initially, and use it to copy the file myscript.py
to a new commit we add to branchA
:
G--H--J <-- branchA (HEAD)
/
...--F
\
I <-- main
using the same git restore
technique (though this time we use --source main
, not --source main~1
, since we did not make a new commit on main
).
Then, though, we git switch
back to main
and run:
git reset --hard HEAD~1
or:
git reset --hard main~1
The ~1
suffix does the same thing as before: find the commit, then step back one hop. This locates commit F
. We could run git log
and use the mouse to cut and paste the raw hash ID of commit F
:
git reset --hard <hash>
here. Next, git reset
:
--hard
).This leaves us with:
G--H--J <-- branchA
/
...--F <-- main (HEAD)
\
I ???
Note how there is no longer any name by which to find commit I
. If you've memorized its hash ID, or written it down on paper, or something, you can still find it that way. Git provides other ways to get it back, for at least 30 days by default, as well. But mostly it seems to be gone, as if it never existed.
So now it looks like the last commit on main
is commit F
, not commit I
. As long as nobody else has commit I
—you never sent it to some other Git repository—it's safe to "get rid of" a commit like this. If you did send it somewhere, it may come back from there, because Git really likes to add commits and will spread them like a virus, given half an opportunity. So it's often unwise to "rewind" or "remove" commits once you've given them out (with git push
, usually).
Upvotes: 1
Reputation: 3261
If you haven't yet committed the file, you can simply move the the new branch and carry on:
git switch -c branchA
If you have already committed the file, you can restore it to a certain commit before moving it to the new branch:
git restore -s <commit> myscript.py
git switch -c branchA
Upvotes: 0