Memorykk

never too late to learn

Git

Git是目前世界上最先进的分布式版本控制系统(没有之一)。


目录


Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。最初由 Linus Torvalds 为了帮助管理 Linux 内核开发而花了两周时间自己用C写出来的。(理那厮·掏袜子真的太绝了⊙o⊙)

1. 版本控制

版本控制系统分为本地式、集中式和分布式。

  • 本地式:适合个人使用,用于记录文件更新。代表:RCS

  • 集中式:必须联网,断网后可编辑无法commit和回滚。版本库都存在中央服务器上,用户的本地只有自己以前所同步的版本,没有完整的版本库。代表:SVN、CVS、VSS。
    假设SVN服务器没了,那你丢掉了所有历史信息,因为你的本地只有当前版本以及部分历史信息。

  • 分布式:只有在push、pull时需要联网,断网后可正常commit。中央服务器只为了修改起来方便,可有可无,每个用户本地都是一个完整的版本库。代表:Git。
    每个人都同时对同一版本修改,commit操作提交到本地,多人协作时需要push操作和别人同步,但是团队人数多会导致非常麻烦,所以出现所谓的(便于修改的)中央服务器。
    假设GitHub服务器没了,你不会丢掉任何git历史信息,因为你的本地有完整的版本库信息,你可以把本地的git库重新上传到另外的git服务商。

Git的特点

  • 直接记录快照,而非差异比较
    Git 只关心文件数据的整体是否发生变化,而大多数其他系统则只关心文件内容的具体差异。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。有变化则保存,无则链接。

  • 近乎所有操作都是本地执行

  • 时刻保持数据完整性
    保存在 Git 数据库中的东西都是用此哈希值来作索引的,而不是靠文件名。

  • 多数操作仅添加数据

2. 安装Git

2.1. Linux

Git 的工作需要调用 curl,zlib,openssl,expat,libiconv 等库的代码,所以需要先安装这些依赖工具。
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel

$ apt-get install libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev
下载源码:http://git-scm.com/download
编译安装
$ tar -zxf git-1.7.2.2.tar.gz
$ cd git-1.7.2.2
$ make prefix=/usr/local all
$ sudo make prefix=/usr/local install

2.2. Linux

2.2.1. Debian/Ubuntu

$ sudo apt-get install git

2.2.2. Centos/RedHat

$ yum install git-core

2.3. Windows

