Git:是否可以让多个项目使用同一个子模块工作副本?

49

我是Git的新手。 假设我有两个Git仓库,它们都添加了相同的库作为子模块:

/home/projects/project1/library_XYZ
/home/projects/project2/library_XYZ

假设我同时在项目和库中工作。当我更改库时,例如在/home/projects/project1/library_XYZ中,我必须将这些更改推送并在/home/projects/project2/library_XYZ中拉取它们,以使它们对project2可用,对吗?我认为这有两个不方便之处:

  • 我必须构建library_XYZ两次。
  • 我有一个不需要的冗余,与实际项目组织相矛盾。

是否有任何办法让Git将子模块library_XYZ克隆到同一本地目录中,即使文件的组织方式如下:

/home/projects/project1
/home/projects/project2
/home/projects/library_XYZ

library_XYZ 仍然是这两个项目的子模块时,我该怎么办?

我认为这可能与这个问题有关,尽管我的设置略有不同,该问题还没有得到解答。


1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - twalberg
@twalberg 这种情况的适当工作流程是什么? - theV0ID
无论是你在问题中提到的方式 - 通过一个中央仓库,两个子模块仓库都与之同步 - 还是设置这两个子模块直接相互传播更改。没有“一种正确的方法”,但有很多方法是不正确的,甚至是非常危险的... - twalberg
5个回答

28

将共享依赖项设置为子模块很容易。 git submodule 命令不会自动执行此操作,但子模块只是一个嵌套的存储库 —— 而且 git 不要求任何实际存储库或其工作树位于特定位置。

设置一个名为libraryXYZ的存储库以用作共享子模块

# a submodule is just a repository. We're going to share this one.
git clone u://r/libraryXYZ 

# and keep its worktree right here:
( cd libraryXYZ; git config core.worktree .. )

然后,从任何地方使用子模块克隆项目并设置为使用共享的项目:

git clone u://r/project1
cd project1
git submodule init
echo gitdir: path/to/shared/libraryXYZ/.git > libraryXYZ/.git

现在,project1libraryXYZ 子模块将使用共享的 libraryXYZ 代码库和工作树。

设置您的构建系统以使用相同的工作树即可完成。当然,您可以让 git 告诉您在任何给定的代码库中它们在哪里:

# for example to see where all a project's submodules' parts are kept
git submodule foreach git rev-parse --git-dir --show-toplevel

# or for just one:
git --git-dir=$project1/libraryXYZ/.git rev-parse --git-dir --show-toplevel

(晚期编辑:值得注意的是@twalberg的评论,这可能使您从一个项目中进行 git submodule update 而没有意识到您还改变了共享依赖项的每个其他项目的构建环境。)


1
我忘记了文本文件子模块.git。+1 - VonC
1
@VonC :-) 事实是,我只是在撰写一个更复杂的答案时才意识到这有多容易。失去对某个即时任务的专注,大脑就会变得更加自由,不是吗? - jthill
工作树功能自git 2.5版本开始得到支持。因此,最好为project2、project3、...、projectn使用工作树功能。 - Yue Lin Ho

13
我有和你类似的问题:一个大型通用工具库作为子模块,许多项目都依赖它。我不想为每个实例创建单独的检出。
jthill建议的解决办法很好,但仅解决了问题的前半部分,即如何让git满意。
缺失的是如何让您的构建系统满意,因为它需要实际的文件来处理,而不关心gitlink引用。
但是,如果将他的想法与符号链接结合起来,您就能得到想要的结果!
为了实现这一点,让我们从您的示例项目开始。
/home/projects/project1
/home/projects/project2
/home/projects/library_XYZ

假设项目1和项目2都已将library_XYZ添加为子模块,并且当前这三个项目都包含library_XYZ的完整检出。为了通过共享符号链接到库的检出来替换库子模块的完整检出,请执行以下操作:
sharedproject="/home/projects/library_XYZ"
superproject="/home/projects/project1"
submodule="library_XYZ"
cd "$superproject"
(cd -- "$submodule" && git status) # Verify that no uncommited changes exist!
(cd -- "$submodule" && git push -- "$sharedproject") # Save any local-only commits
git submodule deinit -- "$submodule" # Get rid of submodule's check-out
rm -rf .git/modules/"$submodule" # as well as of its local repository
mkdir -p .submods
git mv -- "$submodule" .submods/
echo "gitdir: $sharedproject/.git" > ".submods/$submodule/.git"
ln -s -- "$sharedproject" "$submodule"
echo "/$submodule" >> .gitignore

