简体   繁体   中英

How to create a full orphan copy of a current branch in git?

Let's say I have something like this: git log --oneline --graph --decorate

* 286f295 (HEAD -> Super_Branch) Adding super file
* bdeddb8 (origin/empty_base, empty_base) Initial commit

I want make Super_Branch an orphan and create a copy of each commit in that branch. git subtree split does something similar but it extracts a history of some path to an orphan branch; I just want to make existing branch orphaned.

Ideally I don't want to use git checkout or git checkout --orphan cause I don't want to change current HEAD.

An orphan branch doesn't really exist. Really, seriously, it doesn't. That's kind of what makes it an orphan branch. I think maybe what you want is to create a new root commit . There are several ways to do that, but before we get there, I need to provide the right background.

As poke commented , Git is a little weird here. Commits exist by themselves. They are very real entities, with their own separate existence. Each commit has its own unique name—its big ugly hash ID—by which you (or Git) can access that particular commit at any time. And, each commit stores some data—a snapshot of all of your files—and some metadata, such as your name and email address and a timestamp (or actually two of these).

One of the metadata items in each commit is a list of parent hash IDs. This list most often has just one entry in it. When we come across such a commit—a very ordinary one—we like to look at it by comparing it to its parent. That's what git log -p or git show does: they first print out the metadata, formatted suitably so that we see who made the commit and when, then run git diff on the snapshot stored in the parent and the snapshot in this commit, to see what is different in the two. From that, we infer that whoever made the commit, made those changes.

A merge commit is simply any commit with at least two parents. Because it does have two parents, we can't just compare it with its (single) parent to see what changed. So git log -p just shows the metadata, then moves on to show both parents (though one at a time) without attempting a diff. 1

So that covers commits with one parent (ordinary) and with two or more (merges). That leaves only one possible case: a commit with no parent. A commit with no parent is a root commit . When git log -p goes to show a root commit, it shows the metadata as usual, then does a diff against Git's special empty tree , so that it looks as though all files were added in that commit. (After all, they were.)


1 The git show command does attempt a diff, and does it using the combined diff trick. This doesn't show what changed! It can't, really. So instead, it shows some subset of changes as a result of combining each diff against each parent. For any parent-vs-child comparison pair in which some file didn't change, it shows nothing at all. For those files that, in the child, are different from all parents, it mushes together each of the parent-vs-child diffs into the combined diff. Some stuff gets tossed out of this as well, and mostly, you get a view into the areas where someone may have had to resolve conflicts.

This is useful, but it doesn't tell you what actually changed . To find out what changed vs one of the parents, use --first-parent or -m or an explicit git diff of the chosen parent vs the child.


The first commit in a new, empty repository is always a root commit

When you make a new, totally-empty repository:

mkdir somedir; cd somedir; git init

you get a repository that has no commits, and no branch names either. That's because in Git, a branch name like master must hold the raw hash ID of some existing commit. There are no existing commits, so there can be no branch names.

Nonetheless, if you run git status , Git will say that you are on branch master . How can you be on a branch that does not exist?

The answer is: this branch is an orphan branch . It does not exist, yet you are on it.

If you use git checkout -b other to switch from the nonexistent branch master to the new nonexistent branch other , git status will now say on branch other . You continue to be on a branch that does not exist—an orphan branch—and there continue to be no commits.

Until you actually make a commit, no branch can exist. So you can only be on an orphan branch, at this point. The git checkout -b command will keep creating orphan branches, and since orphan branches don't actually exist, the old orphan branch that you were on continues to lack existence, and is not shown by git branch , for instance. 2

Whenever you are in this state—of being on a branch that does not exist, ie, an "orphan" branch—the next commit you make will be a root commit . That is, it will be a commit with no parent (and, like all new commits, a new unique hash ID). Now that this commit exists, Git can finally create the branch name. So it takes the orphan branch name that is being remembered via HEAD , and creates the branch name, pointing to the new root commit. And now you have a real branch name—a regular, ordinary branch name that does exist and will be shown by git branch .


2 Underneath it all, this weird state of affairs is really quite simple. The file .git/HEAD , which holds the name of the current branch, just keeps getting updated in place. No branches are created! We just stuff a new name into .git/HEAD , without doing anything else.


git checkout --orphan name re-creates this weird state

If you run:

git checkout -b branch-X

Git creates the branch name branch-X pointing to the current commit . The branch now exists. The branch name holds the hash ID of the current commit—and, of course, Git attaches HEAD to the new name (ie, writes the name branch-X into .git/HEAD or, if this is an added work-tree, some other more-appropriate file).

But if you run:

git checkout --orphan branch-X

Git doesn't create the branch name. Instead, it attaches HEAD to the name without creating it. This is the same state we had with the totally-empty repository (well, except of course that it is actually possible to create branch names: Git simply chose not to create it right now).

The --orphan option was new in Git 1.7.2 (it's called out in the release notes ). It simply puts Git into a state in which the next commit you make will be a root commit. Creating that root commit will create the branch name, pointing to the new commit just created.

What will the contents of this next commit be? The answer is the same as always. When you run git commit , Git writes out whatever is in the index as the snapshot for the new commit. To this snapshot, it adds the metadata from user.name , user.email , your log message, and so on. It also adds the parent of the current commit—or in this case, no parent since you're on a branch that doesn't exist. That makes this new commit a root commit, with content from the index, and metadata as usual.

In your particular case, you'd like the content—the snapshot—to mirror that of an existing commit. So you can use git checkout --orphan , if you have it—if your Git is at least 1.7.2—by first checking out the existing commit, 286f295 or Super_Branch :

git checkout 286f295         # detached HEAD, or
git checkout Super_Branch    # HEAD attached to Super_Branch

This fills your index and work-tree from commit 286f295 . (Make sure you're not also carrying uncommitted changes: see Checkout another branch when there are uncommitted changes on the current branch .) Then you simply git checkout --orphan a new branch name—you can't use Super_Branch , as that name already exists:

git checkout --orphan tiny_tim

and then git commit to supply a new log message and use the current date-and-time and so on.

But: what if your Git is older than 1.7.2, or you want to re-use the log message from commit 286f295 ? Here, you can cheat: use git commit-tree directly. The git commit-tree command creates a commit given some existing snapshot—a tree object , in Git internal-speak—with the log message coming from its standard input. So:

git log --no-walk --pretty=format:%B 286f295

will extract the commit message from 286f295 . Piping that to git commit-tree will set the log message the way you want. (You will still be the author and committer of the new commit, made "now.") Hence:

git log --no-walk --pretty=format:%B 286f295 | git commit-tree 286f295^{tree}

will create the desired root commit, and print out its hash ID.

Now you simply need to create a branch name, pointing to the new commit:

git branch tiny_tim <hash-id>

where hash_id is from the git commit-tree command.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM