程序员的-Git-指南-全-

程序员的 Git 指南(全)

原文:annas-archive.org/md5/abc3703970cd2e20db03717df99e3edb

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这是一本关于 Git 的书,Git 是全球最流行的版本控制系统。我将解释什么是版本控制,并引导你从入门话题到较为高级的内容。有关你需要的背景经验和软件,请参见第一章(都是免费的)。

本书适合谁阅读

本书适合任何技能水平的程序员。句号。

版本控制是每个编程项目的关键,如第一章所述,Git 是全球最流行的版本控制系统。团队规模越大,使用版本控制的紧迫性越大,但对于单个程序员来说,版本控制也同样重要。

本书内容

第一章简介;你将了解本书的结构,Git 是什么,版本控制系统是什么,Git 的来源,以及可供 Git 使用的工具。你还将学习如何在计算机上安装 Git。

第二章创建你的仓库中,你将学习如何创建一个仓库并从 GitHub 克隆它。你将看到你磁盘上的仓库与 GitHub 上创建的仓库之间的关系。一个简单的示例程序将被介绍。

第三章分支、位置和图形界面中,我们将介绍工作区、索引、仓库、远程仓库和暂存区;它们是什么;以及它们的用途。

第四章合并、拉取请求和处理合并冲突中,你将学习关于分支的内容:这是版本控制的一个核心概念,也是 Git 的一个巨大优势。分支允许你同时处理多个任务,并保持发布代码的整洁。Git 的分支特别快速。

第五章变基、修改和挑选提交中,你将学习如何使用 Rebase 和 Amend 重写历史。Rebase 这个词让一些程序员感到恐惧,但我将向你展示如何掌握这个有用(且安全!)的工具。

第六章交互式变基中,我们将探讨如何修改与提交相关的消息,重新排列多个提交,甚至在将它们推送到服务器之前删除提交。

第七章工作流、笔记和标签中,我们将探讨管理仓库的基本工作流,以及如何添加元数据来保持仓库的清晰和整洁。

第八章别名中,我们将探讨别名以及它们如何帮助你节省大量工作。别名可以将命令和所有标志组合起来,使你的工作更加轻松。

第九章使用日志中,我们将研究非常强大的log命令。日志通常被忽视,但它可以让你洞察项目的当前和过去的每个方面。

第十章重要的 Git 命令和元数据中,我们将继续探讨一些非常有用且重要的 Git 命令。这些强大的命令可以在事情出现问题时帮助你脱离困境。

第十一章查找损坏的提交:二分查找和归咎,我们将介绍一个拯救性命的命令Bisect,它帮助 Git 帮助你找出程序出现问题的地方。

第十二章修复错误,你将学习如果在使用 Git 时出现错误,如何解决问题。

最后,在第十三章下一步,我们将简要查看其他资源。

充分利用本书

  • 你需要对某种编程语言有一定了解。熟悉 C# 是一个很大的加分项,但不是必须的。

  • 你需要在电脑上安装 Git(免费),最好安装最新的(或更好的)Visual Studio 2019 免费版。最后,你还需要下载并安装免费的 GitHub Desktop。这样,你就可以不花钱跟随本书中的示例。

  • 给 macOS 用户的提示:上述所有内容对你同样适用,我不认为你会遇到任何额外的问题。

  • 给 Linux 用户的提示:我不使用 Unix,但我强烈怀疑上述所有内容(除了 Visual Studio)对你也适用。

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。你可以在这里下载:static.packt-cdn.com/downloads/9781801075732_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码字、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“如果你查看日志,git log --oneline,你应该能看到三个提交:一个是你克隆仓库时创建的,另外两个是你手动创建的。”

代码块如下所示:

public int Add (int left, int right)
{
    return left + right;
}
public int Subtract (int left, int right)
{
    return left - right;
} 

当我们希望引起你注意某个代码块的特定部分时,相关的行或项会以粗体显示:

public int Add (int left, int right)
{
    return left + right;
}
**public** **int Subtract (int left, int right)**
{
    return left - right;
} 

所有命令行输入或输出如下所示:

git add .
git commit -m "Add calculator class" 

粗体:表示新术语、重要词汇或你在屏幕上看到的文字。菜单或对话框中的词汇以这种方式显示。例如:“从管理面板中选择系统信息。”

警告或重要说明如下所示。

联系我们

我们欢迎读者的反馈。

一般反馈:如果你对本书的任何内容有疑问,请在邮件主题中提及书名,并通过电子邮件联系 Packt,邮箱地址为customercare@packtpub.com

勘误:虽然我们已经尽力确保内容的准确性,但错误仍然可能发生。如果你在本书中发现错误,请报告给我们。请访问 www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表格链接,并填写相关细节。

盗版:如果你在互联网上遇到任何我们作品的非法复制品,无论是什么形式,我们将非常感激你提供相关地址或网站名称。请通过 copyright@packtpub.com 联系我们,并附上该材料的链接。

如果你有兴趣成为作者:如果你在某个领域有专长,并且有意写书或为书籍做贡献,请访问 authors.packtpub.com

分享你的想法

阅读完Git for Programmers后,我们很想听听你的想法!请 点击这里 直接进入该书的亚马逊评论页面,分享你的反馈。

你的评论对我们以及技术社区非常重要,能够帮助我们确保提供卓越的优质内容。

第一章:简介

在本章中,我们将涵盖以下主题:

  • 版本控制和 Git 的简短历史

  • 获取并设置 Visual Studio 2019、GitHub Desktop 和终端

  • 获取并安装 Git

  • 为 Visual Studio、GitHub Desktop 和 GitHub 命令行配置 Git

开始吧!

本书简介

“从一开始开始,”国王庄严地说,“一直进行到结束,然后停下来。”——《爱丽丝梦游仙境》

在本书中,我们将从 Git 的基础开始,逐步深入到高级主题。无需任何 Git 的使用经验。当然,如果你已经在使用 Git,你可能希望跳过前面的章节。Git 可以说是全球最流行的版本控制系统,但这也引发了一个问题:“什么是版本控制?”

版本控制

在有版本控制之前,我会写一点代码,然后当我开始担心丢失这些代码时,我会备份我的目录。这种做法很慢、低效,占用了大量的磁盘空间,而且很难与他人共享。

版本控制系统VCS)为你完成所有这些工作(还有更多),并且以一种快速、高效、占用最少磁盘空间的方式进行。其中最快和最有效的之一就是 Git,尽管也有其他的工具。本书不会花很多时间说服你 Git 比其他工具更好。首先,市场已经证明了这一点,Git 占据了主导地位。其次,如果你已经购买了本书,那说明你已经做出了选择。如果你还没有购买本书,那就去购买吧。我会在这里等你。

关于代码示例

为了演示 Git 的使用,我们需要有一个可以演变的小程序。代码示例使用 C#编写,但它们非常简单,无论你是否熟悉这门语言,你都能跟得上。例如,如果你能搞清楚这段代码的作用,那你就准备好了。

public class Program
{
    public void PrintHello()
    {
        Console.WriteLine("Hello World!");
    }
} 

这段代码声明了一个类(不用担心它是什么)名为Program。在这个类内部,有一个方法(函数)叫做PrintHello,它会将Hello World打印到控制台(你的屏幕上)。

这大概就是最复杂的地方了,我会在接下来的过程中解释每个代码片段。

简短的历史

2005 年 7 月,在仅仅几个月的工作后,Linux 背后的天才 Linus Torvalds 发布了 Git,以满足他自己的需求、Linux 社区的需求,最终也满足了我们所有人的需求。Git 的目标是快速和高效。它做到了。

虽然当时大多数 VCS 都是集中式的(所有文件都保存在一个大服务器上),但 Git 使用的是分布式系统,其中每个人都有自己的仓库。从技术上讲,Git 不需要中央服务器,尽管如果你在团队中工作,中央的代码共享位置会很方便。但最大的不同在于,使用 Git 时,你与 VCS 的绝大多数交互都是本地的——就在你的磁盘上。

使用 Git 的工具

有许多容易混淆的术语(例如 Git 与 GitHub 的区别),而且有很多工具可以与 Git 一起使用——从命令行到图形用户界面GUI)工具。本节将回顾其中的一些选项。

GitHub 等

有许多服务允许你创建共享的“仓库”(存储程序所有版本的位置)。最著名和流行的包括 GitHub 和微软的 Azure,还有 BitBucket 和 GitLab。Azure 是一个功能强大的 DevOps 系统,而 GitHub 是一个非常直接的托管程序的方式。本书中我们将使用 GitHub。(最近,微软以 75 亿美元的股票收购了 GitHub,并对 GitHub、开源项目以及 Git 做出了巨大的承诺。)

关键点:Git 是本书中讲解的系统。GitHub 是一个用于共享代码的中央仓库系统(我们将在书中稍后更加具体地讲解),而 GitHub Desktop 是一个用于操作 Git 的 GUI。

GUI 和命令行

有许多方式可以与 Git 互动。主要的方式是通过命令行操作或使用 GUI。GUI 工具种类繁多。本书将重点介绍 GitHub for Desktop 和 Visual Studio 2019 以及命令行。事实上,本书的一个不同之处在于,我们将通过这三种工具来讲解 Git。这将帮助你深入理解 Git,同时了解 GUI 工具如何帮助(或阻碍)你的工作。

还有许多其他优秀的 GUI 工具,而且工具的生态系统也在不断发展。话虽如此,我们将使用的这些工具既强大又具有代表性,几乎所有其他 GUI 工具也都提供类似功能。此外,这些工具已经得到广泛应用,未来很可能会继续存在。更重要的是,如果你掌握了一个 GUI 工具,并了解它与命令行指令的关系,你会发现使用其他 GUI 工具也变得轻松。

我们将首先看一下 Git 的命令行界面。虽然 GUI 工具有一些局限性,但如果 Git 可以做到的事,你也能在命令行中完成,也就是说,Git 的所有功能都可以在命令行中操作。此外,正如常常有人指出的那样,了解 Git 的命令行使用方式,会让你使用 GUI 工具更加得心应手,因为你会知道发生了什么。

理解你的车是怎么运作的有帮助吗?完全没有,除非出现故障。然后你会求助于能够修理问题的专业机械师。在 Git 中,专家就是熟悉命令行的程序员。

命令行

在 Windows 10 上,开箱即用的命令行可以直接从终端访问。它提供了你需要的一切,但它有点难看:

图 1.1:Git 在命令行中的表现

它比较难读,但我们可以把它美化,使它更容易阅读。

美化命令行界面

如果你在 Windows 10 上使用 Git,你可以让命令行界面看起来更漂亮且更有用,如下所示:

图 1.2:Pretty Print 示例

为此,请下载并安装 Power Shell。然后,按照 Scott Hanselman 在 jliberty.me/PrettyGit 上提供的出色指示操作。

在 mac 和 Linux 上,你可以通过访问 github.com/diogocavilha/fancy-git 按照指示操作,达到类似的效果。GitHub 网站上还有其他类似的选项,也能做到差不多的效果。

Visual Studio 2019

Visual Studio 2019 内置了对 Git 的广泛支持。专门为 Git 设计了一个独立的菜单,Git 信息也集成到了整体的用户界面中:

图 1.3:Visual Studio 2019 中的 Git 菜单

首要的任务是获取并设置 Visual Studio。

免费获取 Visual Studio

本书中用于示范的三个产品(命令行、Visual Studio 2019 和 GitHub Desktop)都提供免费版本。要获取 Visual Studio 2019,访问 visualstudio.com,然后将鼠标悬停在 Visual Studio 按钮上,选择 Community 2019。它将自动下载,你只需双击并按照指示完成安装:

图 1.4:获取 Visual Studio

如果你使用的是 Mac,Visual Studio 2019 for Windows 和 Visual Studio for Mac 在处理 Git 的方式上存在一些显著的差异。你可能不会遇到太多困难,但你也可以随时使用 Terminal 或 GitHub Desktop。

GitHub Desktop

一款非常流行的图形界面(GUI),尤其适用于 GitHub,是 GitHub Desktop。这款跨平台应用程序让使用 Git 变得异常简单:预判你可能想做的事情,并将其简化。缺点是,像几乎所有图形界面一样,它也有一些限制。

图 1.5:GitHub Desktop

本书中我们将使用这三种工具:命令行、Visual Studio 2019 和 GitHub Desktop。随着内容的推进,上面展示的选择和 Git 命令会变得更易理解;现在,你只需熟悉它们的外观即可。

获取 Git

你需要做的第一件事是安装 Git。Git 的官方网站声明:“即使 Git 已经安装在你的电脑上,重新安装并更新到最新版本也是一个不错的主意。”

在 Windows 上获取 Git

在 Windows 上,还有几种方法可以安装 Git。我推荐使用官方版本。最新版本可以在 Git 网站上获取:git-scm.com/download/win

获取 Git 的另一种方式,并且一举两得,是下载并安装 GitHub Desktop。安装程序还会安装 Git 的命令行版本。你可以在 desktop.github.com/ 获取:

图 1.6:获取 GitHub Desktop

本书将在 Windows 10 上展示演示,使用 Git 版本 2.30.0.windows.2,但这些示例应该适用于几乎任何版本的 Git。

在 Mac 上获取 Git

有几种方式可以在 Mac 上安装 Git。最简单的方法可能是安装 Xcode 命令行工具。你可以通过第一次从终端运行 Git 来实现,如下所示:

$ Git --version 

如果你尚未安装,它会提示你进行安装。

如果你想要更最新版的 Git,也可以通过二进制安装程序进行安装。一个 macOS 的 Git 安装程序由 Git 网站维护并提供下载,网址是 git-scm.com/download/mac

Git macOS 安装器

图 1.7:在 Mac 上安装 Git

你也可以将其作为 GitHub Desktop for macOS 安装的一部分来安装。它们的 GUI Git 工具也有一个选项可以安装命令行工具。你可以从 GitHub Desktop for macOS 网站下载该工具,网址是 desktop.github.com

在 Linux 上获取 Git

本书没有正式支持 Linux,但几乎所有平台上的 Git 都是相同的。

如果你想在 Linux 上安装基本的 Git 工具,通常可以通过随发行版附带的包管理工具来实现。如果你使用的是 Fedora(或任何相关的 RPM 发行版,如 RHEL 或 CentOS),可以使用 dnf

$ sudo dnf install git-all 

如果你使用的是基于 Debian 的发行版,如 Ubuntu,尝试使用apt

$ sudo apt install git-all 

检查你的版本

安装完 Git 后,你的第一个命令应该是以下内容:

git --version 

也就是说,关键字是 git,然后是 version,前面加上两个破折号。这有时被称为“git 双破折号版本”。

我电脑上的输出如下:

˃ Git --version
git version 2.30.0.windows.2 

(你可能会有不同的体验。)

配置 Git – 命令行

本书将会介绍如何配置 Git 来进行个性化设置,但现在,先添加你的名字和电子邮件地址,以便每次使用 Git 时都能正确地记录信息。输入以下命令行命令:

git config --global --edit 

这将打开你的编辑器。找到或创建 [user] 部分,并添加以下内容:

[user]
name = Jesse Liberty
email = jesseliberty@gmail.com 

你可能想使用自己的姓名和电子邮件地址。

配置文件中还有其他条目。暂时忽略它们并保存关闭文件。

配置 Git – Visual Studio

在 Windows 的 Visual Studio 中,点击 Git 菜单,打开一个对话框。在第一个标签页中,输入你的用户名和电子邮件地址:

图 1.8:在 Visual Studio 中设置 Git 选项

配置 Git – GitHub Desktop

要配置 GitHub Desktop,你需要一个 GitHub 账户。我们将在下一章中介绍。一旦你有了账户,进入 文件 | 选项,选择 账户 标签,然后点击 登录

图 1.9:设置 GitHub Desktop

总结

本章介绍了本书的概述,列出了每一章的内容。你还看到了版本控制及 Git 本身的简短历史。

接下来,我们查看了下载你需要的环境:Visual Studio 2019、GitHub Desktop 和 PowerShell 作为你的命令行工具。所有这些都可以免费获得。

软件下载完毕后,我们查看了如何设置 Git,以及如何配置我们将要使用的工具。

第二章:创建你的代码库

本章将教你如何在 GitHub 上创建一个账户,如何创建和克隆你的第一个代码库,从而建立你电脑上的代码库与 GitHub 上代码库之间的链接。

本章内容将包括:

  • 创建你的代码库

  • Git 拉取

  • 推我、拉你

  • 从命令行开始

  • 提交 – 最佳实践

我们将从创建你的 GitHub 代码库开始。

创建你的代码库

创建代码库有很多种不同的方法。我们将介绍如何在 GitHub 上创建代码库并将其克隆到本地磁盘,因为这是最常见的方式。

首先在 GitHub 上创建你的代码库

你首先需要在 GitHub 上注册。访问 github.com 并点击注册。填写你的用户名(如果用户名已被占用,系统会提示)和邮箱,系统可能会要求你验证你是人类。假设你是,点击创建账户

填写他们的微型调查问卷,然后点击创建账户。系统会要求你验证电子邮件,完成后你会看到(一次性)欢迎页面,询问你想要做的第一件事。选择创建代码库

图 2.1:开始使用 GitHub

如果你已经有账户,登录并点击新建代码库。你可能一开始找不到这个选项,这时点击角落的大加号。

无论如何,你都会被引导到创建新代码库页面。第一件事是为你的新代码库命名。我将使用 ProGitForProgrammers。只要 GitHub 没有提示名称已被占用,你可以随意使用任何名称。

现在是时候填写表格了:

图 2.2:创建代码库

首先输入一个简短的项目描述。接下来,非常重要的是,选择你是否希望将这个代码库设置为公开(任何人都可以查看)还是私有(只有你邀请的人可以查看)。

我强烈建议勾选添加 README 文件。这将是用户访问你的代码库时看到的内容。你可以稍后使用 Markdown 来修改该文件。

一定要添加 .gitignore 文件。这告诉 Git 在提交文件到代码库时忽略哪些文件。这非常重要,以防你不小心覆盖了其他程序员的元数据文件。点击下拉菜单,看看支持多少种语言;对于 C#,我建议你搜索并选择 Visual Studio。

如果你的代码库是公开的,请务必为代码选择许可证。我选择了 MIT 许可证。你可以在 opensource.org/licenses/MIT 了解更多关于此许可证的信息。

就是这样!你准备好点击创建代码库了。点击后,你将被带到你的新 GitHub 代码库的主页:

图 2.3:你代码库的初始视图

注意,你已经有了你要求的三个文件,并且可以看到 README 文件的预览以及你输入的描述。

目前,这个仓库只存在于服务器上。你想把它复制到你的磁盘上,以便你可以添加代码并使用命令保持同步。因此,你将“克隆”这个仓库;也就是说,你会在本地仓库中创建一个远程仓库的精确副本。

你如何进行这一步将取决于你使用的是命令行、Visual Studio 还是 GUI。

克隆到你的计算机 – 命令行

克隆到本地仓库非常简单。打开你的终端(或 PowerShell),并将目录更改为你想要仓库存放的位置(在我这里是 GitHub/命令行)。

返回到你的 GitHub 仓库,点击右上角绿色的Code按钮。点击该按钮后会弹出一个小对话框。选择HTTPS,除非你知道自己有SSH(就像我一样)。无论哪种情况,点击剪贴板图标以复制地址:

图 2.4:复制仓库地址

返回到命令行,输入 git clone,然后粘贴地址:

git clone git@github.com:JesseLiberty/ProGitForProgrammers.git 

你应该会看到类似这样的内容:

图 2.5:在命令行中克隆

将目录更改为ProGitForProgrammers,你会看到原本在服务器上的三个文件现在也出现在这里:

图 2.6:目录中的文件

现在,让我们看看如何在 Visual Studio 中执行此操作。

克隆到你的计算机 – Visual Studio

进入你的目录(在我这里是GitHub),创建一个名为 VisualStudio 的目录。

打开没有项目的 Visual Studio,选择文件 | 克隆仓库。填写字段并点击克隆

图 2.7:使用 Visual Studio 克隆到本地仓库

几秒钟后,你会看到三个文件,现在它们显示在解决方案资源管理器中:

图 2.8:在 Visual Studio 中的克隆文件

从 GitHub 仓库克隆到你自己的仓库有很多种方法。其中一种方法是使用专用的 GUI 工具,比如 GitHub Desktop。

克隆到你的计算机 – GitHub for Desktop

再次返回到你的根目录(GitHub),并创建一个新目录。这次命名为GitHubDesktop

现在,返回到 GitHub 并点击Code

图 2.9:通过 GitHub Desktop 直接克隆

请注意,其中一个选项是使用 GitHub Desktop 打开。点击它,一个对话框会弹出。你需要填写的唯一字段是本地路径。点击克隆

图 2.10:使用 HTTP 克隆到 GitHub Desktop

请注意,GitHub Desktop 需要你仓库的 https URL。

现在,你有三个原始仓库的副本,每个副本都在自己的目录中:CommandLineVisualStudioGitHubDesktop。这些可能代表三位程序员在同一个解决方案上工作,或者是一个程序员选择克隆其项目的不同方式。

创建项目

我们需要一个项目。使用 Visual Studio(或您喜欢的编辑器)在 CommandLine 目录下创建一个名为 ProGitForProgrammers 的项目。完成后,您应该会有三个原始文件和一个程序文件夹。该文件夹中将包含 .sln 文件以及一个代码文件夹。

打开命令行并导航到相同的目录。当您到达时,您的命令行应如下所示:

图 2.11:命令行提示符

看看黄色区域,您会看到 +1 ~0 -0+1 表示您添加了一个文件或目录;~0 表示没有文件被修改;-0 表示没有文件被删除。让我们来看一下添加了什么。输入:

git status 

您应该看到类似如下内容:

图 2.12:未跟踪的文件

Git 告诉您您在 main 分支(目前唯一的分支)上,并且您有“未跟踪的文件”——也就是说,这些文件存在于目录中,但 Git 并没有跟踪它们。如果它们是未跟踪的,Git 无法存储它们;实际上,Git 根本不知道它们的存在。让我们来解决这个问题。输入以下命令:

git add ProGitForProgrammers/
git commit -m "First commit – from command line" 

add 告诉 Git 这是一个它应该关注的文件,而 commit 将其添加到本地仓库。

每次 commit 都必须有一条信息,如果您没有提供,Git 会提示您添加信息。我在这里通过使用 -m 标志添加了它。

再次强调,所有这些操作都发生在本地,因此 GitHub 并不知道这一切。我们可以通过将我们的提交推送到服务器来解决这个问题:

git push 

