##// END OF EJS Templates
subrepo: encode unicode path names (issue3610)...
Bryan O'Sullivan -
r17441:cb12d3ce stable
parent child Browse files
Show More
@@ -1,1273 +1,1273 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
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 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 47 l = l.lstrip()
48 48 if not l:
49 49 continue
50 50 try:
51 51 revision, path = l.split(" ", 1)
52 52 except ValueError:
53 53 raise util.Abort(_("invalid subrepository revision "
54 54 "specifier in .hgsubstate line %d")
55 55 % (i + 1))
56 56 rev[path] = revision
57 57 except IOError, err:
58 58 if err.errno != errno.ENOENT:
59 59 raise
60 60
61 61 def remap(src):
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75 return src
76 76
77 77 state = {}
78 78 for path, src in p[''].items():
79 79 kind = 'hg'
80 80 if src.startswith('['):
81 81 if ']' not in src:
82 82 raise util.Abort(_('missing ] in subrepo source'))
83 83 kind, src = src.split(']', 1)
84 84 kind = kind[1:]
85 85 src = src.lstrip() # strip any extra whitespace after ']'
86 86
87 87 if not util.url(src).isabs():
88 88 parent = _abssource(ctx._repo, abort=False)
89 89 if parent:
90 90 parent = util.url(parent)
91 91 parent.path = posixpath.join(parent.path or '', src)
92 92 parent.path = posixpath.normpath(parent.path)
93 93 joined = str(parent)
94 94 # Remap the full joined path and use it if it changes,
95 95 # else remap the original source.
96 96 remapped = remap(joined)
97 97 if remapped == joined:
98 98 src = remap(src)
99 99 else:
100 100 src = remapped
101 101
102 102 src = remap(src)
103 103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104 104
105 105 return state
106 106
107 107 def writestate(repo, state):
108 108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111 111
112 112 def submerge(repo, wctx, mctx, actx, overwrite):
113 113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 114 in working context, merging context and ancestor context"""
115 115 if mctx == actx: # backwards?
116 116 actx = wctx.p1()
117 117 s1 = wctx.substate
118 118 s2 = mctx.substate
119 119 sa = actx.substate
120 120 sm = {}
121 121
122 122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123 123
124 124 def debug(s, msg, r=""):
125 125 if r:
126 126 r = "%s:%s:%s" % r
127 127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128 128
129 129 for s, l in s1.items():
130 130 a = sa.get(s, nullstate)
131 131 ld = l # local state with possible dirty flag for compares
132 132 if wctx.sub(s).dirty():
133 133 ld = (l[0], l[1] + "+")
134 134 if wctx == actx: # overwrite
135 135 a = ld
136 136
137 137 if s in s2:
138 138 r = s2[s]
139 139 if ld == r or r == a: # no change or local is newer
140 140 sm[s] = l
141 141 continue
142 142 elif ld == a: # other side changed
143 143 debug(s, "other changed, get", r)
144 144 wctx.sub(s).get(r, overwrite)
145 145 sm[s] = r
146 146 elif ld[0] != r[0]: # sources differ
147 147 if repo.ui.promptchoice(
148 148 _(' subrepository sources for %s differ\n'
149 149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 150 % (s, l[0], r[0]),
151 151 (_('&Local'), _('&Remote')), 0):
152 152 debug(s, "prompt changed, get", r)
153 153 wctx.sub(s).get(r, overwrite)
154 154 sm[s] = r
155 155 elif ld[1] == a[1]: # local side is unchanged
156 156 debug(s, "other side changed, get", r)
157 157 wctx.sub(s).get(r, overwrite)
158 158 sm[s] = r
159 159 else:
160 160 debug(s, "both sides changed, merge with", r)
161 161 wctx.sub(s).merge(r)
162 162 sm[s] = l
163 163 elif ld == a: # remote removed, local unchanged
164 164 debug(s, "remote removed, remove")
165 165 wctx.sub(s).remove()
166 166 elif a == nullstate: # not present in remote or ancestor
167 167 debug(s, "local added, keep")
168 168 sm[s] = l
169 169 continue
170 170 else:
171 171 if repo.ui.promptchoice(
172 172 _(' local changed subrepository %s which remote removed\n'
173 173 'use (c)hanged version or (d)elete?') % s,
174 174 (_('&Changed'), _('&Delete')), 0):
175 175 debug(s, "prompt remove")
176 176 wctx.sub(s).remove()
177 177
178 178 for s, r in sorted(s2.items()):
179 179 if s in s1:
180 180 continue
181 181 elif s not in sa:
182 182 debug(s, "remote added, get", r)
183 183 mctx.sub(s).get(r)
184 184 sm[s] = r
185 185 elif r != sa[s]:
186 186 if repo.ui.promptchoice(
187 187 _(' remote changed subrepository %s which local removed\n'
188 188 'use (c)hanged version or (d)elete?') % s,
189 189 (_('&Changed'), _('&Delete')), 0) == 0:
190 190 debug(s, "prompt recreate", r)
191 191 wctx.sub(s).get(r)
192 192 sm[s] = r
193 193
194 194 # record merged .hgsubstate
195 195 writestate(repo, sm)
196 196
197 197 def _updateprompt(ui, sub, dirty, local, remote):
198 198 if dirty:
199 199 msg = (_(' subrepository sources for %s differ\n'
200 200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 201 % (subrelpath(sub), local, remote))
202 202 else:
203 203 msg = (_(' subrepository sources for %s differ (in checked out '
204 204 'version)\n'
205 205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 206 % (subrelpath(sub), local, remote))
207 207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208 208
209 209 def reporelpath(repo):
210 210 """return path to this (sub)repo as seen from outermost repo"""
211 211 parent = repo
212 212 while util.safehasattr(parent, '_subparent'):
213 213 parent = parent._subparent
214 214 p = parent.root.rstrip(os.sep)
215 215 return repo.root[len(p) + 1:]
216 216
217 217 def subrelpath(sub):
218 218 """return path to this subrepo as seen from outermost repo"""
219 219 if util.safehasattr(sub, '_relpath'):
220 220 return sub._relpath
221 221 if not util.safehasattr(sub, '_repo'):
222 222 return sub._path
223 223 return reporelpath(sub._repo)
224 224
225 225 def _abssource(repo, push=False, abort=True):
226 226 """return pull/push path of repo - either based on parent repo .hgsub info
227 227 or on the top repo config. Abort or return None if no source found."""
228 228 if util.safehasattr(repo, '_subparent'):
229 229 source = util.url(repo._subsource)
230 230 if source.isabs():
231 231 return str(source)
232 232 source.path = posixpath.normpath(source.path)
233 233 parent = _abssource(repo._subparent, push, abort=False)
234 234 if parent:
235 235 parent = util.url(util.pconvert(parent))
236 236 parent.path = posixpath.join(parent.path or '', source.path)
237 237 parent.path = posixpath.normpath(parent.path)
238 238 return str(parent)
239 239 else: # recursion reached top repo
240 240 if util.safehasattr(repo, '_subtoppath'):
241 241 return repo._subtoppath
242 242 if push and repo.ui.config('paths', 'default-push'):
243 243 return repo.ui.config('paths', 'default-push')
244 244 if repo.ui.config('paths', 'default'):
245 245 return repo.ui.config('paths', 'default')
246 246 if abort:
247 247 raise util.Abort(_("default path for subrepository %s not found") %
248 248 reporelpath(repo))
249 249
250 250 def itersubrepos(ctx1, ctx2):
251 251 """find subrepos in ctx1 or ctx2"""
252 252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 257 for subpath, ctx in sorted(subpaths.iteritems()):
258 258 yield subpath, ctx.sub(subpath)
259 259
260 260 def subrepo(ctx, path):
261 261 """return instance of the right subrepo class for subrepo in path"""
262 262 # subrepo inherently violates our import layering rules
263 263 # because it wants to make repo objects from deep inside the stack
264 264 # so we manually delay the circular imports to not break
265 265 # scripts that don't use our demand-loading
266 266 global hg
267 267 import hg as h
268 268 hg = h
269 269
270 270 scmutil.pathauditor(ctx._repo.root)(path)
271 271 state = ctx.substate[path]
272 272 if state[2] not in types:
273 273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 274 return types[state[2]](ctx, path, state[:2])
275 275
276 276 # subrepo classes need to implement the following abstract class:
277 277
278 278 class abstractsubrepo(object):
279 279
280 280 def dirty(self, ignoreupdate=False):
281 281 """returns true if the dirstate of the subrepo is dirty or does not
282 282 match current stored state. If ignoreupdate is true, only check
283 283 whether the subrepo has uncommitted changes in its dirstate.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def basestate(self):
288 288 """current working directory base state, disregarding .hgsubstate
289 289 state and working directory modifications"""
290 290 raise NotImplementedError
291 291
292 292 def checknested(self, path):
293 293 """check if path is a subrepository within this repository"""
294 294 return False
295 295
296 296 def commit(self, text, user, date):
297 297 """commit the current changes to the subrepo with the given
298 298 log message. Use given user and date if possible. Return the
299 299 new state of the subrepo.
300 300 """
301 301 raise NotImplementedError
302 302
303 303 def remove(self):
304 304 """remove the subrepo
305 305
306 306 (should verify the dirstate is not dirty first)
307 307 """
308 308 raise NotImplementedError
309 309
310 310 def get(self, state, overwrite=False):
311 311 """run whatever commands are needed to put the subrepo into
312 312 this state
313 313 """
314 314 raise NotImplementedError
315 315
316 316 def merge(self, state):
317 317 """merge currently-saved state with the new state."""
318 318 raise NotImplementedError
319 319
320 320 def push(self, opts):
321 321 """perform whatever action is analogous to 'hg push'
322 322
323 323 This may be a no-op on some systems.
324 324 """
325 325 raise NotImplementedError
326 326
327 327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 328 return []
329 329
330 330 def status(self, rev2, **opts):
331 331 return [], [], [], [], [], [], []
332 332
333 333 def diff(self, diffopts, node2, match, prefix, **opts):
334 334 pass
335 335
336 336 def outgoing(self, ui, dest, opts):
337 337 return 1
338 338
339 339 def incoming(self, ui, source, opts):
340 340 return 1
341 341
342 342 def files(self):
343 343 """return filename iterator"""
344 344 raise NotImplementedError
345 345
346 346 def filedata(self, name):
347 347 """return file data"""
348 348 raise NotImplementedError
349 349
350 350 def fileflags(self, name):
351 351 """return file flags"""
352 352 return ''
353 353
354 354 def archive(self, ui, archiver, prefix, match=None):
355 355 if match is not None:
356 356 files = [f for f in self.files() if match(f)]
357 357 else:
358 358 files = self.files()
359 359 total = len(files)
360 360 relpath = subrelpath(self)
361 361 ui.progress(_('archiving (%s)') % relpath, 0,
362 362 unit=_('files'), total=total)
363 363 for i, name in enumerate(files):
364 364 flags = self.fileflags(name)
365 365 mode = 'x' in flags and 0755 or 0644
366 366 symlink = 'l' in flags
367 367 archiver.addfile(os.path.join(prefix, self._path, name),
368 368 mode, symlink, self.filedata(name))
369 369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 370 unit=_('files'), total=total)
371 371 ui.progress(_('archiving (%s)') % relpath, None)
372 372
373 373 def walk(self, match):
374 374 '''
375 375 walk recursively through the directory tree, finding all files
376 376 matched by the match function
377 377 '''
378 378 pass
379 379
380 380 def forget(self, ui, match, prefix):
381 381 return ([], [])
382 382
383 383 def revert(self, ui, substate, *pats, **opts):
384 384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 385 % (substate[0], substate[2]))
386 386 return []
387 387
388 388 class hgsubrepo(abstractsubrepo):
389 389 def __init__(self, ctx, path, state):
390 390 self._path = path
391 391 self._state = state
392 392 r = ctx._repo
393 393 root = r.wjoin(path)
394 394 create = False
395 395 if not os.path.exists(os.path.join(root, '.hg')):
396 396 create = True
397 397 util.makedirs(root)
398 398 self._repo = hg.repository(r.ui, root, create=create)
399 399 self._initrepo(r, state[0], create)
400 400
401 401 def _initrepo(self, parentrepo, source, create):
402 402 self._repo._subparent = parentrepo
403 403 self._repo._subsource = source
404 404
405 405 if create:
406 406 fp = self._repo.opener("hgrc", "w", text=True)
407 407 fp.write('[paths]\n')
408 408
409 409 def addpathconfig(key, value):
410 410 if value:
411 411 fp.write('%s = %s\n' % (key, value))
412 412 self._repo.ui.setconfig('paths', key, value)
413 413
414 414 defpath = _abssource(self._repo, abort=False)
415 415 defpushpath = _abssource(self._repo, True, abort=False)
416 416 addpathconfig('default', defpath)
417 417 if defpath != defpushpath:
418 418 addpathconfig('default-push', defpushpath)
419 419 fp.close()
420 420
421 421 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
422 422 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
423 423 os.path.join(prefix, self._path), explicitonly)
424 424
425 425 def status(self, rev2, **opts):
426 426 try:
427 427 rev1 = self._state[1]
428 428 ctx1 = self._repo[rev1]
429 429 ctx2 = self._repo[rev2]
430 430 return self._repo.status(ctx1, ctx2, **opts)
431 431 except error.RepoLookupError, inst:
432 432 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
433 433 % (inst, subrelpath(self)))
434 434 return [], [], [], [], [], [], []
435 435
436 436 def diff(self, diffopts, node2, match, prefix, **opts):
437 437 try:
438 438 node1 = node.bin(self._state[1])
439 439 # We currently expect node2 to come from substate and be
440 440 # in hex format
441 441 if node2 is not None:
442 442 node2 = node.bin(node2)
443 443 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
444 444 node1, node2, match,
445 445 prefix=os.path.join(prefix, self._path),
446 446 listsubrepos=True, **opts)
447 447 except error.RepoLookupError, inst:
448 448 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
449 449 % (inst, subrelpath(self)))
450 450
451 451 def archive(self, ui, archiver, prefix, match=None):
452 452 self._get(self._state + ('hg',))
453 453 abstractsubrepo.archive(self, ui, archiver, prefix, match)
454 454
455 455 rev = self._state[1]
456 456 ctx = self._repo[rev]
457 457 for subpath in ctx.substate:
458 458 s = subrepo(ctx, subpath)
459 459 submatch = matchmod.narrowmatcher(subpath, match)
460 460 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
461 461
462 462 def dirty(self, ignoreupdate=False):
463 463 r = self._state[1]
464 464 if r == '' and not ignoreupdate: # no state recorded
465 465 return True
466 466 w = self._repo[None]
467 467 if r != w.p1().hex() and not ignoreupdate:
468 468 # different version checked out
469 469 return True
470 470 return w.dirty() # working directory changed
471 471
472 472 def basestate(self):
473 473 return self._repo['.'].hex()
474 474
475 475 def checknested(self, path):
476 476 return self._repo._checknested(self._repo.wjoin(path))
477 477
478 478 def commit(self, text, user, date):
479 479 # don't bother committing in the subrepo if it's only been
480 480 # updated
481 481 if not self.dirty(True):
482 482 return self._repo['.'].hex()
483 483 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
484 484 n = self._repo.commit(text, user, date)
485 485 if not n:
486 486 return self._repo['.'].hex() # different version checked out
487 487 return node.hex(n)
488 488
489 489 def remove(self):
490 490 # we can't fully delete the repository as it may contain
491 491 # local-only history
492 492 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
493 493 hg.clean(self._repo, node.nullid, False)
494 494
495 495 def _get(self, state):
496 496 source, revision, kind = state
497 497 if revision not in self._repo:
498 498 self._repo._subsource = source
499 499 srcurl = _abssource(self._repo)
500 500 other = hg.peer(self._repo.ui, {}, srcurl)
501 501 if len(self._repo) == 0:
502 502 self._repo.ui.status(_('cloning subrepo %s from %s\n')
503 503 % (subrelpath(self), srcurl))
504 504 parentrepo = self._repo._subparent
505 505 shutil.rmtree(self._repo.path)
506 506 other, cloned = hg.clone(self._repo._subparent.ui, {},
507 507 other, self._repo.root,
508 508 update=False)
509 509 self._repo = cloned.local()
510 510 self._initrepo(parentrepo, source, create=True)
511 511 else:
512 512 self._repo.ui.status(_('pulling subrepo %s from %s\n')
513 513 % (subrelpath(self), srcurl))
514 514 self._repo.pull(other)
515 515 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
516 516 srcurl)
517 517
518 518 def get(self, state, overwrite=False):
519 519 self._get(state)
520 520 source, revision, kind = state
521 521 self._repo.ui.debug("getting subrepo %s\n" % self._path)
522 522 hg.clean(self._repo, revision, False)
523 523
524 524 def merge(self, state):
525 525 self._get(state)
526 526 cur = self._repo['.']
527 527 dst = self._repo[state[1]]
528 528 anc = dst.ancestor(cur)
529 529
530 530 def mergefunc():
531 531 if anc == cur and dst.branch() == cur.branch():
532 532 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
533 533 hg.update(self._repo, state[1])
534 534 elif anc == dst:
535 535 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
536 536 else:
537 537 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
538 538 hg.merge(self._repo, state[1], remind=False)
539 539
540 540 wctx = self._repo[None]
541 541 if self.dirty():
542 542 if anc != dst:
543 543 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
544 544 mergefunc()
545 545 else:
546 546 mergefunc()
547 547 else:
548 548 mergefunc()
549 549
550 550 def push(self, opts):
551 551 force = opts.get('force')
552 552 newbranch = opts.get('new_branch')
553 553 ssh = opts.get('ssh')
554 554
555 555 # push subrepos depth-first for coherent ordering
556 556 c = self._repo['']
557 557 subs = c.substate # only repos that are committed
558 558 for s in sorted(subs):
559 559 if c.sub(s).push(opts) == 0:
560 560 return False
561 561
562 562 dsturl = _abssource(self._repo, True)
563 563 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
564 564 (subrelpath(self), dsturl))
565 565 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
566 566 return self._repo.push(other, force, newbranch=newbranch)
567 567
568 568 def outgoing(self, ui, dest, opts):
569 569 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
570 570
571 571 def incoming(self, ui, source, opts):
572 572 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
573 573
574 574 def files(self):
575 575 rev = self._state[1]
576 576 ctx = self._repo[rev]
577 577 return ctx.manifest()
578 578
579 579 def filedata(self, name):
580 580 rev = self._state[1]
581 581 return self._repo[rev][name].data()
582 582
583 583 def fileflags(self, name):
584 584 rev = self._state[1]
585 585 ctx = self._repo[rev]
586 586 return ctx.flags(name)
587 587
588 588 def walk(self, match):
589 589 ctx = self._repo[None]
590 590 return ctx.walk(match)
591 591
592 592 def forget(self, ui, match, prefix):
593 593 return cmdutil.forget(ui, self._repo, match,
594 594 os.path.join(prefix, self._path), True)
595 595
596 596 def revert(self, ui, substate, *pats, **opts):
597 597 # reverting a subrepo is a 2 step process:
598 598 # 1. if the no_backup is not set, revert all modified
599 599 # files inside the subrepo
600 600 # 2. update the subrepo to the revision specified in
601 601 # the corresponding substate dictionary
602 602 ui.status(_('reverting subrepo %s\n') % substate[0])
603 603 if not opts.get('no_backup'):
604 604 # Revert all files on the subrepo, creating backups
605 605 # Note that this will not recursively revert subrepos
606 606 # We could do it if there was a set:subrepos() predicate
607 607 opts = opts.copy()
608 608 opts['date'] = None
609 609 opts['rev'] = substate[1]
610 610
611 611 pats = []
612 612 if not opts['all']:
613 613 pats = ['set:modified()']
614 614 self.filerevert(ui, *pats, **opts)
615 615
616 616 # Update the repo to the revision specified in the given substate
617 617 self.get(substate, overwrite=True)
618 618
619 619 def filerevert(self, ui, *pats, **opts):
620 620 ctx = self._repo[opts['rev']]
621 621 parents = self._repo.dirstate.parents()
622 622 if opts['all']:
623 623 pats = ['set:modified()']
624 624 else:
625 625 pats = []
626 626 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
627 627
628 628 class svnsubrepo(abstractsubrepo):
629 629 def __init__(self, ctx, path, state):
630 630 self._path = path
631 631 self._state = state
632 632 self._ctx = ctx
633 633 self._ui = ctx._repo.ui
634 634 self._exe = util.findexe('svn')
635 635 if not self._exe:
636 636 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
637 637 % self._path)
638 638
639 639 def _svncommand(self, commands, filename='', failok=False):
640 640 cmd = [self._exe]
641 641 extrakw = {}
642 642 if not self._ui.interactive():
643 643 # Making stdin be a pipe should prevent svn from behaving
644 644 # interactively even if we can't pass --non-interactive.
645 645 extrakw['stdin'] = subprocess.PIPE
646 646 # Starting in svn 1.5 --non-interactive is a global flag
647 647 # instead of being per-command, but we need to support 1.4 so
648 648 # we have to be intelligent about what commands take
649 649 # --non-interactive.
650 650 if commands[0] in ('update', 'checkout', 'commit'):
651 651 cmd.append('--non-interactive')
652 652 cmd.extend(commands)
653 653 if filename is not None:
654 654 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
655 655 cmd.append(path)
656 656 env = dict(os.environ)
657 657 # Avoid localized output, preserve current locale for everything else.
658 658 env['LC_MESSAGES'] = 'C'
659 659 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
660 660 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
661 661 universal_newlines=True, env=env, **extrakw)
662 662 stdout, stderr = p.communicate()
663 663 stderr = stderr.strip()
664 664 if not failok:
665 665 if p.returncode:
666 666 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
667 667 if stderr:
668 668 self._ui.warn(stderr + '\n')
669 669 return stdout, stderr
670 670
671 671 @propertycache
672 672 def _svnversion(self):
673 673 output, err = self._svncommand(['--version'], filename=None)
674 674 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
675 675 if not m:
676 676 raise util.Abort(_('cannot retrieve svn tool version'))
677 677 return (int(m.group(1)), int(m.group(2)))
678 678
679 679 def _wcrevs(self):
680 680 # Get the working directory revision as well as the last
681 681 # commit revision so we can compare the subrepo state with
682 682 # both. We used to store the working directory one.
683 683 output, err = self._svncommand(['info', '--xml'])
684 684 doc = xml.dom.minidom.parseString(output)
685 685 entries = doc.getElementsByTagName('entry')
686 686 lastrev, rev = '0', '0'
687 687 if entries:
688 688 rev = str(entries[0].getAttribute('revision')) or '0'
689 689 commits = entries[0].getElementsByTagName('commit')
690 690 if commits:
691 691 lastrev = str(commits[0].getAttribute('revision')) or '0'
692 692 return (lastrev, rev)
693 693
694 694 def _wcrev(self):
695 695 return self._wcrevs()[0]
696 696
697 697 def _wcchanged(self):
698 698 """Return (changes, extchanges, missing) where changes is True
699 699 if the working directory was changed, extchanges is
700 700 True if any of these changes concern an external entry and missing
701 701 is True if any change is a missing entry.
702 702 """
703 703 output, err = self._svncommand(['status', '--xml'])
704 704 externals, changes, missing = [], [], []
705 705 doc = xml.dom.minidom.parseString(output)
706 706 for e in doc.getElementsByTagName('entry'):
707 707 s = e.getElementsByTagName('wc-status')
708 708 if not s:
709 709 continue
710 710 item = s[0].getAttribute('item')
711 711 props = s[0].getAttribute('props')
712 712 path = e.getAttribute('path')
713 713 if item == 'external':
714 714 externals.append(path)
715 715 elif item == 'missing':
716 716 missing.append(path)
717 717 if (item not in ('', 'normal', 'unversioned', 'external')
718 718 or props not in ('', 'none', 'normal')):
719 719 changes.append(path)
720 720 for path in changes:
721 721 for ext in externals:
722 722 if path == ext or path.startswith(ext + os.sep):
723 723 return True, True, bool(missing)
724 724 return bool(changes), False, bool(missing)
725 725
726 726 def dirty(self, ignoreupdate=False):
727 727 if not self._wcchanged()[0]:
728 728 if self._state[1] in self._wcrevs() or ignoreupdate:
729 729 return False
730 730 return True
731 731
732 732 def basestate(self):
733 733 lastrev, rev = self._wcrevs()
734 734 if lastrev != rev:
735 735 # Last committed rev is not the same than rev. We would
736 736 # like to take lastrev but we do not know if the subrepo
737 737 # URL exists at lastrev. Test it and fallback to rev it
738 738 # is not there.
739 739 try:
740 740 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
741 741 return lastrev
742 742 except error.Abort:
743 743 pass
744 744 return rev
745 745
746 746 def commit(self, text, user, date):
747 747 # user and date are out of our hands since svn is centralized
748 748 changed, extchanged, missing = self._wcchanged()
749 749 if not changed:
750 750 return self.basestate()
751 751 if extchanged:
752 752 # Do not try to commit externals
753 753 raise util.Abort(_('cannot commit svn externals'))
754 754 if missing:
755 755 # svn can commit with missing entries but aborting like hg
756 756 # seems a better approach.
757 757 raise util.Abort(_('cannot commit missing svn entries'))
758 758 commitinfo, err = self._svncommand(['commit', '-m', text])
759 759 self._ui.status(commitinfo)
760 760 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
761 761 if not newrev:
762 762 if not commitinfo.strip():
763 763 # Sometimes, our definition of "changed" differs from
764 764 # svn one. For instance, svn ignores missing files
765 765 # when committing. If there are only missing files, no
766 766 # commit is made, no output and no error code.
767 767 raise util.Abort(_('failed to commit svn changes'))
768 768 raise util.Abort(commitinfo.splitlines()[-1])
769 769 newrev = newrev.groups()[0]
770 770 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
771 771 return newrev
772 772
773 773 def remove(self):
774 774 if self.dirty():
775 775 self._ui.warn(_('not removing repo %s because '
776 776 'it has changes.\n' % self._path))
777 777 return
778 778 self._ui.note(_('removing subrepo %s\n') % self._path)
779 779
780 780 def onerror(function, path, excinfo):
781 781 if function is not os.remove:
782 782 raise
783 783 # read-only files cannot be unlinked under Windows
784 784 s = os.stat(path)
785 785 if (s.st_mode & stat.S_IWRITE) != 0:
786 786 raise
787 787 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
788 788 os.remove(path)
789 789
790 790 path = self._ctx._repo.wjoin(self._path)
791 791 shutil.rmtree(path, onerror=onerror)
792 792 try:
793 793 os.removedirs(os.path.dirname(path))
794 794 except OSError:
795 795 pass
796 796
797 797 def get(self, state, overwrite=False):
798 798 if overwrite:
799 799 self._svncommand(['revert', '--recursive'])
800 800 args = ['checkout']
801 801 if self._svnversion >= (1, 5):
802 802 args.append('--force')
803 803 # The revision must be specified at the end of the URL to properly
804 804 # update to a directory which has since been deleted and recreated.
805 805 args.append('%s@%s' % (state[0], state[1]))
806 806 status, err = self._svncommand(args, failok=True)
807 807 if not re.search('Checked out revision [0-9]+.', status):
808 808 if ('is already a working copy for a different URL' in err
809 809 and (self._wcchanged()[:2] == (False, False))):
810 810 # obstructed but clean working copy, so just blow it away.
811 811 self.remove()
812 812 self.get(state, overwrite=False)
813 813 return
814 814 raise util.Abort((status or err).splitlines()[-1])
815 815 self._ui.status(status)
816 816
817 817 def merge(self, state):
818 818 old = self._state[1]
819 819 new = state[1]
820 820 wcrev = self._wcrev()
821 821 if new != wcrev:
822 822 dirty = old == wcrev or self._wcchanged()[0]
823 823 if _updateprompt(self._ui, self, dirty, wcrev, new):
824 824 self.get(state, False)
825 825
826 826 def push(self, opts):
827 827 # push is a no-op for SVN
828 828 return True
829 829
830 830 def files(self):
831 831 output = self._svncommand(['list', '--recursive', '--xml'])[0]
832 832 doc = xml.dom.minidom.parseString(output)
833 833 paths = []
834 834 for e in doc.getElementsByTagName('entry'):
835 835 kind = str(e.getAttribute('kind'))
836 836 if kind != 'file':
837 837 continue
838 838 name = ''.join(c.data for c
839 839 in e.getElementsByTagName('name')[0].childNodes
840 840 if c.nodeType == c.TEXT_NODE)
841 paths.append(name)
841 paths.append(name.encode('utf-8'))
842 842 return paths
843 843
844 844 def filedata(self, name):
845 845 return self._svncommand(['cat'], name)[0]
846 846
847 847
848 848 class gitsubrepo(abstractsubrepo):
849 849 def __init__(self, ctx, path, state):
850 850 self._state = state
851 851 self._ctx = ctx
852 852 self._path = path
853 853 self._relpath = os.path.join(reporelpath(ctx._repo), path)
854 854 self._abspath = ctx._repo.wjoin(path)
855 855 self._subparent = ctx._repo
856 856 self._ui = ctx._repo.ui
857 857 self._ensuregit()
858 858
859 859 def _ensuregit(self):
860 860 try:
861 861 self._gitexecutable = 'git'
862 862 out, err = self._gitnodir(['--version'])
863 863 except OSError, e:
864 864 if e.errno != 2 or os.name != 'nt':
865 865 raise
866 866 self._gitexecutable = 'git.cmd'
867 867 out, err = self._gitnodir(['--version'])
868 868 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
869 869 if not m:
870 870 self._ui.warn(_('cannot retrieve git version'))
871 871 return
872 872 version = (int(m.group(1)), m.group(2), m.group(3))
873 873 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
874 874 # despite the docstring comment. For now, error on 1.4.0, warn on
875 875 # 1.5.0 but attempt to continue.
876 876 if version < (1, 5, 0):
877 877 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
878 878 elif version < (1, 6, 0):
879 879 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
880 880
881 881 def _gitcommand(self, commands, env=None, stream=False):
882 882 return self._gitdir(commands, env=env, stream=stream)[0]
883 883
884 884 def _gitdir(self, commands, env=None, stream=False):
885 885 return self._gitnodir(commands, env=env, stream=stream,
886 886 cwd=self._abspath)
887 887
888 888 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
889 889 """Calls the git command
890 890
891 891 The methods tries to call the git command. versions previor to 1.6.0
892 892 are not supported and very probably fail.
893 893 """
894 894 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
895 895 # unless ui.quiet is set, print git's stderr,
896 896 # which is mostly progress and useful info
897 897 errpipe = None
898 898 if self._ui.quiet:
899 899 errpipe = open(os.devnull, 'w')
900 900 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
901 901 cwd=cwd, env=env, close_fds=util.closefds,
902 902 stdout=subprocess.PIPE, stderr=errpipe)
903 903 if stream:
904 904 return p.stdout, None
905 905
906 906 retdata = p.stdout.read().strip()
907 907 # wait for the child to exit to avoid race condition.
908 908 p.wait()
909 909
910 910 if p.returncode != 0 and p.returncode != 1:
911 911 # there are certain error codes that are ok
912 912 command = commands[0]
913 913 if command in ('cat-file', 'symbolic-ref'):
914 914 return retdata, p.returncode
915 915 # for all others, abort
916 916 raise util.Abort('git %s error %d in %s' %
917 917 (command, p.returncode, self._relpath))
918 918
919 919 return retdata, p.returncode
920 920
921 921 def _gitmissing(self):
922 922 return not os.path.exists(os.path.join(self._abspath, '.git'))
923 923
924 924 def _gitstate(self):
925 925 return self._gitcommand(['rev-parse', 'HEAD'])
926 926
927 927 def _gitcurrentbranch(self):
928 928 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
929 929 if err:
930 930 current = None
931 931 return current
932 932
933 933 def _gitremote(self, remote):
934 934 out = self._gitcommand(['remote', 'show', '-n', remote])
935 935 line = out.split('\n')[1]
936 936 i = line.index('URL: ') + len('URL: ')
937 937 return line[i:]
938 938
939 939 def _githavelocally(self, revision):
940 940 out, code = self._gitdir(['cat-file', '-e', revision])
941 941 return code == 0
942 942
943 943 def _gitisancestor(self, r1, r2):
944 944 base = self._gitcommand(['merge-base', r1, r2])
945 945 return base == r1
946 946
947 947 def _gitisbare(self):
948 948 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
949 949
950 950 def _gitupdatestat(self):
951 951 """This must be run before git diff-index.
952 952 diff-index only looks at changes to file stat;
953 953 this command looks at file contents and updates the stat."""
954 954 self._gitcommand(['update-index', '-q', '--refresh'])
955 955
956 956 def _gitbranchmap(self):
957 957 '''returns 2 things:
958 958 a map from git branch to revision
959 959 a map from revision to branches'''
960 960 branch2rev = {}
961 961 rev2branch = {}
962 962
963 963 out = self._gitcommand(['for-each-ref', '--format',
964 964 '%(objectname) %(refname)'])
965 965 for line in out.split('\n'):
966 966 revision, ref = line.split(' ')
967 967 if (not ref.startswith('refs/heads/') and
968 968 not ref.startswith('refs/remotes/')):
969 969 continue
970 970 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
971 971 continue # ignore remote/HEAD redirects
972 972 branch2rev[ref] = revision
973 973 rev2branch.setdefault(revision, []).append(ref)
974 974 return branch2rev, rev2branch
975 975
976 976 def _gittracking(self, branches):
977 977 'return map of remote branch to local tracking branch'
978 978 # assumes no more than one local tracking branch for each remote
979 979 tracking = {}
980 980 for b in branches:
981 981 if b.startswith('refs/remotes/'):
982 982 continue
983 983 bname = b.split('/', 2)[2]
984 984 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
985 985 if remote:
986 986 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
987 987 tracking['refs/remotes/%s/%s' %
988 988 (remote, ref.split('/', 2)[2])] = b
989 989 return tracking
990 990
991 991 def _abssource(self, source):
992 992 if '://' not in source:
993 993 # recognize the scp syntax as an absolute source
994 994 colon = source.find(':')
995 995 if colon != -1 and '/' not in source[:colon]:
996 996 return source
997 997 self._subsource = source
998 998 return _abssource(self)
999 999
1000 1000 def _fetch(self, source, revision):
1001 1001 if self._gitmissing():
1002 1002 source = self._abssource(source)
1003 1003 self._ui.status(_('cloning subrepo %s from %s\n') %
1004 1004 (self._relpath, source))
1005 1005 self._gitnodir(['clone', source, self._abspath])
1006 1006 if self._githavelocally(revision):
1007 1007 return
1008 1008 self._ui.status(_('pulling subrepo %s from %s\n') %
1009 1009 (self._relpath, self._gitremote('origin')))
1010 1010 # try only origin: the originally cloned repo
1011 1011 self._gitcommand(['fetch'])
1012 1012 if not self._githavelocally(revision):
1013 1013 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1014 1014 (revision, self._relpath))
1015 1015
1016 1016 def dirty(self, ignoreupdate=False):
1017 1017 if self._gitmissing():
1018 1018 return self._state[1] != ''
1019 1019 if self._gitisbare():
1020 1020 return True
1021 1021 if not ignoreupdate and self._state[1] != self._gitstate():
1022 1022 # different version checked out
1023 1023 return True
1024 1024 # check for staged changes or modified files; ignore untracked files
1025 1025 self._gitupdatestat()
1026 1026 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1027 1027 return code == 1
1028 1028
1029 1029 def basestate(self):
1030 1030 return self._gitstate()
1031 1031
1032 1032 def get(self, state, overwrite=False):
1033 1033 source, revision, kind = state
1034 1034 if not revision:
1035 1035 self.remove()
1036 1036 return
1037 1037 self._fetch(source, revision)
1038 1038 # if the repo was set to be bare, unbare it
1039 1039 if self._gitisbare():
1040 1040 self._gitcommand(['config', 'core.bare', 'false'])
1041 1041 if self._gitstate() == revision:
1042 1042 self._gitcommand(['reset', '--hard', 'HEAD'])
1043 1043 return
1044 1044 elif self._gitstate() == revision:
1045 1045 if overwrite:
1046 1046 # first reset the index to unmark new files for commit, because
1047 1047 # reset --hard will otherwise throw away files added for commit,
1048 1048 # not just unmark them.
1049 1049 self._gitcommand(['reset', 'HEAD'])
1050 1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 1051 return
1052 1052 branch2rev, rev2branch = self._gitbranchmap()
1053 1053
1054 1054 def checkout(args):
1055 1055 cmd = ['checkout']
1056 1056 if overwrite:
1057 1057 # first reset the index to unmark new files for commit, because
1058 1058 # the -f option will otherwise throw away files added for
1059 1059 # commit, not just unmark them.
1060 1060 self._gitcommand(['reset', 'HEAD'])
1061 1061 cmd.append('-f')
1062 1062 self._gitcommand(cmd + args)
1063 1063
1064 1064 def rawcheckout():
1065 1065 # no branch to checkout, check it out with no branch
1066 1066 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1067 1067 self._relpath)
1068 1068 self._ui.warn(_('check out a git branch if you intend '
1069 1069 'to make changes\n'))
1070 1070 checkout(['-q', revision])
1071 1071
1072 1072 if revision not in rev2branch:
1073 1073 rawcheckout()
1074 1074 return
1075 1075 branches = rev2branch[revision]
1076 1076 firstlocalbranch = None
1077 1077 for b in branches:
1078 1078 if b == 'refs/heads/master':
1079 1079 # master trumps all other branches
1080 1080 checkout(['refs/heads/master'])
1081 1081 return
1082 1082 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1083 1083 firstlocalbranch = b
1084 1084 if firstlocalbranch:
1085 1085 checkout([firstlocalbranch])
1086 1086 return
1087 1087
1088 1088 tracking = self._gittracking(branch2rev.keys())
1089 1089 # choose a remote branch already tracked if possible
1090 1090 remote = branches[0]
1091 1091 if remote not in tracking:
1092 1092 for b in branches:
1093 1093 if b in tracking:
1094 1094 remote = b
1095 1095 break
1096 1096
1097 1097 if remote not in tracking:
1098 1098 # create a new local tracking branch
1099 1099 local = remote.split('/', 2)[2]
1100 1100 checkout(['-b', local, remote])
1101 1101 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1102 1102 # When updating to a tracked remote branch,
1103 1103 # if the local tracking branch is downstream of it,
1104 1104 # a normal `git pull` would have performed a "fast-forward merge"
1105 1105 # which is equivalent to updating the local branch to the remote.
1106 1106 # Since we are only looking at branching at update, we need to
1107 1107 # detect this situation and perform this action lazily.
1108 1108 if tracking[remote] != self._gitcurrentbranch():
1109 1109 checkout([tracking[remote]])
1110 1110 self._gitcommand(['merge', '--ff', remote])
1111 1111 else:
1112 1112 # a real merge would be required, just checkout the revision
1113 1113 rawcheckout()
1114 1114
1115 1115 def commit(self, text, user, date):
1116 1116 if self._gitmissing():
1117 1117 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1118 1118 cmd = ['commit', '-a', '-m', text]
1119 1119 env = os.environ.copy()
1120 1120 if user:
1121 1121 cmd += ['--author', user]
1122 1122 if date:
1123 1123 # git's date parser silently ignores when seconds < 1e9
1124 1124 # convert to ISO8601
1125 1125 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1126 1126 '%Y-%m-%dT%H:%M:%S %1%2')
1127 1127 self._gitcommand(cmd, env=env)
1128 1128 # make sure commit works otherwise HEAD might not exist under certain
1129 1129 # circumstances
1130 1130 return self._gitstate()
1131 1131
1132 1132 def merge(self, state):
1133 1133 source, revision, kind = state
1134 1134 self._fetch(source, revision)
1135 1135 base = self._gitcommand(['merge-base', revision, self._state[1]])
1136 1136 self._gitupdatestat()
1137 1137 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1138 1138
1139 1139 def mergefunc():
1140 1140 if base == revision:
1141 1141 self.get(state) # fast forward merge
1142 1142 elif base != self._state[1]:
1143 1143 self._gitcommand(['merge', '--no-commit', revision])
1144 1144
1145 1145 if self.dirty():
1146 1146 if self._gitstate() != revision:
1147 1147 dirty = self._gitstate() == self._state[1] or code != 0
1148 1148 if _updateprompt(self._ui, self, dirty,
1149 1149 self._state[1][:7], revision[:7]):
1150 1150 mergefunc()
1151 1151 else:
1152 1152 mergefunc()
1153 1153
1154 1154 def push(self, opts):
1155 1155 force = opts.get('force')
1156 1156
1157 1157 if not self._state[1]:
1158 1158 return True
1159 1159 if self._gitmissing():
1160 1160 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1161 1161 # if a branch in origin contains the revision, nothing to do
1162 1162 branch2rev, rev2branch = self._gitbranchmap()
1163 1163 if self._state[1] in rev2branch:
1164 1164 for b in rev2branch[self._state[1]]:
1165 1165 if b.startswith('refs/remotes/origin/'):
1166 1166 return True
1167 1167 for b, revision in branch2rev.iteritems():
1168 1168 if b.startswith('refs/remotes/origin/'):
1169 1169 if self._gitisancestor(self._state[1], revision):
1170 1170 return True
1171 1171 # otherwise, try to push the currently checked out branch
1172 1172 cmd = ['push']
1173 1173 if force:
1174 1174 cmd.append('--force')
1175 1175
1176 1176 current = self._gitcurrentbranch()
1177 1177 if current:
1178 1178 # determine if the current branch is even useful
1179 1179 if not self._gitisancestor(self._state[1], current):
1180 1180 self._ui.warn(_('unrelated git branch checked out '
1181 1181 'in subrepo %s\n') % self._relpath)
1182 1182 return False
1183 1183 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1184 1184 (current.split('/', 2)[2], self._relpath))
1185 1185 self._gitcommand(cmd + ['origin', current])
1186 1186 return True
1187 1187 else:
1188 1188 self._ui.warn(_('no branch checked out in subrepo %s\n'
1189 1189 'cannot push revision %s\n') %
1190 1190 (self._relpath, self._state[1]))
1191 1191 return False
1192 1192
1193 1193 def remove(self):
1194 1194 if self._gitmissing():
1195 1195 return
1196 1196 if self.dirty():
1197 1197 self._ui.warn(_('not removing repo %s because '
1198 1198 'it has changes.\n') % self._relpath)
1199 1199 return
1200 1200 # we can't fully delete the repository as it may contain
1201 1201 # local-only history
1202 1202 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1203 1203 self._gitcommand(['config', 'core.bare', 'true'])
1204 1204 for f in os.listdir(self._abspath):
1205 1205 if f == '.git':
1206 1206 continue
1207 1207 path = os.path.join(self._abspath, f)
1208 1208 if os.path.isdir(path) and not os.path.islink(path):
1209 1209 shutil.rmtree(path)
1210 1210 else:
1211 1211 os.remove(path)
1212 1212
1213 1213 def archive(self, ui, archiver, prefix, match=None):
1214 1214 source, revision = self._state
1215 1215 if not revision:
1216 1216 return
1217 1217 self._fetch(source, revision)
1218 1218
1219 1219 # Parse git's native archive command.
1220 1220 # This should be much faster than manually traversing the trees
1221 1221 # and objects with many subprocess calls.
1222 1222 tarstream = self._gitcommand(['archive', revision], stream=True)
1223 1223 tar = tarfile.open(fileobj=tarstream, mode='r|')
1224 1224 relpath = subrelpath(self)
1225 1225 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1226 1226 for i, info in enumerate(tar):
1227 1227 if info.isdir():
1228 1228 continue
1229 1229 if match and not match(info.name):
1230 1230 continue
1231 1231 if info.issym():
1232 1232 data = info.linkname
1233 1233 else:
1234 1234 data = tar.extractfile(info).read()
1235 1235 archiver.addfile(os.path.join(prefix, self._path, info.name),
1236 1236 info.mode, info.issym(), data)
1237 1237 ui.progress(_('archiving (%s)') % relpath, i + 1,
1238 1238 unit=_('files'))
1239 1239 ui.progress(_('archiving (%s)') % relpath, None)
1240 1240
1241 1241
1242 1242 def status(self, rev2, **opts):
1243 1243 rev1 = self._state[1]
1244 1244 if self._gitmissing() or not rev1:
1245 1245 # if the repo is missing, return no results
1246 1246 return [], [], [], [], [], [], []
1247 1247 modified, added, removed = [], [], []
1248 1248 self._gitupdatestat()
1249 1249 if rev2:
1250 1250 command = ['diff-tree', rev1, rev2]
1251 1251 else:
1252 1252 command = ['diff-index', rev1]
1253 1253 out = self._gitcommand(command)
1254 1254 for line in out.split('\n'):
1255 1255 tab = line.find('\t')
1256 1256 if tab == -1:
1257 1257 continue
1258 1258 status, f = line[tab - 1], line[tab + 1:]
1259 1259 if status == 'M':
1260 1260 modified.append(f)
1261 1261 elif status == 'A':
1262 1262 added.append(f)
1263 1263 elif status == 'D':
1264 1264 removed.append(f)
1265 1265
1266 1266 deleted = unknown = ignored = clean = []
1267 1267 return modified, added, removed, deleted, unknown, ignored, clean
1268 1268
1269 1269 types = {
1270 1270 'hg': hgsubrepo,
1271 1271 'svn': svnsubrepo,
1272 1272 'git': gitsubrepo,
1273 1273 }
General Comments 0
You need to be logged in to leave comments. Login now