版本控制Git学习——两万字超详细讲解

Git 基础理论

什么是“版本控制”?

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 如果你是位图形或网页设计师,可能会需要保存某一幅图片或页面布局文件的所有修订版本,采用版本控制系统(CVS)是个明智的选择。 有了它你就可以将选定的文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。

本地版本控制系统
image.png
本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。其中最流行的一种叫做 RCS,现今许多计算机系统上都还看得到它的踪影。RCS的工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。
集中化的版本控制系统
image.png
如何让在不同系统上的开发者协同工作? 于是,集中化的版本控制系统(Centralized Version Control Systems,CVCS)应运而生。 这类系统,诸如 CVS、Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。 多年以来,这已成为版本控制系统的标准做法。
集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快。可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟。如果中央服务器宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。 如果中心数据库所在的磁盘发生损坏,又没有做恰当备份,毫无疑问你将丢失所有数据——包括项目的整个变更历史,只剩下人们在各自机器上保留的单独快照。 本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。
分布式版本控制系统
image.png
于是分布式版本控制系统(Distributed Version Control System,DVCS)面世了。 在这类系统中,像 Git、Mercurial、Bazaar 以及 Darcs 等,客户端并不只提取最新版本的文件快照, 而是把代码仓库完整地镜像下来,包括完整的历史记录。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
更进一步,许多这类系统都可以指定和若干不同的远端代码仓库进行交互。籍此,就可以在同一个项目中,分别和不同工作小组的人相互协作。 你可以根据需要设定不同的协作流程,比如层次模型式的工作流,而这在以前的集中式系统中是无法实现的。

Git的介绍

Git工作区域:

  • 工作区(Working Directory)
  • 暂存区(Index/Stage)
  • Git本地仓库(Git local Repository)
  • Git远程仓库(Git remote Repository)

(1)工作区:添加、编辑、修改文件等操作 (自己能看到的本地目录)
(2)暂存区:暂存已修改的文件,最后会统一提交到Git仓库中 (git add所在的区域)
(3)Git仓库:最终确定的文件保存到Git仓库成为一个新的版本 (git push到的区域)

工作区

在本地还有开发者的工作区 Workspace 和暂存区 Index / Stage。工作区是你能看到的目录:
image.png

暂存区-版本库(Repository)

工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
暂存区是存放在".git"文件夹下的index文件中,会记录 git add 添加文件的相关信息,可以用 git status 查看暂存区状态。

Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
image.png
把文件往Git版本库里添加的时候,是分两步执行的:

  1. git add把文件添加进去,实际上就是把文件修改添加到暂存区
  2. git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支

因为创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以,现在,git commit就是往master分支上提交更改。
对readme.txt做个修改,比如加上一行内容:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

然后,在工作区新增一个LICENSE文本文件
用git status查看一下状态:

$ 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.txt

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")

Git非常清楚地告诉我们,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked。
现在,使用两次命令git add,把readme.txt和LICENSE都添加后,用git status再查看一下:

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git add readme.txt LICENSE

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   LICENSE
        modified:   readme.txt

现在,暂存区的状态就变成这样了:
image.png
所以,git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。

$ git commit -m "understand how stage works"
[master 0db95d5] understand how stage works
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 LICENSE

一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:

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

现在版本库变成了这样,暂存区就没有任何内容了:
image.pngimage.png
Git管理的文件三种状态对应Git工作流程:
Git 有三种状态,你的文件可能处于其中之一: 已提交(committed)已修改(modified)已暂存(staged)

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

Git工作流程
(1)在工作区目录中添加、修改、删除文件
(2)将需要进行版本管理的文件放入暂存区**.git**
(3)将暂存区的文件提交到Git仓库中
如果 Git 目录中保存着特定版本的文件,就属于 已提交 状态。 如果文件已修改并放入暂存区,就属于 已暂存 状态。 如果自上次检出后,作了修改但还没有放到暂存区域,就是 已修改 状态。

Git安装与配置

Git 安装

在Linux上安装

$ sudo apt install git-all

要了解更多选择,Git 官方网站上有在各种 Unix 发行版的系统上安装步骤:
Git 官方网站:https://git-scm.com/download/linux

在 Windows 上安装
在Windows上使用Git,可以从Git官网直接下载:
https://git-scm.com/downloads

Git 配置

配置用户信息
安装完 Git 之后,要做的第一件事就是设置你的用户名和邮件地址。 这一点很重要,因为每一个 Git 提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改:

git config --global user.name "ldf2022"
git config --global user.email "1304018570@qq.com"

如果使用了 **–global **选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用那些信息。 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来配置。

检查配置信息
** **如果想要检查你的配置,可以使用 **git config --list 命令来列出所有 Git 当时能找到的配置。 **

git config -l

image.png
获取帮助
** **若你使用 Git 时需要获取帮助,有三种等价的方法可以找到 Git 命令的综合手册(manpage):

$ git help <verb>
$ git <verb> --help
$ man git-<verb>

$ git help config

Git 配置SSH公钥

Git 的使用——提交避免输入用户名和密码
以上的配置会导致每次远程提交的时候,都要登录账号和密码,非常麻烦。这里介绍以下SSH公钥免密的方法,当然也还有其他的方法,可以网上自己寻找。这边贴一个:https://zhuanlan.zhihu.com/p/358721423

git ssh 方式免密提交方式需要将 ssh-keygen 生成的公钥放到服务器上
全局用户名密码配置

git config --global user.name "ldf2022"
git config --global user.email "1304018570@qq.com"

项目初始化,生成 .git 目录,配置 ssh 远程项目地址。

$ git remote add origin git@gitee.com:ldf2022/tiny-httpd.git

生成公钥和私钥
1、首先需要检查你电脑是否已经有 SSH key

$ cd ~/.ssh 
$ ls

