Git:如何还原一个已经同步到远程仓库的Commit?

本文译自git howto: revert a commit already pushed to a remote repository - Christoph Rüegg

如果你刚刚将你本地的分支推送到了远程仓库中,但是却突然发现到其中的一个Commit有很严重的错误,就必须在别人pull这些Commit前还原它,否则被坑了的同事会画小圈圈诅咒你的:)

首先,有两个备选方案可以让你的历史记录完好无损:

备选方案一:在一个新的Commit中改正错误

最简单的方法就是将错误的文件修复好后作为一个新的Commit提交,并同步到远程仓库中。这是一种很直观、也很安全的修复方式,在99%的情况下你都应该使用这种方法,除非这个错的Commit中包含敏感信息。

备选方案二:完全恢复(Revert)这个Commit

有时候你会希望撤销一个Commit的全部更改,幸运的是你可以告诉Git去恢复这个Commit而不用手动地去做这件事。恢复的时候甚至可以不必是最后一个Commit。恢复一个Commit意味着你会撤销这个错误的Commit中的所有更改。就像刚刚的例子中,这个错误的Commit的记录仍然存在,只不过它不再影响当前工作的分支和任何后续的Commit了。

$ git revert dd61ab32

关于历史重写

一般来说我们会避免重写提交的Commit历史,一个很重要的原因就是它会使其他Clone过或Fork过你代码的人和你岔出不同的分支,导致他们不再能pull你重写了的历史记录和后续Commit。如果他们在本地做了一些更改想同步到远程仓库上,需要一些高级的Git知识来理解怎样让Git正常进行工作。

然而有时候你确实就像重写历史记录,无论是因为泄露了敏感信息还是想清除一些本不应该在代码中的非常庞大的文件,亦或者你仅仅就是想要一个干净的历史记录(当然我确实这么做过)。

我平常迁移Subversion或Mercurial项目到Git中时也做过大量的重写历史记录的工作,无论是为了统一LF行结束符,修改提交者的名字、邮箱地址,还是为了从版本库中彻底删除那些大文件。最近我也因为一个大的版本库中一个很早的Commit引起了越来越来多错误而重写了版本库历史。

是的,如果可能的话你应该重写那些已经扩散到了其他fork代码中的历史记录,即使你这样做了也不会世界末日。比如,你仍然可以用Cherry-pick在历史记录中来移动Commit来处理那些在旧历史记录中的Pull请求。

请记住,在开源项目中重写历史记录前先联系版本库的管理者。有的管理者一般情况下不允许任何重写历史,并且会拉黑那些这么做的人,而另外一些管理者倾向于他们自己来做重写。

情形1:删除最后一次Commit

删除最后一次Commit是最简单的一种情况。例如,我们现在有一个拥有master分支的远程仓库mathnet,它现在处于最新的Commitdd61ab32下。我们希望移除这个最新的Commit,或者用Git语言说,我们想force远程仓库mathnetmaster分支到dd61ab32的父Commit上。

$ git push mathnet +dd61ab32^:master

在Git命令中,x^代表x的父节点,+代表一个强制的非快速推送(forced non-fastforward push)。如果你在本地已经处于master分支下了,你也可以通过两个简单的步骤来实现同样的效果:先回退到父Commit,然后强制推送给远程仓库。

$ git reset HEAD^ --hard
$ git push mathnet -f

情形2:删除倒数第二个Commit

现在我们假设需要删除的Commit没有在历史记录的顶部,而是一个稍旧一点的Commit,比如倒数第二个。我们希望删除它同时保留其后的所有Commit,换句话说我们希望重写历史记录然后将重写强制应用到mathnetmaster分支上。重写历史的最简单的方法是对错误的Commit做一个互相的变基:

$ git rebase -i dd61ab32^

这个命令会打开一个vi编辑窗口,显示了从我们想删除的Commit以来的所有Commit。

pick dd61ab32
pick dsadhj278

简单地删除我们想删除的Commit信息,在这里我们删除第一行即可(在vi中删除当前行的命令为dd)。保存退出编辑器(在vi中输入:wq然后按回车键),然后解决一下有可能的冲突,然后强制推送到远程仓库就大功告成了:

$ git push mathnet -f

情形3:修复一个Commit中的拼写错误

这种情形下的操作与情形2十分相似,但不是删除Commit信息,而是将pickedit换一下位置然后保存退出即可。然后变基时,操作将会停在那个Commit上,这时你可以对这个索引做任何你想做的操作。完成后Commit改动然后继续变基(如果你想的话Git会告诉你如何保持Commit的附加信息),然后继续按情形2进行推送。用同样的方法可以将一个Commit分成很多小的Commit,或者将很多Commit合并到一起。

在Android中使用HttpClient 4.3.x出现NoSuchFieldError的问题

升级了 HttpClient

前几天看到Apache新发布了 HttpClient 4.3.5,一时手残,将我写的开源项目nForumSDK中的 HttpClient 升级了。

在纯Java的环境下测试了一下,没发现什么问题,新的包还挺好用的,于是就发布了。

过了两天,有人给我发反馈,说在Android应用上测试的时候报错:

java.lang.NoSuchFieldError:org.apache.http.message.BasicLineFormatter.INSTANCE

看了一下,报错在这句话上:

httpClient = HttpClients.createDefault();

根据StackOverFlow,问题在于Android集成了低版本的HttpClient,如果在项目中引用高版本的HttpClient就会导致出错。有三种解决办法:

  1. Apache专门针对这个问题发布了一个 HttpClient for Android 的版本,如果你使用Maven或者Gradle来管理项目,只需要在依赖中加入:

    dependencies{
        compile group: 'org.apache.httpcomponents', name:'httpclient-android', version: '4.3.5'
    }
    
  2. 或下载编译HttpClient for Android 源码, 替换原来的HttpClient即可
  3. 或将原来的HttpClient改包名,从’org.apache.http’到’thank.you.google.http’,以避免冲突
处理 ActionBarDrawerToggle is Deprecated

更新Android Support Library后,自动构建DrawerFragmentLayout,会发现有这样一个Warning:

'Android.support.v4.app.ActionBarDrawerToggle' is deprecated

替换Support Library

查询官方文档可知,我们需要将v4包中的ActionBarDrawerToggle替换为support-library-v7.appcompact.jar中的ActionBarDrawerToggle

替换后,new ActionBarDrawerToggle的时候会发生编译错误:

image1

替换构造函数

根据StackOverFlow中的这个问题,新的Support Library已经不支持传入一个静态的drawer图片作为icon

我们将构造函数:

mDrawerToggle = new ActionBarDrawerToggle(
    getActivity(),
    mDrawerLayout,
    R.drawable.ic_drawer,
    R.string.navigation_drawer_open,
    R.string.navigation_drawer_close
    ) {...}

替换为:

mDrawerToggle = new ActionBarDrawerToggle(
    getActivity(),
    mDrawerLayout,
    R.string.navigation_drawer_open,
    R.string.navigation_drawer_close
    ) {...}

编译、运行,发现新的drawer icon已经变成炫酷的动画形式了!

Git中HEAD和ORIG_HEAD指针指的是什么

一次版本回退后,在历史记录里面看到了这样一张图:

image6

master和dev指针指向的是本地的master分支和dev分支,origin/master和origin/dev指向的是远程仓库的master分支和dev分支,这个很好理解。

HEAD指针代表当前工作路径,HEAD与master指向同一id说明当前处在master分支,这个也不难理解。

关键是ORIG_HEAD指针是个什么东西?翻了翻官方的说明文档,没有发现对这个指针的说明。

搜索了一下,略微明白了一点。

针对某些危险操作,Git通过记录HEAD指针的上次所在的位置ORIG_HEAD提供了回退的功能。当你发现某些操作失误了,比如错误的reset到了一个很早很早的版本,可以使用git reset --hard ORIG_HEAD回退到上一次reset之前。

Git在1.8.5版本之后,加入了HEAD@{}功能,它通过一个链表记录HEAD的移动路径。

输入$ git reflog,结果类似于:

$ git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
d628164 HEAD@{1}: commit: xxx
ea34578 HEAD@{2}: commit: xxxx
cb926e7 HEAD@{3}: commit: xxxx

