5

I need to list commits that touch one path in the source tree but not another.

D - head

C - touched foo and bar

B - touched foo

A - baseline

Between A and D I want a list of all commits touching folder foo unless they also touch folder bar, in other words, in the example above I would expect to see B but not C.

I tried git rev-list A..D --exclude='bar' -- foo but that gives me both B and C still.

Please advise.

FFD
  • 68
  • 5

2 Answers2

3

Since Git 1.9, and the introduction pathspec magic :(exclude) and its short form :!, the syntax should be:

 git rev-list A..D -- foo ":(exclude)bar"

However, this would still list C, even though the files displayed for that commit would not include files with bar in its path:

 git log --oneline --name-status A..D -- foo ":(exclude)bar"

The pathspec exclusion works, but C is still listed.

Community
  • 1
  • 1
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Does this actually work for this case? I think exclude simply keeps those files from engaging in selection, rather than *de*-selecting commits. – torek Aug 13 '16 at 09:09
  • @torek at least, I have good result with `git log --oneline --format=%H -- foo :!bar` – VonC Aug 13 '16 at 09:15
  • I tried a test and it's not working that way for me (on Git 2.8.1). It's not what I would expect based on the pathspec documentation, either. – torek Aug 13 '16 at 09:20
  • I've edited my answer to include sample output. If one just wants to exclude those files (but still keep the commit if it touches explicitly *in*cluded files), `:(exclude):` or `:!` will do the trick. – torek Aug 13 '16 at 09:36
2

There is nothing built into Git to do this. However, it's easy to construct with a small shell script using the comm command. Simply get a list of all commits that affect interesting files, and a second list of all commits that affect files that make the commit uninteresting, then use comm to extract the interesting items (those that appear in the first list, but not in both the first and second list).

Hence:

git rev-list A..D -- foo > /tmp/list1
git rev-list A..D -- bar > /tmp/list2
comm -23 /tmp/list[12]

Note that the "sorted" requirement for comm is deliberately violate-able (and is violated here); if your particular comm insists on it, you may need to write your own variation.


Edit: I tested VonC's suggestion, in Git 2.8.1, by making a new repository with an initial commit with files A/a and B/b:

$ git --version
git version 2.8.1
$ git log --oneline --name-status
731a059 both
M       A/a
M       B/b
6894130 B only
M       B/b
b4bb2d8 A only
M       A/a
f134515 initial
A       A/a
A       B/b

Adding -- A correctly discards the B only commit:

$ git log --oneline --name-status -- A
731a059 both
M       A/a
b4bb2d8 A only
M       A/a
f134515 initial
A       A/a

However, adding :(exclude):B has no effect on the selected commits:

$ git log --oneline --name-status -- A ':(exclude):B'
731a059 both
M       A/a
b4bb2d8 A only
M       A/a
f134515 initial
A       A/a

Commit 731a059, which touches files A/a and B/b, still appears in the output, as does initial commit f134515. The difference is that when the two commits that also affect B/b are selected, the file B/b is omitted. The original question calls for the entire commit to be excluded.


Edit 2 (using comm and bash's <(...) syntax, to omit the temp files):

[~/tmp/git-exclude]$ comm -23 <(git rev-list HEAD -- A) <(git rev-list HEAD -- B)
b4bb2d84d633e2254081b7ce99681d92b7974a9d

This is the desired output, since only commit b4bb2 affects only the A directory.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Excellent, thanks! I had to add `--nocheck-order` to suppress warnings from comm, but that gives me what I was after. – FFD Aug 13 '16 at 11:48