1

I'm on branch-B and I'm trying to merge branch-A into branch-B, but I want to merge all changes except for just one file. After some search, I found this solution:

git merge --no-commit <merge-branch>
git reset HEAD myfile.txt
git checkout -- myfile.txt
git commit -m "merged <merge-branch>"

I got really confused as to the second line: git reset HEAD myfile.txt; What does this line do? How does it help to the goal of merging all except one file?

EylM
  • 5,196
  • 2
  • 13
  • 24
Kid_Learning_C
  • 1,001
  • 1
  • 13
  • 25
  • 1
    Relevant documentation: [git-reset](https://git-scm.com/docs/git-reset) – wjandrea Jul 22 '19 at 20:22
  • 2
    Possible duplicate of [In plain English, what does "git reset" do?](https://stackoverflow.com/questions/2530060/in-plain-english-what-does-git-reset-do) – Julien Lopez Jul 22 '19 at 21:24

4 Answers4

4

You have a file that you wish should remain unaffected by the merge. But the file may in fact be affected by the merge. So you perform the merge but you stop without declaring it complete.

git merge --no-commit <merge-branch>

Now you undo the effects (if there are any) of the merge upon that file.

git reset HEAD myfile.txt
git checkout -- myfile.txt

Now you declare the merge complete.

git commit -m "merged <merge-branch>"
matt
  • 447,615
  • 74
  • 748
  • 977
3

The thing to remember when using git reset is that git keeps track of two sets of files:

  • Your "working tree", the checked out files you edit directly.
  • The "index", or "staging area", the files that will be committed when you next run git commit, which you generally manage with git add and git rm.

It also keeps track of what commit you have currently got checked out, as well as which branch.

git reset has various modes and arguments, which you can look up in the documentation.

When specified with one or more paths as well as a branch or commit reference (anything "tree-ish", as the git manual puts it), git reset sets those files in the index / staging area to their state at that commit, but doesn't touch the working copy or branch pointer.

Finally, HEAD refers to whatever commit you currently have checked out.

So in this case git reset HEAD myfile.txt changes the index to say "when I next commit, please make the content of myfile.txt the same as in the currently checked out commit". In other words, you don't want to commit any changes to that file.

The next line (git checkout -- myfile.txt) does the same for the working tree. If you run a git status in between, you'll see that all the changes to myfile.txt are listed as "unstaged".

IMSoP
  • 65,743
  • 7
  • 83
  • 127
  • Thank you for the answer. When you say "So in this case git reset HEAD myfile.txt changes the index to say that the version of myfile.txt you'd like to commit next is the same as in the currently checked out commit", is there a missing punctuation? I'm having hard time understand this sentence. my english is bad sorry – Kid_Learning_C Jul 25 '19 at 15:31
  • @Kid_Learning_C Sorry, it wasn't very well written. I've tried to reword it, is that clearer? – IMSoP Jul 25 '19 at 15:44
  • Much better! Thank you very much – Kid_Learning_C Jul 25 '19 at 17:50
2

matt's answer and IMSoP's answer are both correct (and I've upvoted them) but unless you have read about the function of Git's index / staging-area, a few parts of it are somewhat obscure. In fact, the process of doing a merge cannot be explained correctly without diving into some of the details of the index—and git reset's main job is to manipulate the index. So: What, precisely, is the index, and why does it have three names? First, it's useful to take a very brief look at commits.

A Git commit is a frozen snapshot of every source file, plus some metadata

When you make a Git commit, what you are really doing is freezing, for all time, a copy of all of your files (well, all of your committed files, but that's kind of tautological). The commit also stores your name and email address, and a date-and-time stamp—really, two of each of these—and some other useful stuff that we'll just ignore here, in order to concentrate on these frozen copies of every single file.

Obviously, if Git just made a new copy of every file every time, your Git repository would get pretty big pretty fast. So Git doesn't do that. Each frozen file is first compressed, into a read-only, Git-only form. The data stored this way is never changed, and in fact cannot be changed. It's just stored into a big database (Git's object database, which we can mostly ignore here). This means that once they are committed, your files are saved in this form for all time, which is good if you ever need them back. But if the frozen, compressed, read-only, Git-only data for the file is the same as it was in the (or any) earlier commit, it also means that Git can re-use the earlier saved file, instead of saving a new copy. Since most commits mostly don't change most files, this easily saves huge amounts of space.

Frozen is fine for achiving, but useless for getting work done

There are some drawbacks to this frozen, read-only, Git-only, compressed form (which I like to call freeze-dried). The freeze-dried files save space and can be used by many commits, but they're only useful as archives. They have to be expanded out—rehydrated—into a useful form for you to do any work with them, or to change them.

Hence, Git also provides you with a work area, which Git calls the work-tree or work tree or working tree or some variant on that name. Git could stop here, with freeze-dried Git-only committed files and work-tree useful files. Other version control systems do stop here. But Git doesn't. Git goes on to keep a copy—really, a reference—to each of the freeze-dried files that came out of the last commit. So instead of two copies of each file—the frozen one in the HEAD commit, and the working one in the work-tree—Git has three copies of each file. That third copy, which sits sort of between the HEAD copy and the work-tree copy, is what the index is (mainly) about.

The index has several roles

The index, which Git also calls the staging area—in many ways this is a better name but it misses a few corner cases—holds a copy of every file that will go into the next commit. This copy is in the freeze-dried format, i.e., is ready to go into a new commit. One trick here is that the process of expanding a frozen file is faster than the process of re-freezing a work-tree file. So when you first git checkout some commit, Git fills in the index with every file from that commit, as it appears in that commit. That means they're all ready to go into the next commit too. Git then rehydrates these frozen copies into the work-tree, so that you can see and work on/with them.

After you edit the work-tree copies, git add takes the work-tree copy and re-freeze-dries it, and stuffs that version into the index (indirectly but that doesn't really matter here). So now the index is updated—the updated file is staged by being copied into the index, replacing the old version. Again, the index is ready for the next commit. Git will automatically re-use all the unchanged freeze-dried files! It's really quite clever, except when the index gets in your way—which happens sometimes during merges, for instance.

The description above is a little bit of a lie. For the gory details, see Checkout another branch when there are uncommitted changes on the current branch. But it's a reasonable starting model for thinking about Git. This also leads to the third name for the index: Git sometimes calls it the cache, because it caches information about your work-tree. The two common names, though, are still staging area—which makes sense because git add copies things into it, and then it's like a staged scene that you're ready to photograph and archive with a commit—and index.

When you run git merge, this index takes on a greatly expanded role. You only see this when you have merge conflicts. If you don't have any merge conflicts, git merge expands the index, does all its work, and then collapses it back down. To keep this answer from being even longer, let's just assume that this was the case. :-) In that particular case—everything going well—the index now has each merged file in it, which—except for being freeze-dried—matches the merged work-tree copy as well, as if you (or Git) had run git add on all the merged files.

Normally, if git merge is able to do everything on its own like this, git merge goes on to make a merge commit. A merge commit is almost exactly the same as a regular (non-merge) commit—it has a snapshot as usual, made from the index as usual. The only real difference is that in its metadata, a merge commit records both the current HEAD commit and the other commit as its (two) parents. Normal commits only record the current commit as their (one) parent.

Since you used --no-commit, git merge stops before making this merge commit, but leaving things set up so that git commit or git merge --continue will make a merge commit. At this point, remember, the index has all the merged files. This is where git reset comes in.

git reset

What git reset does is ... well, it can be really complicated, because git reset, like too many other Git commands, stuffs too many different things into one user-facing command. But in this case, what git reset does is a kind of counterpart to git add. Let's draw the HEAD -- index -- work-tree setup like this:

  HEAD         index      work-tree
---------    ---------    ---------
README.md    README.md    README.md
Makefile     Makefile     Makefile
myfile.txt   myfile.txt   myfile.txt
...          ...          ...

The contents of each of these copies of these various files may be different. That is, HEAD:README.md has whatever is frozen forever into the current commit's README.md. It's in the freeze-dried format and it cannot be changed. The index copy of README.md—which you can see with git show :README.md, for instance—is also in the freeze-dried format, but you can change it whenever you want. And of course the plain READMD.md file is a plain ordinary file in your work-tree, and you can do anything you want with it.

What git reset HEAD myfile.txt does is tell Git: Copy the freeze-dried HEAD:myfile.txt into the index :myfile.txt. Compare this to git add myfile.txt, which tells Git: Copy the work-tree, rehydrated myfile.txt into :myfile.txt, freeze-drying it in the process.

git checkout

After updating the index copy, you might want to see what you updated. You could use git show :myfile.txt to see it on your screen (or in a window or whatever). But this is where git checkout -- myfile.txt comes in.

Like git reset, git checkout can do way too many things (in Git 2.23 git checkout is to be replaced with two separate Git commands, which each do fewer things). This particular form of git checkout tells Git: Copy the file myfile.txt from the index—from :myfile.txt—to the work-tree, rehydrating it in the process.

You can shorten the whole thing by one command, because git checkout can copy a file from a commit to your work-tree:

git checkout HEAD myfile.txt

will do the job. This kind of git checkout first copies HEAD:myfile.txt to :myfile.txt—it stays in the freeze-dried format for this operation—and then copies the index copy to the work-tree copy.

Summary

The index, or staging area, sits between the current commit and the work-tree. When you work on a Git repository, using files in the work-tree, Git doesn't really care about what you do to the work-tree. Git cares about what you to do the index, because Git makes your next commit—whether it's a regular ordinary commit with one parent, or a merge with two—from whatever you have in the index at the time you run git commit.

The presence of the index is what gives you the ability to change what will go into the next commit. That includes the case of a merge that you stopped on purpose with --no-commit. (The index is also what makes any given file tracked or untracked, and—because it takes on an expanded role during merges—handles the case of unmerged files, where Git was unable to do the merge on its own. It's sometimes tempting to try to ignore the index, because you can't see it directly (well, you can a little, with git show, and more with git ls-files --stage). But it has a huge and central role in so much of Git's operation that ignoring it is unwise.

torek
  • 330,127
  • 43
  • 437
  • 552
-1

It resets it to what is on the head (HEAD) of your current branch.

jason120
  • 84
  • 3