git常用命令总结

来源:互联网 发布:linux下c语言删除文件 编辑:程序博客网 时间:2024/06/05 15:28

       Git常用命令总

Git

总结Git常用的命令与概念, 包含分支, 子模块等。

Git简介

Git诞生

Git的诞生与Linux密切相关.

Linus在1991年创建了开源的Linux, 并且有着为数众多的参与者. 在1991-2002年间, 所有的源代码都由Linus手工合并,因为Linus坚定的反对CVS和SVN, 这些集中式的版本控制系统不但速度慢, 而且必须联网才能使用.

到了2002年, 因为Linux的代码库已经庞大到难以继续通过手工方式管理, 于是整个项目组启用了一个商业版本的分布式版本控制系统BitKeeper来管理和维护代码.开发BitKeeper的 BitMover公司授权Linux社区免费使用. 但在2005年, 由于开发 Samba的Andrew试图破解BitKeeper的协议而被发现, 于是BitMover公司收回了免费使用权.这迫使Linux开源社区不得不吸取教训, 只有开发一套属于自己的版本控制系统才能避免重蹈覆辙.

他们对新的版本控制系统制订了若干目标: 速度, 简单的设计, 对非线性开发模式的强力支持(允许上千个并行开发的分支), 完全分布式, 有能力高效管理类似Linux内核一样的超大规模项目.自2005年诞生以来, Git日臻成熟完善, 2008年使用Git存储的Github网站上线, 无数开源项目迁移至Github, 包括jQeury, PHP, Ruby等.

版本控制系统

版本控制系统可以分为集中式版本控制系统(CVS及SVN), 分布式版本控制系统(Git).

集中式版本控制系统

在集中式的版本控制系统中, 版本库是集中存放在中央服务器的. 工作时先从中央服务器获取最新的版本, 然后开始工作, 工作完成后再将修订推送给中央服务器.在这类系统中, 都有一个单一的集中管理的服务器, 保存所有文件的修订版本, 而协同工作的人员都通过客户端连接到这台服务器, 获取最新的文件或者提交更新.

分布式版本控制系统

与集中式版本控制系统相比, 分布式版本控制系统没有中央服务器, 每个人的电脑上都是一个完整的版本库. 这样不需联网就能工作, 而如果需要多人协作,只需把各自的修改推送给对方即可.

分布式版本控制系统的安全性更高, 因为完整的版本库不只一份, 不用担心版本库丢失.

在实际使用分布式版本控制系统时, 通常也有一台类似与中央服务器的电脑, 但它的作用仅仅是用来方便交换大家的修改.

基本用法

Git初始化

在使用 git 管理代码之前, 首先需要对Git进行初始化.

Git配置

使用Git之前需要设置你的名字和email, 这些就是你提交commit时的签名, 每次提交记录都会包含这些信息. 可以使用 git config 命令进行配置.

1
2
git config --global user.name "xxx"
git config --global user.email "xxx"

执行上述命令之后, 会在用户家目录创建一个 .gitconfig 文件,该文件既是Git的全局配置.

如果你想在某个项目中使用其他的用户配置, 可在使用 git config 命令时不带 --global 选项, 这会在当前项目下创建 .git/config, 从而使用针对当前项目的配置.

获得一个Git仓库

有两种方式可以初始化一个Git仓库, 一种是从已有的Git仓库中clone, 另一种是新建一个仓库, 然后进行版本控制.

Clone一个仓库

为获得一个项目的copy, 我们需要知道这个项目仓库的地址(Git URL). Git可以在许多协议下使用, 所以URL可以是ssh, http(s), git等.

可以使用以下代码进行clone处理:

1
git clone http://xxx/projectname

clone操作完成后, 会发现当前目录下多了一个 projectname 文件夹, 这个文件夹中的内容就是clone下来的代码.

初始化一个新的仓库

对于一个已经存在的目录可以使用初始化的方式使它被Git管理:

1
2
cd projectname
git init

使用以上命令, Git会输出:

1
Initialized empty Git repository in xxx

Git会在该目录下创建一个名叫 .git 的目录, 这意味着一个Git仓库被初始化成功.

一般工作流程

使用Git进行版本管理的一般流程如下:

  1. 创建或修改文件
  2. 使用 git add 命令添加新创建或修改的文件到本地缓存区(Index)
  3. 使用 git commit 命令提交到本地代码库
  4. 使用 git push 命令将本地代码库同步到远端代码库(可选)

下面对每个步骤进行讲解:

首先进行工程目录, 创建新文件:

1
2
cd projectname
touch file1 file2 file3

修改文件内容:

1
2
3
echo "testcontent1" >> file1
echo "testcontent2" >> file2
echo "testcontent3" >> file3

使用 git status 查看当前Git仓库状态:

1
git status

可以发现, 新增的3个文件处于 untracked 状态, 将文件加入到缓存区:

1
git add file1 file2 file3

再次执行 git status 命令, 会发现新增文件已经能够commit.

可以使用 git diff 命令查看缓存区中哪些文件被修改:

1
git diff --cached

如果没有 --cached 参数, 则会显示当前没有加入到缓存区中的修改.

当所有的修改到加入到缓存区之后, 进行提交:

1
git commit -m "add 3 files"

该命令需要使用 -m 参数添加本次修改的注释, 完成后会记录一个新的项目版本. 也可以使用 -a 参数将所有没有加入到缓存区的修改一起提交, 但该参数不会添加新创建的文件.

需要注意, 如果是修改/添加文件, 要使用 git add 命令添加到缓存区才会提交; 如果是删除文件, 则使用 git rm 命令会自动将已删除的文件信息添加到缓存区.

最后, 可以使用 git push 命令将本地仓库同步到远端服务器.

1
git push origin master

分支与合并

