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