然后,针对/home/projects/project2作为$superproject,重复相同的步骤。

这里是已完成的操作的说明:

首先,使用“git submodule deinit”删除子模块检出,将library_XYZ留下为空目录。请务必在执行此操作之前提交任何更改,因为它将删除检出!

接下来,我们使用“git push”将尚未推送到共享项目的本地检出中保存的任何提交保存到/home/projects/library_XYZ。

如果由于未设置远程或refspec而无法工作,则可以执行以下操作:

(saved_from=$(basename -- "$superproject"); \
 cd -- "$submodule" \
 && git push -- "$sharedproject" \
             "refs/heads/*:refs/remotes/$saved_from/*")

这将在/home/projects/library_XYZ中将子模块本地仓库的所有分支保存为远程分支。$superproject目录的基本名称将用作远程的名称,例如在我们的示例中是project1或project2。

当在那里执行"git branch -r"时,保存的分支将显示出来,当然,在/home/projects/library_XYZ中实际上不存在该名称的远程分支。

作为安全保障,上述命令中的refspec不以"+"开头,因此“git push”不会意外覆盖任何已经存在于/home/projects/library_XYZ中的分支。

接下来,.git/modules/library_XYZ将被删除以节省空间。我们可以这样做是因为我们不再需要使用“git submodule init”或“git submodule update”。这是因为我们将共享/home/projects/library_XYZ的检出和.git目录,避免了本地副本的使用。

然后,我们让git将空的子模块目录重命名为“.submods/library_XYZ”,这是一个(隐藏的)目录,项目中的文件永远不会直接使用。

接下来,我们应用jthill的部分解决方案,并在.submods/library_XYZ中创建一个gitlink文件,使git将/home/projects/library_XYZ视为子模块的工作树和git仓库。

现在来了新东西:我们创建了一个符号链接,相对名称为“library_XYZ”,指向/home/projects/library_XYZ。这个符号链接将不会被纳入版本控制,因此我们将其添加到.gitignore文件中。

项目1和项目2中的所有构建文件都将使用library_XYZ符号链接,就像它是一个普通的子目录一样,但实际上在/home/projects/library_XYZ的工作树中查找文件。

除了Git之外,没有人会真正使用.submods/library_XYZ!

然而,由于符号链接./library_XYZ没有被版本控制,因此在检出项目1或项目2时不会被创建。因此,我们需要确保在缺失时会自动创建它。

这应该通过项目1/项目2的构建基础设施来完成,其等效于以下shell命令:

$ test ! -e library_XYZ && ln -s .submods/library_XYZ

例如,如果使用Makefile构建project1,并包含以下目标规则以更新子项目:
library_XYZ/libsharedutils.a:
        cd library_XYZ && $(MAKE) libsharedutils.a

然后我们将上面的那一行作为规则动作的第一行插入:

library_XYZ/libsharedutils.a:
        test ! -e library_XYZ && ln -s .submods/library_XYZ
        cd library_XYZ && $(MAKE) libsharedutils.a

如果你的项目使用其他构建系统,通常可以通过创建用于创建library_XYZ子目录的自定义规则来完成相同的操作。
如果你的项目仅包含脚本或文档,并且根本不使用任何构建系统,你可以添加一个脚本,用户可以运行该脚本来创建“缺失的目录”(实际上是符号链接),如下所示:
(n=create_missing_dirs.sh && cat > "$n" << 'EOF' && chmod +x -- "$n")
#! /bin/sh
for dir in .submods/*
do
        sym=${dir#*/}
        if test -d "$dir" && test ! -e "$sym"
        then
                echo "Creating $sym"
                ln -snf -- "$dir" "$sym"
        fi
done
EOF

这将在.submods中创建所有子模块检出的符号链接,但仅在它们不存在或损坏时才会创建。
到目前为止,传统子模块布局已转换为允许共享的新布局。
一旦您已经提交了该布局,请在某个地方检出超级项目,进入其顶级目录,并按以下顺序执行以启用共享:
sharedproject="/home/projects/library_XYZ"
submodule="library_XYZ"
ln -sn -- "$sharedproject" "$submodule"
echo "gitdir: $sharedproject.git" > ".submods/$submodule/.git"

