文章目录
1. 取得项目的Git仓库
有两种取得 Git 项目仓库的方法。第一种是在现存的目录下,通过导入所有文件来创建新的 Git 仓库。第二种是从已有的 Git 仓库克隆出一个新的镜像仓库来。
1.1 在工作目录中初始化新仓库
要对现有的某个项目开始用 Git 管理,只需到此项目所在的目录,执行:
$ git init
初始化后,在当前目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中。不过目前还没有开始跟踪管理项目中的任何一个文件。
也就是说,git init命令只是让code_test目录拥有了版本管理的能力,无论code_test目录中原来是否存在文件,".git"目录都是新创建出来的,从git仓库的角度来说,这就是一个新的仓库,还没有任何文件被这个仓库所管理。
需要注意
git init命令是把当前目录转化成git repo
git init repo_name 命令是在当前目录中创建一个以repo_name命名的新目录,新目录是一个git repo
如果当前目录下有几个文件想要纳入版本控制,需要先用 git add
命令告诉 Git 开始对这些文件进行跟踪,然后提交:
$ git add *.c
$ git add README
$ git commit -m 'initial project version'
1.2 从现有仓库克隆
如果想对某个开源项目出一份力,可以先把该项目的 Git 仓库复制一份出来,这就需要用到 git clone
命令。
克隆仓库的命令格式为 git clone [url]
。比如,要克隆 Ruby 语言的 Git 代码仓库 Grit,可以用下面的命令:
$ git clone git://github.com/schacon/grit.git
这会在当前目录下创建一个名为grit
的目录,其中包含一个 .git
的目录,用于保存下载下来的所有版本记录,然后从中取出最新版本的文件拷贝。如果进入这个新建的 grit
目录,你会看到项目中的所有文件已经在里边了,准备好后续的开发和使用。如果希望在克隆的时候,自己定义要新建的项目目录名称,可以在上面的命令末尾指定新的名字:
$ git clone git://github.com/schacon/grit.git mygrit
唯一的差别就是,现在新建的目录成了 mygrit
,其他的都和上边的一样。
Git 支持许多数据传输协议。之前的例子使用的是 git://
协议,不过你也可以用 http(s)://
或者 user@server:/path.git
表示的 SSH 传输协议。
2. 记录每次更新
请记住,工作目录下面的所有文件都不外乎这两种状态:已跟踪或未跟踪。已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未更新,已修改或者已放入暂存区。而所有其他文件都属于未跟踪文件。它们既没有上次更新时的快照,也不在当前的暂存区域。初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为未修改。
在编辑过某些文件之后,Git 将这些文件标为已修改。我们逐步把这些修改过的文件放到暂存区域,直到最后一次性提交所有这些暂存起来的文件,如此重复。
2.1 检查当前文件状态
要确定哪些文件当前处于什么状态,可以用 git status
命令。如果在克隆仓库之后立即执行此命令,会看到类似这样的输出:
$ git status
On branch master
nothing to commit, working directory clean
这说明你现在的工作目录相当干净。换句话说,所有已跟踪文件在上次提交后都未被更改过。此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪的新文件,否则 Git 会在这里列出来。最后,该命令还显示了当前所在的分支是 master
,这是默认的分支名称。
现在让我们用 vim 创建一个新文件 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
文件出现在**“Untracked files”**下面。未跟踪的文件意味着Git在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,除非你明明白白地告诉它“我需要跟踪该文件”,因而不用担心把临时文件什么的也归入版本管理。不过现在的例子中,我们确实想要跟踪管理 README 这个文件。
2.2 跟踪新文件
使用命令 git add
开始跟踪一个新文件。所以,要跟踪 README 文件,运行:
$ git add README
此时再运行 git status
命令,会看到 README 文件已被跟踪,并处于暂存状态:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
只要在 “Changes to be committed” 这行下面的,就说明是已暂存状态。如果此时提交,那么该文件此时此刻的版本将被留存在历史记录中。
2.3 暂存已修改文件
现在我们修改下之前已跟踪过的文件 benchmarks.rb
,然后再次运行 status
命令,会看到这样的状态报告:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: 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: benchmarks.rb
文件 benchmarks.rb
出现在 **“Changes not staged for commit” **这行下面,说明已跟踪文件的内容发生了变化,但还没有放到暂存区。要暂存这次更新,需要运行 git add
命令。
很多时候可以通过git status下的提示来进行下一步操作。
现在让我们运行 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
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: benchmarks.rb
怎么回事? benchmarks.rb
文件出现了两次!一次算未暂存,一次算已暂存,这怎么可能呢?好吧,实际上 Git 只不过暂存了你运行 git add
命令时的版本,如果现在提交,那么提交的是添加注释前的版本,而非当前工作目录中的版本。所以,运行了 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
2.4 查看已暂存和未暂存的更新
实际上 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
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: 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
不过是显示还没有暂存起来的改动,而不是这次工作和上次提交之间的差异。
2.5 提交更新
现在的暂存区域已经准备妥当可以提交了。在此之前,请一定要确认还有什么修改过的或新建的文件还没有 git add
过,否则提交的时候不会记录这些还没暂存起来的变化。所以,每次准备提交前,先用 git status
看下,是不是都已暂存起来了,然后再运行提交命令 git commit
:
$ git commit
这种方式会启动文本编辑器以便输入本次提交的说明。(默认会启用 shell 的环境变量 $EDITOR
所指定的软件,一般都是 vim 或 emacs。当然也可以按照第一章介绍的方式,使用 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:
# new file: README
# modified: benchmarks.rb
#
~
~
~
".git/COMMIT_EDITMSG" 10L, 283C
可以看到,默认的提交消息包含最后一次运行 git status
的输出,放在注释行里,另外开头还有一空行,供你输入提交说明。你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪些。(如果觉得这还不够,可以用 -v
选项将修改差异的每一行都包含到注释中来。)退出编辑器时,Git 会丢掉注释行,将说明内容和本次更新提交到仓库。
另外也可以用 -m 参数后跟提交说明的方式,在一行命令中提交更新:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed
2 files changed, 3 insertions(+)
create mode 100644 README
好,现在你已经创建了第一个提交!可以看到,提交后它会告诉你,当前是在哪个分支(master)提交的,本次提交的完整 SHA-1 校验和是什么(463dc4f
),以及在本次提交中,有多少文件修订过,多少行添改和删改过。
记住,提交时记录的是放在暂存区域的快照,任何还未暂存的仍然保持已修改状态,可以在下次提交时纳入版本管理。每一次运行提交操作,都是对你项目作一次快照,以后可以回到这个状态,或者进行比较。
3. 移除文件
要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。可以用 git rm
命令完成此项工作,并连带从工作目录中删除指定的文件。
如果只是简单地从工作目录中手工删除文件,运行 git status
时就会在 “Changes not staged for commit” 部分(也就是未暂存清单)看到:
$ rm grit.gemspec
$ git status
On branch 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: grit.gemspec
no changes added to commit (use "git add" and/or "git commit -a")
然后再运行 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
4. 文件改名
既然如此,当你看到 Git 的 mv
命令时一定会困惑不已。要在 Git 中对文件改名,可以这么做:
$ git mv file_from file_to
它会恰如预期般正常工作。实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:
$ git mv README.txt README
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.txt -> README
其实,运行 git mv
就相当于运行了下面三条命令:
$ mv README.txt README
$ git rm README.txt
$ git add README
5. 忽略某些文件(.gitignore)
一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。
不需要从头写.gitignore
文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
在版本管理的根目录下(与.Git文件夹同级)创建一个 .gitignore(没有文件名,在Windows下需要编写完文本文档后另存为,在win10系统下还是文本文档,只不过属性里能看到是文本文档(.gitignore文件)
文件 .gitignore
的格式规范如下:
- 所有空行或者以注释符号
#
开头的行都会被 Git 忽略。 - 可以使用标准的 glob 模式匹配。
- 匹配模式最后跟反斜杠(
/
)说明要忽略的是目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(
!
)取反。
很简单吧,被过滤掉的文件就不会出现在你的GitHub库中了,当然本地库中还有,只是push的时候不会上传。以及未追踪时,不会显示出未追踪状态。
想象一个场景:我们只需要管理/mtk/目录中的one.txt文件,这个目录中的其他文件都不需要管理。那么我们就需要使用:
- /mtk/
- !/mtk/one.txt
假设我们只有过滤规则没有添加规则,那么我们就需要把/mtk/目录下除了one.txt以外的所有文件都写出来!
删除了.gitignore文件后
之前的a.c文件又出现了未追踪状态。
有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore
忽略了:
$ git add App.class
The following paths are ignored by one of your .gitignore files:
App.class
Use -f if you really want to add them.
如果你确实想添加该文件,可以用-f
强制添加到Git:
$ git add -f App.class
或者你发现,可能是.gitignore
写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore
命令检查:
$ git check-ignore -v App.class
.gitignore:3:*.class App.class
Git会告诉我们,.gitignore
的第3行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。
最后需要强调的一点是,如果你不慎在创建.gitignore文件之前就push了项目,那么即使你在.gitignore文件中写入新的过滤规则,这些规则也不会起作用,Git仍然会对所有文件进行版本管理。
简单来说,出现这种问题的原因就是Git已经开始管理这些文件了,所以你无法再通过过滤规则过滤它们。
所以大家一定要养成在项目开始就创建.gitignore文件的习惯,否则一旦push,处理起来会非常麻烦。
.gitignore
文件本身要放到版本库里,并且可以对.gitignore
做版本管理!
6.查看提交历史
在提交了若干更新之后,又或者克隆了某个项目,想回顾下提交历史,可以使用 git log
命令查看。
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
git log
命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是append GPL
,上一次是add distributed
,最早的一次是wrote a readme file
。
每次更新都有一个 SHA-1 校验和、作者的名字和电子邮件地址、提交时间,最后缩进一个段落显示提交说明。
下面介绍常用的一些选项:
我们常用 -p
选项展开显示每次提交的内容差异,用 -2
则仅显示最近的两次更新
常用的 --pretty
选项,可以指定使用完全不同于默认格式的方式展示提交历史。比如用 oneline
将每个提交放在一行显示,这在提交数很大时非常有用。另外还有 short
,full
和 fuller
可以用,展示的信息或多或少有些不同
$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 changed the version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit
--stat
,仅显示简要的增改行数统计
format
,可以定制要显示的记录格式,这样的输出便于后期编程提取分析,像这样:
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 11 months ago : changed the version number
085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code
a11bef0 - Scott Chacon, 11 months ago : first commit
用 oneline 或 format 时结合 --graph
选项,可以看到开头多出一些 ASCII 字符串表示的简单图形,形象地展示了每个提交所在的分支及其分化衍合情况。在我们之前提到的 Grit 项目仓库中可以看到:
$ 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
7. 撤销操作
7.1 修改最后一次提交
在本地已有的 commit 信息上再次提交改动,而不是在已有的 commit 上再新增一个 commit。
使用 git commit --amend
命令可以达到在现有最新 commit 上再次提交改动的效果。
有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。想要撤消刚才的提交操作,可以使用 --amend
选项重新提交:
$ git commit --amend
此命令将使用当前的暂存区域快照提交。如果刚才提交完没有作任何改动,直接运行此命令的话,相当于有机会重新编辑提交说明,但将要提交的文件快照和之前的一样。
启动文本编辑器后,会看到上次提交时的说明,编辑它确认没问题后保存退出,就会使用新的提交说明覆盖刚才失误的提交。
如果刚才提交时忘了暂存某些修改,可以先补上暂存操作,然后再运行 --amend
提交:
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
上面的三条命令最终只是产生一个提交,第二个提交命令修正了第一个的提交内容。
7.2 取消已经暂存的文件
情景:文件add后想要取消暂存
来看下面的例子,有两个修改过的文件,我们想要分开提交,但不小心用 git add .
全加到了暂存区域。该如何撤消暂存其中的一个文件呢?其实,git status
的命令输出已经告诉了我们该怎么做:
$ git add .
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README.txt
modified: benchmarks.rb
就在 “Changes to be committed” 下面,括号中有提示,可以使用 git reset HEAD <file>...
的方式取消暂存。好吧,我们来试试取消暂存 benchmarks.rb 文件:
$ git reset HEAD benchmarks.rb
Unstaged changes after reset:
M benchmarks.rb
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README.txt
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: benchmarks.rb
现在 benchmarks.rb 文件又回到了之前已修改未暂存的状态。把暂存区的修改回退到工作区。
7.3 取消对文件的修改
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: benchmarks.rb
在第二个括号中,我们看到了抛弃文件修改的命令,让我们试试看:
$ git checkout -- benchmarks.rb
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: README.txt
可以看到,该文件已经恢复到修改前的版本。
命令git checkout -- readme.txt
意思就是,把readme.txt
文件在工作区的修改全部撤销,这里有两种情况:
-
一种是
readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态; -
一种是
readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit
或git add
时的状态。
8.版本回退
在Git中,我们用git log
命令查看:
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
一大串类似1094adb...
的是commit id
(版本号也称SHA-1校验和),和SVN不一样,Git的commit id
不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且每个人都不一样,以你自己的为准。(SHA-1校验和)为什么commit id
需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。
首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD
表示当前版本,也就是最新的提交1094adb...
(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^
比较容易数不过来,所以写成HEAD~100
。HEAD^对应 HEAD~1
实际使用命令时,可以指定版本号或者使用HEAD
实例:
$ git reset --soft HEAD~3 # 回退上上上一个版本
–hard 参数撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到上一次版本,并删除之前的所有信息提交:
现在,我们要把当前版本append GPL
回退到上一个版本add distributed
,就可以使用git reset
命令:
$ git reset --hard HEAD^
HEAD is now at e475afc add distributed
不过且慢,让我们用git log
再看看现在版本库的状态:
$ git log
commit e475afc93c209a690c39c13a46716e8fa000c366 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
最新的那个版本append GPL
已经看不到了!好比你从21世纪坐时光穿梭机来到了19世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个append GPL
的commit id
是1094adb...
,于是就可以指定回到未来的某个版本:如果之前命令行窗口被关掉的话,就还有一条命令,那就是"git reflog"命令,用于查看操作记录。
$ git reflog
aa81e63 (HEAD -> master) HEAD@{0}: reset: moving to aa81e63
526ff66 HEAD@{1}: commit: 5E
866b6de HEAD@{2}: commit: 4D
aa81e63 (HEAD -> master) HEAD@{3}: commit: 3C
2fc70fe HEAD@{4}: commit: 2B
510658e HEAD@{5}: commit (initial): 1A
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
git reset 分为三种:软 --soft,中 —mixed,硬 --hard 对应着三种回滚的程度,程度越硬,回滚的越“狠”
- –soft 已 add,但尚未 commit
- –mixed(git reset 的默认设定,可以省略不写),文件会回退到未 add(未暂存)的状态
- –hard 硬核,彻底,会彻底返回到回退前的版本状态,了无痕迹