每一次移动HEAD指针,Git都会将移动的路径通过链表串起来,链表头部的HEAD@{0}即HEAD指针。

但是HEAD@{1}并不一定是ORIG_HEAD!注意到,ORIG_HEAD仅仅是当进行危险操作(比如merge)时才会变更为HEAD指针的原值,而HEAD@{}链表则记录了每次HEAD的移动(包括commit)。

考虑以下情况:

  • 1.commit -> 2.merge -> 3.commit

此时,HEAD@{0}、HEAD@{1}、HEAD@{2}分别指向3、2、1,而ORIG_HEAD指向的是1而非2。

显然,有了reflog命令后HEAD链表比不知道什么变过的ORIG_HEAD更好用,因此如果你使用的是1.8.5版本之后的Git,推荐使用HEAD{}链表来代替ORIG_HEAD指针。

Github不记录Contributions的问题

Github没有记录我的Contributions

写博客的时候一直用的Sublime Text,然后用配置好的MyGitcommitpush,这次写nForumSDK的时候,用了Eclipse集成的插件EGit,然后发现所有用Eclipsepush的Github都没有记录在Contributions里,Contributions Calendar白白的一片,太伤心了。。

Github为什么不记录

我一直在Dev分支下开发,并未merge到Master分支上,看了一下Github关于Contributions的说明,发现有这样一句话

Commits are only counted if they are made in the default branch (usually master).

只有在默认的Master分支下的Commits才能被算进去,于是我到Master分支下merge了Dev分支,发现仍然没有出现在我的Calendar中……

经过各种排查,发现了问题的所在。我在配置EGit的时候,图方便仅仅将其SSH Key设置为了MyGit的rsa_pub,其他的参数都没有修改,于是在Commit的时候,EGit自动将用户邮箱设为了dss886@我的ip地址.com,这样Github不认为Commit的用户是我……囧。

在Preference->Team->Git->Configuration中添加两条记录:user.name=dss886user.email=dss886@gmail.com,再进行Commit,发现已经记录在Contributions Calendar中了!

什么样的Contribution Github才记录?

翻译一下Gihub关于Contributions的说明,希望能帮到大家,不要让辛辛苦苦写的代码被Github忽视了:)

会被记录的Contribution情形

1. Issues and pull requests

同时满足以下两个条件将会被计入Contribution

  • 这个操作是在一年之内。(Calendar只显示一年之内的Contribution)
  • 这个操作是针对一个独立的仓库。(在Fork的仓库中进行的操作不会被记录)

2. Commits

同时满足以下四个条件将会被计入Contribution

  • Commits是在一年之内。(Calendar只显示一年之内的Contribution)
  • 进行Commits的用户被关联到了你的Github帐号中(使用SSH方式能够不输入帐号密码进行push,如果此时你Commit的帐号不在Github帐号列表中,就不会被计入Contribution)
  • 是在一个独立的版本库中进行Commit。(在Fork的仓库中进行Commit则不会被记录)
  • 是在这个版本库的默认分支(通常是Master)进行的Commit。(如果你在Dev分支下进行开发,你的Commit不会被计入Contribution,但是并不会丢失它们,一旦当你Merge到Master分支后,所有的Commit都会被重新计入。多人协作也是同理,只有被并入Master分支的Commit才会被计入,如果你的Commit在合并时被组长丢弃,在Github看来,你就白干了……)

3. 附加条件

如果你Commit的仓库不是你创建的,那么至少要满足以下四条之一,才会被计入Contribution

  • 你是这个仓库的协作者,或者是这个版本库的拥有组织中的一员。
  • 你fork过这个仓库。
  • 你对这个仓库发起过pull request或者issue。
  • 你对这个仓库标记了Star。

(私有仓库的Commit也会被计入Contribution,没有这个私有仓库权限的用户将看不到这个Commit的跳转链接)

Contributions未被Github计入的几个常见原因

  • 进行Commits的用户没有被关联到你的Github帐号中。
  • 不是在这个版本库的默认分支进行的Commit。
  • 这个仓库是一个Fork仓库,而不是独立仓库。