image.png
如果不是第一次使用,已经存在 id_rsa.pub 或 id_dsa.pub 文件。请执行下面的操作,清理原有 ssh 密钥。

$ mkdir key_backup   
$ cp id_rsa* key_backup   
$ rm id_rsa*

2、执行生成公钥和私钥的命令,生成新的密钥:

$ ssh-keygen -t rsa -C "ldf"

代码参数:

  • -t 指定密钥类型,默认是 rsa ,可以省略。
  • -C 设置注释文字,比如邮箱。

按默认为空,直接按回车3下,生成 id_rsa 和 id_rsa.pub 两个秘钥文件。
执行查看公钥信息:

$ cat ~/.ssh/id_rsa.pub

image.png
3、复制公钥信息,打开 gitee,我的账户-设置-SSH 公钥,如下图所示,把公钥粘贴到公钥文本框中,标题自己定义,然后点击确定按键,输入密码。
image.png
image.pngimage.png
然后,提交时就不再需要用户名和密码了

搭建版本库

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

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

两种方式都会在你的本地机器上得到一个工作就绪的 Git 仓库。

1.创建全新的仓库,需要用git管理的项目的根目录执行:git init
2.另一种方法就是克隆远程目录,由于是将远程服务器上的仓库完全镜像一份至本地:git clone [url]

记录每次更新到仓库

工作目录下的每一个文件都不外乎这两种状态:已跟踪未跟踪。 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。

工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。 初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。
编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 在工作时,可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。

跟踪新文件
** **在项目下编写一个readme.txt文件,内容如下:

Git is a version control system.

使用命令 git add开始跟踪一个文件运行:

$ git add readme.txt

git add命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。
可以用 git status 命令查看哪些文件处于什么状态:

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   readme.txt

只要在 Changes to be committed这行下面的,就说明是已暂存状态。 如果此时提交,那么该文件在你运行 git add 时的版本将被留存在后续的历史记录中。
用命令git commit告诉Git,把文件提交到仓库:

$ git commit -m "wrote a readme file"
[master (root-commit) 0a48055] wrote a readme file
 1 file changed, 1 insertion(+)
 create mode 100644 readme.txt

-m后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
git commit命令执行成功后会告诉你,1 file changed:1个文件被改动(我们新添加的readme.txt文件);1 insertions:插入了一行内容(readme.txt有一行内容)。
继续修改readme.txt文件,改成如下内容:

Git is a distributed version control system.
Git is free software.

运行git status:

$ 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.txt

no changes added to commit (use "git add" and/or "git commit -a")

上面的命令输出告诉我们,readme.txt被修改过了,但还没有准备提交的修改。
用git diff这个命令看看具体修改了什么内容:

$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 7950452..013b5bc 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1 +1,2 @@
-Git is a version control system.
\ No newline at end of file
+Git is a distributed version control system.
+Git is free software.
\ No newline at end of file

提交到仓库:

$ git add readme.txt

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git commit -m "add distributed"
[master 98d9024] add distributed
 1 file changed, 2 insertions(+), 1 deletion(-)

提交后,再用git status命令看看仓库的当前状态:

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

Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working tree clean)的

版本回退

先对test.txt进行修改,然后提交记录
image.png

git add test.txt
git commit -m "test"

随后可以通过git log来查看提交记录

git log

image.png
git log命令显示从最近到最远的提交日志,可以看到3次提交,最近的一次是append GPL,上一次是add distributed,最早的一次是wrote a readme file。
加上
–pretty=oneline
参数,减少输出信息:
image.png
一大串类似dc740…的是commit id(版本号),和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。

把readme.txt回退到上一个版本,也就是add distributed的那个版本,怎么做呢?

  1. 首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD表示当前版本,也就是最新的提交dc740…,上一个版本就是HEAD**,**上上一个版本就是HEAD**,当然往上100个版本写100个比较容易数不过来,所以写成HEAD~100**。
  2. 要把当前版本append GPL回退到上一个版本add distributed,就可以使用git reset命令
git reset --hard HEAD^

image.png
此时的test.txt的内容就回退到之前了,也就是空
用git log再看看现在版本库的状态:
image.png
最新的那个版本append GPL已经看不到了!回不去了?
只要上面的命令行窗口还没有被关掉,就可以顺着往上找啊找啊,找到那个append GPL的commit id是dc74…,于是就可以指定回到未来的某个版本:

$ git reset --hard dc74
HEAD is now at dc74077 append GPL

版本号没必要写全,前几位就可以了,Git会自动去找
image.png
image.png
Git提供了git reflog命令来记录每一次命令: 方便对版本的跳转
image.png

管理修改

为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
对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.

然后,添加:

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git add readme.txt

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   readme.txt

然后,再修改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.

提交:

$  git commit -m "git tracks changes"
[master cd3f0b2] git tracks changes
 1 file changed, 2 insertions(+), 1 deletion(-)

提交后,再看看状态:

$ 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.txt

no changes added to commit (use "git add" and/or "git commit -a")

怎么第二次的修改没有被提交?回顾一下操作过程:
第一次修改 -> git add -> 第二次修改 -> git commit
Git管理的是修改,当用git add命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
提交后,用git diff HEAD – readme.txt命令可以查看工作区和版本库里面最新版本的区别:

$ git diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index db28b2c..295b239 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,5 @@
 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.
\ No newline at end of file
+Git tracks changes.
+Git tracks changes of files.
\ No newline at end of file

可见,第二次修改确实没有被提交。
怎么提交第二次修改呢?可以继续git add再git commit,也可以别着急提交第一次修改,先git add第二次修改,再git commit,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add -> 第二次修改 -> git add -> git commit

撤销修改

假设在readme.txt中错误的添加了一行:My stupid boss still prefers SVN.

