Git

Table of Contents

1. Git 简介

Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.

官方网站:http://www.git-scm.com/
Git 源码:https://github.com/git
Git Documentation Reference: http://www.git-scm.com/docs
本文很多内容整理自书籍:Pro Git (online)
Git FAQ: https://git.wiki.kernel.org/index.php/GitFaq

1.1. 查看命令帮助

有三种方法查看 git 某个子命令的帮助:

$ git help <verb>        # 查看git <verb>的帮助,如git help config
$ git <verb> --help      # 同上
$ man git-<verb>         # 同上

1.2. 配置用户信息

个人的用户名称和电子邮件地址可以通过 git config 进行配置。这两个信息很重要,每次 Git 提交时都会引用这两条信息,说明是谁提交了更新,所以会随更新内容一起被永久纳入历史记录:

$ git config --global user.name "John Doe"
$ git config --global user.email "johndoe@example.com"

上面命令执行完成后,会把相应信息更新到配置文件~/.gitconfig 中。

2. Git 基础

2.1. 本地操作(文件的三种状态)

对于任何一个文件,在 Git 内都只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。如表 1 所示。

Table 1: 文件在 Git 内部的三种状态
状态 说明
已提交(committed) 表示该文件已经被安全地保存在本地数据库中了
已修改(modified) 表示修改了某个文件,但还没有提交保存
已暂存(staged) 表示把已修改的文件放在下次提交时要保存的清单中

Git 中文件三个状态的变化如图 1 所示。使用命令 git checkout 可以实现图中的“checkout the project”操作;使用命令 git add (它还有一个别名 git stage )可以实现图中的“stage files”操作;使用命令 git commit 可以实现图中的“commit”操作。这三种操作都只需要访问本地文件和资源,不用连网。

git_local_operations.png

Figure 1: Git 本地操作中,三个状态的变化

在 Git 的术语中,暂存区域(Staging Area)又被称为索引(Index),图 1 常常也表达为图 2

git_local_operations_alt.png

Figure 2: Git 三棵树

2.1.1. add 命令

使用 git add 命令可以把修改文件添加到暂存区域(Staging Area),如表 2 所示。

Table 2: git add 命令
  New Files Modified Files Deleted Files Comments
git add -Agit add --all Y Y Y Stage all (new, modified, deleted) files
git add . Y Y Y Stage all (new, modified, deleted) files
git add --ignore-removal . Y Y N Stage new and modified files only
git add -ugit add --update N Y Y Stage modified and deleted files only

2.1.2. commit 命令

完成修改,并已暂存后,可以使用 git commit 命令提交修改。

2.1.2.1. commit 消息的格式

书写好的“commit 消息”很重要,其帮助文档( man git-commit )中推荐使用下面的格式(非强制要求):

Though not required, it’s a good idea to begin the commit message with a single short (less than 50 character) line summarizing the change, followed by a blank line and then a more thorough description. The text up to the first blank line in a commit message is treated as the commit title, and that title is used throughout Git. For example, Git-format-patch(1) turns a commit into email, and it uses the title on the Subject line and the rest of the commit in the body.

一般地,良好的 commit 消息格式要满足下面要求:
1、第一行,书写摘要信息(标题),不要超过 50 个字符,使用祈使语气(往往以动词开始,如 Fix/Change/etc.),省略行尾的句号(标题不需要标点符号);
2、第二行为空行;
3、从第三行开始,书写详细的修改内容(正文),单行不要超过 72 个字符。

注 1: git log --oneline 命令仅会显示出 commit 消息的标题部分。
注 2:如果需要书写的 commit 消息很短(如“Fix typo in user guide”),则仅书写标题行(忽略空行和正文行)也可以。

参考:How to Write a Git Commit Message

2.2. 远程操作

Git 远程操作(需要和服务器交互的操作)相关命令如表 3 所示。

Table 3: git 远程操作的简短说明
command 说明
git clone 从远程主机克隆版本库,同时增加远程主机并默认命名为 origin
git remote 管理远程主机
git fetch 把远程主机的更新取回本地,默认取回所有分支的更新
git pull 取回远程主机某个分支的更新,并与本地的指定分支合并(相当于同时运行 git fetch 和 git merge)
git push 将本地分支的更新推送到远程主机

2.2.1. pull 命令

git pull 命令的作用是取回远程仓库中某个分支的更新,再与本地的指定分支合并。它的使用格式为:

$ git pull <远程仓库名> <远程分支名:本地分支名>

其中:“远程仓库名”可以是一个 URL,也可以是远程仓库的名字(远程仓库名字往往为 origin,用命令 git remote -v 可以查看远程仓库名字及其对应的 URL)。

pull 命令实例:

$ git pull origin next:master   # 取回远程仓库origin中分支next上的更新,并与本地master分支合并
$ git pull origin next          # 省略了本地分支名,意味着与本地的当前分支合并
                                #(注:如果本地当前分支为master,则作用和前一条命令相同)
2.2.1.1. 常用选项(--rebase 和--autostash)

当你用 git pull 获取服务器上最新代码时,总是推荐你使用 --rebase 选项,这样可以使修改历史的轨迹更加简洁,这是由于:
git pull = git fetch + git merge
git pull --rebase = git fetch + git rebase

关于 git mergegit rebase 的区别可以参考节 3.4.1

当你本地有修改没有放到暂存区时,你可能遇到下面错误:

$ git pull --rebase
Cannot pull with rebase: You have unstaged changes.
Please commit or stash them.

可行的解决思路是先 stash 它们,更新代码后再把修改重新拉回到工作区中,即:

$ git stash
$ git pull --rebase
$ git stash pop

和上面效果相同,可以使用 --autostash 选项,如:

$ git pull --rebase --autostash     # 和前面3条命令的效果一样

参考:https://stackoverflow.com/questions/23517464/error-cannot-pull-with-rebase-you-have-unstaged-changes

2.2.2. push 命令

git push 命令的作用是将本地分支的更新,推送到远程仓库相应分支上去。它的使用格式为:

$ git push <远程仓库名> <本地分支名:远程分支名>

其中:“远程仓库名”可以是一个 URL,也可以是远程仓库的名字(远程仓库名字往往为 origin,用命令 git remote -v 可以查看远程仓库名字及其对应的 URL)。

当省略“远程分支名”时,这时表示将本地分支推送与之存在“追踪关系”的远程分支(通常两者同名),如果远程分支不存在,则会新建它。

$ git push origin master        # 将本地master分支推送到origin主机的master分支。如果origin主机上没有master分支,则会新建它

