是否可以通过Git别名覆盖Git命令?

66

我的~/.gitconfig文件内容如下:

[alias]
        commit = "!sh commit.sh"

然而,当我输入git commit时,脚本并没有被调用。

这是否可能实现,或者我必须使用另一个别名?


2
mathepic的回答完全正确。当然,我认为这有点无意义。只要你在使用别名,为什么不缩短命令呢?将co别名为!sh checkout.sh,这样你就不必输入所有内容(甚至不用按Tab键自动补全)。 - Cascabel
这真是不幸。我也希望有这个功能,以覆盖“git log”的默认行为,并使用单行格式。我知道你可以使用其他别名,但只要默认的一个存在,习惯使你使用它,从而永远不会学习替代命令 :( - Sridhar Sarnobat
6个回答

56

这是不可能的

这来自于我的git.git克隆:

static int run_argv(int *argcp, const char ***argv)
{
    int done_alias = 0;

    while (1) {
        /* See if it's an internal command */
        handle_internal_command(*argcp, *argv);

        /* .. then try the external ones */
        execv_dashed_external(*argv);

        /* It could be an alias -- this works around the insanity
         * of overriding "git log" with "git show" by having
         * alias.log = show
         */
        if (done_alias || !handle_alias(argcp, argv))
            break;
        done_alias = 1;
    }

    return done_alias;
}

所以这是不可能的。(如果找到命令,handle_internal_command会调用exit)。
你可以通过改变行的顺序并让handle_alias在找到别名时调用exit来修复这个问题。

18
如果尝试创建与内部/外部命令相同的别名,git config 应该发出某种警告...具有讽刺意味的是,我找不到报告 git-scm 问题的地方。谷歌和 Stack Overflow 的结果似乎都被用于管理 Git 的问题工具所淹没。 - Daniel Hershcovich
这需要更改git源代码。我可以用我的自定义版本替换libexec/git-core/git-commit - linquize
@DanielHershcovich同意;我真的很想提交到git(双关语意味深长:p),但他们的开发流程似乎有些混乱。 - DylanYoung
我同意应该发出警告。看起来他们的邮件列表是报告错误的正确地方。我假设这也适用于增强功能,因为他们没有说任何相反的话。不过,您无需订阅该列表即可发送邮件。 - Ian Dunn

44

我决定使用一个bash函数来解决这个问题。如果我调用git clone,它会重定向到git cl,这是我的别名,并带有一些额外的开关。

function git {
  if [[ "$1" == "clone" && "$@" != *"--help"* ]]; then
    shift 1
    command git cl "$@"
  else
    command git "$@"
  fi
}

如果您已经有了这个Bash函数,那么为什么还需要一个cl别名呢? - Gregory Pakosz
@GregoryPakosz 这并不是严格必要的,但在创建bash函数之前我使用了别名。您可以直接将所需参数放入函数中,唯一需要记住的是,如果需要更新参数,则需要重新加载您的shell。 - stefansundin
@stefansundin 嗯...为什么?一个人需要重新加载shell来更新参数吗?你是说它们会在调用之间保持不变吗?还是你只是指出,如果有人希望稍后修改方法(函数)签名(函数接受的参数)/行为,以响应提供给定参数的函数,他们必须重新获取脚本? - NerdyDeeds
1
@NerdyDeeds 是的。 - stefansundin

29

如前所述,无法使用git别名来覆盖git命令。但是,可以使用shell别名覆盖git命令。对于任何POSIXy shell(即不是MS cmd),编写一个简单的可执行脚本,执行所需的修改行为并设置一个shell别名。在我的.bashrc(Linux)和.bash_profile(Mac)中,我有:

export PATH="~/bin:$PATH"
...
alias git='my-git'

在我的 ~/bin 文件夹中,我有一个可执行的Perl脚本,名为 my-git,它检查第一个参数(即git命令)是否为clone。它的大致代码如下:

#!/usr/bin/env perl
use strict;
use warnings;
my $path_to_git = '/usr/local/bin/git';
exit(system($path_to_git, @ARGV))
    if @ARGV < 2 or $ARGV[0] ne 'clone';
# Override git-clone here...

我的功能略微可配置,但你能理解大概的意思。


8
这个答案可能应该被接受。对于许多用户来说,用一个被黑客攻击过的版本替换git并不是一种可行的解决方案。 - tripleee
2
作为别名的替代方案,您可以在PATH早期的目录中将git符号链接到您的包装脚本。显然要确保脚本不会执行自己而不是真正的git - tripleee
2
提升这个答案。人们很可能正在尝试创建别名。 - Harrison Cramer
@tripleee 这 用一个黑客版本替换git,哈哈。这只是一个关于你要用哪种语言进行黑客攻击的问题,但很明显许多人比较喜欢BASH而不是C语言(可能是因为他们害怕编译)。 - DylanYoung
2
在某种程度上这是正确的,但是拥有一个与上游代码不同的 git 可能会变得非常棘手或者根本无法维护,而一个简单的包装器可以让你方便地从上游拉取更新,通常是相当健壮的,并避免了分支的固有问题。 - tripleee

22

不仅不可能,而且不修复

在2009年,http://git.661346.n2.nabble.com/allowing-aliases-to-override-builtins-to-support-default-options-td2438491.html

Hamano 回复::

目前 git 不允许别名覆盖内置命令。我理解这背后的原因,但我想知道是否太过保守。

并不是。

大多数 shell 支持使用别名覆盖命令,我不确定为什么 git 需要比 shell 更加保守。

因为正常的 shell 在脚本中使用时不会扩展别名,并且还提供了一种方便的方法来从命令行打败别名。

$ alias ls='ls -aF'
$ echo ls >script
$ chmod +x script

并进行比较:

$ ./script
$ ls
$ /bin/ls

2
抱歉打扰了,但我注意到Mamano对shell行为的感知可能存在潜在问题。别名可以通过几种方式被击败。执行以下操作: $ alias ls='ls -aF'然后比较这些:$ ls $ \ls $ /bin/ls $ builtin ls第一个调用别名,指向shell内置的ls或外部二进制文件。 第二个绕过别名,直接调用shell内置的ls或外部二进制文件。 第三个通过明确调用外部二进制文件/bin/ls来绕过别名。 第四个,假设有一个内置的ls,则强制调用该内置命令。 - masta
2
@masta 兄弟,哇!你不知道我多久想知道这个精确的信息了 - 或者是否可能。我真的是偶然发现了你在这里的答案,并且已经在脑海中更新了15或20个脚本,如果我已经掌握了这些知识,它们会写得更好。谢谢!我是认真的:谢谢!如果你有机会来堪萨斯城,我欠你一杯啤酒。 - NerdyDeeds

3

顺便说一下,我通过编写以下~/bin/git包装器来解决这个问题(好吧,“绕过它”...),该包装器检查例如~/bin/git-clone并调用那个而不是内置的。

[注意:对于任何“聪明”的bash主义,我表示歉意,但在你经过两个辅助函数(一个用于扩展符号链接,一个用于搜索被包装的可执行文件的$PATH)之后,实际脚本本身只有三行代码...所以我想我毕竟不需要道歉,呵呵!]

#!/usr/bin/env bash

###########################
###  UTILITY FUNCTIONS  ###  ...from my .bashrc
###########################
#
# deref "/path/with/links/to/symlink"
#   - Returns physical path for specified target
#
# __SUPER__
#   - Returns next "$0" in $PATH (that isn't me, or a symlink to me...)

deref() {
  ( # Wrap 'cd's in a sub-shell
    local target="$1"
    local counter=0

    # If the argument itself is a link [to a link, to a link...]
    # NOTE: readlink(1) is not defined by POSIX, but has been shown to
    #  work on at least MacOS X, CentOS, Ubuntu, openSUSE, and OpenBSD
    while [[ -L "$target" ]]; do
        [[ $((++counter)) -ge 30 ]] && return 1
        cd "${target%/*}"; target="$(readlink "$target")"
    done

    # Expand parent directory hierarchy
    cd "${target%/*}" 2>/dev/null \
      && echo "$(pwd -P)/${target##*/}" \
      || echo "$([[ $target != /* ]] && echo "$(pwd -P)/")$target"
  )
}

__SUPER__() {
  local cmd="${1:-${0##*/}}"
  local me="$(deref "$0")"

  # NOTE: We only consider symlinks...  We could check for hardlinks by
  #       comparing device+inode, but stat(1) has portability problems

  local IFS=":"
  for d in $PATH; do
    [[ -x "$d/$cmd" ]] && [[ "$(deref "$d/$cmd")" != "$me" ]] \
      && { echo "$d/$cmd"; return; }
  done

  # else...
  return 1
}

########################################################################

# (1) First, figure out which '$0' we *WOULD* have run...

GIT="$(__SUPER__)" || { echo "${0##*/}: command not found" >&2; exit 1; }

# (2) If we have a "~/bin/git-${command}" wrapper, then
#     prepend '.../libexec/git-core' to $PATH and run it

[[ -f "${HOME}/bin/git-$1" ]] &&
  PATH="$PATH:$( "$GIT" --exec-path )" \
    exec "${HOME}/bin/git-$1" "${@:2}"

# (3) Else fall back to the regular 'git'

exec "$GIT" "$@"

1
这是另一种解决方法,它不依赖于实际的覆盖,而是利用了自动完成的漏洞。这比包装git更安全和透明。
由于我从来没打过完整的命令名称,只需定义一个命令前缀的别名就足以覆盖命令。
例如,要使用别名覆盖“git show-branch”,并且知道我通常输入“git show-”,则可以将“show-br”别名定义为自定义“show-branch”行为。
对于 OP 的示例“git commit”,我通常键入“git com”,因此“git comm”是正确的解决方案。
这种策略有以下几个优点:
- 明确表明你没有调用“原始”命令。 - 如果你的前缀足够长: - 它不会修改你的工作流程 - 明确表明你正在替换哪个命令 - 原始命令仍然很容易访问

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接