Git
一些必备且常用的 Git 知识。
基础
Git 对待数据的方式类似快照流,不同于其他很多基于差异流的版本控制系统。Git 的每次 commit 都是一次对所有文件的快照:
每当用户提交更新或保存项目状态时,Git 就会对当时的全部文件创建一个快照并保存这个快照的索引 (提交对象)。为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
安装
安装 Git 很简单:
sudo dnf in git-all
配置
安装 Git 后需要配置一些变量,这些变量将控制 Git 的外观和行为,它们存储在 3 个不同的位置:
- /etc/gitconfig
该文件包含系统上每一个用户及他们仓库的通用配置。 如果在执行 git config 时带上 --system 选项,那么它就会读写该文件中的配置变量。
- ~/.gitconfig or ~/git/config
该文件只针对当前用户。可以传递 --global 选项让 Git 读写此文件,这会对系统上所有的仓库生效。
- anyrepo/.git/config
每个 Git 仓库都有一个该文件,它只对当前仓库生效。如果在执行 git config 时带上 --local 选项,那么就会读写该文件,这也是默认行为。
每一个级别的配置会覆盖上一级别的配置,所以 .git/config 的配置会覆盖 ~/.gitconfig 中的配置,后者会继续覆盖 /etc/gitconfig 中的配置。
使用 Git 前必须配置用户名和邮箱:
git config --global user.name "John Doe"
git config --global user.email [email protected]
默认的文本编辑器是可选的:
git config --global core.editor vim
创建一个新仓库时其默认分支名称:
git config --global init.defaultBranch main
也可以使用 config 命令查看信息:
git config user.name
这会列出当前操作最近一个配置中的变量值,建议使用下面的操作,它能显示值来自哪个文件:
git config --show-origin user.name
可以配置 Git 命令的别名来减少输入:
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.unstage 'restore --staged'
创建仓库
Git 的大部分操作都是在本地执行的,所以使用 Git 必须在本地拥有一个仓库。
- 初始化本地仓库
使用 init 子命令在本地初始化一个新仓库:
cd anydir
git init
# or
git init anydir
它会在仓库目录下创建一个 .git 目录用于存放仓库信息。
- 克隆远程仓库
使用 clone 子命令克隆一个远程仓库到本地:
git clone https://github.com/libgit2/libgit2
这将在当前目录中创建一个 libgit2 子目录来存放该仓库。也可以手动指定仓库目录:
git clone https://github.com/libgit2/libgit2 mydir
git clone https://github.com/libgit2/libgit2 .
Git 支持很多数据传输协议:https,git,ssh 等。
查看状态
一个 Git 仓库中所有文件只有两种状态:未跟踪和已跟踪。具体来说,已跟踪状态又分为 3 种,一个文件如果已被跟踪,则说明它已被纳入版本管理中。
可以使用 status 子命令查看仓库详细信息:
git status
具体来说,所有文件都只属于下列状态中的一种:
status 命令还支持简短形式的信息展示:
git status -s
一个输出例子:
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。输出中有两栏,左栏指明了暂存区的状态,右栏指明了工作区的状态。例如,上面的状态报告显示: README 文件在工作区已修改但尚未暂存,而 lib/simplegit.rb 文件已修改且已暂存。Rakefile 文件已修改,暂存后又作了修改,因此该文件的修改中既有已暂存的部分,又有未暂存的部分。
暂存文件
add 是一个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。
- 跟踪一个新文件
git add newfile
newfile 将被加入暂存区。
- 将一个已跟踪文件加入暂存区
git add modfile
modfile 是一个已跟踪的文件,但是它被修改了,使用 add 将它加入暂存区中。
创建提交
commit 用于向仓库提交一条记录,它只会提交暂存区中的所有内容:
git commit
它会启动默认编辑器来获取一些提交说明,编辑器中会包含一个空行以供输入,后面是一些注释。这些注释会在提交时被删除,即使在编辑器中删除它们也没问题。
使用 commit 命令的 -m 选项可以直接在命令行中说明注释信息,而不调用编辑器:
git commit -m "Update version"
使用 commit 命令的 -a 选项可以直接跳过暂存区,而直接将所有已跟踪且修改过的文件提交:
git commit -a -m "Update version"
注意:-a 选项只会提交已跟踪过的文件。
使用 commit 命令的 --amend 选项可以修改上一次的提交:
git commit --amend
它的效果就像:在原记录的基础上增加一条提交记录,然后删除原记录。如果修改提交时暂存区非空,则暂存区的内容会随着 --amend 一起被提交。
撤销操作
包含两种撤销操作:撤销暂存区中的文件和撤销工作区中的文件。两种命令都可以使用 restore 命令完成,也可以使用 reset 或 checkout 命令完成。
- 撤销暂存区中的文件
作用:将暂存区中的文件恢复为某次提交中的状态,默认是最近一次提交。
使用 restore 命令的 --staged 选项:
git restore --staged filename
撤销暂存区中的所有文件:
git resotre --staged .
- 撤销工作区中的文件
作用:将工作区中的文件恢复为暂存区中的状态。
使用不带选项的 restore 命令:
git restore <file>
一次性撤销所有文件:
git restore .
注意:这两个操作的交集是暂存区,它们都不影响 HEAD 指向和分支指向。当在暂存区撤销文件时,不会影响工作区。当在工作区撤销文件时,不会影响暂存区。如果要同时撤销暂存区和工作区的所有文件,可以使用 checkout 命令或 reset 命令,但要注意它们的风险:
远程仓库
clone 一个远程仓库时 Git 会自动保存远程仓库的信息,可以使用 remote 命令查看所有远程仓库的信息:
git remote
它只会列出远程仓库在本地的 alias,加上 -v 选项可以查看读写情况和仓库地址。
git remote -v
可以使用 remote 的 show 选项来显示更详细的信息:
git remote show origin
如果不从远程仓库 clone,或者想添加别远程仓库,可以使用 add 选项:
git remote add alias url
这将在本地添加一个远程仓库记录,并且设置该远程仓库的别名。
可以使用 rename 选项改变一个远程仓库的别名:
git remote rename origin newname
使用 remvoe 选项删除一个远程仓库的信息:
git remote remove origin
拉取远程仓库
fetch 命令用于拉取远程仓库的数据到本地:
git fetch origin
fetch 会访问远程仓库,从中拉取所有本地还没有的数据。执行完成后,本地将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
注意:fetch 只是简单的拉取远程仓库的数据,它不会修改本地仓库的信息,也不会自动合并。
fetch 默认拉取远程仓库的所有分支,可以手工指定拉取哪些分支:
git fetch origin master
推送本地分支
push 命令用于将本地记录推送到上游:
git push origin master
只有当用户有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。当和其他人在同一时间克隆,但他们先推送到上游然后用户再推送到上游时,用户的推送就会毫无疑问地被拒绝。用户必须先抓取他们的工作并将其合并进本地仓库后才能推送。
push 的完整语法是:
git push <remote> <local_branch>:<remote_branch>
可以省略远程分支名,这代表远程分支名和本地分支同名。
如果远程仓库是一个空仓库,那么 push 很可能失败,因为 push 不知道要推送到远程仓库的哪个分支。可以给 push 加上 -u 选项,这将为本地分支关联上游分支:
git push -u origin master
如果远程仓库不存在 master,那么 -u 会自动创建。此后,本地分支会跟踪这个远程分支。
分支
分支是 Git 的核心概念,它和 commit 一样,都是很轻量化的概念。分支本质上仅仅是指向提交对象的可变指针。Git 的默认分支名字是 master,并且 master 分支会在每次提交时自动向前移动。
可以看到:每次 commit 都会创建一个快照,而分支就是指向快照索引 (提交对象) 的可变指针。
在本地仓库中,始终存在一个 HEAD 指针,它说明当前所在的位置,HEAD 一般情况下都指向分支,此时 HEAD 相当于分支的别名。
创建分支
branch 命令基于当前提交对象创建一个新分支:
git branch testing
如果没有特殊操作,现在 testing 和 master 都指向同一个提交对象。
另一种创建分支的方法是使用带 -b 选项的 checkout 命令:
git checkout -b testing
它包含两个操作:创建分支并自动切换到该分支上。
切换分支
checkout 另一个作用就是切换分支:
git checkout testing
checkout 做两个工作:修改 HEAD 指针,以及更新工作区。由于切换分支会加载该分支关联的文件快照到工作目录中,所以要注意在切换分支前保存当前工作内容。
移动分支
branch 的 -m 选项用来移动分支,经常用它来重命名一个分支:
git branch -m old new
分支合并
由于分支非常轻量,所以一个项目中可能同时有很多分支,它们可能只关注一个很小的问题,当工作完成后,这个分支的使命就终了。接下来,主分支就可以使用 merge 命令合并它们的工作:
git checkout master
git merge issue53
先切换到主分支,然后合并 issue 分支的内容。这将在主分支上创建一个新记录,这个新纪录有两个祖先:之前 master 的最新记录,以及 issue 的最新记录。
因为 merge 涉及 3 个文件快照,所以它也叫三方合并:
查询分支
branch 命令用于查询本地分支:
git branch
git branch -v
二者只列出所有本地仓库中的分支,加上 -v 选项时,它会附加一些提交信息。
branch 的 -vv 选项会显示本地分支的上游情况:
git branch -vv
如果要查看所有分支,包括远程仓库的分支,可以使用 -a 选项:
git branch -a
合并冲突
合并两个分支时很可能遇到冲突,例如两个分支都对同一文件的相同部分作了修改,Git 无法确定要保留哪个,所以它将暂时终止合并,只有冲突被解决后,才能继续合并。
冲突发生时,冲突所在文件会被修改以标识出冲突部分:
<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div>
>>>>>>> iss53:index.html
用户可以选择保留一个分支中的内容,也可以做额外修改,但最后必须删除<<<<
,====
以及>>>>
。
解决冲突文件后,需要使用 add 命令标识该文件的冲突已被解决,就和暂存它一样。
最后,需要使用 commit 命令手动为这次发生冲突的合并提交记录,就和普通的 commit 一样。
删除分支
当一个分支已被其他分支合并后,就可以安全的删除它:
git branch -d issue53
但是,如果一个分支还没有被任何分支合并,-d 选项时无法删除它的,因为这将导致信息丢失。如果要强行删除它,可以使用:
git branch -D notmerged
跟踪上游
由于本地仓库和远程仓库时分隔的,一种联系本地分支和远程的方式就是让本地分支跟踪远程分支。
- 一个已有的分支
可以使用 branch 的 -u 选项让一个本地分支跟踪远程分支:
git checkout master
git branch -u origin/master
- 直接从远程仓库创建分支
从远程分支上直接创建一个本地分支也会自动跟踪它:
git checkout origin/master
git checkout -b master
- 其他方式
还有很多其他方式:
git checkout -b sf origin/serverfix
git push -u origin master
后一种方式会在推送的同时改变本地分支跟踪的上游。
当一个本地分支有了上游 (跟踪了远程分支),那么它和远程仓库交互就会很方便。
之前拉取远程分支并合并到本地需要两个操作 fetch 和 merge,但是现在可以直接使用 pull 命令,它会自动从当前分支的上游拉取信息,如果有更新,它会自动合并到本地:
git checkout master
git pull
贮藏
stash 命令以及其子命令用于完成贮藏工作。贮藏会将当前工作目录中的变动 (相对于上一次提交) 保存起来,并打包成一个镜像放入栈中。然后工作目录就被恢复成上一次提交时的样子。
- 创建贮藏
git stash
git stash push
上面两条命令等价。但贮藏默认只会保存已跟踪文件,如果目录中有未跟踪文件,贮藏是不会保存的,必须使用 -u 或者 -a 选项:
git stash -u
git stash -a
二者的区别在于:-u 只会保存未跟踪文件,但是 -a 还会保存 .gitignore 中忽略的文件。
- 查看贮藏
使用 stash 的 list 子命令,可以查看分支上所有贮藏镜像:
git stash list
- 删除贮藏
使用 stash 的 drop 子命令,可以删除指定贮藏镜像:
git stash drop 2
- 恢复贮藏
使用 stash 的 apply 子命令,可以恢复指定贮藏到工作区 (不指定则为最近一次记录):
git stash apply 2
注意:apply 默认不会恢复贮藏时的暂存状态,如果需要,使用 --index 选项:
git stash apply --index 2
一种常见的模式是恢复完后立即删除贮藏镜像 apply 和 drop,它们可以组合成一个操作 pop:
git stash pop
pop 会取出最近一次贮藏记录,然后立即删除它。
标签
标签和分支一样轻量:它只是指向某个提交的标记,但标签不随着提交而移动。
标签有两种:轻量标签和附注标签。轻量标签就是一个简单的不动指针,它简单的指向某个提交。而附注标签不同,后者是存在于 Git 中的一个数据对象:附注标签中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GPG 签名并验证。
- 创建标签
使用 tag 子命令为一个提交创建轻量标签:
git tag tag-name
git tag tag-name <commit-hash>
第一种写法会为当前提交创建标签,第二种写法可以为指定提交创建标签。
使用 tag 子命令的 -a 选项创建一个附注标签:
git tag -a tag-name
git tag -a tag-neme -m "message" <commit-hash>
创建附注标签必须提供附注信息,如果没有通过 -m 选项直接提供信息,Git 就会启动默认编辑器。
- 删除标签
使用 tag 子命令的 -d 选项删除一个标签:
git tag -d tag-name
- 列出标签
使用 tag 子命令查看仓库所有标签:
git tag
空的 tag 命令会按字母顺序列出所有标签。如果需要按模式列出标签,可以使用 -l 选项:
git tag -l "v1.0*"
- 远程标签
可以使用 push 子命令将本地标签推送到远程仓库:
git push <remote> tag-name
git push <remote> <local-tag>:<remote-tag>
如果要一次性推送所有的本地标签到远程仓库,可以使用 --tags 选项:
git push <remote> --tags
和在远程仓库中删除分支一样,删除远程仓库中标签的语法是:
git push <remote> :tag-name
变基
变基操作是除合并操作外的另一个分枝整合方案,合并操作完成的是一个三向合并,它最大限度的保留了每个分枝的每个提交信息的完整,但是一旦分枝多了起来,合并操作会让整个提交树十分复杂。
变基不同于合并的是:它将当前分枝与变基目标分枝有分歧的提交全部拿出来,然后加在目标分枝的最后。此时目标分枝就成了当前分枝的祖先,目标分枝可以执行快速向前合并,整个提交树就像一条直线一样。
- 直接变基
使用 rebase 子命令,再加上一个目标分枝,可以把当前分枝变基到目标分枝上:
git checkout dev
git rebase master
如果变基成功,那么现在 master 分枝将是 dev 分枝的祖先。
另一种语法是:
git rebase master dev
- 高级用法
rebase 命令的 --onto 选项是变基的一个高级用法,它涉及 3 个分枝:目标分枝,其他分枝和变基分枝。主要目的当然还是将变基分枝变基到目标分枝上。但是它只会挑选变基分枝和其他分枝产生分歧之后的提交,并将这些提交变基到目标分枝上。
如果有 3 个分枝:master, server, client。
执行以下命令的效果是:
git rebase master server client
这么做的目的是:只想将 client 分枝整合到 master 中,但是不想包含 server 分枝的提交内容。
- 常用配置
在协作开发中,从远程拉取分枝再将本地分枝变基到远程主分枝是如此常见。完成这个操作需要先 fetch 再 rebase,但是 pull 命令默认执行 merge 而不是 rebase。
可以执行下列命令改变配置信息,让 pull 拉取分枝后执行 rebase:
git config --global pull.rebase true