Git 简介

Git是目前世界上最先进的分布式版本控制系统,可以记录每次文件的改动。

版本控制

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。

简单的版本控制图示:

简单的版本控制概念

版本控制发展史:

  1. 文件(多个文件的拷贝副本)
  2. 本地版本控制(单个文件)
  3. 集中式(SVN)
  4. 分布式(Git)

集中式与分布式

集中式版本控制系统

版本库是集中存放在中央服务器的(必须联网才能工作),而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。

20220919142746

分布式版本控制系统

分布式版本控制系统根本没有“中央服务器”(分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。),每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。

20220919143450

Git 安装

Windows上安装Git

  1. 在Git官网直接下载安装程序: https://git-scm.com/downloads
  2. 安装完成后,初始化全局用户信息,在命令行输入:
    1
    2
    3
    # 注意git config命令的--global参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。
    $ git config --global user.name "Your Name"
    $ git config --global user.email "email@example.com"

Git 版本库

版本库又名仓库,英文名 repository,可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

创建版本库

  1. 使用终端进入需要被 git 管理的目录
  2. 通过 git init 命令把这个目录变成 Git 可以管理的仓库(可以发现当前目录下多了一个.git的目录,这个目录是 Git 来跟踪管理版本库的)

文件添加到版本库

所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。

  1. 编写一个 README.md 文件,并放到被 git 管理的目录下(子目录也行),因为这是一个Git仓库,放到其他地方 Git 再厉害也找不到这个文件。
    1
    2
    Git is a version control system.
    Git is free software.
  2. 把文件放到Git仓库
    1. 第一步,用命令 git add 告诉 Git,把文件添加到仓库:
      1
      $ git add README.md
    2. 第二步,用命令 git commit 告诉Git,把文件提交到仓库:
      1
      $ git commit -m "wrote a readme file"

Git 时光机穿梭

工作区和暂存区

20220921230311

工作区(Working Directory)

工作区就是在电脑里被Git管理的目录。(比如 LearnGit 文件夹就是一个工作区)

20220919155758

版本库(Repository)

工作区有一个隐藏目录 .git ,这个不算工作区,而是Git的版本库。

Git 的版本库里存了很多东西,其中最重要的就是称为 stage暂存区
还有Git为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫HEAD

20220919155936

实践

  1. README.md 做个修改,比如加上一行内容:
    1
    2
    3
    Git is a distributed version control system.
    Git is free software distributed under the GPL.
    Git has a mutable index called stage.
  2. 在工作区新增一个 LICENSE 文本文件(内容随便写)。
  3. git status 查看一下状态:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # Git非常清楚地告诉我们,README.md 被修改了,而 LICENSE 还从来没有被添加过,所以它的状态是 Untracked。

    $ git status
    On branch master
    nothing to commit, working tree clean

    Laity@LAPTOP-H514R4JD MINGW64 /e/DevTools/Git/LearnGit (master)
    $ git status
    On branch master
    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)
    LICENSE

    no changes added to commit (use "git add" and/or "git commit -a")
  4. 使用两次命令 git add(git add命令实际上就是把要提交的所有修改放到暂存区) ,把 README.md 和 LICENSE 都添加后,用 git status 再查看一下:
    1
    2
    3
    4
    5
    6
    $ git status
    On branch master
    Changes to be committed:
    (use "git restore --staged <file>..." to unstage)
    new file: LICENSE
    modified: README.md
    现在,暂存区的状态就变成这样了:
    20220919161202
  5. 执行 git commit 就可以一次性把暂存区的所有修改提交到分支。
    1
    2
    3
    4
    $ git commit -m "understand how stage works"
    [master 75d4cdd] understand how stage works
    2 files changed, 8 insertions(+), 1 deletion(-)
    create mode 100644 LICENSE
  6. 一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
    1
    2
    3
    $ git status
    On branch master
    nothing to commit, working tree clean

现在版本库变成了这样,暂存区就没有任何内容了:
20220919161520

版本回退

在 Git中每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为 commit
一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit 恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

git log 命令

git log 命令显示从最近到最远的 commit 提交日志(查看commit id)。

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 5941954c7a1243084b61871c2ad2f57f88a3a331 (HEAD -> master)
Author: Frank <frank000908@gmail.com>
Date: Mon Sep 19 15:05:34 2022 +0800