现在,如果您去 GitHub 并刷新页面,您的项目将会显示在那里。您可以通过文件夹逐层点击,甚至进入 Program.cs 文件,查看代码:

图 2.13:在 GitHub 上查看您的代码

注意左上角告诉您您正在 main 分支。旁边是到 Program.cs 的路径。下面是您添加的提交信息,然后是文件本身。

Git 拉取

将您的提交推送到服务器后,其他开发者可能希望将其拉取到自己的目录,以保持同步。

使用 GitHub Desktop 拉取

将项目上传到服务器后,我们可以轻松地将其拉取到其他位置。例如,打开 GitHub Desktop。它会告诉您仓库中有更改,并热心地提供一个按钮帮助您更新本地仓库。

如果您打开文件资源管理器并导航到 GitHubDesktop 目录,您将看到现在有一个您从命令行推送的文件副本。

拉取到 Visual Studio

点击 Git 菜单并选择 拉取。Visual Studio 会更新为服务器上的代码。现在所有三个仓库都是最新的。这是 Git 的核心:

  • 将文件保存到本地仓库

  • 将文件推送到远程仓库

  • 拉取任何在远程仓库中但本地仓库中不存在的文件

推送我,拉取你

通常,你会推送自己的更改并从其他开发者那里拉取更改。此外,通常你不会在相同的文件上工作,更不会在主分支(main)上工作。我们将在第四章合并分支中讨论如何避免这种情况。现在,我们只需要格外小心。

在目录GitHub/VisualStudio/ProGitForProgrammers中打开 Visual Studio。按照下面的方式向Program.cs中添加一行代码:

namespace ProGitForProgrammers
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
           `**Console.WriteLine("I just added this in Visual Studio");**         }     } }` 

Having made your change, you want to check it in. Since we are in the `VisualStudio` directory, we'll do the work right within Visual Studio. Click the `Git` menu and choose **Commit or Stash**. A Git window will open as a tab next to **Solution Explorer**. Enter a commit message and press **Commit All**: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_14.png) Figure 2.14: Git window in Visual Studio Note that if you drop down the **Commit All** menu, you have a number of shortcuts for adding, committing, and pushing your changes. As you can see, and will see often in this book, you can do almost anything in Visual Studio that you can do at the command line. ### Pushing to the server You have now committed your changes to your local repository. The GitHub repository, however, doesn't know about your changes. (You can prove this to yourself by returning to GitHub and drilling down to `Program.cs`.) The other programmers' repositories (for example, `CommandLine` and `GitHubDesktop`) are equally oblivious. To disseminate this change, you first push your changes up to the server (GitHub) and then pull them down to the other repositories. From within Visual Studio's Git window, press **Staged**. This will stage your changes for committing. Next, click **Commit**. This will put your changes into your local repository (be sure to give the commit a meaningful message). Examine the Git window; there is a lot of information: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_15.png) Figure 2.15: The Git window in Visual Studio You are told that the commit was created locally (and locally is the important part!). Below that is the status of your commit. You have one to push up to the server (outgoing) and none to bring down (incoming): ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_16.png) Figure 2.16: Uploading a commit from Visual Studio Now, find the up-pointing arrow in the upper-right corner. Hover over it and you'll see that it says `Push`. Click that button to push your changes to the server. When it is done, it will give you a success message. Ignore the offer to create a pull request for now. Look to the left of your Git menu and see the local history of your commits: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_17.png) Figure 2.17: The history of commits Each dot signals a commit, and next to each dot is your commit message (and now you can see why meaningful commit messages are both hard to write and worth the effort). There is also an indicator that main is pointing to your last commit. If you check GitHub (remember to refresh the page) you will now see the line in `Program.cs`. Make sure you understand why: this is because after we committed the change, we pushed it to the remote repository. ## Downloading the changes at the command line We created the changes in the `VisualStudio` directory. `CommandLine` and `GitHubDesktop` know nothing of the changes, even though they are now on GitHub. For these directories to know about the changes, you need to pull the changes down. Change directories to `CommandLine`. Examine the contents of `Program.cs`; the new line is not there. Open your terminal and enter `pull`. This will pull any changes from the server to your local repository. The result should look like this: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_18.png) Figure 2.18: Pulling from the remote repository Git is telling you that it formatted and compressed your files and passed them down to your repository. Toward the bottom it says that it used Fast-forward. We'll discuss this in *Chapter 4*, *Merging, Pull Requests, and Handling Merge Conflicts*. Take a look at `Program.cs` now in your command directory; the new addition should now be there. Want to do something cool? Open the `Program.cs` file before updating. After the update you will see the second `WriteLine` pop into view. What is actually happening is that the code that was in your directory is replaced by the new code on the pull. ### Downloading the changes using GitHub Desktop Change directories to `GitHubDesktop` and open the GitHub Desktop program. It will give you a lot of information about the status of your repository (**No Local Changes**) and it will automatically check and inform you that there is one commit to update your local repository with: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_19.png) Figure 2.19: The view from the remote repository Go ahead and click **Pull origin**. It does the pull, and the button disappears. Check your code; the change should now be in your `Program.cs` (and is recorded in your local repository). All three local repositories and the server repository are now in sync. # Starting at the command line You can start the process at any of our repositories. Last time we started in the `VisualStudio` repository and then pulled the changes down to the `CommandLine` and `GitDesktop` repos. This time, let's start at the command line. Open Visual Studio and point it to the project in your `CommandLine` directory. Just to be certain, right-click on **Solution**, select **Open Folder in File Explorer**, and make sure you are in the right directory. To keep this example very simple, we'll just add another line to `Program.cs`: ``` class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.WriteLine("I just added this in Visual Studio"); Console.WriteLine("I just added this in the command line repo"); } } ``` Normally you would make many more changes before checking in, but again, this is a demo and we're more interested in using Git than we are in fussing with this silly program. Save all your files and at the command line get the status by entering: ``` git status ``` This will give you output that looks like this: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_20.png) Figure 2.20: The command line indicating one file has been modified The key piece of information is the modified file. That is just as it should be, as that is the file we modified. You can now add it to the index and then commit it: ``` git add ProGitForProgrammers/ProGitForProgrammers/Program.cs git commit -m "Add writeline indicating we are in command line" ``` On the other hand, you can combine these two steps with the `-a` flag: ``` git commit -a -m "Add writeline indicating we are in command line" ``` You will want to draw a distinction between untracked files and modified files. Untracked files are outside of Git and cannot be manipulated inside Git until they are added; modified files are tracked by Git but have changed since the last commit. If we are happy with the commit we've added, we can (optionally) push it to the server: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_21.png) Figure 2.21: Pushing our commit to the remote repository We'll want to do that because we want to share this code with the other programmers. ## Pulling to GitHub Desktop Switching to GitHub Desktop, we see that it already knows there is something to pull, as we saw last time. (If it doesn't, push the **Fetch** button, which will go to the server to see if there is anything to bring back.) That's two repos that are identical, but the `VisualStudio` repo is not yet up to date. Let's return to Visual Studio in the `VisualStudio` folder. ## Pulling to Visual Studio Open the Git menu item, and select `Pull`. Watch your source code and see the third line pop into existence. Once again, the three local repositories and the remote repo are all in sync. # Commits – best practices Like everything else in programming, best practices in commits are, to some degree, controversial. The first issue is frequency. ## How often should I commit? There are those who say a commit should be atomic: representing exactly one unit of work (one task, one bug fix). No more and no less. So, according to this line of thought, if you are in the middle of work and you get called away, you should not commit, but you should use the stash. The stash is an area where you can put files that you want to come back to later. You can name sets of files that you stash, and then pick the one you want to restore by name. This is a defensible position, but I hold the opposite: commit early and commit often. Commits are cheap and fast in Git, and interactive rebase (see *Chapter 6*, *Interactive Rebasing*) allows you to "squash" commits together. Therefore, if you are working on a feature and you make five interim commits before you are done, you'll have the opportunity to squash them into a single commit with a single message. This is the best of both worlds: you secure your interim work with a commit, and you present only one commit (rather than five) to the server. ## Keep your commit history clean The first way that a programmer reviews your code is to look at the list of commits and then dive into those that are interesting. A good history of commits with well-written messages is a delight to review. A long, tedious history with meaningless messages is only slightly more fun than eating glass. ## A note on commit messages As you will see later in this book, commit messages are very important for anyone (including you) reviewing your commit history. By convention, commit messages should be in the imperative, and should tell you exactly what is in that commit. ``` Fixing some files // bad Fix WriteLine in helloworld.cs // good ``` In practice you'll often find comments in the past tense: ``` Fixed WriteLine in helloworld.cs // good enough ``` > *"In theory, theory and practice are the same; in practice, they never are*." -- Pat Johnson It pays to get into the habit of writing good messages in the right format. Your teammates will thank you. ## When the title isn't enough The message title should be kept to 50 characters. Most of the time this is enough, but if it isn't, leave the `-m` message off and let Git open your editor. There you can add additional information. Skip a line after the header and consider using bullet points or other ways of making the things you want to convey easy to read. Important: By default Git uses vi (a Unix editor). You'll want to enter: ``` git config ––global core editor "code -w" ``` This ensures that Visual Studio Code is your default editor: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_22.png) Figure 2.22: Editing in Visual Studio Code Note that `#` is the comment character, and all lines that begin with `#` will be ignored. When you use `log` (see *Chapter 9*, *Using the Log*) to see your history (or view history in Visual Studio, etc.) you'll see the entire message: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_23.png) Figure 2.23: The output of the log command You can see just the headers if you want, using `git log -–oneline`, but we'll leave the details for *Chapter 9*, *Using the Log*: ![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17441_02_24.png) Figure 2.24: log using the oneline flag # Summary In this chapter, we have covered a number of topics relating to creating and interacting with your repository. We discussed: * Creating your repository * The relationship between your local and remote repositories * Git pull * Git push * Starting at the command line * Using Visual Studio * Commits: best practices In the next chapter, we'll take a look at the various places Git keeps your files, and the relationship between adding an untracked file and committing a tracked file.

第三章:分支、位置和图形界面

在本章中,你将了解 Git 中至关重要的“五个位置”:工作区、索引、局部仓库、远程仓库和暂存区。你将学习如何使用这些位置以及它们是如何协同工作的。

每个概念将通过代码和截图进行说明,大多数 Git 操作将在命令行、Visual Studio 和 GUI(GitHub Desktop)中进行演示。

你还将学习如何创建分支、提交代码,以及如何将提交从工作区移到仓库中,再从仓库移到远程仓库中。

五个位置

作为程序员,我将 Git 分为五个位置:

  1. 工作区

  2. 索引(暂存区)

  3. 局部仓库

  4. 远程仓库

  5. 暂存区

让我们逐一查看这些内容。

工作区

工作区是你当前文件所在的位置。也就是说,如果你打开 Windows 资源管理器并导航到你克隆的目录,你会看到你当前正在处理的程序版本。如果你打开 Visual Studio 2019,这些文件会出现在解决方案资源管理器中。同样,工作区是你当前文件所在的位置;如果你在你的项目中打开 Visual Studio,工作区中的文件就是你会看到的文件。当你切换分支时(见下文),工作区会更新为适当的文件。这可能是 Git 中最难理解的概念之一:当你切换分支时,工作区中的文件会发生变化——也就是说,当前分支的文件会被切换到 Windows(或 Mac 或 Linux)目录中。

注意:可以通过所谓的工作树使用多个工作区。这些超出了本书的范围,你在很长时间内可能都不需要它们。

在我们当前的程序中,工作区在 Windows 资源管理器中的样子如下:

图 3.1:当前工作区

这个相同的目录可以通过命令行查看:

图 3.2:来自命令行的当前工作区

在 Visual Studio 中,相同的工作区如下所示:

图 3.3:来自 Visual Studio 的当前工作区

如果你查看目录中的内容,你会发现完全相同的文件。

索引/暂存区

如果工作区中有你想要提交的文件,你首先需要将它们添加到暂存区(通常称为索引)。从暂存区开始,只需一个命令(commit)就能将这些文件移入局部仓库。

图 3.4:修改 Program.cs

我在我们拥有的三个副本中都添加了几行。

让我们从命令行开始。我有一个习惯,就是在做任何事情之前,先运行git status

图 3.5:命令行中的 git status

要在命令行中将这个文件添加到索引中,只需使用关键字add,然后跟上文件名或一个句点(.),表示你希望将所有文件移到暂存区:

git add  ProGitForProgrammers/ProGitForProgrammers/Program.cs 

Git 不会做出响应,但文件现在已经进入暂存区。如果你再次查看状态,你会看到这一点:

图 3.6:已修改文件已暂存

这次,它显示已修改的文件已准备好提交。要提交这个文件,你只需输入 git commit。因为文件已经暂存,它将立即提交到本地仓库。如果你没有添加 -m "my message",编辑器将打开,供你添加消息。

跳过暂存区

你可以跳过暂存区,直接提交文件,方法是使用 -a 参数。因此,当你的文件未暂存时,你可以写:

git commit ProGitForProgrammers/ProGitForProgrammers/Program.cs -a -m "My message" 

这将立即提交 Program.cs 文件,并附上指定的消息。我承认,我 90% 的时间都是这样提交文件的。(你也可以使用 git commit -a -m "my message" 来提交工作区中所有已修改的文件。)

Visual Studio

在 Visual Studio 中,状态始终可见,位于右下角:

图 3.7:Visual Studio 右下角

0 与上箭头表示你没有文件等待推送。铅笔旁边的 1 表示你有一个已修改的文件。接下来是你的程序名称,最后是你所在的分支。

在 Visual Studio 中有多种提交方式。例如,你可以直接从 Git 菜单提交,或者右键单击文件并选择Git。然后选择提交或暂存。然而,更简单的方法是点击铅笔图标,打开Git 变更菜单:

图 3.8:Visual Studio 变更窗口

在这里,你可以看到已更改的文件及其路径。你可以选择全部提交,或者点击该按钮下拉并选择若干相关选项。填写消息后按下全部提交,Visual Studio 会立即响应,确认你已提交到本地仓库,并提供将更改上传到远程仓库的机会(显示你有一个外发文件):

图 3.9:Visual Studio 提交和推送/拉取菜单

由于我们通常认为上传是“向外”传输,而不是“外发”,所以可能会感到困惑。简单来说,当 Visual Studio 说“外发”时,它指的是从本地到远程服务器,而“内发”则指的是从服务器到本地仓库。

GitHub Desktop

GitHub Desktop 在一个屏幕上给你提供了大量信息:

图 3.10:GitHub Desktop

在顶部行,你可以看到仓库名称和当前分支。在左上角的窗口中,你可以看到有一个文件发生了变化,以及是哪个文件。在右侧,你可以看到实际的变化。

最后,在左下角,你可以输入消息并提交文件。

一旦你提交,页面会清空并显示一个新按钮,允许你将更改推送到服务器。

本地与远程仓库

Git 的第三和第四个区域是本地仓库和远程仓库。我们在前一章中已经讨论过这个话题,这里我只重复一句:commit 将文件提交到本地仓库,push 将你的提交从本地仓库推送到远程仓库。

虽然我提倡频繁提交,但你可能希望在推送之前进行几次提交。这样,你就有机会合并相似的提交,正如我们在第六章中所看到的,交互式变基(Interactive Rebasing)中所述。

暂存区

Git 的第五个也是最后一个区域是暂存区。顾名思义,暂存区是用来存放你修改过但不想提交或者在切换分支时不想丢失的文件的地方。我们将在下面的分支讨论中详细了解这个功能。

分支

使用分支对 Git 的操作至关重要,更不用说对项目成功的影响了。其基本思想是:你有一个“主”分支,所有发布操作都从这个分支进行。每当有代码添加到主分支时,都会进行检查和审查,以确保主分支保持尽可能干净。

当你需要修复一个 bug 或开发一个新功能时,你会创建一个新的分支(通常称为功能分支)。这将创建一个当前主分支代码的副本。你可以在功能分支上工作,而不会影响主分支。完成工作并确保一切正常后,你可以将功能分支“合并”回主分支:

图 3.11:第一个功能分支

请注意,存在一个名为Head的指针。它指向你工作区中的内容。在这种情况下,我们已经分支到Feature 1Head显示该功能分支的代码现在位于我们的工作区中。

这是对分支的一个很好的简化,但实际上还有很多内容。首先,让我们来看一下如何操作。直到现在,你的所有代码都在 Main 分支上——这是一种不好的实践。我们应该在编码前就创建一个功能分支。话虽如此,我们现在仍然可以这样做。

从命令行开始,你只需检查 Main(将 Main 上的最新提交内容放入你的工作区。指针“tip”表示最新的提交)。进入 Main 后,你将从远程仓库 pull 以获取 Main 的最新版本。此时,你已经准备好创建你的第一个分支。命令序列如下所示:

图 3.12:在命令行上创建分支

请注意,创建分支 Calculator 并没有自动检出它;你必须作为一个单独的步骤来完成此操作。但是,如果你使用 -b 标志,那么你可以同时创建并检出分支:

git checkout -b Calculator 

无论哪种情况,新分支都在工作区中。但是,那个分支中是什么内容呢?由于该分支是从主分支创建的,而我们还没有做任何更改,因此新分支与主分支完全相同。从这里开始,它们将分叉。随着你添加代码,代码将存在于新分支(Calculator)中,但不会出现在主分支中。

在深入研究之前,我们先为 Visual Studio 用户和 GitHub Desktop 用户创建分支。

最简单(也是最不混淆)的方法是打开 Windows 资源管理器,导航到VisualStudio文件夹(在我的情况下是GitHub | VisualStudio | ProGitForProgrammers | ProGitForProgrammers)。在该文件夹中有一个.sln文件,我会双击它,打开 Visual Studio。(这本书最初叫做《Pro Git For Programmers》,你会在代码中看到这个名字。)

不要混淆VisualStudio文件夹(我们用它来演示在 Visual Studio 中使用 Git)和程序本身,后者用来修改所有三个地方(CommandLineGitHubDesktopVisualStudio)中的代码。

如果你把这些看作是三个独立的程序员,每个程序员在自己的计算机上(这里通过使用不同的目录来模拟),可能会更容易理解。每个程序员都有一个主分支,现在每个人都在为自己的工作创建分支。

我们也希望把这个放到一个分支上,为了减少混淆,我们将这个分支命名为Book。要创建该分支,点击Git菜单,选择新建分支。给新分支命名为Book,然后按创建

图 3.13:在 Visual Studio 中创建新分支

当你这么做时,左侧会弹出一个窗口,列出该仓库的所有分支,Book会以粗体显示,表明它是当前分支:

图 3.14:Visual Studio 中的分支菜单

现在有两个用户已经从主分支创建了分支。我们用 GitHub Desktop 来创建第三个分支。打开该程序,点击菜单中的仓库。在菜单中点击在资源管理器中显示,确保你处在C:\GitHub\GitHubDesktop\ProGitForProgrammers路径下。

它应该显示你已经从源(服务器)拉取了一次,并且有一个按钮上写着拉取源。点击该按钮。这将下载最新版本的Main。你现在应该看到按钮显示推送源—这表示将现在位于该目录中的两个提交推送到服务器。

创建新分支时,点击分支菜单,并选择新建分支。系统会提示你输入新分支的名称。输入Movie并点击创建。此时界面会询问你是否要发布该分支。GitHub Desktop 中的发布仅仅是将其上传到远程仓库。我们先不发布,先进行一些提交。

程序员 1(命令行)与计算器

CommandLine目录路径中打开 Visual Studio。在解决方案资源管理器中,你应该能看到Program.cs,里面有五个WriteLine语句。添加一个名为Calculator的新类,并将其设置为public

namespace ProGitForProgrammers
{
    public class Calculator
    {
    }
} 

通常情况下,我们不会在如此少量的工作后就创建提交,但为了本书中的简单示例,我们将进行大量提交。返回命令行并查看状态。它会告诉你有一个未跟踪的文件。Git 已经识别出该目录中有一个它不了解的文件。我们的下一步是将其添加到 Git 中:

git add . 

通过使用点号,add命令将把任何修改过或新增的文件添加到暂存区。然后,你只需通过输入以下命令提交新文件:

git commit -m "Add calculator class" 

如果你现在写git status,Git 会告诉你当前所在的分支是计算器,没有任何需要提交的内容,且工作区是干净的。

我们将在第九章《使用日志》中详细讨论log命令,但现在先用它查看我们的提交和相关消息:

git log ––oneline 

这将显示你所有的提交,每个提交一行:

˃ git log --oneline
e5c4db9 (HEAD -> Calculator) Add calculator class
b00ca09 (origin/main, origin/HEAD, main, featureOne) Demonstrating the staging area
4ac9d40 Add a line to program to indicate why it was added
ef16f81 Add writeline indicating we are in command line
d418600 Add informative WriteLine
a3f085e First commit -- from command line
a5798e1 Initial commit 

七位十六进制标识符是“短 ID”,足以唯一标识每个提交。它们按最新到最旧的顺序列出。我们最近的提交是:

e5c4db9 (HEAD -> Calculator) Add calculator class 

这告诉你,当前的 Head 指针指向计算器分支(也就是说,当前工作区中的内容是计算器分支),并显示了我们为该提交添加的消息。从图形上看,可能是这样的:

图 3.15:Head 指针

注意到箭头从计算器指向主分支。每个提交都指向其父提交。

推送新分支

我们可以将这个提交推送到服务器,但服务器并不知道这个分支。当我们输入git push时,会收到以下消息:

> git push
fatal: The current branch Calculator has no upstream branch.
To push the current branch and set the remote as upstream, use
    git push --set-upstream origin Calculator 

它表示无法继续操作(fatal),因为当前分支(即计算器分支)与服务器上的分支不对应。然而,幸运的是,它给出了我们需要使用的命令行。只需复制该命令,粘贴到命令提示符中,然后按Enter键。嘿,搞定了!你已经将分支推送到服务器上:

图 3.16:推送到服务器

目前,你可以忽略其他所有消息;你关心的是最后两行,它们表明你现在在服务器上有一个分支计算器,而且在服务器上这个分支也叫计算器

请注意,从现在开始,在推送计算器分支的提交时,你不需要使用那一行命令;你只需要写git push

查看源

让我们去 GitHub 查看我们的新分支。登录后选择ProGitForProgrammers代码库:

图 3.17:服务器上的代码库

那么,哪里是我们的计算器文件夹呢?注意左上角有一个按钮显示main。点击下拉,选择计算器 —— 这样就会显示计算器分支的内容:

图 3.18:服务器上的计算器分支

