Git使用场景

2,466 阅读12分钟

Git是一个分布式的版本管理系统,用于快速和高效地对项目进行处理。假设小懒是一名从未使用过Git的学生,她入职公司后可能会有以下的Git使用场景。

1. 下载Git

在macOS上通过 Homebrew 下载Git

$ brew install git

查看安装的Git的版本:

$ git --version
git version 2.36.1

git-guigitk安装上,以便之后用gitk命令,调起图形界面,直观地查看提交历史记录:

$ brew install git-gui

2. 创建Github账号

Github和Git是不同的东西,Github是项目代码的托管平台,支持Git,使用Github可以通过网页上的交互来执行Git操作。

Gitlab、Github和码云都是代码托管平台,用法都是类似的,本文以Github为例进行说明。

打开:github.com/ ,点击Sign up 按钮,根据提示逐步操作,注册Github账号。

3. 设置用户信息

使用git config设置全局的名字和邮箱:

$ git config --global user.name "renmo"
$ git config --global user.email "renmo@example.com"

这些用户信息会在提交时用到,每个提交(commit)都会带上对应的用户信息。

这里使用了--global,用户信息会写到全局的~/.gitconfig文件中,如果没有--global,用户信息就会写到项目目录下的.git/config文件中。当提交commit代码时,commit信息会包含配置文件中配置好的用户信息(优先使用.git/config文件中的信息)。

可以通过git config --list查看配置列表(输入q退出)。

4. 初始化版本库

创建一个文件夹,执行 git init 创建一个空的Git版本库。

$ mkdir project-a
$ cd project-a
$ git init

执行git init会使用master作为初始分支的名称。

有了版本库之后,就可以对项目代码进行各种处理,比较常见的用法如下图所示:

Concept map (2).png

git add 将工作目录(Working tree)的修改暂存(stage)到索引(Index)中,索引存储的是整个工作目录的快照;用 git commit 将暂存在索引的修改更新到本地版本库中;用 git push 更新远程版本库的内容(用本地引用更新远程引用);用 git pull 把远程版本库的更改合并到本地版本库的当前分支。

5. 创建远程版本库

登录Github,点击右上角顶部导航条中的 "+" 号,点击 "new repository" 创建远程仓库。

2022-07-02-22-28-25-image.png

进行ssh设置之后,就可以使用ssh协议从版本库拉取/向版本库推送代码。本文选择使用https协议与远程版本库进行交互。

6. 建立与远程仓库的连接

git remote 将本地Git版本库与远程的版本库地址关联,一般使用origin作为远程版本库的名字。

$  git remote add origin https://github.com/renmo/project-a.git

查看远程仓库的地址和对应的名字:

$ git remote -v
origin    https://github.com/renmo/project-a.git (fetch)
origin    https://github.com/renmo/project-a.git (push)

7. 将修改暂存到索引

git add 将工作目录(Working tree)的修改暂存到索引(Index)中,索引存储的是整个工作目录的快照。

(1)小懒在项目目录下创建了一个新的a.txt文件,文件内容为a111。(本文使用了vim编辑器,使用其他编辑器可以不用执行vim命令)

$ vim a.txt
$ cat a.txt
a111

(2)此时执行 git status 查看工作目录的状态:

$ git status
位于分支 master

尚无提交

未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
    a.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

(3)将修改的文件暂存到索引中:

$ git add .

. 表示当前目录,git add .会将当前目录以及其子目录下的所有变更都暂存到索引中。因为目前只修改了a.txt文件的内容,也可以只将a.txt文件添加到索引中:git add a.txt

(4)执行完git add之后,再查看工作目录的状态:

$ git status
位于分支 master

尚无提交

要提交的变更:
  (使用 "git rm --cached <文件>..." 以取消暂存)
    新文件:   a.txt

8. 提交修改到版本库

提交操作会更新版本库的内容,将索引中暂存的修改更新到版本库中。

执行 git commit 提交文件:

$ git commit -m "新增a文件"

执行git status查看工作目录状态:

$ git status
位于分支 master
无文件要提交,干净的工作区

如果提交之后发现-m后写的描述不恰当,可以使用 git commit --amend进行修改。 git commit --amend能修改最新的一个提交的提交信息(实际上是创建一个新的提交替换旧提交)。

9. 更新远程版本库

使用 git push 更新远程版本库:

$ git push -u origin master

目前本地的master分支还没有设置对应的上游远程分支,需要使用--set-upstream设置上游分支,--set-upstream可以简写为-u。设置好之后,下一次推送代码就可以直接写git push了。

push操作需要账号和密码,Github已经移除了密码验证的方式,需要创建个人access token,在需要输入密码的地方使用这个token。

(1)如果还没验证邮箱的话,进行邮箱验证。

        点击右上角的头像 -> 点击settings -> 点击Access 部分的Emails -> 点击 Resend verification email -> 点击发送到邮箱中的链接 。 走完以上步骤验证就完成了。(如果邮箱地址下面没有Resend verification email按钮,说明是已经验证过的邮箱)

(2)生成access token

      点击右上角的头像 -> 点击settings -> 点击 Developer settings -> 点击Personal access tokens -> 点击Generate new token -> 通过Note选项设置token的描述 -> 通过Expiration选项设置token的到期时间 -> 选择要授予这个token的权限(选中repo就能从命令行访问仓库)-> 点击Generate token 按钮 。 执行完以上步骤就能得到一个access token。

10. 创建分支

小懒所在公司,master分支是测试环境的分支,pro是生产环境的分支,dev是开发分支。当master分支发生push操作时,会触发测试环境对master分支代码的构建,代码就发布到了测试环境;规定只能从pro分支创建标签,当远程代码库创建了一个新标签,正式环境会将标签对应的代码进行构建,代码就发布到了正式环境。(不是一定要按这种方式使用分支,比如也可以配置成,dev有合并时触发测试环境的构建,master有合并时触发正式环境的构建。)

现在小懒的本地仓库中只有初始化版本库时创建的初始分支(master),需要创建pro和dev分支,以创建dev分支为例,使用 git branch 创建新分支:

$ git branch dev

使用git branch 或者git branch --list列出分支名称。(输入q退出)

  dev
* master

前面带*表示的是当前分支。

git branch --remote查看远程追踪分支,git branch --all查看本地分支和远程追踪分支。注意这里说的是远程追踪分支,是本地对远程分支的追踪分支,分支中的内容还是存储在本地的,如果不从远程分支拉取代码,远程追踪分支中的代码就不能确保是最新的。

11. 切换分支

git branch dev命令只是创建了dev分支,并没有切换到新的分支(从git branch的结果能看出当前分支依然是master)。

使用 git checkout 切换分支。

$ git checkout dev

这里简单了解一下HEAD,HEAD是指向当前分支的最新提交一个符号引用。创建分支时,使用当前分支工作目录和索引的内容更新新的分支,并且将HEAD指向新的分支(这样后续的提交就会提交到新分支上)。

执行完git checkout dev之后,新分支dev就有和当前分支master一样的代码内容,包括使用git add已经暂存到索引中的内容,以及只在工作目录进行了修改,还没有暂存到索引中的内容。在这之后进行的commit操作会提交到新分支dev上。

创建分支和切换分支可以合并为一步:

$ git checkout -b dev

可以指定切换的起点,而不是使用当前分支的内容,比如想要从远程追踪分支创建并切换到新的分支,可以使用git checkout -b fix-branch origin/master

12. 删除分支

小懒只是尝试着创建了fix-branch分支,但是并不想要保留这个分支。于是执行以下命令将分支删除:

$ git branch -d fix-branch

Git的很多指令的选项都有简写形式,一般是首字母的小写,比如--delete的简写形式就是-d,相同字母的大写是不同的选项的简写,比如-D--delete --force的简写。

13. 查看代码差异

(1)小懒进行了新的需求开发,在a.txt中添加了一行a222

$ vim a.txt
$ cat a.txt
a111
a222

工作目录中的文件内容发生了变化,使用 git diff 查看差异:

$ git diff
diff --git a/a.txt b/a.txt
index f420e93..298e5e0 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1,2 @@
 a111
+a222

git diff查看工作目录发生的,但是还没有暂存到索引中的改变。

(2)执行git add .将文件的修改添加到了索引中。