append GPL

commit 8bf2f95c8947bf355763e9e6805bf00f2bd3fa12
Author: Frank <frank000908@gmail.com>
Date: Mon Sep 19 15:04:11 2022 +0800

add distributed

如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline 参数:(可以看到的一大串类似 5941954c7… 的是 commit id(版本号)是一个 SHA1 计算出来的一个非常大的数字,用十六进制表示。)

1
2
3
4
$ git log --pretty=oneline

# 拓展命令:%h 代表哈希值 %s 代表提交的记录
$ git log --graph --pretty=format:"%h %s"

回退版本

语法:

1
git reset --hard <commit id>

回退版本

  1. 在 Git中,用 HEAD 表示当前版本,上一个版本就是 HEAD^ ,上上一个版本就是 HEAD^^ ,往上100个版本可以写成 HEAD~100
    要把当前版本回退到上一个版本可以使用 git reset 命令:
    1
    2
    $ git reset --hard HEAD^
    HEAD is now at e475afc add distributed
  2. 可以使用 commit id (用 git log 获取 commit id)回到指定的某个版本:(版本号没必要写全,前几位就可以了,Git会自动去找。)
    1
    2
    $ git reset --hard 5941954c
    HEAD is now at 83b0afe append GPL

回退版本后悔药

假如你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 commit id 怎么办?
Git提供了一个命令 git reflog 用来记录你的每一次命令:

  1. 使用 git reflog 命令查找之前的 commit id
  2. git reset —hard

管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件

每次修改,如果不用 git add 到暂存区,那就不会加入到 commit 中。

git diff HEAD -- 文件名 命令可以查看工作区和版本库里面最新版本的区别

撤销修改

场景一:撤销工作区修改

当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时:

  1. 手动删除工作区的文件。
  2. git checkout -- file 把文件在工作区的修改全部撤销(git checkout 其实是用版本库里的版本替换工作区的版本)

场景二:撤销暂存区修改

当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改

解决方案:

  1. 用命令 git reset HEAD <file>,回到工作区的修改
  2. 按场景一操作。(使用:git checkout -- file 把文件在工作区的修改全部撤销)

场景三:撤销版本库修改

已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

删除/复原文件

场景一:删除暂存区的文件

已经提交到暂存区的文件想要删除暂存区的文件和本地文件:

  1. 在文件管理器中把没用的文件删了。
  2. 工作区和版本库就不一致了,git status 命令会立刻告诉你哪些文件被删除了
  3. 用命令 git rm <file> 从版本库中删除该文件,并且 git commit

场景二:复原文件

已经提交到暂存区的文件,误删了本地文件,想要复原。(注意:从来没有被添加到版本库就被删除的文件,是无法恢复的!)

  1. 文件管理器中文件被误删了。
  2. 因为文件已经被提交到暂存区了,版本库里有,所以可以很轻松地把误删的文件恢复到最新版本:
    1
    2
    # git checkout 其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
    $ git checkout -- <file>

Git 远程仓库

场景:一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。
好在有个叫 GitHub 的网站,这个网站就是提供 Git仓库托管服务的,所以,只要注册一个 GitHub账号,就可以免费获得 Git远程仓库。

  1. 创建SSH Key (由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的)在用户主目录下,看看有没有 .ssh 目录,如果有,再看看这个目录下有没有 id_rsaid_rsa .pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:
    1
    $ ssh-keygen -t rsa -C "youremail@example.com"
    如果一切顺利的话,可以在用户主目录里找到 .ssh 目录,里面有 id_rsaid_rsa.pub 两个文件,这两个就是SSH Key的秘钥对, id_rsa 是私钥,不能泄露出去, id_rsa.pub 是公钥,可以放心地告诉任何人。
  2. 登陆GitHub,打开“Account settings”,“SSH Keys”页面:点“New SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容:
    20220919210413

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。

当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。

添加远程库

