Borealis
Borealis

Reputation: 8490

Move code from main to an existing branch

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

Answers (2)

torek
torek

Reputation: 489818

Code—or more precisely, files—reside(s) in commits.

Branches—or more precisely, branch namesselect 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:

  • packages up a new source snapshot;
  • adds some metadata, or information about the new commit we're making right now;
  • writes all of this out as a commit, which gains a new, unique hash ID; and
  • writes the hash ID of the new commit into the branch name.

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 on main, how can I move it to branchA so that it no longer appears on main?

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.

There are more options

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:

  • moves the branch name to the commit we selected;
  • resets Git's index / staging-area; and
  • makes your working tree match (due to the --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

joshmeranda
joshmeranda

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

Related Questions