执行以下命令查看索引和本地版本库中最后一次提交之间的差异:

$ git diff --cached

展示的内容与(1)中相同,此时再执行git diff看到的内容为空。

执行以下命令查看工作目录和最后一次提交之间的差异:

$ git diff HEAD

展示的内容与(1)中相同。

(3)执行git commit -m "a.txt文件中新增a222"提交更新。

提交之后,本地版本库中的内容也更新了,此时工作目录、索引、本地版本库中的内容是一致的,执行git diffgit diff --cached看到的内容都为空。

14. 更新远程版本库

使用 git show-branch 查看分支和对应的提交:

$ git show-branch 
* [dev] a.txt文件中新增a222
 ! [master] 新增a文件
  ! [pro] 新增a文件
---
*   [dev] a.txt文件中新增a222
*++ [master] 新增a文件

当前的分支使用*标识,其余分支使用!标识。如果提交存在于第i个分支上,就会在第i个位置显示+号,否则就会显示空格 -表示合并的提交。

打印出的结果表示,一共有3个分支:devmasterpro,当前所在的分支是dev,包含 "新增a文件" 和 "a.txt文件中新增a222" 这两个提交。

使用上文(9. 更新远程版本库)提到过的git push命令,将dev分支的修改更新到远程版本库:

2022-07-03-10-10-51-image.png

在不同分支中切换容易忘记当前是哪个分支,需要执行git branch命令确认,安装 zsh 之后,能够向上图那样直观地看出当前在哪个分支。

15. 合并分支

目前的代码在dev分支,经过小组的review,小懒的代码没什么问题,现在要将代码发布到测试环境。上文说过,当远程版本库的master分支发生push操作时,会触发测试环境对master分支代码的构建,代码就发布到了测试环境。

执行 git merge 合并分支:

$ git checkout master
切换到分支 'master'
您的分支与上游分支 'origin/master' 一致。
$ git merge dev
更新 012b4af..07387b4
Fast-forward
 a.txt | 1 +
 1 file changed, 1 insertion(+)

git merge dev会将dev分支的提交合并到master分支。

将代码推送到远程仓库:git push

16. 打标签

测试环境测试通过,现在要将代码发布到正式环境。上文说过,当在远程版本库打了一个新标签的时候,正式环境会对标签对应的代码进行构建,代码就发布到了正式环境。

在打标签之前,先切换到pro分支,将修改合并到pro分支:

$ git checkout pro
$ git merge master
$ git push --set-upstream origin pro

标签通常指向某个特定的提交,截止到该提交,代码已经到了一个稳定的状态。使用 git tag 打标签:

$ git tag -a v0.0.1 -m "a111和a222"

-a表示创建的是附注(annotated)标签,-m后面是对这个标签的描述。

将标签推送到远程仓库:

$ git push origin v0.0.1

到此需要小懒执行的发布线上的步骤就已经走完,剩下的就是等代码构建完成了。

可以执行git tag可以查看标签名称。

使用 git show 查看标签对象:

$ git show v0.0.1
tag v0.0.1
Tagger: renmo <renmo@example.com>
Date:   Sun Jul 3 11:52:41 2022 +0800

a111和a222

commit 07387b481556986388fbef05563d95e871061b97 (HEAD -> pro, tag: v0.0.1, origin/master, origin/dev, master, dev)
Author: renmo <renmo@example.com>
Date:   Sun Jul 3 09:36:52 2022 +0800

    a.txt文件中新增a222

diff --git a/a.txt b/a.txt
index f420e93..298e5e0 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1,2 @@
 a111
+a222

打印出的内容中包含标签名称,标签的创建者,创建日期,标签描述,对应的提交,提交的具体修改。

使用 git log 查看提交记录:

$ git log
commit 07387b481556986388fbef05563d95e871061b97 (HEAD -> pro, tag: v0.0.1, origin/master, origin/dev, master, dev)
Author: renmo <renmo@example.com>
Date:   Sun Jul 3 09:36:52 2022 +0800

    a.txt文件中新增a222

commit 012b4afe415687fb369f1062329b1ca751a1e221
Author: renmo <renmo@example.com>
Date:   Sat Jul 2 23:12:13 2022 +0800

    新增a文件

