2

我确信这属于“将 Git 用于非预期目的”类别,但我想分享我正在做的事情,以便专家们对此发表评论。

我在我的实时服务器上使用虚拟主机,这样我就可以同时运行我的网站的 2 个版本。一个是 example.com,另一个是 staging.example.com。staging 站点用于测试新功能,我创建了一种将站点 git repo 的两个分支(例如stagingmaster)链接到它们各自站点根目录的方法。

首先,我在远程服务器上设置了 git,这样当我推送时,我可以自动将最新的 master 签出到 web 根目录(使用这个伟大的技术)。

然后,在我的接收后挂钩中,我放了这个:

#!/bin/sh
GIT_WORK_TREE=/var/www git checkout master -f
GIT_WORK_TREE=/path/to/staging/site/webroot git checkout staging -f

使用这种方法,我可以使用 git 中的两个分支来保持站点的两个版本运行,并且当我推送时,暂存站点会使用暂存分支中的任何新更改进行更新,并且与主站点相同。

我发现这是在公开新功能之前管理演示新功能的好方法。

我不应该这样做吗?有没有更好的办法?其他想法或顾虑?

谢谢,杰森

4

2 回答 2

4

在我看来,您使用 Git 的方式应该是 Git。您有相同文件的两个版本,并且您希望将每个“版本”分别维护为一个分支。

我假设当您准备好合并从 staging 到 master 的更改时,您只需在分支内执行git merge staging/之类的操作,然后从那里开始。git rebase stagingmaster

您根本没有错误地使用 Git。

于 2012-04-07T22:01:40.517 回答
3

你想知道如何变得更漂亮。我将通过 post-receive 电子邮件挂钩脚本来展示它是如何工作的(部分)。这会变得很长!:-)

好的,所以,这是post-receieve-email(重新格式化了一下)的关键部分:

while read oldrev newrev refname; do
    prep_for_email $oldrev $newrev $refname || continue
    generate_email $maxlines | send_mail
done

这会从输入流中读取 old-SHA、new-SHA 和 ref 名称(我认为这是 git 的失败之一:您只能读取此流一次,然后它就消失了;这使得连接一堆原本不相关的钩子过度困难)并调用两个 shell 函数来检查它们,然后忽略每个函数,或者对它们做一些事情。

现在这里是prep_for_emailshell 函数的关键部分,带有注释:

prep_for_email()
{
    oldrev=$(git rev-parse $1)
    newrev=$(git rev-parse $2)
    refname="$3"

此处的 rev-parse(加上我在上面放置的一些允许命令行参数的代码)允许您提供诸如“HEAD”和“HEAD^”之类的东西 rev 名称。实际的接收后挂钩始终获取原始 SHA1,因此 rev-parse 调用是无操作的。

    # --- Interpret
    # 0000->1234 (create)
    # 1234->2345 (update)
    # 2345->0000 (delete)
    if expr "$oldrev" : '0*$' >/dev/null
    then
        change_type="create"
    else
        if expr "$newrev" : '0*$' >/dev/null
        then
            change_type="delete"
        else
            change_type="update"
        fi
    fi

在 post-receive 钩子中,如果“旧”SHA1 是0000000000000000000000000000000000000000(全零,即 40 个 0;expr上面只是检查全零),这意味着“ref”参数以前不存在,现在存在。这通常是一个分支创建操作,但也可以是一个标签创建。另一方面,如果“新”SHA1 全部为零,则“ref”参数之前确实存在,现在不存在:通常是分支删除操作。其他任何东西,用于解析旧版本的引用名称现在解析为新版本。这通常是分支更新,但也可以是例如标签移动。

接下来,电子邮件挂钩有一些很好的“偏执狂风格”编程,交叉检查 ref-name 与 git repo 中的实际底层对象类型:

    # --- Get the revision types
    newrev_type=$(git cat-file -t $newrev 2> /dev/null)
    oldrev_type=$(git cat-file -t "$oldrev" 2> /dev/null)
    case "$change_type" in
    create|update)
        rev="$newrev"
        rev_type="$newrev_type"
        ;;
    delete)
        rev="$oldrev"
        rev_type="$oldrev_type"
        ;;
    esac