已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

  1. 登陆GitHub,创建一个新的仓库
  2. 这个已经创建的仓库还是空的,GitHub告诉我们:
    1. 可以从这个仓库克隆出新的仓库。(参考:从远程库克隆 一节)
    2. 也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
      1. 在本地需要被推送的仓库下运行命令:
        1
        2
        # 关联远程库
        $ git remote add origin git@github.com:<Github用户名>/<仓库名称>.git
        添加后,远程库的名字就是 origin ,这是Git默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。
      2. 把本地库的所有内容推送到远程库上:
        1
        2
        # 用git push命令,实际上是把当前分支master推送到远程。
        $ git push -u origin master
        由于远程库是空的,我们第一次推送 master 分支时,加上了 -u 参数,Git不但会把本地的 master 分支内容推送的远程新的 master 分支,还会把本地的 master 分支和远程的 master 分支关联起来,在以后的推送或者拉取时就可以简化命令 (git push origin master)。

从远程库克隆

要克隆一个仓库,首先必须知道仓库的地址,然后使用 git clone 命令克隆:

1
$ git clone git@github.com:<Github用户名>/<仓库名称>.git

Git支持多种协议,包括 https,但 ssh协议 速度最快。

Git 分支管理

分支在实际中有什么用呢?
假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。

现在有了分支,就不用怕了。你创建了一个属于你自己的分支(相对于主分支这个分支是独立的),别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

分支操作命令

查看分支:git branch

创建分支:git branch <name>

切换分支:git checkout <name> 或者 git switch <name>

创建 + 切换分支:git checkout -b <name> 或者 git switch -c <name>

合并某分支到当前分支:git merge <name> (注意:切换分支再合并

删除分支:git branch -d <name>

分支的概念

在 Git里,master 为默认分支。master 分支是一条线,Git用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点:

20220919215828

每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

20220919215936

从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:

20220919220429

假如我们在dev上的工作完成了,就可以把dev合并到master上。
Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:

20220919220454

合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:

20220919220530

创建dev分支,然后切换到dev分支:

1
2
3
4
5
$ git checkout -b dev

# git checkout 命令加上 -b 参数表示创建并切换,相当于以下两条命令:
$ git branch dev
$ git checkout dev

git branch 命令会列出所有分支,当前分支前面会标一个 * 号。

然后,我们就可以在dev分支上正常提交,比如对 README.md 做个修改,加上一行:

1
Creating a new branch is quick.

然后提交:

1
2
$ git add README.md
$ git commit -m "branch test"

现在,dev分支的工作完成,我们就可以切换回master分支:

1
$ git checkout master

切换回master分支后,再查看一个README.md文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变。

现在,我们把dev分支的工作成果合并到master分支上:

1
2
3
4
5
6
7
8
9
# git merge 命令用于合并指定分支到当前分支。
$ git merge dev
Updating 5941954..d40742b
Fast-forward
test.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 test.txt

# 注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。

合并完成后,就可以放心地删除dev分支了:

1
$ git branch -d dev

删除后,查看branch,就只剩下master分支了:

1
2
$ git branch
* master

因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。

switch 命令:

我们注意到切换分支使用 git checkout <branch> ,而前面讲过的撤销修改则是 git checkout -- <file> ,同一个命令,有两种作用,确实有点令人迷惑。

实际上,切换分支这个动作,用switch更科学。因此,最新版本的Git提供了新的 git switch 命令来切换分支:

创建并切换到新的dev分支,可以使用:

1
$ git switch -c dev

直接切换到已有的master分支,可以使用:

1
$ git switch master

解决冲突

当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

冲突场景:

当两个分支(feature1 和 master)都对同一个文件(如:readme.txt )进行 修改 并 commit。例如:

20220919225737

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:

1
2
3
4
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

果然冲突了!Git告诉我们,readme.txt 文件存在冲突,必须手动解决冲突后再提交git status 也可以告诉我们冲突的文件:

1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")

可以直接使用 cat 命令查看 readme.txt 的内容:

1
2
3
4
5
6
7
8
9
10
$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

Git用 <<<<<<<=======>>>>>>> 标记出不同分支的内容,我们修改如下后保存:

1
Creating a new branch is quick and simple.

再提交:

1
2
3
$ git add readme.txt 
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed

现在,master 分支和 feature1 分支变成了下图所示:

20220919230313

用带参数的 git log 也可以看到分支的合并情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 用 git log --graph 命令可以看到分支合并图。
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file

分支管理策略

Git分支十分强大,在团队开发中应该充分应用。

通常,合并分支时,如果可能,Git会用 Fast forward 模式,但这种模式下,删除分支后,会丢掉分支信息

如果要强制禁用 Fast forward 模式,加上 --no-ff 参数就可以用普通模式合并,Git就会在 merge生成一个新的commit,这样,从分支历史上就可以看出分支信息。

禁用 Fast forward 演示

首先,仍然创建并切换dev分支:

1
2
$ git switch -c dev
Switched to a new branch 'dev'

修改 readme.txt 文件,并提交一个新的 commit:

1
2
3
4
$ git add readme.txt 
$ git commit -m "add merge"
[dev f52c633] add merge
1 file changed, 1 insertion(+)

现在,我们切换回 master:

1
2
$ git switch master
Switched to branch 'master'

准备合并dev分支,请注意 --no-ff 参数,表示禁用 Fast forward:

1
2
3
4
5
# 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
readme.txt | 1 +
1 file changed, 1 insertion(+)

合并后,我们用git log看看分支历史:

1
2
3
4
5
6
7
$ git log --graph --pretty=oneline --abbrev-commit
* e1e9c68 (HEAD -> master) merge with no-ff
|\
| * f52c633 (dev) add merge
|/
* cf810e4 conflict fixed
...

可以看到,不使用 Fast forward 模式,merge 后就像这样:

20220919232230

分支策略

在实际开发中,应该按照几个基本原则进行分支管理:

  1. master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
  2. 干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
  3. 你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

20220919232332

Bug 分支

修复bug时,我们会通过创建新的 bug 分支进行修复,然后合并,最后删除;

当手头工作没有完成时,先把工作现场 git stash 一下,然后去修复 bug,修复后,再 git stash pop ,回到工作现场;

在 master 分支上修复的 bug,想要合并到当前 dev分支,可以用 git cherry-pick <commit id> 命令,把 bug提交的修改“复制”到当前分支,避免重复劳动。

Bug 分支演示

当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支 issue-101 来修复它。
但是,等等,当前正在dev上进行的工作还没有提交,并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?

幸好,Git 还提供了一个 stash 功能,可以把当前工作现场(暂存区)“储藏”起来,等以后恢复现场后继续工作:

1
2
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge

首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:

1
2
3
4
$ git add readme.txt 
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)

