重学git


基础

撤销

修改最后一次提交

git commit --amend

此操作可以修改上一次commit的内容

取消暂存的文件

git reset HEAD filename

任何已经提交到 Git 的都可以被恢复。即便在已经删除的分支中的提交,或者用 –amend 重新改写的提交,都可以被恢复

取消当前修改

git checkout -- file

这个命令有点危险,修改全没了,不要轻易用

远程

抓取

git fetch origin

会抓取从你上次克隆以来别人上传到此远程仓库中的所有更新(或是上次 fetch 以来别人提交的更新)。fetch 命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支。

git remote show origin //查看远程分支以及详细情况

分支

分支其实就是从某个提交对象往回看的历史

可以想象成一个逻辑框图或者树结构……

git checkout master它把 HEAD 指针移回到 master 分支,并把工作目录中的文件换成了 master 分支所指向的快照内容。

新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)

合并

如果顺着一个分支走下去可以到达另一个分支的话,那么 Git 在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)

当你在使用分支及合并的时候,一切都是在你自己的 Git 仓库中进行的 — 完全不涉及与服务器的交互

远程分支

远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置

在 fetch 操作下载好新的远程分支之后,你仍然无法在本地编辑该远程仓库中的分支,拥有的只是一个无法移动的指针而已。

接下来需要用:

git checkout -b local_branch origin/remote_branch

git merge origin/remote_branch   或者合并到当前分支

删除远程分支

git push origin :remote_name
这个`冒号`那可是相当的重要啊

rebase

git checkout branch
git rebase master

它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行衍合的分支)后续的历次提交对象,生成一系列文件补丁,然后以基底分支(也就是主干分支master)最后一个提交对象为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象,从而改写branch的提交历史,使它成为master分支的直接下游

一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。

提交

  • 不要在更新中提交多余的白字符,可以用git diff --check检查
  • 每次提交限定于完成一次逻辑功能,适当分解为多次小更新

在推送数据之前,先确认下要并进来的数据究竟是什么,于是运行 git log 查看:

git fetch origin

git log --no-merges origin/master ^your_branch

暂存

git stash
git stash apply  //暂存后切回来重新应用
git stash list   //查看暂存内容
git stash apply --index   //对文件的变更被重新应用,但是被暂存的文件没有重新被暂存

apply 选项只尝试应用储藏的工作——储藏的内容仍然在栈上。

git stash drop //移除

取消暂存:

git stash unapply //不提供,但是可以配置在git alias里面
或者
git stash show -p | git apply -R

从暂存中创建分支:

git stash branch

创建一个新的分支,检出你储藏工作时的所处的提交,重新应用你的工作,如果成功,将会丢弃储藏

修改提交

修改最近四次的提交,不是3哦~

git rebase -i HEAD~3

HEAD~3..HEAD范围内的每一次提交都会被重写无论你是否修改说明。但是已经推送到服务器的提交最好不要修改,这回使得其它开发者混乱~

交互式的rebase给了你一个即将运行的脚本。它会从你在命令行上指明的提交开始(HEAD~3)然后自上至下重播每次提交里引入的变更。

git commit --amend   //确定要修改啥……

git rebase --continue   //修改之后把不要修改的补上?大概这么理解吧~

将这三个提交合并为单一提交:

squash

拆分提交: «««< HEAD 拆分提交就是撤销一次提交,然后多次部分地暂存或提交直到结束

一定要确认所有的修改不包含已经push到服务器共享的commit,否则会造成紊乱……

清理历史提交

这个比较x,比如不小心提交了安装包,二进制文件等很大的文件,那么就应该应用这个命令了。

git filter-branch --tree-filter 'rm -f passwords.txt' HEAD

--tree-filter选项会在每次检出项目时先执行指定的命令然后重新提交结果。在这个例子中,你会在所有快照中删除一个名叫 password.txt 的文件,无论它是否存在。

你可以观察到 Git 重写目录树并且提交,然后将分支指针移到末尾。一个比较好的办法是在一个测试分支上做这些然后在你确定产物真的是你所要的之后,再 hard-reset 你的主分支。要在你所有的分支上运行filter-branch的话,你可以传递一个–all给命令。

这个会大面积地修改你的历史,所以你很有可能不该去用它,除非你的项目尚未公开,没有其他人在你准备修改的提交的基础上工作。

将一个子目录设置为新的根目录

git filter-branch --subdirectory-filter folder HEAD

现在folder就是根目录了,git会自动删除不受影响的提交……

拆分提交就是撤销一次提交,然后多次部分地暂存或提交直到结束

commit却没有push

git log origin/master..master

内部原理

pro git看到这也不容易啊,mark

Git 是一套内容寻址 (content-addressable) 文件系统

Git从核心上来看不过是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。

Git 往磁盘保存对象时默认使用的格式叫松散对象 (loose object) 格式。Git 时不时地将这些对象打包至一个叫 packfile 的二进制文件以节省空间并提高效率。当仓库中有太多的松散对象,或是手工调用 git gc 命令,或推送至远程服务器时,Git 都会这样做。