当我们想要表达,从commit 012b4afe415687fb369f1062329b1ca751a1e221 到 commit 07387b481556986388fbef05563d95e871061b97 的代码包含了功能 "a111和a222"时,可以简单地描述为:版本v0.0.1的代码包含了功能 "a111和a222"。

17. 处理冲突

Git是分布式管理系统,不同的用户可以在不同的环境进行Git操作,相互之间完全独立。(因为只有一个账号,所以通过在本地版本库和远程版本库分别进行操作来模拟团队协作。)

小懒的同事小勤也在维护这个项目,她在a.txt中新增了一行a123,并将这个修改push到了远程版本库的dev分支。

小懒在本地的dev分支的a.txt中新增了一行a333,修改完成后准备将这个修改push到远程版本库。

$ vim a.txt
$ cat a.txt 
a111
a222
a333
$ git add .
$ git commit -m "在a.txt文件中新增a333"
[dev 2996944] 在a.txt文件中新增a333
 1 file changed, 1 insertion(+)
$ git push
To https://github.com/renmo/project-a.git
 ! [rejected]        dev -> dev (fetch first)
error: 无法推送一些引用到 'https://github.com/renmo/project-a.git'
提示:更新被拒绝,因为远程仓库包含您本地尚不存在的提交。这通常是因为另外
提示:一个仓库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更
提示:(如 'git pull ...')。
提示:详见 'git push --help' 中的 'Note about fast-forwards' 小节。

因为远程分支已经有过一些修改,所以在推送代码之前,需要先执行git pull拉取远程分支的代码到本地,并合并到本地的分支。

git pull 相当于 git fetch(拉取代码) + git merge/ git rebase (将提交应用到当前分支)。

$ git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
展开对象中: 100% (3/3), 639 字节 | 319.00 KiB/s, 完成.
来自 https://github.com/renmo/project-a
   07387b4..d315e52  dev        -> origin/dev
提示:您有偏离的分支,需要指定如何调和它们。您可以在执行下一次
提示:pull 操作之前执行下面一条命令来抑制本消息:
提示:
提示:  git config pull.rebase false  # 合并
提示:  git config pull.rebase true   # 变基
提示:  git config pull.ff only       # 仅快进
提示:
提示:您可以将 "git config" 替换为 "git config --global" 以便为所有仓库设置
提示:缺省的配置项。您也可以在每次执行 pull 命令时添加 --rebase、--no-rebase,
提示:或者 --ff-only 参数覆盖缺省设置。
fatal: 需要指定如何调和偏离的分支。

在拉取代码之后,执行git merge还是git rebase,要根据配置来定,小懒按照提示选择将pull的行为设置为合并。

$ git config pull.rebase false

设置好之后重新执行git pull

$ git pull
自动合并 a.txt
冲突(内容):合并冲突于 a.txt
自动合并失败,修正冲突然后提交修正的结果。
$ cat a.txt
a111
a222
<<<<<<< HEAD
a333
=======
a123
>>>>>>> d315e524f6fc6d4ac3a2c2304ecab3aa6aa2b704

会提示 a.txt 文件中存在冲突,查看 a.txt 文件,发现多出了一些符号,<<<<<<< HEAD 和 ======= 之间的是本地的最新提交,======= 和

>>>>>>> d9f4249b22746bce515e8adb74dee7703a438b4f

之间是远程分支的提交。

解决冲突要看具体的代码内容,调整之后删除多余符号,然后重新提交。小懒发现有冲突之后,首先看了下代码逻辑,a333和a123是可以并存的功能,可以只保留其中一个或者两个都保留,得问问进行这段代码修改的人要实现的需求是什么,于是她使用git log查看了下提交日志:

$ git log d315e524f6fc6d4ac3a2c2304ecab3aa6aa2b704

发现是小勤写的这段修改,她去找小勤讨论了下,最终确定只保留代码a333即可。接下来就修改文件内容,解决冲突之后,提交代码并将代码推送到了远程分支:

$ vim a.txt
$ cat a.txt
a111
a222
a333
$ git commit -a -m "处理a.txt的冲突"
$ git push