Git的分支功能可以让你在主线(master)分支之外进行代码提交, 同时又不会影响代码库主线. 分支的作用体现在多人协作开发中, 例如在一个团队开发中, 如果一个功能需要一个月完成, 就可以新建一个分支, 只把该功能的代码提交到这个分支中, 而其他功能可以继续使用主线开发, 这两者之间的提交不会互相影响. 当功能完成后, 只需要将分支合并到主线即可.

分支

一个Git仓库可以维护很多开发分支. 使用以下代码可以创建分支:

1
git branch branchname

如果不带分支名, 即查看当前的分支列表, 以及当前处于哪个分支.

使用 git checkout 命令可以切换到其他分支:

1
git checkout branchname

使用 git merge 命令进行分支合并, 将 branchname 分支合并到主分支:

1
2
git checkout master
git merge -m "xxx" branchname

如果两个branch修改了相同的文件, 则合并时可能发生冲突导致合并失败.失败之后可以先使用 git status 查看状态, 例如都修改了 file3, 则会看到file3 显示为both modified, 可以直接查看 file3的内容(或使用git diff), 冲突的内容可以直接进行编辑以去掉冲突.之后再提交到master分支即可.

当完成合并之后, 新分支如果不再需要, 可以使用以下命令删除:

1
git branch -d branchname

注意: 该命令只会删除已被当前分支合并的分支, 如果需要强制删除则使用 -D 参数.

撤销一个合并

如果合并后导致代码混乱, 可以使用以下命令放弃当前修改, 回到合并之前的状态:

1
git reset --hard HEAD^

快速向前合并

通常, 一个合并会产生一个合并提交, 把两个分支的每一行内容都合并进来.

但是, 如果当前分支的每一个提交都已经存在另一个分支中(没有内容上的差异), 则Git会执行一个快速向前操作, 不创建任何新提交, 只是将当前分支指向合并进来的分支.

Git日志

查看日志

使用 git log 命令显示所有的提交:

1
git log

如果提交的历史记录很长, 回车会逐步显示, 输入q可以退出.

日志统计

如果在 git log 命令中使用 --stat 参数, 会显示在每个提交中哪些文件被修改了, 分别添加或删除了多少行内容. 相当于打印详细的提交记录.

格式化日志

使用 --pretty 参数来确定显示格式, 例如oneline/short/medium/full/fuller/email/raw等.

1
git log --pretty=oneline

如果以上参数都不符合你的要求, 也可以使用 --pretty=format 参数定义格式.

--graph 选项可以可视化你的提交图, 会用ASCII字符来画出一个很漂亮的提交历史线.

日志排序

日志记录可以按照一个指定的特定顺序显示, 只需要添加顺序参数.

在默认情况下, 提交会按照逆时间顺序显示, 可以指定 --topo-order 参数, 让提交按照拓扑顺序显示(即子提交在父提交前显示).

1
git log --pretty=format:"%h: %s" --topo-order --graph

也可以使用 --reverse 参数来逆向显示所有提交日志.

比较内容

比较提交

首先对项目文件进行一些修改:

1
2
3
cd projectname
echo "new line" >> README.md
echo "new file" >> file1

然后使用 git diff 命令比较修改的或提交的文件内容:

1
git diff

该命令输出当前工作目录中修改的内容, 并不包含新加的文件, 而且这些内容还没有添加到本地缓存区.

如果需要查看缓存区与上次提交之间的差别, 需要使用 --cached 参数.

比较分支

使用 git diff 也可以比较项目中任意两个分支的差异.首先使用以下命令创建一个新的分支, 并进行一些修改:

1
2
3
4
5
6
git branch test
git checkout test
echo "branch test" >> file1
echo "new file2" >> file2
git add *
git commit -m "update test branch"

使用以下命令可以查看 test 分支与 master 分支的差异:

1
git diff master test

更多的比较选项

可以使用以下命令查看当前工作目录与另一个分支的差异:

1
2
git checkout master
git diff test

也可以加上路径限定符以只比较某个文件或目录:

1
git diff test file1

使用 --stat 参数可以统计有哪些文件被改动, 以及改动的行数.

分布式的工作流程

分布式的工作流程

假设当前项目在 /tmp/gitproject 目录下, 这时有另一个用户想协作开发.

首先, 该用户需要对Git仓库进行克隆:

1
2
cd /tmp
git clone /tmp/gitproject myrepo

这样就新建了一个叫 myrepo 的目录, 该目录包含一份 girproject 仓库的一模一样的克隆, 并且拥有原始项目的历史记录.

在 myrepo 中进行一些修改并提交:

1
2
3
4
cd myrepo
echo "newcontent" > newfile
git add newfile
git commit -m "add newfile"

修改提交之后, 原 gitproject 仓库想合并这份修改可以使用以下命令:

1
2
cd /tmp/gitproject
git pull /tmp/myrepo master

这样就把 myrepo 的主分支合并到了 gitproject 的当前分支中, 如果两个分支同时修改了同一个文件, 可能需要手工修复冲突.

如果经常需要操纵远程分支, 可以定义远程分支的缩写:

1
git remote add myrepo /tmp/myrepo

git pull 命令执行两个操作: 从远程分支抓取 git fetch 的内容, 然后把它合并git merge 到当前分支.

在 gitproject 中可以用 git fetch 来执行抓取, 然后用 git log 查看远程分支的所有修改, 然后进行合并:

1
2
3
git fetch myrepo
git log -p master ..myrepo/master
git merge myrepo/master

如果在 myrepo 目录下执行 git pull, 则相当于把 gitproject 上的修改同步到本地.

因为 myrepo 是从 gitproject 仓库克隆的, 所以不需要指定 gitporject 的地址. Git 把 gitproject 仓库的地址存储到 myrepo 的配置文件中, 即git pull 时默认使用的远程仓库:

1
git config --get remote.origin.url

如果两个仓库在不同的主机, 可以使用 ssh 协议进行 clone 和 pull 的操作.

公共Git仓库

在开发过程中, 通常会使用一个公共的仓库, 并clone到自己的开发环境中, 完成一个阶段的代码后会告诉目标仓库的维护者来 pull 自己的代码.

