Skip to content

Pro Git

阅读

Pro Git 第二版中文

起步

初次运行 Git 前的配置

bash
# 用户信息
# 查看所有的配置以及它们所在的文件
git config --list --show origin

# 包含系统上每一个用户及他们仓库的通用配置。
git config --system
git config --system

# 对你系统上 所有 的仓库生效。
git config --global user.name xxx
git config --global user.email xxx@xx.com

# 当前使用仓库
git config --local

# 配置文本编辑器
# 在 Windows 系统上,如果你想要使用别的文本编辑器,那么必须指定可执行文件的完整路径。
git config --global core.editor xxx

# 检查配置信息
# 如果想要检查你的配置,列出所有 Git 当时能找到的配置
git config --list

# 检查 Git 的某一项配置
git config <key>

# 查询 Git 中该变量的 原始 值,它会告诉你哪一个配置文件最后设置了该值
git config --show-origin <key>

获取帮助

bash
# 若你使用 Git 时需要获取帮助,有三种等价的方法可以找到 Git 命令的综合手册(manpage)
git help <verb>
git <verb> --help
man git-<verb>

# 如果你不需要全面的手册,只需要可用选项的快速参考,那么可以用 -h 选项获得更简明的 `help'' 输出
git add -h

Git 保存的不是文件的变化或者差异,而 是一系列不同时刻的 快照 。

Git 基础

获取 Git 仓库

bash
# 在已存在目录中初始化仓库
git init

# 克隆现有的仓库
# 当你执行 gitclone 命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。
# 支持 https://协议、 git://协议、 SSH传输协议。
git clone <url> <自定义本地仓库>

记录每次更新到仓库

bash
# 查看哪些文件处于什么状态
git status
# 以简洁的方式查看更改
git status -s

# 更新 tracked 的文件的状态,使其从工作区添加到暂存区
git add -u

# bash指令-创建文件
touch .gitignore

# bash指令-将文件的内容打印到标准输出
cat .gitignore

# git diff 能通过文件补丁的格式更加具体地显示哪些行发生了改变
# 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异。
git diff

# 这条命令将比对已暂存文件与最后一次提交的文件差异
git diff --staged
git diff --cached

# 查看提交中是否包含空白错误
git diff --check

# 将提交信息与命令放在同一行
git commit -m <message>

#  只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤
git commit -a -m <message>

# 要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。
git rm <文件>

# 如果要删除之前修改过或已经放到暂存区的文件
git rm -f <文件>

# 想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。
git rm --cached <文件>

# 在 Git 中对文件改名
git mv <旧文件> <新文件>

工作目录、暂存区域以及 Git 仓库

文件的状态变化周期

.gitignore 的格式规范如下:

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

glob 模式:

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

查看提交历史

bash
# 回顾下提交历史
git log

# 会显示每次提交所引入的差异(按 补丁 的格式输出)。 你也可以限制显示的日志条目数量,例如使用 -2 选项来只显示最近的两次提交
git log -p|--patch -<n>

# 查看每次提交的简略统计信息
git log --stat

# 使用不同于默认格式的方式展示提交历史
git log --pretty=oneline|short|full|fuller
git log --pretty=format:"%h -%an, %ar : %s"

# 添加了一些 ASCII 字符串来形象地展示你的分支、合并历史
git log --graph

# 仅显示最近的 n 条提交
git log -<n>

# --since 和 --until 这种按照时间作限制的选项很有用
git log --since=2.weeks

# 用 --author 选项显示指定作者的提交
git log --author="<作者名>"

# 用 --grep 选项搜索提交说明中的关键字
git log --grep="<关键字>"

# 接受一个字符串参数,并且只会显示那些添加或删除了该字符串的提交。
git log -S function_name

# 如果只关心某些文件或者目录的历史提交,可以在 git log 选项的最后指定它们的路径。
git log example.js| src/

# 隐藏合并提交
git log --no-merges

git log --pretty=format 常用的选项:

image-20240315010349726

git log 的常用选项:

限制 git log 输出的选项

撤销操作

bash
# 重新提交(可以再次add文件后再使用,第二次提交将代替第一次提交的结果)
git commit --amend

# 取消暂存
git reset HEAD <file>

# 撤销对文件的修改,且不保留,还原成上次提交的样子
git checkout -- <file>

远程仓库的使用

bash
# 查看你已经配置的远程仓库服务器
# origin ——这是 Git 给你克隆的仓库服务器的默认名字
git remote

# 需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。
git remote -v

# 添加远程仓库
git remote add <shortname> <url>

# 从远程仓库中抓取和拉取
git fetch <remote>

# 你可以在命令行中使用克隆的仓库服务器名 来代替整个 URL。
git fetch <shortname>

# git pull 命令来自动抓取后合并该远程分支到当前分支。
git pull <remote>

# 本地master分支推送到origin服务器
git push origin master

# 将当前分支与远程分支关联起来
git branch --set-upstream-to=<new-remote-branch> origin/new-branch

# 在 git push 时使用 --set-upstream 标志来设置新的上游分支
git push --set-upstream origin new-branch

# 查看某一个远程仓库的更多信息
# 这个命令列出了当你在特定的分支上执行 git push 会自动地推送到哪一个远程分支。
# 它也同样地列出了哪些远程分支不在你的本地,哪些远程分支已经从服务器上移除了,
# 还有当你执行 git pull 时哪些本地分支可以与它跟踪的远程分支自动合并。
git remote show <shortname>

# 远程仓库的重命名
git remote rename <oldName> <newName>

# 远程仓库的移除
git remote rm <shortname>

注意 git fetch 命令 只会将数据下载到你的本地仓库——它并不会自动合并或修改你当前的工作。 当准备好时你必须手动将其合并入你的工作。

默认情况下, git clone 命令会自动设置本地 master 分支跟踪克隆的远程仓库的 master 分支(或其它名字的默认分支)。 运行 git pull 通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支。

打标签

bash
# 在 Git 中列出已有的标签
git tag [-l|--list]

# 创建附注标签
git tag -a v1.4 -m "message"

#git show 命令可以看到标签信息和与之对应的提交信息
git show v1.4

# 创建轻量标签
git tag v1.4-lw

# 对过去的提交打标签(在命令的末尾指定提交的校验和(或部分校验和))
git tag -a v1.2 9fceb02

# 共享标签
# 创建完标签后你必须显式地推送标签到共享服务器上
git psuh <remote> <tagname>

# 一次性推送很多标签(这将会把所有不在远程仓库服务器上的标签全部传送到那里)
git push <remote> --tags

# 删除标签
git tag -d <tagname>

# 从远程仓库中移除具体标签
git push <remote> :refs/tags/<tagname>
git push <remote> --delete <tagname>

# 检出标签
# 查看某个标签所指向的文件版本
git checkout <tagname>

Git 支持两种标签 : 轻量标签 ( lightweight )与 附注标签 (annotated)。

轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。

附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。 但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

检出标签:

如果你想查看某个标签所指向的文件版本,可以使用 git checkout 命 令, 虽然这会使你的仓库处于**“分离头指针(detached HEAD)”的 状态**。在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除 非通过确切的提交哈希才能访问。 因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支。

bash
git checkout -b version2 v2.0.0

如果在这之后又进行了一次提交,version2 分支就会因为这个改动向前移动, 此时它就会和 v2.0.0 标签稍微有些不同,这时就要当心了。

Git 别名

bash
# 设置命令别名
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 中添加你自己的取消暂存别名
git conig --global alias.unstage 'reset HEAD --'

# 设置查看最后一次提交的命令别名
git config --global alias.last 'log -1 HEAD'