如果当前本地分支与远程分支存在“追踪关系”,则本地分支名(即当前分支名)和远程分支名都可以省略。如:

$ git push origin       # 将本地当前分支推送到远程仓库origin中与之存在“追踪关系”的远程分支上去

如果当前分支只有一个追踪分支(注:当前分支可以“追踪”不同的远程仓库上的分支,如当前分支可追踪远程 origin1 和 origin2 上的分支),那么远程仓库名也可以省略。如:

$ git push              # 将本地当前分支推送到与之存在“追踪关系”的唯一(或者默认)远程分支上去
2.2.2.1. 查看本地分支和远程分支的“追踪关系”

git branch -vv 可以查看本地分支和远程分支的“追踪关系”。如:

$ git branch -vv
 * master 2398781 [origin/master] local storage sample files

上面输出表明,本地的 master 分支和远程仓库 origin 中的 master 分支存在“追踪关系”。

2.2.2.2. 忽略本地分支意味着“删除远程分支”

提供远程分支名但忽略本地分支名(冒号不能省略)意味着“删除远程分支”。如:

$ git push origin :branch1             # 小心:会删除远程仓库origin中的branch1分支。
$ git push origin --delete branch1     # 作用和上相同。

2.3. 标签

Git 可以给历史中的某一个提交打上标签(tag),以示重要。比较有代表性的是人们会使用这个功能来标记发布结点(如 v1.0 等等)。

2.3.1. 列出标签

输入 git tag 可以按字母顺序列出标签。如:

$ git tag                    # 列出所有标签
v0.1
v1.3

你可以通过 -l 选项指定特定“模式”来查找标签。例如,Git 自身的源代码仓库包含标签的数量超过 500 个。如果只对 v1.8.5 开头的标签感兴趣,可以运行:

$ git tag -l 'v1.8.5*'       # 列出以 v1.8.5 开头的标签
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3
v1.8.5.1
v1.8.5.2
v1.8.5.3
v1.8.5.4
v1.8.5.5

2.3.2. 创建标签

Git 有两种类型的标签:“轻量标签(lightweight)”与“附注标签(annotated)”。

“轻量标签”很像一个不会改变的分支——它只是一个特定 commit 的引用。

“附注标签”是存储在 Git 数据库中的一个完整对象,它会包含打标签者的名字、电子邮件等信息,还包含一个标签信息(通过 -m 选项可以指定标签信息,不提供该选项时会打开编辑器提示你输入),还可以对附注标签进行签名。通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。

2.3.2.1. 创建附注标签(-a)

创建一个附注标签最简单的方式是当你在运行 git tag 命令时指定 -a 选项:

$ git tag -a v1.4 -m 'my version 1.4'   # 创建“附注标签”,标签名为 v1.4
$ git tag v1.4 -m 'my version 1.4'      # 同上,“-m”选项隐含着“-a”

使用 git show your-tag-name 命令可以看到标签信息与对应的提交信息:

$ git show v1.4                         # 查看标签 v1.4 的相关信息
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    change the version number

输出显示了打标签者的信息、打标签的日期时间、附注信息,然后显示具体的 commit 信息。

2.3.2.2. 创建轻量标签

运行 git tag 命令时仅提供标签名字(不使用 -a, -s-m 选项),就可以创建轻量标签。如:

$ git tag v1.4-lw                       # 创建“轻量标签”,标签名为 v1.4-lw

这时,如果在标签上运行 git show ,你不会看到额外的标签信息。命令只会显示出和 tag 关联的 commit 信息:

$ git show v1.4-lw                      # 查看标签 v1.4-lw 的相关信息
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    change the version number
2.3.2.3. 补打标签

你也可以对过去的 commit 打标签。假设 commit 历史是这样的:

$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function
4682c3261057305bdd616e23b64b0857d832627b added a todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a started write support
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme

现在,假设在 v1.2 时你忘记给项目打标签,也就是在“updated rakefile”提交。你可以在之后补上标签。要在那个提交上打标签,你需要在命令的末尾指定提交的校验和(或部分校验和),如:

$ git tag -a v1.2 9fceb02 -m"version 1.2"   # 对 commit 对象 9fceb02 补打附注标签,名为 v1.2

2.3.3. 推送标签到服务器

默认情况下, git push 命令并不会传送标签到远程仓库服务器上。

在创建完标签后你必须“显式地”推送标签到服务器上,相应命令为: git push origin [tagname] ,如:

$ git push origin v1.5                # 推送标签 v1.5 到服务器
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.5 -> v1.5

如果想要一次性推送很多标签,也可以使用带有 --tags 选项的 git push 命令。这将会把所有不在远程仓库服务器上的标签全部传送到那里。

$ git push origin --tags              # 指定 --tags,可推送所有标签到服务器
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.4 -> v1.4
 * [new tag]         v1.4-lw -> v1.4-lw

2.3.4. 基于标签签出代码

在 Git 中你并不能真的签出一个标签,因为它们并不能像分支一样来回移动。如果你想要工作目录与仓库中特定的标签版本完全一样,可以使用 git checkout -b [branchname] tags/[tagname] 在特定的标签上创建一个新分支:

$ git checkout -b version2 tags/v2.0.0  # 基于标签 v2.0.0,创建分支 version2,并切换到改分支
$ git checkout -b version2 v2.0.0       # 同上。不过如果已经存在名为 v2.0.0 的分支,则会失败

当然,如果在这之后又进行了一次提交,version2 分支会因为改动向前移动了,那么 version2 分支就会和 v2.0.0 标签稍微有些不同,这时就应该当心了。

2.3.5. 删除标签

通过 -d 选项可以删除标签,如:

$ git tag -d v1.4                   # 删除名为 v1.4 的标签
Deleted tag 'v1.4' (was ca82a6df)

2.3.6. 删除远程标签

下面命令可以删除远程标签:

$ git push origin :refs/tags/<tag_name>   # 删除远程标签 tag_name
$ git push origin :<tag_name>             # 同上,不过当 tag_name 同时是分支名时会失败

3. Git 分支

Git 的分支是难以置信的轻量级,它的新建操作几乎能在瞬间完成,在不同分支间切换起来也差不多一样快。

3.1. Git 分支和 commit 对象

Git 分支是指向 commit 对象的指针。

在介绍 commit 对象前先简单介绍一下 Git 是如何储存数据的。
为简单起见,假设工作目录中有 3 个文件,准备把它们暂存后提交。

git add README test.rb LICENSE
git commit -m "first commit"

