Git 介绍
1 Licence
本工作以Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License许可。访问https://creativecommons.org/licenses/by-nc-sa/3.0查看该证书。
2 Preface by Scott Chacon
希望你享受这个Pro Git的更新版本。
3 Preface by Ben Straub
希望这本书能够帮助到你。
4 Dedications
谢谢我的家人,我的朋友。
5 Contributors
本书为开源书籍,谢谢各位贡献者。
6 Introduction
下面是对本书的章节梗概介绍。
第七章,将介绍版本控制系统(Version Control Systems: VCSs)以及Git基础,我们不会对其中的技术进行介绍,只是说明什么是Git,以及为什么要创造它,是什么让它与众不同,以及为什么这么多人使用它。我们将介绍如何下载Git以及首次下载如何进行设置。
第八章,我们会介绍大部分的Git使用方法。在阅读本章后,用户可以克隆仓库,查看项目的历史内容,调整文件并贡献修改内容。
第九章,介绍Git中的分支模型。
第十章,将会介绍服务器上的Git。
第十一章,将会介绍不同分布式工作流的细节,以及如何在Git上实现。
第十二章,将会深入介绍GitHub托管服务以及工具。我们将会介绍注册以及管理一个账号,创建并使用Git仓库等内容。
第十三章,提供一些高级的Git命令。
第十四章,介绍配置自己的定制化环境。
第十五章,处理Git以及其他的版本控制系统。包括在SVN(Subversion)中使用Git,以及将其他的VCSs转移到Git中。
第十六章,深入,深入,深入!
附录A,不同环境的例子。有关shell,IDE,文本编辑器等信息。
附录B,脚本与工具。
附录C,编历Git命令。
7 Getting Started
本章介绍一些版本控制系统的背景,介绍如何在自己的系统中运行Git。
7.1 About Version Control
版本控制系统,记录文件或文件集的变化,允许用户回溯到指定的版本。
7.1.1 Local Version Control Systems
最开始是本地VCSs。最知名的是RCS。
7.1.2 Centralized Version Control Systems
与其他开发者进行合作。CVCSs(Centralized Version Control Systems)因此出现。
缺点是,具有中心服务器崩溃的风险。
7.1.3 Distributed Version Control Systems
DVCSs(Distributed Version Control Systems)。在分布式版本控制系统中,客户端不只是检出文件的最新版本的快照,它们完全建立仓库的镜像,包括它所有的历史内容。因此,如果服务器崩溃,这些客户端可以作为服务器的备份。每一份克隆内容都是服务器端的数据备份。
7.2 A Short History of Git
Linux内核是一个涉及面十分广大的软件项目。在早期的内核版本维护中,对软件的改变以patch补丁以及文件的形式传递。在2002年,Linux内核项目开始使用专有的DVCS BitKeeper。
在2005年,Linux内核的开发社区与开发BitKeeper的商业公司关系破裂。这促使Linux开发社区的开发者(尤其是Linux缔造者Linus Torvalds)开发出了自己的工具。新系统的目标是实现如下的功能:
- 速度
- 设计简洁
- 支持非线性开发(成千上万的分支)
- 完全分布式
- 有能力高效管理大规模工程
自2005年诞生之日起,Git一直保有这这样的特性。
7.3 What is Git?
理解Git是什么以及它工作的基本原理,能够方便用户简单高效的使用这个工具。在学习Git时,要了解其他VCSs如CVS,SVN或P4等工具,这帮助你消除使用本工具时的一些疑惑。即便Git的用户接口与其他的VCSs的接口类似,Git对信息的处理与其他的工具是不同的。
7.3.1 Snapshots, Not Differences
Git与其他VCSs的主要区别在于对待数据的行为。大多数其他系统按照基于文件改变的信息进行存储。这些其他的系统将它们存储的信息视为一系列的文件以及对这些文件的改变如下:
Git不以这一方式看代数据。相反,Git将数据视为微文件系统的快照。使用Git时,每次用户提交或保存项目的状态,Git将对用户的所有文件做一个快照。如果文件没有被修改,Git不会重新存储这个文件,只是链接到之前这个文件。Git对待数据更像是快照流:
这使Git更像是一个mini的文件系统,而不仅仅是一个VCS。
7.3.2 Nearly Every Operation Is Local
在Git中的大部分操作只需要本地文件,通常不需要从网络的另一台电脑上获取信息。因为用户在本地盘中拥有项目的完整的历史,大多数操作都是立刻进行的。
例如,浏览项目的历史,Git不需要访问服务器来获取历史并向用户展示,仅需要从本地数据库中读取这个信息。这意味着用户可以马上获取到相应的历史信息。如果用户想要查看当前版本的内容相较于一个月前的版本内容的改变,Git可以查看一个月前的文件,并在本地计算出相关信息,而不是询问远程服务器来完成这个操作,或拉取老版本的内容与本地内容对比。
这也意味着,没有网络或关闭VPN后不能使用。如果用户在飞机上或火车上希望进行工作,可以开心的进行提交修改到本地仓库,在能够访问到网络之后,再将修改的内容上传到服务器中。而在其他的系统中,做类似的操作是不可能或者十分痛苦。在P4中,在不连接服务器的情况下,不能做额外的操作。而在SVN中,虽然可以修改文件,但是用户在连接到网络之前不能将修改提交到数据库。这也许并不算什么,但是用户在实际使用中可能会发现这样的行为可能造成很大的不同。
7.3.3 Git Has Integrity
在Git中的所有内容,在进行存储之前都计算了校验和,在随后的使用中,都与这个校验和有关。这意味着不能随意的对这些内容进行修改。在传输过程中用户不会丢失信息或损坏文件。
Git使用的校验和机制为SHA-1哈希散列。这是一个40个字符长度的字符串,由十六进制的数字组成,由Git中的文件内容以及目录结构计算。一个SHA-1散列看起来像是如下的内容:
24b9da6552252987aa493b52f8696cd6d3b00373
用户将会在Git的所有位置看到这样的哈希散列值。实际上,Git存储所有内容到数据库时,不是以文件名而是以内容的哈希散列值进行。
7.3.4 Git Generally Only Adds Data
当用户在Git中进行了动作,几乎所有的动作都是在Git数据库中添加数据。很难让Git完成不可撤销或擦除数据的操作。在其他的VCS中,用户可能丢失尚未提交的内容,但是在用户像Git提交了快照,很难丢失这个内容。尤其是用户经常将数据库push到另一个仓库的情况下。
7.3.5 The Three States
如果想要后面的学习过程变得顺滑,必须牢记如下的内容。用户的文件在Git中具有三个状态:modified,staged,committed:
- modified:这个状态,意味着用户已经修改了文件,但是还没有将修改提交到本地仓库
- staged:这个状态,意味着用具已经标记了当前版本调整过的文件到下一个要提交的快照
- committed:这个状态,意味着用户数据已经安全地存储到本地的数据库
这可以将Git项目分成三个主要的部分:working tree,staging area,以及Git目录:
working tree是对项目检出的一个版本。这些文件是从Git目录中拉取出的压缩数据库,并将内容放到盘中以使用户使用或调整。
staging area是一个文件,通常包含在用户的Git目录中,它存储下一个要提交的信息。
Git目录是Git存储项目metadata以及object数据库的位置。这是Git最重要的部分,且它是从另一台电脑克隆仓库时的复制内容。
基础的Git工作流如下:
- 用户修改working tree中的文件
- 用户选择要在下一个提交使用的改变的文件部分,将这些改变添加到staging area
- 用户进行提交,将在staging area的文件的快照提交到用户的Git目录
7.4 The Command Line
有多种不同的方式使用Git。如原汁原味的命令行工具,以及具有图形界面的应用。本书在命令行中使用Git。命令行下可以运行所有的Git命令。
7.5 Installing Git
在开始使用Git之前,用户需要在自己的电脑上获取Git。即便已经安装有Git,最好将Git升级到最新的版本。
本书是以Git 2.8.0版本进行书写的。Git是向后兼容的,任何高于2.8版本的Git,都兼容本书的操作内容。
7.5.1 Installing on Linux
如果用户想要通过二进制安装器安装Git工具,可以通过使用自己Linux版本的包管理工具进行安装。如果用户是Fedora系统(或者任何RPM-based版本如RHEL或CentOS),用户可以使用dnf:
$ sudo dnf install git-all
如果用户使用Debian-based版本,如Ubuntu,可以使用apt:
$ sudo apt install git-all
7.5.2 Installing on macOS
有多种方式可以在Mac上安装Git。最简便的方式是安装Xcode命令行工具。在Mavericks(10.9)或以上的版本,用户可以在终端中使用如下的命令:
$ git --version
如果用户尚未安装,将会提示用户安装。
7.5.3 Installing on Windows
有多种方式在Windows系统中安装Git。可以在Git网站下载官方编译版本。
7.5.4 Installing from Source
一些用户可能倾向于使用源码进行安装。因为这样,用户可以获取到最新的版本。
如果用户想要以源码的方式安装,必须先安装Git依赖:autotools,curl,zlib,openssl,expat以及libiconv。例如,在可以使用dnf或apt的系统中安装软件,用户可以使用下面命令的其中一条来安装最小依赖:
$ sudo dnf install dh-autoreconf curl-devel expat-devel gettext-devel \
openssl-devel perl-devel zlib-devel
$ sudo apt-get install dh-autoreconf libcurl4-gnutls-dev libexpat1-dev \
gettext libz-dev libssl-dev
为了能够添加不同格式的文件,如下的额外依赖需要安装:
$ sudo dnf install asciidoc xmlto docbook2X
$ sudo apt-get install asciidoc xmlto docbook2x
RHEL以及RHEL类的Linux版本如CentOS的用户,需要使能EPEL库来下载docbook2X包。
如果用户使用Debian-based版本(Debian/Ubuntu/Ubuntu-derivatives),用户还需要install-info包:
$ sudo apt-get install install-info
如果用户使用RPM-based版本(Fedora/RHEL/RHEL-derivatives),用户还需要getopt包:
$ sudo dnf install getopt
如果用户使用Fedora/RHEL/RHEL-derivatives,用户需要做如下操作:
$ sudo ln -s /usr/bin/db2x_docbook2texi /usr/bin/docbook2x-texi
当必要的依赖安装完毕后,用户可以提取最新版本的源码。之后完成汇编与安装:
$ tar -zxf git-2.8.0.tar.gz
$ cd git-2.8.0
$ make configure
$ ./configure --prefix=/usr
$ make all doc info
$ sudo make install install-doc install-html install-info
完成这个内容之后,用户可以使用Git自身实现对Git的升级:
$ git clone git://git.kernel.org/pub/scm/git/git.git
7.6 First-Time Git Setup
现在用户在自己的系统中拥有了Git,可能想要在自己的Git环境中定制自己的内容。用户应该需要在一台电脑上做一遍这样的操作。用户可以在任何使用通过命令修改它们。
Git使用git config工具使用户设置、获取Git工作与操作的控制变量。这些变量可以存储在这三个位置:
- [path]/etc/gitconfig文件:配置值应用在系统上所有用户以及他们仓库,如果用户向git config传递—system选项,会将配置写入到这个文件中。因为这是系统配置文件,用户需要拥有管理员权限或超级用户权限。
- ~/.gitconfig或~/.config/git/config文件:配置值应用在用户个人上。用户可以向git config传递—global选项,这将影响到用户自己的仓库。
- 当前使用的仓库的Git目录的config文件:配置应用在当前使用的仓库,限定在那个单独的仓库中。用户可以使用—local选项来进行配置,实际上是默认的配置情况。
每一个等级的配置都将覆写前一个等级的内容,因此.git/config覆写[path]/etc/gitconfig中的值。
在Windows系统中,Git查找$HOME目录下的.gitconfig文件。关于Windows内容略。
用户可以通过如下的命令查看设置信息以及在哪里进行了设置:
$ git config --list --show-origin
7.6.1 Your Identity
在安装完Git之后,用户首先要做的是设置用户名以及邮件地址。这是十分重要的,因为每次Git提交都会使用到这个信息,并且将会永久的保留在其中:
$ git config --global user.name "du.xudong"
$ git config --global user.email du.xudong@embedway.com
如果使用—global选项,这个配置操作只需要设置一次,因为Git将在这个系统上为这个用户一直使用这个信息。如果用户想要使用其他的名字或邮件覆盖这个内容,可以在项目中不适用—global选项进行配置。
7.6.2 Your Editor
现在用户身份已经设置完成,用户可以配置Git需要用户输入信息时使用的默认文本编辑器。如果没有进行配置,Git将使用系统默认编辑器。
如果用户想要使用其他的文本编辑器,比如Emacs,用户可以通过如下的命令进行配置:
$ git config --global core.editor emacs
在Windows系统中,如果用户想要使用不同的编辑器,用户必须要指定它可执行文件的完整路径。
如果使用Notepad++,用户可能想要使用32位版本的软件,因为64位版本的软件并不支持所有的插件。如果用户使用的是32位的Windows系统,或者用户在64位系统上有一个64位的编辑器,需要像下面配置一下:
$ git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"
Vim,Emacs以及Notepad++是类Unix系统或Windows系统开发者常用的文本编辑器。如果用户使用其他的编辑器,或一个32位版本,需要使用特殊的命令,连接不再给出。
用户可能发现,如果不这样设置编辑器,可能会进入到一个混乱的境地。
7.6.3 Your default branch name
默认情况下,Git将会在用户使用git init创建新的仓库时创建一个名为master的分支。从Git 2.28版本开始,用户可以为初始分支创建不同的名字。
设置main作为默认分支:
$ git config --global init.defaultBranch main
7.6.4 Checking Your Settings
如果用户想要查看自己配置信息,可以使用git config –list命令来列出Git能够找到的所有设置:
$ 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从不同的文件读取相同的键关键字。这种情况下,Git使用最后它看到的键的值。
用户也可以通过给定键名,查看指定键的值:
$ git config user.name
John Doe
因为Git可以从不同的文件中读取相同的配置,用户可能看到一个变量的值会现得莫名其妙。为了防止这种情况发生,用户可以询问Git那个值的源,它会告知你最终使用的是那个变量:
$ git config --show-origin rerere.autoUpdate
file:/home/johndoe/.gitconfig false
7.7 Getting Help
如果用户在使用Git时想要获取到帮助信息,下面三个等效的方式来获取综合操作手册(manpage)来获取Git命令的帮助信息:
$ git help <verb>
$ git <verb> --help
$ man git-<verb>
例如,用户可以通过如下命令从帮助页中获取到git config命令的帮助信息:
$ git help config
如果想要获取到更多的线上帮助,可以访问指定论坛,略。
另外,如果用户不需要完全的帮助页的帮助信息,而只是想要获取得到Git命令可用选项的快速参考,用户可以通过使用-h选项来查看这个信息:
$ git add -h
usage: git add [<options>] [--] <pathspec>...
-n, --dry-run dry run
-v, --verbose be verbose
-i, --interactive interactive picking
-p, --patch select hunks interactively
-e, --edit edit current diff and apply
-f, --force allow adding otherwise ignored files
-u, --update update tracked files
--renormalize renormalize EOL of tracked files (implies -u)
-N, --intent-to-add record only the fact that the path will be added later
-A, --all add changes from all tracked and untracked files
--ignore-removal ignore paths removed in the working tree (same as --no-all)
--refresh don't add, only refresh the index
--ignore-errors just skip files which cannot be added because of errors
--ignore-missing check if - even missing - files are ignored in dry run
--chmod (+|-)x override the executable bit of the listed files
--pathspec-from-file <file> read pathspec from file
--pathspec-file-nul with --pathspec-from-file, pathspec elements are separated with NUL character
7.8 Summary
现在,用户应该已经对Git是什么以及Git与其他版本的VCSs的区别。同时用户也已经拥有了可以使用的Git版本,并为自己的Git进行了初步设置。
8 Git Basics
本章介绍用户掌握的基础的Git命令。在本章结束,用户应该有能力配置并初始化一个仓库,开始或停止追踪文件,以及stage和提交改变。我们也会讲解如何配置Git,来忽略特定文件以及文件模式,如何快速轻松的撤销一个错误,如何浏览项目的历史,查看不同提交之间的改变,以及如何对远程仓库执行push与pull操作。
8.1 Getting a Git Repository
用户通常使用如下两个方法中的一个来获取一个Git仓库:
- 使用当前不处于版本控制的本地目录,让它变为Git仓库
- 从某个地方克隆一个现存的Git仓库
这两个方法都能够使用户在操作完成后,在自己的设备上拥有自己的Git仓库。
8.1.1 Initializing a Repository in an Existing Directory
如果用户拥有一个当前不处于版本控制的本地目录,想要使用Git对它进行版本控制,首先需要进入到那个项目目录中。用户可能之前没有进行过这样的操作,针对不同的系统,命令不同:
对于Linux:
$ cd /home/user/my_project
对于macOS
$ cd /Users/user/my_project
对于Windows
$ cd C:/Users/user/my_project
之后,键入:
$ git init
这将创建一个新的名为.git子目录,包含所有用户需要的仓库文件——一个Git仓库骨架。此时,用户的项目中没有追踪任何的内容。查看Git Internals来获取哪些文件包含在用户创建的.git目录下的详细信息。
如果用户想要开始对现有的文件进行版本控制(而不是一个空的目录),需要跟踪这些文件并做一个初始提交操作。用户可以通过使用一些git add命令,来指定要跟踪的文件,如下:
$ git add *.c
$ git add LICENSE
$ git commit -m 'Initial project version'
我们将会在后面介绍这些命令做了什么。此时,用户就拥有了一个仓库,在这个仓库中有一些要跟踪的文件以及初始的提交信息。
8.1.2 Cloning an Existing Repository
如果用户想要获取现有Git仓库的一个副本——例如,用户想要贡献的项目,用户需要执行git clone命令。如果用户对其他的VCSs熟悉,比如SVN,用户将会注意到命令是clone而不是checkout。这是一个重要的区别——不仅仅是获取到一个副本,Git获取服务器拥有的几乎所有数据。当运行git clone将会拉取每一个文件的每一个历史版本到本地。实际上,如果用户的服务器盘崩溃,可以使用任何客户端的内容恢复Git仓库到客户端的版本(可能会丢失服务器侧的挂钩)。
用户可以使用git clone <url>来克隆一个仓库,比如,如果用户想要克隆Git链接库libgit2,用户可以使用如下的命令克隆:
$ git clone https://github.com/libgit2/libgit2
这个命令创建一个名为libgit2的目录,并在其中创建一个.git目录,为这个仓库拉取所有的数据,并检出最新版本的所有工作内容的复制。如果用户进入到新的libgit2目录,将会看到其中的项目文件,这些文件现在可以进行工作使用。
如果用户想要克隆仓库到其他名称的目录,而不是libgit2,可以指定新的目录名如下:
$ git clone https://github.com/libgit2/libgit2 mylibgit
这样操作下,获取到与上一个命令相同的内容,除了目录变为mylibgit。
Git为用户提供了不同的传输协议。前一个版本使用https:// 协议,但是用户可能看git:// 或user@server:path/to/repo.git,这两个使用SSH传输协议。
8.2 Recording Changes to the Repository
此时,用户在本地设备上具有了bona fide Git仓库,并且拥有了所有的文件的working copy内容。通常,用户想要进行修改,并在项目到了想要提交的阶段,将这些改变的快照提交到仓库。
牢记用户工作目录的每一个文件具有两个状态:已经处于跟踪状态以及没有处于跟踪状态。跟踪的文件是在上一个快照中的,以及新staged文件,他们可以是未修改的,已修改的或staged。简而言之,跟踪的文件是Git知道的文件。
未跟踪的文件是其他的内容——在工作目录但是没有在上一个快照中且不在用户的staging区的文件。当用户克隆了一个仓库,所有的文件都是被跟踪的,且是未经修改的内容。
一旦用户编辑了这些文件,Git将其视为已经修改的文件,因为在上次提交后改变了它们的内容。用户可选这些修改的文件提交这些到staged部分,这一循环持续。
8.2.1 Checking the Status of Your Files
用户用来确定哪些文件处在哪些状态的工具是git status命令。如果在克隆之后直接运行这个命令,将会看到如下内容:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean
这意味着,用户拥有一个干净的工作目录,换言之,所有跟踪的文件都是未经修改的。Git也不会查看任何未跟踪的文件。最终,这个命令告知用户处于哪一个分支,并告知用户服务器上的分支没有任何改变。这个分支通常是master,这是默认情况。
现在用户要添加一个新的文件到项目中,比如一个简单的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)
README
nothing added to commit but untracked files present (use "git add" to track)
用户可以看到新的README文件是没有被跟踪的,因为它是处于未跟踪文件状态的。未跟踪的状态意味着Git看到在之前的快照中没有这个文件,而且也没有被staged,Git在用户明确的告知它之前,还没有将这个文件包含到提交快照中。这样,它不会误操作,将你不想要的文件添加到生成的二进制包中。用户确定要跟踪这个README文件,让我们来跟踪这个文件吧。
8.2.2 Tracking New Files
为了跟踪这个文件,用户需要使用git add命令。
$ git add README
如果用户现在运行git status命令,可以看到现在README文件已经跟踪了这个文件并且staged它等待提交:
$ 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
现在你知道它是staged因为它是处在等待提交的部分。如果用户现在提交,当前的git add中的文件版本将会出现在后续的历史快照中。现在回顾之前运行了git init,之后运行了git add <files>——开始跟踪用户目录中的文件。命令git add可以接受路径名或文件名,如果它是一个目录,这个命令将递归添加目录中的所有的文件。
8.2.3 Staging Modified Files
现在,让我们修改已经在跟踪的文件。如果用户修改了之前已经处于跟踪状态的文件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: 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
这个CONTRIBUTING.md文件出现在 “Changes not staged for commit” 中——这意味着跟踪目录中的文件被修改,但是还没有staged。如果要stage它,用户运行git add命令。这个命令是多功能命令——用户可以使用它来开始跟踪一个新文件,去stage文件,以及标记合并冲突的文件。以如下的思维去看待这个过程:精确地添加内容到下一次提交中,而不是:添加文件到工程中。让我们运行git add来stage CONTRIBUTING.md文件,再运行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: README
modified: 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: README
modified: CONTRIBUTING.md
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
现在什么情况?CONTRIBUTING.md文件同时出现在了staged以及unstaged中。这是怎么发生的呢?这是因为当运行git add命令将文件添加到Git stages中。如果现在进行提交操作,会将最近一次的git add命令的CONTRIBUTING.md文件版本提交到仓库中,而不是用户的工作目录中的那个文件。如果用户在执行git add之后修改了文件,必须要再次执行git add命令来stage最新版本的文件:
$ 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: README
modified: CONTRIBUTING.md
8.2.4 Short Status
当运行git status命令时,我们可以看到输出是比较全面的,而且内容是比较冗长的。Git具有一个简短的标识来查看更加紧凑的修改信息。如果用户执行git status -s或git status –-short用户将会获取到更加简短的输出:
$ git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
未处于跟踪状态的新文件将在文件旁边有一个??标记,新添加到stage区的文件具有一个A标记,修改的文件具有一个M标记。在这个输出中具有两列内容——左列指示stage区的状态,右列指示工作区的状态。以上面输出中的内容作为例子,README文件为工作区的已经修改但是尚未stage的文件,lib/simplegit.rb文件是进行了修改并且已经staged内容。Rakefile为进行了修改,并且在staged之后又进行了修改,因此它具有staged部分又具有未staged部分。
8.2.5 Ignoring Files
通常,有一类文件,用户不希望Git自动添加甚至要展示为未追踪状态。这些文件通常是自动生成的文件,如日志文件或编译系统生成的文件。在这种情况下,用户可以创建一个文件列表模式以.gitignore文件名命,如下例:
$ cat .gitignore
*.[oa]
*~
在这个文件种,第一行告知Git忽略所有的以.o或.a结尾的文件。第二行告知Git忽略所有以~结尾的文件,这些文件通常被一些文本编辑器如Emacs来标记临时文件。用户可能会要添加日志文件、临时文件、pid目录以及自动生成目录等。通常在新的仓库种设置.gitignore文件是一个好想法,这样用户不会莫名其妙地向Git库种提交一些不想提交的内容。
.gitignore文件的规则如下:
- 空行或#符号开始的行被忽略
- 标准的glob模式工作,将会在整个工作目录递归使用
- 通过以斜线(/)开始模式可以避免递归
- 通过以斜线(/)结束可以指定一个目录
- 通过感叹号(!)可以进行否定模式
Glob模式有点像shell使用的表达。一个星号(*)匹配多个字符,[abc]匹配中括号中的任意字符(a,b或c),一个问号(?)匹配单个字符,[0-9]匹配0到9之间的数字。用户可以使用两个型号(**)来匹配嵌套目录,a/**/z将会匹配a/z,a/b/z,a/b/c/z等。
如下是.gitignore文件中的例子:
# ignore all .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in any directory named build
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory and any of its subdirectories
doc/**/*.pdf
一个简单的情况下,仓库在它的根目录下具有一个.gitignore文件,这个文件的内容将会递归地应用到整个仓库。同时,也可以在子目录中添加附加的.gitignore文件。这些嵌入的.gitignore文件只会应用在他们所在的目录中。Linux内核源码仓库具有206个.gitignore文件。
8.2.6 Viewing Your Staged and Unstaged Changes
有时候git status命令展示内容有点模糊不清——用户希望清楚自己究竟修改了什么,而不只是修改了哪些文件,此时可以使用git diff命令。我们后面将会介绍git diff的细节,用户可以通过git diff命令来得知如下的内容:哪些内容已经修改了但是没有stage,哪些stage了但是没有提交。虽然git status笼统地回答了这两个问题,git diff命令清楚的展示添加了哪些行,删除了哪些行——像是补丁一样。
假定用户编辑并stage了README文件,且编辑了CONTRIBUTING.md文件但是没有stage它,如果现在运行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)
modified: 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
查看哪些内容是已经修改了但还没有stage的,键入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
这个命令比较工作区以及stage区的内容,并告知用户做了哪些修改,哪些修改stage了,哪些没有stage。
如果用户想看哪些是stage的内容,可以使用git diff –staged命令,这个命令将stage中的内容与上一次提交的内容做比较:
$ 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自身并不会展示上次提交后stage的内容,仅仅展示那些没有stage的内容。如果已经stage了所有改变的内容,git diff将不会有输出。
另一个例子中,如果用户stage了CONTRIBUTING.md文件并再次编辑了它,用户可以使用git diff来查看未stage的修改内容。
$ git add CONTRIBUTING.md
$ echo '# test line' >> 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)
modified: CONTRIBUTING.md
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 diff来查看没有stage的内容:
$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
## Starter Projects
See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+# test line
可以使用git diff –cached来查看用户已经stage的内容:
$ git diff --cached
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
8.2.7 Committing Your Changes
现在stage区已经按照用户的想法做了设置,可以对修改的内容进行提交了。需要牢记的是,对文件的修改内容或是新创建的文件,如果还没有使用git add命令添加到stage区,将不会被提交。这些内容会作为修改后的文件存储在盘中。本次情况中,我们假设上次运行git status展示的内容,所有内容已经stage了,现在已经准备好进行提交了。最简单的方法是键入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
# 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 commit传递-v选项。这样将会把修改的区别添加到编辑器中,可以看到要提交的内容做了哪些修改。
当退出编辑器,Git为本次提交创建提交信息(带有评论内容)。
当然,用户可以在命令行内通过-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),修改了多少文件,本次提交添加了几行删除了几行内容。
牢记提交记录stage区的快照。尚未stage的内容依然没有被提交,可以再次使用提交来将这些信息添加到历史中。
8.2.8 Skipping the Staging Area
虽然stage区是十分有用的,但是有时候,这个工作流程现得有点复杂。如果用户想要跳过stage区,Git提供了一个捷径来实现这个功能。在git commit中添加-a选项令Git自动stage所有已经处于跟踪状态的文件,之后进行提交,跳过了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 'Add new benchmarks'
[master 83e38c7] Add new benchmarks
1 file changed, 5 insertions(+), 0 deletions(-)
注意到本次提交不再需要使用git add来添加CONTRIBUTING.md文件再进行提交了,这是因为-a选项自动地进行了这一操作。这是方便的,但是有时这样操作可能会提交一些不希望提交的改变内容。
8.2.9 Removing Files
想要从Git中移除一个文件,用户需要将它从跟踪文件中移除(精确地说是从stage区移除),之后再进行提交。git rm命令实现这个功能,这个命令同时将文件从工作目录中移除,这个文件就不再被视为未被跟踪地文件了。
如果用户只是将文件从工作目录中移除,执行git status命令时将会提示改变但是没有做提交(未stage区):
$ 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命令,stage区地文件将移除:
$ 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
下次做提交,文件将会消失且不再被跟踪。如果用户调整了文件或已经将它添加到stage区,必须使用-f选项来强制删除。这个命令是一个保险,防止用户将没有提交的内容删除掉,这时Git将不能恢复。
用户可能想要在本地盘的工作目录中保留文件,只是从stage区移除。换言之,不让Git跟踪它,而只在本地盘中保有它。这种情况发生在用户忘了在.gitignore中设置,但是stage了某些文件,比如大型的日志文件或.a汇编文件等。可以通过—cache选项实现:
$ git rm --cached README
用户可以传递文件、目录以及通配符等内容到git rm命令,这意味着可以像这样:
$ git rm log/\*.log
在*号之前的反斜杠(\),这是有必要的,因为除了shell的文件名扩展外,Git可以进行自己的文件名扩展。这个命令移除log/目录下所有具有.log扩展的文件。或者,用户可以执行如下命令:
$ git rm \*~
这个命令移除所有名称结尾为~的文件。
8.2.10 Moving Files
不像其他的VCSs,Git不会精确跟踪文件移动。如果用户重命名了Git中的一个文件,没有元数据告知Git用户修改了这个文件的文件名。实际上,Git可以很智慧地推断出这个内容——后面再进行讲解。
Git具有一个mv命令,如果用户想要重命名一个文件,可以执行如下命令:
$ git mv file_from file_to
如果用户运行了这个命令,之后查看status,Git就认为是对文件进行了重命名:
$ 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
这等价于进行如下的操作:
$ mv README.md README
$ git rm README.md
$ git add README
Git暗中分析出这个操作是一次重命名,因此,使用上面的方式或使用mv命令都是可以的。唯一的不同点是,git mv是一个命令,而不是三条命令。
8.3 View the Commit History
再进行过几次提交后,或如果用户克隆了具有提交历史的仓库时,用户可能想要查看过去的历史发生了什么。最基础强效的命令是git log命令。
为了展示这个例子,先克隆一个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
Change version number
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 16:40:33 2008 -0700
Remove unnecessary test
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date: Sat Mar 15 10:31:28 2008 -0700
Initial commit
默认情况下,是没有评论的,git log按照反向时间顺序列出提交过程,即最新的提交排在首位。可以看到,这个命令列出了每次提交的SHA-1校验和,提交用户名以及email地址,写的日期以及提交信息。
很多的选项,可以帮助用户找到自己想找的内容。
其中一个是-p或—patch选项,这个选项展示出每一个提交的区别(以patch输出)。用户可以限制要展示的提交日志的条目数,比如使用-2来控制显式最新的两条:
$ git log -p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change 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
Remove 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
这个选项展示带有diff内容的条目信息。这可以帮助查看代码修改内容。可以对git log使用很多总结类的选项,比如查看简洁的提交状态信息,可以使用—stat选项:
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change 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
Remove 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
Initial commit
README | 6 ++++++
Rakefile | 23 +++++++++++++++++++++++
lib/simplegit.rb | 25 +++++++++++++++++++++++++
3 files changed, 54 insertions(+)
可以看到,--stat选项打印了提交条目的修改的文件列表,修改了多少文件,添加或删除了多少行。并在最后提供一个总结性信息。
另一个有用的选项是—pretty。这个选项修改了日志输出的格式,而不再以默认格式打印。一些预配置选项值可以使用。比如oneline值将每一个提交打印到单行,这在查看很多的提交内容时十分有用。此外还有short、full以及fuller值可以选择,例子如下:
$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 Change version number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 Remove unnecessary test
a11bef06a3f659402fe7563abf99ad00de2209e6 Initial commit
这个选项最有趣的值是format,它允许用户指定自己的日志输出格式。这在写脚本方便设备解析十分有用:
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 6 years ago : Change version number
085bb3b - Scott Chacon, 6 years ago : Remove unnecessary test
a11bef0 - Scott Chacon, 6 years ago : Initial commit
下表列出了可以进行的格式化输出值配置:
Specifier |
Description of Output |
%H |
提交哈希散列 |
%h |
简洁的提交哈希散列 |
%T |
树哈希 |
%t |
简洁的树哈希 |
%P |
父哈希 |
%p |
简洁父哈希 |
%an |
作者名 |
%ae |
作者email |
%ad |
作者日期(--date=option) |
%ar |
作者相对日期 |
%cn |
提交者名 |
%ce |
提交者email |
%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 Add 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
这个类型的输出将会在下一章讲解分支与合并时出现。
下表列出了log命令的选项:
Option |
Description |
-p |
展示每个提交的patch信息 |
--stat |
展示每个提交的文件状态 |
--short-stat |
仅展示—stat中的修改、插入、删除内容 |
--name-only |
在提交信息后展示文件修改信息 |
--name-status |
展示受添加、调整、删除影响的文件信息 |
--abbrev-commit |
展示SHA-1校验和的前几个字符 |
--relative-date |
以相对格式展示日期 |
--graph |
ASCII码图 |
--pretty |
以可选格式展示输出 |
--oneline |
单行显式 |
8.3.1 Limiting Log Output
在附加的输出格式选项中,git log命令使用限制选项,来展示提交信息的子集。用户已经看到过通过-2选项展示两条提交条目。实际上可以使用-<n>来确定要展示的n个条目数。通常不需要使用它,因为Git会将输出内容通过Pager输出到管道,用户只会看到单页日志输出内容。
然而,时间限制选项如—since以及—until十分有用。例如,如下的命令获取两周内的提交信息:
$ git log --since=2.weeks
这个命令可以按照格式进行工作,用户可以指定一个特殊的日期如 2008-01-15,或给出一个相对日期 2 years 1 day 3 minutes ago。
用户也可以按照某种标准过滤提交信息。通过—author宣子昂可以允许用户指定作者,--grep选项可以在提交信息中寻找关键字。
用户可以同时给定多个标准,比如—author以及—grep,这将会限制输出信息到指定的作者或者满足—grep模式,如果添加—all-match选项,将会满足匹配—grep模式的条目。
另一个过滤选项是使用-S选项,它使用一个字符串作为输入,并在提交信息中寻找更改过字符串出现次数的条目。比如,如果用户想要找对一个指定函数的调用的添加或删除条目,可以使用如下命令:
$ git log -S function_name
下一个有用的过滤选项是将路径传递给git log,如果用户指定一个目录或者文件名,可以限制日志输出对这个目录或文件进行过修改的条目。这个内容总是放在选项的最后,并且以双短--分隔选项与路径:
$ git log -- path/to/file
下表给出了过滤选项表:
Option |
Description |
-<n> |
展示最新的n个条目 |
--since, --after |
指定日期限制 |
--until, --before |
指定日期限制 |
--author |
指定作者 |
--committer |
指定提交者 |
--grep |
查找具有特定字符串的条目 |
-S |
查找对字符串进行过添加或删除的条目 |
例如,如果用户想要查看哪些提交对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
这个命令从40000条提交条目中,查找出了6条匹配信息。
8.4 Undoing Things
在任何阶段,用户都可能要撤销一些操作。这里我们将会查看一些撤销改变的基础工具。应该注意,用户并不是总能实现这些撤销操作的。在Git的某些区域,如果撤销操作执行错误,可能会丢失一部分工作内容。
一个可能的撤销场景是,当用户过早提交了工作,可能忘了添加一些想要提交的文件。或者在填写提交信息时弄混了。如果用户想要重新提交一次,对遗忘的内容做补充评论,可以使用—amend选项:
$ git commit --amend
这个命令使用stage区,并用它来做提交动作。如果在上次提交后没有再做出修改(例如,在上次提交后马上执行了这个命令),那么新的快照将一模一样,改变的只有提交信息。
相同的提交信息出现在编辑器中,但是它早已包含有上次提交的信息。用户可以再次编辑这一信息,它将覆盖之前的提交信息。
例如,假设用户提交之后,发现自己忘了将一个文件放入到stage区,可以做如下的操作:
$ git commit -m 'Initial commit'
$ git add forgotten_file
$ git commit --amend
三条指令完成之后,最后只有一个单独的提交——第二个提交替换了第一条提交内容。
需要注意的是,对提交做补充,并不是对在上一个提交的基础上做补充,而是将上一个提交剔除之后,以新的提交信息替代它。这将会造成前一个提交仿佛不存在一般,它将不会出现在仓库的提交历史中。
补充操作只有在本地尚未推送到远端才有效。对一个已经推送到远端的提交的补充,会造成与合作者之间的合作问题。
8.4.1 Unstaging a Staged File
下面两节展示如何stage区以及工作目录的修改。回想之前查看这两个区域状态的命令,再来考虑如何对这两个区域进行撤销操作。例如,现在用户有两个修改了的文件想要分别提交它们,但是却使用了git add *将这两个文件同时stage了。如何将其中一个从stage中提出来呢?看一下git status命令:
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
modified: CONTRIBUTING.md
在 Changes to be committed 文字下面,它告知使用git reset HEAD <file>命令来将stage中的文件踢出。因此,让我们使用这个建议来将CONTRIBUTING.md文件从stage区中移除:
$ 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 status命令可以看到CONTRIBUTING.md文件确实从stage区中移出来了。
要注意,git reset命令可能十分危险,尤其是用户提供—hard标识时。然而,在上述的场景中,没有涉及到工作目录中的文件,相对还是安全的。
8.4.2 Unmodifying a Modified File
如果不希望保留用户修改的CONTRIBUTING.md文件?如何能够简单的取消对它的修改——将它恢复到上一次提交之前的版本(或初始化地克隆版本)?幸运地是,git status命令告知用户如何来完成这个内容。在上一个例子的输出中,未stage区的内容像这样:
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
需要注意的是git checkout --<file>是非常危险的命令。任何本地修改的内容都将会消失——Git只会使用stage区的内容或者上一个提交版本的内容替换掉它。如果你不是完全明白你要执行动作的意义,千万不要使用这个危险的命令。
如果用户想要保留自己修改的内容,但是希望现在从其中剔除。后面会介绍stashing以及分支操作,详细查看Git Branching章节。
需要牢记的是,提交到Git中的内容基本都是可以恢复的。即便是已经删除的分以或通过—amend覆写的提交也是可以被恢复的。然而,任何尚未提交的内容的丢失都永远不能恢复了。
8.4.3 Undoing things with git restore
Git版本2.23.0介绍了一个新的命令:git restore。这是git reset命令的替代命令。在Git 2.23.0版本以上,Git将会使用git restore而不是git reset来实现撤销操作。
让我们来使用git restore命令来撤销操作吧。
8.4.3.1 Unstaging a Staged File with git restore
后面两节使用git restore命令来实现对stage区以及工作目录改变的撤销。前面提及到的查看这两个区的状态的命令也会提示如何通过git restore实现这个操作。例如,现在用户修改了两个文件,想要分别对这两个文件进行提交,但是却使用了git add *命令将两个文件同时加入了stage区。如何将其中一个文件从stage区中剔除呢?git status命令将会提示这个信息:
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: CONTRIBUTING.md
renamed: README.md -> README
在 Changes to be committed 下面,提示使用git restore –staged <file> 来讲文件从stage区移除,现在让我们使用这个命令来将CONTRIBUTING.md文件从stage区中移出来:
$ git restore --staged CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
现在CONTRIBUTING.md文件已经从stage区中移出来了。
8.4.3.2 Unmodifying a Modified File with git restore
现在用户不再想要CONTRIBUTING.md文件中修改过的内容,git status命令再次告知用户如何实现这个动作,上一个例子中的打印中有如下内容:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
它告知用户如何明确地丢弃用户做出的修改。
$ git restore CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: README.md -> README
这个命令同样是十分危险的。
8.5 Working with Remotes
为了参与到Git项目中,用户需要明白如何管理自己的远程仓库。远程仓库是用户在网络上托管的项目版本。用户可以拥有多个项目,不同的项目对于用户而言可以是只读的或是可读可写的。与他人合作管理这些远程仓库涉及到向共享网络推送数据以及从共享网络拉取数据。管理远程仓库包括如何添加远程仓库,移除无效的仓库,管理不同的远程分支以及它们是否被跟踪等。在本节,我们将会介绍进行远程管理的技巧。
注意,远程仓库可以位于用户本地设备上。
8.5.1 Showing Your Remotes
为了查看远程服务器的配置,用户可以运行git remote命令。这个命令将会列出用户指定的每一个远程句柄的短名。如果用户克隆了自己的远程项目,需要至少可以看到origin——它是Git给服务器的默认名:
$ git clone https://github.com/schacon/ticgit
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 268.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.
$ cd ticgit
$ git remote
origin
用户可以指定-v选项,它将展示Git远程存储进行写或读时使用的URLs:
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
如果用户具有多个远程仓库,这个命令将会列出所有的内容。例如,具有多个用于处理多个协作者的远程仓库可能像如下展示的一样:
$ cd grit
$ git remote -v
bakkdoor https://github.com/bakkdoor/grit (fetch)
bakkdoor https://github.com/bakkdoor/grit (push)
cho45 https://github.com/cho45/grit (fetch)
cho45 https://github.com/cho45/grit (push)
defunkt https://github.com/defunkt/grit (fetch)
defunkt https://github.com/defunkt/grit (push)
koke git://github.com/koke/grit.git (fetch)
koke git://github.com/koke/grit.git (push)
origin git@github.com:mojombo/grit.git (fetch)
origin git@github.com:mojombo/grit.git (push)
这意味着我们可以拉取任意的用户仓库。我们可能对这些内容拥有拉取或推送的权限。
8.5.2 Adding Remote Repositories
我们已经提及了git clone命令如何隐式地将origin远程仓库添加到本地仓库。下面是如何显式的添加一个远程仓库。为了添加一个新的远程Git仓库作为一个可以简单引用的短名,运行git remote add <short-name> <url>:
$ 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了。例如,如果用户想要从远程仓库中提取Paul已经拥有但是自己还没有的内容,可以运行git fetch pb命令:
$ 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
Paul的master分支现在可以在本地作为pb/master访问到了——用户可以将它合并到自己的一个分支中,或者当你想要检查它时,可以检出本地分支。
8.5.3 Fetching and Pulling from Your Remotes
就像刚才看到的一样,从自己的远程项目中获取数据可以运行:
$ git fetch <remote>
这个命令去到这个项目,并拉取自己还没有的数据内容。在做完这个操作之后,用户应该具有了来自该远程项目的所有的分支,可以随时进行合并或检查。
如果用户克隆一个仓库,命令将会自动在origin下添加那个远程仓库。因此git fetch origin将会提取所有在克隆后被推送到这个服务器的新的工作。需要注意的是,git fetch命令只将数据下载到本地仓库——它并不会自动与用户的工作合并或调整当前工作的内容。用户必须手动的将它合并到工作中。
如果用户当前的分支设置用来跟踪一个远程分支(查看下一章节),可以使用git pull命令来自动提取并将远程分支合并到用户当前分支中。这也许是更加舒服简单的工作流程;默认情况下,git clone命令自动设置用户本地master分支跟踪远程的master分支(不仅仅限定在master分支上,什么分支都可以)。运行git pull将从克隆源提取数据,自动尝试合并到本地分钟中。
自git 2.27开始,如果pull.rebase变量没有设置,将会产生一条警告信息。直到设置了这个变量。
如果用户想要默认的git行为:git config –global pull.rebase “false”
如果在拉取时rebase:git config –global pull.rebase “true”
8.5.4 Pushing to Your Remotes
当用户的工程进行到一个想要共享的阶段,必须要向上游推送。这个命令是git push <remote> <branch>。如果想要将用户的master分支推送到原本的服务器(克隆的时候自动设置的内容),那么可以执行下面的命令,将自己的工作推送到服务器侧:
$ git push origin master
这个命令只有在用户对该服务器具有写权限,且当前没有其他人正在进行推送,才可以正常工作。如果用户与其他人在同一时间点进行了克隆,那个人进行了推送操作,之后你又进行了推送操作,那么你的推送将会被拒绝。你必须要先从服务器提取他们的工作并将这个工作合并到自己的工作内容中,之后才可以进行推送。
8.5.5 Inspecting a Remote
如果用户想要查看指定远程服务器的信息,那么可以使用git remote show <remot>命令。如果给定一个短名来执行这个命令,比如origin,将会获取到类似如下的信息:
$ git remote show origin
* remote origin
Fetch URL: https://github.com/schacon/ticgit
Push URL: https://github.com/schacon/ticgit
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
它列举出远程仓库的URL以及跟踪的分支信息。这个命令帮助用户知晓自己是否在master分支,运行git pull它将会自动将远程的master分支的内容合并到本地的分支。它也列出了已经拉取了的所有的远程引用。
上面是一个简单的例子。如果你是一个Git重度使用用户,那么当执行git remote show命令的时候,可能看到如下更为复杂的内容:
$ git remote show origin
* remote origin
URL: https://github.com/my-org/complex-project
Fetch URL: https://github.com/my-org/complex-project
Push URL: https://github.com/my-org/complex-project
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
markdown-strip tracked
issue-43 new (next fetch will store in remotes/origin)
issue-45 new (next fetch will store in remotes/origin)
refs/remotes/origin/issue-11 stale (use 'git remote prune' to remove)
Local branches configured for 'git pull':
dev-branch merges with remote dev-branch
master merges with remote master
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 push之后自动推送到远端。它也告知用户哪些服务器上的远程分支用户尚未使用,哪一个用户拥有的分支已经从远程服务器删除,以及多个本地分支在执行git pull之后可以自动与本地的分支合并。
8.5.6 Renaming and Removing Remotes
用户可以运行git remote rename命令来改变远程仓库的短名。例如,如果用户想要将远程仓库pb的名字改为paul,可以这样运行:
$ 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
一旦用户以这个方式删除了远程的引用,所有远程跟踪分支以及相关的配置都被删除。
8.6 Tagging
像大多数的VCSs,Git有能力为仓库的某个历史节点添加标签。通常情况是,使用这个功能来标记发行版本节点。
8.6.1 Listing Your Tags
在Git中列出现存的标签是简洁明了的,只需通过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
8.6.2 Creating Tags
Git支持两种类型的标签:lightweight以及annotated。
一个轻量级的标签非常像一个不再改变的分支——它仅仅是一个特殊提交的节点。
一个注释型的标签,作为完全的对象存储在Git数据库中。它们是经过校验的,包含标签名,email,日期,标签信息可以被GPG(GNU Privacy Guard)签名验证。一般推荐用户创建注释型标签,这样包含所有的这些信息。如果用户需要一个临时标签,或不希望保有其他的信息,那么也可以使用轻量级的标签。
8.6.3 Annotated Tags
Git创建一个注释型的标签十分简单,最简单的方式是在执行git tag命令的时候使用-a选项:
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.1
v1.3
v1.4
这个-m指定标签信息,它与标签一起存储。如果用户不为注释型标签指定这个信息,Git将会启动编辑器,用户可以在其中键入这个内容。
用户可以通过git show指定要查看的标签,查看提交信息:
$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
这个信息展示了标签信息,标签提交的日志,并在提交信息之前展示注释型信息。
8.6.4 Lightweight Tags
另一个贴标签的方式是贴一个轻量级的标签。这是个基础的提交信息——没有其他信息。为了创建一个轻量级的标签,不要使用任何的-a,-s或-m选项,只需要提供要使用的标签就好:
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
此时,如果运行git show并给定标签,不会查看到额外的标签信息。这个命令仅仅展示提交的信息:
$ git show v1.4-lw
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
Change version number
8.6.5 Tagging Later
用户也可以在提交过很久之后为这次提交贴标签,假设用户提交历史看起来像如下内容:
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 Create write support
0d52aaab4479697da7686c15f77a3d64d9165190 One more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc Add commit function
4682c3261057305bdd616e23b64b0857d832627b Add todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a Create write support
9fceb02d0ae598e95dc970b74767f19372d61af8 Update rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc Commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a Update readme
现在,假设用户忘了在Update rakefile中贴版本v1.2的标签。可以现在为它贴上标签,用户在git tag -a v1.2之后指定要贴标签的校验和:
$ git tag -a v1.2 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 -0800
version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <mchacon@gee-mail.com>
Date: Sun Apr 27 20:43:35 2008 -0700
Update rakefile
...
8.6.6 Sharing Tags
默认情况下,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
如果用户具有很多标签想要一次推送给远程仓库,可以在git push中使用-tags选项。它将会推送所有新的标签给远程仓库:
$ 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 push <remote> --follow-tags那么只会推送注释型标签给远程仓库。
8.6.7 Deleting 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>
8.6.8 Checking out Tags
如果用户想要查看标签指定的版本,可以使用git checkout命令来查看这个标签,这个命令将会使你的仓库进入到一个detached HEAD状态,这个状态具有很多坏的副作用:
$ git checkout v2.0.0
Note: switching to 'v2.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 -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 99ada87... Merge pull request #89 from schacon/appendix-final
$ git checkout v2.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
处于detached HEAD状态,如果用户做出了修改并且提交了修改,标签依旧没有改变,你新提交的内容不属于任何分支,且只有通过提交产生的散列才能够访问这个版本。因此,如果你确实需要对老的版本做出修改——比如你修复了一个老版本的bug,这样你可以创建一个分支:
$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
如果你这样做了,并进行了提交,你的version2分支与v2.0.0标签是大大不同的,因为它将会基于这个分支节点持续向前,所以谨慎使用。
8.7 Git Aliases
在我们进入到下一章之前,我们想要介绍一下命令的别名。用户可以通过使用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 ci就等于使用git commit。
这个命令也可以为用户创建自己想要使用的命令。
8.8 Summary
现在,你可以使用Git的所有的基础操作了——创建、克隆仓库,做出修改,提交这些改变,查看修改提交历史。
9 Git Branching
几乎每一个VCS都支持某种形式的分支。分支是主干的一个分叉,可以在这个分叉上做开发而不弄乱主干。在一些VCS工具中,实现这个功能可能是昂贵的过程,通常需要用户创建一个源的新的副本,耗时又劳力。
一些人将Git分支模型作为它的杀手级特性,这个特性也使Git在VCS中特立独行。为什么它这么特别呢?因为Git分支是意想不到的轻量,创建分支的操作是即刻完成的,在分支之间进行切换也是十分迅速的。不想其他的VCSs,Git鼓励工作流中反复的创建分支以及合并分支。了解并掌握这个特性给到用户一个强有力的工具,并完全改变你的开发方式。
9.1 Branches in a Nutshell
为了完全理解Git建立分支的方式,我们需要退回一步,来看看Git是如何存储它的数据的。
你也许还记得What is Git?小节,Git存储数据并不是以一系列的修改的变化集,而是一系列的快照。
当用户进行了一次提交,Git存储包含用户stage的内容的快照的提交对象。这个对象也包含作者的名称以及email地址,用户键入的信息,以及指向这次提交的父类,0父类为初始化提交,一个父类为一个常规提交,多个父类是对多个分支的合并。
为了使这个过程可视化,让我们假设用户有一个目录包含三个文件,并且你stage了它们且提交了它们。stage文件为每一个文件计算出了校验和,将文件的这个版本存储到Git仓库,添加校验和到stage区:
$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'
当运行git commit进行一次提交操作,Gti对每一个子目创建校验和(在本例中,只对根目录做这个操作),并将它们存储到Git仓库的树对象中。Git之后创建一个包含元数据以及指向根项目树的指针的提交对象,这样,在需要时,就可以重新创建这个快照了。
用户的Git仓库现在包含5个对象:三个blobs(每一个代表着三个文件中的一个的内容),一个树可以列出目录的内容并指出哪个blob中存储了哪个文件名,以及一个指向根树的指针以及所有的提交元数据:
如果用户做出了一些修改,并且再次提交,下一个提交存储了直接指向它的指针:
在Git中的分支只是一个轻量级的指向这些提交的可移动指针。在Git中,默认分支名为master。在你开始做提交时,给到你master分支指向你最新的提交。每一次你做出提交,主分支指针自动向前移动。
Git中的master指针不是一个特殊的分支。它与其他分支一样。每一个仓库具有一个master分支的原因是git init命令会默认创建这个分支,且用户并不会特意去修改这个默认情况。
分支以及它们的提交历史的拓扑图如下:
9.1.1 Creating a New Branch
当用户创建一个分支时发生了什么?创建分支时,将会创建一个新的指针用来移动。让我们创建一个新的分支名为testing。可以通过git branch命令来创建:
$ git branch testing
这条命令创建当前提交节点的新指针。
Git是如何清楚你现在所在的分支呢?它保有一个特殊的指针名为HEAD。注意到这个HEAD与之前你所使用的VCSs中的HEAD是不同的内容,比如SVN,又如CVS。在Git中,这是指向你当前所在的分支的指针。在这个情况下,你依旧处于master分支中。git branch命令只会创建一个新的分支——它并不会切换到这个新分支。
用户可以通过git log命令来展示你所处的分支。这个选项为—decorate:
$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit
你可以看到master以及testing分支。
9.1.2 Switching Branches
使用git checkout命令来切换到一个存在的分支。让我们现在切换到testing分支:
$ git checkout testing
这个命令将HEAD移动指向testing分支。
这有什么意义呢?现在让我们做另一个提交:
$ vim test.rb
$ git commit -a -m 'made a change'
很有趣的是,因为你的testing分支向前移动了,而你的master分支保持在提交之前没有移动。现在再次切换回master分支:
$ git checkout master
注意,git log并不会在所有时都展示所有的分支。如果现在你运行git log命令,你可能想要看看刚才创建的testing分支到哪里了,却没有在输出中出现。
这个分支并没有消失,Git并不知道你对这个分支感兴趣,它只会尝试展示你感兴趣的东西。换言之,它只会展示你检出的当前分支下的历史信息。
为了展示某个分支的提交历史,可以明确指定要展示的分支git log testing,如果要展示所有的分支信息,可以使用—all到git log命令。
git checkout master命令做了两个工作。它将HEAD指针移动回到master分支,并将用户工作目录的文件回滚到哪个版本的快照。它回滚回到原来的版本,以方便你进行另外的工作。
现在让我们再次做出修改,并进行提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
现在你的项目历史改变了。你创建了分支,切换到了那个分支并做了一些工作,之后又切换回到原来的版本,做了其他的工作。这些改变在不同的分支之间是隔离的:你可以在不同的分支之间进行切换,也可以将它们进行合并。
如果现在运行git log –oneline –decorate –graph –allit,将会打印用户的提交历史,展示分支指针指向何处,以及历史是如何分叉的:
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Made other changes
| * 87ab2 (testing) Made a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
因为Git中的分支实际上是一个包含指向提交的40个SHA-1校验和的文件,分支的创建与删除都是十分廉价的。创建一个新的分支就只是简单的向一个文件写入41个字符(40字符外加一个新行)。
这对比老版本的VCS工具的分支,可以说是十分锋利了。
如何在创建分支的同时切换到这个分支呢?可以通过如下操作git checkout -b <newbranchname>。自Git 2.23版本以上,用户可以使用git switch而不是git checkout了:
- 切换到一个存在的分支:git switch testing-branch
- 创建一个新的分支,并切换到这个分支:git switch -c new-branch
- 返回到之前检出的分支:git switch -
9.2 Basic Branching and Merging
让我们试试实际工作中可能使用的分支与合并的工作流吧。你需要遵从如下几个步骤:
- 在一个网络站点上做一些操作
- 创建一个新的用户故事分支
- 在这个分支上做一些工作
在这个阶段,你接到一个电话,另一个问题十分紧急,必须要进行一次热修复,你将会做如下工作:
- 切换回到产品分支
- 创建分支来添加热修复
- 测试之后,合并热修复,推送产品
- 切换回到原来的用户故事继续工作
9.2.1 Basic Branching
首先,你在你的工程上工作,并已经在master分支上进行了多次提交:
现在你决定解决问题#53(仅仅是一个代号,表明一个问题)。通过git checkout -b命令,创建并切换到这个新的分支:
$ git checkout -b iss53
Switched to a new branch "iss53"
等效于执行:
$ git branch iss53
$ git checkout iss53
你开始在这个分支上进行工作,并进行了一些提交。这样做导致#53的分支开始生长:
$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'
现在你接到电话,网络站点存在一些问题急需修复。因为有Git,你不需要在#53分支上部署你对这个急切问题的修复,也不需要复杂的操作回滚到原来的版本。你需要做的只是切换回到master分支。
然而,在你切换回到master分支之前,如果你的工作目录或者stage区具有未提交的内容与master分支冲突,Git将不会让你切换分支。最好在切换主分支之前拥有一个干净的工作区。后面我们再细说怎么解决。现在,我们假设你已经提交了所有的修改内容,因此,你可以切换回到你的master分支:
$ git checkout master
Switched to branch 'master'
此时,你的项目工作目录是你切换到#53之前的工作目录了,这样你可以开始进行这次热修复了。这是重要的一点:当你切换分支,Git将你的工作目录重置到你在该分支做的最近一次提交时的状态。它将自动增、删、改写文件来确保你的工作目录是这个样子。
下面,你开始进行你的热修复。创建了一个热修复分支:
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Fix broken email address'
[hotfix 1fb7853] Fix broken email address
1 file changed, 2 insertions(+)
现在对这个热修复,运行测试,以确保修复了问题。最后将热修复分支合并到你的主分支来部署这个产品。通过git merge命令来实现:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
你应该注意到了在合并之后上面打印中出现的的 Fast-forward 语句。因为本次提交的C4是基于你所在的C2,Git将指针基于C2向前移动。以另外一种方式进行解释,当用户尝试提交一个直接指向之前版本的合并,因为没有冲突的内容,Git只需要简单地向前移动指针就可以合并了——这称为 fast-forward。
用户现在修改的内容快照直接指向了master分支,现在可以部署这个修复的版本了。
在这次超级重要的修复部署之后,用户可以切换回到之前的工作内容了。之前为了进行热修复,直接创建了一个分支,因为这个分支已经合并到了master分支,现在可以直接删除这个为了热修复创建的分支了——master分支指向了相同的位置。可以通过-d选项删除这个分支:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
现在用户可以切换回到之前的#53问题的分支继续工作了:
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Finish the new footer [issue 53]'
[iss53 ad82d7a] Finish the new footer [issue 53]
1 file changed, 1 insertion(+)
需要提一下,用户在热修复时候改动过的文件并不会出现在#53分支中。如果希望将这个热修复的分支内容合并到#53问题分支,可以运行git merge master命令,或者你可以一直等到#53问题完成后,将其中的修改合并到master分支。
9.2.2 Basic Merging
假设现在#53已经完成,并且已经可以合并进入到master分支。为了实现这一操作,会将#53分支合并到master分支,就像之前合并热修复分支一样。你要做的只是检出要合并的分支,之后执行git merge命令:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
这看起来与合并热修复分支时有点不同。在本次合并的情况,开发历史已经因为热修复而变化了。因为要提交到的那个点不是待合并分支的直接祖先,Git必须做一些工作。在这个情况下,Git使用master分支与#53分支的共同祖先,以及这两个分支,进行了一个三向合并。
Git此时创建基于本次三向合并的的快照并自动基于此创建一个指向它的新的提交,而不是在当前的分支上简单地生长。这涉及到合并提交合并提交,它的特殊之处在于它有多个父类。
现在你的工作已经合并了,用户不再需要#53问题的分支了,可以关闭这个问题,并删除这个分支了:
$ git branch -d iss53
9.2.3 Basic Merge Conflicts
有时候,合并过程并不总是一帆风顺的。如果用户修改了待合并的两个分支中相同文件的相同部分,Git并不能够干净地将这两个分支合并到一起。如果你在#53分支中修改了热修复分支的相同内容,在进行合并时会看到合并冲突:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git并没有自动创建新的合并提交。它暂停了合并过程,等待用户修复这个冲突。如果想要查看在冲突之后哪些文件没有合并,可以使用git status命令:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何合并冲突,且没有被解决的内容在unmerged中列出。Git添加冲突待解决标记,用户可以手动打开它们并解决这个冲突。你的文件包含类似如下的片段:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
上面例子中,所有HEAD中的版本(因为合并时处于master分支,所以HEAD为master分支)都处于该块的头部(所有在=======之前的内容),而所有#53分支的内容都处于该块的尾部。为了解决冲突,用户可以选择任何一侧或者自己合并这个内容。例如,可以将整块内容修改为:
<div id="footer">
please contact us at email.support@github.com
</div>
这个内容将会包含在每一个部分,且移除了<<<<<<<,=======以及>>>>>>>行。在解决完冲突文件的这些部分之后,运行git add将这些文件标记为已经解决的内容,将这个文件提交打stage区,告知Git这个冲突已经解决了。
如果你想要使用一个图形化的工具来解决这个问题,可以运行git mergetool,它将会启动一个可视化工具,并想你展示这些冲突内容:
$ git mergetool
This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html
Normal merge conflict for 'index.html':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):
如果你想要使用自己的合并工具,而非默认的工具,用户可以在 one of the following tools 打印下面看到支持的工具。只需要键入要使用的工具名即可。
在你退出合并工具之后,Git将询问是否成功地进行了合并。如果你告知脚本已经解决了冲突,那么它将会将文件放入stage区,视它们已经解决。现在用户运行git status,可以看到冲突地内容已经解决了:
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: index.html
如果你满意这个结果,并且已经检查到所有冲突地内容已经处于stage区,可以使用git commit实现最后地合并提交工作。默认地提交信息看起来像是下面这样:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#
9.3 Branch Management
现在用户已经创建过、合并过、删除过一些分支了。现在让我们看一些分支管理工具。
命令git branch不只是创建、删除分支。如果用户在使用这个命令时不在后面追加任何信息,只会得到当前分支地列表:
$ git branch
iss53
* master
testing
注意到 * 符号在master分支地前面:它指明用户当前所在地分支(即HEAD指向的分支)。这意味着如果现在用户进行提交,master分支将会因为新的工作内容得到生长。查看每一个分支的最后提交,可以运行git branch -v:
$ git branch -v
iss53 93b412c Fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 Add scott to the author list in the readme
实用地—merged以及—no-merged选项将会从这个列表中滤出有后没有合并到用户当前所在分支的分支。比如:
$ git branch --merged
iss53
* master
因为你已经将iss53合并到主分支,可以看到它出现在已经合并的列表中了。在这个列表中的不带*的分支可以安全地通过git branch -d删除了。它们中的内容已经进入到其他分支中了,因此删除这些分支并不会丢失任何内容。
查看没有合并的分支:
$ git branch --no-merged
testing
这将会展示其他的分支。因为它包含尚未合并的工作,尝试使用git branch -d删除这个分支,将会失败:
$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.
如果用户确实希望删除这个分支,丢弃这些工作,可以使用-D选项来强制删除。
9.3.1 Changing a branch name
不要修改其他合作者正在使用的分支名。不要在没有阅读 ”修改master分支名” 的内容时对master/main/mainline这些进行重命名。
假设用户有一个分支名为bad-branch-name且希望将它改为correct-branch-name,并保留所有的历史。而且还想要修改远程服务器上的分支名(GitHub,GitLab或其他服务器),如何进行操作呢?
修改本地的分支名可以使用git branch –move命令:
$ git branch --move bad-branch-name corrected-branch-name
这个命令将会修改bad-branch-name为correct-branch-name,在现在这个修改只是针对本地的。如果想要其他人也看到分支被修改了,将它推送到远端:
$ git push --set-upstream origin corrected-branch-name
现在我们可以看看我们所在的位置了:
$ git branch --all
* corrected-branch-name
main
remotes/origin/bad-branch-name
remotes/origin/corrected-branch-name
remotes/origin/main
注意到,现在用户处于correct-branch-name分支,且它现在可以在远程代理上访问到。然而,bad-branch-name分支依然存在,但是可以通过执行如下命令删除它:
$ git push origin --delete bad-branch-name
现在bad-branch-name分支完全被correct-branch-name分支替代了。
修改master分支名:
在进行这个工作之前,要确保与所有的合作者进行过沟通。并且要确保对仓库、引用以及老的分支、脚本进行过彻底地调查。
将本地的master分支修改为main名:
$ git branch --move master main
现在本地不再有master分支了,因为它已经变为main分支了。
要让其他人看到这个main分支,需要将它推送到远端:
$ git push --set-upstream origin main
现在查看分支状态:
git branch --all
* main
remotes/origin/HEAD -> origin/master
remotes/origin/main
remotes/origin/master
本地的master分支已经消失了,它已经被main分支替代。main分支出现在远端。然而,老的master分支依旧在远端。其他合作者可以继续使用这个master分支作为开发分支,直到你做出其他改变。
现在要完成转换,需要完成如下的工作:
- 任何依赖于master的项目需要更新它们的代码、配置
- 升级所有测试运行配置文件
- 调整构建以及发行脚本
- 重定向仓库托管如仓库的默认分支、合并规则以及其他匹配分支名的内容
- 更新文档中对旧分支的引用
- 关闭或合并所有对旧分支的拉取请求
在完成这些工作后,并确定main分支与master分支表现一致后,可以删除master分支:
$ git push origin --delete master
9.4 Branching Workflows
现在用户有了分支以及合并的基础知识。我们能或应该对它们做什么呢?在本节,我们将会介绍一些轻量级的分支管理工作流。
9.4.1 Long-Running Branches
因为Git使用简单的三向合并,经常性地将一个分支合并到另一个分支是可行的。这意味着用户可以具有多个开放的分支,在不同的阶段使用。用户可以将它们与其他分支合并。
一些Git开发者拥抱这个过程,比如在master分支中仅保留稳定版本的分支。在其他的并行分支中来进行开发或进行稳定性测试的内容,它们可以在后续合并到master分支中。
在实际中,我们谈论的是指针在提交过程中向前移动,稳定的版本处于老旧的提交历史中,更加激进的分支出现在最新的历史中。
可以将这个过程看作是发射井,稳定的发射井是久经测试的。
用户可以基于此做不同等级的稳定性测试。一旦一个版本的稳定性达到了一定层级,可以将它合并到上一个稳定的版本中。
9.4.2 Topic Branches
主题分支,是在任何量级的项目中都可以使用的内容。一个主题分支的生命周期很短,可以用创建它来实现单个的特性开发。这在其他的VCSs工作中可能想都不敢想,因为其他的VCS创建、合并分支十分昂贵。但在Git中,这是十分常见的内容。
上一节中我们已经看到了对分支iss53以及热修复分支的使用。(例子略了,大家应该都懂)。这个过程中所有的工作都是在本地完成的,不需要与远端服务器进行沟通。
9.5 Remote Branches
略
9.5.1 Pushing
9.5.2 Tracking Branches
9.5.3 Pulling
9.5.4 Deleting Remote Branches
9.6 Rebasing
在这里,我将Rebase翻译为重置基址。
在Git中,两个主要的方法可以用来将另一个分支中的内容整合到另一个分支中:merge以及rebase。本节将会介绍什么是重置基址,如何重置基址,什么时候可以使用,什么时候不可以使用。
9.6.1 The Basic Rebase
回到前面的Basic Merging小节中,你可以看到,可以分叉工作,并提交内容到两个不同的分支。
最简单的整合两个分支的方法,就是我们已经提到了的merge命令。它实现三向合并,创建一个新的快照。
现在有另外一种方式,你可以将C4中改动的内容向C4打补丁。在Git中,称为rebase。通过这个命令,用户可以将提交到一个分支的内容,应用到另外的分支中。
例如,用户检出experiment分支,之后将它rebase到master分支:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
这个操作将回到两个分支的共同祖先,对比两个提交的不同的内容,保存这些区别到一个临时文件,重置当前的分支到想要rebase的目标分支上,最后将改变应用到目标分支上。
此时,用户可以回到master分支中,并进行一次fast-forward合并:
$ git checkout master
$ git merge experiment
此时,这个指向C4’的快照与之前使用merge合并时的C5快照是一样的。整合的结果并没有什么不同,但是rebase将会产生一个干净的历史。如果用户检查rebase过后分支的日志,它展示的是一个线性的历史:即所有的工作都是线性发生的,即使原初的工作是并行进行的。
通常,用户使用这个方法来保证应用到远程分支的提交是干净的——也许是尝试提交到不是自己维护的项目的工程。在这个例子中,用户在一个分支中完成工作,之后在完成工作想要提交时,将内容rebase到master分支中。这个方式下,维护者不需要做任何的整合工作——这个合并是一个fast-forward干净地应用。
注意,无论使用merge进行合并工作还是使用rebase进行合并工作,最终得到的快照都是一样的。唯一的不同点是它们的历史日志是不同的。
9.6.2 More Interesting Rebases
考虑一个更加复杂的情况,你维护了三个分支master、server、client。
现在你决定将client分支的内容应用到master分支中,并想要继续保有server一侧的修改,只将client与server一侧不同的部分合并到master分支中(C8,C9),可以使用git rebase –onto master server client命令来实现:
$ git rebase --onto master server client
这个命令告知Git,使用client分支,提取出与server分支不同的部分,并将C8、C9看作是直接从master中分支出来的一样,将补丁打到master分支中。虽然有点复杂,但是十分有趣。
现在,用户可以fast-forward合并master分支了:
$ git checkout master
$ git merge client
现在,假设需要将server分支中的内容合并到master分支中。用户可以直接使用git rebase <basebranch> <topicbranch>命令来直接rebase分支,而不需要检出该分支。它将检出topicbranch (server)的内容,应用到basebranch(master):
$ git rebase master server
这个rebase的结果看起来像是下图:
此时,又可以进行fast-forward合并了:
$ git checkout master
$ git merge server
现在可以删除client以及server分支了,因为所有内容都已经整合到master分支了:
$ git branch -d client
$ git branch -d server
此时最终的结果类似于下图:
9.6.3 The Perils of Rebasing
这个功能并不是毫无缺点的。以一句化总结:不要在自己仓库的外部进行rebase提交,且不要在其他人正在工作的内容上实施rebase。
让我们看一下rebase是如何产生问题的。假设用户从一个中央服务器克隆了一个仓库,并基于此做了一些工作。用户的提交历史看起来像是下图:
现在,其他人做了一些工作,包括合并、提交工作到中央服务器。你提取了新的内容,并将新内容合并到自己工作中,现在查看历史,像是下图:
下面,进行过推送合并操作的用户,决定回退并rebase他的工作,执行了一个git push操作,强制覆写了服务器的历史。用户之后从服务器提取(fetch)这个新的内容,此时你本地仓库的历史内容拓扑像是下图:
此时,你们两位用户处于进退两难的境地。如果你执行git pull操作,将会创建包含这两个历史的合并提交,你的仓库将会看起来像下图:
如果你的仓库看起来是这样,执行git log命令,将会看到具有相同作者、日期以及信息的两次提交,这可能使人疑惑。如果你现在想要提交这个历史到服务器,将会向中央服务器引入这些rebase提交。
9.6.4 Rebase When You Rebase
如果用户处于这一境地,Git提供了一些方法可能帮助到用户。
略。
9.6.5 Rebase vs. Merge
略。
9.7 Summary
略。
10 Git on the Server
此时,用户应该已经有能力使用Git实现常见的任务。然而,如果想要通过Git实现与其他用户的合作,用户需要拥有一个远程Git仓库。虽然技术上可以通过个人仓库实现push、pull操作,这样做可能不小心导致一些问题。而且,用户可能希望其他用户在自己的设备下线时依旧能够访问到仓库——拥有一个更加靠得住的公用仓库是十分有用的。因此,最好的方式是设置一个中间仓库,用户与合作者都可以访问,进行push、pull操作。
运行Git服务是十分直观的。首先,用户选择服务器需要支持的协议。在第一节,将会介绍各个支持的协议,以及这些协议中的优缺点。下一节将会解释一些典型的设置。最后将会介绍一些代理的选择。
如果用户对运行自己服务器不感兴趣,用户可以跳过最后一节。
一个远程仓库通常是一个裸的仓库——Git仓库并没有工作目录。
10.1 The Protocols
Git可以使用四种不同的协议来传输数据:本地、HTTP、Secure Shell(SSH)以及Git。我们将会讨论它们是什么,以及什么情况下我们要使用它们。
10.1.1 Local Protocol
最基础的协议是本地协议,这个协议下,远程仓库处于相同主机的不同文件中。这通常出现在组内所有人访问一个共享文件系统如NFS挂载,或者是每一个人可以登录同一台电脑。后面这个情况是不推荐的,因为所有的代码都放在同一台设备上,可能会导致数据丢失的风险。
如果用户具有一个共享挂载文件系统,那么可以基于本地文件仓库实现克隆、push、pull操作。为了以这种方式实现仓库克隆,或者添加一个作为远端到现存的工程中,使用到这个仓库的路径作为URL。例如,克隆一个本地仓库,使用如下命令:
$ git clone /srv/git/project.git
或者,可以使用如下命令:
$ git clone file:///srv/git/project.git
如果指定file://那么Git操作将会与第一个例子有些区别。如果用户只是指定路径,Git尝试使用硬链接或目录复制它需要的文件。如果指定file://,Git调起常规使用网络时的传输数据过程,这有时候是低效的。指定file://的主要原因是,如果用户想要仓库的干净复制,而将额外的引用或对象排除出去。这里我们会一直使用常规的路径,因为这个方法很快。
为了添加一个局部的仓库到一个现存的Git项目,可以运行如下的命令:
$ git remote add local_proj /srv/git/project.git
现在,用户可以通过新的远程名local_proj实现push、pull操作了,虽然这个方法通常是通过网络进行的。
10.1.1.1 The Pros
基于文件的仓库的好处是,它们简单,它们使用现有的文件权限以及网络许可。
10.1.1.2 The Cons
缺点可能是共享方式是比较复杂的。共享文件系统比较难配置,比起基础的网络连接访问方式,不便于从多地访问。比如,如果想要从家中push内容,必须先挂载一个远程磁盘,相比于网络连接的访问方式,配置并不方便,速度也比较缓慢。
10.1.2 The HTTP Protocols
10.1.2.1 SmartHTTP
10.1.2.2 Dumb HTTP
10.1.2.3 The Pros
10.1.2.4 The Cons
10.1.3 The SSH Protocol
10.1.3.1 The Pros
10.1.3.2 The Cons
10.1.4 The Git Protocol
10.1.4.1 The Pros
10.1.4.2 The Cons