你可以看到计算器分支确实有预期的文件。

向分支添加提交

让我们再往分支中添加一个提交。返回 Visual Studio 并为我们的 Calculator 类添加一个 add 方法:

 public int Add (int left, int right)
        {
            return left + right;
        } 

同样,为了有更多的提交,我们再提交一次。最简单的方式是将添加和提交合并,并在单行中添加提交信息:

git commit -a -m "Add the add method" 

为了验证这个提交,重新运行 log 命令:

图 3.19:跟踪 HEAD

如果稍作分析,你不仅会看到我们的提交成功了(它是列表中的第一个),还会发现我们有多个指针。在第一行,我们看到 HEAD 指向我们的 Calculator 分支。很好。

第二行表明,origin(GitHub)上的 Calculator 分支指向上一个提交。我们有一个提交需要推送。

第三行显示,main 分支、HEAD、origin 上的 main 分支以及 featureOne 分支都指向第三个最新的提交。所有这些都是正常的。我们预期 Calculator 分支会从 main 分支分叉,如果需要的话,我们可以推送这个提交,或者等待再提交几个。

Book 分支 – 在 Visual Studio 中的 Git

让我们将注意力转向 Visual Studio 的开发者。你会记得,这个操作发生在 VisualStudio 文件夹中。让我们在该目录下打开 Visual Studio,并注意到右上角显示 1 个待提交 – 这表示我们有一个提交需要推送。点击它,Visual Studio 会打开两个新窗口。

一个窗口显示 Book 是当前分支(见上文),另一个显示你的提交历史(与 log 命令类似)。

中间窗口中有一部分显示了本地提交的内容以及待提交的内容。还注意到,Bookmain 被标记为待提交:

图 3.20:Visual Studio 中的提交窗口

要推送这些提交,找到右上角的小上箭头(这里放大显示):

图 3.21:在 Visual Studio 中推送文件的链接

让我们创建一个 Book 类。这个过程类似于上面创建 Calculator 类的过程。右键点击项目,选择 添加 | 。将新类命名为 "Book"。使 Book 类为 public,并给它三个属性:

图 3.22:新的 Book 类

让我们提交这个。为此,可以点击 Git 菜单项并选择 提交或暂存,或者点击屏幕底部的铅笔图标。无论哪种方式,都将打开提交界面。注意,它显示 1 个待提交。点击上箭头,这将推送我们之前的提交。你会收到一条消息,表示你已经成功将 Book 推送到 origin。

使用 GitHub Desktop 提交

打开位于 GitHubDesktop 目录中的 Visual Studio。这里我们将创建 Movie 类,并为其添加两个属性:Title 和一个人名的集合(我们将使用字符串来表示,以简化操作):

图 3.23:Movie 类

现在打开 GitHub Desktop。它会识别并显示更改(删除的行以红色显示,新增的行以绿色显示)。它还为你提供了检查新更改的机会:

图 3.24:在 GitHub Desktop 中显示的更改

你可以在 图 3.24 中看到上面高亮显示的更改。

状态

你可以看到,图形界面使工作变得更简单,但命令行使步骤更加明确。我们的仓库大概是这样的(从概念上讲):

图 3.25:多个分支

远程仓库是什么状态?

需要记住的是,这个提交历史图仅适用于本地仓库。远程仓库中的内容可能与某个分支的内容不同,这取决于你是否已经推送了所有的提交。在 第六章交互式变基,我们将回顾为什么在推送提交之前你可能希望保留一些提交(简而言之,你将有能力将多个提交合并,以减少审阅者必须逐一检查的提交数量)。

添加更多的提交

我们希望添加一些提交,以便能够查看它们的历史记录,并为未来的章节做准备。为此,我们将要在 CommandLine 项目上工作。你可以像我们已经做的那样,打开 Windows 资源管理器并导航到相应的目录,然后双击 ProGitForProgrammers.sln。另外,你也可以从任何位置打开 Visual Studio,然后选择 文件 | 最近的项目和解决方案,点击你想要的那个,在本例中是 ProGitForProgrammersC:\GitHub\CommandLine\ProGitForProgrammers)。

Visual Studio 会打开 Calculator 文件夹。为了确认你在正确的位置,右键点击项目并选择 在文件资源管理器中打开文件夹。你应该看到一个 Windows 资源管理器窗口在预期的文件夹中打开(无论你开始时在哪个文件夹)。让我们添加一个 Subtract 方法:

 public int Add (int left, int right)
        {
            return left + right;
        }
        public int Subtract (int left, int right)
        {
            return left - right;
        } 

虽然通常我们不会为如此小的更改提交,但现在我们还是先切换到命令行。像往常一样,我们从 git status 开始:

图 3.26:git status

让我们仔细阅读一下。git status 后的第一行确认你当前在 Calculator 分支。接下来是一个通知,告诉你当前领先于 origin/Calculator 一个提交。这意味着你没有推送上次的提交(而这确实如此)。

接下来是一个段落,告诉你 Calculator.cs 已经被修改,并给出了一些你可能想在这种情况下使用的命令。我们将使用 add 命令把修改后的文件添加到索引中。

我们将使用一个句点(.)来代替输入修改文件的名称,表示我们想要提交工作目录中所有更改(在这种情况下,只有一个文件):

git add . 

Git 没有做出实际的确认,但如果你请求状态,你会发现修改的文件现在是不同的颜色(在大多数设置中),并且消息略有不同,因为你现在已将该修改文件添加到索引中:

图 3.27:git status 中的修改文件

Git 会提供一个命令来取消暂存你的文件,如果你选择这么做。但要小心使用restore命令。如果你按照示例使用--staged标志,它会取消暂存你的文件,但如果你不加该标志,你会将文件恢复到最后一次提交的状态,丢失你在这期间所做的所有工作

在我们的例子中,我们想要提交这个更改,因此我们将输入:

git commit -m "Add subtract method" 

注意,我们不需要-a标志,因为我们要提交的文件已经添加到索引中了。

添加一个multiply方法并提交。接下来,添加一个整数除法方法并提交:

public int Add(int left, int right)
 {
     return left + right;
 }
 public int Subtract(int left, int right)
 {
     return left - right;
 }
 public int Multiply(int left, int right)
 {
     return left * right;
 }
 public int Divide(int left, int right)
 {
     return left / right;
 } 

检查你的提交

输入log命令:

git log –oneline 

图 3.28:log 命令

再次仔细查看输出。第一行告诉你,HEAD指向的是Calculator分支,这是我们预期的。下面是几次提交记录,然后你会看到一行,表明远程的Calculator分支仅有提交e5c4db9(即Add calculator class消息的提交)。

这对吗?有几种方法可以确认。最简单的办法是去 GitHub,看看Calculator类是否符合此处的描述:

图 3.29:服务器上的分支

注意,在左上角我们处于Calculator分支。现在深入查看代码。你看到的只是类的初始状态。这与日志显示的内容是一致的。

还有一种方法可以确认。返回到 Visual Studio,并点击应用程序底部右侧向上箭头旁的3

图 3.30:在 Visual Studio 中访问历史记录

当你执行此操作时,会打开一个窗口,显示你的本地历史记录和“待推送”文件——也就是那些你还没有推送的文件:

图 3.31:Visual Studio 中的历史记录

再次强调,这与log所显示的内容一致。

总结

在本章中,你已经了解了 Git 的五个关键位置:工作区、索引、局部仓库、远程仓库和暂存区。你已经了解了如何使用这些位置以及它们如何协同工作。

每个概念都通过代码和截图进行了说明,且每个 Git 操作都在命令行、Visual Studio 和 GitHub Desktop 中进行了展示。

最后,你已经学习了如何创建分支、提交代码,以及如何将提交从工作区移到仓库,再从仓库移到远程仓库。

挑战

在 GitHub 上创建一个名为Contacts的私有仓库,然后将该仓库克隆到本地磁盘的一个文件夹中。使用命令行,创建一个名为Person的功能分支,并在该功能分支中创建一个包含姓名、年龄和社会保障号码的person对象。每添加一个属性时创建一个提交。查看日志,看看你创建了什么,然后将这些提交添加到远程仓库。

答案

这没有唯一的正确方式,但我们将通过一个可能的解决方案来演示。

任务 #1 – 在 GitHub 上创建一个名为Contacts的私有仓库。为此,打开浏览器访问Github.com,并进入你的仓库页面。点击新建,并按照如下图填写各字段:

图 3.32: 服务器上的新仓库

注意,我已经将仓库标记为私有。点击创建仓库按钮。

任务 #2 – 将该仓库克隆到你磁盘上的一个文件夹中。

在同一个 GitHub 页面上,点击代码,然后点击剪贴板图标复制 HTTPS 或 SSH 路径(如果你有 SSH,你会知道它,否则选择 HTTPS):

图 3.33: 从服务器复制地址

打开你希望放置克隆仓库的命令行窗口,输入:

git clone 

然后粘贴你刚才复制的链接:

git clone git@github.com:JesseLiberty/Contacts.git 

你应该看到如下内容:

图 3.34: 从服务器克隆到本地仓库

这表示你已经将Contacts从 GitHub 克隆到一个名为Contacts的目录中,并且你已经切换到该目录。

任务 #3 – 使用命令行创建一个名为Person的功能分支。

要创建一个功能分支,我们将使用branch命令和checkout命令(或者cb别名):

图 3.35: 创建分支

任务 #4 – 在该功能分支中,创建一个包含姓名、年龄和社会保障号码的person对象。每添加一个属性时创建一个提交。

为此,我创建了我的项目(Contacts),然后在一个文件夹内添加了Person类:

图 3.36: 添加 Person 类

创建类时故意让它变得简单:

图 3.37: Person 属性

然后我回到命令行,检查并提交这些更改。

在每次提交之前,记得保存文件,否则命令行会告诉你没有任何要提交的内容。

任务 #5 – 查看日志,看看你创建了什么:

图 3.38: 检查日志

任务 #6 – 将这些提交添加到远程仓库。

我们将尝试推送,但远程仓库尚未识别我们的分支。幸运的是,Git 会告诉我们该怎么做:

图 3.39: 推送到服务器

此时,你已经拥有一个名为Contacts的本地和远程仓库,以及一个名为Person的分支。在名为Person的分支中,你有一个名为Person的骨架类的代码。快速查看 GitHub 可以发现,主分支(main)并没有Person对象(或类,或文件夹):

图 3.40:原点上的分支

但是 Person 分支确实有:

图 3.41:原点上的 Person 分支

如你所见,Person 分支确实包含了预期的代码。

第四章:合并、拉取请求和处理合并冲突

在本章中,你将看到如何使用不同类型的合并来合并分支。你还将看到如何处理合并冲突,以及一些工具来简化冲突管理。你将了解拉取请求以及快进合并与“真正”合并之间的区别。

在本章中,你将学习:

  • 如何将提交推送到服务器

  • 如何通过命令行、Visual Studio 和 GitHub Desktop 管理提交

  • 如何合并到主分支

  • 什么是拉取请求

  • 什么是合并冲突,如何解决它们

  • 什么是快进合并

  • 什么是“真正的”合并

让我们从合并的概述开始。

合并概述

如果你处于一个功能分支,且该功能已经完成并经过充分测试,你可能希望将该分支合并回主分支。有些组织允许你直接合并,而其他组织(大多数?)要求你创建一个拉取请求(PR)PR基本上是说:“请检查我的代码,如果你认为它是正确的,请将其合并到主分支。”

在合并之前让其他人审查你的代码,可以避免日后出现很多麻烦(参见第十二章修复错误(撤销),了解如何修复错误)。

通常,如果你足够小心(见下文),你将顺利合并而不会出现问题。然而,偶尔你会遇到令人头疼的合并冲突。你将在下面看到几种处理冲突的方法。

书籍

你会记得在前一章中,我们有一个目录C:\GitHub\VisualStudio\ProGitForProgrammers,这是《书籍》应用程序的根目录,我们一直在 Visual Studio 中编辑。当然,我们不必仅仅在 Visual Studio 中管理它;我们可以使用任何工具。例如,我可以打开终端并将目录切换到《书籍》应用程序:

图 4.1:打开终端

注意,它显示我有一个提交待推送(由向上箭头和 1 表示)。我一定是上次工作时忘记了提交。然而,我不想直接推送——谁知道里面有什么?有几种方法可以找出。

推送里有什么?

从命令行,我们可以使用git show命令:

图 4.2:查看推送内容

这里有很多信息。首先,我们看到作者和日期。接下来是附加到此提交的消息(添加属性)。然后,Git 对Book.csBook.cs之间做了diff(差异)比较,前者被命名为a,后者为b。标记为a的是此提交之前的Book.cs,标记为b的是此提交中的新内容。

你可能已经注意到那一行显示/dev/null。这表示一个文件正在与空值进行比较,因此所有内容都是新的。

下一行显示/dev/null与文件b(新的Book.cs文件)进行比较:

图 4.3:与dev/null进行比较

接下来是更改内容。删除部分将以红色标出,修改部分以绿色显示,新增代码则以黄色显示。(此显示方式和颜色可能取决于你使用的终端。)我们看到,这次提交中添加了三个using语句、一个命名空间以及类Book。在我们推送之前,让我们看看在 Visual Studio 中能学到什么。

Visual Studio

在 Visual Studio 中打开相同的目录并进入 Git 视图,正如我们预期的那样,显示了一个待推送的提交(外发):

图 4.4:Visual Studio 显示待推送的一个文件

在我们推送之前,让我们看看这次推送中包含了什么内容。点击1 outgoing会打开两个窗口。Branches窗口显示了我们所在的分支(Book):

图 4.5:Visual Studio 显示本地仓库的内容

中间面板展示了非常酷的信息。它告诉你分支的本地历史(与远程分支相对):

图 4.6:Visual Studio 显示提交历史

我们可以看到main有五个提交(从最新到最旧),在main的最新提交之前,我们有一个来自Book分支的外发提交,其提交信息是Add properties。这与我们在命令行中看到的情况一致。

我们可以进一步操作,返回到Solution Explorer。因为在Program.cs(而非Book.cs)中还有更多内容可以查看,所以右键点击Program.cs,选择Git,然后选择History。这将打开Program.csHistory页面:

图 4.7:Visual Studio 显示 Program.cs 的历史记录

请注意,如果我们注册了带有图片的用户,那张图片会显示在最左侧。

这里我们可以看到四个提交。我们可以通过右键点击其中一个,比如第一个,选择Compare with previous来进行比较。两个窗口将会打开。左侧是较旧的提交,右侧是较新的提交。我们可以看到,在较新的提交中添加了一行内容,Visual Studio 用绿色高亮显示:

图 4.8:并排比较

你可以看到,Visual Studio 可以为你提供一个图形化的展示,展示你从命令行中可能获得的相同信息。

细节,细节

让我们关闭所有这些历史窗口,返回到外发和本地历史记录的列表。在Outgoing下,我们看到Add properties。右键点击该行,一个窗口将会在右侧打开。你将看到提交 ID(ID)、提交者的名字、日期等信息。你还会看到提交信息,接着看到被更改的文件列表(在这个案例中是Book.cs):

图 4.9:Visual Studio 显示提交修改的文件

我们想了解Book.cs文件中发生了什么变化。为此,右键点击Book.cs并选择查看历史记录。中间窗口会显示出唯一的提交。双击该提交,你将看到这个提交中添加的Book类。

GitHub Desktop

我们可以打开 GitHub Desktop 并定位到相同的目录。点击文件并选择添加本地仓库...

图 4.10:打开 GitHub Desktop

下一步是告诉 GitHub Desktop 该仓库的位置。会弹出一个对话框,你可以手动输入本地路径,或者点击选择...,这将打开一个 Windows 资源管理器窗口,允许你选择合适的目录。设置完成后,点击添加仓库

图 4.11:添加本地仓库

现在你将被带到主页面。注意,我们仍然在ProGitForProgrammers仓库中,但处于Book分支,并且系统知道我们有一个提交需要推送。它还提供了一个方便的推送到 origin按钮,点击这个按钮将把提交推送到 origin(即服务器;你在 GitHub 上的仓库):

图 4.12:GitHub Desktop 信息条

再次强调,我们想知道自己正在推送的内容。没问题,只需点击历史记录,你将看到提交历史和每个提交的更改内容:

图 4.13:GitHub Desktop 中的历史记录

现在我们已经看过了命令行、Visual Studio 和 GitHub Desktop 如何管理提交,是时候将提交推送到服务器了。

现在就推送它吧

让我们回到命令行并推送我们正在查看的提交:

图 4.14:从命令行推送

如果你此时去查看 Visual Studio,它应该显示0 个待推送,因为你已经推送了之前等待的提交。同样,GitHub Desktop 的按钮也会从推送到 Origin变为创建 Pull Request—接下来很可能需要执行的操作。

现在我们已经处理好了Book分支,终于可以将它合并到main分支了。

Visual Studio

我们的目标是将Book分支合并回main分支。要在 Visual Studio 中完成这项操作,只需点击Git,然后点击管理分支。你的分支窗口将会打开。右键点击main并选择检出。现在你已经准备好将Book合并到main,这个选项也会出现在右键(上下文)菜单中:

图 4.15:在 Visual Studio 中合并

诀窍是,在切换到main分支时,右键点击Book,你会看到一个合并选项。

合并冲突

让我们转向命令行,执行拉取操作,因为我们的分支已经与原始分支分开。当我们这样做时,Git 告诉我们 Program.cs 中存在合并冲突,并且合并失败了。Git 告诉您解决冲突,然后提交结果。虽然在拉取时出现合并冲突是不寻常的,但如您所见,确实会发生。让我们处理这个冲突,然后设置一个更典型的情况:

图 4.16: 合并冲突

有几种处理合并的方法,但最简单的方法是使用合并工具。我使用的是 KDiff3 (sourceforge.net/projects/kdiff3/)。由于我经常使用它,我已经将其放入了我的配置文件中:

git config --edit --global 

图 4.17: 查看配置文件

这将 KDiff3 设置为我的合并工具,并告诉 Git 在哪里找到它。我最喜欢 KDiff 的一件事情是它通常会为您修复问题。

要调用它,我只需要写:

git mergetool 

并且它打开了冲突的 KDiff。

在这里它发现了两个问题,并且能够修复了一个:

图 4.18: KDiff 自动解决一个冲突

然后它会带我们进入一个带有多个窗格的窗口。顶部的窗格显示了冲突的内容:

图 4.19: KDiff 中显示的冲突

您可以看到左侧 (本地) 我们有一行,而右侧 (远程) 我们有两行。显然,其他人已经编辑了这个我们也编辑的文件,现在 Git 不知道该怎么办了。

底部窗格显示了提供上下文的代码,然后显示了您选择放置哪一行的高亮显示行:

图 4.20: KDiff 提供了合并的上下文信息

当您右键单击该行时,您会看到可以选择左窗口(窗口 A)、右窗口(窗口 B)或两者(并且您可以选择它们添加的顺序)的选项:

图 4.21: KDiff 询问应放置哪个版本

完成后,保存文件并关闭 KDiff。 <poof> 冲突解决了。现在 Git 显示您所做的更改,这些更改现在应该提交了:

图 4.22: 控制台指示冲突已解决

现在您可以添加该文件并提交,然后将其推送到原始库:

图 4.23: 提交并推送到原始库

我们已经看到 KDiff 和类似程序在解决合并冲突时可以大大减少工作量。

从命令行合并

当您特定地在本地合并时,出现合并冲突的情况更为常见。要这样做很简单。从命令行中,切换到您想要合并到的分支 (主分支) 然后使用 Git 命令 merge

图 4.24: 合并命令

在这里 Git 使用了 "递归" 策略;一种加快合并速度的方法。

快进

然而,你通常会看到 Git 报告它使用了快速前进合并。快速前进的工作方式如下:假设你的起始点是这样的:

图 4.25:快速前进

现在你想要将Feature1合并到Main。请注意,Feature1是从Main的末端(最新的提交)分支出来的。在这种情况下,从Main的第一个提交到Feature1的最后一个提交之间有一条路径。在这种情况下,Git 只需要将Main的指针移动到Feature1的末端,创建一个单一的分支(它将称之为Main):

图 4.26:移动指针

因为所需要的只是将指针移动到最后一个提交,所以这叫做快速前进

真正的合并

在之前的示例中,Feature1是从Main的末端分支出来的,这个末端仍然是Main的最新提交。但如果其他人已经将某个分支合并进了Main,而你分支出来的那个提交不再是末端提交呢:

图 4.27:Feature1没有从Main的末端分支出来

在这种情况下,你需要进行重基(将在下一章讲解)或“真正的”合并:

图 4.28:需要进行真正的合并

因为从MainFeature1的末端没有任何一条路径能够不遗漏其他提交(例如,Main的末端),所以我们使用了一个新的提交将两个分支合并在一起。

请注意,这种方法会添加一个新的提交(A),这个提交仅用于合并。随着时间的推移,你会看到很多相对没有意义的提交使历史记录变得杂乱无章。解决这个问题的方法是重基,如下一章所示。

在进行快速前进或真正的合并时,你不会做任何改变;你合并,Git 会处理细节

避免冲突

避免冲突通常是一个好习惯,在 Git 中尤其如此。与其一次性解决大量冲突,你更希望在遇到冲突时及时处理(这样每次处理的冲突数量就会更少)。如果你在一个团队中,有些冲突是不可避免的,但有两个很好的经验法则可以大大减少解决冲突时的工作量:

  • 尽量避免多个程序员同时在同一个文件上工作(如果可能的话)

  • 经常将Main合并到你的特性分支中

注意 #2 并没有说将你的特性分支合并到Main,而是反过来。这不会危及主线程,但会迅速揭示出到目前为止是否有任何冲突。如果有,你可以在你的分支中解决它们,然后继续前进。

总结

在本章中,你学习了:

  • 如何合并分支

  • 不同类型的合并

  • 如何解决合并冲突

  • 像 KDiff 这样的工具如何让合并变得更容易

  • 什么是拉取请求

  • 什么是快速前进合并

  • 什么是真正的合并

挑战

假装你们是两个程序员在开发同一个项目——一个包含计算器和华氏转摄氏温度转换器的工具。如果你们实际上有两个人来做这个任务,那就更好了。

设置一个新的仓库并将其克隆到两个不同的文件夹。让一个人将主分支填充上 UtilityKnife 项目的初步内容,提交更改并推送。让另一个人拉取主分支的更改。

好了,你们两个现在都有一个主分支并且上面有一些代码。接下来,让每个程序员创建自己的分支,一个用来开发计算器,另一个用来开发转换器。在此过程中,转换器需要使用计算器的一些方法。尽量避免或减少冲突,频繁合并,并解决出现的冲突。

