0

As is said in

git - How do I commit only some files? - Stack Overflow

we can use

 git commit [--only] a b c -m "only part of files"

However in the following example:

$ mkdir t
$ cd t
$ git init
Initialized empty Git repository in /mnt/c/test/git-test/t/.git/
$ touch a b
$ git add .
$ git commit a -m a
[master (root-commit) c7939f9] a
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a
$ git commit b -m b
[master cf4514a] b
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 b
$ git status
On branch master
nothing to commit, working tree clean
$ ls
a  b

I tried to commit only the file b into the second commit but failed. (With a, b in working tree and working tree clean. This implies the two files are both committed.)

So how to truly commit part of files?

Even git add a single file doesn't work:

$ mkdir t
$ cd t
$ git init
Initialized empty Git repository in /mnt/c/test/git-test/t/.git/
$ touch a b
$ git add a
$ git commit --only a -m "a"
[master (root-commit) 04383c9] a
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a
$ git rm --cached -r .
rm 'a'
$ git add b
$ git commit --only b -m "b"
[master d518916] b
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 b
$ git checkout -f head~
Note: switching to 'head~'.

...
HEAD is now at 04383c9 a
$ ls
a
$ git checkout -f master
Previous HEAD position was 04383c9 a
Switched to branch 'master'
$ ls
a  b

File a is still in the second commit.

Background: Say I have a folder with many many files, and I want to commit file set A into the first commit (i.e. The first commit contains only file set A), set B into the second commit,... Why I do this: Just for curiosity.

