Git 合并 patch 时的冲突处理一例

git version 1.6.0.4

几个新手刚刚开始接触 Git,为了维护核心仓库的“纯洁”,避免太多无关信息被误提交进仓库(再次批评一些图形化工具默认的“Select All”),采用了核心仓库只读,邮件提交 patch,审核后再提交的工作流程。

期间有时会遇到合并冲突,正常的原因一般是未及时下载新版本产生了冲突,特殊一点的原因是手工修改 patch 内容导致的。有时候看注释写得不够准确,忍不住就改了,有时候是 Geany 保存时自动去除了 patch 原文中的行尾空格,有时候是文件回车格式、BOM 等变动了,总之合并 patch 的时候,如果生成 patch 的“原稿”找不到,一般就产生了冲突,比如:

$ git am 0001-BUG-Sybase.patch
Applying: CHG: 读取Sybase如果时间为空,设置默认时间的修改
error: patch failed: source.php:38
error: source.php: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".

刚开始一看有些懵,因为没有任何冲突在哪里的提示,后来找到一种方法,am 操作出问题后先手工 apply:

$ git apply --reject 0001-BUG-Sybase.patch
Checking patch source.php...
error: while searching for:
        // 注释
        // 以下为几行代码片断
error: patch failed: source.php:38
Applying patch source.php with 1 rejects...
Rejected hunk #1.

这样,就把没有冲突的文件先合并了,剩下有冲突的作了标记。先看输出,error: while searching for: 说明是这段代码有冲突,error: patch failed: source.php:38 指明了产生冲突的代码片断的开始行号,相应的,patch 中应该有这么一段:

diff --git a/source.php b/source.php
index 8770441..4e77b8a 100644
--- a/source.php
+++ b/source.php
@@ -38,27 +38,23 @@ class Site extends Module
        // 注释
        // 以下为几行代码片断

同时,还会产生一个 source.php.rej 文件,里面也是上面这段因为冲突无法合并的代码片断。

现在,在这段代码中查找冲突原因,并对文件进行修改,source.php.rej 参考完了可以删掉。改好之后,用 git add 把 source.php 添加到缓冲区,同时也要把其他没有冲突合并成功了的文件也加进来,因为在作 apply 操作的时候他们也发生了变化:

$ git add source.php
$ git add 其他 apply 进来的文件们

最后:

$ git am --resolved
Applying: CHG: 读取Sybase如果时间为空,设置默认时间的修改

大功告成。

中间如果处理乱了,用 git reset 恢复即可,所以合并 patch 在一个“干净”的分支上处理更好。

[Git]初学者注意事项

实在是受不了有些人的 Git 提交,费大力气“回滚”,遂整理了这些刚开始用 git 或者还没有建立 scm 概念时容易犯的错误。

和源码无关的东西,尽量不要进仓库

不得不说一些图形化软件,在提交内容的时候大多提供一个“全选”或者“Select All”功能,这是最不好的了,一些懒惰的同志看都不看就连瓢带碗都提交了。

  • 测试时上传的文件,测试时的临时文件,统统不要
  • 对应上一条,强烈建议把所有文件的上传保存目录另行设置,放到源代码目录以外
  • 编辑器产生的备份文件、临时文件,编译时的中间文件,统统不要
  • 对应上一条,有个例外就是为了实现通过 Git 更新系统,.NET 的 bin 文件要进仓库,导致那个仓库现在都 100+m 了
  • 图片等资源文件,进仓库也可以,但应当使用有意义的文件名,便于后期管理
  • 对应上一条,现在设计网站界面喜欢先作图然后切割,产生一大堆 001_r5_c1.jpg 这样的文件,讨厌之极
  • 使用的外部类库,比如 php 类、js 类等,统统扔到源码目录以外,如果实在没办法要放在目录树中,也可以留出空目录,打包发行的时候再包含进来,依然不进代码仓库
  • 不要中文文件名,主要是跨平台使用有问题,文件名完全能够只用字母数字减号下划线

尽量采用相对小、相对独立的提交

Git 是作什么用的?Git 不是代码上传工具,也不是网站更新工具,而是软件开发过程的记录工具,为了更加准确的定位每个问题、每个功能修改,就需要在每完成一部分可以称得上是“一项”的工作时,就 commit 一次。哪怕只是修改了一两行,只要产生了必要的功能改变,就有价值记录。

当采用代码审核机制或者需要用邮件提交补丁时,较小的提交能够更有效、更容易、更准确的被检查和审核,这个在 linux kernel 开发文档中也有提到。

当然不能矫枉过正,必须有可记录的改变才有提交的价值。对应的,Git 日志大多数情况下主要显示第一行,控制每次提交都能用一句话简单概括,也是有必要的。

注释格式

格式属于个人习惯和团队规范范围,有必要采用相对统一的风格。

Git 本身不允许空注释,同时建议注释的第一行写简要说明,下面留一行空行,再写详细说明。

我的个人习惯,喜欢在每条注释前面用大约三个字母来表示本次修改的性质:

  • Add something
  • Bug [fix|found]: describe the bug or fix.
  • Chg something
  • Del something
  • Enh some treatment
  • New something
  • Tmp for some cause

为了保持语法通顺,也可以采用前三个字母后面加冒号,后面有啥写啥的方法。

最后,我觉得,能够遵守行业规范和团队约定,主动养成良好习惯,应当是鉴别人才的一项重要因素。

Update @ 2012-12-03

关于注释格式,在使用了几年 Git 后,又有了更深的认识, 参见 Git commit 注释格式

[Git]真正回滚已上传的更新

首先,抛弃本地的修改应当用:

$ git reset --hard HEAD

使用 git 自身功能来回滚代码,取消上一次的修改应该用:

$ git revert sha1_of_commit

但要注意,虽然代码是实现了回滚,同时也会自动产生一条“回滚代码”的 log 。

有些时候,由于工作人员粗心,错误提交的内容完全无意义且占用空间颇大,就想真正 undo 掉错误的 commit,连历史记录都不想留。以下是我尝试的做法:

准备一份干净的客户端仓库

在客户端,下载服务器上的每个分支,并更新到最新状态。git branch -a查服务器上有哪些分支,挨个 checkout 过去再 pull。

然后回到有错误 commit 的分支,reset 掉错误的 commit:

$ git reset --hard 671475b1ce

这样在客户端就形成了一个已剔除掉错误 commit 的完整状态了。

从客户端仓库生成服务端仓库

这就是 git 分布式源代码管理的优势,客户端也是完整仓库,只是表现形式与服务端的不同罢了,两者之间可以转换。在本地仓库( repo.client )的上级目录中执行:

$ git clone --bare repo.client repo.git

然后把现在服务端仓库中的 hooks, info 目录和 config, description 两个文件拷贝到新生成的服务端仓库当中。

然后备份旧的服务端仓库,删掉用新生成服务端仓库替代,并调整相关文件权限。

基本上就可以了,小结

在客户端 pull:

$ git pull
From ssh://domain.tld/repo
 + 368b15f...671475b master     -> origin/master  (forced update)
Already up-to-date.

客户端仓库的 HEAD 自动被重置到了错误 commit 之前的。

我这种做法,只适用于用户比较小,可以停掉服务慢慢弄的情况,并且会丢失所有错误 commit 之后的改动,所以要慎用。最好的方法还是搞好用户培训,避免产生离谱的错误提交,一些小的错误还是直接用revert好了。

[Git]提交后自动发email

当然是通过hooks来实现了,对应post-receive,脚本也是已经随git-core安装就有了的:/usr/share/doc/git-core/contrib/hooks/post-receive-email,不过这个脚本用到了sendmail,我想好多机器上都没配这个东东吧,好在有好心人在这个脚本的基础上进行了完善,可以用msmtp发信了,看来我以前换用msmtp配mutt太正确了,另外msmtp也确实比sendmail小巧多了。

