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