1
2
3
4
git clone /path/to/repo
git pull /path/to/other/repo
git clone ssh://host/repo

将修改推送到一个公共仓库

通过 http 或者 git 协议, 其他维护者可以通过远程访问的方式抓取你最近的修改, 但是他们没有些权限.

可以使用 git push 命令将本地的修改推送到远程Git仓库:

1
2
git push ssh://host/repo master:master
git push ssh://host/repo master

该命令的目的仓库可以是 ssh 或 http/https 协议访问.

当推送失败时如何处理

如果推送不是快速向前的, 可能会出错. 这种情况通常是因为没有使用 git pull 获取远程仓库的最新更新, 在本地修改的同时远程仓库已经发生变化, 此时应该先使用git pull 合并最新的修改后再次执行git push 命令.

Git标签

轻量级标签

可以使用 git tag 不带任何参数创建一个标签指定某个提交:

1
2
3
4
cd /tmp/gitproject
git log
git tag stable-1 8c2333
git tag

这样之后, 我们可以使用 stable-1 作为提交ID(8c2333)的代称.

标签对象

如果想为一个tag添加注释或者签名, 就需要创建一个标签对象.

git tag 使用 -a, -s 或 -u 三个参数的任意一个都会创建一个标签对象, 并且需要一个标签消息来为 tag 添加注释. 如果没有 -m 或 -F 参数, 该命令执行时会启动一个编辑器来让用户输入标签消息.

1
2
git tag -a stable-2 8c2333 -m "stable 2"
git tag

当这样的一条命令执行之后, 一个新的对象被添加到Git对象库中, 并且标签引用指向一个标签对象而不再是指向一个提交. 这就是标签对象与轻量级标签的区别.

签名的标签

签名标签可以让提交和标签更加完整. 如果配 GPG key , 则很容易创建签名的标签.

可以在 .git/config 或 ~/.gitconfig 中配置key:

1
2
[user]
signingkey = <gpg-key-id>

或者使用命令行配置:

1
git config (--global) user.signingkey <gpg-key-id>

可以在创建标签时使用 -s 参数来创建签名的标签, 且如果在配置文件中没有key, 使用 -u 参数可以直接指定:

1
2
git tag -s stable-1 1b2
git tag -u <gpg-key-id> stable-1 1b2

中级用法

忽略文件

在项目中经常会生成一些Git系统不需要追踪的文件. 典型的是在编译过程中产生的文件或是编辑器生成的临时备份文件. 当然, 若你不需要追踪这些文件, 可以在平时不使用git add 去把它们加到索引中. 但这样有一些缺点:

  1. 项目中到处都有未追踪的文件
  2. 在这种情况下 git add . 以及 git commit -a 命令就不能被使用
  3. git status 命令还会输出这些文件没有被追踪.

比较好的方式是在顶层工作目录中添加一个名为 .gitignore 的文件, 来定义使Git系统忽略掉哪些文件. 该文件的内容中, 以# 开始的行为注释. 一个内容示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 忽略某个文件
foo.txt
# 忽略所有的 html 文件
*.html
# 在上面的基础上, 追踪 foo.html 文件
!foo.html
# 忽略所有的 .o 和 .a 文件.
*.[oa]
# 忽略整个文件夹
foo/
# 追踪文件夹中的 foo1.txt
!foo/foo1.txt

rebase

rebase

假设你现在的分支基于远程分支 origin , 创建一个名为 mywork 的分支.

1
git checkout -b mywork origin

现在在这个分支上做一些修改, 然后生成两个提交:

1
2
3
4
vi file.txt
git commit
vi otherfile.txt
git commit

但与此同时, 有些人也在 origin 分支上做了一些修改并做了提交. 这意味着 origin 与 mywork 分支各自前进了, 它们之间分叉了. 在这里, 你可以利用 pull 命令把 origin 分支上的修改拉取下来并和你的修改合并, 其结果就像一个新的合并的提交. 但是如果你想让 mywork 分支历史看起来像没有经过任何合并一样, 可以使用git rebase.

1
2
git checkout mywork
git rebase origin

这些命令会将你的 mywork 分支里的每个提交取消掉, 并且把它们临时保存为补丁(在 .git/rebase 目录中), 然后把 mywork 分支更新到最新的 origin 分支, 然后把补丁应用到 mywork 分支上. 当 mywork 分支更新之后, 它会指向这些新创建的提交, 而把老的提交丢弃掉. 如果运行垃圾收集命令, 这些提交就会被删除. 在rebase过程中允许出现冲突, 在这种情况下, Git会停止rebase 并让你去解决这些冲突, 在解决完冲突后, 用git add 命令去更新这些内容的索引, 之后并不需要执行 commit, 只需要执行以下命令:

1
git rebase --continue

这样Git会继续应用余下的补丁. 在任何时候, 都可以使用 --abort 参数来终止rebase过程, 并且 mywork 分支会恢复到 rebase 开始前的状态.

交互式rebase

rebase还有一种交互式使用方式, 这种方式通常用于在向别处推送提交之前对它们进行重写. 交互式rebase提供了一个简单易用的途径让你在和别人分享提交之前对你的提交进行分割、合并或者重排序. 在把从其他开发者处拉取的提交应用到本地时, 也可以使用交互式rebase对它们进行清理. 如果你想在rebase的过程中对一部分提交进行修改, 可以在git rebase 命令中加入-i 或者 --interactive 参数去调用交互模式.

1
git rebase -i origin/master

这个命令会进行交互式rebase操作, 操作对象是那些自最后一次从origin仓库拉取或者向origin推送之后的所有提交. 若想查看将被rebase的提交, 可以使用如下的log命令:

1
git log github/master..