答案

像往常一样,没有唯一正确的方法来完成这个任务。以下是我处理这个问题的方式。

任务 #1:设置一个新的仓库并将其克隆到两个不同的文件夹

注意,我们只使用一个仓库。我们在构建一个单一的程序,但至少在开始时,John 将创建计算器,而 Sara 将创建温度转换器。我们将整个程序称为UtilityKnife。首先,我们访问GitHub.com并创建我们的新仓库:

图 4.29:创建新仓库

读取文档文件是使用 Markdown 编写的。你可以在www.markdownguide.org/cheat-sheet/等网络资源中了解更多关于 Markdown 的内容。

然后我们将仓库克隆到文件夹中(如果有两人或更多人,可以克隆到不同的计算机)。我会创建一个名为John的目录,并将这个仓库克隆到该目录下。

图 4.30:从命令行克隆

John 选择使用命令行,而 Sara 则喜欢使用 Visual Studio。

首先点击文件,然后选择克隆仓库...

图 4.31:打开 Git 菜单

这将弹出一个对话框,在其中你可以粘贴从GitHub.com获取的路径和你新仓库的路径:

图 4.32:从 Visual Studio 克隆

点击克隆按钮,Visual Studio 会设置你克隆的仓库。

解决方案资源管理器验证你已克隆仓库并从 GitHub 拉取了三个文件:

图 4.33:解决方案资源管理器显示克隆结果

任务 #2:让一个人将主分支填充上 UtilityKnife 的初步内容,提交更改并推送

我们让 Sara 在她的目录下为 UtilityKnife 程序创建一个新的解决方案:

图 4.34:创建程序

当项目完成时,她将调整Program.cs,以便为接下来的所有内容搭建框架:

namespace UtilityKnife
{
    public static class Program
    {
        static void Main(string[] args)
        {
            // skeleton program
        }
    }
} 

完成这一步后,她将通过Git菜单提交这些更改:

图 4.35:Visual Studio 中的 Git 菜单

这将打开提交窗口,您可以填写提交信息,然后点击提交所有

图 4.36:Visual Studio 中的 Git 更改菜单

一旦操作完成,视图将更改,移除文件和提交信息,并确认提交,同时显示有一个提交准备好推送:

图 4.37:在 Visual Studio 中的提交确认

这正是我们想要的,所以点击上传按钮(向上的箭头)并将提交推送到 GitHub 上的仓库。

它验证了您的成功并提供了为您创建 Pull Request 的选项,但我们现在不需要这个:

图 4.38:提交后,Visual Studio 显示有一个文件需要推送

Sara 现在有了起始主分支,准备创建一个功能分支。在我们查看之前,让我们先让 John 也拉取主分支:

图 4.39:从源拉取仓库

这是一个稍微复杂的截图。我们首先看到在 C:\GitHub\John 中有一个名为 UtilityKnife 的文件夹。我们切换到该目录,然后执行 git pull。结果是获取了 UtilityKnife 程序的文件。

现在,John 和 Sara 拥有相同的 UtilityKnife 起始程序。

任务 #3:每个程序员创建一个功能分支。然后,每个程序员将他们功能的开始部分放入他们的分支,频繁地提交(比在“真实生活”中更频繁)

使用命令行的 John 通过使用 checkout -b 命令开始他的功能分支,这个命令既创建了一个新分支并切换到该分支:

图 4.40:在命令行创建新分支

他现在准备开始编码了。让我们创建一个文件夹,然后在其中创建我们的类框架和它的第一个方法:

namespace UtilityKnife.Converters
{
    public class FahrenheitToCelsius
    {
        public double FahrenheitToCelsiusConverter(double FahrenheitTemp)
        {
            double _fahreneithTemp = 0.0;
            double _celsius = 0.0;
            return _celsius;
        }
    }
} 

让我们保存并提交这个:

图 4.41:从命令行提交

我们首先查看状态,显示我们有一个未追踪的文件。我们将该文件添加进去(记住,add .意味着将所有未追踪和修改过的文件添加到暂存区),然后我们提交它,并添加一条信息。哎呀,提交信息拼写错了。让我们用新命令 amend 修正它。由于我们还没有推送,所需要做的就是输入 --amend,并使用 -m 添加修正后的信息:

图 4.42:使用 amend 标志

请注意,第二行反映了更改,如果我们使用log查看提交记录,我们将看到提交现在已正确拼写:

图 4.43:使用 log 查看提交记录

John 决定将他在本地仓库的提交推送到源(GitHub 仓库)。然而,当他尝试时,Git 告诉他服务器不知道他的分支,但它很友好地给出了正确的命令:

图 4.44:尝试推送失败,Git 帮助

与此同时,Sara 已经开始了Calculator类的工作。

Sara 和 Calculator

在 Visual Studio 中,她点击Git菜单选项,然后选择新建分支。会弹出一个对话框,注意它假设你要从main分支上创建新分支(当然,如果你有多个分支,也可以从任何一个分支上派生):

图 4.45:创建新分支

她现在准备开始编码了,不管她写什么,都不会影响 John 的代码(或者主分支上的代码)。你可以确认,她甚至看不到 John 的工作。他们在不同的(因此是隔离的)功能分支上。

她将会在自己的文件夹中添加一个Calculator类的框架。

namespace UtilityKnife.Calculator
{
    public class Calculator
    {
        public static int Add (int x, int y)
        {
            return x + y;
        }
    }
} 

Sara 现在准备提交这个更改,但与 John 不同,她不会将它推送到服务器。因此,这个更改仅保存在她的本地仓库中。

选择Git | 提交Commit)或暂存Stash)后,她输入消息并点击提交所有Commit All):

图 4.46:提交所有

如上所述,这将她的提交放入本地仓库。

任务#4:经常将主分支合并到功能分支中,确保如果有冲突,能尽早发现

John 想将主分支合并到他的分支中,以确保他能尽早发现问题。为此,他切换到主分支,执行pull操作更新内容,然后切换回他的功能分支并输入merge main

图 4.47:主分支已经是最新的

这里没有问题。自从我们从主分支派生以来,主分支没有变化,所以temperatureConverter分支完全是最新的。

现在,假设 John 决定将他的功能分支合并到主分支。无论这是否明智,他只需要反转合并的顺序:

图 4.48:反转合并顺序

关键的一行是:

git merge temperatureConverter 

我们当前在主分支上,这将功能分支合并到主分支。你可以看到,Git 能够快速前进合并(fast-forward merge),如前面章节中所描述的。

现在 John 可以继续他的现有功能分支,或者创建一个新分支。另一方面,如果他必须先做一个拉取请求(Pull Request),然后等待他的 PR 被批准才能真正合并,他最好新建一个分支,可能是从temperatureConverter分支派生的。

Sara 已经休息过了,但她准备回去工作。为了小心起见,她首先要将主分支合并到她的功能分支中,以确保没有冲突。记住,John 和 Sara 可能合作得很好,但他们并不会每次提交或合并时都互相通知。

首先,Sara 检出了主分支并执行pull操作,以获取主分支上的最新文件。然后她检出了Calculator,并右键点击main

图 4.49:在 Visual Studio 中将主分支合并到 Calculator 中

她将选择将‘main’合并到‘Calculator’。再一次,执行此操作并不会将她的更改合并到主分支,而只是获取主分支的最新版本,并将其合并到她的功能分支中。

由于 Visual Studio 会小心谨慎,它会询问你是否确定。

图 4.50:Visual Studio 检查你即将执行的操作是否符合你的意图

点击 Yes 将开始合并。现在,请记住 John 做了一些工作,然后将他的分支合并到 main 中。由于没有冲突,Visual Studio 只会告诉 Sara 合并成功:

图 4.51:Visual Studio 表示成功

当然,将 main 合并到 Calculator 中会改变 Calculator,带入 main 中的所有内容。main 中的关键内容是 John 合并的部分,现在我们在 Calculator 中看到了这些:

图 4.52:在 Visual Studio 中检查合并的结果

请注意,由于 Sara 还没有将她的代码合并到 main 分支,John 对 Calculator 类一无所知,也无法访问它。如果我们在 John 的分支中打开 Visual Studio,我们会看到 Converters,但没有 Calculator

图 4.53:John 的分支没有 Calculator

让我们暂停片刻,思考一下 GitHub 上发生了什么。Sara 已经提交了她的更改,但还没有推送,因此 GitHub 并不知道她的分支。John 已经推送了他的更改,并将其合并到 main 分支。我们预计 GitHub 上会有两个分支,一个是 main,另一个是 John;更重要的是,此时 main 和 John 应该是相同的,而 Sara 应该在 GitHub 上有一个分支:

图 4.54:origin 上的分支

main 分支有 Converters(来自 John 的合并),但没有 Calculator(因为 Sara 还没有合并)。John 的分支(temperatureConverter)是相同的:

图 4.55:在 origin 上切换分支

为了更清楚地说明这一点,我们可以请求 GitHub 列出它知道的所有分支:

图 4.56:请求 GitHub 列出所有分支

这些都显示为“由我更新”,因为 Sara 和 John 实际上并不存在。

任务 #5:John 正在构建温度转换器。让他“借用”计算器的代码,看看是否会出现合并冲突

在接下来的四次提交中,Sara 将计算器扩展了减法、乘法、整数除法和除法功能。她还没有推送她的更改:

图 4.57:完善计算器

将华氏度转换为摄氏度的公式是:

(F – 32) * 5/9

John 想要转换 212° 华氏度(水的沸点),并期望得到 100° 摄氏度作为一个好的测试用例。为此,他本可以使用内置的减法和除法运算符,但他选择使用 Sara 的计算器。他的第一次尝试是将 main 合并到他的分支中:

图 4.58:将 main 合并到工作分支

主分支是最新的,temperatureConverter与主分支没有差异。然而,约翰没有他需要的功能。这告诉约翰,他所需的计算器功能还没有推送到 GitHub。他可以给萨拉打电话,请她将这些功能推送上去,以便他可以拉取,或者她可以将这些功能合并到主分支,然后他可以从主分支更新。萨拉还没准备好将功能合并到主分支,但同意推送她分支的提交。

她有四个未推送的提交(即还未推送到远程仓库的提交):

图 4.59:Visual Studio 显示有四个未推送的提交

要推送这些更改,她只需点击向上的箭头:

图 4.60:Visual Studio 中的推送按钮

约翰尝试拉取更改,但碰到了障碍。

图 4.61:无法查看计算器

他的本地仓库从未听说过Calculator分支。解决这个问题有几种方法,但最简单的方式是让萨拉将她的工作合并到main分支:

图 4.62:将 Calculator 合并到 main 分支

当萨拉将 Calculator 合并到 main 分支时,她是在本地进行的。她仍然需要将这些更改推送到远程仓库,这样才能对约翰有用。她像提交任何其他提交一样推送它们。

约翰现在准备pull这些更改。一旦他执行了操作,他意识到萨拉使用了整数,而他需要的是浮点数。他修改了Calculator类,改用浮点数,同时他还将所有方法(以及类)设为静态。(如果你不熟悉 C#,不用担心这意味着什么;重要的是他已经做出了更改)

namespace UtilityKnife.Calculator
{
    public static class Calculator
    {
        public static double Add(double x, double y)
        {
            return x + y;
        }
        public static double Subtract(double x, double y)
        {
            return x - y;
        }
        public static double Multiply(double x, double y)
        {
            return x * y;
        }
        public static int Division (int x, int y)
        {
            return x / y;
        }
        public static double Division (double x, double y)
        {
            return x / y;
        }
    }
} 

图 4.63:拉取分支的更改

第二行表示,我们开始时主分支与origin/main同步。然而,当我们执行pull时,本地 Git 发现有对象需要拉取到主分支。共有 22 个对象。为什么是 22 个,而不是仅仅 4 个提交?这些对象中的一部分是 Git 内部使用的。

后来我们看到合并是快速前进(fast-forward)的,接下来的行显示有 29 个添加项,且没有修改或删除(如果你数一下+标记,你会发现是 29 个)。随后确认有 1 个文件发生了变化,并且插入了 29 行。

约翰快完成了。他的本地 main 分支现在有了他需要的内容,但它在错误的分支上。解决方案是将 main 分支合并到temperatureConverter分支。

因为合并的分支顺序很重要,我总是去 Stack Overflow 查找相关信息:

图 4.64:Stack Overflow 建议

这是约翰需要采取的步骤:

图 4.65:合并

最后两行表示Calculator已经通过合并带了过来,temperatureConverter有两个提交要推送到其仓库。

快速查看日志显示,HEADorigin/temperatureConverterorigin/mainorigin/HEADorigin/Calculator 都指向与 main 相同的提交!因此,John 的分支现在可以访问 Calculator 类:

图 4.66:访问计算器

他现在可以返回到他的程序中,使用这些静态方法:

namespace UtilityKnife.Converters
{
    public class FahrenheitToCelsius
    {
        public double FahrenheitToCelsiusConverter(double fahrenheitTemp)
        {
            double _celsius = 0.0;
            // (F – 32) * 5/9
            var step1 = Calculator.Calculator.Subtract(fahrenheitTemp, 32);
            var step2 = Calculator.Calculator.Multiplication(step1, 5.0);
            _celsius = Calculator.Calculator.Division(step2, 9.0);
            return _celsius;
        }
    }
} 

我同意这看起来非常丑陋,但它有效,且更重要的是,它展示了 John 的 temperatureConverter 可以使用 Calculator 中的代码。而且,John 还可以编辑 Calculator。我们将看看在所有这些合并之后会发生什么。

在 John 和 Sara 之间来回跳跃时,我在 Sara 的文件夹中完成了工作。不过没关系。我们只需要让 Sara 提交这些更改。糟糕,修改是在 main 上进行的。我们来整理一下这一切。首先,在 Sara 的机器上,我们将 main 合并到 Calculator

图 4.67:在 Visual Studio 中将 main 合并到 Calculator

这与之前的合并类似,只不过现在 Calculator 是被选中的分支,我们右键点击 main,选择 将 'main' 合并到 'Calculator' 选项。现在,为了确保一切正常,我们将 Calculator 合并回 main。

此时,Sara 的 mainCalculator 分支是相同的,但 John 仍然没有获得所需的内容。Sara 现在可以通过简单的 push 将 main 推送到 origin。

John 现在可以检索 main,这应该包含他需要的更改:

图 4.68:获取带有更改的 main

很好,main 拥有 John 需要的内容,但他需要将其放到自己的分支中。没问题,我们将把 main 合并到 temperatureConverter

图 4.69:将 main 合并到 temperatureConverter

让我们看看 John 的华氏转换器,看看它现在是否已经是最新的。

namespace UtilityKnife.Converters
{
    public class FahrenheitToCelsius
    {
        public double FahrenheitToCelsiusConverter(double fahrenheitTemp)
        {
            double _celsius = 0.0;
            // (F – 32) * 5/9
            var step1 = Calculator.Calculator.Subtract(fahrenheitTemp, 32);
            var step2 = Calculator.Calculator.Multiplication(step1, 5.0);
            _celsius = Calculator.Calculator.Division(step2, 9.0);
            return _celsius;
        }
    }
} 

我们可以通过将方法输入 212 并希望返回 100 来测试是否成功。让我们查看一下程序:

using System;
using UtilityKnife.Converters;
namespace UtilityKnife
{
    public static class Program
    {
        static void Main(string[] args)
        {
            var converter = new FahrenheitToCelsius();
            var celsius = converter.FahrenheitToCelsiusConverter(212.0);
            Console.WriteLine($"Fahrenheit temp of 212 is {celsius}.");
        }
    }
} 

让我们运行我们的程序:

图 4.70:测试程序

我们已经完成了挑战并管理了所有分支。更重要的是,程序可以正常运行!

第五章:重新基准、修改和樱桃挑选

如果你对大多数初学 Git 的程序员说“重新基准”,他们会立刻泪流满面并尖叫着跑出房间。但事实是,这只是因为在许多书籍和杂志中,重新基准的解释通常非常复杂而且令人困惑,图示中显示了提交被复制并随着密集的技术性文字移动。

事实上,重新基准并不难理解,如果你明白它的用途,它也不难操作。在本章中,我们将无所畏惧地回顾重新基准。

重新基准是一个命令,它允许你将一个特性分支放置到另一个分支的顶部。我们将讨论如何做,更重要的是,为什么要这么做。

修改是一个快速命令,它允许你修改最近的提交。你可以用它来添加你忘记包含的文件或修正一个写错的提交信息。

樱桃挑选是将一个或多个提交从一个分支提取并应用到另一个分支的顶部的能力。

Git 程序员将这三个命令描述为重写历史,这也是它们的共同点。每个命令都可以改变提交如何添加到版本库,从而清理你的提交列表。

重新基准

重新基准无非就是将一个分支添加到另一个分支的顶部,其中顶部只是该分支的最后一个提交。例如,假设你有以下结构:

图 5.1: Git 结构

你在这里无法进行快进操作,因为自从你从Main分支出来后,Main已经有了新的进展。你可以做一个真正的合并,但每次进行真正的合并时,它都会在历史记录中增加一个提交:

图 5.2: 真正的合并

重新基准解决了相同的问题,但不会在提交历史中添加合并记录。

请注意,在查看这个历史记录时,你必须跳过许多提交,因为它们只是合并。重新基准可以去除大部分这样的提交。

这里是关键部分:

  • 你将分支Feature1 合并到 Main,但是你将Feature1 重新基准到 Main

  • 回到我们之前的例子,如果你将Feature1重新基准到Main,它看起来是这样的:

图 5.3: 重新基准后的状态

  • 现在从Main的第一次提交到Feature1的最后一次提交之间,路径已经清晰可见,且没有遗漏任何内容。它通常是这样画的:

图 5.4: 重新基准后查看提交的另一种方式

  • 这强调了你已经将Feature1重新基准到了Main的顶部(顶部是Main的最新提交)。

就这点,重新基准就这么简单。说真的。

Git 是如何做到的

通常,我不会花时间去思考 Git 是如何执行操作的,但在这里值得注意的是,要进行 Rebase,Git 会回滚历史记录到第一个 Feature1 提交,然后创建一个副本并将其添加到 Main 的顶部。接着,它会复制第二个 Feature1 提交,并将其放到最新提交的顶部,依此类推。

之所以这样做很重要,是因为会创建一个副本,因此会有一个不同的 ID。好了,现在你可以忘记这一切,把 Rebase 当作它本来的魔力来看待。

理解它

如你所见,Rebase 并不难。但真正理解发生了什么以及为什么它是可以接受的,可能需要从五分钟到五年的时间。我们将我们的功能分支放到了主分支之上。记住,我们的功能分支最初是从主分支派生出来的。

现在我们将主分支中的所有内容并入我们的功能分支。

这是关键部分。因为我们是在本地进行操作,所以我们只是说:“无论主分支进展到什么程度,我最终都需要将其合并进来。现在,我将进行 Rebase 到主分支,使其成为一个长分支,并确保没有冲突。”

早点 Rebase,频繁 Rebase

经常进行 Rebase 是一个非常好的实践,可以及时发现可能出现的冲突。每次进行 Rebase 时,你会得到一个包含所有之前提交的堆栈,然后是你新提交的内容。如果你刚刚添加的内容和之前的内容之间出现冲突,你将立刻看到并可以立即修复它。

仅在本地进行 Rebase

在本地计算机上进行 Rebase,绝不在云中的共享仓库(例如 GitHub)上进行 Rebase。这是因为 Rebase 会“重写历史”——记住,它会复制 Feature1——如果其他程序员正在处理这个分支,当你进行 Rebase 时,他们会对你不满意。这种“不满意”可能会导致刑事指控。

实践中的 Rebase

让我们创建一个新的仓库,命名为 Rebasing。我们将观察 Adam 创建一个名为 Person 的分支,他将采取以下步骤:

  1. 进入主根目录

  2. 创建一个名为 Person 的分支

  3. 打开 Visual Studio 并创建一个名为 Person 的项目

  4. 创建一个名为 Person 的新类

  5. Rebasing 仓库添加到你的本地仓库

  6. 添加仓库并提交(这也会提交 Person

  7. Person 添加年龄属性并提交(但不要推送):

    namespace Rebasing
    {
        public class Person
        {
            public double Age {get; set;}
        }
    } 
    
  8. 添加一个名称属性并提交,但不要推送

在我们添加一个高度属性后,我们就处于这种情况:

图 5.5:在 Visual Studio 中的提交历史

我们现在可以推送了,但 Person 类上还有更多工作要做。另一方面,我们不希望主分支已发生太大变化,以至于在完成 Person 类后会有太多冲突。答案是:Rebase。确保你处于 Person 分支,然后右击主分支并选择 将 'person' Rebase 到 'main'

图 5.6:在 Visual Studio 中进行 Rebase

完成这个之后,我们可以继续在 Person 分支上工作。

注意,您只有四个待推送的提交:

图 5.7:Rebase 不会添加提交

这次 rebase 没有添加提交,并保持了您的历史记录的整洁。

看到 rebase 在实际操作中的效果

为了确认您的 rebase 确实回滚了提交历史,并将每个提交重新添加到主分支上,请转到命令行并执行命令 git log --name-only --oneline

图 5.8:Rebase 回滚历史

从上到下我们看到的是,首先添加了最后一个属性(height),然后我们对 Person.cs 进行 rebase,添加了 name 属性。接着,我们进行 rebase 并添加了 age 属性。我们一直这样做,直到所有文件都被 rebase 到主分支的最新版本。

冲突

在 rebase 时我们可能会遇到冲突。您还记得我们有一个分支:Person。假设第二个程序员克隆了这个仓库。这个程序员在他们的分支(teacher)上高高兴兴地工作时,意识到他们需要将 person 的年龄修改为不同的值。他们向文件中添加了这一变化(好吧,没有人会这么傻,但这种情况在更微妙的情况下确实会发生):

图 5.9:Rebase 导致了冲突

幸运的是,Git 会告诉您该怎么做。

记住,您可以随时输入 git rebase --abort,并返回到开始 rebase 前的状态。

不过,这次我们将手动修复问题。打开指向的文件 Person.cs

namespace Rebasing
{
    public class Person
    {
<<<<<<< HEAD
        public double Age { get; set; } = 35;
=======
        public double Age { get; set; } = 30;
>>>>>>> cb76bd6 (set age to 30)
        public string Name { get; set; }
        public double Height { get; set; }
        public double Weight { get; set; }
<<<<<<< HEAD
=======
>>>>>>> cb76bd6 (set age to 30)
    }
} 

看起来像这样的部分:

<<<<<<< HEAD
        public double Age { get; set; } = 35;
======= 

是当前版本中的代码;接下来的一组代码:

>>>>>>> cb76bd6 (set age to 30)
        public string Name { get; set; }
        public double Height { get; set; }
        public double Weight { get; set; }
<<<<<<< HEAD 

这些提交来自要进行 rebase 的代码。

哎呀,真是一团糟。您可以手动修复此问题,进行调整后删除冲突标记,或者使用上一章中提到的工具。

为了修复这个问题,我们将在我们进行补丁 rebase 的分支上调整年龄;也就是说,35 岁是权威的年龄,因此我们要保留 35 这一行,并删除其他的。

无论如何,一旦您解决了冲突,返回到命令行并输入 git rebase --continue。这将恢复 rebase,并检查是否有其他冲突。如果没有,Git 会要求您输入一条消息,并完成 rebase:

> git rebase --continue
[detached HEAD 5843a73] Rebased, set age to 30
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/person. 

处理 rebase 冲突的关键是不慌张,而是一个一个地解决它们。放心,如果您没有执行 rebase,等到完成功能分支后再尝试合并到主分支时,您会遇到这些问题,甚至更多。

修正

如果您提交了更改后才发现漏掉了一个文件或者搞乱了提交信息,您可以使用 amend 命令。然而,您只能修改最近的一次提交。

假设我们回到 person,并添加一个 weight 属性,然后提交它。在推送之前,我们意识到遗漏了对 Program.cs 的更改:

namespace Rebasing
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person();
            person.Name = "Jesse";
        }
    }
} 