修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:

1
2
3
4
5
6
7
8
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
readme.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

现在,是时候接着回到dev分支干活了

1
2
3
4
5
$ git switch dev
Switched to branch 'dev'
$ git status
On branch dev
nothing to commit, working tree clean

工作区是干净的,刚才的工作现场存到哪去了?用 git stash list 命令看看:

1
2
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:

  1. git stash apply 恢复,但是恢复后,stash 内容并不删除,你需要用 git stash drop 来删除;
  2. git stash pop ,恢复的同时把 stash 内容也删了:

再用 git stash list 查看,就看不到任何 stash 内容了:

1
$ git stash list

你可以多次 stash ,恢复的时候,先用 git stash list 查看,然后恢复指定的 stash。用命令:

1
$ git stash apply stash@{0}

在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在

同样的bug,要在dev上修复,我们只需要把 4c805e2 fix bug 101 这个提交所做的修改“复制”到dev分支。
注意:我们只想复制 4c805e2 fix bug 101 这个提交所做的修改,并不是把整个master分支merge过来。

为了方便操作,Git专门提供了一个 cherry-pick 命令,让我们能复制一个特定的提交到当前分支:

1
2
3
4
5
6
$ git branch
* dev
master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)

Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。
git cherry-pick ,我们就不需要在dev分支上手动再把修bug的过程重复一遍。

Feature 分支

开发一个新 feature,最好新建一个分支;(你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。)

如果要丢弃一个没有被合并过的分支,可以通过 git branch -D <name> 强行删除

Feature 分支演示

你接到了一个新任务:开发代号为 Vulcan 的新功能,该功能计划用于下一代星际飞船。

于是准备开发:

1
2
$ git switch -c feature-vulcan
Switched to a new branch 'feature-vulcan'

a moments later… 开发完毕:

1
2
3
4
5
6
7
8
9
10
$ git add vulcan.py
$ git status
On branch feature-vulcan
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: vulcan.py
$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
1 file changed, 2 insertions(+)
create mode 100644 vulcan.py

切回 dev ,准备合并:

1
$ git switch dev

但是!

就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:

1
2
3
$ git branch -d feature-vulcan
error: The branch 'feature-vulcan' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.

销毁失败。Git友情提醒,feature-vulcan 分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的 -D 参数。

现在我们强行删除:

1
2
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 287773e).

多人协作

当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是 origin

  1. git remote 查看远程库的信息
  2. git remote -v 显示远程库更详细的信息
    1
    2
    3
    4
    # 如果没有推送权限,就看不到push的地址。
    $ git remote -v
    origin git@github.com:HaloBoys/LearnGit.git (fetch)
    origin git@github.com:HaloBoys/LearnGit.git (push)

补充:

删除已关联的名为origin的远程库:git remote rm origin

远程仓库的默认名称是 origin,如果有多个远程库,我们需要用不同的名称来标识不同的远程库

1
2
3
4
$ git remote add <自定义远程仓库名称> git@gitxxx.com:<用户名>/<仓库名>.git

# 演示
$ git remote add github git@github.com:HaloBoys/LearnGit.git

推送分支

语法:

1
$ git push origin <分支名称>

分支推送原则:

  • master 分支是主分支,因此要时刻与远程同步;
  • dev 分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug 分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature 分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

抓取分支

语法:

1
$ git clone git@github.com:<Github用户名>/<仓库名称>.git

默认情况下 clone的仓库,只能显示 master 分支(虽然不显示其他分支,但是也可以直接使用命令切换到对应分支),可以在本地创建和远程分支对应的分支:

1
$ git checkout -b <分支名称> origin/<分支名称>

现在,就可以在分支上进行修改。

代码冲突

多人协作可能会出现这种问题:多个人对同一个文件进行修改并提交,后提交的人会提交失败,因为与远程文件有冲突,需要解决冲突

解决方案:

  1. git pull 把最新的提交从分支抓下来
    1. 如果 git pull 失败,提示 no tracking information,原因是没有指定本地分支与远程分支的关联,使用命令 git branch --set-upstream-to <branch-name> origin/<branch-name>
    2. 拓展:git pull 命令相当于执行了 git fetchgit merge 操作
  2. 在本地合并,手动解决冲突,提交,再 push

git flow 工作流

Gitflow 工作流 (Gitflow Workflow) 是2010年由Vincent Driessen在他的一篇博客里提出来的。
它定义了一整套完善的基于Git分支模型的框架,结合了版本发布的研发流程,适合管理具有固定发布周期的大型项目

git flow

Pull Request

Pull Request 是自己修改源代码后,请求对方仓库采纳该修改时采取的一种行为。

参与开源项目

  1. 在 github fork 开源仓库(相当于把开源代码拷贝一份到自己的 github 账号)
  2. 在自己的 github 账号下 clone 并参与提交/开发
  3. 如果觉得自己开发的还不错,可以提交一个 Pull Request 申请给开源项目的作者。

Rebase

Git有一种称为 rebase 的操作可以让提交历史是一条干净的直线(变得更加简洁)

场景一:多个记录整合成一个记录

语法:

1
2
3
4
5
# 方法一:将当前的记录和指定 commit id 这个区间合并成一个记录
$ git rebase -i <commit id>

# 方法二:通过 HEAD~n 参数;指定从当前的记录找最近的 n 条记录进行合并
$ git rebase -i <HEAD~n>