当使用暂存操作时(git add),Git 会对每一个文件计算 SHA-1 哈希,然后把当前版本的快照(Git 用 blob 类型的对象保存快照)保存到 Git 仓库(即.git 目录)中。
当使用提交操作时(git commit),Git 会创建一个 tree 对象,它记录着目录树内容及其中各个文件对应 blob 对象的索引。同时 Git 会创建一个 commit 对象,它记录着 tree 对象的索引和其他元数据。
综上,前面的操作会在 Git 仓库中生成 5 个对象:3个 blob 对象,1个 tree 对象,1个 commit 对象。从概念上讲,看起来像图3所示。

git_single_commit_repository_data.png

Figure 3: 单个提交对象在仓库中的数据结构

作些修改后再次提交,那么这次提交对象会包含一个指向上次提交对象的指针(parent 对象)。两次提交后,Git 仓库看起来如图4所示。

git_object_data_for_multiple_commits.png

Figure 4: 多个提交对象之间的链接关系

Git 中的分支,本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。

git_branch_pointing_into_commit.png

Figure 5: 分支是指向提交对象的指针

3.2. 分支的创建、查看、删除(git branch)

使用 git branch 可创建、查看、删除 git 分支。如:

$ git branch                        # 查看本地分支
$ git branch -r                     # 查看已经下载到本地的远程分支
$ git branch -a                     # 查看本地分支和已经下载到本地的远程分支
$ git branch mybranch1              # 基于当前分支创建名为mybranch1的本地分支
$ git branch -d mybranch1           # 删除名为mybranch1的本地分支

3.2.1. 查看远程分支(git ls-remote,不用 clone 工程)

使用 git branch -r 可以查看已经下载到本地的远程分支。

使用 git ls-remote --heads <remote-name> 可以查看所有远程分支(包含还没下载到本地的远程分支)。如:

$ git ls-remote --heads origin

使用 git ls-remote --heads [url] 可以列出工程的所有远程分支(指定 heads 的作用是仅显示分支名,不显示 tag 名), 它不需要 clone 工程。 如:

$ git ls-remote --heads https://github.com/opencv/opencv.git
43f1b72e92deab7e79a92c5d40ca2c561c50c0a0	refs/heads/2.4
a50a355e637d7a3b1519803cde1fdcfa7b9de3cb	refs/heads/master

上面的输出表明 url 所指定的工程中存在两个远程分支,其名字为“2.4”和“master”。

3.2.2. 创建远程分支

创建远程分支的步骤:

$ git checkout -b <your-new-branch-name>        # 基于当前分支新建本地分支,并切换到该分支
$ git push <远程仓库名> <your-new-branch-name>  # 推送分支到远程仓库,远程无同名分支时会创建它

说明,命令中 git checkout -b <your-new-branch-name> 是下面两个命令的简写:

$ git branch <your-new-branch-name>             # 基于当前分支新建本地分支
$ git checkout <your-new-branch-name>           # 切换到该分支

注:要删除远程分支,可参考节 2.2.2.2

3.2.3. 重命名远程分支

在 Git 中重命名远程分支,其实重命名本地分支、删除旧远程分支,再重新提交一个远程分支。

假设远程分支名为“old-name”,现在想把它改名为“new-name”。其操作步骤为:

$ git branch -m old-name new-name   # 重命名分支
$ git push origin :old-name         # 删除旧的远程分支
$ git push origin -u new-name       # 把新本地分支推送到服务器(创建新的远程分支)

参考:Rename a local and remote branch in git

3.2.4. 更新远程分支列表

使用 git branch -a 可以查看所有分支的列表。如:

$ git branch -a
 * develop
   master
   remotes/origin/HEAD -> origin/master
   remotes/origin/develop
   remotes/origin/master
   remotes/origin/bugfix1

如果其它人删除了某个远程分支(如 bugfix1),执行 git branch -a 命令时上面列表并不会自动更新。使用命令 git remote update origin --prune 可以更新远程分支列表,删除不存在的远程分支。如:

$ git remote update origin --prune
Fetching origin
From https://github.com/xxx/proj1
 - [deleted]         (none)     -> origin/bugfix1
$ git branch -a
 * develop
   master
   remotes/origin/HEAD -> origin/master
   remotes/origin/develop
   remotes/origin/master

参考:
https://stackoverflow.com/questions/36358265/when-does-git-refresh-the-list-of-remote-branches

3.3. 分支的合并(git merge)

使用 git merge 可以进行分支的合并。

参考:Git 分支 - 分支的新建与合并

3.3.1. 分支合并实例一

假设当前代码提交历史为图 6 所示。

git_example1_before_merge.png

Figure 6: 分支合并实例一:当前代码提交历史

通过下面命令,可以把分支 hotfix 合并到分支 master 上:

$ git checkout master                # 第一步,切换到分支master
$ git merge hotfix                   # 第二步,使用git merge合并分支hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

完成合并后,代码提交历史变为如图 7 所示。

git_example1_after_merge.png

Figure 7: 分支合并实例一:把分支 hotfix 合并到分支 master 后

3.3.2. 分支合并实例二

现在考虑一个稍微复杂些的分支合并的例子,假设当前代码提交历史为图 8 所示。

git_example2_before_merge.png

Figure 8: 分支合并实例二:当前代码提交历史

通过下面命令,可以把分支 iss53 合并到分支 master 上:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

这和你之前合并 hotfix 分支的时候看起来有一点不一样。在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。 因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并(three-way merge)。如图 9 所示。

git_example2_merging.png

Figure 9: 分支合并实例二:一次典型合并中所用到的三个快照

和实例一中将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。如图 10 所示。

git_example2_after_merge.png

Figure 10: 分支合并实例二:把分支 iss53 合并到分支 master 后

3.3.3. 合并其它分支的 commit 到当前分支(cherry-pick)

如何 Merge 其它分支的 commit 到当前分支呢?使用 git cherry-pick <commit-id> 命令即可,如:

$ git cherry-pick d42c389f

参考:https://stackoverflow.com/questions/881092/how-to-merge-a-specific-commit-in-git

3.3.4. 合并其它分支的 merge commit 到当前分支(cherry-pick -m)

当 cherry-pick 的 commit 不是普通 commit,而是一个 merge commit 时,你需要指定 -m 选项(或者 --mainline )来避免歧义。

比如你的 commit history 是这样的:

- A - B - E - F -   master
   \     /
    C - D           fix

显然,E 是一个 merge commit。如果你现在要在其他地方 git cherry-pick E ,那么就会有歧义,因为 E 既可以是来自 master 分支 B - E 的改动 (diff),也可以是来自 fix 分支 D - E 的改动 (diff)。