由于错误发生在最近的提交中,我们可以修改该提交。我们只需要将 Program.cs 文件放入索引中,然后执行命令 git commit --amend

由于 amend 会重写历史,你只能在推送之前执行此操作(也就是说,在提交仅存在于本地仓库时),原因与上面提到的一样:

图 5.10:修改(推送前!)

我们逐行分析。首先我们注意到有一个提交等待推送。那就是我们要修改的提交。

在第二行,我们将要修改的文件添加到索引中。

接下来,我们添加 amend 命令:git commit --amend

Git 会响应并打开编辑器,让你修改提交消息(我们在这里看到的消息是:add person and in person add weight)。

最后,它会告诉你关于提交的所有常规信息。

然而请注意,你仍然只有一个提交等待推送。修改似乎并没有创建新的提交(实际上它创建了一个新提交,但你可以安全地忽略它)。

如果你使用没有文件名但带有消息标志的提交,只有该最近的提交消息会被修改。你甚至不需要标志;如果索引中没有新的、修改过的或删除的文件,它会自动打开编辑器供你修改:

图 5.11:这将导致打开你的编辑器

如果你现在查看历史记录并使用 log 命令,你会看到修改后的提交消息:

图 5.12:在编辑器中修改的消息

使用 amend 可以节省很多后续的工作,且为提交编写清晰的消息非常重要,以明确提交的目的。

Cherry-picking

有时你只需要将一个分支中的一个或少量提交添加到另一个分支的顶端。一个常见的情况是:你有一个发布分支和一个功能分支。发布分支已经“冻结”,但你需要将功能分支中的一个提交添加到发布分支(可能是修复问题的补丁)。当你执行 cherry-pick 时,被挑选的提交会被添加到你所选的目标分支的顶端。

下面的示例会帮助你理解。我们从这里开始:

图 5.13:Cherry-pick 前

我们发现我们并不想把所有的Feature1合并到Main,但是我们确实需要Feature1B(它包含我们需要的修复或功能集)。要在命令行中执行此操作,你需要输入 git cherry-pick a2cb5f3,其中 a2cb5f3 是你想要 cherry-pick 的功能提交的 ID。

最终结果看起来是这样的:

图 5.14:Cherry-pick 后

请注意,Feature1B 现在已添加到主分支的顶端,但它仍然保留在功能分支中。

Visual Studio 的 cherry-pick 操作

Visual Studio 对 cherry-pick 提供了极好的支持。只需切换到你想要的分支,然后打开历史记录。右键单击你想要的提交,选择 Cherry-Pick

图 5.15:在 Visual Studio 中进行 cherry-pick

当你从开发分支分出,但在功能分支上创建了一个你后来意识到需要的东西时,挑选提交是至关重要的。

总结

在本章中,我们讨论了一些高级话题:

  • 变基

  • 修改

  • 挑选提交

这三者的共同点是,它们都会重写历史。变基通过将一个分支的所有提交复制到另一个分支的末端来实现这一点。修改提交则允许您添加文件和/或更改提交的信息。最后,挑选提交的行为类似于变基,但只涉及一个或少数几个提交。

你看到许多这些操作在 Visual Studio 中更容易,但有些操作在命令行中做起来更加清晰。

最后,我们解释了为什么要合并主分支,但你要主分支上进行变基。

挑战

创建一个名为 Panofy 的新仓库,向其订阅者提供 MP3 音乐。将有三个分支:主分支(创建仓库时自动生成的分支)和两个功能分支。展示以下内容:

  • 创建仓库

  • 两个程序员创建功能分支

  • 频繁地变基

  • 修改提交以添加文件

  • 修改提交以更改信息

  • 将一个提交挑选到主分支

答复

再次提醒,有很多方法可以解决这个挑战。以下是我采取的方式。

在 GitHub 上创建新仓库

我们以前见过这个,因此我将快速操作。我将访问GitHub.com,并填写常规表单,使此示例程序公开:

图 5.16:创建新仓库

一旦创建了仓库,您和其他任何想要基于它进行开发的人(且拥有适当权限)都可以将其克隆到本地。

创建两个虚拟程序员的功能分支

为此,我将创建两个目录并分别克隆到每个目录。我将第一个目录命名为GitHub/DirA,第二个命名为GitHub/DirB。然后我将分别克隆到每个目录:

图 5.17:将程序克隆到本地仓库

只有 Mateo 会在DirA中编程,而只有 Kim 会在DirB中编程。

DirADirB中创建一个 C# 程序。完成后,使用git status确保它们都指向主分支。为了确认,您可以在DirA中做一个小修改,并确保它在DirB中反映出来。由于 Visual Studio 可能指向不同的目录,因此很容易混淆。您可以随时右键单击项目并选择在文件资源管理器中打开文件夹以进行双重检查。

我将采取的步骤,以确认两个目录具有相同的主分支如下:

  1. 在分支 B 中,我将做一个修改并推送它

  2. 在分支 A 中,我将拉取变更以更新本地仓库。

  3. 最后,我将检查分支 A 中的 Visual Studio,以证明它与分支 B 完全相同:

图 5.18:克隆以确保两个仓库完全相同

为了跟踪我所查看的内容,我将 B 项目的名称更改为PanofyB。然后,我将其推送,并在 A 中拉取,以确保两个分支保持同步。

这种方法充满了风险,最重要的一点是容易覆盖其他开发者的工作,或者引发冲突。为了避免这种情况,我将为每个程序员创建一个分支。我将在 A 上创建一个名为 Calculator 的分支,在 B 上创建一个名为 Converter 的分支。

频繁的 rebase

现在我们有了两个分支,为了简单起见,我们将构建一个新的 UtilityKnife 版本,这次包含小功能和频繁合并。Mateo 会首先创建计算器的结构并提交,你将把它合并到主分支中(通常情况下不会这样做——你会先构建功能的几个方面,频繁提交,然后在完成时再合并,但我们需要一些演示代码)。

当我第一次向 Calculator 添加内容时,主分支是相同的,所以当我做 rebase 时,实际上什么都不会发生:

图 5.19:尽早进行 rebase,频繁进行 rebase

让我们在 Calculator 上做一点工作,然后准备推送。但在此之前,让我们先做一次 rebase,以防在其他线程上已经有了对主分支的工作并且推送了:

图 5.20:拉取更改,然后获取计算器,记得在主分支上做 rebase

没错!即使另一个线程(在这个例子中是 converter)被添加到主分支,我们仍然能够将计算器的代码 rebase 到它之上。我们现在知道至少在目前的开发阶段不会发生冲突。

修改提交以添加文件

让我们回到 Calculator 类。我们将添加一个使用双精度数的除法示例,提交并提交它:

图 5.21:使用 amend 将文件添加到最近的提交

在我们做出提交后,我们意识到我们还打算添加一个平方根方法:

public double SquareRoot(double x)
{
    return Math.Sqrt(x);
} 

我们宁愿不为了这个创建一个新的提交。我们要做的是修改最近的提交。我们通过 --amend 命令来实现。

为了做到这一点,我们将要添加的文件放入索引中,并执行 git commit --amend 命令:

图 5.22:修改提交以包括 Calculator.cs 中的更改

我们在上面显示的代码中采取以下步骤:

  1. 输入 git status。注意我们有一个修改过的文件,因此使用 git add 将其添加到索引中。

  2. 执行 git commit --amend,这会将新代码作为上一个提交的一部分提交。

  3. 编辑器会打开;输入新的信息。注意,新消息现在已经显示出来(第二个箭头)。

这让我们能够将消息修改为更有意义的内容。在关于交互式 rebase 的章节中,你将看到另一种做法。

修改提交以更改消息

如果索引中没有任何新的内容,那么 git commit --amend 将只给你一个修改消息的机会:

图 5.23:使用 amend 修改最近提交的消息

我们执行git status来确保索引中没有任何内容。然后,我们像之前一样调用git commit --amend,但我们添加了一条信息(如果没有添加信息,编辑器会自动弹出)。由于索引中没有任何内容,Git 只是更改了提交信息。

将一个提交樱桃挑选到主分支

这里是主分支和计算器分支的日志:

图 5.24:樱桃挑选到主分支

我们不想将计算器的所有内容合并到主分支,但我们确实希望将乘法和除法的提交合并进来。

好吧,这有点傻,但为了一个实际的例子,假设主分支是你的发布分支,而计算器分支有一个重要的功能,你希望将它添加进去。

注意每个提交旁边的七位数字 ID。要将972d77a樱桃挑选到主分支,我们确保主分支是当前分支,然后执行樱桃挑选命令,指定我们要添加的提交的 ID:

图 5.25:执行樱桃挑选命令

你已将972d77a提交到主分支,主分支有一个提交待推送。在我们推送之前,让我们再看一下这些日志:

图 5.26:查看樱桃挑选后的日志

这里有三点需要注意:

  1. 现在主分支包含了添加乘法和除法的提交

  2. 乘法和除法并没有从计算器中移除

  3. 这两个提交的 ID 不同,意味着它们是独立的提交,一个的操作不会影响另一个

这个示例答案符合创建两个功能分支的要求,分支来自一个新的仓库,并且“程序员们”会频繁地进行变基操作。我们还修改了一个信息(在本地),并使用樱桃挑选将一个提交复制到主分支。

第六章:交互式变基

交互式变基是 Git 中一个非常有用的功能,但其名字可能会让人感到困惑。从用户的角度来看,变基和交互式变基几乎没有什么共同点。

交互式变基允许你清理提交,但只能在你推送到服务器之前进行。通过交互式变基,你可以:

  • “压缩”你的提交,以使提交历史更简洁,便于阅读

  • 修改提交信息

  • Fixup,功能类似于 squash,不同的是它不会停止并要求你输入新的提交信息

  • Drop,用于移除一个提交

这里的关键是你正在修改提交,而不是提交中的文件。而且,正如我会一再强调的,你必须在推送提交到 origin 之前进行交互式变基。提交一旦推送到服务器,就不应再修改,因为其他开发人员可能正在与这些提交进行交互,修改可能会导致冲突,而冲突的修复非常耗时。关于合并冲突的处理,请参见第四章,《合并、拉取请求与处理合并冲突》。

交互式变基的工作示例

为了了解这一点的工作原理,我们需要做几个提交。让我们创建一个简单的程序,并为每一行代码创建一个提交。等我们完成后,就可以在命令行和 Visual Studio 中查看如何进行交互式变基。

注意:你通常不会这么频繁地提交,但我们需要一些提交来进行演示。

创建我们的示例

为了多样性,我们来创建一个音乐跟踪应用程序的框架。第一步是在 GitHub 上创建一个仓库:

图 6.1:创建仓库

创建仓库后,我们需要将其克隆到本地仓库。这次我们使用 Visual Studio,并利用 GitHub 与 Visual Studio 的集成。在 GitHub 上点击代码,然后在下拉菜单中选择使用 Visual Studio 打开

图 6.2:使用 Visual Studio 下载提交

当你这么做时,Visual Studio 会打开并提供保存你的项目,选择了名称和默认位置。对于这个练习,创建一个包含该项目的解决方案,然后添加一个Music类:

图 6.3:使用 Visual Studio 克隆一个仓库

点击右下角的克隆,你将进入你的应用程序。如果你尚未在 Visual Studio 中登录 GitHub,系统可能会要求你进行登录。

由于在创建仓库时已选择了勾选框,你的三个文件已经出现在解决方案资源管理器中(README.mdLICENSE.gitignore)。下一步是创建一个项目。在我们的案例中,我们将创建一个名为MusicHandler的控制台应用程序。点击文件 | 新建项目,然后选择控制台应用程序。系统会询问你想使用哪个框架。对于这个控制台应用程序,任意框架都可以,我选择.NET 5。点击右下角的创建

你的应用程序已经创建,并包括Program.cs。让我们将其作为第一次提交。在 Visual Studio 中,我们可以点击Git菜单项,并选择提交或暂存

图 6.4:在 Visual Studio 中的第一次提交

一旦你选择它,右侧窗口(即解决方案资源管理器所在的地方)将变成 Git 处理器:

图 6.5:Visual Studio 的主要 Git 处理器

这里有很多信息。在图形的底部,你可以看到项目和Program.cs被标记为新的(大写字母 A),并且有两个更改(这是正确的)。你可以通过点击+号来暂存它们,或者你可以点击提交全部来同时暂存和提交文件。

输入你的信息并选择提交全部

图 6.6:在 Visual Studio 中创建提交后

视图会立即在几个显著的方面发生变化。顶部显示了一个本地创建的提交——也就是说,提交是在本地仓库中创建的。你还会看到现在有“1 个待推送”——即本地有一个提交尚未推送到服务器。

你还会看到一个修正复选框;如果你想更改最近一次提交的信息,可以使用它。即使我们有一个待推送的提交,我们也不打算推送那个提交。我们需要一些本地提交。

我们需要一个解决方案来继续工作,因此在相同位置创建一个新的项目/解决方案。完成后,你的解决方案资源管理器应该像图 6.7所示:

图 6.7:在仓库文件夹中创建项目

我们现在准备创建我们的Music类:

using System;
public class Music
{
     public Music()
     {
     }
} 

让我们提交它。你会看到文件尚未被追踪。你需要将它们添加到索引中:

git add . 

现在你准备好提交它了:

git commit -m "Add music class" 

如果你查看日志(git log --oneline),你应该会看到所有三个提交:一个是在你克隆仓库时创建的,另两个是你手动创建的:

8147adb (HEAD -> main) Add music class
238230b Initial Commit
b6fc88f (origin/main, origin/HEAD) Initial commit 

让我们创建两个新的提交。我们将首先为Music类添加一些属性,并在添加每个属性时进行提交:

using System;
public class Music
{
    public string Name { get; set; }
    public string Artist { get; set; }
    public DateTime ReleaseDate { get; set; }
} 

我们可以通过进入Git菜单并选择查看分支历史来查看我们的提交(这和git log --oneline很像):

图 6.8:在 Visual Studio 中查看提交

结果是一个包含提交 ID、提交作者、提交日期和提交信息的列表,如图 6.9所示:

图 6.9:在 Visual Studio 中查看提交历史

注意,Visual Studio 会区分那些已经在服务器上的提交(在本地历史下)和那些尚未推送的提交(在待推送下)。

让我们再做三个提交。如果你按照步骤操作,那么你应该和我一样,Music类没有放在任何命名空间下。让我们来修复它:

using System;
namespace MusicHandler
{
    public class Music
    {
        public string Name { get; set; }
        public string Artist { get; set; }
        public DateTime ReleaseDate { get; set; }
    }
} 

现在将 Music.cs 从解决方案中拖出并放入项目中,这样你就修复了我的错误。当从 Git 仓库中通过 Visual Studio 获取解决方案时,这是一个常见的错误。

提交这些更改。

现在我们可以在 Program.cs 中创建一个 Music 实例并提交:

static void Main(string[] args)
{
    var music = new Music();
    music.Name = "Ripple";
    music.Artist = "Grateful Dead";
    music.ReleaseDate = new DateTime(11, 1, 1970);
} 

现在我们可以使用 Visual Studio 中的 Git 菜单来查看完整的提交历史:

图 6.10:返回 Visual Studio 查看完整的提交历史

我们应该在命令行的日志中看到相同的信息:

图 6.11:使用日志查看提交历史

使用互动 rebase 清理你的提交记录

假设我们不希望名称、艺术家和发行日期这三个提交作为独立的提交出现在历史记录中,造成杂乱。这时互动 rebase 就派上用场了。我们只需要计算出从 Add Name property 开始的提交数量,向下数到一或两个。假设我们选择了 7,然后输入:

git rebase -i HEAD~7 

Git 会回应:

hint: waiting for your editor to close the file… 

它将打开 Visual Studio Code,如图 6.12所示:

图 6.12:等待你的编辑器打开