$ 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.
Git tracks changes of files.
My stupid boss still prefers SVN.

在准备提交前,发现了错误。
很容易地纠正它,可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用git status查看一下:

$ 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.txt

no changes added to commit (use "git add" and/or "git commit -a")

可以发现,Git会告诉你,git restore 可以丢弃工作区的修改:

$ git restore readme.txt

命令的意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

  • readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态
  • readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态

总之,就是让这个文件回到最近一次git commit或git add时的状态。现在,看看readme.txt的文件内容:

$ 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.
Git tracks changes of files.

假定不但添加了错误的一行,还git add到暂存区了:
庆幸的是,在commit之前,发现了这个问题。用git status查看一下,修改只是添加到了暂存区,还没有提交:

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   readme.txt

Git同样告诉我们,用命令**git restore --staged **可以把暂存区的修改撤销掉(unstage),重新放回工作区:

$ git restore --staged readme.txt

命令既可以回退版本,也可以把暂存区的修改回退到工作区。
再用git status查看一下,现在暂存区是干净的,工作区有修改:

$ 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.txt

no changes added to commit (use "git add" and/or "git commit -a")

丢弃工作区的修改:

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git restore readme.txt

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$  git status
On branch master
nothing to commit, working tree clean

删除文件

在Git中,删除也是一个修改操作,先添加一个新文件fileGit并且提交:
image.png
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了:
image.png
现在你有两个选择:

  1. 一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit

image.png
现在,文件就从版本库中被删除了。

  • rm命令就是在工作区删文件
  • git rm就是删文件,并且把删文件的修改提交到暂存区。相当于rm删文件后,git add 提交,保存修改
  1. 另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- file

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
file文件又回来了/!


远程仓库

Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?

最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。

可以自己搭建一台运行Git的服务器,不过好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。

不过Github的登录需要VPN才可登录,国内强者也不容落后,创造出了Gitee网站.

我们先以gitee为例子

添加远程库

image.png
目前,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到Gitee仓库。
根据Gitee的提示,在本地的mygit仓库下运行命令:

$ git remote add origin https://gitee.com/ldf2022/tiny-httpd.git

添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:

$ git push -u origin master
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Delta compression using up to 16 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (24/24), 1.89 KiB | 646.00 KiB/s, done.
Total 24 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), done.
To https://github.com/xk1201333/mygit.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。
由于远程库是空的,第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
从现在起,只要本地作了提交,就可以通过命令:

git push origin master

把本地master分支的最新修改推送至Gitee,现在,你就拥有了真正的分布式版本库!

删除远程库

如果添加的时候地址写错了,或者就是想删除远程库,可以用git remote rm 命令。使用前,建议先用git remote -v查看远程库信息:

$ git remote -v
origin  https://github.com/xk1201333/mygit.git (fetch)
origin  https://github.com/xk1201333/mygit.git (push)

此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。

从远程库克隆

假设从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。克隆仓库的命令是 git clone ,要克隆 Git 的链接库 libgit2,可以用下面的命令:

$ git clone https://github.com/libgit2/libgit2

这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。 如果你进入到这个新建的 libgit2 文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。
如果想在克隆远程仓库的时候,自定义本地仓库的名字,你可以通过额外的参数指定新的目录名:

$ git clone https://github.com/libgit2/libgit2 mylibgit

这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit

参与各种开源项目

在GitHub出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个bug,即使能改掉bug,也只能把diff文件用邮件发过去,很不方便。

但是在GitHub上,利用Git极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。
如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:

git clone git@github.com:xk1201333/bootstrap.git

一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。
Bootstrap的官方仓库twbs/bootstrap、你在GitHub上克隆的仓库my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:
image.png
如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。
如果希望bootstrap的官方库能接受你的修改,可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。

分支管理

分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

创建与合并分支

每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。HEAD严格来说不是指向提交,而是指向master,master才是指向提交的,所以,HEAD指向的就是当前分支。
一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:
image.png
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。

当创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
image.png
Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
image.png
假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
image.png
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,就剩下了一条master分支:
image.png
创建dev分支,然后切换到dev分支:

$ git checkout -b dev
Switched to a new branch 'dev'

# 可以分成两步
git branch dev  	#新建分支
git checkout dev 	#切换分支

然后,用git branch命令查看当前分支:

$ git branch
* dev
  master

git branch命令会列出所有分支,当前分支前面会标一个*号。
然后,就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:Creating a new branch is quick.
然后提交:

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (dev)
$ git add readme.txt

31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (dev)
$ git commit -m "branch test"
[dev d0d2fe2] branch test
 1 file changed, 2 insertions(+), 1 deletion(-)

现在,dev分支的工作完成,切换回master分支:

$ git checkout master
Note: switching to 'master'.

切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变:
image.png
现在,把dev分支的工作成果合并到master分支上:

$ git merge dev
Updating ec2d7e6..d0d2fe2
Fast-forward
 readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。
注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
合并完成后,就可以放心地删除dev分支了:

$ git branch -d dev
Deleted branch dev (was d0d2fe2).

$ git branch
* master

解决冲突

最新版本的Git提供了新的git switch命令来切换分支

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

准备新的feature1分支,继续新分支开发:

$ git switch -c feature1
Switched to a new branch 'feature1'

修改test.txt最后一行,改为:Git is test txt.->branch 3 is created.
在feature1分支上提交:

git add test.txt
git commit -m "b3_test.txt"

image.png
切换到master分支:

git switch master

image.png
Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。
在master分支上把test.txt文件的最后一行改为:Creating a new branch is quick & simple.
提交:

git add test.txt
git commit -m "master_test.txt"

image.png
现在,master分支和feature1分支各自都分别有新的提交:
image.png
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突:

git merge b3

image.png
image.png
Git告诉我们,test.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:

git status

