4

When I create an orphan branch I have to remove all the files so the branch is completely empty. I want to be able to cherry-pick another commit on to the empty branch, but I get an error saying the file has been modified on one branch, but deleted on the other. I want to avoid having to manually resolve these conflicts, so I can eventually automate this process. Is there a way to automate conflicts to accept the modified versions of files?

VanAlfredo
  • 193
  • 1
  • 2
  • 6

1 Answers1

2

It's not at all clear to me what kind of result you want—i.e., what your real goal is. Whatever that goal is, though, this is probably the wrong way to go about it.


When I create an orphan branch I have to remove all the files so the branch is completely empty.

This is true, but wrong. :-)

More specifically, let's take the parts of the operation, and the commands you need:

  1. git checkout --orphan newbranch: this modifies your current Git state so that your index and work-tree are unchanged, but you are on branch newbranch, which does not actually exist yet.

  2. git commit: this creates a new commit from whatever is in your index.1 Your work-tree is irrelevant. If you were on a branch that did not exist, such as newbranch from step 1, this has the effect of creating the branch. The tip of the branch you are on is now the commit just created, and the parent of the new commit you just made is the previous commit that was on the branch—which, in the state arising from step 1, is no commit, so that the new commit has no parent.

So when I say that it's true but wrong, I mean that no branch is ever empty. A branch—or more precisely, a branch name—points to one specific commit. That commit must exist! That commit has some parent(s), or no parent if that commit is a root commit. If you are using the word branch to mean loosely specified chain of commits, ending with one specific commit, that too is never empty. What you needed to do was clean out the index, because Git makes new commits from whatever is in the index.

Presumably, what you want is to create a new commit that has no files in it. That's not an empty branch! That's a branch with one commit—the new commit has no parent—in which the one commit has an empty tree. See also Is git's semi-secret empty tree object reliable, and why is there not a symbolic name for it? But there's only limited use for this empty tree object, or for commits that use it.


1If you're not familiar with this usage of the word index—also called the staging area or the cache—and the work-tree, see, e.g., What's the difference between HEAD, working tree and index, in Git? For more about the way Git and Git-users tend to conflate things called branch, see also What exactly do we mean by "branch"?


I want to be able to cherry-pick another commit on to the empty branch, but I get an error saying the file has been modified on one branch, but deleted on the other.

Since empty branch is meaningless, I have to guess here what you mean: that you've done git rm -r .; git commit to create this branch newbranch pointing to a commit whose tree is the empty tree. You now have an orphan branch with one root commit. If you run:

git checkout newbranch

Git removes all your (tracked) files, leaving you with an empty index.

If you now run git cherry-pick <hash> you get a lot of complaints of precisely the form you describe. That's because a cherry-pick is a merge. Let's draw part of the Git commit graph:

... <-F <-G <-H   <-- master

I   <-- newbranch (HEAD)

That is, the name master refers to commit H—the tip of our master branch. The parent of commit H—each uppercase letter here stands in for an actual hash ID—is commit G. G's parent is F, whose parent is in the ... range.

Commit I has no parent; it is a root commit. Our HEAD is attached to the name newbranch, which points to commit I.

At this point, running git cherry-pick <hash-of-G> produces those complaints, and the reason is that it's doing a three-way merge of commits F as merge base, I as --ours, and G as --theirs. A three-way merge consists of running two git diffs, to see who has done what work:

  • git diff --find-renames <hash-of-F> <hash-of-G>: this is what they did. Git compares the snapshot in F to the snapshot in G to see what they changed.

  • git diff --find-renames <hash-of-F> <hash-of-I>: this is what we did. Git compares the snapshot in F to the snapshot in I, to see what we changed. But we made I using the empty tree, so the difference is that we deleted every file!

The job of the merge engine is now to combine our changes—delete all files—with their changes, whatever those changes are. For any file they didn't change, Git takes our change and deletes the file. For any file they did change, Git declares a merge conflict. Unless the trees in F and G match, we're guaranteed at least one merge conflict, and the cherry-pick will stop and leave you to finish the job.

Is there a way to automate conflicts to accept the modified versions of files?

Sure: use git ls-files --stage to find the entries in the index, in their various staging slots. For any file that exists in stages 1 and 3—i.e., is in both the merge base and the --theirs commits, F and G in this example, there will be no file in slot 2, because we removed all files (commit I in this example). Your job is to replace the three higher-numbered staging slots with a single stage-zero entry. You presumably want the file from stage 3—from the --theirs commit—in staging slot zero, and there's an easy way to get that: git add the copy of the file that is in the work-tree.

In this particular conflict case, "deleted in ours" and "modified in theirs", Git leaves the modified file in the work-tree, as well as in staging slot 3. So git add will put the work-tree copy into slot zero and remove the entries in slots 1 and 3—there's nothing in slot 2 to remove in this case. (If there were, git add would remove it, but if there were, what's in the work-tree right now might not be suitable.)

Since this is the case for every file, you can simply run git add . to prepare the index, and then git cherry-pick --continue or git commit to finish the cherry-pick and get a new commit:

... <-F <-G <-H   <-- master

I <-J   <-- newbranch (HEAD)

New commit J contains a copy of every file that is in commit G that was different from the copy of the same file that is in commit F. Commit I remains holding the empty tree, and is therefore pretty much useless.

If commit J really is the desired result, there is an easier2 way to get it:

  1. Create a new empty temporary index.
  2. For each file in F and G, if the files match, do nothing, but if they differ, update the temporary index to hold G's name and hash ID.
  3. Make a new commit from this temporary index.

Step 2 is the only hard part, but is easily automated:

git diff-tree -r --find-renames -z | ...automation-program...

The -r makes git diff-tree recurse into subdirectories. The --find-renames and -z are optional: --find-renames enables the rename detector (otherwise renamed files are Deleted from the first commit and Added to the second under a different name), and -z arranges for the difference data to be ASCII-NUL-terminated instead of newline-terminated, and to avoid the need to "de-quote" difficult file names (see the RAW OUTPUT FORMAT section of the git diff-tree documentation). You'll have to write the automation-program yourself, but it can just invoke git update-index --cacheinfo with each cache information line, using the mode and hash information read from git diff-tree.

Step 1 consists of exporting an environment variable GIT_INDEX_FILE pointing to a name for a temporary file that does not exist: Git will create it as it goes. Step 3 is to run git commit-tree, the plumbing equivalent of git commit, and then use git update-ref to create or update a reference name to refer to the new commit. You can choose the parents—or no parent at all—for J directly. See their documentation for usage.


2Well, computationally easier. Clearly more work to write, vs just running git add ..

torek
  • 448,244
  • 59
  • 642
  • 775