##// END OF EJS Templates
py3: slice over bytes to prevent getting ascii values...
Pulkit Goyal -
r38389:c7eb9bce default
parent child Browse files
Show More
@@ -1,766 +1,767 b''
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to transplant changesets from another branch
9 9
10 10 This extension allows you to transplant changes to another parent revision,
11 11 possibly in another repository. The transplant is done using 'diff' patches.
12 12
13 13 Transplanted patches are recorded in .hg/transplant/transplants, as a
14 14 map from a changeset hash to its hash in the source repository.
15 15 '''
16 16 from __future__ import absolute_import
17 17
18 18 import os
19 19
20 20 from mercurial.i18n import _
21 21 from mercurial import (
22 22 bundlerepo,
23 23 cmdutil,
24 24 error,
25 25 exchange,
26 26 hg,
27 27 logcmdutil,
28 28 match,
29 29 merge,
30 30 node as nodemod,
31 31 patch,
32 32 pycompat,
33 33 registrar,
34 34 revlog,
35 35 revset,
36 36 scmutil,
37 37 smartset,
38 38 util,
39 39 vfs as vfsmod,
40 40 )
41 41 from mercurial.utils import (
42 42 procutil,
43 43 stringutil,
44 44 )
45 45
46 46 class TransplantError(error.Abort):
47 47 pass
48 48
49 49 cmdtable = {}
50 50 command = registrar.command(cmdtable)
51 51 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
52 52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
53 53 # be specifying the version(s) of Mercurial they are tested with, or
54 54 # leave the attribute unspecified.
55 55 testedwith = 'ships-with-hg-core'
56 56
57 57 configtable = {}
58 58 configitem = registrar.configitem(configtable)
59 59
60 60 configitem('transplant', 'filter',
61 61 default=None,
62 62 )
63 63 configitem('transplant', 'log',
64 64 default=None,
65 65 )
66 66
67 67 class transplantentry(object):
68 68 def __init__(self, lnode, rnode):
69 69 self.lnode = lnode
70 70 self.rnode = rnode
71 71
72 72 class transplants(object):
73 73 def __init__(self, path=None, transplantfile=None, opener=None):
74 74 self.path = path
75 75 self.transplantfile = transplantfile
76 76 self.opener = opener
77 77
78 78 if not opener:
79 79 self.opener = vfsmod.vfs(self.path)
80 80 self.transplants = {}
81 81 self.dirty = False
82 82 self.read()
83 83
84 84 def read(self):
85 85 abspath = os.path.join(self.path, self.transplantfile)
86 86 if self.transplantfile and os.path.exists(abspath):
87 87 for line in self.opener.read(self.transplantfile).splitlines():
88 88 lnode, rnode = map(revlog.bin, line.split(':'))
89 89 list = self.transplants.setdefault(rnode, [])
90 90 list.append(transplantentry(lnode, rnode))
91 91
92 92 def write(self):
93 93 if self.dirty and self.transplantfile:
94 94 if not os.path.isdir(self.path):
95 95 os.mkdir(self.path)
96 96 fp = self.opener(self.transplantfile, 'w')
97 97 for list in self.transplants.itervalues():
98 98 for t in list:
99 99 l, r = map(nodemod.hex, (t.lnode, t.rnode))
100 100 fp.write(l + ':' + r + '\n')
101 101 fp.close()
102 102 self.dirty = False
103 103
104 104 def get(self, rnode):
105 105 return self.transplants.get(rnode) or []
106 106
107 107 def set(self, lnode, rnode):
108 108 list = self.transplants.setdefault(rnode, [])
109 109 list.append(transplantentry(lnode, rnode))
110 110 self.dirty = True
111 111
112 112 def remove(self, transplant):
113 113 list = self.transplants.get(transplant.rnode)
114 114 if list:
115 115 del list[list.index(transplant)]
116 116 self.dirty = True
117 117
118 118 class transplanter(object):
119 119 def __init__(self, ui, repo, opts):
120 120 self.ui = ui
121 121 self.path = repo.vfs.join('transplant')
122 122 self.opener = vfsmod.vfs(self.path)
123 123 self.transplants = transplants(self.path, 'transplants',
124 124 opener=self.opener)
125 125 def getcommiteditor():
126 126 editform = cmdutil.mergeeditform(repo[None], 'transplant')
127 127 return cmdutil.getcommiteditor(editform=editform,
128 128 **pycompat.strkwargs(opts))
129 129 self.getcommiteditor = getcommiteditor
130 130
131 131 def applied(self, repo, node, parent):
132 132 '''returns True if a node is already an ancestor of parent
133 133 or is parent or has already been transplanted'''
134 134 if hasnode(repo, parent):
135 135 parentrev = repo.changelog.rev(parent)
136 136 if hasnode(repo, node):
137 137 rev = repo.changelog.rev(node)
138 138 reachable = repo.changelog.ancestors([parentrev], rev,
139 139 inclusive=True)
140 140 if rev in reachable:
141 141 return True
142 142 for t in self.transplants.get(node):
143 143 # it might have been stripped
144 144 if not hasnode(repo, t.lnode):
145 145 self.transplants.remove(t)
146 146 return False
147 147 lnoderev = repo.changelog.rev(t.lnode)
148 148 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
149 149 inclusive=True):
150 150 return True
151 151 return False
152 152
153 153 def apply(self, repo, source, revmap, merges, opts=None):
154 154 '''apply the revisions in revmap one by one in revision order'''
155 155 if opts is None:
156 156 opts = {}
157 157 revs = sorted(revmap)
158 158 p1, p2 = repo.dirstate.parents()
159 159 pulls = []
160 160 diffopts = patch.difffeatureopts(self.ui, opts)
161 161 diffopts.git = True
162 162
163 163 lock = tr = None
164 164 try:
165 165 lock = repo.lock()
166 166 tr = repo.transaction('transplant')
167 167 for rev in revs:
168 168 node = revmap[rev]
169 169 revstr = '%d:%s' % (rev, nodemod.short(node))
170 170
171 171 if self.applied(repo, node, p1):
172 172 self.ui.warn(_('skipping already applied revision %s\n') %
173 173 revstr)
174 174 continue
175 175
176 176 parents = source.changelog.parents(node)
177 177 if not (opts.get('filter') or opts.get('log')):
178 178 # If the changeset parent is the same as the
179 179 # wdir's parent, just pull it.
180 180 if parents[0] == p1:
181 181 pulls.append(node)
182 182 p1 = node
183 183 continue
184 184 if pulls:
185 185 if source != repo:
186 186 exchange.pull(repo, source.peer(), heads=pulls)
187 187 merge.update(repo, pulls[-1], False, False)
188 188 p1, p2 = repo.dirstate.parents()
189 189 pulls = []
190 190
191 191 domerge = False
192 192 if node in merges:
193 193 # pulling all the merge revs at once would mean we
194 194 # couldn't transplant after the latest even if
195 195 # transplants before them fail.
196 196 domerge = True
197 197 if not hasnode(repo, node):
198 198 exchange.pull(repo, source.peer(), heads=[node])
199 199
200 200 skipmerge = False
201 201 if parents[1] != revlog.nullid:
202 202 if not opts.get('parent'):
203 203 self.ui.note(_('skipping merge changeset %d:%s\n')
204 204 % (rev, nodemod.short(node)))
205 205 skipmerge = True
206 206 else:
207 207 parent = source.lookup(opts['parent'])
208 208 if parent not in parents:
209 209 raise error.Abort(_('%s is not a parent of %s') %
210 210 (nodemod.short(parent),
211 211 nodemod.short(node)))
212 212 else:
213 213 parent = parents[0]
214 214
215 215 if skipmerge:
216 216 patchfile = None
217 217 else:
218 218 fd, patchfile = pycompat.mkstemp(prefix='hg-transplant-')
219 219 fp = os.fdopen(fd, r'wb')
220 220 gen = patch.diff(source, parent, node, opts=diffopts)
221 221 for chunk in gen:
222 222 fp.write(chunk)
223 223 fp.close()
224 224
225 225 del revmap[rev]
226 226 if patchfile or domerge:
227 227 try:
228 228 try:
229 229 n = self.applyone(repo, node,
230 230 source.changelog.read(node),
231 231 patchfile, merge=domerge,
232 232 log=opts.get('log'),
233 233 filter=opts.get('filter'))
234 234 except TransplantError:
235 235 # Do not rollback, it is up to the user to
236 236 # fix the merge or cancel everything
237 237 tr.close()
238 238 raise
239 239 if n and domerge:
240 240 self.ui.status(_('%s merged at %s\n') % (revstr,
241 241 nodemod.short(n)))
242 242 elif n:
243 243 self.ui.status(_('%s transplanted to %s\n')
244 244 % (nodemod.short(node),
245 245 nodemod.short(n)))
246 246 finally:
247 247 if patchfile:
248 248 os.unlink(patchfile)
249 249 tr.close()
250 250 if pulls:
251 251 exchange.pull(repo, source.peer(), heads=pulls)
252 252 merge.update(repo, pulls[-1], False, False)
253 253 finally:
254 254 self.saveseries(revmap, merges)
255 255 self.transplants.write()
256 256 if tr:
257 257 tr.release()
258 258 if lock:
259 259 lock.release()
260 260
261 261 def filter(self, filter, node, changelog, patchfile):
262 262 '''arbitrarily rewrite changeset before applying it'''
263 263
264 264 self.ui.status(_('filtering %s\n') % patchfile)
265 265 user, date, msg = (changelog[1], changelog[2], changelog[4])
266 266 fd, headerfile = pycompat.mkstemp(prefix='hg-transplant-')
267 267 fp = os.fdopen(fd, r'wb')
268 268 fp.write("# HG changeset patch\n")
269 269 fp.write("# User %s\n" % user)
270 270 fp.write("# Date %d %d\n" % date)
271 271 fp.write(msg + '\n')
272 272 fp.close()
273 273
274 274 try:
275 275 self.ui.system('%s %s %s' % (filter,
276 276 procutil.shellquote(headerfile),
277 277 procutil.shellquote(patchfile)),
278 278 environ={'HGUSER': changelog[1],
279 279 'HGREVISION': nodemod.hex(node),
280 280 },
281 281 onerr=error.Abort, errprefix=_('filter failed'),
282 282 blockedtag='transplant_filter')
283 283 user, date, msg = self.parselog(open(headerfile, 'rb'))[1:4]
284 284 finally:
285 285 os.unlink(headerfile)
286 286
287 287 return (user, date, msg)
288 288
289 289 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
290 290 filter=None):
291 291 '''apply the patch in patchfile to the repository as a transplant'''
292 292 (manifest, user, (time, timezone), files, message) = cl[:5]
293 293 date = "%d %d" % (time, timezone)
294 294 extra = {'transplant_source': node}
295 295 if filter:
296 296 (user, date, message) = self.filter(filter, node, cl, patchfile)
297 297
298 298 if log:
299 299 # we don't translate messages inserted into commits
300 300 message += '\n(transplanted from %s)' % nodemod.hex(node)
301 301
302 302 self.ui.status(_('applying %s\n') % nodemod.short(node))
303 303 self.ui.note('%s %s\n%s\n' % (user, date, message))
304 304
305 305 if not patchfile and not merge:
306 306 raise error.Abort(_('can only omit patchfile if merging'))
307 307 if patchfile:
308 308 try:
309 309 files = set()
310 310 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
311 311 files = list(files)
312 312 except Exception as inst:
313 313 seriespath = os.path.join(self.path, 'series')
314 314 if os.path.exists(seriespath):
315 315 os.unlink(seriespath)
316 316 p1 = repo.dirstate.p1()
317 317 p2 = node
318 318 self.log(user, date, message, p1, p2, merge=merge)
319 319 self.ui.write(stringutil.forcebytestr(inst) + '\n')
320 320 raise TransplantError(_('fix up the working directory and run '
321 321 'hg transplant --continue'))
322 322 else:
323 323 files = None
324 324 if merge:
325 325 p1, p2 = repo.dirstate.parents()
326 326 repo.setparents(p1, node)
327 327 m = match.always(repo.root, '')
328 328 else:
329 329 m = match.exact(repo.root, '', files)
330 330
331 331 n = repo.commit(message, user, date, extra=extra, match=m,
332 332 editor=self.getcommiteditor())
333 333 if not n:
334 334 self.ui.warn(_('skipping emptied changeset %s\n') %
335 335 nodemod.short(node))
336 336 return None
337 337 if not merge:
338 338 self.transplants.set(n, node)
339 339
340 340 return n
341 341
342 342 def canresume(self):
343 343 return os.path.exists(os.path.join(self.path, 'journal'))
344 344
345 345 def resume(self, repo, source, opts):
346 346 '''recover last transaction and apply remaining changesets'''
347 347 if os.path.exists(os.path.join(self.path, 'journal')):
348 348 n, node = self.recover(repo, source, opts)
349 349 if n:
350 350 self.ui.status(_('%s transplanted as %s\n') %
351 351 (nodemod.short(node),
352 352 nodemod.short(n)))
353 353 else:
354 354 self.ui.status(_('%s skipped due to empty diff\n')
355 355 % (nodemod.short(node),))
356 356 seriespath = os.path.join(self.path, 'series')
357 357 if not os.path.exists(seriespath):
358 358 self.transplants.write()
359 359 return
360 360 nodes, merges = self.readseries()
361 361 revmap = {}
362 362 for n in nodes:
363 363 revmap[source.changelog.rev(n)] = n
364 364 os.unlink(seriespath)
365 365
366 366 self.apply(repo, source, revmap, merges, opts)
367 367
368 368 def recover(self, repo, source, opts):
369 369 '''commit working directory using journal metadata'''
370 370 node, user, date, message, parents = self.readlog()
371 371 merge = False
372 372
373 373 if not user or not date or not message or not parents[0]:
374 374 raise error.Abort(_('transplant log file is corrupt'))
375 375
376 376 parent = parents[0]
377 377 if len(parents) > 1:
378 378 if opts.get('parent'):
379 379 parent = source.lookup(opts['parent'])
380 380 if parent not in parents:
381 381 raise error.Abort(_('%s is not a parent of %s') %
382 382 (nodemod.short(parent),
383 383 nodemod.short(node)))
384 384 else:
385 385 merge = True
386 386
387 387 extra = {'transplant_source': node}
388 388 try:
389 389 p1, p2 = repo.dirstate.parents()
390 390 if p1 != parent:
391 391 raise error.Abort(_('working directory not at transplant '
392 392 'parent %s') % nodemod.hex(parent))
393 393 if merge:
394 394 repo.setparents(p1, parents[1])
395 395 modified, added, removed, deleted = repo.status()[:4]
396 396 if merge or modified or added or removed or deleted:
397 397 n = repo.commit(message, user, date, extra=extra,
398 398 editor=self.getcommiteditor())
399 399 if not n:
400 400 raise error.Abort(_('commit failed'))
401 401 if not merge:
402 402 self.transplants.set(n, node)
403 403 else:
404 404 n = None
405 405 self.unlog()
406 406
407 407 return n, node
408 408 finally:
409 409 # TODO: get rid of this meaningless try/finally enclosing.
410 410 # this is kept only to reduce changes in a patch.
411 411 pass
412 412
413 413 def readseries(self):
414 414 nodes = []
415 415 merges = []
416 416 cur = nodes
417 417 for line in self.opener.read('series').splitlines():
418 418 if line.startswith('# Merges'):
419 419 cur = merges
420 420 continue
421 421 cur.append(revlog.bin(line))
422 422
423 423 return (nodes, merges)
424 424
425 425 def saveseries(self, revmap, merges):
426 426 if not revmap:
427 427 return
428 428
429 429 if not os.path.isdir(self.path):
430 430 os.mkdir(self.path)
431 431 series = self.opener('series', 'w')
432 432 for rev in sorted(revmap):
433 433 series.write(nodemod.hex(revmap[rev]) + '\n')
434 434 if merges:
435 435 series.write('# Merges\n')
436 436 for m in merges:
437 437 series.write(nodemod.hex(m) + '\n')
438 438 series.close()
439 439
440 440 def parselog(self, fp):
441 441 parents = []
442 442 message = []
443 443 node = revlog.nullid
444 444 inmsg = False
445 445 user = None
446 446 date = None
447 447 for line in fp.read().splitlines():
448 448 if inmsg:
449 449 message.append(line)
450 450 elif line.startswith('# User '):
451 451 user = line[7:]
452 452 elif line.startswith('# Date '):
453 453 date = line[7:]
454 454 elif line.startswith('# Node ID '):
455 455 node = revlog.bin(line[10:])
456 456 elif line.startswith('# Parent '):
457 457 parents.append(revlog.bin(line[9:]))
458 458 elif not line.startswith('# '):
459 459 inmsg = True
460 460 message.append(line)
461 461 if None in (user, date):
462 462 raise error.Abort(_("filter corrupted changeset (no user or date)"))
463 463 return (node, user, date, '\n'.join(message), parents)
464 464
465 465 def log(self, user, date, message, p1, p2, merge=False):
466 466 '''journal changelog metadata for later recover'''
467 467
468 468 if not os.path.isdir(self.path):
469 469 os.mkdir(self.path)
470 470 fp = self.opener('journal', 'w')
471 471 fp.write('# User %s\n' % user)
472 472 fp.write('# Date %s\n' % date)
473 473 fp.write('# Node ID %s\n' % nodemod.hex(p2))
474 474 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
475 475 if merge:
476 476 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
477 477 fp.write(message.rstrip() + '\n')
478 478 fp.close()
479 479
480 480 def readlog(self):
481 481 return self.parselog(self.opener('journal'))
482 482
483 483 def unlog(self):
484 484 '''remove changelog journal'''
485 485 absdst = os.path.join(self.path, 'journal')
486 486 if os.path.exists(absdst):
487 487 os.unlink(absdst)
488 488
489 489 def transplantfilter(self, repo, source, root):
490 490 def matchfn(node):
491 491 if self.applied(repo, node, root):
492 492 return False
493 493 if source.changelog.parents(node)[1] != revlog.nullid:
494 494 return False
495 495 extra = source.changelog.read(node)[5]
496 496 cnode = extra.get('transplant_source')
497 497 if cnode and self.applied(repo, cnode, root):
498 498 return False
499 499 return True
500 500
501 501 return matchfn
502 502
503 503 def hasnode(repo, node):
504 504 try:
505 505 return repo.changelog.rev(node) is not None
506 506 except error.RevlogError:
507 507 return False
508 508
509 509 def browserevs(ui, repo, nodes, opts):
510 510 '''interactively transplant changesets'''
511 511 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
512 512 transplants = []
513 513 merges = []
514 514 prompt = _('apply changeset? [ynmpcq?]:'
515 515 '$$ &yes, transplant this changeset'
516 516 '$$ &no, skip this changeset'
517 517 '$$ &merge at this changeset'
518 518 '$$ show &patch'
519 519 '$$ &commit selected changesets'
520 520 '$$ &quit and cancel transplant'
521 521 '$$ &? (show this help)')
522 522 for node in nodes:
523 523 displayer.show(repo[node])
524 524 action = None
525 525 while not action:
526 action = 'ynmpcq?'[ui.promptchoice(prompt)]
526 choice = ui.promptchoice(prompt)
527 action = 'ynmpcq?'[choice:choice + 1]
527 528 if action == '?':
528 529 for c, t in ui.extractchoices(prompt)[1]:
529 530 ui.write('%s: %s\n' % (c, t))
530 531 action = None
531 532 elif action == 'p':
532 533 parent = repo.changelog.parents(node)[0]
533 534 for chunk in patch.diff(repo, parent, node):
534 535 ui.write(chunk)
535 536 action = None
536 537 if action == 'y':
537 538 transplants.append(node)
538 539 elif action == 'm':
539 540 merges.append(node)
540 541 elif action == 'c':
541 542 break
542 543 elif action == 'q':
543 544 transplants = ()
544 545 merges = ()
545 546 break
546 547 displayer.close()
547 548 return (transplants, merges)
548 549
549 550 @command('transplant',
550 551 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
551 552 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
552 553 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
553 554 ('p', 'prune', [], _('skip over REV'), _('REV')),
554 555 ('m', 'merge', [], _('merge at REV'), _('REV')),
555 556 ('', 'parent', '',
556 557 _('parent to choose when transplanting merge'), _('REV')),
557 558 ('e', 'edit', False, _('invoke editor on commit messages')),
558 559 ('', 'log', None, _('append transplant info to log message')),
559 560 ('c', 'continue', None, _('continue last transplant session '
560 561 'after fixing conflicts')),
561 562 ('', 'filter', '',
562 563 _('filter changesets through command'), _('CMD'))],
563 564 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
564 565 '[-m REV] [REV]...'))
565 566 def transplant(ui, repo, *revs, **opts):
566 567 '''transplant changesets from another branch
567 568
568 569 Selected changesets will be applied on top of the current working
569 570 directory with the log of the original changeset. The changesets
570 571 are copied and will thus appear twice in the history with different
571 572 identities.
572 573
573 574 Consider using the graft command if everything is inside the same
574 575 repository - it will use merges and will usually give a better result.
575 576 Use the rebase extension if the changesets are unpublished and you want
576 577 to move them instead of copying them.
577 578
578 579 If --log is specified, log messages will have a comment appended
579 580 of the form::
580 581
581 582 (transplanted from CHANGESETHASH)
582 583
583 584 You can rewrite the changelog message with the --filter option.
584 585 Its argument will be invoked with the current changelog message as
585 586 $1 and the patch as $2.
586 587
587 588 --source/-s specifies another repository to use for selecting changesets,
588 589 just as if it temporarily had been pulled.
589 590 If --branch/-b is specified, these revisions will be used as
590 591 heads when deciding which changesets to transplant, just as if only
591 592 these revisions had been pulled.
592 593 If --all/-a is specified, all the revisions up to the heads specified
593 594 with --branch will be transplanted.
594 595
595 596 Example:
596 597
597 598 - transplant all changes up to REV on top of your current revision::
598 599
599 600 hg transplant --branch REV --all
600 601
601 602 You can optionally mark selected transplanted changesets as merge
602 603 changesets. You will not be prompted to transplant any ancestors
603 604 of a merged transplant, and you can merge descendants of them
604 605 normally instead of transplanting them.
605 606
606 607 Merge changesets may be transplanted directly by specifying the
607 608 proper parent changeset by calling :hg:`transplant --parent`.
608 609
609 610 If no merges or revisions are provided, :hg:`transplant` will
610 611 start an interactive changeset browser.
611 612
612 613 If a changeset application fails, you can fix the merge by hand
613 614 and then resume where you left off by calling :hg:`transplant
614 615 --continue/-c`.
615 616 '''
616 617 with repo.wlock():
617 618 return _dotransplant(ui, repo, *revs, **opts)
618 619
619 620 def _dotransplant(ui, repo, *revs, **opts):
620 621 def incwalk(repo, csets, match=util.always):
621 622 for node in csets:
622 623 if match(node):
623 624 yield node
624 625
625 626 def transplantwalk(repo, dest, heads, match=util.always):
626 627 '''Yield all nodes that are ancestors of a head but not ancestors
627 628 of dest.
628 629 If no heads are specified, the heads of repo will be used.'''
629 630 if not heads:
630 631 heads = repo.heads()
631 632 ancestors = []
632 633 ctx = repo[dest]
633 634 for head in heads:
634 635 ancestors.append(ctx.ancestor(repo[head]).node())
635 636 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
636 637 if match(node):
637 638 yield node
638 639
639 640 def checkopts(opts, revs):
640 641 if opts.get('continue'):
641 642 if opts.get('branch') or opts.get('all') or opts.get('merge'):
642 643 raise error.Abort(_('--continue is incompatible with '
643 644 '--branch, --all and --merge'))
644 645 return
645 646 if not (opts.get('source') or revs or
646 647 opts.get('merge') or opts.get('branch')):
647 648 raise error.Abort(_('no source URL, branch revision, or revision '
648 649 'list provided'))
649 650 if opts.get('all'):
650 651 if not opts.get('branch'):
651 652 raise error.Abort(_('--all requires a branch revision'))
652 653 if revs:
653 654 raise error.Abort(_('--all is incompatible with a '
654 655 'revision list'))
655 656
656 657 opts = pycompat.byteskwargs(opts)
657 658 checkopts(opts, revs)
658 659
659 660 if not opts.get('log'):
660 661 # deprecated config: transplant.log
661 662 opts['log'] = ui.config('transplant', 'log')
662 663 if not opts.get('filter'):
663 664 # deprecated config: transplant.filter
664 665 opts['filter'] = ui.config('transplant', 'filter')
665 666
666 667 tp = transplanter(ui, repo, opts)
667 668
668 669 p1, p2 = repo.dirstate.parents()
669 670 if len(repo) > 0 and p1 == revlog.nullid:
670 671 raise error.Abort(_('no revision checked out'))
671 672 if opts.get('continue'):
672 673 if not tp.canresume():
673 674 raise error.Abort(_('no transplant to continue'))
674 675 else:
675 676 cmdutil.checkunfinished(repo)
676 677 if p2 != revlog.nullid:
677 678 raise error.Abort(_('outstanding uncommitted merges'))
678 679 m, a, r, d = repo.status()[:4]
679 680 if m or a or r or d:
680 681 raise error.Abort(_('outstanding local changes'))
681 682
682 683 sourcerepo = opts.get('source')
683 684 if sourcerepo:
684 685 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
685 686 heads = pycompat.maplist(peer.lookup, opts.get('branch', ()))
686 687 target = set(heads)
687 688 for r in revs:
688 689 try:
689 690 target.add(peer.lookup(r))
690 691 except error.RepoError:
691 692 pass
692 693 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
693 694 onlyheads=sorted(target), force=True)
694 695 else:
695 696 source = repo
696 697 heads = pycompat.maplist(source.lookup, opts.get('branch', ()))
697 698 cleanupfn = None
698 699
699 700 try:
700 701 if opts.get('continue'):
701 702 tp.resume(repo, source, opts)
702 703 return
703 704
704 705 tf = tp.transplantfilter(repo, source, p1)
705 706 if opts.get('prune'):
706 707 prune = set(source[r].node()
707 708 for r in scmutil.revrange(source, opts.get('prune')))
708 709 matchfn = lambda x: tf(x) and x not in prune
709 710 else:
710 711 matchfn = tf
711 712 merges = pycompat.maplist(source.lookup, opts.get('merge', ()))
712 713 revmap = {}
713 714 if revs:
714 715 for r in scmutil.revrange(source, revs):
715 716 revmap[int(r)] = source[r].node()
716 717 elif opts.get('all') or not merges:
717 718 if source != repo:
718 719 alltransplants = incwalk(source, csets, match=matchfn)
719 720 else:
720 721 alltransplants = transplantwalk(source, p1, heads,
721 722 match=matchfn)
722 723 if opts.get('all'):
723 724 revs = alltransplants
724 725 else:
725 726 revs, newmerges = browserevs(ui, source, alltransplants, opts)
726 727 merges.extend(newmerges)
727 728 for r in revs:
728 729 revmap[source.changelog.rev(r)] = r
729 730 for r in merges:
730 731 revmap[source.changelog.rev(r)] = r
731 732
732 733 tp.apply(repo, source, revmap, merges, opts)
733 734 finally:
734 735 if cleanupfn:
735 736 cleanupfn()
736 737
737 738 revsetpredicate = registrar.revsetpredicate()
738 739
739 740 @revsetpredicate('transplanted([set])')
740 741 def revsettransplanted(repo, subset, x):
741 742 """Transplanted changesets in set, or all transplanted changesets.
742 743 """
743 744 if x:
744 745 s = revset.getset(repo, subset, x)
745 746 else:
746 747 s = subset
747 748 return smartset.baseset([r for r in s if
748 749 repo[r].extra().get('transplant_source')])
749 750
750 751 templatekeyword = registrar.templatekeyword()
751 752
752 753 @templatekeyword('transplanted', requires={'ctx'})
753 754 def kwtransplanted(context, mapping):
754 755 """String. The node identifier of the transplanted
755 756 changeset if any."""
756 757 ctx = context.resource(mapping, 'ctx')
757 758 n = ctx.extra().get('transplant_source')
758 759 return n and nodemod.hex(n) or ''
759 760
760 761 def extsetup(ui):
761 762 cmdutil.unfinishedstates.append(
762 763 ['transplant/journal', True, False, _('transplant in progress'),
763 764 _("use 'hg transplant --continue' or 'hg update' to abort")])
764 765
765 766 # tell hggettext to extract docstrings from these functions:
766 767 i18nfunctions = [revsettransplanted, kwtransplanted]
@@ -1,1815 +1,1815 b''
1 1 # subrepo.py - sub-repository classes and factory
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import posixpath
15 15 import re
16 16 import stat
17 17 import subprocess
18 18 import sys
19 19 import tarfile
20 20 import xml.dom.minidom
21 21
22 22 from .i18n import _
23 23 from . import (
24 24 cmdutil,
25 25 encoding,
26 26 error,
27 27 exchange,
28 28 logcmdutil,
29 29 match as matchmod,
30 30 node,
31 31 pathutil,
32 32 phases,
33 33 pycompat,
34 34 scmutil,
35 35 subrepoutil,
36 36 util,
37 37 vfs as vfsmod,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 procutil,
42 42 stringutil,
43 43 )
44 44
45 45 hg = None
46 46 reporelpath = subrepoutil.reporelpath
47 47 subrelpath = subrepoutil.subrelpath
48 48 _abssource = subrepoutil._abssource
49 49 propertycache = util.propertycache
50 50
51 51 def _expandedabspath(path):
52 52 '''
53 53 get a path or url and if it is a path expand it and return an absolute path
54 54 '''
55 55 expandedpath = util.urllocalpath(util.expandpath(path))
56 56 u = util.url(expandedpath)
57 57 if not u.scheme:
58 58 path = util.normpath(os.path.abspath(u.path))
59 59 return path
60 60
61 61 def _getstorehashcachename(remotepath):
62 62 '''get a unique filename for the store hash cache of a remote repository'''
63 63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64 64
65 65 class SubrepoAbort(error.Abort):
66 66 """Exception class used to avoid handling a subrepo error more than once"""
67 67 def __init__(self, *args, **kw):
68 68 self.subrepo = kw.pop(r'subrepo', None)
69 69 self.cause = kw.pop(r'cause', None)
70 70 error.Abort.__init__(self, *args, **kw)
71 71
72 72 def annotatesubrepoerror(func):
73 73 def decoratedmethod(self, *args, **kargs):
74 74 try:
75 75 res = func(self, *args, **kargs)
76 76 except SubrepoAbort as ex:
77 77 # This exception has already been handled
78 78 raise ex
79 79 except error.Abort as ex:
80 80 subrepo = subrelpath(self)
81 81 errormsg = (stringutil.forcebytestr(ex) + ' '
82 82 + _('(in subrepository "%s")') % subrepo)
83 83 # avoid handling this exception by raising a SubrepoAbort exception
84 84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 85 cause=sys.exc_info())
86 86 return res
87 87 return decoratedmethod
88 88
89 89 def _updateprompt(ui, sub, dirty, local, remote):
90 90 if dirty:
91 91 msg = (_(' subrepository sources for %s differ\n'
92 92 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 93 '$$ &Local $$ &Remote')
94 94 % (subrelpath(sub), local, remote))
95 95 else:
96 96 msg = (_(' subrepository sources for %s differ (in checked out '
97 97 'version)\n'
98 98 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 99 '$$ &Local $$ &Remote')
100 100 % (subrelpath(sub), local, remote))
101 101 return ui.promptchoice(msg, 0)
102 102
103 103 def _sanitize(ui, vfs, ignore):
104 104 for dirname, dirs, names in vfs.walk():
105 105 for i, d in enumerate(dirs):
106 106 if d.lower() == ignore:
107 107 del dirs[i]
108 108 break
109 109 if vfs.basename(dirname).lower() != '.hg':
110 110 continue
111 111 for f in names:
112 112 if f.lower() == 'hgrc':
113 113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 114 "in '%s'\n") % vfs.join(dirname))
115 115 vfs.unlink(vfs.reljoin(dirname, f))
116 116
117 117 def _auditsubrepopath(repo, path):
118 118 # auditor doesn't check if the path itself is a symlink
119 119 pathutil.pathauditor(repo.root)(path)
120 120 if repo.wvfs.islink(path):
121 121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
122 122
123 123 SUBREPO_ALLOWED_DEFAULTS = {
124 124 'hg': True,
125 125 'git': False,
126 126 'svn': False,
127 127 }
128 128
129 129 def _checktype(ui, kind):
130 130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
131 131 # disabled period.
132 132 if not ui.configbool('subrepos', 'allowed', True):
133 133 raise error.Abort(_('subrepos not enabled'),
134 134 hint=_("see 'hg help config.subrepos' for details"))
135 135
136 136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
137 137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
138 138 raise error.Abort(_('%s subrepos not allowed') % kind,
139 139 hint=_("see 'hg help config.subrepos' for details"))
140 140
141 141 if kind not in types:
142 142 raise error.Abort(_('unknown subrepo type %s') % kind)
143 143
144 144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
145 145 """return instance of the right subrepo class for subrepo in path"""
146 146 # subrepo inherently violates our import layering rules
147 147 # because it wants to make repo objects from deep inside the stack
148 148 # so we manually delay the circular imports to not break
149 149 # scripts that don't use our demand-loading
150 150 global hg
151 151 from . import hg as h
152 152 hg = h
153 153
154 154 repo = ctx.repo()
155 155 _auditsubrepopath(repo, path)
156 156 state = ctx.substate[path]
157 157 _checktype(repo.ui, state[2])
158 158 if allowwdir:
159 159 state = (state[0], ctx.subrev(path), state[2])
160 160 return types[state[2]](ctx, path, state[:2], allowcreate)
161 161
162 162 def nullsubrepo(ctx, path, pctx):
163 163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
164 164 # subrepo inherently violates our import layering rules
165 165 # because it wants to make repo objects from deep inside the stack
166 166 # so we manually delay the circular imports to not break
167 167 # scripts that don't use our demand-loading
168 168 global hg
169 169 from . import hg as h
170 170 hg = h
171 171
172 172 repo = ctx.repo()
173 173 _auditsubrepopath(repo, path)
174 174 state = ctx.substate[path]
175 175 _checktype(repo.ui, state[2])
176 176 subrev = ''
177 177 if state[2] == 'hg':
178 178 subrev = "0" * 40
179 179 return types[state[2]](pctx, path, (state[0], subrev), True)
180 180
181 181 # subrepo classes need to implement the following abstract class:
182 182
183 183 class abstractsubrepo(object):
184 184
185 185 def __init__(self, ctx, path):
186 186 """Initialize abstractsubrepo part
187 187
188 188 ``ctx`` is the context referring this subrepository in the
189 189 parent repository.
190 190
191 191 ``path`` is the path to this subrepository as seen from
192 192 innermost repository.
193 193 """
194 194 self.ui = ctx.repo().ui
195 195 self._ctx = ctx
196 196 self._path = path
197 197
198 198 def addwebdirpath(self, serverpath, webconf):
199 199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
200 200
201 201 ``serverpath`` is the path component of the URL for this repo.
202 202
203 203 ``webconf`` is the dictionary of hgwebdir entries.
204 204 """
205 205 pass
206 206
207 207 def storeclean(self, path):
208 208 """
209 209 returns true if the repository has not changed since it was last
210 210 cloned from or pushed to a given repository.
211 211 """
212 212 return False
213 213
214 214 def dirty(self, ignoreupdate=False, missing=False):
215 215 """returns true if the dirstate of the subrepo is dirty or does not
216 216 match current stored state. If ignoreupdate is true, only check
217 217 whether the subrepo has uncommitted changes in its dirstate. If missing
218 218 is true, check for deleted files.
219 219 """
220 220 raise NotImplementedError
221 221
222 222 def dirtyreason(self, ignoreupdate=False, missing=False):
223 223 """return reason string if it is ``dirty()``
224 224
225 225 Returned string should have enough information for the message
226 226 of exception.
227 227
228 228 This returns None, otherwise.
229 229 """
230 230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
231 231 return _('uncommitted changes in subrepository "%s"'
232 232 ) % subrelpath(self)
233 233
234 234 def bailifchanged(self, ignoreupdate=False, hint=None):
235 235 """raise Abort if subrepository is ``dirty()``
236 236 """
237 237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
238 238 missing=True)
239 239 if dirtyreason:
240 240 raise error.Abort(dirtyreason, hint=hint)
241 241
242 242 def basestate(self):
243 243 """current working directory base state, disregarding .hgsubstate
244 244 state and working directory modifications"""
245 245 raise NotImplementedError
246 246
247 247 def checknested(self, path):
248 248 """check if path is a subrepository within this repository"""
249 249 return False
250 250
251 251 def commit(self, text, user, date):
252 252 """commit the current changes to the subrepo with the given
253 253 log message. Use given user and date if possible. Return the
254 254 new state of the subrepo.
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def phase(self, state):
259 259 """returns phase of specified state in the subrepository.
260 260 """
261 261 return phases.public
262 262
263 263 def remove(self):
264 264 """remove the subrepo
265 265
266 266 (should verify the dirstate is not dirty first)
267 267 """
268 268 raise NotImplementedError
269 269
270 270 def get(self, state, overwrite=False):
271 271 """run whatever commands are needed to put the subrepo into
272 272 this state
273 273 """
274 274 raise NotImplementedError
275 275
276 276 def merge(self, state):
277 277 """merge currently-saved state with the new state."""
278 278 raise NotImplementedError
279 279
280 280 def push(self, opts):
281 281 """perform whatever action is analogous to 'hg push'
282 282
283 283 This may be a no-op on some systems.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def add(self, ui, match, prefix, explicitonly, **opts):
288 288 return []
289 289
290 290 def addremove(self, matcher, prefix, opts):
291 291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
292 292 return 1
293 293
294 294 def cat(self, match, fm, fntemplate, prefix, **opts):
295 295 return 1
296 296
297 297 def status(self, rev2, **opts):
298 298 return scmutil.status([], [], [], [], [], [], [])
299 299
300 300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
301 301 pass
302 302
303 303 def outgoing(self, ui, dest, opts):
304 304 return 1
305 305
306 306 def incoming(self, ui, source, opts):
307 307 return 1
308 308
309 309 def files(self):
310 310 """return filename iterator"""
311 311 raise NotImplementedError
312 312
313 313 def filedata(self, name, decode):
314 314 """return file data, optionally passed through repo decoders"""
315 315 raise NotImplementedError
316 316
317 317 def fileflags(self, name):
318 318 """return file flags"""
319 319 return ''
320 320
321 321 def getfileset(self, expr):
322 322 """Resolve the fileset expression for this repo"""
323 323 return set()
324 324
325 325 def printfiles(self, ui, m, fm, fmt, subrepos):
326 326 """handle the files command for this subrepo"""
327 327 return 1
328 328
329 329 def archive(self, archiver, prefix, match=None, decode=True):
330 330 if match is not None:
331 331 files = [f for f in self.files() if match(f)]
332 332 else:
333 333 files = self.files()
334 334 total = len(files)
335 335 relpath = subrelpath(self)
336 336 self.ui.progress(_('archiving (%s)') % relpath, 0,
337 337 unit=_('files'), total=total)
338 338 for i, name in enumerate(files):
339 339 flags = self.fileflags(name)
340 340 mode = 'x' in flags and 0o755 or 0o644
341 341 symlink = 'l' in flags
342 342 archiver.addfile(prefix + self._path + '/' + name,
343 343 mode, symlink, self.filedata(name, decode))
344 344 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
345 345 unit=_('files'), total=total)
346 346 self.ui.progress(_('archiving (%s)') % relpath, None)
347 347 return total
348 348
349 349 def walk(self, match):
350 350 '''
351 351 walk recursively through the directory tree, finding all files
352 352 matched by the match function
353 353 '''
354 354
355 355 def forget(self, match, prefix, dryrun, interactive):
356 356 return ([], [])
357 357
358 358 def removefiles(self, matcher, prefix, after, force, subrepos,
359 359 dryrun, warnings):
360 360 """remove the matched files from the subrepository and the filesystem,
361 361 possibly by force and/or after the file has been removed from the
362 362 filesystem. Return 0 on success, 1 on any warning.
363 363 """
364 364 warnings.append(_("warning: removefiles not implemented (%s)")
365 365 % self._path)
366 366 return 1
367 367
368 368 def revert(self, substate, *pats, **opts):
369 369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
370 370 % (substate[0], substate[2]))
371 371 return []
372 372
373 373 def shortid(self, revid):
374 374 return revid
375 375
376 376 def unshare(self):
377 377 '''
378 378 convert this repository from shared to normal storage.
379 379 '''
380 380
381 381 def verify(self):
382 382 '''verify the integrity of the repository. Return 0 on success or
383 383 warning, 1 on any error.
384 384 '''
385 385 return 0
386 386
387 387 @propertycache
388 388 def wvfs(self):
389 389 """return vfs to access the working directory of this subrepository
390 390 """
391 391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
392 392
393 393 @propertycache
394 394 def _relpath(self):
395 395 """return path to this subrepository as seen from outermost repository
396 396 """
397 397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
398 398
399 399 class hgsubrepo(abstractsubrepo):
400 400 def __init__(self, ctx, path, state, allowcreate):
401 401 super(hgsubrepo, self).__init__(ctx, path)
402 402 self._state = state
403 403 r = ctx.repo()
404 404 root = r.wjoin(path)
405 405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
406 406 self._repo = hg.repository(r.baseui, root, create=create)
407 407
408 408 # Propagate the parent's --hidden option
409 409 if r is r.unfiltered():
410 410 self._repo = self._repo.unfiltered()
411 411
412 412 self.ui = self._repo.ui
413 413 for s, k in [('ui', 'commitsubrepos')]:
414 414 v = r.ui.config(s, k)
415 415 if v:
416 416 self.ui.setconfig(s, k, v, 'subrepo')
417 417 # internal config: ui._usedassubrepo
418 418 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
419 419 self._initrepo(r, state[0], create)
420 420
421 421 @annotatesubrepoerror
422 422 def addwebdirpath(self, serverpath, webconf):
423 423 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
424 424
425 425 def storeclean(self, path):
426 426 with self._repo.lock():
427 427 return self._storeclean(path)
428 428
429 429 def _storeclean(self, path):
430 430 clean = True
431 431 itercache = self._calcstorehash(path)
432 432 for filehash in self._readstorehashcache(path):
433 433 if filehash != next(itercache, None):
434 434 clean = False
435 435 break
436 436 if clean:
437 437 # if not empty:
438 438 # the cached and current pull states have a different size
439 439 clean = next(itercache, None) is None
440 440 return clean
441 441
442 442 def _calcstorehash(self, remotepath):
443 443 '''calculate a unique "store hash"
444 444
445 445 This method is used to to detect when there are changes that may
446 446 require a push to a given remote path.'''
447 447 # sort the files that will be hashed in increasing (likely) file size
448 448 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
449 449 yield '# %s\n' % _expandedabspath(remotepath)
450 450 vfs = self._repo.vfs
451 451 for relname in filelist:
452 452 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
453 453 yield '%s = %s\n' % (relname, filehash)
454 454
455 455 @propertycache
456 456 def _cachestorehashvfs(self):
457 457 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
458 458
459 459 def _readstorehashcache(self, remotepath):
460 460 '''read the store hash cache for a given remote repository'''
461 461 cachefile = _getstorehashcachename(remotepath)
462 462 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
463 463
464 464 def _cachestorehash(self, remotepath):
465 465 '''cache the current store hash
466 466
467 467 Each remote repo requires its own store hash cache, because a subrepo
468 468 store may be "clean" versus a given remote repo, but not versus another
469 469 '''
470 470 cachefile = _getstorehashcachename(remotepath)
471 471 with self._repo.lock():
472 472 storehash = list(self._calcstorehash(remotepath))
473 473 vfs = self._cachestorehashvfs
474 474 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
475 475
476 476 def _getctx(self):
477 477 '''fetch the context for this subrepo revision, possibly a workingctx
478 478 '''
479 479 if self._ctx.rev() is None:
480 480 return self._repo[None] # workingctx if parent is workingctx
481 481 else:
482 482 rev = self._state[1]
483 483 return self._repo[rev]
484 484
485 485 @annotatesubrepoerror
486 486 def _initrepo(self, parentrepo, source, create):
487 487 self._repo._subparent = parentrepo
488 488 self._repo._subsource = source
489 489
490 490 if create:
491 491 lines = ['[paths]\n']
492 492
493 493 def addpathconfig(key, value):
494 494 if value:
495 495 lines.append('%s = %s\n' % (key, value))
496 496 self.ui.setconfig('paths', key, value, 'subrepo')
497 497
498 498 defpath = _abssource(self._repo, abort=False)
499 499 defpushpath = _abssource(self._repo, True, abort=False)
500 500 addpathconfig('default', defpath)
501 501 if defpath != defpushpath:
502 502 addpathconfig('default-push', defpushpath)
503 503
504 504 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
505 505
506 506 @annotatesubrepoerror
507 507 def add(self, ui, match, prefix, explicitonly, **opts):
508 508 return cmdutil.add(ui, self._repo, match,
509 509 self.wvfs.reljoin(prefix, self._path),
510 510 explicitonly, **opts)
511 511
512 512 @annotatesubrepoerror
513 513 def addremove(self, m, prefix, opts):
514 514 # In the same way as sub directories are processed, once in a subrepo,
515 515 # always entry any of its subrepos. Don't corrupt the options that will
516 516 # be used to process sibling subrepos however.
517 517 opts = copy.copy(opts)
518 518 opts['subrepos'] = True
519 519 return scmutil.addremove(self._repo, m,
520 520 self.wvfs.reljoin(prefix, self._path), opts)
521 521
522 522 @annotatesubrepoerror
523 523 def cat(self, match, fm, fntemplate, prefix, **opts):
524 524 rev = self._state[1]
525 525 ctx = self._repo[rev]
526 526 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
527 527 prefix, **opts)
528 528
529 529 @annotatesubrepoerror
530 530 def status(self, rev2, **opts):
531 531 try:
532 532 rev1 = self._state[1]
533 533 ctx1 = self._repo[rev1]
534 534 ctx2 = self._repo[rev2]
535 535 return self._repo.status(ctx1, ctx2, **opts)
536 536 except error.RepoLookupError as inst:
537 537 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
538 538 % (inst, subrelpath(self)))
539 539 return scmutil.status([], [], [], [], [], [], [])
540 540
541 541 @annotatesubrepoerror
542 542 def diff(self, ui, diffopts, node2, match, prefix, **opts):
543 543 try:
544 544 node1 = node.bin(self._state[1])
545 545 # We currently expect node2 to come from substate and be
546 546 # in hex format
547 547 if node2 is not None:
548 548 node2 = node.bin(node2)
549 549 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
550 550 node1, node2, match,
551 551 prefix=posixpath.join(prefix, self._path),
552 552 listsubrepos=True, **opts)
553 553 except error.RepoLookupError as inst:
554 554 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
555 555 % (inst, subrelpath(self)))
556 556
557 557 @annotatesubrepoerror
558 558 def archive(self, archiver, prefix, match=None, decode=True):
559 559 self._get(self._state + ('hg',))
560 560 files = self.files()
561 561 if match:
562 562 files = [f for f in files if match(f)]
563 563 rev = self._state[1]
564 564 ctx = self._repo[rev]
565 565 scmutil.prefetchfiles(self._repo, [ctx.rev()],
566 566 scmutil.matchfiles(self._repo, files))
567 567 total = abstractsubrepo.archive(self, archiver, prefix, match)
568 568 for subpath in ctx.substate:
569 569 s = subrepo(ctx, subpath, True)
570 570 submatch = matchmod.subdirmatcher(subpath, match)
571 571 total += s.archive(archiver, prefix + self._path + '/', submatch,
572 572 decode)
573 573 return total
574 574
575 575 @annotatesubrepoerror
576 576 def dirty(self, ignoreupdate=False, missing=False):
577 577 r = self._state[1]
578 578 if r == '' and not ignoreupdate: # no state recorded
579 579 return True
580 580 w = self._repo[None]
581 581 if r != w.p1().hex() and not ignoreupdate:
582 582 # different version checked out
583 583 return True
584 584 return w.dirty(missing=missing) # working directory changed
585 585
586 586 def basestate(self):
587 587 return self._repo['.'].hex()
588 588
589 589 def checknested(self, path):
590 590 return self._repo._checknested(self._repo.wjoin(path))
591 591
592 592 @annotatesubrepoerror
593 593 def commit(self, text, user, date):
594 594 # don't bother committing in the subrepo if it's only been
595 595 # updated
596 596 if not self.dirty(True):
597 597 return self._repo['.'].hex()
598 598 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
599 599 n = self._repo.commit(text, user, date)
600 600 if not n:
601 601 return self._repo['.'].hex() # different version checked out
602 602 return node.hex(n)
603 603
604 604 @annotatesubrepoerror
605 605 def phase(self, state):
606 606 return self._repo[state or '.'].phase()
607 607
608 608 @annotatesubrepoerror
609 609 def remove(self):
610 610 # we can't fully delete the repository as it may contain
611 611 # local-only history
612 612 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
613 613 hg.clean(self._repo, node.nullid, False)
614 614
615 615 def _get(self, state):
616 616 source, revision, kind = state
617 617 parentrepo = self._repo._subparent
618 618
619 619 if revision in self._repo.unfiltered():
620 620 # Allow shared subrepos tracked at null to setup the sharedpath
621 621 if len(self._repo) != 0 or not parentrepo.shared():
622 622 return True
623 623 self._repo._subsource = source
624 624 srcurl = _abssource(self._repo)
625 625 other = hg.peer(self._repo, {}, srcurl)
626 626 if len(self._repo) == 0:
627 627 # use self._repo.vfs instead of self.wvfs to remove .hg only
628 628 self._repo.vfs.rmtree()
629 629
630 630 # A remote subrepo could be shared if there is a local copy
631 631 # relative to the parent's share source. But clone pooling doesn't
632 632 # assemble the repos in a tree, so that can't be consistently done.
633 633 # A simpler option is for the user to configure clone pooling, and
634 634 # work with that.
635 635 if parentrepo.shared() and hg.islocal(srcurl):
636 636 self.ui.status(_('sharing subrepo %s from %s\n')
637 637 % (subrelpath(self), srcurl))
638 638 shared = hg.share(self._repo._subparent.baseui,
639 639 other, self._repo.root,
640 640 update=False, bookmarks=False)
641 641 self._repo = shared.local()
642 642 else:
643 643 # TODO: find a common place for this and this code in the
644 644 # share.py wrap of the clone command.
645 645 if parentrepo.shared():
646 646 pool = self.ui.config('share', 'pool')
647 647 if pool:
648 648 pool = util.expandpath(pool)
649 649
650 650 shareopts = {
651 651 'pool': pool,
652 652 'mode': self.ui.config('share', 'poolnaming'),
653 653 }
654 654 else:
655 655 shareopts = {}
656 656
657 657 self.ui.status(_('cloning subrepo %s from %s\n')
658 658 % (subrelpath(self), srcurl))
659 659 other, cloned = hg.clone(self._repo._subparent.baseui, {},
660 660 other, self._repo.root,
661 661 update=False, shareopts=shareopts)
662 662 self._repo = cloned.local()
663 663 self._initrepo(parentrepo, source, create=True)
664 664 self._cachestorehash(srcurl)
665 665 else:
666 666 self.ui.status(_('pulling subrepo %s from %s\n')
667 667 % (subrelpath(self), srcurl))
668 668 cleansub = self.storeclean(srcurl)
669 669 exchange.pull(self._repo, other)
670 670 if cleansub:
671 671 # keep the repo clean after pull
672 672 self._cachestorehash(srcurl)
673 673 return False
674 674
675 675 @annotatesubrepoerror
676 676 def get(self, state, overwrite=False):
677 677 inrepo = self._get(state)
678 678 source, revision, kind = state
679 679 repo = self._repo
680 680 repo.ui.debug("getting subrepo %s\n" % self._path)
681 681 if inrepo:
682 682 urepo = repo.unfiltered()
683 683 ctx = urepo[revision]
684 684 if ctx.hidden():
685 685 urepo.ui.warn(
686 686 _('revision %s in subrepository "%s" is hidden\n') \
687 687 % (revision[0:12], self._path))
688 688 repo = urepo
689 689 hg.updaterepo(repo, revision, overwrite)
690 690
691 691 @annotatesubrepoerror
692 692 def merge(self, state):
693 693 self._get(state)
694 694 cur = self._repo['.']
695 695 dst = self._repo[state[1]]
696 696 anc = dst.ancestor(cur)
697 697
698 698 def mergefunc():
699 699 if anc == cur and dst.branch() == cur.branch():
700 700 self.ui.debug('updating subrepository "%s"\n'
701 701 % subrelpath(self))
702 702 hg.update(self._repo, state[1])
703 703 elif anc == dst:
704 704 self.ui.debug('skipping subrepository "%s"\n'
705 705 % subrelpath(self))
706 706 else:
707 707 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
708 708 hg.merge(self._repo, state[1], remind=False)
709 709
710 710 wctx = self._repo[None]
711 711 if self.dirty():
712 712 if anc != dst:
713 713 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
714 714 mergefunc()
715 715 else:
716 716 mergefunc()
717 717 else:
718 718 mergefunc()
719 719
720 720 @annotatesubrepoerror
721 721 def push(self, opts):
722 722 force = opts.get('force')
723 723 newbranch = opts.get('new_branch')
724 724 ssh = opts.get('ssh')
725 725
726 726 # push subrepos depth-first for coherent ordering
727 727 c = self._repo['.']
728 728 subs = c.substate # only repos that are committed
729 729 for s in sorted(subs):
730 730 if c.sub(s).push(opts) == 0:
731 731 return False
732 732
733 733 dsturl = _abssource(self._repo, True)
734 734 if not force:
735 735 if self.storeclean(dsturl):
736 736 self.ui.status(
737 737 _('no changes made to subrepo %s since last push to %s\n')
738 738 % (subrelpath(self), dsturl))
739 739 return None
740 740 self.ui.status(_('pushing subrepo %s to %s\n') %
741 741 (subrelpath(self), dsturl))
742 742 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
743 743 res = exchange.push(self._repo, other, force, newbranch=newbranch)
744 744
745 745 # the repo is now clean
746 746 self._cachestorehash(dsturl)
747 747 return res.cgresult
748 748
749 749 @annotatesubrepoerror
750 750 def outgoing(self, ui, dest, opts):
751 751 if 'rev' in opts or 'branch' in opts:
752 752 opts = copy.copy(opts)
753 753 opts.pop('rev', None)
754 754 opts.pop('branch', None)
755 755 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
756 756
757 757 @annotatesubrepoerror
758 758 def incoming(self, ui, source, opts):
759 759 if 'rev' in opts or 'branch' in opts:
760 760 opts = copy.copy(opts)
761 761 opts.pop('rev', None)
762 762 opts.pop('branch', None)
763 763 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
764 764
765 765 @annotatesubrepoerror
766 766 def files(self):
767 767 rev = self._state[1]
768 768 ctx = self._repo[rev]
769 769 return ctx.manifest().keys()
770 770
771 771 def filedata(self, name, decode):
772 772 rev = self._state[1]
773 773 data = self._repo[rev][name].data()
774 774 if decode:
775 775 data = self._repo.wwritedata(name, data)
776 776 return data
777 777
778 778 def fileflags(self, name):
779 779 rev = self._state[1]
780 780 ctx = self._repo[rev]
781 781 return ctx.flags(name)
782 782
783 783 @annotatesubrepoerror
784 784 def printfiles(self, ui, m, fm, fmt, subrepos):
785 785 # If the parent context is a workingctx, use the workingctx here for
786 786 # consistency.
787 787 if self._ctx.rev() is None:
788 788 ctx = self._repo[None]
789 789 else:
790 790 rev = self._state[1]
791 791 ctx = self._repo[rev]
792 792 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
793 793
794 794 @annotatesubrepoerror
795 795 def getfileset(self, expr):
796 796 if self._ctx.rev() is None:
797 797 ctx = self._repo[None]
798 798 else:
799 799 rev = self._state[1]
800 800 ctx = self._repo[rev]
801 801
802 802 files = ctx.getfileset(expr)
803 803
804 804 for subpath in ctx.substate:
805 805 sub = ctx.sub(subpath)
806 806
807 807 try:
808 808 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
809 809 except error.LookupError:
810 810 self.ui.status(_("skipping missing subrepository: %s\n")
811 811 % self.wvfs.reljoin(reporelpath(self), subpath))
812 812 return files
813 813
814 814 def walk(self, match):
815 815 ctx = self._repo[None]
816 816 return ctx.walk(match)
817 817
818 818 @annotatesubrepoerror
819 819 def forget(self, match, prefix, dryrun, interactive):
820 820 return cmdutil.forget(self.ui, self._repo, match,
821 821 self.wvfs.reljoin(prefix, self._path),
822 822 True, dryrun=dryrun, interactive=interactive)
823 823
824 824 @annotatesubrepoerror
825 825 def removefiles(self, matcher, prefix, after, force, subrepos,
826 826 dryrun, warnings):
827 827 return cmdutil.remove(self.ui, self._repo, matcher,
828 828 self.wvfs.reljoin(prefix, self._path),
829 829 after, force, subrepos, dryrun)
830 830
831 831 @annotatesubrepoerror
832 832 def revert(self, substate, *pats, **opts):
833 833 # reverting a subrepo is a 2 step process:
834 834 # 1. if the no_backup is not set, revert all modified
835 835 # files inside the subrepo
836 836 # 2. update the subrepo to the revision specified in
837 837 # the corresponding substate dictionary
838 838 self.ui.status(_('reverting subrepo %s\n') % substate[0])
839 839 if not opts.get(r'no_backup'):
840 840 # Revert all files on the subrepo, creating backups
841 841 # Note that this will not recursively revert subrepos
842 842 # We could do it if there was a set:subrepos() predicate
843 843 opts = opts.copy()
844 844 opts[r'date'] = None
845 845 opts[r'rev'] = substate[1]
846 846
847 847 self.filerevert(*pats, **opts)
848 848
849 849 # Update the repo to the revision specified in the given substate
850 850 if not opts.get(r'dry_run'):
851 851 self.get(substate, overwrite=True)
852 852
853 853 def filerevert(self, *pats, **opts):
854 854 ctx = self._repo[opts[r'rev']]
855 855 parents = self._repo.dirstate.parents()
856 856 if opts.get(r'all'):
857 857 pats = ['set:modified()']
858 858 else:
859 859 pats = []
860 860 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
861 861
862 862 def shortid(self, revid):
863 863 return revid[:12]
864 864
865 865 @annotatesubrepoerror
866 866 def unshare(self):
867 867 # subrepo inherently violates our import layering rules
868 868 # because it wants to make repo objects from deep inside the stack
869 869 # so we manually delay the circular imports to not break
870 870 # scripts that don't use our demand-loading
871 871 global hg
872 872 from . import hg as h
873 873 hg = h
874 874
875 875 # Nothing prevents a user from sharing in a repo, and then making that a
876 876 # subrepo. Alternately, the previous unshare attempt may have failed
877 877 # part way through. So recurse whether or not this layer is shared.
878 878 if self._repo.shared():
879 879 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
880 880
881 881 hg.unshare(self.ui, self._repo)
882 882
883 883 def verify(self):
884 884 try:
885 885 rev = self._state[1]
886 886 ctx = self._repo.unfiltered()[rev]
887 887 if ctx.hidden():
888 888 # Since hidden revisions aren't pushed/pulled, it seems worth an
889 889 # explicit warning.
890 890 ui = self._repo.ui
891 891 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
892 892 (self._relpath, node.short(self._ctx.node())))
893 893 return 0
894 894 except error.RepoLookupError:
895 895 # A missing subrepo revision may be a case of needing to pull it, so
896 896 # don't treat this as an error.
897 897 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
898 898 (self._relpath, node.short(self._ctx.node())))
899 899 return 0
900 900
901 901 @propertycache
902 902 def wvfs(self):
903 903 """return own wvfs for efficiency and consistency
904 904 """
905 905 return self._repo.wvfs
906 906
907 907 @propertycache
908 908 def _relpath(self):
909 909 """return path to this subrepository as seen from outermost repository
910 910 """
911 911 # Keep consistent dir separators by avoiding vfs.join(self._path)
912 912 return reporelpath(self._repo)
913 913
914 914 class svnsubrepo(abstractsubrepo):
915 915 def __init__(self, ctx, path, state, allowcreate):
916 916 super(svnsubrepo, self).__init__(ctx, path)
917 917 self._state = state
918 918 self._exe = procutil.findexe('svn')
919 919 if not self._exe:
920 920 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
921 921 % self._path)
922 922
923 923 def _svncommand(self, commands, filename='', failok=False):
924 924 cmd = [self._exe]
925 925 extrakw = {}
926 926 if not self.ui.interactive():
927 927 # Making stdin be a pipe should prevent svn from behaving
928 928 # interactively even if we can't pass --non-interactive.
929 929 extrakw[r'stdin'] = subprocess.PIPE
930 930 # Starting in svn 1.5 --non-interactive is a global flag
931 931 # instead of being per-command, but we need to support 1.4 so
932 932 # we have to be intelligent about what commands take
933 933 # --non-interactive.
934 934 if commands[0] in ('update', 'checkout', 'commit'):
935 935 cmd.append('--non-interactive')
936 936 cmd.extend(commands)
937 937 if filename is not None:
938 938 path = self.wvfs.reljoin(self._ctx.repo().origroot,
939 939 self._path, filename)
940 940 cmd.append(path)
941 941 env = dict(encoding.environ)
942 942 # Avoid localized output, preserve current locale for everything else.
943 943 lc_all = env.get('LC_ALL')
944 944 if lc_all:
945 945 env['LANG'] = lc_all
946 946 del env['LC_ALL']
947 947 env['LC_MESSAGES'] = 'C'
948 948 p = subprocess.Popen(cmd, bufsize=-1, close_fds=procutil.closefds,
949 949 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
950 950 universal_newlines=True, env=env, **extrakw)
951 951 stdout, stderr = p.communicate()
952 952 stderr = stderr.strip()
953 953 if not failok:
954 954 if p.returncode:
955 955 raise error.Abort(stderr or 'exited with code %d'
956 956 % p.returncode)
957 957 if stderr:
958 958 self.ui.warn(stderr + '\n')
959 959 return stdout, stderr
960 960
961 961 @propertycache
962 962 def _svnversion(self):
963 963 output, err = self._svncommand(['--version', '--quiet'], filename=None)
964 964 m = re.search(br'^(\d+)\.(\d+)', output)
965 965 if not m:
966 966 raise error.Abort(_('cannot retrieve svn tool version'))
967 967 return (int(m.group(1)), int(m.group(2)))
968 968
969 969 def _svnmissing(self):
970 970 return not self.wvfs.exists('.svn')
971 971
972 972 def _wcrevs(self):
973 973 # Get the working directory revision as well as the last
974 974 # commit revision so we can compare the subrepo state with
975 975 # both. We used to store the working directory one.
976 976 output, err = self._svncommand(['info', '--xml'])
977 977 doc = xml.dom.minidom.parseString(output)
978 978 entries = doc.getElementsByTagName('entry')
979 979 lastrev, rev = '0', '0'
980 980 if entries:
981 981 rev = str(entries[0].getAttribute('revision')) or '0'
982 982 commits = entries[0].getElementsByTagName('commit')
983 983 if commits:
984 984 lastrev = str(commits[0].getAttribute('revision')) or '0'
985 985 return (lastrev, rev)
986 986
987 987 def _wcrev(self):
988 988 return self._wcrevs()[0]
989 989
990 990 def _wcchanged(self):
991 991 """Return (changes, extchanges, missing) where changes is True
992 992 if the working directory was changed, extchanges is
993 993 True if any of these changes concern an external entry and missing
994 994 is True if any change is a missing entry.
995 995 """
996 996 output, err = self._svncommand(['status', '--xml'])
997 997 externals, changes, missing = [], [], []
998 998 doc = xml.dom.minidom.parseString(output)
999 999 for e in doc.getElementsByTagName('entry'):
1000 1000 s = e.getElementsByTagName('wc-status')
1001 1001 if not s:
1002 1002 continue
1003 1003 item = s[0].getAttribute('item')
1004 1004 props = s[0].getAttribute('props')
1005 1005 path = e.getAttribute('path')
1006 1006 if item == 'external':
1007 1007 externals.append(path)
1008 1008 elif item == 'missing':
1009 1009 missing.append(path)
1010 1010 if (item not in ('', 'normal', 'unversioned', 'external')
1011 1011 or props not in ('', 'none', 'normal')):
1012 1012 changes.append(path)
1013 1013 for path in changes:
1014 1014 for ext in externals:
1015 1015 if path == ext or path.startswith(ext + pycompat.ossep):
1016 1016 return True, True, bool(missing)
1017 1017 return bool(changes), False, bool(missing)
1018 1018
1019 1019 @annotatesubrepoerror
1020 1020 def dirty(self, ignoreupdate=False, missing=False):
1021 1021 if self._svnmissing():
1022 1022 return self._state[1] != ''
1023 1023 wcchanged = self._wcchanged()
1024 1024 changed = wcchanged[0] or (missing and wcchanged[2])
1025 1025 if not changed:
1026 1026 if self._state[1] in self._wcrevs() or ignoreupdate:
1027 1027 return False
1028 1028 return True
1029 1029
1030 1030 def basestate(self):
1031 1031 lastrev, rev = self._wcrevs()
1032 1032 if lastrev != rev:
1033 1033 # Last committed rev is not the same than rev. We would
1034 1034 # like to take lastrev but we do not know if the subrepo
1035 1035 # URL exists at lastrev. Test it and fallback to rev it
1036 1036 # is not there.
1037 1037 try:
1038 1038 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1039 1039 return lastrev
1040 1040 except error.Abort:
1041 1041 pass
1042 1042 return rev
1043 1043
1044 1044 @annotatesubrepoerror
1045 1045 def commit(self, text, user, date):
1046 1046 # user and date are out of our hands since svn is centralized
1047 1047 changed, extchanged, missing = self._wcchanged()
1048 1048 if not changed:
1049 1049 return self.basestate()
1050 1050 if extchanged:
1051 1051 # Do not try to commit externals
1052 1052 raise error.Abort(_('cannot commit svn externals'))
1053 1053 if missing:
1054 1054 # svn can commit with missing entries but aborting like hg
1055 1055 # seems a better approach.
1056 1056 raise error.Abort(_('cannot commit missing svn entries'))
1057 1057 commitinfo, err = self._svncommand(['commit', '-m', text])
1058 1058 self.ui.status(commitinfo)
1059 1059 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1060 1060 if not newrev:
1061 1061 if not commitinfo.strip():
1062 1062 # Sometimes, our definition of "changed" differs from
1063 1063 # svn one. For instance, svn ignores missing files
1064 1064 # when committing. If there are only missing files, no
1065 1065 # commit is made, no output and no error code.
1066 1066 raise error.Abort(_('failed to commit svn changes'))
1067 1067 raise error.Abort(commitinfo.splitlines()[-1])
1068 1068 newrev = newrev.groups()[0]
1069 1069 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1070 1070 return newrev
1071 1071
1072 1072 @annotatesubrepoerror
1073 1073 def remove(self):
1074 1074 if self.dirty():
1075 1075 self.ui.warn(_('not removing repo %s because '
1076 1076 'it has changes.\n') % self._path)
1077 1077 return
1078 1078 self.ui.note(_('removing subrepo %s\n') % self._path)
1079 1079
1080 1080 self.wvfs.rmtree(forcibly=True)
1081 1081 try:
1082 1082 pwvfs = self._ctx.repo().wvfs
1083 1083 pwvfs.removedirs(pwvfs.dirname(self._path))
1084 1084 except OSError:
1085 1085 pass
1086 1086
1087 1087 @annotatesubrepoerror
1088 1088 def get(self, state, overwrite=False):
1089 1089 if overwrite:
1090 1090 self._svncommand(['revert', '--recursive'])
1091 1091 args = ['checkout']
1092 1092 if self._svnversion >= (1, 5):
1093 1093 args.append('--force')
1094 1094 # The revision must be specified at the end of the URL to properly
1095 1095 # update to a directory which has since been deleted and recreated.
1096 1096 args.append('%s@%s' % (state[0], state[1]))
1097 1097
1098 1098 # SEC: check that the ssh url is safe
1099 1099 util.checksafessh(state[0])
1100 1100
1101 1101 status, err = self._svncommand(args, failok=True)
1102 1102 _sanitize(self.ui, self.wvfs, '.svn')
1103 1103 if not re.search('Checked out revision [0-9]+.', status):
1104 1104 if ('is already a working copy for a different URL' in err
1105 1105 and (self._wcchanged()[:2] == (False, False))):
1106 1106 # obstructed but clean working copy, so just blow it away.
1107 1107 self.remove()
1108 1108 self.get(state, overwrite=False)
1109 1109 return
1110 1110 raise error.Abort((status or err).splitlines()[-1])
1111 1111 self.ui.status(status)
1112 1112
1113 1113 @annotatesubrepoerror
1114 1114 def merge(self, state):
1115 1115 old = self._state[1]
1116 1116 new = state[1]
1117 1117 wcrev = self._wcrev()
1118 1118 if new != wcrev:
1119 1119 dirty = old == wcrev or self._wcchanged()[0]
1120 1120 if _updateprompt(self.ui, self, dirty, wcrev, new):
1121 1121 self.get(state, False)
1122 1122
1123 1123 def push(self, opts):
1124 1124 # push is a no-op for SVN
1125 1125 return True
1126 1126
1127 1127 @annotatesubrepoerror
1128 1128 def files(self):
1129 1129 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1130 1130 doc = xml.dom.minidom.parseString(output)
1131 1131 paths = []
1132 1132 for e in doc.getElementsByTagName('entry'):
1133 1133 kind = pycompat.bytestr(e.getAttribute('kind'))
1134 1134 if kind != 'file':
1135 1135 continue
1136 1136 name = ''.join(c.data for c
1137 1137 in e.getElementsByTagName('name')[0].childNodes
1138 1138 if c.nodeType == c.TEXT_NODE)
1139 1139 paths.append(name.encode('utf-8'))
1140 1140 return paths
1141 1141
1142 1142 def filedata(self, name, decode):
1143 1143 return self._svncommand(['cat'], name)[0]
1144 1144
1145 1145
1146 1146 class gitsubrepo(abstractsubrepo):
1147 1147 def __init__(self, ctx, path, state, allowcreate):
1148 1148 super(gitsubrepo, self).__init__(ctx, path)
1149 1149 self._state = state
1150 1150 self._abspath = ctx.repo().wjoin(path)
1151 1151 self._subparent = ctx.repo()
1152 1152 self._ensuregit()
1153 1153
1154 1154 def _ensuregit(self):
1155 1155 try:
1156 1156 self._gitexecutable = 'git'
1157 1157 out, err = self._gitnodir(['--version'])
1158 1158 except OSError as e:
1159 1159 genericerror = _("error executing git for subrepo '%s': %s")
1160 1160 notfoundhint = _("check git is installed and in your PATH")
1161 1161 if e.errno != errno.ENOENT:
1162 1162 raise error.Abort(genericerror % (
1163 1163 self._path, encoding.strtolocal(e.strerror)))
1164 1164 elif pycompat.iswindows:
1165 1165 try:
1166 1166 self._gitexecutable = 'git.cmd'
1167 1167 out, err = self._gitnodir(['--version'])
1168 1168 except OSError as e2:
1169 1169 if e2.errno == errno.ENOENT:
1170 1170 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1171 1171 " for subrepo '%s'") % self._path,
1172 1172 hint=notfoundhint)
1173 1173 else:
1174 1174 raise error.Abort(genericerror % (self._path,
1175 1175 encoding.strtolocal(e2.strerror)))
1176 1176 else:
1177 1177 raise error.Abort(_("couldn't find git for subrepo '%s'")
1178 1178 % self._path, hint=notfoundhint)
1179 1179 versionstatus = self._checkversion(out)
1180 1180 if versionstatus == 'unknown':
1181 1181 self.ui.warn(_('cannot retrieve git version\n'))
1182 1182 elif versionstatus == 'abort':
1183 1183 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1184 1184 elif versionstatus == 'warning':
1185 1185 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1186 1186
1187 1187 @staticmethod
1188 1188 def _gitversion(out):
1189 1189 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1190 1190 if m:
1191 1191 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1192 1192
1193 1193 m = re.search(br'^git version (\d+)\.(\d+)', out)
1194 1194 if m:
1195 1195 return (int(m.group(1)), int(m.group(2)), 0)
1196 1196
1197 1197 return -1
1198 1198
1199 1199 @staticmethod
1200 1200 def _checkversion(out):
1201 1201 '''ensure git version is new enough
1202 1202
1203 1203 >>> _checkversion = gitsubrepo._checkversion
1204 1204 >>> _checkversion(b'git version 1.6.0')
1205 1205 'ok'
1206 1206 >>> _checkversion(b'git version 1.8.5')
1207 1207 'ok'
1208 1208 >>> _checkversion(b'git version 1.4.0')
1209 1209 'abort'
1210 1210 >>> _checkversion(b'git version 1.5.0')
1211 1211 'warning'
1212 1212 >>> _checkversion(b'git version 1.9-rc0')
1213 1213 'ok'
1214 1214 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1215 1215 'ok'
1216 1216 >>> _checkversion(b'git version 1.9.0.GIT')
1217 1217 'ok'
1218 1218 >>> _checkversion(b'git version 12345')
1219 1219 'unknown'
1220 1220 >>> _checkversion(b'no')
1221 1221 'unknown'
1222 1222 '''
1223 1223 version = gitsubrepo._gitversion(out)
1224 1224 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1225 1225 # despite the docstring comment. For now, error on 1.4.0, warn on
1226 1226 # 1.5.0 but attempt to continue.
1227 1227 if version == -1:
1228 1228 return 'unknown'
1229 1229 if version < (1, 5, 0):
1230 1230 return 'abort'
1231 1231 elif version < (1, 6, 0):
1232 1232 return 'warning'
1233 1233 return 'ok'
1234 1234
1235 1235 def _gitcommand(self, commands, env=None, stream=False):
1236 1236 return self._gitdir(commands, env=env, stream=stream)[0]
1237 1237
1238 1238 def _gitdir(self, commands, env=None, stream=False):
1239 1239 return self._gitnodir(commands, env=env, stream=stream,
1240 1240 cwd=self._abspath)
1241 1241
1242 1242 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1243 1243 """Calls the git command
1244 1244
1245 1245 The methods tries to call the git command. versions prior to 1.6.0
1246 1246 are not supported and very probably fail.
1247 1247 """
1248 1248 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1249 1249 if env is None:
1250 1250 env = encoding.environ.copy()
1251 1251 # disable localization for Git output (issue5176)
1252 1252 env['LC_ALL'] = 'C'
1253 1253 # fix for Git CVE-2015-7545
1254 1254 if 'GIT_ALLOW_PROTOCOL' not in env:
1255 1255 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1256 1256 # unless ui.quiet is set, print git's stderr,
1257 1257 # which is mostly progress and useful info
1258 1258 errpipe = None
1259 1259 if self.ui.quiet:
1260 1260 errpipe = open(os.devnull, 'w')
1261 1261 if self.ui._colormode and len(commands) and commands[0] == "diff":
1262 1262 # insert the argument in the front,
1263 1263 # the end of git diff arguments is used for paths
1264 1264 commands.insert(1, '--color')
1265 1265 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1266 1266 cwd=cwd, env=env, close_fds=procutil.closefds,
1267 1267 stdout=subprocess.PIPE, stderr=errpipe)
1268 1268 if stream:
1269 1269 return p.stdout, None
1270 1270
1271 1271 retdata = p.stdout.read().strip()
1272 1272 # wait for the child to exit to avoid race condition.
1273 1273 p.wait()
1274 1274
1275 1275 if p.returncode != 0 and p.returncode != 1:
1276 1276 # there are certain error codes that are ok
1277 1277 command = commands[0]
1278 1278 if command in ('cat-file', 'symbolic-ref'):
1279 1279 return retdata, p.returncode
1280 1280 # for all others, abort
1281 1281 raise error.Abort(_('git %s error %d in %s') %
1282 1282 (command, p.returncode, self._relpath))
1283 1283
1284 1284 return retdata, p.returncode
1285 1285
1286 1286 def _gitmissing(self):
1287 1287 return not self.wvfs.exists('.git')
1288 1288
1289 1289 def _gitstate(self):
1290 1290 return self._gitcommand(['rev-parse', 'HEAD'])
1291 1291
1292 1292 def _gitcurrentbranch(self):
1293 1293 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1294 1294 if err:
1295 1295 current = None
1296 1296 return current
1297 1297
1298 1298 def _gitremote(self, remote):
1299 1299 out = self._gitcommand(['remote', 'show', '-n', remote])
1300 1300 line = out.split('\n')[1]
1301 1301 i = line.index('URL: ') + len('URL: ')
1302 1302 return line[i:]
1303 1303
1304 1304 def _githavelocally(self, revision):
1305 1305 out, code = self._gitdir(['cat-file', '-e', revision])
1306 1306 return code == 0
1307 1307
1308 1308 def _gitisancestor(self, r1, r2):
1309 1309 base = self._gitcommand(['merge-base', r1, r2])
1310 1310 return base == r1
1311 1311
1312 1312 def _gitisbare(self):
1313 1313 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1314 1314
1315 1315 def _gitupdatestat(self):
1316 1316 """This must be run before git diff-index.
1317 1317 diff-index only looks at changes to file stat;
1318 1318 this command looks at file contents and updates the stat."""
1319 1319 self._gitcommand(['update-index', '-q', '--refresh'])
1320 1320
1321 1321 def _gitbranchmap(self):
1322 1322 '''returns 2 things:
1323 1323 a map from git branch to revision
1324 1324 a map from revision to branches'''
1325 1325 branch2rev = {}
1326 1326 rev2branch = {}
1327 1327
1328 1328 out = self._gitcommand(['for-each-ref', '--format',
1329 1329 '%(objectname) %(refname)'])
1330 1330 for line in out.split('\n'):
1331 1331 revision, ref = line.split(' ')
1332 1332 if (not ref.startswith('refs/heads/') and
1333 1333 not ref.startswith('refs/remotes/')):
1334 1334 continue
1335 1335 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1336 1336 continue # ignore remote/HEAD redirects
1337 1337 branch2rev[ref] = revision
1338 1338 rev2branch.setdefault(revision, []).append(ref)
1339 1339 return branch2rev, rev2branch
1340 1340
1341 1341 def _gittracking(self, branches):
1342 1342 'return map of remote branch to local tracking branch'
1343 1343 # assumes no more than one local tracking branch for each remote
1344 1344 tracking = {}
1345 1345 for b in branches:
1346 1346 if b.startswith('refs/remotes/'):
1347 1347 continue
1348 1348 bname = b.split('/', 2)[2]
1349 1349 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1350 1350 if remote:
1351 1351 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1352 1352 tracking['refs/remotes/%s/%s' %
1353 1353 (remote, ref.split('/', 2)[2])] = b
1354 1354 return tracking
1355 1355
1356 1356 def _abssource(self, source):
1357 1357 if '://' not in source:
1358 1358 # recognize the scp syntax as an absolute source
1359 1359 colon = source.find(':')
1360 1360 if colon != -1 and '/' not in source[:colon]:
1361 1361 return source
1362 1362 self._subsource = source
1363 1363 return _abssource(self)
1364 1364
1365 1365 def _fetch(self, source, revision):
1366 1366 if self._gitmissing():
1367 1367 # SEC: check for safe ssh url
1368 1368 util.checksafessh(source)
1369 1369
1370 1370 source = self._abssource(source)
1371 1371 self.ui.status(_('cloning subrepo %s from %s\n') %
1372 1372 (self._relpath, source))
1373 1373 self._gitnodir(['clone', source, self._abspath])
1374 1374 if self._githavelocally(revision):
1375 1375 return
1376 1376 self.ui.status(_('pulling subrepo %s from %s\n') %
1377 1377 (self._relpath, self._gitremote('origin')))
1378 1378 # try only origin: the originally cloned repo
1379 1379 self._gitcommand(['fetch'])
1380 1380 if not self._githavelocally(revision):
1381 1381 raise error.Abort(_('revision %s does not exist in subrepository '
1382 1382 '"%s"\n') % (revision, self._relpath))
1383 1383
1384 1384 @annotatesubrepoerror
1385 1385 def dirty(self, ignoreupdate=False, missing=False):
1386 1386 if self._gitmissing():
1387 1387 return self._state[1] != ''
1388 1388 if self._gitisbare():
1389 1389 return True
1390 1390 if not ignoreupdate and self._state[1] != self._gitstate():
1391 1391 # different version checked out
1392 1392 return True
1393 1393 # check for staged changes or modified files; ignore untracked files
1394 1394 self._gitupdatestat()
1395 1395 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1396 1396 return code == 1
1397 1397
1398 1398 def basestate(self):
1399 1399 return self._gitstate()
1400 1400
1401 1401 @annotatesubrepoerror
1402 1402 def get(self, state, overwrite=False):
1403 1403 source, revision, kind = state
1404 1404 if not revision:
1405 1405 self.remove()
1406 1406 return
1407 1407 self._fetch(source, revision)
1408 1408 # if the repo was set to be bare, unbare it
1409 1409 if self._gitisbare():
1410 1410 self._gitcommand(['config', 'core.bare', 'false'])
1411 1411 if self._gitstate() == revision:
1412 1412 self._gitcommand(['reset', '--hard', 'HEAD'])
1413 1413 return
1414 1414 elif self._gitstate() == revision:
1415 1415 if overwrite:
1416 1416 # first reset the index to unmark new files for commit, because
1417 1417 # reset --hard will otherwise throw away files added for commit,
1418 1418 # not just unmark them.
1419 1419 self._gitcommand(['reset', 'HEAD'])
1420 1420 self._gitcommand(['reset', '--hard', 'HEAD'])
1421 1421 return
1422 1422 branch2rev, rev2branch = self._gitbranchmap()
1423 1423
1424 1424 def checkout(args):
1425 1425 cmd = ['checkout']
1426 1426 if overwrite:
1427 1427 # first reset the index to unmark new files for commit, because
1428 1428 # the -f option will otherwise throw away files added for
1429 1429 # commit, not just unmark them.
1430 1430 self._gitcommand(['reset', 'HEAD'])
1431 1431 cmd.append('-f')
1432 1432 self._gitcommand(cmd + args)
1433 1433 _sanitize(self.ui, self.wvfs, '.git')
1434 1434
1435 1435 def rawcheckout():
1436 1436 # no branch to checkout, check it out with no branch
1437 1437 self.ui.warn(_('checking out detached HEAD in '
1438 1438 'subrepository "%s"\n') % self._relpath)
1439 1439 self.ui.warn(_('check out a git branch if you intend '
1440 1440 'to make changes\n'))
1441 1441 checkout(['-q', revision])
1442 1442
1443 1443 if revision not in rev2branch:
1444 1444 rawcheckout()
1445 1445 return
1446 1446 branches = rev2branch[revision]
1447 1447 firstlocalbranch = None
1448 1448 for b in branches:
1449 1449 if b == 'refs/heads/master':
1450 1450 # master trumps all other branches
1451 1451 checkout(['refs/heads/master'])
1452 1452 return
1453 1453 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1454 1454 firstlocalbranch = b
1455 1455 if firstlocalbranch:
1456 1456 checkout([firstlocalbranch])
1457 1457 return
1458 1458
1459 1459 tracking = self._gittracking(branch2rev.keys())
1460 1460 # choose a remote branch already tracked if possible
1461 1461 remote = branches[0]
1462 1462 if remote not in tracking:
1463 1463 for b in branches:
1464 1464 if b in tracking:
1465 1465 remote = b
1466 1466 break
1467 1467
1468 1468 if remote not in tracking:
1469 1469 # create a new local tracking branch
1470 1470 local = remote.split('/', 3)[3]
1471 1471 checkout(['-b', local, remote])
1472 1472 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1473 1473 # When updating to a tracked remote branch,
1474 1474 # if the local tracking branch is downstream of it,
1475 1475 # a normal `git pull` would have performed a "fast-forward merge"
1476 1476 # which is equivalent to updating the local branch to the remote.
1477 1477 # Since we are only looking at branching at update, we need to
1478 1478 # detect this situation and perform this action lazily.
1479 1479 if tracking[remote] != self._gitcurrentbranch():
1480 1480 checkout([tracking[remote]])
1481 1481 self._gitcommand(['merge', '--ff', remote])
1482 1482 _sanitize(self.ui, self.wvfs, '.git')
1483 1483 else:
1484 1484 # a real merge would be required, just checkout the revision
1485 1485 rawcheckout()
1486 1486
1487 1487 @annotatesubrepoerror
1488 1488 def commit(self, text, user, date):
1489 1489 if self._gitmissing():
1490 1490 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1491 1491 cmd = ['commit', '-a', '-m', text]
1492 1492 env = encoding.environ.copy()
1493 1493 if user:
1494 1494 cmd += ['--author', user]
1495 1495 if date:
1496 1496 # git's date parser silently ignores when seconds < 1e9
1497 1497 # convert to ISO8601
1498 1498 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1499 1499 '%Y-%m-%dT%H:%M:%S %1%2')
1500 1500 self._gitcommand(cmd, env=env)
1501 1501 # make sure commit works otherwise HEAD might not exist under certain
1502 1502 # circumstances
1503 1503 return self._gitstate()
1504 1504
1505 1505 @annotatesubrepoerror
1506 1506 def merge(self, state):
1507 1507 source, revision, kind = state
1508 1508 self._fetch(source, revision)
1509 1509 base = self._gitcommand(['merge-base', revision, self._state[1]])
1510 1510 self._gitupdatestat()
1511 1511 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1512 1512
1513 1513 def mergefunc():
1514 1514 if base == revision:
1515 1515 self.get(state) # fast forward merge
1516 1516 elif base != self._state[1]:
1517 1517 self._gitcommand(['merge', '--no-commit', revision])
1518 1518 _sanitize(self.ui, self.wvfs, '.git')
1519 1519
1520 1520 if self.dirty():
1521 1521 if self._gitstate() != revision:
1522 1522 dirty = self._gitstate() == self._state[1] or code != 0
1523 1523 if _updateprompt(self.ui, self, dirty,
1524 1524 self._state[1][:7], revision[:7]):
1525 1525 mergefunc()
1526 1526 else:
1527 1527 mergefunc()
1528 1528
1529 1529 @annotatesubrepoerror
1530 1530 def push(self, opts):
1531 1531 force = opts.get('force')
1532 1532
1533 1533 if not self._state[1]:
1534 1534 return True
1535 1535 if self._gitmissing():
1536 1536 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1537 1537 # if a branch in origin contains the revision, nothing to do
1538 1538 branch2rev, rev2branch = self._gitbranchmap()
1539 1539 if self._state[1] in rev2branch:
1540 1540 for b in rev2branch[self._state[1]]:
1541 1541 if b.startswith('refs/remotes/origin/'):
1542 1542 return True
1543 1543 for b, revision in branch2rev.iteritems():
1544 1544 if b.startswith('refs/remotes/origin/'):
1545 1545 if self._gitisancestor(self._state[1], revision):
1546 1546 return True
1547 1547 # otherwise, try to push the currently checked out branch
1548 1548 cmd = ['push']
1549 1549 if force:
1550 1550 cmd.append('--force')
1551 1551
1552 1552 current = self._gitcurrentbranch()
1553 1553 if current:
1554 1554 # determine if the current branch is even useful
1555 1555 if not self._gitisancestor(self._state[1], current):
1556 1556 self.ui.warn(_('unrelated git branch checked out '
1557 1557 'in subrepository "%s"\n') % self._relpath)
1558 1558 return False
1559 1559 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1560 1560 (current.split('/', 2)[2], self._relpath))
1561 1561 ret = self._gitdir(cmd + ['origin', current])
1562 1562 return ret[1] == 0
1563 1563 else:
1564 1564 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1565 1565 'cannot push revision %s\n') %
1566 1566 (self._relpath, self._state[1]))
1567 1567 return False
1568 1568
1569 1569 @annotatesubrepoerror
1570 1570 def add(self, ui, match, prefix, explicitonly, **opts):
1571 1571 if self._gitmissing():
1572 1572 return []
1573 1573
1574 1574 (modified, added, removed,
1575 1575 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1576 1576 clean=True)
1577 1577
1578 1578 tracked = set()
1579 1579 # dirstates 'amn' warn, 'r' is added again
1580 1580 for l in (modified, added, deleted, clean):
1581 1581 tracked.update(l)
1582 1582
1583 1583 # Unknown files not of interest will be rejected by the matcher
1584 1584 files = unknown
1585 1585 files.extend(match.files())
1586 1586
1587 1587 rejected = []
1588 1588
1589 1589 files = [f for f in sorted(set(files)) if match(f)]
1590 1590 for f in files:
1591 1591 exact = match.exact(f)
1592 1592 command = ["add"]
1593 1593 if exact:
1594 1594 command.append("-f") #should be added, even if ignored
1595 1595 if ui.verbose or not exact:
1596 1596 ui.status(_('adding %s\n') % match.rel(f))
1597 1597
1598 1598 if f in tracked: # hg prints 'adding' even if already tracked
1599 1599 if exact:
1600 1600 rejected.append(f)
1601 1601 continue
1602 1602 if not opts.get(r'dry_run'):
1603 1603 self._gitcommand(command + [f])
1604 1604
1605 1605 for f in rejected:
1606 1606 ui.warn(_("%s already tracked!\n") % match.abs(f))
1607 1607
1608 1608 return rejected
1609 1609
1610 1610 @annotatesubrepoerror
1611 1611 def remove(self):
1612 1612 if self._gitmissing():
1613 1613 return
1614 1614 if self.dirty():
1615 1615 self.ui.warn(_('not removing repo %s because '
1616 1616 'it has changes.\n') % self._relpath)
1617 1617 return
1618 1618 # we can't fully delete the repository as it may contain
1619 1619 # local-only history
1620 1620 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1621 1621 self._gitcommand(['config', 'core.bare', 'true'])
1622 1622 for f, kind in self.wvfs.readdir():
1623 1623 if f == '.git':
1624 1624 continue
1625 1625 if kind == stat.S_IFDIR:
1626 1626 self.wvfs.rmtree(f)
1627 1627 else:
1628 1628 self.wvfs.unlink(f)
1629 1629
1630 1630 def archive(self, archiver, prefix, match=None, decode=True):
1631 1631 total = 0
1632 1632 source, revision = self._state
1633 1633 if not revision:
1634 1634 return total
1635 1635 self._fetch(source, revision)
1636 1636
1637 1637 # Parse git's native archive command.
1638 1638 # This should be much faster than manually traversing the trees
1639 1639 # and objects with many subprocess calls.
1640 1640 tarstream = self._gitcommand(['archive', revision], stream=True)
1641 1641 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1642 1642 relpath = subrelpath(self)
1643 1643 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1644 1644 for i, info in enumerate(tar):
1645 1645 if info.isdir():
1646 1646 continue
1647 1647 if match and not match(info.name):
1648 1648 continue
1649 1649 if info.issym():
1650 1650 data = info.linkname
1651 1651 else:
1652 1652 data = tar.extractfile(info).read()
1653 1653 archiver.addfile(prefix + self._path + '/' + info.name,
1654 1654 info.mode, info.issym(), data)
1655 1655 total += 1
1656 1656 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1657 1657 unit=_('files'))
1658 1658 self.ui.progress(_('archiving (%s)') % relpath, None)
1659 1659 return total
1660 1660
1661 1661
1662 1662 @annotatesubrepoerror
1663 1663 def cat(self, match, fm, fntemplate, prefix, **opts):
1664 1664 rev = self._state[1]
1665 1665 if match.anypats():
1666 1666 return 1 #No support for include/exclude yet
1667 1667
1668 1668 if not match.files():
1669 1669 return 1
1670 1670
1671 1671 # TODO: add support for non-plain formatter (see cmdutil.cat())
1672 1672 for f in match.files():
1673 1673 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1674 1674 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1675 1675 pathname=self.wvfs.reljoin(prefix, f))
1676 1676 fp.write(output)
1677 1677 fp.close()
1678 1678 return 0
1679 1679
1680 1680
1681 1681 @annotatesubrepoerror
1682 1682 def status(self, rev2, **opts):
1683 1683 rev1 = self._state[1]
1684 1684 if self._gitmissing() or not rev1:
1685 1685 # if the repo is missing, return no results
1686 1686 return scmutil.status([], [], [], [], [], [], [])
1687 1687 modified, added, removed = [], [], []
1688 1688 self._gitupdatestat()
1689 1689 if rev2:
1690 1690 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1691 1691 else:
1692 1692 command = ['diff-index', '--no-renames', rev1]
1693 1693 out = self._gitcommand(command)
1694 1694 for line in out.split('\n'):
1695 1695 tab = line.find('\t')
1696 1696 if tab == -1:
1697 1697 continue
1698 status, f = line[tab - 1], line[tab + 1:]
1698 status, f = line[tab - 1:tab], line[tab + 1:]
1699 1699 if status == 'M':
1700 1700 modified.append(f)
1701 1701 elif status == 'A':
1702 1702 added.append(f)
1703 1703 elif status == 'D':
1704 1704 removed.append(f)
1705 1705
1706 1706 deleted, unknown, ignored, clean = [], [], [], []
1707 1707
1708 1708 command = ['status', '--porcelain', '-z']
1709 1709 if opts.get(r'unknown'):
1710 1710 command += ['--untracked-files=all']
1711 1711 if opts.get(r'ignored'):
1712 1712 command += ['--ignored']
1713 1713 out = self._gitcommand(command)
1714 1714
1715 1715 changedfiles = set()
1716 1716 changedfiles.update(modified)
1717 1717 changedfiles.update(added)
1718 1718 changedfiles.update(removed)
1719 1719 for line in out.split('\0'):
1720 1720 if not line:
1721 1721 continue
1722 1722 st = line[0:2]
1723 1723 #moves and copies show 2 files on one line
1724 1724 if line.find('\0') >= 0:
1725 1725 filename1, filename2 = line[3:].split('\0')
1726 1726 else:
1727 1727 filename1 = line[3:]
1728 1728 filename2 = None
1729 1729
1730 1730 changedfiles.add(filename1)
1731 1731 if filename2:
1732 1732 changedfiles.add(filename2)
1733 1733
1734 1734 if st == '??':
1735 1735 unknown.append(filename1)
1736 1736 elif st == '!!':
1737 1737 ignored.append(filename1)
1738 1738
1739 1739 if opts.get(r'clean'):
1740 1740 out = self._gitcommand(['ls-files'])
1741 1741 for f in out.split('\n'):
1742 1742 if not f in changedfiles:
1743 1743 clean.append(f)
1744 1744
1745 1745 return scmutil.status(modified, added, removed, deleted,
1746 1746 unknown, ignored, clean)
1747 1747
1748 1748 @annotatesubrepoerror
1749 1749 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1750 1750 node1 = self._state[1]
1751 1751 cmd = ['diff', '--no-renames']
1752 1752 if opts[r'stat']:
1753 1753 cmd.append('--stat')
1754 1754 else:
1755 1755 # for Git, this also implies '-p'
1756 1756 cmd.append('-U%d' % diffopts.context)
1757 1757
1758 1758 gitprefix = self.wvfs.reljoin(prefix, self._path)
1759 1759
1760 1760 if diffopts.noprefix:
1761 1761 cmd.extend(['--src-prefix=%s/' % gitprefix,
1762 1762 '--dst-prefix=%s/' % gitprefix])
1763 1763 else:
1764 1764 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1765 1765 '--dst-prefix=b/%s/' % gitprefix])
1766 1766
1767 1767 if diffopts.ignorews:
1768 1768 cmd.append('--ignore-all-space')
1769 1769 if diffopts.ignorewsamount:
1770 1770 cmd.append('--ignore-space-change')
1771 1771 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1772 1772 and diffopts.ignoreblanklines:
1773 1773 cmd.append('--ignore-blank-lines')
1774 1774
1775 1775 cmd.append(node1)
1776 1776 if node2:
1777 1777 cmd.append(node2)
1778 1778
1779 1779 output = ""
1780 1780 if match.always():
1781 1781 output += self._gitcommand(cmd) + '\n'
1782 1782 else:
1783 1783 st = self.status(node2)[:3]
1784 1784 files = [f for sublist in st for f in sublist]
1785 1785 for f in files:
1786 1786 if match(f):
1787 1787 output += self._gitcommand(cmd + ['--', f]) + '\n'
1788 1788
1789 1789 if output.strip():
1790 1790 ui.write(output)
1791 1791
1792 1792 @annotatesubrepoerror
1793 1793 def revert(self, substate, *pats, **opts):
1794 1794 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1795 1795 if not opts.get(r'no_backup'):
1796 1796 status = self.status(None)
1797 1797 names = status.modified
1798 1798 for name in names:
1799 1799 bakname = scmutil.origpath(self.ui, self._subparent, name)
1800 1800 self.ui.note(_('saving current version of %s as %s\n') %
1801 1801 (name, bakname))
1802 1802 self.wvfs.rename(name, bakname)
1803 1803
1804 1804 if not opts.get(r'dry_run'):
1805 1805 self.get(substate, overwrite=True)
1806 1806 return []
1807 1807
1808 1808 def shortid(self, revid):
1809 1809 return revid[:7]
1810 1810
1811 1811 types = {
1812 1812 'hg': hgsubrepo,
1813 1813 'svn': svnsubrepo,
1814 1814 'git': gitsubrepo,
1815 1815 }
General Comments 0
You need to be logged in to leave comments. Login now