git diff normally will not generate an output, if the two commits it compares are identical, to make it generate explicitly "the two commits are the same", I found the bash script below.
function git_diff() {
# case 1: command line too few parameters
if [ $# -lt 2 ]; then
echo "Usage: git_diff <commit1> <commit2> [git-diff options...]" >&2
return 1
fi
if output=$(git diff "$@" 2>&1); then
if [ -z "$output" ]; then
# case 2: two commits are the same
echo "These two commits $@ are the same."
else
# case 3: two commits are different
echo "Git command output: $output"
fi
else
# case 4: Git command failed (e.g., git diff commit1 abcd, where abcd is ambiguous)
echo "Git command failed: $output" >&2
return 1
fi
}
However, I'm confused here: how could the script support case 2?
In the case of if output=$(git diff commit1 commit1), the git diff commit1 commit1 command succeeded, the exit code shall be 0 means "everything OK", just output becomes empty, how could if output=$(git diff commit1 commit1 2>&1); enter into then, not else?
2 Answers 2
I think the main misunderstanding here is about what the if cmd; then ...; fi syntax means.
How if works
The if condition is not, like in many language, an expression, whose value trigger or not the then or else part of the if.
It is a command. Whole success or failure (exit code) triggers or not then and else part.
if ls
then
echo it works
else
echo "whaaaaat?"
fi
prints "it works". Because ls command was successful. Its exit code was 0.
And, by side-effect, it also, of course, before printing "it works", lists the files in current directory
if ls streinarioar
then
echo "omg, do you really have a streinarioar file or dir?"
else
echo "not this time"
fi
prints "not this time" (unless my choice of filename example was really unlucky)
Because ls streinarioar failed.
As you can see using $?
ls
echo $? # 0
ls streinarioar
echo $? # non-0 (2 on my system)
How if [ ... ] works
Now, what is very convenient, but also increases confusion (by comparison to other languages) is the fact that you can
if [ 1 = 2 ]
then
echo "wow, that is a surprise"
else
echo "1 is not 2 after all"
fi
Which seems a more traditional if (like we see in most language), with a boolean expression as a condition. But in reality, it is not that. It is still the if cmd; then ...; else ...; fi command.
Just, cmd happens to be [ 1 = 2 ]
[ is a program (in reality, it is likely an internal command, but it doesn't matter here)
ls -l /usr/bin/[
# -rwxr-xr-x 1 root root 55744 Apr 5 2024 '/usr/bin/['
# see, just an exec, like /usr/bin/ls is
And that what that program does is testing expression (once upon a time, /usr/bin/[ was just a link to another classical program of unix system, /usr/bin/test, that is roughly the same), do nothing else with passed argument than fail or succeed (it prints nothing. It just fails or not)
[ 1 = 2 ]
echo $?
# 1
[ 2 = 2 ]
echo $?
# 0
git diff
git diff succeeds (exit code 0) if it could evaluate the required diff. And fails other wise.
So, regardless of whether there is a difference or not, if there was no problem with the arguments, it succeeds
On a project of mine
git diff 7d521b15 37fed820 static/interpret.js
# Displays
#--- a/static/interpret.js
#+++ b/static/interpret.js
#@@ -108,60 +108,61 @@ onmessage = function (evt){
# let s=g.sommets[evt.data.clickNode];
#...
echo $?
0
Display a diff, and has an exit code 0, because it successfully computed the diff (which wasn't empty)
git diff 7d521b15 37fed820 static/cloud.js
echo $?
0
Displays nothing (because there was not difference, file cloud.js was untouched). And succeed (return exit code 0) because it successfully computed the diff (which here was empty)
git diff 7d521b15 37fed820 static/cloud.php
# fatal: ambiguous argument 'static/cloud.php': unknown revision or path not in the working tree
echo $?
128
Fails (non-0 exitcode). Because there is no php files in my project (hell no). So it could not compute the difference
result=$(git diff ...)
The only difference between result=$(git diff) command and git diff command, is that it prints nothing, and the output (the standard one) is stored into a variable. But success or failure of the command is the same. So
result=$(git diff 7d521b15 37fed820 static/interpret.js), result=$(git diff 7d521b15 37fed820 static/cloud.js) and result=$(git diff 7d521b15 37fed820 static/cloud.php) would respectively succeeds and store something in result, succeeds and store an empty string in result and fails (exitcode 0, 0 and 128, respectively).
So
if result=$(git diff ...)
then
echo "ok $result"
else
echo "bad diff"
fi
Would print ok and the diff string (whether empty or not) if the diff was successful.
And bad diff is it wasn't (non-existing file, for example)
Note a difference with diff
It is important to understand that there is no general rule about what makes a success and a failure. It is up to each command to decide what constitute a success for them (so what exitcode they return).
Compare for example the 3 cases we saw with git diff with the same 3 cases with plain unix command diff
diff static/cloud.js static/cloud.js
# prints nothing because file are identical
echo $?
0 # success
diff static/cloud.js static/interpret.js
# prints many > and < lines, because files are different
echo $?
1 # failure : files both exist, but not identical
diff static/cloud.js static/cloud.php
# prints an error message, because there is no cloud.php
echo $?
2 # another kind of failure : one of the files do not exist
So, you see, unix command diff acts different than git diff. It returns a non-0 exit code even for perfectly legit files, if they are not identical.
If your bash script were comparing files with diff, you would be right to wonder how it could be possible to be in case 2, since as long as there is a single difference (so a single byte in diff output) you are in case 4 anyway.
(in such a script, you would have either to analyse the content of result (which would be empty in case 4, because there is not diff, and the error message is printed on stderr not stdout) in the else of the first if. Or to rely on exit code, and differenciate between exit code 1 and 2 among failures.
So, point is, it is a case by case thing. You have to know when a command is supposed to fail or succeed before using it in a if. In the case of a git diff it fails only in your case 4. For case 2 and 3, it succeeds, with or without printing things.
1 Comment
Every program has standard streams — stdin, stdout, stderr. But also every program when finished returns to caller an exit code. Usually exit code 0 means "everything Ok" and non-zero exit code means "there was an error, see stderr for details".
The script checks exit code in if. If git diff failed and returned non-zero exit code if immediately jumps to code inside else.
BTW, if [ -z "$output" ] works the same way. [ is a program (/bin/test linked to /bin/[, check with ls -l "/bin/["); it parses its arguments (-z, content of "$output" passed by shell, ]), interprets the test and returns an exit code; 0 in case if the output is empty, non-0 if not empty. if just checks the exit code, no magic, and jumps to the code under then or else.
4 Comments
if output=$(git diff commit1 commit1), the git diff commit1 commit1 command succeeded, the exit code shall be 0 means "everything OK", just output becomes empty, how could if output=$(git diff commit1 commit1 2>&1); enter into then, not else?if works. On exit code 0 it jumps to then. On non-0 exit code it jumps to else.
git diff(besides printing its output) also returns an exit code, indicating if there was an error (which you could check manually withgit ...; echo $?), and the surroundingifchoses the path to proceed upon this exit code (0is true, everything else is false).2>&1just captures everything in the command substitution which ultimately saved via the variableoutputit does not change the exit/return status.