一旦你完成对提交信息的编辑并退出编辑器, 这个新的提交及提交信息会被保存起来. 如果指定进行edit操作, git会完成同样的工作, 但在对下一个提交进行操作之前, 它会返回到命令行让你对提交进行修正, 或者对提交内容进行修改. 例如你想要分割一个提交, 你需要对那个提交指定edit操作: 这个命令会进入命令行, 撤销该提交, 然后创建两个或者更多个提交. 假设提交21d80a5修改了两个文件file1和file2, 你想把这两个修改放到不同的提交里. 使用命令行操作如下:

1
2
3
4
5
6
git reset HEAD
git add file1
git commit -m "first part of split commit"
git add file2
git commit -m "second part of split commit"
git rebase --continue

交互式rebase的最后一个作用是丢弃提交. 如果把一行删除而不是指定picksquashedit 中的任何一个, Git会从历史中移除该提交.

交互式添加

交互式添加提供友好的界面去操作Git索引, 同时也提供了可视化索引的能力. 只需简单键入 git add -i, 即可使用此功能. Git会列出所有修改过的文件及它们的状态.

1
git add -i

在这个例子中,我们可以看到有5个修改过的文件还没有被加入到索引中,甚至可以看到每个文件增加和减少的行数。紧接着是一个交互式的菜单,列出了我们可以在此模式中使用的命令. 如果我们想要暂存这些文件,我们可以键入'2'或者'u'进入更新模式. 然后我们可以通过键入文件的范围(本例中是1-4)来决定把哪些文件加入到索引之中.

如果键入回车,我会回到主菜单中,同时可以看到那些指定文件的状态已经发生了改变:

现在我们可以看到前4个文件已经被暂存,但是最后一个没有. 基本上,这是一个更加紧凑的查看状态的方式,实质上的信息与我们在命令行中运行git status是一致的.

储藏

储藏

当你在做一项复杂的工作时, 发现了一个和当前工作不相关但是又很讨厌的BUG, 你这时想先修复BUG再做手头的工作, 那么就可以使用 git stash 来保存当前的工作状态, 等你修复完BUG后再执行'反储藏'操作就可以回到之前的工作里.

1
git stash save "work in progress for foo feature"

上面这条命令会保存你的本地修改到储藏中, 然后将你的工作目录和索引里的内容全部重置, 回到你当前所在分支的上次提交时的状态. 修改BUG之后进行提交:

1
git commit -a -m "blorpl: typofix"

当你修复完成BUG后, 可以使用以下命令来恢复到以前的工作状态:

1
git stash apply

储藏队列

可以多次使用 git stash 命令, 每执行一次就会把针对当前修改的储藏添加到储藏队列中. 用 git stash list 命令可以查看所有的储藏:

1
git stash list

也可以用以下命令来恢复队列中的任意一个储藏:

1
git stash apply stash@{1}

使用以下命令清空队列:

1
git stash clear

Git树名

Git树名

不使用40个字节长的SHA串来表示一个提交或是其它Git对象, 有很多种其它的名字表示方法. 在Git中, 这些名字就叫做树名(treeish).

Sha短名

如果你的一个提交的SHA名字是 980e3ccdaac54a0d4de358f3fe5d718027d96aae, Git会把下面的串视为等价:

1
2
3
980e3ccdaac54a0d4de358f3fe5d718027d96aae
980e3ccdaac54a0d4
980e3cc

只需要你的SHA短名是不重复的, 它就不会和其它名字冲突(5个字节以上很难重复), Git也会把SHA短名自动补全.

分支,Remote或标签

可以使用分支, remote或标签名来代替SHA串名, 它们只是指向某个对象的指针. 假设你的master分支目前在提交 980e3 上, 现在把它推送到origin上并把它命名为标签 v1.0, 那么下面的串会被Git视为等价:

1
2
3
4
5
6
7
980e3ccdaac54a0d4de358f3fe5d718027d96aae
origin/master
refs/remotes/origin/master
master
refs/heads/master
v1.0
refs/tags/v1.0

日期标识符

Git的引用日志可以让你做一些相对查询操作:

1
2
master@{yesterday}
master@{1 month ago}:

上面第一条命令的意思是: master分支的昨天状态.

注意: 即使是在两个有相同master分支的仓库上执行这条命令, 但是如果这两个仓库在不同机器上, 那么执行结果也很可能不一样.

顺序标识符

用以下命令表达某点前面的第N个提交:

1
master@{5}

意思是master前面的第5个提交.

多个父对象

某个提交的第N个直接父提交. 这种格式在合并提交时特别有用, 这样就可以使提交对象有多于一个直接父对象.

1
master^2

波浪号

波浪号是用来标识一个提交对象的第N级父对象:

1
master~2

以上代表master所指向的提交对象的第一个父对象的第一个父对象. 等价于以下表达式:

1
master^^

可以将这些标识符叠加起来, 下面3个表达式指向同一个提交:

1
2
3
master^^^^^^
master~3^~2
master~6

树对象指针

每一个提交对象是指向一个树对象的, 假如你要得到一个提交对象指向的树对象的SHA串名, 可以使用以下方式:

1
master^{tree}

二进制标识符

如果你需要某个二进制对象的SHA串名, 你可以在树名后添加二进制对象对应的文件路径来得到:

1
master:/path/to/file

区间

可以用'..'来指两个提交之前的区间, 下面的命令会给出在 '7b593b5' 和 '51bea1' 之间除了 '7b593b5' 的所有提交:

1
7b593b5..51bea1

以下命令包括从 7b593b 开始的提交, 两个命令是等价的:

1
2
7b593b..
7b593b..HEAD

追踪分支

在Git中追踪分支是用于联系本地分支和远程分支的. 如果你在追踪分支上执行推送或者拉取时, 它会自动推送或拉取到关联的远程分支上. 如果你经常要从远程仓库里拉取分支到本地, 并且不想麻烦的使用git pull 这种格式, 那么就应当使用追踪分支功能.git clone 命令会自动在本地建立一个master分支, 它是 origin/master 的追踪分支. 而 origin/master 就是被克隆仓库的 master 分支. 你可以在使用git branch 命令时加上--track 参数来手动建立一个追踪分支.

