手元のgitリポジトリ一覧をgithubのwatched reposとsyncする

先日書いた、複数のgitのレポジトリを巡回するスクリプトは結構便利なのだけど、「その日にwatchしたプロジェクトとかはcloneしてこないとupdateをチェックできない」という欠点があって、IRCとかで話題になって興味を持って、家に帰ったらチェックしようと思ってwatchにしておいたプロジェクトを、手でcloneしてくるとかいうふうにしていたのですが、watchにしておいてcloneし忘れることもままあります。

ということでAPI使ってwatchの情報をみて実行したディレクトリ以下に「プロジェクト名/.git」というディレクトリがなければ、cloneしてくるスクリプトを書いた。

環境によっては、jsonだけgemとかで入れる必要があるかも。

require 'uri'
require 'pathname'
require 'logger'
require 'net/https'
 
class API
    API_BASE = URI("http://github.com/api/v2")
 
    def initialize
    end
 
    def get_type type, path
        uri = get_type_uri type, path
        http_client = Net::HTTP.new(uri.host, uri.port)
        res = nil
        http_client.start do
            res = http_client.get(uri.path)
        end
 
        return res.body unless res.nil?
    end
 
    def get_type_uri type, path
        URI("#{API_BASE.to_s}/#{type}/#{path}")
    end
 
    def json path
        res = get_type 'json', path
        require 'json'
        JSON.parse(res)
    end
end
 
Repository = Struct.new("Repository", :name, :owner, :url)
 
class Repository
    def self.from_hash hash
        args = self.members.map {|s| hash[s.to_s] }
        self.new(*args)
    end
 
    def clone_url
        "git://github.com/#{self.owner}/#{self.name}.git"
    end
 
    def push_url
        "git@github.com:#{self.owner}/#{self.name}.git"
    end
 
    # FIXME: how to get collaborator infomation?
    def can_push? owner
        return true if self.owner == owner
    end
end
 
class App
    def initialize args
        @user = args[:user]
        @logger = args[:logger]
        @client = API.new
        @basedir = Pathname('.').realpath
    end
 
    def run
        gitrepos = Pathname.glob("#{@basedir}/**/.git")
 
        json = @client.json("repos/watched/#@user")
        threads = []
        json['repositories'].each do |repo|
            modified_fg = false
            repos = Repository.from_hash(repo)
 
            target_dir = gitrepos.find {|i| i.to_s =~ %r|/#{repos.name}/\.git$| }
            unless target_dir
                target_dir = repos.name
                cmd = "git init #{repos.name}"
                system_with_log(cmd)
                modified_fg = true
            end
 
            modified_fg = git_config_set_safe(target_dir, "remote.#{repos.owner}.url", repos.clone_url)
            if repos.can_push?(@user)
                modified_fg = git_config_set_safe(target_dir, "remote.#{repos.owner}.pushurl", repos.push_url)
            end
 
            if modified_fg
                system_with_log("git --git-dir=#{target_dir} remote update")
            end
        end
    end
 
    def git_config target_dir, key, val=nil
        config_cmd = "git --git-dir=#{target_dir} config #{key}"
 
        if val
            return system_with_log("#{config_cmd} #{val}")
        else
            return system("#{config_cmd} > /dev/null") # no logged for checking.
        end
    end
 
    def git_config_set_safe target_dir, key, val
        unless git_config(target_dir, key)
            return git_config(target_dir, key, val)
        end
 
        return false
    end
 
    def system_with_log cmd
        @logger.debug("execute: #{cmd}")
        system(cmd)
    end
end
 
def main
    app = App.new :user => 'walf443', :logger => Logger.new($stderr)
    app.run
end
 
main()

http://gist.github.com/209622

まだいくつか不満はあって、

  • 自分はcodereposっぽく、~/project/#{lang}/#{project}みたいな構成にしているので、言語を推定してそのディレクトリ以下にcloneしてほしい
  • プロジェクト名がp5-xxxxxだったら、p5をとりのぞいて、xxxxxという名前でcloneしてほしいとか、プロジェクト名は、CamelCaseがのぞましいとか。
  • collaboratorに入れられているプロジェクトだと、pushurlを設定してほしいけど、repos/watchedのAPIからはそれっぽい情報がとれない
  • 複数のforkされたプロジェクトをwatchしている場合は1つのリポジトリに複数のremoteを登録してほしい 対応しました
  • unwatchした場合はどうしよう。。。

とかいった感じですが、ちょっと泥くさくなりそうではあるので、やや手抜きなところもありますが、このあたりが落しどころな気もする。