-a会将自动暂存修改和删除了的文件,但是新创建的文件不会被自动暂存。

18. 变基

git rebase 可以把分支的提交放到另一个分支的最新提交的后面。

小懒在本地dev分支新增3个提交。小勤在远程dev分支新增了两个提交。现在小懒要将远程dev分支的更改添加到本地的代码中。

(1)执行 git fetch 拉取远程分支的代码到本地:

$ git fetch origin dev

(2)执行git rebase进行变基:

$ git rebase origin/dev

使用gitk命令,查看提交记录:

2022-07-03-14-20-08-image.png

可以看到dev分支的提交放到了origin/dev分支的提交的后面。

将本地代码撤回到执行这个rebase之前的状态,执行merge操作,比较两者的不同。

(1)执行 git reflog 查看引用日志(references logs):

72fa1b7 (HEAD -> dev) HEAD@{0}: rebase (finish): returning to refs/heads/dev
72fa1b7 (HEAD -> dev) HEAD@{1}: rebase (pick): 新增b333
4f0459c HEAD@{2}: rebase (pick): 新增b222
64b8037 HEAD@{3}: rebase (pick): 新增b.txt文件
1e8b581 (origin/dev) HEAD@{4}: rebase (start): checkout origin/dev
04193af HEAD@{5}: commit: 新增b333
d8aa0c1 HEAD@{6}: commit: 新增b222
5c7ee64 HEAD@{7}: commit: 新增b.txt文件
...

根据打印出的内容,rabase之前的提交是HEAD@{5}(HEAD@{5}表示5步之前的HEAD)。

(2)执行 git reset 重置到HEAD@{5}提交时的状态。

$ git reset --hard HEAD@{5}

(3)执行 git merge 合并代码:

$ git merge origin/dev

执行gitk命令,查看提交记录:

2022-07-03-14-35-59-image.png

下图简化了gitk命令中展示的提交记录,直观地表示rebasemerge两者的区别,图中的每个小圆点都表示一个提交:

Decision tree (3).png

C、D、E 依次对应本地dev分支上的提交“新增b.txt文件”、"新增b222"、"新增b333",F、G 依次对应远程dev分支上的提交"新增a444"、"新增a555"。

git rebase origin/dev将dev分支上的提交放到了origin/dev分支上的提交的后面,CC^{'}DD^{'}EE^{'}和C、D、E提交包含相同的修改内容,但是它们是不同的提交。

dev分支执行git merge origin/dev会将origin/dev分支的代码合并到dev分支。并且会产生一个合并提交H,合并之后,dev分支就会包含A、B、C、D、E、F、G、H的提交内容。

19. 撤销提交

git revert 会添加一个反向的新的commit到分支中。

小懒在本地进行测试的时候,发现 "新增b333" 部分的提交导致了一个bug,她需要将这部分的修改进行恢复。

使用 git show-branch 查看分支和对应的提交:

$ git show-branch
* [dev] Merge remote-tracking branch 'origin/dev' into dev
 ! [master] a.txt文件中新增a222
  ! [pro] a.txt文件中新增a222
---
-   [dev] Merge remote-tracking branch 'origin/dev' into dev
*   [dev^2] 新增a555
*   [dev^2^] 新增a444
*   [dev^] 新增b333
*   [dev~2] 新增b222
*   [dev~3] 新增b.txt文件
*   [dev~4^2] 新增a123
*   [dev~5] 在a.txt文件中新增a333
*++ [master] a.txt文件中新增a222

根据展示的内容 选择要撤销的提交~表示祖先提交,^表示父提交)。

现在需要撤销的是"新增b333",它是dev分支当前提交的第1个父提交(dev^),执行:

$ git revert dev^

使用gitk查看提交内容:

2022-07-03-16-04-54-image.png

20. 重置修改

git reset 重置当前的HEAD为一个指定的状态。

(1)小懒修改了代码并且已经commit了,但是她发现commit的信息写错了,于是她执行如下语句撤回commit:

$ git reset --soft HEAD^

将最新提交设置为HEAD的前面一个提交(HEAD^)。使用--soft,索引和工作目录的内容都不会发生改变。相当于重置到commit之前。