1
git branch --track experimental origin/experimental

当你运行以下命令时:

1
git pull experimental

它会自动从 origin 抓取内容, 再把远程的 origin/experimental 分支合并到本地的 experimental 分支. 当要把修改推送到 origin 时, 它会将你本地的 experimental 分支中的修改推送到 origin/experimental 分支里而无需指定源.

使用 Git Grep 进行搜索

git grep 命令查找Git仓库里的某段文字是很方便的. 当然, 你也可以使用 unix 下的 grep 命令进行搜索, 但是git grep 命令可以让你不用检出历史文件就能查找它们.

例如, 你需要查看Git仓库中使用 xmmap 函数的地方, 可以使用如下命令:

1
git grep xmmap

还可以使用如下参数:

  1. -n: 显示行号
  2. --name-only: 只显示文件名
  3. -c: 查看每个文件有都少匹配的行

如果想要查找Git仓库中某个特定版本的内容, 在命令末尾加上标签名即可:

1
git grep xmmap v1.5.0

还可以组合一些搜索条件, 例如查看在仓库的哪个地方定义了 SORT_DIRENT:

1
git grep -e "#define" --and -e SORT_DIRENT

除了与条件搜索, 也可以进行或条件搜索:

1
git grep --all-match -e '#define" -e SORT_DIRENT

也可以查找符合一个条件且符合两个条件之一的文件行. 例如查找名字中含有 PATH 或 MAX 的常量定义:

1
git grep -e '#define' --and \(-e PATH -e MAX\)

Git的撤销操作-重置, 检出和撤销

修复未提交文件中的错误

如果你的工作目录中很混乱, 而且还没有进行提交. 则可以使用以下命令让工作目录回到上次提交时的状态:

1
git reset --hard HEAD

该命令会清空所有的未提交内容(不包括未跟踪文件).

如果只需要恢复一个文件, 可以使用如下命令:

1
git checkout -- hello.rb

该命令会将 hello.rb 从 HEAD 中检出并恢复成未修改的样子.

修复已提交文件中的错误

如果你已经做了一个提交, 但是又想对提交进行修改. 有以下两种方式:

一是创建一个新的提交, 在新的提交中撤销老提交所做的修改. 这种做法适用于代码已发布的情况.

二是去修改老提交.

以下命令演示了如果撤销最近的一个提交:

1
git revert HEAD

该命令创建了一个撤销上次提交的新提交. 这样就可以修改新提交里的注释等信息, 也可以撤销更早期的修改, 例如撤销上上次提交:

1
git revert HEAD^

在这种情况下, Git会尝试去撤销老提交, 然后留下完整的老提交之前的版本. 如果你最近的修改和要撤销的修改有冲突, 需要手工解决.

注意: git revert 命令并不会直接创建一个提交, 你需要手动提交一次. git commit 现在支持一个--amend 参数, 通过这个参数可以修改刚才的提交.

维护Git

保证良好的性能

在大的仓库中, Git靠压缩历史信息来节约磁盘和内存空间. 压缩操作不是自动进行的, 需要使用以下命令手动执行:

1
git gc

压缩操作比较耗时.

保持可靠性

可以使用 git fsck 运行一些仓库的一致性检查.

1
git fsck

该操作也比较耗时, 通常的警告信息是 悬空对象. 悬空对象并不是问题, 最坏的情况只是它们多占了一些磁盘空间. 有时它们是找回丢失工作的最后一丝希望.

建立公共仓库

建立一个公共仓库

假设你个人的仓库在目录 ~/proj. 我们先克隆一个新的裸仓库, 并且创建一个标志文件告诉 git-daemon 这是个公共仓库:

1
2
git clone --bae ~/proj proj.git
touch proj.git/git-daemon-export-ok

上面的命令创建了一个proj.git目录, 这个目录里有一个裸仓库(即只有.git目录里的内容, 没有任何检出的文件). 下一步就是把这个 proj.git 目录拷贝到用来托管公共仓库的主机上.

通过git协议导出git仓库

推荐的做法是用git协议导出git仓库. 你需要启动git daemon, 它会在9418端口监听, 默认情况会允许你访问所有的git目录(看目录中是否有git-daemon-export-ok文件). 如果以某些目录作为git-daemon 的参数, 那么它会限制用户通过git协议只能访问这些目录. 可以在inetd service 模式下运行git-daemon.

通过http协议导出git仓库

git协议有不错的性能和可靠性, 但如果主机上已经配置好一个web服务器, 使用http协议可能会更容易. 你需要把新建的裸仓库放到web服务器可以访问的目录里, 同时做一些调整以便web客户端获得它们所需的额外信息:

1
2
3
4
mv proj.git /path/html/proj.git
cd proj.git
git --bare update-server-info
chmod a+x hooks/post-update

然后通过以下命令克隆仓库即可:

1
git clone https://yourserver.com/~you/proj.git

建立一个私有仓库

通过SSH协议访问私有仓库

一般来说最简单的办法是通过SSH协议访问Git. 如果你在一台机器上有一个SSH帐号, 你只需要把Git裸仓库放到任何一个可以通过SSH访问的目录, 然后可以像SSH登录一样简单的使用它. 假设你现在有一个仓库, 并且你要把它建成可以在网上访问的私有仓库. 你可以使用下面的命令导出一个裸仓库, 然后用SCP命令把它们拷贝到服务器:

1
2
git clone --bare /home/user/myrepo/.git /tmp/myrepo.git
scp -r /tmp/myrepo.git myserver.com:/opt/git/myrepo.git

如果其它人也在 myserver.com 这台服务器上有SSH帐号, 那么他可以从这台服务器上克隆代码:

1
git clone myserver.com:/opt/git/myrepo.git

上面的命令会提示你输入SSH密码或是使用公钥.

