git-remote-testpy.pyon commit Merge branch 'jc/t1512-fix' (eb40e51)
   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
  34if sys.hexversion < 0x02000000:
  35    # string.encode() is the limiter
  36    sys.stderr.write("git-remote-testgit: requires Python 2.0 or later.\n")
  37    sys.exit(1)
  38
  39
  40def encode_filepath(path):
  41    """Encodes a Unicode file path to a byte string.
  42
  43    On Python 2 this is a no-op; on Python 3 we encode the string as
  44    suggested by [1] which allows an exact round-trip from the command line
  45    to the filesystem.
  46
  47    [1] http://docs.python.org/3/c-api/unicode.html#file-system-encoding
  48
  49    """
  50    if sys.hexversion < 0x03000000:
  51        return path
  52    return path.encode(sys.getfilesystemencoding(), 'surrogateescape')
  53
  54
  55def get_repo(alias, url):
  56    """Returns a git repository object initialized for usage.
  57    """
  58
  59    repo = GitRepo(url)
  60    repo.get_revs()
  61    repo.get_head()
  62
  63    hasher = _digest()
  64    hasher.update(encode_filepath(repo.path))
  65    repo.hash = hasher.hexdigest()
  66
  67    repo.get_base_path = lambda base: os.path.join(
  68        base, 'info', 'fast-import', repo.hash)
  69
  70    prefix = 'refs/testgit/%s/' % alias
  71    debug("prefix: '%s'", prefix)
  72
  73    repo.gitdir = os.environ["GIT_DIR"]
  74    repo.alias = alias
  75    repo.prefix = prefix
  76
  77    repo.exporter = GitExporter(repo)
  78    repo.importer = GitImporter(repo)
  79    repo.non_local = NonLocalGit(repo)
  80
  81    return repo
  82
  83
  84def local_repo(repo, path):
  85    """Returns a git repository object initalized for usage.
  86    """
  87
  88    local = GitRepo(path)
  89
  90    local.non_local = None
  91    local.gitdir = repo.gitdir
  92    local.alias = repo.alias
  93    local.prefix = repo.prefix
  94    local.hash = repo.hash
  95    local.get_base_path = repo.get_base_path
  96    local.exporter = GitExporter(local)
  97    local.importer = GitImporter(local)
  98
  99    return local
 100
 101
 102def do_capabilities(repo, args):
 103    """Prints the supported capabilities.
 104    """
 105
 106    print("import")
 107    print("export")
 108    print("refspec refs/heads/*:%s*" % repo.prefix)
 109
 110    dirname = repo.get_base_path(repo.gitdir)
 111
 112    if not os.path.exists(dirname):
 113        os.makedirs(dirname)
 114
 115    path = os.path.join(dirname, 'git.marks')
 116
 117    print("*export-marks %s" % path)
 118    if os.path.exists(path):
 119        print("*import-marks %s" % path)
 120
 121    print('') # end capabilities
 122
 123
 124def do_list(repo, args):
 125    """Lists all known references.
 126
 127    Bug: This will always set the remote head to master for non-local
 128    repositories, since we have no way of determining what the remote
 129    head is at clone time.
 130    """
 131
 132    for ref in repo.revs:
 133        debug("? refs/heads/%s", ref)
 134        print("? refs/heads/%s" % ref)
 135
 136    if repo.head:
 137        debug("@refs/heads/%s HEAD" % repo.head)
 138        print("@refs/heads/%s HEAD" % repo.head)
 139    else:
 140        debug("@refs/heads/master HEAD")
 141        print("@refs/heads/master HEAD")
 142
 143    print('') # end list
 144
 145
 146def update_local_repo(repo):
 147    """Updates (or clones) a local repo.
 148    """
 149
 150    if repo.local:
 151        return repo
 152
 153    path = repo.non_local.clone(repo.gitdir)
 154    repo.non_local.update(repo.gitdir)
 155    repo = local_repo(repo, path)
 156    return repo
 157
 158
 159def do_import(repo, args):
 160    """Exports a fast-import stream from testgit for git to import.
 161    """
 162
 163    if len(args) != 1:
 164        die("Import needs exactly one ref")
 165
 166    if not repo.gitdir:
 167        die("Need gitdir to import")
 168
 169    ref = args[0]
 170    refs = [ref]
 171
 172    while True:
 173        line = sys.stdin.readline().decode()
 174        if line == '\n':
 175            break
 176        if not line.startswith('import '):
 177            die("Expected import line.")
 178
 179        # strip of leading 'import '
 180        ref = line[7:].strip()
 181        refs.append(ref)
 182
 183    print("feature done")
 184
 185    if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
 186        die('Told to fail')
 187
 188    repo = update_local_repo(repo)
 189    repo.exporter.export_repo(repo.gitdir, refs)
 190
 191    print("done")
 192
 193
 194def do_export(repo, args):
 195    """Imports a fast-import stream from git to testgit.
 196    """
 197
 198    if not repo.gitdir:
 199        die("Need gitdir to export")
 200
 201    if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
 202        die('Told to fail')
 203
 204    update_local_repo(repo)
 205    changed = repo.importer.do_import(repo.gitdir)
 206
 207    if not repo.local:
 208        repo.non_local.push(repo.gitdir)
 209
 210    for ref in changed:
 211        print("ok %s" % ref)
 212    print('')
 213
 214
 215COMMANDS = {
 216    'capabilities': do_capabilities,
 217    'list': do_list,
 218    'import': do_import,
 219    'export': do_export,
 220}
 221
 222
 223def sanitize(value):
 224    """Cleans up the url.
 225    """
 226
 227    if value.startswith('testgit::'):
 228        value = value[9:]
 229
 230    return value
 231
 232
 233def read_one_line(repo):
 234    """Reads and processes one command.
 235    """
 236
 237    sleepy = os.environ.get("GIT_REMOTE_TESTGIT_SLEEPY")
 238    if sleepy:
 239        debug("Sleeping %d sec before readline" % int(sleepy))
 240        time.sleep(int(sleepy))
 241
 242    line = sys.stdin.readline()
 243
 244    cmdline = line.decode()
 245
 246    if not cmdline:
 247        warn("Unexpected EOF")
 248        return False
 249
 250    cmdline = cmdline.strip().split()
 251    if not cmdline:
 252        # Blank line means we're about to quit
 253        return False
 254
 255    cmd = cmdline.pop(0)
 256    debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline))
 257
 258    if cmd not in COMMANDS:
 259        die("Unknown command, %s", cmd)
 260
 261    func = COMMANDS[cmd]
 262    func(repo, cmdline)
 263    sys.stdout.flush()
 264
 265    return True
 266
 267
 268def main(args):
 269    """Starts a new remote helper for the specified repository.
 270    """
 271
 272    if len(args) != 3:
 273        die("Expecting exactly three arguments.")
 274        sys.exit(1)
 275
 276    if os.getenv("GIT_DEBUG_TESTGIT"):
 277        import git_remote_helpers.util
 278        git_remote_helpers.util.DEBUG = True
 279
 280    alias = sanitize(args[1])
 281    url = sanitize(args[2])
 282
 283    if not alias.isalnum():
 284        warn("non-alnum alias '%s'", alias)
 285        alias = "tmp"
 286
 287    args[1] = alias
 288    args[2] = url
 289
 290    repo = get_repo(alias, url)
 291
 292    debug("Got arguments %s", args[1:])
 293
 294    more = True
 295
 296    # Use binary mode since Python 3 does not permit unbuffered I/O in text
 297    # mode.  Unbuffered I/O is required to avoid data that should be going
 298    # to git-fast-import after an "export" command getting caught in our
 299    # stdin buffer instead.
 300    sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)
 301    while (more):
 302        more = read_one_line(repo)
 303
 304if __name__ == '__main__':
 305    sys.exit(main(sys.argv))