【Git】基础

本节内容主要演示如何配置并初始化一个仓库(repository)、开始或停止跟踪(track)文件、暂存(stage)或提交(commit)更改。

本节也将向你演示如何配置 Git 来忽略指定的文件和文件模式、如何迅速而简单地撤销错误操作、如何浏览你的项目的历史版本以及不同提交(commits)间的差异、如何向你的远程仓库推送(push)以及如何从你的远程仓库拉取(pull)文件。

1 获取仓库

有两种取得 Git 项目仓库的方法。 第一种是在现有项目或目录下导入所有文件到 Git 中;第二种是从一个服务器克隆一个现有的 Git 仓库。

1.1 在现有目录中初始化仓库

在一个本地的文件夹里,使用以下命令会创建一个仓库:

1
git init

该命令将创建一个名为 .git 的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。

但是,在这个时候,我们仅仅是做了一个初始化的操作,你的项目里的文件还没有被跟踪(如果有文件的话)。这时我们还需要把未跟踪的文件加入进来

1
git add <filename>

<filename>可以是明确指定的一个文件,也可以使用通配符,下面两个都是合法的使用方式:

1
2
3
4
5
6
# 把Demo.txt加入进来
git add demo1.txt
# 把所有txt文本加入进来
git add *.txt
# 把所有文件加进来
git add .

不过现在我们只是把文件变成以追踪的状态,但是文件本身的改动还没有提交。使用 commit 命令可以提交变动,它有一个参数 -m 后面跟的是提交时说明信息:

1
git commit -m "Inital Project Version"

1.2 克隆现有的仓库

如果你想获得一份已经存在了的 Git 仓库的拷贝,比如说,你想为某个开源项目贡献自己的一份力,这时就要用到 git clone 命令。命令格式是 git clone [url] :

1
git clone https://github.com/libgit2/libgit2

这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹,从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。如果你想在克隆远程仓库的时候,自定义本地仓库的名字,你可以使用如下命令:

1
git clone https://github.com/libgit2/libgit2 mylibgit

这将执行与上一个命令相同的操作,不过在本地创建的仓库名字变为 mylibgit

2 跟踪文件

现在我们已经有了一个Git仓库,并且里面有很多的文件。我们的任务就是跟踪文件的变化,并提交到仓库中

工作目录下的每一个文件都不外乎这两种状态

  • 已跟踪

    是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能处于未修改已修改已放入暂存区

    初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态。

  • 未跟踪
    工作目录中除已跟踪文件以外的所有其它文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。

编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 我们逐步将这些修改过的文件放入暂存区,然后提交所有暂存了的修改,如此反复。所以使用 Git 时文件的生命周期如下:

2.1 查看文件状态

要查看哪些文件处于什么状态,可以用 git status 命令。如果所有已跟踪文件在上次提交后都未被更改过,会看到类似这样的输出:

1
$ git status
On branch master
nothing to commit, working tree clean

如果我们添加一个新的文件,那么再次使用 status 命令时输出就不同了:

1
2
3
4
5
6
7
8
9
$ ls -al > DirInfo
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)

DirInfo

nothing added to commit but untracked files present (use "git add" to track)

这句话告诉我们出现了一个未跟踪的文件"DirInfo"。未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,除非你明明白白地告诉它“我需要跟踪该文件”。

2.2 跟踪新的文件

和之前一样,我们使用命令 git add 开始跟踪一个文件(并把它放入暂存区中)运行:

1
2
3
4
5
6
7
$ git add DirInfo
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: DirInfo

Changes to be committed 这行下面的,就说明是已暂存状态。如果此时提交(commit),那么该文件此时此刻的版本将被留存在历史记录中。

git add 命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件

2.3 暂存已修改的文件

现在我们来修改一个已被跟踪的文件,加入我们当前的仓库里的文件结构如下:

1
2
3
4
5
6
7
$ ls -al
total 6
drwxr-xr-x 1 zuoyiping 197609 0 Sep 11 10:37 ./
drwxr-xr-x 1 zuoyiping 197609 0 Sep 11 10:19 ../
drwxr-xr-x 1 zuoyiping 197609 0 Sep 11 10:34 .git/
-rw-r--r-- 1 zuoyiping 197609 309 Sep 11 10:27 DirInfo
-rw-r--r-- 1 zuoyiping 197609 45 Sep 11 10:37 demo1.txt

我们现在修改以下demo1.txt的内容,再看一下文件的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ vim demo1.txt
# 修改完了以后
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: DirInfo

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: demo1.txt

文件 demo1.txt 出现在 Changes not staged for commit 这行下面,说明已跟踪文件的内容发生了变化,但还没有放到暂存区。要暂存这次更新,需要运行 git add 命令

这里我们发现add是个多功能命令,它可以:

  • 开始跟踪新文件

  • 把已跟踪的文件放到暂存区

  • 用于合并时把有冲突的文件标记为已解决状态

  • 其他等等

将这个命令理解为“添加内容到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。

1
2
3
4
5
6
7
8
$ git add demo1.txt
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: DirInfo
modified: demo1.txt

现在两个文件都已暂存,下次提交(commit)时就会一并记录到仓库中。

另外add还有几个小窍门:

  • git add -A 提交所有变化

  • git add -u 提交被修改(modified)和被删除(deleted)文件,不包括新文件(new)

  • git add . 提交新文件(new)和被修改(modified)文件,不包括被删除(deleted)文件

2.4 关于提交的一点小问题

现在我们总结一下像把一个文件提交到仓库里需要的步骤:

  1. add到暂存区里
  2. commit到仓库里

