二、版本控制
1. git clone
克隆已有仓库使用命令git clone <url>
。
比如,要克隆 Git 的链接库 libgit2
,可以用下面的命令:
$ git clone https://github.com/libgit2/libgit2
如果要在克隆远程仓库的时候,自定义本地仓库的名字,可以通过额外的参数指定新的目录名:
$ git clone https://github.com/libgit2/libgit2 mylibgit
这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit
。
Git 支持多种数据传输协议。 上面的例子使用的是 https://
协议,不过你也可以使用 git://
协议或者使用 SSH 传输协议,比如 user@server:path/to/repo.git
。 在服务器上搭建 Git 将会介绍所有这些协议在服务器端如何配置使用,以及各种方式之间的利弊。
默认情况下,git clone
命令会自动设置本地 master 分支跟踪克隆的远程仓库的 master
分支(或其它名字的默认分支)。
2. git add
git add
命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。
git add
命令是个多功能命令:
- 可以用它开始跟踪新文件;
- 或者把已跟踪的文件放到暂存区;
- 还能用于合并时把有冲突的文件标记为已解决状态等。
将git add
命令理解为“精确地将内容添加到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。
3. git status
git status
命令的输出十分详细,但其用语有些繁琐。 可以使用 git status -s
命令或 git status --short
命令,得到一种格式更为紧凑的输出。
$ git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
新添加的未跟踪文件前面有 ??
标记,新添加到暂存区中的文件前面有 A
标记,修改过的文件前面有 M
标记。
细究而言,输出中每一行的文件名前方有两栏,左栏指明了暂存区的状态,右栏指明了工作区的状态。例如,上面的状态报告显示: README
文件在工作区已修改但尚未暂存,而 lib/simplegit.rb
文件已修改且已暂存。 Rakefile
文件已修,暂存后又作了修改,因此该文件的修改中既有已暂存的部分,又有未暂存的部分。
4. .gitignore
一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore
的文件,列出要忽略的文件的模式。 来看一个实际的 .gitignore
例子:
$ cat .gitignore
*.[oa]
*~
第一行告诉 Git 忽略所有以 .o
或 .a
结尾的文件,一般这类对象文件和存档文件都是编译过程中出现的。 第二行告诉 Git 忽略所有名字以波浪符(~
)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。 此外,你可能还需要忽略 log,tmp 或者 pid 目录,以及自动生成的文档等等。
要养成一开始就为你的新仓库设置好 .gitignore
文件的习惯,以免将来误提交这类无用的文件。
文件 .gitignore
的格式规范如下:
- 所有空行或者以
#
开头的行都会被 Git 忽略。 - 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
- 匹配模式可以以(
/
)开头防止递归。 - 匹配模式可以以(
/
)结尾指定目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(
!
)取反。
所谓的 glob 模式是指 shell 所使用的简化了的正则表达式:
- 星号(
*
)匹配零个或多个任意字符; [abc]
匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);- 问号(
?
)只匹配一个任意字符; - 如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配(比如
[0-9]
表示匹配所有 0 到 9 的数字); - 使用两个星号(
**
)表示匹配任意中间目录,比如a/**/z
可以匹配a/z
、a/b/z
或a/b/c/z
等。
Tip:GitHub 有一个十分详细的针对数十种项目及语言的
.gitignore
文件列表, 你可以在 https://github.com/github/gitignore 找到它。
Note:在最简单的情况下,一个仓库可能只根目录下有一个 .gitignore
文件,它递归地应用到整个仓库中。 然而,子目录下也可以有额外的 .gitignore
文件。子目录中的 .gitignore
文件中的规则只作用于它所在的目录中。 (Linux 内核的源码库拥有 206 个 .gitignore
文件。)
多个
.gitignore
文件的具体细节超出了本书的范围,更多详情见man gitignore
。
5. git diff
要查看尚未暂存的文件更新了哪些部分,不加参数直接输入 git diff
:
$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
Please include a nice description of your changes when you submit your PR;
if we have to read the whole diff to figure out why you're contributing
in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.
If you are starting to work on a particular area, feel free to submit a PR
that highlights your work in progress (and note in the PR title that it's
此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。
--staged/--cached 选项
若要查看已暂存文件与最后一次 commit 的文件差异,可以用 git diff --staged
命令:
$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project
git diff 的参数 --staged 与--cached 作用相同,它们是同义词。
6. git commit
直接运行 git commit
命令会启动你选择的文本编辑器来输入提交说明,编辑器会显示类似下面的文本信息(本例选用 Vim 的屏显方式展示):
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
# new file: README
# modified: CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C
可以看到,默认的提交消息包含最后一次运行 git status
的输出,放在注释行里,另外开头还有一个空行,供你输入提交说明。 你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪些。
退出编辑器时,Git 会丢弃注释行,用你输入的提交说明生成一次提交。
-m 选项
另外,你也可以在 commit
命令后添加 -m
选项,将提交信息与命令放在同一行,如下所示:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed
2 files changed, 2 insertions(+)
create mode 100644 README
好,现在你已经创建了第一个提交! 可以看到,提交后它会告诉你,当前是在哪个分支(master
)提交的,本次提交的完整 SHA-1 校验和是什么(463dc4f
),以及在本次提交中,有多少文件修订过,多少行添加和删改过。
-a 选项
尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。 Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit
加上 -a
选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add
步骤:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 file changed, 5 insertions(+), 0 deletions(-)
看到了吗?提交之前不再需要 git add
文件“CONTRIBUTING.md”了。 这是因为 -a
选项使本次提交包含了所有修改过的文件。
-a
选项很方便,但是要小心,有时这个选项会将不需要的文件添加到提交中。
--amend 选项
有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend
选项的提交命令来重新提交:
$ git commit --amend
既然是 commit 命令,该命令当然也会将暂存区中的文件提交。 但如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,而你所修改的只是提交信息。
此时文本编辑器启动后,可以看到之前的提交信息,编辑后保存会覆盖原来的提交信息。
例如,你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作:
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
最终你只会有一个提交——第二次提交将代替第一次提交的结果。
当你在修补最后的提交时,并不是通过用改进后的提交 原位替换 掉旧有提交的方式来修复的, 理解这一点非常重要。从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。
修补提交最明显的价值是可以稍微改进你最后的提交,而不会让“啊,忘了添加一个文件”或者 “小修补,修正笔误”这种提交信息弄乱你的仓库历史。
7. git rm
如果只是简单地从工作目录中手工删除文件,运行 git status
时就会在 “Changes not staged for commit” 部分(也就是 未暂存清单)看到:
$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: PROJECTS.md
no changes added to commit (use "git add" and/or "git commit -a")
然后再运行 git rm
记录此次移除文件的操作:
$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: PROJECTS.md
git rm
命令后面可以列出文件或者目录的名字,也可以使用 glob
模式。比如:
$ git rm log/\*.log
-f 选项
如果要删除之前修改过并且已经放到暂存区的文件,则必须使用强制删除选项 -f
(这是一种安全特性,用于防止误删尚未 commit 的数据,因为这样的数据不能被 Git 恢复)。
--cached 选项
如果想把文件从暂存区移除,但仍希望保留在当前工作目录中, 换句话说,只是删除 Git 对该文件的跟踪, 使用 --cached
选项即可。
例如当你忘记添加 .gitignore
文件,不小心把一个很大的日志文件或一堆 .a
这样的编译生成文件添加到暂存区时,这一做法尤其有用:
$ git rm --cached README
8. git mv
不像其它的 VCS 系统,Git 并不显式跟踪文件移动操作。 如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。 不过 Git 非常聪明,它会推断出究竟发生了什么(因而,当你看到 Git 的 mv
命令时一定会困惑不已)。
要在 Git 中对文件改名,可以这么做:
$ git mv file_from file_to
它会恰如预期般正常工作。实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:
$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
然而,运行 git mv
实际上相当于运行了下面三条命令:
$ mv README.md README
$ git rm README.md
$ git add README
即便像上面这样分开操作,Git 也会意识到这是一次重命名,所以不管何种方式结果都一样。
两者唯一的区别是,mv
是一条命令而非三条命令,直接用 git mv
方便得多。
9. git log <filename>
我们使用一个非常简单的 “simplegit” 项目作为示例。 运行下面的命令获取该项目:
$ git clone https://github.com/schacon/simplegit-progit
当你在此项目中运行 git log
命令时,可以看到下面的输出:
$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
若要查看某个文件的历史,使用git log filename
命令。
不传入任何参数的默认情况下,git log
会按时间先后顺序列出所有的提交,最近的更新排在最上面。
正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和(版本号,commit id)、作者的名字和电子邮件地址、提交时间以及提交说明。
git log
有许多选项可以帮助你搜寻你所要找的提交, 下面我们会介绍几个最常用的选项。
-p 选项
-p
或 --patch
选项会显示每次提交所引入的差异,其按 补丁 的格式输出。
同时也可以限制显示的日志条目数量,例如使用 -2
选项来只显示最近的两次提交:
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "simplegit"
- s.version = "0.1.0"
+ s.version = "0.1.1"
s.author = "Scott Chacon"
s.email = "schacon@gee-mail.com"
s.summary = "A simple gem for using Git in Ruby code."
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index a0a60ae..47c6340 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -18,8 +18,3 @@ class SimpleGit
end
end
-
-if $0 == __FILE__
- git = SimpleGit.new
- puts git.show
-end
--stat 选项
你也可以为 git log
附带一系列的总结性选项。 比如你想看到每次提交的简略统计信息,可以使用 --stat
选项:
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
Rakefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test
lib/simplegit.rb | 5 -----
1 file changed, 5 deletions(-)
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
README | 6 ++++++
Rakefile | 23 +++++++++++++++++++++++
lib/simplegit.rb | 25 +++++++++++++++++++++++++
3 files changed, 54 insertions(+)
如上,--stat
选项在每次提交的下面列出了所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了,此外在每次提交的最后还有一个总结。
--pretty 选项
其使用不同于默认格式的方式展示提交历史。该选项还有一些内建的子选项, 比如
oneline
会将每个提交放在一行显示,在浏览大量的提交时非常有用;$ git log --pretty=oneline ca82a6dff817ec66f44342007202690a93763949 changed the version number 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test a11bef06a3f659402fe7563abf99ad00de2209e6 first commit
另外还有
short
,full
和fuller
选项,它们展示信息的格式基本一致,但是详尽程度不一。最有意思的是
format
,可以定制记录的显示格式。 这样的输出对后期提取分析格外有用——因为你知道输出的格式不会随着 Git 的更新而发生改变:$ git log --pretty=format:"%h - %an, %ar : %s" ca82a6d - Scott Chacon, 6 years ago : changed the version number 085bb3b - Scott Chacon, 6 years ago : removed unnecessary test a11bef0 - Scott Chacon, 6 years ago : first commit
下面的
git log --pretty=format
常用的选项 列出了format
接受的常用格式占位符的写法及其代表的意义。选项 说明 %H
提交的完整哈希值 %h
提交的简写哈希值 %T
树的完整哈希值 %t
树的简写哈希值 %P
父提交的完整哈希值 %p
父提交的简写哈希值 %an
作者名字 %ae
作者的电子邮件地址 %ad
作者修订日期(可以用 --date=选项 来定制格式) %ar
作者修订日期,按多久以前的方式显示 %cn
提交者的名字 %ce
提交者的电子邮件地址 %cd
提交日期 %cr
提交日期(距今多长时间) %s
提交说明
当 oneline
或 format
与另一个 log
选项 --graph
结合使用时尤其有用。 这个选项添加了一些 ASCII 字符串来形象地展示你的分支、合并历史:
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
* 11d191e Merge branch 'defunkt' into local
总结——git log
的常用选项:
选项 | 说明 |
---|---|
-p | 按补丁格式显示每个提交引入的差异。 |
--stat | 显示每次提交的文件修改统计信息。 |
--shortstat | 只显示 --stat 中最后的行数修改添加移除统计。 |
--name-only | 仅在提交信息后显示已修改的文件清单。 |
--name-status | 显示新增、修改、删除的文件清单。 |
--abbrev-commit | 仅显示 SHA-1 校验和所有 40 个字符中的前几个字符。 |
--relative-date | 使用较短的相对时间而不是完整格式显示日期(比如“2 weeks ago”)。 |
--graph | 在日志旁以 ASCII 图形显示分支与合并历史。 |
--pretty | 使用其他格式显示历史提交信息。可用的选项包括 oneline、short、full、fuller 和 format(用来定义自己的格式)。 |
--oneline | --pretty=oneline --abbrev-commit 合用的简写。 |
还有一些内容......
10. git reflog
git reflog 命令可用来查看命令历史,可借此确定要回到未来的哪个版本。
11. 撤销操作
记住,在 Git 中任何 已提交 的东西几乎总是可以恢复的。 甚至那些被删除的分支中的提交或使用
--amend
选项覆盖的提交也可以恢复 (阅读 数据恢复 了解数据恢复)。然而,任何你未提交的东西丢失后很可能再也找不到了。
11.1 清除未追踪:git clean
如果想要删除所有未被 git 跟踪过的文件(untracked),你当然可以手动在工作区进行删除,但也可以使用 git clean 命令:
删除未被跟踪的文件:
git clean -f
同时删除未被跟踪的文件和目录:
git clean -fd
连 .gitignore 中涉及的 untracked 文件/目录也一起删掉(慎用):
git clean -xfd
注意,在用上述 git clean 之前,墙裂建议加上 -n 参数来先看看会删掉哪些文件,防止重要文件被误删:
git clean -nxfd git clean -nf git clean -nfd
11.2 撤销已修改:git checkout -- <file>
如果你并不想保留对 CONTRIBUTING.md
文件的修改怎么办? 即希望将它还原成上次提交时的样子。其实在git status
的命令输出中已经告诉了你应该如何做,比如在最后一个例子中,未暂存区域是这样:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
它非常清楚地告诉了你如何撤消之前所做的修改,使用git checkout -- <file>
即可。比如:
$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
可以看到那些修改已经被撤消了。
请务必记得
git checkout -- <file>
是一个危险的命令——你对那个文件在本地的任何修改都会消失,Git 会用最近提交的版本覆盖掉它。 除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。
11.3 撤销已暂存:git reset HEAD <file>
使用 git reset HEAD <file>
来取消暂存。 例如,我们可以这样来取消暂存 CONTRIBUTING.md
文件(这样会使得CONTRIBUTING.md
文件成为修改但未暂存的状态):
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
git reset
确实是个危险的命令,如果加上了--hard
选项则更是如此。 然而在上述场景中,工作目录中的文件尚未修改,因此相对安全一些。
11.4 撤销已提交
11.4.1 git reset
git reset [<mode>] [<commitId>]
注意这条命令的作用范围是整个目录的所有文件,不能针对单个文件。
- mode
- --soft:只回退版本仓库,不会动暂存区与工作区
- -mixed(默认):回退版本仓库与暂存区,不会动工作区(暂存区的修改会返回到工作区)
- --hard:版本仓库、暂存区、工作区全部回退,所有的修改都会消失
- ......
- commitId:如果不填,默认为上一次提交,即 HEAD。
在 Git 中,HEAD 表示当前版本,HEAD表示上一个版本,HEAD^表示上上个版本,依次类推,当数量比较多时,可以用 HEAD~100 表示。
git reset <file>
重置暂存区的指定文件,将其回退到工作区。
与
git checkout -- <file>
作用完全相同???先尽着 checkout 用吧。
11.4.2 git revert
该命令的原理是,在当前提交后面,新增一次提交,抵消掉上一次提交导致的所有变化。它不会改变过去的历史,所以是首选方式,没有任何丢失代码的风险。
以后再说吧......
11.4.3 git restore???
11.5 撤销远程仓库
其实正规来说,远程仓库的修改是不允许被撤销的,这里的撤销其实是一个危险的动作,因为它需要使用分支的强制推送(更建议的是在本地添加新的修改,在新的修改中把文件恢复到过去的状态,然后向远端推送)。
此时撤销操作可如下:
git reset --soft <commitId>
:在本地回到老版本git push origin branchName --force
:强制推送当前版本内容以覆盖远程分支
12. git stash
git stash
能够将所有未提交的修改——工作区和暂存区——贮藏(即保存)到一个栈上,用于后续在任何时候重新应用这些改动(甚至在不同的分支上)。
例如,你现在的 git 仓库是这样的:
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
git stash 或 git stash push
现在想要切换分支,但是还不想要提交之前的工作;所以贮藏修改。 将新的贮藏推送到栈上,运行 git stash
或 git stash push
:
git stash push
可添加注释。
$ git stash
Saved working directory and index state \
"WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")
此时工作目录是干净的了:
$ git status
# On branch master
nothing to commit, working directory clean
在老版本中,git stash push 命令写作 git stash save,该命令已被弃用。
-u 选项
默认情况下,git stash
不会贮藏 未跟踪 的文件,可以通过指定 --include-untracked
或 -u
选项来命令 Git 同时贮藏任何未跟踪文件。
$ git status -s
M index.html
M lib/simplegit.rb
?? new-file.txt
$ git stash -u
Saved working directory and index state WIP on master: 1b65b17 added the index file
HEAD is now at 1b65b17 added the index file
$ git status -s
$
-a 选项
然而,如果此时这些未跟踪的文件已被添加到 .gitignore 文件中,Git 同样不会贮藏它们,你在使用 -u 选项的同时还需额外指定--all
或-a
选项。
--patch 选项(不常用)
如果指定了 --patch
标记,Git 不会贮藏所有修改过的任何东西, 但是会交互式地提示哪些改动想要贮藏、哪些改动需要保存在工作目录中。
$ git stash --patch
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index 66d332e..8bb5674 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb
@@ -16,6 +16,10 @@ class SimpleGit
return `#{git_cmd} 2>&1`.chomp
end
end
+
+ def show(treeish = 'master')
+ command("git show #{treeish}")
+ end
end
test
Stash this hunk [y,n,q,a,d,/,e,?]? y
Saved working directory and index state WIP on master: 1b65b17 added the index file
git stash list
然后,你可以切换分支并在其他地方工作,而你之前的修改被存储在栈上。若要查看贮藏的东西,可以使用 git stash list
:
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log
在本例中,有两个之前的贮藏,所以你接触到了三个不同的贮藏工作。
git stash apply
可以通过git stash apply
命令将你刚刚贮藏的工作重新应用,而如果想要应用其中一个更旧的贮藏,可以通过名字指定它:git stash apply stash@{2}
。
$ git stash apply
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.html
modified: lib/simplegit.rb
no changes added to commit (use "git add" and/or "git commit -a")
--index 选项
此时 Git 重新修改了当你保存贮藏时撤销的文件。但是要注意的是虽然文件的修改被重新应用了,但是之前暂存的文件却没有被重新暂存,需要那样的话,必须使用 --index
选项来运行 git stash apply
命令:
$ git stash apply --index
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
git stash drop
此后,如果需要删除贮藏栈上的保存的某个贮藏,可以使用 git stash drop
命令:
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log
$ git stash drop stash@{0}
Dropped stash@{0} (364e91f3f268f0900bc3ee613f9f733e82aaed43)
git stash pop
为了方便——将git stash apply
与git stash drop
融为一体,也可以运行 git stash pop
来恢复贮藏后立即从栈上扔掉它。
git stash branch <newBranch>
如果你重新应用贮藏时发生了一些冲突而不得不解决,此时若想以一个轻松的方式来再次测试贮藏的改动的话,可以运行 git stash branch <newBranch>
命令以你指定的分支名创建一个新分支,并检出贮藏工作时所在的提交,同时重新在那应用你贮藏的工作,最后会丢弃贮藏:
$ git stash branch testchanges
M index.html
M lib/simplegit.rb
Switched to a new branch 'testchanges'
On branch testchanges
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
Dropped refs/stash@{0} (29d385a81d163dfd45a452a2ce816487a6b8b014)
这是在新分支轻松恢复贮藏工作并继续工作的一个很不错的途径。
13. git checkout
git checkout
命令的作用其实比较多,这里主要阐释其恢复历史版本的功能。
git checkout <commitId>
:将整个仓库的状态检出为某个历史版本git checkout <commitId> <filename>
:将某个文件检出为某个历史版本
分离头指针
在检出某个历史版本的情况下,git 处于“分离头指针”的情况。
在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。 因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支:
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
如果在这之后又进行了一次提交,version2
分支就会因为这个改动向前移动, 此时它就会和 v2.0.0
标签稍微有些不同,这时就要当心了。
14. git 别名
Git 并不会在你输入部分命令时自动推断出你想要的命令,如果不想每次都输入完整的 Git 命令,可以通过 git config
文件来轻松地为每一个命令设置一个别名,比如:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
这意味着,当要输入 git commit
时,只需要输入 git ci
。 随着你继续不断地使用 Git,可能也会经常使用其他命令,所以创建别名时不需要犹豫。
Git 别名中两个可能比较常用的例子:
为了解决取消暂存文件的易用性问题,可以向 Git 中添加你自己的取消暂存别名:
$ git config --global alias.unstage 'reset HEAD --'
这会使下面的两个命令等价:
$ git unstage fileA $ git reset HEAD -- fileA
通常也会添加一个
last
命令,像这样:$ git config --global alias.last 'log -1 HEAD'
这样,可以轻松地看到最后一次提交:
$ git last commit 66938dae3329c7aebe598c2246a8e6af90d04646 Author: Josh Goebel <dreamer3@example.com> Date: Tue Aug 26 19:48:51 2008 +0800 test for current head Signed-off-by: Scott Chacon <schacon@example.com>
可以看出,Git 只是简单地将别名替换为对应的命令。
有时候,你想要定义的别名可能执行的是不属于 Git 的外部命令,在相应命令前面加入 !
符号并使用引号即可。 例如,我们现在演示将 git visual
定义为 gitk
的别名:
$ git config --global alias.visual '!gitk'
15. git submodule
面对比较复杂的项目,我们有可能会将代码根据功能拆解成不同的子模块——主项目对子模块有依赖关系,却又并不关心子模块的内部开发流程细节。这种情况下,通常不会把所有源码都放在同一个 Git 仓库中。
有一种比较简单的方式,是在当前工作目录下,将子模块文件夹加入到 .gitignore
文件内容中,这样主项目就能够无视子项目的存在。这样做有一个弊端就是,使用主项目的人需要有一个先验知识:需要在当前目录下放置一份某版本的子模块代码。还有另外一种方式可供借鉴,可以使用 Git 的 submodule
功能,
使用子模块
git clone --recurse-submodules <repo-url>
:在克隆仓库时会自动初始化并更新仓库中的每一个子模块, 包括可能存在的嵌套子模块。git submodule init
:用来初始化本地配置文件。当克隆含子模块的项目时,默认会包含该子模块目录但其为空,因此使用此条命令初始化。
git submodule update
:从子项目中抓取所有数据并检出父项目中列出的合适的提交git submodule update --init
:相当于git submodule init
和git submodule update
的统一体git submodule sync
:如果修改了本地 .gitmodules 文件中子仓库的 url,可使用此命令将新的 url 同步到本地配置中。
子模块好像脱离了 git 的分支管理功能?
删除子模块
# 下条在 .git/config 中删除了相关配置
$ git submodule deinit path-to-submodule
# 删除了子模块文件夹,并自动在 .gitmodules 中删除相关配置
$ git rm path-to-submodule
# 提交——此条命令可后置
$ git commit -m "Remove submodule"
# 删除残余(缓存?数据库?)——这里的操作不会被 git 版本控制系统监听到
$ rm -rf .git/modules/path-to-submodule