# 要执行外部命令,而不是一个 Git 子命令。 如果是那样的话,可以在命令前面加入 ! 符号。
# 将 git visual 定义为 gitk 的别名
git config --global alias.visual '!gitk'

Git 分支

分支简介

bash
# 创建新分支
git branch <branchname>

# 使用 git log 命令查看各个分支当前所指的对象。
git log --oneline --decorate

# 切换分支
git checkout <branchname>

# 输出你的提交历史、各个分支的指向以及项目的分支分叉情况。
git log --oneline --decorate --graph --all

# 创建新分支的同时切换过去
git checkout -b <newbranchname>

首次提交对象及其树结构

做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 master 分支会在每次提交时自动向前移动。

Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行 符)

项目分支历史

分支的新建与合并

bash
# 新建分支并切换分支
git checkout -b iss53

# 新建分支
git branch iss53

# 切换分支
git checkout iss53

# 合并分支hotfix到master分支上
git checkout master
git merge hotfix

# 删除分支
git branch -d hotfix

# 遇到冲突时的分支合并
# 查看冲突文件状态
git status
# 修改文件后添加到暂存区
git add

# 交互式地将文件或更改添加到暂存区
git add --patch

# 提交修复冲突结果
git commit

分支管理

bash
# 查看所有分支列表
git branch

# 查看每一个分支的最后一次提交
git branch -v

# 要查看哪些分支已经合并到当前分支
git branch --merged

# 查看所有包含未合并工作的分支
git branch --no-merged

# 查看未合并到master分支的的分支情况
git branch --no-merged master

分支开发工作流

长期分支:

许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 他们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。 这样,在确保这些已完成的主题分支(短期分支,比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。

事实上我们刚才讨论的,是随着你的提交而不断右移的指针。 稳定分支的指针总是在提交历史中落后一大截,而前沿分支的指针往往比较靠前。

通常把他们想象成流水线(work silos)可能更好理解一点,那些经过测试考验的提交会被遴选到更加稳定的流水线上去。

你可以用这种方法维护不同层次的稳定性。 一些大型项目还有一个 proposed(建议) 或 pu: proposed updates(建议更新)分支,它可能因包含一些不成熟的内容而不能进入 next 或者 master 分支。 这么做的目的是使你的分支具有不同级别的稳定性;当它们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。 再次强调一下,使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。

主题分支:

主题分支是一种短期分支,它被用来实现单一特性或其相关工作。

你已经在上一节中你创建的 iss53 和 hotfix 主题分支中看到过这种用法。 你在上一节用到的主题分支(iss53 和 hotfix 分支)中提交了一些更新,并且在它们合并入主干分支之后,你又删除了它们。 这项技术能使你快速并且完整地进行上下文切换(context-switch)——因为你的工作被分散到不同的流水线中,在不同的流水线中每个分支都仅与其目标特性相关,因此,在做代码审查之类的工作的时候就能更加容易地看出你做了哪些改动。 你可以把做出的改动在主题分支中保留几分钟、几天甚至几个月,等它们成熟之后再合并,而不用在乎它们建立的顺序或工作进度。

远程分支

bash
# 获得远程引用的完整列表
git ls-remote <remote>
git remote show <remote>

# 与给定的远程仓库同步数据
git fetch <remote>

# 添加远程仓库
git remote add <url>

# 推送
# 推送到远程的分支上
git push <remote> <branch>

# 推送本地branch1到远程branch2
git push <remote> <branch1>:<branch2>

# 合并远程分支工作到当前所在分支
git merge <remote>/<branch>

# 跟踪分支
# 基于远程跟踪分支之上建立新分支
git checkout -b <curBranch> <remote>/<remoteBranch>

# 切换到一个已存在的远程分支,并创建一个本地分支来跟踪这个远程分支。
git checkout --track <remote>/<branch>

# 设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支, 你可以在任意时间使用 -u 或 --setupstream-to 选项运行 git branch 来显式地设置。
# 通过这个命令,可以将一个本地分支设置为跟踪指定的远程分支
git branch -u <remote>/<branch>

# 要查看设置的所有跟踪分支
git branch -vv

# 如果想要统计最新的领先与落后数字,需要在运行此命令前抓取所有的远程仓库。
git fetch --all; git branch -vv

# 拉取
#  git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

# 删除远程分支
git push <remote> --delete <branch>

远程跟踪分支 teamone/master

image-20240318084128314

当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。 换一句话说,这种情况下,不会有一个新的 serverfix 分支——只有一个不可以修改的 origin/serverfix 指针。

从一个远程跟踪分支检出一个本地分支会自动创建所谓的“跟踪分支” (它跟踪的分支叫做“上游分支”)。

当设置好跟踪分支后,可以通过简写 @{upstream}@{u} 来引用它的上游分支。 所以在 master 分支时并且它正在跟踪 origin/master 时,如果愿意的话可以使用 git merge @{u} 来取代 git merge origin/master。

变基

bash
# 变基的基本操作
git rebase <branch>

# 重新定义分支的基础
git rebase --onto <new_base> <oldbase> <branch>

image-20240318105622000

变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

分布式 Git

分布式工作流程

集成管理者工作流

Git 允许多个远程仓库存在,使得这样一种工作流成为可能:每个开发者拥有自己仓库的写权限和其他所有人仓库的读权限。 这种情形下通常会有个代表“官方”项目的权威的仓库。 要为这个项目做贡献,你需要从该项目克隆出一个自己的公开仓库,然后将自己的修改推送上去。 接着你可以请求官方仓库的维护者拉取更新合并到主项目。 维护者可以将你的仓库作为远程仓库添加进来,在本地测试你的变更,将其合并入他们的分支并推送回官方仓库。

工作流程

  1. 项目维护者推送到主仓库。
  2. 贡献者克隆此仓库,做出修改。
  3. 贡献者将数据推送到自己的公开仓库。
  4. 贡献者给维护者发送邮件,请求拉取自己的更新。
  5. 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。
  6. 维护者将合并后的修改推送到主仓库。

主管与副主管工作流

这其实是多仓库工作流程的变种。 一般拥有数百位协作开发者的超大型项目才会用到这样的工作方式,例如著名的 Linux 内核项目。 被称为副主管(lieutenant) 的各个集成管理者分别负责集成项目中的特定部分。 所有这些副主管头上还有一位称为 主管(dictator) 的总集成管理者负责统筹。 主管维护的仓库作为参考仓库,为所有协作者提供他们需要拉取的项目代码。

工作流程

  1. 普通开发者在自己的主题分支上工作,并根据 master 分支进行变基。 这里是主管推送的参考仓库的 master 分支。
  2. 副主管将普通开发者的主题分支合并到自己的 master 分支中。
  3. 主管将所有副主管的 master 分支并入自己的 master 分支中。
  4. 最后,主管将集成后的 master 分支推送到参考仓库中,以便所有其他开发者以此为基础进行变基。

向一个项目贡献

提交准则

私有小型团队

bash
# 日志过滤器,显示在branchB但不在branchA的提交的列表
git log --no-merges branchA...branchB

# 将本地fetureB分支的内容推送到远程featureBee分支,且添加-u后将远程分支与本地分支关联起来。
git push [-u|--set-upstream] origin featureB:featureBee

一个简单的多人 Git 工作流程的通常事件顺序

私有管理团队工作流程的基本顺序

派生的公开项目

bash
# 生成一封包含自定分支(branchA)变更的摘要请求邮件,可提交给指定的仓库维护者,请求合并到目标分支(branchB)。
git request-pull branchA branchB

# 将本地的分支branchA强制推送到远程仓库的branchB分支,并覆盖远程仓库上的同名分支。
git push -f branchA branchB

# 将指定分支(branch)的所有提交合并到当前分支,并生成一个新的提交,但不会保留原有分支的提交历史。
git merge --squash branch