58

以下 bash 脚本在扫描 .git 目录时速度很慢,因为它会查看每个目录。如果我有一个大型存储库的集合,find 需要很长时间才能遍历每个目录,寻找 .git。一旦找到 .git 目录,如果它会修剪 repos 中的目录,它会更快。关于如何做到这一点的任何想法,或者是否有另一种方法来编写完成同样事情的 bash 脚本?

#!/bin/bash

# Update all git directories below current directory or specified directory

HIGHLIGHT="\e[01;34m"
NORMAL='\e[00m'

DIR=.
if [ "$1" != "" ]; then DIR=$1; fi
cd $DIR>/dev/null; echo -e "${HIGHLIGHT}Scanning ${PWD}${NORMAL}"; cd ->/dev/null

for d in `find . -name .git -type d`; do
  cd $d/.. > /dev/null
  echo -e "\n${HIGHLIGHT}Updating `pwd`$NORMAL"
  git pull
  cd - > /dev/null
done

具体来说,您将如何使用这些选项?对于这个问题,不能假设repos的集合都在同一个目录下;它们可能位于嵌套目录中。

top
  repo1
  dirA

  dirB
     dirC
        repo1
4

8 回答 8

54

查看丹尼斯在这篇文章中关于 find 的 -prune 选项的回答:

如何在sh中使用'find'的'-prune'选项?

find . -name .git -type d -prune

会加快速度,因为 find 不会进入 .git 目录,但它仍然会进入 git 存储库,寻找其他 .git 文件夹。这“可能”是一项昂贵的操作。

如果有某种查找前瞻修剪机制会很酷,如果一个文件夹有一个名为 .git 的子文件夹,那么在那个文件夹上修剪......

也就是说,我打赌您的瓶颈在于网络操作“git pull”,而不是在 find 命令中,正如其他人在评论中发布的那样。

于 2012-08-17T02:11:22.860 回答
14

这是一个优化的解决方案:

#!/bin/bash
# Update all git directories below current directory or specified directory
# Skips directories that contain a file called .ignore

HIGHLIGHT="\e[01;34m"
NORMAL='\e[00m'

function update {
  local d="$1"
  if [ -d "$d" ]; then
    if [ -e "$d/.ignore" ]; then 
      echo -e "\n${HIGHLIGHT}Ignoring $d${NORMAL}"
    else
      cd $d > /dev/null
      if [ -d ".git" ]; then
        echo -e "\n${HIGHLIGHT}Updating `pwd`$NORMAL"
        git pull
      else
        scan *
      fi
      cd .. > /dev/null
    fi
  fi
  #echo "Exiting update: pwd=`pwd`"
}

function scan {
  #echo "`pwd`"
  for x in $*; do
    update "$x"
  done
}

if [ "$1" != "" ]; then cd $1 > /dev/null; fi
echo -e "${HIGHLIGHT}Scanning ${PWD}${NORMAL}"
scan *
于 2012-08-17T17:58:26.750 回答
12

我已经花时间将脚本复制粘贴到您的问题中,并将其与您自己的答案的脚本进行比较。这里有一些有趣的结果:

请注意:

  • git pull通过在它们前面加上前缀来禁用echo
  • 我也删除了颜色的东西
  • 我还删除.ignore了解决方案中的文件测试bash
  • > /dev/null并在这里和那里删除了不必要的东西。
  • 删除pwd了两者中的调用。
  • 添加了示例-prune中显然缺少的find
  • find使用“while”而不是“for”,这在示例中也适得其反
  • 相当多地解开了第二个例子以达到重点。
  • 添加了对bash解决方案的测试以不遵循符号链接以避免循环并充当查找解决方案。
  • 添加shopt以允许*扩展为虚线目录名称,以匹配find解决方案的功能。

因此,我们正在比较基于查找的解决方案

#!/bin/bash

find . -name .git -type d -prune | while read d; do
   cd $d/..
   echo "$PWD >" git pull
   cd $OLDPWD
done

使用bash shell 构建解决方案

#!/bin/bash

shopt -s dotglob

update() {
    for d in "$@"; do
        test -d "$d" -a \! -L "$d" || continue
        cd "$d"
        if [ -d ".git" ]; then
            echo "$PWD >" git pull
        else
            update *
        fi
        cd ..
    done
}

update *

注意:内置函数 (functionfor) 不受 MAX_ARGS OS 启动进程的限制。所以*不会在非常大的目录上收支平衡。

解决方案之间的技术差异:

基于查找的解决方案使用 C 函数来爬取存储库,它:

  • 必须为该find命令加载一个新进程。
  • 将避免“.git”内容,但会抓取 git 存储库的工作目录,并在其中丢失一些时间(并最终找到更多匹配的元素)。
  • 每次匹配都必须chdir通过几个深度的子目录并返回。
  • 必须chdir在 find 命令中执行一次,在 bash 部分中执行一次。

基于 bash 的解决方案使用内置(因此接近 C 实现,但已解释)来爬取存储库,请注意:

  • 将只使用一个进程。
  • 将避免 git workdir 子目录。
  • 一次只会执行chdir一个级别。
  • 只会执行chdir一次查找和执行命令。

解决方案之间的实际速度结果:

我有一个 git 存储库的工作开发集合,我在其上启动了脚本:

  • 找到解决方案:~0.080s(bash chdir 需要 ~0.010s)
  • bash 解决方案:~0.017s

我不得不承认,我没有准备好从 bash 内置函数中看到这样的胜利。在对发生的事情进行分析后,它变得更加明显和正常。雪上加霜,如果您将外壳从/bin/bashto更改/bin/sh(您必须注释掉该shopt行,并准备好它不会解析虚线目录),您将跌至 ~0.008s 。打败那个 !

请注意,您可以使用以下方法更聪明地使用 find 解决方案:

find . -type d \( -exec /usr/bin/test -d "{}/.git" -a "{}" != "." \; -print -prune \
       -o -name .git -prune \)

这将有效地删除在找到的 git 存储库中对所有子存储库的爬取,其代价是为每个爬取的目录生成一个进程。我得到的最终 find 解决方案大约是 ~0.030s,比之前的 find 版本快两倍多,但仍然比 bash 解决方案慢 2 倍。

请注意,/usr/bin/test避免搜索$PATH花费时间很重要,而且我需要-o -name .git -prune-a "{}" != "."因为我的主存储库本身就是一个 git 子存储库。

作为结论,我不会使用 bash 内置解决方案,因为它对我来说有太多的极端情况(而且我的第一次测试遇到了一个限制)。但对我来说重要的是要解释为什么在某些情况下它可以(快得多),但find解决方案对我来说似乎更加健壮和一致。

于 2014-04-28T14:42:02.093 回答
7

最重要的答案都依赖于找到一个“.git”存储库。然而,并不是所有的 git repos 都有这些(例如,bare repos)。以下命令将遍历所有目录并询问 git 是否将每个目录视为一个目录。如果是这样,它会从树上剪掉子目录并继续。

find . -type d -exec sh -c 'cd "{}"; git rev-parse --git-dir 2> /dev/null 1>&2' \; -prune -print

它比其他解决方案慢很多,因为它在每个目录中执行一个命令,但它不依赖于特定的存储库结构。例如,对于查找裸 git 存储库可能很有用。

于 2017-09-26T12:12:30.660 回答
3

对于 Windows,您可以将以下内容放入名为 gitlist.bat 的批处理文件中,并将其放在您的 PATH 中。

@echo off
if {%1}=={} goto :usage
for /r %1 /d %%I in (.) do echo %%I | find ".git\."
goto :eof
:usage
echo usage: gitlist ^<path^>
于 2015-12-18T18:53:29.420 回答
3

我使用以下命令列出当前目录中任何位置的所有 git 存储库:

find . -type d -execdir test -d {}/.git \\; -prune -print

这很快,因为一旦找到 git 存储库它就会停止递归。(虽然它不处理裸存储库。)当然,您可以将 更改为.您想要的任何目录。如果需要,您可以将 更改-print-print0空分隔值。

要同时忽略包含.ignore文件的目录:

find . -type d \( -execdir test -e {}/.ignore \; -prune \) -o \( -execdir test -d {}/.git \; -prune -print \)

我已将此别名添加到我的~/.gitconfig文件中:

[alias]
  repos =  !"find -type d -execdir test -d {}/.git \\; -prune -print"

然后我只需要执行:

git repos

获取当前目录中任何位置的所有 git 存储库的完整列表。

于 2020-04-13T16:04:54.993 回答
2

使用 locate 命令查看答案: 有没有办法在终端中列出 git 存储库?

使用 locate 而不是自定义脚本的优点是:

  1. 搜索已编入索引,因此可以扩展
  2. 它不需要使用(和维护)自定义 bash 脚本

使用定位的缺点是:

  1. locate 使用的数据库每周更新一次,因此新创建的 git 存储库不会出现

走定位路线,这里是如何列出一个目录下的所有 git 存储库,对于 OS X:

启用定位索引(在 Linux 上会有所不同):

sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.locate.plist

索引完成后运行此命令(可能需要对 Linux 进行一些调整):

repoBasePath=$HOME
locate '.git' | egrep '.git$' | egrep "^$repoBasePath" | xargs -I {} dirname "{}"
于 2013-01-05T00:27:14.903 回答
0

这个答案结合了@Greg Barrett 提供的部分答案和我上面的优化答案。

#!/bin/bash

# Update all git directories below current directory or specified directory
# Skips directories that contain a file called .ignore

HIGHLIGHT="\e[01;34m"
NORMAL='\e[00m'

export PATH=${PATH/':./:'/:}
export PATH=${PATH/':./bin:'/:}
#echo "$PATH"

DIRS="$( find "$@" -type d \( -execdir test -e {}/.ignore \; -prune \) -o \( -execdir test -d {}/.git \; -prune -print \) )"

echo -e "${HIGHLIGHT}Scanning ${PWD}${NORMAL}"
for d in $DIRS; do
  cd "$d" > /dev/null
  echo -e "\n${HIGHLIGHT}Updating `pwd`$NORMAL"
  git pull 2> >(sed -e 's/X11 forwarding request failed on channel 0//')
  cd - > /dev/null
done
于 2020-04-14T12:17:15.927 回答