3.Git 基础 (Git Basics)
如果你能只读一章就能使用Git,那么这章就是了。本章覆盖了你最终在使用Git过程中花费你最大时间所作的大部分事情需要的每个基本的命令。到本章结束后,你应该会配置和初始化一个库,开始和停止跟踪一个文件,筹划和提交更新。我们也会展示给你怎么设置使Git忽略一些特定的文件以及文件样式,怎么快速和方便地返回错误(undo mistakes),怎么浏览你的项目历史以及查看不同提交之间的更新,怎么从远端库获取以及上传。
3.1 获取一个Git库(Getting a Git Repository)
你可以使用两种方法来获取一个Git项目。第一种是获得一个现存的项目或目录并把它导入到Git中。第二种是从其它服务器上Clone一个已经存在的Git库。
3.1.1 在一个现存目录种初始化一个库(Initializing a Repository in an Existing Directory)
如果你正准备开始在Git中跟踪一个现存的项目,你需要到项目目录中输入以下命令:
$ git init
这会创建一个新的子目录称为.git,在这个目录中包含了所有你必需的库文件--一个Git库的框架。此时,还没有任何你项目中的东西被Git跟踪。(关于你刚创建的.git目录中包含了哪些文件的更详细信息,请参考第9章)。
如果你希望开始对现存文件(相对于一个空目录而言)进行版本控制,你可能需要开始跟踪这些文件并做一个初始化的提交。你可以通过以下命令达到该目的:几个git add 命令来指定你想跟踪的文件,紧接者使用一个commit:
$ git add *.c
$ git add README
$ git commit -m 'initial project version'
稍后我们将仔细查看一下这些命令都做了些什么。到此为止,你有了一个含有跟踪文件和一个初始提交的Git库。
3.1.2 Clone一个已经存在的库(Cloning an Existing Repository)
如果你想获得一个已存在的Git库的copy――例如,一个你想为之作出贡献的项目--那么,你需要git clone 命令。如果你熟悉其它的VCS系统诸如Subversion,你会注意到本命令是clone而不是checkout。这是一个重要的不同—Git接收一个服务器上几乎所有数据的一个Copy。当你运行git clone 命令时,项目历史中的每个文件的每个版本都会被从服务器上取下来。事实上,如果你的服务器的磁盘被毁坏了,你可以使用在任何客户端上的任何clone来恢复服务器的状态到它clone时的状态。(你可能会丢失一些服务器侧的钩子什么的,但所有版本化的数据都会在那里――查看第4章获取相关细节。)
你可以使用git clone [url]来clone 一个库。例如,如果你想clone Ruby 的Git库Grit,你可以如下做:
$ git clone git://github.com/schacon/grit.git
这会创建一个名为”grit”的目录,并在里面初始化一个.git目录,从库中获取所有的数据,检出一个最新版本的工作Copy。如果你进入这个新的grit目录,你会在此看到项目文件,你可以在上面工作或使用它。如果你想clone库到一个非grit的其它名字的目录,你可以用下面的可选参数来指定:
$ git clone git://github.com/schacon/grit.git mygrit
这个命令与前面那个命令所做的事情相同,只不过其目标目录被称为mygrit。
Git具有一系列不同的传输协议你可以使用。前面的例子使用了git://协议,但你可能也会看到http(s):// 或者使用ssh传输协议的user@server:/path.git。 第4章将会介绍所有可用的服务器设置、用来存取你的Git库的选项以及它们每一个的优点和缺点。
3.2 记录变更到库中(Recording Changes to the Repository)
你有了一个真实的Git库和一个该项目文件的检出(checkout)或工作copy。当每次该项目到达你希望的状态时,你需要做一些更改并提交这些更改的快照到你的库中。
请记得:在你工作目录中的每个文件都处于两种状态的一种:被跟踪的状态或未被跟踪的状态。被跟踪的文件是哪些位于最新快照中的文件。他们可以是未修改的,修改的或者缓存的。未被跟踪的文件则是另外的情况――所有位于你工作目录中而没有位于最新的快照以及缓存区的文件。当你初次clone一个库,你的所有文件将都是被跟踪以及未修改的,因为你刚刚检出它们还没有做任何编辑修改。
当你编辑文件时,Git会把它们看作是被修改状态,因为你已经在上次提交后修改了它们。你缓存这些修改的文件然后提交所有你缓存的变更,循环周而复始。这个生命周期图示在图2-1中。
3.2.1 检查你文件的状态(Checking the Status of Your Files)
用来了解哪个文件处于哪种状态的主要工具是git status 命令。如果你在clone后直接运行该命令,你应该看到如下所示:
$ git status
# On branch master
nothing to commit (working directory clean)
这意味者你有一个干净的工作目录――换句话说,目前没有跟踪的以及修改的文件。Git不会查看任何未被跟踪的文件,即使他们可以被列出。最后,这个命令会告诉你你在哪个分支上。现在,这个分支只是master,这是一个缺省的分支;在此你不必关心它。 下一章我们将会详细介绍分支以及参考。
现在,你需要增加一个新的文件到项目中:一个简单的README文件。如果这个文件之前不存在,当你运行 git status,你会看到你未跟踪的文件如下:
$ vim README
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)
你会看到你这个新的README文件是未被跟踪的,因为它位于你状态输出报告”未跟踪文件”标题下。 未被跟踪基本上意味者Git发现了一个文件,该文件在你上一次提交(快照)中不存在,Git不会自动包括这个文件到你的提交快照中除非你明确的告诉它这么做。它这么做是因为你不会意外地将哪些项目产生的二进制文件或其它你不希望包括的文件包括进去。你希望把README文件包括到项目中,那么,让我们来启动跟踪文件。
3.2.2 跟踪新文件(Tracking New Files)
为了开始跟踪一个新文件,你可以使用命令git add。为了开始跟踪README文件,你可以运行以下命令:
$ git add README
如果你重新运行状态命令,你会看到你的README文件现在已经被跟踪且缓存:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
你可以说它已经在缓存状态因为它位于”更新并准备提交”标题下。如果你此时提交,那么你运行git add时的那个文件版本将会进入到历史快照中。你可能还记得当你先前运行git init时, 你运行了git add 文件命令 – 这就是要开始跟踪你目录中的文件。Git add 命令可以接受一个路径名或者一个文件名;如果是一个目录,那么该命令将会递归式的将目录下的所有文件增加进来。
3.2.3 缓存已修改的文件(Staging Modified Files)
让我们来更新哪些已经被跟踪的文件。如果你更改了一个以前被跟踪的文件benchmarks.rb,然后你再次运行status命令,你会看到如下所示:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
文件benchmarks.rb出现在”更改但未更新(Changed but not updated)”段落下,这意味者这个被跟踪的文件已经在工作目录中被修改但没有被缓存(staged)。为了缓存它,你可以运行git add命令(这是一个多目的的命令:你使用它开始跟踪一个新文件,缓存文件,以及做诸如标记一个合并冲突的文件为已解决等的其它事情)。让我们现在运行git add 来缓存benchmarks.rb文件,然后再次运行git status:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
两个文件都被缓存并将会进入到你的下次提交中。此时,架设你记得在文件benchmarks.rb中有一个很小的更改你想在提交前改掉它。你再次打开它并做了更改,你准备好提交了,然而,让我们再次运行git status:
$ vim benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
那又怎样?现在benchmarks.rb被同时列到了staged和unstaged 中。这怎么可能?发生这种事情是因为Git 缓存一个文件仅仅在当你运行了git add命令时。如果你现在提交,那么你最后运行git add命令时的那个benchmarks.rb版本将会进入到提交中,而不是你提交时你的工作目录中的那个版本。如果你在git add 后修改了文件,你需要重新运行git add以便缓存最新版本的文件:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
3.2.4 忽略的文件(Ignoring Files)
通常情况下,你会有一些类型的文件你不希望Git自动将它添加或者甚至显示给你说它们未被跟踪。这些通常是一些自动产生的文件诸如日志文件或者是你的build系统产生的文件。在这种情况下,你可以创建一个文件样式的列表来匹配它们,它被称为.gitignore。以下是一个.gitignore文件的示例:
$ cat .gitignore
*.[oa]
*~
第一行告诉Git忽略任何以.o或.a结尾的文件--目标文件和归档文件,这可能是你编译源码时产生的。第二行告诉git 忽略所有以~结尾的文件,它被很多文本编辑器诸如Emacs来标记临时文件。你也可以包括log,tmp或者pid目录;自动产生的文档等等。在你开始工作之前设置好你的.gitignore文件通常是一个好的注意,它可以使你不至于意外的将一些你并不想提交的文件提交到Git库中。
你可以放在.gitignore文件中的样式规则如下:
空行以及以#起始的行被忽略
标准glob样式
你可以使用以/结尾的样式来指定一个目录
你可以以!开始来否定一个样式
Glob样式非常接近于shell使用的正则表达式。一个*匹配零个或多个字符; [abc]匹配任何位于方括号内的字符(本例为a,b或者c);?匹配一个单一的字符;方括号包围的以-分割的字符([0-9])匹配任何位于它们之间的字符(本例为0到9)。
以下是.gitignore文件的另一个示例:
# a comment - this is ignored
*.a # no .a files
!lib.a # but do track lib.a, even though you're ignoring .a files above
/TODO # only ignore the root TODO file, not subdir/TODO
build/ # ignore all files in the build/ directory
doc/*.txt # ignore doc/notes.txt, but not doc/server/arch.txt
3.2.5 查看你缓存及未缓存的变更(Viewing Your Staged and Unstaged Changes)
如果你觉得git status 太模糊了――你想确切的知道你所进行的更改,而不是哪些文件更改了――你可以使用git diff命令。稍后我们将更详细的介绍git diff;但你可能会经常使用它来回答两个问题:哪些是你已经修改但尚未缓存的?哪些是你已经缓存而准备提交的?尽管git status可以大体上回答这些问题,git diff可以展示给你确切的哪些行被添加或者取消--也就是补丁。
让我们来看一下你又编辑和缓存了README文件,然后编辑了benchmarks.rb文件但没有缓存它。如果你运行status命令,你可以看到如下所示:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
为了查看你已经做的而没有缓存的更改,输入不带参数的git diff命令:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
这个命令比较你工作目录中的内容和你缓存区中的内容。结果会告诉你你已经做的而还没有缓存的更改。
如果你想看一下你已经缓存的准备进入下一个提交的更改,你可以使用 git diff --cached命令。(Git 版本1.6.1及以后,你也可使用 git diff --staged 这可能会更易记忆)。这个命令比较你缓存的更改与你最后的提交:
$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README2
@@ -0,0 +1,5 @@
+grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+Grit is a Ruby library for extracting information from a Git repository
请注意:git diff自身不会显示自你上次提交以来的所有更改――它只显示哪些还未缓存的的更改。这可能会引起混淆,因为如果你已经缓存了你的所有更新,git diff将不会有输出。
另一个例子是,如果你缓存了benchmarks.rb文件然后又编辑了它,你可以使用git diff 来查看那些缓存了的更新以及未缓存的更新:
$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
# On branch master
#
# Changes to be committed:
#
# modified: benchmarks.rb
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
现在,你可以使用 git diff 来查看哪些还未缓存的更新:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
main()
##pp Grit::GitRuby.cache_client.stats
+# test line
用 git diff --cached 来查看那些目前为止已经缓存的更新:
$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
3.2.6 提交你的更新(Committing Your Changes)
现在,你的缓存区已经以你希望的方式建立起来了,你可以提交你的更新了。请记得:哪些任何仍未缓存的东西—任何你已经创建或修改的、自你编辑它们以来没有运行过git add 的文件—不会进入到本次提交中。它们仍然以修改的文件存在你的硬盘上。在这种情况下,你最后运行 git status,你看到所有的东西都已经缓存了,那么你就准备好提交你的更新了。最简单的提交方式是输入git commit命令:
$ git commit
这个命令会调用你选择的编辑器。(编辑器选择是根据你shell 的$EDITOR环境变量来设置的,通常是vim或者emacs,当然你可以如第1章所示通过使用git config –global core.editor命令来配置它为任何你希望使用的编辑器。)
编辑器显示如下文本(本例以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
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
~
~
~
".git/COMMIT_EDITMSG" 10L, 283C
你会看到缺省的提交消息包含了注释掉的最新的git status命令的输出以及一个位于顶部的空行。你可以移除这些注释并输入你的提交消息,你也可以把它们留在那里以便帮助你记得你正在提交些什么。(一个甚至更清除的提示可以告诉你修改了些什么,这可以通过传递-v选项给git commit。这么做会把你更改的不同(diff)放在编辑器中,这样你可以确切的看到你做了些什么。)当你退出编辑器时,Git以你的提交消息(剔除了注释和不同diff)创建你的提交。
作为可选,你也可以在commit 命令中嵌入你的提交消息,这可以通过在commit命令后指定一个-m标记,如下:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master]: created 463dc4f: "Fix benchmarks for speed"
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 README
现在,你已经创建了你的首次提交!你可以看到这个提交给出了一些关于本次提交的一些输出:你提交到了哪个分支(master),本次提交的SHA-1校验和(463dc4f),多少个文件被修改以及关于本次提交移除和增加行的统计信息。
请记住提交记录了你创建在缓存区内的一个快照。任何你没有缓存的东西仍然处于已修改状态;你可以再做一次提交到你的历史库中。每次在你执行一个提交时,你都是正在记录一个你项目的快照,这样你可以在以后恢复到这个快照或者与这个快照做比较。
3.2.7 跳过缓存区(Skipping the Staging Area)
尽管手工提交是如此的有用,你可以准确地知道你需要它们怎么做,但从你工作流程的需要上来看,缓存区还是有一点复杂。如果你想跳过缓存区,Git提供了一个简单的捷径。提供一个-a选项给git commit命令将使Git自动在提交前缓存每个跟踪的文件,这样可以使你跳过git add过程:
$ git status
# On branch master
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 files changed, 5 insertions(+), 0 deletions(-)
注意在本例中,怎么使你不需要提交前针对文件benchmarks.rb运行git add 命令。
3.2.8 移除文件(Removing Files)
为了从Git移除一个文件,你需要从你的跟踪文件中移除它(更精确的说,从你的缓存区移除它)然后提交。Git rm可以做到这一点,该命令同时从你的工作区移除文件这样下次你不会在你的未跟踪文件中看到它。
如果你仅仅简单地从你的工作目录中移除了这个文件,当你使用git status时,它会出现在”修改但未更新”标题下:
$ rm grit.gemspec
$ git status
# On branch master
#
# Changed but not updated:
# (use "git add/rm <file>..." to update what will be committed)
#
# deleted: grit.gemspec
#
那么,如果你运行了git rm,它会缓存文件的移除:
$ git rm grit.gemspec
rm 'grit.gemspec'
$ git status
# On branch master
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: grit.gemspec
#
在你下次提交时,文件会消失而不再被跟踪。如果你编辑了文件且已经把它加入到了索引中,你必需使用-f选项来强制移除它。这是一个安全的功能用来保护在快照中的数据不被意外的移除且不能被Git恢复。
另外一个你希望做的有用的事情是在你的工作树中保留文件但却从缓冲区中移除它。换句话说,你可能仍然想在你的硬盘上保留这个文件但不想让Git再跟踪它。这在当你忘了把什么东西增加到.gitignore文件中(例如一个大的日志文件或者一系列.a编译文件),而意外的把它加到了git库中时尤其有用。为了做到这一点,使用--cached 选项:
$ git rm --cached readme.txt
你可以传递文件,目录以及file-glob样式给git rm命令。这意味者你可以如此做:
$ git rm log/\*.log
注意位于*前面的\。它是必需的因为Git除了你shell的文件名扩展外,还有它自己的文件名扩展。本命令移除了所有位于log/目录下的.log扩展的文件。或者,你可以这么做:
$ git rm \*~
这个命令移除所有以~结尾的文件。
3.2.9 移动文件(Moving Files)
与其它许多VCS系统不同,Git不明确地跟踪文件的移动。如果你在Git中重新命名了一个文件,没有任何元数据会在Git中存储从而告诉你重命名了文件。然而,Git非常聪明,它可以根据事实运算而获知这一点--我们会在稍后介绍对文件移动的检测。
因此看来Git有一个mv命令似乎有一点不解。如果你想在git中对一个文件改名,你可以运行如下命令:
$ git mv file_from file_to
这个命令可以很好的工作。实际上,如果你这样运行了命令并查看了状态,你会发现Git会把它看作是一个重命名的文件:
$ git mv README.txt README
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# renamed: README.txt -> README
#
然而,这与运行以下命令是相当的:
$ mv README.txt README
$ git rm README.txt
$ git add README
Git计算出这是一个潜在的改名,所以这与你使用这种方法或者用mv命令没有关系。仅有的实际的不同是mv是一个命令而不是三个――这是一个非常便利的功能。更重要的是,你可以使用任何你喜欢的工作来对一个文件改名,并在之后提交之前处理add/rm。