contrib / remote-helpers / git-remote-bzron commit Add new remote-bzr transport helper (bee118e)
   1#!/usr/bin/env python
   2#
   3# Copyright (c) 2012 Felipe Contreras
   4#
   5
   6#
   7# Just copy to your ~/bin, or anywhere in your $PATH.
   8# Then you can clone with:
   9# % git clone bzr::/path/to/bzr/repo/or/url
  10#
  11# For example:
  12# % git clone bzr::$HOME/myrepo
  13# or
  14# % git clone bzr::lp:myrepo
  15#
  16
  17import sys
  18
  19import bzrlib
  20bzrlib.initialize()
  21
  22import bzrlib.plugin
  23bzrlib.plugin.load_plugins()
  24
  25import sys
  26import os
  27import json
  28import re
  29
  30NAME_RE = re.compile('^([^<>]+)')
  31AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]*)>$')
  32
  33def die(msg, *args):
  34    sys.stderr.write('ERROR: %s\n' % (msg % args))
  35    sys.exit(1)
  36
  37def warn(msg, *args):
  38    sys.stderr.write('WARNING: %s\n' % (msg % args))
  39
  40def gittz(tz):
  41    return '%+03d%02d' % (tz / 3600, tz % 3600 / 60)
  42
  43class Marks:
  44
  45    def __init__(self, path):
  46        self.path = path
  47        self.tips = {}
  48        self.marks = {}
  49        self.last_mark = 0
  50        self.load()
  51
  52    def load(self):
  53        if not os.path.exists(self.path):
  54            return
  55
  56        tmp = json.load(open(self.path))
  57        self.tips = tmp['tips']
  58        self.marks = tmp['marks']
  59        self.last_mark = tmp['last-mark']
  60
  61    def dict(self):
  62        return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
  63
  64    def store(self):
  65        json.dump(self.dict(), open(self.path, 'w'))
  66
  67    def __str__(self):
  68        return str(self.dict())
  69
  70    def from_rev(self, rev):
  71        return self.marks[rev]
  72
  73    def next_mark(self):
  74        self.last_mark += 1
  75        return self.last_mark
  76
  77    def get_mark(self, rev):
  78        self.last_mark += 1
  79        self.marks[rev] = self.last_mark
  80        return self.last_mark
  81
  82    def is_marked(self, rev):
  83        return self.marks.has_key(rev)
  84
  85    def get_tip(self, branch):
  86        return self.tips.get(branch, None)
  87
  88    def set_tip(self, branch, tip):
  89        self.tips[branch] = tip
  90
  91class Parser:
  92
  93    def __init__(self, repo):
  94        self.repo = repo
  95        self.line = self.get_line()
  96
  97    def get_line(self):
  98        return sys.stdin.readline().strip()
  99
 100    def __getitem__(self, i):
 101        return self.line.split()[i]
 102
 103    def check(self, word):
 104        return self.line.startswith(word)
 105
 106    def each_block(self, separator):
 107        while self.line != separator:
 108            yield self.line
 109            self.line = self.get_line()
 110
 111    def __iter__(self):
 112        return self.each_block('')
 113
 114    def next(self):
 115        self.line = self.get_line()
 116        if self.line == 'done':
 117            self.line = None
 118
 119def rev_to_mark(rev):
 120    global marks
 121    return marks.from_rev(rev)
 122
 123def fixup_user(user):
 124    name = mail = None
 125    user = user.replace('"', '')
 126    m = AUTHOR_RE.match(user)
 127    if m:
 128        name = m.group(1)
 129        mail = m.group(2).strip()
 130    else:
 131        m = NAME_RE.match(user)
 132        if m:
 133            name = m.group(1).strip()
 134
 135    return '%s <%s>' % (name, mail)
 136
 137def get_filechanges(cur, prev):
 138    modified = {}
 139    removed = {}
 140
 141    changes = cur.changes_from(prev)
 142
 143    for path, fid, kind in changes.added:
 144        modified[path] = fid
 145    for path, fid, kind in changes.removed:
 146        removed[path] = None
 147    for path, fid, kind, mod, _ in changes.modified:
 148        modified[path] = fid
 149    for oldpath, newpath, fid, kind, mod, _ in changes.renamed:
 150        removed[oldpath] = None
 151        modified[newpath] = fid
 152
 153    return modified, removed
 154
 155def export_files(tree, files):
 156    global marks, filenodes
 157
 158    final = []
 159    for path, fid in files.iteritems():
 160        h = tree.get_file_sha1(fid)
 161
 162        mode = '100644'
 163
 164        # is the blob already exported?
 165        if h in filenodes:
 166            mark = filenodes[h]
 167        else:
 168            d = tree.get_file_text(fid)
 169
 170            mark = marks.next_mark()
 171            filenodes[h] = mark
 172
 173            print "blob"
 174            print "mark :%u" % mark
 175            print "data %d" % len(d)
 176            print d
 177
 178        final.append((mode, mark, path))
 179
 180    return final
 181
 182def export_branch(branch, name):
 183    global prefix, dirname
 184
 185    ref = '%s/heads/%s' % (prefix, name)
 186    tip = marks.get_tip(name)
 187
 188    repo = branch.repository
 189    repo.lock_read()
 190    revs = branch.iter_merge_sorted_revisions(None, tip, 'exclude', 'forward')
 191    count = 0
 192
 193    revs = [revid for revid, _, _, _ in revs if not marks.is_marked(revid)]
 194
 195    for revid in revs:
 196
 197        rev = repo.get_revision(revid)
 198
 199        parents = rev.parent_ids
 200        time = rev.timestamp
 201        tz = rev.timezone
 202        committer = rev.committer.encode('utf-8')
 203        committer = "%s %u %s" % (fixup_user(committer), time, gittz(tz))
 204        author = committer
 205        msg = rev.message.encode('utf-8')
 206
 207        msg += '\n'
 208
 209        if len(parents) == 0:
 210            parent = bzrlib.revision.NULL_REVISION
 211        else:
 212            parent = parents[0]
 213
 214        cur_tree = repo.revision_tree(revid)
 215        prev = repo.revision_tree(parent)
 216        modified, removed = get_filechanges(cur_tree, prev)
 217
 218        modified_final = export_files(cur_tree, modified)
 219
 220        if len(parents) == 0:
 221            print 'reset %s' % ref
 222
 223        print "commit %s" % ref
 224        print "mark :%d" % (marks.get_mark(revid))
 225        print "author %s" % (author)
 226        print "committer %s" % (committer)
 227        print "data %d" % (len(msg))
 228        print msg
 229
 230        for i, p in enumerate(parents):
 231            try:
 232                m = rev_to_mark(p)
 233            except KeyError:
 234                # ghost?
 235                continue
 236            if i == 0:
 237                print "from :%s" % m
 238            else:
 239                print "merge :%s" % m
 240
 241        for f in modified_final:
 242            print "M %s :%u %s" % f
 243        for f in removed:
 244            print "D %s" % (f)
 245        print
 246
 247        count += 1
 248        if (count % 100 == 0):
 249            print "progress revision %s (%d/%d)" % (revid, count, len(revs))
 250            print "#############################################################"
 251
 252    repo.unlock()
 253
 254    revid = branch.last_revision()
 255
 256    # make sure the ref is updated
 257    print "reset %s" % ref
 258    print "from :%u" % rev_to_mark(revid)
 259    print
 260
 261    marks.set_tip(name, revid)
 262
 263def export_tag(repo, name):
 264    global tags
 265    try:
 266        print "reset refs/tags/%s" % name
 267        print "from :%u" % rev_to_mark(tags[name])
 268        print
 269    except KeyError:
 270        warn("TODO: fetch tag '%s'" % name)
 271
 272def do_import(parser):
 273    global dirname
 274
 275    branch = parser.repo
 276    path = os.path.join(dirname, 'marks-git')
 277
 278    print "feature done"
 279    if os.path.exists(path):
 280        print "feature import-marks=%s" % path
 281    print "feature export-marks=%s" % path
 282    sys.stdout.flush()
 283
 284    while parser.check('import'):
 285        ref = parser[1]
 286        if ref.startswith('refs/heads/'):
 287            name = ref[len('refs/heads/'):]
 288            export_branch(branch, name)
 289        if ref.startswith('refs/tags/'):
 290            name = ref[len('refs/tags/'):]
 291            export_tag(branch, name)
 292        parser.next()
 293
 294    print 'done'
 295
 296    sys.stdout.flush()
 297
 298def do_capabilities(parser):
 299    print "import"
 300    print "refspec refs/heads/*:%s/heads/*" % prefix
 301    print
 302
 303def do_list(parser):
 304    global tags
 305    print "? refs/heads/%s" % 'master'
 306    for tag, revid in parser.repo.tags.get_tag_dict().items():
 307        print "? refs/tags/%s" % tag
 308        tags[tag] = revid
 309    print "@refs/heads/%s HEAD" % 'master'
 310    print
 311
 312def get_repo(url, alias):
 313    origin = bzrlib.controldir.ControlDir.open(url)
 314    return origin.open_branch()
 315
 316def main(args):
 317    global marks, prefix, dirname
 318    global tags, filenodes
 319
 320    alias = args[1]
 321    url = args[2]
 322
 323    prefix = 'refs/bzr/%s' % alias
 324    tags = {}
 325    filenodes = {}
 326
 327    gitdir = os.environ['GIT_DIR']
 328    dirname = os.path.join(gitdir, 'bzr', alias)
 329
 330    if not os.path.exists(dirname):
 331        os.makedirs(dirname)
 332
 333    repo = get_repo(url, alias)
 334
 335    marks_path = os.path.join(dirname, 'marks-int')
 336    marks = Marks(marks_path)
 337
 338    parser = Parser(repo)
 339    for line in parser:
 340        if parser.check('capabilities'):
 341            do_capabilities(parser)
 342        elif parser.check('list'):
 343            do_list(parser)
 344        elif parser.check('import'):
 345            do_import(parser)
 346        else:
 347            die('unhandled command: %s' % line)
 348        sys.stdout.flush()
 349
 350    marks.store()
 351
 352sys.exit(main(sys.argv))