git-remote-testgit.pyon commit Merge branch 'rs/unpack-trees-leakfix' into maint (485cfd1)
   1#!/usr/bin/env python
   2
   3# This command is a simple remote-helper, that is used both as a
   4# testcase for the remote-helper functionality, and as an example to
   5# show remote-helper authors one possible implementation.
   6#
   7# This is a Git <-> Git importer/exporter, that simply uses git
   8# fast-import and git fast-export to consume and produce fast-import
   9# streams.
  10#
  11# To understand better the way things work, one can activate debug
  12# traces by setting (to any value) the environment variables
  13# GIT_TRANSPORT_HELPER_DEBUG and GIT_DEBUG_TESTGIT, to see messages
  14# from the transport-helper side, or from this example remote-helper.
  15
  16# hashlib is only available in python >= 2.5
  17try:
  18    import hashlib
  19    _digest = hashlib.sha1
  20except ImportError:
  21    import sha
  22    _digest = sha.new
  23import sys
  24import os
  25import time
  26sys.path.insert(0, os.getenv("GITPYTHONLIB","."))
  27
  28from git_remote_helpers.util import die, debug, warn
  29from git_remote_helpers.git.repo import GitRepo
  30from git_remote_helpers.git.exporter import GitExporter
  31from git_remote_helpers.git.importer import GitImporter
  32from git_remote_helpers.git.non_local import NonLocalGit
  33
  34def get_repo(alias, url):
  35    """Returns a git repository object initialized for usage.
  36    """
  37
  38    repo = GitRepo(url)
  39    repo.get_revs()
  40    repo.get_head()
  41
  42    hasher = _digest()
  43    hasher.update(repo.path)
  44    repo.hash = hasher.hexdigest()
  45
  46    repo.get_base_path = lambda base: os.path.join(
  47        base, 'info', 'fast-import', repo.hash)
  48
  49    prefix = 'refs/testgit/%s/' % alias
  50    debug("prefix: '%s'", prefix)
  51
  52    repo.gitdir = os.environ["GIT_DIR"]
  53    repo.alias = alias
  54    repo.prefix = prefix
  55
  56    repo.exporter = GitExporter(repo)
  57    repo.importer = GitImporter(repo)
  58    repo.non_local = NonLocalGit(repo)
  59
  60    return repo
  61
  62
  63def local_repo(repo, path):
  64    """Returns a git repository object initalized for usage.
  65    """
  66
  67    local = GitRepo(path)
  68
  69    local.non_local = None
  70    local.gitdir = repo.gitdir
  71    local.alias = repo.alias
  72    local.prefix = repo.prefix
  73    local.hash = repo.hash
  74    local.get_base_path = repo.get_base_path
  75    local.exporter = GitExporter(local)
  76    local.importer = GitImporter(local)
  77
  78    return local
  79
  80
  81def do_capabilities(repo, args):
  82    """Prints the supported capabilities.
  83    """
  84
  85    print "import"
  86    print "export"
  87    print "refspec refs/heads/*:%s*" % repo.prefix
  88
  89    dirname = repo.get_base_path(repo.gitdir)
  90
  91    if not os.path.exists(dirname):
  92        os.makedirs(dirname)
  93
  94    path = os.path.join(dirname, 'testgit.marks')
  95
  96    print "*export-marks %s" % path
  97    if os.path.exists(path):
  98        print "*import-marks %s" % path
  99
 100    print # end capabilities
 101
 102
 103def do_list(repo, args):
 104    """Lists all known references.
 105
 106    Bug: This will always set the remote head to master for non-local
 107    repositories, since we have no way of determining what the remote
 108    head is at clone time.
 109    """
 110
 111    for ref in repo.revs:
 112        debug("? refs/heads/%s", ref)
 113        print "? refs/heads/%s" % ref
 114
 115    if repo.head:
 116        debug("@refs/heads/%s HEAD" % repo.head)
 117        print "@refs/heads/%s HEAD" % repo.head
 118    else:
 119        debug("@refs/heads/master HEAD")
 120        print "@refs/heads/master HEAD"
 121
 122    print # end list
 123
 124
 125def update_local_repo(repo):
 126    """Updates (or clones) a local repo.
 127    """
 128
 129    if repo.local:
 130        return repo
 131
 132    path = repo.non_local.clone(repo.gitdir)
 133    repo.non_local.update(repo.gitdir)
 134    repo = local_repo(repo, path)
 135    return repo
 136
 137
 138def do_import(repo, args):
 139    """Exports a fast-import stream from testgit for git to import.
 140    """
 141
 142    if len(args) != 1:
 143        die("Import needs exactly one ref")
 144
 145    if not repo.gitdir:
 146        die("Need gitdir to import")
 147
 148    ref = args[0]
 149    refs = [ref]
 150
 151    while True:
 152        line = sys.stdin.readline()
 153        if line == '\n':
 154            break
 155        if not line.startswith('import '):
 156            die("Expected import line.")
 157
 158        # strip of leading 'import '
 159        ref = line[7:].strip()
 160        refs.append(ref)
 161
 162    repo = update_local_repo(repo)
 163    repo.exporter.export_repo(repo.gitdir, refs)
 164
 165    print "done"
 166
 167
 168def do_export(repo, args):
 169    """Imports a fast-import stream from git to testgit.
 170    """
 171
 172    if not repo.gitdir:
 173        die("Need gitdir to export")
 174
 175    update_local_repo(repo)
 176    changed = repo.importer.do_import(repo.gitdir)
 177
 178    if not repo.local:
 179        repo.non_local.push(repo.gitdir)
 180
 181    for ref in changed:
 182        print "ok %s" % ref
 183    print
 184
 185
 186COMMANDS = {
 187    'capabilities': do_capabilities,
 188    'list': do_list,
 189    'import': do_import,
 190    'export': do_export,
 191}
 192
 193
 194def sanitize(value):
 195    """Cleans up the url.
 196    """
 197
 198    if value.startswith('testgit::'):
 199        value = value[9:]
 200
 201    return value
 202
 203
 204def read_one_line(repo):
 205    """Reads and processes one command.
 206    """
 207
 208    sleepy = os.environ.get("GIT_REMOTE_TESTGIT_SLEEPY")
 209    if sleepy:
 210        debug("Sleeping %d sec before readline" % int(sleepy))
 211        time.sleep(int(sleepy))
 212
 213    line = sys.stdin.readline()
 214
 215    cmdline = line
 216
 217    if not cmdline:
 218        warn("Unexpected EOF")
 219        return False
 220
 221    cmdline = cmdline.strip().split()
 222    if not cmdline:
 223        # Blank line means we're about to quit
 224        return False
 225
 226    cmd = cmdline.pop(0)
 227    debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline))
 228
 229    if cmd not in COMMANDS:
 230        die("Unknown command, %s", cmd)
 231
 232    func = COMMANDS[cmd]
 233    func(repo, cmdline)
 234    sys.stdout.flush()
 235
 236    return True
 237
 238
 239def main(args):
 240    """Starts a new remote helper for the specified repository.
 241    """
 242
 243    if len(args) != 3:
 244        die("Expecting exactly three arguments.")
 245        sys.exit(1)
 246
 247    if os.getenv("GIT_DEBUG_TESTGIT"):
 248        import git_remote_helpers.util
 249        git_remote_helpers.util.DEBUG = True
 250
 251    alias = sanitize(args[1])
 252    url = sanitize(args[2])
 253
 254    if not alias.isalnum():
 255        warn("non-alnum alias '%s'", alias)
 256        alias = "tmp"
 257
 258    args[1] = alias
 259    args[2] = url
 260
 261    repo = get_repo(alias, url)
 262
 263    debug("Got arguments %s", args[1:])
 264
 265    more = True
 266
 267    sys.stdin = os.fdopen(sys.stdin.fileno(), 'r', 0)
 268    while (more):
 269        more = read_one_line(repo)
 270
 271if __name__ == '__main__':
 272    sys.exit(main(sys.argv))