我的git学习笔记

上手配置

git config

使用方法是: git config –global <配置名称> <配置的值>。配置之后会在系统的home目录创建一个.gitconfig文件来存储global的配置信息,也就是说你的所有项目默认使用这里的信息,

1
2
shiyanlou:~/ $ git config --global user.name "Zhang"
shiyanlou:~/ $ git config --global user.email "1234@qq.com"

那么linux会在home文件夹,Windows会在系统盘的用户文件夹比如Administer下找到.gitconfig文件,里面可能会有如下内容:

1
2
3
[user]
email = 1234@qq.com
name = Zhang

From http://learngitbranching.js.org/

commit

使用git commit -m进行提交。

Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!

Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。

Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。

分支

1
2
3
4
git branch xxx
git checkout xxx
<!-- more -->
git checkout -b xxx # 等于上面两条命令的结合

合并

衍合

第二种合并分支的方法是 git rebase。它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序施用每一个差异补丁文件。

Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。

在提交树上移动

使用cat .git/HEAD查看HEADH指向。如果 HEAD 指向的是一个引用,还可以用git symbolic-ref HEAD查看它的指向。

HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。

HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。

HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。可以使用git checkout <commit hash>来移动HEAD到某次特定提交上。

相对引用

通过指定提交记录哈希值的方式在 Git 中移动不太方便。哈希值在真实的 Git 世界中也会更长,基于 SHA-1。Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。
而且如果只是想移动到前一次提交的话还可以更简单。通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。使用相对引用的话,你就可以从一个易于记忆的地方(比如某个分支或 HEAD)开始计算。比如:

  1. 使用 ^ 向上移动 1 个提交记录
  2. 使用 ~num 向上移动多个提交记录,如 ~3

例如使用git checkout HEAD~4来移动到HEAD的第四个祖先。或者使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:git branch -f master HEAD~3。上面的命令会将 master 分支强制指向 HEAD 的第 3 级父提交。相对引用为我们提供了一种简洁的检出某次具体提交记录 C1 的方式, 而 -f 则容许我们将分支强制移动到那个位置。

撤销变更

在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪咱方式被撤销的)组成。我们这个应用主要关注的是后者。

主要有两种方法用来撤销变更——————是git reset,还有就是git revert

reset

git reset通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset向上移动分支,原来指向的提交记录就跟从来没有提交过一样。例如使用:

1
git reset HEAD~1

revert

虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!

为了撤销更改并分享给别人,我们需要使用 git revert。revert的做法实际上是进行一次新的与想要还原的提交相同的提交来覆盖上一次提交达到撤销的目的。例如使用:

1
git revert HEAD

整理提交记录

Cherry-pick

首先要介绍的第一个命令是 git cherry-pick, 命令形式为:

1
git cherry-pick <提交哈希或分支> [<多个提交哈希或分支>]

如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。这条命令的优点是特别简单。

交互式rebase

交互式 rebase 指的是使用带参数--interactiverebase命令, 简写为-i

如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。

在实际使用时,所谓的 UI 窗口一般会在文本编辑器(如 Vim)中打开一个文件。 当 rebase UI界面打开时, 你能做3件事:

  • 调整提交记录的顺序(通过鼠标拖放来完成)
  • 删除你不想要的提交(通过切换 pick 的状态来完成,关闭就意味着你不想要这个提交记录)
  • 合并提交。 它允许你把多个提交记录合并成一个。

本地栈式提交

来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug 而进入了bigFix分支,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。

这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!

最后就差把 bugFix 分支里的工作合并回 master 分支了。你可以选择通过 fast-forward 快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……

实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用上面的git rebase -i或者git cherry-pick来达到目的。

使用技巧

使用git rebase修改历史提交

接下来这种情况也是很常见的:你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。

此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。

我们可以通过下面的方法来克服困难:

先用git rebase -i将提交重新排序,然后把我们想要修改的提交记录挪到最前,然后用 commit --amend来进行一些小修改,接着再用 git rebase -i 来将他们调回原来的顺序
最后我们把 master 移到修改的最前端,就大功告成啦!

使用git cherry-pick修改历史提交

我们可以使用rebase -i对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用--amend修改它,然后把它们重新排成我们想要的顺序。

