##// END OF EJS Templates
run-tests: add --inotify option to test runner...
Nicolas Dumazet -
r9958:777c1df7 default
parent child Browse files
Show More
@@ -1,1293 +1,1293 b''
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from node import hex, nullid, nullrev, short
8 from node import hex, nullid, nullrev, short
9 from i18n import _
9 from i18n import _
10 import os, sys, errno, re, glob
10 import os, sys, errno, re, glob
11 import mdiff, bdiff, util, templater, patch, error, encoding
11 import mdiff, bdiff, util, templater, patch, error, encoding
12 import match as _match
12 import match as _match
13
13
14 revrangesep = ':'
14 revrangesep = ':'
15
15
16 def findpossible(cmd, table, strict=False):
16 def findpossible(cmd, table, strict=False):
17 """
17 """
18 Return cmd -> (aliases, command table entry)
18 Return cmd -> (aliases, command table entry)
19 for each matching command.
19 for each matching command.
20 Return debug commands (or their aliases) only if no normal command matches.
20 Return debug commands (or their aliases) only if no normal command matches.
21 """
21 """
22 choice = {}
22 choice = {}
23 debugchoice = {}
23 debugchoice = {}
24 for e in table.keys():
24 for e in table.keys():
25 aliases = e.lstrip("^").split("|")
25 aliases = e.lstrip("^").split("|")
26 found = None
26 found = None
27 if cmd in aliases:
27 if cmd in aliases:
28 found = cmd
28 found = cmd
29 elif not strict:
29 elif not strict:
30 for a in aliases:
30 for a in aliases:
31 if a.startswith(cmd):
31 if a.startswith(cmd):
32 found = a
32 found = a
33 break
33 break
34 if found is not None:
34 if found is not None:
35 if aliases[0].startswith("debug") or found.startswith("debug"):
35 if aliases[0].startswith("debug") or found.startswith("debug"):
36 debugchoice[found] = (aliases, table[e])
36 debugchoice[found] = (aliases, table[e])
37 else:
37 else:
38 choice[found] = (aliases, table[e])
38 choice[found] = (aliases, table[e])
39
39
40 if not choice and debugchoice:
40 if not choice and debugchoice:
41 choice = debugchoice
41 choice = debugchoice
42
42
43 return choice
43 return choice
44
44
45 def findcmd(cmd, table, strict=True):
45 def findcmd(cmd, table, strict=True):
46 """Return (aliases, command table entry) for command string."""
46 """Return (aliases, command table entry) for command string."""
47 choice = findpossible(cmd, table, strict)
47 choice = findpossible(cmd, table, strict)
48
48
49 if cmd in choice:
49 if cmd in choice:
50 return choice[cmd]
50 return choice[cmd]
51
51
52 if len(choice) > 1:
52 if len(choice) > 1:
53 clist = choice.keys()
53 clist = choice.keys()
54 clist.sort()
54 clist.sort()
55 raise error.AmbiguousCommand(cmd, clist)
55 raise error.AmbiguousCommand(cmd, clist)
56
56
57 if choice:
57 if choice:
58 return choice.values()[0]
58 return choice.values()[0]
59
59
60 raise error.UnknownCommand(cmd)
60 raise error.UnknownCommand(cmd)
61
61
62 def bail_if_changed(repo):
62 def bail_if_changed(repo):
63 if repo.dirstate.parents()[1] != nullid:
63 if repo.dirstate.parents()[1] != nullid:
64 raise util.Abort(_('outstanding uncommitted merge'))
64 raise util.Abort(_('outstanding uncommitted merge'))
65 modified, added, removed, deleted = repo.status()[:4]
65 modified, added, removed, deleted = repo.status()[:4]
66 if modified or added or removed or deleted:
66 if modified or added or removed or deleted:
67 raise util.Abort(_("outstanding uncommitted changes"))
67 raise util.Abort(_("outstanding uncommitted changes"))
68
68
69 def logmessage(opts):
69 def logmessage(opts):
70 """ get the log message according to -m and -l option """
70 """ get the log message according to -m and -l option """
71 message = opts.get('message')
71 message = opts.get('message')
72 logfile = opts.get('logfile')
72 logfile = opts.get('logfile')
73
73
74 if message and logfile:
74 if message and logfile:
75 raise util.Abort(_('options --message and --logfile are mutually '
75 raise util.Abort(_('options --message and --logfile are mutually '
76 'exclusive'))
76 'exclusive'))
77 if not message and logfile:
77 if not message and logfile:
78 try:
78 try:
79 if logfile == '-':
79 if logfile == '-':
80 message = sys.stdin.read()
80 message = sys.stdin.read()
81 else:
81 else:
82 message = open(logfile).read()
82 message = open(logfile).read()
83 except IOError, inst:
83 except IOError, inst:
84 raise util.Abort(_("can't read commit message '%s': %s") %
84 raise util.Abort(_("can't read commit message '%s': %s") %
85 (logfile, inst.strerror))
85 (logfile, inst.strerror))
86 return message
86 return message
87
87
88 def loglimit(opts):
88 def loglimit(opts):
89 """get the log limit according to option -l/--limit"""
89 """get the log limit according to option -l/--limit"""
90 limit = opts.get('limit')
90 limit = opts.get('limit')
91 if limit:
91 if limit:
92 try:
92 try:
93 limit = int(limit)
93 limit = int(limit)
94 except ValueError:
94 except ValueError:
95 raise util.Abort(_('limit must be a positive integer'))
95 raise util.Abort(_('limit must be a positive integer'))
96 if limit <= 0: raise util.Abort(_('limit must be positive'))
96 if limit <= 0: raise util.Abort(_('limit must be positive'))
97 else:
97 else:
98 limit = sys.maxint
98 limit = sys.maxint
99 return limit
99 return limit
100
100
101 def remoteui(src, opts):
101 def remoteui(src, opts):
102 'build a remote ui from ui or repo and opts'
102 'build a remote ui from ui or repo and opts'
103 if hasattr(src, 'baseui'): # looks like a repository
103 if hasattr(src, 'baseui'): # looks like a repository
104 dst = src.baseui.copy() # drop repo-specific config
104 dst = src.baseui.copy() # drop repo-specific config
105 src = src.ui # copy target options from repo
105 src = src.ui # copy target options from repo
106 else: # assume it's a global ui object
106 else: # assume it's a global ui object
107 dst = src.copy() # keep all global options
107 dst = src.copy() # keep all global options
108
108
109 # copy ssh-specific options
109 # copy ssh-specific options
110 for o in 'ssh', 'remotecmd':
110 for o in 'ssh', 'remotecmd':
111 v = opts.get(o) or src.config('ui', o)
111 v = opts.get(o) or src.config('ui', o)
112 if v:
112 if v:
113 dst.setconfig("ui", o, v)
113 dst.setconfig("ui", o, v)
114 # copy bundle-specific options
114 # copy bundle-specific options
115 r = src.config('bundle', 'mainreporoot')
115 r = src.config('bundle', 'mainreporoot')
116 if r:
116 if r:
117 dst.setconfig('bundle', 'mainreporoot', r)
117 dst.setconfig('bundle', 'mainreporoot', r)
118
118
119 return dst
119 return dst
120
120
121 def revpair(repo, revs):
121 def revpair(repo, revs):
122 '''return pair of nodes, given list of revisions. second item can
122 '''return pair of nodes, given list of revisions. second item can
123 be None, meaning use working dir.'''
123 be None, meaning use working dir.'''
124
124
125 def revfix(repo, val, defval):
125 def revfix(repo, val, defval):
126 if not val and val != 0 and defval is not None:
126 if not val and val != 0 and defval is not None:
127 val = defval
127 val = defval
128 return repo.lookup(val)
128 return repo.lookup(val)
129
129
130 if not revs:
130 if not revs:
131 return repo.dirstate.parents()[0], None
131 return repo.dirstate.parents()[0], None
132 end = None
132 end = None
133 if len(revs) == 1:
133 if len(revs) == 1:
134 if revrangesep in revs[0]:
134 if revrangesep in revs[0]:
135 start, end = revs[0].split(revrangesep, 1)
135 start, end = revs[0].split(revrangesep, 1)
136 start = revfix(repo, start, 0)
136 start = revfix(repo, start, 0)
137 end = revfix(repo, end, len(repo) - 1)
137 end = revfix(repo, end, len(repo) - 1)
138 else:
138 else:
139 start = revfix(repo, revs[0], None)
139 start = revfix(repo, revs[0], None)
140 elif len(revs) == 2:
140 elif len(revs) == 2:
141 if revrangesep in revs[0] or revrangesep in revs[1]:
141 if revrangesep in revs[0] or revrangesep in revs[1]:
142 raise util.Abort(_('too many revisions specified'))
142 raise util.Abort(_('too many revisions specified'))
143 start = revfix(repo, revs[0], None)
143 start = revfix(repo, revs[0], None)
144 end = revfix(repo, revs[1], None)
144 end = revfix(repo, revs[1], None)
145 else:
145 else:
146 raise util.Abort(_('too many revisions specified'))
146 raise util.Abort(_('too many revisions specified'))
147 return start, end
147 return start, end
148
148
149 def revrange(repo, revs):
149 def revrange(repo, revs):
150 """Yield revision as strings from a list of revision specifications."""
150 """Yield revision as strings from a list of revision specifications."""
151
151
152 def revfix(repo, val, defval):
152 def revfix(repo, val, defval):
153 if not val and val != 0 and defval is not None:
153 if not val and val != 0 and defval is not None:
154 return defval
154 return defval
155 return repo.changelog.rev(repo.lookup(val))
155 return repo.changelog.rev(repo.lookup(val))
156
156
157 seen, l = set(), []
157 seen, l = set(), []
158 for spec in revs:
158 for spec in revs:
159 if revrangesep in spec:
159 if revrangesep in spec:
160 start, end = spec.split(revrangesep, 1)
160 start, end = spec.split(revrangesep, 1)
161 start = revfix(repo, start, 0)
161 start = revfix(repo, start, 0)
162 end = revfix(repo, end, len(repo) - 1)
162 end = revfix(repo, end, len(repo) - 1)
163 step = start > end and -1 or 1
163 step = start > end and -1 or 1
164 for rev in xrange(start, end+step, step):
164 for rev in xrange(start, end+step, step):
165 if rev in seen:
165 if rev in seen:
166 continue
166 continue
167 seen.add(rev)
167 seen.add(rev)
168 l.append(rev)
168 l.append(rev)
169 else:
169 else:
170 rev = revfix(repo, spec, None)
170 rev = revfix(repo, spec, None)
171 if rev in seen:
171 if rev in seen:
172 continue
172 continue
173 seen.add(rev)
173 seen.add(rev)
174 l.append(rev)
174 l.append(rev)
175
175
176 return l
176 return l
177
177
178 def make_filename(repo, pat, node,
178 def make_filename(repo, pat, node,
179 total=None, seqno=None, revwidth=None, pathname=None):
179 total=None, seqno=None, revwidth=None, pathname=None):
180 node_expander = {
180 node_expander = {
181 'H': lambda: hex(node),
181 'H': lambda: hex(node),
182 'R': lambda: str(repo.changelog.rev(node)),
182 'R': lambda: str(repo.changelog.rev(node)),
183 'h': lambda: short(node),
183 'h': lambda: short(node),
184 }
184 }
185 expander = {
185 expander = {
186 '%': lambda: '%',
186 '%': lambda: '%',
187 'b': lambda: os.path.basename(repo.root),
187 'b': lambda: os.path.basename(repo.root),
188 }
188 }
189
189
190 try:
190 try:
191 if node:
191 if node:
192 expander.update(node_expander)
192 expander.update(node_expander)
193 if node:
193 if node:
194 expander['r'] = (lambda:
194 expander['r'] = (lambda:
195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
196 if total is not None:
196 if total is not None:
197 expander['N'] = lambda: str(total)
197 expander['N'] = lambda: str(total)
198 if seqno is not None:
198 if seqno is not None:
199 expander['n'] = lambda: str(seqno)
199 expander['n'] = lambda: str(seqno)
200 if total is not None and seqno is not None:
200 if total is not None and seqno is not None:
201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
202 if pathname is not None:
202 if pathname is not None:
203 expander['s'] = lambda: os.path.basename(pathname)
203 expander['s'] = lambda: os.path.basename(pathname)
204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
205 expander['p'] = lambda: pathname
205 expander['p'] = lambda: pathname
206
206
207 newname = []
207 newname = []
208 patlen = len(pat)
208 patlen = len(pat)
209 i = 0
209 i = 0
210 while i < patlen:
210 while i < patlen:
211 c = pat[i]
211 c = pat[i]
212 if c == '%':
212 if c == '%':
213 i += 1
213 i += 1
214 c = pat[i]
214 c = pat[i]
215 c = expander[c]()
215 c = expander[c]()
216 newname.append(c)
216 newname.append(c)
217 i += 1
217 i += 1
218 return ''.join(newname)
218 return ''.join(newname)
219 except KeyError, inst:
219 except KeyError, inst:
220 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
220 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
221 inst.args[0])
221 inst.args[0])
222
222
223 def make_file(repo, pat, node=None,
223 def make_file(repo, pat, node=None,
224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
225
225
226 writable = 'w' in mode or 'a' in mode
226 writable = 'w' in mode or 'a' in mode
227
227
228 if not pat or pat == '-':
228 if not pat or pat == '-':
229 return writable and sys.stdout or sys.stdin
229 return writable and sys.stdout or sys.stdin
230 if hasattr(pat, 'write') and writable:
230 if hasattr(pat, 'write') and writable:
231 return pat
231 return pat
232 if hasattr(pat, 'read') and 'r' in mode:
232 if hasattr(pat, 'read') and 'r' in mode:
233 return pat
233 return pat
234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
235 pathname),
235 pathname),
236 mode)
236 mode)
237
237
238 def expandpats(pats):
238 def expandpats(pats):
239 if not util.expandglobs:
239 if not util.expandglobs:
240 return list(pats)
240 return list(pats)
241 ret = []
241 ret = []
242 for p in pats:
242 for p in pats:
243 kind, name = _match._patsplit(p, None)
243 kind, name = _match._patsplit(p, None)
244 if kind is None:
244 if kind is None:
245 try:
245 try:
246 globbed = glob.glob(name)
246 globbed = glob.glob(name)
247 except re.error:
247 except re.error:
248 globbed = [name]
248 globbed = [name]
249 if globbed:
249 if globbed:
250 ret.extend(globbed)
250 ret.extend(globbed)
251 continue
251 continue
252 ret.append(p)
252 ret.append(p)
253 return ret
253 return ret
254
254
255 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
255 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
256 if not globbed and default == 'relpath':
256 if not globbed and default == 'relpath':
257 pats = expandpats(pats or [])
257 pats = expandpats(pats or [])
258 m = _match.match(repo.root, repo.getcwd(), pats,
258 m = _match.match(repo.root, repo.getcwd(), pats,
259 opts.get('include'), opts.get('exclude'), default)
259 opts.get('include'), opts.get('exclude'), default)
260 def badfn(f, msg):
260 def badfn(f, msg):
261 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
261 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
262 m.bad = badfn
262 m.bad = badfn
263 return m
263 return m
264
264
265 def matchall(repo):
265 def matchall(repo):
266 return _match.always(repo.root, repo.getcwd())
266 return _match.always(repo.root, repo.getcwd())
267
267
268 def matchfiles(repo, files):
268 def matchfiles(repo, files):
269 return _match.exact(repo.root, repo.getcwd(), files)
269 return _match.exact(repo.root, repo.getcwd(), files)
270
270
271 def findrenames(repo, added, removed, threshold):
271 def findrenames(repo, added, removed, threshold):
272 '''find renamed files -- yields (before, after, score) tuples'''
272 '''find renamed files -- yields (before, after, score) tuples'''
273 copies = {}
273 copies = {}
274 ctx = repo['.']
274 ctx = repo['.']
275 for r in removed:
275 for r in removed:
276 if r not in ctx:
276 if r not in ctx:
277 continue
277 continue
278 fctx = ctx.filectx(r)
278 fctx = ctx.filectx(r)
279
279
280 def score(text):
280 def score(text):
281 if not len(text):
281 if not len(text):
282 return 0.0
282 return 0.0
283 if not fctx.cmp(text):
283 if not fctx.cmp(text):
284 return 1.0
284 return 1.0
285 if threshold == 1.0:
285 if threshold == 1.0:
286 return 0.0
286 return 0.0
287 orig = fctx.data()
287 orig = fctx.data()
288 # bdiff.blocks() returns blocks of matching lines
288 # bdiff.blocks() returns blocks of matching lines
289 # count the number of bytes in each
289 # count the number of bytes in each
290 equal = 0
290 equal = 0
291 alines = mdiff.splitnewlines(text)
291 alines = mdiff.splitnewlines(text)
292 matches = bdiff.blocks(text, orig)
292 matches = bdiff.blocks(text, orig)
293 for x1, x2, y1, y2 in matches:
293 for x1, x2, y1, y2 in matches:
294 for line in alines[x1:x2]:
294 for line in alines[x1:x2]:
295 equal += len(line)
295 equal += len(line)
296
296
297 lengths = len(text) + len(orig)
297 lengths = len(text) + len(orig)
298 return equal * 2.0 / lengths
298 return equal * 2.0 / lengths
299
299
300 for a in added:
300 for a in added:
301 bestscore = copies.get(a, (None, threshold))[1]
301 bestscore = copies.get(a, (None, threshold))[1]
302 myscore = score(repo.wread(a))
302 myscore = score(repo.wread(a))
303 if myscore >= bestscore:
303 if myscore >= bestscore:
304 copies[a] = (r, myscore)
304 copies[a] = (r, myscore)
305
305
306 for dest, v in copies.iteritems():
306 for dest, v in copies.iteritems():
307 source, score = v
307 source, score = v
308 yield source, dest, score
308 yield source, dest, score
309
309
310 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
310 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
311 if dry_run is None:
311 if dry_run is None:
312 dry_run = opts.get('dry_run')
312 dry_run = opts.get('dry_run')
313 if similarity is None:
313 if similarity is None:
314 similarity = float(opts.get('similarity') or 0)
314 similarity = float(opts.get('similarity') or 0)
315 # we'd use status here, except handling of symlinks and ignore is tricky
315 # we'd use status here, except handling of symlinks and ignore is tricky
316 added, unknown, deleted, removed = [], [], [], []
316 added, unknown, deleted, removed = [], [], [], []
317 audit_path = util.path_auditor(repo.root)
317 audit_path = util.path_auditor(repo.root)
318 m = match(repo, pats, opts)
318 m = match(repo, pats, opts)
319 for abs in repo.walk(m):
319 for abs in repo.walk(m):
320 target = repo.wjoin(abs)
320 target = repo.wjoin(abs)
321 good = True
321 good = True
322 try:
322 try:
323 audit_path(abs)
323 audit_path(abs)
324 except:
324 except:
325 good = False
325 good = False
326 rel = m.rel(abs)
326 rel = m.rel(abs)
327 exact = m.exact(abs)
327 exact = m.exact(abs)
328 if good and abs not in repo.dirstate:
328 if good and abs not in repo.dirstate:
329 unknown.append(abs)
329 unknown.append(abs)
330 if repo.ui.verbose or not exact:
330 if repo.ui.verbose or not exact:
331 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
331 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
332 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
332 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
333 or (os.path.isdir(target) and not os.path.islink(target))):
333 or (os.path.isdir(target) and not os.path.islink(target))):
334 deleted.append(abs)
334 deleted.append(abs)
335 if repo.ui.verbose or not exact:
335 if repo.ui.verbose or not exact:
336 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
336 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
337 # for finding renames
337 # for finding renames
338 elif repo.dirstate[abs] == 'r':
338 elif repo.dirstate[abs] == 'r':
339 removed.append(abs)
339 removed.append(abs)
340 elif repo.dirstate[abs] == 'a':
340 elif repo.dirstate[abs] == 'a':
341 added.append(abs)
341 added.append(abs)
342 if not dry_run:
342 if not dry_run:
343 repo.remove(deleted)
343 repo.remove(deleted)
344 repo.add(unknown)
344 repo.add(unknown)
345 if similarity > 0:
345 if similarity > 0:
346 for old, new, score in findrenames(repo, added + unknown,
346 for old, new, score in findrenames(repo, added + unknown,
347 removed + deleted, similarity):
347 removed + deleted, similarity):
348 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
348 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
349 repo.ui.status(_('recording removal of %s as rename to %s '
349 repo.ui.status(_('recording removal of %s as rename to %s '
350 '(%d%% similar)\n') %
350 '(%d%% similar)\n') %
351 (m.rel(old), m.rel(new), score * 100))
351 (m.rel(old), m.rel(new), score * 100))
352 if not dry_run:
352 if not dry_run:
353 repo.copy(old, new)
353 repo.copy(old, new)
354
354
355 def copy(ui, repo, pats, opts, rename=False):
355 def copy(ui, repo, pats, opts, rename=False):
356 # called with the repo lock held
356 # called with the repo lock held
357 #
357 #
358 # hgsep => pathname that uses "/" to separate directories
358 # hgsep => pathname that uses "/" to separate directories
359 # ossep => pathname that uses os.sep to separate directories
359 # ossep => pathname that uses os.sep to separate directories
360 cwd = repo.getcwd()
360 cwd = repo.getcwd()
361 targets = {}
361 targets = {}
362 after = opts.get("after")
362 after = opts.get("after")
363 dryrun = opts.get("dry_run")
363 dryrun = opts.get("dry_run")
364
364
365 def walkpat(pat):
365 def walkpat(pat):
366 srcs = []
366 srcs = []
367 m = match(repo, [pat], opts, globbed=True)
367 m = match(repo, [pat], opts, globbed=True)
368 for abs in repo.walk(m):
368 for abs in repo.walk(m):
369 state = repo.dirstate[abs]
369 state = repo.dirstate[abs]
370 rel = m.rel(abs)
370 rel = m.rel(abs)
371 exact = m.exact(abs)
371 exact = m.exact(abs)
372 if state in '?r':
372 if state in '?r':
373 if exact and state == '?':
373 if exact and state == '?':
374 ui.warn(_('%s: not copying - file is not managed\n') % rel)
374 ui.warn(_('%s: not copying - file is not managed\n') % rel)
375 if exact and state == 'r':
375 if exact and state == 'r':
376 ui.warn(_('%s: not copying - file has been marked for'
376 ui.warn(_('%s: not copying - file has been marked for'
377 ' remove\n') % rel)
377 ' remove\n') % rel)
378 continue
378 continue
379 # abs: hgsep
379 # abs: hgsep
380 # rel: ossep
380 # rel: ossep
381 srcs.append((abs, rel, exact))
381 srcs.append((abs, rel, exact))
382 return srcs
382 return srcs
383
383
384 # abssrc: hgsep
384 # abssrc: hgsep
385 # relsrc: ossep
385 # relsrc: ossep
386 # otarget: ossep
386 # otarget: ossep
387 def copyfile(abssrc, relsrc, otarget, exact):
387 def copyfile(abssrc, relsrc, otarget, exact):
388 abstarget = util.canonpath(repo.root, cwd, otarget)
388 abstarget = util.canonpath(repo.root, cwd, otarget)
389 reltarget = repo.pathto(abstarget, cwd)
389 reltarget = repo.pathto(abstarget, cwd)
390 target = repo.wjoin(abstarget)
390 target = repo.wjoin(abstarget)
391 src = repo.wjoin(abssrc)
391 src = repo.wjoin(abssrc)
392 state = repo.dirstate[abstarget]
392 state = repo.dirstate[abstarget]
393
393
394 # check for collisions
394 # check for collisions
395 prevsrc = targets.get(abstarget)
395 prevsrc = targets.get(abstarget)
396 if prevsrc is not None:
396 if prevsrc is not None:
397 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
397 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
398 (reltarget, repo.pathto(abssrc, cwd),
398 (reltarget, repo.pathto(abssrc, cwd),
399 repo.pathto(prevsrc, cwd)))
399 repo.pathto(prevsrc, cwd)))
400 return
400 return
401
401
402 # check for overwrites
402 # check for overwrites
403 exists = os.path.exists(target)
403 exists = os.path.exists(target)
404 if not after and exists or after and state in 'mn':
404 if not after and exists or after and state in 'mn':
405 if not opts['force']:
405 if not opts['force']:
406 ui.warn(_('%s: not overwriting - file exists\n') %
406 ui.warn(_('%s: not overwriting - file exists\n') %
407 reltarget)
407 reltarget)
408 return
408 return
409
409
410 if after:
410 if after:
411 if not exists:
411 if not exists:
412 return
412 return
413 elif not dryrun:
413 elif not dryrun:
414 try:
414 try:
415 if exists:
415 if exists:
416 os.unlink(target)
416 os.unlink(target)
417 targetdir = os.path.dirname(target) or '.'
417 targetdir = os.path.dirname(target) or '.'
418 if not os.path.isdir(targetdir):
418 if not os.path.isdir(targetdir):
419 os.makedirs(targetdir)
419 os.makedirs(targetdir)
420 util.copyfile(src, target)
420 util.copyfile(src, target)
421 except IOError, inst:
421 except IOError, inst:
422 if inst.errno == errno.ENOENT:
422 if inst.errno == errno.ENOENT:
423 ui.warn(_('%s: deleted in working copy\n') % relsrc)
423 ui.warn(_('%s: deleted in working copy\n') % relsrc)
424 else:
424 else:
425 ui.warn(_('%s: cannot copy - %s\n') %
425 ui.warn(_('%s: cannot copy - %s\n') %
426 (relsrc, inst.strerror))
426 (relsrc, inst.strerror))
427 return True # report a failure
427 return True # report a failure
428
428
429 if ui.verbose or not exact:
429 if ui.verbose or not exact:
430 if rename:
430 if rename:
431 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
431 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
432 else:
432 else:
433 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
433 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
434
434
435 targets[abstarget] = abssrc
435 targets[abstarget] = abssrc
436
436
437 # fix up dirstate
437 # fix up dirstate
438 origsrc = repo.dirstate.copied(abssrc) or abssrc
438 origsrc = repo.dirstate.copied(abssrc) or abssrc
439 if abstarget == origsrc: # copying back a copy?
439 if abstarget == origsrc: # copying back a copy?
440 if state not in 'mn' and not dryrun:
440 if state not in 'mn' and not dryrun:
441 repo.dirstate.normallookup(abstarget)
441 repo.dirstate.normallookup(abstarget)
442 else:
442 else:
443 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
443 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
444 if not ui.quiet:
444 if not ui.quiet:
445 ui.warn(_("%s has not been committed yet, so no copy "
445 ui.warn(_("%s has not been committed yet, so no copy "
446 "data will be stored for %s.\n")
446 "data will be stored for %s.\n")
447 % (repo.pathto(origsrc, cwd), reltarget))
447 % (repo.pathto(origsrc, cwd), reltarget))
448 if repo.dirstate[abstarget] in '?r' and not dryrun:
448 if repo.dirstate[abstarget] in '?r' and not dryrun:
449 repo.add([abstarget])
449 repo.add([abstarget])
450 elif not dryrun:
450 elif not dryrun:
451 repo.copy(origsrc, abstarget)
451 repo.copy(origsrc, abstarget)
452
452
453 if rename and not dryrun:
453 if rename and not dryrun:
454 repo.remove([abssrc], not after)
454 repo.remove([abssrc], not after)
455
455
456 # pat: ossep
456 # pat: ossep
457 # dest ossep
457 # dest ossep
458 # srcs: list of (hgsep, hgsep, ossep, bool)
458 # srcs: list of (hgsep, hgsep, ossep, bool)
459 # return: function that takes hgsep and returns ossep
459 # return: function that takes hgsep and returns ossep
460 def targetpathfn(pat, dest, srcs):
460 def targetpathfn(pat, dest, srcs):
461 if os.path.isdir(pat):
461 if os.path.isdir(pat):
462 abspfx = util.canonpath(repo.root, cwd, pat)
462 abspfx = util.canonpath(repo.root, cwd, pat)
463 abspfx = util.localpath(abspfx)
463 abspfx = util.localpath(abspfx)
464 if destdirexists:
464 if destdirexists:
465 striplen = len(os.path.split(abspfx)[0])
465 striplen = len(os.path.split(abspfx)[0])
466 else:
466 else:
467 striplen = len(abspfx)
467 striplen = len(abspfx)
468 if striplen:
468 if striplen:
469 striplen += len(os.sep)
469 striplen += len(os.sep)
470 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
470 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
471 elif destdirexists:
471 elif destdirexists:
472 res = lambda p: os.path.join(dest,
472 res = lambda p: os.path.join(dest,
473 os.path.basename(util.localpath(p)))
473 os.path.basename(util.localpath(p)))
474 else:
474 else:
475 res = lambda p: dest
475 res = lambda p: dest
476 return res
476 return res
477
477
478 # pat: ossep
478 # pat: ossep
479 # dest ossep
479 # dest ossep
480 # srcs: list of (hgsep, hgsep, ossep, bool)
480 # srcs: list of (hgsep, hgsep, ossep, bool)
481 # return: function that takes hgsep and returns ossep
481 # return: function that takes hgsep and returns ossep
482 def targetpathafterfn(pat, dest, srcs):
482 def targetpathafterfn(pat, dest, srcs):
483 if _match.patkind(pat):
483 if _match.patkind(pat):
484 # a mercurial pattern
484 # a mercurial pattern
485 res = lambda p: os.path.join(dest,
485 res = lambda p: os.path.join(dest,
486 os.path.basename(util.localpath(p)))
486 os.path.basename(util.localpath(p)))
487 else:
487 else:
488 abspfx = util.canonpath(repo.root, cwd, pat)
488 abspfx = util.canonpath(repo.root, cwd, pat)
489 if len(abspfx) < len(srcs[0][0]):
489 if len(abspfx) < len(srcs[0][0]):
490 # A directory. Either the target path contains the last
490 # A directory. Either the target path contains the last
491 # component of the source path or it does not.
491 # component of the source path or it does not.
492 def evalpath(striplen):
492 def evalpath(striplen):
493 score = 0
493 score = 0
494 for s in srcs:
494 for s in srcs:
495 t = os.path.join(dest, util.localpath(s[0])[striplen:])
495 t = os.path.join(dest, util.localpath(s[0])[striplen:])
496 if os.path.exists(t):
496 if os.path.exists(t):
497 score += 1
497 score += 1
498 return score
498 return score
499
499
500 abspfx = util.localpath(abspfx)
500 abspfx = util.localpath(abspfx)
501 striplen = len(abspfx)
501 striplen = len(abspfx)
502 if striplen:
502 if striplen:
503 striplen += len(os.sep)
503 striplen += len(os.sep)
504 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
504 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
505 score = evalpath(striplen)
505 score = evalpath(striplen)
506 striplen1 = len(os.path.split(abspfx)[0])
506 striplen1 = len(os.path.split(abspfx)[0])
507 if striplen1:
507 if striplen1:
508 striplen1 += len(os.sep)
508 striplen1 += len(os.sep)
509 if evalpath(striplen1) > score:
509 if evalpath(striplen1) > score:
510 striplen = striplen1
510 striplen = striplen1
511 res = lambda p: os.path.join(dest,
511 res = lambda p: os.path.join(dest,
512 util.localpath(p)[striplen:])
512 util.localpath(p)[striplen:])
513 else:
513 else:
514 # a file
514 # a file
515 if destdirexists:
515 if destdirexists:
516 res = lambda p: os.path.join(dest,
516 res = lambda p: os.path.join(dest,
517 os.path.basename(util.localpath(p)))
517 os.path.basename(util.localpath(p)))
518 else:
518 else:
519 res = lambda p: dest
519 res = lambda p: dest
520 return res
520 return res
521
521
522
522
523 pats = expandpats(pats)
523 pats = expandpats(pats)
524 if not pats:
524 if not pats:
525 raise util.Abort(_('no source or destination specified'))
525 raise util.Abort(_('no source or destination specified'))
526 if len(pats) == 1:
526 if len(pats) == 1:
527 raise util.Abort(_('no destination specified'))
527 raise util.Abort(_('no destination specified'))
528 dest = pats.pop()
528 dest = pats.pop()
529 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
529 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
530 if not destdirexists:
530 if not destdirexists:
531 if len(pats) > 1 or _match.patkind(pats[0]):
531 if len(pats) > 1 or _match.patkind(pats[0]):
532 raise util.Abort(_('with multiple sources, destination must be an '
532 raise util.Abort(_('with multiple sources, destination must be an '
533 'existing directory'))
533 'existing directory'))
534 if util.endswithsep(dest):
534 if util.endswithsep(dest):
535 raise util.Abort(_('destination %s is not a directory') % dest)
535 raise util.Abort(_('destination %s is not a directory') % dest)
536
536
537 tfn = targetpathfn
537 tfn = targetpathfn
538 if after:
538 if after:
539 tfn = targetpathafterfn
539 tfn = targetpathafterfn
540 copylist = []
540 copylist = []
541 for pat in pats:
541 for pat in pats:
542 srcs = walkpat(pat)
542 srcs = walkpat(pat)
543 if not srcs:
543 if not srcs:
544 continue
544 continue
545 copylist.append((tfn(pat, dest, srcs), srcs))
545 copylist.append((tfn(pat, dest, srcs), srcs))
546 if not copylist:
546 if not copylist:
547 raise util.Abort(_('no files to copy'))
547 raise util.Abort(_('no files to copy'))
548
548
549 errors = 0
549 errors = 0
550 for targetpath, srcs in copylist:
550 for targetpath, srcs in copylist:
551 for abssrc, relsrc, exact in srcs:
551 for abssrc, relsrc, exact in srcs:
552 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
552 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
553 errors += 1
553 errors += 1
554
554
555 if errors:
555 if errors:
556 ui.warn(_('(consider using --after)\n'))
556 ui.warn(_('(consider using --after)\n'))
557
557
558 return errors
558 return errors
559
559
560 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
560 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
561 runargs=None):
561 runargs=None):
562 '''Run a command as a service.'''
562 '''Run a command as a service.'''
563
563
564 if opts['daemon'] and not opts['daemon_pipefds']:
564 if opts['daemon'] and not opts['daemon_pipefds']:
565 rfd, wfd = os.pipe()
565 rfd, wfd = os.pipe()
566 if not runargs:
566 if not runargs:
567 runargs = sys.argv[:]
567 runargs = sys.argv[:]
568 runargs.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
568 runargs.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
569 # Don't pass --cwd to the child process, because we've already
569 # Don't pass --cwd to the child process, because we've already
570 # changed directory.
570 # changed directory.
571 for i in xrange(1,len(runargs)):
571 for i in xrange(1,len(runargs)):
572 if runargs[i].startswith('--cwd='):
572 if runargs[i].startswith('--cwd='):
573 del runargs[i]
573 del runargs[i]
574 break
574 break
575 elif runargs[i].startswith('--cwd'):
575 elif runargs[i].startswith('--cwd'):
576 del runargs[i:i+2]
576 del runargs[i:i+2]
577 break
577 break
578 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
578 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
579 runargs[0], runargs)
579 runargs[0], runargs)
580 os.close(wfd)
580 os.close(wfd)
581 os.read(rfd, 1)
581 os.read(rfd, 1)
582 if parentfn:
582 if parentfn:
583 return parentfn(pid)
583 return parentfn(pid)
584 else:
584 else:
585 return
585 return
586
586
587 if initfn:
587 if initfn:
588 initfn()
588 initfn()
589
589
590 if opts['pid_file']:
590 if opts['pid_file']:
591 fp = open(opts['pid_file'], 'w')
591 fp = open(opts['pid_file'], 'a')
592 fp.write(str(os.getpid()) + '\n')
592 fp.write(str(os.getpid()) + '\n')
593 fp.close()
593 fp.close()
594
594
595 if opts['daemon_pipefds']:
595 if opts['daemon_pipefds']:
596 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
596 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
597 os.close(rfd)
597 os.close(rfd)
598 try:
598 try:
599 os.setsid()
599 os.setsid()
600 except AttributeError:
600 except AttributeError:
601 pass
601 pass
602 os.write(wfd, 'y')
602 os.write(wfd, 'y')
603 os.close(wfd)
603 os.close(wfd)
604 sys.stdout.flush()
604 sys.stdout.flush()
605 sys.stderr.flush()
605 sys.stderr.flush()
606
606
607 nullfd = os.open(util.nulldev, os.O_RDWR)
607 nullfd = os.open(util.nulldev, os.O_RDWR)
608 logfilefd = nullfd
608 logfilefd = nullfd
609 if logfile:
609 if logfile:
610 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
610 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
611 os.dup2(nullfd, 0)
611 os.dup2(nullfd, 0)
612 os.dup2(logfilefd, 1)
612 os.dup2(logfilefd, 1)
613 os.dup2(logfilefd, 2)
613 os.dup2(logfilefd, 2)
614 if nullfd not in (0, 1, 2):
614 if nullfd not in (0, 1, 2):
615 os.close(nullfd)
615 os.close(nullfd)
616 if logfile and logfilefd not in (0, 1, 2):
616 if logfile and logfilefd not in (0, 1, 2):
617 os.close(logfilefd)
617 os.close(logfilefd)
618
618
619 if runfn:
619 if runfn:
620 return runfn()
620 return runfn()
621
621
622 class changeset_printer(object):
622 class changeset_printer(object):
623 '''show changeset information when templating not requested.'''
623 '''show changeset information when templating not requested.'''
624
624
625 def __init__(self, ui, repo, patch, diffopts, buffered):
625 def __init__(self, ui, repo, patch, diffopts, buffered):
626 self.ui = ui
626 self.ui = ui
627 self.repo = repo
627 self.repo = repo
628 self.buffered = buffered
628 self.buffered = buffered
629 self.patch = patch
629 self.patch = patch
630 self.diffopts = diffopts
630 self.diffopts = diffopts
631 self.header = {}
631 self.header = {}
632 self.hunk = {}
632 self.hunk = {}
633 self.lastheader = None
633 self.lastheader = None
634
634
635 def flush(self, rev):
635 def flush(self, rev):
636 if rev in self.header:
636 if rev in self.header:
637 h = self.header[rev]
637 h = self.header[rev]
638 if h != self.lastheader:
638 if h != self.lastheader:
639 self.lastheader = h
639 self.lastheader = h
640 self.ui.write(h)
640 self.ui.write(h)
641 del self.header[rev]
641 del self.header[rev]
642 if rev in self.hunk:
642 if rev in self.hunk:
643 self.ui.write(self.hunk[rev])
643 self.ui.write(self.hunk[rev])
644 del self.hunk[rev]
644 del self.hunk[rev]
645 return 1
645 return 1
646 return 0
646 return 0
647
647
648 def show(self, ctx, copies=(), **props):
648 def show(self, ctx, copies=(), **props):
649 if self.buffered:
649 if self.buffered:
650 self.ui.pushbuffer()
650 self.ui.pushbuffer()
651 self._show(ctx, copies, props)
651 self._show(ctx, copies, props)
652 self.hunk[ctx.rev()] = self.ui.popbuffer()
652 self.hunk[ctx.rev()] = self.ui.popbuffer()
653 else:
653 else:
654 self._show(ctx, copies, props)
654 self._show(ctx, copies, props)
655
655
656 def _show(self, ctx, copies, props):
656 def _show(self, ctx, copies, props):
657 '''show a single changeset or file revision'''
657 '''show a single changeset or file revision'''
658 changenode = ctx.node()
658 changenode = ctx.node()
659 rev = ctx.rev()
659 rev = ctx.rev()
660
660
661 if self.ui.quiet:
661 if self.ui.quiet:
662 self.ui.write("%d:%s\n" % (rev, short(changenode)))
662 self.ui.write("%d:%s\n" % (rev, short(changenode)))
663 return
663 return
664
664
665 log = self.repo.changelog
665 log = self.repo.changelog
666 date = util.datestr(ctx.date())
666 date = util.datestr(ctx.date())
667
667
668 hexfunc = self.ui.debugflag and hex or short
668 hexfunc = self.ui.debugflag and hex or short
669
669
670 parents = [(p, hexfunc(log.node(p)))
670 parents = [(p, hexfunc(log.node(p)))
671 for p in self._meaningful_parentrevs(log, rev)]
671 for p in self._meaningful_parentrevs(log, rev)]
672
672
673 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
673 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
674
674
675 branch = ctx.branch()
675 branch = ctx.branch()
676 # don't show the default branch name
676 # don't show the default branch name
677 if branch != 'default':
677 if branch != 'default':
678 branch = encoding.tolocal(branch)
678 branch = encoding.tolocal(branch)
679 self.ui.write(_("branch: %s\n") % branch)
679 self.ui.write(_("branch: %s\n") % branch)
680 for tag in self.repo.nodetags(changenode):
680 for tag in self.repo.nodetags(changenode):
681 self.ui.write(_("tag: %s\n") % tag)
681 self.ui.write(_("tag: %s\n") % tag)
682 for parent in parents:
682 for parent in parents:
683 self.ui.write(_("parent: %d:%s\n") % parent)
683 self.ui.write(_("parent: %d:%s\n") % parent)
684
684
685 if self.ui.debugflag:
685 if self.ui.debugflag:
686 mnode = ctx.manifestnode()
686 mnode = ctx.manifestnode()
687 self.ui.write(_("manifest: %d:%s\n") %
687 self.ui.write(_("manifest: %d:%s\n") %
688 (self.repo.manifest.rev(mnode), hex(mnode)))
688 (self.repo.manifest.rev(mnode), hex(mnode)))
689 self.ui.write(_("user: %s\n") % ctx.user())
689 self.ui.write(_("user: %s\n") % ctx.user())
690 self.ui.write(_("date: %s\n") % date)
690 self.ui.write(_("date: %s\n") % date)
691
691
692 if self.ui.debugflag:
692 if self.ui.debugflag:
693 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
693 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
694 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
694 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
695 files):
695 files):
696 if value:
696 if value:
697 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
697 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
698 elif ctx.files() and self.ui.verbose:
698 elif ctx.files() and self.ui.verbose:
699 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
699 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
700 if copies and self.ui.verbose:
700 if copies and self.ui.verbose:
701 copies = ['%s (%s)' % c for c in copies]
701 copies = ['%s (%s)' % c for c in copies]
702 self.ui.write(_("copies: %s\n") % ' '.join(copies))
702 self.ui.write(_("copies: %s\n") % ' '.join(copies))
703
703
704 extra = ctx.extra()
704 extra = ctx.extra()
705 if extra and self.ui.debugflag:
705 if extra and self.ui.debugflag:
706 for key, value in sorted(extra.items()):
706 for key, value in sorted(extra.items()):
707 self.ui.write(_("extra: %s=%s\n")
707 self.ui.write(_("extra: %s=%s\n")
708 % (key, value.encode('string_escape')))
708 % (key, value.encode('string_escape')))
709
709
710 description = ctx.description().strip()
710 description = ctx.description().strip()
711 if description:
711 if description:
712 if self.ui.verbose:
712 if self.ui.verbose:
713 self.ui.write(_("description:\n"))
713 self.ui.write(_("description:\n"))
714 self.ui.write(description)
714 self.ui.write(description)
715 self.ui.write("\n\n")
715 self.ui.write("\n\n")
716 else:
716 else:
717 self.ui.write(_("summary: %s\n") %
717 self.ui.write(_("summary: %s\n") %
718 description.splitlines()[0])
718 description.splitlines()[0])
719 self.ui.write("\n")
719 self.ui.write("\n")
720
720
721 self.showpatch(changenode)
721 self.showpatch(changenode)
722
722
723 def showpatch(self, node):
723 def showpatch(self, node):
724 if self.patch:
724 if self.patch:
725 prev = self.repo.changelog.parents(node)[0]
725 prev = self.repo.changelog.parents(node)[0]
726 chunks = patch.diff(self.repo, prev, node, match=self.patch,
726 chunks = patch.diff(self.repo, prev, node, match=self.patch,
727 opts=patch.diffopts(self.ui, self.diffopts))
727 opts=patch.diffopts(self.ui, self.diffopts))
728 for chunk in chunks:
728 for chunk in chunks:
729 self.ui.write(chunk)
729 self.ui.write(chunk)
730 self.ui.write("\n")
730 self.ui.write("\n")
731
731
732 def _meaningful_parentrevs(self, log, rev):
732 def _meaningful_parentrevs(self, log, rev):
733 """Return list of meaningful (or all if debug) parentrevs for rev.
733 """Return list of meaningful (or all if debug) parentrevs for rev.
734
734
735 For merges (two non-nullrev revisions) both parents are meaningful.
735 For merges (two non-nullrev revisions) both parents are meaningful.
736 Otherwise the first parent revision is considered meaningful if it
736 Otherwise the first parent revision is considered meaningful if it
737 is not the preceding revision.
737 is not the preceding revision.
738 """
738 """
739 parents = log.parentrevs(rev)
739 parents = log.parentrevs(rev)
740 if not self.ui.debugflag and parents[1] == nullrev:
740 if not self.ui.debugflag and parents[1] == nullrev:
741 if parents[0] >= rev - 1:
741 if parents[0] >= rev - 1:
742 parents = []
742 parents = []
743 else:
743 else:
744 parents = [parents[0]]
744 parents = [parents[0]]
745 return parents
745 return parents
746
746
747
747
748 class changeset_templater(changeset_printer):
748 class changeset_templater(changeset_printer):
749 '''format changeset information.'''
749 '''format changeset information.'''
750
750
751 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
751 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
752 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
752 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
753 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
753 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
754 self.t = templater.templater(mapfile, {'formatnode': formatnode},
754 self.t = templater.templater(mapfile, {'formatnode': formatnode},
755 cache={
755 cache={
756 'parent': '{rev}:{node|formatnode} ',
756 'parent': '{rev}:{node|formatnode} ',
757 'manifest': '{rev}:{node|formatnode}',
757 'manifest': '{rev}:{node|formatnode}',
758 'filecopy': '{name} ({source})'})
758 'filecopy': '{name} ({source})'})
759 # Cache mapping from rev to a tuple with tag date, tag
759 # Cache mapping from rev to a tuple with tag date, tag
760 # distance and tag name
760 # distance and tag name
761 self._latesttagcache = {-1: (0, 0, 'null')}
761 self._latesttagcache = {-1: (0, 0, 'null')}
762
762
763 def use_template(self, t):
763 def use_template(self, t):
764 '''set template string to use'''
764 '''set template string to use'''
765 self.t.cache['changeset'] = t
765 self.t.cache['changeset'] = t
766
766
767 def _meaningful_parentrevs(self, ctx):
767 def _meaningful_parentrevs(self, ctx):
768 """Return list of meaningful (or all if debug) parentrevs for rev.
768 """Return list of meaningful (or all if debug) parentrevs for rev.
769 """
769 """
770 parents = ctx.parents()
770 parents = ctx.parents()
771 if len(parents) > 1:
771 if len(parents) > 1:
772 return parents
772 return parents
773 if self.ui.debugflag:
773 if self.ui.debugflag:
774 return [parents[0], self.repo['null']]
774 return [parents[0], self.repo['null']]
775 if parents[0].rev() >= ctx.rev() - 1:
775 if parents[0].rev() >= ctx.rev() - 1:
776 return []
776 return []
777 return parents
777 return parents
778
778
779 def _latesttaginfo(self, rev):
779 def _latesttaginfo(self, rev):
780 '''return date, distance and name for the latest tag of rev'''
780 '''return date, distance and name for the latest tag of rev'''
781 todo = [rev]
781 todo = [rev]
782 while todo:
782 while todo:
783 rev = todo.pop()
783 rev = todo.pop()
784 if rev in self._latesttagcache:
784 if rev in self._latesttagcache:
785 continue
785 continue
786 ctx = self.repo[rev]
786 ctx = self.repo[rev]
787 tags = [t for t in ctx.tags() if self.repo.tagtype(t) == 'global']
787 tags = [t for t in ctx.tags() if self.repo.tagtype(t) == 'global']
788 if tags:
788 if tags:
789 self._latesttagcache[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
789 self._latesttagcache[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
790 continue
790 continue
791 try:
791 try:
792 # The tuples are laid out so the right one can be found by comparison.
792 # The tuples are laid out so the right one can be found by comparison.
793 pdate, pdist, ptag = max(
793 pdate, pdist, ptag = max(
794 self._latesttagcache[p.rev()] for p in ctx.parents())
794 self._latesttagcache[p.rev()] for p in ctx.parents())
795 except KeyError:
795 except KeyError:
796 # Cache miss - recurse
796 # Cache miss - recurse
797 todo.append(rev)
797 todo.append(rev)
798 todo.extend(p.rev() for p in ctx.parents())
798 todo.extend(p.rev() for p in ctx.parents())
799 continue
799 continue
800 self._latesttagcache[rev] = pdate, pdist + 1, ptag
800 self._latesttagcache[rev] = pdate, pdist + 1, ptag
801 return self._latesttagcache[rev]
801 return self._latesttagcache[rev]
802
802
803 def _show(self, ctx, copies, props):
803 def _show(self, ctx, copies, props):
804 '''show a single changeset or file revision'''
804 '''show a single changeset or file revision'''
805
805
806 def showlist(name, values, plural=None, **args):
806 def showlist(name, values, plural=None, **args):
807 '''expand set of values.
807 '''expand set of values.
808 name is name of key in template map.
808 name is name of key in template map.
809 values is list of strings or dicts.
809 values is list of strings or dicts.
810 plural is plural of name, if not simply name + 's'.
810 plural is plural of name, if not simply name + 's'.
811
811
812 expansion works like this, given name 'foo'.
812 expansion works like this, given name 'foo'.
813
813
814 if values is empty, expand 'no_foos'.
814 if values is empty, expand 'no_foos'.
815
815
816 if 'foo' not in template map, return values as a string,
816 if 'foo' not in template map, return values as a string,
817 joined by space.
817 joined by space.
818
818
819 expand 'start_foos'.
819 expand 'start_foos'.
820
820
821 for each value, expand 'foo'. if 'last_foo' in template
821 for each value, expand 'foo'. if 'last_foo' in template
822 map, expand it instead of 'foo' for last key.
822 map, expand it instead of 'foo' for last key.
823
823
824 expand 'end_foos'.
824 expand 'end_foos'.
825 '''
825 '''
826 if plural: names = plural
826 if plural: names = plural
827 else: names = name + 's'
827 else: names = name + 's'
828 if not values:
828 if not values:
829 noname = 'no_' + names
829 noname = 'no_' + names
830 if noname in self.t:
830 if noname in self.t:
831 yield self.t(noname, **args)
831 yield self.t(noname, **args)
832 return
832 return
833 if name not in self.t:
833 if name not in self.t:
834 if isinstance(values[0], str):
834 if isinstance(values[0], str):
835 yield ' '.join(values)
835 yield ' '.join(values)
836 else:
836 else:
837 for v in values:
837 for v in values:
838 yield dict(v, **args)
838 yield dict(v, **args)
839 return
839 return
840 startname = 'start_' + names
840 startname = 'start_' + names
841 if startname in self.t:
841 if startname in self.t:
842 yield self.t(startname, **args)
842 yield self.t(startname, **args)
843 vargs = args.copy()
843 vargs = args.copy()
844 def one(v, tag=name):
844 def one(v, tag=name):
845 try:
845 try:
846 vargs.update(v)
846 vargs.update(v)
847 except (AttributeError, ValueError):
847 except (AttributeError, ValueError):
848 try:
848 try:
849 for a, b in v:
849 for a, b in v:
850 vargs[a] = b
850 vargs[a] = b
851 except ValueError:
851 except ValueError:
852 vargs[name] = v
852 vargs[name] = v
853 return self.t(tag, **vargs)
853 return self.t(tag, **vargs)
854 lastname = 'last_' + name
854 lastname = 'last_' + name
855 if lastname in self.t:
855 if lastname in self.t:
856 last = values.pop()
856 last = values.pop()
857 else:
857 else:
858 last = None
858 last = None
859 for v in values:
859 for v in values:
860 yield one(v)
860 yield one(v)
861 if last is not None:
861 if last is not None:
862 yield one(last, tag=lastname)
862 yield one(last, tag=lastname)
863 endname = 'end_' + names
863 endname = 'end_' + names
864 if endname in self.t:
864 if endname in self.t:
865 yield self.t(endname, **args)
865 yield self.t(endname, **args)
866
866
867 def showbranches(**args):
867 def showbranches(**args):
868 branch = ctx.branch()
868 branch = ctx.branch()
869 if branch != 'default':
869 if branch != 'default':
870 branch = encoding.tolocal(branch)
870 branch = encoding.tolocal(branch)
871 return showlist('branch', [branch], plural='branches', **args)
871 return showlist('branch', [branch], plural='branches', **args)
872
872
873 def showparents(**args):
873 def showparents(**args):
874 parents = [[('rev', p.rev()), ('node', p.hex())]
874 parents = [[('rev', p.rev()), ('node', p.hex())]
875 for p in self._meaningful_parentrevs(ctx)]
875 for p in self._meaningful_parentrevs(ctx)]
876 return showlist('parent', parents, **args)
876 return showlist('parent', parents, **args)
877
877
878 def showtags(**args):
878 def showtags(**args):
879 return showlist('tag', ctx.tags(), **args)
879 return showlist('tag', ctx.tags(), **args)
880
880
881 def showextras(**args):
881 def showextras(**args):
882 for key, value in sorted(ctx.extra().items()):
882 for key, value in sorted(ctx.extra().items()):
883 args = args.copy()
883 args = args.copy()
884 args.update(dict(key=key, value=value))
884 args.update(dict(key=key, value=value))
885 yield self.t('extra', **args)
885 yield self.t('extra', **args)
886
886
887 def showcopies(**args):
887 def showcopies(**args):
888 c = [{'name': x[0], 'source': x[1]} for x in copies]
888 c = [{'name': x[0], 'source': x[1]} for x in copies]
889 return showlist('file_copy', c, plural='file_copies', **args)
889 return showlist('file_copy', c, plural='file_copies', **args)
890
890
891 files = []
891 files = []
892 def getfiles():
892 def getfiles():
893 if not files:
893 if not files:
894 files[:] = self.repo.status(ctx.parents()[0].node(),
894 files[:] = self.repo.status(ctx.parents()[0].node(),
895 ctx.node())[:3]
895 ctx.node())[:3]
896 return files
896 return files
897 def showfiles(**args):
897 def showfiles(**args):
898 return showlist('file', ctx.files(), **args)
898 return showlist('file', ctx.files(), **args)
899 def showmods(**args):
899 def showmods(**args):
900 return showlist('file_mod', getfiles()[0], **args)
900 return showlist('file_mod', getfiles()[0], **args)
901 def showadds(**args):
901 def showadds(**args):
902 return showlist('file_add', getfiles()[1], **args)
902 return showlist('file_add', getfiles()[1], **args)
903 def showdels(**args):
903 def showdels(**args):
904 return showlist('file_del', getfiles()[2], **args)
904 return showlist('file_del', getfiles()[2], **args)
905 def showmanifest(**args):
905 def showmanifest(**args):
906 args = args.copy()
906 args = args.copy()
907 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
907 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
908 node=hex(ctx.changeset()[0])))
908 node=hex(ctx.changeset()[0])))
909 return self.t('manifest', **args)
909 return self.t('manifest', **args)
910
910
911 def showdiffstat(**args):
911 def showdiffstat(**args):
912 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
912 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
913 files, adds, removes = 0, 0, 0
913 files, adds, removes = 0, 0, 0
914 for i in patch.diffstatdata(util.iterlines(diff)):
914 for i in patch.diffstatdata(util.iterlines(diff)):
915 files += 1
915 files += 1
916 adds += i[1]
916 adds += i[1]
917 removes += i[2]
917 removes += i[2]
918 return '%s: +%s/-%s' % (files, adds, removes)
918 return '%s: +%s/-%s' % (files, adds, removes)
919
919
920 def showlatesttag(**args):
920 def showlatesttag(**args):
921 return self._latesttaginfo(ctx.rev())[2]
921 return self._latesttaginfo(ctx.rev())[2]
922 def showlatesttagdistance(**args):
922 def showlatesttagdistance(**args):
923 return self._latesttaginfo(ctx.rev())[1]
923 return self._latesttaginfo(ctx.rev())[1]
924
924
925 defprops = {
925 defprops = {
926 'author': ctx.user(),
926 'author': ctx.user(),
927 'branches': showbranches,
927 'branches': showbranches,
928 'date': ctx.date(),
928 'date': ctx.date(),
929 'desc': ctx.description().strip(),
929 'desc': ctx.description().strip(),
930 'file_adds': showadds,
930 'file_adds': showadds,
931 'file_dels': showdels,
931 'file_dels': showdels,
932 'file_mods': showmods,
932 'file_mods': showmods,
933 'files': showfiles,
933 'files': showfiles,
934 'file_copies': showcopies,
934 'file_copies': showcopies,
935 'manifest': showmanifest,
935 'manifest': showmanifest,
936 'node': ctx.hex(),
936 'node': ctx.hex(),
937 'parents': showparents,
937 'parents': showparents,
938 'rev': ctx.rev(),
938 'rev': ctx.rev(),
939 'tags': showtags,
939 'tags': showtags,
940 'extras': showextras,
940 'extras': showextras,
941 'diffstat': showdiffstat,
941 'diffstat': showdiffstat,
942 'latesttag': showlatesttag,
942 'latesttag': showlatesttag,
943 'latesttagdistance': showlatesttagdistance,
943 'latesttagdistance': showlatesttagdistance,
944 }
944 }
945 props = props.copy()
945 props = props.copy()
946 props.update(defprops)
946 props.update(defprops)
947
947
948 # find correct templates for current mode
948 # find correct templates for current mode
949
949
950 tmplmodes = [
950 tmplmodes = [
951 (True, None),
951 (True, None),
952 (self.ui.verbose, 'verbose'),
952 (self.ui.verbose, 'verbose'),
953 (self.ui.quiet, 'quiet'),
953 (self.ui.quiet, 'quiet'),
954 (self.ui.debugflag, 'debug'),
954 (self.ui.debugflag, 'debug'),
955 ]
955 ]
956
956
957 types = {'header': '', 'changeset': 'changeset'}
957 types = {'header': '', 'changeset': 'changeset'}
958 for mode, postfix in tmplmodes:
958 for mode, postfix in tmplmodes:
959 for type in types:
959 for type in types:
960 cur = postfix and ('%s_%s' % (type, postfix)) or type
960 cur = postfix and ('%s_%s' % (type, postfix)) or type
961 if mode and cur in self.t:
961 if mode and cur in self.t:
962 types[type] = cur
962 types[type] = cur
963
963
964 try:
964 try:
965
965
966 # write header
966 # write header
967 if types['header']:
967 if types['header']:
968 h = templater.stringify(self.t(types['header'], **props))
968 h = templater.stringify(self.t(types['header'], **props))
969 if self.buffered:
969 if self.buffered:
970 self.header[ctx.rev()] = h
970 self.header[ctx.rev()] = h
971 else:
971 else:
972 self.ui.write(h)
972 self.ui.write(h)
973
973
974 # write changeset metadata, then patch if requested
974 # write changeset metadata, then patch if requested
975 key = types['changeset']
975 key = types['changeset']
976 self.ui.write(templater.stringify(self.t(key, **props)))
976 self.ui.write(templater.stringify(self.t(key, **props)))
977 self.showpatch(ctx.node())
977 self.showpatch(ctx.node())
978
978
979 except KeyError, inst:
979 except KeyError, inst:
980 msg = _("%s: no key named '%s'")
980 msg = _("%s: no key named '%s'")
981 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
981 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
982 except SyntaxError, inst:
982 except SyntaxError, inst:
983 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
983 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
984
984
985 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
985 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
986 """show one changeset using template or regular display.
986 """show one changeset using template or regular display.
987
987
988 Display format will be the first non-empty hit of:
988 Display format will be the first non-empty hit of:
989 1. option 'template'
989 1. option 'template'
990 2. option 'style'
990 2. option 'style'
991 3. [ui] setting 'logtemplate'
991 3. [ui] setting 'logtemplate'
992 4. [ui] setting 'style'
992 4. [ui] setting 'style'
993 If all of these values are either the unset or the empty string,
993 If all of these values are either the unset or the empty string,
994 regular display via changeset_printer() is done.
994 regular display via changeset_printer() is done.
995 """
995 """
996 # options
996 # options
997 patch = False
997 patch = False
998 if opts.get('patch'):
998 if opts.get('patch'):
999 patch = matchfn or matchall(repo)
999 patch = matchfn or matchall(repo)
1000
1000
1001 tmpl = opts.get('template')
1001 tmpl = opts.get('template')
1002 style = None
1002 style = None
1003 if tmpl:
1003 if tmpl:
1004 tmpl = templater.parsestring(tmpl, quoted=False)
1004 tmpl = templater.parsestring(tmpl, quoted=False)
1005 else:
1005 else:
1006 style = opts.get('style')
1006 style = opts.get('style')
1007
1007
1008 # ui settings
1008 # ui settings
1009 if not (tmpl or style):
1009 if not (tmpl or style):
1010 tmpl = ui.config('ui', 'logtemplate')
1010 tmpl = ui.config('ui', 'logtemplate')
1011 if tmpl:
1011 if tmpl:
1012 tmpl = templater.parsestring(tmpl)
1012 tmpl = templater.parsestring(tmpl)
1013 else:
1013 else:
1014 style = ui.config('ui', 'style')
1014 style = ui.config('ui', 'style')
1015
1015
1016 if not (tmpl or style):
1016 if not (tmpl or style):
1017 return changeset_printer(ui, repo, patch, opts, buffered)
1017 return changeset_printer(ui, repo, patch, opts, buffered)
1018
1018
1019 mapfile = None
1019 mapfile = None
1020 if style and not tmpl:
1020 if style and not tmpl:
1021 mapfile = style
1021 mapfile = style
1022 if not os.path.split(mapfile)[0]:
1022 if not os.path.split(mapfile)[0]:
1023 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1023 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1024 or templater.templatepath(mapfile))
1024 or templater.templatepath(mapfile))
1025 if mapname: mapfile = mapname
1025 if mapname: mapfile = mapname
1026
1026
1027 try:
1027 try:
1028 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1028 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1029 except SyntaxError, inst:
1029 except SyntaxError, inst:
1030 raise util.Abort(inst.args[0])
1030 raise util.Abort(inst.args[0])
1031 if tmpl: t.use_template(tmpl)
1031 if tmpl: t.use_template(tmpl)
1032 return t
1032 return t
1033
1033
1034 def finddate(ui, repo, date):
1034 def finddate(ui, repo, date):
1035 """Find the tipmost changeset that matches the given date spec"""
1035 """Find the tipmost changeset that matches the given date spec"""
1036
1036
1037 df = util.matchdate(date)
1037 df = util.matchdate(date)
1038 m = matchall(repo)
1038 m = matchall(repo)
1039 results = {}
1039 results = {}
1040
1040
1041 def prep(ctx, fns):
1041 def prep(ctx, fns):
1042 d = ctx.date()
1042 d = ctx.date()
1043 if df(d[0]):
1043 if df(d[0]):
1044 results[ctx.rev()] = d
1044 results[ctx.rev()] = d
1045
1045
1046 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1046 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1047 rev = ctx.rev()
1047 rev = ctx.rev()
1048 if rev in results:
1048 if rev in results:
1049 ui.status(_("Found revision %s from %s\n") %
1049 ui.status(_("Found revision %s from %s\n") %
1050 (rev, util.datestr(results[rev])))
1050 (rev, util.datestr(results[rev])))
1051 return str(rev)
1051 return str(rev)
1052
1052
1053 raise util.Abort(_("revision matching date not found"))
1053 raise util.Abort(_("revision matching date not found"))
1054
1054
1055 def walkchangerevs(repo, match, opts, prepare):
1055 def walkchangerevs(repo, match, opts, prepare):
1056 '''Iterate over files and the revs in which they changed.
1056 '''Iterate over files and the revs in which they changed.
1057
1057
1058 Callers most commonly need to iterate backwards over the history
1058 Callers most commonly need to iterate backwards over the history
1059 in which they are interested. Doing so has awful (quadratic-looking)
1059 in which they are interested. Doing so has awful (quadratic-looking)
1060 performance, so we use iterators in a "windowed" way.
1060 performance, so we use iterators in a "windowed" way.
1061
1061
1062 We walk a window of revisions in the desired order. Within the
1062 We walk a window of revisions in the desired order. Within the
1063 window, we first walk forwards to gather data, then in the desired
1063 window, we first walk forwards to gather data, then in the desired
1064 order (usually backwards) to display it.
1064 order (usually backwards) to display it.
1065
1065
1066 This function returns an iterator yielding contexts. Before
1066 This function returns an iterator yielding contexts. Before
1067 yielding each context, the iterator will first call the prepare
1067 yielding each context, the iterator will first call the prepare
1068 function on each context in the window in forward order.'''
1068 function on each context in the window in forward order.'''
1069
1069
1070 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1070 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1071 if start < end:
1071 if start < end:
1072 while start < end:
1072 while start < end:
1073 yield start, min(windowsize, end-start)
1073 yield start, min(windowsize, end-start)
1074 start += windowsize
1074 start += windowsize
1075 if windowsize < sizelimit:
1075 if windowsize < sizelimit:
1076 windowsize *= 2
1076 windowsize *= 2
1077 else:
1077 else:
1078 while start > end:
1078 while start > end:
1079 yield start, min(windowsize, start-end-1)
1079 yield start, min(windowsize, start-end-1)
1080 start -= windowsize
1080 start -= windowsize
1081 if windowsize < sizelimit:
1081 if windowsize < sizelimit:
1082 windowsize *= 2
1082 windowsize *= 2
1083
1083
1084 follow = opts.get('follow') or opts.get('follow_first')
1084 follow = opts.get('follow') or opts.get('follow_first')
1085
1085
1086 if not len(repo):
1086 if not len(repo):
1087 return []
1087 return []
1088
1088
1089 if follow:
1089 if follow:
1090 defrange = '%s:0' % repo['.'].rev()
1090 defrange = '%s:0' % repo['.'].rev()
1091 else:
1091 else:
1092 defrange = '-1:0'
1092 defrange = '-1:0'
1093 revs = revrange(repo, opts['rev'] or [defrange])
1093 revs = revrange(repo, opts['rev'] or [defrange])
1094 wanted = set()
1094 wanted = set()
1095 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1095 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1096 fncache = {}
1096 fncache = {}
1097 change = util.cachefunc(repo.changectx)
1097 change = util.cachefunc(repo.changectx)
1098
1098
1099 if not slowpath and not match.files():
1099 if not slowpath and not match.files():
1100 # No files, no patterns. Display all revs.
1100 # No files, no patterns. Display all revs.
1101 wanted = set(revs)
1101 wanted = set(revs)
1102 copies = []
1102 copies = []
1103
1103
1104 if not slowpath:
1104 if not slowpath:
1105 # Only files, no patterns. Check the history of each file.
1105 # Only files, no patterns. Check the history of each file.
1106 def filerevgen(filelog, node):
1106 def filerevgen(filelog, node):
1107 cl_count = len(repo)
1107 cl_count = len(repo)
1108 if node is None:
1108 if node is None:
1109 last = len(filelog) - 1
1109 last = len(filelog) - 1
1110 else:
1110 else:
1111 last = filelog.rev(node)
1111 last = filelog.rev(node)
1112 for i, window in increasing_windows(last, nullrev):
1112 for i, window in increasing_windows(last, nullrev):
1113 revs = []
1113 revs = []
1114 for j in xrange(i - window, i + 1):
1114 for j in xrange(i - window, i + 1):
1115 n = filelog.node(j)
1115 n = filelog.node(j)
1116 revs.append((filelog.linkrev(j),
1116 revs.append((filelog.linkrev(j),
1117 follow and filelog.renamed(n)))
1117 follow and filelog.renamed(n)))
1118 for rev in reversed(revs):
1118 for rev in reversed(revs):
1119 # only yield rev for which we have the changelog, it can
1119 # only yield rev for which we have the changelog, it can
1120 # happen while doing "hg log" during a pull or commit
1120 # happen while doing "hg log" during a pull or commit
1121 if rev[0] < cl_count:
1121 if rev[0] < cl_count:
1122 yield rev
1122 yield rev
1123 def iterfiles():
1123 def iterfiles():
1124 for filename in match.files():
1124 for filename in match.files():
1125 yield filename, None
1125 yield filename, None
1126 for filename_node in copies:
1126 for filename_node in copies:
1127 yield filename_node
1127 yield filename_node
1128 minrev, maxrev = min(revs), max(revs)
1128 minrev, maxrev = min(revs), max(revs)
1129 for file_, node in iterfiles():
1129 for file_, node in iterfiles():
1130 filelog = repo.file(file_)
1130 filelog = repo.file(file_)
1131 if not len(filelog):
1131 if not len(filelog):
1132 if node is None:
1132 if node is None:
1133 # A zero count may be a directory or deleted file, so
1133 # A zero count may be a directory or deleted file, so
1134 # try to find matching entries on the slow path.
1134 # try to find matching entries on the slow path.
1135 if follow:
1135 if follow:
1136 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1136 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1137 slowpath = True
1137 slowpath = True
1138 break
1138 break
1139 else:
1139 else:
1140 continue
1140 continue
1141 for rev, copied in filerevgen(filelog, node):
1141 for rev, copied in filerevgen(filelog, node):
1142 if rev <= maxrev:
1142 if rev <= maxrev:
1143 if rev < minrev:
1143 if rev < minrev:
1144 break
1144 break
1145 fncache.setdefault(rev, [])
1145 fncache.setdefault(rev, [])
1146 fncache[rev].append(file_)
1146 fncache[rev].append(file_)
1147 wanted.add(rev)
1147 wanted.add(rev)
1148 if follow and copied:
1148 if follow and copied:
1149 copies.append(copied)
1149 copies.append(copied)
1150 if slowpath:
1150 if slowpath:
1151 if follow:
1151 if follow:
1152 raise util.Abort(_('can only follow copies/renames for explicit '
1152 raise util.Abort(_('can only follow copies/renames for explicit '
1153 'filenames'))
1153 'filenames'))
1154
1154
1155 # The slow path checks files modified in every changeset.
1155 # The slow path checks files modified in every changeset.
1156 def changerevgen():
1156 def changerevgen():
1157 for i, window in increasing_windows(len(repo) - 1, nullrev):
1157 for i, window in increasing_windows(len(repo) - 1, nullrev):
1158 for j in xrange(i - window, i + 1):
1158 for j in xrange(i - window, i + 1):
1159 yield change(j)
1159 yield change(j)
1160
1160
1161 for ctx in changerevgen():
1161 for ctx in changerevgen():
1162 matches = filter(match, ctx.files())
1162 matches = filter(match, ctx.files())
1163 if matches:
1163 if matches:
1164 fncache[ctx.rev()] = matches
1164 fncache[ctx.rev()] = matches
1165 wanted.add(ctx.rev())
1165 wanted.add(ctx.rev())
1166
1166
1167 class followfilter(object):
1167 class followfilter(object):
1168 def __init__(self, onlyfirst=False):
1168 def __init__(self, onlyfirst=False):
1169 self.startrev = nullrev
1169 self.startrev = nullrev
1170 self.roots = []
1170 self.roots = []
1171 self.onlyfirst = onlyfirst
1171 self.onlyfirst = onlyfirst
1172
1172
1173 def match(self, rev):
1173 def match(self, rev):
1174 def realparents(rev):
1174 def realparents(rev):
1175 if self.onlyfirst:
1175 if self.onlyfirst:
1176 return repo.changelog.parentrevs(rev)[0:1]
1176 return repo.changelog.parentrevs(rev)[0:1]
1177 else:
1177 else:
1178 return filter(lambda x: x != nullrev,
1178 return filter(lambda x: x != nullrev,
1179 repo.changelog.parentrevs(rev))
1179 repo.changelog.parentrevs(rev))
1180
1180
1181 if self.startrev == nullrev:
1181 if self.startrev == nullrev:
1182 self.startrev = rev
1182 self.startrev = rev
1183 return True
1183 return True
1184
1184
1185 if rev > self.startrev:
1185 if rev > self.startrev:
1186 # forward: all descendants
1186 # forward: all descendants
1187 if not self.roots:
1187 if not self.roots:
1188 self.roots.append(self.startrev)
1188 self.roots.append(self.startrev)
1189 for parent in realparents(rev):
1189 for parent in realparents(rev):
1190 if parent in self.roots:
1190 if parent in self.roots:
1191 self.roots.append(rev)
1191 self.roots.append(rev)
1192 return True
1192 return True
1193 else:
1193 else:
1194 # backwards: all parents
1194 # backwards: all parents
1195 if not self.roots:
1195 if not self.roots:
1196 self.roots.extend(realparents(self.startrev))
1196 self.roots.extend(realparents(self.startrev))
1197 if rev in self.roots:
1197 if rev in self.roots:
1198 self.roots.remove(rev)
1198 self.roots.remove(rev)
1199 self.roots.extend(realparents(rev))
1199 self.roots.extend(realparents(rev))
1200 return True
1200 return True
1201
1201
1202 return False
1202 return False
1203
1203
1204 # it might be worthwhile to do this in the iterator if the rev range
1204 # it might be worthwhile to do this in the iterator if the rev range
1205 # is descending and the prune args are all within that range
1205 # is descending and the prune args are all within that range
1206 for rev in opts.get('prune', ()):
1206 for rev in opts.get('prune', ()):
1207 rev = repo.changelog.rev(repo.lookup(rev))
1207 rev = repo.changelog.rev(repo.lookup(rev))
1208 ff = followfilter()
1208 ff = followfilter()
1209 stop = min(revs[0], revs[-1])
1209 stop = min(revs[0], revs[-1])
1210 for x in xrange(rev, stop-1, -1):
1210 for x in xrange(rev, stop-1, -1):
1211 if ff.match(x):
1211 if ff.match(x):
1212 wanted.discard(x)
1212 wanted.discard(x)
1213
1213
1214 def iterate():
1214 def iterate():
1215 if follow and not match.files():
1215 if follow and not match.files():
1216 ff = followfilter(onlyfirst=opts.get('follow_first'))
1216 ff = followfilter(onlyfirst=opts.get('follow_first'))
1217 def want(rev):
1217 def want(rev):
1218 return ff.match(rev) and rev in wanted
1218 return ff.match(rev) and rev in wanted
1219 else:
1219 else:
1220 def want(rev):
1220 def want(rev):
1221 return rev in wanted
1221 return rev in wanted
1222
1222
1223 for i, window in increasing_windows(0, len(revs)):
1223 for i, window in increasing_windows(0, len(revs)):
1224 change = util.cachefunc(repo.changectx)
1224 change = util.cachefunc(repo.changectx)
1225 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1225 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1226 for rev in sorted(nrevs):
1226 for rev in sorted(nrevs):
1227 fns = fncache.get(rev)
1227 fns = fncache.get(rev)
1228 ctx = change(rev)
1228 ctx = change(rev)
1229 if not fns:
1229 if not fns:
1230 def fns_generator():
1230 def fns_generator():
1231 for f in ctx.files():
1231 for f in ctx.files():
1232 if match(f):
1232 if match(f):
1233 yield f
1233 yield f
1234 fns = fns_generator()
1234 fns = fns_generator()
1235 prepare(ctx, fns)
1235 prepare(ctx, fns)
1236 for rev in nrevs:
1236 for rev in nrevs:
1237 yield change(rev)
1237 yield change(rev)
1238 return iterate()
1238 return iterate()
1239
1239
1240 def commit(ui, repo, commitfunc, pats, opts):
1240 def commit(ui, repo, commitfunc, pats, opts):
1241 '''commit the specified files or all outstanding changes'''
1241 '''commit the specified files or all outstanding changes'''
1242 date = opts.get('date')
1242 date = opts.get('date')
1243 if date:
1243 if date:
1244 opts['date'] = util.parsedate(date)
1244 opts['date'] = util.parsedate(date)
1245 message = logmessage(opts)
1245 message = logmessage(opts)
1246
1246
1247 # extract addremove carefully -- this function can be called from a command
1247 # extract addremove carefully -- this function can be called from a command
1248 # that doesn't support addremove
1248 # that doesn't support addremove
1249 if opts.get('addremove'):
1249 if opts.get('addremove'):
1250 addremove(repo, pats, opts)
1250 addremove(repo, pats, opts)
1251
1251
1252 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1252 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1253
1253
1254 def commiteditor(repo, ctx, subs):
1254 def commiteditor(repo, ctx, subs):
1255 if ctx.description():
1255 if ctx.description():
1256 return ctx.description()
1256 return ctx.description()
1257 return commitforceeditor(repo, ctx, subs)
1257 return commitforceeditor(repo, ctx, subs)
1258
1258
1259 def commitforceeditor(repo, ctx, subs):
1259 def commitforceeditor(repo, ctx, subs):
1260 edittext = []
1260 edittext = []
1261 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1261 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1262 if ctx.description():
1262 if ctx.description():
1263 edittext.append(ctx.description())
1263 edittext.append(ctx.description())
1264 edittext.append("")
1264 edittext.append("")
1265 edittext.append("") # Empty line between message and comments.
1265 edittext.append("") # Empty line between message and comments.
1266 edittext.append(_("HG: Enter commit message."
1266 edittext.append(_("HG: Enter commit message."
1267 " Lines beginning with 'HG:' are removed."))
1267 " Lines beginning with 'HG:' are removed."))
1268 edittext.append(_("HG: Leave message empty to abort commit."))
1268 edittext.append(_("HG: Leave message empty to abort commit."))
1269 edittext.append("HG: --")
1269 edittext.append("HG: --")
1270 edittext.append(_("HG: user: %s") % ctx.user())
1270 edittext.append(_("HG: user: %s") % ctx.user())
1271 if ctx.p2():
1271 if ctx.p2():
1272 edittext.append(_("HG: branch merge"))
1272 edittext.append(_("HG: branch merge"))
1273 if ctx.branch():
1273 if ctx.branch():
1274 edittext.append(_("HG: branch '%s'")
1274 edittext.append(_("HG: branch '%s'")
1275 % encoding.tolocal(ctx.branch()))
1275 % encoding.tolocal(ctx.branch()))
1276 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1276 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1277 edittext.extend([_("HG: added %s") % f for f in added])
1277 edittext.extend([_("HG: added %s") % f for f in added])
1278 edittext.extend([_("HG: changed %s") % f for f in modified])
1278 edittext.extend([_("HG: changed %s") % f for f in modified])
1279 edittext.extend([_("HG: removed %s") % f for f in removed])
1279 edittext.extend([_("HG: removed %s") % f for f in removed])
1280 if not added and not modified and not removed:
1280 if not added and not modified and not removed:
1281 edittext.append(_("HG: no files changed"))
1281 edittext.append(_("HG: no files changed"))
1282 edittext.append("")
1282 edittext.append("")
1283 # run editor in the repository root
1283 # run editor in the repository root
1284 olddir = os.getcwd()
1284 olddir = os.getcwd()
1285 os.chdir(repo.root)
1285 os.chdir(repo.root)
1286 text = repo.ui.edit("\n".join(edittext), ctx.user())
1286 text = repo.ui.edit("\n".join(edittext), ctx.user())
1287 text = re.sub("(?m)^HG:.*\n", "", text)
1287 text = re.sub("(?m)^HG:.*\n", "", text)
1288 os.chdir(olddir)
1288 os.chdir(olddir)
1289
1289
1290 if not text.strip():
1290 if not text.strip():
1291 raise util.Abort(_("empty commit message"))
1291 raise util.Abort(_("empty commit message"))
1292
1292
1293 return text
1293 return text
@@ -1,894 +1,901 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2, incorporated herein by reference.
8 # GNU General Public License version 2, incorporated herein by reference.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
39 # (You could use any subset of the tests: test-s* happens to match
39 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it
40 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and
41 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.)
42 # includes some scripts that run daemon processes.)
43
43
44 import difflib
44 import difflib
45 import errno
45 import errno
46 import optparse
46 import optparse
47 import os
47 import os
48 import subprocess
48 import subprocess
49 import shutil
49 import shutil
50 import signal
50 import signal
51 import sys
51 import sys
52 import tempfile
52 import tempfile
53 import time
53 import time
54
54
55 closefds = os.name == 'posix'
55 closefds = os.name == 'posix'
56 def Popen4(cmd, bufsize=-1):
56 def Popen4(cmd, bufsize=-1):
57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
58 close_fds=closefds,
58 close_fds=closefds,
59 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
59 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
60 stderr=subprocess.STDOUT)
60 stderr=subprocess.STDOUT)
61 p.fromchild = p.stdout
61 p.fromchild = p.stdout
62 p.tochild = p.stdin
62 p.tochild = p.stdin
63 p.childerr = p.stderr
63 p.childerr = p.stderr
64 return p
64 return p
65
65
66 # reserved exit code to skip test (used by hghave)
66 # reserved exit code to skip test (used by hghave)
67 SKIPPED_STATUS = 80
67 SKIPPED_STATUS = 80
68 SKIPPED_PREFIX = 'skipped: '
68 SKIPPED_PREFIX = 'skipped: '
69 FAILED_PREFIX = 'hghave check failed: '
69 FAILED_PREFIX = 'hghave check failed: '
70 PYTHON = sys.executable
70 PYTHON = sys.executable
71
71
72 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
72 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
73
73
74 defaults = {
74 defaults = {
75 'jobs': ('HGTEST_JOBS', 1),
75 'jobs': ('HGTEST_JOBS', 1),
76 'timeout': ('HGTEST_TIMEOUT', 180),
76 'timeout': ('HGTEST_TIMEOUT', 180),
77 'port': ('HGTEST_PORT', 20059),
77 'port': ('HGTEST_PORT', 20059),
78 }
78 }
79
79
80 def parseargs():
80 def parseargs():
81 parser = optparse.OptionParser("%prog [options] [tests]")
81 parser = optparse.OptionParser("%prog [options] [tests]")
82 parser.add_option("-C", "--annotate", action="store_true",
82 parser.add_option("-C", "--annotate", action="store_true",
83 help="output files annotated with coverage")
83 help="output files annotated with coverage")
84 parser.add_option("--child", type="int",
84 parser.add_option("--child", type="int",
85 help="run as child process, summary to given fd")
85 help="run as child process, summary to given fd")
86 parser.add_option("-c", "--cover", action="store_true",
86 parser.add_option("-c", "--cover", action="store_true",
87 help="print a test coverage report")
87 help="print a test coverage report")
88 parser.add_option("-f", "--first", action="store_true",
88 parser.add_option("-f", "--first", action="store_true",
89 help="exit on the first test failure")
89 help="exit on the first test failure")
90 parser.add_option("-i", "--interactive", action="store_true",
90 parser.add_option("-i", "--interactive", action="store_true",
91 help="prompt to accept changed output")
91 help="prompt to accept changed output")
92 parser.add_option("-j", "--jobs", type="int",
92 parser.add_option("-j", "--jobs", type="int",
93 help="number of jobs to run in parallel"
93 help="number of jobs to run in parallel"
94 " (default: $%s or %d)" % defaults['jobs'])
94 " (default: $%s or %d)" % defaults['jobs'])
95 parser.add_option("-k", "--keywords",
95 parser.add_option("-k", "--keywords",
96 help="run tests matching keywords")
96 help="run tests matching keywords")
97 parser.add_option("--keep-tmpdir", action="store_true",
97 parser.add_option("--keep-tmpdir", action="store_true",
98 help="keep temporary directory after running tests")
98 help="keep temporary directory after running tests")
99 parser.add_option("--tmpdir", type="string",
99 parser.add_option("--tmpdir", type="string",
100 help="run tests in the given temporary directory"
100 help="run tests in the given temporary directory"
101 " (implies --keep-tmpdir)")
101 " (implies --keep-tmpdir)")
102 parser.add_option("-d", "--debug", action="store_true",
102 parser.add_option("-d", "--debug", action="store_true",
103 help="debug mode: write output of test scripts to console"
103 help="debug mode: write output of test scripts to console"
104 " rather than capturing and diff'ing it (disables timeout)")
104 " rather than capturing and diff'ing it (disables timeout)")
105 parser.add_option("-R", "--restart", action="store_true",
105 parser.add_option("-R", "--restart", action="store_true",
106 help="restart at last error")
106 help="restart at last error")
107 parser.add_option("-p", "--port", type="int",
107 parser.add_option("-p", "--port", type="int",
108 help="port on which servers should listen"
108 help="port on which servers should listen"
109 " (default: $%s or %d)" % defaults['port'])
109 " (default: $%s or %d)" % defaults['port'])
110 parser.add_option("-r", "--retest", action="store_true",
110 parser.add_option("-r", "--retest", action="store_true",
111 help="retest failed tests")
111 help="retest failed tests")
112 parser.add_option("-s", "--cover_stdlib", action="store_true",
112 parser.add_option("-s", "--cover_stdlib", action="store_true",
113 help="print a test coverage report inc. standard libraries")
113 help="print a test coverage report inc. standard libraries")
114 parser.add_option("-S", "--noskips", action="store_true",
114 parser.add_option("-S", "--noskips", action="store_true",
115 help="don't report skip tests verbosely")
115 help="don't report skip tests verbosely")
116 parser.add_option("-t", "--timeout", type="int",
116 parser.add_option("-t", "--timeout", type="int",
117 help="kill errant tests after TIMEOUT seconds"
117 help="kill errant tests after TIMEOUT seconds"
118 " (default: $%s or %d)" % defaults['timeout'])
118 " (default: $%s or %d)" % defaults['timeout'])
119 parser.add_option("-v", "--verbose", action="store_true",
119 parser.add_option("-v", "--verbose", action="store_true",
120 help="output verbose messages")
120 help="output verbose messages")
121 parser.add_option("-n", "--nodiff", action="store_true",
121 parser.add_option("-n", "--nodiff", action="store_true",
122 help="skip showing test changes")
122 help="skip showing test changes")
123 parser.add_option("--with-hg", type="string",
123 parser.add_option("--with-hg", type="string",
124 metavar="HG",
124 metavar="HG",
125 help="test using specified hg script rather than a "
125 help="test using specified hg script rather than a "
126 "temporary installation")
126 "temporary installation")
127 parser.add_option("--local", action="store_true",
127 parser.add_option("--local", action="store_true",
128 help="shortcut for --with-hg=<testdir>/../hg")
128 help="shortcut for --with-hg=<testdir>/../hg")
129 parser.add_option("--pure", action="store_true",
129 parser.add_option("--pure", action="store_true",
130 help="use pure Python code instead of C extensions")
130 help="use pure Python code instead of C extensions")
131 parser.add_option("-3", "--py3k-warnings", action="store_true",
131 parser.add_option("-3", "--py3k-warnings", action="store_true",
132 help="enable Py3k warnings on Python 2.6+")
132 help="enable Py3k warnings on Python 2.6+")
133 parser.add_option("--inotify", action="store_true",
134 help="enable inotify extension when running tests")
133
135
134 for option, default in defaults.items():
136 for option, default in defaults.items():
135 defaults[option] = int(os.environ.get(*default))
137 defaults[option] = int(os.environ.get(*default))
136 parser.set_defaults(**defaults)
138 parser.set_defaults(**defaults)
137 (options, args) = parser.parse_args()
139 (options, args) = parser.parse_args()
138
140
139 if options.with_hg:
141 if options.with_hg:
140 if not (os.path.isfile(options.with_hg) and
142 if not (os.path.isfile(options.with_hg) and
141 os.access(options.with_hg, os.X_OK)):
143 os.access(options.with_hg, os.X_OK)):
142 parser.error('--with-hg must specify an executable hg script')
144 parser.error('--with-hg must specify an executable hg script')
143 if not os.path.basename(options.with_hg) == 'hg':
145 if not os.path.basename(options.with_hg) == 'hg':
144 sys.stderr.write('warning: --with-hg should specify an hg script')
146 sys.stderr.write('warning: --with-hg should specify an hg script')
145 if options.local:
147 if options.local:
146 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
148 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
147 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
149 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
148 if not os.access(hgbin, os.X_OK):
150 if not os.access(hgbin, os.X_OK):
149 parser.error('--local specified, but %r not found or not executable'
151 parser.error('--local specified, but %r not found or not executable'
150 % hgbin)
152 % hgbin)
151 options.with_hg = hgbin
153 options.with_hg = hgbin
152
154
153 options.anycoverage = (options.cover or
155 options.anycoverage = (options.cover or
154 options.cover_stdlib or
156 options.cover_stdlib or
155 options.annotate)
157 options.annotate)
156
158
157 if options.anycoverage and options.with_hg:
159 if options.anycoverage and options.with_hg:
158 # I'm not sure if this is a fundamental limitation or just a
160 # I'm not sure if this is a fundamental limitation or just a
159 # bug. But I don't want to waste people's time and energy doing
161 # bug. But I don't want to waste people's time and energy doing
160 # test runs that don't give the results they want.
162 # test runs that don't give the results they want.
161 parser.error("sorry, coverage options do not work when --with-hg "
163 parser.error("sorry, coverage options do not work when --with-hg "
162 "or --local specified")
164 "or --local specified")
163
165
164 global vlog
166 global vlog
165 if options.verbose:
167 if options.verbose:
166 if options.jobs > 1 or options.child is not None:
168 if options.jobs > 1 or options.child is not None:
167 pid = "[%d]" % os.getpid()
169 pid = "[%d]" % os.getpid()
168 else:
170 else:
169 pid = None
171 pid = None
170 def vlog(*msg):
172 def vlog(*msg):
171 if pid:
173 if pid:
172 print pid,
174 print pid,
173 for m in msg:
175 for m in msg:
174 print m,
176 print m,
175 print
177 print
176 sys.stdout.flush()
178 sys.stdout.flush()
177 else:
179 else:
178 vlog = lambda *msg: None
180 vlog = lambda *msg: None
179
181
180 if options.tmpdir:
182 if options.tmpdir:
181 options.tmpdir = os.path.expanduser(options.tmpdir)
183 options.tmpdir = os.path.expanduser(options.tmpdir)
182
184
183 if options.jobs < 1:
185 if options.jobs < 1:
184 parser.error('--jobs must be positive')
186 parser.error('--jobs must be positive')
185 if options.interactive and options.jobs > 1:
187 if options.interactive and options.jobs > 1:
186 print '(--interactive overrides --jobs)'
188 print '(--interactive overrides --jobs)'
187 options.jobs = 1
189 options.jobs = 1
188 if options.interactive and options.debug:
190 if options.interactive and options.debug:
189 parser.error("-i/--interactive and -d/--debug are incompatible")
191 parser.error("-i/--interactive and -d/--debug are incompatible")
190 if options.debug:
192 if options.debug:
191 if options.timeout != defaults['timeout']:
193 if options.timeout != defaults['timeout']:
192 sys.stderr.write(
194 sys.stderr.write(
193 'warning: --timeout option ignored with --debug\n')
195 'warning: --timeout option ignored with --debug\n')
194 options.timeout = 0
196 options.timeout = 0
195 if options.py3k_warnings:
197 if options.py3k_warnings:
196 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
198 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
197 parser.error('--py3k-warnings can only be used on Python 2.6+')
199 parser.error('--py3k-warnings can only be used on Python 2.6+')
198
200
199 return (options, args)
201 return (options, args)
200
202
201 def rename(src, dst):
203 def rename(src, dst):
202 """Like os.rename(), trade atomicity and opened files friendliness
204 """Like os.rename(), trade atomicity and opened files friendliness
203 for existing destination support.
205 for existing destination support.
204 """
206 """
205 shutil.copy(src, dst)
207 shutil.copy(src, dst)
206 os.remove(src)
208 os.remove(src)
207
209
208 def splitnewlines(text):
210 def splitnewlines(text):
209 '''like str.splitlines, but only split on newlines.
211 '''like str.splitlines, but only split on newlines.
210 keep line endings.'''
212 keep line endings.'''
211 i = 0
213 i = 0
212 lines = []
214 lines = []
213 while True:
215 while True:
214 n = text.find('\n', i)
216 n = text.find('\n', i)
215 if n == -1:
217 if n == -1:
216 last = text[i:]
218 last = text[i:]
217 if last:
219 if last:
218 lines.append(last)
220 lines.append(last)
219 return lines
221 return lines
220 lines.append(text[i:n+1])
222 lines.append(text[i:n+1])
221 i = n + 1
223 i = n + 1
222
224
223 def parsehghaveoutput(lines):
225 def parsehghaveoutput(lines):
224 '''Parse hghave log lines.
226 '''Parse hghave log lines.
225 Return tuple of lists (missing, failed):
227 Return tuple of lists (missing, failed):
226 * the missing/unknown features
228 * the missing/unknown features
227 * the features for which existence check failed'''
229 * the features for which existence check failed'''
228 missing = []
230 missing = []
229 failed = []
231 failed = []
230 for line in lines:
232 for line in lines:
231 if line.startswith(SKIPPED_PREFIX):
233 if line.startswith(SKIPPED_PREFIX):
232 line = line.splitlines()[0]
234 line = line.splitlines()[0]
233 missing.append(line[len(SKIPPED_PREFIX):])
235 missing.append(line[len(SKIPPED_PREFIX):])
234 elif line.startswith(FAILED_PREFIX):
236 elif line.startswith(FAILED_PREFIX):
235 line = line.splitlines()[0]
237 line = line.splitlines()[0]
236 failed.append(line[len(FAILED_PREFIX):])
238 failed.append(line[len(FAILED_PREFIX):])
237
239
238 return missing, failed
240 return missing, failed
239
241
240 def showdiff(expected, output):
242 def showdiff(expected, output):
241 for line in difflib.unified_diff(expected, output,
243 for line in difflib.unified_diff(expected, output,
242 "Expected output", "Test output"):
244 "Expected output", "Test output"):
243 sys.stdout.write(line)
245 sys.stdout.write(line)
244
246
245 def findprogram(program):
247 def findprogram(program):
246 """Search PATH for a executable program"""
248 """Search PATH for a executable program"""
247 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
249 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
248 name = os.path.join(p, program)
250 name = os.path.join(p, program)
249 if os.access(name, os.X_OK):
251 if os.access(name, os.X_OK):
250 return name
252 return name
251 return None
253 return None
252
254
253 def checktools():
255 def checktools():
254 # Before we go any further, check for pre-requisite tools
256 # Before we go any further, check for pre-requisite tools
255 # stuff from coreutils (cat, rm, etc) are not tested
257 # stuff from coreutils (cat, rm, etc) are not tested
256 for p in requiredtools:
258 for p in requiredtools:
257 if os.name == 'nt':
259 if os.name == 'nt':
258 p += '.exe'
260 p += '.exe'
259 found = findprogram(p)
261 found = findprogram(p)
260 if found:
262 if found:
261 vlog("# Found prerequisite", p, "at", found)
263 vlog("# Found prerequisite", p, "at", found)
262 else:
264 else:
263 print "WARNING: Did not find prerequisite tool: "+p
265 print "WARNING: Did not find prerequisite tool: "+p
264
266
265 def cleanup(options):
267 def cleanup(options):
266 if not options.keep_tmpdir:
268 if not options.keep_tmpdir:
267 vlog("# Cleaning up HGTMP", HGTMP)
269 vlog("# Cleaning up HGTMP", HGTMP)
268 shutil.rmtree(HGTMP, True)
270 shutil.rmtree(HGTMP, True)
269
271
270 def usecorrectpython():
272 def usecorrectpython():
271 # some tests run python interpreter. they must use same
273 # some tests run python interpreter. they must use same
272 # interpreter we use or bad things will happen.
274 # interpreter we use or bad things will happen.
273 exedir, exename = os.path.split(sys.executable)
275 exedir, exename = os.path.split(sys.executable)
274 if exename == 'python':
276 if exename == 'python':
275 path = findprogram('python')
277 path = findprogram('python')
276 if os.path.dirname(path) == exedir:
278 if os.path.dirname(path) == exedir:
277 return
279 return
278 vlog('# Making python executable in test path use correct Python')
280 vlog('# Making python executable in test path use correct Python')
279 mypython = os.path.join(BINDIR, 'python')
281 mypython = os.path.join(BINDIR, 'python')
280 try:
282 try:
281 os.symlink(sys.executable, mypython)
283 os.symlink(sys.executable, mypython)
282 except AttributeError:
284 except AttributeError:
283 # windows fallback
285 # windows fallback
284 shutil.copyfile(sys.executable, mypython)
286 shutil.copyfile(sys.executable, mypython)
285 shutil.copymode(sys.executable, mypython)
287 shutil.copymode(sys.executable, mypython)
286
288
287 def installhg(options):
289 def installhg(options):
288 vlog("# Performing temporary installation of HG")
290 vlog("# Performing temporary installation of HG")
289 installerrs = os.path.join("tests", "install.err")
291 installerrs = os.path.join("tests", "install.err")
290 pure = options.pure and "--pure" or ""
292 pure = options.pure and "--pure" or ""
291
293
292 # Run installer in hg root
294 # Run installer in hg root
293 script = os.path.realpath(sys.argv[0])
295 script = os.path.realpath(sys.argv[0])
294 hgroot = os.path.dirname(os.path.dirname(script))
296 hgroot = os.path.dirname(os.path.dirname(script))
295 os.chdir(hgroot)
297 os.chdir(hgroot)
296 nohome = '--home=""'
298 nohome = '--home=""'
297 if os.name == 'nt':
299 if os.name == 'nt':
298 # The --home="" trick works only on OS where os.sep == '/'
300 # The --home="" trick works only on OS where os.sep == '/'
299 # because of a distutils convert_path() fast-path. Avoid it at
301 # because of a distutils convert_path() fast-path. Avoid it at
300 # least on Windows for now, deal with .pydistutils.cfg bugs
302 # least on Windows for now, deal with .pydistutils.cfg bugs
301 # when they happen.
303 # when they happen.
302 nohome = ''
304 nohome = ''
303 cmd = ('%s setup.py %s clean --all'
305 cmd = ('%s setup.py %s clean --all'
304 ' install --force --prefix="%s" --install-lib="%s"'
306 ' install --force --prefix="%s" --install-lib="%s"'
305 ' --install-scripts="%s" %s >%s 2>&1'
307 ' --install-scripts="%s" %s >%s 2>&1'
306 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, nohome,
308 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, nohome,
307 installerrs))
309 installerrs))
308 vlog("# Running", cmd)
310 vlog("# Running", cmd)
309 if os.system(cmd) == 0:
311 if os.system(cmd) == 0:
310 if not options.verbose:
312 if not options.verbose:
311 os.remove(installerrs)
313 os.remove(installerrs)
312 else:
314 else:
313 f = open(installerrs)
315 f = open(installerrs)
314 for line in f:
316 for line in f:
315 print line,
317 print line,
316 f.close()
318 f.close()
317 sys.exit(1)
319 sys.exit(1)
318 os.chdir(TESTDIR)
320 os.chdir(TESTDIR)
319
321
320 usecorrectpython()
322 usecorrectpython()
321
323
322 vlog("# Installing dummy diffstat")
324 vlog("# Installing dummy diffstat")
323 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
325 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
324 f.write('#!' + sys.executable + '\n'
326 f.write('#!' + sys.executable + '\n'
325 'import sys\n'
327 'import sys\n'
326 'files = 0\n'
328 'files = 0\n'
327 'for line in sys.stdin:\n'
329 'for line in sys.stdin:\n'
328 ' if line.startswith("diff "):\n'
330 ' if line.startswith("diff "):\n'
329 ' files += 1\n'
331 ' files += 1\n'
330 'sys.stdout.write("files patched: %d\\n" % files)\n')
332 'sys.stdout.write("files patched: %d\\n" % files)\n')
331 f.close()
333 f.close()
332 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
334 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
333
335
334 if options.py3k_warnings and not options.anycoverage:
336 if options.py3k_warnings and not options.anycoverage:
335 vlog("# Updating hg command to enable Py3k Warnings switch")
337 vlog("# Updating hg command to enable Py3k Warnings switch")
336 f = open(os.path.join(BINDIR, 'hg'), 'r')
338 f = open(os.path.join(BINDIR, 'hg'), 'r')
337 lines = [line.rstrip() for line in f]
339 lines = [line.rstrip() for line in f]
338 lines[0] += ' -3'
340 lines[0] += ' -3'
339 f.close()
341 f.close()
340 f = open(os.path.join(BINDIR, 'hg'), 'w')
342 f = open(os.path.join(BINDIR, 'hg'), 'w')
341 for line in lines:
343 for line in lines:
342 f.write(line + '\n')
344 f.write(line + '\n')
343 f.close()
345 f.close()
344
346
345 if options.anycoverage:
347 if options.anycoverage:
346 vlog("# Installing coverage wrapper")
348 vlog("# Installing coverage wrapper")
347 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
349 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
348 if os.path.exists(COVERAGE_FILE):
350 if os.path.exists(COVERAGE_FILE):
349 os.unlink(COVERAGE_FILE)
351 os.unlink(COVERAGE_FILE)
350 # Create a wrapper script to invoke hg via coverage.py
352 # Create a wrapper script to invoke hg via coverage.py
351 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
353 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
352 f = open(os.path.join(BINDIR, 'hg'), 'w')
354 f = open(os.path.join(BINDIR, 'hg'), 'w')
353 f.write('#!' + sys.executable + '\n')
355 f.write('#!' + sys.executable + '\n')
354 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
356 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
355 '"%s", "-x", "-p", "%s"] + sys.argv[1:])\n' %
357 '"%s", "-x", "-p", "%s"] + sys.argv[1:])\n' %
356 (os.path.join(TESTDIR, 'coverage.py'),
358 (os.path.join(TESTDIR, 'coverage.py'),
357 os.path.join(BINDIR, '_hg.py')))
359 os.path.join(BINDIR, '_hg.py')))
358 f.close()
360 f.close()
359 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
361 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
360
362
361 def outputcoverage(options):
363 def outputcoverage(options):
362
364
363 vlog('# Producing coverage report')
365 vlog('# Producing coverage report')
364 os.chdir(PYTHONDIR)
366 os.chdir(PYTHONDIR)
365
367
366 def covrun(*args):
368 def covrun(*args):
367 start = sys.executable, os.path.join(TESTDIR, 'coverage.py')
369 start = sys.executable, os.path.join(TESTDIR, 'coverage.py')
368 cmd = '"%s" "%s" %s' % (start[0], start[1], ' '.join(args))
370 cmd = '"%s" "%s" %s' % (start[0], start[1], ' '.join(args))
369 vlog('# Running: %s' % cmd)
371 vlog('# Running: %s' % cmd)
370 os.system(cmd)
372 os.system(cmd)
371
373
372 omit = [BINDIR, TESTDIR, PYTHONDIR]
374 omit = [BINDIR, TESTDIR, PYTHONDIR]
373 if not options.cover_stdlib:
375 if not options.cover_stdlib:
374 # Exclude as system paths (ignoring empty strings seen on win)
376 # Exclude as system paths (ignoring empty strings seen on win)
375 omit += [x for x in sys.path if x != '']
377 omit += [x for x in sys.path if x != '']
376 omit = ','.join(omit)
378 omit = ','.join(omit)
377
379
378 covrun('-c') # combine from parallel processes
380 covrun('-c') # combine from parallel processes
379 for fn in os.listdir(TESTDIR):
381 for fn in os.listdir(TESTDIR):
380 if fn.startswith('.coverage.'):
382 if fn.startswith('.coverage.'):
381 os.unlink(os.path.join(TESTDIR, fn))
383 os.unlink(os.path.join(TESTDIR, fn))
382
384
383 covrun('-i', '-r', '"--omit=%s"' % omit) # report
385 covrun('-i', '-r', '"--omit=%s"' % omit) # report
384 if options.annotate:
386 if options.annotate:
385 adir = os.path.join(TESTDIR, 'annotated')
387 adir = os.path.join(TESTDIR, 'annotated')
386 if not os.path.isdir(adir):
388 if not os.path.isdir(adir):
387 os.mkdir(adir)
389 os.mkdir(adir)
388 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
390 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
389
391
390 class Timeout(Exception):
392 class Timeout(Exception):
391 pass
393 pass
392
394
393 def alarmed(signum, frame):
395 def alarmed(signum, frame):
394 raise Timeout
396 raise Timeout
395
397
396 def run(cmd, options):
398 def run(cmd, options):
397 """Run command in a sub-process, capturing the output (stdout and stderr).
399 """Run command in a sub-process, capturing the output (stdout and stderr).
398 Return a tuple (exitcode, output). output is None in debug mode."""
400 Return a tuple (exitcode, output). output is None in debug mode."""
399 # TODO: Use subprocess.Popen if we're running on Python 2.4
401 # TODO: Use subprocess.Popen if we're running on Python 2.4
400 if options.debug:
402 if options.debug:
401 proc = subprocess.Popen(cmd, shell=True)
403 proc = subprocess.Popen(cmd, shell=True)
402 ret = proc.wait()
404 ret = proc.wait()
403 return (ret, None)
405 return (ret, None)
404
406
405 if os.name == 'nt' or sys.platform.startswith('java'):
407 if os.name == 'nt' or sys.platform.startswith('java'):
406 tochild, fromchild = os.popen4(cmd)
408 tochild, fromchild = os.popen4(cmd)
407 tochild.close()
409 tochild.close()
408 output = fromchild.read()
410 output = fromchild.read()
409 ret = fromchild.close()
411 ret = fromchild.close()
410 if ret == None:
412 if ret == None:
411 ret = 0
413 ret = 0
412 else:
414 else:
413 proc = Popen4(cmd)
415 proc = Popen4(cmd)
414 try:
416 try:
415 output = ''
417 output = ''
416 proc.tochild.close()
418 proc.tochild.close()
417 output = proc.fromchild.read()
419 output = proc.fromchild.read()
418 ret = proc.wait()
420 ret = proc.wait()
419 if os.WIFEXITED(ret):
421 if os.WIFEXITED(ret):
420 ret = os.WEXITSTATUS(ret)
422 ret = os.WEXITSTATUS(ret)
421 except Timeout:
423 except Timeout:
422 vlog('# Process %d timed out - killing it' % proc.pid)
424 vlog('# Process %d timed out - killing it' % proc.pid)
423 os.kill(proc.pid, signal.SIGTERM)
425 os.kill(proc.pid, signal.SIGTERM)
424 ret = proc.wait()
426 ret = proc.wait()
425 if ret == 0:
427 if ret == 0:
426 ret = signal.SIGTERM << 8
428 ret = signal.SIGTERM << 8
427 output += ("\n### Abort: timeout after %d seconds.\n"
429 output += ("\n### Abort: timeout after %d seconds.\n"
428 % options.timeout)
430 % options.timeout)
429 return ret, splitnewlines(output)
431 return ret, splitnewlines(output)
430
432
431 def runone(options, test, skips, fails):
433 def runone(options, test, skips, fails):
432 '''tristate output:
434 '''tristate output:
433 None -> skipped
435 None -> skipped
434 True -> passed
436 True -> passed
435 False -> failed'''
437 False -> failed'''
436
438
437 def skip(msg):
439 def skip(msg):
438 if not options.verbose:
440 if not options.verbose:
439 skips.append((test, msg))
441 skips.append((test, msg))
440 else:
442 else:
441 print "\nSkipping %s: %s" % (test, msg)
443 print "\nSkipping %s: %s" % (test, msg)
442 return None
444 return None
443
445
444 def fail(msg):
446 def fail(msg):
445 fails.append((test, msg))
447 fails.append((test, msg))
446 if not options.nodiff:
448 if not options.nodiff:
447 print "\nERROR: %s %s" % (test, msg)
449 print "\nERROR: %s %s" % (test, msg)
448 return None
450 return None
449
451
450 vlog("# Test", test)
452 vlog("# Test", test)
451
453
452 # create a fresh hgrc
454 # create a fresh hgrc
453 hgrc = open(HGRCPATH, 'w+')
455 hgrc = open(HGRCPATH, 'w+')
454 hgrc.write('[ui]\n')
456 hgrc.write('[ui]\n')
455 hgrc.write('slash = True\n')
457 hgrc.write('slash = True\n')
456 hgrc.write('[defaults]\n')
458 hgrc.write('[defaults]\n')
457 hgrc.write('backout = -d "0 0"\n')
459 hgrc.write('backout = -d "0 0"\n')
458 hgrc.write('commit = -d "0 0"\n')
460 hgrc.write('commit = -d "0 0"\n')
459 hgrc.write('tag = -d "0 0"\n')
461 hgrc.write('tag = -d "0 0"\n')
462 if options.inotify:
463 hgrc.write('[extensions]\n')
464 hgrc.write('inotify=\n')
465 hgrc.write('[inotify]\n')
466 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
460 hgrc.close()
467 hgrc.close()
461
468
462 err = os.path.join(TESTDIR, test+".err")
469 err = os.path.join(TESTDIR, test+".err")
463 ref = os.path.join(TESTDIR, test+".out")
470 ref = os.path.join(TESTDIR, test+".out")
464 testpath = os.path.join(TESTDIR, test)
471 testpath = os.path.join(TESTDIR, test)
465
472
466 if os.path.exists(err):
473 if os.path.exists(err):
467 os.remove(err) # Remove any previous output files
474 os.remove(err) # Remove any previous output files
468
475
469 # Make a tmp subdirectory to work in
476 # Make a tmp subdirectory to work in
470 tmpd = os.path.join(HGTMP, test)
477 tmpd = os.path.join(HGTMP, test)
471 os.mkdir(tmpd)
478 os.mkdir(tmpd)
472 os.chdir(tmpd)
479 os.chdir(tmpd)
473
480
474 try:
481 try:
475 tf = open(testpath)
482 tf = open(testpath)
476 firstline = tf.readline().rstrip()
483 firstline = tf.readline().rstrip()
477 tf.close()
484 tf.close()
478 except:
485 except:
479 firstline = ''
486 firstline = ''
480 lctest = test.lower()
487 lctest = test.lower()
481
488
482 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
489 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
483 py3kswitch = options.py3k_warnings and ' -3' or ''
490 py3kswitch = options.py3k_warnings and ' -3' or ''
484 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, testpath)
491 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, testpath)
485 elif lctest.endswith('.bat'):
492 elif lctest.endswith('.bat'):
486 # do not run batch scripts on non-windows
493 # do not run batch scripts on non-windows
487 if os.name != 'nt':
494 if os.name != 'nt':
488 return skip("batch script")
495 return skip("batch script")
489 # To reliably get the error code from batch files on WinXP,
496 # To reliably get the error code from batch files on WinXP,
490 # the "cmd /c call" prefix is needed. Grrr
497 # the "cmd /c call" prefix is needed. Grrr
491 cmd = 'cmd /c call "%s"' % testpath
498 cmd = 'cmd /c call "%s"' % testpath
492 else:
499 else:
493 # do not run shell scripts on windows
500 # do not run shell scripts on windows
494 if os.name == 'nt':
501 if os.name == 'nt':
495 return skip("shell script")
502 return skip("shell script")
496 # do not try to run non-executable programs
503 # do not try to run non-executable programs
497 if not os.path.exists(testpath):
504 if not os.path.exists(testpath):
498 return fail("does not exist")
505 return fail("does not exist")
499 elif not os.access(testpath, os.X_OK):
506 elif not os.access(testpath, os.X_OK):
500 return skip("not executable")
507 return skip("not executable")
501 cmd = '"%s"' % testpath
508 cmd = '"%s"' % testpath
502
509
503 if options.timeout > 0:
510 if options.timeout > 0:
504 signal.alarm(options.timeout)
511 signal.alarm(options.timeout)
505
512
506 vlog("# Running", cmd)
513 vlog("# Running", cmd)
507 ret, out = run(cmd, options)
514 ret, out = run(cmd, options)
508 vlog("# Ret was:", ret)
515 vlog("# Ret was:", ret)
509
516
510 if options.timeout > 0:
517 if options.timeout > 0:
511 signal.alarm(0)
518 signal.alarm(0)
512
519
513 mark = '.'
520 mark = '.'
514
521
515 skipped = (ret == SKIPPED_STATUS)
522 skipped = (ret == SKIPPED_STATUS)
516 # If we're not in --debug mode and reference output file exists,
523 # If we're not in --debug mode and reference output file exists,
517 # check test output against it.
524 # check test output against it.
518 if options.debug:
525 if options.debug:
519 refout = None # to match out == None
526 refout = None # to match out == None
520 elif os.path.exists(ref):
527 elif os.path.exists(ref):
521 f = open(ref, "r")
528 f = open(ref, "r")
522 refout = splitnewlines(f.read())
529 refout = splitnewlines(f.read())
523 f.close()
530 f.close()
524 else:
531 else:
525 refout = []
532 refout = []
526
533
527 if skipped:
534 if skipped:
528 mark = 's'
535 mark = 's'
529 if out is None: # debug mode: nothing to parse
536 if out is None: # debug mode: nothing to parse
530 missing = ['unknown']
537 missing = ['unknown']
531 failed = None
538 failed = None
532 else:
539 else:
533 missing, failed = parsehghaveoutput(out)
540 missing, failed = parsehghaveoutput(out)
534 if not missing:
541 if not missing:
535 missing = ['irrelevant']
542 missing = ['irrelevant']
536 if failed:
543 if failed:
537 fail("hghave failed checking for %s" % failed[-1])
544 fail("hghave failed checking for %s" % failed[-1])
538 skipped = False
545 skipped = False
539 else:
546 else:
540 skip(missing[-1])
547 skip(missing[-1])
541 elif out != refout:
548 elif out != refout:
542 mark = '!'
549 mark = '!'
543 if ret:
550 if ret:
544 fail("output changed and returned error code %d" % ret)
551 fail("output changed and returned error code %d" % ret)
545 else:
552 else:
546 fail("output changed")
553 fail("output changed")
547 if not options.nodiff:
554 if not options.nodiff:
548 showdiff(refout, out)
555 showdiff(refout, out)
549 ret = 1
556 ret = 1
550 elif ret:
557 elif ret:
551 mark = '!'
558 mark = '!'
552 fail("returned error code %d" % ret)
559 fail("returned error code %d" % ret)
553
560
554 if not options.verbose:
561 if not options.verbose:
555 sys.stdout.write(mark)
562 sys.stdout.write(mark)
556 sys.stdout.flush()
563 sys.stdout.flush()
557
564
558 if ret != 0 and not skipped and not options.debug:
565 if ret != 0 and not skipped and not options.debug:
559 # Save errors to a file for diagnosis
566 # Save errors to a file for diagnosis
560 f = open(err, "wb")
567 f = open(err, "wb")
561 for line in out:
568 for line in out:
562 f.write(line)
569 f.write(line)
563 f.close()
570 f.close()
564
571
565 # Kill off any leftover daemon processes
572 # Kill off any leftover daemon processes
566 try:
573 try:
567 fp = open(DAEMON_PIDS)
574 fp = open(DAEMON_PIDS)
568 for line in fp:
575 for line in fp:
569 try:
576 try:
570 pid = int(line)
577 pid = int(line)
571 except ValueError:
578 except ValueError:
572 continue
579 continue
573 try:
580 try:
574 os.kill(pid, 0)
581 os.kill(pid, 0)
575 vlog('# Killing daemon process %d' % pid)
582 vlog('# Killing daemon process %d' % pid)
576 os.kill(pid, signal.SIGTERM)
583 os.kill(pid, signal.SIGTERM)
577 time.sleep(0.25)
584 time.sleep(0.25)
578 os.kill(pid, 0)
585 os.kill(pid, 0)
579 vlog('# Daemon process %d is stuck - really killing it' % pid)
586 vlog('# Daemon process %d is stuck - really killing it' % pid)
580 os.kill(pid, signal.SIGKILL)
587 os.kill(pid, signal.SIGKILL)
581 except OSError, err:
588 except OSError, err:
582 if err.errno != errno.ESRCH:
589 if err.errno != errno.ESRCH:
583 raise
590 raise
584 fp.close()
591 fp.close()
585 os.unlink(DAEMON_PIDS)
592 os.unlink(DAEMON_PIDS)
586 except IOError:
593 except IOError:
587 pass
594 pass
588
595
589 os.chdir(TESTDIR)
596 os.chdir(TESTDIR)
590 if not options.keep_tmpdir:
597 if not options.keep_tmpdir:
591 shutil.rmtree(tmpd, True)
598 shutil.rmtree(tmpd, True)
592 if skipped:
599 if skipped:
593 return None
600 return None
594 return ret == 0
601 return ret == 0
595
602
596 _hgpath = None
603 _hgpath = None
597
604
598 def _gethgpath():
605 def _gethgpath():
599 """Return the path to the mercurial package that is actually found by
606 """Return the path to the mercurial package that is actually found by
600 the current Python interpreter."""
607 the current Python interpreter."""
601 global _hgpath
608 global _hgpath
602 if _hgpath is not None:
609 if _hgpath is not None:
603 return _hgpath
610 return _hgpath
604
611
605 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
612 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
606 pipe = os.popen(cmd % PYTHON)
613 pipe = os.popen(cmd % PYTHON)
607 try:
614 try:
608 _hgpath = pipe.read().strip()
615 _hgpath = pipe.read().strip()
609 finally:
616 finally:
610 pipe.close()
617 pipe.close()
611 return _hgpath
618 return _hgpath
612
619
613 def _checkhglib(verb):
620 def _checkhglib(verb):
614 """Ensure that the 'mercurial' package imported by python is
621 """Ensure that the 'mercurial' package imported by python is
615 the one we expect it to be. If not, print a warning to stderr."""
622 the one we expect it to be. If not, print a warning to stderr."""
616 expecthg = os.path.join(PYTHONDIR, 'mercurial')
623 expecthg = os.path.join(PYTHONDIR, 'mercurial')
617 actualhg = _gethgpath()
624 actualhg = _gethgpath()
618 if actualhg != expecthg:
625 if actualhg != expecthg:
619 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
626 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
620 ' (expected %s)\n'
627 ' (expected %s)\n'
621 % (verb, actualhg, expecthg))
628 % (verb, actualhg, expecthg))
622
629
623 def runchildren(options, tests):
630 def runchildren(options, tests):
624 if INST:
631 if INST:
625 installhg(options)
632 installhg(options)
626 _checkhglib("Testing")
633 _checkhglib("Testing")
627
634
628 optcopy = dict(options.__dict__)
635 optcopy = dict(options.__dict__)
629 optcopy['jobs'] = 1
636 optcopy['jobs'] = 1
630 if optcopy['with_hg'] is None:
637 if optcopy['with_hg'] is None:
631 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
638 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
632 opts = []
639 opts = []
633 for opt, value in optcopy.iteritems():
640 for opt, value in optcopy.iteritems():
634 name = '--' + opt.replace('_', '-')
641 name = '--' + opt.replace('_', '-')
635 if value is True:
642 if value is True:
636 opts.append(name)
643 opts.append(name)
637 elif value is not None:
644 elif value is not None:
638 opts.append(name + '=' + str(value))
645 opts.append(name + '=' + str(value))
639
646
640 tests.reverse()
647 tests.reverse()
641 jobs = [[] for j in xrange(options.jobs)]
648 jobs = [[] for j in xrange(options.jobs)]
642 while tests:
649 while tests:
643 for job in jobs:
650 for job in jobs:
644 if not tests: break
651 if not tests: break
645 job.append(tests.pop())
652 job.append(tests.pop())
646 fps = {}
653 fps = {}
647 for j, job in enumerate(jobs):
654 for j, job in enumerate(jobs):
648 if not job:
655 if not job:
649 continue
656 continue
650 rfd, wfd = os.pipe()
657 rfd, wfd = os.pipe()
651 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
658 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
652 childtmp = os.path.join(HGTMP, 'child%d' % j)
659 childtmp = os.path.join(HGTMP, 'child%d' % j)
653 childopts += ['--tmpdir', childtmp]
660 childopts += ['--tmpdir', childtmp]
654 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
661 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
655 vlog(' '.join(cmdline))
662 vlog(' '.join(cmdline))
656 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
663 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
657 os.close(wfd)
664 os.close(wfd)
658 failures = 0
665 failures = 0
659 tested, skipped, failed = 0, 0, 0
666 tested, skipped, failed = 0, 0, 0
660 skips = []
667 skips = []
661 fails = []
668 fails = []
662 while fps:
669 while fps:
663 pid, status = os.wait()
670 pid, status = os.wait()
664 fp = fps.pop(pid)
671 fp = fps.pop(pid)
665 l = fp.read().splitlines()
672 l = fp.read().splitlines()
666 test, skip, fail = map(int, l[:3])
673 test, skip, fail = map(int, l[:3])
667 split = -fail or len(l)
674 split = -fail or len(l)
668 for s in l[3:split]:
675 for s in l[3:split]:
669 skips.append(s.split(" ", 1))
676 skips.append(s.split(" ", 1))
670 for s in l[split:]:
677 for s in l[split:]:
671 fails.append(s.split(" ", 1))
678 fails.append(s.split(" ", 1))
672 tested += test
679 tested += test
673 skipped += skip
680 skipped += skip
674 failed += fail
681 failed += fail
675 vlog('pid %d exited, status %d' % (pid, status))
682 vlog('pid %d exited, status %d' % (pid, status))
676 failures |= status
683 failures |= status
677 print
684 print
678 if not options.noskips:
685 if not options.noskips:
679 for s in skips:
686 for s in skips:
680 print "Skipped %s: %s" % (s[0], s[1])
687 print "Skipped %s: %s" % (s[0], s[1])
681 for s in fails:
688 for s in fails:
682 print "Failed %s: %s" % (s[0], s[1])
689 print "Failed %s: %s" % (s[0], s[1])
683
690
684 _checkhglib("Tested")
691 _checkhglib("Tested")
685 print "# Ran %d tests, %d skipped, %d failed." % (
692 print "# Ran %d tests, %d skipped, %d failed." % (
686 tested, skipped, failed)
693 tested, skipped, failed)
687 sys.exit(failures != 0)
694 sys.exit(failures != 0)
688
695
689 def runtests(options, tests):
696 def runtests(options, tests):
690 global DAEMON_PIDS, HGRCPATH
697 global DAEMON_PIDS, HGRCPATH
691 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
698 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
692 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
699 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
693
700
694 try:
701 try:
695 if INST:
702 if INST:
696 installhg(options)
703 installhg(options)
697 _checkhglib("Testing")
704 _checkhglib("Testing")
698
705
699 if options.timeout > 0:
706 if options.timeout > 0:
700 try:
707 try:
701 signal.signal(signal.SIGALRM, alarmed)
708 signal.signal(signal.SIGALRM, alarmed)
702 vlog('# Running each test with %d second timeout' %
709 vlog('# Running each test with %d second timeout' %
703 options.timeout)
710 options.timeout)
704 except AttributeError:
711 except AttributeError:
705 print 'WARNING: cannot run tests with timeouts'
712 print 'WARNING: cannot run tests with timeouts'
706 options.timeout = 0
713 options.timeout = 0
707
714
708 tested = 0
715 tested = 0
709 failed = 0
716 failed = 0
710 skipped = 0
717 skipped = 0
711
718
712 if options.restart:
719 if options.restart:
713 orig = list(tests)
720 orig = list(tests)
714 while tests:
721 while tests:
715 if os.path.exists(tests[0] + ".err"):
722 if os.path.exists(tests[0] + ".err"):
716 break
723 break
717 tests.pop(0)
724 tests.pop(0)
718 if not tests:
725 if not tests:
719 print "running all tests"
726 print "running all tests"
720 tests = orig
727 tests = orig
721
728
722 skips = []
729 skips = []
723 fails = []
730 fails = []
724
731
725 for test in tests:
732 for test in tests:
726 if options.retest and not os.path.exists(test + ".err"):
733 if options.retest and not os.path.exists(test + ".err"):
727 skipped += 1
734 skipped += 1
728 continue
735 continue
729
736
730 if options.keywords:
737 if options.keywords:
731 t = open(test).read().lower() + test.lower()
738 t = open(test).read().lower() + test.lower()
732 for k in options.keywords.lower().split():
739 for k in options.keywords.lower().split():
733 if k in t:
740 if k in t:
734 break
741 break
735 else:
742 else:
736 skipped +=1
743 skipped +=1
737 continue
744 continue
738
745
739 ret = runone(options, test, skips, fails)
746 ret = runone(options, test, skips, fails)
740 if ret is None:
747 if ret is None:
741 skipped += 1
748 skipped += 1
742 elif not ret:
749 elif not ret:
743 if options.interactive:
750 if options.interactive:
744 print "Accept this change? [n] ",
751 print "Accept this change? [n] ",
745 answer = sys.stdin.readline().strip()
752 answer = sys.stdin.readline().strip()
746 if answer.lower() in "y yes".split():
753 if answer.lower() in "y yes".split():
747 rename(test + ".err", test + ".out")
754 rename(test + ".err", test + ".out")
748 tested += 1
755 tested += 1
749 fails.pop()
756 fails.pop()
750 continue
757 continue
751 failed += 1
758 failed += 1
752 if options.first:
759 if options.first:
753 break
760 break
754 tested += 1
761 tested += 1
755
762
756 if options.child:
763 if options.child:
757 fp = os.fdopen(options.child, 'w')
764 fp = os.fdopen(options.child, 'w')
758 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
765 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
759 for s in skips:
766 for s in skips:
760 fp.write("%s %s\n" % s)
767 fp.write("%s %s\n" % s)
761 for s in fails:
768 for s in fails:
762 fp.write("%s %s\n" % s)
769 fp.write("%s %s\n" % s)
763 fp.close()
770 fp.close()
764 else:
771 else:
765 print
772 print
766 for s in skips:
773 for s in skips:
767 print "Skipped %s: %s" % s
774 print "Skipped %s: %s" % s
768 for s in fails:
775 for s in fails:
769 print "Failed %s: %s" % s
776 print "Failed %s: %s" % s
770 _checkhglib("Tested")
777 _checkhglib("Tested")
771 print "# Ran %d tests, %d skipped, %d failed." % (
778 print "# Ran %d tests, %d skipped, %d failed." % (
772 tested, skipped, failed)
779 tested, skipped, failed)
773
780
774 if options.anycoverage:
781 if options.anycoverage:
775 outputcoverage(options)
782 outputcoverage(options)
776 except KeyboardInterrupt:
783 except KeyboardInterrupt:
777 failed = True
784 failed = True
778 print "\ninterrupted!"
785 print "\ninterrupted!"
779
786
780 if failed:
787 if failed:
781 sys.exit(1)
788 sys.exit(1)
782
789
783 def main():
790 def main():
784 (options, args) = parseargs()
791 (options, args) = parseargs()
785 if not options.child:
792 if not options.child:
786 os.umask(022)
793 os.umask(022)
787
794
788 checktools()
795 checktools()
789
796
790 # Reset some environment variables to well-known values so that
797 # Reset some environment variables to well-known values so that
791 # the tests produce repeatable output.
798 # the tests produce repeatable output.
792 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
799 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
793 os.environ['TZ'] = 'GMT'
800 os.environ['TZ'] = 'GMT'
794 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
801 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
795 os.environ['CDPATH'] = ''
802 os.environ['CDPATH'] = ''
796 os.environ['COLUMNS'] = '80'
803 os.environ['COLUMNS'] = '80'
797
804
798 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
805 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
799 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
806 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
800 if options.tmpdir:
807 if options.tmpdir:
801 options.keep_tmpdir = True
808 options.keep_tmpdir = True
802 tmpdir = options.tmpdir
809 tmpdir = options.tmpdir
803 if os.path.exists(tmpdir):
810 if os.path.exists(tmpdir):
804 # Meaning of tmpdir has changed since 1.3: we used to create
811 # Meaning of tmpdir has changed since 1.3: we used to create
805 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
812 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
806 # tmpdir already exists.
813 # tmpdir already exists.
807 sys.exit("error: temp dir %r already exists" % tmpdir)
814 sys.exit("error: temp dir %r already exists" % tmpdir)
808
815
809 # Automatically removing tmpdir sounds convenient, but could
816 # Automatically removing tmpdir sounds convenient, but could
810 # really annoy anyone in the habit of using "--tmpdir=/tmp"
817 # really annoy anyone in the habit of using "--tmpdir=/tmp"
811 # or "--tmpdir=$HOME".
818 # or "--tmpdir=$HOME".
812 #vlog("# Removing temp dir", tmpdir)
819 #vlog("# Removing temp dir", tmpdir)
813 #shutil.rmtree(tmpdir)
820 #shutil.rmtree(tmpdir)
814 os.makedirs(tmpdir)
821 os.makedirs(tmpdir)
815 else:
822 else:
816 tmpdir = tempfile.mkdtemp('', 'hgtests.')
823 tmpdir = tempfile.mkdtemp('', 'hgtests.')
817 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
824 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
818 DAEMON_PIDS = None
825 DAEMON_PIDS = None
819 HGRCPATH = None
826 HGRCPATH = None
820
827
821 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
828 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
822 os.environ["HGMERGE"] = "internal:merge"
829 os.environ["HGMERGE"] = "internal:merge"
823 os.environ["HGUSER"] = "test"
830 os.environ["HGUSER"] = "test"
824 os.environ["HGENCODING"] = "ascii"
831 os.environ["HGENCODING"] = "ascii"
825 os.environ["HGENCODINGMODE"] = "strict"
832 os.environ["HGENCODINGMODE"] = "strict"
826 os.environ["HGPORT"] = str(options.port)
833 os.environ["HGPORT"] = str(options.port)
827 os.environ["HGPORT1"] = str(options.port + 1)
834 os.environ["HGPORT1"] = str(options.port + 1)
828 os.environ["HGPORT2"] = str(options.port + 2)
835 os.environ["HGPORT2"] = str(options.port + 2)
829
836
830 if options.with_hg:
837 if options.with_hg:
831 INST = None
838 INST = None
832 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
839 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
833
840
834 # This looks redundant with how Python initializes sys.path from
841 # This looks redundant with how Python initializes sys.path from
835 # the location of the script being executed. Needed because the
842 # the location of the script being executed. Needed because the
836 # "hg" specified by --with-hg is not the only Python script
843 # "hg" specified by --with-hg is not the only Python script
837 # executed in the test suite that needs to import 'mercurial'
844 # executed in the test suite that needs to import 'mercurial'
838 # ... which means it's not really redundant at all.
845 # ... which means it's not really redundant at all.
839 PYTHONDIR = BINDIR
846 PYTHONDIR = BINDIR
840 else:
847 else:
841 INST = os.path.join(HGTMP, "install")
848 INST = os.path.join(HGTMP, "install")
842 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
849 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
843 PYTHONDIR = os.path.join(INST, "lib", "python")
850 PYTHONDIR = os.path.join(INST, "lib", "python")
844
851
845 os.environ["BINDIR"] = BINDIR
852 os.environ["BINDIR"] = BINDIR
846 os.environ["PYTHON"] = PYTHON
853 os.environ["PYTHON"] = PYTHON
847
854
848 if not options.child:
855 if not options.child:
849 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
856 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
850 os.environ["PATH"] = os.pathsep.join(path)
857 os.environ["PATH"] = os.pathsep.join(path)
851
858
852 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
859 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
853 # can run .../tests/run-tests.py test-foo where test-foo
860 # can run .../tests/run-tests.py test-foo where test-foo
854 # adds an extension to HGRC
861 # adds an extension to HGRC
855 pypath = [PYTHONDIR, TESTDIR]
862 pypath = [PYTHONDIR, TESTDIR]
856 # We have to augment PYTHONPATH, rather than simply replacing
863 # We have to augment PYTHONPATH, rather than simply replacing
857 # it, in case external libraries are only available via current
864 # it, in case external libraries are only available via current
858 # PYTHONPATH. (In particular, the Subversion bindings on OS X
865 # PYTHONPATH. (In particular, the Subversion bindings on OS X
859 # are in /opt/subversion.)
866 # are in /opt/subversion.)
860 oldpypath = os.environ.get('PYTHONPATH')
867 oldpypath = os.environ.get('PYTHONPATH')
861 if oldpypath:
868 if oldpypath:
862 pypath.append(oldpypath)
869 pypath.append(oldpypath)
863 os.environ['PYTHONPATH'] = os.pathsep.join(pypath)
870 os.environ['PYTHONPATH'] = os.pathsep.join(pypath)
864
871
865 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
872 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
866
873
867 if len(args) == 0:
874 if len(args) == 0:
868 args = os.listdir(".")
875 args = os.listdir(".")
869 args.sort()
876 args.sort()
870
877
871 tests = []
878 tests = []
872 for test in args:
879 for test in args:
873 if (test.startswith("test-") and '~' not in test and
880 if (test.startswith("test-") and '~' not in test and
874 ('.' not in test or test.endswith('.py') or
881 ('.' not in test or test.endswith('.py') or
875 test.endswith('.bat'))):
882 test.endswith('.bat'))):
876 tests.append(test)
883 tests.append(test)
877 if not tests:
884 if not tests:
878 print "# Ran 0 tests, 0 skipped, 0 failed."
885 print "# Ran 0 tests, 0 skipped, 0 failed."
879 return
886 return
880
887
881 vlog("# Using TESTDIR", TESTDIR)
888 vlog("# Using TESTDIR", TESTDIR)
882 vlog("# Using HGTMP", HGTMP)
889 vlog("# Using HGTMP", HGTMP)
883 vlog("# Using PATH", os.environ["PATH"])
890 vlog("# Using PATH", os.environ["PATH"])
884 vlog("# Using PYTHONPATH", os.environ["PYTHONPATH"])
891 vlog("# Using PYTHONPATH", os.environ["PYTHONPATH"])
885
892
886 try:
893 try:
887 if len(tests) > 1 and options.jobs > 1:
894 if len(tests) > 1 and options.jobs > 1:
888 runchildren(options, tests)
895 runchildren(options, tests)
889 else:
896 else:
890 runtests(options, tests)
897 runtests(options, tests)
891 finally:
898 finally:
892 cleanup(options)
899 cleanup(options)
893
900
894 main()
901 main()
General Comments 0
You need to be logged in to leave comments. Login now