官网下载
镜像下载
默认安装。

  • Git Bash:Unix与Linux风格的命令行(推荐
  • Git CMD:DOS风格的命令
  • Git GUI:图形界面(新手不推荐

打开Git Bash检查一下:

$ git --version
git version 2.29.2.windows.2

3. 配置Git

3.1. 配置用户名和Email

信息将会嵌入你的每次提交中。

配置级别

  • 系统级--system:系统中所有用户适用的配置
    • Linux:/etc/gitconfig
    • Windows:Git/mingw64/etc/gitconfig
  • 用户级--global:当前用户适用的配置
    • Linux:~/.gitconfig
    • Windows:C:/Users/Administrator/.gitconfig
  • 项目级--local:特定项目适用的配置
    • Linux:gitProject/.gitconfig
    • Windows:gitProject/.gitconfig

低级覆盖高级:local<globall<system

$ git config --global user.name "Your Name"
$ git config --global user.email "Your Email"

如果用了--global选项,所有项目都会默认使用这个全局配置,不加则为某个特定的项目信息。

3.2. 增删配置项

3.2.1. 添加配置项

git config [--local|--global|--system] section.key value

例如:

51139@DESKTOP MINGW64 /e/blog
$ git config --global Student.number 2018000001
51139@DESKTOP MINGW64 /e/blog
$ cat C:/Users/51139/.gitconfig
[user]
name = memorykkk
email = 511390937@qq.com
[Student]
number = 2018000001

3.2.2. 删除配置项

git config [--local|--global|--system] --unset section.key

3.3. 更多配置项

git config -l, --list     #list all    列出所有
git config -e, --edit #open an editor 打开一个编辑器
git config --global color.ui true #打开所有的默认终端着色
git config --global alias.ci commit #别名 ci 是commit的别名
user.name #用户名
user.email #邮箱
core.editor #文本编辑器
merge.tool #差异分析工具
core.paper "less -N" #配置显示方式
color.diff true #diff颜色配置
alias.co checkout #设置别名
git config user.name #获得用户名
git config core.filemode false #忽略修改权限的文件

学习config命令,运行 $ git help config

查看手册 git config

4. Git基本概念

  1. Git本地有三个工作区域:工作区(Workspace)、暂存区(Index/Stage)、仓库(Repository),加上远程仓库(Remote)共四个工作区域。

Git 各个命令可以理解为在各个仓库间转移数据,各个命令对应对每个仓库输入输出。

  • Workspace:工作区,平时能看到的存放项目代码的地方
  • Index / Stage:暂存区,临时存放改动,保存即将提交到文件列表信息的文件,有时也叫作索引
  • Repository:仓库区(或本地仓库),安全存放数据的位置,含所有版本的数据,HEAD指向最新版本
  • Remote:远程仓库,托管代码的服务器,用于远程数据交换
  1. 对于本地三个区域的关系如下:
    local_relation
  • Directory:使用Git管理的仓库,包含我们的工作空间和Git的管理空间

  • WorkSpace:需要通过Git进行版本控制的目录和文件组成工作空间

  • .git:存放Git管理信息的目录,初始化仓库的时候自动创建

  • Index/Stage:暂存区,或者叫待提交更新区,在提交进入repo之前存放所有的更新

  • Local Repo:本地仓库,HEAD会只是当前的开发分支(branch)。

  • Stash:隐藏,是一个工作状态保存栈,用于保存/恢复WorkSpace中的临时状态。
    观察命令对各区域的影响:
    git_theory

  • master 是 master 分支所代表的目录树,此时 HEAD 实际是指向 master 分支的一个”游标”,objects 标识的区域为 Git 的对象库,实际位于 “.git/objects” 目录下,里面包含了创建的各种对象及内容。

  • 当对工作区修改(或新增)的文件执行 git add 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。

  • 当执行提交操作 git commit时,暂存区的目录树写到版本库(对象库)中,master 分支会做相应的更新。即 master 指向的目录树就是提交时暂存区的目录树。

  • 当执行 git reset HEAD 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。

  • 当执行 git rm --cached <file> 命令时,会直接从暂存区删除文件,工作区则不做出改变。

  • 当执行 git checkout . 或者 git checkout -- <file> 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。

  • 当执行 git checkout HEAD . 或者 git checkout HEAD <file> 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

  1. 若对四个区域分级:workspace < index < repository < remote,基本的转移如下表,参数和选项决定数据来源。
低等级输入 高等级输入
workspace 手动 git checkout/git stash
index/stage git add git reset
repository git commit git pull
remote git push -

四个区域转换关系如下:
git_transition

5. Git工作流程

一般工作流程如下:

  • 在工作目录中修改某些文件。
  • 对修改后的文件进行快照,然后保存到暂存区域。
  • 提交更新,将保存在暂存区域的文件快照永久转储到 Git 目录中。

git-process

6. Git操作

6.1. Git创建版本库

  • 在工作目录中初始化新仓库
  • 从现有仓库克隆

6.1.1. 在工作目录中初始化

$ git init

在新建空目录或已有目录中,使用命令 git init 初始化一个新仓库。初始化后该目录下自动生成.git目录,用于生成所有 Git 需要的数据和资源。

文件加入版本库
将文件添加到仓库(workspace -> stage)

$ git add <file>

执行后没有任何信息则代表执行成功。

将文件提交到仓库(stage -> repository)

$ git commit -m <message>

多次add不同的文件,但只需一次commit就可以提交。

6.1.2. 从现有仓库克隆

$ git clone <url> <newName>
  • Git 收取的是项目历史的完整数据(每一个文件的每一个版本),而不是某个特定版本,服务器上有的数据克隆之后本地也都有了。
  • url 支持 git 协议或 SSH 传输协议。通常来说,Git协议下载速度最快,SSH协议用于需要用户认证的场合。
  • 克隆之后的目录名为 url 中 .git 前的名称,通过 <newName> 可以自定义。目录中包含.git目录,用于保存下载下来的所有版本记录,然后从中取出最新版本的文件拷贝。

6.2. 文件更新

6.2.1. 文件状态

文件状态主要分两种,已跟踪(tracked)即本来就被纳入版本控制管理的文件,其它文件即未跟踪(untracked)。

  • Untracked:未跟踪,此文件没有加入到git库,不参与版本控制。通过 git add 状态变为staged
  • Unmodify:文件已经入库,未修改,即版本库中的文件快照内容与文件夹中完全一致。这种文件被修改成为Modified,通过git rm移出版本库成为Untracked
  • Modified:文件已修改。这种文件通过git add可变成staged,通过git checkout则丢弃修改回到unmodify
  • Staged:暂存状态。通过git commit则将修改同步到库中,这时库中的文件和本地文件又变为一致,变为Unmodify状态。执行git reset HEAD <filename>取消暂存,变为Modified

初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为未修改(unmodified)。
文件状态变化周期:
file-status

修改后的文件状态是 modified,逐步放在暂存区域,最后一次性提交。

6.2.2. 检查文件状态

#查看指定文件状态
$ git status <filename>

#查看所有文件状态
$ git status

例如创建README之后:

$ vim README
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)

表示当前处在master分支,未跟踪文件里包含 README,即 Git 之前的快照中没有这个文件,未纳入管理。

6.2.3. 跟踪文件

(workspace -> stage;untracked -> staged)

# 添加指定文件到暂存区
$ git add <file1> <file2> ...

# 添加指定目录到暂存区,包括子目录
$ git add <dir>

# 添加当前目录的所有文件到暂存区
$ git add .

例如:

$ git add README
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#

“Changes to be committed:” 表示其下的文件处于暂存区

6.2.4. 暂存已修改文件

(untracked -> staged)
修改一个已跟踪的文件后

$ git status
# On branch master
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#

“Changes not staged for commit:” 表示该文件内容发生变化但还未放到暂存区,需要重新运行 git add 命令。
也就是说,文件修改之后必须再次 git add,否则 git commit 提交的是修改前版本。

$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#

git add 根据目标文件的状态产生不同的效果:

  • 开始跟踪新文件
  • 把已跟踪的文件放到暂存区
  • 合并时把有冲突的文件标记为已解决状态

6.2.5. 忽略某些文件

有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。有以下三种方式:

  1. 创建.gitignore文件

  2. 项目设置中指定排除文件
    临时指定。需要编辑当前项目中的 .git/info/exclude 文件

  3. 定义Git全局的 .gitignore 文件
    设置全局的.gitignore文件来管理所有Git项目的行为。创建.gitignore文件,存放在任意位置,然后使用命令 # git config --global core.excludesfile ~/.gitignore 配置。

格式规范如下:

  • 每行一个,空格不匹配任意文件,可作为分隔符,可用反斜杠转义
  • 所有空行或者以注释符号 开头的行都会被 Git 忽略
  • 可以使用标准的 glob 模式匹配:shell 所使用的简化了的正则表达式
  • 匹配模式最后跟反斜杠/说明要忽略的是目录下所有文件
  • 使用两个星号”**” 表示匹配任意中间目录
  • /结束的模式只匹配文件夹以及在该文件夹路径下的内容,但是不匹配该文件;/开始的模式匹配项目根目录
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号!取反。
  • 关于优先级等更多语法查看Git忽略提交规则 - .gitignore配置运维总结

一般来说每个Git项目中都需要一个“.gitignore”文件,告诉Git哪些文件不需要添加到版本管理中,实际项目中很多文件都是不需要版本管理的,例如日志、缩略图、敏感信息等。

例如:

# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt

6.2.6. 查看未暂存的更新

$ git diff

查看工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。
例如:

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end

+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size

6.2.7. 查看已暂存的更新

$ git diff --cached

查看已经暂存起来的文件和上次提交时的快照之间的差异

6.2.8. 提交更新

(staged -> commited;stage -> repository)

# 提交暂存区到仓库区
$ git commit -m <file>

# 提交暂存区的指定文件到仓库区
$ git commit <file1> <file2> ... -m <message>

# 提交工作区自上次commit之后的变化,直接到仓库区,跳过add,对新文件无效
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 如果我们提交后发现有个文件改错了,将修改过的文件通过"git add"后执行
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m <message>

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend <file1> <file2> ...

使用 git status 查看暂存区域状态,准备妥当之后就可以提交了。
这样看来暂存区的意义在于精心准备每次提交。

  • 直接使用不加 -mgit commit 提交会打开编辑器(通过 $ git config --global core.editor emacs 配置)以便输入本次提交的说明。
  • 使用-m选项比较方便。

例如:

$ git commit -m "Story 182: Fix benchmarks for speed"
[master]: created 463dc4f: "Fix benchmarks for speed"
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 README

显示了提交的分支(master)、SHA-1校验和(463dc4f)、修改过的文件数(2)和增(3)删行数

因为修改后但未暂存的处于已修改态(modified),只能纳入下一次版本。

每次提交都是对项目的一次快照,以后可以回退

6.2.9. 跳过使用暂存区域

$ git commit -a -m <message>

Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤,对新文件无效

6.2.10. 移除文件

  1. 从 Git 中移除文件

未跟踪文件对 Git 来说不存在,手动rm即可,已跟踪文件就必须要从已跟踪文件清单(stage)中移除,然后提交。可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。

$ git rm <file>

如果只是简单地从工作目录中手工删除文件,运行 git status 时就会在 “Changes not staged for commit” 部分(未暂存清单)看到。(判断为“修改”操作?)

  1. 从 Git 中移除,但仍然希望保留在当前工作目录
    (stage -> workspace;tracked:staged -> untracked)
    即不小心纳入仓库后,想要移除跟踪但不删除文件。
$ git rm --cached <file>

6.2.11. 移动文件

$ git mv <oldfileName> <newFileName>

Git 并不跟踪文件移动操作。如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。要在 Git 中对文件改名,执行git mv实际上相当于

$ mv old new
$ git rm old
$ git add new

6.3. 查看提交历史

$ git log

默认不用任何参数,git log 会按提交时间列出所有的更新信息。
例如:

$ git log
commit ca82a6dff817ec66f44342007202690a93763949 #SHA-1校验和
Author: Scott Chacon <schacon@gee-mail.com> #作者<邮箱>
Date: Mon Mar 17 21:52:11 2008 -0700 #时间

changed the version number #提交说明

选项

$ git log <option>
-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(后跟指定格式)。

6.3.1. 限制输出长度

$ git log <option>
-(n) #仅显示最近的 n 条提交
--since, --after #仅显示指定时间之后的提交。
--until, --before #仅显示指定时间之前的提交。
--author #仅显示指定作者相关的提交。
--committer #仅显示指定提交者相关的提交。

6.3.2. 使用图形化工具查阅提交历史

随 Git 一同发布的 gitk 相当于 git log 命令的可视化版本。

6.4. 撤销

任何已经提交到 Git 的都可以被恢复。你可能失去的数据,仅限于没有提交过的,对 Git 来说它们就像从未存在过一样。

6.4.1. 修改最后一次提交

# 如果我们提交后发现有个文件改错或没有加,将修改过的文件通过"git add"后执行
# 修改上一次commit的提交信息
$ git commit --amend -m <message>

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend <file1> <file2> ...

也就是说,在执行git commit之后暂存区为空,此时若再次执行git commit --amend判断为修改上次的提交信息;此时若git add新文件到暂存区,执行此命令相当于添加到上次提交。

6.4.2. 取消已经暂存的文件

(staged -> modified)

$ git reset HEAD <file>

文件从暂存区回到已修改未暂存状态。

6.4.3. 取消对文件的修改

(modified -> unmodified)

$ git checkout -- <file>

6.4.4. 文件操作总结

file-summary.png

7. Git远程仓库

Git 要求每个远程主机都必须指定一个主机名。

7.1. 查看远程仓库

$ git remote

查看每个远程库的简短名字。Git 默认使用“origin”这个名字来标识你所克隆的原始仓库。

$ git remote -v

加上-v选项,显示对应的克隆地址。

7.2. 添加远程仓库

$ git remote add <shortname> <url>

<shortname>即自定义远程主机名,在git remote中显示,

7.3. 从远程仓库抓取数据

git fetch

$ git fetch add <remote-name>
$ git fetch add <remote-name> <branch-name> #指定分支
  • 抓取所有本地仓库没有的数据。但只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支。
  • 通常用来查看其他人的进程,因为它取回的代码对本地代码没有影响。
  • 指定分支所取回的更新,在本地主机上要用”远程主机名/分支名”的形式读取。
  • 类似的,git fetch origin会抓取上次clone以来的别人提交的更新。
  • 如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

git pull

$ git pull <remote-name> <remote-branch-name>:<local-branch-name>
  • 取回远程主机某个分支的更新,再与本地的指定分支合并。
  • 如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。
  • 如果是与当前分支合并,则冒号后面的部分可以省略。
  • 等同于先git fetchgit merge
  • 如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时删除对应的本地分支。这是为了防止由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。除非加上参数 -p 就会在本地删除远程已经删除的分支。

7.4. 推送数据到远程仓库

$ git push <remote-name> <local-branch-name>:<remote-branch-name>
  • 如果省略远程分支名,则表示将本地分支推送与之存在”追踪关系”的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。
  • 如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。
    将本地仓库中的数据推送到远程仓库,只有在所克隆的服务器上有写权限,或者同一时刻没有其他人在推数据,这条命令才会如期完成任务。
  • 如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。
  • 如果是新建分支第一次git push,会提示:
fatal: The current branch dev1 has no upstream branch.
  To push the current branch and set the remote as upstream, use
  git push --set-upstream origin dev1

输入这行命令,然后输入用户名和密码就成功了,以后的push就只需要输入git push origin

  • 如果当前分支只有一个追踪分支,那么主机名都可以省略。
  • 如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机,这样后面就可以不加任何参数使用git push
  • 如果不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用--all选项。
  • 如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做git pull合并差异,然后再推送到远程主机。这时如果你一定要推送,可以使用--force选项。
    结果导致远程主机上更新的版本被覆盖。

7.5. 查看远程仓库信息

$ git remote show <remote-name>

例如:

$ git remote show origin
* remote origin
URL: git@github.com:defunkt/github.git
Remote branch merged with 'git pull' while on branch issues # git pull 时将自动合并的分支
issues
Remote branch merged with 'git pull' while on branch master # git pull 时将自动合并的分支
master
New remote branches (next fetch will store in remotes/origin) # 还没有同步到本地的远端分支
caching
Stale tracking branches (use 'git remote prune') #已同步到本地的远端分支在远端服务器上已被删除
libwalker
walker2
Tracked remote branches
acl
apiv2
dashboard2
issues
master
postgres
Local branch pushed with 'git push' # git push 缺省推送的分支
master:master

7.6. 远程仓库的删除和重命名

7.6.1. 重命名

$ git remote rename pb paul

修改的是某个远程仓库在本地的简称,分支名也会发生变化。

7.6.2. 删除

$ git remote rm <remote-name>

8. 版本标签

8.1. 显示已有标签

$ git tag
$ $ git tag -l '<tag-name>' #显示指定版本

按照字母顺序排列。

8.2. 新建标签

Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用;而含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息。

建议一般使用含附注型的标签,临时性加注标签用轻量级标签。

8.2.1. 含附注的标签

$ git tag -a <tag-name> -m <message>
$ git show '<tag-name>' #查看相应标签的版本信息

-a指定标签名,-m指定对应的标签说明,git show可以显示出。

8.2.2. 轻量级标签

$ git tag <tag-name>

没有选项。

8.2.3. 签署标签

如果你有自己的私钥,还可以用 GPG 来签署标签,只需要把之前的 -a 改为 -s 即可。

$ git tag -s <tag-name> -m <message>

8.2.4. 验证标签

$ git tag -v <tag-name>

验证已经签署的标签,会调用 GPG 来验证签名,所以需要有签署者的公钥,存放在 keyring 中,才能验证,否则报错gpg: Can't check signature: public key not found

8.3. 后期加注标签

$ git tag -a <tag-name> <SHA-1>

只要在打标签的时候跟上对应提交对象的校验和(或前几位字符)即可。
例如:

$ git log --pretty=oneline
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile

$ git tag -a v1.2 9fceb02

$ git show v1.2
version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <mchacon@gee-mail.com>
Date: Sun Apr 27 20:43:35 2008 -0700

updated rakefile

8.4. 分享标签

默认情况下,git push 并不会把标签传送到远端服务器上

  • 使用--tags选项一次推送所有本地新增的标签上去
  • 通过显式命令才能分享标签到远端仓库。
$ git push origin --tags
$ git push origin [tag-name]

9. Git分支

使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作。

9.1. 分支简介

在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针、作者等附属信息、零个或多个指向该提交对象的父对象指针。首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。

举例:对于三个文件暂存后,Git 嫁给你当前版本的快照(blob类型)连同每个文件的额SHA-1保存至仓库。

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'

现在仓库中有五个对象:

  • 三个 blob 对象:表示文件快照内容
  • tree 对象:记录着目录树内容及其中各个文件对应的 blob 对象索引的
  • commit 对象:指向 tree 对象(根目录)的索引和其他提交信息元数据。在需要的时候重现此次快照。
    singleObj.png

作些修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(parent 对象)。两次提交后:
multiObj.png

Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。
masterHistory.png

9.2. 新建分支

$ git branch <branch-name>

使用git branch testing在当前 commit 对象上新建一个分支指针testing
newBranch.png

9.3. HEAD

HEAD 是一个指向你正在工作中的本地分支的指针,即当前分支。

运行 git branch 命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以依然还在 master 分支里工作:

headMaster.png

9.4. 切换分支

$ git checkout <branch-name>
$ git checkout -b <branch-name>

使用git checkout testing切换分支testing
checkout.png

或者运行 git checkout 并加上 -b 参数,新建并切换到该分支。

提交之后:

$ vim test.rb
$ git commit -a -m 'made a change'

headTesting.png
提交后 HEAD 随testing分支一起向前移动,,而 master 分支仍然指向原先 git checkout 时所在的 commit 对象。

如果此时执行git checkout master,切回master分支:

masterHistory.png
做了两件事:

  • HEAD 指针移回到 master 分支
  • 工作目录中的文件换成了 master 分支所指向的快照内容(较旧的进度)
    它的主要作用是将 testing 分支里作出的修改暂时取消,这样你就可以向另一个方向进行开发。

此时如果提交新文件,即:

$ vim test.rb
$ git commit -a -m 'made other changes'

branchHistory.png

Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,Git 的实现与项目复杂度无关,并且每次提交都记录了 parent 对象,所以 Git 分支操作非常廉价。

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
你看,Git创建一个分支很快,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
假如我们在dev上的工作完成了,就可以把dev合并到master上。Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支。

9.5. 合并分支

$ git checkout master
$ git merge iss53

回到 master 分支,运行 git merge 命令指定要合并进来的分支iss53

9.6. 遇到冲突时的分支合并

如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起(逻辑上说,这种问题只能由人来裁决)。结果如下:

$ 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 查看合并时发生冲突的文件:

...
# unmerged: index.html

任何包含未解决冲突的文件都会以未合并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(当前分支)中的内容,下半部分是在 iss53 分支中的内容。

解决办法是删除上面的内容(包括’=’’<’’>’),自行决定怎么写。之后运行git addgit status

...
# modified: index.html
#

确认冲突已解决,标记为modified,再git commit提交。

也可以使用相关可视化工具解决冲突,运行git mergetool

9.7. 删除分支

$ git branch -d <branch-name>
#强制删除
$ git branch -D <branch-name>
# 删除远程分支,分支必须完全合并在其上游分支,或者在HEAD上没有设置上游
$ git push origin --delete <branch-name>
$ git branch -dr <remote-name/branch-name>

9.8. 案例

工作流程

  1. 开发某个网站为
  2. 实现某个新的需求,创建一个分支。
  3. 在这个分支上开展工作。

假设此时突然有个很严重的问题需要紧急修补,那么:

  1. 返回到原先已经发布到生产服务器上的分支。
  2. 为这次紧急修补建立一个新分支,并在其中修复问题。
  3. 通过测试后,回到生产服务器所在的分支,将修补分支合并进来,然后再推送到生产服务器上。
  4. 切换到之前实现新需求的分支,继续工作。
# 新建iss53并切换
$ git checkout -b iss53

# 遇问题切回master
$ git checkout master
# 此时工作区和之前 master 提交时完全一样

# 创建紧急修补分支 hotfix
$ git checkout -b 'hotfix'

Git 的好处:

  • 不需要同时发布这个补丁和 iss53 里作出的修改
  • 不需要在创建和发布该补丁到服务器之前花费大力气来复原这些修改

切换分支时需要留心你的暂存区或者工作目录里还没有提交的修改,它会产生冲突从而阻止切换,最好保持一个清洁的工作区域(也可以通过 stashing 和 commit amending 绕过这种问题)。

hotfixSuccess.png.png

修改、测试之后回到 master 分支并把 hotfix 合并:

$ git checkout master
$ git merge hotfix

请注意,合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把 master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

hotfix分支完成历史使命之后可以删掉,回到正常的 iss53 正常工作:

$ git branch -d hotfix
$ git checkout iss53

normalWork.png

需求#53开发完之后,合并masteriss53分支:

$ git checkout master
$ git merge iss53

这次合并操作的底层实现,并不同于之前 hotfix 的并入方式。因为这次你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算:

merge3.png

这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)(见图 3-17)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。