image.png
查看test.txt的内容:
image.png
Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,修改如下后保存:Creating a new branch is quick and simple.
再提交:
image.png

$ git add test.txt 
$ git commit -m "conflict fixed"

[master aa6fb38] conflict fixed

现在,master分支和feature1分支变成了下图所示:
image.png
用带参数的git log也可以看到分支的合并情况:

$ git log --graph --pretty=oneline --abbrev-commmit
# 以图的形式展示每次的提交记录

image.png
最后,删除b3分支:v

$ git branch -d b3

已删除分支 b3(曾为 529c4ba)。

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。 (这也是与rebase的区别)
仍然创建并切换dev分支:

$ git switch -c dev
切换到一个新分支 'dev'

** **修改test.txt文件,并提交一个新的commit: 添加:new dev branch.

$ git add test.txt
$ git commit -m "dev_test"

image.png
现在,切换回master:

$ git switch master

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

$ git merge --no-ff -m "merge with no-ff" dev

Merge made by the 'recursive' strategy.
 test.txt | 2 ++
 1 file changed, 2 insertions(+)

因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
合并后,用git log看看分支历史:

$ git log --graph --pretty=oneline --abbrev-commit

#--abbrev-commit  省略版本号,只取前小部分的版本号

image.png
可以看到,不使用Fast forward模式,merge后就像这样:
image.png
分支策略
** 在实际开发中,应该按照几个基本原则进行分支管理: **
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
image.png

Bug分支

软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。

当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:

$ git status
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   hello.py

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:   test.txt

并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,**Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作: **

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

image.png
现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:

$ 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 bug-101
Switched to a new branch 'issue-101'

现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:
image.png
修复完成后,切换到master分支,并完成合并,最后删除bug-101分支:
image.png
现在,是时候接着回到dev分支干活了!

$ git switch dev
$ git status

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

$ git stash list

image.png
工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
另一种方式是用git stash pop,恢复的同时把stash内容也删了:

$ git stash pop

image.png
再用git stash list查看,就少了一个stash内容了:
image.png
在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?有木有更简单的方法?
同样的bug,要在dev上修复,只需要把4c805e2 fix bug 101这个提交所做的修改“复制”到dev分支。注意:只想复制4c805e2 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来。

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

$ git branch
* dev
  master

$ git cherry-pick 071e8f3
[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的过程重复一遍。
既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。

Feature分支

软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
现在,接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。于是准备开发:

$ git switch -c feature-vulcan
Switched to anew branch 'feature-vulcan'

5分钟后,开发完毕:

$ git add vulcan.c

$ git status
On branch feature-vulcan
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   vulcan.c

$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
 1 file changed, 2 insertions(+)
 create mode 100644 vulcan.c

切回dev,准备合并:

$ git switch dev

一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。

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

$ git branch -d feature-vulcan
error: 分支 'feature-vulcan' 没有完全合并。
如果您确认要删除它,执行 'git branch -D feature-vulcan'。

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

$ git branch -D feature-vulcan
已删除分支 feature-vulcan(曾为 82b75f2)。

多人协作

当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。
要查看远程库的信息,用git remote:

$ git remote
origin

或者,用git remote -v显示更详细的信息:

$ git remote -v
origin  https://gitee.com/ldf2022/tiny-httpd.git (fetch)
origin  https://gitee.com/ldf2022/tiny-httpd.git (push)

上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。

推送分支

推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:

$ git push origin master

如果要推送其他分支,比如dev,就改成:

$ git push origin dev

但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

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

总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!

抓取分支

多人协作时,大家都会往master和dev分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:

$ git remote add origin https://github.com/xk1201333/mygit.git

当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支:

$ git branch
* master

现在,你的小伙伴要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:

$ git checkout -b dev origin/dev

现在,他就可以在dev上继续修改,然后,时不时地把dev分支push到远程:

$ git add env.txt

$ git commit -m "add env"
[dev 7a5e5dd]add env
 1 file changed, 1 insertion(+)
create mode 100644 env.txt

$ git push origin dev
Counting objects: 3, done.
Delta compressionusing upto 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes | 308.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   f52c633..7a5e5dd  dev -> dev

你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:

$ cat env.txt
env

$ git add env.txt

$ gitcommit -m "add new env"
[dev 7bd91f1]add new env
 1 file changed, 1 insertion(+)
create mode 100644 env.txt

$ git push origin dev
To github.com:michaelliao/learngit.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failedto pushsome refsto 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the tipof yourcurrent branchis behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards'in 'git push --help'for details.

推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送:

$ git pull
There is no tracking informationfor the current branch.
Please specify which branch you want to mergewith.
See git-pull(1)for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> dev

git pull也失败了,原因是没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置dev和origin/dev的链接:

$ git branch--set-upstream-to=origin/dev dev
Branch 'dev'set upto track remote branch 'dev'from 'origin'.

再pull:

$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflictin env.txt
Automatic merge failed; fix conflictsandthen commit the result.

这回git pull成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push:

$ git commit -m "fix env conflict"
[dev 57c53ab] fix env conflict

$ git push origin dev
Counting objects: 6, done.
Delta compressionusing upto 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   7a5e5dd..57c53ab  dev -> dev

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin <branch-name>推送自己的修改
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并
  3. 如果合并有冲突,则解决冲突,并在本地提交
  4. 没有冲突或者解决掉冲突后,再用git push origin <branch-name>推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to origin/
这就是多人协作的工作模式,一旦熟悉了,就非常简单。

Rebase

多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。
每次合并再push后,分支变成了这样:

$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -> master, origin/master) init hello
*   e5e69f1 Merge branch 'dev'
|\
| *   57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 addnew env
| |/
* |   12a631b merged bug fix 101
|\ \
| * | 4c805e2 fix bug 101
|/ /
* |   e1e9c68 merge with no-ff
|\ \
| |/
| * f52c633 add merge
|/
*   cf810e4 conflict fixed

