複数のgitのレポジトリを巡回するスクリプト

ここ最近perl界隈の人たちもgithubに移行してきてて、watchしてるプロジェクトが大幅に増えてくると、githubのtimelineを全部みることもできないし、リポジトリの更新をサボりがちだったのですが、各プロジェクトを巡回してupdateだけ取ってくるスクリプトを書いてみてここ数日試してる感じだとなかなか快適なので、久しぶりにBlogのネタにでもしてみる。

あるディレクトリ以下に、gitのレポジトリを集めまくっている状況で、

ruby git-update.rb

とかいうように実行すると、各リポジトリに対して、git remote update(あるいはgit svn fetch)してくれるというだけのスクリプトです。

git pullとかしないのは、自分の変更とかが途中になってたとか、トピックブランチをチェックアウトしてあった状態でうっかりmergeされるのが嫌なためです。変更があったやつのうち、気になったリポジトリへcdして、

git whatchanged origin/master@{1}..origin/master --reverse --stat

みたいな感じで前回の更新からの変更ログをみて、さらに知りたければ、

git whatchanged origin/master@{1}..origin/master --reverse --stat -p

でコードを読んで、特に問題なければ、git rebase origin/masterしたりしています。(git whatchanged oigin/master@{1}..origin/master --reverse --stat をwtというaliasにしてます。これのorigin/masterのところを任意にできるaliasの定義方法知ってる人がいたら知りたいかも。)

bzrとか、hgとかにも対応しても便利そうですが、今のところあまり巡回してるのはないので個人的にはこれで事足りています。

gitのレポジトリはほとんどgithubなのでアクセスが集中してしまうのがどうかなと思いつつ、いちおうsleep1くらいはするようにしています。

ソースはこちらから。ruby1.9でしか動かしてませんが、ruby1.8.6以降であれば動くと思います。

require 'pathname'
require 'logger'
require 'thread'
require 'open3'
 
class App
    def initialize args
        @logger = args[:logger] or raise "no logger"
        @ignore = args[:ignore] || []
    end
 
    def run base
        targets = Pathname.glob("#{base}/**/.git").sort
        threads = []
        targets.each do |dir|
            next if @ignore.any? {|i| i =~ dir.to_s }
 
            threads << Thread.new do |t|
                @logger.debug("start update #{dir}")
                git_update dir.parent.realpath
                @logger.debug("end update #{dir}")
            end
 
            sleep 1 # stop too many access to github.
 
            # for avoiding fork bomb
            if threads.length >= 10
                while th = threads.pop
                    th.join
                end
            end
        end
 
        threads.each {|th| th.join }
    end
 
    private
 
    def git_update dir
      git_cmd = "git --git-dir=#{dir}/.git"
      Open3.popen3 "#{git_cmd} remote update && #{git_cmd} config --get-regexp svn-remote > /dev/null && #{git_cmd} svn fetch" do |stdin, stdout, stderr|
        stdout.each_line do |line|
            if line =~ /^Updating / # 必ず出るので特別扱い。
                @logger.debug("#{dir}: #{line}")
            else
                @logger.info("#{dir}: #{line}")
            end
        end
        stderr.each_line do |line|
            @logger.error("#{dir}: #{line}")
        end
      end
    end
end
 
def main
    logger = Logger.new($stderr)
    logger.level = Logger::INFO
    app = App.new(:logger => logger, :ignore => [ %r{ruby/(pathobserver|rubycocoa)} ])
    app.run Pathname(__FILE__).parent.realpath
end
 
main()

http://gist.github.com/199109

補足

sshパスフレーズをなるべく何度も設定していなくてもよい設定にするのが継続してチェックするポイントかなと思います。

なので自分は、cloneするurlは、gitプロトコルを利用するもの、pushするurlはsshを利用するものに設定してます。

git configだとこんな感じ。

[remote "origin"]
        url = git://github.com/nekokak/p5-dbix-skinny.git
        pushurl = git@github.com:nekokak/p5-dbix-skinny.git
        fetch = +refs/heads/*:refs/remotes/origin/*

入門Gitで紹介されてて気づきました。RelNotesによると、git 1.6.4とのことなので結構新しめのを入れてないとこの機能はないかもしれません。

他の方法としては、自分はあまり利用したことないのですが、SSHKeychainなどを使うと入力が何度もしなくてもよいようです。(git-svnsshでの書き込みしか公開されていないsvnリポジトリではこの方法しかないかも)

追記

ちょっと、接続先のサーバーの応答が悪いと、thread & systemの組合せで、fork bombっぽくなってしまうので、ちょっと対策入れてみました。もし使ってる人がいたらupdateをおすすめします。