Git alias with positional parameters
Asked Answered
G

7

350

Basically I'm trying to alias:

git files 9fa3

...to execute the command:

git diff --name-status 9fa3^ 9fa3

but git doesn't appear to pass positional parameters to the alias command. I have tried:

[alias]
    files = "!git diff --name-status $1^ $1"
    files = "!git diff --name-status {1}^ {1}"

...and a few others but those didn't work.

The degenerate case would be:

$ git echo_reverse_these_params a b c d e
e d c b a

...how can I make this work?

Griqua answered 23/7, 2010 at 19:3 Comment(5)
Note that in git 1.8.2.1 it's possible to do that without shell function (your original approach with $1 should work).Causation
@Causation Would you care to elaborate in an an answer? It doesn't work for me, and I can't find any documentation about it.Giess
@Causation there's nothing about this in the release notes though.Denticulation
i can confirm i can run shell commands with arguments without any shenanigans in Git 2.11.K2
@Causation Can you maybe create an answer with a detailed explanation?Kilgore
L
469

A shell function could help on this:

[alias]
    files = "!f() { git diff --name-status \"$1^\" \"$1\"; }; f"

An alias without ! is treated as a Git command; e.g. commit-all = commit -a.

With the !, it's run as its own command in the shell, letting you use stronger magic like this.

UPD
Because commands are executed at the root of repository you may use ${GIT_PREFIX} variable when referring to the file names in commands

