The Missing Semester of Your CS Education(第六课,git)笔记
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
为什么说版本控制系统非常有用?即使您只是一个人进行编程工作,它也可以帮您创建项目的快照,记录每个改动的目的、基于多分支并行开发等等。和别人协作开发时,它更是一个无价之宝,您可以看到别人对代码进行的修改,同时解决由于并行开发引起的冲突。
现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:
- 当前模块是谁编写的?
- 这个文件的这一行是什么时候被编辑的?是谁作出的修改?修改原因是什么呢?
- 最近的1000个版本中,何时/为什么导致了单元测试失败?
尽管版本控制系统有很多, 其事实上的标准则是 Git 。而这篇 XKCD 漫画 则反映出了人们对 Git 的评价:
因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。(难怪我一直学不会2333)很多时候您只能死记硬背一些命令行,然后像使用魔法一样使用它们,一旦出现问题,就只能像上面那幅漫画里说的那样去处理了。
尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。丑陋的接口只能靠死记硬背,而优雅的底层设计则非常容易被人理解。因此,我们将通过一种自底向上的方式向您介绍 Git。我们会从数据模型开始,最后再学习它的接口。一旦您搞懂了 Git 的数据模型,再学习其接口并理解这些接口是如何操作数据模型的就非常容易了。
Git 的数据模型
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在Git的术语里,文件被称作Blob对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个blob对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
历史记录建模:关联快照
版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过处于种种原因,Git 并没有采用这样的模型。
在 Git 中,历史记录是一个由快照组成的有向无环图。有向无环图,听上去似乎是什么高大上的数学名词。不过不要怕,您只需要知道这代表 Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。(可以把两个分支合并成一个快照)
在 Git 中,这些快照被称为“提交”(也就是commit)。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
o <-- o <-- o <-- o
^
\
--- o <-- o
上面是一个 ASCII 码构成的简图,其中的 o
表示一次提交(快照)。
箭头指向了当前提交的父辈(左边是父辈)(这是一种“在。。。之前”,而不是“在。。。之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样(最新的合并提交用粗体标记):
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
数据模型及其伪代码表示
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
// 文件就是一组数据
type blob = array<byte>
// 一个包含文件和目录的目录
type tree = map<string, tree | blob>
// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct { (这里除了string,储存的都是指针,即hash ID,用来索引commits和tree)
parent: array<commit> (这里用array的原因是可能有多个父辈)
author: string
message: string
snapshot: tree
}
这是一种简洁的历史模型。
对象和内存寻址
Git 中的对象可以是 blob、树或提交:
type object = blob | tree | commit
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
例如,上面例子中的树(可以通过 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
来进行可视化),看上去是这样的: (git cat-file
命令显示版本库对象的内容、类型及大小信息。-p, 根据对象的类型,以优雅的方式显式对象内容。)
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
树本身会包含一些指向其他内容的指针,例如 baz.txt
(blob) 和 foo
(树)。如果我们用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,即通过哈希值查看 baz.txt 的内容,会得到以下信息:
git is wonderful
引用
现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master
引用通常会指向主分支的最新一次提交。
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。
有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
仓库
最后,我们可以粗略地给出 Git 仓库的定义了:对象
和 引用
。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e
时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e
)
暂存区
Git 中还包括一个和数据模型完全不相关的概念,但它确是创建提交的接口的一部分。
就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但 Git 不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。
Git 处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括那些改动。
Git 的命令行接口
=========自己总结的东西==========
1. git checkout hashID 可以虚假地切换到一个commit,此时做出的修改需要调用git switch才能保存,如果想抛弃这些修改,可以直接checkout到其他地方
2. git checkout hashID之后,调用git log会隐藏more recent commits,调用git log --all --graph --decorate则会显示所有commits,包括隐藏的commits,并且伴有DAG
3. git checkout 如果在commit之前使用,会抛弃之前做出的changes
4. git diff [hashID] [files..] 这个命令会把当前版本和hashID版本进行对比,用GNU diff格式列出所有文件的变动部分,而[files..]会specify哪些文件需要被列出。 当[hashID]缺省时,会把当前版本(比如还没commit的部分)跟HEAD指向版本进行对比。
5. git diff [hashID1] [hashID2] [files..] 可以展示从hashID1 到 hashID2 发生了哪些修改
6. git checkout [file] 抛弃某个文件的改动,把它恢复到HEAD指向的版本
7. git branch -vv 可以展示一些额外的信息,比如一个branch的hash ID和相应的commit message
8. 使用git merge产生conflicts时,可以使用vimdiff或者git mergetool 来处理合并冲突 (TODO:以后可以看看如何使用git mergetool)
回答:Pro git中有关合并的部分:https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%88%86%E6%94%AF%E7%9A%84%E6%96%B0%E5%BB%BA%E4%B8%8E%E5%90%88%E5%B9%B6
9. 在修复完merge conflicts后,可以使用git add 把被修复的文件存储起来,随后使用git merge --continue来继续合并
===============================
HEAD指向你当前正在观看的snapshot,master指向目前的主开发branch
为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读 Pro Git 中文版或可以观看本讲座的视频来学习。
基础
git help <command>
: 获取 git 命令的帮助信息git init
: 创建一个新的 git 仓库,其数据会存放在一个名为.git
的目录下git status
: 显示当前的仓库状态git add <filename>
: 添加文件到暂存区git commit
: 创建一个新的提交git log
: 显示历史日志git log --all --graph --decorate
: 可视化历史记录(有向无环图)git diff <filename>
: 显示与暂存区文件的差异git diff <revision> <filename>
: 显示某个文件两个版本之间的差异git checkout <revision>
: 更新 HEAD 和目前的分支
分支和合并
git branch
: 显示分支git branch <name>
: 创建分支git checkout -b <name>
: 创建分支并切换到该分支- 相当于
git branch <name>; git checkout <name>
- 相当于
git merge <revision>
: 合并到当前分支git mergetool
: 使用工具来处理合并冲突git rebase
: 将一系列补丁变基(rebase)为新的基线
远端操作
git remote
: 列出远端git remote add <name> <url>
: 添加一个远端 比如 git remote add origin <github/gitlab/gitbucket/local folder>git push <remote> <local branch>:<remote branch>
: 将对象传送至远端并更新远端引用 默认情况下 git push origin master:mastergit branch --set-upstream-to=<remote>/<remote branch>
: 创建本地和远端分支的关联关系 设置当前分支和远端分支的关联关系,可以调用git branch -vv查看,设置完成后即可调用git push,而不需要在后面添加参数来完成向远端传输玩意儿git fetch
: 从远端获取对象/索引git pull
: 相当于git fetch; git merge
git clone
: 从远端下载仓库
撤销
git commit --amend
: 编辑提交的内容或信息git reset HEAD <file>
: 恢复暂存的文件 https://www.jianshu.com/p/c2ec5f06cf1a(--hard --soft --mixed三种参数的不同以及应用场景) reset --hard 不仅可以撤销提交,还可以用来把 HEAD 和 branch 移动到其他的任何地方。git checkout -- <file>
: 丢弃修改
Git 高级操作
git config
: Git 是一个 高度可定制的 工具 除了使用git config在命令行配置git之外,还可以使用vim ~/.gitconfig 在纯文本下配置gitgit clone --depth=1
: 浅克隆(shallow clone),不包括完整的版本历史信息git add -p
: 交互式暂存 适用于指向commit同一个文件下的部分改动时git rebase -i
: 交互式变基git blame
: 查看最后修改某行的人git stash
: 暂时移除工作目录下的修改内容git bisect
: 通过二分查找搜索历史记录.gitignore
: 指定 故意不追踪的文件
杂项
- 图形用户界面: Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
- Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh, bash)。Oh My Zsh这样的框架中一般以及集成了这一功能
- 编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 GIt 的常用插件 (TODO: 这个看起来很方便,以后可以看看)
- 工作流: 我们已经讲解了数据模型与一些基础命令,但还没讨论到进行大型项目时的一些惯例 ( 有很多 不同的 处理方法)
- GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作拉取请求(pull request)的方法来向其他项目贡献代码
- 其他 Git 提供商: GitHub 并不是唯一的。还有像 GitLab 和 BitBucket 这样的平台。
资源
- Pro Git ,强烈推荐!学习前五章的内容可以教会您流畅使用 Git 的绝大多数技巧,因为您已经理解了 Git 的数据模型。后面的章节提供了很多有趣的高级主题。(Pro Git 中文版);
- Oh Shit, Git!?! ,简短的介绍了如何从 Git 错误中恢复;
- Git for Computer Scientists ,简短的介绍了 Git 的数据模型,与本文相比包含较少量的伪代码以及大量的精美图片;
- Git from the Bottom Up详细的介绍了 Git 的实现细节,而不仅仅局限于数据模型。好奇的同学可以看看;
- How to explain git in simple words;
- Learn Git Branching 通过基于浏览器的游戏来学习 Git ;
git只提交部分修改的文件(提交指定文件)
在我们的项目中,经常会在本地编译一些代码,还未写完,测试那边来告诉你要改改某个文件的bug,非常着急,此时改完了,提交的时候,自己还在编译的代码并不想提交,此时,你可以利用git这些指令帮助你!
1/ git status -s 查看仓库状态
2/ git add src/components/文件名 添加需要提交的文件名(加路径--参考git status 打印出来的文件路径)
3/ git stash -u -k 忽略其他文件,把现修改的隐藏起来,这样提交的时候就不会提交未被add的文件
4/ git commit -m "哪里做了修改可写入..."
5/ git pull 拉取合并
6/ git push 推送到远程仓库
7/ git stash pop 恢复之前忽略的文件(非常重要的一步)
git还有很多非常方便的指令,笔者正在摸索中...
知识来源:https://www.cnblogs.com/Mojito2020/p/13679550.html
提交同一个文件里的部分改变
提交同一个文件里的部分改变(比如,改变了30行,只提交其中15行)
知识来源:https://stackoverflow.com/questions/1085162/commit-only-part-of-a-file-in-git
使用 git diff --cached 可以查看哪些改动被暂存了
使用 git blame <filename> 可以查看<filename>文件中所有行被修改的commit hash ID以及对应的作者
git show <commit hash ID>:展示某个commit的相关信息
git stash:把当下工作目录的修改内容暂时移除,恢复到上一个commit版本 git stash pop:把暂时移除的修改内容恢复过来
git bisect:可以通过二分查找搜索历史记录,配合一些unit test script,它可以自动化地搜索满足条件的历史记录(TODO:感觉这个很有用!能够极大提高开发效率)
=========================完成这堂课练习记录==============================
1. 目前做到git rebase这道题(here, 目前做到“相对引用”)
git rebase: 将某一分支的修改移动到另一个分支上 来源:https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA
git rebase和git merge效果一样,但是提交历史更为简洁
一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个其他人维护的项目贡献代码时。 在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master
上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。
git rebase --onto master server client
以上命令的意思是:“取出 client
分支,找出它从 server
分支分歧之后的补丁, 然后把这些补丁在 master
分支上重放一遍,让 client
看起来像直接基于 master
修改一样”。这理解起来有一点复杂,不过效果非常酷。
这段知识来源还提到了“变基在远程协作中的风险,以及一些高级解决方案”
来源:https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%8F%98%E5%9F%BA (关于rebase一些更有趣的例子)
git branch -f <branchname> <commit hash ID> 把brachname给移动到某个commit hashID上
git revert 用来在远程分支上撤销一个commit,实现方式是新增一个commit,但这个commit的内容是撤销之前的commit的修改
git cherry-pick C2 C4 可以只挑选一部分commit,将他们的改动应用当当前分支下
交互式rebase:带有--interactive参数,简写为-i 如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
git commit –amend 会用一个新的commit更新并替换最近的commit,这个新的commit会把任何修改内容和上一个commit的内容结合起来。如果当前没有提出任何修改,这个操作就只会把上次的commit重写一遍。
git rebase -i 甚至可以用于修改提交顺序,比如有一个C1, C2, C3的提交顺序,可以改成C1 C3 C2, git cherry-pick也可以
git tag v0 C1 给commit C1添加标签v0
git describe <ref> 输出结果是<tag>_<numCommits>_g<hash> tag
表示的是离 ref
最近的标签, numCommits
是表示这个 ref
与 tag
相差有多少个提交记录, hash
表示的是你所给定的 ref
所表示的提交记录哈希值的前几位。
git branch <branch name > <commit HASHID> 在某个commit上添加分支名<branchname>
git pull 默认是git fetch和git merge的结合体,而git pull --rebase则是git fetch和git rebase的结合体
git checkout -b feature C2 在C2上简历分支feature,并且切换到这个分支
git push origin feature 推送feature分支到remote
git checkout -b foo o/main 我们检出一个名叫 foo
的新分支,让其跟踪远程仓库中的 main
另一种设置远程追踪分支的方法就是使用:git branch -u
命令,执行:git branch -u o/main foo 这样 foo
就会跟踪 o/main
了。如果当前就在 foo 分支上, 还可以省略 foo:git branch -u o/main
git push origin master (这个Push与当前检出分支无关,都会把本地master推送到origin的master分支上) git push(会尝试推送当前检出分支,如果当前HEAD没有指向任何分支,则失败)
git push origin <source>:<destination> 把本地的source分支推送到origin的destination上
git fetch在没有参数的时候,会下载所有的提交记录到各个远程分支
Git 有两种关于 <source>
的用法是比较诡异的,即你可以在 git push 或 git fetch 时不指定任何 source
,方法就是仅保留冒号和 destination 部分,source 部分留空。
git push origin :side
git fetch origin :bugFix
我们分别来看一下这两条命令的作用……
如果 push 空
git push origin :foo 删除远程仓库的foo分支
如果 fetch 空
git fetch origin :bar 在本地创建一个bar分支
很神奇吧!但无论怎么说, 这就是 Git!
下命令在 Git 中是等效的:
git pull origin foo
相当于:
git fetch origin foo; git merge o/foo (无论当前检出分支是哪里)
还有...
git pull origin bar~1:bugFix
相当于:
git fetch origin bar~1:bugFix; git merge bugFix (无论当前检出分支是哪里)
看到了? git pull 实际上就是 fetch + merge 的缩写, git pull 唯一关注的是提交最终合并到哪里(也就是为 git fetch 所提供的 destination 参数)
把敏感数据从github中删掉 https://docs.github.com/cn/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository
使用git filter-branch
清除提交记录
git filter-branch --force --index-filter\
'git rm --cached --ignore-unmatch ./my_password' \
--prune-empty --tag-name-filter cat -- --all
文件my_password以及相应提交记录都会被删掉
配置全局忽略文件
您可以通过执行 git config --global core.excludesfile ~/.gitignore_global
在 ~/.gitignore_global
中创建全局忽略规则。配置您的全局 gitignore 文件来自动忽略系统或编辑器的临时文件,例如 .DS_Store
;
在github上发起pull request
克隆 本课程网站的仓库,找找有没有错别字或其他可以改进的地方,在 GitHub 上发起拉取请求(Pull Request); 首先 fork 本网站仓库,然后克隆 fork 后的仓库
git clone https://github.com/hanxiaomax/missing-semester.git
在本地进行修改后,提交到 fork 后的仓库,然后发起 PR
====================================实际使用中总结的经验==================================
1. 撤销远程分支中的commit https://melvingeorge.me/blog/undo-last-commit-in-remote-git-repo
2. 遇到git push时要求使用token的报错时:https://www.jianshu.com/p/6e86c80c457c (实际上只要在github里的personal access token产生一个新token(挺短的),然后用这个新token当作密码就行了)
3. 查看远程仓库地址命令
git remote -v
4.