总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?
其实是可以做到的!Git有一种称为rebase的操作,有人把它翻译成“变基”

在和远程分支同步后,我们对hello.py这个文件做了两次提交。用git log命令看看:

$ git log --graph --pretty=oneline --abbrev-commit

* 582d922 (HEAD -> master) add author
* 8875536 add comment
* d1be385 (origin/master) init hello
*   e5e69f1 Merge branch 'dev'
|\
| *   57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
...

注意到Git用(HEAD -> master)和(origin/master)标识出当前分支的HEAD和远程origin的位置分别是582d922 add author和d1be385 init hello,本地分支比远程分支快两个提交。
现在尝试推送本地分支:

$ git push origin master
To github.com:michaelliao/learngit.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the remote contains work that youdo
hint:not have locally. Thisis usually causedby another repository pushing
hint:to the same ref. You may wanttofirst 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.

很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先pull一下:

$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:michaelliao/learngit
   d1be385..f005ed4  master     -> origin/master
 * [new tag]         v1.0       -> v1.0
Auto-merging hello.py
Merge made by the 'recursive' strategy.
 hello.py | 1 +
 1 file changed, 1 insertion(+)

再用git status看看状态:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 3 commits.
  (use "git push" to publish your local commits)

nothing tocommit, working tree clean

加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用git log看看:

$ git log--graph --pretty=oneline --abbrev-commit
*   e0ea545 (HEAD -> master) Merge branch 'master' of github.com:michaelliao/learngit
|\
| * f005ed4 (origin/master)set exit=1
* | 582d922add author
* | 8875536add comment
|/
* d1be385 init hello
...

对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,有没有问题?
有!什么问题?不好看!
这个时候,rebase就派上了用场。输入命令git rebase试试:

$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching baseand 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M	hello.py
Falling back to patching baseand 3-way merge...
Auto-merging hello.py

输出了一大堆操作,到底是啥效果?再用git log看看:

$ git log--graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master) add author
* 3611cfe add comment
* f005ed4 (origin/master)set exit=1
* d1be385 init hello
...

原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。
** 这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。 **
最后,通过push操作把本地分支推送到远程:

$ git push origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:michaelliao/learngit.git
   f005ed4..7e61ed4  master -> master

再用git log看看效果:

$ git log--graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4set exit=1
* d1be385 init hello
...

远程分支的提交历史也是一条直线。

标签管理

发布一个版本时,通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

Git有commit,为什么还要引入tag?tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

创建标签

在Git中打标签非常简单,首先,切换到需要打标签的分支上:

$ git branch
* dev
  master
$ git checkout master
Switched to branch 'master'

然后,敲命令git tag 就可以打一个新标签:

$ git tag v1.0

可以用命令git tag查看所有标签:

$ git tag
v1.0

默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,怎么办?
方法是找到历史提交的commit id,然后打上就可以了:

$ git log --pretty=oneline --abbrev-commit
12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
4c805e2 fix bug 101
e1e9c68 merge with no-ff
f52c633 add merge
cf810e4 conflict fixed
5dc6824 & simple
14096d0 AND simple
b17d20e branch test
d46f35e 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

比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令:

$ git tag v0.9 f52c633

再用命令git tag查看标签:

$ git tag
v0.9
v1.0

注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show 查看标签信息:

$ git show v0.9

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

$ git tag -a v0.1 -m "version 0.1 released" 1094adb

用命令git show 可以看到说明文字:

$ git show v0.1
tag v0.1
Tagger: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 22:48:43 2018 +0800

version 0.1 released

commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao <askxuefeng@gmail.com>
Date:   Fri May 18 21:06:15 2018 +0800

    append GPL

diff --git a/readme.txt b/readme.txt
...

注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

操作标签

如果标签打错了,也可以删除:

$ git tag -d v0.9
Deleted tag 'v0.9' (was f15b0dd)

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin

$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v1.0 -> v1.0

或者,一次性推送全部尚未推送到远程的本地标签:

$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
 * [new tag]         v0.9 -> v0.9

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:

$ git tag -d v0.9
Deleted tag 'v0.9' (was f52c633)

然后,从远程删除。删除命令也是push,但是格式如下:

$ git push origin :refs/tags/v0.9
To github.com:michaelliao/learngit.git
 - [deleted]         v0.9

要看看是否真的从远程库删除了标签,可以登陆GitHub查看。
image.png

自定义Git

安装Git时,我们已经配置了user.name和user.email,实际上,Git还有很多可配置项。
比如,让Git显示颜色,会让命令输出看起来更醒目:

$ git config --global color.uitrue

忽略特殊文件

有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files …
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore

忽略文件的原则是:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini文件,因此你需要忽略Windows自动生成的垃圾文件:

# Windows:Thumbs.db
ehthumbs.db
Desktop.ini

然后,继续忽略Python编译产生的.pyc、.pyo、dist等文件或目录:

# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build

加上你自己定义的文件,最终得到一个完整的.gitignore文件,内容如下:

# Windows:Thumbs.db
ehthumbs.db
Desktop.ini

# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build

# My configurations:
db.ini
deploy_key_rsa

最后一步就是把.gitignore也提交到Git,就完成了!当然检验.gitignore的标准是git status命令是不是说working directory clean。

配置别名

有没有经常敲错命令?比如git status?status这个单词真心不好记。
如果敲git st就表示git status那就简单多了。只需要敲一行命令,告诉Git,以后st就表示status:

$ git config --globalalias.st status

当然还有别的命令可以简写,很多人都用co表示checkout,ci表示commit,br表示branch:

$ git config --globalalias.co checkout
$ git config --globalalias.ci commit
$ git config --globalalias.br branch

