Git:命令

Git reset

git reset命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:git add,git commit等)。

简介

1
2
3
git reset [-q] [<tree-ish>] [--] <paths>…
git reset (--patch | -p) [<tree-ish>] [--] [<paths>…]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]

描述

在第一和第二种形式中,将条目从<tree-ish>复制到索引。 在第三种形式中,将当前分支头(HEAD)设置为<commit>,可选择修改索引和工作树进行匹配。所有形式的<tree-ish>/<commit>默认为 HEAD

这里的 HEAD 关键字指的是当前分支最末梢最新的一个提交。也就是版本库中该分支上的最新版本。

示例

以下是一些示例 -

在git的一般使用中,如果发现错误的将不想暂存的文件被git add进入索引之后,想回退取消,则可以使用命令:git reset HEAD <file>,同时git add完毕之后,git也会做相应的提示,比如:

1
2
3
4
# Changes to be committed: 
# (use "git reset HEAD <file>..." to unstage)
#
# new file: test.py

git reset [--hard|soft|mixed|merge|keep] [<commit>或HEAD]:将当前的分支重设(reset)到指定的<commit>或者HEAD(默认,如果不显示指定<commit>,默认是HEAD,即最新的一次提交),并且根据[mode]有可能更新索引和工作目录。mode的取值可以是hardsoftmixedmergedkeep。下面来详细说明每种模式的意义和效果。

A). --hard:重设(reset) 索引和工作目录,自从<commit>以来在工作目录中的任何改变都被丢弃,并把HEAD指向<commit>

下面是具体一个例子,假设有三个commit, 执行 git status结果如下:

1
2
3
commit3: add test3.c
commit2: add test2.c
commit1: add test1.c

执行git reset --hard HEAD~1命令后,
显示:HEAD is now at commit2,运行git log,如下所示 -

1
2
commit2: add test2.c
commit1: add test1.c

应用场景

下面列出一些git reset的典型的应用场景:

(A) 回滚添加操作

1
2
3
4
5
$ edit    file1.c file2.c           # (1) 
$ git add file1.c file1.c # (1.1) 添加两个文件到暂存
$ mailx # (2)
$ git reset # (3)
$ git pull git://info.example.com/ nitfol # (4)

(1). 编辑文件 file1.c, file2.c,做了些更改,并把更改添加到了暂存区。
(2). 查看邮件,发现某人要您执行git pull,有一些改变需要合并下来。
(3). 然而,您已经把暂存区搞乱了,因为暂存区同HEAD commit不匹配了,但是即将git pull下来的东西不会影响已经修改的file1.cfile2.c,因此可以revert这两个文件的改变。在revert后,那些改变应该依旧在工作目录中,因此执行git reset
(4). 然后,执行了git pull之后,自动合并,file1.cfile2.c这些改变依然在工作目录中。

(B)回滚最近一次提交

1
2
3
4
$ git commit -a -m "这是提交的备注信息"
$ git reset --soft HEAD^ #(1)
$ edit code #(2) 编辑代码操作
$ git commit -a -c ORIG_HEAD #(3)

(1) 当提交了之后,又发现代码没有提交完整,或者想重新编辑一下提交的信息,可执行git reset --soft HEAD^,让工作目录还跟reset之前一样,不作任何改变。
HEAD^表示指向HEAD之前最近的一次提交。
(2) 对工作目录下的文件做修改,比如:修改文件中的代码等。
(3) 然后使用reset之前那次提交的注释、作者、日期等信息重新提交。注意,当执行git reset命令时,git会把老的HEAD拷贝到文件.git/ORIG_HEAD中,在命令中可以使用ORIG_HEAD引用这个提交。git commit 命令中 -a参数的意思是告诉git,自动把所有修改的和删除的文件都放进暂存区,未被git跟踪的新建的文件不受影响。commit命令中-c <commit> 或者 -C <commit>意思是拿已经提交的对象中的信息(作者,提交者,注释,时间戳等)提交,那么这条git commit 命令的意思就非常清晰了,把所有更改的文件加入暂存区,并使用上次的提交信息重新提交。

(C) 回滚最近几次提交,并把这几次提交放到指定分支中

回滚最近几次提交,并把这几次提交放到叫做topic/wip的分支上去。

1
2
3
$ git branch topic/wip     (1) 
$ git reset --hard HEAD~3 (2)
$ git checkout topic/wip (3)