接下来是有趣的部分。对于每一行,我们有几个选项:

  • 保留 pick,表示保持提交不变

  • 合并 – 我们想要的那个,下面会解释

  • 删除 – 跳过该提交 – 删除它

  • 标记 – 用标签标记选择的提交(参见第六章使用日志

如果这对你有帮助,你还可以重新排序你的提交。

让我们做我们打算做的事,将发行日期、名称和艺术家合并到上面的提交中(创建 music 类)。在 Visual Studio Code 中进行如图 6.13所示的更改:

图 6.13:互动 rebase 编辑器中的内容

注意,我们设置为将发行日期合并到艺术家中(使它们成为一个提交),然后将该提交合并到名称中,再将所有这些合并到 music 类中。这样,四个提交将合并为一个提交。

保存文件;Git 会重新打开文件并提供修正消息的选项。它首先会显示你之前的消息,如图 6.14所示:

图 6.14:消息历史

你现在可以选择你的消息(并且可以编辑它),包括或排除之前的消息。

我将选择一条有意义的消息:

图 6.15:选择你想保留的消息

再次注意,文件底部有大量评论,帮助你理解发生了什么:

图 6.16:关于互动反馈的进度笔记

当我们保存并关闭这个文件时,Git 告诉我们 rebase 成功完成。

如果此时你询问 Git 当前的状态,你将看到关于 rebase 进度的回顾:

图 6.17:互动 rebase 后的 Git 状态

输入 git rebase ––continue,你应该会看到如 图 6.18 所示的回顾和成功信息:

图 6.18:交互式 rebase 的 Git 摘要

你的 rebase 操作成功了。我们来看看日志:

图 6.19:日志反映了交互式 rebase

让我们回到 Visual Studio 并查看历史:

图 6.20:在 Visual Studio 中查看修改后的历史

注意,中间的提交已消失!它们已合并到 添加音乐课 中。

交互式 rebase 既强大又安全。它可以在推送之前清理你的提交,使得你的队友更容易阅读历史。如果在使用交互式 rebase 时遇到问题,你可以输入:

--abort 

返回到交互式 rebase 开始之前的状态。

我承认,我一直在使用压缩操作,几乎从不使用其他选项;不过,知道它们的存在是很好的。

总结

在本章中,你学到了:

  • 如何使用交互式 rebase 在推送之前压缩提交

  • 交互式 rebase 中的其他选项

  • 交互式 rebase 中压缩操作的影响

  • 如何修正你的 rebase 文件的提交信息

挑战

在这个挑战中,你将创建一个新项目,并使用 GitHub Desktop 克隆到本地仓库。然后,你将至少添加 7-8 次提交。最后,你将使用交互式 rebase 将一些提交压缩到一起。你可以自由地尝试交互式 rebase 中的其他选项。

对于我的项目,我将创建一个追踪《摇滚恐怖秀》的解决方案(这是一部真正的经典)。

第一步,像往常一样,创建新的仓库:

图 6.21:创建仓库

第二步是将仓库克隆到你的磁盘;不过这次我们将使用 GitHub Desktop:

图 6.22:使用 GitHub Desktop 下载仓库

GitHub Desktop 将启动:

图 6.23:启动 GitHub Desktop

GitHub Desktop 会启动并弹出一个模式对话框,要求你确认或更改仓库,并选择将其放置在哪个磁盘位置:

图 6.24:克隆到 GitHub Desktop

如预期,你的仓库已克隆到你指定的文件夹,并且 GitHub Desktop 已设置为你的仓库和主分支:

图 6.25:确认本地仓库已创建

下一步是创建该目录中的一个项目。GitHub Desktop 会立即在左侧显示你输入的文件,在右侧显示已选文件的更改:

图 6.26:在你的仓库目录中创建项目

我们想要提交这个,但不推送它。我们通过左下角的提交区域进行提交:

图 6.27:在 GitHub Desktop 中创建提交(带有信息)

GitHub Desktop 重新配置,展示你的项目当前状态:

图 6.28:你在 GitHub Desktop 中的项目状态

从左到右跟随箭头,我们看到现在没有更改要报告;这一点在标题中得到了重复,第三个箭头指向按钮,表示将更改推送到远程仓库(我们现在不会进行推送)。

在左上角有一个按钮,显示为历史记录;点击该按钮会带你到提交历史页面,列出每次提交中添加或修改的文件:

图 6.29:GitHub Desktop 中的提交历史

这里有很多信息可以查看。高亮显示的提交有一个向上的箭头,表示可以将其推送到远程仓库。它还显示了提交的单行标题,右侧也会显示提交消息。再次强调,点击该提交中的任何文件都会在右侧窗口显示更改(未显示)。

现在让我们像过去一样添加提交。我选择从创建一个名为Showing的类开始,该类将包含电影在波士顿一周内每个放映的地点和时间的属性:

namespace RockyHorror
{
    public class Showing
    {
    }
} 

我会在创建类及其每个属性后进行提交。完成后,Showing看起来是这样的:

namespace RockyHorror
{
    public class Showing
    {
        public string Location { get; set; }
        public int NumberOfSeats { get; set; }
        public List<DateTime> ShowTimes { get; set; }
    }
} 

我第一次保存这个文件时,ShowTimes只是一个DateTime,但我很快意识到,这样会需要在每个位置为每个放映时间创建一个对象,因此我将其更改为DateTime对象的列表。

Program.cs最终看起来像这样:

using System;
using System.Collections.Generic;
namespace RockyHorror
{
    class Program
    {
        static void Main(string[] args)
        {
            var showing = new Showing();
            showing.Location = "Brattle";
            showing.NumberOfSeats = 250;
            showing.ShowTimes = new List<DateTime>
            {
                new DateTime(0,0,0,10,0,0),
                new DateTime(0,0,0,13,0,0),
                new DateTime(0,0,0,16,0,0),
                new DateTime(0,0,0,19,0,0),
                new DateTime(0,0,0,22,0,0),
                new DateTime(0,0,0,0,0,1)
            };
        }
    }
} 

我第一次提交ShowTimes时,忘记了包括午夜(天啊!)。让我们来看一下 GitHub Desktop 中的历史记录:

图 6.30:GitHub Desktop 中的提交历史

在我们推送这些更改之前,让我们用交互式变基清理一下。我们可以合并一些提交,更有趣的是,我们可以丢弃只包含单个时间的提交,因为它已经被将ShowTimes转变为列表的提交所取代。

最简单的方式是使用命令行调出编辑器。

这是我们交互式变基的初始状态:

图 6.31:交互式变基前的状态

这里是最终的样子:

图 6.32:交互式变基后的状态

我们将放弃包含DateTime的提交,因为它被下一个提交替代,在该提交中,ShowTimes变成了DateTime对象的列表。我们还将把所有条目压缩为一个提交。保存后,我们将有机会修正提交消息。

哎呀,我们收到了一个冲突通知:

图 6.33:交互式变基过程中的冲突

Git 为我们提供了几种选择:解决冲突然后继续变基,或者跳过有冲突的提交并中止变基。我们来通过回到 GitHub Desktop 来解决问题:

图 6.34:在 GitHub Desktop 中找到冲突

注意 GitHub Desktop 也知道冲突,并提供多种选择:

  • 在 Visual Studio Code 中打开

  • 在命令行中打开

  • 用你喜欢的工具打开

  • 手动解决

让我们选择在 Visual Studio Code 中打开:

图 6.35:Visual Studio 中的冲突

Visual Studio Code 努力帮助你进行更改。注意小菜单,允许你接受其中一个或两个更改(并且它会给出关于传入更改的消息,确保你知道自己在选择什么)。完成后,文件看起来和你打算的一样:

using System;
using System.Collections.Generic;
namespace RockyHorror
{
    public class Showing
    {
        public string Location { get; set; }
        public int NumberOfSeats { get; set; }
        public List<DateTime> ShowTimes { get; set; }
    }
} 

保存并关闭文件。当你返回命令行时,将修复后的文件添加回去,然后告诉 Git 继续:

git add .
git rebase --continue 

Visual Studio Code 将再次打开,允许你修正提交消息。保存并关闭后,Visual Studio Code 将再次打开,允许你修正所有消息。一旦保存并关闭,非常令你宽慰的是,Git 会告诉你变基成功了:

图 6.36:您的交互式变基已成功!

第七章:工作流程、笔记和标签

在本章中,我们将看到

  • 使用 Git 的标准工作流程

  • 什么是笔记

  • 如何使用笔记

  • 什么是标签

  • 如何使用标签

我们将从检查标准工作流程开始。

标准工作流程

标准工作流程基本上就是我们在前五章中看到的,只是你通常不会这么快速或频繁地提交。通常,它的流程如下:

  1. 创建一个仓库。

  2. 可以从服务器克隆该仓库,或者如果它是本地创建的,可以将其推送到服务器。

  3. 创建一个分支。

  4. 代码。

  5. 测试。

  6. 提交。

  7. 重复步骤 4-6,直到你有一段执行“某些操作”的代码(例如打开一个对话框并处理结果)。

  8. 测试。

  9. 提交。

  10. 推送。

  11. 重复步骤 4-10,直到你完成一个需求(无论是自我设定的还是其他的)。

  12. 合并到主分支(如果你在团队中,创建一个拉取请求)。

这方面有一些变体。有些人喜欢每次提交后就推送,但这样会阻止他们使用交互式变基来重组他们的提交。但是,如果你已经推送了提交,然后意识到有额外的信息你希望能添加到提交信息中,会发生什么呢?

不要修改你已经推送的代码。(我之前说过吗?)那么,该怎么做呢?

如果问题比较严重(你需要修改提交内容等),那么你需要采取更为激烈的措施(参见第十二章《修复错误》)。但如果只是更新消息的话,可以考虑添加一个笔记。

镜像你的仓库

我们想继续讨论笔记,但为了做到这一点,我们需要稍微偏离一下,先镜像我们的仓库,这样我们就可以添加笔记而不会搞乱现有的仓库。

我们想要镜像的仓库应该有足够数量的提交。你可能还记得在上一章中,我通过创建一个名为RockyHorror的仓库来解决挑战。让我们打开这个仓库,在本地机器上使用日志查看提交:

图 7.1:现有仓库中的内容是什么?

注意:你并没有疯:在这一章中,我已经从 PowerShell 切换到了 Bash shell。

复制现有仓库

如你所见,有九次提交,这对于我们的目的已经足够了。不过,我不想修改这个仓库(主要是为了当你下载代码时,在阅读第六章《交互式变基》时,它看起来是正确的)。

要将这个仓库的精确副本(包括提交、消息等)放入另一个仓库,我们将使用 Git 的 --mirror 标志。以下是操作步骤。

切换到 RockyHorror 目录,并使用日志确认你已在本地仓库中,使用--oneline标志,正如之前在图 7.1中所示。

现在,我们需要一个仓库来放置我们的镜像版本。去 GitHub 创建一个名为RockyHorror2的新仓库:

图 7.2:创建新仓库

正如你所预期的,现在你在服务器上有一个仓库 RockyHorror2,但在本地没有。接下来,我们将用 RockyHorror 的内容覆盖服务器上的文件、提交等,给我们提供一个完全相同的副本来进行工作。

要做到这一点,确保你处于原始仓库(RockyHorror)中,然后使用 ––mirror 标志将内容推送到服务器,并推送到新的仓库(RockyHorror2)。你需要新仓库的地址,因此首先去服务器上的克隆按钮,复制地址,但不要克隆仓库

图 7.3:将内容镜像到新仓库

好的,让我们复习一下。在你的终端(Bash、PowerShell、Terminal 等)中,你位于 RockyHorror 目录下,如果你运行 git log --oneline,你将看到如图 7.1所示的结果。

现在你已经准备好将这个仓库镜像到本地。你将在终端中进行此操作,但请记住,这将把这个仓库的镜像推送到服务器,并覆盖 RockyHorror2 中已有的内容(在此案例中仅有 README.mdLICENSE.gitignore 文件)。

这将导致 Git 执行一系列操作,最终的效果是将 RockyHorror 中的所有内容复制到 RockyHorror2

图 7.4:从服务器复制到本地仓库

现在你可以去 GitHub 查看,RockyHorror2 已经更新为 RockyHorror 的完全副本(如果没有看到,记得刷新页面):

图 7.5:查看镜像仓库

图 7.5中有一些有趣的内容。首先,注意左上角显示的是 RockyHorror2,但是如果你查看 README 文件,它上面写的是 RockyHorror。这是因为这个 README 文件来自原始的 RockyHorror 仓库。还要注意,文件不是几分钟前的,而是我在 6 天前在原始仓库中修改的;这是因为这些文件是 6 天前在原仓库中修改的。要点是,虽然这看起来是一个副本,但它实际上是 RockyHorror 的完全复制品。

现在转到你本地的 RockyHorror2 目录。什么?它不在那儿?对,我们只是在服务器上进行了镜像。如果我们想要本地仓库,我们需要克隆我们新的仓库。你可以通过命令行、Visual Studio 或 GitHub Desktop 来完成这一操作(或者任何你喜欢的 GUI,SourceTree 和 Fork 在撰写本文时非常流行)。

当你进行克隆时,请确保复制新仓库的地址,而不是原始仓库的地址:

图 7.6:确保你在正确的位置

现在你可以进入新的 RockyHorror2 目录。进入后查看里面的日志:

图 7.7:获取镜像仓库的日志

这里需要注意的一个关键点是,提交记录以及 HEADorigin 指向的位置与图 7.1所示的 RockyHorror 完全相同。

请注意,ID 也是相同的。我个人觉得这几乎让人吃惊,但就 Git 而言,这只是同一个仓库的另一个副本。然而,从现在开始,你可以改变一个,而不会影响另一个。

添加和显示笔记

现在我们已经准备好为我们的一个提交添加笔记。

让我明确一点,你不需要镜像你的仓库来使用笔记。我们在这里这么做仅仅是为了本书的目的;以确保仓库与每章中所展示的内容相对应。通常,你只需要添加笔记即可。

笔记只是你可以在提交已存在于仓库之后附加到提交上的一些文本。笔记的常见用途是解释某个提交如何与其他提交相关,或者标记一个提交需要修改或重新基准,或者添加你希望粘贴到提交中的任何信息。它不会改变提交;它就像一个便条纸,你贴在上面。

要添加笔记,你需要使用git notes命令并加上一个或多个标志。例如,如果你在图 7.7中有代码,且想要为当前显示“Remove Hello World”的提交添加笔记,我们只需要获取 ID commit id—bb4927c—,然后像这样执行:

git notes add -m "Remove from program.cs" bb4927c 

如果你现在运行git log,你将看到日志列表中的笔记,前面会有“Notes”字样:

![](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/B17741_07_08.png)

图 7.8:查看附加的笔记

如果你想看到带有笔记的更改,可以使用show子命令:

图 7.9:查看带有笔记的更改

再次提醒,当需要时,你可以谷歌搜索很多子命令。

标签

为某个提交标记一个名称可能会很方便。例如,你可以将某个提交标记为开发者版本,另一个标记为正式版本。每次发布新版本时,你都可以添加另一个标签,这样就能快速、清晰地浏览历史记录,查看每个版本之前或之后添加了哪些提交。

让我们再回头看一下图 7.1。我们可能会决定将提交Enter show times视为创建 Show 对象的最后一个提交,并且我们想要表明这一点。我们可以用笔记来做,但在这种情况下,用标签标记这个提交可能更方便。

你可能使用两种类型的标签:简单标签和注解标签。我们先从简单标签开始:

图 7.10:LastShowCommit 的标签

图 7.10中,我们首先获取所有提交的在线日志。然后我们添加标签:

git tag LastShowCommit bf6b900 

如你所见,我们使用关键字tag,后跟标签本身(一个单词,不加引号),然后是我们正在标记的提交的 ID。

请注意,我们标记的是提交,而不是某个特定文件。这个标签适用于该提交中的所有文件。

第二种类型的标签是注解标签,如图 7.11所示:

图 7.11:注解标签

当你使用在线日志时,它与其他标签一样出现,如图 7.12所示:

图 7.12:在线日志中的注解标签

然而,如果你使用show命令,你可以看到标签的附加信息(即创建时的消息)和由谁创建。它非常类似于提交,只不过不会影响文件,并且标记了tagtagger关键词:

图 7.13:注释标签

指向不同的标签

如果你创建了一个标签,但指向了错误的提交,可以通过使用强制标志来更改它指向的内容。例如,假设你有图 7.14中所示的提交列表:

图 7.14:指向错误提交的标签列表

注意,标签TestOfShowObject指向f55eb4e。不幸的是,我们本来是想指向下一个提交(e16d191)。为此,我们可以写:

git tag -f TestOfShowObject e16d191 

我们需要强制标志(-f)来确保 Git 不会报错Fatal: tag TestOfShowObjects already exists

图 7.15:使用强制命令重新分配标签

图 7.15所示,Git 回复了更新标签的消息、标签的名称以及它之前指向的内容。标签现在已经被移动到e16d191,正如我们所希望的那样。

最后,我们可以使用-d标志删除一个标签:

图 7.16:删除标签

图 7.16显示 Git 确认了删除操作,运行日志显示标签已被删除。

总结

在本章中,你学到了

  • 使用 Git 的标准工作流

  • 注释是什么,如何创建注释

  • 标签是什么,如何创建、移动和删除标签

挑战

创建Panofy的本地副本。向其中一个提交添加注释,并确保它存在。向其中一个提交添加标签并确保它存在。最后,改变标签指向的提交。

这是我的回答:

首先,切换到Panofy目录。如果它不在本地机器上,克隆它:

图 7.17:切换到 Panofy 项目

图 7.17所示,当我尝试切换到Panofy目录时,系统告诉我该目录不存在,于是我从服务器上克隆了它。

为了创建镜像,我首先在服务器上创建一个名为Panofy2的新仓库:

图 7.18:镜像

图 7.18所示,这次我没有费心创建许可证文件,因为所有这些内容将在我将 Panofy 镜像过来时被覆盖。为此,我将目录切换到 Panofy(原始仓库)并输入:

git push ––mirror https://github.com/JesseLiberty/Panofy2.git 

这会将我所在的仓库(Panofy)推送到新地址,镜像原始仓库:

图 7.19:推送到服务器上的镜像仓库

我将在服务器上切换到Panofy2,果然,许可证现在已经在那里(来自Panofy),并且有了Panofy文件夹,如图 7.20所示:

图 7.20:Panofy 的许可证

这样一来,我可以安全地更改Panofy2。第一项任务是添加一个注释。我们从日志开始,这样我们就可以看到现有内容:

图 7.21:在镜像仓库中为提交添加注释

让我们为提交添加一个信息为Add Hello Message的注释,内容是Traditional first Hello World

图 7.22:添加注释

图 7.22中,我们看到这次我没有在 Git 提交语句中添加注释。相反,我等待编辑器(在我的情况下是 Visual Studio Code)打开,然后把语句放入其中。当我关闭文件时,注释已被输入,如图 7.23所示:

图 7.23:使用编辑器添加注释

让我们再看一次日志:

图 7.24:检查日志

我将添加一个注释标签到提交中,标签内容是Update csproj

图 7.25:检查注释标签

图 7.25中有四个箭头。第一个指向标签的创建。第二个显示标签名称,第三个显示标签的创建者,最后一个箭头指向标签本身的文本。

让我们再次查看日志,如图 7.26所示:

图 7.26:单行日志中的注释标签

我们可以看到标签位于877348c,但事实证明我们忘记了更新csproj,并在提交4b080ba时修复了这个问题。让我们将标签移到那里:

图 7.27:移动标签

在最后一张图中,图 7.27,你可以看到我们已经将标签ReleaseCandidate移动到了4b080ba,正如预期的那样。

第八章:别名

停止那么辛苦地工作!在这一章中,我们将学习 Git 别名,它们大大减少了你需要输入的内容。别名可以非常简单,也可以带有参数和标志。

别名

别名允许你创建git命令的快捷方式。例如,我有一个st别名,代表状态。因此,我输入:

git st 

就好像我输入了:

git status 

我们很快就会看到更令人兴奋和有用的别名,但首先让我们来看一下这些是如何创建的。要创建一个别名:

  • 输入git

  • 输入关键字config

  • 输入标志--global

  • 输入关键字alias,后跟一个句点,然后是别名本身

  • 输入你想要创建别名的命令

这听起来比实际要复杂。举个例子,要创建st别名,我输入了:

git config --global alias.st status 

当然,你不一定非得使用global。你可以选择systemlocal,但就个人而言,我总是使用global,因为我是这个电脑上唯一的用户,我希望它始终可用。

这里是一个稍微复杂一点的别名,它允许你创建一个分支并切换到该分支:

git config --global alias.bc checkout -b 

这里需要注意的是,你的别名可以带一个或多个标志。

我总是记不住是bc还是cb,所以我创建了另一个别名来执行相同的命令:

git config --global alias.cb checkout -b 

我经常使用的一个别名会提交所有内容并等待我的消息:

git config --global alias.cam commit -a -m 

当我输入git cam时,它会提交所有内容并附上我给定的消息:

git cam "Here is my message" 

最后,这是我最喜欢的别名:

git config –global alias.lg log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(yellow)<%an>%Creset' --abbrev-commit 

这为我提供了一个替代log --oneline的选项,能够提供更多的信息:

图 8.1:git lg

从左到右看,我们看到 SHA 后跟提交消息,接着在括号中可以看到提交的时间以及由谁提交。如第四行所示,如果有标签,它会显示在消息之前,指针(例如HEAD)会显示在 SHA 之后。

我们简单拆解一下这个别名:每种颜色都被%C%Creset包围。一些显示项使用快捷方式显示,比如%h,它会显示 SHA。因此,为了将 SHA 显示为红色,我们使用'%Cred%h%Creset

所有这些都存储在你的全局配置文件中,你可以通过输入以下命令访问:

git config --edit --global 

它会在编辑器中打开全局配置文件。在这里,你会找到多个部分,其中一个部分包含了别名:

[user]
    name = Jesse Liberty
    email = jesseliberty@gmail.com
[alias]
    co = checkout
    bc = checkout
    cb = checkout
    st = status
    cam = commit -a -m
    lg = log –graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(yellow)<%an>%Creset' --abbrev-commit 

注意,别名在那里,但语法稍微不同。如果你喜欢,可以直接在这里添加别名。(注意,红色下划线没有任何意义。这只是 Visual Studio Code 指出它无法识别这些术语。)

请注意:如果你要使用多个标志,你必须把别名放在引号中,如这一行所示(你将在我回答挑战时再次看到它):

git config --global alias.nx  "log --name-only --oneline" 

总结

别名是一个方便的方式,用来缩短本来很长的命令。你可以使用以下序列创建别名:

  • 输入git

  • 输入关键字config

  • 输入标志--global

  • 输入关键字别名,后跟一个点号,然后是别名本身

  • 输入你正在别名化的命令

你可以直接使用以下命令访问配置文件:

git config --edit --global 

在命令行工作时,别名简单、易用且非常实用。

挑战

创建一个替代以下命令的别名:

git log ––name-only --oneline 

答案

为此,我将进入命令行并输入:

git config --global alias.nx  "log --name-only --oneline" 

需要使用双引号,因为你在log中使用了两个标志。

调用此命令的结果如图 8.2所示:

图 8.2:我们的新别名工作示例

请注意,每个提交都在那儿,以单行显示,并且只包含 SHA 和消息(除了当存在标签或指针时,如图 8.2中的第 1 行和第 7 行所示)。

第九章:使用日志

Git 中最强大的命令之一是log。你已经在前几章看到过日志的用法,现在是时候深入了解它了。

日志可以显示每个提交的创建时间、创建者以及关于提交的其他有用信息,比如每个文件的变化内容。你可以对显示内容进行精细控制,正如本章所示。

开始使用日志

让我们快速构建另一个项目和仓库:

图 9.1:创建新仓库

接下来,正如我们之前所做的,我们将把这个仓库克隆到本地计算机:

图 9.2:克隆示例程序

使用这个本地仓库,我们可以开始使用log命令查看其提交记录。要做到这一点,当然,我们需要先创建一个程序并进行一些提交。

LogDemo 程序

LogDemo目录中创建一个程序。将程序修改为公开的,并进行构建和运行,以确保它能够正常工作:

图 9.3:测试程序

我将创建与之前相同的calculator类,并在每个小功能后做提交。我会省略这些内容,只将其放入仓库中。

添加完所有功能后,我们来试一试:

using System;
namespace LogDemo
{
    public class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator.Calculator();
            Console.WriteLine($"5+3 = {calculator.Add(5, 3)}");
            Console.WriteLine($"The square root of 3.14159 is {calculator.squareRoot(3.14159)}");
        }
    }
} 

结果应为:

5+3 = 8
The square root of 3.14159 is 1.7724531023414978 

我现在有了多个提交,我们可以使用在第八章中描述的lg别名来查看这些提交:

图 9.4:使用lg别名查看提交历史

我们可以看到有 12 个提交,并且可以从最后一行看到这些提交都没有被推送,因此我们领先于远程仓库 12 次提交。通过status命令可以确认这一点:

图 9.5:状态显示有 12 个提交等待推送,工作目录为空

一旦提交完成,工作目录就会保持干净。你确实有 12 个提交准备推送,但这并不影响 Git 对工作目录状态的分析。

Visual Studio

另一个很棒的视图展示相同信息的是在 Visual Studio 中。点击Git

图 9.6:点击菜单项 Git

然后点击查看分支历史

图 9.7:分支历史

这显示了 12 个准备推送的提交,以及一个已经在远程仓库的提交(e040fb00)。

GitHub 桌面客户端

GitHub 桌面客户端还有另一种展示相同数据的方式。这一单独页面能让你在一个视图中获取大量信息:

图 9.8:GitHub 桌面客户端

在页面顶部,我们看到仓库、分支和状态。左侧列展示了每个提交及其信息。(点击向上箭头将推送该提交。)中间列显示该提交涉及的文件,右侧则显示选中文件的代码。log命令可以完成所有这些操作,但不能同时进行。

命令行中的 log

你可以向 log 添加大量标志以控制其输出。在创建 lg 别名时,我们已经看到如何使用 log --oneline

图 9.9:在命令行中使用 log

仔细观察,我们看到左列显示短 ID,右列列出了与每个提交相关的消息,在第一个和最后一个提交中,我们还看到了 head 指针的位置;本地和远程都是如此。

哪些文件发生了变化?

如果你只想知道每次提交中哪些文件被更改,但不想看到这些更改的具体内容,你可以使用:

git log ––name-only 

以下是一个摘录:

图 9.10:使用 log 查看文件更改

我们看到两个提交。第一个,在 Program.cs 中,消息为 调用 add 函数,你还可以看到完整的 ID、作者和提交的时间。

当然,你也可以使用我们的 lg 别名来简化输出:

图 9.11:使用快捷键 lg

这里的问题是,垂直间距可能会让人感到困惑。这个例子展示了三个提交。第一个提交的消息是 实例化计算器,在这个提交中,Program.cs 被修改。弄清楚哪个文件属于哪个提交的最好方法是从 ID 开始。

在 Visual Studio 中没有简单的办法做到这一点,正如我们所看到的,GitHub Desktop 会将更改过的文件列表显示为提交历史的一部分。

每个文件发生了什么变化?

我们可以进一步询问 log 哪些文件发生了变化,以及这些变化中涉及到的文件。对应的命令是 git log -p

这将打印出每个提交中每个文件的更改。以下是一个文件的更改:

图 9.12:使用 log 查看每个文件的变化

在我的电脑上,新增的行显示为绿色,并且注意到左边的 + 符号,表示这行是新增的。接下来,我们进入 Program.cs,做更多修改——移除平方根函数并添加对除法函数的调用:

图 9.13:使用 diff 指示器查看变化

