##// END OF EJS Templates
addremove/findrenames: find renames according to the match object (issue1527)...
Benoit Boissinot -
r8489:1a96f1d9 default
parent child Browse files
Show More
@@ -1,1258 +1,1257 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, bisect, stat, errno, re
10 import os, sys, bisect, stat, errno, re
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 # drop repo-specific config
104 dst = src.baseui # 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 # keep all global options
107 dst = src # 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 file name") %
220 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
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 match(repo, pats=[], opts={}, globbed=False, default='relpath'):
238 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
239 if not globbed and default == 'relpath':
239 if not globbed and default == 'relpath':
240 pats = util.expand_glob(pats or [])
240 pats = util.expand_glob(pats or [])
241 m = _match.match(repo.root, repo.getcwd(), pats,
241 m = _match.match(repo.root, repo.getcwd(), pats,
242 opts.get('include'), opts.get('exclude'), default)
242 opts.get('include'), opts.get('exclude'), default)
243 def badfn(f, msg):
243 def badfn(f, msg):
244 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
244 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
245 return False
245 return False
246 m.bad = badfn
246 m.bad = badfn
247 return m
247 return m
248
248
249 def matchall(repo):
249 def matchall(repo):
250 return _match.always(repo.root, repo.getcwd())
250 return _match.always(repo.root, repo.getcwd())
251
251
252 def matchfiles(repo, files):
252 def matchfiles(repo, files):
253 return _match.exact(repo.root, repo.getcwd(), files)
253 return _match.exact(repo.root, repo.getcwd(), files)
254
254
255 def findrenames(repo, added=None, removed=None, threshold=0.5):
255 def findrenames(repo, match=None, threshold=0.5):
256 '''find renamed files -- yields (before, after, score) tuples'''
256 '''find renamed files -- yields (before, after, score) tuples'''
257 if added is None or removed is None:
257 added, removed = repo.status(match=match)[1:3]
258 added, removed = repo.status()[1:3]
259 ctx = repo['.']
258 ctx = repo['.']
260 for a in added:
259 for a in added:
261 aa = repo.wread(a)
260 aa = repo.wread(a)
262 bestname, bestscore = None, threshold
261 bestname, bestscore = None, threshold
263 for r in removed:
262 for r in removed:
264 rr = ctx.filectx(r).data()
263 rr = ctx.filectx(r).data()
265
264
266 # bdiff.blocks() returns blocks of matching lines
265 # bdiff.blocks() returns blocks of matching lines
267 # count the number of bytes in each
266 # count the number of bytes in each
268 equal = 0
267 equal = 0
269 alines = mdiff.splitnewlines(aa)
268 alines = mdiff.splitnewlines(aa)
270 matches = bdiff.blocks(aa, rr)
269 matches = bdiff.blocks(aa, rr)
271 for x1,x2,y1,y2 in matches:
270 for x1,x2,y1,y2 in matches:
272 for line in alines[x1:x2]:
271 for line in alines[x1:x2]:
273 equal += len(line)
272 equal += len(line)
274
273
275 lengths = len(aa) + len(rr)
274 lengths = len(aa) + len(rr)
276 if lengths:
275 if lengths:
277 myscore = equal*2.0 / lengths
276 myscore = equal*2.0 / lengths
278 if myscore >= bestscore:
277 if myscore >= bestscore:
279 bestname, bestscore = r, myscore
278 bestname, bestscore = r, myscore
280 if bestname:
279 if bestname:
281 yield bestname, a, bestscore
280 yield bestname, a, bestscore
282
281
283 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
282 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
284 if dry_run is None:
283 if dry_run is None:
285 dry_run = opts.get('dry_run')
284 dry_run = opts.get('dry_run')
286 if similarity is None:
285 if similarity is None:
287 similarity = float(opts.get('similarity') or 0)
286 similarity = float(opts.get('similarity') or 0)
288 add, remove = [], []
287 add, remove = [], []
289 audit_path = util.path_auditor(repo.root)
288 audit_path = util.path_auditor(repo.root)
290 m = match(repo, pats, opts)
289 m = match(repo, pats, opts)
291 for abs in repo.walk(m):
290 for abs in repo.walk(m):
292 target = repo.wjoin(abs)
291 target = repo.wjoin(abs)
293 good = True
292 good = True
294 try:
293 try:
295 audit_path(abs)
294 audit_path(abs)
296 except:
295 except:
297 good = False
296 good = False
298 rel = m.rel(abs)
297 rel = m.rel(abs)
299 exact = m.exact(abs)
298 exact = m.exact(abs)
300 if good and abs not in repo.dirstate:
299 if good and abs not in repo.dirstate:
301 add.append(abs)
300 add.append(abs)
302 if repo.ui.verbose or not exact:
301 if repo.ui.verbose or not exact:
303 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
302 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
304 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
303 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
305 or (os.path.isdir(target) and not os.path.islink(target))):
304 or (os.path.isdir(target) and not os.path.islink(target))):
306 remove.append(abs)
305 remove.append(abs)
307 if repo.ui.verbose or not exact:
306 if repo.ui.verbose or not exact:
308 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
307 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
309 if not dry_run:
308 if not dry_run:
310 repo.remove(remove)
309 repo.remove(remove)
311 repo.add(add)
310 repo.add(add)
312 if similarity > 0:
311 if similarity > 0:
313 for old, new, score in findrenames(repo, add, remove, similarity):
312 for old, new, score in findrenames(repo, m, similarity):
314 oldexact, newexact = m.exact(old), m.exact(new)
313 oldexact, newexact = m.exact(old), m.exact(new)
315 if repo.ui.verbose or not oldexact or not newexact:
314 if repo.ui.verbose or not oldexact or not newexact:
316 oldrel, newrel = m.rel(old), m.rel(new)
315 oldrel, newrel = m.rel(old), m.rel(new)
317 repo.ui.status(_('recording removal of %s as rename to %s '
316 repo.ui.status(_('recording removal of %s as rename to %s '
318 '(%d%% similar)\n') %
317 '(%d%% similar)\n') %
319 (oldrel, newrel, score * 100))
318 (oldrel, newrel, score * 100))
320 if not dry_run:
319 if not dry_run:
321 repo.copy(old, new)
320 repo.copy(old, new)
322
321
323 def copy(ui, repo, pats, opts, rename=False):
322 def copy(ui, repo, pats, opts, rename=False):
324 # called with the repo lock held
323 # called with the repo lock held
325 #
324 #
326 # hgsep => pathname that uses "/" to separate directories
325 # hgsep => pathname that uses "/" to separate directories
327 # ossep => pathname that uses os.sep to separate directories
326 # ossep => pathname that uses os.sep to separate directories
328 cwd = repo.getcwd()
327 cwd = repo.getcwd()
329 targets = {}
328 targets = {}
330 after = opts.get("after")
329 after = opts.get("after")
331 dryrun = opts.get("dry_run")
330 dryrun = opts.get("dry_run")
332
331
333 def walkpat(pat):
332 def walkpat(pat):
334 srcs = []
333 srcs = []
335 m = match(repo, [pat], opts, globbed=True)
334 m = match(repo, [pat], opts, globbed=True)
336 for abs in repo.walk(m):
335 for abs in repo.walk(m):
337 state = repo.dirstate[abs]
336 state = repo.dirstate[abs]
338 rel = m.rel(abs)
337 rel = m.rel(abs)
339 exact = m.exact(abs)
338 exact = m.exact(abs)
340 if state in '?r':
339 if state in '?r':
341 if exact and state == '?':
340 if exact and state == '?':
342 ui.warn(_('%s: not copying - file is not managed\n') % rel)
341 ui.warn(_('%s: not copying - file is not managed\n') % rel)
343 if exact and state == 'r':
342 if exact and state == 'r':
344 ui.warn(_('%s: not copying - file has been marked for'
343 ui.warn(_('%s: not copying - file has been marked for'
345 ' remove\n') % rel)
344 ' remove\n') % rel)
346 continue
345 continue
347 # abs: hgsep
346 # abs: hgsep
348 # rel: ossep
347 # rel: ossep
349 srcs.append((abs, rel, exact))
348 srcs.append((abs, rel, exact))
350 return srcs
349 return srcs
351
350
352 # abssrc: hgsep
351 # abssrc: hgsep
353 # relsrc: ossep
352 # relsrc: ossep
354 # otarget: ossep
353 # otarget: ossep
355 def copyfile(abssrc, relsrc, otarget, exact):
354 def copyfile(abssrc, relsrc, otarget, exact):
356 abstarget = util.canonpath(repo.root, cwd, otarget)
355 abstarget = util.canonpath(repo.root, cwd, otarget)
357 reltarget = repo.pathto(abstarget, cwd)
356 reltarget = repo.pathto(abstarget, cwd)
358 target = repo.wjoin(abstarget)
357 target = repo.wjoin(abstarget)
359 src = repo.wjoin(abssrc)
358 src = repo.wjoin(abssrc)
360 state = repo.dirstate[abstarget]
359 state = repo.dirstate[abstarget]
361
360
362 # check for collisions
361 # check for collisions
363 prevsrc = targets.get(abstarget)
362 prevsrc = targets.get(abstarget)
364 if prevsrc is not None:
363 if prevsrc is not None:
365 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
364 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
366 (reltarget, repo.pathto(abssrc, cwd),
365 (reltarget, repo.pathto(abssrc, cwd),
367 repo.pathto(prevsrc, cwd)))
366 repo.pathto(prevsrc, cwd)))
368 return
367 return
369
368
370 # check for overwrites
369 # check for overwrites
371 exists = os.path.exists(target)
370 exists = os.path.exists(target)
372 if not after and exists or after and state in 'mn':
371 if not after and exists or after and state in 'mn':
373 if not opts['force']:
372 if not opts['force']:
374 ui.warn(_('%s: not overwriting - file exists\n') %
373 ui.warn(_('%s: not overwriting - file exists\n') %
375 reltarget)
374 reltarget)
376 return
375 return
377
376
378 if after:
377 if after:
379 if not exists:
378 if not exists:
380 return
379 return
381 elif not dryrun:
380 elif not dryrun:
382 try:
381 try:
383 if exists:
382 if exists:
384 os.unlink(target)
383 os.unlink(target)
385 targetdir = os.path.dirname(target) or '.'
384 targetdir = os.path.dirname(target) or '.'
386 if not os.path.isdir(targetdir):
385 if not os.path.isdir(targetdir):
387 os.makedirs(targetdir)
386 os.makedirs(targetdir)
388 util.copyfile(src, target)
387 util.copyfile(src, target)
389 except IOError, inst:
388 except IOError, inst:
390 if inst.errno == errno.ENOENT:
389 if inst.errno == errno.ENOENT:
391 ui.warn(_('%s: deleted in working copy\n') % relsrc)
390 ui.warn(_('%s: deleted in working copy\n') % relsrc)
392 else:
391 else:
393 ui.warn(_('%s: cannot copy - %s\n') %
392 ui.warn(_('%s: cannot copy - %s\n') %
394 (relsrc, inst.strerror))
393 (relsrc, inst.strerror))
395 return True # report a failure
394 return True # report a failure
396
395
397 if ui.verbose or not exact:
396 if ui.verbose or not exact:
398 if rename:
397 if rename:
399 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
398 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
400 else:
399 else:
401 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
400 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
402
401
403 targets[abstarget] = abssrc
402 targets[abstarget] = abssrc
404
403
405 # fix up dirstate
404 # fix up dirstate
406 origsrc = repo.dirstate.copied(abssrc) or abssrc
405 origsrc = repo.dirstate.copied(abssrc) or abssrc
407 if abstarget == origsrc: # copying back a copy?
406 if abstarget == origsrc: # copying back a copy?
408 if state not in 'mn' and not dryrun:
407 if state not in 'mn' and not dryrun:
409 repo.dirstate.normallookup(abstarget)
408 repo.dirstate.normallookup(abstarget)
410 else:
409 else:
411 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
410 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
412 if not ui.quiet:
411 if not ui.quiet:
413 ui.warn(_("%s has not been committed yet, so no copy "
412 ui.warn(_("%s has not been committed yet, so no copy "
414 "data will be stored for %s.\n")
413 "data will be stored for %s.\n")
415 % (repo.pathto(origsrc, cwd), reltarget))
414 % (repo.pathto(origsrc, cwd), reltarget))
416 if repo.dirstate[abstarget] in '?r' and not dryrun:
415 if repo.dirstate[abstarget] in '?r' and not dryrun:
417 repo.add([abstarget])
416 repo.add([abstarget])
418 elif not dryrun:
417 elif not dryrun:
419 repo.copy(origsrc, abstarget)
418 repo.copy(origsrc, abstarget)
420
419
421 if rename and not dryrun:
420 if rename and not dryrun:
422 repo.remove([abssrc], not after)
421 repo.remove([abssrc], not after)
423
422
424 # pat: ossep
423 # pat: ossep
425 # dest ossep
424 # dest ossep
426 # srcs: list of (hgsep, hgsep, ossep, bool)
425 # srcs: list of (hgsep, hgsep, ossep, bool)
427 # return: function that takes hgsep and returns ossep
426 # return: function that takes hgsep and returns ossep
428 def targetpathfn(pat, dest, srcs):
427 def targetpathfn(pat, dest, srcs):
429 if os.path.isdir(pat):
428 if os.path.isdir(pat):
430 abspfx = util.canonpath(repo.root, cwd, pat)
429 abspfx = util.canonpath(repo.root, cwd, pat)
431 abspfx = util.localpath(abspfx)
430 abspfx = util.localpath(abspfx)
432 if destdirexists:
431 if destdirexists:
433 striplen = len(os.path.split(abspfx)[0])
432 striplen = len(os.path.split(abspfx)[0])
434 else:
433 else:
435 striplen = len(abspfx)
434 striplen = len(abspfx)
436 if striplen:
435 if striplen:
437 striplen += len(os.sep)
436 striplen += len(os.sep)
438 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
437 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
439 elif destdirexists:
438 elif destdirexists:
440 res = lambda p: os.path.join(dest,
439 res = lambda p: os.path.join(dest,
441 os.path.basename(util.localpath(p)))
440 os.path.basename(util.localpath(p)))
442 else:
441 else:
443 res = lambda p: dest
442 res = lambda p: dest
444 return res
443 return res
445
444
446 # pat: ossep
445 # pat: ossep
447 # dest ossep
446 # dest ossep
448 # srcs: list of (hgsep, hgsep, ossep, bool)
447 # srcs: list of (hgsep, hgsep, ossep, bool)
449 # return: function that takes hgsep and returns ossep
448 # return: function that takes hgsep and returns ossep
450 def targetpathafterfn(pat, dest, srcs):
449 def targetpathafterfn(pat, dest, srcs):
451 if util.patkind(pat, None)[0]:
450 if util.patkind(pat, None)[0]:
452 # a mercurial pattern
451 # a mercurial pattern
453 res = lambda p: os.path.join(dest,
452 res = lambda p: os.path.join(dest,
454 os.path.basename(util.localpath(p)))
453 os.path.basename(util.localpath(p)))
455 else:
454 else:
456 abspfx = util.canonpath(repo.root, cwd, pat)
455 abspfx = util.canonpath(repo.root, cwd, pat)
457 if len(abspfx) < len(srcs[0][0]):
456 if len(abspfx) < len(srcs[0][0]):
458 # A directory. Either the target path contains the last
457 # A directory. Either the target path contains the last
459 # component of the source path or it does not.
458 # component of the source path or it does not.
460 def evalpath(striplen):
459 def evalpath(striplen):
461 score = 0
460 score = 0
462 for s in srcs:
461 for s in srcs:
463 t = os.path.join(dest, util.localpath(s[0])[striplen:])
462 t = os.path.join(dest, util.localpath(s[0])[striplen:])
464 if os.path.exists(t):
463 if os.path.exists(t):
465 score += 1
464 score += 1
466 return score
465 return score
467
466
468 abspfx = util.localpath(abspfx)
467 abspfx = util.localpath(abspfx)
469 striplen = len(abspfx)
468 striplen = len(abspfx)
470 if striplen:
469 if striplen:
471 striplen += len(os.sep)
470 striplen += len(os.sep)
472 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
471 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
473 score = evalpath(striplen)
472 score = evalpath(striplen)
474 striplen1 = len(os.path.split(abspfx)[0])
473 striplen1 = len(os.path.split(abspfx)[0])
475 if striplen1:
474 if striplen1:
476 striplen1 += len(os.sep)
475 striplen1 += len(os.sep)
477 if evalpath(striplen1) > score:
476 if evalpath(striplen1) > score:
478 striplen = striplen1
477 striplen = striplen1
479 res = lambda p: os.path.join(dest,
478 res = lambda p: os.path.join(dest,
480 util.localpath(p)[striplen:])
479 util.localpath(p)[striplen:])
481 else:
480 else:
482 # a file
481 # a file
483 if destdirexists:
482 if destdirexists:
484 res = lambda p: os.path.join(dest,
483 res = lambda p: os.path.join(dest,
485 os.path.basename(util.localpath(p)))
484 os.path.basename(util.localpath(p)))
486 else:
485 else:
487 res = lambda p: dest
486 res = lambda p: dest
488 return res
487 return res
489
488
490
489
491 pats = util.expand_glob(pats)
490 pats = util.expand_glob(pats)
492 if not pats:
491 if not pats:
493 raise util.Abort(_('no source or destination specified'))
492 raise util.Abort(_('no source or destination specified'))
494 if len(pats) == 1:
493 if len(pats) == 1:
495 raise util.Abort(_('no destination specified'))
494 raise util.Abort(_('no destination specified'))
496 dest = pats.pop()
495 dest = pats.pop()
497 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
496 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
498 if not destdirexists:
497 if not destdirexists:
499 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
498 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
500 raise util.Abort(_('with multiple sources, destination must be an '
499 raise util.Abort(_('with multiple sources, destination must be an '
501 'existing directory'))
500 'existing directory'))
502 if util.endswithsep(dest):
501 if util.endswithsep(dest):
503 raise util.Abort(_('destination %s is not a directory') % dest)
502 raise util.Abort(_('destination %s is not a directory') % dest)
504
503
505 tfn = targetpathfn
504 tfn = targetpathfn
506 if after:
505 if after:
507 tfn = targetpathafterfn
506 tfn = targetpathafterfn
508 copylist = []
507 copylist = []
509 for pat in pats:
508 for pat in pats:
510 srcs = walkpat(pat)
509 srcs = walkpat(pat)
511 if not srcs:
510 if not srcs:
512 continue
511 continue
513 copylist.append((tfn(pat, dest, srcs), srcs))
512 copylist.append((tfn(pat, dest, srcs), srcs))
514 if not copylist:
513 if not copylist:
515 raise util.Abort(_('no files to copy'))
514 raise util.Abort(_('no files to copy'))
516
515
517 errors = 0
516 errors = 0
518 for targetpath, srcs in copylist:
517 for targetpath, srcs in copylist:
519 for abssrc, relsrc, exact in srcs:
518 for abssrc, relsrc, exact in srcs:
520 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
519 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
521 errors += 1
520 errors += 1
522
521
523 if errors:
522 if errors:
524 ui.warn(_('(consider using --after)\n'))
523 ui.warn(_('(consider using --after)\n'))
525
524
526 return errors
525 return errors
527
526
528 def service(opts, parentfn=None, initfn=None, runfn=None):
527 def service(opts, parentfn=None, initfn=None, runfn=None):
529 '''Run a command as a service.'''
528 '''Run a command as a service.'''
530
529
531 if opts['daemon'] and not opts['daemon_pipefds']:
530 if opts['daemon'] and not opts['daemon_pipefds']:
532 rfd, wfd = os.pipe()
531 rfd, wfd = os.pipe()
533 args = sys.argv[:]
532 args = sys.argv[:]
534 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
533 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
535 # Don't pass --cwd to the child process, because we've already
534 # Don't pass --cwd to the child process, because we've already
536 # changed directory.
535 # changed directory.
537 for i in xrange(1,len(args)):
536 for i in xrange(1,len(args)):
538 if args[i].startswith('--cwd='):
537 if args[i].startswith('--cwd='):
539 del args[i]
538 del args[i]
540 break
539 break
541 elif args[i].startswith('--cwd'):
540 elif args[i].startswith('--cwd'):
542 del args[i:i+2]
541 del args[i:i+2]
543 break
542 break
544 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
543 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
545 args[0], args)
544 args[0], args)
546 os.close(wfd)
545 os.close(wfd)
547 os.read(rfd, 1)
546 os.read(rfd, 1)
548 if parentfn:
547 if parentfn:
549 return parentfn(pid)
548 return parentfn(pid)
550 else:
549 else:
551 os._exit(0)
550 os._exit(0)
552
551
553 if initfn:
552 if initfn:
554 initfn()
553 initfn()
555
554
556 if opts['pid_file']:
555 if opts['pid_file']:
557 fp = open(opts['pid_file'], 'w')
556 fp = open(opts['pid_file'], 'w')
558 fp.write(str(os.getpid()) + '\n')
557 fp.write(str(os.getpid()) + '\n')
559 fp.close()
558 fp.close()
560
559
561 if opts['daemon_pipefds']:
560 if opts['daemon_pipefds']:
562 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
561 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
563 os.close(rfd)
562 os.close(rfd)
564 try:
563 try:
565 os.setsid()
564 os.setsid()
566 except AttributeError:
565 except AttributeError:
567 pass
566 pass
568 os.write(wfd, 'y')
567 os.write(wfd, 'y')
569 os.close(wfd)
568 os.close(wfd)
570 sys.stdout.flush()
569 sys.stdout.flush()
571 sys.stderr.flush()
570 sys.stderr.flush()
572 fd = os.open(util.nulldev, os.O_RDWR)
571 fd = os.open(util.nulldev, os.O_RDWR)
573 if fd != 0: os.dup2(fd, 0)
572 if fd != 0: os.dup2(fd, 0)
574 if fd != 1: os.dup2(fd, 1)
573 if fd != 1: os.dup2(fd, 1)
575 if fd != 2: os.dup2(fd, 2)
574 if fd != 2: os.dup2(fd, 2)
576 if fd not in (0, 1, 2): os.close(fd)
575 if fd not in (0, 1, 2): os.close(fd)
577
576
578 if runfn:
577 if runfn:
579 return runfn()
578 return runfn()
580
579
581 class changeset_printer(object):
580 class changeset_printer(object):
582 '''show changeset information when templating not requested.'''
581 '''show changeset information when templating not requested.'''
583
582
584 def __init__(self, ui, repo, patch, diffopts, buffered):
583 def __init__(self, ui, repo, patch, diffopts, buffered):
585 self.ui = ui
584 self.ui = ui
586 self.repo = repo
585 self.repo = repo
587 self.buffered = buffered
586 self.buffered = buffered
588 self.patch = patch
587 self.patch = patch
589 self.diffopts = diffopts
588 self.diffopts = diffopts
590 self.header = {}
589 self.header = {}
591 self.hunk = {}
590 self.hunk = {}
592 self.lastheader = None
591 self.lastheader = None
593
592
594 def flush(self, rev):
593 def flush(self, rev):
595 if rev in self.header:
594 if rev in self.header:
596 h = self.header[rev]
595 h = self.header[rev]
597 if h != self.lastheader:
596 if h != self.lastheader:
598 self.lastheader = h
597 self.lastheader = h
599 self.ui.write(h)
598 self.ui.write(h)
600 del self.header[rev]
599 del self.header[rev]
601 if rev in self.hunk:
600 if rev in self.hunk:
602 self.ui.write(self.hunk[rev])
601 self.ui.write(self.hunk[rev])
603 del self.hunk[rev]
602 del self.hunk[rev]
604 return 1
603 return 1
605 return 0
604 return 0
606
605
607 def show(self, ctx, copies=(), **props):
606 def show(self, ctx, copies=(), **props):
608 if self.buffered:
607 if self.buffered:
609 self.ui.pushbuffer()
608 self.ui.pushbuffer()
610 self._show(ctx, copies, props)
609 self._show(ctx, copies, props)
611 self.hunk[ctx.rev()] = self.ui.popbuffer()
610 self.hunk[ctx.rev()] = self.ui.popbuffer()
612 else:
611 else:
613 self._show(ctx, copies, props)
612 self._show(ctx, copies, props)
614
613
615 def _show(self, ctx, copies, props):
614 def _show(self, ctx, copies, props):
616 '''show a single changeset or file revision'''
615 '''show a single changeset or file revision'''
617 changenode = ctx.node()
616 changenode = ctx.node()
618 rev = ctx.rev()
617 rev = ctx.rev()
619
618
620 if self.ui.quiet:
619 if self.ui.quiet:
621 self.ui.write("%d:%s\n" % (rev, short(changenode)))
620 self.ui.write("%d:%s\n" % (rev, short(changenode)))
622 return
621 return
623
622
624 log = self.repo.changelog
623 log = self.repo.changelog
625 changes = log.read(changenode)
624 changes = log.read(changenode)
626 date = util.datestr(changes[2])
625 date = util.datestr(changes[2])
627 extra = changes[5]
626 extra = changes[5]
628 branch = extra.get("branch")
627 branch = extra.get("branch")
629
628
630 hexfunc = self.ui.debugflag and hex or short
629 hexfunc = self.ui.debugflag and hex or short
631
630
632 parents = [(p, hexfunc(log.node(p)))
631 parents = [(p, hexfunc(log.node(p)))
633 for p in self._meaningful_parentrevs(log, rev)]
632 for p in self._meaningful_parentrevs(log, rev)]
634
633
635 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
634 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
636
635
637 # don't show the default branch name
636 # don't show the default branch name
638 if branch != 'default':
637 if branch != 'default':
639 branch = encoding.tolocal(branch)
638 branch = encoding.tolocal(branch)
640 self.ui.write(_("branch: %s\n") % branch)
639 self.ui.write(_("branch: %s\n") % branch)
641 for tag in self.repo.nodetags(changenode):
640 for tag in self.repo.nodetags(changenode):
642 self.ui.write(_("tag: %s\n") % tag)
641 self.ui.write(_("tag: %s\n") % tag)
643 for parent in parents:
642 for parent in parents:
644 self.ui.write(_("parent: %d:%s\n") % parent)
643 self.ui.write(_("parent: %d:%s\n") % parent)
645
644
646 if self.ui.debugflag:
645 if self.ui.debugflag:
647 self.ui.write(_("manifest: %d:%s\n") %
646 self.ui.write(_("manifest: %d:%s\n") %
648 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
647 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
649 self.ui.write(_("user: %s\n") % changes[1])
648 self.ui.write(_("user: %s\n") % changes[1])
650 self.ui.write(_("date: %s\n") % date)
649 self.ui.write(_("date: %s\n") % date)
651
650
652 if self.ui.debugflag:
651 if self.ui.debugflag:
653 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
652 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
654 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
653 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
655 files):
654 files):
656 if value:
655 if value:
657 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
656 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
658 elif changes[3] and self.ui.verbose:
657 elif changes[3] and self.ui.verbose:
659 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
658 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
660 if copies and self.ui.verbose:
659 if copies and self.ui.verbose:
661 copies = ['%s (%s)' % c for c in copies]
660 copies = ['%s (%s)' % c for c in copies]
662 self.ui.write(_("copies: %s\n") % ' '.join(copies))
661 self.ui.write(_("copies: %s\n") % ' '.join(copies))
663
662
664 if extra and self.ui.debugflag:
663 if extra and self.ui.debugflag:
665 for key, value in sorted(extra.items()):
664 for key, value in sorted(extra.items()):
666 self.ui.write(_("extra: %s=%s\n")
665 self.ui.write(_("extra: %s=%s\n")
667 % (key, value.encode('string_escape')))
666 % (key, value.encode('string_escape')))
668
667
669 description = changes[4].strip()
668 description = changes[4].strip()
670 if description:
669 if description:
671 if self.ui.verbose:
670 if self.ui.verbose:
672 self.ui.write(_("description:\n"))
671 self.ui.write(_("description:\n"))
673 self.ui.write(description)
672 self.ui.write(description)
674 self.ui.write("\n\n")
673 self.ui.write("\n\n")
675 else:
674 else:
676 self.ui.write(_("summary: %s\n") %
675 self.ui.write(_("summary: %s\n") %
677 description.splitlines()[0])
676 description.splitlines()[0])
678 self.ui.write("\n")
677 self.ui.write("\n")
679
678
680 self.showpatch(changenode)
679 self.showpatch(changenode)
681
680
682 def showpatch(self, node):
681 def showpatch(self, node):
683 if self.patch:
682 if self.patch:
684 prev = self.repo.changelog.parents(node)[0]
683 prev = self.repo.changelog.parents(node)[0]
685 chunks = patch.diff(self.repo, prev, node, match=self.patch,
684 chunks = patch.diff(self.repo, prev, node, match=self.patch,
686 opts=patch.diffopts(self.ui, self.diffopts))
685 opts=patch.diffopts(self.ui, self.diffopts))
687 for chunk in chunks:
686 for chunk in chunks:
688 self.ui.write(chunk)
687 self.ui.write(chunk)
689 self.ui.write("\n")
688 self.ui.write("\n")
690
689
691 def _meaningful_parentrevs(self, log, rev):
690 def _meaningful_parentrevs(self, log, rev):
692 """Return list of meaningful (or all if debug) parentrevs for rev.
691 """Return list of meaningful (or all if debug) parentrevs for rev.
693
692
694 For merges (two non-nullrev revisions) both parents are meaningful.
693 For merges (two non-nullrev revisions) both parents are meaningful.
695 Otherwise the first parent revision is considered meaningful if it
694 Otherwise the first parent revision is considered meaningful if it
696 is not the preceding revision.
695 is not the preceding revision.
697 """
696 """
698 parents = log.parentrevs(rev)
697 parents = log.parentrevs(rev)
699 if not self.ui.debugflag and parents[1] == nullrev:
698 if not self.ui.debugflag and parents[1] == nullrev:
700 if parents[0] >= rev - 1:
699 if parents[0] >= rev - 1:
701 parents = []
700 parents = []
702 else:
701 else:
703 parents = [parents[0]]
702 parents = [parents[0]]
704 return parents
703 return parents
705
704
706
705
707 class changeset_templater(changeset_printer):
706 class changeset_templater(changeset_printer):
708 '''format changeset information.'''
707 '''format changeset information.'''
709
708
710 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
709 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
711 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
710 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
712 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
711 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
713 self.t = templater.templater(mapfile, {'formatnode': formatnode},
712 self.t = templater.templater(mapfile, {'formatnode': formatnode},
714 cache={
713 cache={
715 'parent': '{rev}:{node|formatnode} ',
714 'parent': '{rev}:{node|formatnode} ',
716 'manifest': '{rev}:{node|formatnode}',
715 'manifest': '{rev}:{node|formatnode}',
717 'filecopy': '{name} ({source})'})
716 'filecopy': '{name} ({source})'})
718
717
719 def use_template(self, t):
718 def use_template(self, t):
720 '''set template string to use'''
719 '''set template string to use'''
721 self.t.cache['changeset'] = t
720 self.t.cache['changeset'] = t
722
721
723 def _meaningful_parentrevs(self, ctx):
722 def _meaningful_parentrevs(self, ctx):
724 """Return list of meaningful (or all if debug) parentrevs for rev.
723 """Return list of meaningful (or all if debug) parentrevs for rev.
725 """
724 """
726 parents = ctx.parents()
725 parents = ctx.parents()
727 if len(parents) > 1:
726 if len(parents) > 1:
728 return parents
727 return parents
729 if self.ui.debugflag:
728 if self.ui.debugflag:
730 return [parents[0], self.repo['null']]
729 return [parents[0], self.repo['null']]
731 if parents[0].rev() >= ctx.rev() - 1:
730 if parents[0].rev() >= ctx.rev() - 1:
732 return []
731 return []
733 return parents
732 return parents
734
733
735 def _show(self, ctx, copies, props):
734 def _show(self, ctx, copies, props):
736 '''show a single changeset or file revision'''
735 '''show a single changeset or file revision'''
737
736
738 def showlist(name, values, plural=None, **args):
737 def showlist(name, values, plural=None, **args):
739 '''expand set of values.
738 '''expand set of values.
740 name is name of key in template map.
739 name is name of key in template map.
741 values is list of strings or dicts.
740 values is list of strings or dicts.
742 plural is plural of name, if not simply name + 's'.
741 plural is plural of name, if not simply name + 's'.
743
742
744 expansion works like this, given name 'foo'.
743 expansion works like this, given name 'foo'.
745
744
746 if values is empty, expand 'no_foos'.
745 if values is empty, expand 'no_foos'.
747
746
748 if 'foo' not in template map, return values as a string,
747 if 'foo' not in template map, return values as a string,
749 joined by space.
748 joined by space.
750
749
751 expand 'start_foos'.
750 expand 'start_foos'.
752
751
753 for each value, expand 'foo'. if 'last_foo' in template
752 for each value, expand 'foo'. if 'last_foo' in template
754 map, expand it instead of 'foo' for last key.
753 map, expand it instead of 'foo' for last key.
755
754
756 expand 'end_foos'.
755 expand 'end_foos'.
757 '''
756 '''
758 if plural: names = plural
757 if plural: names = plural
759 else: names = name + 's'
758 else: names = name + 's'
760 if not values:
759 if not values:
761 noname = 'no_' + names
760 noname = 'no_' + names
762 if noname in self.t:
761 if noname in self.t:
763 yield self.t(noname, **args)
762 yield self.t(noname, **args)
764 return
763 return
765 if name not in self.t:
764 if name not in self.t:
766 if isinstance(values[0], str):
765 if isinstance(values[0], str):
767 yield ' '.join(values)
766 yield ' '.join(values)
768 else:
767 else:
769 for v in values:
768 for v in values:
770 yield dict(v, **args)
769 yield dict(v, **args)
771 return
770 return
772 startname = 'start_' + names
771 startname = 'start_' + names
773 if startname in self.t:
772 if startname in self.t:
774 yield self.t(startname, **args)
773 yield self.t(startname, **args)
775 vargs = args.copy()
774 vargs = args.copy()
776 def one(v, tag=name):
775 def one(v, tag=name):
777 try:
776 try:
778 vargs.update(v)
777 vargs.update(v)
779 except (AttributeError, ValueError):
778 except (AttributeError, ValueError):
780 try:
779 try:
781 for a, b in v:
780 for a, b in v:
782 vargs[a] = b
781 vargs[a] = b
783 except ValueError:
782 except ValueError:
784 vargs[name] = v
783 vargs[name] = v
785 return self.t(tag, **vargs)
784 return self.t(tag, **vargs)
786 lastname = 'last_' + name
785 lastname = 'last_' + name
787 if lastname in self.t:
786 if lastname in self.t:
788 last = values.pop()
787 last = values.pop()
789 else:
788 else:
790 last = None
789 last = None
791 for v in values:
790 for v in values:
792 yield one(v)
791 yield one(v)
793 if last is not None:
792 if last is not None:
794 yield one(last, tag=lastname)
793 yield one(last, tag=lastname)
795 endname = 'end_' + names
794 endname = 'end_' + names
796 if endname in self.t:
795 if endname in self.t:
797 yield self.t(endname, **args)
796 yield self.t(endname, **args)
798
797
799 def showbranches(**args):
798 def showbranches(**args):
800 branch = ctx.branch()
799 branch = ctx.branch()
801 if branch != 'default':
800 if branch != 'default':
802 branch = encoding.tolocal(branch)
801 branch = encoding.tolocal(branch)
803 return showlist('branch', [branch], plural='branches', **args)
802 return showlist('branch', [branch], plural='branches', **args)
804
803
805 def showparents(**args):
804 def showparents(**args):
806 parents = [[('rev', p.rev()), ('node', p.hex())]
805 parents = [[('rev', p.rev()), ('node', p.hex())]
807 for p in self._meaningful_parentrevs(ctx)]
806 for p in self._meaningful_parentrevs(ctx)]
808 return showlist('parent', parents, **args)
807 return showlist('parent', parents, **args)
809
808
810 def showtags(**args):
809 def showtags(**args):
811 return showlist('tag', ctx.tags(), **args)
810 return showlist('tag', ctx.tags(), **args)
812
811
813 def showextras(**args):
812 def showextras(**args):
814 for key, value in sorted(ctx.extra().items()):
813 for key, value in sorted(ctx.extra().items()):
815 args = args.copy()
814 args = args.copy()
816 args.update(dict(key=key, value=value))
815 args.update(dict(key=key, value=value))
817 yield self.t('extra', **args)
816 yield self.t('extra', **args)
818
817
819 def showcopies(**args):
818 def showcopies(**args):
820 c = [{'name': x[0], 'source': x[1]} for x in copies]
819 c = [{'name': x[0], 'source': x[1]} for x in copies]
821 return showlist('file_copy', c, plural='file_copies', **args)
820 return showlist('file_copy', c, plural='file_copies', **args)
822
821
823 files = []
822 files = []
824 def getfiles():
823 def getfiles():
825 if not files:
824 if not files:
826 files[:] = self.repo.status(ctx.parents()[0].node(),
825 files[:] = self.repo.status(ctx.parents()[0].node(),
827 ctx.node())[:3]
826 ctx.node())[:3]
828 return files
827 return files
829 def showfiles(**args):
828 def showfiles(**args):
830 return showlist('file', ctx.files(), **args)
829 return showlist('file', ctx.files(), **args)
831 def showmods(**args):
830 def showmods(**args):
832 return showlist('file_mod', getfiles()[0], **args)
831 return showlist('file_mod', getfiles()[0], **args)
833 def showadds(**args):
832 def showadds(**args):
834 return showlist('file_add', getfiles()[1], **args)
833 return showlist('file_add', getfiles()[1], **args)
835 def showdels(**args):
834 def showdels(**args):
836 return showlist('file_del', getfiles()[2], **args)
835 return showlist('file_del', getfiles()[2], **args)
837 def showmanifest(**args):
836 def showmanifest(**args):
838 args = args.copy()
837 args = args.copy()
839 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
838 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
840 node=hex(ctx.changeset()[0])))
839 node=hex(ctx.changeset()[0])))
841 return self.t('manifest', **args)
840 return self.t('manifest', **args)
842
841
843 def showdiffstat(**args):
842 def showdiffstat(**args):
844 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
843 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
845 files, adds, removes = 0, 0, 0
844 files, adds, removes = 0, 0, 0
846 for i in patch.diffstatdata(util.iterlines(diff)):
845 for i in patch.diffstatdata(util.iterlines(diff)):
847 files += 1
846 files += 1
848 adds += i[1]
847 adds += i[1]
849 removes += i[2]
848 removes += i[2]
850 return '%s: +%s/-%s' % (files, adds, removes)
849 return '%s: +%s/-%s' % (files, adds, removes)
851
850
852 defprops = {
851 defprops = {
853 'author': ctx.user(),
852 'author': ctx.user(),
854 'branches': showbranches,
853 'branches': showbranches,
855 'date': ctx.date(),
854 'date': ctx.date(),
856 'desc': ctx.description().strip(),
855 'desc': ctx.description().strip(),
857 'file_adds': showadds,
856 'file_adds': showadds,
858 'file_dels': showdels,
857 'file_dels': showdels,
859 'file_mods': showmods,
858 'file_mods': showmods,
860 'files': showfiles,
859 'files': showfiles,
861 'file_copies': showcopies,
860 'file_copies': showcopies,
862 'manifest': showmanifest,
861 'manifest': showmanifest,
863 'node': ctx.hex(),
862 'node': ctx.hex(),
864 'parents': showparents,
863 'parents': showparents,
865 'rev': ctx.rev(),
864 'rev': ctx.rev(),
866 'tags': showtags,
865 'tags': showtags,
867 'extras': showextras,
866 'extras': showextras,
868 'diffstat': showdiffstat,
867 'diffstat': showdiffstat,
869 }
868 }
870 props = props.copy()
869 props = props.copy()
871 props.update(defprops)
870 props.update(defprops)
872
871
873 # find correct templates for current mode
872 # find correct templates for current mode
874
873
875 tmplmodes = [
874 tmplmodes = [
876 (True, None),
875 (True, None),
877 (self.ui.verbose, 'verbose'),
876 (self.ui.verbose, 'verbose'),
878 (self.ui.quiet, 'quiet'),
877 (self.ui.quiet, 'quiet'),
879 (self.ui.debugflag, 'debug'),
878 (self.ui.debugflag, 'debug'),
880 ]
879 ]
881
880
882 types = {'header': '', 'changeset': 'changeset'}
881 types = {'header': '', 'changeset': 'changeset'}
883 for mode, postfix in tmplmodes:
882 for mode, postfix in tmplmodes:
884 for type in types:
883 for type in types:
885 cur = postfix and ('%s_%s' % (type, postfix)) or type
884 cur = postfix and ('%s_%s' % (type, postfix)) or type
886 if mode and cur in self.t:
885 if mode and cur in self.t:
887 types[type] = cur
886 types[type] = cur
888
887
889 try:
888 try:
890
889
891 # write header
890 # write header
892 if types['header']:
891 if types['header']:
893 h = templater.stringify(self.t(types['header'], **props))
892 h = templater.stringify(self.t(types['header'], **props))
894 if self.buffered:
893 if self.buffered:
895 self.header[ctx.rev()] = h
894 self.header[ctx.rev()] = h
896 else:
895 else:
897 self.ui.write(h)
896 self.ui.write(h)
898
897
899 # write changeset metadata, then patch if requested
898 # write changeset metadata, then patch if requested
900 key = types['changeset']
899 key = types['changeset']
901 self.ui.write(templater.stringify(self.t(key, **props)))
900 self.ui.write(templater.stringify(self.t(key, **props)))
902 self.showpatch(ctx.node())
901 self.showpatch(ctx.node())
903
902
904 except KeyError, inst:
903 except KeyError, inst:
905 msg = _("%s: no key named '%s'")
904 msg = _("%s: no key named '%s'")
906 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
905 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
907 except SyntaxError, inst:
906 except SyntaxError, inst:
908 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
907 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
909
908
910 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
909 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
911 """show one changeset using template or regular display.
910 """show one changeset using template or regular display.
912
911
913 Display format will be the first non-empty hit of:
912 Display format will be the first non-empty hit of:
914 1. option 'template'
913 1. option 'template'
915 2. option 'style'
914 2. option 'style'
916 3. [ui] setting 'logtemplate'
915 3. [ui] setting 'logtemplate'
917 4. [ui] setting 'style'
916 4. [ui] setting 'style'
918 If all of these values are either the unset or the empty string,
917 If all of these values are either the unset or the empty string,
919 regular display via changeset_printer() is done.
918 regular display via changeset_printer() is done.
920 """
919 """
921 # options
920 # options
922 patch = False
921 patch = False
923 if opts.get('patch'):
922 if opts.get('patch'):
924 patch = matchfn or matchall(repo)
923 patch = matchfn or matchall(repo)
925
924
926 tmpl = opts.get('template')
925 tmpl = opts.get('template')
927 style = None
926 style = None
928 if tmpl:
927 if tmpl:
929 tmpl = templater.parsestring(tmpl, quoted=False)
928 tmpl = templater.parsestring(tmpl, quoted=False)
930 else:
929 else:
931 style = opts.get('style')
930 style = opts.get('style')
932
931
933 # ui settings
932 # ui settings
934 if not (tmpl or style):
933 if not (tmpl or style):
935 tmpl = ui.config('ui', 'logtemplate')
934 tmpl = ui.config('ui', 'logtemplate')
936 if tmpl:
935 if tmpl:
937 tmpl = templater.parsestring(tmpl)
936 tmpl = templater.parsestring(tmpl)
938 else:
937 else:
939 style = ui.config('ui', 'style')
938 style = ui.config('ui', 'style')
940
939
941 if not (tmpl or style):
940 if not (tmpl or style):
942 return changeset_printer(ui, repo, patch, opts, buffered)
941 return changeset_printer(ui, repo, patch, opts, buffered)
943
942
944 mapfile = None
943 mapfile = None
945 if style and not tmpl:
944 if style and not tmpl:
946 mapfile = style
945 mapfile = style
947 if not os.path.split(mapfile)[0]:
946 if not os.path.split(mapfile)[0]:
948 mapname = (templater.templatepath('map-cmdline.' + mapfile)
947 mapname = (templater.templatepath('map-cmdline.' + mapfile)
949 or templater.templatepath(mapfile))
948 or templater.templatepath(mapfile))
950 if mapname: mapfile = mapname
949 if mapname: mapfile = mapname
951
950
952 try:
951 try:
953 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
952 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
954 except SyntaxError, inst:
953 except SyntaxError, inst:
955 raise util.Abort(inst.args[0])
954 raise util.Abort(inst.args[0])
956 if tmpl: t.use_template(tmpl)
955 if tmpl: t.use_template(tmpl)
957 return t
956 return t
958
957
959 def finddate(ui, repo, date):
958 def finddate(ui, repo, date):
960 """Find the tipmost changeset that matches the given date spec"""
959 """Find the tipmost changeset that matches the given date spec"""
961 df = util.matchdate(date)
960 df = util.matchdate(date)
962 get = util.cachefunc(lambda r: repo[r].changeset())
961 get = util.cachefunc(lambda r: repo[r].changeset())
963 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
962 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
964 results = {}
963 results = {}
965 for st, rev, fns in changeiter:
964 for st, rev, fns in changeiter:
966 if st == 'add':
965 if st == 'add':
967 d = get(rev)[2]
966 d = get(rev)[2]
968 if df(d[0]):
967 if df(d[0]):
969 results[rev] = d
968 results[rev] = d
970 elif st == 'iter':
969 elif st == 'iter':
971 if rev in results:
970 if rev in results:
972 ui.status(_("Found revision %s from %s\n") %
971 ui.status(_("Found revision %s from %s\n") %
973 (rev, util.datestr(results[rev])))
972 (rev, util.datestr(results[rev])))
974 return str(rev)
973 return str(rev)
975
974
976 raise util.Abort(_("revision matching date not found"))
975 raise util.Abort(_("revision matching date not found"))
977
976
978 def walkchangerevs(ui, repo, pats, change, opts):
977 def walkchangerevs(ui, repo, pats, change, opts):
979 '''Iterate over files and the revs in which they changed.
978 '''Iterate over files and the revs in which they changed.
980
979
981 Callers most commonly need to iterate backwards over the history
980 Callers most commonly need to iterate backwards over the history
982 in which they are interested. Doing so has awful (quadratic-looking)
981 in which they are interested. Doing so has awful (quadratic-looking)
983 performance, so we use iterators in a "windowed" way.
982 performance, so we use iterators in a "windowed" way.
984
983
985 We walk a window of revisions in the desired order. Within the
984 We walk a window of revisions in the desired order. Within the
986 window, we first walk forwards to gather data, then in the desired
985 window, we first walk forwards to gather data, then in the desired
987 order (usually backwards) to display it.
986 order (usually backwards) to display it.
988
987
989 This function returns an (iterator, matchfn) tuple. The iterator
988 This function returns an (iterator, matchfn) tuple. The iterator
990 yields 3-tuples. They will be of one of the following forms:
989 yields 3-tuples. They will be of one of the following forms:
991
990
992 "window", incrementing, lastrev: stepping through a window,
991 "window", incrementing, lastrev: stepping through a window,
993 positive if walking forwards through revs, last rev in the
992 positive if walking forwards through revs, last rev in the
994 sequence iterated over - use to reset state for the current window
993 sequence iterated over - use to reset state for the current window
995
994
996 "add", rev, fns: out-of-order traversal of the given file names
995 "add", rev, fns: out-of-order traversal of the given file names
997 fns, which changed during revision rev - use to gather data for
996 fns, which changed during revision rev - use to gather data for
998 possible display
997 possible display
999
998
1000 "iter", rev, None: in-order traversal of the revs earlier iterated
999 "iter", rev, None: in-order traversal of the revs earlier iterated
1001 over with "add" - use to display data'''
1000 over with "add" - use to display data'''
1002
1001
1003 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1002 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1004 if start < end:
1003 if start < end:
1005 while start < end:
1004 while start < end:
1006 yield start, min(windowsize, end-start)
1005 yield start, min(windowsize, end-start)
1007 start += windowsize
1006 start += windowsize
1008 if windowsize < sizelimit:
1007 if windowsize < sizelimit:
1009 windowsize *= 2
1008 windowsize *= 2
1010 else:
1009 else:
1011 while start > end:
1010 while start > end:
1012 yield start, min(windowsize, start-end-1)
1011 yield start, min(windowsize, start-end-1)
1013 start -= windowsize
1012 start -= windowsize
1014 if windowsize < sizelimit:
1013 if windowsize < sizelimit:
1015 windowsize *= 2
1014 windowsize *= 2
1016
1015
1017 m = match(repo, pats, opts)
1016 m = match(repo, pats, opts)
1018 follow = opts.get('follow') or opts.get('follow_first')
1017 follow = opts.get('follow') or opts.get('follow_first')
1019
1018
1020 if not len(repo):
1019 if not len(repo):
1021 return [], m
1020 return [], m
1022
1021
1023 if follow:
1022 if follow:
1024 defrange = '%s:0' % repo['.'].rev()
1023 defrange = '%s:0' % repo['.'].rev()
1025 else:
1024 else:
1026 defrange = '-1:0'
1025 defrange = '-1:0'
1027 revs = revrange(repo, opts['rev'] or [defrange])
1026 revs = revrange(repo, opts['rev'] or [defrange])
1028 wanted = set()
1027 wanted = set()
1029 slowpath = m.anypats() or (m.files() and opts.get('removed'))
1028 slowpath = m.anypats() or (m.files() and opts.get('removed'))
1030 fncache = {}
1029 fncache = {}
1031
1030
1032 if not slowpath and not m.files():
1031 if not slowpath and not m.files():
1033 # No files, no patterns. Display all revs.
1032 # No files, no patterns. Display all revs.
1034 wanted = set(revs)
1033 wanted = set(revs)
1035 copies = []
1034 copies = []
1036 if not slowpath:
1035 if not slowpath:
1037 # Only files, no patterns. Check the history of each file.
1036 # Only files, no patterns. Check the history of each file.
1038 def filerevgen(filelog, node):
1037 def filerevgen(filelog, node):
1039 cl_count = len(repo)
1038 cl_count = len(repo)
1040 if node is None:
1039 if node is None:
1041 last = len(filelog) - 1
1040 last = len(filelog) - 1
1042 else:
1041 else:
1043 last = filelog.rev(node)
1042 last = filelog.rev(node)
1044 for i, window in increasing_windows(last, nullrev):
1043 for i, window in increasing_windows(last, nullrev):
1045 revs = []
1044 revs = []
1046 for j in xrange(i - window, i + 1):
1045 for j in xrange(i - window, i + 1):
1047 n = filelog.node(j)
1046 n = filelog.node(j)
1048 revs.append((filelog.linkrev(j),
1047 revs.append((filelog.linkrev(j),
1049 follow and filelog.renamed(n)))
1048 follow and filelog.renamed(n)))
1050 for rev in reversed(revs):
1049 for rev in reversed(revs):
1051 # only yield rev for which we have the changelog, it can
1050 # only yield rev for which we have the changelog, it can
1052 # happen while doing "hg log" during a pull or commit
1051 # happen while doing "hg log" during a pull or commit
1053 if rev[0] < cl_count:
1052 if rev[0] < cl_count:
1054 yield rev
1053 yield rev
1055 def iterfiles():
1054 def iterfiles():
1056 for filename in m.files():
1055 for filename in m.files():
1057 yield filename, None
1056 yield filename, None
1058 for filename_node in copies:
1057 for filename_node in copies:
1059 yield filename_node
1058 yield filename_node
1060 minrev, maxrev = min(revs), max(revs)
1059 minrev, maxrev = min(revs), max(revs)
1061 for file_, node in iterfiles():
1060 for file_, node in iterfiles():
1062 filelog = repo.file(file_)
1061 filelog = repo.file(file_)
1063 if not len(filelog):
1062 if not len(filelog):
1064 if node is None:
1063 if node is None:
1065 # A zero count may be a directory or deleted file, so
1064 # A zero count may be a directory or deleted file, so
1066 # try to find matching entries on the slow path.
1065 # try to find matching entries on the slow path.
1067 if follow:
1066 if follow:
1068 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1067 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1069 slowpath = True
1068 slowpath = True
1070 break
1069 break
1071 else:
1070 else:
1072 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1071 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1073 % (file_, short(node)))
1072 % (file_, short(node)))
1074 continue
1073 continue
1075 for rev, copied in filerevgen(filelog, node):
1074 for rev, copied in filerevgen(filelog, node):
1076 if rev <= maxrev:
1075 if rev <= maxrev:
1077 if rev < minrev:
1076 if rev < minrev:
1078 break
1077 break
1079 fncache.setdefault(rev, [])
1078 fncache.setdefault(rev, [])
1080 fncache[rev].append(file_)
1079 fncache[rev].append(file_)
1081 wanted.add(rev)
1080 wanted.add(rev)
1082 if follow and copied:
1081 if follow and copied:
1083 copies.append(copied)
1082 copies.append(copied)
1084 if slowpath:
1083 if slowpath:
1085 if follow:
1084 if follow:
1086 raise util.Abort(_('can only follow copies/renames for explicit '
1085 raise util.Abort(_('can only follow copies/renames for explicit '
1087 'file names'))
1086 'file names'))
1088
1087
1089 # The slow path checks files modified in every changeset.
1088 # The slow path checks files modified in every changeset.
1090 def changerevgen():
1089 def changerevgen():
1091 for i, window in increasing_windows(len(repo) - 1, nullrev):
1090 for i, window in increasing_windows(len(repo) - 1, nullrev):
1092 for j in xrange(i - window, i + 1):
1091 for j in xrange(i - window, i + 1):
1093 yield j, change(j)[3]
1092 yield j, change(j)[3]
1094
1093
1095 for rev, changefiles in changerevgen():
1094 for rev, changefiles in changerevgen():
1096 matches = filter(m, changefiles)
1095 matches = filter(m, changefiles)
1097 if matches:
1096 if matches:
1098 fncache[rev] = matches
1097 fncache[rev] = matches
1099 wanted.add(rev)
1098 wanted.add(rev)
1100
1099
1101 class followfilter:
1100 class followfilter:
1102 def __init__(self, onlyfirst=False):
1101 def __init__(self, onlyfirst=False):
1103 self.startrev = nullrev
1102 self.startrev = nullrev
1104 self.roots = []
1103 self.roots = []
1105 self.onlyfirst = onlyfirst
1104 self.onlyfirst = onlyfirst
1106
1105
1107 def match(self, rev):
1106 def match(self, rev):
1108 def realparents(rev):
1107 def realparents(rev):
1109 if self.onlyfirst:
1108 if self.onlyfirst:
1110 return repo.changelog.parentrevs(rev)[0:1]
1109 return repo.changelog.parentrevs(rev)[0:1]
1111 else:
1110 else:
1112 return filter(lambda x: x != nullrev,
1111 return filter(lambda x: x != nullrev,
1113 repo.changelog.parentrevs(rev))
1112 repo.changelog.parentrevs(rev))
1114
1113
1115 if self.startrev == nullrev:
1114 if self.startrev == nullrev:
1116 self.startrev = rev
1115 self.startrev = rev
1117 return True
1116 return True
1118
1117
1119 if rev > self.startrev:
1118 if rev > self.startrev:
1120 # forward: all descendants
1119 # forward: all descendants
1121 if not self.roots:
1120 if not self.roots:
1122 self.roots.append(self.startrev)
1121 self.roots.append(self.startrev)
1123 for parent in realparents(rev):
1122 for parent in realparents(rev):
1124 if parent in self.roots:
1123 if parent in self.roots:
1125 self.roots.append(rev)
1124 self.roots.append(rev)
1126 return True
1125 return True
1127 else:
1126 else:
1128 # backwards: all parents
1127 # backwards: all parents
1129 if not self.roots:
1128 if not self.roots:
1130 self.roots.extend(realparents(self.startrev))
1129 self.roots.extend(realparents(self.startrev))
1131 if rev in self.roots:
1130 if rev in self.roots:
1132 self.roots.remove(rev)
1131 self.roots.remove(rev)
1133 self.roots.extend(realparents(rev))
1132 self.roots.extend(realparents(rev))
1134 return True
1133 return True
1135
1134
1136 return False
1135 return False
1137
1136
1138 # it might be worthwhile to do this in the iterator if the rev range
1137 # it might be worthwhile to do this in the iterator if the rev range
1139 # is descending and the prune args are all within that range
1138 # is descending and the prune args are all within that range
1140 for rev in opts.get('prune', ()):
1139 for rev in opts.get('prune', ()):
1141 rev = repo.changelog.rev(repo.lookup(rev))
1140 rev = repo.changelog.rev(repo.lookup(rev))
1142 ff = followfilter()
1141 ff = followfilter()
1143 stop = min(revs[0], revs[-1])
1142 stop = min(revs[0], revs[-1])
1144 for x in xrange(rev, stop-1, -1):
1143 for x in xrange(rev, stop-1, -1):
1145 if ff.match(x):
1144 if ff.match(x):
1146 wanted.discard(x)
1145 wanted.discard(x)
1147
1146
1148 def iterate():
1147 def iterate():
1149 if follow and not m.files():
1148 if follow and not m.files():
1150 ff = followfilter(onlyfirst=opts.get('follow_first'))
1149 ff = followfilter(onlyfirst=opts.get('follow_first'))
1151 def want(rev):
1150 def want(rev):
1152 return ff.match(rev) and rev in wanted
1151 return ff.match(rev) and rev in wanted
1153 else:
1152 else:
1154 def want(rev):
1153 def want(rev):
1155 return rev in wanted
1154 return rev in wanted
1156
1155
1157 for i, window in increasing_windows(0, len(revs)):
1156 for i, window in increasing_windows(0, len(revs)):
1158 yield 'window', revs[0] < revs[-1], revs[-1]
1157 yield 'window', revs[0] < revs[-1], revs[-1]
1159 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1158 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1160 for rev in sorted(nrevs):
1159 for rev in sorted(nrevs):
1161 fns = fncache.get(rev)
1160 fns = fncache.get(rev)
1162 if not fns:
1161 if not fns:
1163 def fns_generator():
1162 def fns_generator():
1164 for f in change(rev)[3]:
1163 for f in change(rev)[3]:
1165 if m(f):
1164 if m(f):
1166 yield f
1165 yield f
1167 fns = fns_generator()
1166 fns = fns_generator()
1168 yield 'add', rev, fns
1167 yield 'add', rev, fns
1169 for rev in nrevs:
1168 for rev in nrevs:
1170 yield 'iter', rev, None
1169 yield 'iter', rev, None
1171 return iterate(), m
1170 return iterate(), m
1172
1171
1173 def commit(ui, repo, commitfunc, pats, opts):
1172 def commit(ui, repo, commitfunc, pats, opts):
1174 '''commit the specified files or all outstanding changes'''
1173 '''commit the specified files or all outstanding changes'''
1175 date = opts.get('date')
1174 date = opts.get('date')
1176 if date:
1175 if date:
1177 opts['date'] = util.parsedate(date)
1176 opts['date'] = util.parsedate(date)
1178 message = logmessage(opts)
1177 message = logmessage(opts)
1179
1178
1180 # extract addremove carefully -- this function can be called from a command
1179 # extract addremove carefully -- this function can be called from a command
1181 # that doesn't support addremove
1180 # that doesn't support addremove
1182 if opts.get('addremove'):
1181 if opts.get('addremove'):
1183 addremove(repo, pats, opts)
1182 addremove(repo, pats, opts)
1184
1183
1185 m = match(repo, pats, opts)
1184 m = match(repo, pats, opts)
1186 if pats:
1185 if pats:
1187 modified, added, removed = repo.status(match=m)[:3]
1186 modified, added, removed = repo.status(match=m)[:3]
1188 files = sorted(modified + added + removed)
1187 files = sorted(modified + added + removed)
1189
1188
1190 def is_dir(f):
1189 def is_dir(f):
1191 name = f + '/'
1190 name = f + '/'
1192 i = bisect.bisect(files, name)
1191 i = bisect.bisect(files, name)
1193 return i < len(files) and files[i].startswith(name)
1192 return i < len(files) and files[i].startswith(name)
1194
1193
1195 for f in m.files():
1194 for f in m.files():
1196 if f == '.':
1195 if f == '.':
1197 continue
1196 continue
1198 if f not in files:
1197 if f not in files:
1199 rf = repo.wjoin(f)
1198 rf = repo.wjoin(f)
1200 rel = repo.pathto(f)
1199 rel = repo.pathto(f)
1201 try:
1200 try:
1202 mode = os.lstat(rf)[stat.ST_MODE]
1201 mode = os.lstat(rf)[stat.ST_MODE]
1203 except OSError:
1202 except OSError:
1204 if is_dir(f): # deleted directory ?
1203 if is_dir(f): # deleted directory ?
1205 continue
1204 continue
1206 raise util.Abort(_("file %s not found!") % rel)
1205 raise util.Abort(_("file %s not found!") % rel)
1207 if stat.S_ISDIR(mode):
1206 if stat.S_ISDIR(mode):
1208 if not is_dir(f):
1207 if not is_dir(f):
1209 raise util.Abort(_("no match under directory %s!")
1208 raise util.Abort(_("no match under directory %s!")
1210 % rel)
1209 % rel)
1211 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1210 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1212 raise util.Abort(_("can't commit %s: "
1211 raise util.Abort(_("can't commit %s: "
1213 "unsupported file type!") % rel)
1212 "unsupported file type!") % rel)
1214 elif f not in repo.dirstate:
1213 elif f not in repo.dirstate:
1215 raise util.Abort(_("file %s not tracked!") % rel)
1214 raise util.Abort(_("file %s not tracked!") % rel)
1216 m = matchfiles(repo, files)
1215 m = matchfiles(repo, files)
1217 try:
1216 try:
1218 return commitfunc(ui, repo, message, m, opts)
1217 return commitfunc(ui, repo, message, m, opts)
1219 except ValueError, inst:
1218 except ValueError, inst:
1220 raise util.Abort(str(inst))
1219 raise util.Abort(str(inst))
1221
1220
1222 def commiteditor(repo, ctx, added, updated, removed):
1221 def commiteditor(repo, ctx, added, updated, removed):
1223 if ctx.description():
1222 if ctx.description():
1224 return ctx.description()
1223 return ctx.description()
1225 return commitforceeditor(repo, ctx, added, updated, removed)
1224 return commitforceeditor(repo, ctx, added, updated, removed)
1226
1225
1227 def commitforceeditor(repo, ctx, added, updated, removed):
1226 def commitforceeditor(repo, ctx, added, updated, removed):
1228 edittext = []
1227 edittext = []
1229 if ctx.description():
1228 if ctx.description():
1230 edittext.append(ctx.description())
1229 edittext.append(ctx.description())
1231 edittext.append("")
1230 edittext.append("")
1232 edittext.append("") # Empty line between message and comments.
1231 edittext.append("") # Empty line between message and comments.
1233 edittext.append(_("HG: Enter commit message."
1232 edittext.append(_("HG: Enter commit message."
1234 " Lines beginning with 'HG:' are removed."))
1233 " Lines beginning with 'HG:' are removed."))
1235 edittext.append("HG: --")
1234 edittext.append("HG: --")
1236 edittext.append(_("HG: user: %s") % ctx.user())
1235 edittext.append(_("HG: user: %s") % ctx.user())
1237 if ctx.p2():
1236 if ctx.p2():
1238 edittext.append(_("HG: branch merge"))
1237 edittext.append(_("HG: branch merge"))
1239 if ctx.branch():
1238 if ctx.branch():
1240 edittext.append(_("HG: branch '%s'")
1239 edittext.append(_("HG: branch '%s'")
1241 % encoding.tolocal(ctx.branch()))
1240 % encoding.tolocal(ctx.branch()))
1242 edittext.extend([_("HG: added %s") % f for f in added])
1241 edittext.extend([_("HG: added %s") % f for f in added])
1243 edittext.extend([_("HG: changed %s") % f for f in updated])
1242 edittext.extend([_("HG: changed %s") % f for f in updated])
1244 edittext.extend([_("HG: removed %s") % f for f in removed])
1243 edittext.extend([_("HG: removed %s") % f for f in removed])
1245 if not added and not updated and not removed:
1244 if not added and not updated and not removed:
1246 edittext.append(_("HG: no files changed"))
1245 edittext.append(_("HG: no files changed"))
1247 edittext.append("")
1246 edittext.append("")
1248 # run editor in the repository root
1247 # run editor in the repository root
1249 olddir = os.getcwd()
1248 olddir = os.getcwd()
1250 os.chdir(repo.root)
1249 os.chdir(repo.root)
1251 text = repo.ui.edit("\n".join(edittext), ctx.user())
1250 text = repo.ui.edit("\n".join(edittext), ctx.user())
1252 text = re.sub("(?m)^HG:.*\n", "", text)
1251 text = re.sub("(?m)^HG:.*\n", "", text)
1253 os.chdir(olddir)
1252 os.chdir(olddir)
1254
1253
1255 if not text.strip():
1254 if not text.strip():
1256 raise util.Abort(_("empty commit message"))
1255 raise util.Abort(_("empty commit message"))
1257
1256
1258 return text
1257 return text
@@ -1,49 +1,67 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 hg init rep; cd rep
3 hg init rep; cd rep
4
4
5 touch empty-file
5 touch empty-file
6 python -c 'for x in range(10000): print x' > large-file
6 python -c 'for x in range(10000): print x' > large-file
7
7
8 hg addremove
8 hg addremove
9
9
10 hg commit -m A
10 hg commit -m A
11
11
12 rm large-file empty-file
12 rm large-file empty-file
13 python -c 'for x in range(10,10000): print x' > another-file
13 python -c 'for x in range(10,10000): print x' > another-file
14
14
15 hg addremove -s50
15 hg addremove -s50
16
16
17 hg commit -m B
17 hg commit -m B
18
18
19 echo % comparing two empty files caused ZeroDivisionError in the past
19 echo % comparing two empty files caused ZeroDivisionError in the past
20 hg update -C 0
20 hg update -C 0
21 rm empty-file
21 rm empty-file
22 touch another-empty-file
22 touch another-empty-file
23 hg addremove -s50
23 hg addremove -s50
24
24
25 cd ..
25 cd ..
26
26
27 hg init rep2; cd rep2
27 hg init rep2; cd rep2
28
28
29 python -c 'for x in range(10000): print x' > large-file
29 python -c 'for x in range(10000): print x' > large-file
30 python -c 'for x in range(50): print x' > tiny-file
30 python -c 'for x in range(50): print x' > tiny-file
31
31
32 hg addremove
32 hg addremove
33
33
34 hg commit -m A
34 hg commit -m A
35
35
36 python -c 'for x in range(70): print x' > small-file
36 python -c 'for x in range(70): print x' > small-file
37 rm tiny-file
37 rm tiny-file
38 rm large-file
38 rm large-file
39
39
40 hg addremove -s50
40 hg addremove -s50
41
41
42 hg commit -m B
42 hg commit -m B
43
43
44 echo % should all fail
44 echo % should all fail
45 hg addremove -s foo
45 hg addremove -s foo
46 hg addremove -s -1
46 hg addremove -s -1
47 hg addremove -s 1e6
47 hg addremove -s 1e6
48
48
49 cd ..
50
51 echo '% issue 1527'
52 hg init rep3; cd rep3
53 mkdir d
54 echo a > d/a
55 hg add d/a
56 hg commit -m 1
57
58 mv d/a d/b
59 hg addremove -s80
60 hg debugstate
61 mv d/b c
62 echo "% no copies found here (since the target isn't in d"
63 hg addremove -s80 d
64 echo "% copies here"
65 hg addremove -s80
66
49 true
67 true
@@ -1,20 +1,32 b''
1 adding empty-file
1 adding empty-file
2 adding large-file
2 adding large-file
3 adding another-file
3 adding another-file
4 removing empty-file
4 removing empty-file
5 removing large-file
5 removing large-file
6 recording removal of large-file as rename to another-file (99% similar)
6 recording removal of large-file as rename to another-file (99% similar)
7 % comparing two empty files caused ZeroDivisionError in the past
7 % comparing two empty files caused ZeroDivisionError in the past
8 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
8 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
9 adding another-empty-file
9 adding another-empty-file
10 removing empty-file
10 removing empty-file
11 adding large-file
11 adding large-file
12 adding tiny-file
12 adding tiny-file
13 removing large-file
13 removing large-file
14 adding small-file
14 adding small-file
15 removing tiny-file
15 removing tiny-file
16 recording removal of tiny-file as rename to small-file (82% similar)
16 recording removal of tiny-file as rename to small-file (82% similar)
17 % should all fail
17 % should all fail
18 abort: similarity must be a number
18 abort: similarity must be a number
19 abort: similarity must be between 0 and 100
19 abort: similarity must be between 0 and 100
20 abort: similarity must be between 0 and 100
20 abort: similarity must be between 0 and 100
21 % issue 1527
22 removing d/a
23 adding d/b
24 recording removal of d/a as rename to d/b (100% similar)
25 r 0 0 1970-01-01 00:00:00 d/a
26 a 0 -1 unset d/b
27 copy: d/a -> d/b
28 % no copies found here (since the target isn't in d
29 removing d/b
30 % copies here
31 adding c
32 recording removal of d/a as rename to c (100% similar)
General Comments 0
You need to be logged in to leave comments. Login now