值得一提的是 Git 可以自己裁决哪个共同祖先才是最佳合并基础;这和 CVS 或 Subversion(1.5 以后的版本)不同,它们需要开发者手工指定合并基础。所以此特性让 Git 的合并操作比其他系统都要简单不少。

mergeAutoCommit.png

此时iss53没用了,可以删除。

9.9. 分支管理

查看所有分支

$ git branch
iss53
* master
testing

git branch 命令不加参数列出所有分支。 master 分支前的 * 字符表示当前所在的分支。也就是说如果现在提交更新,master 分支将随着开发进度前移。

查看各分支最后一个 commit 对象的信息

$ git branch -v

查看与HEAD已合并/或未合并分支

# 查看已并入当前分支的==哪些分支是当前分支的直接上游
$ git branch --merged
# 查看尚未合并的
$ git branch --no-merged

对于已合并的,可以用git branch -d直接删除,不会有损失。
对于已合并的,用git branch -d删除会报错,因为这样做会丢失数据,除非-D强制删除。

9.10. 远程分支

remote-name/remote-branch-name描述远程分支,区别于本地分支。

在第一次git clone之后,下载的数据命名为origin/master,无法修改。之后 Git 创建属于本地的 master 分支,二者都指向远程originmaster分支。
remoteBranch1.png

