##// END OF EJS Templates
hg: added compat for fetching revision using new hg 4.9 code
marcink -
r660:f4b31a7a default
parent child Browse files
Show More
@@ -1,809 +1,807 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 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 import traceback
24 24
25 25 from hgext import largefiles, rebase
26 26 from hgext.strip import strip as hgext_strip
27 27 from mercurial import commands
28 28 from mercurial import unionrepo
29 29 from mercurial import verify
30 30
31 31 import vcsserver
32 32 from vcsserver import exceptions
33 33 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original
34 34 from vcsserver.hgcompat import (
35 archival, bin, clone, config as hgconfig, diffopts, hex, revsymbol,
35 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
36 36 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
37 37 makepeer, instance, match, memctx, exchange, memfilectx, nullrev,
38 38 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
39 RepoLookupError, ProgrammingError, InterventionRequired, RequirementError)
39 RepoLookupError, InterventionRequired, RequirementError)
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 def make_ui_from_config(repo_config):
45 45 baseui = ui.ui()
46 46
47 47 # clean the baseui object
48 48 baseui._ocfg = hgconfig.config()
49 49 baseui._ucfg = hgconfig.config()
50 50 baseui._tcfg = hgconfig.config()
51 51
52 52 for section, option, value in repo_config:
53 53 baseui.setconfig(section, option, value)
54 54
55 55 # make our hgweb quiet so it doesn't print output
56 56 baseui.setconfig('ui', 'quiet', 'true')
57 57
58 58 baseui.setconfig('ui', 'paginate', 'never')
59 59 # for better Error reporting of Mercurial
60 60 baseui.setconfig('ui', 'message-output', 'stderr')
61 61
62 62 # force mercurial to only use 1 thread, otherwise it may try to set a
63 63 # signal in a non-main thread, thus generating a ValueError.
64 64 baseui.setconfig('worker', 'numcpus', 1)
65 65
66 66 # If there is no config for the largefiles extension, we explicitly disable
67 67 # it here. This overrides settings from repositories hgrc file. Recent
68 68 # mercurial versions enable largefiles in hgrc on clone from largefile
69 69 # repo.
70 70 if not baseui.hasconfig('extensions', 'largefiles'):
71 71 log.debug('Explicitly disable largefiles extension for repo.')
72 72 baseui.setconfig('extensions', 'largefiles', '!')
73 73
74 74 return baseui
75 75
76 76
77 77 def reraise_safe_exceptions(func):
78 78 """Decorator for converting mercurial exceptions to something neutral."""
79 79 def wrapper(*args, **kwargs):
80 80 try:
81 81 return func(*args, **kwargs)
82 82 except (Abort, InterventionRequired) as e:
83 83 raise_from_original(exceptions.AbortException(e))
84 84 except RepoLookupError as e:
85 85 raise_from_original(exceptions.LookupException(e))
86 86 except RequirementError as e:
87 87 raise_from_original(exceptions.RequirementException(e))
88 88 except RepoError as e:
89 89 raise_from_original(exceptions.VcsException(e))
90 90 except LookupError as e:
91 91 raise_from_original(exceptions.LookupException(e))
92 92 except Exception as e:
93 93 if not hasattr(e, '_vcs_kind'):
94 94 log.exception("Unhandled exception in hg remote call")
95 95 raise_from_original(exceptions.UnhandledException(e))
96 96
97 97 raise
98 98 return wrapper
99 99
100 100
101 101 class MercurialFactory(RepoFactory):
102 102 repo_type = 'hg'
103 103
104 104 def _create_config(self, config, hooks=True):
105 105 if not hooks:
106 106 hooks_to_clean = frozenset((
107 107 'changegroup.repo_size', 'preoutgoing.pre_pull',
108 108 'outgoing.pull_logger', 'prechangegroup.pre_push'))
109 109 new_config = []
110 110 for section, option, value in config:
111 111 if section == 'hooks' and option in hooks_to_clean:
112 112 continue
113 113 new_config.append((section, option, value))
114 114 config = new_config
115 115
116 116 baseui = make_ui_from_config(config)
117 117 return baseui
118 118
119 119 def _create_repo(self, wire, create):
120 120 baseui = self._create_config(wire["config"])
121 121 return instance(baseui, wire["path"], create)
122 122
123 123
124 124 class HgRemote(object):
125 125
126 126 def __init__(self, factory):
127 127 self._factory = factory
128 128
129 129 self._bulk_methods = {
130 130 "affected_files": self.ctx_files,
131 131 "author": self.ctx_user,
132 132 "branch": self.ctx_branch,
133 133 "children": self.ctx_children,
134 134 "date": self.ctx_date,
135 135 "message": self.ctx_description,
136 136 "parents": self.ctx_parents,
137 137 "status": self.ctx_status,
138 138 "obsolete": self.ctx_obsolete,
139 139 "phase": self.ctx_phase,
140 140 "hidden": self.ctx_hidden,
141 141 "_file_paths": self.ctx_list,
142 142 }
143 143
144 def _get_ctx(self, repo, ref):
145 return get_ctx(repo, ref)
146
144 147 @reraise_safe_exceptions
145 148 def discover_hg_version(self):
146 149 from mercurial import util
147 150 return util.version()
148 151
149 152 @reraise_safe_exceptions
150 153 def archive_repo(self, archive_path, mtime, file_info, kind):
151 154 if kind == "tgz":
152 155 archiver = archival.tarit(archive_path, mtime, "gz")
153 156 elif kind == "tbz2":
154 157 archiver = archival.tarit(archive_path, mtime, "bz2")
155 158 elif kind == 'zip':
156 159 archiver = archival.zipit(archive_path, mtime)
157 160 else:
158 161 raise exceptions.ArchiveException()(
159 162 'Remote does not support: "%s".' % kind)
160 163
161 164 for f_path, f_mode, f_is_link, f_content in file_info:
162 165 archiver.addfile(f_path, f_mode, f_is_link, f_content)
163 166 archiver.done()
164 167
165 168 @reraise_safe_exceptions
166 169 def bookmarks(self, wire):
167 170 repo = self._factory.repo(wire)
168 171 return dict(repo._bookmarks)
169 172
170 173 @reraise_safe_exceptions
171 174 def branches(self, wire, normal, closed):
172 175 repo = self._factory.repo(wire)
173 176 iter_branches = repo.branchmap().iterbranches()
174 177 bt = {}
175 178 for branch_name, _heads, tip, is_closed in iter_branches:
176 179 if normal and not is_closed:
177 180 bt[branch_name] = tip
178 181 if closed and is_closed:
179 182 bt[branch_name] = tip
180 183
181 184 return bt
182 185
183 186 @reraise_safe_exceptions
184 187 def bulk_request(self, wire, rev, pre_load):
185 188 result = {}
186 189 for attr in pre_load:
187 190 try:
188 191 method = self._bulk_methods[attr]
189 192 result[attr] = method(wire, rev)
190 193 except KeyError as e:
191 194 raise exceptions.VcsException(e)(
192 195 'Unknown bulk attribute: "%s"' % attr)
193 196 return result
194 197
195 198 @reraise_safe_exceptions
196 199 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
197 200 baseui = self._factory._create_config(wire["config"], hooks=hooks)
198 201 clone(baseui, source, dest, noupdate=not update_after_clone)
199 202
200 203 @reraise_safe_exceptions
201 204 def commitctx(
202 205 self, wire, message, parents, commit_time, commit_timezone,
203 206 user, files, extra, removed, updated):
204 207
205 208 repo = self._factory.repo(wire)
206 209 baseui = self._factory._create_config(wire['config'])
207 210 publishing = baseui.configbool('phases', 'publish')
208 211 if publishing:
209 212 new_commit = 'public'
210 213 else:
211 214 new_commit = 'draft'
212 215
213 216 def _filectxfn(_repo, ctx, path):
214 217 """
215 218 Marks given path as added/changed/removed in a given _repo. This is
216 219 for internal mercurial commit function.
217 220 """
218 221
219 222 # check if this path is removed
220 223 if path in removed:
221 224 # returning None is a way to mark node for removal
222 225 return None
223 226
224 227 # check if this path is added
225 228 for node in updated:
226 229 if node['path'] == path:
227 230 return memfilectx(
228 231 _repo,
229 232 changectx=ctx,
230 233 path=node['path'],
231 234 data=node['content'],
232 235 islink=False,
233 236 isexec=bool(node['mode'] & stat.S_IXUSR),
234 237 copied=False)
235 238
236 239 raise exceptions.AbortException()(
237 240 "Given path haven't been marked as added, "
238 241 "changed or removed (%s)" % path)
239 242
240 243 with repo.ui.configoverride({('phases', 'new-commit'): new_commit}):
241 244
242 245 commit_ctx = memctx(
243 246 repo=repo,
244 247 parents=parents,
245 248 text=message,
246 249 files=files,
247 250 filectxfn=_filectxfn,
248 251 user=user,
249 252 date=(commit_time, commit_timezone),
250 253 extra=extra)
251 254
252 255 n = repo.commitctx(commit_ctx)
253 256 new_id = hex(n)
254 257
255 258 return new_id
256 259
257 260 @reraise_safe_exceptions
258 261 def ctx_branch(self, wire, revision):
259 262 repo = self._factory.repo(wire)
260 ctx = repo[revision]
263 ctx = self._get_ctx(repo, revision)
261 264 return ctx.branch()
262 265
263 266 @reraise_safe_exceptions
264 267 def ctx_children(self, wire, revision):
265 268 repo = self._factory.repo(wire)
266 ctx = repo[revision]
269 ctx = self._get_ctx(repo, revision)
267 270 return [child.rev() for child in ctx.children()]
268 271
269 272 @reraise_safe_exceptions
270 273 def ctx_date(self, wire, revision):
271 274 repo = self._factory.repo(wire)
272 ctx = repo[revision]
275 ctx = self._get_ctx(repo, revision)
273 276 return ctx.date()
274 277
275 278 @reraise_safe_exceptions
276 279 def ctx_description(self, wire, revision):
277 280 repo = self._factory.repo(wire)
278 ctx = repo[revision]
281 ctx = self._get_ctx(repo, revision)
279 282 return ctx.description()
280 283
281 284 @reraise_safe_exceptions
282 285 def ctx_files(self, wire, revision):
283 286 repo = self._factory.repo(wire)
284 ctx = repo[revision]
287 ctx = self._get_ctx(repo, revision)
285 288 return ctx.files()
286 289
287 290 @reraise_safe_exceptions
288 291 def ctx_list(self, path, revision):
289 292 repo = self._factory.repo(path)
290 ctx = repo[revision]
293 ctx = self._get_ctx(repo, revision)
291 294 return list(ctx)
292 295
293 296 @reraise_safe_exceptions
294 297 def ctx_parents(self, wire, revision):
295 298 repo = self._factory.repo(wire)
296 ctx = repo[revision]
299 ctx = self._get_ctx(repo, revision)
297 300 return [parent.rev() for parent in ctx.parents()]
298 301
299 302 @reraise_safe_exceptions
300 303 def ctx_phase(self, wire, revision):
301 304 repo = self._factory.repo(wire)
302 ctx = repo[revision]
305 ctx = self._get_ctx(repo, revision)
303 306 # public=0, draft=1, secret=3
304 307 return ctx.phase()
305 308
306 309 @reraise_safe_exceptions
307 310 def ctx_obsolete(self, wire, revision):
308 311 repo = self._factory.repo(wire)
309 ctx = repo[revision]
312 ctx = self._get_ctx(repo, revision)
310 313 return ctx.obsolete()
311 314
312 315 @reraise_safe_exceptions
313 316 def ctx_hidden(self, wire, revision):
314 317 repo = self._factory.repo(wire)
315 ctx = repo[revision]
318 ctx = self._get_ctx(repo, revision)
316 319 return ctx.hidden()
317 320
318 321 @reraise_safe_exceptions
319 322 def ctx_substate(self, wire, revision):
320 323 repo = self._factory.repo(wire)
321 ctx = repo[revision]
324 ctx = self._get_ctx(repo, revision)
322 325 return ctx.substate
323 326
324 327 @reraise_safe_exceptions
325 328 def ctx_status(self, wire, revision):
326 329 repo = self._factory.repo(wire)
327 ctx = repo[revision]
330 ctx = self._get_ctx(repo, revision)
328 331 status = repo[ctx.p1().node()].status(other=ctx.node())
329 332 # object of status (odd, custom named tuple in mercurial) is not
330 333 # correctly serializable, we make it a list, as the underling
331 334 # API expects this to be a list
332 335 return list(status)
333 336
334 337 @reraise_safe_exceptions
335 338 def ctx_user(self, wire, revision):
336 339 repo = self._factory.repo(wire)
337 ctx = repo[revision]
340 ctx = self._get_ctx(repo, revision)
338 341 return ctx.user()
339 342
340 343 @reraise_safe_exceptions
341 344 def check_url(self, url, config):
342 345 _proto = None
343 346 if '+' in url[:url.find('://')]:
344 347 _proto = url[0:url.find('+')]
345 348 url = url[url.find('+') + 1:]
346 349 handlers = []
347 350 url_obj = url_parser(url)
348 351 test_uri, authinfo = url_obj.authinfo()
349 352 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
350 353 url_obj.query = obfuscate_qs(url_obj.query)
351 354
352 355 cleaned_uri = str(url_obj)
353 356 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
354 357
355 358 if authinfo:
356 359 # create a password manager
357 360 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
358 361 passmgr.add_password(*authinfo)
359 362
360 363 handlers.extend((httpbasicauthhandler(passmgr),
361 364 httpdigestauthhandler(passmgr)))
362 365
363 366 o = urllib2.build_opener(*handlers)
364 367 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
365 368 ('Accept', 'application/mercurial-0.1')]
366 369
367 370 q = {"cmd": 'between'}
368 371 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
369 372 qs = '?%s' % urllib.urlencode(q)
370 373 cu = "%s%s" % (test_uri, qs)
371 374 req = urllib2.Request(cu, None, {})
372 375
373 376 try:
374 377 log.debug("Trying to open URL %s", cleaned_uri)
375 378 resp = o.open(req)
376 379 if resp.code != 200:
377 380 raise exceptions.URLError()('Return Code is not 200')
378 381 except Exception as e:
379 382 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
380 383 # means it cannot be cloned
381 384 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
382 385
383 386 # now check if it's a proper hg repo, but don't do it for svn
384 387 try:
385 388 if _proto == 'svn':
386 389 pass
387 390 else:
388 391 # check for pure hg repos
389 392 log.debug(
390 393 "Verifying if URL is a Mercurial repository: %s",
391 394 cleaned_uri)
392 395 ui = make_ui_from_config(config)
393 396 peer_checker = makepeer(ui, url)
394 397 peer_checker.lookup('tip')
395 398 except Exception as e:
396 399 log.warning("URL is not a valid Mercurial repository: %s",
397 400 cleaned_uri)
398 401 raise exceptions.URLError(e)(
399 402 "url [%s] does not look like an hg repo org_exc: %s"
400 403 % (cleaned_uri, e))
401 404
402 405 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
403 406 return True
404 407
405 408 @reraise_safe_exceptions
406 409 def diff(
407 410 self, wire, rev1, rev2, file_filter, opt_git, opt_ignorews,
408 411 context):
409 412 repo = self._factory.repo(wire)
410 413
411 414 if file_filter:
412 415 match_filter = match(file_filter[0], '', [file_filter[1]])
413 416 else:
414 417 match_filter = file_filter
415 418 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context)
416 419
417 420 try:
418 421 return "".join(patch.diff(
419 422 repo, node1=rev1, node2=rev2, match=match_filter, opts=opts))
420 423 except RepoLookupError as e:
421 424 raise exceptions.LookupException(e)()
422 425
423 426 @reraise_safe_exceptions
424 427 def node_history(self, wire, revision, path, limit):
425 428 repo = self._factory.repo(wire)
426 429
427 ctx = repo[revision]
430 ctx = self._get_ctx(repo, revision)
428 431 fctx = ctx.filectx(path)
429 432
430 433 def history_iter():
431 434 limit_rev = fctx.rev()
432 435 for obj in reversed(list(fctx.filelog())):
433 436 obj = fctx.filectx(obj)
434 437 if limit_rev >= obj.rev():
435 438 yield obj
436 439
437 440 history = []
438 441 for cnt, obj in enumerate(history_iter()):
439 442 if limit and cnt >= limit:
440 443 break
441 444 history.append(hex(obj.node()))
442 445
443 446 return [x for x in history]
444 447
445 448 @reraise_safe_exceptions
446 449 def node_history_untill(self, wire, revision, path, limit):
447 450 repo = self._factory.repo(wire)
448 ctx = repo[revision]
451 ctx = self._get_ctx(repo, revision)
449 452 fctx = ctx.filectx(path)
450 453
451 454 file_log = list(fctx.filelog())
452 455 if limit:
453 456 # Limit to the last n items
454 457 file_log = file_log[-limit:]
455 458
456 459 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
457 460
458 461 @reraise_safe_exceptions
459 462 def fctx_annotate(self, wire, revision, path):
460 463 repo = self._factory.repo(wire)
461 ctx = repo[revision]
464 ctx = self._get_ctx(repo, revision)
462 465 fctx = ctx.filectx(path)
463 466
464 467 result = []
465 468 for i, annotate_obj in enumerate(fctx.annotate(), 1):
466 469 ln_no = i
467 470 sha = hex(annotate_obj.fctx.node())
468 471 content = annotate_obj.text
469 472 result.append((ln_no, sha, content))
470 473 return result
471 474
472 475 @reraise_safe_exceptions
473 476 def fctx_data(self, wire, revision, path):
474 477 repo = self._factory.repo(wire)
475 ctx = repo[revision]
478 ctx = self._get_ctx(repo, revision)
476 479 fctx = ctx.filectx(path)
477 480 return fctx.data()
478 481
479 482 @reraise_safe_exceptions
480 483 def fctx_flags(self, wire, revision, path):
481 484 repo = self._factory.repo(wire)
482 ctx = repo[revision]
485 ctx = self._get_ctx(repo, revision)
483 486 fctx = ctx.filectx(path)
484 487 return fctx.flags()
485 488
486 489 @reraise_safe_exceptions
487 490 def fctx_size(self, wire, revision, path):
488 491 repo = self._factory.repo(wire)
489 ctx = repo[revision]
492 ctx = self._get_ctx(repo, revision)
490 493 fctx = ctx.filectx(path)
491 494 return fctx.size()
492 495
493 496 @reraise_safe_exceptions
494 497 def get_all_commit_ids(self, wire, name):
495 498 repo = self._factory.repo(wire)
496 499 repo = repo.filtered(name)
497 500 revs = map(lambda x: hex(x[7]), repo.changelog.index)
498 501 return revs
499 502
500 503 @reraise_safe_exceptions
501 504 def get_config_value(self, wire, section, name, untrusted=False):
502 505 repo = self._factory.repo(wire)
503 506 return repo.ui.config(section, name, untrusted=untrusted)
504 507
505 508 @reraise_safe_exceptions
506 509 def get_config_bool(self, wire, section, name, untrusted=False):
507 510 repo = self._factory.repo(wire)
508 511 return repo.ui.configbool(section, name, untrusted=untrusted)
509 512
510 513 @reraise_safe_exceptions
511 514 def get_config_list(self, wire, section, name, untrusted=False):
512 515 repo = self._factory.repo(wire)
513 516 return repo.ui.configlist(section, name, untrusted=untrusted)
514 517
515 518 @reraise_safe_exceptions
516 519 def is_large_file(self, wire, path):
517 520 return largefiles.lfutil.isstandin(path)
518 521
519 522 @reraise_safe_exceptions
520 523 def in_largefiles_store(self, wire, sha):
521 524 repo = self._factory.repo(wire)
522 525 return largefiles.lfutil.instore(repo, sha)
523 526
524 527 @reraise_safe_exceptions
525 528 def in_user_cache(self, wire, sha):
526 529 repo = self._factory.repo(wire)
527 530 return largefiles.lfutil.inusercache(repo.ui, sha)
528 531
529 532 @reraise_safe_exceptions
530 533 def store_path(self, wire, sha):
531 534 repo = self._factory.repo(wire)
532 535 return largefiles.lfutil.storepath(repo, sha)
533 536
534 537 @reraise_safe_exceptions
535 538 def link(self, wire, sha, path):
536 539 repo = self._factory.repo(wire)
537 540 largefiles.lfutil.link(
538 541 largefiles.lfutil.usercachepath(repo.ui, sha), path)
539 542
540 543 @reraise_safe_exceptions
541 544 def localrepository(self, wire, create=False):
542 545 self._factory.repo(wire, create=create)
543 546
544 547 @reraise_safe_exceptions
545 548 def lookup(self, wire, revision, both):
546 549
547 550 repo = self._factory.repo(wire)
548 551
549 552 if isinstance(revision, int):
550 553 # NOTE(marcink):
551 554 # since Mercurial doesn't support negative indexes properly
552 555 # we need to shift accordingly by one to get proper index, e.g
553 556 # repo[-1] => repo[-2]
554 557 # repo[0] => repo[-1]
555 558 if revision <= 0:
556 559 revision = revision + -1
557 560 try:
558 try:
559 ctx = repo[revision]
560 except ProgrammingError:
561 # we're unable to find the rev using a regular lookup, we fallback
562 # to slower, but backward compat revsymbol usage
563 ctx = revsymbol(repo, revision)
561 ctx = self._get_ctx(repo, revision)
564 562 except (TypeError, RepoLookupError) as e:
565 563 e._org_exc_tb = traceback.format_exc()
566 564 raise exceptions.LookupException(e)(revision)
567 565 except LookupError as e:
568 566 e._org_exc_tb = traceback.format_exc()
569 567 raise exceptions.LookupException(e)(e.name)
570 568
571 569 if not both:
572 570 return ctx.hex()
573 571
574 572 ctx = repo[ctx.hex()]
575 573 return ctx.hex(), ctx.rev()
576 574
577 575 @reraise_safe_exceptions
578 576 def pull(self, wire, url, commit_ids=None):
579 577 repo = self._factory.repo(wire)
580 578 # Disable any prompts for this repo
581 579 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
582 580
583 581 remote = peer(repo, {}, url)
584 582 # Disable any prompts for this remote
585 583 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
586 584
587 585 if commit_ids:
588 586 commit_ids = [bin(commit_id) for commit_id in commit_ids]
589 587
590 588 return exchange.pull(
591 589 repo, remote, heads=commit_ids, force=None).cgresult
592 590
593 591 @reraise_safe_exceptions
594 592 def sync_push(self, wire, url):
595 593 if not self.check_url(url, wire['config']):
596 594 return
597 595
598 596 repo = self._factory.repo(wire)
599 597
600 598 # Disable any prompts for this repo
601 599 repo.ui.setconfig('ui', 'interactive', 'off', '-y')
602 600
603 601 bookmarks = dict(repo._bookmarks).keys()
604 602 remote = peer(repo, {}, url)
605 603 # Disable any prompts for this remote
606 604 remote.ui.setconfig('ui', 'interactive', 'off', '-y')
607 605
608 606 return exchange.push(
609 607 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
610 608
611 609 @reraise_safe_exceptions
612 610 def revision(self, wire, rev):
613 611 repo = self._factory.repo(wire)
614 ctx = repo[rev]
612 ctx = self._get_ctx(repo, rev)
615 613 return ctx.rev()
616 614
617 615 @reraise_safe_exceptions
618 616 def rev_range(self, wire, filter):
619 617 repo = self._factory.repo(wire)
620 618 revisions = [rev for rev in revrange(repo, filter)]
621 619 return revisions
622 620
623 621 @reraise_safe_exceptions
624 622 def rev_range_hash(self, wire, node):
625 623 repo = self._factory.repo(wire)
626 624
627 625 def get_revs(repo, rev_opt):
628 626 if rev_opt:
629 627 revs = revrange(repo, rev_opt)
630 628 if len(revs) == 0:
631 629 return (nullrev, nullrev)
632 630 return max(revs), min(revs)
633 631 else:
634 632 return len(repo) - 1, 0
635 633
636 634 stop, start = get_revs(repo, [node + ':'])
637 635 revs = [hex(repo[r].node()) for r in xrange(start, stop + 1)]
638 636 return revs
639 637
640 638 @reraise_safe_exceptions
641 639 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
642 640 other_path = kwargs.pop('other_path', None)
643 641
644 642 # case when we want to compare two independent repositories
645 643 if other_path and other_path != wire["path"]:
646 644 baseui = self._factory._create_config(wire["config"])
647 645 repo = unionrepo.makeunionrepository(baseui, other_path, wire["path"])
648 646 else:
649 647 repo = self._factory.repo(wire)
650 648 return list(repo.revs(rev_spec, *args))
651 649
652 650 @reraise_safe_exceptions
653 651 def strip(self, wire, revision, update, backup):
654 652 repo = self._factory.repo(wire)
655 ctx = repo[revision]
653 ctx = self._get_ctx(repo, revision)
656 654 hgext_strip(
657 655 repo.baseui, repo, ctx.node(), update=update, backup=backup)
658 656
659 657 @reraise_safe_exceptions
660 658 def verify(self, wire,):
661 659 repo = self._factory.repo(wire)
662 660 baseui = self._factory._create_config(wire['config'])
663 661 baseui.setconfig('ui', 'quiet', 'false')
664 662 output = io.BytesIO()
665 663
666 664 def write(data, **unused_kwargs):
667 665 output.write(data)
668 666 baseui.write = write
669 667
670 668 repo.ui = baseui
671 669 verify.verify(repo)
672 670 return output.getvalue()
673 671
674 672 @reraise_safe_exceptions
675 673 def tag(self, wire, name, revision, message, local, user,
676 674 tag_time, tag_timezone):
677 675 repo = self._factory.repo(wire)
678 ctx = repo[revision]
676 ctx = self._get_ctx(repo, revision)
679 677 node = ctx.node()
680 678
681 679 date = (tag_time, tag_timezone)
682 680 try:
683 681 hg_tag.tag(repo, name, node, message, local, user, date)
684 682 except Abort as e:
685 683 log.exception("Tag operation aborted")
686 684 # Exception can contain unicode which we convert
687 685 raise exceptions.AbortException(e)(repr(e))
688 686
689 687 @reraise_safe_exceptions
690 688 def tags(self, wire):
691 689 repo = self._factory.repo(wire)
692 690 return repo.tags()
693 691
694 692 @reraise_safe_exceptions
695 693 def update(self, wire, node=None, clean=False):
696 694 repo = self._factory.repo(wire)
697 695 baseui = self._factory._create_config(wire['config'])
698 696 commands.update(baseui, repo, node=node, clean=clean)
699 697
700 698 @reraise_safe_exceptions
701 699 def identify(self, wire):
702 700 repo = self._factory.repo(wire)
703 701 baseui = self._factory._create_config(wire['config'])
704 702 output = io.BytesIO()
705 703 baseui.write = output.write
706 704 # This is required to get a full node id
707 705 baseui.debugflag = True
708 706 commands.identify(baseui, repo, id=True)
709 707
710 708 return output.getvalue()
711 709
712 710 @reraise_safe_exceptions
713 711 def pull_cmd(self, wire, source, bookmark=None, branch=None, revision=None,
714 712 hooks=True):
715 713 repo = self._factory.repo(wire)
716 714 baseui = self._factory._create_config(wire['config'], hooks=hooks)
717 715
718 716 # Mercurial internally has a lot of logic that checks ONLY if
719 717 # option is defined, we just pass those if they are defined then
720 718 opts = {}
721 719 if bookmark:
722 720 opts['bookmark'] = bookmark
723 721 if branch:
724 722 opts['branch'] = branch
725 723 if revision:
726 724 opts['rev'] = revision
727 725
728 726 commands.pull(baseui, repo, source, **opts)
729 727
730 728 @reraise_safe_exceptions
731 729 def heads(self, wire, branch=None):
732 730 repo = self._factory.repo(wire)
733 731 baseui = self._factory._create_config(wire['config'])
734 732 output = io.BytesIO()
735 733
736 734 def write(data, **unused_kwargs):
737 735 output.write(data)
738 736
739 737 baseui.write = write
740 738 if branch:
741 739 args = [branch]
742 740 else:
743 741 args = []
744 742 commands.heads(baseui, repo, template='{node} ', *args)
745 743
746 744 return output.getvalue()
747 745
748 746 @reraise_safe_exceptions
749 747 def ancestor(self, wire, revision1, revision2):
750 748 repo = self._factory.repo(wire)
751 749 changelog = repo.changelog
752 750 lookup = repo.lookup
753 751 a = changelog.ancestor(lookup(revision1), lookup(revision2))
754 752 return hex(a)
755 753
756 754 @reraise_safe_exceptions
757 755 def push(self, wire, revisions, dest_path, hooks=True,
758 756 push_branches=False):
759 757 repo = self._factory.repo(wire)
760 758 baseui = self._factory._create_config(wire['config'], hooks=hooks)
761 759 commands.push(baseui, repo, dest=dest_path, rev=revisions,
762 760 new_branch=push_branches)
763 761
764 762 @reraise_safe_exceptions
765 763 def merge(self, wire, revision):
766 764 repo = self._factory.repo(wire)
767 765 baseui = self._factory._create_config(wire['config'])
768 766 repo.ui.setconfig('ui', 'merge', 'internal:dump')
769 767
770 768 # In case of sub repositories are used mercurial prompts the user in
771 769 # case of merge conflicts or different sub repository sources. By
772 770 # setting the interactive flag to `False` mercurial doesn't prompt the
773 771 # used but instead uses a default value.
774 772 repo.ui.setconfig('ui', 'interactive', False)
775 773
776 774 commands.merge(baseui, repo, rev=revision)
777 775
778 776 @reraise_safe_exceptions
779 777 def commit(self, wire, message, username, close_branch=False):
780 778 repo = self._factory.repo(wire)
781 779 baseui = self._factory._create_config(wire['config'])
782 780 repo.ui.setconfig('ui', 'username', username)
783 781 commands.commit(baseui, repo, message=message, close_branch=close_branch)
784 782
785 783 @reraise_safe_exceptions
786 784 def rebase(self, wire, source=None, dest=None, abort=False):
787 785 repo = self._factory.repo(wire)
788 786 baseui = self._factory._create_config(wire['config'])
789 787 repo.ui.setconfig('ui', 'merge', 'internal:dump')
790 788 rebase.rebase(
791 789 baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
792 790
793 791 @reraise_safe_exceptions
794 792 def bookmark(self, wire, bookmark, revision=None):
795 793 repo = self._factory.repo(wire)
796 794 baseui = self._factory._create_config(wire['config'])
797 795 commands.bookmark(baseui, repo, bookmark, rev=revision, force=True)
798 796
799 797 @reraise_safe_exceptions
800 798 def install_hooks(self, wire, force=False):
801 799 # we don't need any special hooks for Mercurial
802 800 pass
803 801
804 802 @reraise_safe_exceptions
805 803 def get_hooks_info(self, wire):
806 804 return {
807 805 'pre_version': vcsserver.__version__,
808 806 'post_version': vcsserver.__version__,
809 807 }
@@ -1,63 +1,74 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2019 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 Mercurial libs compatibility
20 20 """
21 21
22 22 import mercurial
23 23 from mercurial import demandimport
24 24 # patch demandimport, due to bug in mercurial when it always triggers
25 25 # demandimport.enable()
26 26 demandimport.enable = lambda *args, **kwargs: 1
27 27
28 28 from mercurial import ui
29 29 from mercurial import patch
30 30 from mercurial import config
31 31 from mercurial import extensions
32 32 from mercurial import scmutil
33 33 from mercurial import archival
34 34 from mercurial import discovery
35 35 from mercurial import unionrepo
36 36 from mercurial import localrepo
37 37 from mercurial import merge as hg_merge
38 38 from mercurial import subrepo
39 39 from mercurial import tags as hg_tag
40 40
41 41 from mercurial.commands import clone, nullid, pull
42 42 from mercurial.context import memctx, memfilectx
43 43 from mercurial.error import (
44 44 LookupError, RepoError, RepoLookupError, Abort, InterventionRequired,
45 45 RequirementError, ProgrammingError)
46 46 from mercurial.hgweb import hgweb_mod
47 47 from mercurial.localrepo import instance
48 48 from mercurial.match import match
49 49 from mercurial.mdiff import diffopts
50 50 from mercurial.node import bin, hex
51 51 from mercurial.encoding import tolocal
52 52 from mercurial.discovery import findcommonoutgoing
53 53 from mercurial.hg import peer
54 54 from mercurial.httppeer import makepeer
55 55 from mercurial.util import url as hg_url
56 56 from mercurial.scmutil import revrange, revsymbol
57 57 from mercurial.node import nullrev
58 58 from mercurial import exchange
59 59 from hgext import largefiles
60 60
61 61 # those authnadlers are patched for python 2.6.5 bug an
62 62 # infinit looping when given invalid resources
63 63 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
64
65
66 def get_ctx(repo, ref):
67 try:
68 ctx = repo[ref]
69 except ProgrammingError:
70 # we're unable to find the rev using a regular lookup, we fallback
71 # to slower, but backward compat revsymbol usage
72 ctx = revsymbol(repo, ref)
73
74 return ctx
@@ -1,710 +1,711 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-2019 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 from vcsserver.hgcompat import get_ctx
36 37
37 38 log = logging.getLogger(__name__)
38 39
39 40
40 41 class HooksHttpClient(object):
41 42 connection = None
42 43
43 44 def __init__(self, hooks_uri):
44 45 self.hooks_uri = hooks_uri
45 46
46 47 def __call__(self, method, extras):
47 48 connection = HTTPConnection(self.hooks_uri)
48 49 body = self._serialize(method, extras)
49 50 try:
50 51 connection.request('POST', '/', body)
51 52 except Exception:
52 53 log.error('Connection failed on %s', connection)
53 54 raise
54 55 response = connection.getresponse()
55 56
56 57 response_data = response.read()
57 58
58 59 try:
59 60 return json.loads(response_data)
60 61 except Exception:
61 62 log.exception('Failed to decode hook response json data. '
62 63 'response_code:%s, raw_data:%s',
63 64 response.status, response_data)
64 65 raise
65 66
66 67 def _serialize(self, hook_name, extras):
67 68 data = {
68 69 'method': hook_name,
69 70 'extras': extras
70 71 }
71 72 return json.dumps(data)
72 73
73 74
74 75 class HooksDummyClient(object):
75 76 def __init__(self, hooks_module):
76 77 self._hooks_module = importlib.import_module(hooks_module)
77 78
78 79 def __call__(self, hook_name, extras):
79 80 with self._hooks_module.Hooks() as hooks:
80 81 return getattr(hooks, hook_name)(extras)
81 82
82 83
83 84 class RemoteMessageWriter(object):
84 85 """Writer base class."""
85 86 def write(self, message):
86 87 raise NotImplementedError()
87 88
88 89
89 90 class HgMessageWriter(RemoteMessageWriter):
90 91 """Writer that knows how to send messages to mercurial clients."""
91 92
92 93 def __init__(self, ui):
93 94 self.ui = ui
94 95
95 96 def write(self, message):
96 97 # TODO: Check why the quiet flag is set by default.
97 98 old = self.ui.quiet
98 99 self.ui.quiet = False
99 100 self.ui.status(message.encode('utf-8'))
100 101 self.ui.quiet = old
101 102
102 103
103 104 class GitMessageWriter(RemoteMessageWriter):
104 105 """Writer that knows how to send messages to git clients."""
105 106
106 107 def __init__(self, stdout=None):
107 108 self.stdout = stdout or sys.stdout
108 109
109 110 def write(self, message):
110 111 self.stdout.write(message.encode('utf-8'))
111 112
112 113
113 114 class SvnMessageWriter(RemoteMessageWriter):
114 115 """Writer that knows how to send messages to svn clients."""
115 116
116 117 def __init__(self, stderr=None):
117 118 # SVN needs data sent to stderr for back-to-client messaging
118 119 self.stderr = stderr or sys.stderr
119 120
120 121 def write(self, message):
121 122 self.stderr.write(message.encode('utf-8'))
122 123
123 124
124 125 def _handle_exception(result):
125 126 exception_class = result.get('exception')
126 127 exception_traceback = result.get('exception_traceback')
127 128
128 129 if exception_traceback:
129 130 log.error('Got traceback from remote call:%s', exception_traceback)
130 131
131 132 if exception_class == 'HTTPLockedRC':
132 133 raise exceptions.RepositoryLockedException()(*result['exception_args'])
133 134 elif exception_class == 'HTTPBranchProtected':
134 135 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
135 136 elif exception_class == 'RepositoryError':
136 137 raise exceptions.VcsException()(*result['exception_args'])
137 138 elif exception_class:
138 139 raise Exception('Got remote exception "%s" with args "%s"' %
139 140 (exception_class, result['exception_args']))
140 141
141 142
142 143 def _get_hooks_client(extras):
143 144 if 'hooks_uri' in extras:
144 145 protocol = extras.get('hooks_protocol')
145 146 return HooksHttpClient(extras['hooks_uri'])
146 147 else:
147 148 return HooksDummyClient(extras['hooks_module'])
148 149
149 150
150 151 def _call_hook(hook_name, extras, writer):
151 152 hooks_client = _get_hooks_client(extras)
152 153 log.debug('Hooks, using client:%s', hooks_client)
153 154 result = hooks_client(hook_name, extras)
154 155 log.debug('Hooks got result: %s', result)
155 156
156 157 _handle_exception(result)
157 158 writer.write(result['output'])
158 159
159 160 return result['status']
160 161
161 162
162 163 def _extras_from_ui(ui):
163 164 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
164 165 if not hook_data:
165 166 # maybe it's inside environ ?
166 167 env_hook_data = os.environ.get('RC_SCM_DATA')
167 168 if env_hook_data:
168 169 hook_data = env_hook_data
169 170
170 171 extras = {}
171 172 if hook_data:
172 173 extras = json.loads(hook_data)
173 174 return extras
174 175
175 176
176 177 def _rev_range_hash(repo, node, check_heads=False):
177 178
178 179 commits = []
179 180 revs = []
180 start = repo[node].rev()
181 start = get_ctx(repo, node).rev()
181 182 end = len(repo)
182 183 for rev in range(start, end):
183 184 revs.append(rev)
184 ctx = repo[rev]
185 ctx = get_ctx(repo, rev)
185 186 commit_id = mercurial.node.hex(ctx.node())
186 187 branch = ctx.branch()
187 188 commits.append((commit_id, branch))
188 189
189 190 parent_heads = []
190 191 if check_heads:
191 192 parent_heads = _check_heads(repo, start, end, revs)
192 193 return commits, parent_heads
193 194
194 195
195 196 def _check_heads(repo, start, end, commits):
196 197 changelog = repo.changelog
197 198 parents = set()
198 199
199 200 for new_rev in commits:
200 201 for p in changelog.parentrevs(new_rev):
201 202 if p == mercurial.node.nullrev:
202 203 continue
203 204 if p < start:
204 205 parents.add(p)
205 206
206 207 for p in parents:
207 branch = repo[p].branch()
208 branch = get_ctx(repo, p).branch()
208 209 # The heads descending from that parent, on the same branch
209 210 parent_heads = set([p])
210 211 reachable = set([p])
211 212 for x in xrange(p + 1, end):
212 if repo[x].branch() != branch:
213 if get_ctx(repo, x).branch() != branch:
213 214 continue
214 215 for pp in changelog.parentrevs(x):
215 216 if pp in reachable:
216 217 reachable.add(x)
217 218 parent_heads.discard(pp)
218 219 parent_heads.add(x)
219 220 # More than one head? Suggest merging
220 221 if len(parent_heads) > 1:
221 222 return list(parent_heads)
222 223
223 224 return []
224 225
225 226
226 227 def _get_git_env():
227 228 env = {}
228 229 for k, v in os.environ.items():
229 230 if k.startswith('GIT'):
230 231 env[k] = v
231 232
232 233 # serialized version
233 234 return [(k, v) for k, v in env.items()]
234 235
235 236
236 237 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
237 238 env = {}
238 239 for k, v in os.environ.items():
239 240 if k.startswith('HG'):
240 241 env[k] = v
241 242
242 243 env['HG_NODE'] = old_rev
243 244 env['HG_NODE_LAST'] = new_rev
244 245 env['HG_TXNID'] = txnid
245 246 env['HG_PENDING'] = repo_path
246 247
247 248 return [(k, v) for k, v in env.items()]
248 249
249 250
250 251 def repo_size(ui, repo, **kwargs):
251 252 extras = _extras_from_ui(ui)
252 253 return _call_hook('repo_size', extras, HgMessageWriter(ui))
253 254
254 255
255 256 def pre_pull(ui, repo, **kwargs):
256 257 extras = _extras_from_ui(ui)
257 258 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
258 259
259 260
260 261 def pre_pull_ssh(ui, repo, **kwargs):
261 262 extras = _extras_from_ui(ui)
262 263 if extras and extras.get('SSH'):
263 264 return pre_pull(ui, repo, **kwargs)
264 265 return 0
265 266
266 267
267 268 def post_pull(ui, repo, **kwargs):
268 269 extras = _extras_from_ui(ui)
269 270 return _call_hook('post_pull', extras, HgMessageWriter(ui))
270 271
271 272
272 273 def post_pull_ssh(ui, repo, **kwargs):
273 274 extras = _extras_from_ui(ui)
274 275 if extras and extras.get('SSH'):
275 276 return post_pull(ui, repo, **kwargs)
276 277 return 0
277 278
278 279
279 280 def pre_push(ui, repo, node=None, **kwargs):
280 281 """
281 282 Mercurial pre_push hook
282 283 """
283 284 extras = _extras_from_ui(ui)
284 285 detect_force_push = extras.get('detect_force_push')
285 286
286 287 rev_data = []
287 288 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
288 289 branches = collections.defaultdict(list)
289 290 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
290 291 for commit_id, branch in commits:
291 292 branches[branch].append(commit_id)
292 293
293 294 for branch, commits in branches.items():
294 295 old_rev = kwargs.get('node_last') or commits[0]
295 296 rev_data.append({
296 297 'total_commits': len(commits),
297 298 'old_rev': old_rev,
298 299 'new_rev': commits[-1],
299 300 'ref': '',
300 301 'type': 'branch',
301 302 'name': branch,
302 303 })
303 304
304 305 for push_ref in rev_data:
305 306 push_ref['multiple_heads'] = _heads
306 307
307 308 repo_path = os.path.join(
308 309 extras.get('repo_store', ''), extras.get('repository', ''))
309 310 push_ref['hg_env'] = _get_hg_env(
310 311 old_rev=push_ref['old_rev'],
311 312 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
312 313 repo_path=repo_path)
313 314
314 315 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
315 316 extras['commit_ids'] = rev_data
316 317
317 318 return _call_hook('pre_push', extras, HgMessageWriter(ui))
318 319
319 320
320 321 def pre_push_ssh(ui, repo, node=None, **kwargs):
321 322 extras = _extras_from_ui(ui)
322 323 if extras.get('SSH'):
323 324 return pre_push(ui, repo, node, **kwargs)
324 325
325 326 return 0
326 327
327 328
328 329 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
329 330 """
330 331 Mercurial pre_push hook for SSH
331 332 """
332 333 extras = _extras_from_ui(ui)
333 334 if extras.get('SSH'):
334 335 permission = extras['SSH_PERMISSIONS']
335 336
336 337 if 'repository.write' == permission or 'repository.admin' == permission:
337 338 return 0
338 339
339 340 # non-zero ret code
340 341 return 1
341 342
342 343 return 0
343 344
344 345
345 346 def post_push(ui, repo, node, **kwargs):
346 347 """
347 348 Mercurial post_push hook
348 349 """
349 350 extras = _extras_from_ui(ui)
350 351
351 352 commit_ids = []
352 353 branches = []
353 354 bookmarks = []
354 355 tags = []
355 356
356 357 commits, _heads = _rev_range_hash(repo, node)
357 358 for commit_id, branch in commits:
358 359 commit_ids.append(commit_id)
359 360 if branch not in branches:
360 361 branches.append(branch)
361 362
362 363 if hasattr(ui, '_rc_pushkey_branches'):
363 364 bookmarks = ui._rc_pushkey_branches
364 365
365 366 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
366 367 extras['commit_ids'] = commit_ids
367 368 extras['new_refs'] = {
368 369 'branches': branches,
369 370 'bookmarks': bookmarks,
370 371 'tags': tags
371 372 }
372 373
373 374 return _call_hook('post_push', extras, HgMessageWriter(ui))
374 375
375 376
376 377 def post_push_ssh(ui, repo, node, **kwargs):
377 378 """
378 379 Mercurial post_push hook for SSH
379 380 """
380 381 if _extras_from_ui(ui).get('SSH'):
381 382 return post_push(ui, repo, node, **kwargs)
382 383 return 0
383 384
384 385
385 386 def key_push(ui, repo, **kwargs):
386 387 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
387 388 # store new bookmarks in our UI object propagated later to post_push
388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
389 ui._rc_pushkey_branches = get_ctx(repo, kwargs['key']).bookmarks()
389 390 return
390 391
391 392
392 393 # backward compat
393 394 log_pull_action = post_pull
394 395
395 396 # backward compat
396 397 log_push_action = post_push
397 398
398 399
399 400 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
400 401 """
401 402 Old hook name: keep here for backward compatibility.
402 403
403 404 This is only required when the installed git hooks are not upgraded.
404 405 """
405 406 pass
406 407
407 408
408 409 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
409 410 """
410 411 Old hook name: keep here for backward compatibility.
411 412
412 413 This is only required when the installed git hooks are not upgraded.
413 414 """
414 415 pass
415 416
416 417
417 418 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
418 419
419 420
420 421 def git_pre_pull(extras):
421 422 """
422 423 Pre pull hook.
423 424
424 425 :param extras: dictionary containing the keys defined in simplevcs
425 426 :type extras: dict
426 427
427 428 :return: status code of the hook. 0 for success.
428 429 :rtype: int
429 430 """
430 431 if 'pull' not in extras['hooks']:
431 432 return HookResponse(0, '')
432 433
433 434 stdout = io.BytesIO()
434 435 try:
435 436 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
436 437 except Exception as error:
437 438 status = 128
438 439 stdout.write('ERROR: %s\n' % str(error))
439 440
440 441 return HookResponse(status, stdout.getvalue())
441 442
442 443
443 444 def git_post_pull(extras):
444 445 """
445 446 Post pull hook.
446 447
447 448 :param extras: dictionary containing the keys defined in simplevcs
448 449 :type extras: dict
449 450
450 451 :return: status code of the hook. 0 for success.
451 452 :rtype: int
452 453 """
453 454 if 'pull' not in extras['hooks']:
454 455 return HookResponse(0, '')
455 456
456 457 stdout = io.BytesIO()
457 458 try:
458 459 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
459 460 except Exception as error:
460 461 status = 128
461 462 stdout.write('ERROR: %s\n' % error)
462 463
463 464 return HookResponse(status, stdout.getvalue())
464 465
465 466
466 467 def _parse_git_ref_lines(revision_lines):
467 468 rev_data = []
468 469 for revision_line in revision_lines or []:
469 470 old_rev, new_rev, ref = revision_line.strip().split(' ')
470 471 ref_data = ref.split('/', 2)
471 472 if ref_data[1] in ('tags', 'heads'):
472 473 rev_data.append({
473 474 # NOTE(marcink):
474 475 # we're unable to tell total_commits for git at this point
475 476 # but we set the variable for consistency with GIT
476 477 'total_commits': -1,
477 478 'old_rev': old_rev,
478 479 'new_rev': new_rev,
479 480 'ref': ref,
480 481 'type': ref_data[1],
481 482 'name': ref_data[2],
482 483 })
483 484 return rev_data
484 485
485 486
486 487 def git_pre_receive(unused_repo_path, revision_lines, env):
487 488 """
488 489 Pre push hook.
489 490
490 491 :param extras: dictionary containing the keys defined in simplevcs
491 492 :type extras: dict
492 493
493 494 :return: status code of the hook. 0 for success.
494 495 :rtype: int
495 496 """
496 497 extras = json.loads(env['RC_SCM_DATA'])
497 498 rev_data = _parse_git_ref_lines(revision_lines)
498 499 if 'push' not in extras['hooks']:
499 500 return 0
500 501 empty_commit_id = '0' * 40
501 502
502 503 detect_force_push = extras.get('detect_force_push')
503 504
504 505 for push_ref in rev_data:
505 506 # store our git-env which holds the temp store
506 507 push_ref['git_env'] = _get_git_env()
507 508 push_ref['pruned_sha'] = ''
508 509 if not detect_force_push:
509 510 # don't check for forced-push when we don't need to
510 511 continue
511 512
512 513 type_ = push_ref['type']
513 514 new_branch = push_ref['old_rev'] == empty_commit_id
514 515 delete_branch = push_ref['new_rev'] == empty_commit_id
515 516 if type_ == 'heads' and not (new_branch or delete_branch):
516 517 old_rev = push_ref['old_rev']
517 518 new_rev = push_ref['new_rev']
518 519 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
519 520 stdout, stderr = subprocessio.run_command(
520 521 cmd, env=os.environ.copy())
521 522 # means we're having some non-reachable objects, this forced push was used
522 523 if stdout:
523 524 push_ref['pruned_sha'] = stdout.splitlines()
524 525
525 526 extras['hook_type'] = 'pre_receive'
526 527 extras['commit_ids'] = rev_data
527 528 return _call_hook('pre_push', extras, GitMessageWriter())
528 529
529 530
530 531 def git_post_receive(unused_repo_path, revision_lines, env):
531 532 """
532 533 Post push hook.
533 534
534 535 :param extras: dictionary containing the keys defined in simplevcs
535 536 :type extras: dict
536 537
537 538 :return: status code of the hook. 0 for success.
538 539 :rtype: int
539 540 """
540 541 extras = json.loads(env['RC_SCM_DATA'])
541 542 if 'push' not in extras['hooks']:
542 543 return 0
543 544
544 545 rev_data = _parse_git_ref_lines(revision_lines)
545 546
546 547 git_revs = []
547 548
548 549 # N.B.(skreft): it is ok to just call git, as git before calling a
549 550 # subcommand sets the PATH environment variable so that it point to the
550 551 # correct version of the git executable.
551 552 empty_commit_id = '0' * 40
552 553 branches = []
553 554 tags = []
554 555 for push_ref in rev_data:
555 556 type_ = push_ref['type']
556 557
557 558 if type_ == 'heads':
558 559 if push_ref['old_rev'] == empty_commit_id:
559 560 # starting new branch case
560 561 if push_ref['name'] not in branches:
561 562 branches.append(push_ref['name'])
562 563
563 564 # Fix up head revision if needed
564 565 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
565 566 try:
566 567 subprocessio.run_command(cmd, env=os.environ.copy())
567 568 except Exception:
568 569 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
569 570 'refs/heads/%s' % push_ref['name']]
570 571 print("Setting default branch to %s" % push_ref['name'])
571 572 subprocessio.run_command(cmd, env=os.environ.copy())
572 573
573 574 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
574 575 '--format=%(refname)', 'refs/heads/*']
575 576 stdout, stderr = subprocessio.run_command(
576 577 cmd, env=os.environ.copy())
577 578 heads = stdout
578 579 heads = heads.replace(push_ref['ref'], '')
579 580 heads = ' '.join(head for head
580 581 in heads.splitlines() if head) or '.'
581 582 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
582 583 '--pretty=format:%H', '--', push_ref['new_rev'],
583 584 '--not', heads]
584 585 stdout, stderr = subprocessio.run_command(
585 586 cmd, env=os.environ.copy())
586 587 git_revs.extend(stdout.splitlines())
587 588 elif push_ref['new_rev'] == empty_commit_id:
588 589 # delete branch case
589 590 git_revs.append('delete_branch=>%s' % push_ref['name'])
590 591 else:
591 592 if push_ref['name'] not in branches:
592 593 branches.append(push_ref['name'])
593 594
594 595 cmd = [settings.GIT_EXECUTABLE, 'log',
595 596 '{old_rev}..{new_rev}'.format(**push_ref),
596 597 '--reverse', '--pretty=format:%H']
597 598 stdout, stderr = subprocessio.run_command(
598 599 cmd, env=os.environ.copy())
599 600 git_revs.extend(stdout.splitlines())
600 601 elif type_ == 'tags':
601 602 if push_ref['name'] not in tags:
602 603 tags.append(push_ref['name'])
603 604 git_revs.append('tag=>%s' % push_ref['name'])
604 605
605 606 extras['hook_type'] = 'post_receive'
606 607 extras['commit_ids'] = git_revs
607 608 extras['new_refs'] = {
608 609 'branches': branches,
609 610 'bookmarks': [],
610 611 'tags': tags,
611 612 }
612 613
613 614 if 'repo_size' in extras['hooks']:
614 615 try:
615 616 _call_hook('repo_size', extras, GitMessageWriter())
616 617 except:
617 618 pass
618 619
619 620 return _call_hook('post_push', extras, GitMessageWriter())
620 621
621 622
622 623 def _get_extras_from_txn_id(path, txn_id):
623 624 extras = {}
624 625 try:
625 626 cmd = ['svnlook', 'pget',
626 627 '-t', txn_id,
627 628 '--revprop', path, 'rc-scm-extras']
628 629 stdout, stderr = subprocessio.run_command(
629 630 cmd, env=os.environ.copy())
630 631 extras = json.loads(base64.urlsafe_b64decode(stdout))
631 632 except Exception:
632 633 log.exception('Failed to extract extras info from txn_id')
633 634
634 635 return extras
635 636
636 637
637 638 def _get_extras_from_commit_id(commit_id, path):
638 639 extras = {}
639 640 try:
640 641 cmd = ['svnlook', 'pget',
641 642 '-r', commit_id,
642 643 '--revprop', path, 'rc-scm-extras']
643 644 stdout, stderr = subprocessio.run_command(
644 645 cmd, env=os.environ.copy())
645 646 extras = json.loads(base64.urlsafe_b64decode(stdout))
646 647 except Exception:
647 648 log.exception('Failed to extract extras info from commit_id')
648 649
649 650 return extras
650 651
651 652
652 653 def svn_pre_commit(repo_path, commit_data, env):
653 654 path, txn_id = commit_data
654 655 branches = []
655 656 tags = []
656 657
657 658 if env.get('RC_SCM_DATA'):
658 659 extras = json.loads(env['RC_SCM_DATA'])
659 660 else:
660 661 # fallback method to read from TXN-ID stored data
661 662 extras = _get_extras_from_txn_id(path, txn_id)
662 663 if not extras:
663 664 return 0
664 665
665 666 extras['hook_type'] = 'pre_commit'
666 667 extras['commit_ids'] = []
667 668 extras['txn_id'] = txn_id
668 669 extras['new_refs'] = {
669 670 'total_commits': 1,
670 671 'branches': branches,
671 672 'bookmarks': [],
672 673 'tags': tags,
673 674 }
674 675
675 676 return _call_hook('pre_push', extras, SvnMessageWriter())
676 677
677 678
678 679 def svn_post_commit(repo_path, commit_data, env):
679 680 """
680 681 commit_data is path, rev, txn_id
681 682 """
682 683 path, commit_id, txn_id = commit_data
683 684 branches = []
684 685 tags = []
685 686
686 687 if env.get('RC_SCM_DATA'):
687 688 extras = json.loads(env['RC_SCM_DATA'])
688 689 else:
689 690 # fallback method to read from TXN-ID stored data
690 691 extras = _get_extras_from_commit_id(commit_id, path)
691 692 if not extras:
692 693 return 0
693 694
694 695 extras['hook_type'] = 'post_commit'
695 696 extras['commit_ids'] = [commit_id]
696 697 extras['txn_id'] = txn_id
697 698 extras['new_refs'] = {
698 699 'branches': branches,
699 700 'bookmarks': [],
700 701 'tags': tags,
701 702 'total_commits': 1,
702 703 }
703 704
704 705 if 'repo_size' in extras['hooks']:
705 706 try:
706 707 _call_hook('repo_size', extras, SvnMessageWriter())
707 708 except Exception:
708 709 pass
709 710
710 711 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now