Finding Uncommitted Changes with PowerShell and fd
Please note the improved techniques added at the end.
When I switched my Emacs configuration to straight.el, I needed to find all the immediate children of a particular directory that had uncommitted Git changes. I spent 10 minutes thinking about the best way to do this, which seemed smarter than spending a couple of minutes doing it manually. I considered trying to do it in Emacs with Dired, but I’ve been attempting to translate my CLI knowledge to PowerShell in recent months and thought I might be able to do it with the ever-useful fd. I even knew how to tackle the quoting issues I’d had in the past. The final invocation was:
PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '||' echo 'CHANGED: {}' ';'
That’s right: since cd is a builtin, not an executable, I had to use cmd to run it through fd. The output looked like this:
fountain-mode.el "CHANGED: fountain-mode.git" livescript-mode.el "CHANGED: livescript-mode" separedit.el "CHANGED: separedit.el" .gitignore .travis.yml Cask README.rst features/highlight.feature features/imenu.feature features/indent.feature features/step-definitions/jsonconfig-mode-steps.el features/support/env.el jsonconfig-config.el jsonconfig-spec.el "CHANGED: jsonconfig-mode" ace-jump-mode.el "CHANGED: ace-jump-mode" c++/LICENSE.txt c++/ekam-provider/c++header c++/ekam-provider/canonical "CHANGED: capnproto"
Inelegant, but it worked. I didn’t know how to make git diff-index suppress its output entirely
(that would be --quiet, as it turns out) and didn’t
mind looking for the CHANGED
lines. In retrospect, I could have filtered the result with
the (also amazing) ripgrep command, like so:
PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '||' echo '!!!CHANGED!!!: {}' ';' | rg -F '!!!CHANGED!!!'
But it would have been even better to redirect the Git output, which I can confirm works:
PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '>' 'NUL' '||' echo 'CHANGED:' '{}' ';'
I’ll remember that for next time. I would like to do it without cmd, but git -C still has quoting issues:
> fd -t d --maxdepth 1 -x git -C "$(pwd)/{}" diff-index --exit-code --name-only HEAD . '>' NUL '||' echo 'CHANGED:' '{}' ';' fatal: >: no such path in the working tree. Use 'git <command> -- <path>...' to specify paths that do not exist locally. fatal: >: no such path in the working tree. Use 'git <command> -- <path>...' to specify paths that do not exist locally. fatal: >: no such path in the working tree. Use 'git <command> -- <path>...' to specify paths that do not exist locally. fatal: >: no such path in the working tree. Use 'git <command> -- <path>...' to specify paths that do not exist locally.
Funnily enough, all the changed directories revealed themselves to be false positives—mostly whitespace issues, for some reason, which I ought to have told Git to ignore—so I was able to safely proceed.
Addendum: The right way
I was wrong. The quoting is correct. The real problem is that whereas cmd is a shell and therefore
treats >
and ||
are specially, fd simply includes those characters as arguments, so Git is
passed the literals >
and ||
. I don’t think it’s possible to sequence commands like I’m trying
to do without some sort of shell. Using PowerShell works:
PowerShellfd -t d --maxdepth 1 -x pwsh -c '& {Set-Location {} && git diff-index --exit-code --name-only --quiet HEAD .; if ($LASTEXITCODE -ne 0) { echo {} }}' ';'
(&&
and ||
don’t treat an exit code of 128 as false, for some reason, so I explicitly test
$LASTEXITCODE
.)
However, the best solution I’ve found dispenses with cmd and fd entirely:
PowerShellGet-ChildItem -Directory -Name | ForEach-Object { git -C $_ diff-index -b --exit-code --name-only --quiet HEAD .; if ($LASTEXITCODE -ne 0) { echo $_ } }
You can even omit -Name to get proper file objects if you need to use more than just the names in your pipeline.