但这样做就唯一的问题就是要进行两次排序,而这有可能造成由rebase而导致的冲突。

记住cherry-pick可以将提交树上任何地方的提交记录取过来追加到HEAD上(只要不是HEAD上游的提交就没问题)。

所以可以使用git checkout先移动到想要修改的提交处进行git commit --amend,然后再利用git cherry-pick把需要的提交拿来重新提交一次。

Tags

分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。

有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?

Git 的 tag 可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。

更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。使用方法为:

1
git tag tag-name [commit-hash]

如果你不指定提交记录,Git 会默认使用 HEAD 所指向的位置。tag也可以作为定位的标识,比如使用git checkout <tag>

Git Describe

由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe。

Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。git describe 的语法是:

1
git describe [ref]

ref 可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。而当 ref 提交记录上有某个标签时,则只输出标签名称。

选择父提交记录

操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。

但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。

Git 默认选择合并提交的“第一个”父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。

而且!这两个操作符支持链式操作!例如:

1
git checkout HEAD~^2~2

远程仓库与团队合作

远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝。你可以通过因特网与这台计算机通信 —— 也就是增加或是获取提交记录

话虽如此, 远程仓库却有一系列强大的特性:

  • 首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。

  • 还有就是, 远程让代码社交化了! 既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)

现在用网站来对远程仓库进行可视化操作变得越发流行了(像 Github 或 Phabricator), 但远程仓库永远是这些工具的顶梁柱, 因此理解其概念非常的重要!

你可能注意到的第一个事就是在我们的本地仓库多了一个名为 xxx/master 的分支(命名规范是 /\,远程仓库默认名为origin), 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别。
远程分支有一个特别的属性,在你检出时自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作, (更新了远程分支之后)再用远程分享你的工作成果。

fetch

Git 远程仓库相当的操作实际可以归纳为两点:向远程仓库传输数据以及从远程仓库获取数据。既然我们能与远程仓库同步,你会看到当我们从远程仓库获取数据时, 远程分支也会更新以反映最新的远程仓库。git fetch 完成了仅有的但是很重要的两步:

  • 从远程仓库下载本地仓库中缺失的提交记录
  • 更新远程分支指针(如 o/master)
  • git fetch 实际上将本地仓库中的远程分支更新成了远程仓库相应分支最新的状态。如果远程仓库有多个分支,那么多个分支都会被更新。

