Merge branch 'fc/remote-hg'
[git.git] / git-remote-testpy.py
blobca6789996adcdcb0e4c7a045db8ea4091a9c0058
1 #!/usr/bin/env python
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.
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.
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.
16 # hashlib is only available in python >= 2.5
17 try:
18 import hashlib
19 _digest = hashlib.sha1
20 except ImportError:
21 import sha
22 _digest = sha.new
23 import sys
24 import os
25 import time
26 sys.path.insert(0, os.getenv("GITPYTHONLIB","."))
28 from git_remote_helpers.util import die, debug, warn
29 from git_remote_helpers.git.repo import GitRepo
30 from git_remote_helpers.git.exporter import GitExporter
31 from git_remote_helpers.git.importer import GitImporter
32 from git_remote_helpers.git.non_local import NonLocalGit
34 if 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)
40 def encode_filepath(path):
41 """Encodes a Unicode file path to a byte string.
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.
47 [1] http://docs.python.org/3/c-api/unicode.html#file-system-encoding
49 """
50 if sys.hexversion < 0x03000000:
51 return path
52 return path.encode(sys.getfilesystemencoding(), 'surrogateescape')
55 def get_repo(alias, url):
56 """Returns a git repository object initialized for usage.
57 """
59 repo = GitRepo(url)
60 repo.get_revs()
61 repo.get_head()
63 hasher = _digest()
64 hasher.update(encode_filepath(repo.path))
65 repo.hash = hasher.hexdigest()
67 repo.get_base_path = lambda base: os.path.join(
68 base, 'info', 'fast-import', repo.hash)
70 prefix = 'refs/testgit/%s/' % alias
71 debug("prefix: '%s'", prefix)
73 repo.gitdir = os.environ["GIT_DIR"]
74 repo.alias = alias
75 repo.prefix = prefix
77 repo.exporter = GitExporter(repo)
78 repo.importer = GitImporter(repo)
79 repo.non_local = NonLocalGit(repo)
81 return repo
84 def local_repo(repo, path):
85 """Returns a git repository object initalized for usage.
86 """
88 local = GitRepo(path)
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)
99 return local
102 def do_capabilities(repo, args):
103 """Prints the supported capabilities.
106 print("import")
107 print("export")
108 print("refspec refs/heads/*:%s*" % repo.prefix)
110 dirname = repo.get_base_path(repo.gitdir)
112 if not os.path.exists(dirname):
113 os.makedirs(dirname)
115 path = os.path.join(dirname, 'git.marks')
117 print("*export-marks %s" % path)
118 if os.path.exists(path):
119 print("*import-marks %s" % path)
121 print('') # end capabilities
124 def do_list(repo, args):
125 """Lists all known references.
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.
132 for ref in repo.revs:
133 debug("? refs/heads/%s", ref)
134 print("? refs/heads/%s" % ref)
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")
143 print('') # end list
146 def update_local_repo(repo):
147 """Updates (or clones) a local repo.
150 if repo.local:
151 return repo
153 path = repo.non_local.clone(repo.gitdir)
154 repo.non_local.update(repo.gitdir)
155 repo = local_repo(repo, path)
156 return repo
159 def do_import(repo, args):
160 """Exports a fast-import stream from testgit for git to import.
163 if len(args) != 1:
164 die("Import needs exactly one ref")
166 if not repo.gitdir:
167 die("Need gitdir to import")
169 ref = args[0]
170 refs = [ref]
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.")
179 # strip of leading 'import '
180 ref = line[7:].strip()
181 refs.append(ref)
183 print("feature done")
185 if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
186 die('Told to fail')
188 repo = update_local_repo(repo)
189 repo.exporter.export_repo(repo.gitdir, refs)
191 print("done")
194 def do_export(repo, args):
195 """Imports a fast-import stream from git to testgit.
198 if not repo.gitdir:
199 die("Need gitdir to export")
201 if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"):
202 die('Told to fail')
204 update_local_repo(repo)
205 changed = repo.importer.do_import(repo.gitdir)
207 if not repo.local:
208 repo.non_local.push(repo.gitdir)
210 for ref in changed:
211 print("ok %s" % ref)
212 print('')
215 COMMANDS = {
216 'capabilities': do_capabilities,
217 'list': do_list,
218 'import': do_import,
219 'export': do_export,
223 def sanitize(value):
224 """Cleans up the url.
227 if value.startswith('testgit::'):
228 value = value[9:]
230 return value
233 def read_one_line(repo):
234 """Reads and processes one command.
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))
242 line = sys.stdin.readline()
244 cmdline = line.decode()
246 if not cmdline:
247 warn("Unexpected EOF")
248 return False
250 cmdline = cmdline.strip().split()
251 if not cmdline:
252 # Blank line means we're about to quit
253 return False
255 cmd = cmdline.pop(0)
256 debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline))
258 if cmd not in COMMANDS:
259 die("Unknown command, %s", cmd)
261 func = COMMANDS[cmd]
262 func(repo, cmdline)
263 sys.stdout.flush()
265 return True
268 def main(args):
269 """Starts a new remote helper for the specified repository.
272 if len(args) != 3:
273 die("Expecting exactly three arguments.")
274 sys.exit(1)
276 if os.getenv("GIT_DEBUG_TESTGIT"):
277 import git_remote_helpers.util
278 git_remote_helpers.util.DEBUG = True
280 alias = sanitize(args[1])
281 url = sanitize(args[2])
283 if not alias.isalnum():
284 warn("non-alnum alias '%s'", alias)
285 alias = "tmp"
287 args[1] = alias
288 args[2] = url
290 repo = get_repo(alias, url)
292 debug("Got arguments %s", args[1:])
294 more = True
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)
304 if __name__ == '__main__':
305 sys.exit(main(sys.argv))