Git是一个分布式的版本管理系统,用于快速和高效地对项目进行处理。假设小懒是一名从未使用过Git的学生,她入职公司后可能会有以下的Git使用场景。
1. 下载Git
$ brew install git
查看安装的Git的版本:
$ git --version
git version 2.36.1
把git-gui和gitk安装上,以便之后用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作为初始分支的名称。
有了版本库之后,就可以对项目代码进行各种处理,比较常见的用法如下图所示:
用 git add 将工作目录(Working tree)的修改暂存(stage)到索引(Index)中,索引存储的是整个工作目录的快照;用 git commit 将暂存在索引的修改更新到本地版本库中;用 git push 更新远程版本库的内容(用本地引用更新远程引用);用 git pull 把远程版本库的更改合并到本地版本库的当前分支。
5. 创建远程版本库
登录Github,点击右上角顶部导航条中的 "+" 号,点击 "new repository" 创建远程仓库。
进行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 diff 、git 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个分支:dev、master、pro,当前所在的分支是dev,包含 "新增a文件" 和 "a.txt文件中新增a222" 这两个提交。
使用上文(9. 更新远程版本库)提到过的git push命令,将dev分支的修改更新到远程版本库:
在不同分支中切换容易忘记当前是哪个分支,需要执行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命令,查看提交记录:
可以看到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命令,查看提交记录:
下图简化了gitk命令中展示的提交记录,直观地表示rebase和merge两者的区别,图中的每个小圆点都表示一个提交:
C、D、E 依次对应本地dev分支上的提交“新增b.txt文件”、"新增b222"、"新增b333",F、G 依次对应远程dev分支上的提交"新增a444"、"新增a555"。
git rebase origin/dev将dev分支上的提交放到了origin/dev分支上的提交的后面,、、和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查看提交内容:
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)