在这里,log 显示了平方根方法被移除(我的屏幕上是红色,并且最左边有一个减号),而除法方法被添加(再次注意最左边的加号)。

在该提交的消息下方,我们看到了一行有趣的内容:

diff --git a/LogDemo/LogDemo/Program.cs b/LogDemo/LogDemo/Program.cs 

Git 使用了 diff 命令,将原始版本(a/LogDemo/LogDemo/Program.cs)与新版本(b/LogDemo/LogDemo/Program.cs)进行了区分。正是 diff 的这种使用,使得 log 能够显示变化。

diff

没有什么能阻止你自己使用 diff。最强大的用途是,它可以在你提交更改之前,显示你自上次提交以来所做工作的变化。

假设你正在工作,然后被叫走了。如果你像我一样,可能已经完全忘记了自己做到了哪里以及到底要做什么。我们添加平方根方法并删除除法方法来测试程序,但在提交之前,我们先查看一下更改:

图 9.14:使用 diff

这与之前的示例非常相似,唯一的不同是,这里展示的是我的工作目录与上一个提交之间的差异。我可以通过提示符中的~1知道这是工作目录中的更改——表示有一个文件被修改但尚未提交。

Visual Studio

假设我在我的程序中工作,并且为计算器添加了一个Absolute方法:

public double Absolute (double x)
 {
     return Math.Abs(x);
 } 

我保存了它,然后去做程序的其他部分。当我回到计算器时,我知道我做了更改,但不记得是什么。在 Visual Studio 中,右键单击Calculator.cs并选择:

git compare with unmodified 

Visual Studio 打开并显示并排窗口,显示自上次提交以来你在此文件中做的更改:

图 9.15:并排比较

这样你的更改立即变得显而易见。

这个文件随时间发生了什么变化?

如果你想查看给定文件的更改历史,可以输入:

git log <filename> 

图 9.16:检查一个文件随时间变化的更改

在这里,我请求查看Calculator.cs的日志(提供完整路径),并获取该文件随时间变化的每个更改。正如你预期的那样,我可以使用lg让它更易读:

图 9.17:使用 lg 查看随时间变化的内容

现在我们可以看到实际发生了什么。日志提供了它通常所提供的所有信息,但仅限于所选文件。注意,提示符仍然显示我们有 14 个文件待上传,这表明这个列表并不是等待推送的所有提交。

搜索

假设我们想找到所有提交中包含单词"calculator"的文件。为此,我们使用-S搜索标志,紧接着输入我们要搜索的词:

git log -Scalculator 

这将返回所有包含单词"calculator"的提交,无论该词出现在其中一个或多个文件中:

图 9.18:在提交中搜索

注意:你还可以使用git log -Gcalculator,这将允许你使用正则表达式进行搜索。

再次,lg别名可以让它更易读:

图 9.19:使用 lg 进行搜索

搜索虽然不常见,但当你需要它时,Git 提供了一个非常强大的工具。

我的提交在哪里?

有时你只想查看某个特定人添加的提交列表。为此,你可以使用:

git lg ––committer="Jesse" 

当然,对于这个示例,这将是所有的提交:

图 9.20:使用 lg 查看随时间变化的内容

注意,搜索是区分大小写的,所以搜索"jesse liberty"不会返回任何记录,但"Jesse"会。

实际上,你可能会发现你更关心作者而非是谁做了提交,这样做的方式是一样的,只不过需要使用--author

然而,事实证明,我只想要“Jesse Liberty”在过去 80 分钟内的提交。为此,我们使用since标志(你可以输入任何合理的时间段,例如--since="one week"):

git lg --since="80 minutes" 

这样可以生成一个更加易于管理的列表:

图 9.21:按时间限制日志输出

你可以完全按作者进行相同的搜索,以防他们不相同:

图 9.22:基于作者和时间限制日志输出

限制日志显示特定时间段内的提交,可以大大帮助你集中注意力查看感兴趣的更改。

总结

在本节中,你已经看到log命令的强大功能。我们讨论了其中的一些标志:

log 标志 含义
--oneline 每个提交只显示一行
--name-only 只显示每个提交中更改的文件名称
-p 有什么更改?
git log <filename> 该文件中发生了什么变化?
-Sfoo 在每个提交中搜索foo
--committer="name" 按名称搜索所有提交
--author="name" 按名称搜索所有作者
--since="1 week" 与提交者或作者一起使用,在指定时间段内进行搜索

熟练掌握最常用的log命令可以让你更轻松地处理提交。当然,还有很多其他命令和标志,但你可以通过谷歌搜索找到你需要的命令。

挑战

在这个挑战中,你将使用log命令查看一组提交:

  1. 创建一个新的仓库

  2. 在该仓库中创建一个程序

  3. 添加至少 6 个提交

  4. 查找每个提交中更改的文件名称

  5. 查找某个文件在时间推移中的变化

  6. 查找过去一小时内(或任何合适的时间段)你提交的所有文件

请注意,你将使用log查看一个文件如何随时间变化,并查找每个提交中更改的文件名称。这展示了log命令的多功能性。

答案

对于这个挑战,没有一个正确答案,但与其他一些挑战不同,你在一定程度上受限于log命令的常见用法。

创建一个新的仓库

我将去 GitHub.com 创建LogChallenge仓库:

图 9.23:创建仓库

接下来,我需要将那个仓库克隆到我的本地机器:

git clone git@github.com:JesseLiberty/LogChallenge.git 

至少添加 6 个提交

首先,我们必须追踪新程序:

图 9.24:追踪程序

我们将向Program.cs文件添加一个更改:

namespace LogChallenge
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            Console.WriteLine("Log Challenge!");
        }
    }
} 

现在是时候提交这个更改了。让我们从查看状态(st)开始:

图 9.25:处理未追踪和修改的文件

这是一个棘手的图像。确保你理解为什么前三个是“待提交的”,而最后一个没有被暂存提交。(提示:前三个已经在暂存区中。)

为了简化操作,我们一次性将所有未暂存的文件都添加进来:

图 9.26:添加未暂存文件

现在确保你理解为什么有三个文件需要提交而不是四个(提示:查看 Program.cs)。

继续提交这些文件:

图 9.27:创建提交

再次提醒,我使用的是上一章中的别名。注意 cam 添加了文件,但实际上没有文件被添加。没关系。然后它进行了提交并等待消息。此时不要推送提交

让我们通过创建 Calculator 类并添加每个方法,在每次更改后提交,从而增加更多的提交记录,就像你之前看到的那样。

最后,让我们测试一下 Program.cs 中的 SquareRoot 函数:

namespace LogChallenge
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var calculator = new Calculator.Calculator();
            Console.WriteLine($"The square root of 2 is {calculator.SquareRoot(2)}");
        }
    }
}
namespace LogChallenge.Calculator
{
    public class Calculator
    {
        public int Add(int left, int right)
        {
            return left + right;
        }
        public int Subtract(int left, int right)
        {
            return left - right;
        }
        public int Multiply(int left, int right)
        {
            return left * right;
        }
        public int IntDivision(int left, int right)
        {
            return left / right;
        }
        public double Division(double left, double right)
        {
            return left / right;
        }
        public int Modulus(int left, int right)
        {
            return left % right;
        }
        public double SquareRoot(double x)
        {
            return Math.Sqrt(x);
        }
    }
} 

运行此命令可以得到正确的结果,且类型为 double:

The square root of 2 is 1.4142135623730951 

让我们查看所有提交的消息:

> git lg
43dd70e  | test the square root function [Jesse Liberty]  (5 minutes ago)  (HEAD -> main)
0fa51fa  | make Main public and remove writelines [Jesse Liberty]  (8 minutes ago)
51dd6e9  | Add square root function [Jesse Liberty]  (8 minutes ago)
711c4e8  | add modulus operator [Jesse Liberty]  (9 minutes ago)
06d7319  | add division function [Jesse Liberty]  (10 minutes ago)
31f5873  | Add intDivision function [Jesse Liberty]  (11 minutes ago)
9639a70  | add the multiply function [Jesse Liberty]  (12 minutes ago)
d1eaff5  | capitalize subtract function [Jesse Liberty]  (12 minutes ago)
d92657d  | Add subtract function [Jesse Liberty]  (13 minutes ago)
b5e945f  | Capitalize add function [Jesse Liberty]  (13 minutes ago)
f535d26  | Add the add function [Jesse Liberty]  (14 minutes ago)
a8cf101  | make calculator class public [Jesse Liberty]  (15 minutes ago)
acd2ce4  | Create calculator class [Jesse Liberty]  (15 minutes ago)
29cfe55  | First commit [Jesse Liberty]  (19 minutes ago)
d0518a1  | Initial commit [Jesse Liberty]  (41 minutes ago)  (origin/main, origin/HEAD) 

这里有一些额外的信息(如 ID、作者等),但我们确实看到了所有提交的消息:

图 9.28:仅使用名称标志的 lg

现在我们已经了解了如何处理已暂存和未暂存的文件,并通过 log 命令检查它们的内容,接下来我们来看一下如何找到在一次提交中实际更改的所有文件。

查找每次提交中更改的所有文件名

这有点棘手,因为存在空格问题。让我们先看第一个;首先,我们看到的是最新提交的 ID 和消息,然后在稍后的位置看到受影响的文件。类似地,在第二个框中,我们看到 添加平方根函数,但受影响的文件 Calculator.cs 离它的显示位置稍远。这在处理多个文件时会更加清晰。让我们试试这样做。

我们将向 Calculator 添加 Round 方法:

 public decimal Round(decimal x)
        {
           return  Math.Round(x);
        } 

我们将在程序中进行测试:

public static void Main(string[] args)
 {
     var calculator = new Calculator.Calculator();
     Console.WriteLine($"The square root of 93 is {calculator.SquareRoot(93)}");
     Console.WriteLine($"93.64 rounded is " + $"{calculator.Round((decimal)93.64)}");
 } 

我们现在将其提交,并且已经更改了两个文件:

图 9.29:看到有两个文件被修改

我们将其提交,现在已经有 15 次提交:

图 9.30:使用 lg 查看 15 次提交

查找随时间变化的指定文件的变更

要查看文件随时间变化的内容,我们使用简单的 log 命令,后面加上要查看的文件名。例如,我们可以查看 Program.cs 的变更(记得包括相对路径):

git log LogChallenge/LogChallenge/Program.cs 

结果显示了所有的提交记录,最新的位于顶部:

图 9.31:查看一个文件的所有提交记录

我们已经看到如何查找某个文件随时间变化的所有提交记录,但这可能会让人感到有些不知所措。我们可能只需要查看最近的提交记录。

查找过去一小时内你提交的所有文件(或者根据需要选择合适的时间段)

为此,我们只需要使用 since 标志:

图 9.32:使用 since 标志

我们通过以下步骤完成了挑战:

  • 创建一个新仓库

  • 在该仓库中创建一个程序

  • 添加提交

  • 查找每次提交中更改的所有文件名

  • 查看一个文件随时间变化的更改记录

  • 查找过去一小时内所有提交的文件

第十章:重要的 Git 命令和元数据

Git 命令和标志成百上千。在本章中,我们将尽力回顾一些我们到目前为止没有看过的最重要的命令。包括:

  • 创建 stash

  • 列出 stash 中的内容

  • 从 stash 中恢复

  • 清理命令,用于删除不需要的未跟踪文件

  • 如何查看元数据并选择你想查看的数据

让我们从挖掘 stash 开始。

你可以在 git-scm.com/docs 找到完整的列表。

Stash

当我们回顾 Git 的五个领域时,我们提到了一个叫做 stash 的领域,但并没有深入讲解 stash 是什么。简而言之,stash 是一个可以存放(stash)你修改过但尚未提交的文件的地方:

图 10.1:Git 的五个领域

Stash 非常重要。假设你正在开发一个功能,突然被要求处理一个非常重要的 bug。你还不准备提交当前的代码,但在工作区有未提交的文件时,你无法切换分支。

为了解决这个问题,你可以只需备份你的目录,然后删除那些未提交的文件,但这种方法非常慢且容易出错。相反,你应该把这些文件存放在某个地方,当你准备好时可以将它们取回,这正是 stash 的目的所在。

要看到这个工作效果,我们需要一个包含一些提交的仓库。让我们快速创建一个 RockyHorror2 仓库的镜像。为此,我们首先需要确保磁盘上有 RockyHorror2 仓库,如果没有,我们就从服务器拉取它。

让我们去 GitHub 创建一个名为 RockyHorrorStash 的仓库:

图 10.2:创建仓库

注意,我并没有麻烦去创建 readme、.gitignore 或许可证,因为当我执行镜像操作时,这些文件都会被覆盖。

点击 创建仓库,创建后,获取其地址。

我们现在准备将 RockyHorror2 镜像到 RockyHorrorStash。请确保切换到 RockyHorror2 目录并输入以下命令:

图 10.3:将镜像仓库推送到远程

我们现在在服务器上有了镜像仓库,但在本地没有,所以让我们克隆它:

图 10.4:将镜像仓库拉回到本地仓库

太好了,我们有一个可以操作的仓库。让我们通过快速调用日志查看其中的内容:

图 10.5:检查本地仓库的日志

假设我们正在这个项目中工作,并且修改了两个文件。首先,我们修改了 Showing 类来记录售出了多少盒爆米花:

图 10.6:添加 Showing 类

接下来,我们修改 Program.cs,让它显示 Brattle 剧院有 500 个座位。如果我们查看状态,就会看到工作区中有两个已修改的文件:

图 10.7:git status 显示两个已修改的文件

我们有两个未提交的修改文件。我们还有更多的工作要做,但老板打来电话,告诉我们另一个项目中有一个紧急 bug。这时,暂存就派上用场了。我们可以提交现在的工作,但我们还没有准备好,所以让我们将其暂存起来:

图 10.8:添加到暂存区

你输入git stash,它会把工作区和索引中的所有内容都放入暂存区,而不会提交。此时,工作区会重置到你开始修改文件之前的状态——也就是HEAD的先前位置。

你可以使用stash list命令查看暂存中的内容:

图 10.9:列出暂存中的内容

暂存中已经加入了WIP on main的标记。WIP 意味着“进行中的工作”。

目前,你在暂存区只有一项内容。然而,你可能会发现,在修复那个“非常重要的 bug”时,你的老板打来电话,告诉你停止这个工作,改去修复一个致命 bug。再次,你可能需要暂存你的工作。为了避免设置所有内容,我们只需在主程序中做一个小改动,然后将其暂存。我们来改一下剧院的名字:

图 10.10:暂存更多工作

嘿!增加 500 个座位的事怎么了?记得吗?我们从未提交过那个改动,我们将它暂存了,并且工作区已重置。让我们看一下状态:

图 10.11:检查状态

正如我们预期的那样,我们看到了我们所做的修改。让我们使用git stash将其暂存起来:

图 10.12:暂存修改

现在,我们在暂存区有两项内容。如果我们请求列出内容,应该会看到这两项:

图 10.13:列出暂存中的项目

果然,两个暂存的项目都在那儿。通常它们会有不同的 ID 和消息,但由于我们是从同一位置暂存的,最终出现了这种异常情况。你可以使用stash show查看暂存中的内容:

图 10.14:显示暂存内容

你可以从暂存中删除项目,也可以使用stash clear清空整个暂存。

清理

有时,你会发现状态中列出了未跟踪的文件。99%的情况下,这些是你创建的文件,你希望它们被跟踪,这可以通过将它们添加到索引中来实现(如前所示)。然而,有时你可能会发现一些你不需要的未跟踪文件:

图 10.15:未跟踪的文件

在这种情况下,我们有几个选择。我们可以将Untracked.cs添加到索引中,或者将其删除。为了实现这一点,我们可以使用git clean

图 10.16:使用 clean 删除未跟踪文件(失败)

由于 git clean 是少数几个真正具有破坏性的命令之一——一旦执行,未追踪的文件将被永久删除——Git 会以讽刺的回应“拒绝清理”。为了真正清理,Git 要求你通过使用 -f(强制)标志来告诉它你是认真的:

图 10.17:使用如上所示的 clean 命令,但加上了 force 标志(成功执行)

-f 标志基本上意味着“我知道自己在做什么”——所以确保你确实知道。

元数据

每次提交、合并等操作都会伴随元数据。你可以通过使用日志获取大量元数据,但有时你只是想提取一些重要的元数据。为此,你可以使用 show 命令:

图 10.18:使用 show 查看元数据

在这个示例中,我们使用 show 查找作者的姓名和邮箱,以及 ID 和元数据,告诉我们 main 的尖端位置。让我们逐步解析:

  • git showshow 命令。

  • -s—静默模式(或安静模式),它会抑制差异输出(可以尝试去掉它看看效果)。

  • HEAD 告诉 show 你感兴趣的提交。

  • %an 是作者的名字。

  • %ae 是作者的电子邮件地址。

我们将这段代码放入字符串中,并将其分配给 format 标志。

让我们看看日志,看看我们还可以使用 show 查看哪些元数据:

图 10.19:查看日志

让我们集中查看与这些条目之一相关的元数据。为此,我们使用 ID:

图 10.20:使用 show -s 查看元数据

你也可以指定一个条目的范围:

图 10.21:指定条目的范围

我们能够聚焦于 stash 中需要的内容。要恢复暂存的文件,请使用 git stash apply。这会将暂存中的所有内容应用到当前工作目录中。一旦确定你得到了需要的内容,就可以调用 git stash pop,这将再次应用更改并将其从暂存中移除。

总结

在本章中,我们回顾了我们之前没有查看的一些最重要的命令。这些命令包括:

  • 创建 stash

  • 列出 stash 中的内容

  • 从 stash 中恢复

  • clean 命令用于删除不需要的未追踪文件

  • 如何查看元数据并选择你想查看的数据

挑战

镜像一个仓库,或者如果你不介意修改现有仓库,可以使用一个已经存在的仓库。检查提交列表。开始进行一些更改,但不提交这些更改。切换到另一个仓库继续工作。在第二个仓库中创建或修改一些文件,但不要提交它们。开始第三个仓库的工作。放弃这项工作并返回到第一个仓库。检查 stash 并恢复你需要的 stash 以继续工作。

答案

再次提醒,有许多方法可以回答这个问题。我将通过将 RockyHorrorStash 仓库镜像到 RockyHorrorStash2,然后立即将其克隆到我的本地仓库来开始。

我将对 Panofy(创建 PanofyStash)和 musicHandler2(创建 musicHandler2Stash)做同样的操作。现在我们有三个仓库可以操作:

  • musicHandler2Stash

  • PanofyStash

  • RockyHorrorStash

让我们从 musicHandler2Stash 开始,先切换目录并获取已有日志。然后,我们将在 Visual Studio 中打开它并进行工作:

图 10.22:MusicHandler2Stash 的日志

让我们在 Visual Studio 中打开该目录并做出两处修改。我们需要修改几个文件,所以我们仅添加一些注释。当我们执行 status 命令时,我们看到有两个已修改的文件:

图 10.23:MusicHandler 文件在修改后和提交前的状态

在我们工作进行到一半时,突然被要求处理一个 bug。我们还没准备好提交这些文件,所以我们将它们加入到暂存区:

图 10.24:调用暂存命令,将修改的文件放入暂存区,然后调用 status 查看工作目录为空

现在我们可以自由切换到 RockyHorrorStash 目录,在这里我们将开始修复 bug(我们会通过添加注释来表示实际工作)。哦哦,我们需要处理一个更大的 bug。让我们将 RockyHorrorStash 中的工作暂存起来。

我们将切换到 PanofyStash,做一些修改并提交。现在我们准备好返回到我们在 musicHandler2stash 中正在处理的 bug,因为这已经成为优先处理的任务。首先要做的是列出这个仓库中暂存区的内容(记住,暂存区是按仓库分开的):

图 10.25:musicHandler2stash 中的暂存列表

让我们恢复这些。我们可以通过两种方式来实现:

  • apply 会应用暂存的文件,但会将它们保留在暂存区。

  • pop 会应用暂存的文件,但会从暂存区中移除它们。

我建议使用 apply,因为它会保留暂存项的副本在暂存区:

图 10.26:暂存应用

图 10.26中,我们首先执行 git stash apply。这一步需要非常小心——有一个不同的命令 git apply,这并不是我们在这里需要的。

一旦应用,你可以看到两个修改过的文件已经回来了。我们执行了 status 命令,确保一切如我们所预期。现在我们知道没有问题,我们可以清理暂存区:

图 10.27:删除缓存

图 10.27中,我们列出了暂存区中的内容,然后我们删除了暂存区的文件,接着再次列出,确保它们已经消失。

在这个挑战中,我们镜像了三个仓库,这样我们就可以在不改变它们原始状态的情况下进行操作。然后,我们查看了有哪些提交,并在不提交的情况下添加了新文件。为了能够切换仓库,我们将未提交的文件暂存了起来。当我们回到原始项目时,我们能够恢复暂存的文件。

第十一章:查找坏的提交:Bisect 和 Blame

迟早你会发现你的程序中有一个错误,它是在过去某个时间点引入的。你可以通过所有以前的提交来查找,但那样既费时又低效。Git 提供了一个命令,bisect,可以为你完成所有繁琐的工作。

它是如何工作的:bisect 会要求你提供一个已知的“坏的”提交。通常这是最后一个提交。然后它会要求提供一个已知的“好的”提交——也就是说,一个已知可以正常工作的提交。你不需要尝试不同的提交来找到这个提交;只需要回溯到足够远的提交,确保它当时是正常的。

如果你小心的话,可能希望检出一个好的提交并运行它来确保没有问题。

Bisect 会进行一系列的二分查找,试图找到第一个坏的提交。如果你有好的单元测试,bisect 可以自己完成这个过程;否则你必须测试每个它找到的提交并报告它是好的还是坏的。

你开始时输入git bisect start。这会进入 bisect 状态,类似于我们进入 rebase 状态的方式。

你的第二个命令是告诉 bisect 当前的提交是坏的(不工作,或者用 bisect 的术语来说是坏的)。你可以通过输入git bisect bad来表示这一点。

你现在需要告诉 bisect 一个好的检出。你可以通过两种方式做到这一点:要么提供一个好的提交的 ID,要么告诉它从当前提交回溯多少次,例如git checkout HEAD~12,表示我们知道十二个提交前一切正常。

Git 会将剩余的提交大致分为两半,并检出一个提交。假设这是当前提交之前六个提交的那个。你接着测试该提交并告诉 bisect 它是好是坏。

