代码托管与版本控制

Git 基本概念

关于版本控制

版本控制(Version Control)是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。最重要的功能是可以记录文件修改历史记录,从而让用户能够查看历史版本,方便版本切换。

常见的版本控制系统有三类:

本地版本控制系统

采用某种简单的数据库来记录文件的历次更新差异。

例如在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。

集中化的版本控制系统

单一的集中管理的服务器保存所有文件的修订版本。

协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。

分布式版本控制系统:

客户端把代码仓库完整地镜像下来,包括完整的历史记录。

任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。

Git简史

Git 是一个免费的 、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目,并且易于学习, 占用空间小,性能极快,优于传统 SCM(Source code management)工具,如 Subversion、CVS、Perforce 和 ClearCase等非分布式版本控制系统。

Git 应用于整个软件生存期,具有廉价的本地分支、方便的暂存区域和多个工作流等功能。它保存的不是文件的变化或者差异,而是一系列不同时刻的快照。

Git 基本概念

文件基本状态

    Git 中文件有两种基本状态:已跟踪(tracked)、未跟踪(untracked)

    • 已跟踪: 被纳入了版本控制的文件,在上一次的快照中有它们的记录
    • 未跟踪: 除已跟踪之外的所有文件都是未跟踪,既不存在于上次快照的记录中,也没有被放入暂存区

    已跟踪文件状态

      已跟踪的文件又分为三个状态: 已提交(committed)、已修改(modified) 和已暂存(staged)。

      • 已修改:修改了文件,但还没保存到数据库中
      • 已暂存:对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
      • 已提交:数据已经安全地保存在本地数据库中

      存储区域

        三种文件状态对应了 Git 存储的三个区域:工作区暂存区Git 仓库

        • 工作区(Working Directory):包含已经更改的文件,添加、修改、删除文件的操作都发生在工作区,这些操作尚未提交至本地仓库。
        • 暂存区(Staging Area):工作区中文件的变化的文件并不会直接被提交至本地仓库,而是需要先将修改进行标记,经过更改并进行过标记而尚未提交的文件会被存储在暂存区,提交更改是提交暂存区记录的那些更改。暂存区只会记录文件更改信息,所记录的信息只是一个索引(index),而不会真正地存储文件的拷贝,因此暂存区也被称为“索引区”,索引文件是本地仓库中 .git/index 文件。
          • 添加了一个文件时,这个文件默认不会被提交,而是需要先将其添加至暂存区后才能提交,这避免了提交不需要提交的文件。避免提交不需要提交的文件更通用的做法是在 .gitignore 文件标记不希望提交的文件。
        • 版本库(Repository:创建 Git 存储仓库后,仓库根目录下会生成一个 .git 目录,该目录包含项目的元数据及对象数据库,也是一个 Git 存储仓库最重要的部分,其中包含已提交的数据。已提交意味着数据已经安全地存储在本地存储库中,通过存储库也可以将工作目录恢复到任意一次提交时的状态。

        Git 对象

        blob 对象(数据对象)

        • 单个文件内容的快照,不保存文件名。
        • 不可变性: 由于 Git 对象的不可变性,一旦创建了 blob 对象,其内容不会再改变。如果文件内容发生变化,Git 将创建一个新的 blob 对象来存储新的内容。
        • 哈希引用: blob 对象的唯一标识是其内容的哈希值。这意味着相同内容的文件会生成相同的 blob 对象,而不管它们的位置或文件名

        树对象

        • 解决文件名保存的问题,将多个文件组织到一起。
        • Git 以简化版 UNIX 文件系统的方式存储内容,所有内容以树对象和数据对象形式存储。
        • 树对象对应 UNIX 中的目录项。
        • 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针, 以及相应的模式、类型、文件名信息。

        提交对象

        当使用 git commit 进行提交操作时,Git 会创建一个提交对象(commits),该对象包含:

        • 一个指向顶层树对象的指针,代表项目快照
        • 指向父对象(父提交)的指针
        • 作者的姓名和邮箱
        • 时间戳
        • 提交时输入的信息

        首次提交产生的提交对象没有父对象,普通提交操 作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象。

        Git 分支

          暂存操作会使用 SHA-1 哈希算法计算每个文件的校验和,然后把文件快照保存到 blob 对象中,最终将校验和加入到暂存区域等待提交:

          # 添加文件到暂存区
          $ git add README.md hello.c .gitignore
          # 提交修改
          $ git commit -m 'add README.md hello.c .gitignore'

          当使用 git commit 进行提交操作时,Git 会先计算每一个子目录的校验和, 然后在 Git 仓库中将这些校验和保存为树对象。然后创建一个提交对象, 包含指向这个树对象的指针。 如此一来,Git 就可以重现此次保存的快照。

          现在仓库中存有五个对象:三个 blob 对象(文件快照)、一个树对象(目录结构)、一个提交对象(指向树对象的指针和提交信息):

          做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针:

          Git 分支的本质是指向提交对象的可变指针,这些指针指向的是分支内的最新提交对象,在每次提交时会自动移动。例如下图有三个分支,Master、Develop、Topic,他们分别指向的是提交对象 C1、C4、C5。

          理解 Git 中的三棵树

          “树”意为“文件的集合”,而不是特定的数据结构。Git 中有下面三棵树:

          用途
          HEAD上一次提交的快照,下一次提交的父节点
          Index预期的下一次提交的快照
          Working Directory沙盒

          HEAD

          HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 理解 HEAD 的最简方式,就是将它看做 该分支上的最后一次提交 的快照。

          Index

          索引是 预期的下一次提交。也被称为 Git 的“暂存区”,它就是运行 git commit 时 Git 存储的快照。

          Git 将上一次检出到工作目录中的所有文件填充到索引区,它们看起来就像最初被检出时的样子。之后开发者将其中一些文件替换为新版本,接着通过 git commit 将它们转换为树来用作新的提交。

          确切来说,索引在技术上并非树结构,它其实是以扁平的清单实现的。

          Working Directory

          工作目录(通常也叫 工作区),开发者在这里直接修改代码文件。

          另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。工作目录会将它们解包为实际的文件以便编辑。可以把工作目录理解为沙盒,在将修改提交到暂存区并记录到历史之前,可以随意更改。

          分支的本质

          Git – Branches in a Nutshell

          A branch in Git is simply a lightweight movable pointer to one of these commits. Create a new branch means createing a new pointer to the same commit you’re currently on. How does Git know what branch you’re currently on? It keeps a special pointer called HEAD, a pointer to the local branch you’re currently on.

          Git 基础操作

          本地 Git 常用的操作如下图所示:

          获取 Git 仓库

            通常有两种获取 Git 项目仓库的方式:

            1. 将尚未进行版本控制的本地目录转换为 Git 仓库。
            2. 从其它服务器克隆 一个已存在的 Git 仓库。

            在已存在目录中初始化仓库

            要用 Git 来控制一个尚未进行版本控制的项目目录,首先需要进入该项目目录中执行初始化git init

            $ cd /home/user/my_project
            $ git init

            该命令将创建:

            1. 名为.git的子目录,含有初始化 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。 但是项目里的文件还没有被跟踪。
            2. 一个没有任何 commit(提交记录)的初始分支,这一分支默认名称为master

            克隆现有的仓库

            Git 是去中心化的源码管理工具,每个开发人员设备上都是一个完整的库。通过 Git 托管平台,即可实现开发者之间仓库的同步。在托管平台上,仓库也是以完整的库的形式保存的。

            git clone用来获取一份现有 Git 仓库的拷贝,例如从 github 上克隆 flowable-engine 的仓库:

            git clone https://github.com/flowable/flowable-engine.git

            Git 克隆的是该 Git 仓库服务器上的几乎所有数据,这意味着即使服务器数据丢失,也可以通过本地存在的镜像仓库恢复。

            国际主流 Git 代码托管平台:GitHubGitlab等。

            国内主流 Git 代码托管平台:Gitee、阿里云 Codeup、华为云 CodeHub 等。

            将文件纳入 Git 控制

            工作目录下的每一个文件都不外乎这两种状态:已跟踪未跟踪,简而言之,已跟踪的文件就是纳入 Git 控制的文件。

            自上次提交之后,编辑过的已跟踪文件会被 Git 标记为已修改。可以选择性将这些修改过的文件放入暂存区,然后提交所有已暂存的更改,如此反复。

            检查当前文件状态

            使用git status查看文件所处的状态,一般会显示四部分内容:

            • 当前所在分支
            • 暂存区的文件(包括已跟踪的新文件)
            • 已修改的文件
            • 处于未跟踪状态的新文件
            $ git status
            On branch master
            Your branch is up to date with 'origin/master'.
            
            Changes to be committed:
              (use "git restore --staged <file>..." to unstage)
                    modified:   hello.c
                    new file:   tracked.c
            
            Changes not staged for commit:
              (use "git add <file>..." to update what will be committed)
              (use "git restore <file>..." to discard changes in working directory)
                    modified:   README.md
            
            Untracked files:
              (use "git add <file>..." to include in what will be committed)
                    untracked.c

            如果在克隆仓库之后立刻使用该命令,会看到类似这样的输出:

            $ git status
            On branch master
            Your branch is up-to-date with 'origin/master'.
            nothing to commit, working directory clean

            这意味着:

            • 当前处于 master 分支
            • 所有已跟踪的文件在上次提交后都没被修改,当前目录下也没有出现处于未跟踪态的新文件,否则 Git 会在输出中罗列出来

            如果在工作目录下创建一个新的 README.md 文件,再使用git status会看到 Git 列出了这个新的未跟踪文件:

            $ echo 'This is my project' > README.md
            $ git status
            On branch master
            
            No commits yet
            
            Untracked files:
              (use "git add <file>..." to include in what will be committed)
                    README.md
            
            nothing added to commit but untracked files present (use "git add" to track)

            跟踪新文件

            使用git add <filename>开始跟踪一个文件,指定的文件被添加到暂存区,将包含在下一次提交中。

            要追踪 README.md 文件,只需执行:

            $ git add README.md
            $ git status
            On branch master
            Your branch is up to date with 'origin/master'.
            
            Changes to be committed:
              (use "git restore --staged <file>..." to unstage)
                    new file:   README.md

            暂存已修改的文件

            如果修改 README.md文件,在git status中会出现如下输出:

            Changes not staged for commit:
              (use "git add <file>..." to update what will be committed)
              (use "git restore <file>..." to discard changes in working directory)
                    modified:   README.md

            使用git add <filename>暂存这次更新,这和开始跟踪一个新文件执行的命令一样,可以把新建文件也理解为一次要暂存的更新。

            git add本质上记录文件的快照,提交到仓库存储的是这个快照,而不是工作目录里的文件。如果在已经暂存文件的基础上,又修改了文件 README.md,可以看到git status中有如下输出:

            # 暂存区
            Changes to be committed:
              (use "git restore --staged <file>..." to unstage)
                    modified:   README.md
            
            # 非暂存区
            Changes not staged for commit:
              (use "git add <file>..." to update what will be committed)
              (use "git restore <file>..." to discard changes in working directory)
                    modified:   README.md

            README.md 即在暂存区、又在非暂存区,这并不矛盾,因为暂存区的只是文件的一个快照。如果在暂存之后又修改了文件,需要再次使用git add暂存新的修改。

            提交更新

            使用git commit -m "<message>"将暂存区的更新提交到仓库中,未暂存的修改将不会被提交。所以在提交前务必使用git status确认是否遗漏其它需要暂存的文件。

            参数message是此次提交的描述信息,方便自己或其他开发者日后检索更改。

            例如提交 README.md 的更新,需要执行下面的命令:

            $ git commit -m "modified README.md"
            [master 329d542] modified README.md
             1 file changed, 1 insertion(+), 1 deletion(-)

            忽略文件

            有些文件是不需要(不应该)加入版本控制的,例如编译的临时文件、日志文件。通过创建.gitignore文件来配置要忽略的文件模式。

            文件.gitignore的格式规范如下:

            • 所有空行或者以 # 开头的行都会被 Git 忽略
            • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中
            • 匹配模式可以以(/)开头防止递归
            • 匹配模式可以以(/)结尾指定目录
            • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反
            # .gitiggnore
            # 忽略任何目录下的 build 目录
            build/
            # 忽略当前目录下的 hello.txt 文件,而不忽略子目录下的
            /hello.txt
            # 忽略所有以 .log 结尾的文件
            *.log
            # 忽略 doc 目录下的所有 .txt 结尾的文件
            doc/*.txt
            # 跟踪 doc/config.txt,即使前面忽略了 doc/*.txt
            !doc/*.txt

            查看提交历史

            在开发中,有时需要从宏观角度看项目的发展情况、回顾提交历史,可以使用git log命令。

            $ git log
            commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3 (HEAD -> master)
            Author: author
            Date:   Thu Aug 10 20:36:48 2023 +0800
            
                modified README.md
            
            commit ce3f589374e6551c2705f54c28e5a32e2acaeee5
            Author: author
            Date:   Thu Aug 10 20:33:49 2023 +0800
            
                add README.md

            不传入任何参数的默认情况下,git log会按时间先后顺序列出所有的提交,最近的更新排在最上面。它会列出每个提交的校验和作者名邮件提交时间提交说明

            撤销操作

            修补最近一次提交

            使用git commit --amend修补最近一次提交,具体来说:在最近一次提交的基础之上,追加当前暂存区的修改,并且覆盖提交信息。原来的提交被完全替换,不会出现在仓库的历史中。

            下面的例子展示了,如何向最近一次提交内追加内容。可以看到 “add newfile1” 完全被替换了,没有出现在历史记录中。

            # 新建文件 newfile1 并提交
            $ touch newfile1
            $ git add newfile1 
            $ git commit -m "add newfile1"
            [master 71f7292] add newfile1
             1 file changed, 0 insertions(+), 0 deletions(-)
             create mode 100644 newfile1
             
            $ git log
            commit 71f7292cd47547ec7ad787db25cc537612c9b14f (HEAD -> master)
            Author: author
            Date:   Thu Aug 10 21:29:05 2023 +0800
            
                add newfile1
            
            commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3
            Author: forza.cbw <849085093@qq.com>
            Date:   Thu Aug 10 20:36:48 2023 +0800
            
                modified README.md
                
            # 新建文件 newfile2 并追加到最近一次提交
            $ touch newfile2
            $ git add newfile2
            $ git commit --amend -m "add newfile1 & newfile2"
            [master 19c2a08] add newfile1 & newfile2
             Date: Thu Aug 10 21:29:05 2023 +0800
             2 files changed, 0 insertions(+), 0 deletions(-)
             create mode 100644 newfile1
             create mode 100644 newfile2
             
            $ git log
            commit 19c2a084a11976121aed6a348c6bf5a5ceac9f4f (HEAD -> master)
            Author: author
            Date:   Thu Aug 10 21:29:05 2023 +0800
            
                add newfile1 & newfile2
            
            commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3
            Author: author
            Date:   Thu Aug 10 20:36:48 2023 +0800
            
                modified README.md

            取消暂存

            当不小心添加了多余的文件到到暂存区时,使用git reset HEAD <filename>取消暂存。

            下面的例子展示了取消暂存 newfile2 的步骤:

            # 暂存 newfile1 & newfile2
            $ git add newfile1
            $ git add newfile2
            $ git status
            On branch master
            Your branch is up to date with 'origin/master'.
            
            Changes to be committed:
              (use "git restore --staged <file>..." to unstage)
                    modified:   newfile1
                    modified:   newfile2
            
            # 取消暂存 newfile2
            $ git reset HEAD newfile2
            Unstaged changes after reset:
            M       newfile2
            $ git status
            On branch master
            Your branch is up to date with 'origin/master'.
            
            Changes to be committed:
              (use "git restore --staged <file>..." to unstage)
                    modified:   newfile1
            
            Changes not staged for commit:
              (use "git add <file>..." to update what will be committed)
              (use "git restore <file>..." to discard changes in working directory)
                    modified:   newfile2

            撤销修改

            如果不想保留对文件的修改,可以使用git checkout -- <filename>撤销修改,Git 会把指定文件恢复到上次提交的样子,而那个文件在工作目录下的任何修改都会丢失!

            --强调传入的参数是文件名,而不是分支名,避免文件与分支同名产生的二义性。

            下面的例子展示了撤销修改 newfile2 的步骤:

            # 查看原始内容
            $ cat newfile2
            This is newfile2.
            # 修改
            $ echo 'Hello world!' >> newfile2
            $ cat newfile2
            This is newfile2.
            Hello world!
            # 撤销修改
            $ git checkout -- newfile2
            $ cat newfile2
            This is newfile2.

            远程仓库的使用

            远程仓库是托管在网络中的版本库,开发者公用该仓库从而及时同步进展、方便项目协作。“远程”只是一个相对的概念,表示它相对于工作目录是在别处的,即使在存在本地也可以称为远程仓库。

            查看远程仓库

              如果想查看已经配置的远程仓库服务器,可以运行git remote -v命令,它会列出本地指定的远程服务器简称对应的地址。如果是克隆下来的仓库,至少能看到 origin — 这是 Git 给克隆的仓库服务器的默认 名字:

              # 在克隆下来的仓库内
              $ git remote -v
              origin  git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (fetch)
              origin  git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (push)

              添加远程仓库

              git clone命令会自动添加远程仓库,如果前面是在已存在的目录中初始化仓库的话,不会绑定有远程仓库。使用git remote add <shortname> <url>添加一个新的远程 Git 仓库,<short> 指定仓库的简称。

              # 在已存在的目录中初始化的仓库内
              $ git remote -v
              $ git remote add repo git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git
              $ git remote -v
              repo    git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (fetch)
              repo    git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (push)

              从远程仓库获取数据

              使用git fetch <remote>从远程仓库获得数据,这个命令执行后:

              • 从远程仓库拉取所有本地没有的数据
              • 获得远程仓库中所有分支的引用
              • 不会自动合并或修改当前的工作目录
              # 从 origin 仓库抓取内容
              $ git fetch origin

              如果当前分支设置了跟踪远程分支, 可以用git pull命令 来自动抓取后合并该远程分支到当前分支。在本地新建、并推送到远程的分支,默认跟踪了远程对应的新分支;在本地克隆下来的分支默认也跟踪了对应的远程分支。

              推送到远程仓库

              当需要分享项目时,使用git push <remote> <branch>将其推送到远程仓库。

              # 将 master 分支推送到 origin 服务器
              $ git push origin master

              只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。 当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先抓取他们的工作并将其合并进你的工作后才能推送。

              Git 分支管理

              在版本控制过程中,可能同时推进多个任务,每个任务可以有自己单独的分支,使用分支意味着程序员可以把自己的工作从开发主线上分离出来,开发自己分支的时候不会影响主线分支的运行,因此分支可以简单理解为一个单独的副本。使用分支可以并行推进多个功能开发,提高开发效率。

              许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在master分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 他们还有一些名为develop或者next的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入master分支了。

              分支操作

              列出分支

              使用git branch列出本地分支,使用参数-r列出远程分支、-a列出本地和远程的分支。当前所在的分支由*标识,git 使用 HEAD 指针指向当前所在的本地分支。

              # 列出本地分支
              $ git branch
              * master
              # 列出远程分支
              $ git branch -r
                origin/master
               # 列出本地和远程分支
              $ git branch -a
              * master
                remotes/origin/master

              新建分支

              使用git branch <branch_name>创建一个新的分支。实际上 Git 只是创建了一个可以移动的新的指针,比如创建 Develop 分支:

              $ git branch
              * master
               # 创建 Develop 分支
              $ git branch Develop
              $ git branch 
                Develop
              * master
               # 查看各个分支所指的对象
              $ git log --oneline --decorate
              19c2a08 (HEAD -> master, Develop) add newfile1 & newfile2
              329d542 modified README.md

              切换分支

              使用git checkout <branch_name>切换到一个已存在的分支,这条命令会做两件事:

              1. 移动 HEAD 到目标分支上
              2. 将工作目录恢复成目标分支所指向的快照内容

              如果工作目录有没提交的修改,git 会拒绝切换分支,你会看到类似的报错信息。根据提示,可以先提交修改或使用git stash将修改临时入栈,然后再切换分支。

              $ echo "newline" >> newfile1
              # 切换到 master 分支
              $ git checkout master
              error: Your local changes to the following files would be overwritten by checkout:
                      newfile1
              Please commit your changes or stash them before you switch branches.
              Aborting

              提交完所有修改后切换到 Develop 分支,可以看到 HEAD 指向了 Develop 分支。但是因为 master 和 Develop 分支指向的是同一个提交对象(快照内容),工作目录并不会有变化。

              # 切换到 Develop 分支
              $ git checkout Develop
              Switched to branch 'Develop'
              $ git log --oneline --decorate
              19c2a08 (HEAD -> Develop, master) add newfile1 & newfile2
              329d542 modified README.md

              对 Develop 分支进行修改,再次观察指针,发现 Develop 指针移到了最新的提交上:

              $ echo "This is newfile1" >> newfile1
              $ git add newfile1
              $ git commit -m "modified newfile1"
              [Develop 649fb31] modified newfile1
               1 file changed, 1 insertion(+)
              $ git log --oneline --decorate
              649fb31 (HEAD -> Develop) modified newfile1
              19c2a08 (master) add newfile1 & newfile2
              329d542 modified README.md

              合并分支

              一般来说,master 分支是用于发布的稳定版本,develop 是用于开发的分支。当 develop 分支开发到一个阶段时,需要将其合并到 matser 分支用于发布。

              使用git merge <branch_name>,将指定的分支合并到当前所在分支上。例如把 Develop 分支的修改合并到 master 上:

              $ git checkout master
              Switched to branch 'master'
              Your branch is up to date with 'origin/master'.
              # 将 Develop 合并到当前分支 master 上
              $ git merge Develop
              Updating 19c2a08..649fb31
              Fast-forward
               newfile1 | 1 +
               1 file changed, 1 insertion(+)
              $ git log --oneline --decorate
              649fb31 (HEAD -> master, Develop) modified newfile1
              19c2a08 (origin/master, master) add newfile1 & newfile2
              329d542 modified README.md

              代码冲突

              在 Git 中,冲突(Conflict)是指在合并(Merge)或变基(Rebase)操作时,发现两个分支上对同一部分代码进行了不同的修改,导致无法自动合并这些修改的情况。当 Git 无法确定如何将两个分支的修改整合在一起时,就会发生冲突。

              这种情况在多人协作开发时很容易出现,因此需要具备解决冲突的能力。

              冲突案例

              在多人协同开发的情景下,push 与 pull 很容易产生冲突。考虑这样一种场景:user_a 与 user_b 两位开发者在获取代码后同时对代码进行了修改,并先后进行了提交,先提交的user_a 自然不会遭遇任何异常,后提交的 user_b 则会看见错误提示如下:

              [user_b] $ git push
              To xxx/xxx.git
               ! [rejected]        main -> main (fetch first)
              error: failed to push some refs to 'xxx/xxx.git'
              hint: Updates were rejected because the remote contains work that you do
              hint: not have locally. This is usually caused by another repository pushing
              hint: to the same ref. You may want to first integrate the remote changes
              hint: (e.g., 'git pull ...') before pushing again.
              hint: See the 'Note about fast-forwards' in 'git push --help' for details.

              在 push 的时候,git 会比对本地与远程仓库的 commit 是否匹配。因为远程包含了本地没有的提交记录,因此 push 被拒绝了。如下图所示:

              要解决这一问题,user_b 必须将远程仓库中的最新代码拉取回本地,解决冲突后再重新提交:

              [user_b] $ git pull
              git pull
              remote: Enumerating objects: 4, done.
              remote: Counting objects: 100% (4/4), done.
              remote: Compressing objects: 100% (2/2), done.
              remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
              Unpacking objects: 100% (3/3), 244 bytes | 122.00 KiB/s, done.
              From xxx/xxx
                 3acb560..e8b3207  main       -> origin/main
              CONFLICT (add/add): Merge conflict in conflict_demo.txt
              Auto-merging conflict_demo.txt
              Automatic merge failed; fix conflicts and then commit the result.
              [user_b] $ cat conflict_demo.txt
              aaa
              <<<<<<< HEAD
              I'm userB
              =======
              I'm userA
              >>>>>>> e8b32079d5995effd93d4a0e4c7f4ba3762db59c
              bbb

              git pull 后的提示中,我们可以得知 conflict_demo.txt 出现了冲突,GIt 很聪明地帮助我们对文本进行了对比,并将不同之处标记了出来:HEAD 表示本地的内容,那一串哈希表示是远程服务器中存储的内容,我们现在就可以顺着 conflict_demo.txt 中的提示将文件修改成我们希望的样子,再重新 commit,如下图所示:

              因此,在 push 前先进行 pull 解决冲突问题是一种好的实践。Git 也有一定的自动解决冲突的能力,例如 A、B 添加了不同的文件,Git 可以将文件进行自动合并,遇到冲突也可以使用 git stash 命令解决。但这种简单的合并方式会引发许多意想不到的问题,Git 还没有智能到可以分析解决代码中依赖、冲突的问题。在实践中,团队需要选择并遵守相应的 Git 应用方法论,遵循一定的规范,外加对 分支的使用,才是解决这类问题行之有效的方法。

              分支开发工作流

              Git 功能强大,它允许多种分支策略与工作流程。但在协作开发的背景下,如果组织没有采取统一的 Git 应用方法论,则很有可能出现工作流程不明确、过于复杂的问题,从而难以追踪查找问题,代码也容易产生冲突。因此,有许多 Git 应用的方法论被提出来,它们也被称为 workflow。

              主流的 Git 应用方法论包括 Git Flow、Github Flow 以及 Gitlab Flow 等。

              Git Flow

              Git Flow 概述

              是著名代码版本控制软件 Gitkraken 使用的模式:A successful Git branching model

              • 根据 Git Flow 的建议,代码主要的分支有masterdevelophotfixrelease 以及feature这五种分支。
              • 其中master以及develop这两个分支又被称作长期分支,因为他们会一直存活在整个 Git Flow 里,而其它的分支大多会因任务结束而被删除。

              图片出处:https://nvie.com/posts/a-successful-git-branching-model/

              Git Flow 分支功能

              • master分支
                • 主要是用来放稳定、随时可上线的版本。这个分支的来源只能从別的分支合并过来,开发者不会直接 Commit 到这个分支。因为是稳定版本,所以通常也会在这个分支上的 Commit 上打上版本 tag。
              • develop分支
                • 主要是所有开发的基础分支,当要新增功能的时候,所有的 feature 分支都是从这个分支切出去的。而 feature 分支的功能完成后,也都会合并回这个分支。
              • hotfix分支
                • 当线上产品发生紧急问题的時候,会从 master 分支开一个 hotfix 分支出来进行修复,hotfix 分支修复完成之后,会合并回 master 分支,也同时会合并一份到 develop 分支。
              • release分支
                • 当认为 develop 分支够成熟了,就可以将 develop 分支合并到 release 分支,在这里进行上线前的最后测试。测试完成后,release 分支将会同时合并到 master 以及 develop 这两个分支上。
              • feature分支
                • 当要开始新增功能的时候,就使用 feature 分支。feature 分支都是从 develop 分支来的,完成之后会再并回 develop 分支。

              Git Flow 的局限性

                • Git Flow 相对复杂,需要同时维护两个长期分支;
                • Git Flow 适合需要“多个版本共存”的场景,但现在的互联网产品不需要“多版本共存”,只需要部署最新版,不适合持续交付。

                Gitlab Flow

                  Gitlab Flow 是前两者的综合,吸收了前两者的优点,针对 Github Flow 中“每次合并功能分支时即部署到生产环境”这一操作进行了优化,提出创建一个反映代码部署的生产分支production,通过将开发master分支合并到production来部署最新版本,且支持定时部署。

                  图片出处:Gitlab flow | Workflow | Help | GitLab

                  基本思想:代码依然如 Github Flow 那样合并入master,但通过专用production分支进行部署,当master足够稳定时才会并入 production 分支,甚至可以增加 pre-production 进行缓冲。

                  发表评论

                  您的邮箱地址不会被公开。 必填项已用 * 标注

                  Index
                  滚动至顶部