以后提交就可以简写成:

$ git ci -m "bala bala bala..."

-global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。

配置文件

配置Git的时候,加上–global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。
配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中:

$ cat .git/config
[core]
    repositoryformatversion = 0
    filemode =true
    bare =false
    logallrefupdates =true
    ignorecase =true
    precomposeunicode =true
[remote "origin"]
    url = git@github.com:michaelliao/learngit.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[alias]
    last = log -1

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。
而当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

$ cat .gitconfig
[alias]
    co = checkout
    ci = commit
    br = branch
    st = status
[user]
    name = Your Name
    email = your@email.com

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。


搭建Git服务器

搭建Git服务器需要准备一台运行Linux的机器,强烈推荐用Ubuntu或Debian,这样,通过几条简单的apt命令就可以完成安装。
假设你已经有sudo权限的用户账号,下面,正式开始安装。
第一步,安装git:

$ sudo apt-get install git

第二步,创建一个git用户,用来运行git服务:

$ sudo adduser git

第三步,创建证书登录:
收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个。
第四步,初始化Git仓库:
先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:

$ sudo git init --bare sample.git

Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。然后,把owner改为git:

$ sudo chown -R git:git sample.git

第五步,禁用shell登录:
出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:

git:x:1001:1001:,,,:/home/git:/bin/bash

改为:

git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

这样,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。
第六步,克隆远程仓库:
现在,可以通过git clone命令克隆远程仓库了,在各自的电脑上运行:

$ git clone git@server:/srv/sample.git
Cloning into 'sample'...
warning: You appear to have cloned an empty repository.

剩下的推送就简单了。

管理公钥

如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥

管理权限

有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。


Git 简单小试牛刀

1. 先创建一个新的仓库,然后克隆到本地

在github上创建了一个公开的代码仓:https://github.com/tinygyro/git_rampup.git
image.png
在文件夹的目录下点击鼠标右键,再选择 git bash 即可进入:
image.png
克隆Remote代码的命令:

$ git clone https://github.com/tinygyro/git_rampup.git

完成代码克隆后,我们进入到项目文件夹,发现和远程仓库一样的结构:
image.png

2. git user初始配置

在第一次commit之前,我们要来配置git用户的姓名和邮箱。在项目的根目录下运行:

$ git config --global user.name "Tiny Gyro"
$ git config --global user.email "tinygyro@github.com"

3. 代码修改和提交

我们运行命令 git status 查看暂存区状态:所在分支和工作区的状态。

$ git status

image.png
创建一个新文件 happy_summer.txt
image.png
改动只发生在工作区
image.png
运行 git status ,显示新添加的happy_summer.txt 是新文件不受git的追踪
image.png
运行命令 git add 添加到暂存区

git add happy_summer.txt

再次运行 git status
image.png
我们的更改已经推进到了暂存区
image.png
代码修改提交到本地仓库:

$ git commit -m "first commit"

image.png
此时更改已经推进到本地仓库区
image.png
** 本地仓库推送到远程仓库**

$ git push

image.png
至此,我们便完成了本地到云端的全部修改:
image.png
远程仓库更新:
image.png

Git 常用指令

Git 学习网站
image.png

命令	                                        说明
git diff										比较文件的不同,即暂存区和工作区的差异
git commit -m "注解"							提交暂存区到本地仓库
git reset --hard [版本号]   					 回退到此版本号,也可以回退到未来
git checkout -- [filename]  				回退到上次的修改
git reflog   									显示从开始到当前提前的所有文件
git log     									查看git提交历史记录
git log --oneline           					可以查看简约版历史提交记录
git mv 旧文件名 新文件名    					    来将工作区和暂存区文件重命名
git rm --cached [filename] 						删除远程上的文件,执行完命令之后要commit与push


分支:
列出本地分支:   								git branch
列出远程分支:    								git branch -r
新建一个分支,但仍停留在当前分支:   			    git branch [branch-name]
分支之间的切换:  								git checkout [branch-name]
新建一个分支,并切换到该分支:       			    git checkout -b [branch]
合并指定分支到当前分支 : 						git merge [branch-name]
把dev分支提交到origin中:  						git push origin dev
在branch分支下创建新的分支child_branch:			git branch [child_branch] [branch]
拉取分支branch_name: 							git pull origin [branch-name]
删除本地分支:    								git branch -d [branch-name]
可以删除远程分支:								git push origin --delete [branch-name]   
# 第一次初始化(方式1):
git init
git add .
git commit -m 'first commit'
git remote add origin git@github.com:帐号名/仓库名.git
git pull origin master
git push origin master # -f 强推
# 第一次初始化(方式2):
git clone git@github.com:git帐号名/仓库名.git

# 平时工作基本操作:
git checkout master # 切到主分支
git fetch origin  # 获取最新变更
git checkout -b dev origin/master # 基于主分支创建dev分支
git add . # 添加到缓存
git commit -m 'xxx' # 提交到本地仓库
git fetch origin # 获取最新变更
git rebase dev origin/master # 合并到主分支
git push origin dev # 推送到远程分支

git chekout master # 切到主分支
git merge dev # 合并开发分支

git clone -b 远程分支 仓库地址 # 本地不存在仓库 拉取远程分支代码
git checkout -b 远程分支 origin/远程分支 # 本地存在仓库,拉取远程分支

git config

git相关配置

git config --global user.name “用户名” # 设置用户名
git config --global user.email “用户邮箱” #设置邮箱
git config --global user.name # 查看用户名是否配置成功
git config --global user.email # 查看邮箱是否配置

git init

初始化仓库
创建全新的仓库

git status

查看仓库当前状态

git add

将文件添加到仓库

git add 文件名 # 将工作区的某个文件添加到暂存区   
git add . # 将当前工作区的所有文件都加入暂存区
git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件
git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件
git add -i # 进入交互界面模式,按需添加文件到缓存区

