Merge ~cjwatson/turnip:set-symbolic-ref into turnip:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: eea2842e739a4c3d3314f31c7d8937f7cc2542e2
Merged at revision: cf713d479b3fd067cb2a837428b276565b33f0a4
Proposed branch: ~cjwatson/turnip:set-symbolic-ref
Merge into: turnip:master
Diff against target: 497 lines (+297/-26)
8 files modified
README (+16/-0)
turnip/pack/git.py (+83/-9)
turnip/pack/hookrpc.py (+6/-2)
turnip/pack/http.py (+5/-2)
turnip/pack/ssh.py (+2/-1)
turnip/pack/tests/test_functional.py (+65/-11)
turnip/pack/tests/test_git.py (+118/-0)
turnip/pack/tests/test_http.py (+2/-1)
Reviewer Review Type Date Requested Status
Otto Co-Pilot Needs Fixing
William Grant code Approve
Review via email: mp+309702@code.launchpad.net

Commit message

Add turnip-set-symbolic-ref extension service

This makes it possible to set a repository's HEAD by talking directly to
turnip over HTTPS, which is needed for git-to-git code imports.

LP: #1469459

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) wrote :

All fixed, thanks.

Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

I tried to merge it but there are some problems. Typically you want to merge or rebase and try again.

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/README b/README
index 7b26510..4ebc443 100644
--- a/README
+++ b/README
@@ -68,6 +68,22 @@ The only additional parameters implemented today are
68'turnip-stateless-rpc' and 'turnip-advertise-refs', which are used by68'turnip-stateless-rpc' and 'turnip-advertise-refs', which are used by
69the smart HTTP server to proxy to the standard pack protocol.69the smart HTTP server to proxy to the standard pack protocol.
7070
71turnip implements one externally-visible extension: a
72'turnip-set-symbolic-ref' service that sets a symbolic ref (currently only
73'HEAD' is permitted) to a given target. This may be used over the various
74protocols (git, SSH, smart HTTP), requesting the service in the same way as
75the existing 'git-upload-pack' and 'git-receive-pack' services.
76
77 turnip-set-symbolic-ref-request = set-symbolic-ref-line
78 flush-pkt
79 set-symbolic-ref-line = PKT-LINE(refname SP refname)
80
81The server replies with an ACK indicating the symbolic ref name that was
82changed, or an error message.
83
84 turnip-set-symbolic-ref-response = set-symbolic-ref-ack / error-line
85 set-symbolic-ref-ack = PKT-LINE("ACK" SP refname)
86
7187
72Development88Development
73===========89===========
diff --git a/turnip/pack/git.py b/turnip/pack/git.py
index 0e5d928..93bc91b 100644
--- a/turnip/pack/git.py
+++ b/turnip/pack/git.py
@@ -278,7 +278,7 @@ class GitProcessProtocol(protocol.ProcessProtocol):
278 'git exited {code} with no output; synthesising an error',278 'git exited {code} with no output; synthesising an error',
279 code=code)279 code=code)
280 self.peer.sendPacket(ERROR_PREFIX + 'backend exited %d' % code)280 self.peer.sendPacket(ERROR_PREFIX + 'backend exited %d' % code)
281 self.peer.transport.loseConnection()281 self.peer.processEnded(reason)
282282
283 def pauseProducing(self):283 def pauseProducing(self):
284 self.transport.pauseProducing()284 self.transport.pauseProducing()
@@ -392,43 +392,117 @@ class PackBackendProtocol(PackServerProtocol):
392 """392 """
393393
394 hookrpc_key = None394 hookrpc_key = None
395 expect_set_symbolic_ref = False
395396
396 def requestReceived(self, command, raw_pathname, params):397 def requestReceived(self, command, raw_pathname, params):
397 self.extractRequestMeta(command, raw_pathname, params)398 self.extractRequestMeta(command, raw_pathname, params)
399 self.command = command
400 self.raw_pathname = raw_pathname
401 self.path = compose_path(self.factory.root, self.raw_pathname)
402
403 if command == b'turnip-set-symbolic-ref':
404 self.expect_set_symbolic_ref = True
405 self.resumeProducing()
406 return
398407
399 path = compose_path(self.factory.root, raw_pathname)408 write_operation = False
400 if command == b'git-upload-pack':409 if command == b'git-upload-pack':
401 subcmd = b'upload-pack'410 subcmd = b'upload-pack'
402 elif command == b'git-receive-pack':411 elif command == b'git-receive-pack':
403 subcmd = b'receive-pack'412 subcmd = b'receive-pack'
413 write_operation = True
404 else:414 else:
405 self.die(b'Unsupported command in request')415 self.die(b'Unsupported command in request')
406 return416 return
407417
408 cmd = b'git'418 args = []
409 args = [b'git', subcmd]
410 if params.pop(b'turnip-stateless-rpc', None):419 if params.pop(b'turnip-stateless-rpc', None):
411 args.append(b'--stateless-rpc')420 args.append(b'--stateless-rpc')
412 if params.pop(b'turnip-advertise-refs', None):421 if params.pop(b'turnip-advertise-refs', None):
413 args.append(b'--advertise-refs')422 args.append(b'--advertise-refs')
414 args.append(path)423 args.append(self.path)
424 self.spawnGit(subcmd, args, write_operation=write_operation)
425
426 def spawnGit(self, subcmd, extra_args, write_operation=False,
427 send_path_as_option=False):
428 cmd = b'git'
429 args = [b'git']
430 if send_path_as_option:
431 args.extend([b'-C', self.path])
432 args.append(subcmd)
433 args.extend(extra_args)
415434
416 env = {}435 env = {}
417 if subcmd == b'receive-pack' and self.factory.hookrpc_handler:436 if write_operation and self.factory.hookrpc_handler:
418 # This is a write operation, so prepare config, hooks, the hook437 # This is a write operation, so prepare config, hooks, the hook
419 # RPC server, and the environment variables that link them up.438 # RPC server, and the environment variables that link them up.
420 ensure_config(path)439 ensure_config(self.path)
421 self.hookrpc_key = str(uuid.uuid4())440 self.hookrpc_key = str(uuid.uuid4())
422 self.factory.hookrpc_handler.registerKey(441 self.factory.hookrpc_handler.registerKey(
423 self.hookrpc_key, raw_pathname, [])442 self.hookrpc_key, self.raw_pathname, [])
424 ensure_hooks(path)443 ensure_hooks(self.path)
425 env[b'TURNIP_HOOK_RPC_SOCK'] = self.factory.hookrpc_sock444 env[b'TURNIP_HOOK_RPC_SOCK'] = self.factory.hookrpc_sock
426 env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key445 env[b'TURNIP_HOOK_RPC_KEY'] = self.hookrpc_key
427446
428 self.log.info('Spawning {args}', args=args)447 self.log.info('Spawning {args}', args=args)
429 self.peer = GitProcessProtocol(self)448 self.peer = GitProcessProtocol(self)
449 self.spawnProcess(cmd, args, env=env)
450
451 def spawnProcess(self, cmd, args, env=None):
430 reactor.spawnProcess(self.peer, cmd, args, env=env)452 reactor.spawnProcess(self.peer, cmd, args, env=env)
431453
454 def packetReceived(self, data):
455 if self.expect_set_symbolic_ref:
456 if data is None:
457 self.die(b'Bad request: flush-pkt instead')
458 return
459 self.pauseProducing()
460 self.expect_set_symbolic_ref = False
461 if b' ' not in data:
462 self.die(b'Invalid set-symbolic-ref-line')
463 return
464 name, target = data.split(b' ', 1)
465 # Be careful about extending this to anything other than HEAD.
466 # We use "git symbolic-ref" because it gives us locking and
467 # logging, but it doesn't prevent writing a ref to ../something.
468 # Fortunately it does at least refuse to point HEAD outside of
469 # refs/.
470 if name != b'HEAD':
471 self.die(b'Symbolic ref name must be "HEAD"')
472 return
473 if target.startswith(b'-'):
474 self.die(b'Symbolic ref target may not start with "-"')
475 return
476 elif b' ' in target:
477 self.die(b'Symbolic ref target may not contain " "')
478 return
479 self.symbolic_ref_name = name
480 self.spawnGit(
481 b'symbolic-ref', [name, target], write_operation=True,
482 send_path_as_option=True)
483 return
484
485 PackServerProtocol.packetReceived(self, data)
486
487 @defer.inlineCallbacks
488 def processEnded(self, reason):
489 message = None
490 if self.command == b'turnip-set-symbolic-ref':
491 if reason.check(error.ProcessDone):
492 try:
493 yield self.factory.hookrpc_handler.notify(self.path)
494 self.sendPacket(b'ACK %s\n' % self.symbolic_ref_name)
495 except Exception as e:
496 message = str(e)
497 else:
498 message = (
499 'git symbolic-ref exited with status %d' %
500 reason.value.exitCode)
501 if message is None:
502 self.transport.loseConnection()
503 else:
504 self.die(message)
505
432 def readConnectionLost(self):506 def readConnectionLost(self):
433 # Forward the closed stdin down the stack.507 # Forward the closed stdin down the stack.
434 if self.peer is not None:508 if self.peer is not None:
diff --git a/turnip/pack/hookrpc.py b/turnip/pack/hookrpc.py
index d273a9c..bd3bc0e 100644
--- a/turnip/pack/hookrpc.py
+++ b/turnip/pack/hookrpc.py
@@ -126,11 +126,15 @@ class HookRPCHandler(object):
126 return [rule.decode('utf-8') for rule in self.ref_rules[args['key']]]126 return [rule.decode('utf-8') for rule in self.ref_rules[args['key']]]
127127
128 @defer.inlineCallbacks128 @defer.inlineCallbacks
129 def notify(self, path):
130 proxy = xmlrpc.Proxy(self.virtinfo_url, allowNone=True)
131 yield proxy.callRemote(b'notify', path)
132
133 @defer.inlineCallbacks
129 def notifyPush(self, proto, args):134 def notifyPush(self, proto, args):
130 """Notify the virtinfo service about a push."""135 """Notify the virtinfo service about a push."""
131 path = self.ref_paths[args['key']]136 path = self.ref_paths[args['key']]
132 proxy = xmlrpc.Proxy(self.virtinfo_url, allowNone=True)137 yield self.notify(path)
133 yield proxy.callRemote(b'notify', path)
134138
135139
136class HookRPCServerFactory(RPCServerFactory):140class HookRPCServerFactory(RPCServerFactory):
diff --git a/turnip/pack/http.py b/turnip/pack/http.py
index 5e5f87e..42fed27 100644
--- a/turnip/pack/http.py
+++ b/turnip/pack/http.py
@@ -680,7 +680,8 @@ class HTTPAuthResource(resource.Resource):
680class SmartHTTPFrontendResource(resource.Resource):680class SmartHTTPFrontendResource(resource.Resource):
681 """HTTP resource to translate Git smart HTTP requests to pack protocol."""681 """HTTP resource to translate Git smart HTTP requests to pack protocol."""
682682
683 allowed_services = frozenset((b'git-upload-pack', b'git-receive-pack'))683 allowed_services = frozenset((
684 b'git-upload-pack', b'git-receive-pack', b'turnip-set-symbolic-ref'))
684685
685 def __init__(self, backend_host, config):686 def __init__(self, backend_host, config):
686 resource.Resource.__init__(self)687 resource.Resource.__init__(self)
@@ -737,7 +738,9 @@ class SmartHTTPFrontendResource(resource.Resource):
737 content_type = request.getHeader(b'Content-Type')738 content_type = request.getHeader(b'Content-Type')
738 if content_type is None:739 if content_type is None:
739 return False740 return False
740 return content_type.startswith(b'application/x-git-')741 return (
742 content_type.startswith(b'application/x-git-') or
743 content_type.startswith(b'application/x-turnip-'))
741744
742 def getChild(self, path, request):745 def getChild(self, path, request):
743 if self._isGitRequest(request):746 if self._isGitRequest(request):
diff --git a/turnip/pack/ssh.py b/turnip/pack/ssh.py
index 006331d..b753220 100644
--- a/turnip/pack/ssh.py
+++ b/turnip/pack/ssh.py
@@ -120,7 +120,8 @@ class SSHPackClientFactory(protocol.ClientFactory):
120class SmartSSHSession(DoNothingSession):120class SmartSSHSession(DoNothingSession):
121 """SSH session allowing only Git smart SSH requests."""121 """SSH session allowing only Git smart SSH requests."""
122122
123 allowed_services = frozenset((b'git-upload-pack', b'git-receive-pack'))123 allowed_services = frozenset((
124 b'git-upload-pack', b'git-receive-pack', b'turnip-set-symbolic-ref'))
124125
125 def __init__(self, *args, **kwargs):126 def __init__(self, *args, **kwargs):
126 super(SmartSSHSession, self).__init__(*args, **kwargs)127 super(SmartSSHSession, self).__init__(*args, **kwargs)
diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py
index 61fd5c4..e6dbd20 100644
--- a/turnip/pack/tests/test_functional.py
+++ b/turnip/pack/tests/test_functional.py
@@ -8,8 +8,10 @@ from __future__ import (
8 unicode_literals,8 unicode_literals,
9 )9 )
1010
11import base64
11from collections import defaultdict12from collections import defaultdict
12import hashlib13import hashlib
14import io
13import os15import os
14import random16import random
15import shutil17import shutil
@@ -33,10 +35,7 @@ from fixtures import (
33from lazr.sshserver.auth import NoSuchPersonWithName35from lazr.sshserver.auth import NoSuchPersonWithName
34from testtools import TestCase36from testtools import TestCase
35from testtools.content import text_content37from testtools.content import text_content
36from testtools.deferredruntest import (38from testtools.deferredruntest import AsynchronousDeferredRunTest
37 assert_fails_with,
38 AsynchronousDeferredRunTest,
39 )
40from testtools.matchers import StartsWith39from testtools.matchers import StartsWith
41from twisted.internet import (40from twisted.internet import (
42 defer,41 defer,
@@ -45,11 +44,12 @@ from twisted.internet import (
45 )44 )
46from twisted.web import (45from twisted.web import (
47 client,46 client,
48 error,47 http_headers,
49 server,48 server,
50 xmlrpc,49 xmlrpc,
51 )50 )
5251
52from turnip.pack import helpers
53from turnip.pack.git import (53from turnip.pack.git import (
54 PackBackendFactory,54 PackBackendFactory,
55 PackFrontendFactory,55 PackFrontendFactory,
@@ -480,14 +480,68 @@ class TestSmartHTTPFrontendFunctional(FrontendFunctionalTestMixin, TestCase):
480480
481 @defer.inlineCallbacks481 @defer.inlineCallbacks
482 def test_root_revision_header(self):482 def test_root_revision_header(self):
483 factory = client.HTTPClientFactory(483 response = yield client.Agent(reactor).request(
484 b'http://localhost:%d/' % self.port, method=b'HEAD',484 b'HEAD', b'http://localhost:%d/' % self.port)
485 followRedirect=False)485 self.assertEqual(302, response.code)
486 reactor.connectTCP(b'localhost', self.port, factory)
487 yield assert_fails_with(factory.deferred, error.PageRedirect)
488 self.assertEqual(486 self.assertEqual(
489 [version_info['revision_id']],487 [version_info['revision_id']],
490 factory.response_headers[b'x-turnip-revision'])488 response.headers.getRawHeaders(b'X-Turnip-Revision'))
489
490 def make_set_symbolic_ref_request(self, line):
491 parsed_url = urlsplit(self.url)
492 url = urlunsplit([
493 parsed_url.scheme,
494 b'%s:%d' % (parsed_url.hostname, parsed_url.port),
495 parsed_url.path + b'/turnip-set-symbolic-ref', b'', b''])
496 headers = {
497 b'Content-Type': [
498 b'application/x-turnip-set-symbolic-ref-request',
499 ],
500 }
501 if parsed_url.username:
502 headers[b'Authorization'] = [
503 b'Basic ' + base64.b64encode(
504 b'%s:%s' % (parsed_url.username, parsed_url.password)),
505 ]
506 data = helpers.encode_packet(line) + helpers.encode_packet(None)
507 return client.Agent(reactor).request(
508 b'POST', url, headers=http_headers.Headers(headers),
509 bodyProducer=client.FileBodyProducer(io.BytesIO(data)))
510
511 @defer.inlineCallbacks
512 def get_symbolic_ref(self, path, name):
513 out = yield utils.getProcessOutput(
514 b'git', (b'symbolic-ref', name), env=os.environ, path=path)
515 defer.returnValue(out.rstrip(b'\n'))
516
517 @defer.inlineCallbacks
518 def test_turnip_set_symbolic_ref(self):
519 repo = os.path.join(self.root, self.internal_name)
520 head_target = yield self.get_symbolic_ref(repo, b'HEAD')
521 self.assertEqual(b'refs/heads/master', head_target)
522 response = yield self.make_set_symbolic_ref_request(
523 b'HEAD refs/heads/new-head')
524 self.assertEqual(200, response.code)
525 body = yield client.readBody(response)
526 self.assertEqual((b'ACK HEAD\n', ''), helpers.decode_packet(body))
527 head_target = yield self.get_symbolic_ref(repo, b'HEAD')
528 self.assertEqual(b'refs/heads/new-head', head_target)
529
530 @defer.inlineCallbacks
531 def test_turnip_set_symbolic_ref_error(self):
532 repo = os.path.join(self.root, self.internal_name)
533 head_target = yield self.get_symbolic_ref(repo, b'HEAD')
534 self.assertEqual(b'refs/heads/master', head_target)
535 response = yield self.make_set_symbolic_ref_request(b'HEAD --evil')
536 # This is a little weird since an error occurred, but it's
537 # consistent with other HTTP pack protocol responses.
538 self.assertEqual(200, response.code)
539 body = yield client.readBody(response)
540 self.assertEqual(
541 (b'ERR Symbolic ref target may not start with "-"\n', ''),
542 helpers.decode_packet(body))
543 head_target = yield self.get_symbolic_ref(repo, b'HEAD')
544 self.assertEqual(b'refs/heads/master', head_target)
491545
492546
493class TestSmartHTTPFrontendWithAuthFunctional(TestSmartHTTPFrontendFunctional):547class TestSmartHTTPFrontendWithAuthFunctional(TestSmartHTTPFrontendFunctional):
diff --git a/turnip/pack/tests/test_git.py b/turnip/pack/tests/test_git.py
index d19574d..4153234 100644
--- a/turnip/pack/tests/test_git.py
+++ b/turnip/pack/tests/test_git.py
@@ -7,13 +7,23 @@ from __future__ import (
7 unicode_literals,7 unicode_literals,
8 )8 )
99
10import os.path
11
12from fixtures import TempDir
13from pygit2 import init_repository
10from testtools import TestCase14from testtools import TestCase
15from testtools.matchers import (
16 ContainsDict,
17 Equals,
18 MatchesListwise,
19 )
11from twisted.test import proto_helpers20from twisted.test import proto_helpers
1221
13from turnip.pack import (22from turnip.pack import (
14 git,23 git,
15 helpers,24 helpers,
16 )25 )
26from turnip.pack.tests.test_hooks import MockHookRPCHandler
1727
1828
19class DummyPackServerProtocol(git.PackServerProtocol):29class DummyPackServerProtocol(git.PackServerProtocol):
@@ -87,3 +97,111 @@ class TestPackServerProtocol(TestCase):
87 # dropped.97 # dropped.
88 self.proto.dataReceived(b'0000')98 self.proto.dataReceived(b'0000')
89 self.assertKilledWith(b'Bad request: flush-pkt instead')99 self.assertKilledWith(b'Bad request: flush-pkt instead')
100
101
102class DummyPackBackendProtocol(git.PackBackendProtocol):
103
104 test_process = None
105
106 def spawnProcess(self, cmd, args, env=None):
107 if self.test_process is not None:
108 raise AssertionError('Process already spawned.')
109 self.test_process = (cmd, args, env)
110
111
112class TestPackBackendProtocol(TestCase):
113 """Test the Git pack backend protocol."""
114
115 def setUp(self):
116 super(TestPackBackendProtocol, self).setUp()
117 self.root = self.useFixture(TempDir()).path
118 self.hookrpc_handler = MockHookRPCHandler()
119 self.hookrpc_sock = os.path.join(self.root, 'hookrpc_sock')
120 self.factory = git.PackBackendFactory(
121 self.root, self.hookrpc_handler, self.hookrpc_sock)
122 self.proto = DummyPackBackendProtocol()
123 self.proto.factory = self.factory
124 self.transport = proto_helpers.StringTransportWithDisconnection()
125 self.transport.protocol = self.proto
126 self.proto.makeConnection(self.transport)
127
128 def assertKilledWith(self, message):
129 self.assertFalse(self.transport.connected)
130 self.assertEqual(
131 (b'ERR ' + message + b'\n', b''),
132 helpers.decode_packet(self.transport.value()))
133
134 def test_git_upload_pack_calls_spawnProcess(self):
135 # If the command is git-upload-pack, requestReceived calls
136 # spawnProcess with appropriate arguments.
137 self.proto.requestReceived(
138 b'git-upload-pack', b'/foo.git', {b'host': b'example.com'})
139 self.assertEqual(
140 (b'git',
141 [b'git', b'upload-pack', os.path.join(self.root, b'foo.git')],
142 {}),
143 self.proto.test_process)
144
145 def test_git_receive_pack_calls_spawnProcess(self):
146 # If the command is git-receive-pack, requestReceived calls
147 # spawnProcess with appropriate arguments.
148 repo_dir = os.path.join(self.root, 'foo.git')
149 init_repository(repo_dir, bare=True)
150 self.proto.requestReceived(
151 b'git-receive-pack', b'/foo.git', {b'host': b'example.com'})
152 self.assertThat(
153 self.proto.test_process, MatchesListwise([
154 Equals(b'git'),
155 Equals([b'git', b'receive-pack', repo_dir.encode('utf-8')]),
156 ContainsDict(
157 {b'TURNIP_HOOK_RPC_SOCK': Equals(self.hookrpc_sock)})]))
158
159 def test_turnip_set_symbolic_ref_calls_spawnProcess(self):
160 # If the command is turnip-set-symbolic-ref, requestReceived does
161 # not spawn a process, but packetReceived calls spawnProcess with
162 # appropriate arguments.
163 repo_dir = os.path.join(self.root, 'foo.git')
164 init_repository(repo_dir, bare=True)
165 self.proto.requestReceived(b'turnip-set-symbolic-ref', b'/foo.git', {})
166 self.assertIsNone(self.proto.test_process)
167 self.proto.packetReceived(b'HEAD refs/heads/master')
168 self.assertThat(
169 self.proto.test_process, MatchesListwise([
170 Equals(b'git'),
171 Equals([
172 b'git', b'-C', repo_dir.encode('utf-8'), b'symbolic-ref',
173 b'HEAD', b'refs/heads/master']),
174 ContainsDict(
175 {b'TURNIP_HOOK_RPC_SOCK': Equals(self.hookrpc_sock)})]))
176
177 def test_turnip_set_symbolic_ref_requires_valid_line(self):
178 # The turnip-set-symbolic-ref command requires a valid
179 # set-symbolic-ref-line packet.
180 self.proto.requestReceived(b'turnip-set-symbolic-ref', b'/foo.git', {})
181 self.assertIsNone(self.proto.test_process)
182 self.proto.packetReceived(b'HEAD')
183 self.assertKilledWith(b'Invalid set-symbolic-ref-line')
184
185 def test_turnip_set_symbolic_ref_name_must_be_HEAD(self):
186 # The turnip-set-symbolic-ref command's "name" parameter must be
187 # "HEAD".
188 self.proto.requestReceived(b'turnip-set-symbolic-ref', b'/foo.git', {})
189 self.assertIsNone(self.proto.test_process)
190 self.proto.packetReceived(b'another-symref refs/heads/master')
191 self.assertKilledWith(b'Symbolic ref name must be "HEAD"')
192
193 def test_turnip_set_symbolic_ref_target_not_option(self):
194 # The turnip-set-symbolic-ref command's "target" parameter may not
195 # start with "-".
196 self.proto.requestReceived(b'turnip-set-symbolic-ref', b'/foo.git', {})
197 self.assertIsNone(self.proto.test_process)
198 self.proto.packetReceived(b'HEAD --evil')
199 self.assertKilledWith(b'Symbolic ref target may not start with "-"')
200
201 def test_turnip_set_symbolic_ref_target_no_space(self):
202 # The turnip-set-symbolic-ref command's "target" parameter may not
203 # contain " ".
204 self.proto.requestReceived(b'turnip-set-symbolic-ref', b'/foo.git', {})
205 self.assertIsNone(self.proto.test_process)
206 self.proto.packetReceived(b'HEAD evil lies')
207 self.assertKilledWith(b'Symbolic ref target may not contain " "')
diff --git a/turnip/pack/tests/test_http.py b/turnip/pack/tests/test_http.py
index 45cafad..6e16b77 100644
--- a/turnip/pack/tests/test_http.py
+++ b/turnip/pack/tests/test_http.py
@@ -64,7 +64,8 @@ def render_resource(resource, request):
6464
65class FakeRoot(object):65class FakeRoot(object):
6666
67 allowed_services = frozenset((b'git-upload-pack', b'git-receive-pack'))67 allowed_services = frozenset((
68 b'git-upload-pack', b'git-receive-pack', b'turnip-set-symbolic-ref'))
6869
69 def __init__(self):70 def __init__(self):
70 self.backend_transport = None71 self.backend_transport = None

Subscribers

People subscribed via source and target branches