我希望你能明白这个意思:project1和project2使用的library_XYZ子目录是一个未版本化的符号链接,而不是与“.gitmodules”中定义的子模块路径相对应。
构建基础设施将自动创建符号链接并将其指向.submods/library_XYZ,但是仅当符号链接不存在时才会这样做,这一点非常重要。
这使得可以手动创建符号链接,而不是让构建系统创建它,因此也可以将其指向单个共享的checkout,而不是.submods/library_XYZ。
这样,如果您想要在自己的机器上使用共享的checkout,就可以手动创建符号链接。
但是,如果另一个人什么都不做,只是检出project1并执行正常的“git submodule update --init library_XYZ”,那么事情仍然可以像没有共享checkout一样工作。
两种情况下都不需要更改已检出的构建文件!
换句话说,在不需要其他人遵循特殊说明的情况下,project1和project2的检出将像往常一样工作。
但是,通过在构建系统有机会创建符号链接之前手动创建gitlink文件和library_XYZ符号链接,您可以在本地“覆盖”符号链接并强制执行库的共享checkout。
甚至还有另一个好处:事实证明,如果使用上述解决方案,则根本不需要处理“git submodule init”或“git submodule update”:它可以自动工作!
这是因为"git submodule init"只是为了准备"git submodule update"而必要的。但是你不需要后者,因为库已经被检出到其他地方,并且已经有了自己的.git目录。所以"git submodule update"无需操作,我们也不需要它。
由于不再使用"git submodule update",也不需要.git/module子目录。对于子模块,也没有必要设置alternates(--reference选项)。
此外,在/home/projects/project1和/home/projects/project2中推送/拉取/home/projects/library_XYZ时,你也不需要任何远程内容。因此,可以从project1和project2中删除访问library_XYZ的远程内容。
这是一个双赢的局面!
这种解决方案唯一明显的缺点是它需要符号链接才能工作。
这意味着,在VFAT文件系统上无法检出project1等项目。
但是,即使这样做,像http://sourceforge.net/projects/posixovl这样的项目仍然可以解决文件系统中任何符号链接限制的问题。
最后,给Windows用户一些建议:
自VISTA以来,符号链接可通过mklink命令使用,但它需要特殊权限。
但是,当使用sysinternals中的“junction”命令时,甚至在Windows XP时期就已经可以创建对目录的符号链接。
此外,你还可以选择使用CygWin,它可以(据我所知)即使在没有操作系统支持的情况下模拟符号链接。

2
这行代码 echo "gitdir: $sharedproject.git" > ".submods/$submodule/.git" 似乎缺少一个“/”。现在的写法会导致 git status 返回致命错误。不过,我测试了这个答案中的这行代码 echo "gitdir: $sharedproject/.git" > ".submods/$submodule/.git" 是可以工作的。 - IBrum
我相信@IBrum是正确的,你应该修改你的回答。除此之外,更新“superproject”以使用“sharedproject”的最新提交的最佳实践方法是什么?git submodule sync和push更改? - TalL

4

从 git 2.5 开始,你可以执行以下操作:

  1. 删除 /home/projects/project2/library_XYZ
  2. 删除 /home/projects/project2/.git/modules/library_XYZ
  3. cd /home/projects/project1/library_XYZ
  4. /home/projects/project1/library_XYZ 中创建一个名为 project2 的分支
  5. 运行 git worktree add ../../project2/library_XYZ project2

现在,/home/projects/project1/.git/modules/library_XYZ 被两个项目共享。


从git 2.5开始,这不是对原始问题的正确答案吗? - folq
1
git worktree add中添加一个-b project2选项肯定是个好主意,并在/home/projects/project2中运行git submodule update - Joseph

1
我选择了一种相对简单的方法(在我看来):
我按如下设置共享子模块:
  1. 将应作为共享子模块的项目克隆到本地某个目录
  2. 在共享子模块中,在 [core] 下添加 'worktree = ..',修改此项目的本地配置
对于所有共享此子模块的项目:
  1. 添加子模块(这意味着,实际上我现在有两个已检出的该项目副本)
  2. 删除./git/submodule/中相应的子模块文件夹
  3. 删除子模块目录中除.git文件以外的内容
  4. 修改子模块目录中的.git,使其指向在步骤1)中设置的项目路径
附注:这似乎不能很好地与Sourcetree配合使用。由于第5步,出于某种原因,Sourcetree无法正确获取项目引用,并始终假定文件已被删除。但是,使用命令行可以无缝运行,这让我相信设置是正确的,但Sourcetree存在错误。

1
在Linux上,只需简单地:
sudo mount -o bind /home/projects/library_XYZ /home/projects/project1/library_XYZ

如果您已经初始化了子模块,也许您应该像Guenther Brunthaler建议的那样去做。
cd /home/projects/project1/
git submodule deinit library_XYZ
rm -rf .git/modules/library_XYZ

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