zhangsanshi / issue-blog

It's a blog rather than issue
0 stars 0 forks source link

git submodule #51

Open zhangsanshi opened 6 years ago

zhangsanshi commented 6 years ago

前言

submodule 目前对 git 仓库拆分的已有实现之一。环境 git version 2.7.4.windows.1

准备工作

  1. 首先创建主仓库 subrepo-master,随意提交一次文本,接着拉取到本地

  2. 建立子仓库 subreposubrepo1,随意提交一次文本

操作

  1. 在主仓库下运行如下命令后,可以看到在仓库中多出来文件 subrepo 以及 .gitmodules

    git submodule add [subrepo url]
  2. 运行 git status

    On branch master
    Changes to be committed:
    (use "git reset HEAD <file>..." to unstage)
    
            new file:   .gitmodules
            new file:   subrepo
  3. 接着添加 submodule1,并指定路径

    git submodule add [subrepo1 url] ./module/module1
  4. 运行 git status 得到

    On branch master
    Changes to be committed:
    (use "git reset HEAD <file>..." to unstage)
    
            new file:   .gitmodules
            new file:   module/module1
            new file:   subrepo

    cat .gitmodules 得到

    [submodule "subrepo"]
            path = subrepo
            url = [subrepo url]
    [submodule "module/module1"]
            path = module/module1
            url = [subrepo1 url]

    这是一份子模块与路径的映射关系图,这份文件很重要,git 根据这份文件去识别 submodule,所以这份文件应该被加入版本控制

  5. 接着运行提交命令,可以看到三个目录都被添加到仓库了,注意子模块下面的文件并没有被添加进去。160000 的含义是这是 Git 中的一种特殊模式,基本上意味着您将提交记录为目录条目而不是子目录或文件。 然后提交到远端,就有了一个 submodule 的仓库 :)

    $ git commit -m "add submodule"
    [master 5c88033] add submodule
    3 files changed, 8 insertions(+)
    create mode 100644 .gitmodules
    create mode 160000 module/module1
    create mode 160000 subrepo
  6. 接下来模拟多人协作,首先新建一个文件夹,运行命令后,会发现 subrepo 以及 module/module1 目录并没有文件。

    git clone [subrepo-master url]
  7. 这时候需要运行命令 git submodule init 去初始化本地配置文件以及 git submodule update 拉取代码。

    $ git submodule init
    Submodule 'module/module1' (https://github.com/xxx/subrepo1.git) registered for path 'module/module1'
    Submodule 'subrepo' (https://github.com/xxx/subrepo.git) registered for path 'subrepo'
    
    $ git submodule update
    Cloning into 'module/module1'...
    remote: Counting objects: 3, done.
    remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
    Unpacking objects: 100% (3/3), done.
    Checking connectivity... done.
    Submodule path 'module/module1': checked out '5c47ee69895b8acd3291eb0551f751ba43  488c68'
    Cloning into 'subrepo'...
    remote: Counting objects: 3, done.
    remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
    Unpacking objects: 100% (3/3), done.
    Checking connectivity... done.
    Submodule path 'subrepo': checked out '1b6f8742270c5affc9fc055ce1bf03907cf9a0f8'
    
  8. 通过 6、7 两步完成还是太过麻烦,好在有简单的命令,在 clone 那一步运行 git clone --recursive 就会自动完成 6、7两步。

  9. 如果只更改主仓库的代码,那各种操作大家都会。下面主要是子仓库远端修改、子仓库改分支、子仓库本地修改推送远端、主仓库下的子仓库的引用同时被多人修改等等。

  10. 子仓库远端修改

    修改子仓库远端的内容,接着同步到本地。一种方式,进入子仓库目录 git fetch && git merge,另一种方式 git submodule update --remote [子仓库目录]。 运行 git diff,可以看到

    $ git diff
    diff --git a/module/module1 b/module/module1
    index 5c47ee6..e55e7ef 160000
    --- a/module/module1
    +++ b/module/module1
    @@ -1 +1 @@
    -Subproject commit 5c47ee69895b8acd3291eb0551f751ba43488c68
    +Subproject commit e55e7efb44f1c07d23e2deede0a13fa8a953f96d
    diff --git a/subrepo b/subrepo
    index 1b6f874..afdc5bf 160000
    --- a/subrepo
    +++ b/subrepo
    @@ -1 +1 @@
    -Subproject commit 1b6f8742270c5affc9fc055ce1bf03907cf9a0f8
    +Subproject commit afdc5bf559beacb08032e23d22a2beaa65d3ca9c
  11. 子仓库改分支

    在子仓库远端新建分支 a,然后在主仓库运行命令,再运行 cat .gitmodules 查看,发现 subrepo 多了 branch 指向。运行 git submodule status 可以看到 submodule 的状态。运行 git log -p --submodule 可以查看到子模块的日志修改。同时可以将 branch 指向 tag ,但不支持 commitid。这里有一点,子模块记录的是 commitid 即使指定了 branch,同时它不会主动升级(主动升级会导致每个人的代码不一致)。

    git config -f .gitmodules submodule.subrepo.branch a
    git submodule update --remote
    $ cat .gitmodules
    [submodule "subrepo"]
            path = subrepo
            url = https://xx/subrepo.git
            branch = a
    [submodule "module/module1"]
            path = module/module1
            url = https://xx/subrepo1.git
    
    $ git submodule status
    +e55e7efb44f1c07d23e2deede0a13fa8a953f96d module/module1 (remotes/origin/HEAD)
    +afdc5bf559beacb08032e23d22a2beaa65d3ca9c subrepo (remotes/origin/HEAD)

    指向 commitid 的方案是运行如下指令,通过将这个子模块文件夹的 commitid 指定为新的,即可完成指定相应 commitid 的动作。此时观察 .gitmodules,会发现 subrepo 指向的 branch 字段消失。

    cd subrepo
    git checkout fa1317a
    cd ..
    git add subrepo
    git commit ...

    其他人在各自目录运行,即可获取最新更新

    git submodule update
  12. 子仓库本地修改推送远端

    • 子模块本地提交代码

      当本地子模块没有指定 branch 的时候,是处于一个称作 “游离的 HEAD”(detached HEAD) 的状态(git 提示的是 commitid 而不是分支名)。这个状态下你可以正常的 git 操作,但是此时是没有分支进行跟踪的,也就没办法推送代码。想摆脱这种状态,在子模块运行 git checkout branch 即可。

    • 子模块拉取服务端代码

      然后对远端子模块做一次修改(称为 patchA),并运行 git submodule update --remote --merge,就可以看到 subrepo 发生了改变,此时可以提交这个改变,即将 subrepo 指向的旧的 commitid 换成 patchAcommitid。后面的人更新代码的时候会拿到新的 commitid,如果没有人运行 update 命令并提交,则所有人拿到的都是旧的代码。

      $ git status
      On branch master
      Your branch is up-to-date with 'origin/master'.
      Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
      
              modified:   subrepo (new commits)
      
      no changes added to commit (use "git add" and/or "git commit -a")
      
    • 本地提交与远端合并代码

      接下来对本地的 subrepo 做一次提交,接着对远端的子模块也做一次提交。然后更新子模块。可能会运行 git submodule update --remote,此命令只会更新成远端的代码,会发现本地的代码丢失,此时不需要慌张,运行 git checkout branch 即可获取本地代码。合并的命令是 git submodule update --remote 后面加上 --merge 或者 --rebase 即可,会遇到冲突的情况,进入目录手工解决即可。运行 git diff -p --submodule,可以查看到子模块的修改,现在修改的内容包含本地提交以及远端合并的代码,接下来就需要进行推送。

    • 本地提交到远端 在主仓库进行一次提交,提交子仓库最新的 commitid

      如果我们在主项目中提交并推送但并不推送子模块上的改动,其他尝试检出我们修改的人会遇到麻烦,因为他们无法得到依赖的子模块改动。那些改动只存在于我们本地的拷贝中。

      为了确保这不会发生,你可以让 Git 在推送到主项目前检查所有子模块是否已推送。 git push --recurse-submodules=check (如果子模块没有提交,会直接报错)或者 git push --recurse-submodules=on-demand(如果子模块没有提交,会尝试提交,提交不成功同时会阻止主仓库的推送)

  13. 主仓库下的子仓库的引用同时被多人修改

    比如 subrepo 指向的 commitida,A 成员修改为 b, B 成员修改为 c,这时候就需要去合并代码,让其指向最新的 a1

    如果 bc 是祖先关系,则 git 会直接快进式合并。下面模拟分叉的情况。

    检出一个本地仓库, git reset --HARD oldcommitid,然后将子模块的代码进行修改,接着添加到主仓库进行提交。接下来运行 git pull,会发现有冲突了。

    $ git pull origin master
    From https://xx/subrepo-master
    * branch            master     -> FETCH_HEAD
    warning: Failed to merge submodule subrepo (commits don't follow merge-base)
    Auto-merging subrepo
    CONFLICT (submodule): Merge conflict in subrepo
    Automatic merge failed; fix conflicts and then commit the result.

    解决此问题的方案是,首先 git diff 查看一下代码

    diff --cc subrepo
    index 4660574,55167f5..0000000
    --- a/subrepo
    +++ b/subrepo
    diff --git a/module/module1 b/module/module1
    index e55e7ef..5c47ee6 160000
    --- a/module/module1
    +++ b/module/module1
    @@ -1 +1 @@
    -Subproject commit e55e7efb44f1c07d23e2deede0a13fa8a953f96d
    +Subproject commit 5c47ee69895b8acd3291eb0551f751ba43488c68
    

    找到冲突的 commitid(其中 4660574 是公有的, 55167f5 是上游的),进入子模块,然后检出为分支 git branch merge1 55167f5 && git merge merge1,如果有冲突,此时就需要手动解决冲突了,解决完成,提交。再回到主目录,查看是否有冲突,对主仓库进行提交。此时再 git pull 则无冲突,然后就可以用 git push --recurse-submodules=on-demand 提交本次修改。

  14. 删除子模块后会发现 .gitmodules 文件内容同时发生了改变。如果需要备份,请提前备份目录

    git submodule deinit module/module1
    rm -rf .git/modules/module
    git rm -f module/module1
    vi .git/config

技巧

这样运行更新命令的时候,就简化为 git supdate,其他同理。

缺点

  1. 例如在有子模块的项目中切换分支可能会造成麻烦。 如果你创建一个新分支,在其中添加一个子模块,之后切换到没有该子模块的分支上时,你仍然会有一个还未跟踪的子模块目录,这时候如果不小心提交了这个子模块(git commit -am "message"),就会有问题了。

  2. 对子模块的更新略显复杂,每次操作都需要所有人手动同步更新,增加了学习成本。

  3. 对子模块做了修改,需要先推送子模块再主模块,同时拉取的时候也需要先主模块,再子模块。

  4. 对子模块做本地修改需要先检出分支,否则有可能在 “游离的 HEAD” 上做修改。

  5. 删除子模块,需要的步骤有点复杂。

  6. 如果子模块被高频次更新,会有大量合并代码的工作,参考上面的 10-13

  7. 如果你的同事更新了 submodule,然后更新了父项目中依赖的版本号。你需要在 git pull 之后,调用 git submodule update 来更新 submodule 信息。这儿的坑在于,如果你 git pull 之后,忘记了调用 git submodule update,那么你极有可能再次把旧的submodule 依赖信息提交上去(使用 git submit -am "message" 或者 git add . 提交的人会遇到这种事)。

优点

  1. 不需要获取子模块整个代码库

  2. 主仓库只是获取到了子仓库的引用

参考

  1. https://git-scm.com/book/en/v2/Git-Tools-Submodules

  2. https://medium.com/@porteneuve/mastering-git-submodules-34c65e940407 最后的总结很好