Git
Git 官网对 Git 的解释是
a free and open-source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.
即一个免费的开源分布式版本控制系统. 它可以快速高效的处理大大小小的不同系统.
不同于 SVN (subversion), Git 是分布式的. 也就是说, 虽然像 GitHub 之类的服务器让我们使用 Git 更方便, 但是中心化的服务器并不是 Git 的必要部分. 这种去中心化的结构使得 Git 在项目管理中有更高的灵活度, 使得它非常适合开源项目的管理.
- 官方指南 (有中文版)
不建议在 Git 系统中加入过多的二进制文件或者压缩文件 (比如
.docx
,.paper
,.jpg
). 那些必须的二进制文件最好是放在其他地方 (比如图床), 然后把它们的路径加入 Git 文件中管理. 经常修改的二进制文件会让 Git 仓库体积爆炸, 拖慢整个系统. 在 Git 中推荐使用 Markdown 作为文字记录工具.
安装
在 Debian 系统中, Git 可以用 sudo apt-get install git
直接安装.
在其他 Linux 系统中都可以用相应的包管理器类似的安装 Git.
Git 官网提供了一个非常好用的 Windown shell: GitBash.
它包含了一些可以在 Windows 上使用的常用工具, 包括 Git.
术语
为了理解 Git 的功能, 我们首先需要介绍一些术语.
Repository
仓库 (repostiory) 是 Git 系统的核心, 一般被缩写为 repo.
正如它的名字, 它是存放一个项目中所有文件 (以及所有历史版本) 的一个仓库.
仓库看起来与一般的工作文件夹没有太多区别.
所有那些 dirty works 都会被藏在 .git/
文件夹中.
它包含了所有版本控制需要的数据.
Workspace/Working Tree
Working tree 就是仓库中所有在 .git/
文件之外的内容.
我们在项目中写代码的时候可以完全当 Git 不存在.
这个 working tree 就像普通的文件夹一样.
除了这个工作区以外, repo 中的文件结构并不重要. 在工作区外的内容并不是存在某个真的”文件夹”或者”路径”下的. 它们都是一些抽象的内容, 会被 Git 分块压缩储存.
Stage/Index
暂存区 (stage) 是 Git 用来存放一个工程的临时版本的地方. 在 Git 中标准叫法是”索引” (index). 在工作的时候我们可以非常简单的暂存一个项目的当前状态, 并且在需要的时候回滚到上一次暂存的版本.
Commit
一旦一个项目完成了一个重要的节点, 你可以把当前版本归档到 Git. 你只需要把你暂存区中的内容提交到仓库历史即可. 每一个提交都包含了该项目的当前版本, 以及与上一个版本的差别的信息. 这些提交会根据依赖历史构成一条链. 正是这些提交使得 Git 构成了一个良好的版本控制系统.
每一个提交都会带上它的散列值 (hash) 作为 id, 以及提交者的名字和时间戳. 在提交的时候最好要带上一些注记. 这样在之后我们可以更容易理解项目提交历史. 另外, 我们也可以用 GPG 对提交进行签名. 这可以避免其他人非法的秘密修改你的提交.
Head
Head 是指向当前提交的指针. 它被用来检出 (check out) 历史版本或者进行版本回滚.
Branch
Git 允许你的项目平行存储多个版本. 每一个平行的版本被称为分支 (branch). 每一次工作区中只会有一个版本存在. 但是你可以用 Git 在不同版本之间跳转. 正是分支管理使得 Git 可以进行多人合作.
注意, 当你在不同分支间跳转的时候, 所有没有被提交的内容都会丢失.
如果你不想提交当前版本, 你需要记得把它临时用 stash
储藏起来.
使用 Git
新建仓库
我们可以在任何文件夹中用 init
创建一个仓库:
:path_to_repo $ git init
Initialized empty Git repository in [path_to_repo]
因为我们还没有向暂存区存入任何文件, 所以它是一个空的仓库 (即使你这个文件夹本来非空).
临时版本控制
我们可以把文件加入暂存区:
$ git add [file] [another file]
你可以一次加入多个文件, 或者用点 .
一次加入所以文件:
$ git add .
你可以用 git status
查看自上一次暂存起, 哪些文件被修改了 (或者尚未被加入 Git, 或者被删除了).
类似于 git add
, 我们可以用 git rm
删除暂存区中 (不是工作区) 里的文件.
提交到历史树
一旦我们把文件暂存, 它们就可以被提交到历史树中去了. 这个可以由
$ git commit -m "一些注记"
[master (root-commit) b2df984] '一些注记'
1 file changed, 1 insertion(+)
create mode 100644 demo.txt
这里 b2df984
是这个提交的 id.
如果我们不使用 -m
参数加入注记, Git 会弹出一个编辑器让你添加注记.
你可以加入 -S
参数去使用 [GPG] 签名当前提交.
$ git commit -S -m "sign the commit"
[master 7e45c33] sign the commit
1 file changed, 1 insertion(+), 1 deletion(-)
每次提交的信息都可以在 git log
中查到
$ git log
commit 7e45c334a3f60be6fa3d31cb91305ab0bd383376 (HEAD -> master)
Author: Demo <demo@demo>
Date: Thu Apr 25 23:12:15 2019 +0800
sign the commit
commit b6b7e26f3b2089cec745e90e7c073a7cd6a39695
Author: Demo <demo@demo>
Date: Thu Apr 25 23:06:15 2019 +0800
Another Commit
版本比较
diff
是 Git 的一个强大功能.
它可以被用来比较任意两个文件
当我们想比较两次提交或者是暂存区与当前文件的区别是这很有用.
$ git diff
diff --git a/demo.txt b/demo.txt
index 0f22871..e019be0 100644
--- a/demo.txt
+++ b/demo.txt
@@ -1 +1 @@
-extra
+second
a
和 b
是用来标记两个不同文件的索引.
在输出中 -
符号代表的是文件 a
中被删除了内容, +
符号代表着 b
中被添加了内容.
在 @@
符号之间的数值代表了被修改的文字的行数.
下面跟着的内容是两个文件间具体的差别.
我们可以利用一些工具, 比如 GitHub Desktop 来帮助我们阅读 diff
的输出.
这个强大的工具只能被用于比较文本文件. 这也是为什么不推荐在 Git 中管理二进制文件.
diff
的使用细节见最后的 cheat sheet.
回滚
作为版本控制系统, Git 提供了几种不同的回滚方法. 其中主要的是两种:
revert
reset
revert
命令仅仅把项目的状态回滚到历史版本.
这个命令在回滚的同时会提交一个新的历史, 记录这次回滚操作.
如果你想把历史记录也回滚了, 那么就需要使用 reset
命令.
如果想要复位 (reset) 到某一次提交, 我们需要提供它的 id.
这可以在 log 中找到.
如果不提供 id, 默认是复位到 HEAD
的位置.
我们也可以使用 HEAD^^^
指明某次提交.
这里的每一 ^
都代表着上一个提交.
也就是说 HEAD^^^
代表着 HEAD
向前数 3 个提交.
由于 Git 是一个两步版本管理系统, 拥有多个不同的存放位置, 复位的时候需要指明复位到哪里. 具体的使用见 cheat sheet.
reset
仅仅复位了HEAD
的位置. 提交本身并不会被删除. 你可以在reflog
中找到所以的历史, 并找回那些提交.$ git reflog e475afc HEAD@{1}: reset: moving to HEAD^ 1094adb (HEAD -> master) HEAD@{2}: commit: sign the commit e475afc HEAD@{3}: commit: Another commit eaadf4e HEAD@{4}: commit (initial): Some notes
这个历史包含了所有的提交的 id, 包括那些被重置的, 比如
1094adb
.
改变工作区
Git 可以并行维护多个分支.
这保证了团队中不同的人可以独立的修改项目中不同的部分, 最后再组装到一起.
也就是说, 每一个人都可以生成一个他自己的分支, 然后在工作完成后合并进主支.
传统上主支被称为 master
.
你可以下载其他人的分支并且检出他们当前的进度.
如果需要在不同分支间跳转, 你需要检出命令 checkout
$ git checkout master
Switched to branch 'master'
M demo.txt
这里的 M
代表着这个文件在 checkout
前后有变化.
如果我们在 checkout
的同时加入了 -b
参数, 那么如果这个分支不存在, 它就会被创建.
这等价于先 git branch [brangh]
, 然后 git checkout [branch]
.
(参数 -B
的效果类似, 但是不推荐使用, 因为它不管原来分支是否存在, 都会覆盖这个分支.)
checkout
同样可以被用来查看历史版本的文件
$ git checkout demo.txt
Updated 1 path from the index
如果我们不指定提交, 那么它默认检出的是暂存区里的文件.
不同于 reset
, checkout
并不会移动 HEAD
的位置 (除非你检出的是另一个分支, 它会把 HEAD
指向那个分支).
远程仓库
作为一个分布式的系统, Git 长仓库可以通过网络分布到不同的设备上. Git 中并不存在所谓的’中心服务器’. 所有的服务器都是平等的. 但是在实际操作中大家一般都会使用一个单独的服务器, 这避免了同步的难题.
仓库一般通过 ssh 隧道在不同设备间传输.
为了链接上一台 Git 服务器, 你需要将你的 ssh 公钥发给服务器.
所有主流的服务商, 比如 GitHub, Gitlab, 以及码云都提供了如何使用 ssh 的简单指导.
主要服务商可能同时提供了 https://
协议的传输机制.
这对初学者而言可能更加简单.
但是一般这个协议需要你每次操作都输入你的账号密码.
在 ssh 配置好之后, 你剩下要做的就是找到远程仓库的地址.
这一般会长成 git@github.com:用户名/仓库名.git
的样子.
你可以将远程仓库链接到你的本地仓库
$ git remote add origin git@github.com:用户名/仓库名.git master
origin
是远程仓库的默认名称.
在配置好远程仓库之后, 你就可以同步远程与本地仓库了.
如果想把本地仓库推送去远程, 只需要使用前面设置好的别名 origin
$ git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.64 KiB | 560.00 KiB/s, done.
Total 20 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To git@github.com:UserName/Demo.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
第一次推送的时候必须指明分支 master
.
参数 -u
代表着创建本地 master
分支与远程 master
分支之间的链接.
这可以在输出的最后一行看到.
一旦这个链接搭好, 之后的推送都不需要指定分支名了.
合并
在一般的项目中, 一般会使用独立的分支开发新的功能.
这个分支通常被命名为 dev
, 或者 dev-功能名
.
一旦新的功能被做好, 我们就需要把它加入主支
这需要在主支中用 merge
合并.
如果 dev
与主支 master
之间没有冲突, 它会使用快速模式 Fast-forward
:
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
demo.txt | 1 +
1 file changed, 1 insertion(+)
相反, 如果出现冲突你会看到 Git 的提示并且进入 merging
模式.
$ git merge dev
Auto-merging demo.txt
CONFLICT (content): Merge conflict in demo.txt
Automatic merge failed; fix conflicts and then commit the result.
在这个模式中, git status
会告诉你冲突的位置.
你可以用 git merge --abort
放弃本次合并, 并且回滚到一般模式.
你也可以直接编辑列出的冲突文件, 解决冲突.
在 merging
模式中, 你会看到如下的冲突行
<<<<<<< HEAD
This is master branch.
=======
This is develop branch.
>>>>>>> dev
在 <<<<<<< HEAD
与 =======
直接的行属于 HEAD
.
而在 =======
之下的行属于 dev
分支.
你需要把这一块代码替换为你最后需要的代码, 然后保存文件.
完成修改后你可以 add
文件到暂存区, 然后 commit
本次合并.
分支的分叉与合并会在 Git 记录中形成环路.
你可以用 git log --graph
中用图模式看到这些环路.
$ git log --graph --pretty=oneline --abbrev-commit
* 2f128c4 conflict solved
|\
| * 1c85f25 a conflict commit
| * dd37a3b commit in dev
* | a22211f second commit
|/
* 1c7e0d8 first commit
不过在命令行中修改这次冲突相对麻烦.
GitBash 有一个内置的 GUI 可以辅助阅读这些记录.
它可以用 git gui
打开.
但最好还是使用其他的 GUI 工具修改冲突.
我个人倾向于使用 VSCode.
它的界面更复合人的阅读习惯, 而且更智能.
仅仅是无插件的 VSCode 就足以处理所有的轻量级项目了.
如果需要修改默认工具, 只需要把下面的代码复制进 Git 的配置文件:
[merge]
tool = vscode
[mergetool "vscode"]
cmd = "code --wait $MERGED"
[diff]
tool = vscode
[difftool "vscode"]
cmd = "code --wait --diff $LOCAL $REMOTE"
这个文件可以用 git config -e
修改.
其他特性
Git 还提供了其他的很好用的特性.
储藏
储藏提供了一个临时保存工作区的位置.
如果你需要紧急切换到另一个分支, 比如主支 master
上突然发现了重要 bug, 你当前分支里没有提交的修改将会消失.
如果你不想提交当前状态, 那么你可以用 stash
储藏工作区.
git stash list
可以列出所有储藏的记录, 而 git stash pop
将会取出最后一次储藏的记录.
如果你需要取出更老的版本, 你需要在命令后面指明它的 hash.
这可以在 stash list
中被找到.
标签
记提交的 id 这件事还是挺麻烦的.
你可能需要每次都读一遍提交历史, 然后找到你想要的那个旧版本.
对于一些重要的节点, 你可以用 tag
给它们打上标签, 比如 v0.1
.
可以用 git tag
列出当前分支中的所有标签.
然后用 git show [tag]
来查看对于标签的信息
$ git tag -a v0.1 -m "Some notes"
将在当前提交上添加一个标签 v0.1
.
这个参数 -a
代表着你想给这个标签加上你的信息以及时间戳.
参数 -m
表示将在标签里加上注记.
你也可以用 -s
替代 -a
来额外用 GPG 公钥给它签名.
旧的提交也可以用指明 id 的方法添加标签.
比如 $ git tag -a v0.1 ffffff
.
标签并不会被自动推到远程仓库.
你可以通过在 push
时指明 --tag
来推送标签.
Cheat Sheet
- 配置
-
用
git config -e
来编辑配置文件. 这里-e
是--edit
的缩写.config
的默认参数是--local
, 对应的是当前仓库.--global
代表的是当前用户的配置, 而--system
是所有用户的配置.
diff
的默认目标是暂存区
命令 | 应用目标 |
---|---|
diff |
工作区 vs. 暂存区 |
diff head |
工作区 vs. HEAD |
diff --cached |
暂存区 vs. HEAD |
reset
的默认目标是 HEAD
命令 | 应用目标 |
---|---|
reset --soft |
HEAD -> HEAD |
reset --mixed |
HEAD -> 暂存区 |
reset --hard |
HEAD -> 暂存区 -> 工作区 |
checkout
的默认目标是 HEAD
命令 | 应用目标 |
---|---|
checkout |
- |
checkout [branch] |
[branch].HEAD -> 工作区 |
checkout [commit] |
[commit] -> 工作区 |