Lou answered 23/7, 2010 at 21:13 Comment(14)
Thanks, this looks exactly right: [alias] files = "!f() { echo $3 $2 $1; }; f" ; $ git files a b c => c b aGriqua
@jefromi @mipadi I'm not a hotshot shell script writer; can you elaborate on why the ! is needed at beginning of the function definition? Thanks!Proteinase
@KohányiRóbert: That's actually not a shell script question; that's a particular of git config. An alias without ! is treated as a Git command; e.g. commit-all = commit -a. With the !, it's run as its own command in the shell, letting you use stronger magic like this.Lou
@Jefromi Ah! That's really good to know! I've stumbled upon this problem, where creating a new git alias for an already defined alias (like sta = status and st = sta) would fail with some error message like: "xy isn't a git command". Thanks for the info!Proteinase
HA. I came here trying to accomplish your "files" function. Wish I could up vote twice since you answered my initial question as well as the one I didn't even ask...Ichthyic
Be careful, ! will run at the root of the repository, so using relative paths when calling your alias will not give the results you might expect.Ripping
This solution breaks tab completion for branch names.Morea
@RobertDailey It doesn't break it, it just doesn't implement it. See stackoverflow.com/questions/342969/… for how to add it.Lou
Note: This doesn't quote arguments (which is dangerous in general). Also, a function is unnecessary. See my answer for more explanation.Borate
With no arguments this borks fatal: bad revision '^'. What you would expect it do is a matter of taste. Arguably, you'd want it run on HEAD. This would achieve that: files = "!f() { git diff --name-status \"${1:-HEAD}\"^ \"${1:-HEAD}\"; }; f". Alternatively, and possibly saner, you want to run it against the working tree: files = "!f() { [ -n \"$1\" ] && git diff --name-status \"$1\"^ \"$1\" || git diff --name-status; }; f".Bamberg
Why do you wrap it in a function f ?Korten
How could I run another command if there are no positional arguments?Eskridge
@Lou Then why is this not working br-f = "!f(){ git branch -a | grep -i \"$1*\" | grep -o '[^/]*$'; }; f;" ?? This when fired like git br-f qa-d throws the error: qa-d: command not foundFlaring
This only works for me, if I pass the input paramter on, into the function, i.e ($1 at end important!): f() { echo \"$1\"; ....}; f $1Halpern
B
150

The alias you are looking for is:

files = "!git diff --name-status \"$1\"^ \"$1\" #"

With argument validation:

files = "!cd -- \"${GIT_PREFIX:-.}\" && [ x$# != x1 ] && echo commit-ish required >&2 || git diff --name-status \"$1\"^ \"$1\" #"

The final # is important - it prevents all the user-supplied arguments from being processed by the shell (it comments them out).

Note: git puts all user-supplied arguments at the end of the command line. To see this in action, try: GIT_TRACE=2 git files a b c d

The escaped (due to nesting) quotes are important for filenames containing spaces or "; rm -rf --no-preserve-root /;)

Borate answered 16/9, 2016 at 3:54 Comment(11)
For the simplest cases this is the right answer, there's really no need to complicate by wrapping it in a function or sh -c.Dimitry
Yes, ! already implies sh -c (shown when prepending GIT_TRACE=2), so there's no need to run a another sub-shell. What issues do you see in more complicated cases?Borate
Does this work if you want to set default arguments? e.g. I want to do this to fetch a Github PR: fp = "! 1=${1:-$(git headBranch)}; 2=${2:-up}; git fetch -fu $2 pull/$1/head:$1; git checkout $1; git branch -u $2 #". This works great without the first two statements, but falls down if you use them. (I have headBranch = symbolic-ref --short HEAD as well).Teplitz
Worked it out, it works if you set new params, so this is fine: fp = "! a=${1:-$(git headBranch)}; b=${2:-up}; git fetch -fu $b pull/$a/head:$a; git checkout $a; git branch -u $b #".Teplitz
why " quotes are required?Amphitryon
@EugenKonkov Quotes are needed since the expansion of variables could contain spaces, and we want to keep them as a single shell token.Borate
This is amazing, thank you Tom! I just used this to define my on git cat command, see https://mcmap.net/q/12430/-view-a-file-in-a-different-git-branch-without-changing-branchesWoodham
@Jim are you sure you've only got one bang character?Borate
Why is the extra -- included after the cd? It seems to work fine without it. The Bash man page for cd says nothing about -- that I can see.Infra
For Windows, I had to use the ugly contraption && echo instead of #. This would echo the arguments after the command is executed, but using REM to simply ignore them didn't work at all!Hungary
Had a really hard time creating my own ! git alias (which I knew from before), but was hitting an issue which seemed that parameters were being fed at the end of my command too. I even tried placing a # at the end, to no avail. Turns out the missing piece was quoting the whole command, including the hash :) Weird that's needed, although it kinda makes sense - git is weird in weird ways, right? lolAlpenhorn
T
108

You can also reference sh directly (instead of creating a function):

[alias]
        files = !sh -c 'git diff --name-status $1^ $1' -

(Note the dash at the end of the line -- you'll need that.)

Torch answered 24/7, 2010 at 21:22 Comment(11)
...are there any tangible benefits as compared to jefromi's answer? I mean: the function def in his answer is "alias-local" and means you don't have bash calling bash, right? In any case thanks for the alternative implementation.Griqua
I don't think either solution really has benefits over the other -- just two different ways to do the same thing.Torch
I suspect jefromi's solution is better if you want to share the aliases, or use multiple shells; what if sh doesn't call your current shell?Vulnerary
If you're sharing the command, you probably want to use sh, since that is in itself a shell, and it's available on the vast majority of systems. Using the default shell only works if the command works as written for all shells.Markswoman
I prefer -- to - as it's more familiar and less likely to accidentally mean stdin at some point. ("An argument of - is equivalent to --" in bash(1) is ungoogleable)Micronucleus
See also Official Git Wiki - Advanced aliases with arguments.Accordance
I've tried both - and -- and gotten the two-line error sh: -c: line 0: unexpected EOF while looking for matching '' sh: -c: line 1: syntax error: unexpected end of file with bash 4.2.1 on cygwin.Motherless
What is the exact meaning of the ending '-' and where is it documented ?Zuber
Note: This doesn't quote arguments (which is dangerous in general). Creating a sub-shell (with sh -c) is also unnecessary. See my answer for an alternative.Borate
Note that this creates an extra copy of the shell (so two in total); additionally, FreeBSD’s (and their derivatives’) sh has broken -- handling (and - is plain wrong) for compatibility reasons. I’d agree your best bet is to write the alias in the form from the accepted answer and assume the user’s shell is POSIX sh compatible enough.Gasper
@Zuber it appears to me to be only documented in the wiki entry linked in the comment just above yours, not in the alias or syntax sections of git help config This might fit with the idea I've seen in the mailing list that shell quirks are considered out of scope for git documentation. (I'd disagree, myself, when it affects git usage like this, but I could see arguments to avoid overdoing it.)Semiquaver
M
35

Use GIT_TRACE=1 described on the git man page to make the alias processing transparent:

$ git config alias.files
!git diff --name-status $1^ $1
$ GIT_TRACE=1 git files 1d49ec0
trace: exec: 'git-files' '1d49ec0'
trace: run_command: 'git-files' '1d49ec0'
trace: run_command: 'git diff --name-status $1^ $1' '1d49ec0'
trace: exec: '/bin/sh' '-c' 'git diff --name-status $1^ $1 "$@"' 'git diff --name-status $1^ $1' '1d49ec0'
trace: built-in: git 'diff' '--name-status' '1d49ec0^' '1d49ec0' '1d49ec0'
trace: run_command: 'less -R'
trace: exec: '/bin/sh' '-c' 'less -R' 'less -R'
MM      TODO

Your original commands work with git version 1.8.3.4 (Eimantas noted this changed in 1.8.2.1).

The sh -c '..' -- and f() {..}; f options both cleanly handle the "$@" parameters in different ways (see with GIT_TRACE). Appending "#" to an alias would also allow positional parameters without leaving the trailing ones.

Micronucleus answered 26/8, 2013 at 1:27 Comment(1)
thanks for the explanations: those commands work for me on the original problem, following your advice: files = "!git diff --name-status $1^ $1 #" files = "!git diff --name-status $1^"Esteresterase
R
21

As stated by Drealmer above:

« Be careful, ! will run at the root of the repository, so using relative paths when calling your alias will not give the results you might expect. – Drealmer Aug 8 '13 at 16:28 »

GIT_PREFIX being set by git to the subdirectory you're in, you can circumvent this by first changing the directory :

git config --global alias.ls '!cd "${GIT_PREFIX:-.}"; ls -al'

Retina answered 26/2, 2014 at 10:47 Comment(5)
I'm having trouble with this as well (commands being run at the root of the repository) but this solution doesn't seem to do anything. (If it matters, I'm using OS X.)Brilliantine
Oops... git alias is an alias I made.Retina
(since git 1.8.2) git config --set alias.alias = '! git config --global alias.$1 "$2"'Retina
This is what ended up working for me: "prefix your git aliases (that run shell commands and need the right pwd) with cd ${GIT_PREFIX:-.} &&." (source: https://mcmap.net/q/12431/-why-does-running-command-as-git-alias-gives-different-results)Brilliantine
Do quote this. !cd "${GIT_PREFIX:-.}" && ls -alGasper
R
11

I wanted to do this with an alias that does this:

git checkout $1;
git merge --ff-only $2;
git branch -d $2;

In the end, I created a shell script named git-m that has this content:

#!/bin/bash -x
set -e

#by naming this git-m and putting it in your PATH, git will be able to run it when you type "git m ..."

if [ "$#" -ne 2 ]
then
  echo "Wrong number of arguments. Should be 2, was $#";
  exit 1;
fi

git checkout $1;
git merge --ff-only $2;
git branch -d $2;

This has the benefit that it's much more legible because it's on multiple lines. Plus I like being able to call bash with -x and set -e. You can probably do this whole thing as an alias, but it would be super ugly and difficult to maintain.

Because the file is named git-m you can run it like this: git m foo bar

Rundle answered 21/6, 2014 at 3:27 Comment(3)
I like this a lot more too, but I haven't been able to figure out how to use the autocomplete I want with this approach. On aliases you can do this: '!f() { : git branch ; ... }; f' and it will autocomplete the alias as a branch which is super handy.Neuralgia
Yeah, I think I prefer having the non-trivial things done as individual script files on the path. The down side though is yes, you loose automatic completion of things like references. You can though fix this up by manually configuring your own auto-completion. Again though, I like that you can just drop a script into a folder on the path and it will start working, but for the auto-completion, you need to 'load' it, so usually it's in my .bashrc file that I source. But I don't think I change how I auto-complete arguments to a script as much as the script itself, and it'd only be during dev.Flowerless
GOSH, YES. I was killing myself over trying to use a shell variable on the ! inline git alias.......Alpenhorn
D
7

Just bumped into something similar; hope it's oK to post my notes. One thing that confuses me about git aliases with arguments, probably comes from the git help config (I have git version 1.7.9.5):

If the alias expansion is prefixed with an exclamation point, it will be treated as a shell command. For example, defining "alias.new = !gitk --all --not ORIG_HEAD", the invocation "git new" is equivalent to running the shell command "gitk --all --not ORIG_HEAD". Note that shell commands will be executed from the top-level directory of a repository, which may not necessarily be the current directory. [...]

The way I see it - if an alias "will be treated as a shell command" when prefixed with an exclamation point - why would I need to use a function, or sh -c with arguments; why not just write my command as-is?

I still don't know the answer - but I think actually there is a slight difference in the outcome. Here's a little test - throw this in your .git/config or your ~/.gitconfig:

[alias]
  # ...
  ech = "! echo rem: "
  shech = "! sh -c 'echo rem:' "
  fech = "! f() { echo rem: ; }; f " # must have ; after echo!
  echargs = "! echo 0[[\"$0\"]] 1-\"$1\"/ A-"$@"/ "
  fechargs = "! f() { echo 0[[\"$0\"]] 1-\"$1\"/ A-"$@"/ ; }; f "

Here is what I get running these aliases:

$ git ech word1 word2
rem: word1 word2

$ git shech word1 word2
rem:

$ git fech word1 word2
rem:

$ git echargs word1 word2
0[[ echo 0[["$0"]] 1-"$1"/ A-$@/ ]] 1-word1/ A-word1 word2/ word1 word2

$ git fechargs word1 word2
0[[ f() { echo 0[["$0"]] 1-"$1"/ A-$@/ ; }; f ]] 1-word1/ A-word1 word2/

... or: when you're using a "plain" command after the ! "as-is" in a git alias - then git automatically appends the arguments list to that command! A way to avoid it, is indeed, to call your script as either a function - or as the argument to sh -c.

Another interesting thing here (for me), is that in a shell script, one typically expects the automatic variable $0 to be the filename of the script. But for a git alias function, the $0 argument is, basically, the content of the entire string specifying that command (as entered in the config file).

Which is why, I guess, if you happen to misquote - in the below case, that would be escaping the outer double quotes:

[alias]
  # ...
  fail = ! \"echo 'A' 'B'\"

... - then git would fail with (for me, at least) somewhat cryptic message:

$ git fail
 "echo 'A' 'B'": 1: echo 'A' 'B': not found
fatal: While expanding alias 'fail': ' "echo 'A' 'B'"': No such file or directory

I think, since git "saw" a whole string as only one argument to ! - it tried to run it as an executable file; and correspondingly it failed finding "echo 'A' 'B'" as a file.

In any case, in the context of the git help config quote above, I'd speculate that it's more accurate to state something like: " ... the invocation "git new" is equivalent to running the shell command "gitk --all --not ORIG_HEAD $@", where $@ are the arguments passed to the git command alias from the command line at runtime. ... ". I think that would also explain, why the "direct" approach in OP doesn't work with positional parameters.

Devilmaycare answered 22/4, 2013 at 20:10 Comment(2)
nice test. A quick way to check all possibilities!Aspirator
fail is trying to run a command called "echo 'A' 'B" (ie. 10 chars long). Same error from sh -c "'echo a b'" and same cause, too many layers of quotesMicronucleus

© 2022 - 2024 — McMap. All rights reserved.