远程分支反映了远程仓库在你最后一次与它通信时的状态,git fetch 就是你与远程仓库通信的方式了!git fetch 通常通过互联网(使用 http:// 或 git:// 协议) 与远程仓库通信。
git fetch 并不会改变你本地仓库的状态。它不会更新你的 master 分支,也不会修改你磁盘上的文件。理解这一点很重要,因为许多开发人员误以为执行了 git fetch 以后,他们本地仓库就与远程仓库同步了。它可能已经将进行这一操作所需的所有数据都下载了下来,但是并没有修改你本地的文件。

所以, 你可以将 git fetch 的理解为单纯的下载操作。

pull

当远程分支中有新的提交时,你可以像合并本地分支那样来合并远程分支。也就是说就是你可以执行以下命令:

  • git cherry-pick o/master
  • git rebase o/master
  • git merge o/master
  • 等等

实际上,由于先fetch更新再merge到本地分支这个流程很常用,因此 Git 提供了一个专门的命令来完成这两个操作。它就是我们要讲的 git pull。

push

git push 负责将你的变更上传到指定的远程仓库,并在远程仓库上合并你的新提交记录。一旦 git push 完成, 你的朋友们就可以从这个远程仓库下载你分享的成果了!

git push 不带任何参数时的行为与 Git 的一个名为 push.default 的配置有关。它的默认值取决于你正使用的 Git 的版本,在你的项目中进行推送之前,最好检查一下这个配置。

远程仓库接收了本地的新提交,远程仓库中的 master 分支也被更新到指向 C2 了,我们的远程分支 (origin/master) 也同样被更新了。所有的分支得以同步。

偏移的历史

困难来自于远程库提交历史的偏离。

假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。

这种情况下, git push 就不知道该如何操作了。如果你执行 git push,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,异或由于你的提交已经过时而直接忽略你的提交?

因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push 变更的。实际上它会强
制你先合并远程最新的代码,然后才能分享你的工作。

1
git pull --rebase

上述命令是fetch和rebase的合并。learngitbranch系列教程中介绍rebase和merge更有优劣,一个提交树干净清爽,另一个历史完整。不过《ProGit》中不推荐用rebase去衍合会被push到远程仓库的提交。

跟踪远程分支

Git 好像知道 master 与 o/master 是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 master 和本地的 master 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:

  • pull 操作时, 提交记录会被先下载到 o/master 上,之后再合并到本地的 master 分支。隐含的合并目标由这个关联确定的。

  • push 操作时, 我们把工作从 master 推到远程仓库中的 master 分支(同时会更新远程分支 o/master) 。这个推送的目的地也是由这种关联确定的!

直接了当地讲,master 和 o/master 的关联关系就是由分支的“remote tracking”属性决定的。master 被设定为跟踪 o/master —— 这意味着为 master 分支指定了推送的目的地以及拉取后合并的目标。有两种方法设置这个属性,第一种就是通过远程分支检出一个新的分支,执行:

1
git checkout -b foo o/master

这条命令可以创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 o/master。

另一种设置远程追踪分支的方法就是使用:git branch -u 命令,在建立foo分支后执行:

1
git branch -u o/master foo

这样 foo 就会跟踪 o/master 了。如果当前就在 foo 分支上, 还可以省略 foo:

1
git branch -u o/master

命令参数

push
1
git push <remote> <place>

这个命令的作用是:切到本地仓库中的<place>分支,获取所有的提交,再到远程仓库<remote>中找到<place>分支,将远程仓库中没有的提交记录都添加上去,搞定之后告诉我。

我们通过“place”参数来告诉 Git 提交记录来自于 master, 要推送到远程仓库中的 master。它实际就是要同步的两个仓库的位置。要同时为源和目的地指定 的话,只需要用冒号 : 将二者连起来就可以了。如果你要推送到的目的分支不存在,Git 会在远程仓库中根据你提供的名称帮你创建这个分支!

1
git push <remote> <source>:<destination>

例如:

1
git push origin foo^:master

这个参数实际的值是个 refspec,refspec意思是 Git 能识别的位置(比如分支 foo 或者 HEAD~1)。一旦你指定了独立的来源和目的地,就可以组织出言简意赅的远程操作命令了。

此时当place参数中的source部分如果置空,那么push的结果是删除远程分支,比如:

1
git push origin :foo

这条命令会删除远程仓库中的foo分支,然后删除本地的远程跟踪分支o/foo。

需要注意的是,因为我们通过指定参数告诉了 Git 所有它需要的信息, 所以它就忽略了我们所检出的分支的属性!也就是说忽视当前HEAD指向的分支,而如果在没有参数的情况下,push命令默认推送HEAD当前指向的分值,如果没有指向任何分支,则命令失败。

fetch

fetch 命令和 push 命令的参数格式十分相近。例如:

1
git fetch origin foo

Git 会到远程仓库的 foo 分支上,然后获取所有本地不存在的提交,放到本地的 o/foo 上。注意被改变的是本地的o/foo分支,因为fetch命令不会修改本地数据,除非手动合并或者使用pull。

同样place参数可以用于指定更详细的信息,<source>:<destination>形式的place参数中,source是指远程仓库中的位置,而 才是要放置提交的本地仓库的位置。但是注意在这种情况下,fetch会直接更新本地分支而不是去更新本地的远程跟踪分支。

类似于push,fetch的place参数中的source参数也可以置空,此时命令的作用就是在本地创建新分支,例如:git fetch origin :bar会在本地当前位置创建一个新的bar分支。

pull

git的参数和前面的命令相比没有什么新的内容。git pull 到头来就是 fetch 后跟 merge 的缩写。你可以理解为用同样的参数执行 git fetch,然后再 merge 你所抓取到的提交记录。

pull命令会默认用当前分支去merge参数place中的destination分支。还是像前面一样,指定destination时会合并本地分支,place不区分source和destination的时候会合并本地的远程跟踪分支。


参考内容:

  1. Git Pro Book
  2. 《Git Community Book》
  3. http://learngitbranching.js.org/
  4. 深入理解git
  5. 使用git微命令深入理解git工作机制
本站总访问量