Git简明教程

1. 个人基本故事线

下图是从个人开发者角度所能观察到的场景:

image-20210317010024234

1.1 如何从远程仓库获取代码

  • git clone

  • 第一次时用克隆

  • git fetch

  • 第二次开始

    • 将远程仓库代码拉取到本地仓库
    • 无冲突时checkout到工作区
    • 有冲突时merge到工作区
  • git pull

  • 第二次开始

    • 掌握pull和fetch的区别
      • pull = fetch+merge

1.2 提交代码到远程仓库

  • git add .

  • 工作区添加到暂存区

  • git commit

  • 暂存区更新到本地仓库

  • git commit -a = git add . + git commit

  • git push

    • 本地仓库更新到远程仓库

1.3 手工创建本地仓库并与远程仓库同步

  • git init
    • 初始化一个本地仓库
  • git remote add origin <远程仓库地址>
    • 将本地仓库关联到远程仓库
    • 若有已经关联的远程仓库,使用git remote rm origin删除老的远程仓库
  • 拉取远程仓库,与本地merge
    • git pull 拉取分支
  • git add . && git commit
    • 本地修改后,提交到本地仓库
  • git push,在远端分支上创建新版本
    • 将本地仓库推送到远程仓库

1.4 文件级别的操作

​ 上述操作大多是基于分支和提交版本的,又是需要对部分文件进行提交和检出:

  • git add *files* 把当前文件放入暂存区域。
  • git commit 给暂存区域生成快照并提交到本地仓库
  • git reset -- *files* 用来撤销最后一次git add *files*,你也可以用git reset 撤销所有暂存区域文件。
  • git checkout -- *files* 把文件从暂存区域复制到工作目录,用来丢弃本地修改。

[注:此处为文件的checkout操作,分支操作也用checkout但含义稍有区别]

你也可以跳过暂存区域直接从仓库取出文件或者直接提交代码:

  • git commit -a 相当于运行 git add 把所有当前目录下的文件加入暂存区域再运行git commit
  • git commit *files* 进行一次包含最后一次提交加上工作目录中文件快照的提交。并且文件被添加到暂存区域。
  • git checkout HEAD -- *files* 回滚到复制最后一次提交。

2. 理解远程仓库

2.1 创建远程仓库(通过页面操作实现)

  • 创建操作create

  • 复制操作fork

    • fork的远程仓库如果与源仓库需要合并,则需要Pull Request操作
  • 导入操作import

2.2 克隆远程仓库(通过本地git命令操作)

  • 克隆命令git clone仓库地址

    • 要在没有本地仓库的地方执行
    • 从本人可见的远程仓库复制到本地电脑
    • 可以做本地修改,但仅有开发权限的项目成员才能向远程仓库推送

2.3 拉取远程仓库代码(通过本地git命令操作)

  • 拉取命令git pull [分支名]

    • 要在本地仓库内执行

      • 拉取的内容会被合并到本地工作区本地仓库同步合并)
    • 只有有开发权限的项目成员才能够拉取,并在修改后推送

    • git pull [分支名] = git fetch [分支名]+ git merge

  • 拉取命令git fetch [分支名]

    • 要在本地仓库内执行

      • 拉去的内容会被合并到本地仓库
    • 需要配套git checkout [分支名]

      • 本地仓库拉取并覆盖到本地工作区后使用
    • 或者配套git mearge

      • 本地仓库拉取并合并到本地工作区后使用

2.4 推送代码到远程仓库(通过本地git命令操作)

  • 推送命令git push [分支名]

    • 远程仓库新于本地仓库时,需要先pull,使本地仓库远程仓库同步,并merge解决所有冲突后,才能够push
    • 要先配置好用户名和邮箱,以便远程仓库记录操作用户

3 团队故事线

image-20210317010024234

3.1 角色区分

  • Team leader

    • 负责主远程仓库的创建

    • 新版本开发过程中(可通过页面操作实现)

      • 收到程序员A和B的pull Request
      • 检查程序员A和B的pull Request
      • 当pull request出现冲突时,解决冲突
      • 接受pull Request,合并至主仓库
    • 新版本开发完成(可通过页面操作实现)

      • 创建本地release分支
      • 合并本地主分支到release分支
      • 推送本地分支到远程仓库
      • 版本的标签管理和推送
  • 程序员A

    • 新任务生命周期

      • fork创建个人远程仓库(可通过页面操作完成)
      • 针对某个任务完成个人故事线
      • pull request远程操作(可通过页面操作完成)
  • 程序员B

    • 新任务生命周期

      • fork创建个人远程仓库(可通过页面操作完成)
      • 针对某个任务完成个人故事线
      • pull request远程操作(可通过页面操作完成)