按照这个例子,fix 是被 merge 到 master 的。因此, 命令 git cherry-pick E -m 1 意思就是使用 B - E 的改动,命令 git cherry-pick E -m 2 意思就是使用 D - E 的改动。 记住一点就好,1 是 “主干”,确切点儿说是被 merge 了代码的 branch,2 是 merge 来源。

参考:https://segmentfault.com/q/1010000010185984

3.3.5. 查看两个分支的第一个公共祖先(merge-base)

使用 git merge-base 可以查看两个分支的第一个公共祖先。如:

$ git merge-base branch1 branch2
050dc022f3a65bdc78d97e2b1ac9b595a924c3f2

参考:https://stackoverflow.com/questions/1549146/how-to-find-the-most-recent-common-ancestor-of-two-git-branches

3.3.6. 回滚已 push 到服务器的 commit(merge -s ours)

使用 git reset 可以让当前分支回滚到任何一个历史版本,直接移除那以后的所有提交。但这更改了 Git 的历史,Git 服务通常会禁止这样做。下面介绍一种安全回滚到历史版本的方式。

第一步,通过 git log 找到你想要回到的版本的 commit hash。假设想回到 commit hash 为 4a50c9f 的版本(即放弃最近的两个版本)。

$ git log --graph --oneline
 * d4ccf59 (HEAD -> master) version 4 (harttle screwed it up, again)
 * 5b7d48e version 3 (harttle screwed it up)
 * 4a50c9f version 2                                        <---- 想回到这个版本
 * 491c6e0 version 1

第二步,基于 4a50c9f 创建一个本地分支(假设名为 v2),并切换到 v2。如:

$ git checkout -b v2 4a50c9f

这时,commit log 如下:

$ git log --graph --oneline
 * d4ccf59 (master) version 4 (harttle screwed it up, again)
 * 5b7d48e version 3 (harttle screwed it up)
 * 4a50c9f (HEAD -> v2) version 2
 * 491c6e0 version 1

现在 HEAD 已经指向 v2 分支。

第三步,通过 -s ours 指定 ours 合并策略来合并 master 分支到 v2。如:

$ git merge -s ours master

-s ours master 合并的结果是 产生了一个基于 master 的 commit,但 HEAD 中的代码和 v2 完全相同。

这时,commit log 如下:

$ git log --graph --oneline
 *   94fa8a7 (HEAD -> v2) Merge branch 'master' into v2
 |\
 | * d4ccf59 (master) version 4 (harttle screwed it up, again)
 | * 5b7d48e version 3 (harttle screwed it up)
 |/
 * 4a50c9f version 2
 * 491c6e0 version 1

第四步,push 分支 v2 到远程 master 分支。如:

$ git push origin v2:master

参考:安全地回滚远程分支

3.3.7. merge -s ours VS merge -X ours

merge -s oursmerge -X ours (它是 merge -s recursive -X ours 的简写)是不同的。关于 merge -s ours 的作用可参考上一节内容。

Git 合并策略有很多种(可参考MERGE STRATEGIES),如 recursive/ours/subtree 等等,通过 -s 可以指定合并策略。通过 -X 可以指定合并策略的更具体选项,如合并策略 recursive 支持很多具体选项如 ours/theirs,它们的含义如下:

$ git merge branch1                         # 是 git merge -s recursive branch1 的简写
$ git merge -s recursive branch1            # 使用recursive策略合并branch1到当前分支,有冲突时,需要用户手动resolve
$ git merge -s recursive -X ours branch1    # 使用recursive策略合并branch1到当前分支,遇到冲突时自动采用当前分支的中版本
$ git merge -s recursive -X theirs branch1  # 使用recursive策略合并branch1到当前分支,遇到冲突时自动采用分支branch1中的版本

3.3.8. Fast-Forward merge VS. No Fast-Forward merge

假设你从 master 分支拉了一个新分支,取名为 speedup,你在 speedup 分支上进行开发,且提交了 3 次,如图 11 所示。

git_fast_forward_example.png

Figure 11: 从 master 分支拉了一个新分支 speedup

现在你想把 speedup 分支 merge 回 master。假设从你拉分支 speedup 出来后的这段时间内,master 分支上一直没有新的 commit,则你可以选择 Fast-Forward merge(默认行为)和 No Fast-Forward merge(可通过 -no-ff 来指定)。它们的区别如图 12 所示,采用 No Fast-Forward merge 时多了一个提交记录(多了虚线圆圈所表示的 commit)。

git_fast_forward_merge.png

Figure 12: Fast-forward merge(left), No Fast-Forward merge (right)

说明:如果从你拉分支 speedup 出来后的这段时间内,master 分支上有新的 commit,则把 speedup 分支 merge 回 master 时是无法进行 Fast-Forward merge 的(不过,你也不用特意指定 -no-ff ,git 发现无法进行 Fast-Forward merge 时会自动转为 No Fast-Forward 方式进行 merge)。

参考:https://ariya.io/2013/09/fast-forward-git-merge

3.4. 分支的变基(git rebase)

你可以使用 git rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

假设,当前分支为 topic,提交历史为:

                     A---B---C topic
                    /
               D---E---F---G master

现在,执行下面两条命令之一:

$ git rebase master topic  # 它相当于两条命令 git checkout topic; git rebase master。由于当前分支为topic,所以checkout可省略
$ git rebase master

其结果是把 master 分支上的所有修改都移到了当前分支 topic 上,执行完上面命令后提交历史变为了:

                             A'--B'--C' topic
                            /
               D---E---F---G master

它的原理是 首先找到两个分支(即当前分支 topic、变基操作的目标基底分支 master)的最近共同祖先 E,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 G, 最后以此将之前另存为临时文件的修改依序应用过来。

3.4.1. git merge VS git rebase


git merge 和 git rebase 从最终效果来看没有任何区别,都是将不同分支的代码融合在一起,但是生成的代码树就稍微有些不同,如图13所示, 使用 rebase 后提交历史是一条直线没有分叉。

git_merge_vs_rebase.png

Figure 13: git merge VS git rebase

参考:http://www.cnblogs.com/iammatthew/archive/2011/12/06/2277383.html

可以把衍合当成一种在推送之前清理提交历史的手段,仅仅衍合那些永远不会公开的 commit。换句话说,当本地有 commit,而需要和别人的 commit 合并时,建议使用 rebase 来保证修改历史的轨迹的简洁。

一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。
如果衍合那些已经公开的 commit,而与此同时其他人已经用这些 commit 进行了后续的开发工作,那你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。

3.5. 分支命令总结

Git 分支相关命令如表 4 所示。