高级用法

创建新的空分支

在有些情况下, 你可能会想要保留那些与你的代码没有共同祖先的分支. 例如在这些分支上保留生成的文档或者其他一些东西. 如果你需要创建一个不实用当前代码库作为父提交的分支, 方法如下:

1
2
3
4
5
6
git symbolic-ref HEAD refs/heads/newbranch
rm .git/index
git clean -fdx
<do work>
git add your file
git commit -m "Initial commit"

修改历史

交互式衍合是修改单个提交的好方法. git filter-branch 是修改大量提交的好方法.

高级分支和合并

在合并过程中得到解决冲突的协助

git会把所有何以自动合并的修改加入到索引中, 所以 git diff 只会显示有冲突的部分.

在我们解决冲突之后, 得到的提交会有两个而不是一个父提交: 一个父提交会成为HEAD, 也就是当前分支的tip; 另一个父提交会成为另一个分支的tip, 暂时保存在MERGE_HEAD中. 在合并过程中, 索引中保存着每个文件的三个版本. 三个文件暂存中的每一个都代表了文件的不同版本:

1
2
3
git show :1:file.txt
git show :2:file.txt
git show :3:file.txt

当使用 git diff 去显示冲突时, 它在工作树, 暂存2和暂存3之间执行三路diff操作, 只显示那些两方都有的块(即当一个块的合并结果只从暂存2中得到时是不会被显示的).

Git不在每行前面加上单个+或者-, 而是使用两栏去显示差异: 第一栏用于显示第一个父提交与工作目录文件拷贝的差异; 第二栏用户显示第二个父提交与工作文件拷贝的差异. 解决冲突之后显示示例如下:

1
2
3
4
5
6
7
8
9
git diff
diff --cc file.txt
index 802992c,2b60207..0000000
--- a/file.txt
+++ b/file.txt
@@@ -1,1 -1,1 +1,1 @@@
- Hello world
-Goodbye
++Goodbye world

上面的输出表示删除了第一个父版本的"Hello world" 和第二个父版本的"Goodbye", 然后加入了 "Goodbye world".

有些diff选项可以对比工作目录和三个暂存中的任何一个的差异:

1
2
3
4
5
6
git diff -1 file.txt
git diff --base file.txt
git diff -2 file.txt
git diff --ours file.txt
git diff -3 file.txt
git diff --theirs file.txt

git loggitk 命令也为合并提供了特别的协助:

1
2
git log --merge
gitk --merge

这会显示那些只在HEAD或只在MERGE_HEAD中的提交, 还有那些更新了为合并文件的提交. 也可以使用 git mergetool, 它允许你使用外部工具如emacskdiff3 去合并文件, 在每次解决冲突之后都应该使用 git add 更新索引.

完成索引更新之后, git diff 默认不再显示那个文件的差异, 所以那个文件的不同暂存版本被折叠了起来.

多路合并

可以一次合并多个头, 只需要简单把它们作为git merge的参数列出:

1
git merge scott/master rick/master tom/master

等价于:

1
2
3
git merge scott/master
git merge rick/master
git merge tom/master

子树

有时会出现你想要在项目中引入其他独立开发项目的内容的情况. 在没有路径冲突的前提下, 只需简单的从其他项目拉取内容即可, 如果有冲突文件, 你可以选择合并这些冲突, 但是更好的解决方案是把外部项目作为一个子目录进行合并, 这种情况不被递归合并策略支持, 而需要子树合并策略.

假设你有一个仓库位于 /path/to/B, 你想要合并这个仓库的master分支到你当前仓库的dir-B子目录下:

1
2
3
4
5
git remote add -f Bproject /path/to/B
git merge -s ours --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

子树合并的好处是它并没有给你仓库的用户增加太多的管理负担, 它兼容于较老版本的客户端, 克隆完成之后马上可以得到代码. 然后如果过你使用子模块, 你可以选择不传输这些子模块对象, 这可能在子树合并过程中造成问题. 另外, 如果你需要修改内嵌外部项目的内容, 使用子模块方式可以更容易地提交修改.

查找问题的利器

Git Bisect

假设你在项目的 2.6.18 版本上工作, 但是你当前的代码崩溃了. 有时解决这种问题的最好办法是: 手工逐步恢复项目历史, 找出是哪个提交导致了问题. 但是git-bisect 可以更好的帮你解决问题:

1
2
3
git bisect start
git bisect good v2.6.18
git bisect bad master

如果你现在运行 git branch, 会发现你现在所在的是 "no branch", 这时分支指向提交 "69543", 此提交刚好是 "v2.6.18" 和 "master" 中间的位置. 现在在这个分支里编译并测试项目代码, 如果它崩溃了, 那么运行下面的命令:

1
git bisect bad

现在git会自动检出一个更老的版本. 继续这样做, git会使用二分查找检出good和bad之间的一个版本, 当找出导致问题的提交, 运行:

1
git bisect reset

回到执行 git bisect start 之间的状态. 也可以手工选择版本:

1
git bisect visualize

这会运行gitk, 界面上会标识出 git bisect 命令自动选择的提交. 你可以选择一个相邻的提交, 记住它的SHA值. 用下面的命令检出:

1
git reset --hard fb47ddb

这样反复执行, 直到找出导致问题的提交为止.

Git Blame

如果你要查看文件的每个部分是谁修改的, 那么 git blame 是不二选择. 只要运行该命令, 你就会得到整个文件的每一行的详细修改信息, 包括SHA, 日期和作者.

1
git blame sha1_file.c

如果文件被修改了或是编译失败了, 可以通过这个命令去查找导致错误的详细信息. 可以用 -L 参数指定开始和结束行:

1
git blame -L160,+10 sha1_file.c

Git和Email

向一个项目提交补丁

如果只做了少量改动, 最简单的提交方式就是把它们做成补丁用邮件发送出去. 首先生成补丁:

1
git format-patch origin

