Merge ~pappacena/turnip:enable-http-v2 into turnip:master

Proposed by Thiago F. Pappacena
Status: Work in progress
Proposed branch: ~pappacena/turnip:enable-http-v2
Merge into: turnip:master
Prerequisite: ~pappacena/turnip:run-v2-commands
Diff against target: 684 lines (+166/-217)
7 files modified
turnip/pack/git.py (+20/-58)
turnip/pack/helpers.py (+30/-63)
turnip/pack/http.py (+27/-7)
turnip/pack/tests/test_functional.py (+8/-5)
turnip/pack/tests/test_git.py (+4/-56)
turnip/pack/tests/test_helpers.py (+41/-28)
turnip/pack/tests/test_http.py (+36/-0)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+389131@code.launchpad.net

Commit message

Enabling protocol v2 compatibility on HTTP frontend

To post a comment you must log in.
~pappacena/turnip:enable-http-v2 updated
6bd0254... by Thiago F. Pappacena

Cleaning up tests

Unmerged commits

6bd0254... by Thiago F. Pappacena

Cleaning up tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/pack/git.py b/turnip/pack/git.py
2index 1b34ac0..85fa57e 100644
3--- a/turnip/pack/git.py
4+++ b/turnip/pack/git.py
5@@ -31,14 +31,11 @@ from turnip.config import config
6 from turnip.helpers import compose_path
7 from turnip.pack.helpers import (
8 decode_packet,
9- DELIM_PKT,
10 decode_request,
11 encode_packet,
12 encode_request,
13 ensure_config,
14 ensure_hooks,
15- FLUSH_PKT,
16- get_capabilities_advertisement,
17 INCOMPLETE_PKT,
18 translate_xmlrpc_fault,
19 )
20@@ -242,20 +239,15 @@ class GitProcessProtocol(protocol.ProcessProtocol):
21
22 _err_buffer = b''
23
24- def __init__(self, peer, cmd_input=None):
25+ def __init__(self, peer):
26 self.peer = peer
27- self.cmd_input = cmd_input
28 self.out_started = False
29
30 def connectionMade(self):
31 self.peer.setPeer(self)
32 self.peer.transport.registerProducer(self, True)
33- if not self.cmd_input:
34- self.transport.registerProducer(
35- UnstoppableProducerWrapper(self.peer.transport), True)
36- else:
37- self.transport.write(self.cmd_input)
38- self.loseWriteConnection()
39+ self.transport.registerProducer(
40+ UnstoppableProducerWrapper(self.peer.transport), True)
41 self.peer.resumeProducing()
42
43 def outReceived(self, data):
44@@ -440,29 +432,12 @@ class PackBackendProtocol(PackServerProtocol):
45 hookrpc_key = None
46 expect_set_symbolic_ref = False
47
48- def getV2CommandInput(self, params):
49- """Reconstruct what should be sent to git's stdin from the
50- parameters received."""
51- cmd_input = encode_packet(b"command=%s\n" % params.get(b'command'))
52- for capability in params["capabilities"].split(b"\n"):
53- cmd_input += encode_packet(b"%s\n" % capability)
54- cmd_input += DELIM_PKT
55- ignore_keys = (b'capabilities', b'version')
56- for k, v in params.items():
57- k = six.ensure_binary(k)
58- if k.startswith(b"turnip-") or k in ignore_keys:
59- continue
60- for param_value in v.split(b'\n'):
61- value = (b"" if not param_value else b" %s" % param_value)
62- cmd_input += encode_packet(b"%s%s\n" % (k, value))
63- cmd_input += FLUSH_PKT
64- return cmd_input
65-
66 @defer.inlineCallbacks
67 def requestReceived(self, command, raw_pathname, params):
68 self.extractRequestMeta(command, raw_pathname, params)
69 self.command = command
70 self.raw_pathname = raw_pathname
71+ self.params = params
72 self.path = compose_path(self.factory.root, self.raw_pathname)
73 auth_params = self.createAuthParams(params)
74
75@@ -482,31 +457,20 @@ class PackBackendProtocol(PackServerProtocol):
76 self.resumeProducing()
77 return
78
79- send_path_as_option = False
80- cmd_input = None
81 cmd_env = {}
82 write_operation = False
83- if not get_capabilities_advertisement(params.get(b'version', 1)):
84- if command == b'git-upload-pack':
85- subcmd = b'upload-pack'
86- elif command == b'git-receive-pack':
87- subcmd = b'receive-pack'
88- write_operation = True
89- else:
90- self.die(b'Unsupported command in request')
91- return
92- else:
93- v2_command = params.get(b'command')
94- if command == b'git-upload-pack' and not v2_command:
95- self.expectNextCommand()
96- self.transport.loseConnection()
97- return
98- subcmd = b'upload-pack'
99- cmd_env["GIT_PROTOCOL"] = 'version=2'
100- send_path_as_option = True
101- # Do not include "advertise-refs" parameter.
102+ version = self.params.get(b'version', 0)
103+ cmd_env["GIT_PROTOCOL"] = 'version=%s' % version
104+ if version == b'2':
105 params.pop(b'turnip-advertise-refs', None)
106- cmd_input = self.getV2CommandInput(params)
107+ if command == b'git-upload-pack':
108+ subcmd = b'upload-pack'
109+ elif command == b'git-receive-pack':
110+ subcmd = b'receive-pack'
111+ write_operation = True
112+ else:
113+ self.die(b'Unsupported command in request')
114+ return
115
116 args = []
117 if params.pop(b'turnip-stateless-rpc', None):
118@@ -516,14 +480,12 @@ class PackBackendProtocol(PackServerProtocol):
119 args.append(self.path)
120 self.spawnGit(
121 subcmd, args,
122- write_operation=write_operation,
123- auth_params=auth_params,
124- send_path_as_option=send_path_as_option,
125- cmd_env=cmd_env, cmd_input=cmd_input)
126+ write_operation=write_operation, auth_params=auth_params,
127+ cmd_env=cmd_env)
128
129 def spawnGit(self, subcmd, extra_args, write_operation=False,
130 send_path_as_option=False, auth_params=None,
131- cmd_env=None, cmd_input=None):
132+ cmd_env=None):
133 cmd = b'git'
134 args = [b'git']
135 if send_path_as_option:
136@@ -545,7 +507,7 @@ class PackBackendProtocol(PackServerProtocol):
137 env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key
138
139 self.log.info('Spawning {args}', args=args)
140- self.peer = GitProcessProtocol(self, cmd_input)
141+ self.peer = GitProcessProtocol(self)
142 self.spawnProcess(cmd, args, env=env)
143
144 def spawnProcess(self, cmd, args, env=None):
145@@ -674,7 +636,7 @@ class PackVirtServerProtocol(PackProxyServerProtocol):
146 @defer.inlineCallbacks
147 def requestReceived(self, command, pathname, params):
148 self.extractRequestMeta(command, pathname, params)
149- permission = 'read' if command == b'git-upload-pack' else 'write'
150+ permission = 'read' if command != b'git-receive-pack' else 'write'
151 proxy = xmlrpc.Proxy(self.factory.virtinfo_endpoint, allowNone=True)
152 try:
153 auth_params = self.createAuthParams(params)
154diff --git a/turnip/pack/helpers.py b/turnip/pack/helpers.py
155index 93aba33..5d78d68 100644
156--- a/turnip/pack/helpers.py
157+++ b/turnip/pack/helpers.py
158@@ -24,10 +24,10 @@ import six
159 import yaml
160
161 import turnip.pack.hooks
162-
163+from turnip.version_info import version_info
164
165 FLUSH_PKT = b'0000'
166-DELIM_PKT = b'0001'
167+DELIM_PKT = object()
168 PKT_LEN_SIZE = 4
169 PKT_PAYLOAD_MAX = 65520
170 INCOMPLETE_PKT = object()
171@@ -37,6 +37,8 @@ def encode_packet(payload):
172 if payload is None:
173 # flush-pkt.
174 return FLUSH_PKT
175+ if payload is DELIM_PKT:
176+ return b'0001'
177 else:
178 # data-pkt
179 if len(payload) > PKT_PAYLOAD_MAX:
180@@ -48,63 +50,25 @@ def encode_packet(payload):
181
182 def decode_packet(input):
183 """Consume a packet, returning the payload and any unconsumed tail."""
184+ if input.startswith(b'0001'):
185+ return (DELIM_PKT, input[PKT_LEN_SIZE:])
186 if len(input) < PKT_LEN_SIZE:
187 return (INCOMPLETE_PKT, input)
188 if input.startswith(FLUSH_PKT):
189 # flush-pkt
190 return (None, input[PKT_LEN_SIZE:])
191- # data-pkt
192- try:
193- pkt_len = int(input[:PKT_LEN_SIZE], 16)
194- except ValueError:
195- pkt_len = 0
196- if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)):
197- raise ValueError("Invalid pkt-len")
198- if len(input) < pkt_len:
199- # Some of the packet is yet to be received.
200- return (INCOMPLETE_PKT, input)
201- # v2 protocol "hides" extra parameters after the end of the packet.
202- if len(input) > pkt_len and b'version=2\x00' in input:
203- if FLUSH_PKT not in input:
204- return INCOMPLETE_PKT, input
205- end = input.index(FLUSH_PKT)
206- return input[PKT_LEN_SIZE:end], input[end + len(FLUSH_PKT):]
207- return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:])
208-
209-
210-def decode_packet_list(data):
211- remaining = data
212- retval = []
213- while remaining:
214- pkt, remaining = decode_packet(remaining)
215- retval.append(pkt)
216- return retval
217-
218-
219-def decode_protocol_v2_params(data):
220- """Parse the protocol v2 extra parameters hidden behind the end of v1
221- protocol.
222-
223- :return: An ordered dict with parsed v2 parameters.
224- """
225- params = OrderedDict()
226- cmd, remaining = decode_packet(data)
227- cmd = cmd.split(b'=', 1)[-1].strip()
228- capabilities, args = remaining.split(DELIM_PKT)
229- params[b"command"] = cmd
230- params[b"capabilities"] = decode_packet_list(capabilities)
231- for arg in decode_packet_list(args):
232- if arg is None:
233- continue
234- arg = arg.strip('\n')
235- if b' ' in arg:
236- k, v = arg.split(b' ', 1)
237- if k not in params:
238- params[k] = []
239- params[k].append(v)
240- else:
241- params[arg] = b""
242- return params
243+ else:
244+ # data-pkt
245+ try:
246+ pkt_len = int(input[:PKT_LEN_SIZE], 16)
247+ except ValueError:
248+ pkt_len = 0
249+ if not (PKT_LEN_SIZE <= pkt_len <= (PKT_LEN_SIZE + PKT_PAYLOAD_MAX)):
250+ raise ValueError("Invalid pkt-len")
251+ if len(input) < pkt_len:
252+ # Some of the packet is yet to be received.
253+ return (INCOMPLETE_PKT, input)
254+ return (input[PKT_LEN_SIZE:pkt_len], input[pkt_len:])
255
256
257 def decode_request(data):
258@@ -121,7 +85,7 @@ def decode_request(data):
259 # Following the command is a pathname, then any number of named
260 # parameters. Each of these is NUL-terminated.
261 # After that, v1 should end (v2 might have extra commands).
262- if len(bits) < 2 or (b'version=2' not in bits and bits[-1] != b''):
263+ if len(bits) < 2 or bits[-1] != b'':
264 raise ValueError('Invalid git-proto-request')
265 pathname = bits[0]
266 params = OrderedDict()
267@@ -139,12 +103,6 @@ def decode_request(data):
268 raise ValueError('Parameters must not be repeated')
269 params[name] = value
270
271- # If there are remaining bits at the end, we must be dealing with v2
272- # protocol. So, we append v2 parameters at the end of original parameters.
273- if bits[-1]:
274- for k, v in decode_protocol_v2_params(bits[-1]).items():
275- params[k] = v
276-
277 return command, pathname, params
278
279
280@@ -297,5 +255,14 @@ def get_capabilities_advertisement(version='1'):
281
282 If no binary data is sent, no advertisement is done and we declare to
283 not be compatible with that specific version."""
284- # XXX pappacena 2020-08-11: Return the correct data for protocol v2.
285- return b""
286+ if version != '2':
287+ return b""
288+ turnip_version = six.ensure_binary(version_info.get("revision_id", '-1'))
289+ return (
290+ encode_packet(b"version 2\n") +
291+ encode_packet(b"agent=turnip/%s\n" % turnip_version) +
292+ encode_packet(b"ls-refs\n") +
293+ encode_packet(b"fetch=shallow\n") +
294+ encode_packet(b"server-option\n") +
295+ FLUSH_PKT
296+ )
297diff --git a/turnip/pack/http.py b/turnip/pack/http.py
298index c0becf0..9e0661c 100644
299--- a/turnip/pack/http.py
300+++ b/turnip/pack/http.py
301@@ -29,6 +29,7 @@ from paste.auth.cookie import (
302 decode as decode_cookie,
303 encode as encode_cookie,
304 )
305+import six
306 from twisted.internet import (
307 defer,
308 error,
309@@ -58,8 +59,8 @@ from turnip.pack.helpers import (
310 encode_packet,
311 encode_request,
312 translate_xmlrpc_fault,
313- TurnipFaultCode,
314- )
315+ TurnipFaultCode, get_capabilities_advertisement,
316+)
317 try:
318 from turnip.version_info import version_info
319 except ImportError:
320@@ -80,6 +81,16 @@ def fail_request(request, message, code=http.INTERNAL_SERVER_ERROR):
321 return b''
322
323
324+def get_protocol_version_from_request(request):
325+ version_header = request.requestHeaders.getRawHeaders(
326+ 'git-protocol', ['version=1'])[0]
327+ try:
328+ return version_header.split('version=', 1)[1]
329+ except IndexError:
330+ pass
331+ return 1
332+
333+
334 class HTTPPackClientProtocol(PackProtocol):
335 """Abstract bridge between a Git pack connection and a smart HTTP request.
336
337@@ -99,7 +110,11 @@ class HTTPPackClientProtocol(PackProtocol):
338
339 def startGoodResponse(self):
340 """Prepare the HTTP response for forwarding from the backend."""
341- raise NotImplementedError()
342+ self.factory.http_request.write(
343+ get_capabilities_advertisement(self.getProtocolVersion()))
344+
345+ def getProtocolVersion(self):
346+ return get_protocol_version_from_request(self.factory.http_request)
347
348 def backendConnected(self):
349 """Called when the backend is connected and has sent a good packet."""
350@@ -209,6 +224,7 @@ class HTTPPackClientRefsProtocol(HTTPPackClientProtocol):
351 self.factory.http_request.setHeader(
352 b'Content-Type',
353 b'application/x-%s-advertisement' % self.factory.command)
354+ super(HTTPPackClientRefsProtocol, self).startGoodResponse()
355
356 def backendConnected(self):
357 HTTPPackClientProtocol.backendConnected(self)
358@@ -221,10 +237,11 @@ class HTTPPackClientCommandProtocol(HTTPPackClientProtocol):
359
360 def startGoodResponse(self):
361 """Prepare the HTTP response for forwarding from the backend."""
362- self.factory.http_request.setResponseCode(http.OK)
363- self.factory.http_request.setHeader(
364- b'Content-Type',
365- b'application/x-%s-result' % self.factory.command)
366+ if self.getProtocolVersion() != b'2':
367+ self.factory.http_request.setResponseCode(http.OK)
368+ self.factory.http_request.setHeader(
369+ b'Content-Type',
370+ b'application/x-%s-result' % self.factory.command)
371
372
373 class HTTPPackClientFactory(protocol.ClientFactory):
374@@ -280,8 +297,11 @@ class BaseSmartHTTPResource(resource.Resource):
375 by the virt service, if any.
376 """
377 params = {
378+ b'turnip-frontend': b'http',
379 b'turnip-can-authenticate': b'yes',
380 b'turnip-request-id': str(uuid.uuid4()),
381+ b'version': six.ensure_binary(
382+ get_protocol_version_from_request(request))
383 }
384 authenticated_params = yield self.authenticateUser(request)
385 for key, value in authenticated_params.items():
386diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py
387index 27bc26e..c811b2a 100644
388--- a/turnip/pack/tests/test_functional.py
389+++ b/turnip/pack/tests/test_functional.py
390@@ -815,16 +815,19 @@ class TestSmartHTTPFrontendWithAuthFunctional(TestSmartHTTPFrontendFunctional):
391 test_root = self.useFixture(TempDir()).path
392 clone = os.path.join(test_root, 'clone')
393 yield self.assertCommandSuccess((b'git', b'clone', self.ro_url, clone))
394+ expected_requests = 1 if self.protocol_version == '1' else 2
395 self.assertEqual(
396- [(b'test-user', b'test-password')], self.virtinfo.authentications)
397- self.assertThat(self.virtinfo.translations, MatchesListwise([
398- MatchesListwise([
399+ [(b'test-user', b'test-password')] * expected_requests,
400+ self.virtinfo.authentications)
401+ self.assertEqual(expected_requests, len(self.virtinfo.translations))
402+ for translation in self.virtinfo.translations:
403+ self.assertThat(translation, MatchesListwise([
404 Equals(b'/test'), Equals(b'read'),
405 MatchesDict({
406 b'can-authenticate': Is(True),
407 b'request-id': Not(Is(None)),
408- b'user': Equals(b'test-user'),
409- })])]))
410+ b'user': Equals(b'test-user')})
411+ ]))
412
413 @defer.inlineCallbacks
414 def test_authenticated_push(self):
415diff --git a/turnip/pack/tests/test_git.py b/turnip/pack/tests/test_git.py
416index b69049c..d1a8162 100644
417--- a/turnip/pack/tests/test_git.py
418+++ b/turnip/pack/tests/test_git.py
419@@ -9,7 +9,6 @@ from __future__ import (
420
421 import hashlib
422 import os.path
423-from collections import OrderedDict
424
425 from fixtures import TempDir, MonkeyPatch
426 from pygit2 import init_repository
427@@ -35,7 +34,6 @@ from turnip.pack import (
428 git,
429 helpers,
430 )
431-from turnip.pack.git import GitProcessProtocol
432 from turnip.pack.tests.fake_servers import FakeVirtInfoService
433 from turnip.pack.tests.test_hooks import MockHookRPCHandler
434 from turnip.tests.compat import mock
435@@ -51,18 +49,6 @@ class DummyPackServerProtocol(git.PackServerProtocol):
436 self.test_request = (command, pathname, host)
437
438
439-class TestGitProcessProtocol(TestCase):
440- def test_can_write_to_stdin_directly(self):
441- peer = mock.Mock()
442- transport = mock.Mock()
443- protocol = GitProcessProtocol(peer, b"this is the stdin")
444- protocol.transport = transport
445- protocol.connectionMade()
446- self.assertEqual(
447- [mock.call(b'this is the stdin', )],
448- transport.write.call_args_list)
449-
450-
451 class TestPackServerProtocol(TestCase):
452 """Test the base implementation of the git pack network protocol."""
453
454@@ -243,8 +229,9 @@ class TestPackBackendProtocol(TestCase):
455 [('foo.git', )], self.virtinfo.confirm_repo_creation_call_args)
456
457 self.assertEqual(
458- (b'git', [b'git', b'upload-pack', full_path], {}),
459- self.proto.test_process)
460+ (b'git', [b'git', b'upload-pack', full_path], {
461+ 'GIT_PROTOCOL': 'version=0'
462+ }), self.proto.test_process)
463
464 @defer.inlineCallbacks
465 def test_create_repo_fails_to_confirm(self):
466@@ -281,47 +268,8 @@ class TestPackBackendProtocol(TestCase):
467 self.assertEqual(
468 (b'git',
469 [b'git', b'upload-pack', full_path],
470- {}),
471- self.proto.test_process)
472-
473- def test_git_upload_pack_v2_calls_spawnProcess(self):
474- # If the command is git-upload-pack using v2 protocol, requestReceived
475- # calls spawnProcess with appropriate arguments.
476- advertise_capabilities = mock.Mock()
477- self.useFixture(
478- MonkeyPatch("turnip.pack.git.get_capabilities_advertisement",
479- advertise_capabilities))
480- advertise_capabilities.return_value = b'fake capability'
481-
482- self.proto.requestReceived(
483- b'git-upload-pack', b'/foo.git', OrderedDict([
484- (b'turnip-x', b'yes'),
485- (b'turnip-request-id', b'123'),
486- (b'version', b'2'),
487- (b'command', b'ls-refs'),
488- (b'capabilities', b'agent=git/2.25.1'),
489- (b'peel', b''),
490- (b'symrefs', b''),
491- (b'ref-prefix', b'HEAD\nrefs/heads/\nrefs/tags/')
492- ]))
493- full_path = os.path.join(six.ensure_binary(self.root), b'foo.git')
494- self.assertEqual(
495- (b'git',
496- [b'git', b'-C', full_path, b'upload-pack', full_path],
497- {'GIT_PROTOCOL': 'version=2'}),
498+ {'GIT_PROTOCOL': 'version=0'}),
499 self.proto.test_process)
500- stdin_content = (
501- b'0014command=ls-refs\n'
502- b'0015agent=git/2.25.1\n'
503- b'00010014command ls-refs\n'
504- b'0009peel\n'
505- b'000csymrefs\n'
506- b'0014ref-prefix HEAD\n'
507- b'001bref-prefix refs/heads/\n'
508- b'001aref-prefix refs/tags/\n'
509- b'0000'
510- )
511- self.assertEqual(stdin_content, self.proto.peer.cmd_input)
512
513 def test_git_receive_pack_calls_spawnProcess(self):
514 # If the command is git-receive-pack, requestReceived calls
515diff --git a/turnip/pack/tests/test_helpers.py b/turnip/pack/tests/test_helpers.py
516index bad7117..a5ba913 100644
517--- a/turnip/pack/tests/test_helpers.py
518+++ b/turnip/pack/tests/test_helpers.py
519@@ -7,25 +7,32 @@ from __future__ import (
520 unicode_literals,
521 )
522
523+from collections import OrderedDict
524 import os.path
525 import re
526-from collections import OrderedDict
527-
528-import stat
529-import sys
530-from textwrap import dedent
531-import time
532+import shutil
533+import subprocess
534+import tempfile
535
536 from fixtures import TempDir
537 from pygit2 import (
538 Config,
539 init_repository,
540 )
541+import six
542+import stat
543+import sys
544 from testtools import TestCase
545+from textwrap import dedent
546+import time
547
548 from turnip.pack import helpers
549 import turnip.pack.hooks
550-from turnip.pack.helpers import FLUSH_PKT
551+from turnip.pack.helpers import (
552+ get_capabilities_advertisement,
553+ encode_packet,
554+ )
555+from turnip.version_info import version_info
556
557 TEST_DATA = b'0123456789abcdef'
558 TEST_PKT = b'00140123456789abcdef'
559@@ -175,27 +182,6 @@ class TestDecodeRequest(TestCase):
560 b'git-do-stuff /foo\0host=foo\0host=bar\0',
561 b'Parameters must not be repeated')
562
563- def test_v2_extra_commands(self):
564- data = (
565- b"git-upload-pack b306d" +
566- b"\x00turnip-x=yes\x00turnip-request-id=123\x00" +
567- b"version=2\x000014command=ls-refs\n0014agent=git/2.25.1" +
568- b'00010009peel\n000csymrefs\n0014ref-prefix HEAD\n' +
569- b'001bref-prefix refs/heads/\n'
570- b'001aref-prefix refs/tags/\n0000' +
571- FLUSH_PKT)
572- decoded = helpers.decode_request(data)
573- self.assertEqual((b'git-upload-pack', b'b306d', OrderedDict([
574- (b'turnip-x', b'yes'),
575- (b'turnip-request-id', b'123'),
576- (b'version', b'2'),
577- (b'command', b'ls-refs'),
578- (b'capabilities', [b'agent=git/2.25.1']),
579- (b'peel', b''),
580- (b'symrefs', b''),
581- (b'ref-prefix', [b'HEAD', b'refs/heads/', b'refs/tags/'])
582- ])), decoded)
583-
584
585 class TestEncodeRequest(TestCase):
586 """Test git-proto-request encoding."""
587@@ -344,3 +330,30 @@ class TestEnsureHooks(TestCase):
588 self.assertEqual(expected_bytes, actual.read())
589 # The hook is executable.
590 self.assertTrue(os.stat(self.hook('hook.py')).st_mode & stat.S_IXUSR)
591+
592+
593+class TestCapabilityAdvertisement(TestCase):
594+ def test_returning_same_output_as_git_command(self):
595+ """Make sure that our hard-coded feature advertisement matches what
596+ our git command advertises."""
597+ root = tempfile.mkdtemp(prefix=b'turnip-test-root-')
598+ self.addCleanup(shutil.rmtree, root, ignore_errors=True)
599+ # Create a dummy repository
600+ subprocess.call(['git', 'init', root])
601+
602+ git_version = subprocess.check_output(['git', '--version'])
603+ git_version_num = six.ensure_binary(git_version.split(' ')[-1].strip())
604+ git_agent = encode_packet(b"agent=git/%s\n" % git_version_num)
605+
606+ proc = subprocess.Popen(
607+ ['git', 'upload-pack', root], env={"GIT_PROTOCOL": "version=2"},
608+ stdout=subprocess.PIPE, stdin=subprocess.PIPE)
609+ git_advertised_capabilities, _ = proc.communicate()
610+
611+ turnip_capabilities = get_capabilities_advertisement(version=b'2')
612+ turnip_agent = encode_packet(
613+ b"agent=turnip/%s\n" % version_info["revision_id"])
614+
615+ self.assertEqual(
616+ turnip_capabilities,
617+ git_advertised_capabilities.replace(git_agent, turnip_agent))
618diff --git a/turnip/pack/tests/test_http.py b/turnip/pack/tests/test_http.py
619index 26e2754..9c547c1 100644
620--- a/turnip/pack/tests/test_http.py
621+++ b/turnip/pack/tests/test_http.py
622@@ -24,7 +24,10 @@ from turnip.pack import (
623 helpers,
624 http,
625 )
626+from turnip.pack.helpers import encode_packet
627 from turnip.pack.tests.fake_servers import FakeVirtInfoService
628+from turnip.tests.compat import mock
629+from turnip.version_info import version_info
630
631
632 class LessDummyRequest(requesthelper.DummyRequest):
633@@ -74,12 +77,14 @@ class FakeRoot(object):
634
635 def __init__(self):
636 self.backend_transport = None
637+ self.client_factory = None
638 self.backend_connected = defer.Deferred()
639
640 def authenticateWithPassword(self, user, password):
641 return {}
642
643 def connectToBackend(self, client_factory):
644+ self.client_factory = client_factory
645 self.backend_transport = testing.StringTransportWithDisconnection()
646 p = client_factory.buildProtocol(None)
647 self.backend_transport.protocol = p
648@@ -186,6 +191,37 @@ class TestSmartHTTPRefsResource(ErrorTestMixin, TestCase):
649 'And I am raw, since we got a good packet to start with.',
650 self.request.value)
651
652+ @defer.inlineCallbacks
653+ def test_good_v2_included_version_and_capabilities(self):
654+ self.request.requestHeaders.addRawHeader("Git-Protocol", "version=2")
655+ yield self.performRequest(
656+ helpers.encode_packet(b'I am git protocol data.') +
657+ b'And I am raw, since we got a good packet to start with.')
658+ self.assertEqual(200, self.request.responseCode)
659+ self.assertEqual(self.root.client_factory.params, {
660+ 'version': '2',
661+ 'turnip-frontend': 'http',
662+ 'turnip-advertise-refs': 'yes',
663+ 'turnip-can-authenticate': 'yes',
664+ 'turnip-request-id': mock.ANY,
665+ 'turnip-stateless-rpc': 'yes'})
666+
667+ ver = version_info["revision_id"]
668+ capabilities = (
669+ encode_packet(b'version 2\n') +
670+ encode_packet(b'agent=turnip/%s\n' % ver) +
671+ encode_packet(b'ls-refs\n') +
672+ encode_packet(b'fetch=shallow\n') +
673+ encode_packet(b'server-option\n') +
674+ b'0000'
675+ )
676+ self.assertEqual(
677+ capabilities +
678+ '001e# service=git-upload-pack\n'
679+ '0000001bI am git protocol data.'
680+ 'And I am raw, since we got a good packet to start with.',
681+ self.request.value)
682+
683
684 class TestSmartHTTPCommandResource(ErrorTestMixin, TestCase):
685

Subscribers

People subscribed via source and target branches