git commit

将暂存区文件提交到本地仓库

**git commit -m “提交说明” **# 将暂存区内容提交到本地仓库
git commit -a -m “提交说明” # 跳过缓存区操作,直接把工作区内容提交到本地仓库

  • Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!
  • Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
  • Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因.

git diff

比较文件异同

git diff # 工作区与暂存区的差异
git diff 分支名 #工作区与某分支的差异,远程分支这样写:remotes/origin/分支名
git diff HEAD # 工作区与HEAD指针指向的内容差异
git diff 提交id 文件路径 # 工作区某文件当前版本与历史版本的差异
**git diff --stage **# 工作区文件与上次提交的差异(1.6 版本前用 --cached)
**git diff 版本TAG **# 查看从某个版本后都改动内容
git diff 分支A 分支B # 比较从分支A和分支B的差异(也支持比较两个TAG)
git diff 分支A…分支B # 比较两分支在分开后各自的改动

git log

查看历史记录

git log # 查看所有commit记录(SHA-A校验和,作者名称,邮箱,提交时间,提交说明)
git log -p -次数 # 查看最近多少次的提交记录
git log --stat # 简略显示每次提交的内容更改
**git log --name-only **# 仅显示已修改的文件清单
git log --name-status # 显示新增,修改,删除的文件清单
git log --oneline # 让提交记录以精简的一行输出
git log –graph –all --online # 图形展示分支的合并历史
**git log --author=作者 ** # 查询作者的提交记录(和grep同时使用要加一个–all–match参数)
git log --grep=过滤信息 # 列出提交信息中包含过滤信息的提交记录
**git log -S查询内容 **# 和–grep类似,S和查询内容间没有空格
git log fileName # 查看某文件的修改记录,找背锅专用

git reset

代码回滚,撤销变更

git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。

git reset HEAD^ # 恢复成上次提交的版本
git reset HEAD^^ # 恢复成上上次提交的版本,就是多个^,以此类推或用~次数
git reflog git reset --hard 版本号
–soft:只是改变HEAD指针指向,缓存区和工作区不变;
–mixed:修改HEAD指针指向,暂存区内容丢失,工作区不变;
–hard:修改HEAD指针指向,暂存区内容丢失,工作区恢复以前状态;

主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert
image.png

git reset HEAD~1

image.png
Git 把 main 分支移回到 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了

git Revert

为了撤销更改并分享给别人,我们需要使用 git Revert。
image.png

git revert HEAD

image.png
奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录C2’引入了更改 —— 这些更改刚好是用来撤销C2这个提交的。也就是说C2’的状态与C1是相同的。revert 之后就可以把你的更改推送到远程仓库与别人分享啦。

git push -u origin master

同步远程仓库

git rm

git rm 文件名 删除版本库文件

git switch

注意:在 Git 2.23 版本中,引入了一个名为 git switch 的新命令,最终会取代 git checkout,因为 checkout 作为单个命令有点超载(它承载了很多独立的功能)。

git checkout

切换分支
git checkout – test.txt #版本库里的版本替换工作区的版本

git checkout -b dev //-b表示创建并切换分支
上面一条命令相当于一面的二条:
git branch dev //创建分支
git checkout dev //切换分支
git add .
git commit -m “dev0903”
git push -u origin dev //提交到分支上

git remote

查看远程库信息
git remote add origin git@github.com:帐号名/仓库名.git #本地仓库内容推送到远程仓库

git clone

git clone git@github.com:git帐号名/仓库名.git # 从远程仓库克隆项目到本地

git Branch

创建分支,查看分支

git branch newImage //创建分支
git branch -d dev //删除分支

  • Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:
早建分支!多用分支!
  • 这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。
  • 在将分支和提交记录结合起来后,我们会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”

案例:

  1. 创建一个到名为 **newImage** 的分支
git branch newImage

创建分支就是这么容易!新创建的分支 newImage 指向的是提交记录 C1

  1. 现在咱们试着往新分支里提交一些东西

为什么 main 分支前进了,但 newImage 分支还待在原地呢?!这是因为我们没有“在”这个新分支上,看到 main 分支上的那个星号(*)了吗?这表示当前所在的分支是 main

  1. 告诉 Git 我们想要切换到新的分支上
git checkout newImage
git commit

让我们在提交修改之前先切换到新的分支上

有个更简洁的方式:如果你想创建一个新的分支同时切换到新创建的分支的话,可以通过 **git checkout -b <your-branch-name>** 来实现。

git checkout -b newImage

git merge

合并分支

git merge dev #用于合并指定分支到当前分支
git merge --no-ff -m “merge with no-ff” dev
#加上–no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并

**分支与合并:**就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。

  • 在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”

案例:
准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。

1. 我们要把 **bugFix** 合并到 **main**
image.png

git merge bugFix

首先,**main**** 现在指向了一个拥有两个父节点的提交记录。**假如从 main 开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 main 包含了对代码库的所有修改。↓↓↓
image.png
2. 再把 **main** 分支合并到 **bugFix**

因为 main 继承自 bugFix,Git 什么都不用做,只是简单地把 bugFix 移动到 main 所指向的那个提交记录。表明每一个分支都包含了代码库的所有修改

git checkout bugFix
git merge main

image.png

git Rebase

对当前分支的记录做一个拷贝,然后连接在主之路的记录的后面。

第二种合并分支的方法是 git rebase

  • Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
  • Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。

还是准备了两个分支;注意当前所在的分支是 bugFix(星号标识的是当前分支)
image.png

  1. 我们想要把 bugFix 分支里的工作直接移到 main 分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。

咱们这次用 git rebase 实现此目标

git rebase main

