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 commit命令

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

2.1.1.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远程操作(需要和服务器交互的操作)相关命令如表 2 所示。

Table 2: 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.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] [tagname] 在特定的标签上创建一个新分支:

$ git checkout -b version2 v2.0.0     # 基于标签v2.0.0,创建分支version2,并切换到改分支
Switched to a new branch 'version2'

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

2.3.5 删除标签

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

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

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分支相关命令

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

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

3.2.1 创建远程分支

创建远程分支的步骤:

$ 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.2 分支的合并(git merge)

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

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

3.2.2.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.2.2.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.2.2.3 回滚已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.2.2.4 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.2.3 分支的变基(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.2.4 git merge VS git rebase

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

git_merge_vs_rebase.png

Figure 11: git merge VS git rebase

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

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

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

3.3 Tips

3.3.1 Merge其它分支的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.2 不clone工程,列出所有远程分支名(ls-remote)

使用 git branch -a 可以列出所有本地分支和远程分支,但它要求你本地已经clone过工程。

使用 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.3.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

4 Git工具

4.1 储藏(stash)

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

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

4.3 日志搜索(log)

git log 可以查看git中的提交日志。

参考:https://git-scm.com/book/zh/v2/Git-工具-搜索

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 显示某个用户的提交

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

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

4.3.3 显示新增和删除某个字符串的提交(-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.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: 10gic <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: 10gic <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.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 放弃所有的本地修改

下面命令可以放弃所有的本地修改,让本地代码和远程的某个分支代码一样:

$ 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 放弃某个文件的本地修改

使用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.3 放弃本地的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.4 撤销最近一次提交到服务器的commit

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

$ git pull                       # 确保你的代码和服务器一致
$ git reset --hard HEAD~         # 在本地删除最近的一次commit
$ git push origin +master        # 推送到服务器,其中+号表示强行推送

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

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 检测本地的代码是不是最新的

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

$ git fetch -v --dry-run

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

6.3 push前修改commit message

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

$ git commit --amend

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

6.4 忽略文件或目录(.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.5 查看本地Repository是从哪里克隆的

忘记了本地Repository最初从哪里克隆,怎么办?
可先进入到本地Repository所在目录,再运行命令 git remote -v ,如:

$ git remote -v
origin	https://github.com/rust-lang/rust.git (fetch)
origin	https://github.com/rust-lang/rust.git (push)

参考:http://stackoverflow.com/questions/4089430/how-can-i-determine-the-url-that-a-local-git-repository-was-originally-cloned-fr

6.6 仅克隆某个分支(–single-branch)

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

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

6.7 仅克隆最新的版本,加快速度(–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


Author: cig01

Created: <2013-11-09 Sat 00:00>

Last updated: <2018-05-03 Thu 14:11>

Creator: Emacs 25.3.1 (Org mode 9.1.4)