场景演示:

  1. 模拟多个提交版本
    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

    $ touch 1.js

    $ git add 1.js

    $ git commit -m "v1"
    [master (root-commit) b6c0387] v1
    1 file changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 1.js

    $ touch 2.js

    $ git add 2.js

    $ git commit -m "v2"
    [master b139149] v2
    1 file changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 2.js

    $ touch 3.js

    $ git add 3.js

    $ git commit -m "v3"
    [master 462c44c] v3
    1 file changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 3.js
  2. 使用 git rebase 命令指定合并记录
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $ git log
    commit 462c44c7f2ce7f3e0e21f42312c29427c7dad0ad (HEAD -> master)
    Author: Frank <frank000908@gmail.com>
    Date: Thu Sep 22 14:36:17 2022 +0800

    v3

    commit b1391492c657fd3d94abd80715c9e76f7a501b50
    Author: Frank <frank000908@gmail.com>
    Date: Thu Sep 22 14:36:05 2022 +0800

    v2

    commit b6c038759462e4e6589c934850f78b421906531f
    Author: Frank <frank000908@gmail.com>
    Date: Thu Sep 22 14:35:44 2022 +0800

    v1

    $ git rebase -i b6c038759462e4e6589c934850f78b421906531f
  3. 跳转到 vim 编辑界面
    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
    pick b139149 v2
    pick 462c44c v3

    # Rebase b6c0387..462c44c onto b6c0387 (2 commands)
    #
    # Commands:
    # p, pick <commit> = use commit
    # r, reword <commit> = use commit, but edit the commit message
    # e, edit <commit> = use commit, but stop for amending
    # s, squash <commit> = use commit, but meld into previous commit
    # f, fixup <commit> = like "squash", but discard this commit's log message
    # x, exec <command> = run command (the rest of the line) using shell
    # b, break = stop here (continue rebase later with 'git rebase --continue')
    # d, drop <commit> = remove commit
    # l, label <label> = label current HEAD with a name
    # t, reset <label> = reset HEAD to a label
    # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
    # . create a merge commit using the original merge commit's
    # . message (or the oneline, if no original merge commit was
    # . specified). Use -c <commit> to reword the commit message.
    #
    # These lines can be re-ordered; they are executed from top to bottom.
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    #
    # However, if you remove everything, the rebase will be aborted.
    #
    # Note that empty commits are commented out
    修改并 esc + :wq 保存:
    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
    pick b139149 v2
    s 462c44c v3

    # Rebase b6c0387..462c44c onto b6c0387 (2 commands)
    #
    # Commands:
    # p, pick <commit> = use commit
    # r, reword <commit> = use commit, but edit the commit message
    # e, edit <commit> = use commit, but stop for amending
    # s, squash <commit> = use commit, but meld into previous commit
    # f, fixup <commit> = like "squash", but discard this commit's log message
    # x, exec <command> = run command (the rest of the line) using shell
    # b, break = stop here (continue rebase later with 'git rebase --continue')
    # d, drop <commit> = remove commit
    # l, label <label> = label current HEAD with a name
    # t, reset <label> = reset HEAD to a label
    # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
    # . create a merge commit using the original merge commit's
    # . message (or the oneline, if no original merge commit was
    # . specified). Use -c <commit> to reword the commit message.
    #
    # These lines can be re-ordered; they are executed from top to bottom.
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    #
    # However, if you remove everything, the rebase will be aborted.
    #
    # Note that empty commits are commented out
  4. 保存后跳转到编写提交信息窗口
    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
    # This is a combination of 2 commits.
    # This is the 1st commit message:

    v2

    # This is the commit message #2:

    v3

    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    #
    # Date: Thu Sep 22 14:36:05 2022 +0800
    #
    # interactive rebase in progress; onto b6c0387
    # Last commands done (2 commands done):
    # pick b139149 v2
    # squash 462c44c v3
    # No commands remaining.
    # You are currently rebasing branch 'master' on 'b6c0387'.
    #
    # Changes to be committed:
    # new file: 2.js
    # new file: 3.js
    #
    编写提交信息 :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # This is a combination of 2 commits.
    # This is the 1st commit message:

    v2 & v3

    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    #
    # Date: Thu Sep 22 14:36:05 2022 +0800
    #
    # interactive rebase in progress; onto b6c0387
    # Last commands done (2 commands done):
    # pick b139149 v2
    # squash 462c44c v3
    # No commands remaining.
    # You are currently rebasing branch 'master' on 'b6c0387'.
    #
    # Changes to be committed:
    # new file: 2.js
    # new file: 3.js
    #
  5. 变基完成!v2 和 v3 合并成一条记录:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $ git log
    commit a3edd69fcfb5b7fe6a58017330791a437576033e (HEAD -> master)
    Author: Frank <frank000908@gmail.com>
    Date: Thu Sep 22 14:36:05 2022 +0800

    v2 & v3

    commit b6c038759462e4e6589c934850f78b421906531f
    Author: Frank <frank000908@gmail.com>
    Date: Thu Sep 22 14:35:44 2022 +0800

    v1

注意:合并记录建议不要合并已经push到远程的记录

场景二:解决:多分枝合并产生分叉

每次合并再 push 会让分支产生分叉,结构看上去很乱。git rebase 命令也可以将分叉提交历史整理成直线