现在 bugFix 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。
image.png
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3’ 是我们 Rebase 到 main 分支上的 C3 的副本。

  1. 切换到了 main 上。把它 rebase 到 bugFix 分支上……

由于 bugFix 继承自 main,所以 Git 只是简单的把 main 分支的引用向前移动了一下而已。
image.png

HEAD

在提交树上移动
在接触 Git 更高级功能之前,我们有必要先学习在你项目的提交树上前后移动的几种方法。

  • HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。
  • HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。
  • HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。

image.png

git checkout c1
git checkout main
git commit
git checkout c2

HEAD 指向了 main,随着提交向前移动
实际这些命令并不是真的在查看 HEAD 指向,看下一屏就了解了。如果想看 HEAD 指向,可以通过 cat .git/HEAD 查看
image.png

分离的 HEAD
分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态
如下所示:
image.png

HEAD -> main -> C1
HEAD 指向 main, main 指向 C1
git checkout c1  //HEAD-》C1

image.png
目标:

git checkout c4


相对引用[^]

  • 通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像本程序中这么漂亮的可视化提交树供你参考,所以你就不得不用 **git log** 来查查看提交记录的哈希值。
  • 并且哈希值在真实的 Git 世界中也会更长(译者注:基于 SHA-1,共 40 位)。例如前一关的介绍中的提交记录的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌头都快打结了吧…
  • 比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此我可以仅输入fed2 而不是上面的一长串字符。

使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法

  • 使用 **^** 向上移动 1 个提交记录
  • **使用 **~<num>** 向上移动多个提交记录,如 ****~3**

1. 首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。
image.png
所以 main^ 相当于“main 的父节点”。 main^^main 的第二个父节点
现在咱们切换到 main 的父节点

git checkout main^

image.png
你也可以将 HEAD 作为相对引用的参照。下面咱们就用 HEAD 在提交树中向上移动几次。

git checkout c3
git checkout HEAD^
git checkout HEAD^
git checkout HEAD^

image.png

image.png
目标
image.png

git checkout bugFix^

相对引用【~】

如果你想在提交树中向上移动很多步的话,敲那么多 ^ 貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~

  • 该操作符后面可以跟一个数字(可选,不跟数字时与 **^** 相同,向上移动一次),指定向上移动多少次。

~<num> 一次后退四步。
image.png

git checkout HEAD~4

image.png

强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 **-f** 选项让分支指向另一个提交。例如:

git branch -f main HEAD~3

上面的命令会将 main 分支强制指向 HEAD 的第 3 级父提交。
image.png
相对引用为我们提供了一种简洁的引用提交记录 C1 的方式, 而 -f 则容许我们将分支强制移动到那个位置。

git fetch

拉取远程分支到本地仓库

git checkout -b 本地分支 远程分支 # 会在本地新建分支,并自动切换到该分支
git fetch origin 远程分支:本地分支 # 会在本地新建分支,但不会自动切换,还需checkout
git branch --set-upstream 本地分支 远程分支 # 建立本地分支与远程分支的链接

Git最基本的使用指令

git init  #初始化
git add   #将文件放入暂存区
git commit -m "name"  #从暂存区放到本地仓库
git push

#一般是不敢直接并到主分支上的,所有基本是有属于自己的分支
git clone // git pull  #将远程仓库的文件拉下来
git branch -b branch1  #创建分支并打开
git add file
git commit -m "b1"
git push -u origin branch1  #提交到分支b1上

git checkout b2   #切换分支
git pull --rebase

Git面试题

1. rebase和merge的区别

image.png
区别:
1、rebase把当前的commit放到公共分支的最后面,merge把当前的commit和公共分支合并在一起;
2、用merge命令解决完冲突后会产生一个commit,而用rebase命令解决完冲突后不会产生额外的commit。

2. 什么是git? Git和SVN有什么区别?

git是一个常用的分布式版本管理工具(DVCS)。它可以跟踪文件的更改,并允许你恢复到任何特定版本的更改。
image.png
一个主要优点是它不依赖于中央服务器来存储项目文件的所有版本。

3. git常用的命令有哪些?

  • git add 添加文件到暂存区
  • git commit 提交文件到本地仓库
  • git pull 从远程仓库拉取项目到本地
  • git push 将本地仓库的新的改变推送到远程仓库
  • git clone 将远程仓库复制到本地
  • git fetch 抓取
  • git merge 合并

4. git pull、git merge、git fetch三个命令的区别?

  • git clone:是在本地没有版本库的情况下,从远程仓库克隆一份到本地,是一个本地版本库从无到有的过程
  • git pull:是在本地仓库已经存在的情况下,将远程最新的commits抓取并合并到本地版本库的过程
  • git fetch: 从远程版本库抓取最新的commits,不会进行合并
  • git merge:合并

所以git pull = git fetch + git merge


5. push之前一定要进行哪个操作?

**push之前一定要进行本地更新操作。**使用git pull命令或者使用git fetch和git merge的命令组合。这时候,可能会出现版本冲突,如果出现的话,需要解决完冲突再进行代码push。

6. 如何解决版本冲突?

版本冲突多出现在合并操作(合并远程仓库代码或者合并分支代码)中。如果出现版本冲突,需要具体分析出现冲突的代码区,手动进行代码合并,然后再进行提交。

7. 在 Git 中提交的命令是什么?

用于写入提交的命令是 git commit -a
现在解释一下 -a 标志, 通过在命令行上加 -a 指示 git 提交已修改的所有被跟踪文件的新内容。还要提一下,如果你是第一次需要提交新文件,可以在在 git commit -a 之前先 git add

8. 如何在Git中创建存储库?

要创建存储库,先为项目创建一个目录(如果该目录不存在),然后运行命令 git init。通过运行此命令,将在项目的目录中创建 .git 目录。

下期将会讲解Gerrit的使用,,

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值