首先把刚才说到的脚本以及依赖文件functions一起下载到某地,加上执行属性(公用),然后ln到仓库的hooks目录下:

$ cd test.git/hooks/
$ mv post-receive post-receive.origin
$ ln -s ../../gittools/post-receive-email post-receive
$ ln -s ../../gittools/functions

然后修改仓库里的config文件,注意不是客户端的:

[hooks "post-receive-email"]
    mailinglist = list1@domain.tld, list2@domain.tld
    announcelist =
    envelopesender = mailsender@domain.tld
    sendmail = /usr/bin/msmtp

其中:

  • mailinglist 默认的收信人,留空就不发信了
  • announcelist 创建tag时发送从上次创建tag以来的汇总邮件的收信人,比如用版本号作为tag的时候这就生成了每个版本的changelog,留空则用mailinglist的值
  • envelopesender 发信人/账号,要和msmtp的发信账号对应起来
  • sendmail sendmail或其它发信程序的路径

还有个环境变量$USER_EMAIL,也是个发信人,不过是显示在邮件From:里的。如果要设置邮件里的From:,需要自己修改post-receive-email或在其之前执行的脚本,设置环境变量:

export USER_EMAIL="GIT <mailsender@domain.tld>"

这一版修改中作者还去掉了原来的emailprefix设置参数,固定为用仓库里description文件的内容加上[]替代。

最后,在git(git服务器运行用户)用户的HOME下放一个.msmtprc,配置发信认证信息:

defaults
    tls_trust_file /etc/ssl/certs/ca-certificates.crt
account gmail
    host smtp.gmail.com
    port 465
    auth on
    tls on
    tls_starttls off
account mailsender@domain.tld : gmail
#   from must be same as From: in mail, exclude Name, only mail address
#   Because maybe msmtp select account using from
    from mailsender@domain.tld
    user "mailsender@domain.tld"
    password my_passwd
account default : mailsender@domain.tld

现在就可以测试push,看自动发信是否正常了。如果提示functions语法错误,比如:

hooks/functions: 213: Syntax error: Bad for loop variable

可以把这两个脚本第一行的#!/bin/sh改为#!/bin/bash

注意msmtp调用的是提交动作所属用户的.msmtprc,从本机提交和远程ssh进来可能用的就不是一个.msmtprc了。

参考

Git起步

我的学习路径:git basic, http webdav, git-svn, gitosis, gitweb,下面是一些学习记录。

http webdav方式访问

ubuntu下安装非常简单,用apt装上git-core包即可。git仓库的外部访问,首先就是最熟悉的用http协议走apache上的webdav服务,方便啊。git自身的身份认证和权限分配弱了一些,不过可以借助apache实现用户分配,passwd文件直接用svn的也没问题。apache配置示例如下:

Alias /git "/big2/git"
<Location /git/test.git>
    DAV on
    AuthType Basic
    AuthName "Test git freely"
    AuthUserFile "/big2/svn/svnpasswd"
    #Require user Fwolf
    Require valid-user
</Location>

但这样远不如使用ssh/git协议访问仓库速度快和稳定,但更适合公开/对外发布。另外和svn的webdav方式不同,git webdav是一种dumb transport,不仅慢而且比较傻,只能进行简单的存、取文件操作,所以hooks是不起作用的(很多文章中说的提交后要在hooks/post-update里启用git-update-server-info,指的是git/ssh方式更新后,为webdav对外发布作准备)不会自动进行和git相关的其它处理。

最好用的还是gitosis,后面会提到。有些内容先以webdav方式下来记,使用的时候根据不同的方式转换用户角色即可。

创建服务端的仓库

其实和svn创建仓库也是比较类似的,找一空目录,比如叫test.git,执行:

$ git --bare init
$ sudo chown www-data:www-data * -R

客户端就可以下载了,但还有些问题:

$ git clone https://domain.tld/git/test.git gittest
Initialized empty Git repository in /home/fwolf/dev/gittest/.git/

error: server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt

首先遇到的是证书错误,git使用curl,所以证书的处理和svn也不一样,用firefox浏览git网站,在Page Info->View Certificate->Details中把网站的根证书(Issued By/root)导出为一个pem文件,然后将这个pem文件的内容添加到/etc/ssl/certs/ca-certificates.crt的末尾就可以了。接着遇到的是401身份验证错误:

error: The requested URL returned error: 401

git不像svn会自动提示用户名/密码,因为它用的是webdav的原生功能,所以需要在$HOME下创建文件.netrc(记得将权限修改为600):

machine domain.tld
login Fwolf
password your_passwd

最后,还是仓库本身不完善的问题:

warning: remote HEAD refers to nonexistent ref, unable to checkout.

在服务器上仓库目录,执行$ git update-server-info并再次chown到www-data就行了。初次push得指明服务器上的origin对应本地master分支/branch:

$ git push origin master

而下载之前,还要设置下载后放到那个分支、与哪个分支的现有内容进行merge:

$ git config branch.master.remote origin
$ git config branch.master.merge master
$ git pull  # 注意如果服务器是新建的空repo,得随便push点东西才好pull

gitosis

还是通过apt安装,gitosis使用SSH key来认证用户,但用户不需要在主机上开账号,而是公用主机上的一个受限账号。首先创建这个账号(名称可变):

sudo adduser \
    --system \
    --shell /bin/sh \
    --gecos 'git user' \
    --group \
    --disabled-password \
    --home /big2/gitosis \
    git

然后生成自己用户的ssh密钥,可以在git用户的$HOME下操作:

ssh-keygen -t rsa

不要密码,可以起名叫git_rsa[.pub],然后用公钥来初始化gitosis的目录:

$ sudo -H -u git gitosis-init < /home/fwolf/.ssh/git_rsa.pub 
Initialized empty Git repository in /big2/gitosis/repositories/gitosis-admin.git/
Reinitialized existing Git repository in /big2/gitosis/repositories/gitosis-admin.git/

可以看出gitosis自己也在用git仓库的形式存储东西,传递公钥进去是为了配置自动登录,将公钥写入git用户的$HOME/.ssh/authorized_keys。接下来就启用gitosis仓库的post-update hook:

$ sudo chmod 755 repositories/gitosis-admin.git/hooks/post-update

其实用apt安装的gitosis,这个post-update早已指向其它文件并具有x属性了。

一点提示:把gitosis的repositories和自己原先的git repo目录弄到一起,以后方便,因为gitosis自己的管理也是要通过git来提交的,在客户端先修改$home/.ssh/config

Host git.domain.tld
Compression yes
IdentityFile ~/.ssh/git_rsa # 注意这个是私钥,不是pub公钥

然后从客户端clone gitosis的配置文档:

$ git clone git@git.domain.tld:gitosis-admin.git

注意实际上这个gitosis-admin.git应该是在git用户的$HOME下的repositories目录下,但这么写才能下载。clone得到一个配置文件gitosis.conf和目录keydir/,对了,git配置的修改、上传也是通过git提交,并且由于刚才设置的post-update hook,立即生效。所以现在就能够远程修改、提交配置了。下面是一份比较完整的自说明的gitosis.conf

# example: http://eagain.net/gitweb/?p=gitosis.git;a=blob;f=example.conf
[gitosis]

## Allow gitweb to show all known repositories. If you want gitweb,
## you need either this or a [repo foo] section for each repository
## you want visible in gitweb.
gitweb = no

[group gitosis-admin]
writable = gitosis-admin
# member is key file name, without (.pub)
members = fwolf

# user group
[group all]
# members = jdoe alice bob @anothergroup
members = fwolf
# this user group is writeable to ... repo, eg: test.git.
# writeable = foo bar baz/thud
writeable = test
#readonly = somerepo

[repo test]
## Allow gitweb to show this repository.
gitweb = yes
## Oneline description of the project, mostly for gitweb.
description = Fwolf's test git repository
## Owner of this repository. Used in gitweb list of projects.
owner = Fwolf
## Allow git-daemon to publish this repository.
#daemon = yes
daemon = no

平时就是为其它用户添加SSH key,然后用git@git.domain.tld:repo.git来连接服务器,gitosis会自动比对服务器上的pub key,判断出用户是谁。

gitweb

用apt安装,得到/usr/lib/cgi-bin/gitweb.cgi,由于cgi-bin已经由ubuntu设置好了,apache只要设置一下资源文件路径即可:

Alias /gitweb "/usr/share/gitweb"

然后修改/etc/gitweb.conf

# path to git projects (<project>.git)
$projectroot = "/big2/git";

$site_name = "Fwolf's Git Repositories";
@git_base_url_list = ("ssh://git\@git.domain.tld");

# directory to use for temp files
$git_temp = "/tmp";

# target of the home link on top of all pages
#$home_link = $my_uri || "/";

# html text to include at home page
$home_text = "indextext.html";

# file with project list; by default, simply scan the projectroot dir.
$projects_list = "/big2/gitosis/gitosis/projects.list";
# Avoid use guess repo's address and edit url manually
$strict_export = 1;

# stylesheet to use
$stylesheet = "/gitweb/gitweb.css";
# logo to use
$logo = "/gitweb/git-logo.png";
# the 'favicon'
$favicon = "/gitweb/git-favicon.png";

重启apache后就能够用http://git.domain.tld/cgi-bin/gitweb.cgi访问了。

git-svn混用

首先还要安装git-svn包,git和svn混用的好处就是既可以用git进行本地提交和灵活的分支,又能够用svn实现集中管理和分发,缺点我想可能就是svn的revision号在git里没有了,另外git有些太灵活,不是很好入门。

首先导入svn的现有项目到一个空目录:

$ git svn clone --username your-name -s https://domain.tld/svn/repo
# older versions of git: replace "-s" with "-Ttrunk -bbranches -ttags"

就这么简单,很多工作git-svn都代劳了,剩下就当正常的git目录使用即可,比如为他们指定git仓库的位置,并下载文件:

$ git config remote.origin.url https://domain.tld/git/test.git
$ git config remote.origin.fetch +refs/heads/*:refs/remotes/origin/*
$ git config branch.master.remote origin
$ git config branch.master.merge refs/heads/master
$ git checkout master   #回到master分支
$ git pull

向svn仓库提交:

$ git svn dcommit

从svn仓库接受更新:

$ git svn rebase

在git-svn的使用中还发现,如果在机器A上使用git-svn,但是在另外一台机器B上用纯git进行了push,那么B上的commit是不会被git-svn认出来并dcommit的,只有pull下来,然后再假commit一下才能让git-svn看到。

仓库地址变更

服务器上直接修改仓库目录名和apache设置,客户端上:

$ git config remote.origin.url https://domain.tld/git/testnew.git
$ git pull  # 这个必须要做,不然不能push
Merge made by recursive.
$ git push

git-svn方式下变更svn的地址

首先要修改所有commit中的地址:

$ git filter-branch --msg-filter 'sed "s/domain_old\/trunk/domain_new\/trunk/g"' $(cat .git/packed-refs | awk '// {print $2}' | grep -v 'pack-refs')

然后删掉缓存,修改地址,重新下载:

$ rm -rf .git/svn
# 编辑`.git/config`修改`[svn-remote "svn"]`的url。
$ git svn rebase (--all)

我还曾经遇到过这样的错误:

$ git svn rebase
Unable to determine upstream SVN information from working tree history

不太好处理,这里有些参考信息,我是在一个repo里pull另外一个仓库的内容了,照下面方式处理也不管用:

  • 从svn里重新clone出一份
  • git里创建个branch保存一下
  • git连接上服务器,pull
  • git checkout 到刚才创建的branch
  • git merge –no-ff master
  • git svn rebase/dcommit

效果不好,不仅产生大量的conflict,而且还是只能在新创建的branch上rebase/dcommit。大概是出现了两“棵”树,怎么也捏不到一起,最后只好重建仓库。

其它使用技巧/tips

  • 回上个版本 $ git reset --hard HEAD^
  • 得到一个文件5个版本以前的状态 $ git checkout HEAD~5 foo.c
  • 由于git是分布式的scm,所以$Id被彻底消灭掉了。
  • 不像svn,git能够记录文件的执行属性。
  • 为显示加上颜色 $ git config color.(branch|diff|interactive|status) auto,或者$ git config color.ui true
  • hooks不算仓库的内容,每个repo的hooks可以不同。
  • 当心svn的hooks,如果git里已经commit,却因为svn的hooks使git-svn的dcommit失败,很麻烦。
  • .gitignore里可以用config.php忽略文件,并且用!mt/config.php指明个别文件还是要归入scm管理的。
  • 从现有的客户端目录中生成仓库目录 $ git clone --bare dev_dir repo_dir 每个客户端都是一个完整备份,方便。
  • 用于ignore文件,.gitignore是要提交的版本,.git/info/exclude则是只在你本地生效的版本,不提交。
  • 仅给ssh登录的开发者push/pull的访问权限,将其shell设置为/usr/bin/git-shell(未测试).
  • info/allowed-users可以控制用户对branch/tag的访问权限(未测试)。
  • git --bare init --shared=group可以让仓库被同group的用户共享,不过有gitosis就不用这个了,我没测试。

参考

Update @ 2009-03-27

使用git-svn的时候,有时候会出现git和svn树不太同步的情况,在进行svn rebase的时候总是重新merge文件,这种现象多是由于以前没有正确rebase引起的,而rebase不成功的一种原因就是git的fast forward。简单理解一下,git是按照树形结构排下来的,如果遇到前后两个节点被判定为“重复”的,git就聪明的作了个fast forward,直接使用后者了。这在git里没事,因为都是一颗树,而对于svn就不行了,它还记录着要从前面那个节点开始呢,所以合并的时候就出现了两个源头的情况,自然会产生问题。所以大体上向svn更新的顺序应该是这样的:

  • git push # git更新
  • git pull –no-ff # 此处的–no-ff最为重要,平时可以不用,这里要用
  • git svn rebase # 准备提交到svn之前先和当前的树合并
  • git svn dcommit
  • git pull –no-ff
  • # 后面就是检查了,前面操作正常的话不会产生新的有效更新了
  • git push
  • git svn dcommit
  • git svn rebase
  • git pull
  • git push

附一张比较典型的出错后来回折腾的结果图: git-svn rebase dcommit error

参考 @ 2009-03-29

有了git track以后,新建要和服务器同步的branch:

$ git branch fwolf
Branch fwolf set up to track local branch refs/heads/master.

$ git track fwolf origin
tracking origin/fwolf

$ git checkout fwolf
Switched to branch "fwolf"

$ git push origin fwolf
Total 0 (delta 0), reused 0 (delta 0)
To git@domain.tld:repo.git
 * [new branch]      fwolf -> fwolf

而从其它客户端下载这个branch就简单多了:

$ git branch --track fwolf origin/fwolf
Branch fwolf set up to track remote branch refs/remotes/origin/fwolf.

掌握原理,不用git track也一样可以实现的。

Update @ 2009-04-18

MediaTemple使用的是CentOS,安装git也很简单:

$ rpm -Uvh http://mirror.centos.org/centos/5/os/i386/CentOS/python-iniparse-0.2.3-4.el5.noarch.rpm
$ rpm -Uvh http://mirror.centos.org/centos/5/os/i386/CentOS/yum-3.2.19-18.el5.centos.noarch.rpm http://mirror.centos.org/centos/5/os/i386/CentOS/yum-fastestmirror-1.1.16-13.el5.centos.noarch.rpm  # 版本号可能会有变化
$ rpm -Uvh http://download.fedora.redhat.com/pub/epel/5/i386/epel-release-5-2.noarch.rpm
$ yum -y update
$ yum install git

参考:

Update @ 2009-04-27

升级到ubuntu 9.04 jaunty后,客户端push时会遇到错误:

$ git push
/usr/bin/gitosis-serve:5: UserWarning: Unbuilt egg for ClientCookie [unknown version] (/usr/lib/python2.6/dist-packages)
  from pkg_resources import load_entry_point
Traceback (most recent call last):
  File "/usr/bin/gitosis-serve", line 5, in <module>
    from pkg_resources import load_entry_point
  File "/usr/lib/python2.6/dist-packages/pkg_resources.py", line 2562, in <module>  
    working_set.require(__requires__)
  File "/usr/lib/python2.6/dist-packages/pkg_resources.py", line 626, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/usr/lib/python2.6/dist-packages/pkg_resources.py", line 524, in resolve
    raise DistributionNotFound(req)  # XXX put more info here
pkg_resources.DistributionNotFound: gitosis==0.2
fatal: The remote end hung up unexpectedly

半天查不出原因,只是怀疑和python的版本有关系(2.5/2.6),因为gitosis在debian包中的文件,显示应该是对应python 2.5的,不过,试着自己用python(2.6)重新下载安装了一遍gitosis,好了:

	$ git clone git://eagain.net/gitosis
	$ cd gitosis
	$ sudo python setup.py install

不懂Python太吃亏了呀。另外aptitude reinstall gitosis是没用的。

Update @ 2009-05-15

Tsung那里学来一招,可以更改 Git 默认的信息存放目录 .git :

修改 Shell 變數 GIT_DIR => "export GIT_DIR=.test"

Update @ 2010-08-03

升级到 Ubuntu 10.04 Lucid 以后,gitosis 自带的 hook 文件 post-update,就是修改权限等等 commit 后,自动出发 gitosis 更新相关配置的钩子,原来 ln 到的 /usr/share/python-support/gitosis/gitosis-0.2-py2.5.egg/gitosis/templates/admin/hooks/post-update 无效了,更换成 /usr/share/pyshared/gitosis/templates/admin/hooks/post-update 就可以了,参见Bug报告。挺让人无语的bug,我折腾了半天还以为是权限设置出了什么问题呢。

利用SVN更新网站

如果你有一个假设在公网上的SVN服务器,而你的网站所在的主机允许你使用SVN客户端,并且开放了php的exec函数,那么你有福了,你可以利用SVN作为中转,更新你的网站程序。

首先,也是前提,就是你的网站程序是用SVN管理的,那么只要你的网站主机能够访问SVN,就能够使用SVN的update功能来更新程序。

准备工作一:将网站程序加上svn的控制标记,由于SVN的控制信息都存在程序所在目录的.svn子目录中,所以需要找一个空目录,并且将网站现有程序checkout到这个目录里面来,注意是checkout而不是export,因为接下来要将最新的网站程序连同他里面包含的很多个.svn目录一同上传到服务器上去。为了不让.svn目录泄露机密,要在.htaccess文件或者是httpd.conf中设定如下规则,禁止对.svn目录的访问。

	<directory ~ "\.svn">
		Order Allow,Deny
		Deny from all
	</directory>

准备工作二:作一段小程序,调用服务器上的svn命令行命令,update网站程序,下面是一个写好了的简单例子。

	//setup update target path
	$target_ar['fwolf'] = 'd:\fwolf';
 
	//setup commandline
	$svn_cmd1 = 'd:\server\svn\bin\svn.exe update ';
	$svn_cmd2 = ' --username updatebot --password xxxxxx --no-auth-cache';
 
	//output html string
	$html = '';	
 
	//recieve get parameter
	$target = isset($_GET['target']) ? $_GET['target'] : '';
	if (empty($target) || !isset($target_ar[$target]))
	{
		$html = 'Target does not correct.';
	}
	else
	{
		//execute svn update command
		$cmd = $svn_cmd1 . $target_ar[$target] . $svn_cmd2;
		$ar = array();
		$status = 0;
		exec($cmd, $ar, $status);
		for ($i=0; $i < count($ar); $i++)
			$ar[$i] = htmlspecialchars($ar[$i]);
		$html .= 'Status: ' . $status . "<br />\r\n";
		$html .= implode('<br />' . "\r\n", $ar);
	}
	echo $html;

把这个程序放到服务器上能够访问到的地方,也可以在此基础上加上一些访问限制,那么只要访问这个程序/页面,服务器就会自动更新你的网站啦。

	Status: 0
	D H:\cvswork\svntest\update_from_svn.php
	Updated to revision 44.

这样,在利用SVN很好的管理网站程序的基础上,还实现了服务器程序的很方便的更新,一举两得!尤其是在程序上传不是十分方便的场合,用起来就更舒服了,我就是在更换了一个劣质防火墙,ftp无法正常使用的情况下想出这个怪招儿的:-)。

Update @ 2007-07-31

如果在windows主机上使用本方法,而svn服务器是采用了ssl的https://…地址,那么会遇到一点小麻烦,就是在执行svn update的时候,由于使用的是web的用户,在出现确认证书的提示信息时,用户是无法输入的:

Error validating server certificate for 'https://20070731.fwolf.com':
 - The certificate is not issued by a trusted authority. Use the
   fingerprint to validate the certificate manually!
Certificate information:
 - Hostname: 20070731.fwolf.com
 - Valid: from Jul 31 06:49:53 2007 GMT until Jul 28 06:49:53 2017 GMT
 - Issuer: Fwolf, US
 - Fingerprint: 38:43:0b:29:75:1t:ba:d8:29:8f:94:9a:10:42:a0:fe:ae:93:4d:91
(R)eject, accept (t)emporarily or accept (p)ermanently?

这时就只能用变通的方法了,首先在dos方式中使用svn up,svn会自动缓存身份验证以及ssl确认信息,这些信息保存在C:\Documents and Settings\Administrator\Application Data\Subversion目录下,然后把这个目录整体拷贝到C:\Documents and Settings\Default User\Application Data\Subversion就可以了,测试环境windows2003,并且试过复制到All Users的对应目录无效。

另外修改配置文件servers,添加ssl-trust-default-ca = yes的方式在windows下好像没有作用。

Update @ 2009-04-07

Git(ssh证书登录)也可以这样用,略有不同,我使用的客户端是msysgit

  • Admin用户使用桌面操作(Git Bash)时,.ssh目录位于C:\Documents and Settings\Administrator\下。
  • 直接使用C:\Program Files\Git\bin\ssh.exe当ssh客户端时,.ssh目录要放到C:\Program Files\Git\下,并且似乎忽略.ssh/config文件,私钥文件只认identity id_rsa id_dsa三个。
  • 系统服务比如Apache运行时,使用上面一条的设置,很是诡异。

说公钥文件只认固定的三个,是从这里猜到的:

	Program Files/Git$ grep identity share -R
	share/git-gui/lib/sshkey.tcl:   foreach name {~/.ssh/id_dsa.pub ~/.ssh/id_rsa.pub ~/.ssh/identity.pub} {

好像是写死了,并且我用config定义其他私钥它也不认。第一条中使用Git的时候没事,很正常。

同时,为了安全起见,apache里还要增加如下设置:

	<Directory ~ "\.(git|svn)">
		Order allow,deny
		Deny from all
		Satisfy All
	</Directory>