草 稿

一步一步搞定 Git 之:从无到有创建子树

子树(Subtree),既不是子模块(Submodule)也不是合并策略中的子树合并(Subtree Merge),它虽然不是 Git 内建的功能,但子模块和子树合并的优点都兼而有之,并且它比子模块更简单,比子树合并更全能。

 

子树能干什么?最典型的应用场景就是你有一个项目,这个项目里面包含有子项目,你希望子项目可以单独拥有版本控制(因为子项目可能可以重用于其他项目),但由于子项目被包含在主项目中,因此你也不希望总是在两/多个仓库(Git Repository)之前来回切换。于是子树可以做到的就是:

 

1. 所有的操作都在主项目中完成,使用的都是 Git 的常规指令,唯一要学习的就是子树子命令

2. 子项目拥有自己的版本控制系统,也就意味着可以拥有独立的历史记录

3. 本地发生的变更可以分别推送至主项目和子项目,并且只有涉及到子项目相关的变更才会被推送至子项目,子树分离的细节由 Git 在背后掌控

4. 分离的子树由于拥有独立的版本控制,所以可以很方便的整合进其他的项目,并且可以做到一处变更多处生效

5. 子树的版本仓库同样拥有完整的 Git 特性,比如分支、标签等,因此分离/整合子树也可以做到分支和标签控制等

 

好处固然很多,但是坑也不会少。本单(系列)的目的就是简明扼要的介绍子树的使用方法以及注意事项。

 

对于子树的使用,有一些很经典的场景,比如说创建子树的过程可以是从无到有的,也可以是从一到多的。具体而言,从无到有就是在项目启动的一开始就有计划的使用子树在其中,而从一到多则是项目已经进行了一阵子才决定总中分离出一个或多个子树。二者的主要区别在于是否有涉及到主项目或子项目的代码产生,因此创建子树时的考量和具体步骤也会略有差别。

 

本单介绍第一种,即从无到有创建子树。

  1. 项目初始化

    为了便于描述,我们假设有一个项目叫做 `project`,其下有两个子目录 `client` 和 `server` ,分别对应两个子项目,在 Git 的概念中也就对应了两个子树。显然,`project` 就是主项目,即主树(基本上我们不会提及主树这个词,因为不太用得着)。

     

    因为子树是拥有独立的版本控制的,因此本质上一个主项目和两个子项目其实会有三个版本仓库,只不过在本地操作的时候,两个子项目是包含在主项目中的,因此我们不需要在不同的项目之中来回切换。

     

    也就是说:三个项目仓库(你可以看作远程的,虽然它们也可以保存在本地)= 一个本地项目(包含着两个子项目)。以下我们使用 Github 作为远程仓库服务,并且虚拟一个用户名为 `example`。

  2. 创建三个远程项目仓库

    如何在 Github 上创建仓库,这属于 Git 的入门知识,此处不再赘述。

     

    需要注意的是,你不能只创建“空的”远程仓库,每一个远程仓库都必须至少包含一次提交,也就是说远程仓库里得有历史记录,得有 refs(至少得有 master 分支的引用),这样后面添加子树的操作才能成功,否则会报错的(找不到可用的 refs,即引用)。

     

    典型的操作过程是我们先初始化远程仓库,然后初始化本地仓库,接着绑定 remote 关系 -> 做一次 commit -> 最后 push。

     

    示例:

    mkdir project && cd project

    git init && git remote add origin https://github.com/example/project

    touch .gitignore && git add .gitignore

    git commit -m "Initial project repo" && git push -u origin master

     

    不过呢,使用 Github 创建的时候你可以勾选 initialize this repository with a README 选项,这样一来可以省去上述步骤,二来则可以不用在本地单独创建子项目的仓库。

     

    三个仓库都创建好之后,你会拥有如下三个远程仓库的地址:

     

    https://github.com/example/project.git

    https://github.com/example/project-client.git

    https://github.com/example/project-server.git

     

    这就是我们所需要的前置准备工作。

  3. 把主项目克隆至本地

    简单的:

     

    git clone https://github.com/example/project.git

     

    完事儿。

  4. 从无到有创建子树

    前面提到过:子树都有一个独立的版本仓库,那么对应在主项目里子树又是什么呢?答案是子目录。

     

    如何去做?只需要一条命令——

     

    git subtree add --prefix=client https://github.com/example/project-client.git master

     

    分段解释如下:

  5. git subtree add

    subtree 是子树的子命令

    add 则是子树子命令的子命令,顾名思义:添加子树

  6. --prefix=client

    --prefix 选项用于指明子树所对应的子目录,该目录不必事先存在,Git 会自动创建

     

    -P 是这个选项的简写;另外除了 = 之外,也可以用空格分割,如: -P client

  7. https://github.com/example/project-client.git master

    后面这一串是指明一个引用的位置:

     

    它可以是一个 commit(用 SHAs 或者基于 HEAD 的相对引用等)

    也可以是一个 repository + refs,比如本例所写——其中 refs 可以是分支/标签/提交中的任意一种(在 Git 的概念中引用本身就是一个泛指概念,本来不需要这么啰嗦的说明的,遗憾的是很多人都学了一个半调子……)

     

    总而言之就是要你指明添加子树的时候,从“哪里”开始去寻找子树的历史记录。

  8. ……好长,我记不住怎么办?

    长么?其实也就是远程仓库的地址占地方罢了。好消息是这件事情你只需要做一次,但如果你真的觉得不方便,那么也可以使用命名远程仓库,这个特性其实大家都做过:

     

    先 git remote add client https://github.com/example/project-client.git

    再 git subtree add --prefix=client client master

     

    搞定。

  9. 结语

    本单主要用来测试轻单这个应用的,内容截取自我写的“玩转 Git Subtree”里的第一部分。最后测试的感想就是:用于技术分享的应用不支持轻量排版格式(如 Markdown 等)都是耍流氓。

     

    直到支持好用的排版格式之前,不会再用轻单搞技术类分享了,费劲。

评论(1

确实做这个要很费劲,阅读也很费劲。