发现了吗?是先addcommit,下面说的很重要,请记住:

Git 只会暂存了你运行 git add 命令时的版本,当运行commit命令时提交的版本是你最后一次运行 git add 命令时的那个版本,而不是你运行 git commit 时,在工作目录中的当前版本。所以,运行了 git add 之后又作了修订的文件,需要重新运行 git add 把最新版本重新暂存起来

举个栗子,我们现在再次修改一下demo1.txt的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ truncate -s 0 demo1.txt
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: DirInfo
modified: demo1.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: demo1.txt

怎么回事?现在 demo1.txt 文件同时出现在暂存区和非暂存区。这怎么可能呢?

好吧,确实有可能,因为这两个demo1.txt根本就不是一个!前面一个是之前add以后一直存在的文件,而后面那个才是我们刚刚那修改过的在工作区的文件。如果你确实希望将修改过后的demo1.txt提交,那就需要先addcommit:

1
2
3
4
5
6
7
8
$ git add demo1.txt
$ git commit -m "New Test"
[master 7b560a8] New Test
2 files changed, 6 insertions(+), 1 deletion(-)
create mode 100644 DirInfo
$ git status
On branch master
nothing to commit, working tree clean

2.5 忽略文件

在使用Git的过程中,我们喜欢有的文件比如日志,临时文件,编译的中间文件等不要提交到代码仓库,这时就要设置相应的忽略规则,来忽略这些文件的提交。

在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。

  • 星号(*)匹配零个或多个任意字符
  • [abc] 匹配任何一个列在方括号中的字符。使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配,比如 [0-9] 表示匹配所有 0 到 9 的数字
  • 问号(?)只匹配一个任意字符
  • 使用两个星号(*) 表示匹配任意中间目录,比如a/**/z 可以匹配 a/z, a/b/za/b/c/z等。

2.6 查看已暂存和未暂存的修改

如果你想知道具体修改了什么地方,可以用 git diff 命令,你可能通常会用它来回答这两个问题:当前做的哪些更新还没有暂存?有哪些更新已经暂存起来准备好了下次提交?

现在我们先做一下前期准备工作:

  • 修改一下DirInfo的内容,但是不暂存
  • 修改以下demo1.txt的内容并且暂存
1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: demo1.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: DirInfo

使用git diff比较的是工作目录中当前文件和暂存区域快照之间的差异, 也就是修改之后还没有暂存起来的变化内容

若要查看已暂存的将要添加到下次提交里的内容,可以用 git diff --cached 命令。(Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的,但更好记些。)

2.7 提交更新

建议每次准备提交前,先用 git status 看下,是不是都已暂存起来了, 然后再运行提交命令 git commit

如果不带参数-m

1
$ git commit

这种方式会启动文本编辑器以便输入本次提交的说明。默认会启用 shell 的环境变量 $EDITOR 所指定的软件,一般都是 vim 或 emacs。可以使用 git config --global core.editor 命令设定你喜欢的编辑软件。

1
2
3
$ git commit
[master 01b34e7] This is a simple test!
2 files changed, 1 insertion(+), 4 deletions(-)

提交之后的信息会提醒你在那个分值(master)提交的,本次提交的完整 SHA-1 校验和是什么(01b34e7),以及在本次提交中,有多少文件修订过,多少行添加和删改过。

请记住,提交时记录的是放在暂存区域的快照。任何还未暂存的仍然保持已修改状态,可以在下次提交时纳入版本管理。 每一次运行提交操作,都是对你项目作一次快照,以后可以回到这个状态,或者进行比较。

当然,尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。Git 提供了一个跳过使用暂存区域的方式,只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤

2.8 删除文件

删除文件?直接使用rm不就行了吗?不,事实上没有那么简单。用 rm 来删除文件,仅仅是删除了物理文件,没有将其从 git 的记录中剔除。

删除一个未暂存的文件

比如这里我们删除了demo1.txt文件以后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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: demo1.txt

no changes added to commit (use "git add" and/or "git commit -a")

$ git commit -m "And remove it again"
# 注意到失败了
On branch master
Changes not staged for commit:
deleted: demo1.txt

no changes added to commit

我们发现提交失败。

所以要删除的话就要使用git rm命令:

1
2
3
4
5
6
7
8
$ git rm DirInfo
rm 'DirInfo'
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

deleted: DirInfo

删除一个已经暂存的文件

要注意的是之前的操作要求我们的文件未修改、不存在于暂存区中,否则无法成功。举个例子,比如我们现在对demo1.txt进行一些修改,然后add到暂存区中:

1
2
3
4
5
6
$ echo "Hello world" > demo1.txt
$ git add demo1.txt
$ git rm demo1.txt
error: the following file has changes staged in the index:
demo1.txt
(use --cached to keep the file, or -f to force removal)

可以看到我们有两个选择:

  1. git rm -f demo1.txt

    强制删除文件

  2. git rm --cached demo1.txt

    这种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。

git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。 比方说:

1
$ git rm log/\*.log

2.9 移动文件

使用git mv file_from file_to可以移动或者重命名文件。

其实,运行 git mv 就相当于运行了下面三条命令:

1
2
3
$ mv file_from  file_to
$ git rm file_from
$ git add file_to

3 查看提交的历史

可以使用git log来查看提交的历史纪录。

git log 有许多选项可以帮助你搜寻你所要找的提交, 接下来我们介绍些最常用的。

一个常用的选项是 -p,用来显示每次提交的内容差异。 你也可以加上 -2 来仅显示最近两次提交。