| #!/usr/bin/env python | 
 |  | 
 | # This command is a simple remote-helper, that is used both as a | 
 | # testcase for the remote-helper functionality, and as an example to | 
 | # show remote-helper authors one possible implementation. | 
 | # | 
 | # This is a Git <-> Git importer/exporter, that simply uses git | 
 | # fast-import and git fast-export to consume and produce fast-import | 
 | # streams. | 
 | # | 
 | # To understand better the way things work, one can activate debug | 
 | # traces by setting (to any value) the environment variables | 
 | # GIT_TRANSPORT_HELPER_DEBUG and GIT_DEBUG_TESTGIT, to see messages | 
 | # from the transport-helper side, or from this example remote-helper. | 
 |  | 
 | # hashlib is only available in python >= 2.5 | 
 | try: | 
 |     import hashlib | 
 |     _digest = hashlib.sha1 | 
 | except ImportError: | 
 |     import sha | 
 |     _digest = sha.new | 
 | import sys | 
 | import os | 
 | import time | 
 | sys.path.insert(0, os.getenv("GITPYTHONLIB",".")) | 
 |  | 
 | from git_remote_helpers.util import die, debug, warn | 
 | from git_remote_helpers.git.repo import GitRepo | 
 | from git_remote_helpers.git.exporter import GitExporter | 
 | from git_remote_helpers.git.importer import GitImporter | 
 | from git_remote_helpers.git.non_local import NonLocalGit | 
 |  | 
 | if sys.hexversion < 0x02000000: | 
 |     # string.encode() is the limiter | 
 |     sys.stderr.write("git-remote-testgit: requires Python 2.0 or later.\n") | 
 |     sys.exit(1) | 
 |  | 
 |  | 
 | def encode_filepath(path): | 
 |     """Encodes a Unicode file path to a byte string. | 
 |  | 
 |     On Python 2 this is a no-op; on Python 3 we encode the string as | 
 |     suggested by [1] which allows an exact round-trip from the command line | 
 |     to the filesystem. | 
 |  | 
 |     [1] http://docs.python.org/3/c-api/unicode.html#file-system-encoding | 
 |  | 
 |     """ | 
 |     if sys.hexversion < 0x03000000: | 
 |         return path | 
 |     return path.encode(sys.getfilesystemencoding(), 'surrogateescape') | 
 |  | 
 |  | 
 | def get_repo(alias, url): | 
 |     """Returns a git repository object initialized for usage. | 
 |     """ | 
 |  | 
 |     repo = GitRepo(url) | 
 |     repo.get_revs() | 
 |     repo.get_head() | 
 |  | 
 |     hasher = _digest() | 
 |     hasher.update(encode_filepath(repo.path)) | 
 |     repo.hash = hasher.hexdigest() | 
 |  | 
 |     repo.get_base_path = lambda base: os.path.join( | 
 |         base, 'info', 'fast-import', repo.hash) | 
 |  | 
 |     prefix = 'refs/testgit/%s/' % alias | 
 |     debug("prefix: '%s'", prefix) | 
 |  | 
 |     repo.gitdir = os.environ["GIT_DIR"] | 
 |     repo.alias = alias | 
 |     repo.prefix = prefix | 
 |  | 
 |     repo.exporter = GitExporter(repo) | 
 |     repo.importer = GitImporter(repo) | 
 |     repo.non_local = NonLocalGit(repo) | 
 |  | 
 |     return repo | 
 |  | 
 |  | 
 | def local_repo(repo, path): | 
 |     """Returns a git repository object initalized for usage. | 
 |     """ | 
 |  | 
 |     local = GitRepo(path) | 
 |  | 
 |     local.non_local = None | 
 |     local.gitdir = repo.gitdir | 
 |     local.alias = repo.alias | 
 |     local.prefix = repo.prefix | 
 |     local.hash = repo.hash | 
 |     local.get_base_path = repo.get_base_path | 
 |     local.exporter = GitExporter(local) | 
 |     local.importer = GitImporter(local) | 
 |  | 
 |     return local | 
 |  | 
 |  | 
 | def do_capabilities(repo, args): | 
 |     """Prints the supported capabilities. | 
 |     """ | 
 |  | 
 |     print("import") | 
 |     print("export") | 
 |     print("refspec refs/heads/*:%s*" % repo.prefix) | 
 |  | 
 |     dirname = repo.get_base_path(repo.gitdir) | 
 |  | 
 |     if not os.path.exists(dirname): | 
 |         os.makedirs(dirname) | 
 |  | 
 |     path = os.path.join(dirname, 'git.marks') | 
 |  | 
 |     print("*export-marks %s" % path) | 
 |     if os.path.exists(path): | 
 |         print("*import-marks %s" % path) | 
 |  | 
 |     print('') # end capabilities | 
 |  | 
 |  | 
 | def do_list(repo, args): | 
 |     """Lists all known references. | 
 |  | 
 |     Bug: This will always set the remote head to master for non-local | 
 |     repositories, since we have no way of determining what the remote | 
 |     head is at clone time. | 
 |     """ | 
 |  | 
 |     for ref in repo.revs: | 
 |         debug("? refs/heads/%s", ref) | 
 |         print("? refs/heads/%s" % ref) | 
 |  | 
 |     if repo.head: | 
 |         debug("@refs/heads/%s HEAD" % repo.head) | 
 |         print("@refs/heads/%s HEAD" % repo.head) | 
 |     else: | 
 |         debug("@refs/heads/master HEAD") | 
 |         print("@refs/heads/master HEAD") | 
 |  | 
 |     print('') # end list | 
 |  | 
 |  | 
 | def update_local_repo(repo): | 
 |     """Updates (or clones) a local repo. | 
 |     """ | 
 |  | 
 |     if repo.local: | 
 |         return repo | 
 |  | 
 |     path = repo.non_local.clone(repo.gitdir) | 
 |     repo.non_local.update(repo.gitdir) | 
 |     repo = local_repo(repo, path) | 
 |     return repo | 
 |  | 
 |  | 
 | def do_import(repo, args): | 
 |     """Exports a fast-import stream from testgit for git to import. | 
 |     """ | 
 |  | 
 |     if len(args) != 1: | 
 |         die("Import needs exactly one ref") | 
 |  | 
 |     if not repo.gitdir: | 
 |         die("Need gitdir to import") | 
 |  | 
 |     ref = args[0] | 
 |     refs = [ref] | 
 |  | 
 |     while True: | 
 |         line = sys.stdin.readline().decode() | 
 |         if line == '\n': | 
 |             break | 
 |         if not line.startswith('import '): | 
 |             die("Expected import line.") | 
 |  | 
 |         # strip of leading 'import ' | 
 |         ref = line[7:].strip() | 
 |         refs.append(ref) | 
 |  | 
 |     print("feature done") | 
 |  | 
 |     if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"): | 
 |         die('Told to fail') | 
 |  | 
 |     repo = update_local_repo(repo) | 
 |     repo.exporter.export_repo(repo.gitdir, refs) | 
 |  | 
 |     print("done") | 
 |  | 
 |  | 
 | def do_export(repo, args): | 
 |     """Imports a fast-import stream from git to testgit. | 
 |     """ | 
 |  | 
 |     if not repo.gitdir: | 
 |         die("Need gitdir to export") | 
 |  | 
 |     if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"): | 
 |         die('Told to fail') | 
 |  | 
 |     update_local_repo(repo) | 
 |     changed = repo.importer.do_import(repo.gitdir) | 
 |  | 
 |     if not repo.local: | 
 |         repo.non_local.push(repo.gitdir) | 
 |  | 
 |     for ref in changed: | 
 |         print("ok %s" % ref) | 
 |     print('') | 
 |  | 
 |  | 
 | COMMANDS = { | 
 |     'capabilities': do_capabilities, | 
 |     'list': do_list, | 
 |     'import': do_import, | 
 |     'export': do_export, | 
 | } | 
 |  | 
 |  | 
 | def sanitize(value): | 
 |     """Cleans up the url. | 
 |     """ | 
 |  | 
 |     if value.startswith('testgit::'): | 
 |         value = value[9:] | 
 |  | 
 |     return value | 
 |  | 
 |  | 
 | def read_one_line(repo): | 
 |     """Reads and processes one command. | 
 |     """ | 
 |  | 
 |     sleepy = os.environ.get("GIT_REMOTE_TESTGIT_SLEEPY") | 
 |     if sleepy: | 
 |         debug("Sleeping %d sec before readline" % int(sleepy)) | 
 |         time.sleep(int(sleepy)) | 
 |  | 
 |     line = sys.stdin.readline() | 
 |  | 
 |     cmdline = line.decode() | 
 |  | 
 |     if not cmdline: | 
 |         warn("Unexpected EOF") | 
 |         return False | 
 |  | 
 |     cmdline = cmdline.strip().split() | 
 |     if not cmdline: | 
 |         # Blank line means we're about to quit | 
 |         return False | 
 |  | 
 |     cmd = cmdline.pop(0) | 
 |     debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline)) | 
 |  | 
 |     if cmd not in COMMANDS: | 
 |         die("Unknown command, %s", cmd) | 
 |  | 
 |     func = COMMANDS[cmd] | 
 |     func(repo, cmdline) | 
 |     sys.stdout.flush() | 
 |  | 
 |     return True | 
 |  | 
 |  | 
 | def main(args): | 
 |     """Starts a new remote helper for the specified repository. | 
 |     """ | 
 |  | 
 |     if len(args) != 3: | 
 |         die("Expecting exactly three arguments.") | 
 |         sys.exit(1) | 
 |  | 
 |     if os.getenv("GIT_DEBUG_TESTGIT"): | 
 |         import git_remote_helpers.util | 
 |         git_remote_helpers.util.DEBUG = True | 
 |  | 
 |     alias = sanitize(args[1]) | 
 |     url = sanitize(args[2]) | 
 |  | 
 |     if not alias.isalnum(): | 
 |         warn("non-alnum alias '%s'", alias) | 
 |         alias = "tmp" | 
 |  | 
 |     args[1] = alias | 
 |     args[2] = url | 
 |  | 
 |     repo = get_repo(alias, url) | 
 |  | 
 |     debug("Got arguments %s", args[1:]) | 
 |  | 
 |     more = True | 
 |  | 
 |     # Use binary mode since Python 3 does not permit unbuffered I/O in text | 
 |     # mode.  Unbuffered I/O is required to avoid data that should be going | 
 |     # to git-fast-import after an "export" command getting caught in our | 
 |     # stdin buffer instead. | 
 |     sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) | 
 |     while (more): | 
 |         more = read_one_line(repo) | 
 |  | 
 | if __name__ == '__main__': | 
 |     sys.exit(main(sys.argv)) |