##// END OF EJS Templates
hg: fixed code after version upgrade to 4.6.0 release
marcink -
r432:92b33b73 default
parent child Browse files
Show More
@@ -1,776 +1,789 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import logging
20 20 import stat
21 21 import urllib
22 22 import urllib2
23 23
24 24 from hgext import largefiles, rebase
25 25 from hgext.strip import strip as hgext_strip
26 26 from mercurial import commands
27 27 from mercurial import unionrepo
28 28 from mercurial import verify
29 29
30 30 from vcsserver import exceptions
31 31 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
32 32 from vcsserver.hgcompat import (
33 33 archival, bin, clone, config as hgconfig, diffopts, hex,
34 34 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
35 35 httppeer, localrepository, match, memctx, exchange, memfilectx, nullrev,
36 36 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
37 37 RepoLookupError, InterventionRequired, RequirementError)
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 def make_ui_from_config(repo_config):
43 43 baseui = ui.ui()
44 44
45 45 # clean the baseui object
46 46 baseui._ocfg = hgconfig.config()
47 47 baseui._ucfg = hgconfig.config()
48 48 baseui._tcfg = hgconfig.config()
49 49
50 50 for section, option, value in repo_config:
51 51 baseui.setconfig(section, option, value)
52 52
53 53 # make our hgweb quiet so it doesn't print output
54 54 baseui.setconfig('ui', 'quiet', 'true')
55 55
56 56 baseui.setconfig('ui', 'paginate', 'never')
57 57 # force mercurial to only use 1 thread, otherwise it may try to set a
58 58 # signal in a non-main thread, thus generating a ValueError.
59 59 baseui.setconfig('worker', 'numcpus', 1)
60 60
61 61 # If there is no config for the largefiles extension, we explicitly disable
62 62 # it here. This overrides settings from repositories hgrc file. Recent
63 63 # mercurial versions enable largefiles in hgrc on clone from largefile
64 64 # repo.
65 65 if not baseui.hasconfig('extensions', 'largefiles'):
66 66 log.debug('Explicitly disable largefiles extension for repo.')
67 67 baseui.setconfig('extensions', 'largefiles', '!')
68 68
69 69 return baseui
70 70
71 71
72 72 def reraise_safe_exceptions(func):
73 73 """Decorator for converting mercurial exceptions to something neutral."""
74 74 def wrapper(*args, **kwargs):
75 75 try:
76 76 return func(*args, **kwargs)
77 77 except (Abort, InterventionRequired):
78 78 raise_from_original(exceptions.AbortException)
79 79 except RepoLookupError:
80 80 raise_from_original(exceptions.LookupException)
81 81 except RequirementError:
82 82 raise_from_original(exceptions.RequirementException)
83 83 except RepoError:
84 84 raise_from_original(exceptions.VcsException)
85 85 except LookupError:
86 86 raise_from_original(exceptions.LookupException)
87 87 except Exception as e:
88 88 if not hasattr(e, '_vcs_kind'):
89 89 log.exception("Unhandled exception in hg remote call")
90 90 raise_from_original(exceptions.UnhandledException)
91 91 raise
92 92 return wrapper
93 93
94 94
95 95 class MercurialFactory(RepoFactory):
96 96
97 97 def _create_config(self, config, hooks=True):
98 98 if not hooks:
99 99 hooks_to_clean = frozenset((
100 100 'changegroup.repo_size', 'preoutgoing.pre_pull',
101 101 'outgoing.pull_logger', 'prechangegroup.pre_push'))
102 102 new_config = []
103 103 for section, option, value in config:
104 104 if section == 'hooks' and option in hooks_to_clean:
105 105 continue
106 106 new_config.append((section, option, value))
107 107 config = new_config
108 108
109 109 baseui = make_ui_from_config(config)
110 110 return baseui
111 111
112 112 def _create_repo(self, wire, create):
113 113 baseui = self._create_config(wire["config"])
114 114 return localrepository(baseui, wire["path"], create)
115 115
116 116
117 117 class HgRemote(object):
118 118
119 119 def __init__(self, factory):
120 120 self._factory = factory
121 121
122 122 self._bulk_methods = {
123 123 "affected_files": self.ctx_files,
124 124 "author": self.ctx_user,
125 125 "branch": self.ctx_branch,
126 126 "children": self.ctx_children,
127 127 "date": self.ctx_date,
128 128 "message": self.ctx_description,
129 129 "parents": self.ctx_parents,
130 130 "status": self.ctx_status,
131 131 "obsolete": self.ctx_obsolete,
132 132 "phase": self.ctx_phase,
133 133 "hidden": self.ctx_hidden,
134 134 "_file_paths": self.ctx_list,
135 135 }
136 136
137 137 @reraise_safe_exceptions
138 138 def discover_hg_version(self):
139 139 from mercurial import util
140 140 return util.version()
141 141
142 142 @reraise_safe_exceptions
143 143 def archive_repo(self, archive_path, mtime, file_info, kind):
144 144 if kind == "tgz":
145 145 archiver = archival.tarit(archive_path, mtime, "gz")
146 146 elif kind == "tbz2":
147 147 archiver = archival.tarit(archive_path, mtime, "bz2")
148 148 elif kind == 'zip':
149 149 archiver = archival.zipit(archive_path, mtime)
150 150 else:
151 151 raise exceptions.ArchiveException(
152 152 'Remote does not support: "%s".' % kind)
153 153
154 154 for f_path, f_mode, f_is_link, f_content in file_info:
155 155 archiver.addfile(f_path, f_mode, f_is_link, f_content)
156 156 archiver.done()
157 157
158 158 @reraise_safe_exceptions
159 159 def bookmarks(self, wire):
160 160 repo = self._factory.repo(wire)
161 161 return dict(repo._bookmarks)
162 162
163 163 @reraise_safe_exceptions
164 164 def branches(self, wire, normal, closed):
165 165 repo = self._factory.repo(wire)
166 166 iter_branches = repo.branchmap().iterbranches()
167 167 bt = {}
168 168 for branch_name, _heads, tip, is_closed in iter_branches:
169 169 if normal and not is_closed:
170 170 bt[branch_name] = tip
171 171 if closed and is_closed:
172 172 bt[branch_name] = tip
173 173
174 174 return bt
175 175
176 176 @reraise_safe_exceptions
177 177 def bulk_request(self, wire, rev, pre_load):
178 178 result = {}
179 179 for attr in pre_load:
180 180 try:
181 181 method = self._bulk_methods[attr]
182 182 result[attr] = method(wire, rev)
183 183 except KeyError:
184 184 raise exceptions.VcsException(
185 185 'Unknown bulk attribute: "%s"' % attr)
186 186 return result
187 187
188 188 @reraise_safe_exceptions
189 189 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
190 190 baseui = self._factory._create_config(wire["config"], hooks=hooks)
191 191 clone(baseui, source, dest, noupdate=not update_after_clone)
192 192
193 193 @reraise_safe_exceptions
194 194 def commitctx(
195 195 self, wire, message, parents, commit_time, commit_timezone,
196 196 user, files, extra, removed, updated):
197 197
198 198 def _filectxfn(_repo, memctx, path):
199 199 """
200 200 Marks given path as added/changed/removed in a given _repo. This is
201 201 for internal mercurial commit function.
202 202 """
203 203
204 204 # check if this path is removed
205 205 if path in removed:
206 206 # returning None is a way to mark node for removal
207 207 return None
208 208
209 209 # check if this path is added
210 210 for node in updated:
211 211 if node['path'] == path:
212 212 return memfilectx(
213 213 _repo,
214 changectx=memctx,
214 215 path=node['path'],
215 216 data=node['content'],
216 217 islink=False,
217 218 isexec=bool(node['mode'] & stat.S_IXUSR),
218 copied=False,
219 memctx=memctx)
219 copied=False)
220 220
221 221 raise exceptions.AbortException(
222 222 "Given path haven't been marked as added, "
223 223 "changed or removed (%s)" % path)
224 224
225 225 repo = self._factory.repo(wire)
226 226
227 227 commit_ctx = memctx(
228 228 repo=repo,
229 229 parents=parents,
230 230 text=message,
231 231 files=files,
232 232 filectxfn=_filectxfn,
233 233 user=user,
234 234 date=(commit_time, commit_timezone),
235 235 extra=extra)
236 236
237 237 n = repo.commitctx(commit_ctx)
238 238 new_id = hex(n)
239 239
240 240 return new_id
241 241
242 242 @reraise_safe_exceptions
243 243 def ctx_branch(self, wire, revision):
244 244 repo = self._factory.repo(wire)
245 245 ctx = repo[revision]
246 246 return ctx.branch()
247 247
248 248 @reraise_safe_exceptions
249 249 def ctx_children(self, wire, revision):
250 250 repo = self._factory.repo(wire)
251 251 ctx = repo[revision]
252 252 return [child.rev() for child in ctx.children()]
253 253
254 254 @reraise_safe_exceptions
255 255 def ctx_date(self, wire, revision):
256 256 repo = self._factory.repo(wire)
257 257 ctx = repo[revision]
258 258 return ctx.date()
259 259
260 260 @reraise_safe_exceptions
261 261 def ctx_description(self, wire, revision):
262 262 repo = self._factory.repo(wire)
263 263 ctx = repo[revision]
264 264 return ctx.description()
265 265
266 266 @reraise_safe_exceptions
267 267 def ctx_diff(
268 268 self, wire, revision, git=True, ignore_whitespace=True, context=3):
269 269 repo = self._factory.repo(wire)
270 270 ctx = repo[revision]
271 271 result = ctx.diff(
272 272 git=git, ignore_whitespace=ignore_whitespace, context=context)
273 273 return list(result)
274 274
275 275 @reraise_safe_exceptions
276 276 def ctx_files(self, wire, revision):
277 277 repo = self._factory.repo(wire)
278 278 ctx = repo[revision]
279 279 return ctx.files()
280 280
281 281 @reraise_safe_exceptions
282 282 def ctx_list(self, path, revision):
283 283 repo = self._factory.repo(path)
284 284 ctx = repo[revision]
285 285 return list(ctx)
286 286
287 287 @reraise_safe_exceptions
288 288 def ctx_parents(self, wire, revision):
289 289 repo = self._factory.repo(wire)
290 290 ctx = repo[revision]
291 291 return [parent.rev() for parent in ctx.parents()]
292 292
293 293 @reraise_safe_exceptions
294 294 def ctx_phase(self, wire, revision):
295 295 repo = self._factory.repo(wire)
296 296 ctx = repo[revision]
297 297 # public=0, draft=1, secret=3
298 298 return ctx.phase()
299 299
300 300 @reraise_safe_exceptions
301 301 def ctx_obsolete(self, wire, revision):
302 302 repo = self._factory.repo(wire)
303 303 ctx = repo[revision]
304 304 return ctx.obsolete()
305 305
306 306 @reraise_safe_exceptions
307 307 def ctx_hidden(self, wire, revision):
308 308 repo = self._factory.repo(wire)
309 309 ctx = repo[revision]
310 310 return ctx.hidden()
311 311
312 312 @reraise_safe_exceptions
313 313 def ctx_substate(self, wire, revision):
314 314 repo = self._factory.repo(wire)
315 315 ctx = repo[revision]
316 316 return ctx.substate
317 317
318 318 @reraise_safe_exceptions
319 319 def ctx_status(self, wire, revision):
320 320 repo = self._factory.repo(wire)
321 321 ctx = repo[revision]
322 322 status = repo[ctx.p1().node()].status(other=ctx.node())
323 323 # object of status (odd, custom named tuple in mercurial) is not
324 324 # correctly serializable, we make it a list, as the underling
325 325 # API expects this to be a list
326 326 return list(status)
327 327
328 328 @reraise_safe_exceptions
329 329 def ctx_user(self, wire, revision):
330 330 repo = self._factory.repo(wire)
331 331 ctx = repo[revision]
332 332 return ctx.user()
333 333
334 334 @reraise_safe_exceptions
335 335 def check_url(self, url, config):
336 336 _proto = None
337 337 if '+' in url[:url.find('://')]:
338 338 _proto = url[0:url.find('+')]
339 339 url = url[url.find('+') + 1:]
340 340 handlers = []
341 341 url_obj = url_parser(url)
342 342 test_uri, authinfo = url_obj.authinfo()
343 343 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
344 344 url_obj.query = obfuscate_qs(url_obj.query)
345 345
346 346 cleaned_uri = str(url_obj)
347 347 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
348 348
349 349 if authinfo:
350 350 # create a password manager
351 351 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
352 352 passmgr.add_password(*authinfo)
353 353
354 354 handlers.extend((httpbasicauthhandler(passmgr),
355 355 httpdigestauthhandler(passmgr)))
356 356
357 357 o = urllib2.build_opener(*handlers)
358 358 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
359 359 ('Accept', 'application/mercurial-0.1')]
360 360
361 361 q = {"cmd": 'between'}
362 362 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
363 363 qs = '?%s' % urllib.urlencode(q)
364 364 cu = "%s%s" % (test_uri, qs)
365 365 req = urllib2.Request(cu, None, {})
366 366
367 367 try:
368 368 log.debug("Trying to open URL %s", cleaned_uri)
369 369 resp = o.open(req)
370 370 if resp.code != 200:
371 371 raise exceptions.URLError('Return Code is not 200')
372 372 except Exception as e:
373 373 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
374 374 # means it cannot be cloned
375 375 raise exceptions.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
376 376
377 377 # now check if it's a proper hg repo, but don't do it for svn
378 378 try:
379 379 if _proto == 'svn':
380 380 pass
381 381 else:
382 382 # check for pure hg repos
383 383 log.debug(
384 384 "Verifying if URL is a Mercurial repository: %s",
385 385 cleaned_uri)
386 386 httppeer(make_ui_from_config(config), url).lookup('tip')
387 387 except Exception as e:
388 388 log.warning("URL is not a valid Mercurial repository: %s",
389 389 cleaned_uri)
390 390 raise exceptions.URLError(
391 391 "url [%s] does not look like an hg repo org_exc: %s"
392 392 % (cleaned_uri, e))
393 393
394 394 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
395 395 return True
396 396
397 397 @reraise_safe_exceptions
398 398 def diff(
399 399 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
400 400 context):
401 401 repo = self._factory.repo(wire)
402 402
403 403 if file_filter:
404 404 match_filter = match(file_filter[0], '', [file_filter[1]])
405 405 else:
406 406 match_filter = file_filter
407 407 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
408 408
409 409 try:
410 410 return "".join(patch.diff(
411 411 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
412 412 except RepoLookupError:
413 413 raise exceptions.LookupException()
414 414
415 415 @reraise_safe_exceptions
416 416 def file_history(self, wire, revision, path, limit):
417 417 repo = self._factory.repo(wire)
418 418
419 419 ctx = repo[revision]
420 420 fctx = ctx.filectx(path)
421 421
422 422 def history_iter():
423 423 limit_rev = fctx.rev()
424 424 for obj in reversed(list(fctx.filelog())):
425 425 obj = fctx.filectx(obj)
426 426 if limit_rev >= obj.rev():
427 427 yield obj
428 428
429 429 history = []
430 430 for cnt, obj in enumerate(history_iter()):
431 431 if limit and cnt >= limit:
432 432 break
433 433 history.append(hex(obj.node()))
434 434
435 435 return [x for x in history]
436 436
437 437 @reraise_safe_exceptions
438 438 def file_history_untill(self, wire, revision, path, limit):
439 439 repo = self._factory.repo(wire)
440 440 ctx = repo[revision]
441 441 fctx = ctx.filectx(path)
442 442
443 443 file_log = list(fctx.filelog())
444 444 if limit:
445 445 # Limit to the last n items
446 446 file_log = file_log[-limit:]
447 447
448 448 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
449 449
450 450 @reraise_safe_exceptions
451 451 def fctx_annotate(self, wire, revision, path):
452 452 repo = self._factory.repo(wire)
453 453 ctx = repo[revision]
454 454 fctx = ctx.filectx(path)
455 455
456 456 result = []
457 for i, (a_line, content) in enumerate(fctx.annotate()):
458 ln_no = i + 1
459 sha = hex(a_line.fctx.node())
457 for i, annotate_obj in enumerate(fctx.annotate(), 1):
458 ln_no = i
459 sha = hex(annotate_obj.fctx.node())
460 content = annotate_obj.text
460 461 result.append((ln_no, sha, content))
461 462 return result
462 463
463 464 @reraise_safe_exceptions
464 465 def fctx_data(self, wire, revision, path):
465 466 repo = self._factory.repo(wire)
466 467 ctx = repo[revision]
467 468 fctx = ctx.filectx(path)
468 469 return fctx.data()
469 470
470 471 @reraise_safe_exceptions
471 472 def fctx_flags(self, wire, revision, path):
472 473 repo = self._factory.repo(wire)
473 474 ctx = repo[revision]
474 475 fctx = ctx.filectx(path)
475 476 return fctx.flags()
476 477
477 478 @reraise_safe_exceptions
478 479 def fctx_size(self, wire, revision, path):
479 480 repo = self._factory.repo(wire)
480 481 ctx = repo[revision]
481 482 fctx = ctx.filectx(path)
482 483 return fctx.size()
483 484
484 485 @reraise_safe_exceptions
485 486 def get_all_commit_ids(self, wire, name):
486 487 repo = self._factory.repo(wire)
487 488 revs = repo.filtered(name).changelog.index
488 489 return map(lambda x: hex(x[7]), revs)[:-1]
489 490
490 491 @reraise_safe_exceptions
491 492 def get_config_value(self, wire, section, name, untrusted=False):
492 493 repo = self._factory.repo(wire)
493 494 return repo.ui.config(section, name, untrusted=untrusted)
494 495
495 496 @reraise_safe_exceptions
496 497 def get_config_bool(self, wire, section, name, untrusted=False):
497 498 repo = self._factory.repo(wire)
498 499 return repo.ui.configbool(section, name, untrusted=untrusted)
499 500
500 501 @reraise_safe_exceptions
501 502 def get_config_list(self, wire, section, name, untrusted=False):
502 503 repo = self._factory.repo(wire)
503 504 return repo.ui.configlist(section, name, untrusted=untrusted)
504 505
505 506 @reraise_safe_exceptions
506 507 def is_large_file(self, wire, path):
507 508 return largefiles.lfutil.isstandin(path)
508 509
509 510 @reraise_safe_exceptions
510 511 def in_largefiles_store(self, wire, sha):
511 512 repo = self._factory.repo(wire)
512 513 return largefiles.lfutil.instore(repo, sha)
513 514
514 515 @reraise_safe_exceptions
515 516 def in_user_cache(self, wire, sha):
516 517 repo = self._factory.repo(wire)
517 518 return largefiles.lfutil.inusercache(repo.ui, sha)
518 519
519 520 @reraise_safe_exceptions
520 521 def store_path(self, wire, sha):
521 522 repo = self._factory.repo(wire)
522 523 return largefiles.lfutil.storepath(repo, sha)
523 524
524 525 @reraise_safe_exceptions
525 526 def link(self, wire, sha, path):
526 527 repo = self._factory.repo(wire)
527 528 largefiles.lfutil.link(
528 529 largefiles.lfutil.usercachepath(repo.ui, sha), path)
529 530
530 531 @reraise_safe_exceptions
531 532 def localrepository(self, wire, create=False):
532 533 self._factory.repo(wire, create=create)
533 534
534 535 @reraise_safe_exceptions
535 536 def lookup(self, wire, revision, both):
536 # TODO Paris: Ugly hack to "deserialize" long for msgpack
537 if isinstance(revision, float):
538 revision = long(revision)
537
539 538 repo = self._factory.repo(wire)
539
540 if isinstance(revision, int):
541 # NOTE(marcink):
542 # since Mercurial doesn't support indexes properly
543 # we need to shift accordingly by one to get proper index, e.g
544 # repo[-1] => repo[-2]
545 # repo[0] => repo[-1]
546 # repo[1] => repo[2] we also never call repo[0] because
547 # it's actually second commit
548 if revision <= 0:
549 revision = revision + -1
550 else:
551 revision = revision + 1
552
540 553 try:
541 554 ctx = repo[revision]
542 555 except RepoLookupError:
543 556 raise exceptions.LookupException(revision)
544 557 except LookupError as e:
545 558 raise exceptions.LookupException(e.name)
546 559
547 560 if not both:
548 561 return ctx.hex()
549 562
550 563 ctx = repo[ctx.hex()]
551 564 return ctx.hex(), ctx.rev()
552 565
553 566 @reraise_safe_exceptions
554 567 def pull(self, wire, url, commit_ids=None):
555 568 repo = self._factory.repo(wire)
556 569 # Disable any prompts for this repo
557 570 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
558 571
559 572 remote = peer(repo, {}, url)
560 573 # Disable any prompts for this remote
561 574 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
562 575
563 576 if commit_ids:
564 577 commit_ids = [bin(commit_id) for commit_id in commit_ids]
565 578
566 579 return exchange.pull(
567 580 repo, remote, heads=commit_ids, force=None).cgresult
568 581
569 582 @reraise_safe_exceptions
570 583 def sync_push(self, wire, url):
571 584 if self.check_url(url, wire['config']):
572 585 repo = self._factory.repo(wire)
573 586
574 587 # Disable any prompts for this repo
575 588 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
576 589
577 590 bookmarks = dict(repo._bookmarks).keys()
578 591 remote = peer(repo, {}, url)
579 592 # Disable any prompts for this remote
580 593 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
581 594
582 595 return exchange.push(
583 596 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
584 597
585 598 @reraise_safe_exceptions
586 599 def revision(self, wire, rev):
587 600 repo = self._factory.repo(wire)
588 601 ctx = repo[rev]
589 602 return ctx.rev()
590 603
591 604 @reraise_safe_exceptions
592 605 def rev_range(self, wire, filter):
593 606 repo = self._factory.repo(wire)
594 607 revisions = [rev for rev in revrange(repo, filter)]
595 608 return revisions
596 609
597 610 @reraise_safe_exceptions
598 611 def rev_range_hash(self, wire, node):
599 612 repo = self._factory.repo(wire)
600 613
601 614 def get_revs(repo, rev_opt):
602 615 if rev_opt:
603 616 revs = revrange(repo, rev_opt)
604 617 if len(revs) == 0:
605 618 return (nullrev, nullrev)
606 619 return max(revs), min(revs)
607 620 else:
608 621 return len(repo) - 1, 0
609 622
610 623 stop, start = get_revs(repo, [node + ':'])
611 624 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
612 625 return revs
613 626
614 627 @reraise_safe_exceptions
615 628 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
616 629 other_path = kwargs.pop('other_path', None)
617 630
618 631 # case when we want to compare two independent repositories
619 632 if other_path and other_path != wire["path"]:
620 633 baseui = self._factory._create_config(wire["config"])
621 634 repo = unionrepo.unionrepository(baseui, other_path, wire["path"])
622 635 else:
623 636 repo = self._factory.repo(wire)
624 637 return list(repo.revs(rev_spec, *args))
625 638
626 639 @reraise_safe_exceptions
627 640 def strip(self, wire, revision, update, backup):
628 641 repo = self._factory.repo(wire)
629 642 ctx = repo[revision]
630 643 hgext_strip(
631 644 repo.baseui, repo, ctx.node(), update=update, backup=backup)
632 645
633 646 @reraise_safe_exceptions
634 647 def verify(self, wire,):
635 648 repo = self._factory.repo(wire)
636 649 baseui = self._factory._create_config(wire['config'])
637 650 baseui.setconfig('ui', 'quiet', 'false')
638 651 output = io.BytesIO()
639 652
640 653 def write(data, **unused_kwargs):
641 654 output.write(data)
642 655 baseui.write = write
643 656
644 657 repo.ui = baseui
645 658 verify.verify(repo)
646 659 return output.getvalue()
647 660
648 661 @reraise_safe_exceptions
649 662 def tag(self, wire, name, revision, message, local, user,
650 663 tag_time, tag_timezone):
651 664 repo = self._factory.repo(wire)
652 665 ctx = repo[revision]
653 666 node = ctx.node()
654 667
655 668 date = (tag_time, tag_timezone)
656 669 try:
657 670 hg_tag.tag(repo, name, node, message, local, user, date)
658 671 except Abort as e:
659 672 log.exception("Tag operation aborted")
660 673 # Exception can contain unicode which we convert
661 674 raise exceptions.AbortException(repr(e))
662 675
663 676 @reraise_safe_exceptions
664 677 def tags(self, wire):
665 678 repo = self._factory.repo(wire)
666 679 return repo.tags()
667 680
668 681 @reraise_safe_exceptions
669 682 def update(self, wire, node=None, clean=False):
670 683 repo = self._factory.repo(wire)
671 684 baseui = self._factory._create_config(wire['config'])
672 685 commands.update(baseui, repo, node=node, clean=clean)
673 686
674 687 @reraise_safe_exceptions
675 688 def identify(self, wire):
676 689 repo = self._factory.repo(wire)
677 690 baseui = self._factory._create_config(wire['config'])
678 691 output = io.BytesIO()
679 692 baseui.write = output.write
680 693 # This is required to get a full node id
681 694 baseui.debugflag = True
682 695 commands.identify(baseui, repo, id=True)
683 696
684 697 return output.getvalue()
685 698
686 699 @reraise_safe_exceptions
687 700 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
688 701 hooks=True):
689 702 repo = self._factory.repo(wire)
690 703 baseui = self._factory._create_config(wire['config'], hooks=hooks)
691 704
692 705 # Mercurial internally has a lot of logic that checks ONLY if
693 706 # option is defined, we just pass those if they are defined then
694 707 opts = {}
695 708 if bookmark:
696 709 opts['bookmark'] = bookmark
697 710 if branch:
698 711 opts['branch'] = branch
699 712 if revision:
700 713 opts['rev'] = revision
701 714
702 715 commands.pull(baseui, repo, source, **opts)
703 716
704 717 @reraise_safe_exceptions
705 718 def heads(self, wire, branch=None):
706 719 repo = self._factory.repo(wire)
707 720 baseui = self._factory._create_config(wire['config'])
708 721 output = io.BytesIO()
709 722
710 723 def write(data, **unused_kwargs):
711 724 output.write(data)
712 725
713 726 baseui.write = write
714 727 if branch:
715 728 args = [branch]
716 729 else:
717 730 args = []
718 731 commands.heads(baseui, repo, template='{node} ', *args)
719 732
720 733 return output.getvalue()
721 734
722 735 @reraise_safe_exceptions
723 736 def ancestor(self, wire, revision1, revision2):
724 737 repo = self._factory.repo(wire)
725 738 changelog = repo.changelog
726 739 lookup = repo.lookup
727 740 a = changelog.ancestor(lookup(revision1), lookup(revision2))
728 741 return hex(a)
729 742
730 743 @reraise_safe_exceptions
731 744 def push(self, wire, revisions, dest_path, hooks=True,
732 745 push_branches=False):
733 746 repo = self._factory.repo(wire)
734 747 baseui = self._factory._create_config(wire['config'], hooks=hooks)
735 748 commands.push(baseui, repo, dest=dest_path, rev=revisions,
736 749 new_branch=push_branches)
737 750
738 751 @reraise_safe_exceptions
739 752 def merge(self, wire, revision):
740 753 repo = self._factory.repo(wire)
741 754 baseui = self._factory._create_config(wire['config'])
742 755 repo.ui.setconfig('ui', 'merge', 'internal:dump')
743 756
744 757 # In case of sub repositories are used mercurial prompts the user in
745 758 # case of merge conflicts or different sub repository sources. By
746 759 # setting the interactive flag to `False` mercurial doesn't prompt the
747 760 # used but instead uses a default value.
748 761 repo.ui.setconfig('ui', 'interactive', False)
749 762
750 763 commands.merge(baseui, repo, rev=revision)
751 764
752 765 @reraise_safe_exceptions
753 766 def commit(self, wire, message, username, close_branch=False):
754 767 repo = self._factory.repo(wire)
755 768 baseui = self._factory._create_config(wire['config'])
756 769 repo.ui.setconfig('ui', 'username', username)
757 770 commands.commit(baseui, repo, message=message, close_branch=close_branch)
758 771
759 772 @reraise_safe_exceptions
760 773 def rebase(self, wire, source=None, dest=None, abort=False):
761 774 repo = self._factory.repo(wire)
762 775 baseui = self._factory._create_config(wire['config'])
763 776 repo.ui.setconfig('ui', 'merge', 'internal:dump')
764 777 rebase.rebase(
765 778 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
766 779
767 780 @reraise_safe_exceptions
768 781 def bookmark(self, wire, bookmark, revision=None):
769 782 repo = self._factory.repo(wire)
770 783 baseui = self._factory._create_config(wire['config'])
771 784 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
772 785
773 786 @reraise_safe_exceptions
774 787 def install_hooks(self, wire, force=False):
775 788 # we don't need any special hooks for Mercurial
776 789 pass
@@ -1,134 +1,134 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 """
19 19 Adjustments to Mercurial
20 20
21 21 Intentionally kept separate from `hgcompat` and `hg`, so that these patches can
22 22 be applied without having to import the whole Mercurial machinery.
23 23
24 24 Imports are function local, so that just importing this module does not cause
25 25 side-effects other than these functions being defined.
26 26 """
27 27
28 28 import logging
29 29
30 30
31 31 def patch_largefiles_capabilities():
32 32 """
33 33 Patches the capabilities function in the largefiles extension.
34 34 """
35 35 from vcsserver import hgcompat
36 36 lfproto = hgcompat.largefiles.proto
37 37 wrapper = _dynamic_capabilities_wrapper(
38 38 lfproto, hgcompat.extensions.extensions)
39 lfproto.capabilities = wrapper
39 lfproto._capabilities = wrapper
40 40
41 41
42 42 def _dynamic_capabilities_wrapper(lfproto, extensions):
43 43
44 wrapped_capabilities = lfproto.capabilities
44 wrapped_capabilities = lfproto._capabilities
45 45 logger = logging.getLogger('vcsserver.hg')
46 46
47 def _dynamic_capabilities(repo, proto):
47 def _dynamic_capabilities(orig, repo, proto):
48 48 """
49 49 Adds dynamic behavior, so that the capability is only added if the
50 50 extension is enabled in the current ui object.
51 51 """
52 52 if 'largefiles' in dict(extensions(repo.ui)):
53 53 logger.debug('Extension largefiles enabled')
54 54 calc_capabilities = wrapped_capabilities
55 return calc_capabilities(orig, repo, proto)
55 56 else:
56 57 logger.debug('Extension largefiles disabled')
57 calc_capabilities = lfproto.capabilitiesorig
58 return calc_capabilities(repo, proto)
58 return orig(repo, proto)
59 59
60 60 return _dynamic_capabilities
61 61
62 62
63 63 def patch_subrepo_type_mapping():
64 64 from collections import defaultdict
65 65 from hgcompat import subrepo
66 66 from exceptions import SubrepoMergeException
67 67
68 68 class NoOpSubrepo(subrepo.abstractsubrepo):
69 69
70 70 def __init__(self, ctx, path, *args, **kwargs):
71 71 """Initialize abstractsubrepo part
72 72
73 73 ``ctx`` is the context referring this subrepository in the
74 74 parent repository.
75 75
76 76 ``path`` is the path to this subrepository as seen from
77 77 innermost repository.
78 78 """
79 79 self.ui = ctx.repo().ui
80 80 self._ctx = ctx
81 81 self._path = path
82 82
83 83 def storeclean(self, path):
84 84 """
85 85 returns true if the repository has not changed since it was last
86 86 cloned from or pushed to a given repository.
87 87 """
88 88 return True
89 89
90 90 def dirty(self, ignoreupdate=False, missing=False):
91 91 """returns true if the dirstate of the subrepo is dirty or does not
92 92 match current stored state. If ignoreupdate is true, only check
93 93 whether the subrepo has uncommitted changes in its dirstate.
94 94 """
95 95 return False
96 96
97 97 def basestate(self):
98 98 """current working directory base state, disregarding .hgsubstate
99 99 state and working directory modifications"""
100 100 substate = subrepo.state(self._ctx, self.ui)
101 101 file_system_path, rev, repotype = substate.get(self._path)
102 102 return rev
103 103
104 104 def remove(self):
105 105 """remove the subrepo
106 106
107 107 (should verify the dirstate is not dirty first)
108 108 """
109 109 pass
110 110
111 111 def get(self, state, overwrite=False):
112 112 """run whatever commands are needed to put the subrepo into
113 113 this state
114 114 """
115 115 pass
116 116
117 117 def merge(self, state):
118 118 """merge currently-saved state with the new state."""
119 119 raise SubrepoMergeException()
120 120
121 121 def push(self, opts):
122 122 """perform whatever action is analogous to 'hg push'
123 123
124 124 This may be a no-op on some systems.
125 125 """
126 126 pass
127 127
128 128 # Patch subrepo type mapping to always return our NoOpSubrepo class
129 129 # whenever a subrepo class is looked up.
130 130 subrepo.types = {
131 131 'hg': NoOpSubrepo,
132 132 'git': NoOpSubrepo,
133 133 'svn': NoOpSubrepo
134 134 }
@@ -1,541 +1,542 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2018 RhodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import base64
27 27
28 28 from httplib import HTTPConnection
29 29
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33 import simplejson as json
34 34
35 35 from vcsserver import exceptions, subprocessio, settings
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HooksHttpClient(object):
41 41 connection = None
42 42
43 43 def __init__(self, hooks_uri):
44 44 self.hooks_uri = hooks_uri
45 45
46 46 def __call__(self, method, extras):
47 47 connection = HTTPConnection(self.hooks_uri)
48 48 body = self._serialize(method, extras)
49 49 try:
50 50 connection.request('POST', '/', body)
51 51 except Exception:
52 52 log.error('Connection failed on %s', connection)
53 53 raise
54 54 response = connection.getresponse()
55 55 return json.loads(response.read())
56 56
57 57 def _serialize(self, hook_name, extras):
58 58 data = {
59 59 'method': hook_name,
60 60 'extras': extras
61 61 }
62 62 return json.dumps(data)
63 63
64 64
65 65 class HooksDummyClient(object):
66 66 def __init__(self, hooks_module):
67 67 self._hooks_module = importlib.import_module(hooks_module)
68 68
69 69 def __call__(self, hook_name, extras):
70 70 with self._hooks_module.Hooks() as hooks:
71 71 return getattr(hooks, hook_name)(extras)
72 72
73 73
74 74 class RemoteMessageWriter(object):
75 75 """Writer base class."""
76 76 def write(self, message):
77 77 raise NotImplementedError()
78 78
79 79
80 80 class HgMessageWriter(RemoteMessageWriter):
81 81 """Writer that knows how to send messages to mercurial clients."""
82 82
83 83 def __init__(self, ui):
84 84 self.ui = ui
85 85
86 86 def write(self, message):
87 87 # TODO: Check why the quiet flag is set by default.
88 88 old = self.ui.quiet
89 89 self.ui.quiet = False
90 90 self.ui.status(message.encode('utf-8'))
91 91 self.ui.quiet = old
92 92
93 93
94 94 class GitMessageWriter(RemoteMessageWriter):
95 95 """Writer that knows how to send messages to git clients."""
96 96
97 97 def __init__(self, stdout=None):
98 98 self.stdout = stdout or sys.stdout
99 99
100 100 def write(self, message):
101 101 self.stdout.write(message.encode('utf-8'))
102 102
103 103
104 104 class SvnMessageWriter(RemoteMessageWriter):
105 105 """Writer that knows how to send messages to svn clients."""
106 106
107 107 def __init__(self, stderr=None):
108 108 # SVN needs data sent to stderr for back-to-client messaging
109 109 self.stderr = stderr or sys.stderr
110 110
111 111 def write(self, message):
112 112 self.stderr.write(message.encode('utf-8'))
113 113
114 114
115 115 def _handle_exception(result):
116 116 exception_class = result.get('exception')
117 117 exception_traceback = result.get('exception_traceback')
118 118
119 119 if exception_traceback:
120 120 log.error('Got traceback from remote call:%s', exception_traceback)
121 121
122 122 if exception_class == 'HTTPLockedRC':
123 123 raise exceptions.RepositoryLockedException(*result['exception_args'])
124 124 elif exception_class == 'RepositoryError':
125 125 raise exceptions.VcsException(*result['exception_args'])
126 126 elif exception_class:
127 127 raise Exception('Got remote exception "%s" with args "%s"' %
128 128 (exception_class, result['exception_args']))
129 129
130 130
131 131 def _get_hooks_client(extras):
132 132 if 'hooks_uri' in extras:
133 133 protocol = extras.get('hooks_protocol')
134 134 return HooksHttpClient(extras['hooks_uri'])
135 135 else:
136 136 return HooksDummyClient(extras['hooks_module'])
137 137
138 138
139 139 def _call_hook(hook_name, extras, writer):
140 140 hooks_client = _get_hooks_client(extras)
141 141 log.debug('Hooks, using client:%s', hooks_client)
142 142 result = hooks_client(hook_name, extras)
143 143 log.debug('Hooks got result: %s', result)
144 144 writer.write(result['output'])
145 145 _handle_exception(result)
146 146
147 147 return result['status']
148 148
149 149
150 150 def _extras_from_ui(ui):
151 151 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
152 152 if not hook_data:
153 153 # maybe it's inside environ ?
154 154 env_hook_data = os.environ.get('RC_SCM_DATA')
155 155 if env_hook_data:
156 156 hook_data = env_hook_data
157 157
158 158 extras = {}
159 159 if hook_data:
160 160 extras = json.loads(hook_data)
161 161 return extras
162 162
163 163
164 164 def _rev_range_hash(repo, node):
165 165
166 166 commits = []
167 for rev in xrange(repo[node], len(repo)):
167 start = repo[node].rev()
168 for rev in xrange(start, len(repo)):
168 169 ctx = repo[rev]
169 170 commit_id = mercurial.node.hex(ctx.node())
170 171 branch = ctx.branch()
171 172 commits.append((commit_id, branch))
172 173
173 174 return commits
174 175
175 176
176 177 def repo_size(ui, repo, **kwargs):
177 178 extras = _extras_from_ui(ui)
178 179 return _call_hook('repo_size', extras, HgMessageWriter(ui))
179 180
180 181
181 182 def pre_pull(ui, repo, **kwargs):
182 183 extras = _extras_from_ui(ui)
183 184 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
184 185
185 186
186 187 def pre_pull_ssh(ui, repo, **kwargs):
187 188 extras = _extras_from_ui(ui)
188 189 if extras and extras.get('SSH'):
189 190 return pre_pull(ui, repo, **kwargs)
190 191 return 0
191 192
192 193
193 194 def post_pull(ui, repo, **kwargs):
194 195 extras = _extras_from_ui(ui)
195 196 return _call_hook('post_pull', extras, HgMessageWriter(ui))
196 197
197 198
198 199 def post_pull_ssh(ui, repo, **kwargs):
199 200 extras = _extras_from_ui(ui)
200 201 if extras and extras.get('SSH'):
201 202 return post_pull(ui, repo, **kwargs)
202 203 return 0
203 204
204 205
205 206 def pre_push(ui, repo, node=None, **kwargs):
206 207 extras = _extras_from_ui(ui)
207 208
208 209 rev_data = []
209 210 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
210 211 branches = collections.defaultdict(list)
211 212 for commit_id, branch in _rev_range_hash(repo, node):
212 213 branches[branch].append(commit_id)
213 214
214 215 for branch, commits in branches.iteritems():
215 216 old_rev = kwargs.get('node_last') or commits[0]
216 217 rev_data.append({
217 218 'old_rev': old_rev,
218 219 'new_rev': commits[-1],
219 220 'ref': '',
220 221 'type': 'branch',
221 222 'name': branch,
222 223 })
223 224
224 225 extras['commit_ids'] = rev_data
225 226 return _call_hook('pre_push', extras, HgMessageWriter(ui))
226 227
227 228
228 229 def pre_push_ssh(ui, repo, node=None, **kwargs):
229 230 if _extras_from_ui(ui).get('SSH'):
230 231 return pre_push(ui, repo, node, **kwargs)
231 232
232 233 return 0
233 234
234 235
235 236 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
236 237 extras = _extras_from_ui(ui)
237 238 if extras.get('SSH'):
238 239 permission = extras['SSH_PERMISSIONS']
239 240
240 241 if 'repository.write' == permission or 'repository.admin' == permission:
241 242 return 0
242 243
243 244 # non-zero ret code
244 245 return 1
245 246
246 247 return 0
247 248
248 249
249 250 def post_push(ui, repo, node, **kwargs):
250 251 extras = _extras_from_ui(ui)
251 252
252 253 commit_ids = []
253 254 branches = []
254 255 bookmarks = []
255 256 tags = []
256 257
257 258 for commit_id, branch in _rev_range_hash(repo, node):
258 259 commit_ids.append(commit_id)
259 260 if branch not in branches:
260 261 branches.append(branch)
261 262
262 263 if hasattr(ui, '_rc_pushkey_branches'):
263 264 bookmarks = ui._rc_pushkey_branches
264 265
265 266 extras['commit_ids'] = commit_ids
266 267 extras['new_refs'] = {
267 268 'branches': branches,
268 269 'bookmarks': bookmarks,
269 270 'tags': tags
270 271 }
271 272
272 273 return _call_hook('post_push', extras, HgMessageWriter(ui))
273 274
274 275
275 276 def post_push_ssh(ui, repo, node, **kwargs):
276 277 if _extras_from_ui(ui).get('SSH'):
277 278 return post_push(ui, repo, node, **kwargs)
278 279 return 0
279 280
280 281
281 282 def key_push(ui, repo, **kwargs):
282 283 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
283 284 # store new bookmarks in our UI object propagated later to post_push
284 285 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
285 286 return
286 287
287 288
288 289 # backward compat
289 290 log_pull_action = post_pull
290 291
291 292 # backward compat
292 293 log_push_action = post_push
293 294
294 295
295 296 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
296 297 """
297 298 Old hook name: keep here for backward compatibility.
298 299
299 300 This is only required when the installed git hooks are not upgraded.
300 301 """
301 302 pass
302 303
303 304
304 305 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
305 306 """
306 307 Old hook name: keep here for backward compatibility.
307 308
308 309 This is only required when the installed git hooks are not upgraded.
309 310 """
310 311 pass
311 312
312 313
313 314 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
314 315
315 316
316 317 def git_pre_pull(extras):
317 318 """
318 319 Pre pull hook.
319 320
320 321 :param extras: dictionary containing the keys defined in simplevcs
321 322 :type extras: dict
322 323
323 324 :return: status code of the hook. 0 for success.
324 325 :rtype: int
325 326 """
326 327 if 'pull' not in extras['hooks']:
327 328 return HookResponse(0, '')
328 329
329 330 stdout = io.BytesIO()
330 331 try:
331 332 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
332 333 except Exception as error:
333 334 status = 128
334 335 stdout.write('ERROR: %s\n' % str(error))
335 336
336 337 return HookResponse(status, stdout.getvalue())
337 338
338 339
339 340 def git_post_pull(extras):
340 341 """
341 342 Post pull hook.
342 343
343 344 :param extras: dictionary containing the keys defined in simplevcs
344 345 :type extras: dict
345 346
346 347 :return: status code of the hook. 0 for success.
347 348 :rtype: int
348 349 """
349 350 if 'pull' not in extras['hooks']:
350 351 return HookResponse(0, '')
351 352
352 353 stdout = io.BytesIO()
353 354 try:
354 355 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
355 356 except Exception as error:
356 357 status = 128
357 358 stdout.write('ERROR: %s\n' % error)
358 359
359 360 return HookResponse(status, stdout.getvalue())
360 361
361 362
362 363 def _parse_git_ref_lines(revision_lines):
363 364 rev_data = []
364 365 for revision_line in revision_lines or []:
365 366 old_rev, new_rev, ref = revision_line.strip().split(' ')
366 367 ref_data = ref.split('/', 2)
367 368 if ref_data[1] in ('tags', 'heads'):
368 369 rev_data.append({
369 370 'old_rev': old_rev,
370 371 'new_rev': new_rev,
371 372 'ref': ref,
372 373 'type': ref_data[1],
373 374 'name': ref_data[2],
374 375 })
375 376 return rev_data
376 377
377 378
378 379 def git_pre_receive(unused_repo_path, revision_lines, env):
379 380 """
380 381 Pre push hook.
381 382
382 383 :param extras: dictionary containing the keys defined in simplevcs
383 384 :type extras: dict
384 385
385 386 :return: status code of the hook. 0 for success.
386 387 :rtype: int
387 388 """
388 389 extras = json.loads(env['RC_SCM_DATA'])
389 390 rev_data = _parse_git_ref_lines(revision_lines)
390 391 if 'push' not in extras['hooks']:
391 392 return 0
392 393 extras['commit_ids'] = rev_data
393 394 return _call_hook('pre_push', extras, GitMessageWriter())
394 395
395 396
396 397 def git_post_receive(unused_repo_path, revision_lines, env):
397 398 """
398 399 Post push hook.
399 400
400 401 :param extras: dictionary containing the keys defined in simplevcs
401 402 :type extras: dict
402 403
403 404 :return: status code of the hook. 0 for success.
404 405 :rtype: int
405 406 """
406 407 extras = json.loads(env['RC_SCM_DATA'])
407 408 if 'push' not in extras['hooks']:
408 409 return 0
409 410
410 411 rev_data = _parse_git_ref_lines(revision_lines)
411 412
412 413 git_revs = []
413 414
414 415 # N.B.(skreft): it is ok to just call git, as git before calling a
415 416 # subcommand sets the PATH environment variable so that it point to the
416 417 # correct version of the git executable.
417 418 empty_commit_id = '0' * 40
418 419 branches = []
419 420 tags = []
420 421 for push_ref in rev_data:
421 422 type_ = push_ref['type']
422 423
423 424 if type_ == 'heads':
424 425 if push_ref['old_rev'] == empty_commit_id:
425 426 # starting new branch case
426 427 if push_ref['name'] not in branches:
427 428 branches.append(push_ref['name'])
428 429
429 430 # Fix up head revision if needed
430 431 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
431 432 try:
432 433 subprocessio.run_command(cmd, env=os.environ.copy())
433 434 except Exception:
434 435 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
435 436 'refs/heads/%s' % push_ref['name']]
436 437 print("Setting default branch to %s" % push_ref['name'])
437 438 subprocessio.run_command(cmd, env=os.environ.copy())
438 439
439 440 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
440 441 '--format=%(refname)', 'refs/heads/*']
441 442 stdout, stderr = subprocessio.run_command(
442 443 cmd, env=os.environ.copy())
443 444 heads = stdout
444 445 heads = heads.replace(push_ref['ref'], '')
445 446 heads = ' '.join(head for head in heads.splitlines() if head)
446 447 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
447 448 '--pretty=format:%H', '--', push_ref['new_rev'],
448 449 '--not', heads]
449 450 stdout, stderr = subprocessio.run_command(
450 451 cmd, env=os.environ.copy())
451 452 git_revs.extend(stdout.splitlines())
452 453 elif push_ref['new_rev'] == empty_commit_id:
453 454 # delete branch case
454 455 git_revs.append('delete_branch=>%s' % push_ref['name'])
455 456 else:
456 457 if push_ref['name'] not in branches:
457 458 branches.append(push_ref['name'])
458 459
459 460 cmd = [settings.GIT_EXECUTABLE, 'log',
460 461 '{old_rev}..{new_rev}'.format(**push_ref),
461 462 '--reverse', '--pretty=format:%H']
462 463 stdout, stderr = subprocessio.run_command(
463 464 cmd, env=os.environ.copy())
464 465 git_revs.extend(stdout.splitlines())
465 466 elif type_ == 'tags':
466 467 if push_ref['name'] not in tags:
467 468 tags.append(push_ref['name'])
468 469 git_revs.append('tag=>%s' % push_ref['name'])
469 470
470 471 extras['commit_ids'] = git_revs
471 472 extras['new_refs'] = {
472 473 'branches': branches,
473 474 'bookmarks': [],
474 475 'tags': tags,
475 476 }
476 477
477 478 if 'repo_size' in extras['hooks']:
478 479 try:
479 480 _call_hook('repo_size', extras, GitMessageWriter())
480 481 except:
481 482 pass
482 483
483 484 return _call_hook('post_push', extras, GitMessageWriter())
484 485
485 486
486 487 def svn_pre_commit(repo_path, commit_data, env):
487 488 path, txn_id = commit_data
488 489 branches = []
489 490 tags = []
490 491
491 492 cmd = ['svnlook', 'pget',
492 493 '-t', txn_id,
493 494 '--revprop', path, 'rc-scm-extras']
494 495 stdout, stderr = subprocessio.run_command(
495 496 cmd, env=os.environ.copy())
496 497 extras = json.loads(base64.urlsafe_b64decode(stdout))
497 498
498 499 extras['commit_ids'] = []
499 500 extras['txn_id'] = txn_id
500 501 extras['new_refs'] = {
501 502 'branches': branches,
502 503 'bookmarks': [],
503 504 'tags': tags,
504 505 }
505 506 sys.stderr.write(str(extras))
506 507 return _call_hook('pre_push', extras, SvnMessageWriter())
507 508
508 509
509 510 def svn_post_commit(repo_path, commit_data, env):
510 511 """
511 512 commit_data is path, rev, txn_id
512 513 """
513 514 path, commit_id, txn_id = commit_data
514 515 branches = []
515 516 tags = []
516 517
517 518 cmd = ['svnlook', 'pget',
518 519 '-r', commit_id,
519 520 '--revprop', path, 'rc-scm-extras']
520 521 stdout, stderr = subprocessio.run_command(
521 522 cmd, env=os.environ.copy())
522 523
523 524 extras = json.loads(base64.urlsafe_b64decode(stdout))
524 525
525 526 extras['commit_ids'] = [commit_id]
526 527 extras['txn_id'] = txn_id
527 528 extras['new_refs'] = {
528 529 'branches': branches,
529 530 'bookmarks': [],
530 531 'tags': tags,
531 532 }
532 533
533 534 if 'repo_size' in extras['hooks']:
534 535 try:
535 536 _call_hook('repo_size', extras, SvnMessageWriter())
536 537 except:
537 538 pass
538 539
539 540 return _call_hook('post_push', extras, SvnMessageWriter())
540 541
541 542
@@ -1,229 +1,234 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import logging
20 20 import itertools
21 21
22 22 import mercurial
23 23 import mercurial.error
24 import mercurial.wireprotoserver
24 25 import mercurial.hgweb.common
25 26 import mercurial.hgweb.hgweb_mod
26 import mercurial.hgweb.protocol
27 27 import webob.exc
28 28
29 29 from vcsserver import pygrack, exceptions, settings, git_lfs
30 30
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 # propagated from mercurial documentation
36 36 HG_UI_SECTIONS = [
37 37 'alias', 'auth', 'decode/encode', 'defaults', 'diff', 'email', 'extensions',
38 38 'format', 'merge-patterns', 'merge-tools', 'hooks', 'http_proxy', 'smtp',
39 39 'patch', 'paths', 'profiling', 'server', 'trusted', 'ui', 'web',
40 40 ]
41 41
42 42
43 43 class HgWeb(mercurial.hgweb.hgweb_mod.hgweb):
44 44 """Extension of hgweb that simplifies some functions."""
45 45
46 46 def _get_view(self, repo):
47 47 """Views are not supported."""
48 48 return repo
49 49
50 50 def loadsubweb(self):
51 51 """The result is only used in the templater method which is not used."""
52 52 return None
53 53
54 54 def run(self):
55 55 """Unused function so raise an exception if accidentally called."""
56 56 raise NotImplementedError
57 57
58 58 def templater(self, req):
59 59 """Function used in an unreachable code path.
60 60
61 61 This code is unreachable because we guarantee that the HTTP request,
62 62 corresponds to a Mercurial command. See the is_hg method. So, we are
63 63 never going to get a user-visible url.
64 64 """
65 65 raise NotImplementedError
66 66
67 67 def archivelist(self, nodeid):
68 68 """Unused function so raise an exception if accidentally called."""
69 69 raise NotImplementedError
70 70
71 71 def __call__(self, environ, start_response):
72 72 """Run the WSGI application.
73 73
74 74 This may be called by multiple threads.
75 75 """
76 req = mercurial.hgweb.request.wsgirequest(environ, start_response)
77 gen = self.run_wsgi(req)
76 from mercurial.hgweb import request as requestmod
77 req = requestmod.parserequestfromenv(environ)
78 res = requestmod.wsgiresponse(req, start_response)
79 gen = self.run_wsgi(req, res)
78 80
79 81 first_chunk = None
80 82
81 83 try:
82 84 data = gen.next()
83 def first_chunk(): yield data
85
86 def first_chunk():
87 yield data
84 88 except StopIteration:
85 89 pass
86 90
87 91 if first_chunk:
88 92 return itertools.chain(first_chunk(), gen)
89 93 return gen
90 94
91 def _runwsgi(self, req, repo):
92 cmd = req.form.get('cmd', [''])[0]
93 if not mercurial.hgweb.protocol.iscmd(cmd):
94 req.respond(
95 mercurial.hgweb.common.ErrorResponse(
96 mercurial.hgweb.common.HTTP_BAD_REQUEST),
97 mercurial.hgweb.protocol.HGTYPE
98 )
99 return ['']
95 def _runwsgi(self, req, res, repo):
100 96
101 return super(HgWeb, self)._runwsgi(req, repo)
97 cmd = req.qsparams.get('cmd', '')
98 if not mercurial.wireprotoserver.iscmd(cmd):
99 # NOTE(marcink): for unsupported commands, we return bad request
100 # internally from HG
101 from mercurial.hgweb.common import statusmessage
102 res.status = statusmessage(mercurial.hgweb.common.HTTP_BAD_REQUEST)
103 res.setbodybytes('')
104 return res.sendresponse()
105
106 return super(HgWeb, self)._runwsgi(req, res, repo)
102 107
103 108
104 109 def make_hg_ui_from_config(repo_config):
105 110 baseui = mercurial.ui.ui()
106 111
107 112 # clean the baseui object
108 113 baseui._ocfg = mercurial.config.config()
109 114 baseui._ucfg = mercurial.config.config()
110 115 baseui._tcfg = mercurial.config.config()
111 116
112 117 for section, option, value in repo_config:
113 118 baseui.setconfig(section, option, value)
114 119
115 120 # make our hgweb quiet so it doesn't print output
116 121 baseui.setconfig('ui', 'quiet', 'true')
117 122
118 123 return baseui
119 124
120 125
121 126 def update_hg_ui_from_hgrc(baseui, repo_path):
122 127 path = os.path.join(repo_path, '.hg', 'hgrc')
123 128
124 129 if not os.path.isfile(path):
125 130 log.debug('hgrc file is not present at %s, skipping...', path)
126 131 return
127 132 log.debug('reading hgrc from %s', path)
128 133 cfg = mercurial.config.config()
129 134 cfg.read(path)
130 135 for section in HG_UI_SECTIONS:
131 136 for k, v in cfg.items(section):
132 137 log.debug('settings ui from file: [%s] %s=%s', section, k, v)
133 138 baseui.setconfig(section, k, v)
134 139
135 140
136 141 def create_hg_wsgi_app(repo_path, repo_name, config):
137 142 """
138 143 Prepares a WSGI application to handle Mercurial requests.
139 144
140 145 :param config: is a list of 3-item tuples representing a ConfigObject
141 146 (it is the serialized version of the config object).
142 147 """
143 148 log.debug("Creating Mercurial WSGI application")
144 149
145 150 baseui = make_hg_ui_from_config(config)
146 151 update_hg_ui_from_hgrc(baseui, repo_path)
147 152
148 153 try:
149 154 return HgWeb(repo_path, name=repo_name, baseui=baseui)
150 155 except mercurial.error.RequirementError as exc:
151 156 raise exceptions.RequirementException(exc)
152 157
153 158
154 159 class GitHandler(object):
155 160 """
156 161 Handler for Git operations like push/pull etc
157 162 """
158 163 def __init__(self, repo_location, repo_name, git_path, update_server_info,
159 164 extras):
160 165 if not os.path.isdir(repo_location):
161 166 raise OSError(repo_location)
162 167 self.content_path = repo_location
163 168 self.repo_name = repo_name
164 169 self.repo_location = repo_location
165 170 self.extras = extras
166 171 self.git_path = git_path
167 172 self.update_server_info = update_server_info
168 173
169 174 def __call__(self, environ, start_response):
170 175 app = webob.exc.HTTPNotFound()
171 176 candidate_paths = (
172 177 self.content_path, os.path.join(self.content_path, '.git'))
173 178
174 179 for content_path in candidate_paths:
175 180 try:
176 181 app = pygrack.GitRepository(
177 182 self.repo_name, content_path, self.git_path,
178 183 self.update_server_info, self.extras)
179 184 break
180 185 except OSError:
181 186 continue
182 187
183 188 return app(environ, start_response)
184 189
185 190
186 191 def create_git_wsgi_app(repo_path, repo_name, config):
187 192 """
188 193 Creates a WSGI application to handle Git requests.
189 194
190 195 :param config: is a dictionary holding the extras.
191 196 """
192 197 git_path = settings.GIT_EXECUTABLE
193 198 update_server_info = config.pop('git_update_server_info')
194 199 app = GitHandler(
195 200 repo_path, repo_name, git_path, update_server_info, config)
196 201
197 202 return app
198 203
199 204
200 205 class GitLFSHandler(object):
201 206 """
202 207 Handler for Git LFS operations
203 208 """
204 209
205 210 def __init__(self, repo_location, repo_name, git_path, update_server_info,
206 211 extras):
207 212 if not os.path.isdir(repo_location):
208 213 raise OSError(repo_location)
209 214 self.content_path = repo_location
210 215 self.repo_name = repo_name
211 216 self.repo_location = repo_location
212 217 self.extras = extras
213 218 self.git_path = git_path
214 219 self.update_server_info = update_server_info
215 220
216 221 def get_app(self, git_lfs_enabled, git_lfs_store_path):
217 222 app = git_lfs.create_app(git_lfs_enabled, git_lfs_store_path)
218 223 return app
219 224
220 225
221 226 def create_git_lfs_wsgi_app(repo_path, repo_name, config):
222 227 git_path = settings.GIT_EXECUTABLE
223 228 update_server_info = config.pop('git_update_server_info')
224 229 git_lfs_enabled = config.pop('git_lfs_enabled')
225 230 git_lfs_store_path = config.pop('git_lfs_store_path')
226 231 app = GitLFSHandler(
227 232 repo_path, repo_name, git_path, update_server_info, config)
228 233
229 234 return app.get_app(git_lfs_enabled, git_lfs_store_path)
@@ -1,130 +1,124 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2018 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import mock
19 19 import pytest
20 20
21 21 from vcsserver import hgcompat, hgpatches
22 22
23 23
24 24 LARGEFILES_CAPABILITY = 'largefiles=serve'
25 25
26 26
27 27 def test_patch_largefiles_capabilities_applies_patch(
28 28 patched_capabilities):
29 29 lfproto = hgcompat.largefiles.proto
30 30 hgpatches.patch_largefiles_capabilities()
31 assert lfproto.capabilities.func_name == '_dynamic_capabilities'
31 assert lfproto._capabilities.func_name == '_dynamic_capabilities'
32 32
33 33
34 34 def test_dynamic_capabilities_uses_original_function_if_not_enabled(
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
35 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
36 orig_capabilities):
36 37 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
37 38 hgcompat.largefiles.proto, stub_extensions)
38 39
39 caps = dynamic_capabilities(stub_repo, stub_proto)
40 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
40 41
41 42 stub_extensions.assert_called_once_with(stub_ui)
42 43 assert LARGEFILES_CAPABILITY not in caps
43 44
44 45
45 def test_dynamic_capabilities_uses_updated_capabilitiesorig(
46 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
47 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
48 hgcompat.largefiles.proto, stub_extensions)
49
50 # This happens when the extension is loaded for the first time, important
51 # to ensure that an updated function is correctly picked up.
52 hgcompat.largefiles.proto.capabilitiesorig = mock.Mock(
53 return_value='REPLACED')
54
55 caps = dynamic_capabilities(stub_repo, stub_proto)
56 assert 'REPLACED' == caps
57
58
59 46 def test_dynamic_capabilities_ignores_updated_capabilities(
60 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
47 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
48 orig_capabilities):
61 49 stub_extensions.return_value = [('largefiles', mock.Mock())]
62 50 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
63 51 hgcompat.largefiles.proto, stub_extensions)
64 52
65 53 # This happens when the extension is loaded for the first time, important
66 54 # to ensure that an updated function is correctly picked up.
67 hgcompat.largefiles.proto.capabilities = mock.Mock(
55 hgcompat.largefiles.proto._capabilities = mock.Mock(
68 56 side_effect=Exception('Must not be called'))
69 57
70 dynamic_capabilities(stub_repo, stub_proto)
58 dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
71 59
72 60
73 61 def test_dynamic_capabilities_uses_largefiles_if_enabled(
74 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities):
62 stub_repo, stub_proto, stub_ui, stub_extensions, patched_capabilities,
63 orig_capabilities):
75 64 stub_extensions.return_value = [('largefiles', mock.Mock())]
76 65
77 66 dynamic_capabilities = hgpatches._dynamic_capabilities_wrapper(
78 67 hgcompat.largefiles.proto, stub_extensions)
79 68
80 caps = dynamic_capabilities(stub_repo, stub_proto)
69 caps = dynamic_capabilities(orig_capabilities, stub_repo, stub_proto)
81 70
82 71 stub_extensions.assert_called_once_with(stub_ui)
83 72 assert LARGEFILES_CAPABILITY in caps
84 73
85 74
86 75 def test_hgsubversion_import():
87 76 from hgsubversion import svnrepo
88 77 assert svnrepo
89 78
90 79
91 80 @pytest.fixture
92 81 def patched_capabilities(request):
93 82 """
94 83 Patch in `capabilitiesorig` and restore both capability functions.
95 84 """
96 85 lfproto = hgcompat.largefiles.proto
97 orig_capabilities = lfproto.capabilities
98 orig_capabilitiesorig = lfproto.capabilitiesorig
99
100 lfproto.capabilitiesorig = mock.Mock(return_value='ORIG')
86 orig_capabilities = lfproto._capabilities
101 87
102 88 @request.addfinalizer
103 89 def restore():
104 lfproto.capabilities = orig_capabilities
105 lfproto.capabilitiesorig = orig_capabilitiesorig
90 lfproto._capabilities = orig_capabilities
106 91
107 92
108 93 @pytest.fixture
109 94 def stub_repo(stub_ui):
110 95 repo = mock.Mock()
111 96 repo.ui = stub_ui
112 97 return repo
113 98
114 99
115 100 @pytest.fixture
116 101 def stub_proto(stub_ui):
117 102 proto = mock.Mock()
118 103 proto.ui = stub_ui
119 104 return proto
120 105
121 106
122 107 @pytest.fixture
108 def orig_capabilities():
109 from mercurial.wireprotov1server import wireprotocaps
110
111 def _capabilities(repo, proto):
112 return wireprotocaps
113 return _capabilities
114
115
116 @pytest.fixture
123 117 def stub_ui():
124 118 return hgcompat.ui.ui()
125 119
126 120
127 121 @pytest.fixture
128 122 def stub_extensions():
129 123 extensions = mock.Mock(return_value=tuple())
130 124 return extensions
General Comments 0
You need to be logged in to leave comments. Login now