如果你说它是好的,那么意味着它之前的所有提交都是好的。如果你说它是坏的,那么意味着之后的所有提交都是坏的。假设提交 6 是好的。Bisect 现在会将其范围视为 6 到 12,并可能会检出提交 9。

你测试了提交 9,发现它是坏的。这意味着 9 之后的每个提交都是坏的。Bisect 现在的范围是从 6 到 9,并检出 7。如果 7 是好的,那么坏的提交就是 8 或 9。我们测试 8 得到答案:如果它是好的,那么 9 是第一个坏的提交,否则就是 8。

所有这一切通过一个示例更容易理解。让我们创建一个名为BisectTest的新仓库:

图 11.1:演示 bisect 的仓库

现在,像之前一样将该仓库克隆到本地磁盘。为了演示这个过程,我们将创建 12 次提交,其中有一个中间的提交存在错误,直到有人注意到程序在检查提交 12 后坏掉了。“哦不,”我们能听到那个程序员喊道,“这个问题可能已经存在很久了,但没人发现。我需要用 bisect 来找出哪个提交是坏的,并修复它。”

让我们用我们久经考验的 Calculator 类创建 12 个提交。第一个只会创建项目:

图 11.2: 我们程序的开始

我们将保存并提交作为第一个提交:

图 11.3: 保存并提交

接下来,我们将创建 Calculator 类并提交:

namespace BisectTest
{
    public class Calculator
    {
    }
} 

这样就有了三个提交:克隆时创建的初始提交,创建程序后的提交,以及创建 Calculator 类后的提交:

图 11.4: 三个提交

现在我们将添加四个函数(加法、减法、乘法和整数除法),每完成一个就提交。完成后,我们有了七个提交。

让我们添加取模运算符、实数除法和平方根,每完成一个就提交一次。

这给我们提供了十个提交。接下来,我们回到程序中,使用计算器打印出 23/4 的整数除法结果,得到值 5。让我们检查一下。

接下来,我们将在相同的除法运算中使用取模运算符:

namespace BisectTest
{
    public static class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator();
            Console.WriteLine($"Integer division of 23/4 is {calculator.Divide(23, 4)}");
            Console.WriteLine($"Modulus 23%4 is {calculator.Modulus(23, 4)}");
        }
    }
} 

最后,我们将使用双精度除法:

using System;
namespace BisectTest
{
    public static class Program
    {
        static void Main(string[] args)
        {
            var calculator = new Calculator();
            Console.WriteLine($"Integer division of 23/4 is {calculator.Divide(23, 4)}");
            Console.WriteLine($"Modulus 23%4 is {calculator.Modulus(23, 4)}");
            Console.WriteLine($"Real division of 23/4 is {calculator.Divide(23.0, 4.0)}");
        }
    }
} 

好的,我们准备在下次代码审查时展示我们的结果。我们运行程序,得到了:

Integer division of 23/4 is 5
Modulus 23%4 is 5
Real division of 23/4 is 5.75 

这个结果不可能是对的。在这种情况下,问题很明显;我们的取模运算符出了问题。但是在实际情况下,问题的答案要模糊得多,更别提它是在哪里引入的。

让我们使用 bisect 来找出出现问题的提交。我们启动 bisect,然后告诉它当前的提交是坏的:

图 11.5: 开始 bisect

现在我们需要告诉它一个好的提交。让我们查看日志:

图 11.6: 寻找好的提交

我们知道第二个提交是好的,因为我们所做的就是创建项目。让我们告诉 bisect 这一点:

图 11.7: 一个已知的好提交

你可能会收到很多关于分离头指针的警告(真疼)。你可以安全地忽略这些警告。为了形式上的完整,我们将测试当前检出的提交,当然,它是没问题的。所以我们告诉 bisect 当前的检出是好的:

图 11.8: 告诉 Bisect 这个提交是好的

它返回了一些有趣的信息。它告诉你,如果原始提交是坏的,而这个提交是好的,那么剩下的五个修订需要测试,大约需要三步。它还告诉你,它已经检出了信息为“添加除法函数”的提交。现在让我们检查一下(当前的)程序,看看它是否正确。(通常,在这里,你会运行程序看看是否得到预期的结果。更好的是,你可能会运行单元测试套件,看看是否通过。)

在 Visual Studio 中查看,我们看到工作目录看起来是这样的:

namespace BisectTest
{
    public class Calculator
    {
        public int Add(int x, int y)
        {
            return x + y;
        }
        public int Subtract(int x, int y)
        {
            return x + y;
        }
        public int Multiply(int x, int y)
        {
            return x * y;
        }
        public int Divide(int x, int y)
        {
            return x / y;
        }
    }
} 

看起来不错。注意,某些函数缺失,这是因为 bisect 检出了一个较早的提交。我们可以告诉 bisect 这个提交是好的:

图 11.9: 另一个好的提交

它返回并告诉我们,我们确实已经缩小了范围。现在只剩下两个修复项需要测试。看看原始日志:

图 11.10:演示二分查找的代码库

我们告诉它最新的提交有问题,第二个是好的。然后它为我们提供了“添加除法函数”这一提交信息。也就是说,二分查找大致将我们的提交列表分成了两半,并检查了一个提交让我们进行测试。我们告诉二分查找它让我们测试的提交是好的。所以,二分查找认为:“嗯,除法是好的,真正的除法有问题,那就再把它一分为二(给我们‘添加平方根’),看看是好是坏。无论如何,我们只剩下一个测试,得到最终答案。”

当我们尝试这段代码时,它不对,无法按预期工作。所以,我们告诉二分查找平方根是坏的。作为回应,它检查了“添加真实除法”提交。再次查看日志。这个提交有问题,还是下面那个有问题。我们已经告诉它“添加除法函数”是好的,并且已经告诉它平方根提交是坏的:

图 11.11:锁定问题所在

它已经检查了“添加除法函数”,我们来测试一下。

那个提交有问题,所以我们告诉二分查找:

图 11.12:我们找到了问题

它返回并告诉我们“添加模运算符”一定是罪魁祸首,而且没有更多的测试项了。我们找到了。我们来看一下——果然,模运算符使用了除法运算符:

public int Modulus(int x, int y)
 {
     return x / y;
 } 

尽管我们使用了一个简单的假例子,但你可以看到二分查找如何缩小提交范围,找到第一个出现问题的提交。现在我们知道哪里出错了,可以进行修复。

Blame

这个命令名字虽然不幸,但在逐行追踪谁更改了你的代码时非常有帮助。从这里,你可以与程序员讨论并了解他们的意图,或者提供修改的机会。

要在 Visual Studio 中打开 Blame,右键点击文件并选择Git,然后选择Blame。文件会打开,左侧会列出谁修改了该行代码。

挑战

创建一个有二十个提交的程序。在其中一个较早的提交中放置一个不会破坏正常程序的错误(以便它可以被隐藏)。使用二分查找找到错误。

第一步是创建一个包含 20 个提交的程序。我决定创建一个包含书籍信息的程序:

namespace BisectTest
{
    public class Book
    {
        public string Author { get; set; }
        public string BookName { get; set; }
        public double Price { get; set; }
        public double DiscountPrice { get; set; }
        internal double WholeSalePrice { get; set; }
        internal double DiscontinuedPrice { get; set; }
        public void ComputePrice()
        {
            Price = WholeSalePrice + (WholeSalePrice * .5);         
        }
        public void ComputeDiscountPrice()
        {
            DiscountPrice = Price * 2;
        }
        public void ComputeDiscontinued()
        {
            DiscontinuedPrice = DiscontinuedPrice * 0.8;
        }
    }
} 

我们还有一个与书籍类互动并显示结果的程序:

using System;
namespace BisectTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var book = new Book();
            book.Author = "Jesse Liberty";
            book.BookName = "Pro Git for Programmers";
            book.WholeSalePrice = 10.0;
            book.ComputePrice();
            Console.WriteLine($"{book.BookName} by {book.Author}" );
            Console.WriteLine($"{book.BookName}: {book.Price}");
            Console.WriteLine($"Discount price is {book.DiscountPrice}");
            Console.WriteLine($"Discontinued price is {book.DiscontinuedPrice}");
        }
    }
} 

让我们运行程序并检查输出:

Pro Git for Programmers by Jesse Liberty
Pro Git for Programmers: 15
Discount price is 0
Discontinued price is 0 

这显然不是我们想要的。折扣价和停产价都是 0,实际上它们应该是 15 的一小部分。哦!我们忘记调用计算价格的方法了。当我们这样做并显示所有价格时,得到的是:

Pro Git for Programmers by Jesse Liberty
Pro Git for Programmers: 15
Pro Git for Programmers discount price = 30
Discontinued price is 0 

更好,但还是不对。为什么Discontinued的结果是零?为了找出原因,我们将使用二分查找。首先我们输入:

git bisect start 

接下来,我们需要告诉 Bisect 当前(最新的)提交是坏的,方法是输入:

git bisect bad 

查看日志后,我看到第一个提交的 ID 是 7259bb3。所以我们将输入:

Git checkout 7259bb3
Git bisect good 

它为我检查一个版本进行测试,并告诉我在此之后还有 11 个版本需要测试(假设这是坏的),并且大约需要 4 个步骤。我们继续二分查找,直到找到第一个出错的代码实例,正如我们上面所看到的。

第十二章:修正错误

在 Git 中犯错最常见的反应是恐慌。如果你刚刚丢失了所有的工作怎么办?更糟的是,如果你搞砸了主分支呢?

本章将回顾一些常见的 Git 错误以及如何修复它们。当然,第一条规则是保持冷静,或者像道格拉斯·亚当斯所说的那样,不要恐慌

我们将回顾一些常见的 Git 错误以及如何修复它们的问题:

  • 您在提交中写了错误的消息。

  • 您忘记添加上次提交的更改文件。

  • 提交的顺序或它们的消息有问题。

  • 您需要撤消提交中所做的更改。

  • 您错误命名了分支。

  • 您提交到错误的分支。

  • 您在之前的提交中损坏了一个文件。

  • 您通过推送一个损坏的分支搞乱了远程。

要查看答案的运行情况,让我们将 Panofy 镜像到 ErrorsDemo。以下是我们将要执行的步骤:

  1. 在远程上,创建 ErrorsDemo 并获取其 URL。

  2. 转到您想要镜像的本地分支(在我们的例子中为 Panofy)。

  3. 使用ErrorsDemo的 URL,将其推送到服务器上的镜像命令。

  4. 克隆新分支(确保在您想要的目录中克隆它)。

  5. 切换到新的(克隆的)目录(ErrorsDemo)。

您可以在 第十章重要的 Git 命令和元数据 中看到这一点的具体步骤。

您在上次提交中写了错误的消息

让我们从日志开始,这样我们就可以看到更改:

图 12.1:初始状态的日志

这很容易;您只需输入:

git commit --amend 

您的编辑器将打开并允许您更改消息。要更改消息的措辞,只需将 "pick" 更改为 "reword"。

保存文件后,最后一个提交的消息将如 图 12.2 所示更改:

图 12.2:修订后的日志(在最后一次提交中更改了消息)

您忘记添加上次提交的更改文件

您通过与上次提交修复消息的方式解决此问题:使用 --amend

首先,将新的或更改的文件暂存。然后输入:

git --amend 

如果您不想在添加文件时编辑消息,请输入:

git --amend --no-edit 

提交的顺序或它们的消息有问题

如果问题不是在最后一次提交中(在这种情况下,您将使用 --amend),那么现在是时候使用交互式 rebase 了,如 第八章交互式 Rebase 所示。如果您尚未推送,交互式 rebase 将让您执行所有这些操作及更多操作。

需要撤消提交中所做的更改。

在这里您所需做的就是调用日志,获取您想要撤消的提交的 ObjectID,并调用:

git revert ObjectID 

让我们回到日志:

图 12.3:日志,起始点

现在让我们回滚添加 hello 消息的提交:

git revert c507abf 

因为我在分支中间撤销了一个更改,所以遇到合并冲突不足为奇:

图 12.4:合并冲突

为了解决这个问题,我将调用git mergetool,启动我在第四章合并、拉取请求与解决合并冲突中设置的工具。Kdiff3 足够智能,能够在没有我的帮助下解决所有冲突:

图 12.5:Kdiff3 为我解决了冲突

果然,当我们打开Program.cs时,Hello World 不见了:

图 12.6:Program.cs 在回退后的样子

你给分支命名错误

检出相关分支并输入:

git branch -m <currentName> <desiredName> 

这是我们得到的结果:

图 12.7:将分支 foo 重命名为 bar

图 12.7中,你创建了 foo 分支并将其检出。最后,你按上面所示重命名它,分支名称就被改变了。

你提交到了错误的分支

这种情况对我来说(一次又一次!)通常是我忘记创建新分支,所以在开发分支或主分支上进行更改。要解决这个问题,请输入:

git branch <new branch>
git reset HEAD~ --hard 

你正在创建一个新的分支,然后从 main(HEAD~)中移除该提交,但将文件保留在新分支中。

你在之前的提交中丢失了一个文件

你破坏了一个文件,但在之后的几个提交中才发现,真痛。使用git log查找问题提交之前的提交的 ObjectID。现在,我们只想获取那个提交中的文件。为此,我们输入:

git checkout ObjectID --<path to file> 

(文件的路径是相对于项目根目录的。)

现在你已经在暂存区有了较早的版本。你可以将其“取消暂存”并从工作区进行编辑。

使用 ObjectID 的替代方法是从HEAD向后计数,例如:

git checkout HEAD~4 --<path to file> 

这只是说“回退到 4 个提交之前并从那里获取文件。”这两种方法同样有效。

你通过推送一个损坏的分支弄坏了远程仓库

如果(以及当)你通过推送一个不完整且损坏的本地副本破坏了主分支,别难过,振作起来!这是可以修复的。

注意,这不应该是可能的。如果你正在使用 Azure DevOps(或类似工具),你的流水线不应该接受任何无法编译的合并(可以说,应该通过一组单元测试)。不过,我有点跑题了……

你需要的第一个命令是:

git reset --hard <remoteRepository> / <Yourbranch>@{1} 

这会将你本地的<Yourbranch>重置为<remoteRepo>的最后同步版本。因此,如果你的分支是 Feature1 并且它在 origin 上,你需要输入:

git reset --hard origin/Feature1@{1} 

现在你想将远程仓库恢复到你破坏它之前的状态:

git push -f <remoteRepository><Yourbranch> 

测验

本章的挑战是一个测验。答案在测验的最后。

  1. 如果你在上次提交中漏掉了一个更改的文件,该怎么办?

  2. 如果你提交到了错误的分支,该怎么办?

  3. 如果你在之前的提交中损坏了一个文件,该怎么办?

  4. 如果你需要撤销某次提交中的更改,该怎么办?

  5. 如果你通过推送一个损坏的分支破坏了主分支,该怎么办?

答案

如果你在上次提交中漏掉了一个更改的文件,该怎么办?

你可以使用与修改上次提交消息相同的命令,使用--amend,但你需要表明你不想编辑消息(确保你的文件已暂存):

git --amend --no-edit 

如果你提交到了错误的分支,该怎么办?

检出或创建你想要提交的分支,然后使用 reset 命令将变更从远程分支中移除,但保留文件在暂存区,以便提交到新的分支:

git branch <new branch>
git reset HEAD~ --hard 

如果你在之前的提交中破坏了某个文件,该怎么办?

首先,使用 git log 查找损坏之前的提交,获取该提交的 ObjectID。接下来,从正常的提交中获取问题文件(仅获取该文件):

git checkout ObjectID --<path to file> 

请记住:文件的路径是相对于项目根目录的。

你现在已经在暂存区得到了该文件的健康版本。如果该文件需要编辑,你可以将其取消暂存,但更常见的情况是,你可以直接使用这个较旧的版本。在这种情况下,你可以直接提交。

如果你需要撤销某个提交中的变更,该怎么办?

在这种情况下,打开日志,获取你想撤销的提交的 ObjectID。现在,你可以在该 ObjectID 上调用 revert 命令:

git revert ObjectID 

如果你通过推送一个损坏的分支把 Master 分支弄坏了,该怎么办?

如果你的 DevOps 系统允许你将一个损坏的分支推送到 Master,立即修复这个问题。事实上,如果可能的话,告诉你的团队在你修复之前不要提交到 Master。等他们不再对你大喊大叫后,按照以下步骤操作:

git reset --hard <remoteRepository> / <Yourbranch>@{1} 

这将把你本地的 <Yourbranch> 重置为 <remoteRepo> 的最后同步版本。因此,如果你的分支是 myFeature,并且它在 origin 上,你将写:

git reset --hard origin/myFeature@{1} 

现在,你需要将远程仓库恢复到破坏之前的状态:

git push -f <remoteRepository><Yourbranch> 

现在,Master 分支应该已经修复。

做得好!将这一章留着,以备未来你需要时用到。

第十三章:下一步

在这本精彩的书中,你已经了解了:

  • 在命令行、Visual Studio 和 GitHub Desktop 中安装 Git

  • 如何在 GitHub 上创建远程仓库

  • 如何通过命令行、Visual Studio 或 GitHub Desktop 克隆一个仓库到本地仓库

  • 如何将更改拉取到本地库

  • 如何将更改推送到远程仓库(origin)

  • 提交频率的最佳实践

  • 如何在提交时编写有效的消息

  • 什么是工作区

  • 什么是暂存区

  • 什么是索引/暂存区

  • 什么是本地仓库

  • 什么是远程仓库

  • 如何暂存并提交

  • 如何在不暂存的情况下提交

  • 什么是分支以及如何创建它们

  • 如何推送新分支

  • 什么是 HEAD 指针

  • 如何通过 Log 查看你的提交

  • 如何将提交推送到服务器

  • 如何通过命令行、Visual Studio 和 GitHub Desktop 管理你的提交

  • 如何合并到主分支

  • 什么是拉取请求

  • 什么是合并冲突以及如何解决

  • 什么是快速合并

  • 什么是真正的合并

  • 什么是变基以及如何使用它

  • 如何使用 amend 修改上一个提交

  • 如何从一个分支挑选提交到另一个分支

  • 如何使用交互式变基来更改历史

  • 使用 Git 的标准工作流程

  • 什么是备注以及如何使用它们

  • 什么是标签以及如何使用它们

  • 使用 log 查看你的提交记录

  • 使用 log 标志和命令聚焦在你想看到的信息上

  • 如何在 Visual Studio 和 GitHub Desktop 中查看 log 提供的信息

  • 使用 Diff 查看更改了什么

  • 使用别名节省时间并简化 Git 的使用

  • 在一组提交中搜索单词或短语

  • 创建暂存区

  • 列出暂存区中的内容

  • 从暂存区恢复

  • 使用 clean 命令删除不需要的未跟踪文件

  • 如何查看元数据并选择你想看到的数据

  • 使用 bisect 查找损坏的提交

  • 使用 blame 查看每个更改文件的程序员

  • 修复各种错误

尽管这个看似全面的列表涵盖了很多内容,但仍有一些高级或特殊情况没有涉及。此外,几乎所有我们讨论过的命令都还有额外的标志。你可以在多个地方学习它们。学习 Git 的关键位置包括:

如果这些资源没有回答你的问题,你可以通过 jesseliberty@gmail.com 联系我,尽管我有时回复较慢。

Git 命令应该不会发生太大变化,几乎可以肯定,Visual Studio 和 GitHub Desktop 会及时跟进任何新变化。

一定要查看一些其他优秀的 Git 图形界面,例如:

  • Fork

  • SourceTree

  • Tortoise Git

有太多内容无法一一列出,但请查看jliberty.me/GitGUI,那里有一个几乎完整的 GUI 客户端列表。

祝你好运,愿你在生活中获得你想要的一切。

Jesse Liberty

http://jesseliberty.com

@jesseliberty

![Logo 说明自动生成](https://github.com/OpenDocCN/freelearn-devops-pt7-zh/raw/master/docs/git-prog/img/Image17554.png)

packt.com

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,并获得行业领先的工具,帮助你规划个人发展并推进职业生涯。欲了解更多信息,请访问我们的网站。

为什么要订阅?

  • 用来自 4,000 多位行业专家的实用电子书和视频,减少学习时间,增加编码时间

  • 更好地学习,享受专为你设计的技能计划

  • 每月获取免费的电子书或视频

  • 完全可搜索,轻松访问重要信息

  • 复制粘贴、打印和收藏内容

你知道吗,Packt 为每本出版的书提供电子书版本,PDF 和 ePub 文件可用?你可以在www.Packt.com升级到电子书版本,作为纸质书的购买者,你还可以享受电子书的折扣。欲了解更多详情,请通过 customercare@packtpub.com 联系我们。

www.Packt.com上,你还可以阅读一系列免费的技术文章,订阅各种免费的新闻通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。

第十四章:其他您可能喜欢的书籍

如果您喜欢这本书,您可能对 Packt 的以下其他书籍感兴趣:

专家 Python 编程 – 第四版 Michał Jaworski Tarek Ziadé

ISBN:978-1-80107-110-9

  • 探索设置可重复且一致的 Python 开发环境的现代方法

  • 高效地将 Python 代码打包供社区和生产使用

  • 了解 Python 编程的现代语法元素,如 f-string、数据类、枚举和 lambda 函数

  • 用元类揭秘 Python 中的元编程

  • 在 Python 中编写并发代码

  • 监控并优化 Python 应用程序的性能

  • 使用不同语言编写的代码扩展和集成 Python

学习 Tableau 2020 - 第四版 Joshua N. Milligan

ISBN:978-1-80020-036-4

  • 开发引人注目的可视化,清晰地解释复杂数据

  • 探索令人兴奋的全新数据模型功能

  • 连接到各种数据源,将所有数据汇集在一起

  • 利用 Tableau Prep Builder 强大的数据清洗和结构化功能

  • 创建并使用计算来解决问题并丰富分析

  • 精通集合、LOD 计算等高级主题

  • 通过数据聚类、分布和预测实现智能决策

  • 分享您的数据故事,建立信任与行动的文化

Packt 正在寻找像您这样的作者

如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并立即申请。我们与成千上万的开发者和技术专业人士合作,帮助他们与全球技术社区分享他们的见解。您可以进行一般申请,申请特定的热门话题,或提交您的创意。

分享您的想法

现在您已完成Git for Programmers,我们非常希望听到您的想法!如果您是通过亚马逊购买的这本书,请点击这里直接进入亚马逊评论页面,分享您的反馈或在您购买的站点留下评论。

您的评论对我们和技术社区非常重要,将帮助我们确保提供高质量的内容。

posted @ 2025-07-02 17:47  绝不原创的飞龙  阅读(61)  评论(0)    收藏  举报