##// END OF EJS Templates
merge with stable
Matt Mackall -
r15534:80ec6309 merge default
parent child Browse files
Show More
@@ -1,66 +1,66
1 1 Valid URLs are of the form::
2 2
3 3 local/filesystem/path[#revision]
4 file://localhost/filesystem/path[#revision]
4 file://local/filesystem/path[#revision]
5 5 http://[user[:pass]@]host[:port]/[path][#revision]
6 6 https://[user[:pass]@]host[:port]/[path][#revision]
7 7 ssh://[user@]host[:port]/[path][#revision]
8 8
9 9 Paths in the local filesystem can either point to Mercurial
10 10 repositories or to bundle files (as created by :hg:`bundle` or :hg:`
11 11 incoming --bundle`). See also :hg:`help paths`.
12 12
13 13 An optional identifier after # indicates a particular branch, tag, or
14 14 changeset to use from the remote repository. See also :hg:`help
15 15 revisions`.
16 16
17 17 Some features, such as pushing to http:// and https:// URLs are only
18 18 possible if the feature is explicitly enabled on the remote Mercurial
19 19 server.
20 20
21 21 Note that the security of HTTPS URLs depends on proper configuration of
22 22 web.cacerts.
23 23
24 24 Some notes about using SSH with Mercurial:
25 25
26 26 - SSH requires an accessible shell account on the destination machine
27 27 and a copy of hg in the remote path or specified with as remotecmd.
28 28 - path is relative to the remote user's home directory by default. Use
29 29 an extra slash at the start of a path to specify an absolute path::
30 30
31 31 ssh://example.com//tmp/repository
32 32
33 33 - Mercurial doesn't use its own compression via SSH; the right thing
34 34 to do is to configure it in your ~/.ssh/config, e.g.::
35 35
36 36 Host *.mylocalnetwork.example.com
37 37 Compression no
38 38 Host *
39 39 Compression yes
40 40
41 41 Alternatively specify "ssh -C" as your ssh command in your
42 42 configuration file or with the --ssh command line option.
43 43
44 44 These URLs can all be stored in your configuration file with path
45 45 aliases under the [paths] section like so::
46 46
47 47 [paths]
48 48 alias1 = URL1
49 49 alias2 = URL2
50 50 ...
51 51
52 52 You can then use the alias for any command that uses a URL (for
53 53 example :hg:`pull alias1` will be treated as :hg:`pull URL1`).
54 54
55 55 Two path aliases are special because they are used as defaults when
56 56 you do not provide the URL to a command:
57 57
58 58 default:
59 59 When you create a repository with hg clone, the clone command saves
60 60 the location of the source repository as the new repository's
61 61 'default' path. This is then used when you omit path from push- and
62 62 pull-like commands (including incoming and outgoing).
63 63
64 64 default-push:
65 65 The push command will look for a path named 'default-push', and
66 66 prefer it over 'default' if both are defined.
@@ -1,1132 +1,1135
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 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 re
9 9 import parser, util, error, discovery, hbisect
10 10 import node as nodemod
11 11 import bookmarks as bookmarksmod
12 12 import match as matchmod
13 13 from i18n import _
14 14
15 15 elements = {
16 16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 17 "~": (18, None, ("ancestor", 18)),
18 18 "^": (18, None, ("parent", 18), ("parentpost", 18)),
19 19 "-": (5, ("negate", 19), ("minus", 5)),
20 20 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
21 21 ("dagrangepost", 17)),
22 22 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
23 23 ("dagrangepost", 17)),
24 24 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
25 25 "not": (10, ("not", 10)),
26 26 "!": (10, ("not", 10)),
27 27 "and": (5, None, ("and", 5)),
28 28 "&": (5, None, ("and", 5)),
29 29 "or": (4, None, ("or", 4)),
30 30 "|": (4, None, ("or", 4)),
31 31 "+": (4, None, ("or", 4)),
32 32 ",": (2, None, ("list", 2)),
33 33 ")": (0, None, None),
34 34 "symbol": (0, ("symbol",), None),
35 35 "string": (0, ("string",), None),
36 36 "end": (0, None, None),
37 37 }
38 38
39 39 keywords = set(['and', 'or', 'not'])
40 40
41 41 def tokenize(program):
42 42 pos, l = 0, len(program)
43 43 while pos < l:
44 44 c = program[pos]
45 45 if c.isspace(): # skip inter-token whitespace
46 46 pass
47 47 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
48 48 yield ('::', None, pos)
49 49 pos += 1 # skip ahead
50 50 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
51 51 yield ('..', None, pos)
52 52 pos += 1 # skip ahead
53 53 elif c in "():,-|&+!~^": # handle simple operators
54 54 yield (c, None, pos)
55 55 elif (c in '"\'' or c == 'r' and
56 56 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
57 57 if c == 'r':
58 58 pos += 1
59 59 c = program[pos]
60 60 decode = lambda x: x
61 61 else:
62 62 decode = lambda x: x.decode('string-escape')
63 63 pos += 1
64 64 s = pos
65 65 while pos < l: # find closing quote
66 66 d = program[pos]
67 67 if d == '\\': # skip over escaped characters
68 68 pos += 2
69 69 continue
70 70 if d == c:
71 71 yield ('string', decode(program[s:pos]), s)
72 72 break
73 73 pos += 1
74 74 else:
75 75 raise error.ParseError(_("unterminated string"), s)
76 76 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
77 77 s = pos
78 78 pos += 1
79 79 while pos < l: # find end of symbol
80 80 d = program[pos]
81 81 if not (d.isalnum() or d in "._" or ord(d) > 127):
82 82 break
83 83 if d == '.' and program[pos - 1] == '.': # special case for ..
84 84 pos -= 1
85 85 break
86 86 pos += 1
87 87 sym = program[s:pos]
88 88 if sym in keywords: # operator keywords
89 89 yield (sym, None, s)
90 90 else:
91 91 yield ('symbol', sym, s)
92 92 pos -= 1
93 93 else:
94 94 raise error.ParseError(_("syntax error"), pos)
95 95 pos += 1
96 96 yield ('end', None, pos)
97 97
98 98 # helpers
99 99
100 100 def getstring(x, err):
101 101 if x and (x[0] == 'string' or x[0] == 'symbol'):
102 102 return x[1]
103 103 raise error.ParseError(err)
104 104
105 105 def getlist(x):
106 106 if not x:
107 107 return []
108 108 if x[0] == 'list':
109 109 return getlist(x[1]) + [x[2]]
110 110 return [x]
111 111
112 112 def getargs(x, min, max, err):
113 113 l = getlist(x)
114 114 if len(l) < min or len(l) > max:
115 115 raise error.ParseError(err)
116 116 return l
117 117
118 118 def getset(repo, subset, x):
119 119 if not x:
120 120 raise error.ParseError(_("missing argument"))
121 121 return methods[x[0]](repo, subset, *x[1:])
122 122
123 123 # operator methods
124 124
125 125 def stringset(repo, subset, x):
126 126 x = repo[x].rev()
127 127 if x == -1 and len(subset) == len(repo):
128 128 return [-1]
129 129 if len(subset) == len(repo) or x in subset:
130 130 return [x]
131 131 return []
132 132
133 133 def symbolset(repo, subset, x):
134 134 if x in symbols:
135 135 raise error.ParseError(_("can't use %s here") % x)
136 136 return stringset(repo, subset, x)
137 137
138 138 def rangeset(repo, subset, x, y):
139 139 m = getset(repo, subset, x)
140 140 if not m:
141 141 m = getset(repo, range(len(repo)), x)
142 142
143 143 n = getset(repo, subset, y)
144 144 if not n:
145 145 n = getset(repo, range(len(repo)), y)
146 146
147 147 if not m or not n:
148 148 return []
149 149 m, n = m[0], n[-1]
150 150
151 151 if m < n:
152 152 r = range(m, n + 1)
153 153 else:
154 154 r = range(m, n - 1, -1)
155 155 s = set(subset)
156 156 return [x for x in r if x in s]
157 157
158 158 def andset(repo, subset, x, y):
159 159 return getset(repo, getset(repo, subset, x), y)
160 160
161 161 def orset(repo, subset, x, y):
162 162 xl = getset(repo, subset, x)
163 163 s = set(xl)
164 164 yl = getset(repo, [r for r in subset if r not in s], y)
165 165 return xl + yl
166 166
167 167 def notset(repo, subset, x):
168 168 s = set(getset(repo, subset, x))
169 169 return [r for r in subset if r not in s]
170 170
171 171 def listset(repo, subset, a, b):
172 172 raise error.ParseError(_("can't use a list in this context"))
173 173
174 174 def func(repo, subset, a, b):
175 175 if a[0] == 'symbol' and a[1] in symbols:
176 176 return symbols[a[1]](repo, subset, b)
177 177 raise error.ParseError(_("not a function: %s") % a[1])
178 178
179 179 # functions
180 180
181 181 def adds(repo, subset, x):
182 182 """``adds(pattern)``
183 183 Changesets that add a file matching pattern.
184 184 """
185 185 # i18n: "adds" is a keyword
186 186 pat = getstring(x, _("adds requires a pattern"))
187 187 return checkstatus(repo, subset, pat, 1)
188 188
189 189 def ancestor(repo, subset, x):
190 190 """``ancestor(single, single)``
191 191 Greatest common ancestor of the two changesets.
192 192 """
193 193 # i18n: "ancestor" is a keyword
194 194 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
195 195 r = range(len(repo))
196 196 a = getset(repo, r, l[0])
197 197 b = getset(repo, r, l[1])
198 198 if len(a) != 1 or len(b) != 1:
199 199 # i18n: "ancestor" is a keyword
200 200 raise error.ParseError(_("ancestor arguments must be single revisions"))
201 201 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
202 202
203 203 return [r for r in an if r in subset]
204 204
205 205 def ancestors(repo, subset, x):
206 206 """``ancestors(set)``
207 207 Changesets that are ancestors of a changeset in set.
208 208 """
209 209 args = getset(repo, range(len(repo)), x)
210 210 if not args:
211 211 return []
212 212 s = set(repo.changelog.ancestors(*args)) | set(args)
213 213 return [r for r in subset if r in s]
214 214
215 215 def ancestorspec(repo, subset, x, n):
216 216 """``set~n``
217 217 Changesets that are the Nth ancestor (first parents only) of a changeset in set.
218 218 """
219 219 try:
220 220 n = int(n[1])
221 221 except (TypeError, ValueError):
222 222 raise error.ParseError(_("~ expects a number"))
223 223 ps = set()
224 224 cl = repo.changelog
225 225 for r in getset(repo, subset, x):
226 226 for i in range(n):
227 227 r = cl.parentrevs(r)[0]
228 228 ps.add(r)
229 229 return [r for r in subset if r in ps]
230 230
231 231 def author(repo, subset, x):
232 232 """``author(string)``
233 233 Alias for ``user(string)``.
234 234 """
235 235 # i18n: "author" is a keyword
236 236 n = getstring(x, _("author requires a string")).lower()
237 237 return [r for r in subset if n in repo[r].user().lower()]
238 238
239 239 def bisect(repo, subset, x):
240 240 """``bisect(string)``
241 241 Changesets marked in the specified bisect status:
242 242
243 243 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
244 244 - ``goods``, ``bads`` : csets topologicaly good/bad
245 245 - ``range`` : csets taking part in the bisection
246 246 - ``pruned`` : csets that are goods, bads or skipped
247 247 - ``untested`` : csets whose fate is yet unknown
248 248 - ``ignored`` : csets ignored due to DAG topology
249 249 """
250 250 status = getstring(x, _("bisect requires a string")).lower()
251 251 return [r for r in subset if r in hbisect.get(repo, status)]
252 252
253 253 # Backward-compatibility
254 254 # - no help entry so that we do not advertise it any more
255 255 def bisected(repo, subset, x):
256 256 return bisect(repo, subset, x)
257 257
258 258 def bookmark(repo, subset, x):
259 259 """``bookmark([name])``
260 260 The named bookmark or all bookmarks.
261 261 """
262 262 # i18n: "bookmark" is a keyword
263 263 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
264 264 if args:
265 265 bm = getstring(args[0],
266 266 # i18n: "bookmark" is a keyword
267 267 _('the argument to bookmark must be a string'))
268 268 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
269 269 if not bmrev:
270 270 raise util.Abort(_("bookmark '%s' does not exist") % bm)
271 271 bmrev = repo[bmrev].rev()
272 272 return [r for r in subset if r == bmrev]
273 273 bms = set([repo[r].rev()
274 274 for r in bookmarksmod.listbookmarks(repo).values()])
275 275 return [r for r in subset if r in bms]
276 276
277 277 def branch(repo, subset, x):
278 278 """``branch(string or set)``
279 279 All changesets belonging to the given branch or the branches of the given
280 280 changesets.
281 281 """
282 282 try:
283 283 b = getstring(x, '')
284 284 if b in repo.branchmap():
285 285 return [r for r in subset if repo[r].branch() == b]
286 286 except error.ParseError:
287 287 # not a string, but another revspec, e.g. tip()
288 288 pass
289 289
290 290 s = getset(repo, range(len(repo)), x)
291 291 b = set()
292 292 for r in s:
293 293 b.add(repo[r].branch())
294 294 s = set(s)
295 295 return [r for r in subset if r in s or repo[r].branch() in b]
296 296
297 297 def checkstatus(repo, subset, pat, field):
298 298 m = matchmod.match(repo.root, repo.getcwd(), [pat])
299 299 s = []
300 300 fast = (m.files() == [pat])
301 301 for r in subset:
302 302 c = repo[r]
303 303 if fast:
304 304 if pat not in c.files():
305 305 continue
306 306 else:
307 307 for f in c.files():
308 308 if m(f):
309 309 break
310 310 else:
311 311 continue
312 312 files = repo.status(c.p1().node(), c.node())[field]
313 313 if fast:
314 314 if pat in files:
315 315 s.append(r)
316 316 else:
317 317 for f in files:
318 318 if m(f):
319 319 s.append(r)
320 320 break
321 321 return s
322 322
323 323 def children(repo, subset, x):
324 324 """``children(set)``
325 325 Child changesets of changesets in set.
326 326 """
327 327 cs = set()
328 328 cl = repo.changelog
329 329 s = set(getset(repo, range(len(repo)), x))
330 330 for r in xrange(0, len(repo)):
331 331 for p in cl.parentrevs(r):
332 332 if p in s:
333 333 cs.add(r)
334 334 return [r for r in subset if r in cs]
335 335
336 336 def closed(repo, subset, x):
337 337 """``closed()``
338 338 Changeset is closed.
339 339 """
340 340 # i18n: "closed" is a keyword
341 341 getargs(x, 0, 0, _("closed takes no arguments"))
342 342 return [r for r in subset if repo[r].extra().get('close')]
343 343
344 344 def contains(repo, subset, x):
345 345 """``contains(pattern)``
346 346 Revision contains a file matching pattern. See :hg:`help patterns`
347 347 for information about file patterns.
348 348 """
349 349 # i18n: "contains" is a keyword
350 350 pat = getstring(x, _("contains requires a pattern"))
351 351 m = matchmod.match(repo.root, repo.getcwd(), [pat])
352 352 s = []
353 353 if m.files() == [pat]:
354 354 for r in subset:
355 355 if pat in repo[r]:
356 356 s.append(r)
357 357 else:
358 358 for r in subset:
359 359 for f in repo[r].manifest():
360 360 if m(f):
361 361 s.append(r)
362 362 break
363 363 return s
364 364
365 365 def date(repo, subset, x):
366 366 """``date(interval)``
367 367 Changesets within the interval, see :hg:`help dates`.
368 368 """
369 369 # i18n: "date" is a keyword
370 370 ds = getstring(x, _("date requires a string"))
371 371 dm = util.matchdate(ds)
372 372 return [r for r in subset if dm(repo[r].date()[0])]
373 373
374 374 def desc(repo, subset, x):
375 375 """``desc(string)``
376 376 Search commit message for string. The match is case-insensitive.
377 377 """
378 378 # i18n: "desc" is a keyword
379 379 ds = getstring(x, _("desc requires a string")).lower()
380 380 l = []
381 381 for r in subset:
382 382 c = repo[r]
383 383 if ds in c.description().lower():
384 384 l.append(r)
385 385 return l
386 386
387 387 def descendants(repo, subset, x):
388 388 """``descendants(set)``
389 389 Changesets which are descendants of changesets in set.
390 390 """
391 391 args = getset(repo, range(len(repo)), x)
392 392 if not args:
393 393 return []
394 394 s = set(repo.changelog.descendants(*args)) | set(args)
395 395 return [r for r in subset if r in s]
396 396
397 397 def filelog(repo, subset, x):
398 398 """``filelog(pattern)``
399 399 Changesets connected to the specified filelog.
400 400 """
401 401
402 402 pat = getstring(x, _("filelog requires a pattern"))
403 403 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath')
404 404 s = set()
405 405
406 406 if not m.anypats():
407 407 for f in m.files():
408 408 fl = repo.file(f)
409 409 for fr in fl:
410 410 s.add(fl.linkrev(fr))
411 411 else:
412 412 for f in repo[None]:
413 413 if m(f):
414 414 fl = repo.file(f)
415 415 for fr in fl:
416 416 s.add(fl.linkrev(fr))
417 417
418 418 return [r for r in subset if r in s]
419 419
420 420 def first(repo, subset, x):
421 421 """``first(set, [n])``
422 422 An alias for limit().
423 423 """
424 424 return limit(repo, subset, x)
425 425
426 426 def follow(repo, subset, x):
427 427 """``follow([file])``
428 428 An alias for ``::.`` (ancestors of the working copy's first parent).
429 429 If a filename is specified, the history of the given file is followed,
430 430 including copies.
431 431 """
432 432 # i18n: "follow" is a keyword
433 433 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
434 434 p = repo['.'].rev()
435 435 if l:
436 436 x = getstring(l[0], _("follow expected a filename"))
437 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
437 if x in repo['.']:
438 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
439 else:
440 return []
438 441 else:
439 442 s = set(repo.changelog.ancestors(p))
440 443
441 444 s |= set([p])
442 445 return [r for r in subset if r in s]
443 446
444 447 def followfile(repo, subset, x):
445 448 """``follow()``
446 449 An alias for ``::.`` (ancestors of the working copy's first parent).
447 450 """
448 451 # i18n: "follow" is a keyword
449 452 getargs(x, 0, 0, _("follow takes no arguments"))
450 453 p = repo['.'].rev()
451 454 s = set(repo.changelog.ancestors(p)) | set([p])
452 455 return [r for r in subset if r in s]
453 456
454 457 def getall(repo, subset, x):
455 458 """``all()``
456 459 All changesets, the same as ``0:tip``.
457 460 """
458 461 # i18n: "all" is a keyword
459 462 getargs(x, 0, 0, _("all takes no arguments"))
460 463 return subset
461 464
462 465 def grep(repo, subset, x):
463 466 """``grep(regex)``
464 467 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
465 468 to ensure special escape characters are handled correctly. Unlike
466 469 ``keyword(string)``, the match is case-sensitive.
467 470 """
468 471 try:
469 472 # i18n: "grep" is a keyword
470 473 gr = re.compile(getstring(x, _("grep requires a string")))
471 474 except re.error, e:
472 475 raise error.ParseError(_('invalid match pattern: %s') % e)
473 476 l = []
474 477 for r in subset:
475 478 c = repo[r]
476 479 for e in c.files() + [c.user(), c.description()]:
477 480 if gr.search(e):
478 481 l.append(r)
479 482 break
480 483 return l
481 484
482 485 def hasfile(repo, subset, x):
483 486 """``file(pattern)``
484 487 Changesets affecting files matched by pattern.
485 488 """
486 489 # i18n: "file" is a keyword
487 490 pat = getstring(x, _("file requires a pattern"))
488 491 m = matchmod.match(repo.root, repo.getcwd(), [pat])
489 492 s = []
490 493 for r in subset:
491 494 for f in repo[r].files():
492 495 if m(f):
493 496 s.append(r)
494 497 break
495 498 return s
496 499
497 500 def head(repo, subset, x):
498 501 """``head()``
499 502 Changeset is a named branch head.
500 503 """
501 504 # i18n: "head" is a keyword
502 505 getargs(x, 0, 0, _("head takes no arguments"))
503 506 hs = set()
504 507 for b, ls in repo.branchmap().iteritems():
505 508 hs.update(repo[h].rev() for h in ls)
506 509 return [r for r in subset if r in hs]
507 510
508 511 def heads(repo, subset, x):
509 512 """``heads(set)``
510 513 Members of set with no children in set.
511 514 """
512 515 s = getset(repo, subset, x)
513 516 ps = set(parents(repo, subset, x))
514 517 return [r for r in s if r not in ps]
515 518
516 519 def keyword(repo, subset, x):
517 520 """``keyword(string)``
518 521 Search commit message, user name, and names of changed files for
519 522 string. The match is case-insensitive.
520 523 """
521 524 # i18n: "keyword" is a keyword
522 525 kw = getstring(x, _("keyword requires a string")).lower()
523 526 l = []
524 527 for r in subset:
525 528 c = repo[r]
526 529 t = " ".join(c.files() + [c.user(), c.description()])
527 530 if kw in t.lower():
528 531 l.append(r)
529 532 return l
530 533
531 534 def limit(repo, subset, x):
532 535 """``limit(set, [n])``
533 536 First n members of set, defaulting to 1.
534 537 """
535 538 # i18n: "limit" is a keyword
536 539 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
537 540 try:
538 541 lim = 1
539 542 if len(l) == 2:
540 543 # i18n: "limit" is a keyword
541 544 lim = int(getstring(l[1], _("limit requires a number")))
542 545 except (TypeError, ValueError):
543 546 # i18n: "limit" is a keyword
544 547 raise error.ParseError(_("limit expects a number"))
545 548 ss = set(subset)
546 549 os = getset(repo, range(len(repo)), l[0])[:lim]
547 550 return [r for r in os if r in ss]
548 551
549 552 def last(repo, subset, x):
550 553 """``last(set, [n])``
551 554 Last n members of set, defaulting to 1.
552 555 """
553 556 # i18n: "last" is a keyword
554 557 l = getargs(x, 1, 2, _("last requires one or two arguments"))
555 558 try:
556 559 lim = 1
557 560 if len(l) == 2:
558 561 # i18n: "last" is a keyword
559 562 lim = int(getstring(l[1], _("last requires a number")))
560 563 except (TypeError, ValueError):
561 564 # i18n: "last" is a keyword
562 565 raise error.ParseError(_("last expects a number"))
563 566 ss = set(subset)
564 567 os = getset(repo, range(len(repo)), l[0])[-lim:]
565 568 return [r for r in os if r in ss]
566 569
567 570 def maxrev(repo, subset, x):
568 571 """``max(set)``
569 572 Changeset with highest revision number in set.
570 573 """
571 574 os = getset(repo, range(len(repo)), x)
572 575 if os:
573 576 m = max(os)
574 577 if m in subset:
575 578 return [m]
576 579 return []
577 580
578 581 def merge(repo, subset, x):
579 582 """``merge()``
580 583 Changeset is a merge changeset.
581 584 """
582 585 # i18n: "merge" is a keyword
583 586 getargs(x, 0, 0, _("merge takes no arguments"))
584 587 cl = repo.changelog
585 588 return [r for r in subset if cl.parentrevs(r)[1] != -1]
586 589
587 590 def minrev(repo, subset, x):
588 591 """``min(set)``
589 592 Changeset with lowest revision number in set.
590 593 """
591 594 os = getset(repo, range(len(repo)), x)
592 595 if os:
593 596 m = min(os)
594 597 if m in subset:
595 598 return [m]
596 599 return []
597 600
598 601 def modifies(repo, subset, x):
599 602 """``modifies(pattern)``
600 603 Changesets modifying files matched by pattern.
601 604 """
602 605 # i18n: "modifies" is a keyword
603 606 pat = getstring(x, _("modifies requires a pattern"))
604 607 return checkstatus(repo, subset, pat, 0)
605 608
606 609 def node(repo, subset, x):
607 610 """``id(string)``
608 611 Revision non-ambiguously specified by the given hex string prefix.
609 612 """
610 613 # i18n: "id" is a keyword
611 614 l = getargs(x, 1, 1, _("id requires one argument"))
612 615 # i18n: "id" is a keyword
613 616 n = getstring(l[0], _("id requires a string"))
614 617 if len(n) == 40:
615 618 rn = repo[n].rev()
616 619 else:
617 620 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
618 621 return [r for r in subset if r == rn]
619 622
620 623 def outgoing(repo, subset, x):
621 624 """``outgoing([path])``
622 625 Changesets not found in the specified destination repository, or the
623 626 default push location.
624 627 """
625 628 import hg # avoid start-up nasties
626 629 # i18n: "outgoing" is a keyword
627 630 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
628 631 # i18n: "outgoing" is a keyword
629 632 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
630 633 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
631 634 dest, branches = hg.parseurl(dest)
632 635 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
633 636 if revs:
634 637 revs = [repo.lookup(rev) for rev in revs]
635 638 other = hg.peer(repo, {}, dest)
636 639 repo.ui.pushbuffer()
637 640 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
638 641 repo.ui.popbuffer()
639 642 cl = repo.changelog
640 643 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
641 644 return [r for r in subset if r in o]
642 645
643 646 def p1(repo, subset, x):
644 647 """``p1([set])``
645 648 First parent of changesets in set, or the working directory.
646 649 """
647 650 if x is None:
648 651 p = repo[x].p1().rev()
649 652 return [r for r in subset if r == p]
650 653
651 654 ps = set()
652 655 cl = repo.changelog
653 656 for r in getset(repo, range(len(repo)), x):
654 657 ps.add(cl.parentrevs(r)[0])
655 658 return [r for r in subset if r in ps]
656 659
657 660 def p2(repo, subset, x):
658 661 """``p2([set])``
659 662 Second parent of changesets in set, or the working directory.
660 663 """
661 664 if x is None:
662 665 ps = repo[x].parents()
663 666 try:
664 667 p = ps[1].rev()
665 668 return [r for r in subset if r == p]
666 669 except IndexError:
667 670 return []
668 671
669 672 ps = set()
670 673 cl = repo.changelog
671 674 for r in getset(repo, range(len(repo)), x):
672 675 ps.add(cl.parentrevs(r)[1])
673 676 return [r for r in subset if r in ps]
674 677
675 678 def parents(repo, subset, x):
676 679 """``parents([set])``
677 680 The set of all parents for all changesets in set, or the working directory.
678 681 """
679 682 if x is None:
680 683 ps = tuple(p.rev() for p in repo[x].parents())
681 684 return [r for r in subset if r in ps]
682 685
683 686 ps = set()
684 687 cl = repo.changelog
685 688 for r in getset(repo, range(len(repo)), x):
686 689 ps.update(cl.parentrevs(r))
687 690 return [r for r in subset if r in ps]
688 691
689 692 def parentspec(repo, subset, x, n):
690 693 """``set^0``
691 694 The set.
692 695 ``set^1`` (or ``set^``), ``set^2``
693 696 First or second parent, respectively, of all changesets in set.
694 697 """
695 698 try:
696 699 n = int(n[1])
697 700 if n not in (0, 1, 2):
698 701 raise ValueError
699 702 except (TypeError, ValueError):
700 703 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
701 704 ps = set()
702 705 cl = repo.changelog
703 706 for r in getset(repo, subset, x):
704 707 if n == 0:
705 708 ps.add(r)
706 709 elif n == 1:
707 710 ps.add(cl.parentrevs(r)[0])
708 711 elif n == 2:
709 712 parents = cl.parentrevs(r)
710 713 if len(parents) > 1:
711 714 ps.add(parents[1])
712 715 return [r for r in subset if r in ps]
713 716
714 717 def present(repo, subset, x):
715 718 """``present(set)``
716 719 An empty set, if any revision in set isn't found; otherwise,
717 720 all revisions in set.
718 721 """
719 722 try:
720 723 return getset(repo, subset, x)
721 724 except error.RepoLookupError:
722 725 return []
723 726
724 727 def removes(repo, subset, x):
725 728 """``removes(pattern)``
726 729 Changesets which remove files matching pattern.
727 730 """
728 731 # i18n: "removes" is a keyword
729 732 pat = getstring(x, _("removes requires a pattern"))
730 733 return checkstatus(repo, subset, pat, 2)
731 734
732 735 def rev(repo, subset, x):
733 736 """``rev(number)``
734 737 Revision with the given numeric identifier.
735 738 """
736 739 # i18n: "rev" is a keyword
737 740 l = getargs(x, 1, 1, _("rev requires one argument"))
738 741 try:
739 742 # i18n: "rev" is a keyword
740 743 l = int(getstring(l[0], _("rev requires a number")))
741 744 except (TypeError, ValueError):
742 745 # i18n: "rev" is a keyword
743 746 raise error.ParseError(_("rev expects a number"))
744 747 return [r for r in subset if r == l]
745 748
746 749 def reverse(repo, subset, x):
747 750 """``reverse(set)``
748 751 Reverse order of set.
749 752 """
750 753 l = getset(repo, subset, x)
751 754 l.reverse()
752 755 return l
753 756
754 757 def roots(repo, subset, x):
755 758 """``roots(set)``
756 759 Changesets with no parent changeset in set.
757 760 """
758 761 s = getset(repo, subset, x)
759 762 cs = set(children(repo, subset, x))
760 763 return [r for r in s if r not in cs]
761 764
762 765 def sort(repo, subset, x):
763 766 """``sort(set[, [-]key...])``
764 767 Sort set by keys. The default sort order is ascending, specify a key
765 768 as ``-key`` to sort in descending order.
766 769
767 770 The keys can be:
768 771
769 772 - ``rev`` for the revision number,
770 773 - ``branch`` for the branch name,
771 774 - ``desc`` for the commit message (description),
772 775 - ``user`` for user name (``author`` can be used as an alias),
773 776 - ``date`` for the commit date
774 777 """
775 778 # i18n: "sort" is a keyword
776 779 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
777 780 keys = "rev"
778 781 if len(l) == 2:
779 782 keys = getstring(l[1], _("sort spec must be a string"))
780 783
781 784 s = l[0]
782 785 keys = keys.split()
783 786 l = []
784 787 def invert(s):
785 788 return "".join(chr(255 - ord(c)) for c in s)
786 789 for r in getset(repo, subset, s):
787 790 c = repo[r]
788 791 e = []
789 792 for k in keys:
790 793 if k == 'rev':
791 794 e.append(r)
792 795 elif k == '-rev':
793 796 e.append(-r)
794 797 elif k == 'branch':
795 798 e.append(c.branch())
796 799 elif k == '-branch':
797 800 e.append(invert(c.branch()))
798 801 elif k == 'desc':
799 802 e.append(c.description())
800 803 elif k == '-desc':
801 804 e.append(invert(c.description()))
802 805 elif k in 'user author':
803 806 e.append(c.user())
804 807 elif k in '-user -author':
805 808 e.append(invert(c.user()))
806 809 elif k == 'date':
807 810 e.append(c.date()[0])
808 811 elif k == '-date':
809 812 e.append(-c.date()[0])
810 813 else:
811 814 raise error.ParseError(_("unknown sort key %r") % k)
812 815 e.append(r)
813 816 l.append(e)
814 817 l.sort()
815 818 return [e[-1] for e in l]
816 819
817 820 def tag(repo, subset, x):
818 821 """``tag([name])``
819 822 The specified tag by name, or all tagged revisions if no name is given.
820 823 """
821 824 # i18n: "tag" is a keyword
822 825 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
823 826 cl = repo.changelog
824 827 if args:
825 828 tn = getstring(args[0],
826 829 # i18n: "tag" is a keyword
827 830 _('the argument to tag must be a string'))
828 831 if not repo.tags().get(tn, None):
829 832 raise util.Abort(_("tag '%s' does not exist") % tn)
830 833 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
831 834 else:
832 835 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
833 836 return [r for r in subset if r in s]
834 837
835 838 def tagged(repo, subset, x):
836 839 return tag(repo, subset, x)
837 840
838 841 def user(repo, subset, x):
839 842 """``user(string)``
840 843 User name contains string. The match is case-insensitive.
841 844 """
842 845 return author(repo, subset, x)
843 846
844 847 symbols = {
845 848 "adds": adds,
846 849 "all": getall,
847 850 "ancestor": ancestor,
848 851 "ancestors": ancestors,
849 852 "author": author,
850 853 "bisect": bisect,
851 854 "bisected": bisected,
852 855 "bookmark": bookmark,
853 856 "branch": branch,
854 857 "children": children,
855 858 "closed": closed,
856 859 "contains": contains,
857 860 "date": date,
858 861 "desc": desc,
859 862 "descendants": descendants,
860 863 "file": hasfile,
861 864 "filelog": filelog,
862 865 "first": first,
863 866 "follow": follow,
864 867 "grep": grep,
865 868 "head": head,
866 869 "heads": heads,
867 870 "id": node,
868 871 "keyword": keyword,
869 872 "last": last,
870 873 "limit": limit,
871 874 "max": maxrev,
872 875 "merge": merge,
873 876 "min": minrev,
874 877 "modifies": modifies,
875 878 "outgoing": outgoing,
876 879 "p1": p1,
877 880 "p2": p2,
878 881 "parents": parents,
879 882 "present": present,
880 883 "removes": removes,
881 884 "rev": rev,
882 885 "reverse": reverse,
883 886 "roots": roots,
884 887 "sort": sort,
885 888 "tag": tag,
886 889 "tagged": tagged,
887 890 "user": user,
888 891 }
889 892
890 893 methods = {
891 894 "range": rangeset,
892 895 "string": stringset,
893 896 "symbol": symbolset,
894 897 "and": andset,
895 898 "or": orset,
896 899 "not": notset,
897 900 "list": listset,
898 901 "func": func,
899 902 "ancestor": ancestorspec,
900 903 "parent": parentspec,
901 904 "parentpost": p1,
902 905 }
903 906
904 907 def optimize(x, small):
905 908 if x is None:
906 909 return 0, x
907 910
908 911 smallbonus = 1
909 912 if small:
910 913 smallbonus = .5
911 914
912 915 op = x[0]
913 916 if op == 'minus':
914 917 return optimize(('and', x[1], ('not', x[2])), small)
915 918 elif op == 'dagrange':
916 919 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
917 920 ('func', ('symbol', 'ancestors'), x[2])), small)
918 921 elif op == 'dagrangepre':
919 922 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
920 923 elif op == 'dagrangepost':
921 924 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
922 925 elif op == 'rangepre':
923 926 return optimize(('range', ('string', '0'), x[1]), small)
924 927 elif op == 'rangepost':
925 928 return optimize(('range', x[1], ('string', 'tip')), small)
926 929 elif op == 'negate':
927 930 return optimize(('string',
928 931 '-' + getstring(x[1], _("can't negate that"))), small)
929 932 elif op in 'string symbol negate':
930 933 return smallbonus, x # single revisions are small
931 934 elif op == 'and' or op == 'dagrange':
932 935 wa, ta = optimize(x[1], True)
933 936 wb, tb = optimize(x[2], True)
934 937 w = min(wa, wb)
935 938 if wa > wb:
936 939 return w, (op, tb, ta)
937 940 return w, (op, ta, tb)
938 941 elif op == 'or':
939 942 wa, ta = optimize(x[1], False)
940 943 wb, tb = optimize(x[2], False)
941 944 if wb < wa:
942 945 wb, wa = wa, wb
943 946 return max(wa, wb), (op, ta, tb)
944 947 elif op == 'not':
945 948 o = optimize(x[1], not small)
946 949 return o[0], (op, o[1])
947 950 elif op == 'parentpost':
948 951 o = optimize(x[1], small)
949 952 return o[0], (op, o[1])
950 953 elif op == 'group':
951 954 return optimize(x[1], small)
952 955 elif op in 'range list parent ancestorspec':
953 956 if op == 'parent':
954 957 # x^:y means (x^) : y, not x ^ (:y)
955 958 post = ('parentpost', x[1])
956 959 if x[2][0] == 'dagrangepre':
957 960 return optimize(('dagrange', post, x[2][1]), small)
958 961 elif x[2][0] == 'rangepre':
959 962 return optimize(('range', post, x[2][1]), small)
960 963
961 964 wa, ta = optimize(x[1], small)
962 965 wb, tb = optimize(x[2], small)
963 966 return wa + wb, (op, ta, tb)
964 967 elif op == 'func':
965 968 f = getstring(x[1], _("not a symbol"))
966 969 wa, ta = optimize(x[2], small)
967 970 if f in ("author branch closed date desc file grep keyword "
968 971 "outgoing user"):
969 972 w = 10 # slow
970 973 elif f in "modifies adds removes":
971 974 w = 30 # slower
972 975 elif f == "contains":
973 976 w = 100 # very slow
974 977 elif f == "ancestor":
975 978 w = 1 * smallbonus
976 979 elif f in "reverse limit first":
977 980 w = 0
978 981 elif f in "sort":
979 982 w = 10 # assume most sorts look at changelog
980 983 else:
981 984 w = 1
982 985 return w + wa, (op, x[1], ta)
983 986 return 1, x
984 987
985 988 class revsetalias(object):
986 989 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
987 990 args = None
988 991
989 992 def __init__(self, name, value):
990 993 '''Aliases like:
991 994
992 995 h = heads(default)
993 996 b($1) = ancestors($1) - ancestors(default)
994 997 '''
995 998 if isinstance(name, tuple): # parameter substitution
996 999 self.tree = name
997 1000 self.replacement = value
998 1001 else: # alias definition
999 1002 m = self.funcre.search(name)
1000 1003 if m:
1001 1004 self.tree = ('func', ('symbol', m.group(1)))
1002 1005 self.args = [x.strip() for x in m.group(2).split(',')]
1003 1006 for arg in self.args:
1004 1007 value = value.replace(arg, repr(arg))
1005 1008 else:
1006 1009 self.tree = ('symbol', name)
1007 1010
1008 1011 self.replacement, pos = parse(value)
1009 1012 if pos != len(value):
1010 1013 raise error.ParseError(_('invalid token'), pos)
1011 1014
1012 1015 def process(self, tree):
1013 1016 if isinstance(tree, tuple):
1014 1017 if self.args is None:
1015 1018 if tree == self.tree:
1016 1019 return self.replacement
1017 1020 elif tree[:2] == self.tree:
1018 1021 l = getlist(tree[2])
1019 1022 if len(l) != len(self.args):
1020 1023 raise error.ParseError(
1021 1024 _('invalid number of arguments: %s') % len(l))
1022 1025 result = self.replacement
1023 1026 for a, v in zip(self.args, l):
1024 1027 valalias = revsetalias(('string', a), v)
1025 1028 result = valalias.process(result)
1026 1029 return result
1027 1030 return tuple(map(self.process, tree))
1028 1031 return tree
1029 1032
1030 1033 def findaliases(ui, tree):
1031 1034 for k, v in ui.configitems('revsetalias'):
1032 1035 alias = revsetalias(k, v)
1033 1036 tree = alias.process(tree)
1034 1037 return tree
1035 1038
1036 1039 parse = parser.parser(tokenize, elements).parse
1037 1040
1038 1041 def match(ui, spec):
1039 1042 if not spec:
1040 1043 raise error.ParseError(_("empty query"))
1041 1044 tree, pos = parse(spec)
1042 1045 if (pos != len(spec)):
1043 1046 raise error.ParseError(_("invalid token"), pos)
1044 1047 if ui:
1045 1048 tree = findaliases(ui, tree)
1046 1049 weight, tree = optimize(tree, True)
1047 1050 def mfunc(repo, subset):
1048 1051 return getset(repo, subset, tree)
1049 1052 return mfunc
1050 1053
1051 1054 def formatspec(expr, *args):
1052 1055 '''
1053 1056 This is a convenience function for using revsets internally, and
1054 1057 escapes arguments appropriately. Aliases are intentionally ignored
1055 1058 so that intended expression behavior isn't accidentally subverted.
1056 1059
1057 1060 Supported arguments:
1058 1061
1059 1062 %r = revset expression, parenthesized
1060 1063 %d = int(arg), no quoting
1061 1064 %s = string(arg), escaped and single-quoted
1062 1065 %b = arg.branch(), escaped and single-quoted
1063 1066 %n = hex(arg), single-quoted
1064 1067 %% = a literal '%'
1065 1068
1066 1069 Prefixing the type with 'l' specifies a parenthesized list of that type.
1067 1070
1068 1071 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1069 1072 '(10 or 11):: and ((this()) or (that()))'
1070 1073 >>> formatspec('%d:: and not %d::', 10, 20)
1071 1074 '10:: and not 20::'
1072 1075 >>> formatspec('%ld or %ld', [], [1])
1073 1076 '(0-0) or (1)'
1074 1077 >>> formatspec('keyword(%s)', 'foo\\xe9')
1075 1078 "keyword('foo\\\\xe9')"
1076 1079 >>> b = lambda: 'default'
1077 1080 >>> b.branch = b
1078 1081 >>> formatspec('branch(%b)', b)
1079 1082 "branch('default')"
1080 1083 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1081 1084 "root(('a' or 'b' or 'c' or 'd'))"
1082 1085 '''
1083 1086
1084 1087 def quote(s):
1085 1088 return repr(str(s))
1086 1089
1087 1090 def argtype(c, arg):
1088 1091 if c == 'd':
1089 1092 return str(int(arg))
1090 1093 elif c == 's':
1091 1094 return quote(arg)
1092 1095 elif c == 'r':
1093 1096 parse(arg) # make sure syntax errors are confined
1094 1097 return '(%s)' % arg
1095 1098 elif c == 'n':
1096 1099 return quote(nodemod.hex(arg))
1097 1100 elif c == 'b':
1098 1101 return quote(arg.branch())
1099 1102
1100 1103 ret = ''
1101 1104 pos = 0
1102 1105 arg = 0
1103 1106 while pos < len(expr):
1104 1107 c = expr[pos]
1105 1108 if c == '%':
1106 1109 pos += 1
1107 1110 d = expr[pos]
1108 1111 if d == '%':
1109 1112 ret += d
1110 1113 elif d in 'dsnbr':
1111 1114 ret += argtype(d, args[arg])
1112 1115 arg += 1
1113 1116 elif d == 'l':
1114 1117 # a list of some type
1115 1118 pos += 1
1116 1119 d = expr[pos]
1117 1120 if args[arg]:
1118 1121 lv = ' or '.join(argtype(d, e) for e in args[arg])
1119 1122 else:
1120 1123 lv = '0-0' # a minimal way to represent an empty set
1121 1124 ret += '(%s)' % lv
1122 1125 arg += 1
1123 1126 else:
1124 1127 raise util.Abort('unexpected revspec format character %s' % d)
1125 1128 else:
1126 1129 ret += c
1127 1130 pos += 1
1128 1131
1129 1132 return ret
1130 1133
1131 1134 # tell hggettext to extract docstrings from these functions:
1132 1135 i18nfunctions = symbols.values()
@@ -1,1133 +1,1142
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
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 l in ctx['.hgsubstate'].data().splitlines():
47 47 revision, path = l.split(" ", 1)
48 48 rev[path] = revision
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52
53 53 def remap(src):
54 54 for pattern, repl in p.items('subpaths'):
55 55 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
56 56 # does a string decode.
57 57 repl = repl.encode('string-escape')
58 58 # However, we still want to allow back references to go
59 59 # through unharmed, so we turn r'\\1' into r'\1'. Again,
60 60 # extra escapes are needed because re.sub string decodes.
61 61 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
62 62 try:
63 63 src = re.sub(pattern, repl, src, 1)
64 64 except re.error, e:
65 65 raise util.Abort(_("bad subrepository pattern in %s: %s")
66 66 % (p.source('subpaths', pattern), e))
67 67 return src
68 68
69 69 state = {}
70 70 for path, src in p[''].items():
71 71 kind = 'hg'
72 72 if src.startswith('['):
73 73 if ']' not in src:
74 74 raise util.Abort(_('missing ] in subrepo source'))
75 75 kind, src = src.split(']', 1)
76 76 kind = kind[1:]
77 77 src = src.lstrip() # strip any extra whitespace after ']'
78 78
79 79 if not util.url(src).isabs():
80 80 parent = _abssource(ctx._repo, abort=False)
81 81 if parent:
82 82 parent = util.url(parent)
83 83 parent.path = posixpath.join(parent.path or '', src)
84 84 parent.path = posixpath.normpath(parent.path)
85 85 joined = str(parent)
86 86 # Remap the full joined path and use it if it changes,
87 87 # else remap the original source.
88 88 remapped = remap(joined)
89 89 if remapped == joined:
90 90 src = remap(src)
91 91 else:
92 92 src = remapped
93 93
94 94 src = remap(src)
95 95 state[path] = (src.strip(), rev.get(path, ''), kind)
96 96
97 97 return state
98 98
99 99 def writestate(repo, state):
100 100 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
101 101 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
102 102 repo.wwrite('.hgsubstate', ''.join(lines), '')
103 103
104 104 def submerge(repo, wctx, mctx, actx, overwrite):
105 105 """delegated from merge.applyupdates: merging of .hgsubstate file
106 106 in working context, merging context and ancestor context"""
107 107 if mctx == actx: # backwards?
108 108 actx = wctx.p1()
109 109 s1 = wctx.substate
110 110 s2 = mctx.substate
111 111 sa = actx.substate
112 112 sm = {}
113 113
114 114 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
115 115
116 116 def debug(s, msg, r=""):
117 117 if r:
118 118 r = "%s:%s:%s" % r
119 119 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
120 120
121 121 for s, l in s1.items():
122 122 a = sa.get(s, nullstate)
123 123 ld = l # local state with possible dirty flag for compares
124 124 if wctx.sub(s).dirty():
125 125 ld = (l[0], l[1] + "+")
126 126 if wctx == actx: # overwrite
127 127 a = ld
128 128
129 129 if s in s2:
130 130 r = s2[s]
131 131 if ld == r or r == a: # no change or local is newer
132 132 sm[s] = l
133 133 continue
134 134 elif ld == a: # other side changed
135 135 debug(s, "other changed, get", r)
136 136 wctx.sub(s).get(r, overwrite)
137 137 sm[s] = r
138 138 elif ld[0] != r[0]: # sources differ
139 139 if repo.ui.promptchoice(
140 140 _(' subrepository sources for %s differ\n'
141 141 'use (l)ocal source (%s) or (r)emote source (%s)?')
142 142 % (s, l[0], r[0]),
143 143 (_('&Local'), _('&Remote')), 0):
144 144 debug(s, "prompt changed, get", r)
145 145 wctx.sub(s).get(r, overwrite)
146 146 sm[s] = r
147 147 elif ld[1] == a[1]: # local side is unchanged
148 148 debug(s, "other side changed, get", r)
149 149 wctx.sub(s).get(r, overwrite)
150 150 sm[s] = r
151 151 else:
152 152 debug(s, "both sides changed, merge with", r)
153 153 wctx.sub(s).merge(r)
154 154 sm[s] = l
155 155 elif ld == a: # remote removed, local unchanged
156 156 debug(s, "remote removed, remove")
157 157 wctx.sub(s).remove()
158 158 elif a == nullstate: # not present in remote or ancestor
159 159 debug(s, "local added, keep")
160 160 sm[s] = l
161 161 continue
162 162 else:
163 163 if repo.ui.promptchoice(
164 164 _(' local changed subrepository %s which remote removed\n'
165 165 'use (c)hanged version or (d)elete?') % s,
166 166 (_('&Changed'), _('&Delete')), 0):
167 167 debug(s, "prompt remove")
168 168 wctx.sub(s).remove()
169 169
170 170 for s, r in sorted(s2.items()):
171 171 if s in s1:
172 172 continue
173 173 elif s not in sa:
174 174 debug(s, "remote added, get", r)
175 175 mctx.sub(s).get(r)
176 176 sm[s] = r
177 177 elif r != sa[s]:
178 178 if repo.ui.promptchoice(
179 179 _(' remote changed subrepository %s which local removed\n'
180 180 'use (c)hanged version or (d)elete?') % s,
181 181 (_('&Changed'), _('&Delete')), 0) == 0:
182 182 debug(s, "prompt recreate", r)
183 183 wctx.sub(s).get(r)
184 184 sm[s] = r
185 185
186 186 # record merged .hgsubstate
187 187 writestate(repo, sm)
188 188
189 189 def _updateprompt(ui, sub, dirty, local, remote):
190 190 if dirty:
191 191 msg = (_(' subrepository sources for %s differ\n'
192 192 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
193 193 % (subrelpath(sub), local, remote))
194 194 else:
195 195 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
196 196 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
197 197 % (subrelpath(sub), local, remote))
198 198 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
199 199
200 200 def reporelpath(repo):
201 201 """return path to this (sub)repo as seen from outermost repo"""
202 202 parent = repo
203 203 while util.safehasattr(parent, '_subparent'):
204 204 parent = parent._subparent
205 205 p = parent.root.rstrip(os.sep)
206 206 return repo.root[len(p) + 1:]
207 207
208 208 def subrelpath(sub):
209 209 """return path to this subrepo as seen from outermost repo"""
210 210 if util.safehasattr(sub, '_relpath'):
211 211 return sub._relpath
212 212 if not util.safehasattr(sub, '_repo'):
213 213 return sub._path
214 214 return reporelpath(sub._repo)
215 215
216 216 def _abssource(repo, push=False, abort=True):
217 217 """return pull/push path of repo - either based on parent repo .hgsub info
218 218 or on the top repo config. Abort or return None if no source found."""
219 219 if util.safehasattr(repo, '_subparent'):
220 220 source = util.url(repo._subsource)
221 221 if source.isabs():
222 222 return str(source)
223 223 source.path = posixpath.normpath(source.path)
224 224 parent = _abssource(repo._subparent, push, abort=False)
225 225 if parent:
226 226 parent = util.url(util.pconvert(parent))
227 227 parent.path = posixpath.join(parent.path or '', source.path)
228 228 parent.path = posixpath.normpath(parent.path)
229 229 return str(parent)
230 230 else: # recursion reached top repo
231 231 if util.safehasattr(repo, '_subtoppath'):
232 232 return repo._subtoppath
233 233 if push and repo.ui.config('paths', 'default-push'):
234 234 return repo.ui.config('paths', 'default-push')
235 235 if repo.ui.config('paths', 'default'):
236 236 return repo.ui.config('paths', 'default')
237 237 if abort:
238 238 raise util.Abort(_("default path for subrepository %s not found") %
239 239 reporelpath(repo))
240 240
241 241 def itersubrepos(ctx1, ctx2):
242 242 """find subrepos in ctx1 or ctx2"""
243 243 # Create a (subpath, ctx) mapping where we prefer subpaths from
244 244 # ctx1. The subpaths from ctx2 are important when the .hgsub file
245 245 # has been modified (in ctx2) but not yet committed (in ctx1).
246 246 subpaths = dict.fromkeys(ctx2.substate, ctx2)
247 247 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
248 248 for subpath, ctx in sorted(subpaths.iteritems()):
249 249 yield subpath, ctx.sub(subpath)
250 250
251 251 def subrepo(ctx, path):
252 252 """return instance of the right subrepo class for subrepo in path"""
253 253 # subrepo inherently violates our import layering rules
254 254 # because it wants to make repo objects from deep inside the stack
255 255 # so we manually delay the circular imports to not break
256 256 # scripts that don't use our demand-loading
257 257 global hg
258 258 import hg as h
259 259 hg = h
260 260
261 261 scmutil.pathauditor(ctx._repo.root)(path)
262 262 state = ctx.substate.get(path, nullstate)
263 263 if state[2] not in types:
264 264 raise util.Abort(_('unknown subrepo type %s') % state[2])
265 265 return types[state[2]](ctx, path, state[:2])
266 266
267 267 # subrepo classes need to implement the following abstract class:
268 268
269 269 class abstractsubrepo(object):
270 270
271 271 def dirty(self, ignoreupdate=False):
272 272 """returns true if the dirstate of the subrepo is dirty or does not
273 273 match current stored state. If ignoreupdate is true, only check
274 274 whether the subrepo has uncommitted changes in its dirstate.
275 275 """
276 276 raise NotImplementedError
277 277
278 278 def checknested(self, path):
279 279 """check if path is a subrepository within this repository"""
280 280 return False
281 281
282 282 def commit(self, text, user, date):
283 283 """commit the current changes to the subrepo with the given
284 284 log message. Use given user and date if possible. Return the
285 285 new state of the subrepo.
286 286 """
287 287 raise NotImplementedError
288 288
289 289 def remove(self):
290 290 """remove the subrepo
291 291
292 292 (should verify the dirstate is not dirty first)
293 293 """
294 294 raise NotImplementedError
295 295
296 296 def get(self, state, overwrite=False):
297 297 """run whatever commands are needed to put the subrepo into
298 298 this state
299 299 """
300 300 raise NotImplementedError
301 301
302 302 def merge(self, state):
303 303 """merge currently-saved state with the new state."""
304 304 raise NotImplementedError
305 305
306 306 def push(self, force):
307 307 """perform whatever action is analogous to 'hg push'
308 308
309 309 This may be a no-op on some systems.
310 310 """
311 311 raise NotImplementedError
312 312
313 313 def add(self, ui, match, dryrun, prefix):
314 314 return []
315 315
316 316 def status(self, rev2, **opts):
317 317 return [], [], [], [], [], [], []
318 318
319 319 def diff(self, diffopts, node2, match, prefix, **opts):
320 320 pass
321 321
322 322 def outgoing(self, ui, dest, opts):
323 323 return 1
324 324
325 325 def incoming(self, ui, source, opts):
326 326 return 1
327 327
328 328 def files(self):
329 329 """return filename iterator"""
330 330 raise NotImplementedError
331 331
332 332 def filedata(self, name):
333 333 """return file data"""
334 334 raise NotImplementedError
335 335
336 336 def fileflags(self, name):
337 337 """return file flags"""
338 338 return ''
339 339
340 340 def archive(self, ui, archiver, prefix):
341 341 files = self.files()
342 342 total = len(files)
343 343 relpath = subrelpath(self)
344 344 ui.progress(_('archiving (%s)') % relpath, 0,
345 345 unit=_('files'), total=total)
346 346 for i, name in enumerate(files):
347 347 flags = self.fileflags(name)
348 348 mode = 'x' in flags and 0755 or 0644
349 349 symlink = 'l' in flags
350 350 archiver.addfile(os.path.join(prefix, self._path, name),
351 351 mode, symlink, self.filedata(name))
352 352 ui.progress(_('archiving (%s)') % relpath, i + 1,
353 353 unit=_('files'), total=total)
354 354 ui.progress(_('archiving (%s)') % relpath, None)
355 355
356 356 def walk(self, match):
357 357 '''
358 358 walk recursively through the directory tree, finding all files
359 359 matched by the match function
360 360 '''
361 361 pass
362 362
363 363 def forget(self, files):
364 364 pass
365 365
366 366 class hgsubrepo(abstractsubrepo):
367 367 def __init__(self, ctx, path, state):
368 368 self._path = path
369 369 self._state = state
370 370 r = ctx._repo
371 371 root = r.wjoin(path)
372 372 create = False
373 373 if not os.path.exists(os.path.join(root, '.hg')):
374 374 create = True
375 375 util.makedirs(root)
376 376 self._repo = hg.repository(r.ui, root, create=create)
377 377 self._initrepo(r, state[0], create)
378 378
379 379 def _initrepo(self, parentrepo, source, create):
380 380 self._repo._subparent = parentrepo
381 381 self._repo._subsource = source
382 382
383 383 if create:
384 384 fp = self._repo.opener("hgrc", "w", text=True)
385 385 fp.write('[paths]\n')
386 386
387 387 def addpathconfig(key, value):
388 388 if value:
389 389 fp.write('%s = %s\n' % (key, value))
390 390 self._repo.ui.setconfig('paths', key, value)
391 391
392 392 defpath = _abssource(self._repo, abort=False)
393 393 defpushpath = _abssource(self._repo, True, abort=False)
394 394 addpathconfig('default', defpath)
395 395 if defpath != defpushpath:
396 396 addpathconfig('default-push', defpushpath)
397 397 fp.close()
398 398
399 399 def add(self, ui, match, dryrun, prefix):
400 400 return cmdutil.add(ui, self._repo, match, dryrun, True,
401 401 os.path.join(prefix, self._path))
402 402
403 403 def status(self, rev2, **opts):
404 404 try:
405 405 rev1 = self._state[1]
406 406 ctx1 = self._repo[rev1]
407 407 ctx2 = self._repo[rev2]
408 408 return self._repo.status(ctx1, ctx2, **opts)
409 409 except error.RepoLookupError, inst:
410 410 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
411 411 % (inst, subrelpath(self)))
412 412 return [], [], [], [], [], [], []
413 413
414 414 def diff(self, diffopts, node2, match, prefix, **opts):
415 415 try:
416 416 node1 = node.bin(self._state[1])
417 417 # We currently expect node2 to come from substate and be
418 418 # in hex format
419 419 if node2 is not None:
420 420 node2 = node.bin(node2)
421 421 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
422 422 node1, node2, match,
423 423 prefix=os.path.join(prefix, self._path),
424 424 listsubrepos=True, **opts)
425 425 except error.RepoLookupError, inst:
426 426 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
427 427 % (inst, subrelpath(self)))
428 428
429 429 def archive(self, ui, archiver, prefix):
430 430 self._get(self._state + ('hg',))
431 431 abstractsubrepo.archive(self, ui, archiver, prefix)
432 432
433 433 rev = self._state[1]
434 434 ctx = self._repo[rev]
435 435 for subpath in ctx.substate:
436 436 s = subrepo(ctx, subpath)
437 437 s.archive(ui, archiver, os.path.join(prefix, self._path))
438 438
439 439 def dirty(self, ignoreupdate=False):
440 440 r = self._state[1]
441 441 if r == '' and not ignoreupdate: # no state recorded
442 442 return True
443 443 w = self._repo[None]
444 444 if r != w.p1().hex() and not ignoreupdate:
445 445 # different version checked out
446 446 return True
447 447 return w.dirty() # working directory changed
448 448
449 449 def checknested(self, path):
450 450 return self._repo._checknested(self._repo.wjoin(path))
451 451
452 452 def commit(self, text, user, date):
453 453 # don't bother committing in the subrepo if it's only been
454 454 # updated
455 455 if not self.dirty(True):
456 456 return self._repo['.'].hex()
457 457 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
458 458 n = self._repo.commit(text, user, date)
459 459 if not n:
460 460 return self._repo['.'].hex() # different version checked out
461 461 return node.hex(n)
462 462
463 463 def remove(self):
464 464 # we can't fully delete the repository as it may contain
465 465 # local-only history
466 466 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
467 467 hg.clean(self._repo, node.nullid, False)
468 468
469 469 def _get(self, state):
470 470 source, revision, kind = state
471 471 if revision not in self._repo:
472 472 self._repo._subsource = source
473 473 srcurl = _abssource(self._repo)
474 474 other = hg.peer(self._repo.ui, {}, srcurl)
475 475 if len(self._repo) == 0:
476 476 self._repo.ui.status(_('cloning subrepo %s from %s\n')
477 477 % (subrelpath(self), srcurl))
478 478 parentrepo = self._repo._subparent
479 479 shutil.rmtree(self._repo.path)
480 480 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
481 481 self._repo.root, update=False)
482 482 self._initrepo(parentrepo, source, create=True)
483 483 else:
484 484 self._repo.ui.status(_('pulling subrepo %s from %s\n')
485 485 % (subrelpath(self), srcurl))
486 486 self._repo.pull(other)
487 487 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
488 488
489 489 def get(self, state, overwrite=False):
490 490 self._get(state)
491 491 source, revision, kind = state
492 492 self._repo.ui.debug("getting subrepo %s\n" % self._path)
493 493 hg.clean(self._repo, revision, False)
494 494
495 495 def merge(self, state):
496 496 self._get(state)
497 497 cur = self._repo['.']
498 498 dst = self._repo[state[1]]
499 499 anc = dst.ancestor(cur)
500 500
501 501 def mergefunc():
502 502 if anc == cur:
503 503 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
504 504 hg.update(self._repo, state[1])
505 505 elif anc == dst:
506 506 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
507 507 else:
508 508 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
509 509 hg.merge(self._repo, state[1], remind=False)
510 510
511 511 wctx = self._repo[None]
512 512 if self.dirty():
513 513 if anc != dst:
514 514 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
515 515 mergefunc()
516 516 else:
517 517 mergefunc()
518 518 else:
519 519 mergefunc()
520 520
521 521 def push(self, force):
522 522 # push subrepos depth-first for coherent ordering
523 523 c = self._repo['']
524 524 subs = c.substate # only repos that are committed
525 525 for s in sorted(subs):
526 526 if not c.sub(s).push(force):
527 527 return False
528 528
529 529 dsturl = _abssource(self._repo, True)
530 530 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
531 531 (subrelpath(self), dsturl))
532 532 other = hg.peer(self._repo.ui, {}, dsturl)
533 533 return self._repo.push(other, force)
534 534
535 535 def outgoing(self, ui, dest, opts):
536 536 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
537 537
538 538 def incoming(self, ui, source, opts):
539 539 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
540 540
541 541 def files(self):
542 542 rev = self._state[1]
543 543 ctx = self._repo[rev]
544 544 return ctx.manifest()
545 545
546 546 def filedata(self, name):
547 547 rev = self._state[1]
548 548 return self._repo[rev][name].data()
549 549
550 550 def fileflags(self, name):
551 551 rev = self._state[1]
552 552 ctx = self._repo[rev]
553 553 return ctx.flags(name)
554 554
555 555 def walk(self, match):
556 556 ctx = self._repo[None]
557 557 return ctx.walk(match)
558 558
559 559 def forget(self, files):
560 560 ctx = self._repo[None]
561 561 ctx.forget(files)
562 562
563 563 class svnsubrepo(abstractsubrepo):
564 564 def __init__(self, ctx, path, state):
565 565 self._path = path
566 566 self._state = state
567 567 self._ctx = ctx
568 568 self._ui = ctx._repo.ui
569 569 self._exe = util.findexe('svn')
570 570 if not self._exe:
571 571 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
572 572 % self._path)
573 573
574 574 def _svncommand(self, commands, filename='', failok=False):
575 575 cmd = [self._exe]
576 576 extrakw = {}
577 577 if not self._ui.interactive():
578 578 # Making stdin be a pipe should prevent svn from behaving
579 579 # interactively even if we can't pass --non-interactive.
580 580 extrakw['stdin'] = subprocess.PIPE
581 581 # Starting in svn 1.5 --non-interactive is a global flag
582 582 # instead of being per-command, but we need to support 1.4 so
583 583 # we have to be intelligent about what commands take
584 584 # --non-interactive.
585 585 if commands[0] in ('update', 'checkout', 'commit'):
586 586 cmd.append('--non-interactive')
587 587 cmd.extend(commands)
588 588 if filename is not None:
589 589 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
590 590 cmd.append(path)
591 591 env = dict(os.environ)
592 592 # Avoid localized output, preserve current locale for everything else.
593 593 env['LC_MESSAGES'] = 'C'
594 594 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
595 595 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
596 596 universal_newlines=True, env=env, **extrakw)
597 597 stdout, stderr = p.communicate()
598 598 stderr = stderr.strip()
599 599 if not failok:
600 600 if p.returncode:
601 601 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
602 602 if stderr:
603 603 self._ui.warn(stderr + '\n')
604 604 return stdout, stderr
605 605
606 606 @propertycache
607 607 def _svnversion(self):
608 608 output, err = self._svncommand(['--version'], filename=None)
609 609 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
610 610 if not m:
611 611 raise util.Abort(_('cannot retrieve svn tool version'))
612 612 return (int(m.group(1)), int(m.group(2)))
613 613
614 614 def _wcrevs(self):
615 615 # Get the working directory revision as well as the last
616 616 # commit revision so we can compare the subrepo state with
617 617 # both. We used to store the working directory one.
618 618 output, err = self._svncommand(['info', '--xml'])
619 619 doc = xml.dom.minidom.parseString(output)
620 620 entries = doc.getElementsByTagName('entry')
621 621 lastrev, rev = '0', '0'
622 622 if entries:
623 623 rev = str(entries[0].getAttribute('revision')) or '0'
624 624 commits = entries[0].getElementsByTagName('commit')
625 625 if commits:
626 626 lastrev = str(commits[0].getAttribute('revision')) or '0'
627 627 return (lastrev, rev)
628 628
629 629 def _wcrev(self):
630 630 return self._wcrevs()[0]
631 631
632 632 def _wcchanged(self):
633 633 """Return (changes, extchanges) where changes is True
634 634 if the working directory was changed, and extchanges is
635 635 True if any of these changes concern an external entry.
636 636 """
637 637 output, err = self._svncommand(['status', '--xml'])
638 638 externals, changes = [], []
639 639 doc = xml.dom.minidom.parseString(output)
640 640 for e in doc.getElementsByTagName('entry'):
641 641 s = e.getElementsByTagName('wc-status')
642 642 if not s:
643 643 continue
644 644 item = s[0].getAttribute('item')
645 645 props = s[0].getAttribute('props')
646 646 path = e.getAttribute('path')
647 647 if item == 'external':
648 648 externals.append(path)
649 649 if (item not in ('', 'normal', 'unversioned', 'external')
650 650 or props not in ('', 'none', 'normal')):
651 651 changes.append(path)
652 652 for path in changes:
653 653 for ext in externals:
654 654 if path == ext or path.startswith(ext + os.sep):
655 655 return True, True
656 656 return bool(changes), False
657 657
658 658 def dirty(self, ignoreupdate=False):
659 659 if not self._wcchanged()[0]:
660 660 if self._state[1] in self._wcrevs() or ignoreupdate:
661 661 return False
662 662 return True
663 663
664 664 def commit(self, text, user, date):
665 665 # user and date are out of our hands since svn is centralized
666 666 changed, extchanged = self._wcchanged()
667 667 if not changed:
668 668 return self._wcrev()
669 669 if extchanged:
670 670 # Do not try to commit externals
671 671 raise util.Abort(_('cannot commit svn externals'))
672 672 commitinfo, err = self._svncommand(['commit', '-m', text])
673 673 self._ui.status(commitinfo)
674 674 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
675 675 if not newrev:
676 676 raise util.Abort(commitinfo.splitlines()[-1])
677 677 newrev = newrev.groups()[0]
678 678 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
679 679 return newrev
680 680
681 681 def remove(self):
682 682 if self.dirty():
683 683 self._ui.warn(_('not removing repo %s because '
684 684 'it has changes.\n' % self._path))
685 685 return
686 686 self._ui.note(_('removing subrepo %s\n') % self._path)
687 687
688 688 def onerror(function, path, excinfo):
689 689 if function is not os.remove:
690 690 raise
691 691 # read-only files cannot be unlinked under Windows
692 692 s = os.stat(path)
693 693 if (s.st_mode & stat.S_IWRITE) != 0:
694 694 raise
695 695 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
696 696 os.remove(path)
697 697
698 698 path = self._ctx._repo.wjoin(self._path)
699 699 shutil.rmtree(path, onerror=onerror)
700 700 try:
701 701 os.removedirs(os.path.dirname(path))
702 702 except OSError:
703 703 pass
704 704
705 705 def get(self, state, overwrite=False):
706 706 if overwrite:
707 707 self._svncommand(['revert', '--recursive'])
708 708 args = ['checkout']
709 709 if self._svnversion >= (1, 5):
710 710 args.append('--force')
711 711 # The revision must be specified at the end of the URL to properly
712 712 # update to a directory which has since been deleted and recreated.
713 713 args.append('%s@%s' % (state[0], state[1]))
714 714 status, err = self._svncommand(args, failok=True)
715 715 if not re.search('Checked out revision [0-9]+.', status):
716 716 if ('is already a working copy for a different URL' in err
717 717 and (self._wcchanged() == (False, False))):
718 718 # obstructed but clean working copy, so just blow it away.
719 719 self.remove()
720 720 self.get(state, overwrite=False)
721 721 return
722 722 raise util.Abort((status or err).splitlines()[-1])
723 723 self._ui.status(status)
724 724
725 725 def merge(self, state):
726 726 old = self._state[1]
727 727 new = state[1]
728 728 if new != self._wcrev():
729 729 dirty = old == self._wcrev() or self._wcchanged()[0]
730 730 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
731 731 self.get(state, False)
732 732
733 733 def push(self, force):
734 734 # push is a no-op for SVN
735 735 return True
736 736
737 737 def files(self):
738 738 output = self._svncommand(['list'])
739 739 # This works because svn forbids \n in filenames.
740 740 return output.splitlines()
741 741
742 742 def filedata(self, name):
743 743 return self._svncommand(['cat'], name)
744 744
745 745
746 746 class gitsubrepo(abstractsubrepo):
747 747 def __init__(self, ctx, path, state):
748 748 # TODO add git version check.
749 749 self._state = state
750 750 self._ctx = ctx
751 751 self._path = path
752 752 self._relpath = os.path.join(reporelpath(ctx._repo), path)
753 753 self._abspath = ctx._repo.wjoin(path)
754 754 self._subparent = ctx._repo
755 755 self._ui = ctx._repo.ui
756 756
757 757 def _gitcommand(self, commands, env=None, stream=False):
758 758 return self._gitdir(commands, env=env, stream=stream)[0]
759 759
760 760 def _gitdir(self, commands, env=None, stream=False):
761 761 return self._gitnodir(commands, env=env, stream=stream,
762 762 cwd=self._abspath)
763 763
764 764 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
765 765 """Calls the git command
766 766
767 767 The methods tries to call the git command. versions previor to 1.6.0
768 768 are not supported and very probably fail.
769 769 """
770 770 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
771 771 # unless ui.quiet is set, print git's stderr,
772 772 # which is mostly progress and useful info
773 773 errpipe = None
774 774 if self._ui.quiet:
775 775 errpipe = open(os.devnull, 'w')
776 776 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
777 777 close_fds=util.closefds,
778 778 stdout=subprocess.PIPE, stderr=errpipe)
779 779 if stream:
780 780 return p.stdout, None
781 781
782 782 retdata = p.stdout.read().strip()
783 783 # wait for the child to exit to avoid race condition.
784 784 p.wait()
785 785
786 786 if p.returncode != 0 and p.returncode != 1:
787 787 # there are certain error codes that are ok
788 788 command = commands[0]
789 789 if command in ('cat-file', 'symbolic-ref'):
790 790 return retdata, p.returncode
791 791 # for all others, abort
792 792 raise util.Abort('git %s error %d in %s' %
793 793 (command, p.returncode, self._relpath))
794 794
795 795 return retdata, p.returncode
796 796
797 797 def _gitmissing(self):
798 798 return not os.path.exists(os.path.join(self._abspath, '.git'))
799 799
800 800 def _gitstate(self):
801 801 return self._gitcommand(['rev-parse', 'HEAD'])
802 802
803 803 def _gitcurrentbranch(self):
804 804 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
805 805 if err:
806 806 current = None
807 807 return current
808 808
809 809 def _gitremote(self, remote):
810 810 out = self._gitcommand(['remote', 'show', '-n', remote])
811 811 line = out.split('\n')[1]
812 812 i = line.index('URL: ') + len('URL: ')
813 813 return line[i:]
814 814
815 815 def _githavelocally(self, revision):
816 816 out, code = self._gitdir(['cat-file', '-e', revision])
817 817 return code == 0
818 818
819 819 def _gitisancestor(self, r1, r2):
820 820 base = self._gitcommand(['merge-base', r1, r2])
821 821 return base == r1
822 822
823 823 def _gitisbare(self):
824 824 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
825 825
826 def _gitupdatestat(self):
827 """This must be run before git diff-index.
828 diff-index only looks at changes to file stat;
829 this command looks at file contents and updates the stat."""
830 self._gitcommand(['update-index', '-q', '--refresh'])
831
826 832 def _gitbranchmap(self):
827 833 '''returns 2 things:
828 834 a map from git branch to revision
829 835 a map from revision to branches'''
830 836 branch2rev = {}
831 837 rev2branch = {}
832 838
833 839 out = self._gitcommand(['for-each-ref', '--format',
834 840 '%(objectname) %(refname)'])
835 841 for line in out.split('\n'):
836 842 revision, ref = line.split(' ')
837 843 if (not ref.startswith('refs/heads/') and
838 844 not ref.startswith('refs/remotes/')):
839 845 continue
840 846 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
841 847 continue # ignore remote/HEAD redirects
842 848 branch2rev[ref] = revision
843 849 rev2branch.setdefault(revision, []).append(ref)
844 850 return branch2rev, rev2branch
845 851
846 852 def _gittracking(self, branches):
847 853 'return map of remote branch to local tracking branch'
848 854 # assumes no more than one local tracking branch for each remote
849 855 tracking = {}
850 856 for b in branches:
851 857 if b.startswith('refs/remotes/'):
852 858 continue
853 859 bname = b.split('/', 2)[2]
854 860 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
855 861 if remote:
856 862 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
857 863 tracking['refs/remotes/%s/%s' %
858 864 (remote, ref.split('/', 2)[2])] = b
859 865 return tracking
860 866
861 867 def _abssource(self, source):
862 868 if '://' not in source:
863 869 # recognize the scp syntax as an absolute source
864 870 colon = source.find(':')
865 871 if colon != -1 and '/' not in source[:colon]:
866 872 return source
867 873 self._subsource = source
868 874 return _abssource(self)
869 875
870 876 def _fetch(self, source, revision):
871 877 if self._gitmissing():
872 878 source = self._abssource(source)
873 879 self._ui.status(_('cloning subrepo %s from %s\n') %
874 880 (self._relpath, source))
875 881 self._gitnodir(['clone', source, self._abspath])
876 882 if self._githavelocally(revision):
877 883 return
878 884 self._ui.status(_('pulling subrepo %s from %s\n') %
879 885 (self._relpath, self._gitremote('origin')))
880 886 # try only origin: the originally cloned repo
881 887 self._gitcommand(['fetch'])
882 888 if not self._githavelocally(revision):
883 889 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
884 890 (revision, self._relpath))
885 891
886 892 def dirty(self, ignoreupdate=False):
887 893 if self._gitmissing():
888 894 return self._state[1] != ''
889 895 if self._gitisbare():
890 896 return True
891 897 if not ignoreupdate and self._state[1] != self._gitstate():
892 898 # different version checked out
893 899 return True
894 900 # check for staged changes or modified files; ignore untracked files
901 self._gitupdatestat()
895 902 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
896 903 return code == 1
897 904
898 905 def get(self, state, overwrite=False):
899 906 source, revision, kind = state
900 907 if not revision:
901 908 self.remove()
902 909 return
903 910 self._fetch(source, revision)
904 911 # if the repo was set to be bare, unbare it
905 912 if self._gitisbare():
906 913 self._gitcommand(['config', 'core.bare', 'false'])
907 914 if self._gitstate() == revision:
908 915 self._gitcommand(['reset', '--hard', 'HEAD'])
909 916 return
910 917 elif self._gitstate() == revision:
911 918 if overwrite:
912 919 # first reset the index to unmark new files for commit, because
913 920 # reset --hard will otherwise throw away files added for commit,
914 921 # not just unmark them.
915 922 self._gitcommand(['reset', 'HEAD'])
916 923 self._gitcommand(['reset', '--hard', 'HEAD'])
917 924 return
918 925 branch2rev, rev2branch = self._gitbranchmap()
919 926
920 927 def checkout(args):
921 928 cmd = ['checkout']
922 929 if overwrite:
923 930 # first reset the index to unmark new files for commit, because
924 931 # the -f option will otherwise throw away files added for
925 932 # commit, not just unmark them.
926 933 self._gitcommand(['reset', 'HEAD'])
927 934 cmd.append('-f')
928 935 self._gitcommand(cmd + args)
929 936
930 937 def rawcheckout():
931 938 # no branch to checkout, check it out with no branch
932 939 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
933 940 self._relpath)
934 941 self._ui.warn(_('check out a git branch if you intend '
935 942 'to make changes\n'))
936 943 checkout(['-q', revision])
937 944
938 945 if revision not in rev2branch:
939 946 rawcheckout()
940 947 return
941 948 branches = rev2branch[revision]
942 949 firstlocalbranch = None
943 950 for b in branches:
944 951 if b == 'refs/heads/master':
945 952 # master trumps all other branches
946 953 checkout(['refs/heads/master'])
947 954 return
948 955 if not firstlocalbranch and not b.startswith('refs/remotes/'):
949 956 firstlocalbranch = b
950 957 if firstlocalbranch:
951 958 checkout([firstlocalbranch])
952 959 return
953 960
954 961 tracking = self._gittracking(branch2rev.keys())
955 962 # choose a remote branch already tracked if possible
956 963 remote = branches[0]
957 964 if remote not in tracking:
958 965 for b in branches:
959 966 if b in tracking:
960 967 remote = b
961 968 break
962 969
963 970 if remote not in tracking:
964 971 # create a new local tracking branch
965 972 local = remote.split('/', 2)[2]
966 973 checkout(['-b', local, remote])
967 974 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
968 975 # When updating to a tracked remote branch,
969 976 # if the local tracking branch is downstream of it,
970 977 # a normal `git pull` would have performed a "fast-forward merge"
971 978 # which is equivalent to updating the local branch to the remote.
972 979 # Since we are only looking at branching at update, we need to
973 980 # detect this situation and perform this action lazily.
974 981 if tracking[remote] != self._gitcurrentbranch():
975 982 checkout([tracking[remote]])
976 983 self._gitcommand(['merge', '--ff', remote])
977 984 else:
978 985 # a real merge would be required, just checkout the revision
979 986 rawcheckout()
980 987
981 988 def commit(self, text, user, date):
982 989 if self._gitmissing():
983 990 raise util.Abort(_("subrepo %s is missing") % self._relpath)
984 991 cmd = ['commit', '-a', '-m', text]
985 992 env = os.environ.copy()
986 993 if user:
987 994 cmd += ['--author', user]
988 995 if date:
989 996 # git's date parser silently ignores when seconds < 1e9
990 997 # convert to ISO8601
991 998 env['GIT_AUTHOR_DATE'] = util.datestr(date,
992 999 '%Y-%m-%dT%H:%M:%S %1%2')
993 1000 self._gitcommand(cmd, env=env)
994 1001 # make sure commit works otherwise HEAD might not exist under certain
995 1002 # circumstances
996 1003 return self._gitstate()
997 1004
998 1005 def merge(self, state):
999 1006 source, revision, kind = state
1000 1007 self._fetch(source, revision)
1001 1008 base = self._gitcommand(['merge-base', revision, self._state[1]])
1009 self._gitupdatestat()
1002 1010 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1003 1011
1004 1012 def mergefunc():
1005 1013 if base == revision:
1006 1014 self.get(state) # fast forward merge
1007 1015 elif base != self._state[1]:
1008 1016 self._gitcommand(['merge', '--no-commit', revision])
1009 1017
1010 1018 if self.dirty():
1011 1019 if self._gitstate() != revision:
1012 1020 dirty = self._gitstate() == self._state[1] or code != 0
1013 1021 if _updateprompt(self._ui, self, dirty,
1014 1022 self._state[1][:7], revision[:7]):
1015 1023 mergefunc()
1016 1024 else:
1017 1025 mergefunc()
1018 1026
1019 1027 def push(self, force):
1020 1028 if not self._state[1]:
1021 1029 return True
1022 1030 if self._gitmissing():
1023 1031 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1024 1032 # if a branch in origin contains the revision, nothing to do
1025 1033 branch2rev, rev2branch = self._gitbranchmap()
1026 1034 if self._state[1] in rev2branch:
1027 1035 for b in rev2branch[self._state[1]]:
1028 1036 if b.startswith('refs/remotes/origin/'):
1029 1037 return True
1030 1038 for b, revision in branch2rev.iteritems():
1031 1039 if b.startswith('refs/remotes/origin/'):
1032 1040 if self._gitisancestor(self._state[1], revision):
1033 1041 return True
1034 1042 # otherwise, try to push the currently checked out branch
1035 1043 cmd = ['push']
1036 1044 if force:
1037 1045 cmd.append('--force')
1038 1046
1039 1047 current = self._gitcurrentbranch()
1040 1048 if current:
1041 1049 # determine if the current branch is even useful
1042 1050 if not self._gitisancestor(self._state[1], current):
1043 1051 self._ui.warn(_('unrelated git branch checked out '
1044 1052 'in subrepo %s\n') % self._relpath)
1045 1053 return False
1046 1054 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1047 1055 (current.split('/', 2)[2], self._relpath))
1048 1056 self._gitcommand(cmd + ['origin', current])
1049 1057 return True
1050 1058 else:
1051 1059 self._ui.warn(_('no branch checked out in subrepo %s\n'
1052 1060 'cannot push revision %s') %
1053 1061 (self._relpath, self._state[1]))
1054 1062 return False
1055 1063
1056 1064 def remove(self):
1057 1065 if self._gitmissing():
1058 1066 return
1059 1067 if self.dirty():
1060 1068 self._ui.warn(_('not removing repo %s because '
1061 1069 'it has changes.\n') % self._relpath)
1062 1070 return
1063 1071 # we can't fully delete the repository as it may contain
1064 1072 # local-only history
1065 1073 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1066 1074 self._gitcommand(['config', 'core.bare', 'true'])
1067 1075 for f in os.listdir(self._abspath):
1068 1076 if f == '.git':
1069 1077 continue
1070 1078 path = os.path.join(self._abspath, f)
1071 1079 if os.path.isdir(path) and not os.path.islink(path):
1072 1080 shutil.rmtree(path)
1073 1081 else:
1074 1082 os.remove(path)
1075 1083
1076 1084 def archive(self, ui, archiver, prefix):
1077 1085 source, revision = self._state
1078 1086 if not revision:
1079 1087 return
1080 1088 self._fetch(source, revision)
1081 1089
1082 1090 # Parse git's native archive command.
1083 1091 # This should be much faster than manually traversing the trees
1084 1092 # and objects with many subprocess calls.
1085 1093 tarstream = self._gitcommand(['archive', revision], stream=True)
1086 1094 tar = tarfile.open(fileobj=tarstream, mode='r|')
1087 1095 relpath = subrelpath(self)
1088 1096 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1089 1097 for i, info in enumerate(tar):
1090 1098 if info.isdir():
1091 1099 continue
1092 1100 if info.issym():
1093 1101 data = info.linkname
1094 1102 else:
1095 1103 data = tar.extractfile(info).read()
1096 1104 archiver.addfile(os.path.join(prefix, self._path, info.name),
1097 1105 info.mode, info.issym(), data)
1098 1106 ui.progress(_('archiving (%s)') % relpath, i + 1,
1099 1107 unit=_('files'))
1100 1108 ui.progress(_('archiving (%s)') % relpath, None)
1101 1109
1102 1110
1103 1111 def status(self, rev2, **opts):
1104 1112 rev1 = self._state[1]
1105 1113 if self._gitmissing() or not rev1:
1106 1114 # if the repo is missing, return no results
1107 1115 return [], [], [], [], [], [], []
1108 1116 modified, added, removed = [], [], []
1117 self._gitupdatestat()
1109 1118 if rev2:
1110 1119 command = ['diff-tree', rev1, rev2]
1111 1120 else:
1112 1121 command = ['diff-index', rev1]
1113 1122 out = self._gitcommand(command)
1114 1123 for line in out.split('\n'):
1115 1124 tab = line.find('\t')
1116 1125 if tab == -1:
1117 1126 continue
1118 1127 status, f = line[tab - 1], line[tab + 1:]
1119 1128 if status == 'M':
1120 1129 modified.append(f)
1121 1130 elif status == 'A':
1122 1131 added.append(f)
1123 1132 elif status == 'D':
1124 1133 removed.append(f)
1125 1134
1126 1135 deleted = unknown = ignored = clean = []
1127 1136 return modified, added, removed, deleted, unknown, ignored, clean
1128 1137
1129 1138 types = {
1130 1139 'hg': hgsubrepo,
1131 1140 'svn': svnsubrepo,
1132 1141 'git': gitsubrepo,
1133 1142 }
@@ -1,501 +1,510
1 1 $ "$TESTDIR/hghave" git || exit 80
2 2
3 3 make git commits repeatable
4 4
5 5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11 11
12 12 root hg repo
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 $ cd ..
20 20
21 21 new external git repo
22 22
23 23 $ mkdir gitroot
24 24 $ cd gitroot
25 25 $ git init -q
26 26 $ echo g > g
27 27 $ git add g
28 28 $ git commit -q -m g
29 29
30 30 add subrepo clone
31 31
32 32 $ cd ../t
33 33 $ echo 's = [git]../gitroot' > .hgsub
34 34 $ git clone -q ../gitroot s
35 35 $ hg add .hgsub
36 36 $ hg commit -m 'new git subrepo'
37 37 committing subrepository s
38 38 $ hg debugsub
39 39 path s
40 40 source ../gitroot
41 41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42 42
43 43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 46 $ git checkout -q -b testing
47 47 $ echo gg >> g
48 48 $ git commit -q -a -m gg
49 49
50 50 $ cd ../t/s
51 51 $ git pull -q >/dev/null 2>/dev/null
52 52 $ git checkout -q -b testing origin/testing >/dev/null
53 53
54 54 $ cd ..
55 55 $ hg status --subrepos
56 56 M s/g
57 57 $ hg commit -m 'update git subrepo'
58 58 committing subrepository s
59 59 $ hg debugsub
60 60 path s
61 61 source ../gitroot
62 62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63 63
64 64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
65 65
66 66 $ cd ..
67 67 $ git clone gitroot gitrootbare --bare -q
68 68 $ rm -rf gitroot
69 69 $ mv gitrootbare gitroot
70 70
71 71 clone root
72 72
73 73 $ cd t
74 74 $ hg clone . ../tc
75 75 updating to branch default
76 76 cloning subrepo s from $TESTTMP/gitroot
77 77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 $ cd ../tc
79 79 $ hg debugsub
80 80 path s
81 81 source ../gitroot
82 82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
83 83
84 84 update to previous substate
85 85
86 86 $ hg update 1 -q
87 87 $ cat s/g
88 88 g
89 89 $ hg debugsub
90 90 path s
91 91 source ../gitroot
92 92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
93 93
94 94 clone root, make local change
95 95
96 96 $ cd ../t
97 97 $ hg clone . ../ta
98 98 updating to branch default
99 99 cloning subrepo s from $TESTTMP/gitroot
100 100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 101
102 102 $ cd ../ta
103 103 $ echo ggg >> s/g
104 104 $ hg status --subrepos
105 105 M s/g
106 106 $ hg commit --subrepos -m ggg
107 107 committing subrepository s
108 108 $ hg debugsub
109 109 path s
110 110 source ../gitroot
111 111 revision 79695940086840c99328513acbe35f90fcd55e57
112 112
113 113 clone root separately, make different local change
114 114
115 115 $ cd ../t
116 116 $ hg clone . ../tb
117 117 updating to branch default
118 118 cloning subrepo s from $TESTTMP/gitroot
119 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 120
121 121 $ cd ../tb/s
122 122 $ echo f > f
123 123 $ git add f
124 124 $ cd ..
125 125
126 126 $ hg status --subrepos
127 127 A s/f
128 128 $ hg commit --subrepos -m f
129 129 committing subrepository s
130 130 $ hg debugsub
131 131 path s
132 132 source ../gitroot
133 133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
134 134
135 135 user b push changes
136 136
137 137 $ hg push 2>/dev/null
138 138 pushing to $TESTTMP/t
139 139 pushing branch testing of subrepo s
140 140 searching for changes
141 141 adding changesets
142 142 adding manifests
143 143 adding file changes
144 144 added 1 changesets with 1 changes to 1 files
145 145
146 146 user a pulls, merges, commits
147 147
148 148 $ cd ../ta
149 149 $ hg pull
150 150 pulling from $TESTTMP/t
151 151 searching for changes
152 152 adding changesets
153 153 adding manifests
154 154 adding file changes
155 155 added 1 changesets with 1 changes to 1 files (+1 heads)
156 156 (run 'hg heads' to see heads, 'hg merge' to merge)
157 157 $ hg merge 2>/dev/null
158 158 pulling subrepo s from $TESTTMP/gitroot
159 159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 (branch merge, don't forget to commit)
161 161 $ cat s/f
162 162 f
163 163 $ cat s/g
164 164 g
165 165 gg
166 166 ggg
167 167 $ hg commit --subrepos -m 'merge'
168 168 committing subrepository s
169 169 $ hg status --subrepos --rev 1:5
170 170 M .hgsubstate
171 171 M s/g
172 172 A s/f
173 173 $ hg debugsub
174 174 path s
175 175 source ../gitroot
176 176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
177 177 $ hg push 2>/dev/null
178 178 pushing to $TESTTMP/t
179 179 pushing branch testing of subrepo s
180 180 searching for changes
181 181 adding changesets
182 182 adding manifests
183 183 adding file changes
184 184 added 2 changesets with 2 changes to 1 files
185 185
186 186 make upstream git changes
187 187
188 188 $ cd ..
189 189 $ git clone -q gitroot gitclone
190 190 $ cd gitclone
191 191 $ echo ff >> f
192 192 $ git commit -q -a -m ff
193 193 $ echo fff >> f
194 194 $ git commit -q -a -m fff
195 195 $ git push origin testing 2>/dev/null
196 196
197 197 make and push changes to hg without updating the subrepo
198 198
199 199 $ cd ../t
200 200 $ hg clone . ../td
201 201 updating to branch default
202 202 cloning subrepo s from $TESTTMP/gitroot
203 203 checking out detached HEAD in subrepo s
204 204 check out a git branch if you intend to make changes
205 205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 206 $ cd ../td
207 207 $ echo aa >> a
208 208 $ hg commit -m aa
209 209 $ hg push
210 210 pushing to $TESTTMP/t
211 211 searching for changes
212 212 adding changesets
213 213 adding manifests
214 214 adding file changes
215 215 added 1 changesets with 1 changes to 1 files
216 216
217 217 sync to upstream git, distribute changes
218 218
219 219 $ cd ../ta
220 220 $ hg pull -u -q
221 221 $ cd s
222 222 $ git pull -q >/dev/null 2>/dev/null
223 223 $ cd ..
224 224 $ hg commit -m 'git upstream sync'
225 225 committing subrepository s
226 226 $ hg debugsub
227 227 path s
228 228 source ../gitroot
229 229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
230 230 $ hg push -q
231 231
232 232 $ cd ../tb
233 233 $ hg pull -q
234 234 $ hg update 2>/dev/null
235 235 pulling subrepo s from $TESTTMP/gitroot
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237 $ hg debugsub
238 238 path s
239 239 source ../gitroot
240 240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
241 241
242 242 update to a revision without the subrepo, keeping the local git repository
243 243
244 244 $ cd ../t
245 245 $ hg up 0
246 246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 247 $ ls -a s
248 248 .
249 249 ..
250 250 .git
251 251
252 252 $ hg up 2
253 253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ ls -a s
255 255 .
256 256 ..
257 257 .git
258 258 g
259 259
260 260 archive subrepos
261 261
262 262 $ cd ../tc
263 263 $ hg pull -q
264 264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
265 265 pulling subrepo s from $TESTTMP/gitroot
266 266 $ cd ../archive
267 267 $ cat s/f
268 268 f
269 269 $ cat s/g
270 270 g
271 271 gg
272 272 ggg
273 273
274 274 create nested repo
275 275
276 276 $ cd ..
277 277 $ hg init outer
278 278 $ cd outer
279 279 $ echo b>b
280 280 $ hg add b
281 281 $ hg commit -m b
282 282
283 283 $ hg clone ../t inner
284 284 updating to branch default
285 285 cloning subrepo s from $TESTTMP/gitroot
286 286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ echo inner = inner > .hgsub
288 288 $ hg add .hgsub
289 289 $ hg commit -m 'nested sub'
290 290 committing subrepository inner
291 291
292 292 nested commit
293 293
294 294 $ echo ffff >> inner/s/f
295 295 $ hg status --subrepos
296 296 M inner/s/f
297 297 $ hg commit --subrepos -m nested
298 298 committing subrepository inner
299 299 committing subrepository inner/s
300 300
301 301 nested archive
302 302
303 303 $ hg archive --subrepos ../narchive
304 304 $ ls ../narchive/inner/s | grep -v pax_global_header
305 305 f
306 306 g
307 307
308 308 relative source expansion
309 309
310 310 $ cd ..
311 311 $ mkdir d
312 312 $ hg clone t d/t
313 313 updating to branch default
314 314 cloning subrepo s from $TESTTMP/gitroot
315 315 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
316 316
317 317 Don't crash if the subrepo is missing
318 318
319 319 $ hg clone t missing -q
320 320 $ cd missing
321 321 $ rm -rf s
322 322 $ hg status -S
323 323 $ hg sum | grep commit
324 324 commit: 1 subrepos
325 325 $ hg push -q
326 326 abort: subrepo s is missing
327 327 [255]
328 328 $ hg commit --subrepos -qm missing
329 329 abort: subrepo s is missing
330 330 [255]
331 331 $ hg update -C
332 332 cloning subrepo s from $TESTTMP/gitroot
333 333 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 334 $ hg sum | grep commit
335 335 commit: (clean)
336 336
337 337 Don't crash if the .hgsubstate entry is missing
338 338
339 339 $ hg update 1 -q
340 340 $ hg rm .hgsubstate
341 341 $ hg commit .hgsubstate -m 'no substate'
342 342 created new head
343 343 $ hg tag -l nosubstate
344 344 $ hg manifest
345 345 .hgsub
346 346 a
347 347
348 348 $ hg status -S
349 349 $ hg sum | grep commit
350 350 commit: 1 subrepos
351 351
352 352 $ hg commit -m 'restore substate'
353 353 committing subrepository s
354 354 $ hg manifest
355 355 .hgsub
356 356 .hgsubstate
357 357 a
358 358 $ hg sum | grep commit
359 359 commit: (clean)
360 360
361 361 $ hg update -qC nosubstate
362 362 $ ls s
363 363
364 issue3109: false positives in git diff-index
365
366 $ hg update -q
367 $ touch -t 200001010000 s/g
368 $ hg status --subrepos
369 $ touch -t 200001010000 s/g
370 $ hg sum | grep commit
371 commit: (clean)
372
364 373 Check hg update --clean
365 374 $ cd $TESTTMP/ta
366 375 $ echo > s/g
367 376 $ cd s
368 377 $ echo c1 > f1
369 378 $ echo c1 > f2
370 379 $ git add f1
371 380 $ cd ..
372 381 $ hg status -S
373 382 M s/g
374 383 A s/f1
375 384 $ ls s
376 385 f
377 386 f1
378 387 f2
379 388 g
380 389 $ hg update --clean
381 390 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 391 $ hg status -S
383 392 $ ls s
384 393 f
385 394 f1
386 395 f2
387 396 g
388 397
389 398 Sticky subrepositories, no changes
390 399 $ cd $TESTTMP/ta
391 400 $ hg id -n
392 401 7
393 402 $ cd s
394 403 $ git rev-parse HEAD
395 404 32a343883b74769118bb1d3b4b1fbf9156f4dddc
396 405 $ cd ..
397 406 $ hg update 1 > /dev/null 2>&1
398 407 $ hg id -n
399 408 1
400 409 $ cd s
401 410 $ git rev-parse HEAD
402 411 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
403 412 $ cd ..
404 413
405 414 Sticky subrepositorys, file changes
406 415 $ touch s/f1
407 416 $ cd s
408 417 $ git add f1
409 418 $ cd ..
410 419 $ hg id -n
411 420 1
412 421 $ cd s
413 422 $ git rev-parse HEAD
414 423 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
415 424 $ cd ..
416 425 $ hg update 4
417 426 subrepository sources for s differ
418 427 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
419 428 l
420 429 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
421 430 $ hg id -n
422 431 4+
423 432 $ cd s
424 433 $ git rev-parse HEAD
425 434 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
426 435 $ cd ..
427 436 $ hg update --clean tip > /dev/null 2>&1
428 437
429 438 Sticky subrepository, revision updates
430 439 $ hg id -n
431 440 7
432 441 $ cd s
433 442 $ git rev-parse HEAD
434 443 32a343883b74769118bb1d3b4b1fbf9156f4dddc
435 444 $ cd ..
436 445 $ cd s
437 446 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
438 447 Previous HEAD position was 32a3438... fff
439 448 HEAD is now at aa84837... f
440 449 $ cd ..
441 450 $ hg update 1
442 451 subrepository sources for s differ (in checked out version)
443 452 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
444 453 l
445 454 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
446 455 $ hg id -n
447 456 1+
448 457 $ cd s
449 458 $ git rev-parse HEAD
450 459 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
451 460 $ cd ..
452 461
453 462 Sticky subrepository, file changes and revision updates
454 463 $ touch s/f1
455 464 $ cd s
456 465 $ git add f1
457 466 $ git rev-parse HEAD
458 467 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
459 468 $ cd ..
460 469 $ hg id -n
461 470 1+
462 471 $ hg update 7
463 472 subrepository sources for s differ
464 473 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
465 474 l
466 475 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
467 476 $ hg id -n
468 477 7
469 478 $ cd s
470 479 $ git rev-parse HEAD
471 480 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
472 481 $ cd ..
473 482
474 483 Sticky repository, update --clean
475 484 $ hg update --clean tip
476 485 Previous HEAD position was aa84837... f
477 486 HEAD is now at 32a3438... fff
478 487 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
479 488 $ hg id -n
480 489 7
481 490 $ cd s
482 491 $ git rev-parse HEAD
483 492 32a343883b74769118bb1d3b4b1fbf9156f4dddc
484 493 $ cd ..
485 494
486 495 Test subrepo already at intended revision:
487 496 $ cd s
488 497 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
489 498 HEAD is now at 32a3438... fff
490 499 $ cd ..
491 500 $ hg update 1
492 501 Previous HEAD position was 32a3438... fff
493 502 HEAD is now at da5f5b1... g
494 503 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
495 504 $ hg id -n
496 505 1
497 506 $ cd s
498 507 $ git rev-parse HEAD
499 508 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
500 509 $ cd ..
501 510
General Comments 0
You need to be logged in to leave comments. Login now