如果 ref-name 具有表单refs/tags/*并且更新是针对带注释的标记,则应该将$oldrev_type和都设置$newrev_typetag。(如果它是一个轻量级标签,它们都将是commit。如果一个轻量级标签变成一个带注释的标签,旧类型将是提交,新类型将是标签,等等。)当然,如果你要删除一个分支或一个标签,“新” rev 将全为 0,并且$newrev_type将是空字符串,因为git cat-file -t它将简单地失败(这就是为什么2> /dev/null)。

(除此之外:没有充分的理由git cat-file -t引用它的论点,而一个没有。可能只是有人在遇到通常的空字符串参数问题后对引用过于满意,但错过了一个。幸运的是,在这种情况下,无论哪种方式都是无害的. :-) )

然后电子邮件脚本有一个很长的case语句:

case "$refname","$rev_type" in
    ...
esac

这确保了一个操作refs/heads/*始终是一个commit. 如果没有,它将向 stderr 打印一条消息(在 a 上git push,该消息将发送给进行推送的任何人,前缀为remote:,以便有人可以看到它):

    refs/heads/*,commit)
        # branch
        refname_type="branch"
        short_refname=${refname##refs/heads/}
        ;;
    ...
    *)
        # Anything else (is there anything else?)
        echo >&2 "*** Unknown type of update to $refname ($rev_type)"
        echo >&2 "***  - no email generated"
        return 1
        ;;

现在从generate_email. 查看实际脚本以了解详细信息,但是,在这种情况下,我们只真正关心对 的调用generate_email_header,以及调用 的情况generate_update_branch_email

这是标题:

generate_email_header()
{
    # --- Email (all stdout will be the email)
    # Generate header
    cat <<-EOF
    To: $recipients
    Subject: ${emailprefix}$projectdesc $refname_type $short_refname ${change_type}d. $describe
    X-Git-Refname: $refname
    X-Git-Reftype: $refname_type
    X-Git-Oldrev: $oldrev
    X-Git-Newrev: $newrev

    This is an automated email from the git hooks/post-receive script. It was
    generated because a ref change was pushed to the repository containing
    the project "$projectdesc".

    The $refname_type, $short_refname has been ${change_type}d
    EOF
}

$change_type, 在我们关心的情况下, 是update这样说的: branch zorg updated. ($describe是 的输出git describe $newrev, 或者如果它是空的 - 即没有注释标签git describe可供使用 - 只是$newrev. $recipients, $emailprefix, 并且$projectdesc来自各种可配置项。)

generate_update_branch_email中,有很多东西要计算和打印以发送电子邮件,确切地说是删除和添加了哪些提交;然后以:

    echo "Summary of changes:"
    git diff-tree --stat --summary --find-copies-harder $oldrev..$newrev

基本上,$refname形式的分支引用refs/heads/*(比如,refs/heads/zorg),已经是updated(从不是全 0 的现有$oldrev到不是全 0 的新$newrev),意味着所说的(长格式)分支名称,人们通常称为$short_refname( zorg),已移动该分支尖端。这比找出这个动作的确切含义的代码要简单得多

在您的情况下,您无需担心分支创建;您可以将其视为定期更新。您可能(或可能不)想要在删除分支时做一些特别的事情。大多数时候,您只关心分支更新……而您要做的就是,如果staging已更新,则更新您的特殊暂存副本;如果master已更新,请更新您的特殊master副本。在每种情况下,更新都将是“到新的分支提示”,这真的很容易访问。

因此,如果我们将所有这些放在一起,我们会得到以下(未经测试,但非常简单)的钩子,我已经添加了从命令行调用它的能力。为了使它更有用,当从命令行调用时,您可以指定要在任何地方签出的修订版,例如,您可以说./hookscript unused master~2 refs/heads/staging将 rev 推master~2入暂存副本区域(假设您出于某种原因想要这样做)。

#! /bin/bash
#
# handle updates to our two interesting branches, staging and master.

# function to dump given commit state to target directory
# arguments: $1 - rev; $2 - target dir
copy_to_dir() {
    GIT_WORK_TREE="$2" git checkout -q -f "$1"
}

# function to handle an update to staging branch.
# arguments: $1 - rev to check out
update_staging() {
    copy_to_dir $1 /path/to/staging/site/webroot
}

# function to handle an update to master branch.
update_master() {
    copy_to_dir $1 /var/www
}

# function to handle one reference-change.
# arguments:
#    $1 - old revision, or all-0s on create
#    $2 - new revision, or all-0s on removal
#    $3 - reference (refs/heads/*, refs/tags/*, etc)
refchange() {
    local oldrev="$1" newrev="$2" ref="$3"
    local deleted=false
    local short_revname

    if expr "$newrev" : '0*$' >/dev/null; then
        deleted=true
    elif ! git rev-parse "$newrev"; then
        return # git rev-parse already printed an error
    fi

    case $ref in
    refs/heads/staging|refs/heads/master)
        shortref=${refname#refs/heads/};;
    *)
        return;;
    esac

    # someone pushed a change to staging branch or master branch
    if $deleted; then {
        echo "WARNING: you've deleted branch $shortref"
        echo "are you sure you wanted to do that?"
        echo "The operating copy is still operating, and"
        echo "will be updated when the branch is re-created."
        } 1>&2
        return
    fi

    # update either the staging copy or the master copy
    update_$shortref "$newrev"
}

# main driver: update from input stream (if no arguments) or use arguments
case $# in
3)  refchange "$1" "$2" "$3";;
0)  while read oldrev newrev refname; do
        refchange $oldrev $newrev $refname
    done;;
*)  echo "ERROR: update hook called with $# arguments, expected 0 or 3" 1>&2;;
esac

请注意,因为我git checkout -q -f在已解析的 rev 上使用(而不是像stagingor之类的名称master),所以每次都会将推送到的分支带到“分离的 HEAD”状态。更重要的是,如果您在 Web 服务器上使用“普通”存储库中的命令行技巧,它将解开您当前的分支。这不是致命的,但很容易令人讨厌。为避免这种情况,请将 的内容替换为copy_to_dir,例如git archive $1 | (rm -rf "$2" && mkdir "$2" && cd "$2" && tar xf -).

(通常你会推送到一个--bare仓库,改变它“当前分支”的想法不是问题,因为没有人关心这个。)

于 2012-04-08T20:52:34.260 回答