Git 使用技巧与原理(一)—— 基础操作
1、起步
1.1 版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 版本控制系统(VCS,Version Control System)通常可以分为三类:
- 本地版本控制系统:大多都是采用某种简单的数据库来记录文件的历次更新差异。最流行的一种叫做 RCS,其工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。
- 集中化的版本控制系统(CVCS,Centralized Version Control Systems):解决了在不同系统上的开发者可以协同工作的问题。诸如 CVS、Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。缺点是中央服务器的单点故障会使得开发者几乎无法做任何版本控制的任务,并且如果中心数据库所在的磁盘发生损坏,又没有做恰当备份,将丢失包括项目的整个变更历史在内的所有数据
- 分布式版本控制系统(DVCS,Distributed Version Control System):典型的 DVCS 有 Git、Mercurial 以及 Darcs 等,客户端将代码仓库完整地镜像下来,而不只是提取最新版本的文件快照。这样,任意协同工作用的服务器发生故障,都可以用任何一个镜像出来的本地仓库恢复。
1.2 Git 简介
Git 与其他版本控制系统(包括 Subversion 和近似工具)的重要差别在于 Git 对待数据的方式。 像 CVS、Subversion、Perforce 等等基于差异(delta-based)的版本控制是以文件变更列表的方式存储信息,将这些信息看作是一组基本文件和每个文件随时间逐步累积的差异:
而 Git 则把数据看作是对小型文件系统的一系列快照。当提交或更新文件时,Git 会对当时的全部文件创建一个快照并保存这个快照的索引。 如果文件没有修改,不会重新存储该文件而是保留一个连接指向之前的文件。因此 Git 对待数据更像是一个快照流:
除此之外,Git 相比于 CVCS 还有一大优点就是近乎所有操作都是本地执行,因为你在本地磁盘上就有项目的完整历史,所以大部分操作看起来瞬间完成。比如本地数据库已经保存了完整的项目历史,因此你无需外连服务器就访问它们。还有当你没有网络时也可以提交项目代码(到本地副本),直到有网络之后再上传到服务器。而像 Perforce 如果没有网络就无法连接到服务器,也就几乎不能做什么事;而 用 Subversion 和 CVS 虽然能修改文件,但由于本地数据库离线是不能向数据库提交代码的。
Git 通过 SHA-1 哈希机制计算校验和保证存储数据的完整性。即你只要修改了文件或目录,就会生成对应的通过 SHA-1 计算出来的 40 位哈希值,如果在传输过程中丢失信息或损坏文件,这个哈希值的校验会失败。Git 数据库保存的信息都是以文件内容的哈希值来索引,而不是文件名:
24b9da6552252987aa493b52f8696cd6d3b00373
Git 一般只添加数据,很难使用 Git 从数据库中删除数据,它几乎不会执行任何可能导致文件不可恢复的操作,这样能尽可能地避免数据丢失。
在 Git 中,文件有三种状态:
- 已修改(modified):修改了文件,但还没保存到数据库中
- 已暂存(staged):对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
- 已提交(committed):数据已经安全地保存在本地数据库中
三种状态以及切换图例:
工作区存放的是对项目中的某个版本(不一定是最新版本,因为你可以指定基于过往的某一个版本),从 Git 数据库的压缩数据库中独立提取出的文件,放在磁盘上供我们使用或修改。
暂存区是一个文件,保存了下次将要提交的文件列表信息。
Git 仓库目录用于保存项目的元数据和对象数据库,从其他计算机克隆仓库时,复制的就是这里的数据。
基本的 Git 工作流程如下:
- 在工作区中修改文件。
- 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。
- 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。
1.3 初次运行 Git 前的配置
初次运行 Git 前,通常需要对用户信息以及文本编辑器进行配置,其中必须配置用户信息中的用户名与邮箱,因为每一个提交都要用到这些信息。
通过 Git 自带的 git config
工具配置外观和行为变量,这些变量可以存在于三个层次的配置文件中,级别由高到低依次为:
- 系统上所有用户与仓库的配置文件
/etc/gitconfig
:执行git config
时带上--system
选项就会读写该文件中的配置变量 - 当前用户的配置文件
~/.gitconfig
或~/.config/git/config
:传递--global
选项读写此文件,对当前用户的所有仓库生效 - 当前使用的仓库的 Git 目录中的 config 文件
.git/config
:传递--local
选项,当然作为默认选项不传也是修改该文件,仅对当前
对于相同的配置变量,低级别的配置会覆盖高级别的配置。对于用户名以及邮箱配置,使用 --global
选项即可:
$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com
--global
的命令只需运行一次,后续在该系统上执行其他 Git 操作时都会带上这些信息。
此外,如果你不习惯 Git 自带的文本编辑器,也可以通过 git config
配置,比如想使用 Emacs:
$ git config --global core.editor emacs
Windows 系统则需要指定可执行文件的完整路径,例如 Sublime Text:
git config --global core.editor "'C:\Program Files\Sublime Text 3\sublime_text.exe' -w"
每个可执行文件路径后还需要加若干参数,不同的编辑器参数不同,具体需要参考 A3.1 附录 C: Git 命令 - 设置与配置。
配置完成后可以通过 git config --list
列出当前所有配置:
$ git config --list
user.name=John Doe
user.email=johndoe@example.com
color.status=auto
color.branch=auto
color.interactive=auto
color.diff=auto
...
有时你会看到多个同名变量,这是因为前面提到的三个级别的配置文件中可能保存了同名变量,这种情况下 Git 会使用每个变量的最后一个配置,你也可以使用传入 --show-origin
选项列出每个配置所在的文件:
$ git config --list --show-origin
file:D:/Program Files/Git/etc/gitconfig http.sslcainfo=D:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
file:D:/Program Files/Git/etc/gitconfig http.sslbackend=openssl
file:D:/Program Files/Git/etc/gitconfig diff.astextplain.textconv=astextplain
file:D:/Program Files/Git/etc/gitconfig core.autocrlf=true
file:D:/Program Files/Git/etc/gitconfig core.fscache=true
file:D:/Program Files/Git/etc/gitconfig core.symlinks=false
file:D:/Program Files/Git/etc/gitconfig credential.helper=manager
file:C:/Users/Administrator/.gitconfig user.name=John Doe
file:C:/Users/Administrator/.gitconfig user.email=John Doe@example.com
file:C:/Users/Administrator/.gitconfig https.proxy=https://127.0.0.1:7890
file:C:/Users/Administrator/.gitconfig http.proxy=http://127.0.0.1:7890
file:.git/config core.repositoryformatversion=0
file:.git/config core.filemode=false
file:.git/config core.bare=false
file:.git/config core.logallrefupdates=true
file:.git/config core.symlinks=false
file:.git/config core.ignorecase=true
file:.git/config core.sparsecheckout=true
能清楚的看出,系统级的配置文件 /etc/gitconfig
是在 Git 的安装目录下,而用户级的配置文件 .gitconfig
是在用户的个人目录下,而仓库级的配置文件 .git/config
就在当前仓库的目录下。
2、Git 基础
2.1 获取 Git 仓库
通常有两种获取 Git 项目仓库的方式:
- 将尚未进行版本控制的本地目录转换为 Git 仓库;
- 从其他服务器克隆一个已存在的 Git 仓库。
在已存在目录中初始化仓库
切换到项目目录后执行 git init
命令:
$ cd /c/user/my_project
$ git init
执行该命令后会在当前目录中创建一个 .git
目录,该目录包含初始化的 Git 仓库的所有必须文件。但此时项目中的文件还没有被 Git 跟踪(track),需要进行初始化提交:
$ git add *.c
$ git add LICENSE
$ git commit -m 'initial project version'
通过 git commit
将项目文件提交到 Git 后才得到一个存在被追踪文件且完成了初始化的 Git 仓库。
克隆现有的仓库
git clone
命令可以拷贝 Git 仓库服务器上几乎所有数据,而不仅仅是复制你工作所需要的文件。默认配置下,远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。
比如要克隆 Git 的链接库 libgit2
,可以通过 git clone <url>
的方式:
$ git clone https://github.com/libgit2/libgit2
这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git
文件夹, 从远程仓库拉取下所有数据放入 .git
文件夹,然后从中读取最新版本的文件的拷贝。
如果你想在克隆远程仓库的时候,自定义本地仓库的名字,你可以通过额外的参数指定新的目录名:
$ git clone https://github.com/libgit2/libgit2 mylibgit
这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit
。
2.2 记录每次更新到仓库
先来看文件的状态变化周期:
工作目录中的每一个文件都不外乎已跟踪或未跟踪两种状态:
- 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已暂存。简而言之,已跟踪的文件就是 Git 已经知道的文件。
- 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。
工作目录中新创建的文件是未跟踪状态,通过 git add
将其添加到暂存区变成已暂存状态后,该文件才变成已跟踪状态。已暂存文件通过 git commit
提交到 Git 本地仓库后会变为未修改状态。已经提交到 Git 仓库中的文件,可以进行两种操作:
- 如果需要删除某个文件,可以从仓库中移除该文件使其变为未跟踪状态
- 如果需要编辑某个文件,可以从仓库中取出该文件进行编辑,使其变为已修改状态。已修改状态的文件可以通过
git add
将其暂存
检查当前文件状态
使用 git status
可以查看文件状态,这里有很多种情况,比如在克隆仓库后立即使用此命令:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean
说明当前工作目录相当干净,也就是已跟踪文件在上次提交后都未被更改过。
在项目下新创建 README 文件,然后使用 git status
能查看到该文件未被跟踪:
$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:(use "git add <file>..." to include in what will be committed)READMEnothing added to commit but untracked files present (use "git add" to track)
未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,需要通过命令明确告知 Git 要跟踪该文件。
跟踪新文件
可以使用 git add
开始跟踪一个新文件:
$ git add README
再运行 git status
会看到 README
文件已被跟踪,并处于暂存状态:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:(use "git restore --staged <file>..." to unstage)new file: README
暂存已修改的文件
修改已被跟踪的文件 CONTRIBUTING.md
,再运行 git status
命令:
$ 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)new file: READMEChanges 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
说明 CONTRIBUTING.md
还未被放入暂存区,此时需要使用 git add
将其放入暂存区。git add
是一个多功能命令,它可以:
- 开始跟踪新文件
- 把已跟踪的文件放到暂存区
- 用于合并时把有冲突的文件标记为已解决状态
使用该命令后,再用 git status
查看文件状态:
$ git add CONTRIBUTING.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)new file: READMEmodified: CONTRIBUTING.md
假如此时再在 CONTRIBUTING.md
里再加条注释,存盘后再运行 git status
:
$ vim CONTRIBUTING.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)new file: READMEmodified: CONTRIBUTING.mdChanges 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
可以看到 CONTRIBUTING.md
同时出现在暂存区和非暂存区, 暂存区保存的是上一次运行了 git add
后的版本,即没添加注释的老版本;非暂存区的则是刚刚新添加了注释的版本,因此若想把新的修改添加到暂存区,需要再次运行 git add
。
状态简览
使用 git status -s
或 git status --short
会得到格式更为紧凑的输出:
$ git status -sM README # 在工作区已修改但尚未暂存
MM Rakefile # 已修改,暂存后又作了修改
A lib/git.rb # 新添加到暂存区中的文件
M lib/simplegit.rb # 文件已修改且已暂存
?? LICENSE.txt # 新添加的未跟踪文件
状态标记实际上有两位,带有 M
的就表示文件已修改,但第一位的 M
与第二位的 M
与 MM
的含义又进行了细分。
忽略文件
通常,项目中那些自动生成的文件,如日志文件、编译过程中创建的临时文件等无需纳入 Git 管理,也不希望它们出现在文件跟踪列表中,因此要在 .gitignore
文件中列出要忽略的文件的模式,如:
$ cat .gitignore
*.[oa]
*~
文件 .gitignore
的格式规范如下:
- 所有空行或者以
#
开头的行都会被 Git 忽略。 - 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
- 匹配模式可以以(
/
)开头防止递归。 - 匹配模式可以以(
/
)结尾指定目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(
!
)取反。
更多实例:
# 忽略所有的 .a 文件
*.a# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO# 忽略任何目录下名为 build 的文件夹
build/# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf
GitHub 有一个十分详细的针对数十种项目及语言的
.gitignore
文件列表, 你可以在 https://github.com/github/gitignore 找到它。
仓库根目录下的 .gitignore
可以递归应用到整个仓库,子目录中的 .gitignore
只对其所在目录有效。
查看已暂存和未暂存的修改
git diff
比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。git diff --staged
比对已暂存文件与最后一次提交的文件差异。
当然,也可以使用图形化工具或外部 diff 工具来比较差异。
提交更新
提交前务必确认是否有修改或新建的文件还没有 git add
过,否则提交的时候不会记录这些尚未暂存的变化。 这些已修改但未暂存的文件只会保留在本地磁盘。所以,每次准备提交前,先用 git status
看下,你所需要的文件是不是都已暂存起来了, 然后再运行提交命令 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
另外,你也可以在 commit
命令后添加 -m
选项,将提交信息与命令放在同一行:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed2 files changed, 2 insertions(+)create mode 100644 README
跳过使用暂存区域
使用暂存区有时略显繁琐,因此可以使用 git commit -a
略过 git add
,自动把所有已经跟踪过的文件暂存起来一并提交,但是要小心,有时这个选项会将不需要的文件添加到提交中。
移除文件
git rm
用于移除文件,但根据文件所处的不同位置与状态有不同的操作参数与方式。
直接从仓库中删除文件,并连带从工作目录中删除该文件,直接使用 git rm
后再提交即可:
Administrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git rm readme.txt
rm 'readme.txt'Administrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git status
On branch master
Changes to be committed:(use "git restore --staged <file>..." to unstage)deleted: readme.txtAdministrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git commit -m 'remove readme'
[master 3d88e2d] remove readme1 file changed, 1 deletion(-)delete mode 100644 readme.txt
如果你只想删除仓库中的文件,但保留工作目录中的文件,即想让文件保留在磁盘,但是并不想让 Git 继续跟踪(当你忘记添加 .gitignore
文件,不小心把一个很大的日志文件或一堆 .a
这样的编译生成文件添加到暂存区时,这一做法尤其有用。 ),此时使用 --cached
选项:
Administrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git rm --cached readme.txt
rm 'readme.txt'Administrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git status
On branch master
Changes to be committed:(use "git restore --staged <file>..." to unstage)deleted: readme.txtUntracked files:(use "git add <file>..." to include in what will be committed)readme.txtAdministrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ git commit -m 'remove readme'
[master c2cd2b7] remove readme1 file changed, 1 deletion(-)delete mode 100644 readme.txtAdministrator@Frank MINGW64 /f/Code/git/git-learning (master)
$ ls -al
total 9
drwxr-xr-x 1 Administrator 197108 0 7月 9 11:50 ./
drwxr-xr-x 1 Administrator 197108 0 7月 8 17:31 ../
drwxr-xr-x 1 Administrator 197108 0 7月 9 11:52 .git/
-rw-r--r-- 1 Administrator 197108 16 7月 9 11:50 readme.txt
可以看到使用 --cached
选项后,确实会移除仓库中的文件,同时在未跟踪区保留了该文件,commit 后本地文件中仍有 readme.txt,但仓库中的 readme.txt 被删除了。
以上说的是文件被跟踪但未修改的情况。假如被跟踪的文件被修改了或者已经添加到暂存区了,需要使用强制删除选项 -f
(译注:即 force 的首字母), 这是一种安全特性,用于防止误删尚未添加到快照(提交)的数据,这样的数据不能被 Git 恢复。
删除也可以使用
glob
模式。比如删除log/
目录下扩展名为.log
的所有文件。:git rm log/\*.log
移动文件
git mv
可以移动文件,也可以用于修改文件名:
$ 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 rm
相当于运行了下面三条命令:
$ mv README.md README
$ git rm README.md
$ git add README
2.3 查看提交历史
git log
会按时间先后顺序列出所有的提交,最近的更新排在最上面:
$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700changed the version numbercommit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700removed unnecessary testcommit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700first commit
这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。
显示提交引入的差异
使用 -p
或 --patch
会显示每次提交所引入的差异,结合 -<number>
或 -n <number>
限制显示的提交数量,可以在进行代码审查时快速浏览提交带来的变化:
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700changed the version numberdiff --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::RUBYs.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 -0700removed unnecessary testdiff --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 SimpleGitendend
-
-if $0 == __FILE__
- git = SimpleGit.new
- puts git.show
-end
显示简略统计信息
使用 --stat
选项,在每次提交的下面列出所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了。 在每次提交的最后还有一个总结:
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700changed the version numberRakefile | 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 -0700removed unnecessary testlib/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 -0700first commitREADME | 6 ++++++Rakefile | 23 +++++++++++++++++++++++lib/simplegit.rb | 25 +++++++++++++++++++++++++3 files changed, 54 insertions(+)
定制输出格式
使用 --pretty
选项可以使用非默认格式展示提交历史,它有多个子选项,比如 oneline
会将每个提交放在一行显示,在浏览大量的提交时非常有用:
$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 changed the version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit
类似的子选项有 short
,full
和 fuller
,它们展示信息的格式基本一致,只是详尽程度不一。
简写为
git log --oneline
也可以。
format
子选项可以定制显示格式,对后期提取分析格外有用:
$ 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
它的常用选项可以参考 –pretty=format 的常用选项。
--graph
添加了一些 ASCII 字符串来形象地展示你的分支、合并历史,与 oneline
或 format
与 --graph
结合使用时很有用:
$ 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
限制输出长度
前面提到的 -<number>
或 -n <number>
算是一种,不过实践中用的不多,因为 Git 默认会将所有的输出传送到分页程序中,一次只会看到一页的内容。
类似 --since
和 --until
这种按照时间作限制的选项很有用,比如列出最近两周的所有提交:
$ git log --since=2.weeks
该命令可用的格式十分丰富——可以是类似 "2008-01-15"
的具体的某一天,也可以是类似 "2 years 1 day 3 minutes ago"
的相对日期。
此外,还可以过滤出匹配指定条件的提交。 用 --author
选项显示指定作者的提交,用 --grep
选项搜索提交说明中的关键字。
还有一个 -S
过滤器,接收字符串参数,只显示添加或删除了该字符串的提交。假设你想找出添加或删除了对某一个特定函数的引用的提交,可以调用:
$ git log -S function_name
如果只关心某些文件或者目录的历史提交,使用 --path
选项。
最后再说一个 --no-merges
,不显示合并提交以免弄乱历史记录。一个综合例子,如果要在 Git 源码库中查看 Junio Hamano 在 2008 年 10 月其间, 除了合并提交之外的哪一个提交修改了测试文件,可以使用下面的命令:
$ git log --pretty="%h - %s" --author='Junio C Hamano' --since="2008-10-01" \--before="2008-11-01" --no-merges -- t/
5610e3b - Fix testcase failure when extended attributes are in use
acd3b9e - Enhance hold_lock_file_for_{update,append}() API
f563754 - demonstrate breakage of detached checkout with symbolic link HEAD
d1a43f2 - reset --hard/read-tree --reset -u: remove unmerged new paths
51a94af - Fix "checkout --track -b newbranch" on detached HEAD
b0ad11e - pull: allow "git pull origin $something:$current_branch" into an unborn branch
在近 4 万条提交中,输出了 6 条符合条件的记录。
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 合用的简写。 |
使用图形化工具查看
使用 gitk
命令可以打开一个图形化界面:
注意在提交记录中有 Author 与 Committer 两种身份。Author 是作者,即实际作出修改的人,而提交者 Committer 指的是最后将此工作成果提交到仓库的人。 比如我修改了一个文件,那么我就是这个提交的作者,但这个提交由我的同事 merge 或者 cherry-pick 到 master 分支提交,那么同事就是提交者。
2.4 撤销操作
修正提交
有时提交完代码才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend
选项的提交命令来重新提交:
git commit --amend
该命令会提交暂存区的文件,并打开文本编辑器让你“重新”编辑提交信息。如果自上次提交以来还没有对文件做任何修改(例如,在上次提交后马上执行了此命令),那么快照(提交)会保持不变,修改的只是提交信息。最终效果就是只有一个提交 —— 第二次提交将代替第一次提交的结果。
amend 是修正的意思,但实际上它不是修正原有的第一次提交,而更像是用第二次提交替换第一次提交,旧提交就像从未存在过一样,不会出现在仓库历史中。它的价值所在就是可以稍微进你最后的提交,不让小修补弄乱你的仓库提交历史。
取消暂存的文件
假如修改了两个文件想分两次独立提交它们,但是意外输入了 git add *
将两个文件都暂存了,需要取消其中一个的暂存,此时 git status
会提示你使用 git reset HEAD <file>
:
$ git add *
$ git status
On branch master
Changes to be committed:(use "git reset HEAD <file>..." to unstage)renamed: README.md -> READMEmodified: 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 -> READMEChanges 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
可以看到 CONTRIBUTING.md
变成了已修改但未暂存的状态。
git reset
是一个危险的命令,如果再加上--hard
选项则更是如此,后续在详解reset
时会介绍更多。
撤消对文件的修改
git status
给出了不同状态下的文件应该如何撤销修改的提示。
对于已修改但未暂存的文件,可以使用 git checkout
丢弃这些在工作目录下的修改:
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 -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:(use "git reset HEAD <file>..." to unstage)renamed: README.md -> README
可以看到对 CONTRIBUTING.md
的修改已经被撤消了。
请务必记得
git checkout — <file>
是一个危险的命令。Git 会用最近提交的版本覆盖掉本地文件,本地的修改会消失。在 Git 中,任何已提交的东西几乎都是可以恢复的,甚至那些被删除的分支中的提交或使用--amend
选项覆盖的提交也可以恢复(详见数据恢复)。然而,任何你未提交的东西丢失后很可能再也找不到了。
2.5 远程仓库的使用
查看远程仓库
通过 git remote
查看远程仓库:
$ git remote
origin
origin 是远程仓库服务器的默认名字。你也可以指定选项 -v
,会显示需要读写远程仓库的简写与其对应的 URL:
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
添加远程仓库
前面演示过使用 git clone
自行添加远程仓库,实际上还可以通过 git remote add <shortname> <url>
添加一个新的远程 Git 仓库,同时指定一个方便使用的简写仓库名:
$ git remote
origin
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
pb https://github.com/paulboone/ticgit (fetch)
pb https://github.com/paulboone/ticgit (push)
然后就可以使用 pb
代替整个 URL:
$ git fetch pb
remote: Counting objects: 43, done.
remote: Compressing objects: 100% (36/36), done.
remote: Total 43 (delta 10), reused 31 (delta 5)
Unpacking objects: 100% (43/43), done.
From https://github.com/paulboone/ticgit* [new branch] master -> pb/master* [new branch] ticgit -> pb/ticgit
从远程仓库中抓取与拉取
从远程仓库拉取所有本地还没有的数据:
$ git fetch <remote>
执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
结合实例例子来说,假如你使用 git clone
克隆了一个仓库,该命令会自动将其添加为远程仓库并默认以 “origin” 为简写。这样,git fetch origin
会抓取克隆(或上一次抓取)后新提交的所有内容。但需注意 fetch
命令只会将数据下载到你的本地仓库,不会自动合并或修改你当前的工作,需要你自己手动完成。
而 git pull
则会自动抓取后合并该远程分支到当前分支。
推送到远程仓库
使用 git push <remote> <branch>
将本地代码推送到远程仓库。假如想将 master
分支推送到 origin
服务器时(克隆时通常会自动帮你设置好那两个名字):
$ git push origin master
当你拥有该服务器的写权限,且在你推送之前没有其他人推送过代码,你才能成功推送。如果有人先于你推送了代码,你需要先抓取他们的代码合并进你的代码后才可推送你的代码。
查看某个远程仓库
使用 git remote show <remote>
查看远程仓库的更多信息:
$ git remote show origin
* remote origin# 远程仓库的 URLURL: https://github.com/my-org/complex-projectFetch URL: https://github.com/my-org/complex-projectPush URL: https://github.com/my-org/complex-project# 跟踪分支:告诉你正处于 master 分支HEAD branch: master# 远程分支信息Remote branches:master trackeddev-branch trackedmarkdown-strip tracked# issue-43 与 issue-45 后面是 new 表示这两条分支不在本地issue-43 new (next fetch will store in remotes/origin)issue-45 new (next fetch will store in remotes/origin)# stale 译为不新鲜的,表示该远程分支已从远程服务器上移除了,但你的本地可能还保留(缓存过)# 了这个分支的引用,因此需要 git remote prune 手动清理掉本地缓存的过期分支refs/remotes/origin/issue-11 stale (use 'git remote prune' to remove)# 执行 git pull 时哪些本地分支可以与它跟踪的远程分支自动合并Local branches configured for 'git pull':dev-branch merges with remote dev-branchmaster merges with remote master# 在特定的分支上执行 git push 会自动地推送到哪一个远程分支Local refs configured for 'git push':dev-branch pushes to dev-branch (up to date)markdown-strip pushes to markdown-strip (up to date)master pushes to master (up to date)
远程仓库的重命名与移除
使用 git remote rename
来重命名远程仓库:
$ git remote rename pb paul
$ git remote
origin
paul
远程分支名也同样会跟随仓库重命名,比如过去的 pb/master
会变为 paul/master
。
使用 git remote remove
或 git remote rm
删除远程仓库:
$ git remote remove paul
$ git remote
origin
所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除。
2.6 打标签
给某一个提交打上标签,以示重要,比如标记发布节点(v1.0
、 v2.0
等等)。
列出标签
输入 git tag
(可带上可选的 -l
选项 --list
)以字母顺序列出标签:
$ git tag
v1.0
v2.0
也可按照特定的模式查找标签。比如 Git 自身的源代码仓库包含标签的数量超过 500 个,如果只对 1.8.5 系列感兴趣,可以运行:
$ git tag -l "v1.8.5*"
v1.8.5
v1.8.5-rc0
v1.8.5-rc1
v1.8.5-rc2
v1.8.5-rc3
v1.8.5.1
v1.8.5.2
v1.8.5.3
v1.8.5.4
v1.8.5.5
当像上面这样提供了一个匹配标签名的通配模式时,-l
或 --list
就是强制使用的。
创建标签
Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated):
- 轻量标签只是某个特定提交的引用
- 附注标签是存储在 Git 数据库中的一个完整对象,可以被校验。它包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证
通常建议创建附注标签,因为可以拥有更多信息。但如果只想用一个临时标签,或者出于某些原因不想保存附注标签所带有的信息,也可以使用轻量标签。
两种标签都使用 git tag
命令创建,只不过使用的选项不同。
对于附注标签而言,最简单的方式是使用 -a
选项:
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4
-m
用于指定存储在标签中的信息,如果这里没有指定,Git 会启动编辑器要求输入相关信息(类似于 git commit
时如不指定 -m
会启动编辑器要求输入提交描述信息)。
通过 git show <tag version>
可以查看标签信息和与之对应的提交信息:
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700my version 1.4commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700changed the version number
而创建轻量标签本质上是将提交的校验和存储到一个文件中,不保存其他任何消息,无需使用 -a
、-s
或 -m
选项,只需要提供标签名字:
$ git tag v1.4-lw
$ git tag
v1.3
v1.4
v1.4-lw
对轻量标签使用 git show <tag version>
不会看到额外信息,只有提交信息:
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700changed the version number
后期打标签
假设提交历史是这样的:
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function
4682c3261057305bdd616e23b64b0857d832627b added a todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a started write support
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme
现在想在 “updated rakefile” 这个提交上补上 v1.2 的标签,可以通过 -a
选项创建 v1.2 标签时指定(部分)校验和:
$ git tag -a v1.2 9fceb02
再查看发现已经在 9fceb02 这个提交上打上了标签:
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
v1.5$ git show v1.2
tag v1.2
Tagger: Scott Chacon <schacon@gee-mail.com>
Date: Mon Feb 9 15:32:16 2009 -0800version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <mchacon@gee-mail.com>
Date: Sun Apr 27 20:43:35 2008 -0700updated rakefile
...
共享标签
默认情况下,git push
命令并不会传送标签到远程仓库服务器上,需要手动执行 git push origin <tagname>
实现:
$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git* [new tag] v1.5 -> v1.5
如果想要一次性推送多个标签,也可以使用带有 --tags
选项的 git push
命令。这将会把所有不在远程仓库服务器上的标签全部传送到那里:
$ git push origin --tags
Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git* [new tag] v1.4 -> v1.4* [new tag] v1.4-lw -> v1.4-lw
这样他人从仓库中克隆或拉取也能获得这些标签。
git push <remote> --tags
不区分轻量标签和附注标签,没有简单的选项能够让你只选择推送一种标签。
删除标签
使用 git tag -d <tagname>
删除本地仓库标签:
$ git tag -d v1.4-lw
Deleted tag 'v1.4-lw' (was e7d5add)
再通过 git push <remote> :refs/tags/<tagname>
删除远程仓库标签:
$ git push origin :refs/tags/v1.4-lw
To /git@github.com:schacon/simplegit.git- [deleted] v1.4-lw
注意它删除远程标签的原理是把冒号前面的空值推动到远程标签名,从而高效地删除它。
还有一种更直观的删除远程标签的方式是:
$ git push origin --delete <tagname>
检出(Checking out)标签
使用 git checkout
命令可以查看某个标签指向的文件版本,但该命令有一个副作用就是会是你的仓库处于“头指针分离(detached HEAD)”的状态:
$ git checkout 2.0.0
Note: checking out '2.0.0'.You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:git checkout -b <new-branch>HEAD is now at 99ada87... Merge pull request #89 from schacon/appendix-final$ git checkout 2.0-beta-0.1
Previous HEAD position was 99ada87... Merge pull request #89 from schacon/appendix-final
HEAD is now at df3f601... add atlas.json and cover image
命令提示说的很清楚,当前处于头指针分离状态下,你可以查看、试验性修改并提交变更,在此状态下的所有提交都不会影响任何分支,只需切换到其他分支即可完全丢弃这些提交。如果想保留这些提交,需要通过 git checkout -b <new-branch>
命令创建新分支:
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
在新分支上提交这些修改。
2.7 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 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 +0800test for current headSigned-off-by: Scott Chacon <schacon@example.com>