语法:

1
$ git rebase <分支名称>

场景演示:

目前有两个分支,master 和 dev,现在他们需要合并且不产生分叉

master 和 dev 分支:

1
2
3
4
5
6
7
8
9
# master 分支
Laity@LAPTOP-H514R4JD MINGW64 /e/DevTools/Git/GitRebase (master)
$ ls
1.js 2.js 3.js master1.js

# dev分支
Laity@LAPTOP-H514R4JD MINGW64 /e/DevTools/Git/GitRebase (dev)
$ ls
1.js 2.js 3.js dev1.js
  1. 切换到 dev 分支,执行 git rebase master
    1
    2
    3
    $ git rebase master
    First, rewinding head to replay your work on top of it...
    Applying: dev1
  2. 切换到 master 分支,执行 git merge dev
    1
    2
    3
    4
    5
    6
    $ git merge dev
    Updating e7bc88f..ec10061
    Fast-forward
    dev1.js | 0
    1 file changed, 0 insertions(+), 0 deletions(-)
    create mode 100644 dev1.js
  3. 变基完成!master 和 dev 分支合并

场景三:解决 git pull 默认产生的分叉

当使用 git pull 命令从远程仓库拉取代码的时候,会默认与本地版本合并,会产生一个分叉。

解决方法:将 git pull 命令替换成 git fetchgit rebase

语法:

1
2
$ git fetch origin 分支名称
$ git rebase origin/分支名称

补充:git rebase 冲突时解决方案

  1. 手动解决冲突
  2. 使用 git rebase --continue 命令继续合并

Git 标签管理

tag 就是一个让人容易记住的有意义的名字,它跟某个 commit 绑在一起。

创建标签

在 Git 中打标签非常简单,首先,切换到需要打标签的分支上,然后,敲命令 git tag <name> 就可以打一个新标签,可以用命令 git tag 查看所有标签(标签不是按时间顺序列出,而是按字母排序的)。

默认标签是打在最新提交的 commit(HEAD) 上的。也可以给过去的某次提交打标签,找到历史提交的 commit id ,然后打上就可以了:

1
2
3
4
5
# 查询历史提交的 commit id
$ git log --pretty=oneline --abbrev-commit

# 给指定 commit id 打标签
$ git tag <name> <commit id>

可以用 git show <tagname> 查看某个标签信息:

1
2
3
4
5
6
$ git show v1.0.0
commit 6e7a0b80f7c096b071efe5b0bd4f8ff4e812eed3 (HEAD -> main, tag: v1.0.0, origin/main)
Author: Frank <frank000908@gmail.com>
Date: Sun Sep 18 23:22:33 2022 +0800

v1.0.0

还可以创建带有说明的标签,用 -a 指定标签名,-m 指定说明文字:

1
$ git tag -a v0.1 -m "version 0.1 released" [commit id]

操作标签

git push origin <tagname> 可以推送一个本地标签;
git push origin --tags 可以推送全部未推送过的本地标签;
git tag -d <tagname> 可以删除一个本地标签;
git push origin :refs/tags/<tagname> 可以删除一个远程标签。

Git 自定义

Beyond Compare

Beyond Compare 是个快速解决冲突的软件

  1. 安装 Beyond Compare
  2. 在 git 中配置
    1
    2
    3
    git config --local merge.tool bc3
    git config --local mergetool. path 'Beyond Compare 的安装路径'
    git config --local mergetool.keepBackup false
  3. 应用 Beyond Compare 解决冲突
    1
    git mergetool

Github 免密登录

在Github中有三种实现免密登录的方式:

  1. URL
  2. SSH 实现(常用)
  3. Git自动管理凭证

SSH 实现

  1. 生成公钥和私钥(默认放在 ~/.ssh 目录下,id_rsa.pub公钥、id_rsa私钥)
    1
    ssh-keygen
  2. 拷贝公钥的内容,并设置到github中。
  3. 在git本地中配置ssh地址
    git remote add origin git@github.com:xxx/xxx.git
  4. 以后使用
    1
    git push origin <branch name>

Git 显示颜色

1
$ git config --global color.ui true

.gitignore

在Git 工作区的根目录下创建一个特殊的 .gitignore 文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。

gitignore 文件模板:https://github.com/github/gitignore