3.2 四种可能的协作方式

  • 集中式协作工作流

    • **大家共用一个远程仓库 **
    • 大家都工作在master分支上
    • 不同角色通过pull和push操作远程仓库,他们各自在本地维护和控制冲突,并推送到远程仓库
  • 功能分支工作流

    • 大家共用一个远程仓库
    • 只有团队leader可以操控master分支,开发者们工作在各自的功能分支上
    • 允许开发者创建各自的功能分支,并推送到远程仓库
    • 开发者完成开发后,向团队leader交Pull Request,由leader进行代码审查并合入master分支

  • GitFlow工作流程

    • 大家共用一个远程仓库
    • 设置master分支、develop分支、release分支和hotfix分支
      • master分支:
        • 只用来储存官方发布历史,通常我们会在master分支的提交中打上版本标签号
        • master分支由team leader管控
      • develop分支:
        • 开发者打交道最多的分支,源自master分支,用于整合各功能分支,类似于前面两种方式的master分支
        • develop分支由team leader管控
      • feature分支:
        • 每个新功能都放在自己的分支中,并以develop分支作为父分支,因此可以由很多功能分支
        • 当一个功能完成后,开发这向团队leader提交Pull Request,由团队leader完成合并后推送到develop分支
        • 功能分支永远不应该直接与master分支交互,而应先合会到develop分支,再由develop分支合并回master分支
        • 命名规范遵循feature-*或者feature/*
        • feature分支可由开发人员创建和维护
      • release分支:
        • release分支是develop分支和master分支之间的一个过渡阶段,主要用于该发布版本的功能完善或bug修复
        • release分支基于develop分支创建,只有和发布相关的任务才在这个分支上进行,如修复bug,生成文档等等
        • release分支的所有功能均完善后,将并入master分支和develop分支(此时develop分支也许正在并行开发)
        • 并入master分支和develop分支后,该release分支应当被删除
        • release分支可以确保一个团队完善当前发布,而不影响其他团队继续在develop上开发下一个待发布的功能或在master上修复产品bug
        • 命名规范遵循release-*或者release/*
        • release分支由teame leader负责管控,也可由release版维护人员采用集中式工作流程方式自行解决冲突
      • hotfix分支:
        • 与release分支不同,hotfix分支基于master分支创建,主要用于master版本做快速的bug修复
        • hotfix分支完成后,应当合并入master分支和develop分支,会影响到下一个release分支,但不会影响当前release分支,以免带来更多bug
        • 并入master分支和develop分支后,该hotfix分支应当被删除,而master分支应当打上更新的标签
        • hotfix分支可以被看作 master 分支的临时发布分支,它使得团队能够及时处理issues,而不打断其他工作流。
  • Forking工作流程

    • 每个角色有自己的远程仓库 ---- 推荐得多人协作方式
    • 所有角色都有自己的远程仓库,其中team leader拥有主仓库的权限
    • 不同角色通过fork和pull request保持与主仓库的协作,由team leader协调不同pull request的冲突

4 冲突的控制

4.1 fork与pull request中产生的冲突

  • 冲突的产生:冲突在主仓库产生,由team leader处理冲突

  • 由team leader将主仓库和PR仓库都拉取到本地,在本地通过merge tool合并有冲突的代码后,推送到主仓库,PR仓库可以通过与主仓库同步自动更新版本

image-20210317010222818

4.2 pull和push过程中产生的冲突

  • 冲突的产生:冲突在角色本地产生,每个角色都可以在本地处理冲突,但会不断受到其他角色新push的影响

    image-20210317010326020

  • 冲突的解决

    image-20210317010335025

4.3 总结

image-20210317010355629

5 分支的管理

5.1 分支管理的目的

  • 在不中断主分支的情况下,同时开展其他工作
  • 通常在完成该工作后,将成果合并到主分支

5.2 远程仓库分支管理

  • 创建远程分支(页面操作)

  • 本地命令创建远程分支

    • 先创建本地分支:git branch [新分支名]
    • 推送到远程仓库:git push -u origin [新分支名]
  • 删除远程分支(页面操作)

  • 使用远程分支

    • git pull/fetch/push [指定分支名称]

5.3 本地仓库分支管理

  • 本地分支管理

    • 创建当前版本的本地分支

    • git branch [新分支名]

    • 删除本地分支B的命令

      • git branch -d分支B
  • 切换本地分支 ,或者检出内容到工作区

    • git checkout [已有分支名]
    • 或者git checkout -b [新分支名称]
      • 创建并检出一个新分支

image-20210328152736904

  • 合并分支得大致流程(将B分支合并到A分支的过程)

    • 检出分支B: git checkout 分支B
    • 修改并提交分支B: 完成分支B工作区修改,并提交
    • 检出分支A: git checkout 分支A
    • 合并分支B到当前分支A: git merge B
  • 拉取远程仓库分支

    • git pull <远程主机名> <远程分支名>:<本地分支名>
  • 推送远程仓库分支

    • git push <远程主机名> <本地分支名>:<远程分支名>

6 标签管理

6.1 标签管理的目的

​ 用于版本管理

6.2 常用命令

  • 在本地创建标签

    • git tag -a 标签名
  • 删除本地标签

    • git tag -d 标签名
  • 列出本地标签

    • git tag
  • 切换本地标签

    • git checkout 标签名
  • 推送远程标签

    • git push
  • 拉取远程标签

    • git pull

7 查看历史记录

7.1 命令行

  • git log

    • 查看所有commit行为
    • 输入q退出git log

7.2 web页面

  • 选择仓库的“统计”按钮查看

7.3 IDE工具

  • 不同IDE通常会提供可视化的git历史记录

8 常见场景速查手册

以下为一些常见的小场景及其对策:

场景1. 本地已经存在的项目/分支与如何远程仓库关联

1
git remote add origin <your-repo-git-url>

场景2. 刚刚提交了的commit log发现错了,想修改

1
git commit --amend -m "your new log"

场景3. 查看某次提交的日志和ID

1
git reflog

场景4. 查看某次提交的内容

1
git show <commit_id>

场景5. 只是修改了工作区的文件,想恢复到原来修改前的样子

1
2
git reset --hard HEAD
git checkout -- <file_name>

场景6. 被修改的文件已经添加到了暂存区,想撤销添加

1
git reset --mixed HEAD

场景7. 被修改的文件已经commit提交,想撤销提交

1
git reset --soft HEAD^

场景8. 已经提交到远程主机的文件,想撤销

1
2
git revert <commit_id>
git revert HEAD

场景9. 已经开发一半的功能,但是没有开发完,这时候有个bug要紧急处理,需要放下手头的功能,赶去修改BUG

1
2
3
4
5
6
7
8

// 保存现场
git add .
git stash
// 恢复现场
git stash pop [stash_num]
或者恢复但不删除工作现场
git stash apply [stash_num]

场景10. 加入过历史版本的文件,因某些原因被删除了想恢复

1
git checkout <commit_id> -- <file_name>

另外你也可以用reset命令来完成

场景11. 需要单独把多次提交中的某一次提交从你的分支迁移到另外一个分支上,即跨分支应用commit

1
git cherry-pick <commit_id>

比如:我想把以下分支

1
2
3
A-B  master
\
C-D-E-F-G develop

中的D,F两次提交移动到master分支,而保持其他commit不变,结果就像这样

1
2
3
A-B-D-F  master
\
C-E-G develop

那么,思路是将D,F用cherry-pick应用到master分支上,然后将develop分支对master分支变基。

1
2
3
4
5
$ git checkout master  
$ git cherry-pick D
$ git cherry-pick F
$ git checkout develop
$ git rebase master

注意有些情况下使用cherry-pick会存在冲突,解决方法和我们平时合并分支遇到冲突一样。

场景12. 遇到文件冲突,可以手动解决,或者用你配置的工具解决,记得把文件标位resolved:add/rm

如:常见的拉取同事的代码合并引起冲突

1
2
3
4
1. 手动处理冲突
2. 文件标志位置为resolved:git add <file_name>
3. 继续合并 git merge --continue
当然也可以选择放弃合并:git merge --abort

场景13. 让自己本地分支上面的每一次提交日志变得更有意义,有时候需要我们选择有意义的提交日志信息合并上去

比如我们在bugfix分支上面由于修改bug提交了很多次,修复好了之后,我们想把这些提交合并入我们的master分支

1
2
3
git checkout master
git merge --squash bugfix
git commit -m "bug fixed"

上面操作会将bugfix分支上的所有commit都合并为一个commit,并把它并入我们的master分支上去。这里还有一点需要注意的是:–squash含义代表的是本地内容与不使用该选项的合并结果相同,但是不提交,不移动HEAD指针,所以我们要另外多一条语句来移动我们的HEAD指针,即最后的commit。

场景14. 有时候需要整理我们本地的commits,可以使用Squash

1
git rebase -i <commit>

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
git rebase -i HEAD~5

执行完后,Git会把所有commit列出来,让你进行一些修改,修改完成之后会根据你的修改来rebase。HEAD-5的意思是只修改最近的5个commit。

pick 033beb4 b1
pick b426a8a b2
pick c216era b3
pick d627c9a b4
pick e416c8b b5

# Rebase 033beb4..e416c8b onto 033beb4
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

上面pick是要执行的commit指令,另外还有reword、edit、squash、fixup、exec这5个,具体的含义可以看上面的注释解释,比较简单,这里就不说了。 我们要合并就需要修改前面的pick指令:

1
2
3
4
5
pick 033beb4 b1
squash b426a8a b2
squash c216era b3
squash d627c9a b4
squash e416c8b b5

也就是下面这4个提交合并到最前面的那个提交里面,按esc,打上:wq提交保存离开。 接着是输入新的commit message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
b
# This is a combination of 2 commits.
# The first commit's message is:
# b1
#
# This is the 2nd commit message:
#
# b2
#
# This is the 3rd commit message:
#
# b3
#
# This is the 4th commit message:
#
# b4
#
# This is the 5th commit message:
#
# b5
#
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: a.txt
#

其中第一行的b就是需要我们输入的新信息,同样编辑完保存,出现类似下面的信息:

1
Successfully rebased and updated refs/heads/develop.

最后可以用git log指令来验证commits是不是我们要变成的样子。

场景15. 多人协作开发项目,想知道某个文件的当前改动情况

通常查问题时想知道某个文件的某部分代码是谁改动的,那么git blame就派上用场了。

1
git blame <file_name>

你也可以具体指定到某一行或者某几行代码

1
git blame -L <start_line>,<end_line> <file_name>

场景16. 执行push命令向多个仓库同时提交代码

有时候会做代码备份,将代码保存在几个不同的Git代码管理平台,这时候就需要用到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
修改本地仓库目录下.git/config文件

[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:yuxingxin/blog.git
url = ……
url = ……
fetch = +refs/heads/*:refs/remotes/origin/*

如上在remote处可以添加多个远程地址。

场景17. 从多次提交中快速定位某一次提交的bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 开始 bisect
$ git bisect start

# 录入正确的 commit
$ git bisect good xxxxxx

# 录入出错的 commit
$ git bisect bad xxxxxx

# 然后 git 开始在出错的 commit 与正确的 commit 之间开始二分查找,这个过程中你需要不断的验证你的应用是否正常
$ git bisect bad
$ git bisect good
$ git bisect good
...

# 直到定位到出错的 commit,退出 bisect
$ git bisect reset

9 常用命令图解

后文中以下面的形式使用图片。绿色的5位字符表示提交的ID,分别指向父节点。分支用橘色显示,分别指向特定的提交。当前分支由附在其上的HEAD标识。 这张图片里显示最后5次提交,ed489是最新提交。 main分支指向此次提交,另一个stable分支指向祖父提交节点。

(1)Diff

有许多种方法查看两次提交之间的变动。下面是一些示例。

(2)Commit

提交时,git用暂存区域的文件创建一个新的提交,并把此时的节点设为父节点。然后把当前分支指向新的提交节点。下图中,当前分支是main。 在运行命令之前,main指向ed489,提交后,main指向新的节点f0cec并以ed489作为父节点。

即便当前分支是某次提交的祖父节点,git会同样操作。下图中,在main分支的祖父节点stable分支进行一次提交,生成了1800b。 这样,stable分支就不再是main分支的祖父节点。此时,合并 (或者 衍合) 是必须的。

如果想更改一次提交,使用 git commit --amend。git会使用与当前提交相同的父节点进行一次新提交,旧的提交会被取消。

(3)Checkout

​ checkout命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。

​ 当给定某个文件名(或者打开-p选项,或者文件名和-p选项同时打开)时,git会从指定的提交中拷贝文件到暂存区域和工作目录。比如,git checkout HEAD~ foo.c会将提交节点HEAD~(即当前提交节点的父节点)中的foo.c复制到工作目录并且加到暂存区域中。(如果命令中没有指定提交节点,则会从暂存区域中拷贝内容。)注意当前分支不会发生变化。

​ 当不指定文件名,而是给出一个(本地)分支时,那么HEAD标识会移动到那个分支(也就是说,我们“切换”到那个分支了),然后暂存区域和工作目录中的内容会和HEAD对应的提交节点一致。新提交节点(下图中的a47c3)中的所有文件都会被复制(到暂存区域和工作目录中);只存在于老的提交节点(ed489)中的文件会被删除;不属于上述两者的文件会被忽略,不受影响。

如果既没有指定文件名,也没有指定分支名,而是一个标签、远程分支、SHA-1值或者是像main~3类似的东西,就得到一个匿名分支,称作detached HEAD(被分离的HEAD标识)。这样可以很方便地在历史版本之间互相切换。比如说你想要编译1.6.6.1版本的git,你可以运行git checkout v1.6.6.1(这是一个标签,而非分支名),编译,安装,然后切换回另一个分支,比如说git checkout main。然而,当提交操作涉及到“分离的HEAD”时,其行为会略有不同,详情见在下面

(4)HEAD标识处于分离状态时的提交和检出操作

HEAD处于分离状态(不依附于任一分支)时,提交操作可以正常进行,但是不会更新任何已命名的分支。(你可以认为这是在更新一个匿名分支。)

一旦此后你切换到别的分支,比如说main,那么这个提交节点(可能)再也不会被引用到,然后就会被丢弃掉了。注意这个命令之后就不会有东西引用2eecb

但是,如果你想保存这个状态,可以用命令git checkout -b *name*来创建一个新的分支。

(5)Reset

reset命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。

如果不给选项,那么当前分支指向到那个提交。如果用–hard选项,那么工作目录也更新,如果用–soft选项,那么都不变。

如果没有给出提交点的版本号,那么默认用HEAD。这样,分支指向不变,但是索引会回滚到最后一次提交,如果用–hard选项,工作目录也同样。

如果给了文件名(或者 -p选项), 那么工作效果和带文件名的checkout差不多,除了索引被更新。

(6)Merge

merge命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。 另一种情况是如果当前提交是另一个分支的祖父节点,就导致fast-forward合并。指向只是简单的移动,并生成一个新的提交。

image-20210328174604793

否则就是一次真正的合并。默认把当前提交(ed489如下所示)和另一个提交(33104)以及他们的共同祖父节点(b325c)进行一次三方合并。结果是先保存当前目录和索引,然后和父节点33104一起做一次新提交。

(7)Cherry Pick

cherry-pick命令"复制"一个提交节点并在当前分支做一次完全一样的新提交。

(9)Rebase

衍合是合并命令的另一种选择。合并把两个父分支合并进行一次提交,提交历史不是线性的。衍合在当前分支上重演另一个分支的历史,提交历史是线性的。 本质上,这是线性化的自动的 cherry-pick

上面的命令都在topic分支中进行,而不是main分支,在main分支上重演,并且把分支指向新的节点。注意旧提交没有被引用,将被回收。

要限制回滚范围,使用--onto选项。下面的命令在main分支上重演当前分支从169a6以来的最近几个提交,即2c33a

同样有git rebase --interactive让你更方便的完成一些复杂操作,比如丢弃、重排、修改、合并提交。没有图片体现这些,细节看这里:git-rebase(1)

推荐链接

  1. 在线图解练习
  2. 图解Git
  3. shortcutFoo在线练习
  4. Pro Git Book
  5. Pro Git Book(Gitee)
  6. GitHub中文指南
  7. Git community book
  8. 猴子都能懂得Git入门
  9. Git菜单