移除对象

因为前几天就干了一件傻事,将一个80M的安装包传上去了,导致战友们fetch的时候,一个劲的慢……都怪我没经验哪……以后commit的时候也不要-A 或者*了,切记切记!!

为了移除对一个大文件的引用,从最早包含该引用的 tree 对象开始之后的所有 commit 对象都会被重写。

下面是详细的过程:

假设不小心添加了文件,这个文件很大,比如:python_64.tar.gz。

查看占用空间大小

git gc  

查看添加的文件新增了多少空间,其中size-pack 是以千字节为单位表示的 packfiles 的大小

git count-objects -v  

然后找到这个添加的大文件(假设我还不知道是哪个……)

git verify-pack -v .git/objects/pack/pack-e6d87e7ee0e062e996cda8702a6ebf527d5aa6a6.idx | sort -k 3 -n

按大小排序,最大的那几个大文件里面找~

查看这到底是哪个文件:

git rev-list --objects --all | grep 12f1fbe821e8ed

该文件从历史记录的所有 tree 中移除,找出哪些commit 修改了这个文件

git log --pretty=oneline --branches -- python_64.tar.gz

重写从列出来的最后一个开始的所有 commit 才能将文件从 Git 历史中完全移除

git filter-branch --index-filter 'git rm --cached --ignore-unmatch python_64.tar.gz' -- 684565215b6^..

--index-filter是修改暂存区域或索引。不能用rm file命令来删除一个特定文件,而是必须用git rm --cached来删除它 ── 即从索引而不是磁盘删除它。这样做是出于速度考虑 ── 由于Git在运行你的filter之前无需将所有版本签出到磁盘上,这个操作会快得多。git rm 的 --ignore-unmatch选项指定当你试图删除的内容并不存在时不显示错误。最后,因为你清楚问题是从哪个commit开始的,使用filter-branch重写自xxxx这个commit开始的所有历史记录。不这么做的话会重写所有历史记录,花费不必要的更多时间。

将引用删除并对仓库进行 repack 操作:

rm -Rf .git/refs/original
rm -Rf .git/logs/
git gc

完全把这个对象删除:

git prune --expire now

终于可以push了……

##最后有点混淆的也搞定一下

git pull = git fetch + merge to local

事实上,fetch比较安全,因为在merge之前可以先diff一下~

团队合作

segmentfault借鉴……

git rebase是对commit history的改写。当你要改写的commit history还没有被提交到远程repo的时候,也就是说,还没有与他人共享之前,commit history是你私人所有的,那么想怎么改写都可以

而一旦被提交到远程后,这时如果再改写history,那么势必和他人的history长的就不一样了。git push的时候,git会比较commit history,如果不一致,commit动作会被拒绝,唯一的办法就是带上-f参数,强制要求commit,这时git会以committer的history覆写远程repo,从而完成代码的提交。虽然代码提交上去了,但是这样可能会造成别人工作成果的丢失,所以使用-f参数要慎重。

楼主遇到的问题,就是改写了公有的commit history造成的。要解决这个问题,就要从提交流程上做规范。

举个正确流程的栗子:

假设team中有两个developer:tom和jerry,他们共同使用一个远程repo,并各自clone到自己的机器上,为了简化描述,这里假设只有一个branch:master。

这时tom机器的repo有两个branch master, origin/master 而jerry的机器上也是有两个branch master, origin/master

tom和jerry分别各自开发自己的新feature,不断有新的commit提交到他们各自私有的commit history中,所以他们的master指针不断的向前推移,分别指向不同的commit。而又由于他们都没有git fetch和git push,所以他们的origin/master都维持不变。

jerry的repo如下

tom的repo如下,注意T1和上图的J1,分别是两个不同的commit

这时Tom首先把他的commit提交的远程repo中,那么他本机origin/master指针则会前进,和master指针保持一致,如下

远程repo如下

现在jerry也想把他的commit提交到远程repo上去,运行git push,毫无意外的失败了,所以他git fetch了一下,把远程repo,也就是之前tom提交的T1给拉到了他本机repo中,如下

commit history出现了分叉,要想把tom之前提交的内容包含到自己的工作中来,有一个方法就是git merge,它会自动生成一个commit,既包含tom的提交,也包含jerry的提交,这样就把两个分叉的commit重新又合并在一起。但是这个自动生成的commit会有两个parent,review代码的时候必须要比较两次,很不方便

jerry为了保证commit history的线性,决定采用另外一种方法,就是git rebase。jerry的提交J1这时还没有被提交到远程repo上去,也就是他完全私有的一个commit,所以使用git rebase改写J1的history完全没有问题,改写之后,如下

注意J1被改写到T1后面了,变成了J1`

git push后,本机repo

而远程repo

异常的轻松,一条直线,没有-f

所以,在不用-f的前提下,想维持树的整洁,方法就是:在git push之前,先git fetch,再git rebase

git fetch origin master
git rebase origin/master
git push origin master

revert & reset

  • git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit
  • 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入
  • git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容

上个图

img

然后也可以参考下阮一峰的博客,写得也挺全面的