kakakali
  • 159
  • 1
  • 11
  • use `git commit -p ./fiepath` command. see more here https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--p – Noman Gul Jan 31 '21 at 14:35
  • Use `git add -pi` before commit. read the help with `?` after you run the command. – caramba Jan 31 '21 at 14:38
  • @NomanGul It doesn't work. It's just committed all staged files. – kakakali Jan 31 '21 at 14:50
  • @caramba I think `git add -pi` is to choose which change chunks to be staged, not for file choosing. For example, it would show 'no changes' when you have some blank files. – kakakali Jan 31 '21 at 14:58
  • I think you're misunderstanding something. In your second example, you have not committed the removal of `a`, so it is still there in the second commit. What is it you are trying to achieve, and what for? I think that'll make it easier to explain this (or to realize that you're in fact correct :p) – lucidbrot Jan 31 '21 at 18:04

3 Answers3

2

To add to Mark Adelsberger's answer and address your comment here:

I used to take commit as a snapshot but not diff content. So I expect the commit command just takes a snapshot of the index and stores it.

This is correct. However, when you use git commit --only, the way Git achieves this is complicated. (It's also not well documented.)

I normally talk about "the" index / staging-area / cache. Git does have one particular distinguished index, "the" index, although it is actually per-work-tree: if you run git worktree add, you not only get a new work-tree, but also a new index (and new HEAD, and other work-tree-specific refs such as those for git bisect). But Git is capable of working with additional temporary index files, and this is what git commit --only and git commit --include do.

Let's look at your setup again:

$ mkdir t
$ cd t
$ git init
Initialized empty Git repository in /mnt/c/test/git-test/t/.git/
$ touch a b
$ git add .

At this point, "the" index (the main one in .git/index) contains two files. Here they are:

$ git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       a
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       b

Now, however, you run git commit a -m a, creating the initial commit (a root commit, with no parents). This command—git commit --only a, more or less—works by:

  1. creating a new temporary index, .git/indexdigits;
  2. initializing that index from the current commit;1
  3. running the equivalent of GIT_INDEX_FILE=.git/indexdigits add a;
  4. running the equivalent of cp .git/index .git/index.moredigits to create a second temporary index;
  5. running the equivalent of GIT_INDEX_FILE=.git/index.moredigits add a;2
  6. building a commit from the first temporary index, in the way git commit normally builds a commit from the main index;3 and
  7. finishing off the commit by renaming the second temporary index to .git/index, so that it becomes the primary index.

What this does is:

  • Create and use a temporary index for the commit that contains the HEAD commit plus the --only files. The main index is undisturbed in case the new commit fails (though in your case it succeeds).
  • Create and set up a second temporary index to be used if the commit succeeds.
  • Attempt the commit using the first temporary index.

If the commit succeeds, the first temporary index is discarded and the second temporary index becomes the main index (via a rename operation so that it's all atomic). If the commit fails, both temporary index files are removed.

This means that after a successful git commit --only, the main index is updated as if you had run git add on the --only files. After a failed one—the commit can fail due to pre-commit hooks, or you erasing the commit message, for instance—all is as if you had never run git commit --only at all.

(In your case, since you didn't modify the file a before running git commit --only a, you can't tell some of these cases apart.)

When you went on to run git commit --only b, these steps repeated but with file b instead of file a.


1There is no current commit, as you haven't created any yet, so this is treated as a special case: Git creates this as an empty index.

2This git add winds up having no effect, since the file named a is still empty. Had you modified the file named a in your work-tree at this point, though, it would have updated the second temporary index.

3Since Git is not using the file .git/index to build this new commit, any pre-commit hook that assumes that the index is named .git/index will do the wrong thing. Note that with added work-trees, the main index for that added work-tree has a different name as well (.git/worktrees/<name>/index, if I remember correctly offhand).

torek
  • 448,244
  • 59
  • 642
  • 775
  • Perfect answer and helped me a lot :)! Could you please tell me where I can find similar Git detail? – kakakali Feb 02 '21 at 05:41
  • If you mean this stuff about `git commit` specifically, I only know how this works because I looked at the `git commit` source code. If you mean Git in general, the single best book I know of right now is the [Pro Git book](https://git-scm.com/book/en/v2), but I don't really think any one single resource is sufficient. – torek Feb 02 '21 at 09:28
1

git commit [--only] path commits changes only to the specified files.

So you started with an empty repo. You staged two new files: a and b. Now you have two things staged (as you could confirm with git status):

  • new file a
  • new file b

You said

git commit a -m a

If you ran git status at this time, you would see that indeed only the change to a was committed; a is now committed and b is still staged as a "new file". Then you said

git commit b -m b

which committed only b, leaving a unchanged from the previous commit.

Again you can confirm that each commit only affected the files you specified with

git log --name-status

which will show you that the first commit only added a and the 2nd only added b.

It sounds like you want a command to create a commit that only contains specified files. To do that, you need to commit changes to not only any new files you're adding, but also any old files you want to remove. That's why your second attempt failed: you successfully created a staging area that only contains the file you wanted, but then you told git not to apply changes to anything but the new file. If instead of

git commit --only b -m "b"

you had simply said

git commit -m "b"

it would've done what you're trying to do.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • It works! I'm still confusing about COMMIT CHANGES. I used to take commit as a snapshot but not diff content. So I expect the `commit` command just takes a snapshot of the index and stores it. Could you please provide some docs on the commit mechanism? – kakakali Feb 01 '21 at 02:56
  • kakakali, you've got it right, you're stumbling somewhere on working out the implications. `git add` adds the new version of the content at a path, the index lists your last-checked-out/committed content as amended by you (with `git add` and `git rm` and other more esoteric e.g. bulk-work commands). `git commit` with a path ignores any staged changes you've made with those commands and uses only the current work tree versions of the paths listed to replace whatever the current tip aka `HEAD` commit has there, but whatever *other* paths are in that snapshot stay untouched. – jthill Feb 01 '21 at 03:39
  • @kakakali Well, *a commit* is a snapshot. The act of committing is whatever it's coded to be, and as the docs specify, in this case - i.e. when you give paths to the commit command - it's coded to construct a snapshot that differs from the previous commit only in those paths. (https://git-scm.com/docs/git-commit) Several commands treat commits as if they were the difference between themselves and their parents - think about how rebase works. This is a case where git could be more consistent with terminology, and certainly not the only one, but it is what it is – Mark Adelsberger Feb 01 '21 at 14:50
  • Yeah, I got it now. The doc for `commit --only ` really is: Make a commit (**from current commit**) by taking the updated working tree contents of the paths specified... – kakakali Feb 02 '21 at 05:46
0

The solution is pretty simple: add to stage only file that you are interested in. For example add . means - add to stage all files from current directory. Instead go to folder with a and type add a.

gardner
  • 49
  • 11
  • I tried with `git rm --cached` but also failed. You can see it in the question now – kakakali Jan 31 '21 at 15:46
  • firstly: add `"` quotes to commit message after `m` . secondly, try the same with additional flag `-o` which means `only` – gardner Jan 31 '21 at 16:21
  • `--only` is the default option when files are given. Still, I tried as your description and updated my question. Have you tried it successfully? – kakakali Jan 31 '21 at 16:44
  • yes, I staged both files and then I committed first one with option `-o (--only)` I couldn't commit next one :`On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits)` and this is expected behaviour – gardner Jan 31 '21 at 17:01
  • Well, I could successfully commit the first file. The key point is that you commit the first file into the first commit and then commit the second file into the second commit. After the two commits is finished, you checkout to the second commit with -f. You should find the first file is still there. – kakakali Jan 31 '21 at 17:23
  • Let's remove some confusion that was introduced in the above comments: Quoting of the message has nothing to do with it. Including or omitting the `--only` option keywork has nothing to do with it. Understanding what it means to "only commit specified files" has everything to do with it. – Mark Adelsberger Feb 01 '21 at 00:47