11
10
2016
14

数据让 git 给吃了!

之前一直觉得 git 是很安全的,除非用户显式指定(比如 --force 啦,reset --hard 啦,checkout xxxx 啦),git 在用户会失去数据时都会停下来,让不小心的用户有机会处理被遗忘的修改。直到有一天,我们有个文件让 git 给吃了!

嗯,是「我们」,不是「我」。这是我们的代码部署服务器上出的事。这仓库不是我使用的,整个操作流程我也没有参与设计与评估。实际上我只是作为 troubleshooter 参与到这次神秘事件之中的。

要让 git 愉快地吃掉数据,只要这样就可以了:

  • 提交 A 不包含文件 f
  • 提交 B 包含文件 f
  • 当前工作区为提交 A,并且包含一份未被 git 管理的文件 f,并且 f 被 gitignore 忽略掉了

然后做如下操作,未被管理的那份 f 就会消失不见了:

  • 将工作区切换到提交 B。因为 f 被忽略,所以 git 不会报错(代码
  • 将工作区再切换回 A。因为 A 不包含 f,所以 f 被删掉了

正在吃 f 的 git:主人遗弃了的 f 就交给我好了~

要避免出现这种问题,当然是在 git 工作区会有修改的时候,不要依靠 git 来在多个版本间切换啦~btrfs 或者 zfs 的快照多好!如果文件系统不支持快照的话,那就用多个目录吧。

Category: 版本控制 | Tags: linux Git
2
8
2015
7

使用 git 底层命令创建提交

通常,创建 git 提交使用的是 git add、git commit 这些高层命令。这些命令有个特点:它们都需要工作区的存在,并且很可能会改变工作区里的文件。

如果没有工作区,或者工作区不能用的时候怎么办呢?比如想在服务器上的纯(旧称:祼)版本库里的钩子脚本里创建提交,在版本库被更新时自动生成点东西什么的。一个简单的解决方案是再克隆一份,然后在那边弄好了再 push 过来。但那样的话,你得区分一次 push 是不是由你的脚本自身触发的了。这当然是可行的,但是,不觉得直接操作纯版本库更有意思吗 O(∩_∩)O~

或者,如果你的工作区里有一个文件叫「test」,另一个文件叫「Test」,而你当前能使用的文件系统和/或操作系统是不区分文件名大小写的,这样可能就没有办法通过工作区做想要的改动了。

我之前也曾用 pygit2 来直接创建提交。那个还是比较基础的,还是需要工作区。这次让我向大家介绍一下我的新玩法,深入 git 底层,围观一下一个提交的诞生历程~~(其实是早就玩过了的,只是一直没有分享出来而已 >_<

首先,克隆一个版本库来玩儿。直接在已有的版本库里玩太危险了,万一不小心玩坏了就囧了(虽然也不是多大的问题,毕竟我有备份的嘛=w=

git clone --bare ~/.vim dotvim

读者想要一起玩的话,可以从网络克隆我的 vim 配置版本库,如

git clone --bare git://github.com/lilydjwg/dotvim dotvim

进去 dotvim 目录里 ls 一下,可以看到,只剩下以前在 .git 目录里会见到的文件了呢:

>>> ls
branches  config  description  HEAD  hooks  index  info  objects  packed-refs  refs

要创建的提交是在 master 分支上。我们先使用 ls-tree 命令看看这个分支上有哪些文件。ls-tree 其实是列出 tree 对象用的,加上 -r 参数就会递归地把 tree 对象里的 tree 对象给列出来,就像 -R 之于 ls 命令一样。

不过你给它提交(commit)对象也可以的啦。它会自动取这个提交所指向的 tree 对象:

git ls-tree -r master

相当于

git ls-tree -r 'master^{tree}'

嗯,分支名实际上是指向这个分支上的最后一个对象的符号引用。

我要把 vimrc 文件里的注释行全删掉。要想修改一个文件,就得先找到要修改的文件,而不像添加文件那样直接加进去就可以了。让我们把要修改的 vimrc 文件的 hash 值找出来:

old_vimrc=$(git ls-tree -r master | awk '$4 == "vimrc" { print $3 }')

当然这里是不必用 -r 的啦。写在这里方便嘛,下一次想改 plugin 目录下的东西可以直接改路径就可以了,不用担心要改其它可能会忘记的东西。

然后拿 cat-file 命令看看这个 blob 对象。cat-file 命令我用得挺多的,因为经常会想看看另一个分支、或者另一个提交里某个文件长什么样子。

git cat-file -p $old_vimrc

其实这里可以直接指定要显示的对象的,比如master:vimrc就是 master 对应的提交上的 vimrc 文件。如果使用 zsh 的话,冒号后边的路径部分也是可以补全的哦。这里为了阐述原理,就做「分解动作」了。

得到了文件内容,就可以修改了。修改完毕,使用 hash-object 命令将文件存入 git 对象数据库里。这一句命令相当于修改好文件之后再做 git add 操作。hash-object 会返回对象的 hash 值。我们得把它记下来。

new_vimrc=$(git cat-file -p $old_vimrc | sed '/^"/d' | git hash-object -w --stdin --path vimrc)

不管之前有没有,先删一下 index,也就是所谓的「staging area」。已经添加但是还未提交的目录树就存在这个文件里边了。

rm -f index

然后,创建我们需要的 index。得使用 ls-tree 命令列出所有文件的信息,然后把我们修改过的信息加到末尾,会自动覆盖之前已有的项。如果删除这个列表里的某些项的话,就相当于是删除了那些文件。如果添加原本不存在的项,就是添加文件了。

{git ls-tree -r master; echo -e "100644 blob $new_vimrc\tvimrc";} | git update-index --add --index-info

index 已经准备好了。我们把目录树写到 git 对象数据库里吧:

new_master_tree=$(git write-tree)

我们得到了一个新的 tree 对象的 hash 值。当然因为目录树是树状的,以上命令实际上会写入多个 tree 对象。我们只要有根 tree 对象的 hash 值就可以了。

该创建提交对象了:

commit=$(git commit-tree -p master -m '在没有工作区的情况下创建的提交' $new_master_tree)

提交对象创建好还不够。那只是一个提交对象而已,我们还没有更新分支的信息呢。我们把这个提交作为新的 master 分支的头:

git update-ref refs/heads/master "$commit"

大功告成!

可以使用git showgit log看看成果了哦~

当然,如果想要钩子脚本里使用的话,记得在修改前加锁哦。

参考资料

Category: 版本控制 | Tags: git
8
20
2012
7

GM 脚本:修正 github

从某时起,Github 和 Linux 一样,开始有着越来越多的 bug 和让人不舒服的地方。本文所附的 GreaseMonkey 脚本修正以下问题:

  • 项目首页默认下载文件格式是 zip 而不是 gzip
  • 新建项目后,从已有项目创建的提示命令使用 HTTPS 而不再是 SSH 协议。这直接导致 git 向用户询问用户名和密码,而不使用用户已经上传并确认的密钥。

Google Code 后来也加入了 git 支持,但是我极少使用。为什么呢?因为我讨厌输入用户名和密码!虽然 Github 没有像 Google Code 那样给你生成个随机密码,但这种麻烦且不安全的方式能避免我就决不容忍。你的密码会比密钥还长吗?你使用密钥时需要输入或者显示密钥的内容吗??

// ==UserScript==
// @name           github fixes
// @namespace      http://lilydjwg.is-programmer.com/ 
// @description    下载默认 gzip 格式,新建项目时使用 ssh 协议
// @include        https://github.com/*
// @version	   1.4
// @grant          none
// ==/UserScript==
 
var dl = document.querySelector('[icon_class=octicon-cloud-download]');
if(!dl){
  dl = document.querySelector('a[title^=Download]');
}
if(dl){
  dl.title = dl.title.replace('zip', 'gzip');
  dl.href = dl.href.replace(/archive\/[^\/]+.zip/, 'tarball/master');
  var infotext = dl.childNodes[dl.childNodes.length-1];
  infotext.textContent = infotext.textContent.replace('ZIP', 'GZIP');
}

var repourl = document.querySelectorAll('.js-live-clone-url');
var re = /https:\/\/github\.com\/([^\/]+)\/(.*)/;
var span, m;
var i, len;
for(i=0, len=repourl.length; i<len; i++){
  span = repourl[i];
  m = re.exec(span.textContent);
  if(m){
    span.textContent = 'git@github.com:'+m[1]+'/'+m[2];
  }
}

点此安装

2012年9月9日更新:跟随 github 的更新,修正修改默认下载的格式失败的问题。

2012年11月27日更新:跟随 github 更新,使用带版本号的下载链接地址。

2013年5月17日更新:跟随 github 更新,更新 CSS 选择器。

2013年6月18日更新:支持 新版界面

Category: 版本控制 | Tags: github GreaseMonkey
4
22
2011
5

初次使用 git 的“核弹级选项”:filter-branch

当初看 Pro Git 时就被作者这个“核弹级选项”的称呼吓到了,因此一直没敢好奇地去尝试。核弹啊,用对了威力无穷,用错了破坏力无穷!

但是,今天,我不得不用了,因为我想把我的 Python 脚本放到 github 上去公开。由于之前没想过要公开,所以不能肯定是不是把诸如密码之类的敏感数据直接写代码里了。于是我就用 git 的 pickaxe 选项-S来找找看。这个我也是今天才学到的哦,来源是stackoverflow上的若干问题,具体记不清了。

这个选项结合git log,其功能是把提交中包含某个文本的提交找出来。比如git log --stat -S密码就把所有提交中包含“密码”二字的提交找出来了,同时--stat告诉 git 我希望看到文件列表,以确定敏感数据在哪个文件里。另外说下zsh,我设置了hist_ignore_space选项,所以当查找密码时我在命令行最开始输入个空格,这样 zsh 就不会把这条命令写到命令历史里。

找了下,还真发现了两个包含密码的文件(其中之一 base64 过,另一个直接明文。。。)。我决定把它们转移到其它目录下,所以要删除这些文件。同时我意外地发现,有些提交中我引用了之前的提交,比如“(after xxx) fix xxx”这种。重写 git 历史后这些提交的 sha1 值会改变,所以这些提交信息也要重写。最开始我想把提交 sha1 修改为重写后的值,后来发现有点麻烦,得通过提交 sha1 查询一个备份中的提交信息(如果不事先保存相关信息的话),然后再通过提交信息查询重写后的提交的 sha1 值,还不知道 filter-branch 时能不能查询历史,于是作罢。还好这个仓库中只有一个这样的提交,所以我直接修改成~n表示引用第 n 次前的提交算了。最后,整个命令出来了:

git filter-branch --tree-filter 'rm -rf files_to_remove' --msg-filter '
sed s/d101601/~8/
' --prune-empty -f HEAD --all

命令挺复杂的,所以我是调用 Vim 写好的,然后在一个 clone 出来的仓库里先试运行。先解释下各个参数:

  • --tree-filter表示修改文件列表。
  • --msg-filter表示修改提交信息,原提交信息从标准输入读入,新提交信息输出到标准输出。
  • --prune-empty表示如果修改后的提交为空则扔掉不要。在一次试运行中我发现虽然文件被删除了,但是还剩下个空的提交,就查了下 man 文档,找到了这个选项。
  • -f是忽略备份。不加这个选项第二次运行这个命令时会出错,意思是 git 上次做了备份,现在再要运行的话得处理掉上次的备份。
  • --all是针对所有的分支。

试运行了几次,看到 150 多次提交逐一被重写,然后检查下,发现要删除的文件确实被删除了。于是高兴地到 github 建立新仓库,把脚本上传了。仓库名叫winterpy,因为网友 Vayn 建议我用 summerpy,而我更喜欢冬季 :-P

折腾完毕,我更加喜欢 git 了 :-)

Category: 版本控制 | Tags: python Git

| Theme: Aeros 2.0 by TheBuckmaker.com