撤回commit之后,重新执行git commit -m "正确的描述信息"提交修改。

(2)小懒新创建了一个c.txt文件,这个文件只是本地使用的,应该使用.gitignore忽略的,但是她还没往.gitignore中添加这个文件,就使用git add .将它暂存到索引了,还没有提交。于是执行以下语句撤回暂存:

$ git commit reset --mixed HEAD

将最新提交设置为HEAD提交。使用--mixed,索引的内容会发生改变,但是工作目录的内容不会发生改变。相当于重置到git add之前。

撤回暂存之后在.gitignore文件中添加上c.txt,就不会再把 c.txt 文件包含在内了。

(3)小懒在提交了修改之后,发现这部分修改是完全错误的,于是她执行以下语句撤回提交。

$ git reset --hard HEAD^

将最新提交设置为HEAD的前面一个提交。暂存区的内容和工作目录的内容都会改变。相当于重置到修改代码之前。

21. 暂存(stash)文件

git stash 将更改存储在一个脏的工作目录中。它会将工作目录恢复为当前HEAD的状态,但是工作目录到目前为止进行的修改是保存起来的。git stash不会保存新增的文件和被.gitignore忽略的文件。

小懒在本地dev分支,修改了a.txt文件,现在她需要去pro分支做一个小修改,提交代码之后马上回来。此时不能使用git checkout pro直接切换到pro分支:

$ git checkout pro
error: 您对下列文件的本地修改将被检出操作覆盖:
    a.txt
请在切换分支前提交或贮藏您的修改。
正在终止

(1)执行以下命令将修改存储起来

$ git stash save

执行git stash list命令可以查看暂存的修改。

(2)切换到pro分支修改pro分支的代码并提交。

(3)切换回dev分支,执行git stash pop将暂存的内容应用到工作目录中。继续进行开发。

22. 应用指定提交

可以使用git cherry-pick把一个提交添加到分支中。

小懒执行以下步骤,把dev分支的"新增a555"这个提交的修改应用到pro分支,在dev已经查看到这个提交的ID是:1e8b5817fa38d0ecb7dd81ec2fc31d4490d45ac7。

$ git checkout pro
$ cat a.txt
a111
a222
$ git cherry-pick 1e8b5817fa38d0ecb7dd81ec2fc31d4490d45ac7
自动合并 a.txt
冲突(内容):合并冲突于 a.txt
error: 不能应用 1e8b581... 新增a555
提示:解决所有冲突之后,用 "git add/rm <路径规格>" 标记它们,
提示:然后执行 "git cherry-pick --continue"。您也可以执行
提示:"git cherry-pick --skip" 命令跳过这个提交。如果想要终止执行并回到
提示:执行 "git cherry-pick" 之前的状态,执行 "git cherry-pick --abort"。
$ cat a.txt
a111
a222
<<<<<<< HEAD
=======
a333
a444
a555
>>>>>>> 1e8b581 (新增a555)
$ vim a.txt
$ cat a.txt
a111
a222
a333
a444
a555
$ git add .
$ git commit -m "应用dev的"新增a555"提交到pro分支"

23. 强制覆盖代码

除非必要,否则不要使用强制覆盖。

(1)用远程代码强制覆盖本地代码。

$ git pull --force origin master:dev

master是远程分支的名字,dev是本地分支的名字。

(2)用本地代码强制覆盖远程代码。

$ git push --force origin master

24.修改账号名

创建了账号之后最好不要修改账号名,因为我这个Github账号目前影响很小,所以改了笔名之后也把账号名改了一下。电脑上已经存在的代码仓库会自行重定向到新的代码仓库,但是会给出提示:

remote: This repository moved. Please use the new location:

remote:   git@github.com:rengmo/myBlog.git

所以需要手动修改下本地仓库对应的远程仓库地址:

$ git remote -v
origin	git@github.com:renmo/myBlog.git (fetch)
origin	git@github.com:renmo/myBlog.git (push)
$ git remote set-url origin git@github.com:rengmo/myBlog.git
$ git remote -v
origin	git@github.com:rengmo/myBlog.git (fetch)
origin	git@github.com:rengmo/myBlog.git (push)