如果在本地 master 分支做了些改动,在本地的提交历史正朝向不同方向发展,与此同时,其他人向 git.ourcompany.com 推送了他们的更新,服务器上的 master 分支就会向前推进。不过只要不和服务器通讯,本地的 origin/master 指针仍然保持原位不会移动。
remoteBranch2.png

可以运行 git fetch origin 来同步远程服务器上的数据到本地。从 origin 上获取尚未拥有的数据,更新你本地的数据库,然后把 origin/master 的指针移到最新位置。
remoteBranch3.png

假设,还有另一个仅供你的敏捷开发小组使用的内部服务器 git.team1.ourcompany.com。加为当前项目的远程分支之一,并命名为 teamone。

现在可以用 git fetch teamone 来获取小组服务器上你还没有的数据了。由于当前该服务器上的内容是你 origin 服务器上的子集,Git 不会下载任何数据,而只是简单地创建一个名为 teamone/master 的远程分支,指向 teamone 服务器上 master 分支所在的提交对象 31b8e
remoteBranch4.png

master并不是多么神秘复杂的东西,分清楚远程的别人的origin/master、本地的别人的origin/master、本地的自己的master就可以了。

9.11. 推送本地分支

$ git push <remote-name> <branch-name>

要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。你创建的本地分支不会因为你的写入操作而被自动同步到你引入的远程服务器上,你需要明确地执行推送分支的操作。

