##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r227:ddd92633 merge stable
parent child Browse files
Show More
@@ -1,5 +1,5 b''
1 [bumpversion]
1 [bumpversion]
2 current_version = 4.7.2
2 current_version = 4.8.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:vcsserver/VERSION]
5 [bumpversion:file:vcsserver/VERSION]
@@ -5,12 +5,10 b' done = false'
5 done = true
5 done = true
6
6
7 [task:fixes_on_stable]
7 [task:fixes_on_stable]
8 done = true
9
8
10 [task:pip2nix_generated]
9 [task:pip2nix_generated]
11 done = true
12
10
13 [release]
11 [release]
14 state = prepared
12 state = in_progress
15 version = 4.7.2
13 version = 4.8.0
16
14
@@ -32,7 +32,7 b' use = egg:waitress#main'
32 ### LOGGING CONFIGURATION ####
32 ### LOGGING CONFIGURATION ####
33 ################################
33 ################################
34 [loggers]
34 [loggers]
35 keys = root, vcsserver, pyro4, beaker
35 keys = root, vcsserver, beaker
36
36
37 [handlers]
37 [handlers]
38 keys = console
38 keys = console
@@ -59,12 +59,6 b' handlers ='
59 qualname = beaker
59 qualname = beaker
60 propagate = 1
60 propagate = 1
61
61
62 [logger_pyro4]
63 level = DEBUG
64 handlers =
65 qualname = Pyro4
66 propagate = 1
67
68
62
69 ##############
63 ##############
70 ## HANDLERS ##
64 ## HANDLERS ##
@@ -20,8 +20,7 b' use = egg:gunicorn#main'
20 workers = 2
20 workers = 2
21 ## process name
21 ## process name
22 proc_name = rhodecode_vcsserver
22 proc_name = rhodecode_vcsserver
23 ## type of worker class, one of sync, gevent
23 ## type of worker class, currently `sync` is the only option allowed.
24 ## recommended for bigger setup is using of of other than sync one
25 worker_class = sync
24 worker_class = sync
26 ## The maximum number of simultaneous clients. Valid only for Gevent
25 ## The maximum number of simultaneous clients. Valid only for Gevent
27 #worker_connections = 10
26 #worker_connections = 10
@@ -56,7 +55,7 b' beaker.cache.repo_object.enabled = true'
56 ### LOGGING CONFIGURATION ####
55 ### LOGGING CONFIGURATION ####
57 ################################
56 ################################
58 [loggers]
57 [loggers]
59 keys = root, vcsserver, pyro4, beaker
58 keys = root, vcsserver, beaker
60
59
61 [handlers]
60 [handlers]
62 keys = console
61 keys = console
@@ -83,12 +82,6 b' handlers ='
83 qualname = beaker
82 qualname = beaker
84 propagate = 1
83 propagate = 1
85
84
86 [logger_pyro4]
87 level = DEBUG
88 handlers =
89 qualname = Pyro4
90 propagate = 1
91
92
85
93 ##############
86 ##############
94 ## HANDLERS ##
87 ## HANDLERS ##
@@ -120,6 +120,7 b' let'
120 cp -v vcsserver/VERSION $out/nix-support/rccontrol/version
120 cp -v vcsserver/VERSION $out/nix-support/rccontrol/version
121 echo "DONE: Meta information for rccontrol written"
121 echo "DONE: Meta information for rccontrol written"
122
122
123 # python based programs need to be wrapped
123 ln -s ${self.pyramid}/bin/* $out/bin/
124 ln -s ${self.pyramid}/bin/* $out/bin/
124 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
125 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
125
126
@@ -132,12 +133,14 b' let'
132 ln -s ${self.mercurial}/bin/hg $out/bin
133 ln -s ${self.mercurial}/bin/hg $out/bin
133 ln -s ${pkgs.subversion}/bin/svn* $out/bin
134 ln -s ${pkgs.subversion}/bin/svn* $out/bin
134
135
135 for file in $out/bin/*; do
136 for file in $out/bin/*;
137 do
136 wrapProgram $file \
138 wrapProgram $file \
137 --set PATH $PATH \
139 --set PATH $PATH \
138 --set PYTHONPATH $PYTHONPATH \
140 --set PYTHONPATH $PYTHONPATH \
139 --set PYTHONHASHSEED random
141 --set PYTHONHASHSEED random
140 done
142 done
143
141 '';
144 '';
142
145
143 });
146 });
@@ -67,19 +67,6 b''
67 license = [ pkgs.lib.licenses.mit ];
67 license = [ pkgs.lib.licenses.mit ];
68 };
68 };
69 };
69 };
70 Pyro4 = super.buildPythonPackage {
71 name = "Pyro4-4.41";
72 buildInputs = with self; [];
73 doCheck = false;
74 propagatedBuildInputs = with self; [serpent];
75 src = fetchurl {
76 url = "https://pypi.python.org/packages/56/2b/89b566b4bf3e7f8ba790db2d1223852f8cb454c52cab7693dd41f608ca2a/Pyro4-4.41.tar.gz";
77 md5 = "ed69e9bfafa9c06c049a87cb0c4c2b6c";
78 };
79 meta = {
80 license = [ pkgs.lib.licenses.mit ];
81 };
82 };
83 WebOb = super.buildPythonPackage {
70 WebOb = super.buildPythonPackage {
84 name = "WebOb-1.3.1";
71 name = "WebOb-1.3.1";
85 buildInputs = with self; [];
72 buildInputs = with self; [];
@@ -249,6 +236,19 b''
249 license = [ pkgs.lib.licenses.mit ];
236 license = [ pkgs.lib.licenses.mit ];
250 };
237 };
251 };
238 };
239 hg-evolve = super.buildPythonPackage {
240 name = "hg-evolve-6.0.1";
241 buildInputs = with self; [];
242 doCheck = false;
243 propagatedBuildInputs = with self; [];
244 src = fetchurl {
245 url = "https://pypi.python.org/packages/c4/31/0673a5657c201ebb46e63c4bba8668f96cf5d7a8a0f8a91892d022ccc32b/hg-evolve-6.0.1.tar.gz";
246 md5 = "9c1ce7ac24792abc0eedee09a3344d06";
247 };
248 meta = {
249 license = [ { fullName = "GPLv2+"; } ];
250 };
251 };
252 hgsubversion = super.buildPythonPackage {
252 hgsubversion = super.buildPythonPackage {
253 name = "hgsubversion-1.8.6";
253 name = "hgsubversion-1.8.6";
254 buildInputs = with self; [];
254 buildInputs = with self; [];
@@ -302,13 +302,13 b''
302 };
302 };
303 };
303 };
304 ipython-genutils = super.buildPythonPackage {
304 ipython-genutils = super.buildPythonPackage {
305 name = "ipython-genutils-0.1.0";
305 name = "ipython-genutils-0.2.0";
306 buildInputs = with self; [];
306 buildInputs = with self; [];
307 doCheck = false;
307 doCheck = false;
308 propagatedBuildInputs = with self; [];
308 propagatedBuildInputs = with self; [];
309 src = fetchurl {
309 src = fetchurl {
310 url = "https://pypi.python.org/packages/71/b7/a64c71578521606edbbce15151358598f3dfb72a3431763edc2baf19e71f/ipython_genutils-0.1.0.tar.gz";
310 url = "https://pypi.python.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz";
311 md5 = "9a8afbe0978adbcbfcb3b35b2d015a56";
311 md5 = "5a4f9781f78466da0ea1a648f3e1f79f";
312 };
312 };
313 meta = {
313 meta = {
314 license = [ pkgs.lib.licenses.bsdOriginal ];
314 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -393,13 +393,13 b''
393 };
393 };
394 };
394 };
395 prompt-toolkit = super.buildPythonPackage {
395 prompt-toolkit = super.buildPythonPackage {
396 name = "prompt-toolkit-1.0.9";
396 name = "prompt-toolkit-1.0.14";
397 buildInputs = with self; [];
397 buildInputs = with self; [];
398 doCheck = false;
398 doCheck = false;
399 propagatedBuildInputs = with self; [six wcwidth];
399 propagatedBuildInputs = with self; [six wcwidth];
400 src = fetchurl {
400 src = fetchurl {
401 url = "https://pypi.python.org/packages/83/14/5ac258da6c530eca02852ee25c7a9ff3ca78287bb4c198d0d0055845d856/prompt_toolkit-1.0.9.tar.gz";
401 url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz";
402 md5 = "a39f91a54308fb7446b1a421c11f227c";
402 md5 = "f24061ae133ed32c6b764e92bd48c496";
403 };
403 };
404 meta = {
404 meta = {
405 license = [ pkgs.lib.licenses.bsdOriginal ];
405 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -588,28 +588,15 b''
588 };
588 };
589 };
589 };
590 rhodecode-vcsserver = super.buildPythonPackage {
590 rhodecode-vcsserver = super.buildPythonPackage {
591 name = "rhodecode-vcsserver-4.7.2";
591 name = "rhodecode-vcsserver-4.8.0";
592 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
592 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
593 doCheck = true;
593 doCheck = true;
594 propagatedBuildInputs = with self; [Beaker configobj decorator dulwich hgsubversion infrae.cache mercurial msgpack-python pyramid pyramid-jinja2 pyramid-mako repoze.lru simplejson subprocess32 subvertpy six translationstring WebOb wheel zope.deprecation zope.interface ipdb ipython gevent greenlet gunicorn waitress Pyro4 serpent pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage];
594 propagatedBuildInputs = with self; [Beaker configobj decorator dulwich hgsubversion hg-evolve infrae.cache mercurial msgpack-python pyramid pyramid-jinja2 pyramid-mako repoze.lru simplejson subprocess32 subvertpy six translationstring WebOb wheel zope.deprecation zope.interface ipdb ipython gevent greenlet gunicorn waitress pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage];
595 src = ./.;
595 src = ./.;
596 meta = {
596 meta = {
597 license = [ { fullName = "GPL V3"; } { fullName = "GNU General Public License v3 or later (GPLv3+)"; } ];
597 license = [ { fullName = "GPL V3"; } { fullName = "GNU General Public License v3 or later (GPLv3+)"; } ];
598 };
598 };
599 };
599 };
600 serpent = super.buildPythonPackage {
601 name = "serpent-1.15";
602 buildInputs = with self; [];
603 doCheck = false;
604 propagatedBuildInputs = with self; [];
605 src = fetchurl {
606 url = "https://pypi.python.org/packages/7b/38/b2b27673a882ff2ea5871bb3e3e6b496ebbaafd1612e51990ffb158b9254/serpent-1.15.tar.gz";
607 md5 = "e27b1aad5c218e16442f52abb7c7053a";
608 };
609 meta = {
610 license = [ pkgs.lib.licenses.mit ];
611 };
612 };
613 setuptools = super.buildPythonPackage {
600 setuptools = super.buildPythonPackage {
614 name = "setuptools-30.1.0";
601 name = "setuptools-30.1.0";
615 buildInputs = with self; [];
602 buildInputs = with self; [];
@@ -702,13 +689,13 b''
702 };
689 };
703 };
690 };
704 traitlets = super.buildPythonPackage {
691 traitlets = super.buildPythonPackage {
705 name = "traitlets-4.3.1";
692 name = "traitlets-4.3.2";
706 buildInputs = with self; [];
693 buildInputs = with self; [];
707 doCheck = false;
694 doCheck = false;
708 propagatedBuildInputs = with self; [ipython-genutils six decorator enum34];
695 propagatedBuildInputs = with self; [ipython-genutils six decorator enum34];
709 src = fetchurl {
696 src = fetchurl {
710 url = "https://pypi.python.org/packages/b1/d6/5b5aa6d5c474691909b91493da1e8972e309c9f01ecfe4aeafd272eb3234/traitlets-4.3.1.tar.gz";
697 url = "https://pypi.python.org/packages/a5/98/7f5ef2fe9e9e071813aaf9cb91d1a732e0a68b6c44a32b38cb8e14c3f069/traitlets-4.3.2.tar.gz";
711 md5 = "dd0b1b6e5d31ce446d55a4b5e5083c98";
698 md5 = "3068663f2f38fd939a9eb3a500ccc154";
712 };
699 };
713 meta = {
700 meta = {
714 license = [ pkgs.lib.licenses.bsdOriginal ];
701 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -6,6 +6,7 b' configobj==5.0.6'
6 decorator==4.0.11
6 decorator==4.0.11
7 dulwich==0.13.0
7 dulwich==0.13.0
8 hgsubversion==1.8.6
8 hgsubversion==1.8.6
9 hg-evolve==6.0.1
9 infrae.cache==1.0.1
10 infrae.cache==1.0.1
10 mercurial==4.1.2
11 mercurial==4.1.2
11 msgpack-python==0.4.8
12 msgpack-python==0.4.8
@@ -35,9 +36,5 b' greenlet==0.4.10'
35 gunicorn==19.6.0
36 gunicorn==19.6.0
36 waitress==1.0.1
37 waitress==1.0.1
37
38
38 # Pyro/Deprecated TODO(Marcink): remove in 4.7 release.
39 Pyro4==4.41
40 serpent==1.15
41
42 ## test related requirements
39 ## test related requirements
43 -r requirements_test.txt
40 -r requirements_test.txt
@@ -26,7 +26,7 b' beaker.cache.repo_object.enabled = true'
26 ### LOGGING CONFIGURATION ####
26 ### LOGGING CONFIGURATION ####
27 ################################
27 ################################
28 [loggers]
28 [loggers]
29 keys = root, vcsserver, pyro4, beaker
29 keys = root, vcsserver, beaker
30
30
31 [handlers]
31 [handlers]
32 keys = console
32 keys = console
@@ -53,12 +53,6 b' handlers ='
53 qualname = beaker
53 qualname = beaker
54 propagate = 1
54 propagate = 1
55
55
56 [logger_pyro4]
57 level = DEBUG
58 handlers =
59 qualname = Pyro4
60 propagate = 1
61
62
56
63 ##############
57 ##############
64 ## HANDLERS ##
58 ## HANDLERS ##
@@ -1,1 +1,1 b''
1 4.7.2 No newline at end of file
1 4.8.0 No newline at end of file
@@ -287,6 +287,25 b' class HgRemote(object):'
287 return [parent.rev() for parent in ctx.parents()]
287 return [parent.rev() for parent in ctx.parents()]
288
288
289 @reraise_safe_exceptions
289 @reraise_safe_exceptions
290 def ctx_phase(self, wire, revision):
291 repo = self._factory.repo(wire)
292 ctx = repo[revision]
293 # public=0, draft=1, secret=3
294 return ctx.phase()
295
296 @reraise_safe_exceptions
297 def ctx_obsolete(self, wire, revision):
298 repo = self._factory.repo(wire)
299 ctx = repo[revision]
300 return ctx.obsolete()
301
302 @reraise_safe_exceptions
303 def ctx_hidden(self, wire, revision):
304 repo = self._factory.repo(wire)
305 ctx = repo[revision]
306 return ctx.hidden()
307
308 @reraise_safe_exceptions
290 def ctx_substate(self, wire, revision):
309 def ctx_substate(self, wire, revision):
291 repo = self._factory.repo(wire)
310 repo = self._factory.repo(wire)
292 ctx = repo[revision]
311 ctx = repo[revision]
@@ -298,7 +317,7 b' class HgRemote(object):'
298 ctx = repo[revision]
317 ctx = repo[revision]
299 status = repo[ctx.p1().node()].status(other=ctx.node())
318 status = repo[ctx.p1().node()].status(other=ctx.node())
300 # object of status (odd, custom named tuple in mercurial) is not
319 # object of status (odd, custom named tuple in mercurial) is not
301 # correctly serializable via Pyro, we make it a list, as the underling
320 # correctly serializable, we make it a list, as the underling
302 # API expects this to be a list
321 # API expects this to be a list
303 return list(status)
322 return list(status)
304
323
@@ -30,7 +30,6 b' from httplib import HTTPConnection'
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33 import Pyro4
34 import simplejson as json
33 import simplejson as json
35
34
36 from vcsserver import exceptions
35 from vcsserver import exceptions
@@ -68,18 +67,9 b' class HooksDummyClient(object):'
68 return getattr(hooks, hook_name)(extras)
67 return getattr(hooks, hook_name)(extras)
69
68
70
69
71 class HooksPyro4Client(object):
72 def __init__(self, hooks_uri):
73 self.hooks_uri = hooks_uri
74
75 def __call__(self, hook_name, extras):
76 with Pyro4.Proxy(self.hooks_uri) as hooks:
77 return getattr(hooks, hook_name)(extras)
78
79
80 class RemoteMessageWriter(object):
70 class RemoteMessageWriter(object):
81 """Writer base class."""
71 """Writer base class."""
82 def write(message):
72 def write(self, message):
83 raise NotImplementedError()
73 raise NotImplementedError()
84
74
85
75
@@ -126,11 +116,7 b' def _handle_exception(result):'
126 def _get_hooks_client(extras):
116 def _get_hooks_client(extras):
127 if 'hooks_uri' in extras:
117 if 'hooks_uri' in extras:
128 protocol = extras.get('hooks_protocol')
118 protocol = extras.get('hooks_protocol')
129 return (
119 return HooksHttpClient(extras['hooks_uri'])
130 HooksHttpClient(extras['hooks_uri'])
131 if protocol == 'http'
132 else HooksPyro4Client(extras['hooks_uri'])
133 )
134 else:
120 else:
135 return HooksDummyClient(extras['hooks_module'])
121 return HooksDummyClient(extras['hooks_module'])
136
122
@@ -161,13 +147,25 b' def post_pull(ui, repo, **kwargs):'
161 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
162
148
163
149
150 def _rev_range_hash(repo, node):
151
152 commits = []
153 for rev in xrange(repo[node], len(repo)):
154 ctx = repo[rev]
155 commit_id = mercurial.node.hex(ctx.node())
156 branch = ctx.branch()
157 commits.append((commit_id, branch))
158
159 return commits
160
161
164 def pre_push(ui, repo, node=None, **kwargs):
162 def pre_push(ui, repo, node=None, **kwargs):
165 extras = _extras_from_ui(ui)
163 extras = _extras_from_ui(ui)
166
164
167 rev_data = []
165 rev_data = []
168 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
166 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
169 branches = collections.defaultdict(list)
167 branches = collections.defaultdict(list)
170 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
168 for commit_id, branch in _rev_range_hash(repo, node):
171 branches[branch].append(commit_id)
169 branches[branch].append(commit_id)
172
170
173 for branch, commits in branches.iteritems():
171 for branch, commits in branches.iteritems():
@@ -184,30 +182,38 b' def pre_push(ui, repo, node=None, **kwar'
184 return _call_hook('pre_push', extras, HgMessageWriter(ui))
182 return _call_hook('pre_push', extras, HgMessageWriter(ui))
185
183
186
184
187 def _rev_range_hash(repo, node, with_branch=False):
185 def post_push(ui, repo, node, **kwargs):
186 extras = _extras_from_ui(ui)
187
188 commit_ids = []
189 branches = []
190 bookmarks = []
191 tags = []
188
192
189 commits = []
193 for commit_id, branch in _rev_range_hash(repo, node):
190 for rev in xrange(repo[node], len(repo)):
194 commit_ids.append(commit_id)
191 ctx = repo[rev]
195 if branch not in branches:
192 commit_id = mercurial.node.hex(ctx.node())
196 branches.append(branch)
193 branch = ctx.branch()
194 if with_branch:
195 commits.append((commit_id, branch))
196 else:
197 commits.append(commit_id)
198
197
199 return commits
198 if hasattr(ui, '_rc_pushkey_branches'):
200
199 bookmarks = ui._rc_pushkey_branches
201
200
202 def post_push(ui, repo, node, **kwargs):
203 commit_ids = _rev_range_hash(repo, node)
204
205 extras = _extras_from_ui(ui)
206 extras['commit_ids'] = commit_ids
201 extras['commit_ids'] = commit_ids
202 extras['new_refs'] = {
203 'branches': branches,
204 'bookmarks': bookmarks,
205 'tags': tags
206 }
207
207
208 return _call_hook('post_push', extras, HgMessageWriter(ui))
208 return _call_hook('post_push', extras, HgMessageWriter(ui))
209
209
210
210
211 def key_push(ui, repo, **kwargs):
212 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
213 # store new bookmarks in our UI object propagated later to post_push
214 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
215 return
216
211 # backward compat
217 # backward compat
212 log_pull_action = post_pull
218 log_pull_action = post_pull
213
219
@@ -327,12 +333,12 b' def _run_command(arguments):'
327 # Probably this should be using subprocessio.
333 # Probably this should be using subprocessio.
328 process = subprocess.Popen(
334 process = subprocess.Popen(
329 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
335 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
330 stdout, _ = process.communicate()
336 stdout, stderr = process.communicate()
331
337
332 if process.returncode != 0:
338 if process.returncode != 0:
333 raise Exception(
339 raise Exception(
334 'Command %s exited with exit code %s' % (arguments,
340 'Command %s exited with exit code %s: stderr:%s' % (
335 process.returncode))
341 arguments, process.returncode, stderr))
336
342
337 return stdout
343 return stdout
338
344
@@ -359,10 +365,16 b' def git_post_receive(unused_repo_path, r'
359 # subcommand sets the PATH environment variable so that it point to the
365 # subcommand sets the PATH environment variable so that it point to the
360 # correct version of the git executable.
366 # correct version of the git executable.
361 empty_commit_id = '0' * 40
367 empty_commit_id = '0' * 40
368 branches = []
369 tags = []
362 for push_ref in rev_data:
370 for push_ref in rev_data:
363 type_ = push_ref['type']
371 type_ = push_ref['type']
372
364 if type_ == 'heads':
373 if type_ == 'heads':
365 if push_ref['old_rev'] == empty_commit_id:
374 if push_ref['old_rev'] == empty_commit_id:
375 # starting new branch case
376 if push_ref['name'] not in branches:
377 branches.append(push_ref['name'])
366
378
367 # Fix up head revision if needed
379 # Fix up head revision if needed
368 cmd = ['git', 'show', 'HEAD']
380 cmd = ['git', 'show', 'HEAD']
@@ -386,14 +398,24 b' def git_post_receive(unused_repo_path, r'
386 # delete branch case
398 # delete branch case
387 git_revs.append('delete_branch=>%s' % push_ref['name'])
399 git_revs.append('delete_branch=>%s' % push_ref['name'])
388 else:
400 else:
401 if push_ref['name'] not in branches:
402 branches.append(push_ref['name'])
403
389 cmd = ['git', 'log',
404 cmd = ['git', 'log',
390 '{old_rev}..{new_rev}'.format(**push_ref),
405 '{old_rev}..{new_rev}'.format(**push_ref),
391 '--reverse', '--pretty=format:%H']
406 '--reverse', '--pretty=format:%H']
392 git_revs.extend(_run_command(cmd).splitlines())
407 git_revs.extend(_run_command(cmd).splitlines())
393 elif type_ == 'tags':
408 elif type_ == 'tags':
409 if push_ref['name'] not in tags:
410 tags.append(push_ref['name'])
394 git_revs.append('tag=>%s' % push_ref['name'])
411 git_revs.append('tag=>%s' % push_ref['name'])
395
412
396 extras['commit_ids'] = git_revs
413 extras['commit_ids'] = git_revs
414 extras['new_refs'] = {
415 'branches': branches,
416 'bookmarks': [],
417 'tags': tags,
418 }
397
419
398 if 'repo_size' in extras['hooks']:
420 if 'repo_size' in extras['hooks']:
399 try:
421 try:
@@ -95,7 +95,13 b' class GitRepository(object):'
95
95
96 :param path:
96 :param path:
97 """
97 """
98 return path.split(self.repo_name, 1)[-1].strip('/')
98 path = path.split(self.repo_name, 1)[-1]
99 if path.startswith('.git'):
100 # for bare repos we still get the .git prefix inside, we skip it
101 # here, and remove from the service command
102 path = path[4:]
103
104 return path.strip('/')
99
105
100 def inforefs(self, request, unused_environ):
106 def inforefs(self, request, unused_environ):
101 """
107 """
@@ -15,16 +15,5 b''
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
19 PYRO_PORT = 9900
20
21 PYRO_GIT = 'git_remote'
22 PYRO_HG = 'hg_remote'
23 PYRO_SVN = 'svn_remote'
24 PYRO_VCSSERVER = 'vcs_server'
25 PYRO_GIT_REMOTE_WSGI = 'git_remote_wsgi'
26 PYRO_HG_REMOTE_WSGI = 'hg_remote_wsgi'
27
28 WIRE_ENCODING = 'UTF-8'
18 WIRE_ENCODING = 'UTF-8'
29
30 GIT_EXECUTABLE = 'git'
19 GIT_EXECUTABLE = 'git'
@@ -23,6 +23,7 b' import posixpath as vcspath'
23 import StringIO
23 import StringIO
24 import subprocess
24 import subprocess
25 import urllib
25 import urllib
26 import traceback
26
27
27 import svn.client
28 import svn.client
28 import svn.core
29 import svn.core
@@ -46,8 +47,17 b' svn_compatible_versions = set(['
46 'pre-1.5-compatible',
47 'pre-1.5-compatible',
47 'pre-1.6-compatible',
48 'pre-1.6-compatible',
48 'pre-1.8-compatible',
49 'pre-1.8-compatible',
50 'pre-1.9-compatible',
49 ])
51 ])
50
52
53 svn_compatible_versions_map = {
54 'pre-1.4-compatible': '1.3',
55 'pre-1.5-compatible': '1.4',
56 'pre-1.6-compatible': '1.5',
57 'pre-1.8-compatible': '1.7',
58 'pre-1.9-compatible': '1.8',
59 }
60
51
61
52 def reraise_safe_exceptions(func):
62 def reraise_safe_exceptions(func):
53 """Decorator for converting svn exceptions to something neutral."""
63 """Decorator for converting svn exceptions to something neutral."""
@@ -67,14 +77,15 b' class SubversionFactory(RepoFactory):'
67 def _create_repo(self, wire, create, compatible_version):
77 def _create_repo(self, wire, create, compatible_version):
68 path = svn.core.svn_path_canonicalize(wire['path'])
78 path = svn.core.svn_path_canonicalize(wire['path'])
69 if create:
79 if create:
70 fs_config = {}
80 fs_config = {'compatible-version': '1.9'}
71 if compatible_version:
81 if compatible_version:
72 if compatible_version not in svn_compatible_versions:
82 if compatible_version not in svn_compatible_versions:
73 raise Exception('Unknown SVN compatible version "{}"'
83 raise Exception('Unknown SVN compatible version "{}"'
74 .format(compatible_version))
84 .format(compatible_version))
75 log.debug('Create SVN repo with compatible version "%s"',
85 fs_config['compatible-version'] = \
76 compatible_version)
86 svn_compatible_versions_map[compatible_version]
77 fs_config[compatible_version] = '1'
87
88 log.debug('Create SVN repo with config "%s"', fs_config)
78 repo = svn.repos.create(path, "", "", None, fs_config)
89 repo = svn.repos.create(path, "", "", None, fs_config)
79 else:
90 else:
80 repo = svn.repos.open(path)
91 repo = svn.repos.open(path)
@@ -87,7 +98,6 b' class SubversionFactory(RepoFactory):'
87 return self._repo(wire, create_new_repo)
98 return self._repo(wire, create_new_repo)
88
99
89
100
90
91 NODE_TYPE_MAPPING = {
101 NODE_TYPE_MAPPING = {
92 svn.core.svn_node_file: 'file',
102 svn.core.svn_node_file: 'file',
93 svn.core.svn_node_dir: 'dir',
103 svn.core.svn_node_dir: 'dir',
@@ -120,8 +130,9 b' class SvnRemote(object):'
120 # throws exception
130 # throws exception
121 try:
131 try:
122 svnrepo.svnremoterepo(baseui, url).svn.uuid
132 svnrepo.svnremoterepo(baseui, url).svn.uuid
123 except:
133 except Exception:
124 log.debug("Invalid svn url: %s", url)
134 tb = traceback.format_exc()
135 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
125 raise URLError(
136 raise URLError(
126 '"%s" is not a valid Subversion source url.' % (url, ))
137 '"%s" is not a valid Subversion source url.' % (url, ))
127 return True
138 return True
@@ -130,10 +141,23 b' class SvnRemote(object):'
130 try:
141 try:
131 svn.repos.open(path)
142 svn.repos.open(path)
132 except svn.core.SubversionException:
143 except svn.core.SubversionException:
133 log.debug("Invalid Subversion path %s", path)
144 tb = traceback.format_exc()
145 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
134 return False
146 return False
135 return True
147 return True
136
148
149 @reraise_safe_exceptions
150 def verify(self, wire,):
151 repo_path = wire['path']
152 if not self.is_path_valid_repository(wire, repo_path):
153 raise Exception(
154 "Path %s is not a valid Subversion repository." % repo_path)
155
156 load = subprocess.Popen(
157 ['svnadmin', 'info', repo_path],
158 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
159 return ''.join(load.stdout)
160
137 def lookup(self, wire, revision):
161 def lookup(self, wire, revision):
138 if revision not in [-1, None, 'HEAD']:
162 if revision not in [-1, None, 'HEAD']:
139 raise NotImplementedError
163 raise NotImplementedError
@@ -29,46 +29,6 b' import simplejson as json'
29 from vcsserver import hooks
29 from vcsserver import hooks
30
30
31
31
32 class HooksStub(object):
33 """
34 Simulates a Proy4.Proxy object.
35
36 Will always return `result`, no matter which hook has been called on it.
37 """
38
39 def __init__(self, result):
40 self._result = result
41
42 def __call__(self, hooks_uri):
43 return self
44
45 def __enter__(self):
46 return self
47
48 def __exit__(self, exc_type, exc_value, traceback):
49 pass
50
51 def __getattr__(self, name):
52 return mock.Mock(return_value=self._result)
53
54
55 @contextlib.contextmanager
56 def mock_hook_response(
57 status=0, output='', exception=None, exception_args=None):
58 response = {
59 'status': status,
60 'output': output,
61 }
62 if exception:
63 response.update({
64 'exception': exception,
65 'exception_args': exception_args,
66 })
67
68 with mock.patch('Pyro4.Proxy', HooksStub(response)):
69 yield
70
71
72 def get_hg_ui(extras=None):
32 def get_hg_ui(extras=None):
73 """Create a Config object with a valid RC_SCM_DATA entry."""
33 """Create a Config object with a valid RC_SCM_DATA entry."""
74 extras = extras or {}
34 extras = extras or {}
@@ -89,126 +49,6 b' def get_hg_ui(extras=None):'
89 return hg_ui
49 return hg_ui
90
50
91
51
92 def test_call_hook_no_error(capsys):
93 extras = {
94 'hooks_uri': 'fake_hook_uri',
95 }
96 expected_output = 'My mock outptut'
97 writer = mock.Mock()
98
99 with mock_hook_response(status=1, output=expected_output):
100 hooks._call_hook('hook_name', extras, writer)
101
102 out, err = capsys.readouterr()
103
104 writer.write.assert_called_with(expected_output)
105 assert err == ''
106
107
108 def test_call_hook_with_exception(capsys):
109 extras = {
110 'hooks_uri': 'fake_hook_uri',
111 }
112 expected_output = 'My mock outptut'
113 writer = mock.Mock()
114
115 with mock_hook_response(status=1, output=expected_output,
116 exception='TypeError',
117 exception_args=('Mock exception', )):
118 with pytest.raises(Exception) as excinfo:
119 hooks._call_hook('hook_name', extras, writer)
120
121 assert excinfo.type == Exception
122 assert 'Mock exception' in str(excinfo.value)
123
124 out, err = capsys.readouterr()
125
126 writer.write.assert_called_with(expected_output)
127 assert err == ''
128
129
130 def test_call_hook_with_locked_exception(capsys):
131 extras = {
132 'hooks_uri': 'fake_hook_uri',
133 }
134 expected_output = 'My mock outptut'
135 writer = mock.Mock()
136
137 with mock_hook_response(status=1, output=expected_output,
138 exception='HTTPLockedRC',
139 exception_args=('message',)):
140 with pytest.raises(Exception) as excinfo:
141 hooks._call_hook('hook_name', extras, writer)
142
143 assert excinfo.value._vcs_kind == 'repo_locked'
144 assert 'message' == str(excinfo.value)
145
146 out, err = capsys.readouterr()
147
148 writer.write.assert_called_with(expected_output)
149 assert err == ''
150
151
152 def test_call_hook_with_stdout():
153 extras = {
154 'hooks_uri': 'fake_hook_uri',
155 }
156 expected_output = 'My mock outptut'
157
158 stdout = io.BytesIO()
159 with mock_hook_response(status=1, output=expected_output):
160 hooks._call_hook('hook_name', extras, stdout)
161
162 assert stdout.getvalue() == expected_output
163
164
165 def test_repo_size():
166 hg_ui = get_hg_ui()
167
168 with mock_hook_response(status=1):
169 assert hooks.repo_size(hg_ui, None) == 1
170
171
172 def test_pre_pull():
173 hg_ui = get_hg_ui()
174
175 with mock_hook_response(status=1):
176 assert hooks.pre_pull(hg_ui, None) == 1
177
178
179 def test_post_pull():
180 hg_ui = get_hg_ui()
181
182 with mock_hook_response(status=1):
183 assert hooks.post_pull(hg_ui, None) == 1
184
185
186 def test_pre_push():
187 hg_ui = get_hg_ui()
188
189 with mock_hook_response(status=1):
190 assert hooks.pre_push(hg_ui, None) == 1
191
192
193 def test_post_push():
194 hg_ui = get_hg_ui()
195
196 with mock_hook_response(status=1):
197 with mock.patch('vcsserver.hooks._rev_range_hash', return_value=[]):
198 assert hooks.post_push(hg_ui, None, None) == 1
199
200
201 def test_git_pre_receive():
202 extras = {
203 'hooks': ['push'],
204 'hooks_uri': 'fake_hook_uri',
205 }
206 with mock_hook_response(status=1):
207 response = hooks.git_pre_receive(None, None,
208 {'RC_SCM_DATA': json.dumps(extras)})
209 assert response == 1
210
211
212 def test_git_pre_receive_is_disabled():
52 def test_git_pre_receive_is_disabled():
213 extras = {'hooks': ['pull']}
53 extras = {'hooks': ['pull']}
214 response = hooks.git_pre_receive(None, None,
54 response = hooks.git_pre_receive(None, None,
@@ -217,18 +57,6 b' def test_git_pre_receive_is_disabled():'
217 assert response == 0
57 assert response == 0
218
58
219
59
220 def test_git_post_receive_no_subprocess_call():
221 extras = {
222 'hooks': ['push'],
223 'hooks_uri': 'fake_hook_uri',
224 }
225 # Setting revision_lines to '' avoid all subprocess_calls
226 with mock_hook_response(status=1):
227 response = hooks.git_post_receive(None, '',
228 {'RC_SCM_DATA': json.dumps(extras)})
229 assert response == 1
230
231
232 def test_git_post_receive_is_disabled():
60 def test_git_post_receive_is_disabled():
233 extras = {'hooks': ['pull']}
61 extras = {'hooks': ['pull']}
234 response = hooks.git_post_receive(None, '',
62 response = hooks.git_post_receive(None, '',
@@ -242,7 +70,8 b' def test_git_post_receive_calls_repo_siz'
242 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
243 hooks.git_post_receive(
71 hooks.git_post_receive(
244 None, '', {'RC_SCM_DATA': json.dumps(extras)})
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
245 extras.update({'commit_ids': []})
73 extras.update({'commit_ids': [],
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
246 expected_calls = [
75 expected_calls = [
247 mock.call('repo_size', extras, mock.ANY),
76 mock.call('repo_size', extras, mock.ANY),
248 mock.call('post_push', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
@@ -255,7 +84,8 b' def test_git_post_receive_does_not_call_'
255 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
256 hooks.git_post_receive(
85 hooks.git_post_receive(
257 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
258 extras.update({'commit_ids': []})
87 extras.update({'commit_ids': [],
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
259 expected_calls = [
89 expected_calls = [
260 mock.call('post_push', extras, mock.ANY)
90 mock.call('post_push', extras, mock.ANY)
261 ]
91 ]
@@ -279,122 +109,16 b' def test_repo_size_exception_does_not_af'
279 assert result == status
109 assert result == status
280
110
281
111
282 @mock.patch('vcsserver.hooks._run_command')
283 def test_git_post_receive_first_commit_sub_branch(cmd_mock):
284 def cmd_mock_returns(args):
285 if args == ['git', 'show', 'HEAD']:
286 raise
287 if args == ['git', 'for-each-ref', '--format=%(refname)',
288 'refs/heads/*']:
289 return 'refs/heads/test-branch2/sub-branch'
290 if args == ['git', 'log', '--reverse', '--pretty=format:%H', '--',
291 '9695eef57205c17566a3ae543be187759b310bb7', '--not',
292 'refs/heads/test-branch2/sub-branch']:
293 return ''
294
295 cmd_mock.side_effect = cmd_mock_returns
296
297 extras = {
298 'hooks': ['push'],
299 'hooks_uri': 'fake_hook_uri'
300 }
301 rev_lines = ['0000000000000000000000000000000000000000 '
302 '9695eef57205c17566a3ae543be187759b310bb7 '
303 'refs/heads/feature/sub-branch\n']
304 with mock_hook_response(status=0):
305 response = hooks.git_post_receive(None, rev_lines,
306 {'RC_SCM_DATA': json.dumps(extras)})
307
308 calls = [
309 mock.call(['git', 'show', 'HEAD']),
310 mock.call(['git', 'symbolic-ref', 'HEAD',
311 'refs/heads/feature/sub-branch']),
312 ]
313 cmd_mock.assert_has_calls(calls, any_order=True)
314 assert response == 0
315
316
317 @mock.patch('vcsserver.hooks._run_command')
318 def test_git_post_receive_first_commit_revs(cmd_mock):
319 extras = {
320 'hooks': ['push'],
321 'hooks_uri': 'fake_hook_uri'
322 }
323 rev_lines = [
324 '0000000000000000000000000000000000000000 '
325 '9695eef57205c17566a3ae543be187759b310bb7 refs/heads/master\n']
326 with mock_hook_response(status=0):
327 response = hooks.git_post_receive(
328 None, rev_lines, {'RC_SCM_DATA': json.dumps(extras)})
329
330 calls = [
331 mock.call(['git', 'show', 'HEAD']),
332 mock.call(['git', 'for-each-ref', '--format=%(refname)',
333 'refs/heads/*']),
334 mock.call(['git', 'log', '--reverse', '--pretty=format:%H',
335 '--', '9695eef57205c17566a3ae543be187759b310bb7', '--not',
336 ''])
337 ]
338 cmd_mock.assert_has_calls(calls, any_order=True)
339
340 assert response == 0
341
342
343 def test_git_pre_pull():
344 extras = {
345 'hooks': ['pull'],
346 'hooks_uri': 'fake_hook_uri',
347 }
348 with mock_hook_response(status=1, output='foo'):
349 assert hooks.git_pre_pull(extras) == hooks.HookResponse(1, 'foo')
350
351
352 def test_git_pre_pull_exception_is_caught():
353 extras = {
354 'hooks': ['pull'],
355 'hooks_uri': 'fake_hook_uri',
356 }
357 with mock_hook_response(status=2, exception=Exception('foo')):
358 assert hooks.git_pre_pull(extras).status == 128
359
360
361 def test_git_pre_pull_is_disabled():
112 def test_git_pre_pull_is_disabled():
362 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
363
114
364
115
365 def test_git_post_pull():
366 extras = {
367 'hooks': ['pull'],
368 'hooks_uri': 'fake_hook_uri',
369 }
370 with mock_hook_response(status=1, output='foo'):
371 assert hooks.git_post_pull(extras) == hooks.HookResponse(1, 'foo')
372
373
374 def test_git_post_pull_exception_is_caught():
375 extras = {
376 'hooks': ['pull'],
377 'hooks_uri': 'fake_hook_uri',
378 }
379 with mock_hook_response(status=2, exception='Exception',
380 exception_args=('foo',)):
381 assert hooks.git_post_pull(extras).status == 128
382
383
384 def test_git_post_pull_is_disabled():
116 def test_git_post_pull_is_disabled():
385 assert (
117 assert (
386 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
387
119
388
120
389 class TestGetHooksClient(object):
121 class TestGetHooksClient(object):
390 def test_returns_pyro_client_when_protocol_matches(self):
391 hooks_uri = 'localhost:8000'
392 result = hooks._get_hooks_client({
393 'hooks_uri': hooks_uri,
394 'hooks_protocol': 'pyro4'
395 })
396 assert isinstance(result, hooks.HooksPyro4Client)
397 assert result.hooks_uri == hooks_uri
398
122
399 def test_returns_http_client_when_protocol_matches(self):
123 def test_returns_http_client_when_protocol_matches(self):
400 hooks_uri = 'localhost:8000'
124 hooks_uri = 'localhost:8000'
@@ -405,14 +129,6 b' class TestGetHooksClient(object):'
405 assert isinstance(result, hooks.HooksHttpClient)
129 assert isinstance(result, hooks.HooksHttpClient)
406 assert result.hooks_uri == hooks_uri
130 assert result.hooks_uri == hooks_uri
407
131
408 def test_returns_pyro4_client_when_no_protocol_is_specified(self):
409 hooks_uri = 'localhost:8000'
410 result = hooks._get_hooks_client({
411 'hooks_uri': hooks_uri
412 })
413 assert isinstance(result, hooks.HooksPyro4Client)
414 assert result.hooks_uri == hooks_uri
415
416 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
417 fake_module = mock.Mock()
133 fake_module = mock.Mock()
418 import_patcher = mock.patch.object(
134 import_patcher = mock.patch.object(
@@ -487,30 +203,6 b' class TestHooksDummyClient(object):'
487 assert result == hooks_module.Hooks().__enter__().post_push()
203 assert result == hooks_module.Hooks().__enter__().post_push()
488
204
489
205
490 class TestHooksPyro4Client(object):
491 def test_init_sets_hooks_uri(self):
492 uri = 'localhost:3000'
493 client = hooks.HooksPyro4Client(uri)
494 assert client.hooks_uri == uri
495
496 def test_call_returns_hook_value(self):
497 hooks_uri = 'localhost:3000'
498 client = hooks.HooksPyro4Client(hooks_uri)
499 hooks_module = mock.Mock()
500 context_manager = mock.MagicMock()
501 context_manager.__enter__.return_value = hooks_module
502 pyro4_patcher = mock.patch.object(
503 hooks.Pyro4, 'Proxy', return_value=context_manager)
504 extras = {
505 'test': 'test'
506 }
507 with pyro4_patcher as pyro4_mock:
508 result = client('post_push', extras)
509 pyro4_mock.assert_called_once_with(hooks_uri)
510 hooks_module.post_push.assert_called_once_with(extras)
511 assert result == hooks_module.post_push.return_value
512
513
514 @pytest.fixture
206 @pytest.fixture
515 def http_mirror(request):
207 def http_mirror(request):
516 server = MirrorHttpServer()
208 server = MirrorHttpServer()
@@ -18,24 +18,24 b''
18 import mock
18 import mock
19 import pytest
19 import pytest
20
20
21 from vcsserver import main
21 from vcsserver import http_main
22 from vcsserver.base import obfuscate_qs
22 from vcsserver.base import obfuscate_qs
23
23
24
24
25 @mock.patch('vcsserver.main.VcsServerCommand', mock.Mock())
25 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
26 @mock.patch('vcsserver.hgpatches.patch_largefiles_capabilities')
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
27 def test_applies_largefiles_patch(patch_largefiles_capabilities):
28 main.main([])
28 http_main.main([])
29 patch_largefiles_capabilities.assert_called_once_with()
29 patch_largefiles_capabilities.assert_called_once_with()
30
30
31
31
32 @mock.patch('vcsserver.main.VcsServerCommand', mock.Mock())
32 @mock.patch('vcsserver.http_main.VCS', mock.Mock())
33 @mock.patch('vcsserver.main.MercurialFactory', None)
33 @mock.patch('vcsserver.http_main.MercurialFactory', None)
34 @mock.patch(
34 @mock.patch(
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
35 'vcsserver.hgpatches.patch_largefiles_capabilities',
36 mock.Mock(side_effect=Exception("Must not be called")))
36 mock.Mock(side_effect=Exception("Must not be called")))
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
37 def test_applies_largefiles_patch_only_if_mercurial_is_available():
38 main.main([])
38 http_main.main([])
39
39
40
40
41 @pytest.mark.parametrize('given, expected', [
41 @pytest.mark.parametrize('given, expected', [
@@ -46,7 +46,7 b' class RequestWrapperTween(object):'
46 finally:
46 finally:
47 end = time.time()
47 end = time.time()
48
48
49 log.info('IP: %s Request to %s time: %.3fs' % (
49 log.info('IP: %s Request to path: `%s` time: %.3fs' % (
50 '127.0.0.1',
50 '127.0.0.1',
51 safe_str(get_access_path(request)), end - start)
51 safe_str(get_access_path(request)), end - start)
52 )
52 )
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (508 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now