0

I wonder, what does

git diff --name-only "$CI_COMMIT_SHA"^!

do exactly? Does it check changes in one last commit or what?

Dims
  • 47,675
  • 117
  • 331
  • 600
  • I only found the duplicate after writing my own answer. If you think this question should be separate from the duplicate, let me know and I will unlink them. They are _very_ similar, but not exactly the same question, because the linked dupe asks about the behavior when used with the "first commit" (i.e. the root commit or a commit with zero parents). – knittl Sep 28 '22 at 10:48

1 Answers1

2

See git help revisions, specifially the section Revision Range Summary:

<rev>^!, e.g. HEAD^!

A suffix ^ followed by an exclamation mark is the same as giving commit <rev> and then all its parents prefixed with ^ to exclude them (and their ancestors).

So for a regular (non-merge) commit, e.g. HEAD, this is simply a shorthand notification for HEAD ^HEAD^. But for a merge commit, this resolves to HEAD ^HEAD^1 ^HEAD^2 ^HEAD^... (normally, merges have only 2 parents).

The easiest way to find out what those revision specifiers mean is to run them through git rev-parse, e.g. git rev-parse HEAD^!, git rev-parse HEAD^@, or git rev-parse ^HEAD.

In the context of git diff, this will call the following form of git diff:

git diff [<options>] <commit> <commit>…​ <commit> [--] [<path>…​]

This form is to view the results of a merge commit. The first listed <commit> must be the merge itself; the remaining two or more commits should be its parents. A convenient way to produce the desired set of revisions is to use the ^@ suffix. For instance, if master names a merge commit, git diff master master^@ gives the same combined diff as git show master.

So to view the combined diff of a merge commit, you would need to run git diff HEAD HEAD^@, or shorter git diff HEAD^!

I'd expect to get a combined diff of a merge commit by running git diff HEAD HEAD^@ or at least with git diff --cc HEAD HEAD^@, but quick tests didn't confirm that.

In fact, it looks like git diff HEAD^! is equivalent to running git diff HEAD HEAD^ even for a merge-commit which doesn't make much sense to me because it shows the "reverse diff" (going from the current commit to its first parent). This might well be a bug OR something simply not supported by git diff, i.e. undefined behavior. Why? Because git diff is all about endpoints and not revision ranges – and HEAD^! specifies a range of commits.

knittl
  • 246,190
  • 53
  • 318
  • 364
  • Take the last paragraph with a grain of salt, I have to check this first. The documentation is a bit unclear on this and the behavior is not immediately obvious. I will edit my answer shortly – knittl Sep 28 '22 at 10:31
  • What are commit "parents"? :D – Dims Sep 28 '22 at 10:40
  • @Dims every commit in Git has at least one parent – with the exception of the very first commit in a repository – and merge commits typically have 2 parents (but can have more). Following the parent pointers gives you the "history", i.e. what some people keep calling "the branch". Run `git diff --format=fuller` and you will see the parent commit(s) listed with each commit. – knittl Sep 28 '22 at 10:47
  • @Dims: for non-merge commits (i.e. "normal" commits) there's only one parent and it's the commit that happened immediately before it. Merge-commits have one parent per branch that is being merged (usually exactly 2, but technically it could be any number). – Joachim Sauer Sep 28 '22 at 10:48
  • Assuming this code is for Gitlab merging to master, what does it do? Compares the content of the branch with the master? – Dims Sep 28 '22 at 11:04
  • No, please read my answer (and the linked dupe): it compares the commit with its _direct_ parent(s). Or at least it claims to. I'm not really sure what happens here and if the output is in any way sensible. If you want to compare a commit against master, then simply `git diff master commit`, or if you want to compare a commit to its merge-base with master `git diff master...commit` – knittl Sep 28 '22 at 11:07
  • Assuming that in Gitlab all changes in the branch are first squashed and then merged into master (is this true?) then whith what this diff will be? I understand, that incoming branch has changes, but what about another parent? What changes can it introduce? – Dims Sep 28 '22 at 11:10
  • @Dims do you squash before pushing? Or are you talking about "squash before merge" option? Where do you want to run this command? I'm unclear what exactly you are trying to ask (and about what part of my answer, your question, or the comments). The syntax is `git diff A B` and shows you the changes to get from A to B. That's it. It doesn't matter from which branches these commits are reachable or not. `git diff A B` compares commits _two commits_. – knittl Sep 28 '22 at 11:13
  • @knittl I found this code in Gitlab CI job which is running under condition `if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH` and trying to understand, what it does exaclty. Sorry for being unable to capture your answers fast! I have an impression, that it can be replaced with something more explicit and which can run in any branch... – Dims Sep 28 '22 at 11:28
  • @knittl if I understood you correctly, it won't work as expected, if squash option not set... – Dims Sep 28 '22 at 11:29
  • @Dims but this is now no longer about `git diff` nor `^!`, but how GitLab pipelines work and what `$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH` does. This will only run the pipeline if the commit for which the pipeline was created is on the default branch. But this should be a different question – knittl Sep 28 '22 at 11:31
  • 1
    The `^!` suffix was never officially defined for `git diff`. It turns out to have some weird properties that some people were somewhat depending on, that changed when I made `git diff` handle three-dot merge base syntax better, and there's some mailing list discussion now about this. It's best *not to use this syntax at all* with `git diff` for now, since it's undocumented and behaves differently in different Git versions. – torek Sep 28 '22 at 16:42
  • 1
    Currently, the internal revision parser turns `rev^!` into not `rev ^rev^1 ^rev^2 ... ^rev^N` but rather into `^rev^1 ^rev^2 ... ^rev^N rev`, i.e., the order is different from what's shown in gitrevisions. This current behavior (which hasn't changed) directed `git diff` one way in the past and another way in 2.28 and later. – torek Sep 28 '22 at 16:44
  • 1
    Yes, this seems to be a bug (?) or at least a change in behavior in Git 2.28 that is currently being discussed on the mailing list. By chance I stumbled over the thread: http://public-inbox.org/git/bbcb9a3f-c338-50ea-1d63-1a6939ecd735@web.de/T/#mece8ba548282aec843bb9312230fa22220b851d7 – the sender looks familiar :) – knittl Sep 28 '22 at 18:12