版本控制
版本控制就是控制版本,版本控制系统帮助控制(管理)某个事物(通常指源代码)的不同版本
现在流行的版本控制:
- git
- Subversion
-
Mercurial
版本控制系统分为两个类别:Centralized and Distributed (集中式模型和分布式模型)
版本控制系统的主要目的是帮助你保留项目的详细历史记录,并且能够在不同的版本上进行工作。保留详细的项目历史记录很重要,因为这样可以看出一段时间内项目的进度。如果需要,你还可以回到项目的某个阶段,并恢复数据或文件。
版本控制术语
- 提交(Commit)
Git 将数据看做微型文件系统的一组快照。每次 commit(在 Git 中保持项目状态),它都对文件当时的状况拍照,并存储对该快照的引用。你可以将其看做游戏中的保存点,它会保存项目的文件和关于文件的所有信息。 - 仓库(Repository / repo)
仓库是一个包含项目内容以及几个文件(在 Mac OS X 上默认地处于隐藏状态)的目录,用来与 Git 进行通信。仓库可以存储在本地,或作为远程副本存储在其他计算机上。仓库是由 commit 构成的。 - 工作目录 / 工作区(Working Directory)
工作目录是你在计算机的文件系统中看到的文件。当你在代码编辑器中打开项目文件时,你是在工作目录中处理文件 - 检出(Checkout)
检出是指将仓库中的内容复制到工作目录下。 - 暂存区 / 暂存索引 / 索引(Staging Area / Staging Index / Index)
Git 目录下的一个文件,存储的是即将进入下个 commit 内容的信息。可以将暂存区看做准备工作台,Git 将在此区域获取下个 commit。暂存索引中的文件是准备添加到仓库中的文件。 - SHA
SHA 是每个 commit 的 ID 编号。以下是 commit 的 SHA 示例:e2adf8ae3e2e4ed40add75cc44cf9d0a869afeb6
- 分支(Branch)
分支是从主开发流程中分支出来的新的开发流程。这种分支开发流程可以在不更改主流程的情况下继续延伸下去。
配置git
# 设置你的 Git 用户名
git config --global user.name "<Your-Full-Name>"
# 设置你的 Git 邮箱
git config --global user.email "<your-email-address>"
# 确保 Git 输出内容带有颜色标记
git config --global color.ui auto
# 对比显示原始状态
git config --global merge.conflictstyle diff3
git config --list
Git 与代码编辑器
最后一个配置步骤是让 Git 能与你的代码编辑器结合使用。以下是三个最热门的代码编辑器。如果你使用的是其他编辑器,则在 Google 中搜索“修改 Git 默认编辑器为 X 编辑器”(将 X 替换为你的代码编辑器的名称)。
Atom Editor 设置
git config --global core.editor "atom --wait"
Sublime Text 设置
git config --global core.editor "'C:/Program Files/Sublime Text 2/sublime_text.exe' -n -w"
VSCode 设置
git config --global core.editor "code --wait"
创建git仓库
在对 Git 仓库进行 commit 或执行任何其他操作之前,需要一个实际存在的仓库。要使用 Git 新建一个仓库,我们将使用 git init
命令。
init
子命令是"initialize"(初始化)的简称,这个命令很有用,因为它将进行所有仓库初始设置
-
ls
- 用来列出文件和目录 -
mkdir
- 用来新建目录 -
cd
- 用来更改目录 -
rm
z- 用来删除文件和目录
使用 cd 可以更改工作目录,使用 ls(单独使用)会列出工作目录下的文件。如果你忘记 shell 的当前工作目录,可以使用 pwd 命令(表示 print working directory 输出工作目录)输出该目录的名称。
git init 命令的作用
运行 git init 命令会初始化 Git 跟踪所有内容会用到的所有必要文件和目录。所有这些文件都存储在叫做 .git(注意开头有个 .,表示在 Mac/Linux 上,它将是一个隐藏目录)的目录下。这个 .git 目录是一个库!Git 会将所有 commit 记录在这里,并跟踪所有内容
克隆现有仓库
在 Git 上进行克隆的方法是调用我们将在终端上运行的命令 git clone
,然后传入要克隆的 Git 仓库的路径(通常是 URL)。
验证终端位置
提示:在克隆任何内容之前,确保命令行工具已定位于正确的目录下。克隆项目会新建一个目录,并将克隆的 Git 仓库放在其中。问题是无法创建嵌套的 Git 仓库。因此,确保终端的当前工作目录没有位于 Git 仓库中。如果当前工作目录没有在 shell 的提示符中显示,输入 pwd 输出工作目录。
判断仓库的状态
git-status
On branch master
– 这部分告诉我们 Git 位于 master 分支上。你已在术语表中获取了对分支的介绍,那么这是"master"分支(也就是默认分支)。我们将在第 5 节课深入了解分支。Your branch is up-to-date with 'origin/master'
. – 因为我们使用 git clone 从另一台计算机上复制了此仓库,因此这部分告诉我们项目是否与所复制的仓库保持同步状态。我们不会在其他计算机上处理该项目,因此这一行可以忽略。nothing to commit, working directory clean
– 表示没有任何待定的更改。
可以将这一输出结果看作“休息状态(resting state)”(这并不是官方解释,只是我喜欢这么理解!)。因为没有新的文件、没有对文件作出更改、暂存区没有任何需要 commit 的内容……没有更改或操作,因此我喜欢将其称为休息状态。
该命令将:
- 告诉我们已在工作目录中被创建但 Git 尚未开始跟踪的新文件
- Git 正在跟踪的已修改文件
查看仓库的历史记录
git log
显示有关现有提交信息
git show
显示有关给定提交的信息,需要向其提交ID,也就是SHA
- 要向下滚动,按下
- j 或 ↓ 一次向下移动一行
- d 按照一半的屏幕幅面移动
- f 按照整个屏幕幅面移动
- 要 向上滚动,按上
- k 或 ↑ 一次向上移动一行
- u 按照一半的屏幕幅面移动
- b 按照整个屏幕幅面移动
- 按下 q 可以退出日志(返回普通的命令提示符)
显示内容:
SHA - git log 将显示每个 commit 的完整 SHA。每个 SHA 都是唯一的,因此,我不需要查看整个 SHA。只需知道前 6-8 个字符即可。如果我们能只显示 SHA 的前 5 个左右的字符,是不是会节省一些空间?
作者 - git log 输出结果显示了每个 commit 的作者!其他仓库可能有多个人协作,因此情况可能不同,但是对于此仓库,只有一个人提交了所有的 commit,因此所有 commit 的作者是一样的。我们需要查看每个 commit 的作者吗?如果我们想隐藏这一信息呢?
日期 - 默认情况下,git log 将显示每个 commit 的日期。但是我们真的关心 commit 的日期吗?知道日期有时会很重要,但是每次都知道日期并不十分重要,在很多情况下都可以忽略。能否隐藏该信息,以便节省空间?
commit 消息 - 这是 commit 消息最重要的部分…我们通常都希望看到此信息,如何使输出结果更简短呢
git log
命令有一个选项,可以用来更改仓库信息的显示方式。该选项为 --oneline:
- git log --oneline
此命令:
- 每行显示一个 commit
- 显示 commit 的 SHA 的前 7 个字符
- 显示 commit 的消息
git log --stat
简介
此命令会:
- 显示被修改的文件
- 显示添加/删除的行数
- 显示一个摘要,其中包含修改/删除的总文件数和总行数
git log -p
git log
命令具有一个可用来显示对文件作出实际更改的选项。该选项是 --patch,可以简写为 -p:
- 添加的行所在的位置以及添加了多少行
- -15,83 表示旧版本(用 - 表示)从第 15 行开始,显示了 83 行
- +15,85 表示当前版本(用 + 表示)从第 15 行开始,现在变成了 85 行...这 85 行显示在下方
- 在 commit 中实际进行的更改
- 用红色标示并以减号 (-) 开头的行是位于文件-
- 原始版本中,但是被 commit 删除的行
用绿色标示并以加号 (+) 开头的行是 commit 新加的行
查看特定的commit
显示特定 commit 的详情,而不用关心仓库中的所有其他内容
实际上有两种实现方式!
- 向 git log 提供你要查看的 commit 的 SHA
- 使用新的 git show 命令
-
git log -p fdf5493
通过提供 SHA,git log -p 命令将从这条 commit 开始!无需滚动并逐条查阅!注意,它还会显示在所提供的 SHA 之前提交的所有 commit 信息。 -
git show
运行上述示例命令将仅显示最近的 commit。通常,将 SHA 作为最后一个参数提供给命令:git show fdf5493
git show 命令将仅显示一个 commit。因此,如果你看不到任何其他 commit,不要惊慌。它只显示一个 commit。git show 命令的输出和 git log -p 命令的完全一样。因此默认情况下,git show 会显示:
- commit
- 作者
- 日期
- commit 消息
- 补丁信息
但是,git show 可以与我们了解过的大部分其他选项一起使用:
- --stat - 显示更改了多少文件,以及添加/删除的行数
- -p 或 --patch - 显示默认补丁信息,但是如果使用了 --stat,将不显示补丁信息,因此传入 -p 以再次添加该信息
- -w - 忽略空格变化
向仓库中添加commit
要将所有文件提交到仓库中,首先需要将这些文件从工作目录移到暂存区。我们将使用 git add 命令将这三个文件移到暂存区。
使用
git add
将index.html
添加到暂存区:
git add index.html
输出结果中现在出现了全新的区域:"Changes to be committed"区域!这一新的"Changes to be committed"区域显示了位于暂存区的文件!目前只显示了 index.html 文件,因此暂存区只有这个文件。
git rm --cached
与 shell 的rm
命令不同。git rm --cached
不会破坏任何属于你的文件,它只是从暂存区删掉了文件。此外,帮助文本中出现了"unstage"(撤消暂存)字眼。将文件从工作目录移到暂存区叫做"staging"(暂存)。如果已移动文件,则叫做"staged"(已暂存)。从暂存区将文件移回工作目录将"unstage"(撤消暂存)。如果你阅读的文档中提示“stage the following files”,则表明你应该使用 git add 命令。
index.html 文件已暂存。我们再暂存另外两个文件。现在我们可以运行以下命令:
$ git add css/app.css js/app.js
句点 .
句点指代当前目录,可以用来表示所有文件和目录(包括所有嵌套文件和目录!)。
$ git add css/app.css js/app.js
# 等同于
$ git add .
git commit
第一段精确地告诉了我们需要执行的操作 - 我们需要为该 commit 提供一条消息。此外 ,任何以字符 # 开头的行将被忽略。在后面还提示:这将是初始 commit。最后,给出了将提交 commit 的文件列表。
因为这是存储库的第一个 commit,我们将使用 commit 消息 "Initial commit"。文本 "Initial commit" 并不特殊,只是第一个 commit 的常用消息。如果你想使用其他消息,完全可以!
关键在于使每个 commit 都有其侧重点。每个 commit 应该记录一项更改。这种说法可能比较主观(完全没问题),但是每个 commit 应该只对项目的一个方面做出更改。
git commit 小结
此命令:
- 将打开配置中指定的代码编辑器
- 请参阅第一节课中的 git 配置流程,了解如何配置编辑器)
在代码编辑器中: - 必须提供提交说明
- 以 # 开头的行是注释,将不会被记录
- 添加提交说明后保存文件
- 关闭编辑器以进行提交
然后使用 git log
检查你刚刚提交的 commit!
git diff
git diff
命令可以用来查看已被加入但是尚未提交的更改。
.gitignore
如果你想将某个文件保留在项目的目录结构中,但是确保它不会意外地提交到项目中,可以使用名称特殊的文件 .gitignore(注意文件名开头的点,很重要!)。将此文件添加到 new-git-project项目根目录。你只需列出希望 git ignore(忽略,不跟踪)的文件名,git 将忽略这些文件。
note: 可以使用touch .gitigore
创建该文件
通配符允许你使用特殊的字符来表示某些格式/字符。在 .gitignore 文件中,你可以使用
- 空白行作为空格
- # - 将行标记为注释
- 与 0 个或多个字符匹配
- ? - 与 1 个字符匹配
- [abc] - 与 a、b 或 c 匹配
- ** - 与嵌套目录匹配 - a/**/z 与以下项匹配
- a/z
- a/b/z
- a/b/c/z
标签、分支、合并
git tag
运行 git tag -a v1.0 将为最近的 commit 添加标签。但是如果你想向仓库中很久之前的 Commit 添加标签呢?
只需提供要添加标签的 commit 的 SHA 即可!
$ git tag -a v1.0 a87984
git branch
git branch 命令用来与 git 的分支进行交互:
- 列出仓库中的所有分支名称
- 创建新的分支
- 删除分支
要创建分支,只需使用 git branch 并提供要创建的分支对应的名称。因此,如果你想创建一个叫做"sidebar"的分支,只需运行以下命令:
$ git branch sidebar
git checkout
注意,在进行 commit 时,该 commit 将添加到当前分支上。虽然我们创建了新的 sidebar 分支,但是没有向其添加新的 commit,因为我们尚未切换到该分支。如果我们现在进行 commit 的话,该 commit 将添加到 master 分支,而不是 sidebar 分支。我们已经在演示中看到这一情况,要在分支之间进行切换,我们需要使用 git 的 checkout 命令。
$ git checkout sidebar
请务必了解该命令的工作方式。运行该命令将:
- 从工作目录中删除 git 跟踪的所有文件和目录
(git 跟踪的文件存储在仓库中,因此什么也不会丢失) - 转到仓库,并提取分支指向的 commit 所对应的所有文件和目
日志中的分支
提示符中的分支信息很有用,但是最清晰的查看方式是查看git log
的输出结果。就像我们需要使用 --decorate
选项来显示 git 标签一样,我们也需要该选项来显示分支。
$ git log --oneline --decorate
活跃分支
提示符将显示活跃分支。但这是我们对提示符进行的特殊自定义,如果你使用的是不同的计算机,判断活跃分支的最快速方式是查看 git branch
命令的输出结果。活跃分支名称旁边会显示一个星号
删除分支
分支用来进行开发或对项目进行修正,不会影响到项目(因为更改是在分支上进行的)。在分支上做出更改后,你可以将该分支组合到 master 分支上(这种“分支组合过程”叫做“合并”(merge),稍后将详细讲解)
合并了分支的更改后,你可能不再需要该分支了。如果你想删除分支,可以使用 -d 选项。下面的命令包含 -d 选项,告诉 git 删掉给出的分支(这里是"sidebar"分支)。
$ git branch -d sidebar
注意,无法删除当前所在的分支。因此要删除 sidebar 分支,你需要切换到 master 分支,或者创建并切换到新的分支。
删除内容让人比较紧张。但是不用担心。如果某个分支上有任何其他分支上都没有包含的 commit(也就是这个 commit 是要被删除的分支独有的),git 不会删除该分支。如果你创建了 sidebar 分支,向其添加了 commit,然后尝试使用 git branch -d sidebar 删除该分支,git 不会让你删除该分支,因为你无法删除当前所在的分支。如果你切换到 master 分支并尝试删除 sidebar 分支,git 也不会让你删除,因为 sidebar 分支上的新 commit 会丢失!要强制删除,你需要使用大写的 D 选项 - git branch -D sidebar。
同时查看所有分支
$ git log --oneline --decorate --graph --all
--graph 选项将条目和行添加到输出的最左侧。显示了实际的分支。--all 选项会显示仓库中的所有分支。
合并
当你在主题分支上做出更改后,如果觉得不想要该分支上的更改,则可以删掉该分支,或者你决定要保留更改,则可以将该分支上的更改与其他分支上的更改合并。
将分支组合到一起称为合并
注意 git 中的两种合并:普通合并和快进合并。
但是如果你在错误的分支上进行了合并,可以使用以下命令撤消合并:
git reset --hard HEAD^
快进合并
在我们的项目中,我们检出了 master 分支,我希望它拥有 footer 分支上的更改。用语言描述的话就是“我想要合并 footer 分支。”。注意表述“合并…”;在进行合并时,另一个分支上的更改将出现在当前检出的分支上。
我再强调下,当我们合并时,我们将其他分支合并到当前(检出的)分支上。我们不是将两个分支合并到一个新的分支上。也不是将当前分支合并到其他分支上。
因为 footer 直接在 master 前面,因此这种合并最简单。将 footer 合并到 master 中将导致快进合并(Fast-forward merge)。快进合并将使当前检出的分支向前移动,直到它指向与另一个分支(这里是 footer)指向的 commit 一样为止。
要合并 footer 分支,运行:
$ git merge footer
进行普通合并
现在我们将进行更常见的合并,其中两个分支完全不一样。你会惊讶地发现,实际合并 sidebar 这样的独特分支,操作是完全一样的!
要合并 sidebar 分支,确保你位于 master 分支上,并运行:
$ git merge sidebar
因为合并的是两个完全不一样的分支,因此将提交 commit。在进行 commit 时,需要提供 commit 消息。因为这是合并 commit,因此已经提供了默认消息。你也可以更改消息,但通常都会直接使用默认的合并 commit 消息。因此当你的代码编辑器打开并包含该消息时,直接关闭编辑器以确认使用该 commit 消息。
合并冲突
合并冲突指示符解释
编辑器具有以下合并冲突指示符:
- <<<<<<< HEAD 此行下方的所有内容(直到下个指示符)显示了当前分支上的行
- ||||||| merged common ancestors 此行下方的所有内容(直到下个指示符)显示了原始行的内容
- ======= 表示原始行内容的结束位置,之后的所有行(直到下个指示符)是被合并的当前分支上的行的内容
- >>>>>>> heading-update 是要被合并的分支(此例中是 heading-update 分支)上的行结束指示符
解决合并冲突
git 使用合并冲突指示符来告诉你两个不同分支上的哪些行导致了合并冲突,以及原始行是什么。要解决合并冲突,你需要:
- 选择保留哪些行
- 删掉所有带指示符的行
更改最后一个commit
借助 --amend 选项,你可以更改最近的 commit。
$ git commit --amend
还原commit
当你告诉 git 还原(revert) 具体的 commit 时,git 会执行和 commit 中的更改完全相反的更改。我们详细讲解下。假设 commit A 添加了一个字符,如果 git 还原 commit A,那么 git 将创建一个新的 commit,并删掉该字符。如果删掉了一个字符,那么还原该 commit 将把该内容添加回来!
现在我创建了一个包含一些更改的 commit,我可以使用 git revert 命令还原它
$ git revert <SHA-of-commit-to-revert>
因为最近的 commit 的 SHA 是 db7e87a,要还原该 commit: 我需要运行 git revert db7e87a
(随即弹出代码编辑器,以便编辑/确认提供的 commit 消息)
重置commit
初看,重置(reset) 似乎和 还原(revert) 相似,但它们实际上差别很大。还原会创建一个新的 commit,并还原或撤消之前的 commit。但是重置会清除 commit!
git reset 命令用来重置(清除)commit:
可以用来:
- 将 HEAD 和当前分支指针移到目标 commit
- 清除 commit
- 将 commit 的更改移到暂存区
- 取消暂存 commit 的更改