Table 4: Git 分支相关命令
command 说明
git branch 创建,查看,删除分支(-d)
git checkout 切换分支(即:移动 HEAD 指针到指定分支;把工作目录中文件替换成指定分支的快照内容)
git merge 合并分支
git rebase 衍合分支

4. Git 工具

4.1. 储藏(stash)

Git 维护了一个内部栈,可以保存你本地对代码的修改。

Table 5: git stash 相关命令
命令 说明
git stash 备份工作区中所有修改到栈上,这样你的工作区是干净的了。
git stash pop 从栈上把修改重新拉回到工作区中
git stash list 查看栈上保存的记录

4.1.1. stash 应用实例

在使用 git pull 更新代码时,可能会碰到冲突的情况,提示信息:

$ git pull
error: Your local changes to 'file1' would be overwritten by merge.  Aborting.
Please, commit your changes or stash them before you can merge.

解决办法:
第一步,先把你本地的修改放入到内部栈中,即:

$ git stash

第二步,运行 git pull ,不再会有冲突的提示了。

第三步,恢复本地的修改到工作区中(会提示冲突),即:

$ git stash pop
Auto-merging file1
CONFLICT (content): Merge conflict in file1

第四步,打开冲突文件,手动解决冲突。如:

<<<<<<< Updated upstream
  This foo
=======
  This bar
>>>>>>> Stashed changes

其中,“<<<<<<< Updated upstream”和“=======”之间的内容是 pull 下来的内容,而“=======”和“>>>>>>> Stashed changes”之间的内容是本地修改的内容。

4.2. 清理(clean)

git clean 命令用来删除没有被 tracked 过的文件(如编译生成的.o 等文件)。

Table 6: git clean 相关命令
命令 说明
git clean 删除没有被 tracked 过的文件。
git clean -d 删除没有被 tracked 过的文件和目录。
git clean -d -n 显示可能被 clean 删除的文件和目录,而不会真正删除它们。“-n”是“--dry-run”的缩写。

4.3. 日志搜索(log)

4.3.1. 显示 commit tree(--graph)

使用 git log --graph 可以显示 commit tree。如:

$ git log --graph --oneline
 * 7add224 change2
 *   fd44ce9 Merge branch 'master' into rollback
 |\
 | * 21162bc update1
 |/
 * 1733aa6 first commit

4.3.2. 显示某个用户的 commit

只显示某个用户的提交日志:

$ git log --author=<name or email>

4.3.3. 显示未 push 到远程服务器的 commit

有时使用 git status 命令并不会显示还未 push 到远程服务器的 commit。

下面这些命令都可以显示还未 push 到远程服务器的 commit:

$ git log origin/master..HEAD           # 查看分支origin/master上还未push到远程服务器的commit
$ git cherry -v origin/master           # 查看分支origin/master上还未push到远程服务器的commit
$ git log --branches --not --remotes    # 查看所有分支上还未push到远程服务器的commit

参考:https://stackoverflow.com/questions/2016901/viewing-unpushed-git-commits

4.3.4. 显示新增和删除某个字符串的 commit(-S/-G)

例如,如果我们想找到“ZLIB_BUF_MAX”常量是什么时候引入的,我们可以使用 -S<string> 选项来显示新增和删除该字符串的提交。

$ git log -SZLIB_BUF_MAX --oneline
e01503b zlib: allow feeding more than 4GB in one go
ef49a7a zlib: zlib can only process 4GB at a time

如果我们查看这些提交的详细信息,我们可以看到在 ef49a7a 这个提交引入了常量,并且在 e01503b 这个提交中被修改了。

如果你希望得到更精确的结果,你可以使用 -G<regex> 选项来使用正则表达式搜索。

4.3.5. 显示文件中某些行的所有修改历史(-L 10,15:file.txt)

要查看文件“file.txt”中第 10 行到 15 行的所有修改历史,可以执行下面命令:

$ git log -L 10,15:file.txt

参考:https://stackoverflow.com/questions/8435343/retrieve-the-commit-log-for-a-specific-line-in-a-file

4.3.6. 显示所有提交者及其 email 地址

显示所有提交者及其 email 地址:

$ git log --all --format='%aN <%cE>' | sort -u

4.3.7. 统计提交者的 commit 数量(shortlog)

使用 git shortlog -s 可以显示每个提交者的 commit 数量,再加上选项 -n 可以按 commit 数量进行排序。如在 redis 源码根目录执行 git shortlog -sn 可得到下面输出:

$ git shortlog -sn
  5339  antirez
   512  Salvatore Sanfilippo
   510  Pieter Noordhuis
   151  Matt Stancliff
   116  zhaozhao.zz
    87  artix
    54  Oran Agra
    48  Itamar Haber
    35  Yossi Gottlieb
    34  Dvir Volk
......

4.4. 查看 commit 修改内容(git show)

使用 git show 可以查看某个 commit 的修改内容。

默认地,使用 git show <commit-hashid> 会显示某个 commit 中所有的修改内容,如增加哪些行,删除哪些行等待,它的输出一般比较多。

使用 git show --stat <commit-hashid> 可以显示某个 commit 的摘要信息(比如所修改文件的列表等),如:

$ git show --stat f1ecd6bf64e87b3b82e1b515762d079148a91f44
commit f1ecd6bf64e87b3b82e1b515762d079148a91f44
Author: user1 <xxx@xxx>
Date:   Mon Nov 27 10:05:41 2017 +0800

    fix some issues reported by shellcheck

 bin/find_pdfs_by_keyword_meta |  4 ++--
 bin/kill_ipcs                 |  2 +-
 bin/org2pdf                   | 35 +++++++++++++++++------------------
 bin/perl_ftrace               |  2 +-
 bin/top_java_thread           | 14 +++++++-------
 install.sh                    |  6 +++---
 6 files changed, 31 insertions(+), 32 deletions(-)

使用 git show <commit-hashid> filename 可以显示某个 commit 中对文件 filename 的修改。如:

$ git show f1ecd6bf64e87b3b82e1b515762d079148a91f44 bin/kill_ipcs
commit f1ecd6bf64e87b3b82e1b515762d079148a91f44
Author: user1 <xxx@xxx>
Date:   Mon Nov 27 10:05:41 2017 +0800

    fix some issues reported by shellcheck

diff --git a/bin/kill_ipcs b/bin/kill_ipcs
index d6f7908..7c1ec3a 100755
--- a/bin/kill_ipcs
+++ b/bin/kill_ipcs
@@ -10,7 +10,7 @@ fi
 user="$1"
 if [ ! "$1" ]; then
     # Default, whoami and "id -un" is not available in Solaris.