这会在当前目录生成一个系统编号的补丁文件, 包含了当前分支和origin/HEAD之间的差异内容. 然后通过 git send-email 命令利用邮件发送补丁.

向一个项目中导入补丁

Git提供了一个名为 git am 的工具去应用哪些通过email寄来的系列补丁. 你需要将所有包含补丁的消息存入单个的mailbox文件, 例如 patches.mbox, 然后运行:

1
git am -3 patches.mbox

Git会按照顺序应用每一个补丁, 如果发生冲突, Git会让你手工解决冲突而完成合并. -3选项让git执行合并操作, 如果你需要中止并不改动你的工作树和索引可以省略该选项. 在解决冲突和更新所有之后, 不需要在创建一个新的提交, 只需要运行:

1
git am --resolved

这时git会为你创建一个提交, 然后继续应用mailbox中余下的补丁.

最后的效果是, git产生了一系列提交, 每个提交是原来mailbox中的一个补丁, 补丁中的作者信息和提交日志也被记录下来.

定制Git

更改编辑器

1
git config --global core.editor vim

添加别名

1
2
3
git config --global alias.last "cat-file commit HEAD"
git last
git cat-file commit HEAD

添加颜色

1
2
3
4
git config color.branch auto
git config color.diff auto
git config color.interactive auto
git config color.status auto

或者通过以下命令把颜色全部打开:

1
git config color.ui true

提交模版

1
git config commit.template '/etc/git-commit-template'

日志格式

1
git config format.pretty online

Git Hooks

钩子(hooks)是一些在 "$GIT-DIR/hooks" 目录的脚本, 在被特定的事件触发后被调用, 默认情况下这些钩子是不生效的, 把这些钩子文件的 ".sample" 文件名后缀去掉就可以让它们生效了.

applypatch-msg

该钩子文件为:

1
GIT_DIR/hooks/applypatch-msg

git-am 命令执行时这个钩子被调用. 它只有一个参数: 就是存有提交信息的文件的名字. 如果钩子的执行结果为非零, 那么补丁就不会被应用.

这个钩子用于在其他地方编辑提交信息, 并且可以把这些消息规范成项目的标准. 也可以在分析完消息文件后拒绝此次提交.

pre-applypatch

该钩子文件为:

1
GIT_DIR/hooks/pre-applypatch

当'git-am'命令执行时, 这个钩子就被调用. 它没有参数, 并且是在一个补丁被应用后还未提交前被调用. 如果钩子的执行结果是非零, 那么刚才应用的补丁就不会被提交.

它用于检查当前的工作树, 当提交的补丁不能通过特定的测试就拒绝将它提交进仓库.

post-applypatch

该钩子文件为:

1
GIT_DIR/hooks/post-applypatch

当'git-am'命令执行时, 这个钩子就被调用. 它没有参数, 并且是在一个补丁被应用且在完成提交情况下被调用. 这个钩子的主要用途是通知提示(notification), 它并不会影响'git-am'的执行和输出.

pre-commit

该钩子文件为:

1
GIT_DIR/hooks/pre-commit

这个钩子被 'git-commit' 命令调用,, 而且可以通过在命令中添加--no-verify 参数来跳过. 这个钩子没有参数, 在得到提交消息和开始提交前被调用. 如果钩子执行结果是非零, 那么 'git-commit' 命令就会中止执行. 当默认的'pre-commit'钩子开启时, 如果它发现文件尾部有空白行, 那么就会中止此次提交.

prepare-commit-msg

该钩子文件为:

1
GIT_DIR/hooks/prepare-commit-msg

当'git-commit'命令执行时: 在编辑启动前, 默认提交消息准备好后, 这个钩子就被调用. 如果钩子的执行结果是非零的话, 那么'git-commit'命令就会被中止执行.

commit-msg

该钩子文件为:

1
GIT_DIR/hooks/commit-msg

当'git-commit'命令执行时, 这个钩子被调用, 也可以在命令中添加--no-verify参数来跳过. 这个钩子有一个参数: 就是被选定的提交消息文件的名字. 如这个钩子的执行结果是非零, 那么'git-commit'命令就会中止执行.

这个钩子为提交消息更适当, 可以用于规范提交消息使之符合项目的标准; 如果它检查完提交消息后, 发现内容不符合某些标准, 它也可以拒绝此次提交. 默认的'commit-msg'钩子启用后, 它检查里面是否有重复的签名结束线, 如果找到它就是中止此次提交操作.

post-commit

该钩子文件为:

1
GIT_DIR/hooks/post-commit

当'git-commit'命令执行时, 这个钩子就被调用. 它没有参数, 并且是在一个提交完成时被调用. 这个钩子的主要用途是通知提示, 它并不会影响'git-commit'的执行和输出.

pre-rebase

该钩子文件为:

1
GIT_DIR/hooks/pre-rebase

当'git-base'命令执行时, 这个钩子就被调用, 主要目的是阻止那些不应被rebase的分支被rebase(例如, 一个已经发布的分支提交就不应被rebase).

post-checkout

该钩子文件为:

1
GIT_DIR/hooks/post-checkout

当'git-checkout'命令更新完整个工作树后, 这个钩子就会被调用. 这个钩子有三个参数: 前一个HEAD的 ref, 新HEAD的 ref, 判断一个签出是分支签出还是文件签出的标识符(分支签出=1, 文件签出=0). 这个钩子不会影响'git-checkout'命令的输出. 这个钩子可以用于检查仓库的一致性, 自动显示签出前后的代码的区别, 也可以用于设置目录的元数据属性.

post-merge

该钩子文件为:

1
GIT_DIR/hooks/post-merge

pre-receive

当用户在本地仓库执行 git push 命令时, 服务端上远程仓库就会对应执行 git-receive-pack 命令, 该命令会调用pre-receive 钩子. 在开始更新远程仓库上的ref之前, 这个钩子被调用. 钩子的执行结果决定此次更新能否成功. 每次执行一个接受操作都会调用一次这个钩子. 它没有命令行参数, 但是它会从标准输入读取需要更新的ref, 格式如下:

1
<old-value> SP <new-value> SP <ref-name> LF

其中SP是空格, LF是回车. 是保存在ref中的老对象的名字, 是保存在ref里的新对象的名字, 是此次更新的ref的全名. 如果是创建一个新的ref, 那么 就是由40个0组成的字符串. 如果钩子的执行结果为非零, 那么没有引用会被更新. 如果执行结果为零, 更新操作还可以被后面的 <<update,'update>> 钩子所阻止. 钩子的标准输出和标准错误输出会通过 'git-send-pack' 转发给客户端.

update

该钩子文件为:

1
GIT_DIR/hooks/update

当用户在本地仓库执行'git-push'命令时, 服务器上远端仓库就会对应执行'git-receive-pack', 而'git-receive-pack'会调用 update 钩子. 在更新远程仓库上的ref之前, 这个钩子被调用. 钩子的执行结果决定此次update能否成功.

post-receive

该钩子文件为:

1
GIT_DIR/hooks/post-receive

当用户在本地仓库执行'git-push'命令时, 服务器上远端仓库就会对应执行'git-receive-pack'命令, 在所有远程仓库的引用(ref)都更新后, 这个钩子就会被'git-receive-pack'调用.

找回丢失的对象

准备

首先创建一个实验用的仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ mkdir recovery
$ cd recovery
$ git init
$ touch file
$ git add file
$ git commit -m "First commit"
$ echo "Hello World" > file
$ git add .
$ git commit -m "Greetings"
$ git branch cool_branch 
$ git checkout cool_branch
$ echo "What up world?" > cool_file
$ git add .
$ git commit -m "Now that was cool"
$ git checkout master
$ echo "What does that mean?" >> file

恢复已删除的分支提交

现在repo中有两个分支 (master和cool_branch), 首先存储当前仓库未提交的改动:

1
git stash save "temp save"

删除一个分支:

1
git branch -D cool_branch

找出刚才删除的分支里的提交对象:

1
git fsck --lost-found

查看对象的内容:

1
git show 2e43cd56ee4fb08664cd843cd32836b54fbf594a

恢复:

1
git rebase 2e43cd56ee4fb08664cd843cd32836b54fbf594a

也可以使用 git merge 进行恢复, 把刚才恢复的提交删除:

1
git reset --hard HEAD^

把刚删除的提交找回:

1
git fsck --lost-found

用合并命令恢复:

1
git merge 2e43cd56ee4fb08664cd843cd32836b54fbf594a

git stash的恢复

在上一个演示中我们把没有提交的内容利用 git stash 进行了存储, 如果这个存储不小心删除了怎么办呢?

1
2
git stash list
git stash clear

先找回删除:

1
git fsck --lost-found

然后恢复:

1
2
git show 674c0618ca7d0c251902f0953987ff71860cb067
git merge 674c0618ca7d0c251902f0953987ff71860cb067

子模块

子模块

一个大项目通常由很多较小的, 自完备的模块组成.

1
2
3
4
5
6
7
8
9
10
11
12
mkdir ~/git
cd ~/git
for i in a b c d
do
mkdir $i
cd $i
git init
echo "module $i" > $i.txt
git add $i.txt
git commit -m "Initial commit, submodule $i"
cd ..
done

创建父项目, 加入所有的子模块:

1
2
3
4
5
6
7
mkdir super
cd super
git init
for i in a b c d
do
git submodule add ~/git/$i $i
done

git submodule add 命令进行了如下的操作:

它在当前目录下克隆各个子模块, 默认检出master分支. 把子模块的克隆路径加入到 gitmodules 文件中, 然后把这个文件加入到索引. 把子模块的当前提交ID加入到索引中, 准备进行提交.

提交父项目:

1
git commit -m "Add submodules a, b, c and d."

现在克隆父项目:

1
2
3
$ cd ..
$ git clone super cloned
$ cd cloned

子模块的目录已经创建, 但是没有内容, 然后拉取子模块:

1
2
git submodule init
git submodule update

通过init将子模块的url加入到 .git/config, 然后用udpate去克隆子模块仓库和检出父项目中指定的版本, 子模块处于头指针分离状态, 不在任何一个分支.

如果需要对子模块进行修改, 那么应该创建或检出一个分支, 进行修改, 然后发布子模块的修改, 更新父项目让其引用新的提交:

1
2
cd a
git checkout master

或者

1
2
cd a
git checkout -b fix-up

然后:

1
2
3
4
5
6
7
8
$ echo "adding a line again" >> a.txt
$ git commit -a -m "Updated the submodule from within the superproject."
$ git push
$ cd ..
$ git diff
$ git add a
$ git commit -m "Updated submodule a."
$ git push

如果需要更新子模块, 应该在 git pull 之后运行 git submodule update.

子模块方式的陷阱

应该总是在发布父项目的修改之前发布子模块修改. 如果忘记发布子模块修改, 其他人就不能克隆你的仓库:

1
2
3
4
5
6
7
8
9
10
$ cd ~/git/super/a
$ echo i added another line to this file >> a.txt
$ git commit -a -m "doing it wrong this time"
$ cd ..
$ git add a
$ git commit -m "Updated submodule a again."
$ git push
$ cd ~/git/cloned
$ git pull
$ git submodule update

如果你暂存了一个更新过的子模块, 准备进行手工提交, 注意不要在路径后面加上斜杠, git会认为你想要移除哪个子模块然后检出那个目录内容到父仓库:

1
2
3
4
5
6
$ cd ~/git/super/a
$ echo i added another line to this file >> a.txt
$ git commit -a -m "doing it wrong this time"
$ cd ..
$ git add a/
$ git status

为了修正这个错误, 我们应该重置这个修改:

1
2
3
$ git reset HEAD A
$ git add a
$ git status
原创粉丝点击