(1) 假设已经提交了一些代码,但是此时发现这些提交还不够成熟,不能进入master分支,希望在新的branch上暂存这些改动。因此执行了git branch命令在当前的HEAD上建立了新的叫做 topic/wip 的分支。
(2) 然后回滚master分支上的最近三次提交。HEAD~3指向当前HEAD-3个提交,git reset --hard HEAD~3,即删除最近的三个提交(删除HEAD, HEAD^, HEAD~2),将HEAD指向HEAD~3

(D) 永久删除最后几个提交

1
2
$ git commit ## 执行一些提交
$ git reset --hard HEAD~3 (1)

(1) 最后三个提交(即HEAD, HEAD^HEAD~2)提交有问题,想永久删除这三个提交。

(E) 回滚merge和pull操作

1
2
3
4
5
6
7
8
9
10
$ git pull                         (1) 
Auto-merging nitfol
CONFLICT (content): Merge conflict in nitfol
Automatic merge failed; fix conflicts and then commit the result.
$ git reset --hard (2)
$ git pull . topic/branch (3)
Updating from 41223... to 13134...
Fast-forward
$ git reset --hard ORIG_HEAD (4)
`

(1) 从origin拉取下来一些更新,但是产生了很多冲突,但您暂时没有这么多时间去解决这些冲突,因此决定稍候有空的时候再重新执行git pull操作。
(2) 由于git pull操作产生了冲突,因此所有拉取下来的改变尚未提交,仍然再暂存区中,这种情况下git reset --hardgit reset --hard HEAD意思相同,即都是清除索引和工作区中被搞乱的东西。
(3) 将topic/branch分支合并到当前的分支,这次没有产生冲突,并且合并后的更改自动提交。
(4) 但是此时又发现将topic/branch合并过来为时尚早,因此决定退滚合并,执行git reset --hard ORIG_HEAD回滚刚才的pull/merge操作。说明:前面讲过,执行git reset时,git会把reset之前的HEAD放入.git/ORIG_HEAD文件中,命令行中使用ORIG_HEAD引用这个提交。同样的,执行git pullgit merge操作时,git都会把执行操作前的HEAD放入ORIG_HEAD中,以防回滚操作。

(F) 在污染的工作区中回滚合并或者拉取

1
2
3
4
5
6
$ git pull                         (1) 
Auto-merging nitfol
Merge made by recursive.
nitfol | 20 +++++----
...
$ git reset --merge ORIG_HEAD (2)

(1) 即便你已经在本地更改了工作区中的一些东西,可安全的执行git pull操作,前提是要知道将要git pull下面的内容不会覆盖工作区中的内容。
(2) git pull完后,发现这次拉取下来的修改不满意,想要回滚到git pull之前的状态,从前面的介绍知道,我们可以执行git reset --hard ORIG_HEAD,但是这个命令有个副作用就是清空工作区,即丢弃本地未使用git add的那些改变。为了避免丢弃工作区中的内容,可以使用git reset --merge ORIG_HEAD,注意其中的--hard 换成了 --merge,这样就可以避免在回滚时清除工作区。

(G) 中断的工作流程处理

在实际开发中经常出现这样的情形:你正在开发一个大的新功能(工作在分支:feature 中),此时来了一个紧急的bug需要修复,但是目前在工作区中的内容还没有成型,还不足以提交,但是又必须切换的另外的分支去修改bug。请看下面的例子 -

1
2
3
4
5
6
7
8
9
$ git checkout feature ;# you were working in "feature" branch and 
$ work work work ;# got interrupted
$ git commit -a -m "snapshot WIP" (1)
$ git checkout master
$ fix fix fix
$ git commit ;# commit with real log
$ git checkout feature
$ git reset --soft HEAD^ ;# go back to WIP state (2)
$ git reset (3)

(1) 这次属于临时提交,因此随便添加一个临时注释即可。
(2) 这次reset删除了WIP commit,并且把工作区设置成提交WIP快照之前的状态。
(3) 此时,在索引中依然遗留着“snapshot WIP”提交时所做的未提交变化,git reset将会清理索引成为尚未提交”snapshot WIP“时的状态便于接下来继续工作。

(H) 重置单独的一个文件

假设你已经添加了一个文件进入索引,但是而后又不打算把这个文件提交,此时可以使用git reset把这个文件从索引中去除。

1
2
3
$ git reset -- frotz.c                      (1) 
$ git commit -m "Commit files in index" (2)
$ git add frotz.c (3)

(1) 把文件frotz.c从索引中去除,
(2) 把索引中的文件提交
(3) 再次把frotz.c加入索引

(I) 保留工作区并丢弃一些之前的提交

假设你正在编辑一些文件,并且已经提交,接着继续工作,但是现在你发现当前在工作区中的内容应该属于另一个分支,与之前的提交没有什么关系。此时,可以开启一个新的分支,并且保留着工作区中的内容。

1
2
3
4
5
6
7
$ git tag start 
$ git checkout -b branch1
$ edit
$ git commit ... (1)
$ edit
$ git checkout -b branch2 (2)
$ git reset --keep start (3)

(1) 这次是把在branch1中的改变提交了。
(2) 此时发现,之前的提交不属于这个分支,此时新建了branch2分支,并切换到了branch2上。
(3) 此时可以用reset --keep把在start之后的提交清除掉,但是保持工作区不变。

Git checkout

git checkout命令用于切换分支或恢复工作树文件。git checkout是git最常用的命令之一,同时也是一个很危险的命令,因为这条命令会重写工作区。

使用语法

1
2
3
4
5
6
git checkout [-q] [-f] [-m] [<branch>]
git checkout [-q] [-f] [-m] --detach [<branch>]
git checkout [-q] [-f] [-m] [--detach] <commit>
git checkout [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>…
git checkout [-p|--patch] [<tree-ish>] [--] [<paths>…]

描述

更新工作树中的文件以匹配索引或指定树中的版本。如果没有给出路径 - git checkout还会更新HEAD,将指定的分支设置为当前分支。

示例

以下是一些示例 -

示例-1

以下顺序检查主分支,将Makefile还原为两个修订版本,错误地删除hello.c,并从索引中取回。

1
2
3
4
$ git checkout master             #(1)
$ git checkout master~2 Makefile #(2)
$ rm -f hello.c
$ git checkout hello.c #(3)

(1) 切换分支
(2) 从另一个提交中取出文件
(3)从索引中恢复hello.c

如果想要检出索引中的所有C源文件,可以使用以下命令 -

1
$ git checkout -- '*.c'

注意:*.c是使用引号的。 文件hello.c也将被检出,即使它不再在工作树中,因为文件globbing用于匹配索引中的条目(而不是在shell的工作树中)。

如果有一个分支也命名为hello.c,这一步将被混淆为一个指令切换到该分支。应该写:

1
$ git checkout -- hello.c

示例-2

在错误的分支工作后,想切换到正确的分支,则使用:

1
$ git checkout mytopic

但是,您的“错误”分支和正确的“mytopic”分支可能会在在本地修改的文件中有所不同,在这种情况下,上述检出将会失败:

1
2
$ git checkout mytopic
error: You have local changes to 'frotz'; not switching branches.

可以将-m标志赋给命令,这将尝试三路合并:

1
2
$ git checkout -m mytopic
Auto-merging frotz

在这种三路合并之后,本地的修改没有在索引文件中注册,所以git diff会显示从新分支的提示之后所做的更改。

示例-3

当使用-m选项切换分支时发生合并冲突时,会看到如下所示:

1
2
3
4
$ git checkout -m mytopic
Auto-merging frotz
ERROR: Merge conflict in frotz
fatal: merge program failed

此时,git diff会显示上一个示例中干净合并的更改以及冲突文件中的更改。 编辑并解决冲突,并用常规方式用git add来标记它:

1
2
$ edit frotz # 编辑 frotz 文件中内容,然后重新添加
$ git add frotz

其它示例

git checkout的主要功能就是迁出一个分支的特定版本。默认是迁出分支的HEAD版本
一此用法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git checkout master     #//取出master版本的head。
$ git checkout tag_name #//在当前分支上 取出 tag_name 的版本
$ git checkout master file_name #//放弃当前对文件file_name的修改
$ git checkout commit_id file_name #//取文件file_name的 在commit_id是的版本。commit_id为 git commit 时的sha值。

$ git checkout -b dev/1.5.4 origin/dev/1.5.4

# 从远程dev/1.5.4分支取得到本地分支/dev/1.5.4
$ git checkout -- hello.rb
#这条命令把hello.rb从HEAD中签出.
$ git checkout .
#这条命令把 当前目录所有修改的文件 从HEAD中签出并且把它恢复成未修改时的样子.
#注意:在使用 git checkout 时,如果其对应的文件被修改过,那么该修改会被覆盖掉。

Git merge

git merge命令用于将两个或两个以上的开发历史加入(合并)一起。

使用语法

1
2
3
4
5
6
git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
[-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--[no-]allow-unrelated-histories]
[--[no-]rerere-autoupdate] [-m <msg>] [<commit>…]
git merge --abort
git merge --continue

描述

将来自命名提交的更改(从其历史从当前分支转移到当前分支之后)。 该命令由git pull用于合并来自另一个存储库的更改,可以手动使用将更改从一个分支合并到另一个分支。

示例

以下是一些示例 -

示例-1

合并分支fixesenhancements在当前分支的顶部,使它们合并:

1
$ git merge fixes enhancements

示例-2

合并obsolete分支到当前分支,使用ours合并策略:

1
$ git merge -s ours obsolete

示例-3

将分支maint合并到当前分支中,但不要自动进行新的提交:

1
$ git merge --no-commit maint

当您想要对合并进行进一步更改时,可以使用此选项,或者想要自己编写合并提交消息。应该不要滥用这个选项来潜入到合并提交中。小修补程序,如版本名称将是可以接受的。

示例-4

将分支dev合并到当前分支中,自动进行新的提交:

1
$ git merge dev

Git fetch

Git pull

git pull命令用于从另一个存储库或本地分支获取并集成(整合)。git pull命令的作用是:取回远程主机某个分支的更新,再与本地的指定分支合并,它的完整格式稍稍有点复杂。

使用语法

1
git pull [options] [<repository> [<refspec>…]]

描述

将远程存储库中的更改合并到当前分支中。在默认模式下,git pullgit fetch后跟git merge FETCH_HEAD的缩写。

更准确地说,git pull使用给定的参数运行git fetch,并调用git merge将检索到的分支头合并到当前分支中。 使用--rebase,它运行git rebase而不是git merge

示例

以下是一些示例 -

1
$ git pull <远程主机名> <远程分支名>:<本地分支名>

比如,要取回origin主机的next分支,与本地的master分支合并,需要写成下面这样 -

1
$ git pull origin next:master

如果远程分支(next)要与当前分支合并,则冒号后面的部分可以省略。上面命令可以简写为:

1
$ git pull origin next

上面命令表示,取回origin/next分支,再与当前分支合并。实质上,这等同于先做git fetch,再执行git merge

1
2
$ git fetch origin
$ git merge origin/next

在某些场合,Git会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。比如,在git clone的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,也就是说,本地的master分支自动”追踪”origin/master分支。

Git也允许手动建立追踪关系。

1
$ git branch --set-upstream master origin/next

上面命令指定master分支追踪origin/next分支。

如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

1
$ git pull origin

上面命令表示,本地的当前分支自动与对应的origin主机”追踪分支”(remote-tracking branch)进行合并。

如果当前分支只有一个追踪分支,连远程主机名都可以省略。

1
$ git pull

上面命令表示,当前分支自动与唯一一个追踪分支进行合并。

如果合并需要采用rebase模式,可以使用–rebase选项。

1
$ git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

git fetch和git pull的区别

  1. git fetch:相当于是从远程获取最新版本到本地,不会自动合并。
1
2
3
$ git fetch origin master
$ git log -p master..origin/master
$ git merge origin/master

以上命令的含义:

  • 首先从远程的originmaster主分支下载最新的版本到origin/master分支上
  • 然后比较本地的master分支和origin/master分支的差别
  • 最后进行合并

上述过程其实可以用以下更清晰的方式来进行:

1
2
3
$ git fetch origin master:tmp
$ git diff tmp
$ git merge tmp
  1. git pull:相当于是从远程获取最新版本并merge到本地
1
git pull origin master

上述命令其实相当于git fetchgit merge
在实际使用中,git fetch更安全一些,因为在merge前,我们可以查看更新情况,然后再决定是否合并。

Git push

git push命令用于将本地分支的更新,推送到远程主机。它的格式与git pull命令相似。

1
$ git push <远程主机名> <本地分支名>:<远程分支名>

使用语法

1
2
3
4
5
6
git push [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-v | --verbose]
[-u | --set-upstream] [--push-option=<string>]
[--[no-]signed|--sign=(true|false|if-asked)]
[--force-with-lease[=<refname>[:<expect>]]]
[--no-verify] [<repository> [<refspec>…]]

描述

使用本地引用更新远程引用,同时发送完成给定引用所需的对象。可以在每次推入存储库时,通过在那里设置挂钩触发一些事件。

当命令行不指定使用<repository>参数推送的位置时,将查询当前分支的branch.*.remote配置以确定要在哪里推送。 如果配置丢失,则默认为origin

示例

以下是一些示例 -

1
$ git push origin master

上面命令表示,将本地的master分支推送到origin主机的master分支。如果master不存在,则会被新建。

如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。

1
2
3
$ git push origin :master
# 等同于
$ git push origin --delete master

上面命令表示删除origin主机的master分支。如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。

1
$ git push origin

上面命令表示,将当前分支推送到origin主机的对应分支。如果当前分支只有一个追踪分支,那么主机名都可以省略。

1
$ git push

如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机,这样后面就可以不加任何参数使用git push

1
$ git push -u origin master

上面命令将本地的master分支推送到origin主机,同时指定origin为默认主机,后面就可以不加任何参数使用git push了。

不带任何参数的git push,默认只推送当前分支,这叫做simple方式。此外,还有一种matching方式,会推送所有有对应的远程分支的本地分支。Git 2.0版本之前,默认采用matching方法,现在改为默认采用simple方式。如果要修改这个设置,可以采用git config命令。

1
2
3
$ git config --global push.default matching
# 或者
$ git config --global push.default simple

还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用–all选项。

1
$ git push --all origin

上面命令表示,将所有本地分支都推送到origin主机。
如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做git pull合并差异,然后再推送到远程主机。这时,如果你一定要推送,可以使用–force选项。

1
$ git push --force origin

上面命令使用-–force选项,结果导致在远程主机产生一个”非直进式”的合并(non-fast-forward merge)。除非你很确定要这样做,否则应该尽量避免使用–-force选项。

最后,git push不会推送标签(tag),除非使用–tags选项。

1
$ git push origin --tags

将当前分支推送到远程的同名的简单方法,如下 -

1
$ git push origin HEAD

将当前分支推送到源存储库中的远程引用匹配主机。 这种形式方便推送当前分支,而不考虑其本地名称。如下 -

1
$ git push origin HEAD:master

其它示例

1.推送本地分支lbranch-1到新大远程分支rbranch-1

1
$ git push origin lbranch-1:refs/rbranch-1

2.推送lbranch-2到已有的rbranch-1,用于补充rbranch-1

1
2
3
$ git checkout lbranch-2
$ git rebase rbranch-1
$ git push origin lbranch-2:refs/rbranch-1

3.用本地分支lbranch-3覆盖远程分支rbranch-1

1
$ git push -f origin lbranch-2:refs/rbranch-1

或者 -

1
2
$ git push origin :refs/rbranch-1   //删除远程的rbranch-1分支
$ git push origin lbranch-1:refs/rbranch-1

4.查看push的结果

1
$ gitk rbranch-1

5.推送tag

1
$ git push origin tag_name

6.删除远程标签

1
$ git push origin :tag_name

Git rebase

git rebase命令在另一个分支基础之上重新应用,用于把一个分支的修改合并到当前分支。

使用语法

1
2
3
4
5
git rebase [-i | --interactive] [options] [--exec <cmd>] [--onto <newbase>]
[<upstream> [<branch>]]
git rebase [-i | --interactive] [options] [--exec <cmd>] [--onto <newbase>]
--root [<branch>]
git rebase --continue | --skip | --abort | --quit | --edit-todo

示例

假设你现在基于远程分支”origin“,创建一个叫”mywork“的分支。

1
$ git checkout -b mywork origin

结果如下所示 -

img

现在我们在这个分支(mywork)做一些修改,然后生成两个提交(commit).

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

但是与此同时,有些人也在”origin“分支上做了一些修改并且做了提交了,这就意味着”origin“和”mywork“这两个分支各自”前进”了,它们之间”分叉”了。

img

在这里,你可以用”pull“命令把”origin“分支上的修改拉下来并且和你的修改合并; 结果看起来就像一个新的”合并的提交”(merge commit):

img

但是,如果你想让”mywork“分支历史看起来像没有经过任何合并一样,也可以用 git rebase,如下所示:

1
2
$ git checkout mywork
$ git rebase origin

这些命令会把你的”mywork“分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到”.git/rebase“目录中),然后把”mywork“分支更新 到最新的”origin“分支,最后把保存的这些补丁应用到”mywork“分支上。

img

当’mywork‘分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。 如果运行垃圾收集命令(pruning garbage collection), 这些被丢弃的提交就会删除.

img

现在我们可以看一下用合并(merge)和用rebase所产生的历史的区别:

img

rebase的过程中,也许会出现冲突(conflict)。在这种情况,Git会停止rebase并会让你去解决冲突;在解决完冲突后,用”git add“命令去更新这些内容的索引(index), 然后,你无需执行 git commit,只要执行:

1
$ git rebase --continue

这样git会继续应用(apply)余下的补丁。

撤销操作git rebase –abort

在任何时候,可以用--abort参数来终止rebase的操作,并且”mywork“ 分支会回到rebase开始前的状态。

1
$ git rebase --abort

Git Commit

git commit –amend # 增补提交,会使用与当前提交节点相同的父节点进行一次新的提交,旧的提交将会被取消。

参考

  1. 易百教程