-    user=`id | sed s"/) .*//" | sed "s/.*(//"`  # current user.
+    user=$(id | sed s"/) .*//" | sed "s/.*(//")  # current user.
 fi

 read -r -p "Are you sure to kill all System V IPC resources belong to ${user}? [y/N] " response

4.4.1. 查看通过 merge 创建的 commit 的修改内容(-m)

直接使用 git show 无法查看通过 merge 创建的 commit 的具体修改内容。如:

$ git show c05f017         # 我们期望显示commit的具体修改内容,但并没有显示
commit c0f50178901e09a1237f7b9d9173ec5d1c4936c
Merge: ed234b ded051
Author: abc
Date:   Mon Nov 21 15:56:33 2016 -0800

    Merge branch 'abc'

有两种方法可以查看通过 merge 创建的 commit 的修改内容:

$ git show -m c05f017    # 方法一,使用 -m 选项
$ git show ded051        # 方法二,直接查看merge commit id(ded051来自于Merge: ed234b ded051)

参考:https://stackoverflow.com/questions/40986518/git-show-of-a-merge-commit

4.5. 文件标注(blame)

使用 git blame file1 可查看文件 file1 每一行的最后修改时间以及是被谁修改的。

下面例子使用 -L 选项来限制输出范围在第 12 至 22 行:

$ git blame -L 12,22 simplegit.rb
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 12)  def show(tree = 'master')
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 13)   command("git show #{tree}")
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 14)  end
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 15)
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 16)  def log(tree = 'master')
79eaf55d (Scott Chacon  2008-04-06 10:15:08 -0700 17)   command("git log #{tree}")
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 18)  end
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 19)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20)  def blame(path)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21)   command("git blame #{path}")
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22)  end

说明:前面 4 行第一个字段为 ^4832fe2 ,其中第一个字母为 ^ 表示这些行加入到项目后从未被修改过。

参考:Git 工具 - 使用 Git 调试

4.5.1. 显示某行代码从哪里复制而来(-C)

如果你在 git blame 后面加上一个 -C ,Git 会分析你正在标注的文件,并且尝试找出文件中从别的地方复制过来的代码片段的原始出处。

比如,你将 GITServerHandler.m 这个文件拆分为数个文件,其中一个文件是 GITPackUpload.m。 对 GITPackUpload.m 执行带 -C 参数的 blame 命令,你就可以看到代码块的原始出处:

$ git blame -C -L 141,153 GITPackUpload.m
f344f58d GITServerHandler.m (Scott 2009-01-04 141)
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 148)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 149)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 150)
56ef2caf GITServerHandler.m (Scott 2009-01-05 151)         if(commit) {
56ef2caf GITServerHandler.m (Scott 2009-01-05 152)                 [refDict setOb
56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

4.6. 撤销操作(reset)

4.6.1. 放弃所有的本地 commit

下面命令可以放弃所有的本地 commit(没有 push 到远程仓库的 commit),让本地代码和远程的某个分支代码一样:

$ git reset --hard origin/<branch_name>  # 替换<branch_name>为远程分支名
$ git reset --hard origin/master         # 让本地代码和origin/master代码一样,丢掉所有本地修改

参考:https://stackoverflow.com/questions/5097456/throw-away-local-commits-in-git

4.6.2. 放弃最近一次(或多次)本地 commit

删除最近的一次 commit,但保留你的修改:

$ git reset --soft HEAD~1

删除最近的一次 commit,而且删除你的修改:

$ git reset --hard HEAD~1

如果你需要放弃多个本地的 commit,连续运行多次即可。

参考:https://stackoverflow.com/questions/3197413/how-do-i-delete-unpushed-git-commits

4.6.2.1. Undo git reset --hard

如果不小心执行了 git reset --hard HEAD~1 ,把本地的 commit 弄丢了,如何恢复呢?执行下面命令即可:

$ git reset --hard HEAD@{1}

参考:https://stackoverflow.com/questions/5473/how-can-i-undo-git-reset-hard-head1

4.6.3. 放弃某个文件的本地修改

使用 checkout 可放弃某个文件的本地修改。如:

$ git checkout path/to/file     # 放弃文件path/to/file的修改
$ git checkout .                # 放弃当前目录的修改

不过,如果本地文件有一些 merger 冲突未解决,上面命令可以提示错误“error: path 'path/to/file' is unmerged”。这时,可以用下面命令来放弃文件的本地修改:

#### 放弃文件 path/to/file 的本地修改
$ git reset path/to/file
$ git checkout path/to/file

参考:http://stackoverflow.com/questions/1146973/how-do-i-revert-all-local-changes-in-git-managed-project-to-previous-state

4.6.4. 撤销最近一次(或多次)提交到服务器的 commit

下面步骤可撤销最近一次(或多次)提交到服务器的 commit:

$ git pull                   # 确保你的代码和服务器一致
$ git reset --hard HEAD~     # 在本地删除最近的一次 commit。要撤销多个 commit,请运行多次。使用 --soft 可保留修改
$ git push origin +master    # 推送到服务器,其中 + 号表示强行推送

参考:https://ncona.com/2011/07/how-to-delete-a-commit-in-git-local-and-remote/

4.7. 更安全的撤销操作(revert)

使用 git revert 也可以撤销一个 commit。它和 git reset 的区别在于: git revert 是用一个新的 commit 来抵消之前的 commit(之前的 commit 没有被删除,还在历史记录中),而 git reset 是直接删除指定的 commit。

4.8. 连接多个工作目录(worktree)

有时,我们可以需要经常切换分支。比如,我们已经开发完 feature-1 的功能,正在开发 feature-2 的功能。现在突然 feature-1 上报了个紧急 bug,这时我们需要 checkout 到 feature-1 分支解决 bug,修复完成后再 checkout 到 feature-2 中继续开发。在切换分支时,如果不同分支有不同的依赖,可能你还需要更新一下依赖,如果这两个分支的依赖是冲突的,则更加麻烦了。当然,我们克隆仓库到两个不同的目录可以解决这个问题。不过,这里将介绍一个更好的办法:git worktree,有了它,你再也不用克隆多份仓库了。

使用 git worktree 可以把一个 git 仓库连接到多个工作目录中。它的基本用法为:

$ git worktree add <path> <commit-ish>      # commit-ish 可以是分支名、tag名、commit hash

上面代码会创建 path,并把 commit-ish 对应的代码签出到前面创建的 path 中。比如:

$ git worktree add ../proj-feature-1 feature-1

会在父级目录中创建目录 proj-feature-1 ,并签出分支 feature-1 的代码。

要删除其中一个工作目录,直接删除文件夹即可。随后使用 git worktree prune 清除多余的已经被删的工作目录。

4.9. 保存 git 登录信息(Credential Storage)

git 可以使用下面几种方式来保存登录信息:

$ git config --global credential.helper cache         # 登录信息缓存在内存中,默认 15 分钟失效
$ git config --global credential.helper store         # 登录信息明文保存在 ~/.git-credentials
$ git config --global credential.helper osxkeychain   # 仅 Mac 中可用
$ git config --global credential.helper wincred       # 仅 Windows 中可用

参考:https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage

5. Git 内部原理

从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户接口。

当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的对象。如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。该目录的结构及其说明如下:

$ ls -F1
HEAD                   # 指示目前被 checkout 的分支
config*                # 包含项目特有的配置选项
description            # 仅供 GitWeb 程序使用,无需关心
hooks/                 # hooks 目录包含客户端或服务端的钩子脚本(hook scripts)
info/                  # info 包含一个全局性排除(global exclude)文件(类似于 .gitignore)
objects/               # objects 目录存储所有数据内容
refs/                  # refs 目录存储指向数据(分支)的提交对象的指针

参考:Git 内部原理 - 底层命令和高层命令

5.1. 实现 key-value 数据库(hash-object/cat-file)

Git 是一个内容寻址文件系统。这是什么意思呢? 这意味着, Git 的核心部分是一个简单的键值对(key-value)数据库。你可以向该数据库插入任意类型的内容,它会返回一个键值,通过该键值可以在任意时刻再次检索(retrieve)该内容。

可以通过底层命令 hash-object (存数据到 .git 目录中)和 cat-file (从 .git 目录中取数据)来演示上述效果。

首先,我们需要初始化一个新的 Git 版本库,并确认 objects 目录为空:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

可以看到 Git 对 objects 目录进行了初始化,并创建了 pack 和 info 子目录,但均为空。

接着,使用 hash-object -w 往 Git 数据库存入一些文本:

$ echo 'test content' > file1
$ git hash-object -w file1
d670460b4b4aece5915caf5c68d12f560a9fe3e4

-w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。

现在我们可以查看 Git 是如何存储数据的:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

从上面命令可发现,校验和的“前 2 个字符”用于命名子目录,“余下的 38 个字符”则用作文件名。

可以通过 cat-file 命令从 Git 那里取回数据。这个命令简直就是一把剖析 Git 对象的瑞士军刀。为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

至此,你已经掌握了如何向 Git 中存入内容,以及如何将它们取出。

6. Git 技巧

6.1. bash 中自动补全命令

首先,下载 git-completion.bash 文件(https://github.com/git/git/blob/master/contrib/completion/git-completion.bash
其次,把下面一行增加到~/.bashrc 中

source /path/to/git-completion.bash

重启 bash,输入 git 的部分命令后,按两次 tab 键就能看到所有匹配的命令。

6.2. hooks

git 有两种形式的 hooks:服务端 hook 和客户端 hook。本文仅介绍客户端 hook。

项目初始化时,会在目录.git/hooks 中生成一些 hook 样例,如:

$ ls .git/hooks/
applypatch-msg.sample*     pre-applypatch.sample*     pre-receive.sample*
commit-msg.sample*         pre-commit.sample*         prepare-commit-msg.sample*
fsmonitor-watchman.sample* pre-push.sample*           update.sample*
post-update.sample*        pre-rebase.sample*

默认这些 hook 样例不会被应用,如果你想启用它们,删除它们的.sample 后缀即可。

6.2.1. 实例:commit 前自动格式化代码

下面是在 commit 前自动格式化 golang 源代码的 hook 实例(下面脚本中, git diff --cached --name-only 的作用是寻找本次 commit 所修改的文件):

#!/bin/sh
# To use, store as .git/hooks/pre-commit inside your repository and make sure
# it has execute permissions.
#
# This script does not handle file names that contain spaces.

gofiles=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$')
[ -z "$gofiles" ] && exit 0

unformatted=$(gofmt -l $gofiles)
[ -z "$unformatted" ] && exit 0

# Some files are not gofmt'd. Print message and fail.

echo >&2 "Go files must be formatted with gofmt before commit. Please run:"
for fn in $unformatted; do
    echo >&2 "  gofmt -w $PWD/$fn"
done

exit 1

# copy from https://golang.org/misc/git/pre-commit

把上面脚本命名为 pre-commit 后,复制到.git/hooks 目录中,并设置可执行权限即可生效。

不过,由于.git 目录中的文件不能 push 到服务器,我们一般把它放到项目根目录的其它目录中(比如 githooks),再把 githooks 中的文件软链接到.git/hooks 中:

$ find githooks -type f -exec ln -sf ../../{} .git/hooks/ \;

6.3. 配置图形工具(SourceGear DiffMerge)

SourceGear DiffMerge 是免费好用的“diff 查看”工具和“merge 冲突解决”工具,它支持 Mac,Windows,Linux。

说明:对于 Mac 版本的 SourceGear DiffMerge,有两种形式的安装包:Installer 和 DMG。如果下载 DMG 形式的安装包,则安装后你需要进行下面的配置(不配置的话在 shell 中会找不到 diffmerge):

$ ln -s /Applications/DiffMerge.app/Contents/Resources/diffmerge.sh /usr/local/bin/diffmerge

参考:http://twobitlabs.com/2011/08/install-diffmerge-git-mac-os-x/

6.3.1. 解决 merge 冲突(git mergetool)

设置 SourceGear DiffMerge 作为解决 merge 冲突的图形工具:

$ git config --global merge.tool diffmerge
$ git config --global mergetool.diffmerge.cmd 'diffmerge --merge --result="$MERGED" "$LOCAL" "$(if test -f "$BASE"; then echo "$BASE"; else echo "$LOCAL"; fi)" "$REMOTE"'
$ git config --global mergetool.diffmerge.trustExitCode true
$ git config --global mergetool.keepBackup false

执行完 merge 命令,如果提示有冲突,则可以使用下面命令启动图形工具 SourceGear DiffMerge 解决冲突:

$ git mergetool             # 启动图形工具diffmerge解决冲突

6.3.2. 查看不同(git difftool)

设置 SourceGear DiffMerge 作为查看 diff 的图形工具:

$ git config --global diff.tool diffmerge
$ git config --global difftool.diffmerge.cmd 'diffmerge "$LOCAL" "$REMOTE"'

下面是使用 git difftool 的一些例子:

#### diff the local file.m against the checked-in version
$ git difftool file.m

#### diff the local file.m against the version in some-feature-branch
$ git difftool some-feature-branch file.m

#### diff the file.m from the Build-54 tag to the Build-55 tag
$ git difftool Build-54..Build-55 file.m

#### diff all files from the Build-54 tag to the Build-55 tag
$ git difftool Build-54..Build-55

6.4. 加快克隆

6.4.1. 仅克隆某个分支(--single-branch)

使用 --single-branch -b <branch_name> 可以只克隆指定的某个分支。如:

$ git clone --single-branch -b <branch_name> <remote_repo_url>

6.4.2. 仅克隆最新的版本,加快速度(--depth=1)

如果一个项目非常大,直接克隆会下载所在的历史版本记录,导致克隆时间比较长。若仅需要最新的版本,可以使用 git clone --depth=1 <remote_repo_url> ,可加快克隆速度,如:

$ git clone --depth=1 https://github.com/rust-lang/rust.git
Cloning into 'rust'...
remote: Counting objects: 7247, done.
remote: Compressing objects: 100% (6469/6469), done.
remote: Total 7247 (delta 3958), reused 1643 (delta 736), pack-reused 0
Receiving objects: 100% (7247/7247), 8.29 MiB | 100.00 KiB/s, done.
Resolving deltas: 100% (3958/3958), done.
Checking connectivity... done.

参考:
git help clone
https://git.wiki.kernel.org/index.php/GitFaq#How_do_I_do_a_quick_clone_without_history_revisions.3F

6.4.3. 仅克隆某个 tag 的代码

如果仅想克隆某个 tag 下的代码,在执行 clone 命令时指定 --branch=<tag_name> 即可,当然同时指定 --depth=1 可以进一步加快克隆速度,如:

$ git clone https://github.com/torvalds/linux.git --depth=1 --branch=v5.0

6.5. 修改 commit 信息

6.5.1. push 前修改 commit message

commit 后(还未 push),发现填写的 message 不正确,如何修改呢?可用下面方法:

$ git commit --amend

参考:http://stackoverflow.com/questions/179123/edit-an-incorrect-commit-message-in-git

6.5.2. 修改历史 commit 中的 author 信息

如何修改历史 commit 中的 author 信息,比如 author 的名称或者其邮件地址?答案是使用 git filter-branch 命令。

下面实例将修改历史 commit 的 author 信息:把 commit 中所有的“your-old-email@example.com”邮件地址修改为“your-correct-email@example.com”,同时名称修改为“Your Correct Name”。

第一步,以“裸版本库”的方式克隆工程:

$ git clone --bare https://github.com/user/repo.git
$ cd repo.git

第二步:执行下面 git filter-branch 命令把旧邮件地址修改为新邮件地址:

$ git filter-branch --env-filter '

OLD_EMAIL="your-old-email@example.com"          # 请修改它
CORRECT_NAME="Your Correct Name"                # 请修改它
CORRECT_EMAIL="your-correct-email@example.com"  # 请修改它

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

第三步:执行下面命令,以 push 到服务器:

$ git push --force --tags origin 'refs/heads/*'

第四步:删除之前克隆的工程。

$ cd ..
$ rm -rf repo.git

注:这样的修改不会改变 commit 的提交时间。

参考:https://help.github.com/en/articles/changing-author-info

6.5.3. 从历史记录中彻底删除二进制文件

不小心提交了二进制文件,简单地删除,其实它还是位于历史记录中。下面介绍如何从历史记录中彻底删除二进制文件。

第一步,使用 --bare 选项克隆仓库:

$ git clone --bare  https://github.com/your_name/your_project.git

第二步,找到所有二进制文件:

$ cd your_project.git
$ git log --all --numstat \
    | grep '^-' \
    | cut -f3 \
    | sed -r 's|(.*)\{(.*) => (.*)\}(.*)|\1\2\4\n\1\3\4|g' \
    | sort -u

第三步,删除它:

$ git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch path/to/your/binary/file" \
  --prune-empty --tag-name-filter cat -- --all

修改上面命令中的“path/to/your/binary/file”为第二步中找到的二进制文件的完整路径,如果有多个,请替换后执行多次。

最后一步,提交修改:

$ git push --force --tags origin 'refs/heads/*'

6.5.4. 清除所有的 commit 信息

下面步骤将清除 master 分支所有的 commit 信息,并把当前代码重新 push 到 master 分支:

git checkout --orphan latest_branch   # Checkout to branch latest_branch
git add -A                            # Add all the files
git commit -am "commit message"       # Commit the changes
git branch -D master                  # Delete the branch master
git branch -m master                  # Rename the current branch to master
git push -f origin master             # Finally, force update your repository

参考:https://stackoverflow.com/questions/13716658/how-to-delete-all-commit-history-in-github

6.6. 检测本地的代码是不是最新的

用下面命令可检测本地的代码是不是最新的。

$ git fetch -v --dry-run

参考:http://stackoverflow.com/questions/3258243/git-check-if-pull-needed

6.7. 得到本地修改文件的列表(git diff --name-only)

要得到本地修改文件的列表,可以执行:

$ git diff --name-only

6.8. 忽略文件或目录(.gitignore)

把文件或目录加入到项目根目录中文件 .gitignore 中,可以使 git 忽略它(不对其进行版本管理)。

如下面的例子会忽略文件 file1 和目录 dir1,需要注意的是这个配置还会忽略其所有子目录的文件 file1 和目录 dir1。

$ cat .gitignore
file1
dir1/

如果只想忽略同.gitignore 文在同一个目录中的文件 file1 和目录 dir1,则可以在配置项前加一个 / ,如:

$ cat .gitignore
/file1
/dir1/

参考:https://git-scm.com/docs/gitignore

6.8.1. 全局地忽略文件或目录(~/.gitignore_global)

如果你想要所有 git 仓库都忽略某些文件或目录,你可以把它们写到文件“~/.gitignore_global”中,然后执行下面命令:

$ git config --global core.excludesfile ~/.gitignore_global

参考:https://help.github.com/en/github/using-git/ignoring-files#create-a-global-gitignore

6.9. 把公开仓库克隆为私有仓库(git push --mirror)

要把公开仓库克隆为私有仓库,执行下面操作即可:

$ git clone --bare https://github.com/exampleuser/public-repo.git
$ cd public-repo.git
$ git push --mirror https://github.com/yourname/private-repo.git
$ cd ..
$ rm -rf public-repo.git

参考:https://medium.com/@bilalbayasut/github-how-to-make-a-fork-of-public-repository-private-6ee8cacaf9d3

Author: cig01

Created: <2013-11-09 Sat>

Last updated: <2021-01-18 Mon>

Creator: Emacs 27.1 (Org mode 9.4)