强制添加被 .gitignore 忽略的文件,可以用 -f 强制添加到Git:

1
$ git add -f <文件名>

检查某个文件被 .gitignore 中的哪一行命令所忽略:

演示:

1
2
$ git check-ignore -v App.class
.gitignore:3:*.class App.class # Git 告诉我们,.gitignore的第3行规则忽略了该文件

配置别名

1
2
3
4
$ git config --global alias.<自定义别名> <Git命令>
# 演示
$ git config --global alias.st status
$ git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

配置文件:

每个仓库的Git配置文件都放在 .git/config 文件中:
别名就在 [alias] 后面,要删除别名,直接把对应的行删掉即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = git@github.com:HaloBoys/LearnGit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[branch "dev"]
remote = origin
merge = refs/heads/dev
[alias]
lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

Git 命令速查

命令 功能
配置
git init 新建一个Git代码库
git clone [url] 下载一个项目和它的整个代码历史
git config —list 显示当前的Git配置
git config [—global] user.name “[name]” 设置提交代码时的用户信息
git config [—global] user.email “[email address]” 设置提交代码时的用户信息
增加/删除文件
git add . 添加当前目录的所有文件到暂存区
git add [file1] [file2] … 添加指定文件到暂存区
git add [dir] 添加指定目录到暂存区,包括子目录
git rm [file1] [file2] … 删除工作区文件,并且将这次删除放入暂存区
代码提交
git commit -m [message] 提交暂存区到仓库区
git commit [file1] [file2] … -m [message] 提交暂存区的指定文件到仓库区
git commit -v 提交时显示所有diff信息
分支
git branch 列出所有本地分支
git branch -r 列出所有远程分支
git branch -a 列出所有本地分支和远程分支
git branch [branch-name] 新建一个分支,但依然停留在当前分支
git checkout -b [branch] 新建一个分支,并切换到该分支
git checkout - 切换到上一个分支
git merge [branch] 合并指定分支到当前分支
git cherry-pick [commit] 选择一个commit,合并进当前分支
git branch -d [branch-name] 删除分支
git push origin —delete [branch-name] 删除远程分支
标签
git tag 列出所有tag
git tag [tag] 新建一个tag在当前commit
git tag [tag] [commit] 新建一个tag在指定commit
git tag -d [tag] 删除本地tag
git push origin :refs/tags/[tagName] 删除远程tag
git show [tag] 查看tag信息
git push [remote] [tag] 提交指定tag
git push [remote] —tags 提交所有tag
git checkout -b [branch] [tag] 新建一个分支,指向某个tag
查看信息
git status 显示有变更的文件
git log 显示当前分支的版本历史
git log —stat 显示commit历史,以及每次commit发生变更的文件
git log -S [keyword] 搜索提交历史,根据关键词
git log -p [file] 显示指定文件相关的每一次diff
git log -5 —pretty —oneline 显示过去5次提交
git shortlog -sn 显示所有提交过的用户,按提交次数排序
git blame [file] 显示指定文件是什么人在什么时间修改过
git diff 显示暂存区和工作区的差异
git diff —shortstat “@{0 day ago}” 显示今天你写了多少行代码
git reflog 显示当前分支的最近几次提交
远程同步
git fetch [remote] 下载远程仓库的所有变动
git remote -v 显示所有远程仓库
git remote show [remote] 显示某个远程仓库的信息
git remote add [shortname] [url] 增加一个新的远程仓库,并命名
git pull [remote] [branch] 取回远程仓库的变化,并与本地分支合并
git push [remote] [branch] 上传本地指定分支到远程仓库
git push [remote] —force 强行推送当前分支到远程仓库,即使有冲突
git push [remote] —all 推送所有分支到远程仓库
撤销
git checkout . 恢复暂存区的所有文件到工作区
git checkout [file] 恢复暂存区的指定文件到工作区
git checkout [commit] [file] 恢复某个commit的指定文件到暂存区和工作区
git reset [file] 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset —hard 重置暂存区与工作区,与上一次commit保持一致
git reset [commit] 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset —hard [commit] 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset —keep [commit] 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git revert [commit] 新建一个commit,用来撤销指定commit
git stash 暂时将未提交的变化移除,稍后再移入

Todo

git 解决冲突

回退版本

多人协作的流程