git push origin serverfix:serverfix意思是“上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支”。通过此语法,你可以把本地分支推送到某个命名不同的远程分支:若想把远程分支叫作 awesomebranch,可以用 git push origin serverfix:awesomebranch 来推送数据。

9.12. 跟踪远程分支

$ git checkout -b <branch-name> <remote-name/branch-name>

从远程分支 checkout 出来的本地分支,称为跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。

9.13. 删除远程分支

$ git push <remote-name> :<branch-name>

对于git push <remote-name> <local-branch-name>:<remote-branch-name> 语法,如果省略 ,那就等于是在说“在这里提取空白然后把它变成”。

9.14. 分支衍合

$ git checkout <rebase-branch-name>
$ git rebase <master-branch-name>

把一个分支中的修改整合到另一个分支有两种办法:mergerebase
merge
merge.png

rebase
还有可以把在 C3 里产生的变化补丁在 C4 的基础上重新打一遍,这种操作叫做衍合(rebase)。即把在一个分支里提交的改变移到另一个分支里重放一遍。

原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支 experiment)后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,然后以基底分支(也就是主干分支 master)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3’),从而改写 experiment 的提交历史,使它成为 master 分支的直接下游:
rebase.png

再进行快进合并:
speedMerge.png

现在的 C3’ 对应的快照,其实和普通的三方合并,即 C5 对应的快照内容一模一样,结果没有任何区别,只不过提交历史不同。但衍合能产生一个更为整洁的提交历史,仿佛所有修改都是在一根线上先后进行的。

一般我们使用衍合的目的,是想要得到一个能在远程分支上干净应用的补丁。项目志愿开发者通过衍合提交,维护者就不需要做任何整合工作。实际上是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。

衍合是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一起。

10. 参考链接