| #!/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)) |