##// END OF EJS Templates
commit: fix unmodified message detection for the "--- >8 ----" magic...
Yuya Nishihara -
r30724:ee47e951 default
parent child Browse files
Show More
@@ -1,3474 +1,3475 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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import os
11 import os
12 import re
12 import re
13 import tempfile
13 import tempfile
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 bin,
17 bin,
18 hex,
18 hex,
19 nullid,
19 nullid,
20 nullrev,
20 nullrev,
21 short,
21 short,
22 )
22 )
23
23
24 from . import (
24 from . import (
25 bookmarks,
25 bookmarks,
26 changelog,
26 changelog,
27 copies,
27 copies,
28 crecord as crecordmod,
28 crecord as crecordmod,
29 dirstateguard as dirstateguardmod,
29 dirstateguard as dirstateguardmod,
30 encoding,
30 encoding,
31 error,
31 error,
32 formatter,
32 formatter,
33 graphmod,
33 graphmod,
34 lock as lockmod,
34 lock as lockmod,
35 match as matchmod,
35 match as matchmod,
36 mergeutil,
36 mergeutil,
37 obsolete,
37 obsolete,
38 patch,
38 patch,
39 pathutil,
39 pathutil,
40 phases,
40 phases,
41 pycompat,
41 pycompat,
42 repair,
42 repair,
43 revlog,
43 revlog,
44 revset,
44 revset,
45 scmutil,
45 scmutil,
46 templatekw,
46 templatekw,
47 templater,
47 templater,
48 util,
48 util,
49 )
49 )
50 stringio = util.stringio
50 stringio = util.stringio
51
51
52 # special string such that everything below this line will be ingored in the
52 # special string such that everything below this line will be ingored in the
53 # editor text
53 # editor text
54 _linebelow = "^HG: ------------------------ >8 ------------------------$"
54 _linebelow = "^HG: ------------------------ >8 ------------------------$"
55
55
56 def ishunk(x):
56 def ishunk(x):
57 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
57 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
58 return isinstance(x, hunkclasses)
58 return isinstance(x, hunkclasses)
59
59
60 def newandmodified(chunks, originalchunks):
60 def newandmodified(chunks, originalchunks):
61 newlyaddedandmodifiedfiles = set()
61 newlyaddedandmodifiedfiles = set()
62 for chunk in chunks:
62 for chunk in chunks:
63 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
63 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
64 originalchunks:
64 originalchunks:
65 newlyaddedandmodifiedfiles.add(chunk.header.filename())
65 newlyaddedandmodifiedfiles.add(chunk.header.filename())
66 return newlyaddedandmodifiedfiles
66 return newlyaddedandmodifiedfiles
67
67
68 def parsealiases(cmd):
68 def parsealiases(cmd):
69 return cmd.lstrip("^").split("|")
69 return cmd.lstrip("^").split("|")
70
70
71 def setupwrapcolorwrite(ui):
71 def setupwrapcolorwrite(ui):
72 # wrap ui.write so diff output can be labeled/colorized
72 # wrap ui.write so diff output can be labeled/colorized
73 def wrapwrite(orig, *args, **kw):
73 def wrapwrite(orig, *args, **kw):
74 label = kw.pop('label', '')
74 label = kw.pop('label', '')
75 for chunk, l in patch.difflabel(lambda: args):
75 for chunk, l in patch.difflabel(lambda: args):
76 orig(chunk, label=label + l)
76 orig(chunk, label=label + l)
77
77
78 oldwrite = ui.write
78 oldwrite = ui.write
79 def wrap(*args, **kwargs):
79 def wrap(*args, **kwargs):
80 return wrapwrite(oldwrite, *args, **kwargs)
80 return wrapwrite(oldwrite, *args, **kwargs)
81 setattr(ui, 'write', wrap)
81 setattr(ui, 'write', wrap)
82 return oldwrite
82 return oldwrite
83
83
84 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
84 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
85 if usecurses:
85 if usecurses:
86 if testfile:
86 if testfile:
87 recordfn = crecordmod.testdecorator(testfile,
87 recordfn = crecordmod.testdecorator(testfile,
88 crecordmod.testchunkselector)
88 crecordmod.testchunkselector)
89 else:
89 else:
90 recordfn = crecordmod.chunkselector
90 recordfn = crecordmod.chunkselector
91
91
92 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
92 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
93
93
94 else:
94 else:
95 return patch.filterpatch(ui, originalhunks, operation)
95 return patch.filterpatch(ui, originalhunks, operation)
96
96
97 def recordfilter(ui, originalhunks, operation=None):
97 def recordfilter(ui, originalhunks, operation=None):
98 """ Prompts the user to filter the originalhunks and return a list of
98 """ Prompts the user to filter the originalhunks and return a list of
99 selected hunks.
99 selected hunks.
100 *operation* is used for to build ui messages to indicate the user what
100 *operation* is used for to build ui messages to indicate the user what
101 kind of filtering they are doing: reverting, committing, shelving, etc.
101 kind of filtering they are doing: reverting, committing, shelving, etc.
102 (see patch.filterpatch).
102 (see patch.filterpatch).
103 """
103 """
104 usecurses = crecordmod.checkcurses(ui)
104 usecurses = crecordmod.checkcurses(ui)
105 testfile = ui.config('experimental', 'crecordtest', None)
105 testfile = ui.config('experimental', 'crecordtest', None)
106 oldwrite = setupwrapcolorwrite(ui)
106 oldwrite = setupwrapcolorwrite(ui)
107 try:
107 try:
108 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
108 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
109 testfile, operation)
109 testfile, operation)
110 finally:
110 finally:
111 ui.write = oldwrite
111 ui.write = oldwrite
112 return newchunks, newopts
112 return newchunks, newopts
113
113
114 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
114 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
115 filterfn, *pats, **opts):
115 filterfn, *pats, **opts):
116 from . import merge as mergemod
116 from . import merge as mergemod
117 if not ui.interactive():
117 if not ui.interactive():
118 if cmdsuggest:
118 if cmdsuggest:
119 msg = _('running non-interactively, use %s instead') % cmdsuggest
119 msg = _('running non-interactively, use %s instead') % cmdsuggest
120 else:
120 else:
121 msg = _('running non-interactively')
121 msg = _('running non-interactively')
122 raise error.Abort(msg)
122 raise error.Abort(msg)
123
123
124 # make sure username is set before going interactive
124 # make sure username is set before going interactive
125 if not opts.get('user'):
125 if not opts.get('user'):
126 ui.username() # raise exception, username not provided
126 ui.username() # raise exception, username not provided
127
127
128 def recordfunc(ui, repo, message, match, opts):
128 def recordfunc(ui, repo, message, match, opts):
129 """This is generic record driver.
129 """This is generic record driver.
130
130
131 Its job is to interactively filter local changes, and
131 Its job is to interactively filter local changes, and
132 accordingly prepare working directory into a state in which the
132 accordingly prepare working directory into a state in which the
133 job can be delegated to a non-interactive commit command such as
133 job can be delegated to a non-interactive commit command such as
134 'commit' or 'qrefresh'.
134 'commit' or 'qrefresh'.
135
135
136 After the actual job is done by non-interactive command, the
136 After the actual job is done by non-interactive command, the
137 working directory is restored to its original state.
137 working directory is restored to its original state.
138
138
139 In the end we'll record interesting changes, and everything else
139 In the end we'll record interesting changes, and everything else
140 will be left in place, so the user can continue working.
140 will be left in place, so the user can continue working.
141 """
141 """
142
142
143 checkunfinished(repo, commit=True)
143 checkunfinished(repo, commit=True)
144 wctx = repo[None]
144 wctx = repo[None]
145 merge = len(wctx.parents()) > 1
145 merge = len(wctx.parents()) > 1
146 if merge:
146 if merge:
147 raise error.Abort(_('cannot partially commit a merge '
147 raise error.Abort(_('cannot partially commit a merge '
148 '(use "hg commit" instead)'))
148 '(use "hg commit" instead)'))
149
149
150 def fail(f, msg):
150 def fail(f, msg):
151 raise error.Abort('%s: %s' % (f, msg))
151 raise error.Abort('%s: %s' % (f, msg))
152
152
153 force = opts.get('force')
153 force = opts.get('force')
154 if not force:
154 if not force:
155 vdirs = []
155 vdirs = []
156 match.explicitdir = vdirs.append
156 match.explicitdir = vdirs.append
157 match.bad = fail
157 match.bad = fail
158
158
159 status = repo.status(match=match)
159 status = repo.status(match=match)
160 if not force:
160 if not force:
161 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
161 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
162 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
162 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
163 diffopts.nodates = True
163 diffopts.nodates = True
164 diffopts.git = True
164 diffopts.git = True
165 diffopts.showfunc = True
165 diffopts.showfunc = True
166 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
166 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
167 originalchunks = patch.parsepatch(originaldiff)
167 originalchunks = patch.parsepatch(originaldiff)
168
168
169 # 1. filter patch, since we are intending to apply subset of it
169 # 1. filter patch, since we are intending to apply subset of it
170 try:
170 try:
171 chunks, newopts = filterfn(ui, originalchunks)
171 chunks, newopts = filterfn(ui, originalchunks)
172 except patch.PatchError as err:
172 except patch.PatchError as err:
173 raise error.Abort(_('error parsing patch: %s') % err)
173 raise error.Abort(_('error parsing patch: %s') % err)
174 opts.update(newopts)
174 opts.update(newopts)
175
175
176 # We need to keep a backup of files that have been newly added and
176 # We need to keep a backup of files that have been newly added and
177 # modified during the recording process because there is a previous
177 # modified during the recording process because there is a previous
178 # version without the edit in the workdir
178 # version without the edit in the workdir
179 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
179 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
180 contenders = set()
180 contenders = set()
181 for h in chunks:
181 for h in chunks:
182 try:
182 try:
183 contenders.update(set(h.files()))
183 contenders.update(set(h.files()))
184 except AttributeError:
184 except AttributeError:
185 pass
185 pass
186
186
187 changed = status.modified + status.added + status.removed
187 changed = status.modified + status.added + status.removed
188 newfiles = [f for f in changed if f in contenders]
188 newfiles = [f for f in changed if f in contenders]
189 if not newfiles:
189 if not newfiles:
190 ui.status(_('no changes to record\n'))
190 ui.status(_('no changes to record\n'))
191 return 0
191 return 0
192
192
193 modified = set(status.modified)
193 modified = set(status.modified)
194
194
195 # 2. backup changed files, so we can restore them in the end
195 # 2. backup changed files, so we can restore them in the end
196
196
197 if backupall:
197 if backupall:
198 tobackup = changed
198 tobackup = changed
199 else:
199 else:
200 tobackup = [f for f in newfiles if f in modified or f in \
200 tobackup = [f for f in newfiles if f in modified or f in \
201 newlyaddedandmodifiedfiles]
201 newlyaddedandmodifiedfiles]
202 backups = {}
202 backups = {}
203 if tobackup:
203 if tobackup:
204 backupdir = repo.join('record-backups')
204 backupdir = repo.join('record-backups')
205 try:
205 try:
206 os.mkdir(backupdir)
206 os.mkdir(backupdir)
207 except OSError as err:
207 except OSError as err:
208 if err.errno != errno.EEXIST:
208 if err.errno != errno.EEXIST:
209 raise
209 raise
210 try:
210 try:
211 # backup continues
211 # backup continues
212 for f in tobackup:
212 for f in tobackup:
213 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
213 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
214 dir=backupdir)
214 dir=backupdir)
215 os.close(fd)
215 os.close(fd)
216 ui.debug('backup %r as %r\n' % (f, tmpname))
216 ui.debug('backup %r as %r\n' % (f, tmpname))
217 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
217 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
218 backups[f] = tmpname
218 backups[f] = tmpname
219
219
220 fp = stringio()
220 fp = stringio()
221 for c in chunks:
221 for c in chunks:
222 fname = c.filename()
222 fname = c.filename()
223 if fname in backups:
223 if fname in backups:
224 c.write(fp)
224 c.write(fp)
225 dopatch = fp.tell()
225 dopatch = fp.tell()
226 fp.seek(0)
226 fp.seek(0)
227
227
228 # 2.5 optionally review / modify patch in text editor
228 # 2.5 optionally review / modify patch in text editor
229 if opts.get('review', False):
229 if opts.get('review', False):
230 patchtext = (crecordmod.diffhelptext
230 patchtext = (crecordmod.diffhelptext
231 + crecordmod.patchhelptext
231 + crecordmod.patchhelptext
232 + fp.read())
232 + fp.read())
233 reviewedpatch = ui.edit(patchtext, "",
233 reviewedpatch = ui.edit(patchtext, "",
234 extra={"suffix": ".diff"})
234 extra={"suffix": ".diff"})
235 fp.truncate(0)
235 fp.truncate(0)
236 fp.write(reviewedpatch)
236 fp.write(reviewedpatch)
237 fp.seek(0)
237 fp.seek(0)
238
238
239 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
239 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
240 # 3a. apply filtered patch to clean repo (clean)
240 # 3a. apply filtered patch to clean repo (clean)
241 if backups:
241 if backups:
242 # Equivalent to hg.revert
242 # Equivalent to hg.revert
243 m = scmutil.matchfiles(repo, backups.keys())
243 m = scmutil.matchfiles(repo, backups.keys())
244 mergemod.update(repo, repo.dirstate.p1(),
244 mergemod.update(repo, repo.dirstate.p1(),
245 False, True, matcher=m)
245 False, True, matcher=m)
246
246
247 # 3b. (apply)
247 # 3b. (apply)
248 if dopatch:
248 if dopatch:
249 try:
249 try:
250 ui.debug('applying patch\n')
250 ui.debug('applying patch\n')
251 ui.debug(fp.getvalue())
251 ui.debug(fp.getvalue())
252 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
252 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
253 except patch.PatchError as err:
253 except patch.PatchError as err:
254 raise error.Abort(str(err))
254 raise error.Abort(str(err))
255 del fp
255 del fp
256
256
257 # 4. We prepared working directory according to filtered
257 # 4. We prepared working directory according to filtered
258 # patch. Now is the time to delegate the job to
258 # patch. Now is the time to delegate the job to
259 # commit/qrefresh or the like!
259 # commit/qrefresh or the like!
260
260
261 # Make all of the pathnames absolute.
261 # Make all of the pathnames absolute.
262 newfiles = [repo.wjoin(nf) for nf in newfiles]
262 newfiles = [repo.wjoin(nf) for nf in newfiles]
263 return commitfunc(ui, repo, *newfiles, **opts)
263 return commitfunc(ui, repo, *newfiles, **opts)
264 finally:
264 finally:
265 # 5. finally restore backed-up files
265 # 5. finally restore backed-up files
266 try:
266 try:
267 dirstate = repo.dirstate
267 dirstate = repo.dirstate
268 for realname, tmpname in backups.iteritems():
268 for realname, tmpname in backups.iteritems():
269 ui.debug('restoring %r to %r\n' % (tmpname, realname))
269 ui.debug('restoring %r to %r\n' % (tmpname, realname))
270
270
271 if dirstate[realname] == 'n':
271 if dirstate[realname] == 'n':
272 # without normallookup, restoring timestamp
272 # without normallookup, restoring timestamp
273 # may cause partially committed files
273 # may cause partially committed files
274 # to be treated as unmodified
274 # to be treated as unmodified
275 dirstate.normallookup(realname)
275 dirstate.normallookup(realname)
276
276
277 # copystat=True here and above are a hack to trick any
277 # copystat=True here and above are a hack to trick any
278 # editors that have f open that we haven't modified them.
278 # editors that have f open that we haven't modified them.
279 #
279 #
280 # Also note that this racy as an editor could notice the
280 # Also note that this racy as an editor could notice the
281 # file's mtime before we've finished writing it.
281 # file's mtime before we've finished writing it.
282 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
282 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
283 os.unlink(tmpname)
283 os.unlink(tmpname)
284 if tobackup:
284 if tobackup:
285 os.rmdir(backupdir)
285 os.rmdir(backupdir)
286 except OSError:
286 except OSError:
287 pass
287 pass
288
288
289 def recordinwlock(ui, repo, message, match, opts):
289 def recordinwlock(ui, repo, message, match, opts):
290 with repo.wlock():
290 with repo.wlock():
291 return recordfunc(ui, repo, message, match, opts)
291 return recordfunc(ui, repo, message, match, opts)
292
292
293 return commit(ui, repo, recordinwlock, pats, opts)
293 return commit(ui, repo, recordinwlock, pats, opts)
294
294
295 def findpossible(cmd, table, strict=False):
295 def findpossible(cmd, table, strict=False):
296 """
296 """
297 Return cmd -> (aliases, command table entry)
297 Return cmd -> (aliases, command table entry)
298 for each matching command.
298 for each matching command.
299 Return debug commands (or their aliases) only if no normal command matches.
299 Return debug commands (or their aliases) only if no normal command matches.
300 """
300 """
301 choice = {}
301 choice = {}
302 debugchoice = {}
302 debugchoice = {}
303
303
304 if cmd in table:
304 if cmd in table:
305 # short-circuit exact matches, "log" alias beats "^log|history"
305 # short-circuit exact matches, "log" alias beats "^log|history"
306 keys = [cmd]
306 keys = [cmd]
307 else:
307 else:
308 keys = table.keys()
308 keys = table.keys()
309
309
310 allcmds = []
310 allcmds = []
311 for e in keys:
311 for e in keys:
312 aliases = parsealiases(e)
312 aliases = parsealiases(e)
313 allcmds.extend(aliases)
313 allcmds.extend(aliases)
314 found = None
314 found = None
315 if cmd in aliases:
315 if cmd in aliases:
316 found = cmd
316 found = cmd
317 elif not strict:
317 elif not strict:
318 for a in aliases:
318 for a in aliases:
319 if a.startswith(cmd):
319 if a.startswith(cmd):
320 found = a
320 found = a
321 break
321 break
322 if found is not None:
322 if found is not None:
323 if aliases[0].startswith("debug") or found.startswith("debug"):
323 if aliases[0].startswith("debug") or found.startswith("debug"):
324 debugchoice[found] = (aliases, table[e])
324 debugchoice[found] = (aliases, table[e])
325 else:
325 else:
326 choice[found] = (aliases, table[e])
326 choice[found] = (aliases, table[e])
327
327
328 if not choice and debugchoice:
328 if not choice and debugchoice:
329 choice = debugchoice
329 choice = debugchoice
330
330
331 return choice, allcmds
331 return choice, allcmds
332
332
333 def findcmd(cmd, table, strict=True):
333 def findcmd(cmd, table, strict=True):
334 """Return (aliases, command table entry) for command string."""
334 """Return (aliases, command table entry) for command string."""
335 choice, allcmds = findpossible(cmd, table, strict)
335 choice, allcmds = findpossible(cmd, table, strict)
336
336
337 if cmd in choice:
337 if cmd in choice:
338 return choice[cmd]
338 return choice[cmd]
339
339
340 if len(choice) > 1:
340 if len(choice) > 1:
341 clist = choice.keys()
341 clist = choice.keys()
342 clist.sort()
342 clist.sort()
343 raise error.AmbiguousCommand(cmd, clist)
343 raise error.AmbiguousCommand(cmd, clist)
344
344
345 if choice:
345 if choice:
346 return choice.values()[0]
346 return choice.values()[0]
347
347
348 raise error.UnknownCommand(cmd, allcmds)
348 raise error.UnknownCommand(cmd, allcmds)
349
349
350 def findrepo(p):
350 def findrepo(p):
351 while not os.path.isdir(os.path.join(p, ".hg")):
351 while not os.path.isdir(os.path.join(p, ".hg")):
352 oldp, p = p, os.path.dirname(p)
352 oldp, p = p, os.path.dirname(p)
353 if p == oldp:
353 if p == oldp:
354 return None
354 return None
355
355
356 return p
356 return p
357
357
358 def bailifchanged(repo, merge=True):
358 def bailifchanged(repo, merge=True):
359 if merge and repo.dirstate.p2() != nullid:
359 if merge and repo.dirstate.p2() != nullid:
360 raise error.Abort(_('outstanding uncommitted merge'))
360 raise error.Abort(_('outstanding uncommitted merge'))
361 modified, added, removed, deleted = repo.status()[:4]
361 modified, added, removed, deleted = repo.status()[:4]
362 if modified or added or removed or deleted:
362 if modified or added or removed or deleted:
363 raise error.Abort(_('uncommitted changes'))
363 raise error.Abort(_('uncommitted changes'))
364 ctx = repo[None]
364 ctx = repo[None]
365 for s in sorted(ctx.substate):
365 for s in sorted(ctx.substate):
366 ctx.sub(s).bailifchanged()
366 ctx.sub(s).bailifchanged()
367
367
368 def logmessage(ui, opts):
368 def logmessage(ui, opts):
369 """ get the log message according to -m and -l option """
369 """ get the log message according to -m and -l option """
370 message = opts.get('message')
370 message = opts.get('message')
371 logfile = opts.get('logfile')
371 logfile = opts.get('logfile')
372
372
373 if message and logfile:
373 if message and logfile:
374 raise error.Abort(_('options --message and --logfile are mutually '
374 raise error.Abort(_('options --message and --logfile are mutually '
375 'exclusive'))
375 'exclusive'))
376 if not message and logfile:
376 if not message and logfile:
377 try:
377 try:
378 if logfile == '-':
378 if logfile == '-':
379 message = ui.fin.read()
379 message = ui.fin.read()
380 else:
380 else:
381 message = '\n'.join(util.readfile(logfile).splitlines())
381 message = '\n'.join(util.readfile(logfile).splitlines())
382 except IOError as inst:
382 except IOError as inst:
383 raise error.Abort(_("can't read commit message '%s': %s") %
383 raise error.Abort(_("can't read commit message '%s': %s") %
384 (logfile, inst.strerror))
384 (logfile, inst.strerror))
385 return message
385 return message
386
386
387 def mergeeditform(ctxorbool, baseformname):
387 def mergeeditform(ctxorbool, baseformname):
388 """return appropriate editform name (referencing a committemplate)
388 """return appropriate editform name (referencing a committemplate)
389
389
390 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
390 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
391 merging is committed.
391 merging is committed.
392
392
393 This returns baseformname with '.merge' appended if it is a merge,
393 This returns baseformname with '.merge' appended if it is a merge,
394 otherwise '.normal' is appended.
394 otherwise '.normal' is appended.
395 """
395 """
396 if isinstance(ctxorbool, bool):
396 if isinstance(ctxorbool, bool):
397 if ctxorbool:
397 if ctxorbool:
398 return baseformname + ".merge"
398 return baseformname + ".merge"
399 elif 1 < len(ctxorbool.parents()):
399 elif 1 < len(ctxorbool.parents()):
400 return baseformname + ".merge"
400 return baseformname + ".merge"
401
401
402 return baseformname + ".normal"
402 return baseformname + ".normal"
403
403
404 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
404 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
405 editform='', **opts):
405 editform='', **opts):
406 """get appropriate commit message editor according to '--edit' option
406 """get appropriate commit message editor according to '--edit' option
407
407
408 'finishdesc' is a function to be called with edited commit message
408 'finishdesc' is a function to be called with edited commit message
409 (= 'description' of the new changeset) just after editing, but
409 (= 'description' of the new changeset) just after editing, but
410 before checking empty-ness. It should return actual text to be
410 before checking empty-ness. It should return actual text to be
411 stored into history. This allows to change description before
411 stored into history. This allows to change description before
412 storing.
412 storing.
413
413
414 'extramsg' is a extra message to be shown in the editor instead of
414 'extramsg' is a extra message to be shown in the editor instead of
415 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
415 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
416 is automatically added.
416 is automatically added.
417
417
418 'editform' is a dot-separated list of names, to distinguish
418 'editform' is a dot-separated list of names, to distinguish
419 the purpose of commit text editing.
419 the purpose of commit text editing.
420
420
421 'getcommiteditor' returns 'commitforceeditor' regardless of
421 'getcommiteditor' returns 'commitforceeditor' regardless of
422 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
422 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
423 they are specific for usage in MQ.
423 they are specific for usage in MQ.
424 """
424 """
425 if edit or finishdesc or extramsg:
425 if edit or finishdesc or extramsg:
426 return lambda r, c, s: commitforceeditor(r, c, s,
426 return lambda r, c, s: commitforceeditor(r, c, s,
427 finishdesc=finishdesc,
427 finishdesc=finishdesc,
428 extramsg=extramsg,
428 extramsg=extramsg,
429 editform=editform)
429 editform=editform)
430 elif editform:
430 elif editform:
431 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
431 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
432 else:
432 else:
433 return commiteditor
433 return commiteditor
434
434
435 def loglimit(opts):
435 def loglimit(opts):
436 """get the log limit according to option -l/--limit"""
436 """get the log limit according to option -l/--limit"""
437 limit = opts.get('limit')
437 limit = opts.get('limit')
438 if limit:
438 if limit:
439 try:
439 try:
440 limit = int(limit)
440 limit = int(limit)
441 except ValueError:
441 except ValueError:
442 raise error.Abort(_('limit must be a positive integer'))
442 raise error.Abort(_('limit must be a positive integer'))
443 if limit <= 0:
443 if limit <= 0:
444 raise error.Abort(_('limit must be positive'))
444 raise error.Abort(_('limit must be positive'))
445 else:
445 else:
446 limit = None
446 limit = None
447 return limit
447 return limit
448
448
449 def makefilename(repo, pat, node, desc=None,
449 def makefilename(repo, pat, node, desc=None,
450 total=None, seqno=None, revwidth=None, pathname=None):
450 total=None, seqno=None, revwidth=None, pathname=None):
451 node_expander = {
451 node_expander = {
452 'H': lambda: hex(node),
452 'H': lambda: hex(node),
453 'R': lambda: str(repo.changelog.rev(node)),
453 'R': lambda: str(repo.changelog.rev(node)),
454 'h': lambda: short(node),
454 'h': lambda: short(node),
455 'm': lambda: re.sub('[^\w]', '_', str(desc))
455 'm': lambda: re.sub('[^\w]', '_', str(desc))
456 }
456 }
457 expander = {
457 expander = {
458 '%': lambda: '%',
458 '%': lambda: '%',
459 'b': lambda: os.path.basename(repo.root),
459 'b': lambda: os.path.basename(repo.root),
460 }
460 }
461
461
462 try:
462 try:
463 if node:
463 if node:
464 expander.update(node_expander)
464 expander.update(node_expander)
465 if node:
465 if node:
466 expander['r'] = (lambda:
466 expander['r'] = (lambda:
467 str(repo.changelog.rev(node)).zfill(revwidth or 0))
467 str(repo.changelog.rev(node)).zfill(revwidth or 0))
468 if total is not None:
468 if total is not None:
469 expander['N'] = lambda: str(total)
469 expander['N'] = lambda: str(total)
470 if seqno is not None:
470 if seqno is not None:
471 expander['n'] = lambda: str(seqno)
471 expander['n'] = lambda: str(seqno)
472 if total is not None and seqno is not None:
472 if total is not None and seqno is not None:
473 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
473 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
474 if pathname is not None:
474 if pathname is not None:
475 expander['s'] = lambda: os.path.basename(pathname)
475 expander['s'] = lambda: os.path.basename(pathname)
476 expander['d'] = lambda: os.path.dirname(pathname) or '.'
476 expander['d'] = lambda: os.path.dirname(pathname) or '.'
477 expander['p'] = lambda: pathname
477 expander['p'] = lambda: pathname
478
478
479 newname = []
479 newname = []
480 patlen = len(pat)
480 patlen = len(pat)
481 i = 0
481 i = 0
482 while i < patlen:
482 while i < patlen:
483 c = pat[i]
483 c = pat[i]
484 if c == '%':
484 if c == '%':
485 i += 1
485 i += 1
486 c = pat[i]
486 c = pat[i]
487 c = expander[c]()
487 c = expander[c]()
488 newname.append(c)
488 newname.append(c)
489 i += 1
489 i += 1
490 return ''.join(newname)
490 return ''.join(newname)
491 except KeyError as inst:
491 except KeyError as inst:
492 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
492 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
493 inst.args[0])
493 inst.args[0])
494
494
495 class _unclosablefile(object):
495 class _unclosablefile(object):
496 def __init__(self, fp):
496 def __init__(self, fp):
497 self._fp = fp
497 self._fp = fp
498
498
499 def close(self):
499 def close(self):
500 pass
500 pass
501
501
502 def __iter__(self):
502 def __iter__(self):
503 return iter(self._fp)
503 return iter(self._fp)
504
504
505 def __getattr__(self, attr):
505 def __getattr__(self, attr):
506 return getattr(self._fp, attr)
506 return getattr(self._fp, attr)
507
507
508 def __enter__(self):
508 def __enter__(self):
509 return self
509 return self
510
510
511 def __exit__(self, exc_type, exc_value, exc_tb):
511 def __exit__(self, exc_type, exc_value, exc_tb):
512 pass
512 pass
513
513
514 def makefileobj(repo, pat, node=None, desc=None, total=None,
514 def makefileobj(repo, pat, node=None, desc=None, total=None,
515 seqno=None, revwidth=None, mode='wb', modemap=None,
515 seqno=None, revwidth=None, mode='wb', modemap=None,
516 pathname=None):
516 pathname=None):
517
517
518 writable = mode not in ('r', 'rb')
518 writable = mode not in ('r', 'rb')
519
519
520 if not pat or pat == '-':
520 if not pat or pat == '-':
521 if writable:
521 if writable:
522 fp = repo.ui.fout
522 fp = repo.ui.fout
523 else:
523 else:
524 fp = repo.ui.fin
524 fp = repo.ui.fin
525 return _unclosablefile(fp)
525 return _unclosablefile(fp)
526 if util.safehasattr(pat, 'write') and writable:
526 if util.safehasattr(pat, 'write') and writable:
527 return pat
527 return pat
528 if util.safehasattr(pat, 'read') and 'r' in mode:
528 if util.safehasattr(pat, 'read') and 'r' in mode:
529 return pat
529 return pat
530 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
530 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
531 if modemap is not None:
531 if modemap is not None:
532 mode = modemap.get(fn, mode)
532 mode = modemap.get(fn, mode)
533 if mode == 'wb':
533 if mode == 'wb':
534 modemap[fn] = 'ab'
534 modemap[fn] = 'ab'
535 return open(fn, mode)
535 return open(fn, mode)
536
536
537 def openrevlog(repo, cmd, file_, opts):
537 def openrevlog(repo, cmd, file_, opts):
538 """opens the changelog, manifest, a filelog or a given revlog"""
538 """opens the changelog, manifest, a filelog or a given revlog"""
539 cl = opts['changelog']
539 cl = opts['changelog']
540 mf = opts['manifest']
540 mf = opts['manifest']
541 dir = opts['dir']
541 dir = opts['dir']
542 msg = None
542 msg = None
543 if cl and mf:
543 if cl and mf:
544 msg = _('cannot specify --changelog and --manifest at the same time')
544 msg = _('cannot specify --changelog and --manifest at the same time')
545 elif cl and dir:
545 elif cl and dir:
546 msg = _('cannot specify --changelog and --dir at the same time')
546 msg = _('cannot specify --changelog and --dir at the same time')
547 elif cl or mf or dir:
547 elif cl or mf or dir:
548 if file_:
548 if file_:
549 msg = _('cannot specify filename with --changelog or --manifest')
549 msg = _('cannot specify filename with --changelog or --manifest')
550 elif not repo:
550 elif not repo:
551 msg = _('cannot specify --changelog or --manifest or --dir '
551 msg = _('cannot specify --changelog or --manifest or --dir '
552 'without a repository')
552 'without a repository')
553 if msg:
553 if msg:
554 raise error.Abort(msg)
554 raise error.Abort(msg)
555
555
556 r = None
556 r = None
557 if repo:
557 if repo:
558 if cl:
558 if cl:
559 r = repo.unfiltered().changelog
559 r = repo.unfiltered().changelog
560 elif dir:
560 elif dir:
561 if 'treemanifest' not in repo.requirements:
561 if 'treemanifest' not in repo.requirements:
562 raise error.Abort(_("--dir can only be used on repos with "
562 raise error.Abort(_("--dir can only be used on repos with "
563 "treemanifest enabled"))
563 "treemanifest enabled"))
564 dirlog = repo.manifestlog._revlog.dirlog(dir)
564 dirlog = repo.manifestlog._revlog.dirlog(dir)
565 if len(dirlog):
565 if len(dirlog):
566 r = dirlog
566 r = dirlog
567 elif mf:
567 elif mf:
568 r = repo.manifestlog._revlog
568 r = repo.manifestlog._revlog
569 elif file_:
569 elif file_:
570 filelog = repo.file(file_)
570 filelog = repo.file(file_)
571 if len(filelog):
571 if len(filelog):
572 r = filelog
572 r = filelog
573 if not r:
573 if not r:
574 if not file_:
574 if not file_:
575 raise error.CommandError(cmd, _('invalid arguments'))
575 raise error.CommandError(cmd, _('invalid arguments'))
576 if not os.path.isfile(file_):
576 if not os.path.isfile(file_):
577 raise error.Abort(_("revlog '%s' not found") % file_)
577 raise error.Abort(_("revlog '%s' not found") % file_)
578 r = revlog.revlog(scmutil.opener(pycompat.getcwd(), audit=False),
578 r = revlog.revlog(scmutil.opener(pycompat.getcwd(), audit=False),
579 file_[:-2] + ".i")
579 file_[:-2] + ".i")
580 return r
580 return r
581
581
582 def copy(ui, repo, pats, opts, rename=False):
582 def copy(ui, repo, pats, opts, rename=False):
583 # called with the repo lock held
583 # called with the repo lock held
584 #
584 #
585 # hgsep => pathname that uses "/" to separate directories
585 # hgsep => pathname that uses "/" to separate directories
586 # ossep => pathname that uses os.sep to separate directories
586 # ossep => pathname that uses os.sep to separate directories
587 cwd = repo.getcwd()
587 cwd = repo.getcwd()
588 targets = {}
588 targets = {}
589 after = opts.get("after")
589 after = opts.get("after")
590 dryrun = opts.get("dry_run")
590 dryrun = opts.get("dry_run")
591 wctx = repo[None]
591 wctx = repo[None]
592
592
593 def walkpat(pat):
593 def walkpat(pat):
594 srcs = []
594 srcs = []
595 if after:
595 if after:
596 badstates = '?'
596 badstates = '?'
597 else:
597 else:
598 badstates = '?r'
598 badstates = '?r'
599 m = scmutil.match(repo[None], [pat], opts, globbed=True)
599 m = scmutil.match(repo[None], [pat], opts, globbed=True)
600 for abs in repo.walk(m):
600 for abs in repo.walk(m):
601 state = repo.dirstate[abs]
601 state = repo.dirstate[abs]
602 rel = m.rel(abs)
602 rel = m.rel(abs)
603 exact = m.exact(abs)
603 exact = m.exact(abs)
604 if state in badstates:
604 if state in badstates:
605 if exact and state == '?':
605 if exact and state == '?':
606 ui.warn(_('%s: not copying - file is not managed\n') % rel)
606 ui.warn(_('%s: not copying - file is not managed\n') % rel)
607 if exact and state == 'r':
607 if exact and state == 'r':
608 ui.warn(_('%s: not copying - file has been marked for'
608 ui.warn(_('%s: not copying - file has been marked for'
609 ' remove\n') % rel)
609 ' remove\n') % rel)
610 continue
610 continue
611 # abs: hgsep
611 # abs: hgsep
612 # rel: ossep
612 # rel: ossep
613 srcs.append((abs, rel, exact))
613 srcs.append((abs, rel, exact))
614 return srcs
614 return srcs
615
615
616 # abssrc: hgsep
616 # abssrc: hgsep
617 # relsrc: ossep
617 # relsrc: ossep
618 # otarget: ossep
618 # otarget: ossep
619 def copyfile(abssrc, relsrc, otarget, exact):
619 def copyfile(abssrc, relsrc, otarget, exact):
620 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
620 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
621 if '/' in abstarget:
621 if '/' in abstarget:
622 # We cannot normalize abstarget itself, this would prevent
622 # We cannot normalize abstarget itself, this would prevent
623 # case only renames, like a => A.
623 # case only renames, like a => A.
624 abspath, absname = abstarget.rsplit('/', 1)
624 abspath, absname = abstarget.rsplit('/', 1)
625 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
625 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
626 reltarget = repo.pathto(abstarget, cwd)
626 reltarget = repo.pathto(abstarget, cwd)
627 target = repo.wjoin(abstarget)
627 target = repo.wjoin(abstarget)
628 src = repo.wjoin(abssrc)
628 src = repo.wjoin(abssrc)
629 state = repo.dirstate[abstarget]
629 state = repo.dirstate[abstarget]
630
630
631 scmutil.checkportable(ui, abstarget)
631 scmutil.checkportable(ui, abstarget)
632
632
633 # check for collisions
633 # check for collisions
634 prevsrc = targets.get(abstarget)
634 prevsrc = targets.get(abstarget)
635 if prevsrc is not None:
635 if prevsrc is not None:
636 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
636 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
637 (reltarget, repo.pathto(abssrc, cwd),
637 (reltarget, repo.pathto(abssrc, cwd),
638 repo.pathto(prevsrc, cwd)))
638 repo.pathto(prevsrc, cwd)))
639 return
639 return
640
640
641 # check for overwrites
641 # check for overwrites
642 exists = os.path.lexists(target)
642 exists = os.path.lexists(target)
643 samefile = False
643 samefile = False
644 if exists and abssrc != abstarget:
644 if exists and abssrc != abstarget:
645 if (repo.dirstate.normalize(abssrc) ==
645 if (repo.dirstate.normalize(abssrc) ==
646 repo.dirstate.normalize(abstarget)):
646 repo.dirstate.normalize(abstarget)):
647 if not rename:
647 if not rename:
648 ui.warn(_("%s: can't copy - same file\n") % reltarget)
648 ui.warn(_("%s: can't copy - same file\n") % reltarget)
649 return
649 return
650 exists = False
650 exists = False
651 samefile = True
651 samefile = True
652
652
653 if not after and exists or after and state in 'mn':
653 if not after and exists or after and state in 'mn':
654 if not opts['force']:
654 if not opts['force']:
655 if state in 'mn':
655 if state in 'mn':
656 msg = _('%s: not overwriting - file already committed\n')
656 msg = _('%s: not overwriting - file already committed\n')
657 if after:
657 if after:
658 flags = '--after --force'
658 flags = '--after --force'
659 else:
659 else:
660 flags = '--force'
660 flags = '--force'
661 if rename:
661 if rename:
662 hint = _('(hg rename %s to replace the file by '
662 hint = _('(hg rename %s to replace the file by '
663 'recording a rename)\n') % flags
663 'recording a rename)\n') % flags
664 else:
664 else:
665 hint = _('(hg copy %s to replace the file by '
665 hint = _('(hg copy %s to replace the file by '
666 'recording a copy)\n') % flags
666 'recording a copy)\n') % flags
667 else:
667 else:
668 msg = _('%s: not overwriting - file exists\n')
668 msg = _('%s: not overwriting - file exists\n')
669 if rename:
669 if rename:
670 hint = _('(hg rename --after to record the rename)\n')
670 hint = _('(hg rename --after to record the rename)\n')
671 else:
671 else:
672 hint = _('(hg copy --after to record the copy)\n')
672 hint = _('(hg copy --after to record the copy)\n')
673 ui.warn(msg % reltarget)
673 ui.warn(msg % reltarget)
674 ui.warn(hint)
674 ui.warn(hint)
675 return
675 return
676
676
677 if after:
677 if after:
678 if not exists:
678 if not exists:
679 if rename:
679 if rename:
680 ui.warn(_('%s: not recording move - %s does not exist\n') %
680 ui.warn(_('%s: not recording move - %s does not exist\n') %
681 (relsrc, reltarget))
681 (relsrc, reltarget))
682 else:
682 else:
683 ui.warn(_('%s: not recording copy - %s does not exist\n') %
683 ui.warn(_('%s: not recording copy - %s does not exist\n') %
684 (relsrc, reltarget))
684 (relsrc, reltarget))
685 return
685 return
686 elif not dryrun:
686 elif not dryrun:
687 try:
687 try:
688 if exists:
688 if exists:
689 os.unlink(target)
689 os.unlink(target)
690 targetdir = os.path.dirname(target) or '.'
690 targetdir = os.path.dirname(target) or '.'
691 if not os.path.isdir(targetdir):
691 if not os.path.isdir(targetdir):
692 os.makedirs(targetdir)
692 os.makedirs(targetdir)
693 if samefile:
693 if samefile:
694 tmp = target + "~hgrename"
694 tmp = target + "~hgrename"
695 os.rename(src, tmp)
695 os.rename(src, tmp)
696 os.rename(tmp, target)
696 os.rename(tmp, target)
697 else:
697 else:
698 util.copyfile(src, target)
698 util.copyfile(src, target)
699 srcexists = True
699 srcexists = True
700 except IOError as inst:
700 except IOError as inst:
701 if inst.errno == errno.ENOENT:
701 if inst.errno == errno.ENOENT:
702 ui.warn(_('%s: deleted in working directory\n') % relsrc)
702 ui.warn(_('%s: deleted in working directory\n') % relsrc)
703 srcexists = False
703 srcexists = False
704 else:
704 else:
705 ui.warn(_('%s: cannot copy - %s\n') %
705 ui.warn(_('%s: cannot copy - %s\n') %
706 (relsrc, inst.strerror))
706 (relsrc, inst.strerror))
707 return True # report a failure
707 return True # report a failure
708
708
709 if ui.verbose or not exact:
709 if ui.verbose or not exact:
710 if rename:
710 if rename:
711 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
711 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
712 else:
712 else:
713 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
713 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
714
714
715 targets[abstarget] = abssrc
715 targets[abstarget] = abssrc
716
716
717 # fix up dirstate
717 # fix up dirstate
718 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
718 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
719 dryrun=dryrun, cwd=cwd)
719 dryrun=dryrun, cwd=cwd)
720 if rename and not dryrun:
720 if rename and not dryrun:
721 if not after and srcexists and not samefile:
721 if not after and srcexists and not samefile:
722 util.unlinkpath(repo.wjoin(abssrc))
722 util.unlinkpath(repo.wjoin(abssrc))
723 wctx.forget([abssrc])
723 wctx.forget([abssrc])
724
724
725 # pat: ossep
725 # pat: ossep
726 # dest ossep
726 # dest ossep
727 # srcs: list of (hgsep, hgsep, ossep, bool)
727 # srcs: list of (hgsep, hgsep, ossep, bool)
728 # return: function that takes hgsep and returns ossep
728 # return: function that takes hgsep and returns ossep
729 def targetpathfn(pat, dest, srcs):
729 def targetpathfn(pat, dest, srcs):
730 if os.path.isdir(pat):
730 if os.path.isdir(pat):
731 abspfx = pathutil.canonpath(repo.root, cwd, pat)
731 abspfx = pathutil.canonpath(repo.root, cwd, pat)
732 abspfx = util.localpath(abspfx)
732 abspfx = util.localpath(abspfx)
733 if destdirexists:
733 if destdirexists:
734 striplen = len(os.path.split(abspfx)[0])
734 striplen = len(os.path.split(abspfx)[0])
735 else:
735 else:
736 striplen = len(abspfx)
736 striplen = len(abspfx)
737 if striplen:
737 if striplen:
738 striplen += len(pycompat.ossep)
738 striplen += len(pycompat.ossep)
739 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
739 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
740 elif destdirexists:
740 elif destdirexists:
741 res = lambda p: os.path.join(dest,
741 res = lambda p: os.path.join(dest,
742 os.path.basename(util.localpath(p)))
742 os.path.basename(util.localpath(p)))
743 else:
743 else:
744 res = lambda p: dest
744 res = lambda p: dest
745 return res
745 return res
746
746
747 # pat: ossep
747 # pat: ossep
748 # dest ossep
748 # dest ossep
749 # srcs: list of (hgsep, hgsep, ossep, bool)
749 # srcs: list of (hgsep, hgsep, ossep, bool)
750 # return: function that takes hgsep and returns ossep
750 # return: function that takes hgsep and returns ossep
751 def targetpathafterfn(pat, dest, srcs):
751 def targetpathafterfn(pat, dest, srcs):
752 if matchmod.patkind(pat):
752 if matchmod.patkind(pat):
753 # a mercurial pattern
753 # a mercurial pattern
754 res = lambda p: os.path.join(dest,
754 res = lambda p: os.path.join(dest,
755 os.path.basename(util.localpath(p)))
755 os.path.basename(util.localpath(p)))
756 else:
756 else:
757 abspfx = pathutil.canonpath(repo.root, cwd, pat)
757 abspfx = pathutil.canonpath(repo.root, cwd, pat)
758 if len(abspfx) < len(srcs[0][0]):
758 if len(abspfx) < len(srcs[0][0]):
759 # A directory. Either the target path contains the last
759 # A directory. Either the target path contains the last
760 # component of the source path or it does not.
760 # component of the source path or it does not.
761 def evalpath(striplen):
761 def evalpath(striplen):
762 score = 0
762 score = 0
763 for s in srcs:
763 for s in srcs:
764 t = os.path.join(dest, util.localpath(s[0])[striplen:])
764 t = os.path.join(dest, util.localpath(s[0])[striplen:])
765 if os.path.lexists(t):
765 if os.path.lexists(t):
766 score += 1
766 score += 1
767 return score
767 return score
768
768
769 abspfx = util.localpath(abspfx)
769 abspfx = util.localpath(abspfx)
770 striplen = len(abspfx)
770 striplen = len(abspfx)
771 if striplen:
771 if striplen:
772 striplen += len(pycompat.ossep)
772 striplen += len(pycompat.ossep)
773 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
773 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
774 score = evalpath(striplen)
774 score = evalpath(striplen)
775 striplen1 = len(os.path.split(abspfx)[0])
775 striplen1 = len(os.path.split(abspfx)[0])
776 if striplen1:
776 if striplen1:
777 striplen1 += len(pycompat.ossep)
777 striplen1 += len(pycompat.ossep)
778 if evalpath(striplen1) > score:
778 if evalpath(striplen1) > score:
779 striplen = striplen1
779 striplen = striplen1
780 res = lambda p: os.path.join(dest,
780 res = lambda p: os.path.join(dest,
781 util.localpath(p)[striplen:])
781 util.localpath(p)[striplen:])
782 else:
782 else:
783 # a file
783 # a file
784 if destdirexists:
784 if destdirexists:
785 res = lambda p: os.path.join(dest,
785 res = lambda p: os.path.join(dest,
786 os.path.basename(util.localpath(p)))
786 os.path.basename(util.localpath(p)))
787 else:
787 else:
788 res = lambda p: dest
788 res = lambda p: dest
789 return res
789 return res
790
790
791 pats = scmutil.expandpats(pats)
791 pats = scmutil.expandpats(pats)
792 if not pats:
792 if not pats:
793 raise error.Abort(_('no source or destination specified'))
793 raise error.Abort(_('no source or destination specified'))
794 if len(pats) == 1:
794 if len(pats) == 1:
795 raise error.Abort(_('no destination specified'))
795 raise error.Abort(_('no destination specified'))
796 dest = pats.pop()
796 dest = pats.pop()
797 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
797 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
798 if not destdirexists:
798 if not destdirexists:
799 if len(pats) > 1 or matchmod.patkind(pats[0]):
799 if len(pats) > 1 or matchmod.patkind(pats[0]):
800 raise error.Abort(_('with multiple sources, destination must be an '
800 raise error.Abort(_('with multiple sources, destination must be an '
801 'existing directory'))
801 'existing directory'))
802 if util.endswithsep(dest):
802 if util.endswithsep(dest):
803 raise error.Abort(_('destination %s is not a directory') % dest)
803 raise error.Abort(_('destination %s is not a directory') % dest)
804
804
805 tfn = targetpathfn
805 tfn = targetpathfn
806 if after:
806 if after:
807 tfn = targetpathafterfn
807 tfn = targetpathafterfn
808 copylist = []
808 copylist = []
809 for pat in pats:
809 for pat in pats:
810 srcs = walkpat(pat)
810 srcs = walkpat(pat)
811 if not srcs:
811 if not srcs:
812 continue
812 continue
813 copylist.append((tfn(pat, dest, srcs), srcs))
813 copylist.append((tfn(pat, dest, srcs), srcs))
814 if not copylist:
814 if not copylist:
815 raise error.Abort(_('no files to copy'))
815 raise error.Abort(_('no files to copy'))
816
816
817 errors = 0
817 errors = 0
818 for targetpath, srcs in copylist:
818 for targetpath, srcs in copylist:
819 for abssrc, relsrc, exact in srcs:
819 for abssrc, relsrc, exact in srcs:
820 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
820 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
821 errors += 1
821 errors += 1
822
822
823 if errors:
823 if errors:
824 ui.warn(_('(consider using --after)\n'))
824 ui.warn(_('(consider using --after)\n'))
825
825
826 return errors != 0
826 return errors != 0
827
827
828 ## facility to let extension process additional data into an import patch
828 ## facility to let extension process additional data into an import patch
829 # list of identifier to be executed in order
829 # list of identifier to be executed in order
830 extrapreimport = [] # run before commit
830 extrapreimport = [] # run before commit
831 extrapostimport = [] # run after commit
831 extrapostimport = [] # run after commit
832 # mapping from identifier to actual import function
832 # mapping from identifier to actual import function
833 #
833 #
834 # 'preimport' are run before the commit is made and are provided the following
834 # 'preimport' are run before the commit is made and are provided the following
835 # arguments:
835 # arguments:
836 # - repo: the localrepository instance,
836 # - repo: the localrepository instance,
837 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
837 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
838 # - extra: the future extra dictionary of the changeset, please mutate it,
838 # - extra: the future extra dictionary of the changeset, please mutate it,
839 # - opts: the import options.
839 # - opts: the import options.
840 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
840 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
841 # mutation of in memory commit and more. Feel free to rework the code to get
841 # mutation of in memory commit and more. Feel free to rework the code to get
842 # there.
842 # there.
843 extrapreimportmap = {}
843 extrapreimportmap = {}
844 # 'postimport' are run after the commit is made and are provided the following
844 # 'postimport' are run after the commit is made and are provided the following
845 # argument:
845 # argument:
846 # - ctx: the changectx created by import.
846 # - ctx: the changectx created by import.
847 extrapostimportmap = {}
847 extrapostimportmap = {}
848
848
849 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
849 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
850 """Utility function used by commands.import to import a single patch
850 """Utility function used by commands.import to import a single patch
851
851
852 This function is explicitly defined here to help the evolve extension to
852 This function is explicitly defined here to help the evolve extension to
853 wrap this part of the import logic.
853 wrap this part of the import logic.
854
854
855 The API is currently a bit ugly because it a simple code translation from
855 The API is currently a bit ugly because it a simple code translation from
856 the import command. Feel free to make it better.
856 the import command. Feel free to make it better.
857
857
858 :hunk: a patch (as a binary string)
858 :hunk: a patch (as a binary string)
859 :parents: nodes that will be parent of the created commit
859 :parents: nodes that will be parent of the created commit
860 :opts: the full dict of option passed to the import command
860 :opts: the full dict of option passed to the import command
861 :msgs: list to save commit message to.
861 :msgs: list to save commit message to.
862 (used in case we need to save it when failing)
862 (used in case we need to save it when failing)
863 :updatefunc: a function that update a repo to a given node
863 :updatefunc: a function that update a repo to a given node
864 updatefunc(<repo>, <node>)
864 updatefunc(<repo>, <node>)
865 """
865 """
866 # avoid cycle context -> subrepo -> cmdutil
866 # avoid cycle context -> subrepo -> cmdutil
867 from . import context
867 from . import context
868 extractdata = patch.extract(ui, hunk)
868 extractdata = patch.extract(ui, hunk)
869 tmpname = extractdata.get('filename')
869 tmpname = extractdata.get('filename')
870 message = extractdata.get('message')
870 message = extractdata.get('message')
871 user = opts.get('user') or extractdata.get('user')
871 user = opts.get('user') or extractdata.get('user')
872 date = opts.get('date') or extractdata.get('date')
872 date = opts.get('date') or extractdata.get('date')
873 branch = extractdata.get('branch')
873 branch = extractdata.get('branch')
874 nodeid = extractdata.get('nodeid')
874 nodeid = extractdata.get('nodeid')
875 p1 = extractdata.get('p1')
875 p1 = extractdata.get('p1')
876 p2 = extractdata.get('p2')
876 p2 = extractdata.get('p2')
877
877
878 nocommit = opts.get('no_commit')
878 nocommit = opts.get('no_commit')
879 importbranch = opts.get('import_branch')
879 importbranch = opts.get('import_branch')
880 update = not opts.get('bypass')
880 update = not opts.get('bypass')
881 strip = opts["strip"]
881 strip = opts["strip"]
882 prefix = opts["prefix"]
882 prefix = opts["prefix"]
883 sim = float(opts.get('similarity') or 0)
883 sim = float(opts.get('similarity') or 0)
884 if not tmpname:
884 if not tmpname:
885 return (None, None, False)
885 return (None, None, False)
886
886
887 rejects = False
887 rejects = False
888
888
889 try:
889 try:
890 cmdline_message = logmessage(ui, opts)
890 cmdline_message = logmessage(ui, opts)
891 if cmdline_message:
891 if cmdline_message:
892 # pickup the cmdline msg
892 # pickup the cmdline msg
893 message = cmdline_message
893 message = cmdline_message
894 elif message:
894 elif message:
895 # pickup the patch msg
895 # pickup the patch msg
896 message = message.strip()
896 message = message.strip()
897 else:
897 else:
898 # launch the editor
898 # launch the editor
899 message = None
899 message = None
900 ui.debug('message:\n%s\n' % message)
900 ui.debug('message:\n%s\n' % message)
901
901
902 if len(parents) == 1:
902 if len(parents) == 1:
903 parents.append(repo[nullid])
903 parents.append(repo[nullid])
904 if opts.get('exact'):
904 if opts.get('exact'):
905 if not nodeid or not p1:
905 if not nodeid or not p1:
906 raise error.Abort(_('not a Mercurial patch'))
906 raise error.Abort(_('not a Mercurial patch'))
907 p1 = repo[p1]
907 p1 = repo[p1]
908 p2 = repo[p2 or nullid]
908 p2 = repo[p2 or nullid]
909 elif p2:
909 elif p2:
910 try:
910 try:
911 p1 = repo[p1]
911 p1 = repo[p1]
912 p2 = repo[p2]
912 p2 = repo[p2]
913 # Without any options, consider p2 only if the
913 # Without any options, consider p2 only if the
914 # patch is being applied on top of the recorded
914 # patch is being applied on top of the recorded
915 # first parent.
915 # first parent.
916 if p1 != parents[0]:
916 if p1 != parents[0]:
917 p1 = parents[0]
917 p1 = parents[0]
918 p2 = repo[nullid]
918 p2 = repo[nullid]
919 except error.RepoError:
919 except error.RepoError:
920 p1, p2 = parents
920 p1, p2 = parents
921 if p2.node() == nullid:
921 if p2.node() == nullid:
922 ui.warn(_("warning: import the patch as a normal revision\n"
922 ui.warn(_("warning: import the patch as a normal revision\n"
923 "(use --exact to import the patch as a merge)\n"))
923 "(use --exact to import the patch as a merge)\n"))
924 else:
924 else:
925 p1, p2 = parents
925 p1, p2 = parents
926
926
927 n = None
927 n = None
928 if update:
928 if update:
929 if p1 != parents[0]:
929 if p1 != parents[0]:
930 updatefunc(repo, p1.node())
930 updatefunc(repo, p1.node())
931 if p2 != parents[1]:
931 if p2 != parents[1]:
932 repo.setparents(p1.node(), p2.node())
932 repo.setparents(p1.node(), p2.node())
933
933
934 if opts.get('exact') or importbranch:
934 if opts.get('exact') or importbranch:
935 repo.dirstate.setbranch(branch or 'default')
935 repo.dirstate.setbranch(branch or 'default')
936
936
937 partial = opts.get('partial', False)
937 partial = opts.get('partial', False)
938 files = set()
938 files = set()
939 try:
939 try:
940 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
940 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
941 files=files, eolmode=None, similarity=sim / 100.0)
941 files=files, eolmode=None, similarity=sim / 100.0)
942 except patch.PatchError as e:
942 except patch.PatchError as e:
943 if not partial:
943 if not partial:
944 raise error.Abort(str(e))
944 raise error.Abort(str(e))
945 if partial:
945 if partial:
946 rejects = True
946 rejects = True
947
947
948 files = list(files)
948 files = list(files)
949 if nocommit:
949 if nocommit:
950 if message:
950 if message:
951 msgs.append(message)
951 msgs.append(message)
952 else:
952 else:
953 if opts.get('exact') or p2:
953 if opts.get('exact') or p2:
954 # If you got here, you either use --force and know what
954 # If you got here, you either use --force and know what
955 # you are doing or used --exact or a merge patch while
955 # you are doing or used --exact or a merge patch while
956 # being updated to its first parent.
956 # being updated to its first parent.
957 m = None
957 m = None
958 else:
958 else:
959 m = scmutil.matchfiles(repo, files or [])
959 m = scmutil.matchfiles(repo, files or [])
960 editform = mergeeditform(repo[None], 'import.normal')
960 editform = mergeeditform(repo[None], 'import.normal')
961 if opts.get('exact'):
961 if opts.get('exact'):
962 editor = None
962 editor = None
963 else:
963 else:
964 editor = getcommiteditor(editform=editform, **opts)
964 editor = getcommiteditor(editform=editform, **opts)
965 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
965 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
966 extra = {}
966 extra = {}
967 for idfunc in extrapreimport:
967 for idfunc in extrapreimport:
968 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
968 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
969 try:
969 try:
970 if partial:
970 if partial:
971 repo.ui.setconfig('ui', 'allowemptycommit', True)
971 repo.ui.setconfig('ui', 'allowemptycommit', True)
972 n = repo.commit(message, user,
972 n = repo.commit(message, user,
973 date, match=m,
973 date, match=m,
974 editor=editor, extra=extra)
974 editor=editor, extra=extra)
975 for idfunc in extrapostimport:
975 for idfunc in extrapostimport:
976 extrapostimportmap[idfunc](repo[n])
976 extrapostimportmap[idfunc](repo[n])
977 finally:
977 finally:
978 repo.ui.restoreconfig(allowemptyback)
978 repo.ui.restoreconfig(allowemptyback)
979 else:
979 else:
980 if opts.get('exact') or importbranch:
980 if opts.get('exact') or importbranch:
981 branch = branch or 'default'
981 branch = branch or 'default'
982 else:
982 else:
983 branch = p1.branch()
983 branch = p1.branch()
984 store = patch.filestore()
984 store = patch.filestore()
985 try:
985 try:
986 files = set()
986 files = set()
987 try:
987 try:
988 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
988 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
989 files, eolmode=None)
989 files, eolmode=None)
990 except patch.PatchError as e:
990 except patch.PatchError as e:
991 raise error.Abort(str(e))
991 raise error.Abort(str(e))
992 if opts.get('exact'):
992 if opts.get('exact'):
993 editor = None
993 editor = None
994 else:
994 else:
995 editor = getcommiteditor(editform='import.bypass')
995 editor = getcommiteditor(editform='import.bypass')
996 memctx = context.makememctx(repo, (p1.node(), p2.node()),
996 memctx = context.makememctx(repo, (p1.node(), p2.node()),
997 message,
997 message,
998 user,
998 user,
999 date,
999 date,
1000 branch, files, store,
1000 branch, files, store,
1001 editor=editor)
1001 editor=editor)
1002 n = memctx.commit()
1002 n = memctx.commit()
1003 finally:
1003 finally:
1004 store.close()
1004 store.close()
1005 if opts.get('exact') and nocommit:
1005 if opts.get('exact') and nocommit:
1006 # --exact with --no-commit is still useful in that it does merge
1006 # --exact with --no-commit is still useful in that it does merge
1007 # and branch bits
1007 # and branch bits
1008 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1008 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1009 elif opts.get('exact') and hex(n) != nodeid:
1009 elif opts.get('exact') and hex(n) != nodeid:
1010 raise error.Abort(_('patch is damaged or loses information'))
1010 raise error.Abort(_('patch is damaged or loses information'))
1011 msg = _('applied to working directory')
1011 msg = _('applied to working directory')
1012 if n:
1012 if n:
1013 # i18n: refers to a short changeset id
1013 # i18n: refers to a short changeset id
1014 msg = _('created %s') % short(n)
1014 msg = _('created %s') % short(n)
1015 return (msg, n, rejects)
1015 return (msg, n, rejects)
1016 finally:
1016 finally:
1017 os.unlink(tmpname)
1017 os.unlink(tmpname)
1018
1018
1019 # facility to let extensions include additional data in an exported patch
1019 # facility to let extensions include additional data in an exported patch
1020 # list of identifiers to be executed in order
1020 # list of identifiers to be executed in order
1021 extraexport = []
1021 extraexport = []
1022 # mapping from identifier to actual export function
1022 # mapping from identifier to actual export function
1023 # function as to return a string to be added to the header or None
1023 # function as to return a string to be added to the header or None
1024 # it is given two arguments (sequencenumber, changectx)
1024 # it is given two arguments (sequencenumber, changectx)
1025 extraexportmap = {}
1025 extraexportmap = {}
1026
1026
1027 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1027 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1028 opts=None, match=None):
1028 opts=None, match=None):
1029 '''export changesets as hg patches.'''
1029 '''export changesets as hg patches.'''
1030
1030
1031 total = len(revs)
1031 total = len(revs)
1032 revwidth = max([len(str(rev)) for rev in revs])
1032 revwidth = max([len(str(rev)) for rev in revs])
1033 filemode = {}
1033 filemode = {}
1034
1034
1035 def single(rev, seqno, fp):
1035 def single(rev, seqno, fp):
1036 ctx = repo[rev]
1036 ctx = repo[rev]
1037 node = ctx.node()
1037 node = ctx.node()
1038 parents = [p.node() for p in ctx.parents() if p]
1038 parents = [p.node() for p in ctx.parents() if p]
1039 branch = ctx.branch()
1039 branch = ctx.branch()
1040 if switch_parent:
1040 if switch_parent:
1041 parents.reverse()
1041 parents.reverse()
1042
1042
1043 if parents:
1043 if parents:
1044 prev = parents[0]
1044 prev = parents[0]
1045 else:
1045 else:
1046 prev = nullid
1046 prev = nullid
1047
1047
1048 shouldclose = False
1048 shouldclose = False
1049 if not fp and len(template) > 0:
1049 if not fp and len(template) > 0:
1050 desc_lines = ctx.description().rstrip().split('\n')
1050 desc_lines = ctx.description().rstrip().split('\n')
1051 desc = desc_lines[0] #Commit always has a first line.
1051 desc = desc_lines[0] #Commit always has a first line.
1052 fp = makefileobj(repo, template, node, desc=desc, total=total,
1052 fp = makefileobj(repo, template, node, desc=desc, total=total,
1053 seqno=seqno, revwidth=revwidth, mode='wb',
1053 seqno=seqno, revwidth=revwidth, mode='wb',
1054 modemap=filemode)
1054 modemap=filemode)
1055 shouldclose = True
1055 shouldclose = True
1056 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1056 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1057 repo.ui.note("%s\n" % fp.name)
1057 repo.ui.note("%s\n" % fp.name)
1058
1058
1059 if not fp:
1059 if not fp:
1060 write = repo.ui.write
1060 write = repo.ui.write
1061 else:
1061 else:
1062 def write(s, **kw):
1062 def write(s, **kw):
1063 fp.write(s)
1063 fp.write(s)
1064
1064
1065 write("# HG changeset patch\n")
1065 write("# HG changeset patch\n")
1066 write("# User %s\n" % ctx.user())
1066 write("# User %s\n" % ctx.user())
1067 write("# Date %d %d\n" % ctx.date())
1067 write("# Date %d %d\n" % ctx.date())
1068 write("# %s\n" % util.datestr(ctx.date()))
1068 write("# %s\n" % util.datestr(ctx.date()))
1069 if branch and branch != 'default':
1069 if branch and branch != 'default':
1070 write("# Branch %s\n" % branch)
1070 write("# Branch %s\n" % branch)
1071 write("# Node ID %s\n" % hex(node))
1071 write("# Node ID %s\n" % hex(node))
1072 write("# Parent %s\n" % hex(prev))
1072 write("# Parent %s\n" % hex(prev))
1073 if len(parents) > 1:
1073 if len(parents) > 1:
1074 write("# Parent %s\n" % hex(parents[1]))
1074 write("# Parent %s\n" % hex(parents[1]))
1075
1075
1076 for headerid in extraexport:
1076 for headerid in extraexport:
1077 header = extraexportmap[headerid](seqno, ctx)
1077 header = extraexportmap[headerid](seqno, ctx)
1078 if header is not None:
1078 if header is not None:
1079 write('# %s\n' % header)
1079 write('# %s\n' % header)
1080 write(ctx.description().rstrip())
1080 write(ctx.description().rstrip())
1081 write("\n\n")
1081 write("\n\n")
1082
1082
1083 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1083 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1084 write(chunk, label=label)
1084 write(chunk, label=label)
1085
1085
1086 if shouldclose:
1086 if shouldclose:
1087 fp.close()
1087 fp.close()
1088
1088
1089 for seqno, rev in enumerate(revs):
1089 for seqno, rev in enumerate(revs):
1090 single(rev, seqno + 1, fp)
1090 single(rev, seqno + 1, fp)
1091
1091
1092 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1092 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1093 changes=None, stat=False, fp=None, prefix='',
1093 changes=None, stat=False, fp=None, prefix='',
1094 root='', listsubrepos=False):
1094 root='', listsubrepos=False):
1095 '''show diff or diffstat.'''
1095 '''show diff or diffstat.'''
1096 if fp is None:
1096 if fp is None:
1097 write = ui.write
1097 write = ui.write
1098 else:
1098 else:
1099 def write(s, **kw):
1099 def write(s, **kw):
1100 fp.write(s)
1100 fp.write(s)
1101
1101
1102 if root:
1102 if root:
1103 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1103 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1104 else:
1104 else:
1105 relroot = ''
1105 relroot = ''
1106 if relroot != '':
1106 if relroot != '':
1107 # XXX relative roots currently don't work if the root is within a
1107 # XXX relative roots currently don't work if the root is within a
1108 # subrepo
1108 # subrepo
1109 uirelroot = match.uipath(relroot)
1109 uirelroot = match.uipath(relroot)
1110 relroot += '/'
1110 relroot += '/'
1111 for matchroot in match.files():
1111 for matchroot in match.files():
1112 if not matchroot.startswith(relroot):
1112 if not matchroot.startswith(relroot):
1113 ui.warn(_('warning: %s not inside relative root %s\n') % (
1113 ui.warn(_('warning: %s not inside relative root %s\n') % (
1114 match.uipath(matchroot), uirelroot))
1114 match.uipath(matchroot), uirelroot))
1115
1115
1116 if stat:
1116 if stat:
1117 diffopts = diffopts.copy(context=0)
1117 diffopts = diffopts.copy(context=0)
1118 width = 80
1118 width = 80
1119 if not ui.plain():
1119 if not ui.plain():
1120 width = ui.termwidth()
1120 width = ui.termwidth()
1121 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1121 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1122 prefix=prefix, relroot=relroot)
1122 prefix=prefix, relroot=relroot)
1123 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1123 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1124 width=width):
1124 width=width):
1125 write(chunk, label=label)
1125 write(chunk, label=label)
1126 else:
1126 else:
1127 for chunk, label in patch.diffui(repo, node1, node2, match,
1127 for chunk, label in patch.diffui(repo, node1, node2, match,
1128 changes, diffopts, prefix=prefix,
1128 changes, diffopts, prefix=prefix,
1129 relroot=relroot):
1129 relroot=relroot):
1130 write(chunk, label=label)
1130 write(chunk, label=label)
1131
1131
1132 if listsubrepos:
1132 if listsubrepos:
1133 ctx1 = repo[node1]
1133 ctx1 = repo[node1]
1134 ctx2 = repo[node2]
1134 ctx2 = repo[node2]
1135 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1135 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1136 tempnode2 = node2
1136 tempnode2 = node2
1137 try:
1137 try:
1138 if node2 is not None:
1138 if node2 is not None:
1139 tempnode2 = ctx2.substate[subpath][1]
1139 tempnode2 = ctx2.substate[subpath][1]
1140 except KeyError:
1140 except KeyError:
1141 # A subrepo that existed in node1 was deleted between node1 and
1141 # A subrepo that existed in node1 was deleted between node1 and
1142 # node2 (inclusive). Thus, ctx2's substate won't contain that
1142 # node2 (inclusive). Thus, ctx2's substate won't contain that
1143 # subpath. The best we can do is to ignore it.
1143 # subpath. The best we can do is to ignore it.
1144 tempnode2 = None
1144 tempnode2 = None
1145 submatch = matchmod.subdirmatcher(subpath, match)
1145 submatch = matchmod.subdirmatcher(subpath, match)
1146 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1146 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1147 stat=stat, fp=fp, prefix=prefix)
1147 stat=stat, fp=fp, prefix=prefix)
1148
1148
1149 def _changesetlabels(ctx):
1149 def _changesetlabels(ctx):
1150 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1150 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1151 if ctx.troubled():
1151 if ctx.troubled():
1152 labels.append('changeset.troubled')
1152 labels.append('changeset.troubled')
1153 for trouble in ctx.troubles():
1153 for trouble in ctx.troubles():
1154 labels.append('trouble.%s' % trouble)
1154 labels.append('trouble.%s' % trouble)
1155 return ' '.join(labels)
1155 return ' '.join(labels)
1156
1156
1157 class changeset_printer(object):
1157 class changeset_printer(object):
1158 '''show changeset information when templating not requested.'''
1158 '''show changeset information when templating not requested.'''
1159
1159
1160 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1160 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1161 self.ui = ui
1161 self.ui = ui
1162 self.repo = repo
1162 self.repo = repo
1163 self.buffered = buffered
1163 self.buffered = buffered
1164 self.matchfn = matchfn
1164 self.matchfn = matchfn
1165 self.diffopts = diffopts
1165 self.diffopts = diffopts
1166 self.header = {}
1166 self.header = {}
1167 self.hunk = {}
1167 self.hunk = {}
1168 self.lastheader = None
1168 self.lastheader = None
1169 self.footer = None
1169 self.footer = None
1170
1170
1171 def flush(self, ctx):
1171 def flush(self, ctx):
1172 rev = ctx.rev()
1172 rev = ctx.rev()
1173 if rev in self.header:
1173 if rev in self.header:
1174 h = self.header[rev]
1174 h = self.header[rev]
1175 if h != self.lastheader:
1175 if h != self.lastheader:
1176 self.lastheader = h
1176 self.lastheader = h
1177 self.ui.write(h)
1177 self.ui.write(h)
1178 del self.header[rev]
1178 del self.header[rev]
1179 if rev in self.hunk:
1179 if rev in self.hunk:
1180 self.ui.write(self.hunk[rev])
1180 self.ui.write(self.hunk[rev])
1181 del self.hunk[rev]
1181 del self.hunk[rev]
1182 return 1
1182 return 1
1183 return 0
1183 return 0
1184
1184
1185 def close(self):
1185 def close(self):
1186 if self.footer:
1186 if self.footer:
1187 self.ui.write(self.footer)
1187 self.ui.write(self.footer)
1188
1188
1189 def show(self, ctx, copies=None, matchfn=None, **props):
1189 def show(self, ctx, copies=None, matchfn=None, **props):
1190 if self.buffered:
1190 if self.buffered:
1191 self.ui.pushbuffer(labeled=True)
1191 self.ui.pushbuffer(labeled=True)
1192 self._show(ctx, copies, matchfn, props)
1192 self._show(ctx, copies, matchfn, props)
1193 self.hunk[ctx.rev()] = self.ui.popbuffer()
1193 self.hunk[ctx.rev()] = self.ui.popbuffer()
1194 else:
1194 else:
1195 self._show(ctx, copies, matchfn, props)
1195 self._show(ctx, copies, matchfn, props)
1196
1196
1197 def _show(self, ctx, copies, matchfn, props):
1197 def _show(self, ctx, copies, matchfn, props):
1198 '''show a single changeset or file revision'''
1198 '''show a single changeset or file revision'''
1199 changenode = ctx.node()
1199 changenode = ctx.node()
1200 rev = ctx.rev()
1200 rev = ctx.rev()
1201 if self.ui.debugflag:
1201 if self.ui.debugflag:
1202 hexfunc = hex
1202 hexfunc = hex
1203 else:
1203 else:
1204 hexfunc = short
1204 hexfunc = short
1205 # as of now, wctx.node() and wctx.rev() return None, but we want to
1205 # as of now, wctx.node() and wctx.rev() return None, but we want to
1206 # show the same values as {node} and {rev} templatekw
1206 # show the same values as {node} and {rev} templatekw
1207 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1207 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1208
1208
1209 if self.ui.quiet:
1209 if self.ui.quiet:
1210 self.ui.write("%d:%s\n" % revnode, label='log.node')
1210 self.ui.write("%d:%s\n" % revnode, label='log.node')
1211 return
1211 return
1212
1212
1213 date = util.datestr(ctx.date())
1213 date = util.datestr(ctx.date())
1214
1214
1215 # i18n: column positioning for "hg log"
1215 # i18n: column positioning for "hg log"
1216 self.ui.write(_("changeset: %d:%s\n") % revnode,
1216 self.ui.write(_("changeset: %d:%s\n") % revnode,
1217 label=_changesetlabels(ctx))
1217 label=_changesetlabels(ctx))
1218
1218
1219 # branches are shown first before any other names due to backwards
1219 # branches are shown first before any other names due to backwards
1220 # compatibility
1220 # compatibility
1221 branch = ctx.branch()
1221 branch = ctx.branch()
1222 # don't show the default branch name
1222 # don't show the default branch name
1223 if branch != 'default':
1223 if branch != 'default':
1224 # i18n: column positioning for "hg log"
1224 # i18n: column positioning for "hg log"
1225 self.ui.write(_("branch: %s\n") % branch,
1225 self.ui.write(_("branch: %s\n") % branch,
1226 label='log.branch')
1226 label='log.branch')
1227
1227
1228 for nsname, ns in self.repo.names.iteritems():
1228 for nsname, ns in self.repo.names.iteritems():
1229 # branches has special logic already handled above, so here we just
1229 # branches has special logic already handled above, so here we just
1230 # skip it
1230 # skip it
1231 if nsname == 'branches':
1231 if nsname == 'branches':
1232 continue
1232 continue
1233 # we will use the templatename as the color name since those two
1233 # we will use the templatename as the color name since those two
1234 # should be the same
1234 # should be the same
1235 for name in ns.names(self.repo, changenode):
1235 for name in ns.names(self.repo, changenode):
1236 self.ui.write(ns.logfmt % name,
1236 self.ui.write(ns.logfmt % name,
1237 label='log.%s' % ns.colorname)
1237 label='log.%s' % ns.colorname)
1238 if self.ui.debugflag:
1238 if self.ui.debugflag:
1239 # i18n: column positioning for "hg log"
1239 # i18n: column positioning for "hg log"
1240 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1240 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1241 label='log.phase')
1241 label='log.phase')
1242 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1242 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1243 label = 'log.parent changeset.%s' % pctx.phasestr()
1243 label = 'log.parent changeset.%s' % pctx.phasestr()
1244 # i18n: column positioning for "hg log"
1244 # i18n: column positioning for "hg log"
1245 self.ui.write(_("parent: %d:%s\n")
1245 self.ui.write(_("parent: %d:%s\n")
1246 % (pctx.rev(), hexfunc(pctx.node())),
1246 % (pctx.rev(), hexfunc(pctx.node())),
1247 label=label)
1247 label=label)
1248
1248
1249 if self.ui.debugflag and rev is not None:
1249 if self.ui.debugflag and rev is not None:
1250 mnode = ctx.manifestnode()
1250 mnode = ctx.manifestnode()
1251 # i18n: column positioning for "hg log"
1251 # i18n: column positioning for "hg log"
1252 self.ui.write(_("manifest: %d:%s\n") %
1252 self.ui.write(_("manifest: %d:%s\n") %
1253 (self.repo.manifestlog._revlog.rev(mnode),
1253 (self.repo.manifestlog._revlog.rev(mnode),
1254 hex(mnode)),
1254 hex(mnode)),
1255 label='ui.debug log.manifest')
1255 label='ui.debug log.manifest')
1256 # i18n: column positioning for "hg log"
1256 # i18n: column positioning for "hg log"
1257 self.ui.write(_("user: %s\n") % ctx.user(),
1257 self.ui.write(_("user: %s\n") % ctx.user(),
1258 label='log.user')
1258 label='log.user')
1259 # i18n: column positioning for "hg log"
1259 # i18n: column positioning for "hg log"
1260 self.ui.write(_("date: %s\n") % date,
1260 self.ui.write(_("date: %s\n") % date,
1261 label='log.date')
1261 label='log.date')
1262
1262
1263 if ctx.troubled():
1263 if ctx.troubled():
1264 # i18n: column positioning for "hg log"
1264 # i18n: column positioning for "hg log"
1265 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1265 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1266 label='log.trouble')
1266 label='log.trouble')
1267
1267
1268 if self.ui.debugflag:
1268 if self.ui.debugflag:
1269 files = ctx.p1().status(ctx)[:3]
1269 files = ctx.p1().status(ctx)[:3]
1270 for key, value in zip([# i18n: column positioning for "hg log"
1270 for key, value in zip([# i18n: column positioning for "hg log"
1271 _("files:"),
1271 _("files:"),
1272 # i18n: column positioning for "hg log"
1272 # i18n: column positioning for "hg log"
1273 _("files+:"),
1273 _("files+:"),
1274 # i18n: column positioning for "hg log"
1274 # i18n: column positioning for "hg log"
1275 _("files-:")], files):
1275 _("files-:")], files):
1276 if value:
1276 if value:
1277 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1277 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1278 label='ui.debug log.files')
1278 label='ui.debug log.files')
1279 elif ctx.files() and self.ui.verbose:
1279 elif ctx.files() and self.ui.verbose:
1280 # i18n: column positioning for "hg log"
1280 # i18n: column positioning for "hg log"
1281 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1281 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1282 label='ui.note log.files')
1282 label='ui.note log.files')
1283 if copies and self.ui.verbose:
1283 if copies and self.ui.verbose:
1284 copies = ['%s (%s)' % c for c in copies]
1284 copies = ['%s (%s)' % c for c in copies]
1285 # i18n: column positioning for "hg log"
1285 # i18n: column positioning for "hg log"
1286 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1286 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1287 label='ui.note log.copies')
1287 label='ui.note log.copies')
1288
1288
1289 extra = ctx.extra()
1289 extra = ctx.extra()
1290 if extra and self.ui.debugflag:
1290 if extra and self.ui.debugflag:
1291 for key, value in sorted(extra.items()):
1291 for key, value in sorted(extra.items()):
1292 # i18n: column positioning for "hg log"
1292 # i18n: column positioning for "hg log"
1293 self.ui.write(_("extra: %s=%s\n")
1293 self.ui.write(_("extra: %s=%s\n")
1294 % (key, value.encode('string_escape')),
1294 % (key, value.encode('string_escape')),
1295 label='ui.debug log.extra')
1295 label='ui.debug log.extra')
1296
1296
1297 description = ctx.description().strip()
1297 description = ctx.description().strip()
1298 if description:
1298 if description:
1299 if self.ui.verbose:
1299 if self.ui.verbose:
1300 self.ui.write(_("description:\n"),
1300 self.ui.write(_("description:\n"),
1301 label='ui.note log.description')
1301 label='ui.note log.description')
1302 self.ui.write(description,
1302 self.ui.write(description,
1303 label='ui.note log.description')
1303 label='ui.note log.description')
1304 self.ui.write("\n\n")
1304 self.ui.write("\n\n")
1305 else:
1305 else:
1306 # i18n: column positioning for "hg log"
1306 # i18n: column positioning for "hg log"
1307 self.ui.write(_("summary: %s\n") %
1307 self.ui.write(_("summary: %s\n") %
1308 description.splitlines()[0],
1308 description.splitlines()[0],
1309 label='log.summary')
1309 label='log.summary')
1310 self.ui.write("\n")
1310 self.ui.write("\n")
1311
1311
1312 self.showpatch(ctx, matchfn)
1312 self.showpatch(ctx, matchfn)
1313
1313
1314 def showpatch(self, ctx, matchfn):
1314 def showpatch(self, ctx, matchfn):
1315 if not matchfn:
1315 if not matchfn:
1316 matchfn = self.matchfn
1316 matchfn = self.matchfn
1317 if matchfn:
1317 if matchfn:
1318 stat = self.diffopts.get('stat')
1318 stat = self.diffopts.get('stat')
1319 diff = self.diffopts.get('patch')
1319 diff = self.diffopts.get('patch')
1320 diffopts = patch.diffallopts(self.ui, self.diffopts)
1320 diffopts = patch.diffallopts(self.ui, self.diffopts)
1321 node = ctx.node()
1321 node = ctx.node()
1322 prev = ctx.p1().node()
1322 prev = ctx.p1().node()
1323 if stat:
1323 if stat:
1324 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1324 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1325 match=matchfn, stat=True)
1325 match=matchfn, stat=True)
1326 if diff:
1326 if diff:
1327 if stat:
1327 if stat:
1328 self.ui.write("\n")
1328 self.ui.write("\n")
1329 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1329 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1330 match=matchfn, stat=False)
1330 match=matchfn, stat=False)
1331 self.ui.write("\n")
1331 self.ui.write("\n")
1332
1332
1333 class jsonchangeset(changeset_printer):
1333 class jsonchangeset(changeset_printer):
1334 '''format changeset information.'''
1334 '''format changeset information.'''
1335
1335
1336 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1336 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1337 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1337 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1338 self.cache = {}
1338 self.cache = {}
1339 self._first = True
1339 self._first = True
1340
1340
1341 def close(self):
1341 def close(self):
1342 if not self._first:
1342 if not self._first:
1343 self.ui.write("\n]\n")
1343 self.ui.write("\n]\n")
1344 else:
1344 else:
1345 self.ui.write("[]\n")
1345 self.ui.write("[]\n")
1346
1346
1347 def _show(self, ctx, copies, matchfn, props):
1347 def _show(self, ctx, copies, matchfn, props):
1348 '''show a single changeset or file revision'''
1348 '''show a single changeset or file revision'''
1349 rev = ctx.rev()
1349 rev = ctx.rev()
1350 if rev is None:
1350 if rev is None:
1351 jrev = jnode = 'null'
1351 jrev = jnode = 'null'
1352 else:
1352 else:
1353 jrev = str(rev)
1353 jrev = str(rev)
1354 jnode = '"%s"' % hex(ctx.node())
1354 jnode = '"%s"' % hex(ctx.node())
1355 j = encoding.jsonescape
1355 j = encoding.jsonescape
1356
1356
1357 if self._first:
1357 if self._first:
1358 self.ui.write("[\n {")
1358 self.ui.write("[\n {")
1359 self._first = False
1359 self._first = False
1360 else:
1360 else:
1361 self.ui.write(",\n {")
1361 self.ui.write(",\n {")
1362
1362
1363 if self.ui.quiet:
1363 if self.ui.quiet:
1364 self.ui.write(('\n "rev": %s') % jrev)
1364 self.ui.write(('\n "rev": %s') % jrev)
1365 self.ui.write((',\n "node": %s') % jnode)
1365 self.ui.write((',\n "node": %s') % jnode)
1366 self.ui.write('\n }')
1366 self.ui.write('\n }')
1367 return
1367 return
1368
1368
1369 self.ui.write(('\n "rev": %s') % jrev)
1369 self.ui.write(('\n "rev": %s') % jrev)
1370 self.ui.write((',\n "node": %s') % jnode)
1370 self.ui.write((',\n "node": %s') % jnode)
1371 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1371 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1372 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1372 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1373 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1373 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1374 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1374 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1375 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1375 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1376
1376
1377 self.ui.write((',\n "bookmarks": [%s]') %
1377 self.ui.write((',\n "bookmarks": [%s]') %
1378 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1378 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1379 self.ui.write((',\n "tags": [%s]') %
1379 self.ui.write((',\n "tags": [%s]') %
1380 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1380 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1381 self.ui.write((',\n "parents": [%s]') %
1381 self.ui.write((',\n "parents": [%s]') %
1382 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1382 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1383
1383
1384 if self.ui.debugflag:
1384 if self.ui.debugflag:
1385 if rev is None:
1385 if rev is None:
1386 jmanifestnode = 'null'
1386 jmanifestnode = 'null'
1387 else:
1387 else:
1388 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1388 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1389 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1389 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1390
1390
1391 self.ui.write((',\n "extra": {%s}') %
1391 self.ui.write((',\n "extra": {%s}') %
1392 ", ".join('"%s": "%s"' % (j(k), j(v))
1392 ", ".join('"%s": "%s"' % (j(k), j(v))
1393 for k, v in ctx.extra().items()))
1393 for k, v in ctx.extra().items()))
1394
1394
1395 files = ctx.p1().status(ctx)
1395 files = ctx.p1().status(ctx)
1396 self.ui.write((',\n "modified": [%s]') %
1396 self.ui.write((',\n "modified": [%s]') %
1397 ", ".join('"%s"' % j(f) for f in files[0]))
1397 ", ".join('"%s"' % j(f) for f in files[0]))
1398 self.ui.write((',\n "added": [%s]') %
1398 self.ui.write((',\n "added": [%s]') %
1399 ", ".join('"%s"' % j(f) for f in files[1]))
1399 ", ".join('"%s"' % j(f) for f in files[1]))
1400 self.ui.write((',\n "removed": [%s]') %
1400 self.ui.write((',\n "removed": [%s]') %
1401 ", ".join('"%s"' % j(f) for f in files[2]))
1401 ", ".join('"%s"' % j(f) for f in files[2]))
1402
1402
1403 elif self.ui.verbose:
1403 elif self.ui.verbose:
1404 self.ui.write((',\n "files": [%s]') %
1404 self.ui.write((',\n "files": [%s]') %
1405 ", ".join('"%s"' % j(f) for f in ctx.files()))
1405 ", ".join('"%s"' % j(f) for f in ctx.files()))
1406
1406
1407 if copies:
1407 if copies:
1408 self.ui.write((',\n "copies": {%s}') %
1408 self.ui.write((',\n "copies": {%s}') %
1409 ", ".join('"%s": "%s"' % (j(k), j(v))
1409 ", ".join('"%s": "%s"' % (j(k), j(v))
1410 for k, v in copies))
1410 for k, v in copies))
1411
1411
1412 matchfn = self.matchfn
1412 matchfn = self.matchfn
1413 if matchfn:
1413 if matchfn:
1414 stat = self.diffopts.get('stat')
1414 stat = self.diffopts.get('stat')
1415 diff = self.diffopts.get('patch')
1415 diff = self.diffopts.get('patch')
1416 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1416 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1417 node, prev = ctx.node(), ctx.p1().node()
1417 node, prev = ctx.node(), ctx.p1().node()
1418 if stat:
1418 if stat:
1419 self.ui.pushbuffer()
1419 self.ui.pushbuffer()
1420 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1420 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1421 match=matchfn, stat=True)
1421 match=matchfn, stat=True)
1422 self.ui.write((',\n "diffstat": "%s"')
1422 self.ui.write((',\n "diffstat": "%s"')
1423 % j(self.ui.popbuffer()))
1423 % j(self.ui.popbuffer()))
1424 if diff:
1424 if diff:
1425 self.ui.pushbuffer()
1425 self.ui.pushbuffer()
1426 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1426 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1427 match=matchfn, stat=False)
1427 match=matchfn, stat=False)
1428 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1428 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1429
1429
1430 self.ui.write("\n }")
1430 self.ui.write("\n }")
1431
1431
1432 class changeset_templater(changeset_printer):
1432 class changeset_templater(changeset_printer):
1433 '''format changeset information.'''
1433 '''format changeset information.'''
1434
1434
1435 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1435 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1436 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1436 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1437 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1437 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1438 filters = {'formatnode': formatnode}
1438 filters = {'formatnode': formatnode}
1439 defaulttempl = {
1439 defaulttempl = {
1440 'parent': '{rev}:{node|formatnode} ',
1440 'parent': '{rev}:{node|formatnode} ',
1441 'manifest': '{rev}:{node|formatnode}',
1441 'manifest': '{rev}:{node|formatnode}',
1442 'file_copy': '{name} ({source})',
1442 'file_copy': '{name} ({source})',
1443 'extra': '{key}={value|stringescape}'
1443 'extra': '{key}={value|stringescape}'
1444 }
1444 }
1445 # filecopy is preserved for compatibility reasons
1445 # filecopy is preserved for compatibility reasons
1446 defaulttempl['filecopy'] = defaulttempl['file_copy']
1446 defaulttempl['filecopy'] = defaulttempl['file_copy']
1447 assert not (tmpl and mapfile)
1447 assert not (tmpl and mapfile)
1448 if mapfile:
1448 if mapfile:
1449 self.t = templater.templater.frommapfile(mapfile, filters=filters,
1449 self.t = templater.templater.frommapfile(mapfile, filters=filters,
1450 cache=defaulttempl)
1450 cache=defaulttempl)
1451 else:
1451 else:
1452 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1452 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1453 filters=filters,
1453 filters=filters,
1454 cache=defaulttempl)
1454 cache=defaulttempl)
1455
1455
1456 self.cache = {}
1456 self.cache = {}
1457
1457
1458 # find correct templates for current mode
1458 # find correct templates for current mode
1459 tmplmodes = [
1459 tmplmodes = [
1460 (True, None),
1460 (True, None),
1461 (self.ui.verbose, 'verbose'),
1461 (self.ui.verbose, 'verbose'),
1462 (self.ui.quiet, 'quiet'),
1462 (self.ui.quiet, 'quiet'),
1463 (self.ui.debugflag, 'debug'),
1463 (self.ui.debugflag, 'debug'),
1464 ]
1464 ]
1465
1465
1466 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1466 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1467 'docheader': '', 'docfooter': ''}
1467 'docheader': '', 'docfooter': ''}
1468 for mode, postfix in tmplmodes:
1468 for mode, postfix in tmplmodes:
1469 for t in self._parts:
1469 for t in self._parts:
1470 cur = t
1470 cur = t
1471 if postfix:
1471 if postfix:
1472 cur += "_" + postfix
1472 cur += "_" + postfix
1473 if mode and cur in self.t:
1473 if mode and cur in self.t:
1474 self._parts[t] = cur
1474 self._parts[t] = cur
1475
1475
1476 if self._parts['docheader']:
1476 if self._parts['docheader']:
1477 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1477 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1478
1478
1479 def close(self):
1479 def close(self):
1480 if self._parts['docfooter']:
1480 if self._parts['docfooter']:
1481 if not self.footer:
1481 if not self.footer:
1482 self.footer = ""
1482 self.footer = ""
1483 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1483 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1484 return super(changeset_templater, self).close()
1484 return super(changeset_templater, self).close()
1485
1485
1486 def _show(self, ctx, copies, matchfn, props):
1486 def _show(self, ctx, copies, matchfn, props):
1487 '''show a single changeset or file revision'''
1487 '''show a single changeset or file revision'''
1488 props = props.copy()
1488 props = props.copy()
1489 props.update(templatekw.keywords)
1489 props.update(templatekw.keywords)
1490 props['templ'] = self.t
1490 props['templ'] = self.t
1491 props['ctx'] = ctx
1491 props['ctx'] = ctx
1492 props['repo'] = self.repo
1492 props['repo'] = self.repo
1493 props['ui'] = self.repo.ui
1493 props['ui'] = self.repo.ui
1494 props['revcache'] = {'copies': copies}
1494 props['revcache'] = {'copies': copies}
1495 props['cache'] = self.cache
1495 props['cache'] = self.cache
1496
1496
1497 # write header
1497 # write header
1498 if self._parts['header']:
1498 if self._parts['header']:
1499 h = templater.stringify(self.t(self._parts['header'], **props))
1499 h = templater.stringify(self.t(self._parts['header'], **props))
1500 if self.buffered:
1500 if self.buffered:
1501 self.header[ctx.rev()] = h
1501 self.header[ctx.rev()] = h
1502 else:
1502 else:
1503 if self.lastheader != h:
1503 if self.lastheader != h:
1504 self.lastheader = h
1504 self.lastheader = h
1505 self.ui.write(h)
1505 self.ui.write(h)
1506
1506
1507 # write changeset metadata, then patch if requested
1507 # write changeset metadata, then patch if requested
1508 key = self._parts['changeset']
1508 key = self._parts['changeset']
1509 self.ui.write(templater.stringify(self.t(key, **props)))
1509 self.ui.write(templater.stringify(self.t(key, **props)))
1510 self.showpatch(ctx, matchfn)
1510 self.showpatch(ctx, matchfn)
1511
1511
1512 if self._parts['footer']:
1512 if self._parts['footer']:
1513 if not self.footer:
1513 if not self.footer:
1514 self.footer = templater.stringify(
1514 self.footer = templater.stringify(
1515 self.t(self._parts['footer'], **props))
1515 self.t(self._parts['footer'], **props))
1516
1516
1517 def gettemplate(ui, tmpl, style):
1517 def gettemplate(ui, tmpl, style):
1518 """
1518 """
1519 Find the template matching the given template spec or style.
1519 Find the template matching the given template spec or style.
1520 """
1520 """
1521
1521
1522 # ui settings
1522 # ui settings
1523 if not tmpl and not style: # template are stronger than style
1523 if not tmpl and not style: # template are stronger than style
1524 tmpl = ui.config('ui', 'logtemplate')
1524 tmpl = ui.config('ui', 'logtemplate')
1525 if tmpl:
1525 if tmpl:
1526 return templater.unquotestring(tmpl), None
1526 return templater.unquotestring(tmpl), None
1527 else:
1527 else:
1528 style = util.expandpath(ui.config('ui', 'style', ''))
1528 style = util.expandpath(ui.config('ui', 'style', ''))
1529
1529
1530 if not tmpl and style:
1530 if not tmpl and style:
1531 mapfile = style
1531 mapfile = style
1532 if not os.path.split(mapfile)[0]:
1532 if not os.path.split(mapfile)[0]:
1533 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1533 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1534 or templater.templatepath(mapfile))
1534 or templater.templatepath(mapfile))
1535 if mapname:
1535 if mapname:
1536 mapfile = mapname
1536 mapfile = mapname
1537 return None, mapfile
1537 return None, mapfile
1538
1538
1539 if not tmpl:
1539 if not tmpl:
1540 return None, None
1540 return None, None
1541
1541
1542 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1542 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1543
1543
1544 def show_changeset(ui, repo, opts, buffered=False):
1544 def show_changeset(ui, repo, opts, buffered=False):
1545 """show one changeset using template or regular display.
1545 """show one changeset using template or regular display.
1546
1546
1547 Display format will be the first non-empty hit of:
1547 Display format will be the first non-empty hit of:
1548 1. option 'template'
1548 1. option 'template'
1549 2. option 'style'
1549 2. option 'style'
1550 3. [ui] setting 'logtemplate'
1550 3. [ui] setting 'logtemplate'
1551 4. [ui] setting 'style'
1551 4. [ui] setting 'style'
1552 If all of these values are either the unset or the empty string,
1552 If all of these values are either the unset or the empty string,
1553 regular display via changeset_printer() is done.
1553 regular display via changeset_printer() is done.
1554 """
1554 """
1555 # options
1555 # options
1556 matchfn = None
1556 matchfn = None
1557 if opts.get('patch') or opts.get('stat'):
1557 if opts.get('patch') or opts.get('stat'):
1558 matchfn = scmutil.matchall(repo)
1558 matchfn = scmutil.matchall(repo)
1559
1559
1560 if opts.get('template') == 'json':
1560 if opts.get('template') == 'json':
1561 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1561 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1562
1562
1563 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1563 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1564
1564
1565 if not tmpl and not mapfile:
1565 if not tmpl and not mapfile:
1566 return changeset_printer(ui, repo, matchfn, opts, buffered)
1566 return changeset_printer(ui, repo, matchfn, opts, buffered)
1567
1567
1568 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1568 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1569
1569
1570 def showmarker(fm, marker, index=None):
1570 def showmarker(fm, marker, index=None):
1571 """utility function to display obsolescence marker in a readable way
1571 """utility function to display obsolescence marker in a readable way
1572
1572
1573 To be used by debug function."""
1573 To be used by debug function."""
1574 if index is not None:
1574 if index is not None:
1575 fm.write('index', '%i ', index)
1575 fm.write('index', '%i ', index)
1576 fm.write('precnode', '%s ', hex(marker.precnode()))
1576 fm.write('precnode', '%s ', hex(marker.precnode()))
1577 succs = marker.succnodes()
1577 succs = marker.succnodes()
1578 fm.condwrite(succs, 'succnodes', '%s ',
1578 fm.condwrite(succs, 'succnodes', '%s ',
1579 fm.formatlist(map(hex, succs), name='node'))
1579 fm.formatlist(map(hex, succs), name='node'))
1580 fm.write('flag', '%X ', marker.flags())
1580 fm.write('flag', '%X ', marker.flags())
1581 parents = marker.parentnodes()
1581 parents = marker.parentnodes()
1582 if parents is not None:
1582 if parents is not None:
1583 fm.write('parentnodes', '{%s} ',
1583 fm.write('parentnodes', '{%s} ',
1584 fm.formatlist(map(hex, parents), name='node', sep=', '))
1584 fm.formatlist(map(hex, parents), name='node', sep=', '))
1585 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1585 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1586 meta = marker.metadata().copy()
1586 meta = marker.metadata().copy()
1587 meta.pop('date', None)
1587 meta.pop('date', None)
1588 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1588 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1589 fm.plain('\n')
1589 fm.plain('\n')
1590
1590
1591 def finddate(ui, repo, date):
1591 def finddate(ui, repo, date):
1592 """Find the tipmost changeset that matches the given date spec"""
1592 """Find the tipmost changeset that matches the given date spec"""
1593
1593
1594 df = util.matchdate(date)
1594 df = util.matchdate(date)
1595 m = scmutil.matchall(repo)
1595 m = scmutil.matchall(repo)
1596 results = {}
1596 results = {}
1597
1597
1598 def prep(ctx, fns):
1598 def prep(ctx, fns):
1599 d = ctx.date()
1599 d = ctx.date()
1600 if df(d[0]):
1600 if df(d[0]):
1601 results[ctx.rev()] = d
1601 results[ctx.rev()] = d
1602
1602
1603 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1603 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1604 rev = ctx.rev()
1604 rev = ctx.rev()
1605 if rev in results:
1605 if rev in results:
1606 ui.status(_("found revision %s from %s\n") %
1606 ui.status(_("found revision %s from %s\n") %
1607 (rev, util.datestr(results[rev])))
1607 (rev, util.datestr(results[rev])))
1608 return str(rev)
1608 return str(rev)
1609
1609
1610 raise error.Abort(_("revision matching date not found"))
1610 raise error.Abort(_("revision matching date not found"))
1611
1611
1612 def increasingwindows(windowsize=8, sizelimit=512):
1612 def increasingwindows(windowsize=8, sizelimit=512):
1613 while True:
1613 while True:
1614 yield windowsize
1614 yield windowsize
1615 if windowsize < sizelimit:
1615 if windowsize < sizelimit:
1616 windowsize *= 2
1616 windowsize *= 2
1617
1617
1618 class FileWalkError(Exception):
1618 class FileWalkError(Exception):
1619 pass
1619 pass
1620
1620
1621 def walkfilerevs(repo, match, follow, revs, fncache):
1621 def walkfilerevs(repo, match, follow, revs, fncache):
1622 '''Walks the file history for the matched files.
1622 '''Walks the file history for the matched files.
1623
1623
1624 Returns the changeset revs that are involved in the file history.
1624 Returns the changeset revs that are involved in the file history.
1625
1625
1626 Throws FileWalkError if the file history can't be walked using
1626 Throws FileWalkError if the file history can't be walked using
1627 filelogs alone.
1627 filelogs alone.
1628 '''
1628 '''
1629 wanted = set()
1629 wanted = set()
1630 copies = []
1630 copies = []
1631 minrev, maxrev = min(revs), max(revs)
1631 minrev, maxrev = min(revs), max(revs)
1632 def filerevgen(filelog, last):
1632 def filerevgen(filelog, last):
1633 """
1633 """
1634 Only files, no patterns. Check the history of each file.
1634 Only files, no patterns. Check the history of each file.
1635
1635
1636 Examines filelog entries within minrev, maxrev linkrev range
1636 Examines filelog entries within minrev, maxrev linkrev range
1637 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1637 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1638 tuples in backwards order
1638 tuples in backwards order
1639 """
1639 """
1640 cl_count = len(repo)
1640 cl_count = len(repo)
1641 revs = []
1641 revs = []
1642 for j in xrange(0, last + 1):
1642 for j in xrange(0, last + 1):
1643 linkrev = filelog.linkrev(j)
1643 linkrev = filelog.linkrev(j)
1644 if linkrev < minrev:
1644 if linkrev < minrev:
1645 continue
1645 continue
1646 # only yield rev for which we have the changelog, it can
1646 # only yield rev for which we have the changelog, it can
1647 # happen while doing "hg log" during a pull or commit
1647 # happen while doing "hg log" during a pull or commit
1648 if linkrev >= cl_count:
1648 if linkrev >= cl_count:
1649 break
1649 break
1650
1650
1651 parentlinkrevs = []
1651 parentlinkrevs = []
1652 for p in filelog.parentrevs(j):
1652 for p in filelog.parentrevs(j):
1653 if p != nullrev:
1653 if p != nullrev:
1654 parentlinkrevs.append(filelog.linkrev(p))
1654 parentlinkrevs.append(filelog.linkrev(p))
1655 n = filelog.node(j)
1655 n = filelog.node(j)
1656 revs.append((linkrev, parentlinkrevs,
1656 revs.append((linkrev, parentlinkrevs,
1657 follow and filelog.renamed(n)))
1657 follow and filelog.renamed(n)))
1658
1658
1659 return reversed(revs)
1659 return reversed(revs)
1660 def iterfiles():
1660 def iterfiles():
1661 pctx = repo['.']
1661 pctx = repo['.']
1662 for filename in match.files():
1662 for filename in match.files():
1663 if follow:
1663 if follow:
1664 if filename not in pctx:
1664 if filename not in pctx:
1665 raise error.Abort(_('cannot follow file not in parent '
1665 raise error.Abort(_('cannot follow file not in parent '
1666 'revision: "%s"') % filename)
1666 'revision: "%s"') % filename)
1667 yield filename, pctx[filename].filenode()
1667 yield filename, pctx[filename].filenode()
1668 else:
1668 else:
1669 yield filename, None
1669 yield filename, None
1670 for filename_node in copies:
1670 for filename_node in copies:
1671 yield filename_node
1671 yield filename_node
1672
1672
1673 for file_, node in iterfiles():
1673 for file_, node in iterfiles():
1674 filelog = repo.file(file_)
1674 filelog = repo.file(file_)
1675 if not len(filelog):
1675 if not len(filelog):
1676 if node is None:
1676 if node is None:
1677 # A zero count may be a directory or deleted file, so
1677 # A zero count may be a directory or deleted file, so
1678 # try to find matching entries on the slow path.
1678 # try to find matching entries on the slow path.
1679 if follow:
1679 if follow:
1680 raise error.Abort(
1680 raise error.Abort(
1681 _('cannot follow nonexistent file: "%s"') % file_)
1681 _('cannot follow nonexistent file: "%s"') % file_)
1682 raise FileWalkError("Cannot walk via filelog")
1682 raise FileWalkError("Cannot walk via filelog")
1683 else:
1683 else:
1684 continue
1684 continue
1685
1685
1686 if node is None:
1686 if node is None:
1687 last = len(filelog) - 1
1687 last = len(filelog) - 1
1688 else:
1688 else:
1689 last = filelog.rev(node)
1689 last = filelog.rev(node)
1690
1690
1691 # keep track of all ancestors of the file
1691 # keep track of all ancestors of the file
1692 ancestors = set([filelog.linkrev(last)])
1692 ancestors = set([filelog.linkrev(last)])
1693
1693
1694 # iterate from latest to oldest revision
1694 # iterate from latest to oldest revision
1695 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1695 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1696 if not follow:
1696 if not follow:
1697 if rev > maxrev:
1697 if rev > maxrev:
1698 continue
1698 continue
1699 else:
1699 else:
1700 # Note that last might not be the first interesting
1700 # Note that last might not be the first interesting
1701 # rev to us:
1701 # rev to us:
1702 # if the file has been changed after maxrev, we'll
1702 # if the file has been changed after maxrev, we'll
1703 # have linkrev(last) > maxrev, and we still need
1703 # have linkrev(last) > maxrev, and we still need
1704 # to explore the file graph
1704 # to explore the file graph
1705 if rev not in ancestors:
1705 if rev not in ancestors:
1706 continue
1706 continue
1707 # XXX insert 1327 fix here
1707 # XXX insert 1327 fix here
1708 if flparentlinkrevs:
1708 if flparentlinkrevs:
1709 ancestors.update(flparentlinkrevs)
1709 ancestors.update(flparentlinkrevs)
1710
1710
1711 fncache.setdefault(rev, []).append(file_)
1711 fncache.setdefault(rev, []).append(file_)
1712 wanted.add(rev)
1712 wanted.add(rev)
1713 if copied:
1713 if copied:
1714 copies.append(copied)
1714 copies.append(copied)
1715
1715
1716 return wanted
1716 return wanted
1717
1717
1718 class _followfilter(object):
1718 class _followfilter(object):
1719 def __init__(self, repo, onlyfirst=False):
1719 def __init__(self, repo, onlyfirst=False):
1720 self.repo = repo
1720 self.repo = repo
1721 self.startrev = nullrev
1721 self.startrev = nullrev
1722 self.roots = set()
1722 self.roots = set()
1723 self.onlyfirst = onlyfirst
1723 self.onlyfirst = onlyfirst
1724
1724
1725 def match(self, rev):
1725 def match(self, rev):
1726 def realparents(rev):
1726 def realparents(rev):
1727 if self.onlyfirst:
1727 if self.onlyfirst:
1728 return self.repo.changelog.parentrevs(rev)[0:1]
1728 return self.repo.changelog.parentrevs(rev)[0:1]
1729 else:
1729 else:
1730 return filter(lambda x: x != nullrev,
1730 return filter(lambda x: x != nullrev,
1731 self.repo.changelog.parentrevs(rev))
1731 self.repo.changelog.parentrevs(rev))
1732
1732
1733 if self.startrev == nullrev:
1733 if self.startrev == nullrev:
1734 self.startrev = rev
1734 self.startrev = rev
1735 return True
1735 return True
1736
1736
1737 if rev > self.startrev:
1737 if rev > self.startrev:
1738 # forward: all descendants
1738 # forward: all descendants
1739 if not self.roots:
1739 if not self.roots:
1740 self.roots.add(self.startrev)
1740 self.roots.add(self.startrev)
1741 for parent in realparents(rev):
1741 for parent in realparents(rev):
1742 if parent in self.roots:
1742 if parent in self.roots:
1743 self.roots.add(rev)
1743 self.roots.add(rev)
1744 return True
1744 return True
1745 else:
1745 else:
1746 # backwards: all parents
1746 # backwards: all parents
1747 if not self.roots:
1747 if not self.roots:
1748 self.roots.update(realparents(self.startrev))
1748 self.roots.update(realparents(self.startrev))
1749 if rev in self.roots:
1749 if rev in self.roots:
1750 self.roots.remove(rev)
1750 self.roots.remove(rev)
1751 self.roots.update(realparents(rev))
1751 self.roots.update(realparents(rev))
1752 return True
1752 return True
1753
1753
1754 return False
1754 return False
1755
1755
1756 def walkchangerevs(repo, match, opts, prepare):
1756 def walkchangerevs(repo, match, opts, prepare):
1757 '''Iterate over files and the revs in which they changed.
1757 '''Iterate over files and the revs in which they changed.
1758
1758
1759 Callers most commonly need to iterate backwards over the history
1759 Callers most commonly need to iterate backwards over the history
1760 in which they are interested. Doing so has awful (quadratic-looking)
1760 in which they are interested. Doing so has awful (quadratic-looking)
1761 performance, so we use iterators in a "windowed" way.
1761 performance, so we use iterators in a "windowed" way.
1762
1762
1763 We walk a window of revisions in the desired order. Within the
1763 We walk a window of revisions in the desired order. Within the
1764 window, we first walk forwards to gather data, then in the desired
1764 window, we first walk forwards to gather data, then in the desired
1765 order (usually backwards) to display it.
1765 order (usually backwards) to display it.
1766
1766
1767 This function returns an iterator yielding contexts. Before
1767 This function returns an iterator yielding contexts. Before
1768 yielding each context, the iterator will first call the prepare
1768 yielding each context, the iterator will first call the prepare
1769 function on each context in the window in forward order.'''
1769 function on each context in the window in forward order.'''
1770
1770
1771 follow = opts.get('follow') or opts.get('follow_first')
1771 follow = opts.get('follow') or opts.get('follow_first')
1772 revs = _logrevs(repo, opts)
1772 revs = _logrevs(repo, opts)
1773 if not revs:
1773 if not revs:
1774 return []
1774 return []
1775 wanted = set()
1775 wanted = set()
1776 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1776 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1777 opts.get('removed'))
1777 opts.get('removed'))
1778 fncache = {}
1778 fncache = {}
1779 change = repo.changectx
1779 change = repo.changectx
1780
1780
1781 # First step is to fill wanted, the set of revisions that we want to yield.
1781 # First step is to fill wanted, the set of revisions that we want to yield.
1782 # When it does not induce extra cost, we also fill fncache for revisions in
1782 # When it does not induce extra cost, we also fill fncache for revisions in
1783 # wanted: a cache of filenames that were changed (ctx.files()) and that
1783 # wanted: a cache of filenames that were changed (ctx.files()) and that
1784 # match the file filtering conditions.
1784 # match the file filtering conditions.
1785
1785
1786 if match.always():
1786 if match.always():
1787 # No files, no patterns. Display all revs.
1787 # No files, no patterns. Display all revs.
1788 wanted = revs
1788 wanted = revs
1789 elif not slowpath:
1789 elif not slowpath:
1790 # We only have to read through the filelog to find wanted revisions
1790 # We only have to read through the filelog to find wanted revisions
1791
1791
1792 try:
1792 try:
1793 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1793 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1794 except FileWalkError:
1794 except FileWalkError:
1795 slowpath = True
1795 slowpath = True
1796
1796
1797 # We decided to fall back to the slowpath because at least one
1797 # We decided to fall back to the slowpath because at least one
1798 # of the paths was not a file. Check to see if at least one of them
1798 # of the paths was not a file. Check to see if at least one of them
1799 # existed in history, otherwise simply return
1799 # existed in history, otherwise simply return
1800 for path in match.files():
1800 for path in match.files():
1801 if path == '.' or path in repo.store:
1801 if path == '.' or path in repo.store:
1802 break
1802 break
1803 else:
1803 else:
1804 return []
1804 return []
1805
1805
1806 if slowpath:
1806 if slowpath:
1807 # We have to read the changelog to match filenames against
1807 # We have to read the changelog to match filenames against
1808 # changed files
1808 # changed files
1809
1809
1810 if follow:
1810 if follow:
1811 raise error.Abort(_('can only follow copies/renames for explicit '
1811 raise error.Abort(_('can only follow copies/renames for explicit '
1812 'filenames'))
1812 'filenames'))
1813
1813
1814 # The slow path checks files modified in every changeset.
1814 # The slow path checks files modified in every changeset.
1815 # This is really slow on large repos, so compute the set lazily.
1815 # This is really slow on large repos, so compute the set lazily.
1816 class lazywantedset(object):
1816 class lazywantedset(object):
1817 def __init__(self):
1817 def __init__(self):
1818 self.set = set()
1818 self.set = set()
1819 self.revs = set(revs)
1819 self.revs = set(revs)
1820
1820
1821 # No need to worry about locality here because it will be accessed
1821 # No need to worry about locality here because it will be accessed
1822 # in the same order as the increasing window below.
1822 # in the same order as the increasing window below.
1823 def __contains__(self, value):
1823 def __contains__(self, value):
1824 if value in self.set:
1824 if value in self.set:
1825 return True
1825 return True
1826 elif not value in self.revs:
1826 elif not value in self.revs:
1827 return False
1827 return False
1828 else:
1828 else:
1829 self.revs.discard(value)
1829 self.revs.discard(value)
1830 ctx = change(value)
1830 ctx = change(value)
1831 matches = filter(match, ctx.files())
1831 matches = filter(match, ctx.files())
1832 if matches:
1832 if matches:
1833 fncache[value] = matches
1833 fncache[value] = matches
1834 self.set.add(value)
1834 self.set.add(value)
1835 return True
1835 return True
1836 return False
1836 return False
1837
1837
1838 def discard(self, value):
1838 def discard(self, value):
1839 self.revs.discard(value)
1839 self.revs.discard(value)
1840 self.set.discard(value)
1840 self.set.discard(value)
1841
1841
1842 wanted = lazywantedset()
1842 wanted = lazywantedset()
1843
1843
1844 # it might be worthwhile to do this in the iterator if the rev range
1844 # it might be worthwhile to do this in the iterator if the rev range
1845 # is descending and the prune args are all within that range
1845 # is descending and the prune args are all within that range
1846 for rev in opts.get('prune', ()):
1846 for rev in opts.get('prune', ()):
1847 rev = repo[rev].rev()
1847 rev = repo[rev].rev()
1848 ff = _followfilter(repo)
1848 ff = _followfilter(repo)
1849 stop = min(revs[0], revs[-1])
1849 stop = min(revs[0], revs[-1])
1850 for x in xrange(rev, stop - 1, -1):
1850 for x in xrange(rev, stop - 1, -1):
1851 if ff.match(x):
1851 if ff.match(x):
1852 wanted = wanted - [x]
1852 wanted = wanted - [x]
1853
1853
1854 # Now that wanted is correctly initialized, we can iterate over the
1854 # Now that wanted is correctly initialized, we can iterate over the
1855 # revision range, yielding only revisions in wanted.
1855 # revision range, yielding only revisions in wanted.
1856 def iterate():
1856 def iterate():
1857 if follow and match.always():
1857 if follow and match.always():
1858 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1858 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1859 def want(rev):
1859 def want(rev):
1860 return ff.match(rev) and rev in wanted
1860 return ff.match(rev) and rev in wanted
1861 else:
1861 else:
1862 def want(rev):
1862 def want(rev):
1863 return rev in wanted
1863 return rev in wanted
1864
1864
1865 it = iter(revs)
1865 it = iter(revs)
1866 stopiteration = False
1866 stopiteration = False
1867 for windowsize in increasingwindows():
1867 for windowsize in increasingwindows():
1868 nrevs = []
1868 nrevs = []
1869 for i in xrange(windowsize):
1869 for i in xrange(windowsize):
1870 rev = next(it, None)
1870 rev = next(it, None)
1871 if rev is None:
1871 if rev is None:
1872 stopiteration = True
1872 stopiteration = True
1873 break
1873 break
1874 elif want(rev):
1874 elif want(rev):
1875 nrevs.append(rev)
1875 nrevs.append(rev)
1876 for rev in sorted(nrevs):
1876 for rev in sorted(nrevs):
1877 fns = fncache.get(rev)
1877 fns = fncache.get(rev)
1878 ctx = change(rev)
1878 ctx = change(rev)
1879 if not fns:
1879 if not fns:
1880 def fns_generator():
1880 def fns_generator():
1881 for f in ctx.files():
1881 for f in ctx.files():
1882 if match(f):
1882 if match(f):
1883 yield f
1883 yield f
1884 fns = fns_generator()
1884 fns = fns_generator()
1885 prepare(ctx, fns)
1885 prepare(ctx, fns)
1886 for rev in nrevs:
1886 for rev in nrevs:
1887 yield change(rev)
1887 yield change(rev)
1888
1888
1889 if stopiteration:
1889 if stopiteration:
1890 break
1890 break
1891
1891
1892 return iterate()
1892 return iterate()
1893
1893
1894 def _makefollowlogfilematcher(repo, files, followfirst):
1894 def _makefollowlogfilematcher(repo, files, followfirst):
1895 # When displaying a revision with --patch --follow FILE, we have
1895 # When displaying a revision with --patch --follow FILE, we have
1896 # to know which file of the revision must be diffed. With
1896 # to know which file of the revision must be diffed. With
1897 # --follow, we want the names of the ancestors of FILE in the
1897 # --follow, we want the names of the ancestors of FILE in the
1898 # revision, stored in "fcache". "fcache" is populated by
1898 # revision, stored in "fcache". "fcache" is populated by
1899 # reproducing the graph traversal already done by --follow revset
1899 # reproducing the graph traversal already done by --follow revset
1900 # and relating revs to file names (which is not "correct" but
1900 # and relating revs to file names (which is not "correct" but
1901 # good enough).
1901 # good enough).
1902 fcache = {}
1902 fcache = {}
1903 fcacheready = [False]
1903 fcacheready = [False]
1904 pctx = repo['.']
1904 pctx = repo['.']
1905
1905
1906 def populate():
1906 def populate():
1907 for fn in files:
1907 for fn in files:
1908 fctx = pctx[fn]
1908 fctx = pctx[fn]
1909 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1909 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1910 for c in fctx.ancestors(followfirst=followfirst):
1910 for c in fctx.ancestors(followfirst=followfirst):
1911 fcache.setdefault(c.rev(), set()).add(c.path())
1911 fcache.setdefault(c.rev(), set()).add(c.path())
1912
1912
1913 def filematcher(rev):
1913 def filematcher(rev):
1914 if not fcacheready[0]:
1914 if not fcacheready[0]:
1915 # Lazy initialization
1915 # Lazy initialization
1916 fcacheready[0] = True
1916 fcacheready[0] = True
1917 populate()
1917 populate()
1918 return scmutil.matchfiles(repo, fcache.get(rev, []))
1918 return scmutil.matchfiles(repo, fcache.get(rev, []))
1919
1919
1920 return filematcher
1920 return filematcher
1921
1921
1922 def _makenofollowlogfilematcher(repo, pats, opts):
1922 def _makenofollowlogfilematcher(repo, pats, opts):
1923 '''hook for extensions to override the filematcher for non-follow cases'''
1923 '''hook for extensions to override the filematcher for non-follow cases'''
1924 return None
1924 return None
1925
1925
1926 def _makelogrevset(repo, pats, opts, revs):
1926 def _makelogrevset(repo, pats, opts, revs):
1927 """Return (expr, filematcher) where expr is a revset string built
1927 """Return (expr, filematcher) where expr is a revset string built
1928 from log options and file patterns or None. If --stat or --patch
1928 from log options and file patterns or None. If --stat or --patch
1929 are not passed filematcher is None. Otherwise it is a callable
1929 are not passed filematcher is None. Otherwise it is a callable
1930 taking a revision number and returning a match objects filtering
1930 taking a revision number and returning a match objects filtering
1931 the files to be detailed when displaying the revision.
1931 the files to be detailed when displaying the revision.
1932 """
1932 """
1933 opt2revset = {
1933 opt2revset = {
1934 'no_merges': ('not merge()', None),
1934 'no_merges': ('not merge()', None),
1935 'only_merges': ('merge()', None),
1935 'only_merges': ('merge()', None),
1936 '_ancestors': ('ancestors(%(val)s)', None),
1936 '_ancestors': ('ancestors(%(val)s)', None),
1937 '_fancestors': ('_firstancestors(%(val)s)', None),
1937 '_fancestors': ('_firstancestors(%(val)s)', None),
1938 '_descendants': ('descendants(%(val)s)', None),
1938 '_descendants': ('descendants(%(val)s)', None),
1939 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1939 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1940 '_matchfiles': ('_matchfiles(%(val)s)', None),
1940 '_matchfiles': ('_matchfiles(%(val)s)', None),
1941 'date': ('date(%(val)r)', None),
1941 'date': ('date(%(val)r)', None),
1942 'branch': ('branch(%(val)r)', ' or '),
1942 'branch': ('branch(%(val)r)', ' or '),
1943 '_patslog': ('filelog(%(val)r)', ' or '),
1943 '_patslog': ('filelog(%(val)r)', ' or '),
1944 '_patsfollow': ('follow(%(val)r)', ' or '),
1944 '_patsfollow': ('follow(%(val)r)', ' or '),
1945 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1945 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1946 'keyword': ('keyword(%(val)r)', ' or '),
1946 'keyword': ('keyword(%(val)r)', ' or '),
1947 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1947 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1948 'user': ('user(%(val)r)', ' or '),
1948 'user': ('user(%(val)r)', ' or '),
1949 }
1949 }
1950
1950
1951 opts = dict(opts)
1951 opts = dict(opts)
1952 # follow or not follow?
1952 # follow or not follow?
1953 follow = opts.get('follow') or opts.get('follow_first')
1953 follow = opts.get('follow') or opts.get('follow_first')
1954 if opts.get('follow_first'):
1954 if opts.get('follow_first'):
1955 followfirst = 1
1955 followfirst = 1
1956 else:
1956 else:
1957 followfirst = 0
1957 followfirst = 0
1958 # --follow with FILE behavior depends on revs...
1958 # --follow with FILE behavior depends on revs...
1959 it = iter(revs)
1959 it = iter(revs)
1960 startrev = next(it)
1960 startrev = next(it)
1961 followdescendants = startrev < next(it, startrev)
1961 followdescendants = startrev < next(it, startrev)
1962
1962
1963 # branch and only_branch are really aliases and must be handled at
1963 # branch and only_branch are really aliases and must be handled at
1964 # the same time
1964 # the same time
1965 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1965 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1966 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1966 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1967 # pats/include/exclude are passed to match.match() directly in
1967 # pats/include/exclude are passed to match.match() directly in
1968 # _matchfiles() revset but walkchangerevs() builds its matcher with
1968 # _matchfiles() revset but walkchangerevs() builds its matcher with
1969 # scmutil.match(). The difference is input pats are globbed on
1969 # scmutil.match(). The difference is input pats are globbed on
1970 # platforms without shell expansion (windows).
1970 # platforms without shell expansion (windows).
1971 wctx = repo[None]
1971 wctx = repo[None]
1972 match, pats = scmutil.matchandpats(wctx, pats, opts)
1972 match, pats = scmutil.matchandpats(wctx, pats, opts)
1973 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1973 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1974 opts.get('removed'))
1974 opts.get('removed'))
1975 if not slowpath:
1975 if not slowpath:
1976 for f in match.files():
1976 for f in match.files():
1977 if follow and f not in wctx:
1977 if follow and f not in wctx:
1978 # If the file exists, it may be a directory, so let it
1978 # If the file exists, it may be a directory, so let it
1979 # take the slow path.
1979 # take the slow path.
1980 if os.path.exists(repo.wjoin(f)):
1980 if os.path.exists(repo.wjoin(f)):
1981 slowpath = True
1981 slowpath = True
1982 continue
1982 continue
1983 else:
1983 else:
1984 raise error.Abort(_('cannot follow file not in parent '
1984 raise error.Abort(_('cannot follow file not in parent '
1985 'revision: "%s"') % f)
1985 'revision: "%s"') % f)
1986 filelog = repo.file(f)
1986 filelog = repo.file(f)
1987 if not filelog:
1987 if not filelog:
1988 # A zero count may be a directory or deleted file, so
1988 # A zero count may be a directory or deleted file, so
1989 # try to find matching entries on the slow path.
1989 # try to find matching entries on the slow path.
1990 if follow:
1990 if follow:
1991 raise error.Abort(
1991 raise error.Abort(
1992 _('cannot follow nonexistent file: "%s"') % f)
1992 _('cannot follow nonexistent file: "%s"') % f)
1993 slowpath = True
1993 slowpath = True
1994
1994
1995 # We decided to fall back to the slowpath because at least one
1995 # We decided to fall back to the slowpath because at least one
1996 # of the paths was not a file. Check to see if at least one of them
1996 # of the paths was not a file. Check to see if at least one of them
1997 # existed in history - in that case, we'll continue down the
1997 # existed in history - in that case, we'll continue down the
1998 # slowpath; otherwise, we can turn off the slowpath
1998 # slowpath; otherwise, we can turn off the slowpath
1999 if slowpath:
1999 if slowpath:
2000 for path in match.files():
2000 for path in match.files():
2001 if path == '.' or path in repo.store:
2001 if path == '.' or path in repo.store:
2002 break
2002 break
2003 else:
2003 else:
2004 slowpath = False
2004 slowpath = False
2005
2005
2006 fpats = ('_patsfollow', '_patsfollowfirst')
2006 fpats = ('_patsfollow', '_patsfollowfirst')
2007 fnopats = (('_ancestors', '_fancestors'),
2007 fnopats = (('_ancestors', '_fancestors'),
2008 ('_descendants', '_fdescendants'))
2008 ('_descendants', '_fdescendants'))
2009 if slowpath:
2009 if slowpath:
2010 # See walkchangerevs() slow path.
2010 # See walkchangerevs() slow path.
2011 #
2011 #
2012 # pats/include/exclude cannot be represented as separate
2012 # pats/include/exclude cannot be represented as separate
2013 # revset expressions as their filtering logic applies at file
2013 # revset expressions as their filtering logic applies at file
2014 # level. For instance "-I a -X a" matches a revision touching
2014 # level. For instance "-I a -X a" matches a revision touching
2015 # "a" and "b" while "file(a) and not file(b)" does
2015 # "a" and "b" while "file(a) and not file(b)" does
2016 # not. Besides, filesets are evaluated against the working
2016 # not. Besides, filesets are evaluated against the working
2017 # directory.
2017 # directory.
2018 matchargs = ['r:', 'd:relpath']
2018 matchargs = ['r:', 'd:relpath']
2019 for p in pats:
2019 for p in pats:
2020 matchargs.append('p:' + p)
2020 matchargs.append('p:' + p)
2021 for p in opts.get('include', []):
2021 for p in opts.get('include', []):
2022 matchargs.append('i:' + p)
2022 matchargs.append('i:' + p)
2023 for p in opts.get('exclude', []):
2023 for p in opts.get('exclude', []):
2024 matchargs.append('x:' + p)
2024 matchargs.append('x:' + p)
2025 matchargs = ','.join(('%r' % p) for p in matchargs)
2025 matchargs = ','.join(('%r' % p) for p in matchargs)
2026 opts['_matchfiles'] = matchargs
2026 opts['_matchfiles'] = matchargs
2027 if follow:
2027 if follow:
2028 opts[fnopats[0][followfirst]] = '.'
2028 opts[fnopats[0][followfirst]] = '.'
2029 else:
2029 else:
2030 if follow:
2030 if follow:
2031 if pats:
2031 if pats:
2032 # follow() revset interprets its file argument as a
2032 # follow() revset interprets its file argument as a
2033 # manifest entry, so use match.files(), not pats.
2033 # manifest entry, so use match.files(), not pats.
2034 opts[fpats[followfirst]] = list(match.files())
2034 opts[fpats[followfirst]] = list(match.files())
2035 else:
2035 else:
2036 op = fnopats[followdescendants][followfirst]
2036 op = fnopats[followdescendants][followfirst]
2037 opts[op] = 'rev(%d)' % startrev
2037 opts[op] = 'rev(%d)' % startrev
2038 else:
2038 else:
2039 opts['_patslog'] = list(pats)
2039 opts['_patslog'] = list(pats)
2040
2040
2041 filematcher = None
2041 filematcher = None
2042 if opts.get('patch') or opts.get('stat'):
2042 if opts.get('patch') or opts.get('stat'):
2043 # When following files, track renames via a special matcher.
2043 # When following files, track renames via a special matcher.
2044 # If we're forced to take the slowpath it means we're following
2044 # If we're forced to take the slowpath it means we're following
2045 # at least one pattern/directory, so don't bother with rename tracking.
2045 # at least one pattern/directory, so don't bother with rename tracking.
2046 if follow and not match.always() and not slowpath:
2046 if follow and not match.always() and not slowpath:
2047 # _makefollowlogfilematcher expects its files argument to be
2047 # _makefollowlogfilematcher expects its files argument to be
2048 # relative to the repo root, so use match.files(), not pats.
2048 # relative to the repo root, so use match.files(), not pats.
2049 filematcher = _makefollowlogfilematcher(repo, match.files(),
2049 filematcher = _makefollowlogfilematcher(repo, match.files(),
2050 followfirst)
2050 followfirst)
2051 else:
2051 else:
2052 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2052 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2053 if filematcher is None:
2053 if filematcher is None:
2054 filematcher = lambda rev: match
2054 filematcher = lambda rev: match
2055
2055
2056 expr = []
2056 expr = []
2057 for op, val in sorted(opts.iteritems()):
2057 for op, val in sorted(opts.iteritems()):
2058 if not val:
2058 if not val:
2059 continue
2059 continue
2060 if op not in opt2revset:
2060 if op not in opt2revset:
2061 continue
2061 continue
2062 revop, andor = opt2revset[op]
2062 revop, andor = opt2revset[op]
2063 if '%(val)' not in revop:
2063 if '%(val)' not in revop:
2064 expr.append(revop)
2064 expr.append(revop)
2065 else:
2065 else:
2066 if not isinstance(val, list):
2066 if not isinstance(val, list):
2067 e = revop % {'val': val}
2067 e = revop % {'val': val}
2068 else:
2068 else:
2069 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2069 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2070 expr.append(e)
2070 expr.append(e)
2071
2071
2072 if expr:
2072 if expr:
2073 expr = '(' + ' and '.join(expr) + ')'
2073 expr = '(' + ' and '.join(expr) + ')'
2074 else:
2074 else:
2075 expr = None
2075 expr = None
2076 return expr, filematcher
2076 return expr, filematcher
2077
2077
2078 def _logrevs(repo, opts):
2078 def _logrevs(repo, opts):
2079 # Default --rev value depends on --follow but --follow behavior
2079 # Default --rev value depends on --follow but --follow behavior
2080 # depends on revisions resolved from --rev...
2080 # depends on revisions resolved from --rev...
2081 follow = opts.get('follow') or opts.get('follow_first')
2081 follow = opts.get('follow') or opts.get('follow_first')
2082 if opts.get('rev'):
2082 if opts.get('rev'):
2083 revs = scmutil.revrange(repo, opts['rev'])
2083 revs = scmutil.revrange(repo, opts['rev'])
2084 elif follow and repo.dirstate.p1() == nullid:
2084 elif follow and repo.dirstate.p1() == nullid:
2085 revs = revset.baseset()
2085 revs = revset.baseset()
2086 elif follow:
2086 elif follow:
2087 revs = repo.revs('reverse(:.)')
2087 revs = repo.revs('reverse(:.)')
2088 else:
2088 else:
2089 revs = revset.spanset(repo)
2089 revs = revset.spanset(repo)
2090 revs.reverse()
2090 revs.reverse()
2091 return revs
2091 return revs
2092
2092
2093 def getgraphlogrevs(repo, pats, opts):
2093 def getgraphlogrevs(repo, pats, opts):
2094 """Return (revs, expr, filematcher) where revs is an iterable of
2094 """Return (revs, expr, filematcher) where revs is an iterable of
2095 revision numbers, expr is a revset string built from log options
2095 revision numbers, expr is a revset string built from log options
2096 and file patterns or None, and used to filter 'revs'. If --stat or
2096 and file patterns or None, and used to filter 'revs'. If --stat or
2097 --patch are not passed filematcher is None. Otherwise it is a
2097 --patch are not passed filematcher is None. Otherwise it is a
2098 callable taking a revision number and returning a match objects
2098 callable taking a revision number and returning a match objects
2099 filtering the files to be detailed when displaying the revision.
2099 filtering the files to be detailed when displaying the revision.
2100 """
2100 """
2101 limit = loglimit(opts)
2101 limit = loglimit(opts)
2102 revs = _logrevs(repo, opts)
2102 revs = _logrevs(repo, opts)
2103 if not revs:
2103 if not revs:
2104 return revset.baseset(), None, None
2104 return revset.baseset(), None, None
2105 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2105 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2106 if opts.get('rev'):
2106 if opts.get('rev'):
2107 # User-specified revs might be unsorted, but don't sort before
2107 # User-specified revs might be unsorted, but don't sort before
2108 # _makelogrevset because it might depend on the order of revs
2108 # _makelogrevset because it might depend on the order of revs
2109 if not (revs.isdescending() or revs.istopo()):
2109 if not (revs.isdescending() or revs.istopo()):
2110 revs.sort(reverse=True)
2110 revs.sort(reverse=True)
2111 if expr:
2111 if expr:
2112 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2112 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2113 revs = matcher(repo, revs)
2113 revs = matcher(repo, revs)
2114 if limit is not None:
2114 if limit is not None:
2115 limitedrevs = []
2115 limitedrevs = []
2116 for idx, rev in enumerate(revs):
2116 for idx, rev in enumerate(revs):
2117 if idx >= limit:
2117 if idx >= limit:
2118 break
2118 break
2119 limitedrevs.append(rev)
2119 limitedrevs.append(rev)
2120 revs = revset.baseset(limitedrevs)
2120 revs = revset.baseset(limitedrevs)
2121
2121
2122 return revs, expr, filematcher
2122 return revs, expr, filematcher
2123
2123
2124 def getlogrevs(repo, pats, opts):
2124 def getlogrevs(repo, pats, opts):
2125 """Return (revs, expr, filematcher) where revs is an iterable of
2125 """Return (revs, expr, filematcher) where revs is an iterable of
2126 revision numbers, expr is a revset string built from log options
2126 revision numbers, expr is a revset string built from log options
2127 and file patterns or None, and used to filter 'revs'. If --stat or
2127 and file patterns or None, and used to filter 'revs'. If --stat or
2128 --patch are not passed filematcher is None. Otherwise it is a
2128 --patch are not passed filematcher is None. Otherwise it is a
2129 callable taking a revision number and returning a match objects
2129 callable taking a revision number and returning a match objects
2130 filtering the files to be detailed when displaying the revision.
2130 filtering the files to be detailed when displaying the revision.
2131 """
2131 """
2132 limit = loglimit(opts)
2132 limit = loglimit(opts)
2133 revs = _logrevs(repo, opts)
2133 revs = _logrevs(repo, opts)
2134 if not revs:
2134 if not revs:
2135 return revset.baseset([]), None, None
2135 return revset.baseset([]), None, None
2136 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2136 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2137 if expr:
2137 if expr:
2138 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2138 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2139 revs = matcher(repo, revs)
2139 revs = matcher(repo, revs)
2140 if limit is not None:
2140 if limit is not None:
2141 limitedrevs = []
2141 limitedrevs = []
2142 for idx, r in enumerate(revs):
2142 for idx, r in enumerate(revs):
2143 if limit <= idx:
2143 if limit <= idx:
2144 break
2144 break
2145 limitedrevs.append(r)
2145 limitedrevs.append(r)
2146 revs = revset.baseset(limitedrevs)
2146 revs = revset.baseset(limitedrevs)
2147
2147
2148 return revs, expr, filematcher
2148 return revs, expr, filematcher
2149
2149
2150 def _graphnodeformatter(ui, displayer):
2150 def _graphnodeformatter(ui, displayer):
2151 spec = ui.config('ui', 'graphnodetemplate')
2151 spec = ui.config('ui', 'graphnodetemplate')
2152 if not spec:
2152 if not spec:
2153 return templatekw.showgraphnode # fast path for "{graphnode}"
2153 return templatekw.showgraphnode # fast path for "{graphnode}"
2154
2154
2155 templ = formatter.gettemplater(ui, 'graphnode', spec)
2155 templ = formatter.gettemplater(ui, 'graphnode', spec)
2156 cache = {}
2156 cache = {}
2157 if isinstance(displayer, changeset_templater):
2157 if isinstance(displayer, changeset_templater):
2158 cache = displayer.cache # reuse cache of slow templates
2158 cache = displayer.cache # reuse cache of slow templates
2159 props = templatekw.keywords.copy()
2159 props = templatekw.keywords.copy()
2160 props['templ'] = templ
2160 props['templ'] = templ
2161 props['cache'] = cache
2161 props['cache'] = cache
2162 def formatnode(repo, ctx):
2162 def formatnode(repo, ctx):
2163 props['ctx'] = ctx
2163 props['ctx'] = ctx
2164 props['repo'] = repo
2164 props['repo'] = repo
2165 props['ui'] = repo.ui
2165 props['ui'] = repo.ui
2166 props['revcache'] = {}
2166 props['revcache'] = {}
2167 return templater.stringify(templ('graphnode', **props))
2167 return templater.stringify(templ('graphnode', **props))
2168 return formatnode
2168 return formatnode
2169
2169
2170 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2170 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2171 filematcher=None):
2171 filematcher=None):
2172 formatnode = _graphnodeformatter(ui, displayer)
2172 formatnode = _graphnodeformatter(ui, displayer)
2173 state = graphmod.asciistate()
2173 state = graphmod.asciistate()
2174 styles = state['styles']
2174 styles = state['styles']
2175
2175
2176 # only set graph styling if HGPLAIN is not set.
2176 # only set graph styling if HGPLAIN is not set.
2177 if ui.plain('graph'):
2177 if ui.plain('graph'):
2178 # set all edge styles to |, the default pre-3.8 behaviour
2178 # set all edge styles to |, the default pre-3.8 behaviour
2179 styles.update(dict.fromkeys(styles, '|'))
2179 styles.update(dict.fromkeys(styles, '|'))
2180 else:
2180 else:
2181 edgetypes = {
2181 edgetypes = {
2182 'parent': graphmod.PARENT,
2182 'parent': graphmod.PARENT,
2183 'grandparent': graphmod.GRANDPARENT,
2183 'grandparent': graphmod.GRANDPARENT,
2184 'missing': graphmod.MISSINGPARENT
2184 'missing': graphmod.MISSINGPARENT
2185 }
2185 }
2186 for name, key in edgetypes.items():
2186 for name, key in edgetypes.items():
2187 # experimental config: experimental.graphstyle.*
2187 # experimental config: experimental.graphstyle.*
2188 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2188 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2189 styles[key])
2189 styles[key])
2190 if not styles[key]:
2190 if not styles[key]:
2191 styles[key] = None
2191 styles[key] = None
2192
2192
2193 # experimental config: experimental.graphshorten
2193 # experimental config: experimental.graphshorten
2194 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2194 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2195
2195
2196 for rev, type, ctx, parents in dag:
2196 for rev, type, ctx, parents in dag:
2197 char = formatnode(repo, ctx)
2197 char = formatnode(repo, ctx)
2198 copies = None
2198 copies = None
2199 if getrenamed and ctx.rev():
2199 if getrenamed and ctx.rev():
2200 copies = []
2200 copies = []
2201 for fn in ctx.files():
2201 for fn in ctx.files():
2202 rename = getrenamed(fn, ctx.rev())
2202 rename = getrenamed(fn, ctx.rev())
2203 if rename:
2203 if rename:
2204 copies.append((fn, rename[0]))
2204 copies.append((fn, rename[0]))
2205 revmatchfn = None
2205 revmatchfn = None
2206 if filematcher is not None:
2206 if filematcher is not None:
2207 revmatchfn = filematcher(ctx.rev())
2207 revmatchfn = filematcher(ctx.rev())
2208 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2208 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2209 lines = displayer.hunk.pop(rev).split('\n')
2209 lines = displayer.hunk.pop(rev).split('\n')
2210 if not lines[-1]:
2210 if not lines[-1]:
2211 del lines[-1]
2211 del lines[-1]
2212 displayer.flush(ctx)
2212 displayer.flush(ctx)
2213 edges = edgefn(type, char, lines, state, rev, parents)
2213 edges = edgefn(type, char, lines, state, rev, parents)
2214 for type, char, lines, coldata in edges:
2214 for type, char, lines, coldata in edges:
2215 graphmod.ascii(ui, state, type, char, lines, coldata)
2215 graphmod.ascii(ui, state, type, char, lines, coldata)
2216 displayer.close()
2216 displayer.close()
2217
2217
2218 def graphlog(ui, repo, *pats, **opts):
2218 def graphlog(ui, repo, *pats, **opts):
2219 # Parameters are identical to log command ones
2219 # Parameters are identical to log command ones
2220 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2220 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2221 revdag = graphmod.dagwalker(repo, revs)
2221 revdag = graphmod.dagwalker(repo, revs)
2222
2222
2223 getrenamed = None
2223 getrenamed = None
2224 if opts.get('copies'):
2224 if opts.get('copies'):
2225 endrev = None
2225 endrev = None
2226 if opts.get('rev'):
2226 if opts.get('rev'):
2227 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2227 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2228 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2228 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2229 displayer = show_changeset(ui, repo, opts, buffered=True)
2229 displayer = show_changeset(ui, repo, opts, buffered=True)
2230 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2230 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2231 filematcher)
2231 filematcher)
2232
2232
2233 def checkunsupportedgraphflags(pats, opts):
2233 def checkunsupportedgraphflags(pats, opts):
2234 for op in ["newest_first"]:
2234 for op in ["newest_first"]:
2235 if op in opts and opts[op]:
2235 if op in opts and opts[op]:
2236 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2236 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2237 % op.replace("_", "-"))
2237 % op.replace("_", "-"))
2238
2238
2239 def graphrevs(repo, nodes, opts):
2239 def graphrevs(repo, nodes, opts):
2240 limit = loglimit(opts)
2240 limit = loglimit(opts)
2241 nodes.reverse()
2241 nodes.reverse()
2242 if limit is not None:
2242 if limit is not None:
2243 nodes = nodes[:limit]
2243 nodes = nodes[:limit]
2244 return graphmod.nodes(repo, nodes)
2244 return graphmod.nodes(repo, nodes)
2245
2245
2246 def add(ui, repo, match, prefix, explicitonly, **opts):
2246 def add(ui, repo, match, prefix, explicitonly, **opts):
2247 join = lambda f: os.path.join(prefix, f)
2247 join = lambda f: os.path.join(prefix, f)
2248 bad = []
2248 bad = []
2249
2249
2250 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2250 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2251 names = []
2251 names = []
2252 wctx = repo[None]
2252 wctx = repo[None]
2253 cca = None
2253 cca = None
2254 abort, warn = scmutil.checkportabilityalert(ui)
2254 abort, warn = scmutil.checkportabilityalert(ui)
2255 if abort or warn:
2255 if abort or warn:
2256 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2256 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2257
2257
2258 badmatch = matchmod.badmatch(match, badfn)
2258 badmatch = matchmod.badmatch(match, badfn)
2259 dirstate = repo.dirstate
2259 dirstate = repo.dirstate
2260 # We don't want to just call wctx.walk here, since it would return a lot of
2260 # We don't want to just call wctx.walk here, since it would return a lot of
2261 # clean files, which we aren't interested in and takes time.
2261 # clean files, which we aren't interested in and takes time.
2262 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2262 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2263 True, False, full=False)):
2263 True, False, full=False)):
2264 exact = match.exact(f)
2264 exact = match.exact(f)
2265 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2265 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2266 if cca:
2266 if cca:
2267 cca(f)
2267 cca(f)
2268 names.append(f)
2268 names.append(f)
2269 if ui.verbose or not exact:
2269 if ui.verbose or not exact:
2270 ui.status(_('adding %s\n') % match.rel(f))
2270 ui.status(_('adding %s\n') % match.rel(f))
2271
2271
2272 for subpath in sorted(wctx.substate):
2272 for subpath in sorted(wctx.substate):
2273 sub = wctx.sub(subpath)
2273 sub = wctx.sub(subpath)
2274 try:
2274 try:
2275 submatch = matchmod.subdirmatcher(subpath, match)
2275 submatch = matchmod.subdirmatcher(subpath, match)
2276 if opts.get('subrepos'):
2276 if opts.get('subrepos'):
2277 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2277 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2278 else:
2278 else:
2279 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2279 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2280 except error.LookupError:
2280 except error.LookupError:
2281 ui.status(_("skipping missing subrepository: %s\n")
2281 ui.status(_("skipping missing subrepository: %s\n")
2282 % join(subpath))
2282 % join(subpath))
2283
2283
2284 if not opts.get('dry_run'):
2284 if not opts.get('dry_run'):
2285 rejected = wctx.add(names, prefix)
2285 rejected = wctx.add(names, prefix)
2286 bad.extend(f for f in rejected if f in match.files())
2286 bad.extend(f for f in rejected if f in match.files())
2287 return bad
2287 return bad
2288
2288
2289 def forget(ui, repo, match, prefix, explicitonly):
2289 def forget(ui, repo, match, prefix, explicitonly):
2290 join = lambda f: os.path.join(prefix, f)
2290 join = lambda f: os.path.join(prefix, f)
2291 bad = []
2291 bad = []
2292 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2292 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2293 wctx = repo[None]
2293 wctx = repo[None]
2294 forgot = []
2294 forgot = []
2295
2295
2296 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2296 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2297 forget = sorted(s[0] + s[1] + s[3] + s[6])
2297 forget = sorted(s[0] + s[1] + s[3] + s[6])
2298 if explicitonly:
2298 if explicitonly:
2299 forget = [f for f in forget if match.exact(f)]
2299 forget = [f for f in forget if match.exact(f)]
2300
2300
2301 for subpath in sorted(wctx.substate):
2301 for subpath in sorted(wctx.substate):
2302 sub = wctx.sub(subpath)
2302 sub = wctx.sub(subpath)
2303 try:
2303 try:
2304 submatch = matchmod.subdirmatcher(subpath, match)
2304 submatch = matchmod.subdirmatcher(subpath, match)
2305 subbad, subforgot = sub.forget(submatch, prefix)
2305 subbad, subforgot = sub.forget(submatch, prefix)
2306 bad.extend([subpath + '/' + f for f in subbad])
2306 bad.extend([subpath + '/' + f for f in subbad])
2307 forgot.extend([subpath + '/' + f for f in subforgot])
2307 forgot.extend([subpath + '/' + f for f in subforgot])
2308 except error.LookupError:
2308 except error.LookupError:
2309 ui.status(_("skipping missing subrepository: %s\n")
2309 ui.status(_("skipping missing subrepository: %s\n")
2310 % join(subpath))
2310 % join(subpath))
2311
2311
2312 if not explicitonly:
2312 if not explicitonly:
2313 for f in match.files():
2313 for f in match.files():
2314 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2314 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2315 if f not in forgot:
2315 if f not in forgot:
2316 if repo.wvfs.exists(f):
2316 if repo.wvfs.exists(f):
2317 # Don't complain if the exact case match wasn't given.
2317 # Don't complain if the exact case match wasn't given.
2318 # But don't do this until after checking 'forgot', so
2318 # But don't do this until after checking 'forgot', so
2319 # that subrepo files aren't normalized, and this op is
2319 # that subrepo files aren't normalized, and this op is
2320 # purely from data cached by the status walk above.
2320 # purely from data cached by the status walk above.
2321 if repo.dirstate.normalize(f) in repo.dirstate:
2321 if repo.dirstate.normalize(f) in repo.dirstate:
2322 continue
2322 continue
2323 ui.warn(_('not removing %s: '
2323 ui.warn(_('not removing %s: '
2324 'file is already untracked\n')
2324 'file is already untracked\n')
2325 % match.rel(f))
2325 % match.rel(f))
2326 bad.append(f)
2326 bad.append(f)
2327
2327
2328 for f in forget:
2328 for f in forget:
2329 if ui.verbose or not match.exact(f):
2329 if ui.verbose or not match.exact(f):
2330 ui.status(_('removing %s\n') % match.rel(f))
2330 ui.status(_('removing %s\n') % match.rel(f))
2331
2331
2332 rejected = wctx.forget(forget, prefix)
2332 rejected = wctx.forget(forget, prefix)
2333 bad.extend(f for f in rejected if f in match.files())
2333 bad.extend(f for f in rejected if f in match.files())
2334 forgot.extend(f for f in forget if f not in rejected)
2334 forgot.extend(f for f in forget if f not in rejected)
2335 return bad, forgot
2335 return bad, forgot
2336
2336
2337 def files(ui, ctx, m, fm, fmt, subrepos):
2337 def files(ui, ctx, m, fm, fmt, subrepos):
2338 rev = ctx.rev()
2338 rev = ctx.rev()
2339 ret = 1
2339 ret = 1
2340 ds = ctx.repo().dirstate
2340 ds = ctx.repo().dirstate
2341
2341
2342 for f in ctx.matches(m):
2342 for f in ctx.matches(m):
2343 if rev is None and ds[f] == 'r':
2343 if rev is None and ds[f] == 'r':
2344 continue
2344 continue
2345 fm.startitem()
2345 fm.startitem()
2346 if ui.verbose:
2346 if ui.verbose:
2347 fc = ctx[f]
2347 fc = ctx[f]
2348 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2348 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2349 fm.data(abspath=f)
2349 fm.data(abspath=f)
2350 fm.write('path', fmt, m.rel(f))
2350 fm.write('path', fmt, m.rel(f))
2351 ret = 0
2351 ret = 0
2352
2352
2353 for subpath in sorted(ctx.substate):
2353 for subpath in sorted(ctx.substate):
2354 submatch = matchmod.subdirmatcher(subpath, m)
2354 submatch = matchmod.subdirmatcher(subpath, m)
2355 if (subrepos or m.exact(subpath) or any(submatch.files())):
2355 if (subrepos or m.exact(subpath) or any(submatch.files())):
2356 sub = ctx.sub(subpath)
2356 sub = ctx.sub(subpath)
2357 try:
2357 try:
2358 recurse = m.exact(subpath) or subrepos
2358 recurse = m.exact(subpath) or subrepos
2359 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2359 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2360 ret = 0
2360 ret = 0
2361 except error.LookupError:
2361 except error.LookupError:
2362 ui.status(_("skipping missing subrepository: %s\n")
2362 ui.status(_("skipping missing subrepository: %s\n")
2363 % m.abs(subpath))
2363 % m.abs(subpath))
2364
2364
2365 return ret
2365 return ret
2366
2366
2367 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2367 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2368 join = lambda f: os.path.join(prefix, f)
2368 join = lambda f: os.path.join(prefix, f)
2369 ret = 0
2369 ret = 0
2370 s = repo.status(match=m, clean=True)
2370 s = repo.status(match=m, clean=True)
2371 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2371 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2372
2372
2373 wctx = repo[None]
2373 wctx = repo[None]
2374
2374
2375 if warnings is None:
2375 if warnings is None:
2376 warnings = []
2376 warnings = []
2377 warn = True
2377 warn = True
2378 else:
2378 else:
2379 warn = False
2379 warn = False
2380
2380
2381 subs = sorted(wctx.substate)
2381 subs = sorted(wctx.substate)
2382 total = len(subs)
2382 total = len(subs)
2383 count = 0
2383 count = 0
2384 for subpath in subs:
2384 for subpath in subs:
2385 count += 1
2385 count += 1
2386 submatch = matchmod.subdirmatcher(subpath, m)
2386 submatch = matchmod.subdirmatcher(subpath, m)
2387 if subrepos or m.exact(subpath) or any(submatch.files()):
2387 if subrepos or m.exact(subpath) or any(submatch.files()):
2388 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2388 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2389 sub = wctx.sub(subpath)
2389 sub = wctx.sub(subpath)
2390 try:
2390 try:
2391 if sub.removefiles(submatch, prefix, after, force, subrepos,
2391 if sub.removefiles(submatch, prefix, after, force, subrepos,
2392 warnings):
2392 warnings):
2393 ret = 1
2393 ret = 1
2394 except error.LookupError:
2394 except error.LookupError:
2395 warnings.append(_("skipping missing subrepository: %s\n")
2395 warnings.append(_("skipping missing subrepository: %s\n")
2396 % join(subpath))
2396 % join(subpath))
2397 ui.progress(_('searching'), None)
2397 ui.progress(_('searching'), None)
2398
2398
2399 # warn about failure to delete explicit files/dirs
2399 # warn about failure to delete explicit files/dirs
2400 deleteddirs = util.dirs(deleted)
2400 deleteddirs = util.dirs(deleted)
2401 files = m.files()
2401 files = m.files()
2402 total = len(files)
2402 total = len(files)
2403 count = 0
2403 count = 0
2404 for f in files:
2404 for f in files:
2405 def insubrepo():
2405 def insubrepo():
2406 for subpath in wctx.substate:
2406 for subpath in wctx.substate:
2407 if f.startswith(subpath + '/'):
2407 if f.startswith(subpath + '/'):
2408 return True
2408 return True
2409 return False
2409 return False
2410
2410
2411 count += 1
2411 count += 1
2412 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2412 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2413 isdir = f in deleteddirs or wctx.hasdir(f)
2413 isdir = f in deleteddirs or wctx.hasdir(f)
2414 if (f in repo.dirstate or isdir or f == '.'
2414 if (f in repo.dirstate or isdir or f == '.'
2415 or insubrepo() or f in subs):
2415 or insubrepo() or f in subs):
2416 continue
2416 continue
2417
2417
2418 if repo.wvfs.exists(f):
2418 if repo.wvfs.exists(f):
2419 if repo.wvfs.isdir(f):
2419 if repo.wvfs.isdir(f):
2420 warnings.append(_('not removing %s: no tracked files\n')
2420 warnings.append(_('not removing %s: no tracked files\n')
2421 % m.rel(f))
2421 % m.rel(f))
2422 else:
2422 else:
2423 warnings.append(_('not removing %s: file is untracked\n')
2423 warnings.append(_('not removing %s: file is untracked\n')
2424 % m.rel(f))
2424 % m.rel(f))
2425 # missing files will generate a warning elsewhere
2425 # missing files will generate a warning elsewhere
2426 ret = 1
2426 ret = 1
2427 ui.progress(_('deleting'), None)
2427 ui.progress(_('deleting'), None)
2428
2428
2429 if force:
2429 if force:
2430 list = modified + deleted + clean + added
2430 list = modified + deleted + clean + added
2431 elif after:
2431 elif after:
2432 list = deleted
2432 list = deleted
2433 remaining = modified + added + clean
2433 remaining = modified + added + clean
2434 total = len(remaining)
2434 total = len(remaining)
2435 count = 0
2435 count = 0
2436 for f in remaining:
2436 for f in remaining:
2437 count += 1
2437 count += 1
2438 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2438 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2439 warnings.append(_('not removing %s: file still exists\n')
2439 warnings.append(_('not removing %s: file still exists\n')
2440 % m.rel(f))
2440 % m.rel(f))
2441 ret = 1
2441 ret = 1
2442 ui.progress(_('skipping'), None)
2442 ui.progress(_('skipping'), None)
2443 else:
2443 else:
2444 list = deleted + clean
2444 list = deleted + clean
2445 total = len(modified) + len(added)
2445 total = len(modified) + len(added)
2446 count = 0
2446 count = 0
2447 for f in modified:
2447 for f in modified:
2448 count += 1
2448 count += 1
2449 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2449 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2450 warnings.append(_('not removing %s: file is modified (use -f'
2450 warnings.append(_('not removing %s: file is modified (use -f'
2451 ' to force removal)\n') % m.rel(f))
2451 ' to force removal)\n') % m.rel(f))
2452 ret = 1
2452 ret = 1
2453 for f in added:
2453 for f in added:
2454 count += 1
2454 count += 1
2455 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2455 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2456 warnings.append(_("not removing %s: file has been marked for add"
2456 warnings.append(_("not removing %s: file has been marked for add"
2457 " (use 'hg forget' to undo add)\n") % m.rel(f))
2457 " (use 'hg forget' to undo add)\n") % m.rel(f))
2458 ret = 1
2458 ret = 1
2459 ui.progress(_('skipping'), None)
2459 ui.progress(_('skipping'), None)
2460
2460
2461 list = sorted(list)
2461 list = sorted(list)
2462 total = len(list)
2462 total = len(list)
2463 count = 0
2463 count = 0
2464 for f in list:
2464 for f in list:
2465 count += 1
2465 count += 1
2466 if ui.verbose or not m.exact(f):
2466 if ui.verbose or not m.exact(f):
2467 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2467 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2468 ui.status(_('removing %s\n') % m.rel(f))
2468 ui.status(_('removing %s\n') % m.rel(f))
2469 ui.progress(_('deleting'), None)
2469 ui.progress(_('deleting'), None)
2470
2470
2471 with repo.wlock():
2471 with repo.wlock():
2472 if not after:
2472 if not after:
2473 for f in list:
2473 for f in list:
2474 if f in added:
2474 if f in added:
2475 continue # we never unlink added files on remove
2475 continue # we never unlink added files on remove
2476 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2476 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2477 repo[None].forget(list)
2477 repo[None].forget(list)
2478
2478
2479 if warn:
2479 if warn:
2480 for warning in warnings:
2480 for warning in warnings:
2481 ui.warn(warning)
2481 ui.warn(warning)
2482
2482
2483 return ret
2483 return ret
2484
2484
2485 def cat(ui, repo, ctx, matcher, prefix, **opts):
2485 def cat(ui, repo, ctx, matcher, prefix, **opts):
2486 err = 1
2486 err = 1
2487
2487
2488 def write(path):
2488 def write(path):
2489 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2489 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2490 pathname=os.path.join(prefix, path))
2490 pathname=os.path.join(prefix, path))
2491 data = ctx[path].data()
2491 data = ctx[path].data()
2492 if opts.get('decode'):
2492 if opts.get('decode'):
2493 data = repo.wwritedata(path, data)
2493 data = repo.wwritedata(path, data)
2494 fp.write(data)
2494 fp.write(data)
2495 fp.close()
2495 fp.close()
2496
2496
2497 # Automation often uses hg cat on single files, so special case it
2497 # Automation often uses hg cat on single files, so special case it
2498 # for performance to avoid the cost of parsing the manifest.
2498 # for performance to avoid the cost of parsing the manifest.
2499 if len(matcher.files()) == 1 and not matcher.anypats():
2499 if len(matcher.files()) == 1 and not matcher.anypats():
2500 file = matcher.files()[0]
2500 file = matcher.files()[0]
2501 mfl = repo.manifestlog
2501 mfl = repo.manifestlog
2502 mfnode = ctx.manifestnode()
2502 mfnode = ctx.manifestnode()
2503 try:
2503 try:
2504 if mfnode and mfl[mfnode].find(file)[0]:
2504 if mfnode and mfl[mfnode].find(file)[0]:
2505 write(file)
2505 write(file)
2506 return 0
2506 return 0
2507 except KeyError:
2507 except KeyError:
2508 pass
2508 pass
2509
2509
2510 for abs in ctx.walk(matcher):
2510 for abs in ctx.walk(matcher):
2511 write(abs)
2511 write(abs)
2512 err = 0
2512 err = 0
2513
2513
2514 for subpath in sorted(ctx.substate):
2514 for subpath in sorted(ctx.substate):
2515 sub = ctx.sub(subpath)
2515 sub = ctx.sub(subpath)
2516 try:
2516 try:
2517 submatch = matchmod.subdirmatcher(subpath, matcher)
2517 submatch = matchmod.subdirmatcher(subpath, matcher)
2518
2518
2519 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2519 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2520 **opts):
2520 **opts):
2521 err = 0
2521 err = 0
2522 except error.RepoLookupError:
2522 except error.RepoLookupError:
2523 ui.status(_("skipping missing subrepository: %s\n")
2523 ui.status(_("skipping missing subrepository: %s\n")
2524 % os.path.join(prefix, subpath))
2524 % os.path.join(prefix, subpath))
2525
2525
2526 return err
2526 return err
2527
2527
2528 def commit(ui, repo, commitfunc, pats, opts):
2528 def commit(ui, repo, commitfunc, pats, opts):
2529 '''commit the specified files or all outstanding changes'''
2529 '''commit the specified files or all outstanding changes'''
2530 date = opts.get('date')
2530 date = opts.get('date')
2531 if date:
2531 if date:
2532 opts['date'] = util.parsedate(date)
2532 opts['date'] = util.parsedate(date)
2533 message = logmessage(ui, opts)
2533 message = logmessage(ui, opts)
2534 matcher = scmutil.match(repo[None], pats, opts)
2534 matcher = scmutil.match(repo[None], pats, opts)
2535
2535
2536 # extract addremove carefully -- this function can be called from a command
2536 # extract addremove carefully -- this function can be called from a command
2537 # that doesn't support addremove
2537 # that doesn't support addremove
2538 if opts.get('addremove'):
2538 if opts.get('addremove'):
2539 if scmutil.addremove(repo, matcher, "", opts) != 0:
2539 if scmutil.addremove(repo, matcher, "", opts) != 0:
2540 raise error.Abort(
2540 raise error.Abort(
2541 _("failed to mark all new/missing files as added/removed"))
2541 _("failed to mark all new/missing files as added/removed"))
2542
2542
2543 return commitfunc(ui, repo, message, matcher, opts)
2543 return commitfunc(ui, repo, message, matcher, opts)
2544
2544
2545 def samefile(f, ctx1, ctx2):
2545 def samefile(f, ctx1, ctx2):
2546 if f in ctx1.manifest():
2546 if f in ctx1.manifest():
2547 a = ctx1.filectx(f)
2547 a = ctx1.filectx(f)
2548 if f in ctx2.manifest():
2548 if f in ctx2.manifest():
2549 b = ctx2.filectx(f)
2549 b = ctx2.filectx(f)
2550 return (not a.cmp(b)
2550 return (not a.cmp(b)
2551 and a.flags() == b.flags())
2551 and a.flags() == b.flags())
2552 else:
2552 else:
2553 return False
2553 return False
2554 else:
2554 else:
2555 return f not in ctx2.manifest()
2555 return f not in ctx2.manifest()
2556
2556
2557 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2557 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2558 # avoid cycle context -> subrepo -> cmdutil
2558 # avoid cycle context -> subrepo -> cmdutil
2559 from . import context
2559 from . import context
2560
2560
2561 # amend will reuse the existing user if not specified, but the obsolete
2561 # amend will reuse the existing user if not specified, but the obsolete
2562 # marker creation requires that the current user's name is specified.
2562 # marker creation requires that the current user's name is specified.
2563 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2563 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2564 ui.username() # raise exception if username not set
2564 ui.username() # raise exception if username not set
2565
2565
2566 ui.note(_('amending changeset %s\n') % old)
2566 ui.note(_('amending changeset %s\n') % old)
2567 base = old.p1()
2567 base = old.p1()
2568 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2568 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2569
2569
2570 wlock = lock = newid = None
2570 wlock = lock = newid = None
2571 try:
2571 try:
2572 wlock = repo.wlock()
2572 wlock = repo.wlock()
2573 lock = repo.lock()
2573 lock = repo.lock()
2574 with repo.transaction('amend') as tr:
2574 with repo.transaction('amend') as tr:
2575 # See if we got a message from -m or -l, if not, open the editor
2575 # See if we got a message from -m or -l, if not, open the editor
2576 # with the message of the changeset to amend
2576 # with the message of the changeset to amend
2577 message = logmessage(ui, opts)
2577 message = logmessage(ui, opts)
2578 # ensure logfile does not conflict with later enforcement of the
2578 # ensure logfile does not conflict with later enforcement of the
2579 # message. potential logfile content has been processed by
2579 # message. potential logfile content has been processed by
2580 # `logmessage` anyway.
2580 # `logmessage` anyway.
2581 opts.pop('logfile')
2581 opts.pop('logfile')
2582 # First, do a regular commit to record all changes in the working
2582 # First, do a regular commit to record all changes in the working
2583 # directory (if there are any)
2583 # directory (if there are any)
2584 ui.callhooks = False
2584 ui.callhooks = False
2585 activebookmark = repo._bookmarks.active
2585 activebookmark = repo._bookmarks.active
2586 try:
2586 try:
2587 repo._bookmarks.active = None
2587 repo._bookmarks.active = None
2588 opts['message'] = 'temporary amend commit for %s' % old
2588 opts['message'] = 'temporary amend commit for %s' % old
2589 node = commit(ui, repo, commitfunc, pats, opts)
2589 node = commit(ui, repo, commitfunc, pats, opts)
2590 finally:
2590 finally:
2591 repo._bookmarks.active = activebookmark
2591 repo._bookmarks.active = activebookmark
2592 repo._bookmarks.recordchange(tr)
2592 repo._bookmarks.recordchange(tr)
2593 ui.callhooks = True
2593 ui.callhooks = True
2594 ctx = repo[node]
2594 ctx = repo[node]
2595
2595
2596 # Participating changesets:
2596 # Participating changesets:
2597 #
2597 #
2598 # node/ctx o - new (intermediate) commit that contains changes
2598 # node/ctx o - new (intermediate) commit that contains changes
2599 # | from working dir to go into amending commit
2599 # | from working dir to go into amending commit
2600 # | (or a workingctx if there were no changes)
2600 # | (or a workingctx if there were no changes)
2601 # |
2601 # |
2602 # old o - changeset to amend
2602 # old o - changeset to amend
2603 # |
2603 # |
2604 # base o - parent of amending changeset
2604 # base o - parent of amending changeset
2605
2605
2606 # Update extra dict from amended commit (e.g. to preserve graft
2606 # Update extra dict from amended commit (e.g. to preserve graft
2607 # source)
2607 # source)
2608 extra.update(old.extra())
2608 extra.update(old.extra())
2609
2609
2610 # Also update it from the intermediate commit or from the wctx
2610 # Also update it from the intermediate commit or from the wctx
2611 extra.update(ctx.extra())
2611 extra.update(ctx.extra())
2612
2612
2613 if len(old.parents()) > 1:
2613 if len(old.parents()) > 1:
2614 # ctx.files() isn't reliable for merges, so fall back to the
2614 # ctx.files() isn't reliable for merges, so fall back to the
2615 # slower repo.status() method
2615 # slower repo.status() method
2616 files = set([fn for st in repo.status(base, old)[:3]
2616 files = set([fn for st in repo.status(base, old)[:3]
2617 for fn in st])
2617 for fn in st])
2618 else:
2618 else:
2619 files = set(old.files())
2619 files = set(old.files())
2620
2620
2621 # Second, we use either the commit we just did, or if there were no
2621 # Second, we use either the commit we just did, or if there were no
2622 # changes the parent of the working directory as the version of the
2622 # changes the parent of the working directory as the version of the
2623 # files in the final amend commit
2623 # files in the final amend commit
2624 if node:
2624 if node:
2625 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2625 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2626
2626
2627 user = ctx.user()
2627 user = ctx.user()
2628 date = ctx.date()
2628 date = ctx.date()
2629 # Recompute copies (avoid recording a -> b -> a)
2629 # Recompute copies (avoid recording a -> b -> a)
2630 copied = copies.pathcopies(base, ctx)
2630 copied = copies.pathcopies(base, ctx)
2631 if old.p2:
2631 if old.p2:
2632 copied.update(copies.pathcopies(old.p2(), ctx))
2632 copied.update(copies.pathcopies(old.p2(), ctx))
2633
2633
2634 # Prune files which were reverted by the updates: if old
2634 # Prune files which were reverted by the updates: if old
2635 # introduced file X and our intermediate commit, node,
2635 # introduced file X and our intermediate commit, node,
2636 # renamed that file, then those two files are the same and
2636 # renamed that file, then those two files are the same and
2637 # we can discard X from our list of files. Likewise if X
2637 # we can discard X from our list of files. Likewise if X
2638 # was deleted, it's no longer relevant
2638 # was deleted, it's no longer relevant
2639 files.update(ctx.files())
2639 files.update(ctx.files())
2640 files = [f for f in files if not samefile(f, ctx, base)]
2640 files = [f for f in files if not samefile(f, ctx, base)]
2641
2641
2642 def filectxfn(repo, ctx_, path):
2642 def filectxfn(repo, ctx_, path):
2643 try:
2643 try:
2644 fctx = ctx[path]
2644 fctx = ctx[path]
2645 flags = fctx.flags()
2645 flags = fctx.flags()
2646 mctx = context.memfilectx(repo,
2646 mctx = context.memfilectx(repo,
2647 fctx.path(), fctx.data(),
2647 fctx.path(), fctx.data(),
2648 islink='l' in flags,
2648 islink='l' in flags,
2649 isexec='x' in flags,
2649 isexec='x' in flags,
2650 copied=copied.get(path))
2650 copied=copied.get(path))
2651 return mctx
2651 return mctx
2652 except KeyError:
2652 except KeyError:
2653 return None
2653 return None
2654 else:
2654 else:
2655 ui.note(_('copying changeset %s to %s\n') % (old, base))
2655 ui.note(_('copying changeset %s to %s\n') % (old, base))
2656
2656
2657 # Use version of files as in the old cset
2657 # Use version of files as in the old cset
2658 def filectxfn(repo, ctx_, path):
2658 def filectxfn(repo, ctx_, path):
2659 try:
2659 try:
2660 return old.filectx(path)
2660 return old.filectx(path)
2661 except KeyError:
2661 except KeyError:
2662 return None
2662 return None
2663
2663
2664 user = opts.get('user') or old.user()
2664 user = opts.get('user') or old.user()
2665 date = opts.get('date') or old.date()
2665 date = opts.get('date') or old.date()
2666 editform = mergeeditform(old, 'commit.amend')
2666 editform = mergeeditform(old, 'commit.amend')
2667 editor = getcommiteditor(editform=editform, **opts)
2667 editor = getcommiteditor(editform=editform, **opts)
2668 if not message:
2668 if not message:
2669 editor = getcommiteditor(edit=True, editform=editform)
2669 editor = getcommiteditor(edit=True, editform=editform)
2670 message = old.description()
2670 message = old.description()
2671
2671
2672 pureextra = extra.copy()
2672 pureextra = extra.copy()
2673 extra['amend_source'] = old.hex()
2673 extra['amend_source'] = old.hex()
2674
2674
2675 new = context.memctx(repo,
2675 new = context.memctx(repo,
2676 parents=[base.node(), old.p2().node()],
2676 parents=[base.node(), old.p2().node()],
2677 text=message,
2677 text=message,
2678 files=files,
2678 files=files,
2679 filectxfn=filectxfn,
2679 filectxfn=filectxfn,
2680 user=user,
2680 user=user,
2681 date=date,
2681 date=date,
2682 extra=extra,
2682 extra=extra,
2683 editor=editor)
2683 editor=editor)
2684
2684
2685 newdesc = changelog.stripdesc(new.description())
2685 newdesc = changelog.stripdesc(new.description())
2686 if ((not node)
2686 if ((not node)
2687 and newdesc == old.description()
2687 and newdesc == old.description()
2688 and user == old.user()
2688 and user == old.user()
2689 and date == old.date()
2689 and date == old.date()
2690 and pureextra == old.extra()):
2690 and pureextra == old.extra()):
2691 # nothing changed. continuing here would create a new node
2691 # nothing changed. continuing here would create a new node
2692 # anyway because of the amend_source noise.
2692 # anyway because of the amend_source noise.
2693 #
2693 #
2694 # This not what we expect from amend.
2694 # This not what we expect from amend.
2695 return old.node()
2695 return old.node()
2696
2696
2697 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2697 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2698 try:
2698 try:
2699 if opts.get('secret'):
2699 if opts.get('secret'):
2700 commitphase = 'secret'
2700 commitphase = 'secret'
2701 else:
2701 else:
2702 commitphase = old.phase()
2702 commitphase = old.phase()
2703 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2703 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2704 newid = repo.commitctx(new)
2704 newid = repo.commitctx(new)
2705 finally:
2705 finally:
2706 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2706 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2707 if newid != old.node():
2707 if newid != old.node():
2708 # Reroute the working copy parent to the new changeset
2708 # Reroute the working copy parent to the new changeset
2709 repo.setparents(newid, nullid)
2709 repo.setparents(newid, nullid)
2710
2710
2711 # Move bookmarks from old parent to amend commit
2711 # Move bookmarks from old parent to amend commit
2712 bms = repo.nodebookmarks(old.node())
2712 bms = repo.nodebookmarks(old.node())
2713 if bms:
2713 if bms:
2714 marks = repo._bookmarks
2714 marks = repo._bookmarks
2715 for bm in bms:
2715 for bm in bms:
2716 ui.debug('moving bookmarks %r from %s to %s\n' %
2716 ui.debug('moving bookmarks %r from %s to %s\n' %
2717 (marks, old.hex(), hex(newid)))
2717 (marks, old.hex(), hex(newid)))
2718 marks[bm] = newid
2718 marks[bm] = newid
2719 marks.recordchange(tr)
2719 marks.recordchange(tr)
2720 #commit the whole amend process
2720 #commit the whole amend process
2721 if createmarkers:
2721 if createmarkers:
2722 # mark the new changeset as successor of the rewritten one
2722 # mark the new changeset as successor of the rewritten one
2723 new = repo[newid]
2723 new = repo[newid]
2724 obs = [(old, (new,))]
2724 obs = [(old, (new,))]
2725 if node:
2725 if node:
2726 obs.append((ctx, ()))
2726 obs.append((ctx, ()))
2727
2727
2728 obsolete.createmarkers(repo, obs)
2728 obsolete.createmarkers(repo, obs)
2729 if not createmarkers and newid != old.node():
2729 if not createmarkers and newid != old.node():
2730 # Strip the intermediate commit (if there was one) and the amended
2730 # Strip the intermediate commit (if there was one) and the amended
2731 # commit
2731 # commit
2732 if node:
2732 if node:
2733 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2733 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2734 ui.note(_('stripping amended changeset %s\n') % old)
2734 ui.note(_('stripping amended changeset %s\n') % old)
2735 repair.strip(ui, repo, old.node(), topic='amend-backup')
2735 repair.strip(ui, repo, old.node(), topic='amend-backup')
2736 finally:
2736 finally:
2737 lockmod.release(lock, wlock)
2737 lockmod.release(lock, wlock)
2738 return newid
2738 return newid
2739
2739
2740 def commiteditor(repo, ctx, subs, editform=''):
2740 def commiteditor(repo, ctx, subs, editform=''):
2741 if ctx.description():
2741 if ctx.description():
2742 return ctx.description()
2742 return ctx.description()
2743 return commitforceeditor(repo, ctx, subs, editform=editform,
2743 return commitforceeditor(repo, ctx, subs, editform=editform,
2744 unchangedmessagedetection=True)
2744 unchangedmessagedetection=True)
2745
2745
2746 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2746 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2747 editform='', unchangedmessagedetection=False):
2747 editform='', unchangedmessagedetection=False):
2748 if not extramsg:
2748 if not extramsg:
2749 extramsg = _("Leave message empty to abort commit.")
2749 extramsg = _("Leave message empty to abort commit.")
2750
2750
2751 forms = [e for e in editform.split('.') if e]
2751 forms = [e for e in editform.split('.') if e]
2752 forms.insert(0, 'changeset')
2752 forms.insert(0, 'changeset')
2753 templatetext = None
2753 templatetext = None
2754 while forms:
2754 while forms:
2755 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2755 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2756 if tmpl:
2756 if tmpl:
2757 templatetext = committext = buildcommittemplate(
2757 templatetext = committext = buildcommittemplate(
2758 repo, ctx, subs, extramsg, tmpl)
2758 repo, ctx, subs, extramsg, tmpl)
2759 break
2759 break
2760 forms.pop()
2760 forms.pop()
2761 else:
2761 else:
2762 committext = buildcommittext(repo, ctx, subs, extramsg)
2762 committext = buildcommittext(repo, ctx, subs, extramsg)
2763
2763
2764 # run editor in the repository root
2764 # run editor in the repository root
2765 olddir = pycompat.getcwd()
2765 olddir = pycompat.getcwd()
2766 os.chdir(repo.root)
2766 os.chdir(repo.root)
2767
2767
2768 # make in-memory changes visible to external process
2768 # make in-memory changes visible to external process
2769 tr = repo.currenttransaction()
2769 tr = repo.currenttransaction()
2770 repo.dirstate.write(tr)
2770 repo.dirstate.write(tr)
2771 pending = tr and tr.writepending() and repo.root
2771 pending = tr and tr.writepending() and repo.root
2772
2772
2773 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2773 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2774 editform=editform, pending=pending)
2774 editform=editform, pending=pending)
2775 text = editortext
2775
2776
2776 # strip away anything below this special string (used for editors that want
2777 # strip away anything below this special string (used for editors that want
2777 # to display the diff)
2778 # to display the diff)
2778 stripbelow = re.search(_linebelow, editortext, flags=re.MULTILINE)
2779 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2779 if stripbelow:
2780 if stripbelow:
2780 editortext = editortext[:stripbelow.start()]
2781 text = text[:stripbelow.start()]
2781
2782
2782 text = re.sub("(?m)^HG:.*(\n|$)", "", editortext)
2783 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2783 os.chdir(olddir)
2784 os.chdir(olddir)
2784
2785
2785 if finishdesc:
2786 if finishdesc:
2786 text = finishdesc(text)
2787 text = finishdesc(text)
2787 if not text.strip():
2788 if not text.strip():
2788 raise error.Abort(_("empty commit message"))
2789 raise error.Abort(_("empty commit message"))
2789 if unchangedmessagedetection and editortext == templatetext:
2790 if unchangedmessagedetection and editortext == templatetext:
2790 raise error.Abort(_("commit message unchanged"))
2791 raise error.Abort(_("commit message unchanged"))
2791
2792
2792 return text
2793 return text
2793
2794
2794 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2795 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2795 ui = repo.ui
2796 ui = repo.ui
2796 tmpl, mapfile = gettemplate(ui, tmpl, None)
2797 tmpl, mapfile = gettemplate(ui, tmpl, None)
2797
2798
2798 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2799 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2799
2800
2800 for k, v in repo.ui.configitems('committemplate'):
2801 for k, v in repo.ui.configitems('committemplate'):
2801 if k != 'changeset':
2802 if k != 'changeset':
2802 t.t.cache[k] = v
2803 t.t.cache[k] = v
2803
2804
2804 if not extramsg:
2805 if not extramsg:
2805 extramsg = '' # ensure that extramsg is string
2806 extramsg = '' # ensure that extramsg is string
2806
2807
2807 ui.pushbuffer()
2808 ui.pushbuffer()
2808 t.show(ctx, extramsg=extramsg)
2809 t.show(ctx, extramsg=extramsg)
2809 return ui.popbuffer()
2810 return ui.popbuffer()
2810
2811
2811 def hgprefix(msg):
2812 def hgprefix(msg):
2812 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2813 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2813
2814
2814 def buildcommittext(repo, ctx, subs, extramsg):
2815 def buildcommittext(repo, ctx, subs, extramsg):
2815 edittext = []
2816 edittext = []
2816 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2817 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2817 if ctx.description():
2818 if ctx.description():
2818 edittext.append(ctx.description())
2819 edittext.append(ctx.description())
2819 edittext.append("")
2820 edittext.append("")
2820 edittext.append("") # Empty line between message and comments.
2821 edittext.append("") # Empty line between message and comments.
2821 edittext.append(hgprefix(_("Enter commit message."
2822 edittext.append(hgprefix(_("Enter commit message."
2822 " Lines beginning with 'HG:' are removed.")))
2823 " Lines beginning with 'HG:' are removed.")))
2823 edittext.append(hgprefix(extramsg))
2824 edittext.append(hgprefix(extramsg))
2824 edittext.append("HG: --")
2825 edittext.append("HG: --")
2825 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2826 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2826 if ctx.p2():
2827 if ctx.p2():
2827 edittext.append(hgprefix(_("branch merge")))
2828 edittext.append(hgprefix(_("branch merge")))
2828 if ctx.branch():
2829 if ctx.branch():
2829 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2830 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2830 if bookmarks.isactivewdirparent(repo):
2831 if bookmarks.isactivewdirparent(repo):
2831 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2832 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2832 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2833 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2833 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2834 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2834 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2835 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2835 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2836 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2836 if not added and not modified and not removed:
2837 if not added and not modified and not removed:
2837 edittext.append(hgprefix(_("no files changed")))
2838 edittext.append(hgprefix(_("no files changed")))
2838 edittext.append("")
2839 edittext.append("")
2839
2840
2840 return "\n".join(edittext)
2841 return "\n".join(edittext)
2841
2842
2842 def commitstatus(repo, node, branch, bheads=None, opts=None):
2843 def commitstatus(repo, node, branch, bheads=None, opts=None):
2843 if opts is None:
2844 if opts is None:
2844 opts = {}
2845 opts = {}
2845 ctx = repo[node]
2846 ctx = repo[node]
2846 parents = ctx.parents()
2847 parents = ctx.parents()
2847
2848
2848 if (not opts.get('amend') and bheads and node not in bheads and not
2849 if (not opts.get('amend') and bheads and node not in bheads and not
2849 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2850 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2850 repo.ui.status(_('created new head\n'))
2851 repo.ui.status(_('created new head\n'))
2851 # The message is not printed for initial roots. For the other
2852 # The message is not printed for initial roots. For the other
2852 # changesets, it is printed in the following situations:
2853 # changesets, it is printed in the following situations:
2853 #
2854 #
2854 # Par column: for the 2 parents with ...
2855 # Par column: for the 2 parents with ...
2855 # N: null or no parent
2856 # N: null or no parent
2856 # B: parent is on another named branch
2857 # B: parent is on another named branch
2857 # C: parent is a regular non head changeset
2858 # C: parent is a regular non head changeset
2858 # H: parent was a branch head of the current branch
2859 # H: parent was a branch head of the current branch
2859 # Msg column: whether we print "created new head" message
2860 # Msg column: whether we print "created new head" message
2860 # In the following, it is assumed that there already exists some
2861 # In the following, it is assumed that there already exists some
2861 # initial branch heads of the current branch, otherwise nothing is
2862 # initial branch heads of the current branch, otherwise nothing is
2862 # printed anyway.
2863 # printed anyway.
2863 #
2864 #
2864 # Par Msg Comment
2865 # Par Msg Comment
2865 # N N y additional topo root
2866 # N N y additional topo root
2866 #
2867 #
2867 # B N y additional branch root
2868 # B N y additional branch root
2868 # C N y additional topo head
2869 # C N y additional topo head
2869 # H N n usual case
2870 # H N n usual case
2870 #
2871 #
2871 # B B y weird additional branch root
2872 # B B y weird additional branch root
2872 # C B y branch merge
2873 # C B y branch merge
2873 # H B n merge with named branch
2874 # H B n merge with named branch
2874 #
2875 #
2875 # C C y additional head from merge
2876 # C C y additional head from merge
2876 # C H n merge with a head
2877 # C H n merge with a head
2877 #
2878 #
2878 # H H n head merge: head count decreases
2879 # H H n head merge: head count decreases
2879
2880
2880 if not opts.get('close_branch'):
2881 if not opts.get('close_branch'):
2881 for r in parents:
2882 for r in parents:
2882 if r.closesbranch() and r.branch() == branch:
2883 if r.closesbranch() and r.branch() == branch:
2883 repo.ui.status(_('reopening closed branch head %d\n') % r)
2884 repo.ui.status(_('reopening closed branch head %d\n') % r)
2884
2885
2885 if repo.ui.debugflag:
2886 if repo.ui.debugflag:
2886 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2887 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2887 elif repo.ui.verbose:
2888 elif repo.ui.verbose:
2888 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2889 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2889
2890
2890 def postcommitstatus(repo, pats, opts):
2891 def postcommitstatus(repo, pats, opts):
2891 return repo.status(match=scmutil.match(repo[None], pats, opts))
2892 return repo.status(match=scmutil.match(repo[None], pats, opts))
2892
2893
2893 def revert(ui, repo, ctx, parents, *pats, **opts):
2894 def revert(ui, repo, ctx, parents, *pats, **opts):
2894 parent, p2 = parents
2895 parent, p2 = parents
2895 node = ctx.node()
2896 node = ctx.node()
2896
2897
2897 mf = ctx.manifest()
2898 mf = ctx.manifest()
2898 if node == p2:
2899 if node == p2:
2899 parent = p2
2900 parent = p2
2900
2901
2901 # need all matching names in dirstate and manifest of target rev,
2902 # need all matching names in dirstate and manifest of target rev,
2902 # so have to walk both. do not print errors if files exist in one
2903 # so have to walk both. do not print errors if files exist in one
2903 # but not other. in both cases, filesets should be evaluated against
2904 # but not other. in both cases, filesets should be evaluated against
2904 # workingctx to get consistent result (issue4497). this means 'set:**'
2905 # workingctx to get consistent result (issue4497). this means 'set:**'
2905 # cannot be used to select missing files from target rev.
2906 # cannot be used to select missing files from target rev.
2906
2907
2907 # `names` is a mapping for all elements in working copy and target revision
2908 # `names` is a mapping for all elements in working copy and target revision
2908 # The mapping is in the form:
2909 # The mapping is in the form:
2909 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2910 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2910 names = {}
2911 names = {}
2911
2912
2912 with repo.wlock():
2913 with repo.wlock():
2913 ## filling of the `names` mapping
2914 ## filling of the `names` mapping
2914 # walk dirstate to fill `names`
2915 # walk dirstate to fill `names`
2915
2916
2916 interactive = opts.get('interactive', False)
2917 interactive = opts.get('interactive', False)
2917 wctx = repo[None]
2918 wctx = repo[None]
2918 m = scmutil.match(wctx, pats, opts)
2919 m = scmutil.match(wctx, pats, opts)
2919
2920
2920 # we'll need this later
2921 # we'll need this later
2921 targetsubs = sorted(s for s in wctx.substate if m(s))
2922 targetsubs = sorted(s for s in wctx.substate if m(s))
2922
2923
2923 if not m.always():
2924 if not m.always():
2924 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2925 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2925 names[abs] = m.rel(abs), m.exact(abs)
2926 names[abs] = m.rel(abs), m.exact(abs)
2926
2927
2927 # walk target manifest to fill `names`
2928 # walk target manifest to fill `names`
2928
2929
2929 def badfn(path, msg):
2930 def badfn(path, msg):
2930 if path in names:
2931 if path in names:
2931 return
2932 return
2932 if path in ctx.substate:
2933 if path in ctx.substate:
2933 return
2934 return
2934 path_ = path + '/'
2935 path_ = path + '/'
2935 for f in names:
2936 for f in names:
2936 if f.startswith(path_):
2937 if f.startswith(path_):
2937 return
2938 return
2938 ui.warn("%s: %s\n" % (m.rel(path), msg))
2939 ui.warn("%s: %s\n" % (m.rel(path), msg))
2939
2940
2940 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2941 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2941 if abs not in names:
2942 if abs not in names:
2942 names[abs] = m.rel(abs), m.exact(abs)
2943 names[abs] = m.rel(abs), m.exact(abs)
2943
2944
2944 # Find status of all file in `names`.
2945 # Find status of all file in `names`.
2945 m = scmutil.matchfiles(repo, names)
2946 m = scmutil.matchfiles(repo, names)
2946
2947
2947 changes = repo.status(node1=node, match=m,
2948 changes = repo.status(node1=node, match=m,
2948 unknown=True, ignored=True, clean=True)
2949 unknown=True, ignored=True, clean=True)
2949 else:
2950 else:
2950 changes = repo.status(node1=node, match=m)
2951 changes = repo.status(node1=node, match=m)
2951 for kind in changes:
2952 for kind in changes:
2952 for abs in kind:
2953 for abs in kind:
2953 names[abs] = m.rel(abs), m.exact(abs)
2954 names[abs] = m.rel(abs), m.exact(abs)
2954
2955
2955 m = scmutil.matchfiles(repo, names)
2956 m = scmutil.matchfiles(repo, names)
2956
2957
2957 modified = set(changes.modified)
2958 modified = set(changes.modified)
2958 added = set(changes.added)
2959 added = set(changes.added)
2959 removed = set(changes.removed)
2960 removed = set(changes.removed)
2960 _deleted = set(changes.deleted)
2961 _deleted = set(changes.deleted)
2961 unknown = set(changes.unknown)
2962 unknown = set(changes.unknown)
2962 unknown.update(changes.ignored)
2963 unknown.update(changes.ignored)
2963 clean = set(changes.clean)
2964 clean = set(changes.clean)
2964 modadded = set()
2965 modadded = set()
2965
2966
2966 # split between files known in target manifest and the others
2967 # split between files known in target manifest and the others
2967 smf = set(mf)
2968 smf = set(mf)
2968
2969
2969 # determine the exact nature of the deleted changesets
2970 # determine the exact nature of the deleted changesets
2970 deladded = _deleted - smf
2971 deladded = _deleted - smf
2971 deleted = _deleted - deladded
2972 deleted = _deleted - deladded
2972
2973
2973 # We need to account for the state of the file in the dirstate,
2974 # We need to account for the state of the file in the dirstate,
2974 # even when we revert against something else than parent. This will
2975 # even when we revert against something else than parent. This will
2975 # slightly alter the behavior of revert (doing back up or not, delete
2976 # slightly alter the behavior of revert (doing back up or not, delete
2976 # or just forget etc).
2977 # or just forget etc).
2977 if parent == node:
2978 if parent == node:
2978 dsmodified = modified
2979 dsmodified = modified
2979 dsadded = added
2980 dsadded = added
2980 dsremoved = removed
2981 dsremoved = removed
2981 # store all local modifications, useful later for rename detection
2982 # store all local modifications, useful later for rename detection
2982 localchanges = dsmodified | dsadded
2983 localchanges = dsmodified | dsadded
2983 modified, added, removed = set(), set(), set()
2984 modified, added, removed = set(), set(), set()
2984 else:
2985 else:
2985 changes = repo.status(node1=parent, match=m)
2986 changes = repo.status(node1=parent, match=m)
2986 dsmodified = set(changes.modified)
2987 dsmodified = set(changes.modified)
2987 dsadded = set(changes.added)
2988 dsadded = set(changes.added)
2988 dsremoved = set(changes.removed)
2989 dsremoved = set(changes.removed)
2989 # store all local modifications, useful later for rename detection
2990 # store all local modifications, useful later for rename detection
2990 localchanges = dsmodified | dsadded
2991 localchanges = dsmodified | dsadded
2991
2992
2992 # only take into account for removes between wc and target
2993 # only take into account for removes between wc and target
2993 clean |= dsremoved - removed
2994 clean |= dsremoved - removed
2994 dsremoved &= removed
2995 dsremoved &= removed
2995 # distinct between dirstate remove and other
2996 # distinct between dirstate remove and other
2996 removed -= dsremoved
2997 removed -= dsremoved
2997
2998
2998 modadded = added & dsmodified
2999 modadded = added & dsmodified
2999 added -= modadded
3000 added -= modadded
3000
3001
3001 # tell newly modified apart.
3002 # tell newly modified apart.
3002 dsmodified &= modified
3003 dsmodified &= modified
3003 dsmodified |= modified & dsadded # dirstate added may need backup
3004 dsmodified |= modified & dsadded # dirstate added may need backup
3004 modified -= dsmodified
3005 modified -= dsmodified
3005
3006
3006 # We need to wait for some post-processing to update this set
3007 # We need to wait for some post-processing to update this set
3007 # before making the distinction. The dirstate will be used for
3008 # before making the distinction. The dirstate will be used for
3008 # that purpose.
3009 # that purpose.
3009 dsadded = added
3010 dsadded = added
3010
3011
3011 # in case of merge, files that are actually added can be reported as
3012 # in case of merge, files that are actually added can be reported as
3012 # modified, we need to post process the result
3013 # modified, we need to post process the result
3013 if p2 != nullid:
3014 if p2 != nullid:
3014 mergeadd = dsmodified - smf
3015 mergeadd = dsmodified - smf
3015 dsadded |= mergeadd
3016 dsadded |= mergeadd
3016 dsmodified -= mergeadd
3017 dsmodified -= mergeadd
3017
3018
3018 # if f is a rename, update `names` to also revert the source
3019 # if f is a rename, update `names` to also revert the source
3019 cwd = repo.getcwd()
3020 cwd = repo.getcwd()
3020 for f in localchanges:
3021 for f in localchanges:
3021 src = repo.dirstate.copied(f)
3022 src = repo.dirstate.copied(f)
3022 # XXX should we check for rename down to target node?
3023 # XXX should we check for rename down to target node?
3023 if src and src not in names and repo.dirstate[src] == 'r':
3024 if src and src not in names and repo.dirstate[src] == 'r':
3024 dsremoved.add(src)
3025 dsremoved.add(src)
3025 names[src] = (repo.pathto(src, cwd), True)
3026 names[src] = (repo.pathto(src, cwd), True)
3026
3027
3027 # distinguish between file to forget and the other
3028 # distinguish between file to forget and the other
3028 added = set()
3029 added = set()
3029 for abs in dsadded:
3030 for abs in dsadded:
3030 if repo.dirstate[abs] != 'a':
3031 if repo.dirstate[abs] != 'a':
3031 added.add(abs)
3032 added.add(abs)
3032 dsadded -= added
3033 dsadded -= added
3033
3034
3034 for abs in deladded:
3035 for abs in deladded:
3035 if repo.dirstate[abs] == 'a':
3036 if repo.dirstate[abs] == 'a':
3036 dsadded.add(abs)
3037 dsadded.add(abs)
3037 deladded -= dsadded
3038 deladded -= dsadded
3038
3039
3039 # For files marked as removed, we check if an unknown file is present at
3040 # For files marked as removed, we check if an unknown file is present at
3040 # the same path. If a such file exists it may need to be backed up.
3041 # the same path. If a such file exists it may need to be backed up.
3041 # Making the distinction at this stage helps have simpler backup
3042 # Making the distinction at this stage helps have simpler backup
3042 # logic.
3043 # logic.
3043 removunk = set()
3044 removunk = set()
3044 for abs in removed:
3045 for abs in removed:
3045 target = repo.wjoin(abs)
3046 target = repo.wjoin(abs)
3046 if os.path.lexists(target):
3047 if os.path.lexists(target):
3047 removunk.add(abs)
3048 removunk.add(abs)
3048 removed -= removunk
3049 removed -= removunk
3049
3050
3050 dsremovunk = set()
3051 dsremovunk = set()
3051 for abs in dsremoved:
3052 for abs in dsremoved:
3052 target = repo.wjoin(abs)
3053 target = repo.wjoin(abs)
3053 if os.path.lexists(target):
3054 if os.path.lexists(target):
3054 dsremovunk.add(abs)
3055 dsremovunk.add(abs)
3055 dsremoved -= dsremovunk
3056 dsremoved -= dsremovunk
3056
3057
3057 # action to be actually performed by revert
3058 # action to be actually performed by revert
3058 # (<list of file>, message>) tuple
3059 # (<list of file>, message>) tuple
3059 actions = {'revert': ([], _('reverting %s\n')),
3060 actions = {'revert': ([], _('reverting %s\n')),
3060 'add': ([], _('adding %s\n')),
3061 'add': ([], _('adding %s\n')),
3061 'remove': ([], _('removing %s\n')),
3062 'remove': ([], _('removing %s\n')),
3062 'drop': ([], _('removing %s\n')),
3063 'drop': ([], _('removing %s\n')),
3063 'forget': ([], _('forgetting %s\n')),
3064 'forget': ([], _('forgetting %s\n')),
3064 'undelete': ([], _('undeleting %s\n')),
3065 'undelete': ([], _('undeleting %s\n')),
3065 'noop': (None, _('no changes needed to %s\n')),
3066 'noop': (None, _('no changes needed to %s\n')),
3066 'unknown': (None, _('file not managed: %s\n')),
3067 'unknown': (None, _('file not managed: %s\n')),
3067 }
3068 }
3068
3069
3069 # "constant" that convey the backup strategy.
3070 # "constant" that convey the backup strategy.
3070 # All set to `discard` if `no-backup` is set do avoid checking
3071 # All set to `discard` if `no-backup` is set do avoid checking
3071 # no_backup lower in the code.
3072 # no_backup lower in the code.
3072 # These values are ordered for comparison purposes
3073 # These values are ordered for comparison purposes
3073 backupinteractive = 3 # do backup if interactively modified
3074 backupinteractive = 3 # do backup if interactively modified
3074 backup = 2 # unconditionally do backup
3075 backup = 2 # unconditionally do backup
3075 check = 1 # check if the existing file differs from target
3076 check = 1 # check if the existing file differs from target
3076 discard = 0 # never do backup
3077 discard = 0 # never do backup
3077 if opts.get('no_backup'):
3078 if opts.get('no_backup'):
3078 backupinteractive = backup = check = discard
3079 backupinteractive = backup = check = discard
3079 if interactive:
3080 if interactive:
3080 dsmodifiedbackup = backupinteractive
3081 dsmodifiedbackup = backupinteractive
3081 else:
3082 else:
3082 dsmodifiedbackup = backup
3083 dsmodifiedbackup = backup
3083 tobackup = set()
3084 tobackup = set()
3084
3085
3085 backupanddel = actions['remove']
3086 backupanddel = actions['remove']
3086 if not opts.get('no_backup'):
3087 if not opts.get('no_backup'):
3087 backupanddel = actions['drop']
3088 backupanddel = actions['drop']
3088
3089
3089 disptable = (
3090 disptable = (
3090 # dispatch table:
3091 # dispatch table:
3091 # file state
3092 # file state
3092 # action
3093 # action
3093 # make backup
3094 # make backup
3094
3095
3095 ## Sets that results that will change file on disk
3096 ## Sets that results that will change file on disk
3096 # Modified compared to target, no local change
3097 # Modified compared to target, no local change
3097 (modified, actions['revert'], discard),
3098 (modified, actions['revert'], discard),
3098 # Modified compared to target, but local file is deleted
3099 # Modified compared to target, but local file is deleted
3099 (deleted, actions['revert'], discard),
3100 (deleted, actions['revert'], discard),
3100 # Modified compared to target, local change
3101 # Modified compared to target, local change
3101 (dsmodified, actions['revert'], dsmodifiedbackup),
3102 (dsmodified, actions['revert'], dsmodifiedbackup),
3102 # Added since target
3103 # Added since target
3103 (added, actions['remove'], discard),
3104 (added, actions['remove'], discard),
3104 # Added in working directory
3105 # Added in working directory
3105 (dsadded, actions['forget'], discard),
3106 (dsadded, actions['forget'], discard),
3106 # Added since target, have local modification
3107 # Added since target, have local modification
3107 (modadded, backupanddel, backup),
3108 (modadded, backupanddel, backup),
3108 # Added since target but file is missing in working directory
3109 # Added since target but file is missing in working directory
3109 (deladded, actions['drop'], discard),
3110 (deladded, actions['drop'], discard),
3110 # Removed since target, before working copy parent
3111 # Removed since target, before working copy parent
3111 (removed, actions['add'], discard),
3112 (removed, actions['add'], discard),
3112 # Same as `removed` but an unknown file exists at the same path
3113 # Same as `removed` but an unknown file exists at the same path
3113 (removunk, actions['add'], check),
3114 (removunk, actions['add'], check),
3114 # Removed since targe, marked as such in working copy parent
3115 # Removed since targe, marked as such in working copy parent
3115 (dsremoved, actions['undelete'], discard),
3116 (dsremoved, actions['undelete'], discard),
3116 # Same as `dsremoved` but an unknown file exists at the same path
3117 # Same as `dsremoved` but an unknown file exists at the same path
3117 (dsremovunk, actions['undelete'], check),
3118 (dsremovunk, actions['undelete'], check),
3118 ## the following sets does not result in any file changes
3119 ## the following sets does not result in any file changes
3119 # File with no modification
3120 # File with no modification
3120 (clean, actions['noop'], discard),
3121 (clean, actions['noop'], discard),
3121 # Existing file, not tracked anywhere
3122 # Existing file, not tracked anywhere
3122 (unknown, actions['unknown'], discard),
3123 (unknown, actions['unknown'], discard),
3123 )
3124 )
3124
3125
3125 for abs, (rel, exact) in sorted(names.items()):
3126 for abs, (rel, exact) in sorted(names.items()):
3126 # target file to be touch on disk (relative to cwd)
3127 # target file to be touch on disk (relative to cwd)
3127 target = repo.wjoin(abs)
3128 target = repo.wjoin(abs)
3128 # search the entry in the dispatch table.
3129 # search the entry in the dispatch table.
3129 # if the file is in any of these sets, it was touched in the working
3130 # if the file is in any of these sets, it was touched in the working
3130 # directory parent and we are sure it needs to be reverted.
3131 # directory parent and we are sure it needs to be reverted.
3131 for table, (xlist, msg), dobackup in disptable:
3132 for table, (xlist, msg), dobackup in disptable:
3132 if abs not in table:
3133 if abs not in table:
3133 continue
3134 continue
3134 if xlist is not None:
3135 if xlist is not None:
3135 xlist.append(abs)
3136 xlist.append(abs)
3136 if dobackup:
3137 if dobackup:
3137 # If in interactive mode, don't automatically create
3138 # If in interactive mode, don't automatically create
3138 # .orig files (issue4793)
3139 # .orig files (issue4793)
3139 if dobackup == backupinteractive:
3140 if dobackup == backupinteractive:
3140 tobackup.add(abs)
3141 tobackup.add(abs)
3141 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3142 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3142 bakname = scmutil.origpath(ui, repo, rel)
3143 bakname = scmutil.origpath(ui, repo, rel)
3143 ui.note(_('saving current version of %s as %s\n') %
3144 ui.note(_('saving current version of %s as %s\n') %
3144 (rel, bakname))
3145 (rel, bakname))
3145 if not opts.get('dry_run'):
3146 if not opts.get('dry_run'):
3146 if interactive:
3147 if interactive:
3147 util.copyfile(target, bakname)
3148 util.copyfile(target, bakname)
3148 else:
3149 else:
3149 util.rename(target, bakname)
3150 util.rename(target, bakname)
3150 if ui.verbose or not exact:
3151 if ui.verbose or not exact:
3151 if not isinstance(msg, basestring):
3152 if not isinstance(msg, basestring):
3152 msg = msg(abs)
3153 msg = msg(abs)
3153 ui.status(msg % rel)
3154 ui.status(msg % rel)
3154 elif exact:
3155 elif exact:
3155 ui.warn(msg % rel)
3156 ui.warn(msg % rel)
3156 break
3157 break
3157
3158
3158 if not opts.get('dry_run'):
3159 if not opts.get('dry_run'):
3159 needdata = ('revert', 'add', 'undelete')
3160 needdata = ('revert', 'add', 'undelete')
3160 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3161 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3161 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3162 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3162
3163
3163 if targetsubs:
3164 if targetsubs:
3164 # Revert the subrepos on the revert list
3165 # Revert the subrepos on the revert list
3165 for sub in targetsubs:
3166 for sub in targetsubs:
3166 try:
3167 try:
3167 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3168 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3168 except KeyError:
3169 except KeyError:
3169 raise error.Abort("subrepository '%s' does not exist in %s!"
3170 raise error.Abort("subrepository '%s' does not exist in %s!"
3170 % (sub, short(ctx.node())))
3171 % (sub, short(ctx.node())))
3171
3172
3172 def _revertprefetch(repo, ctx, *files):
3173 def _revertprefetch(repo, ctx, *files):
3173 """Let extension changing the storage layer prefetch content"""
3174 """Let extension changing the storage layer prefetch content"""
3174 pass
3175 pass
3175
3176
3176 def _performrevert(repo, parents, ctx, actions, interactive=False,
3177 def _performrevert(repo, parents, ctx, actions, interactive=False,
3177 tobackup=None):
3178 tobackup=None):
3178 """function that actually perform all the actions computed for revert
3179 """function that actually perform all the actions computed for revert
3179
3180
3180 This is an independent function to let extension to plug in and react to
3181 This is an independent function to let extension to plug in and react to
3181 the imminent revert.
3182 the imminent revert.
3182
3183
3183 Make sure you have the working directory locked when calling this function.
3184 Make sure you have the working directory locked when calling this function.
3184 """
3185 """
3185 parent, p2 = parents
3186 parent, p2 = parents
3186 node = ctx.node()
3187 node = ctx.node()
3187 excluded_files = []
3188 excluded_files = []
3188 matcher_opts = {"exclude": excluded_files}
3189 matcher_opts = {"exclude": excluded_files}
3189
3190
3190 def checkout(f):
3191 def checkout(f):
3191 fc = ctx[f]
3192 fc = ctx[f]
3192 repo.wwrite(f, fc.data(), fc.flags())
3193 repo.wwrite(f, fc.data(), fc.flags())
3193
3194
3194 def doremove(f):
3195 def doremove(f):
3195 try:
3196 try:
3196 util.unlinkpath(repo.wjoin(f))
3197 util.unlinkpath(repo.wjoin(f))
3197 except OSError:
3198 except OSError:
3198 pass
3199 pass
3199 repo.dirstate.remove(f)
3200 repo.dirstate.remove(f)
3200
3201
3201 audit_path = pathutil.pathauditor(repo.root)
3202 audit_path = pathutil.pathauditor(repo.root)
3202 for f in actions['forget'][0]:
3203 for f in actions['forget'][0]:
3203 if interactive:
3204 if interactive:
3204 choice = repo.ui.promptchoice(
3205 choice = repo.ui.promptchoice(
3205 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3206 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3206 if choice == 0:
3207 if choice == 0:
3207 repo.dirstate.drop(f)
3208 repo.dirstate.drop(f)
3208 else:
3209 else:
3209 excluded_files.append(repo.wjoin(f))
3210 excluded_files.append(repo.wjoin(f))
3210 else:
3211 else:
3211 repo.dirstate.drop(f)
3212 repo.dirstate.drop(f)
3212 for f in actions['remove'][0]:
3213 for f in actions['remove'][0]:
3213 audit_path(f)
3214 audit_path(f)
3214 if interactive:
3215 if interactive:
3215 choice = repo.ui.promptchoice(
3216 choice = repo.ui.promptchoice(
3216 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3217 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3217 if choice == 0:
3218 if choice == 0:
3218 doremove(f)
3219 doremove(f)
3219 else:
3220 else:
3220 excluded_files.append(repo.wjoin(f))
3221 excluded_files.append(repo.wjoin(f))
3221 else:
3222 else:
3222 doremove(f)
3223 doremove(f)
3223 for f in actions['drop'][0]:
3224 for f in actions['drop'][0]:
3224 audit_path(f)
3225 audit_path(f)
3225 repo.dirstate.remove(f)
3226 repo.dirstate.remove(f)
3226
3227
3227 normal = None
3228 normal = None
3228 if node == parent:
3229 if node == parent:
3229 # We're reverting to our parent. If possible, we'd like status
3230 # We're reverting to our parent. If possible, we'd like status
3230 # to report the file as clean. We have to use normallookup for
3231 # to report the file as clean. We have to use normallookup for
3231 # merges to avoid losing information about merged/dirty files.
3232 # merges to avoid losing information about merged/dirty files.
3232 if p2 != nullid:
3233 if p2 != nullid:
3233 normal = repo.dirstate.normallookup
3234 normal = repo.dirstate.normallookup
3234 else:
3235 else:
3235 normal = repo.dirstate.normal
3236 normal = repo.dirstate.normal
3236
3237
3237 newlyaddedandmodifiedfiles = set()
3238 newlyaddedandmodifiedfiles = set()
3238 if interactive:
3239 if interactive:
3239 # Prompt the user for changes to revert
3240 # Prompt the user for changes to revert
3240 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3241 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3241 m = scmutil.match(ctx, torevert, matcher_opts)
3242 m = scmutil.match(ctx, torevert, matcher_opts)
3242 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3243 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3243 diffopts.nodates = True
3244 diffopts.nodates = True
3244 diffopts.git = True
3245 diffopts.git = True
3245 reversehunks = repo.ui.configbool('experimental',
3246 reversehunks = repo.ui.configbool('experimental',
3246 'revertalternateinteractivemode',
3247 'revertalternateinteractivemode',
3247 True)
3248 True)
3248 if reversehunks:
3249 if reversehunks:
3249 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3250 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3250 else:
3251 else:
3251 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3252 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3252 originalchunks = patch.parsepatch(diff)
3253 originalchunks = patch.parsepatch(diff)
3253 operation = 'discard' if node == parent else 'revert'
3254 operation = 'discard' if node == parent else 'revert'
3254
3255
3255 try:
3256 try:
3256
3257
3257 chunks, opts = recordfilter(repo.ui, originalchunks,
3258 chunks, opts = recordfilter(repo.ui, originalchunks,
3258 operation=operation)
3259 operation=operation)
3259 if reversehunks:
3260 if reversehunks:
3260 chunks = patch.reversehunks(chunks)
3261 chunks = patch.reversehunks(chunks)
3261
3262
3262 except patch.PatchError as err:
3263 except patch.PatchError as err:
3263 raise error.Abort(_('error parsing patch: %s') % err)
3264 raise error.Abort(_('error parsing patch: %s') % err)
3264
3265
3265 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3266 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3266 if tobackup is None:
3267 if tobackup is None:
3267 tobackup = set()
3268 tobackup = set()
3268 # Apply changes
3269 # Apply changes
3269 fp = stringio()
3270 fp = stringio()
3270 for c in chunks:
3271 for c in chunks:
3271 # Create a backup file only if this hunk should be backed up
3272 # Create a backup file only if this hunk should be backed up
3272 if ishunk(c) and c.header.filename() in tobackup:
3273 if ishunk(c) and c.header.filename() in tobackup:
3273 abs = c.header.filename()
3274 abs = c.header.filename()
3274 target = repo.wjoin(abs)
3275 target = repo.wjoin(abs)
3275 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3276 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3276 util.copyfile(target, bakname)
3277 util.copyfile(target, bakname)
3277 tobackup.remove(abs)
3278 tobackup.remove(abs)
3278 c.write(fp)
3279 c.write(fp)
3279 dopatch = fp.tell()
3280 dopatch = fp.tell()
3280 fp.seek(0)
3281 fp.seek(0)
3281 if dopatch:
3282 if dopatch:
3282 try:
3283 try:
3283 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3284 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3284 except patch.PatchError as err:
3285 except patch.PatchError as err:
3285 raise error.Abort(str(err))
3286 raise error.Abort(str(err))
3286 del fp
3287 del fp
3287 else:
3288 else:
3288 for f in actions['revert'][0]:
3289 for f in actions['revert'][0]:
3289 checkout(f)
3290 checkout(f)
3290 if normal:
3291 if normal:
3291 normal(f)
3292 normal(f)
3292
3293
3293 for f in actions['add'][0]:
3294 for f in actions['add'][0]:
3294 # Don't checkout modified files, they are already created by the diff
3295 # Don't checkout modified files, they are already created by the diff
3295 if f not in newlyaddedandmodifiedfiles:
3296 if f not in newlyaddedandmodifiedfiles:
3296 checkout(f)
3297 checkout(f)
3297 repo.dirstate.add(f)
3298 repo.dirstate.add(f)
3298
3299
3299 normal = repo.dirstate.normallookup
3300 normal = repo.dirstate.normallookup
3300 if node == parent and p2 == nullid:
3301 if node == parent and p2 == nullid:
3301 normal = repo.dirstate.normal
3302 normal = repo.dirstate.normal
3302 for f in actions['undelete'][0]:
3303 for f in actions['undelete'][0]:
3303 checkout(f)
3304 checkout(f)
3304 normal(f)
3305 normal(f)
3305
3306
3306 copied = copies.pathcopies(repo[parent], ctx)
3307 copied = copies.pathcopies(repo[parent], ctx)
3307
3308
3308 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3309 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3309 if f in copied:
3310 if f in copied:
3310 repo.dirstate.copy(copied[f], f)
3311 repo.dirstate.copy(copied[f], f)
3311
3312
3312 def command(table):
3313 def command(table):
3313 """Returns a function object to be used as a decorator for making commands.
3314 """Returns a function object to be used as a decorator for making commands.
3314
3315
3315 This function receives a command table as its argument. The table should
3316 This function receives a command table as its argument. The table should
3316 be a dict.
3317 be a dict.
3317
3318
3318 The returned function can be used as a decorator for adding commands
3319 The returned function can be used as a decorator for adding commands
3319 to that command table. This function accepts multiple arguments to define
3320 to that command table. This function accepts multiple arguments to define
3320 a command.
3321 a command.
3321
3322
3322 The first argument is the command name.
3323 The first argument is the command name.
3323
3324
3324 The options argument is an iterable of tuples defining command arguments.
3325 The options argument is an iterable of tuples defining command arguments.
3325 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3326 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3326
3327
3327 The synopsis argument defines a short, one line summary of how to use the
3328 The synopsis argument defines a short, one line summary of how to use the
3328 command. This shows up in the help output.
3329 command. This shows up in the help output.
3329
3330
3330 The norepo argument defines whether the command does not require a
3331 The norepo argument defines whether the command does not require a
3331 local repository. Most commands operate against a repository, thus the
3332 local repository. Most commands operate against a repository, thus the
3332 default is False.
3333 default is False.
3333
3334
3334 The optionalrepo argument defines whether the command optionally requires
3335 The optionalrepo argument defines whether the command optionally requires
3335 a local repository.
3336 a local repository.
3336
3337
3337 The inferrepo argument defines whether to try to find a repository from the
3338 The inferrepo argument defines whether to try to find a repository from the
3338 command line arguments. If True, arguments will be examined for potential
3339 command line arguments. If True, arguments will be examined for potential
3339 repository locations. See ``findrepo()``. If a repository is found, it
3340 repository locations. See ``findrepo()``. If a repository is found, it
3340 will be used.
3341 will be used.
3341 """
3342 """
3342 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3343 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3343 inferrepo=False):
3344 inferrepo=False):
3344 def decorator(func):
3345 def decorator(func):
3345 func.norepo = norepo
3346 func.norepo = norepo
3346 func.optionalrepo = optionalrepo
3347 func.optionalrepo = optionalrepo
3347 func.inferrepo = inferrepo
3348 func.inferrepo = inferrepo
3348 if synopsis:
3349 if synopsis:
3349 table[name] = func, list(options), synopsis
3350 table[name] = func, list(options), synopsis
3350 else:
3351 else:
3351 table[name] = func, list(options)
3352 table[name] = func, list(options)
3352 return func
3353 return func
3353 return decorator
3354 return decorator
3354
3355
3355 return cmd
3356 return cmd
3356
3357
3357 def checkunresolved(ms):
3358 def checkunresolved(ms):
3358 ms._repo.ui.deprecwarn('checkunresolved moved from cmdutil to mergeutil',
3359 ms._repo.ui.deprecwarn('checkunresolved moved from cmdutil to mergeutil',
3359 '4.1')
3360 '4.1')
3360 return mergeutil.checkunresolved(ms)
3361 return mergeutil.checkunresolved(ms)
3361
3362
3362 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3363 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3363 # commands.outgoing. "missing" is "missing" of the result of
3364 # commands.outgoing. "missing" is "missing" of the result of
3364 # "findcommonoutgoing()"
3365 # "findcommonoutgoing()"
3365 outgoinghooks = util.hooks()
3366 outgoinghooks = util.hooks()
3366
3367
3367 # a list of (ui, repo) functions called by commands.summary
3368 # a list of (ui, repo) functions called by commands.summary
3368 summaryhooks = util.hooks()
3369 summaryhooks = util.hooks()
3369
3370
3370 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3371 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3371 #
3372 #
3372 # functions should return tuple of booleans below, if 'changes' is None:
3373 # functions should return tuple of booleans below, if 'changes' is None:
3373 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3374 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3374 #
3375 #
3375 # otherwise, 'changes' is a tuple of tuples below:
3376 # otherwise, 'changes' is a tuple of tuples below:
3376 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3377 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3377 # - (desturl, destbranch, destpeer, outgoing)
3378 # - (desturl, destbranch, destpeer, outgoing)
3378 summaryremotehooks = util.hooks()
3379 summaryremotehooks = util.hooks()
3379
3380
3380 # A list of state files kept by multistep operations like graft.
3381 # A list of state files kept by multistep operations like graft.
3381 # Since graft cannot be aborted, it is considered 'clearable' by update.
3382 # Since graft cannot be aborted, it is considered 'clearable' by update.
3382 # note: bisect is intentionally excluded
3383 # note: bisect is intentionally excluded
3383 # (state file, clearable, allowcommit, error, hint)
3384 # (state file, clearable, allowcommit, error, hint)
3384 unfinishedstates = [
3385 unfinishedstates = [
3385 ('graftstate', True, False, _('graft in progress'),
3386 ('graftstate', True, False, _('graft in progress'),
3386 _("use 'hg graft --continue' or 'hg update' to abort")),
3387 _("use 'hg graft --continue' or 'hg update' to abort")),
3387 ('updatestate', True, False, _('last update was interrupted'),
3388 ('updatestate', True, False, _('last update was interrupted'),
3388 _("use 'hg update' to get a consistent checkout"))
3389 _("use 'hg update' to get a consistent checkout"))
3389 ]
3390 ]
3390
3391
3391 def checkunfinished(repo, commit=False):
3392 def checkunfinished(repo, commit=False):
3392 '''Look for an unfinished multistep operation, like graft, and abort
3393 '''Look for an unfinished multistep operation, like graft, and abort
3393 if found. It's probably good to check this right before
3394 if found. It's probably good to check this right before
3394 bailifchanged().
3395 bailifchanged().
3395 '''
3396 '''
3396 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3397 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3397 if commit and allowcommit:
3398 if commit and allowcommit:
3398 continue
3399 continue
3399 if repo.vfs.exists(f):
3400 if repo.vfs.exists(f):
3400 raise error.Abort(msg, hint=hint)
3401 raise error.Abort(msg, hint=hint)
3401
3402
3402 def clearunfinished(repo):
3403 def clearunfinished(repo):
3403 '''Check for unfinished operations (as above), and clear the ones
3404 '''Check for unfinished operations (as above), and clear the ones
3404 that are clearable.
3405 that are clearable.
3405 '''
3406 '''
3406 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3407 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3407 if not clearable and repo.vfs.exists(f):
3408 if not clearable and repo.vfs.exists(f):
3408 raise error.Abort(msg, hint=hint)
3409 raise error.Abort(msg, hint=hint)
3409 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3410 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3410 if clearable and repo.vfs.exists(f):
3411 if clearable and repo.vfs.exists(f):
3411 util.unlink(repo.join(f))
3412 util.unlink(repo.join(f))
3412
3413
3413 afterresolvedstates = [
3414 afterresolvedstates = [
3414 ('graftstate',
3415 ('graftstate',
3415 _('hg graft --continue')),
3416 _('hg graft --continue')),
3416 ]
3417 ]
3417
3418
3418 def howtocontinue(repo):
3419 def howtocontinue(repo):
3419 '''Check for an unfinished operation and return the command to finish
3420 '''Check for an unfinished operation and return the command to finish
3420 it.
3421 it.
3421
3422
3422 afterresolvedstates tuples define a .hg/{file} and the corresponding
3423 afterresolvedstates tuples define a .hg/{file} and the corresponding
3423 command needed to finish it.
3424 command needed to finish it.
3424
3425
3425 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3426 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3426 a boolean.
3427 a boolean.
3427 '''
3428 '''
3428 contmsg = _("continue: %s")
3429 contmsg = _("continue: %s")
3429 for f, msg in afterresolvedstates:
3430 for f, msg in afterresolvedstates:
3430 if repo.vfs.exists(f):
3431 if repo.vfs.exists(f):
3431 return contmsg % msg, True
3432 return contmsg % msg, True
3432 workingctx = repo[None]
3433 workingctx = repo[None]
3433 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3434 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3434 for s in workingctx.substate)
3435 for s in workingctx.substate)
3435 if dirty:
3436 if dirty:
3436 return contmsg % _("hg commit"), False
3437 return contmsg % _("hg commit"), False
3437 return None, None
3438 return None, None
3438
3439
3439 def checkafterresolved(repo):
3440 def checkafterresolved(repo):
3440 '''Inform the user about the next action after completing hg resolve
3441 '''Inform the user about the next action after completing hg resolve
3441
3442
3442 If there's a matching afterresolvedstates, howtocontinue will yield
3443 If there's a matching afterresolvedstates, howtocontinue will yield
3443 repo.ui.warn as the reporter.
3444 repo.ui.warn as the reporter.
3444
3445
3445 Otherwise, it will yield repo.ui.note.
3446 Otherwise, it will yield repo.ui.note.
3446 '''
3447 '''
3447 msg, warning = howtocontinue(repo)
3448 msg, warning = howtocontinue(repo)
3448 if msg is not None:
3449 if msg is not None:
3449 if warning:
3450 if warning:
3450 repo.ui.warn("%s\n" % msg)
3451 repo.ui.warn("%s\n" % msg)
3451 else:
3452 else:
3452 repo.ui.note("%s\n" % msg)
3453 repo.ui.note("%s\n" % msg)
3453
3454
3454 def wrongtooltocontinue(repo, task):
3455 def wrongtooltocontinue(repo, task):
3455 '''Raise an abort suggesting how to properly continue if there is an
3456 '''Raise an abort suggesting how to properly continue if there is an
3456 active task.
3457 active task.
3457
3458
3458 Uses howtocontinue() to find the active task.
3459 Uses howtocontinue() to find the active task.
3459
3460
3460 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3461 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3461 a hint.
3462 a hint.
3462 '''
3463 '''
3463 after = howtocontinue(repo)
3464 after = howtocontinue(repo)
3464 hint = None
3465 hint = None
3465 if after[1]:
3466 if after[1]:
3466 hint = after[0]
3467 hint = after[0]
3467 raise error.Abort(_('no %s in progress') % task, hint=hint)
3468 raise error.Abort(_('no %s in progress') % task, hint=hint)
3468
3469
3469 class dirstateguard(dirstateguardmod.dirstateguard):
3470 class dirstateguard(dirstateguardmod.dirstateguard):
3470 def __init__(self, repo, name):
3471 def __init__(self, repo, name):
3471 dirstateguardmod.dirstateguard.__init__(self, repo, name)
3472 dirstateguardmod.dirstateguard.__init__(self, repo, name)
3472 repo.ui.deprecwarn(
3473 repo.ui.deprecwarn(
3473 'dirstateguard has moved from cmdutil to dirstateguard',
3474 'dirstateguard has moved from cmdutil to dirstateguard',
3474 '4.1')
3475 '4.1')
@@ -1,790 +1,793 b''
1 commit date test
1 commit date test
2
2
3 $ hg init test
3 $ hg init test
4 $ cd test
4 $ cd test
5 $ echo foo > foo
5 $ echo foo > foo
6 $ hg add foo
6 $ hg add foo
7 $ cat > $TESTTMP/checkeditform.sh <<EOF
7 $ cat > $TESTTMP/checkeditform.sh <<EOF
8 > env | grep HGEDITFORM
8 > env | grep HGEDITFORM
9 > true
9 > true
10 > EOF
10 > EOF
11 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg commit -m ""
11 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg commit -m ""
12 HGEDITFORM=commit.normal.normal
12 HGEDITFORM=commit.normal.normal
13 abort: empty commit message
13 abort: empty commit message
14 [255]
14 [255]
15 $ hg commit -d '0 0' -m commit-1
15 $ hg commit -d '0 0' -m commit-1
16 $ echo foo >> foo
16 $ echo foo >> foo
17 $ hg commit -d '1 4444444' -m commit-3
17 $ hg commit -d '1 4444444' -m commit-3
18 abort: impossible time zone offset: 4444444
18 abort: impossible time zone offset: 4444444
19 [255]
19 [255]
20 $ hg commit -d '1 15.1' -m commit-4
20 $ hg commit -d '1 15.1' -m commit-4
21 abort: invalid date: '1\t15.1'
21 abort: invalid date: '1\t15.1'
22 [255]
22 [255]
23 $ hg commit -d 'foo bar' -m commit-5
23 $ hg commit -d 'foo bar' -m commit-5
24 abort: invalid date: 'foo bar'
24 abort: invalid date: 'foo bar'
25 [255]
25 [255]
26 $ hg commit -d ' 1 4444' -m commit-6
26 $ hg commit -d ' 1 4444' -m commit-6
27 $ hg commit -d '111111111111 0' -m commit-7
27 $ hg commit -d '111111111111 0' -m commit-7
28 abort: date exceeds 32 bits: 111111111111
28 abort: date exceeds 32 bits: 111111111111
29 [255]
29 [255]
30 $ hg commit -d '-111111111111 0' -m commit-7
30 $ hg commit -d '-111111111111 0' -m commit-7
31 abort: date exceeds 32 bits: -111111111111
31 abort: date exceeds 32 bits: -111111111111
32 [255]
32 [255]
33 $ echo foo >> foo
33 $ echo foo >> foo
34 $ hg commit -d '1901-12-13 20:45:52 +0000' -m commit-7-2
34 $ hg commit -d '1901-12-13 20:45:52 +0000' -m commit-7-2
35 $ echo foo >> foo
35 $ echo foo >> foo
36 $ hg commit -d '-2147483648 0' -m commit-7-3
36 $ hg commit -d '-2147483648 0' -m commit-7-3
37 $ hg log -T '{rev} {date|isodatesec}\n' -l2
37 $ hg log -T '{rev} {date|isodatesec}\n' -l2
38 3 1901-12-13 20:45:52 +0000
38 3 1901-12-13 20:45:52 +0000
39 2 1901-12-13 20:45:52 +0000
39 2 1901-12-13 20:45:52 +0000
40 $ hg commit -d '1901-12-13 20:45:51 +0000' -m commit-7
40 $ hg commit -d '1901-12-13 20:45:51 +0000' -m commit-7
41 abort: date exceeds 32 bits: -2147483649
41 abort: date exceeds 32 bits: -2147483649
42 [255]
42 [255]
43 $ hg commit -d '-2147483649 0' -m commit-7
43 $ hg commit -d '-2147483649 0' -m commit-7
44 abort: date exceeds 32 bits: -2147483649
44 abort: date exceeds 32 bits: -2147483649
45 [255]
45 [255]
46
46
47 commit added file that has been deleted
47 commit added file that has been deleted
48
48
49 $ echo bar > bar
49 $ echo bar > bar
50 $ hg add bar
50 $ hg add bar
51 $ rm bar
51 $ rm bar
52 $ hg commit -m commit-8
52 $ hg commit -m commit-8
53 nothing changed (1 missing files, see 'hg status')
53 nothing changed (1 missing files, see 'hg status')
54 [1]
54 [1]
55 $ hg commit -m commit-8-2 bar
55 $ hg commit -m commit-8-2 bar
56 abort: bar: file not found!
56 abort: bar: file not found!
57 [255]
57 [255]
58
58
59 $ hg -q revert -a --no-backup
59 $ hg -q revert -a --no-backup
60
60
61 $ mkdir dir
61 $ mkdir dir
62 $ echo boo > dir/file
62 $ echo boo > dir/file
63 $ hg add
63 $ hg add
64 adding dir/file (glob)
64 adding dir/file (glob)
65 $ hg -v commit -m commit-9 dir
65 $ hg -v commit -m commit-9 dir
66 committing files:
66 committing files:
67 dir/file
67 dir/file
68 committing manifest
68 committing manifest
69 committing changelog
69 committing changelog
70 committed changeset 4:1957363f1ced
70 committed changeset 4:1957363f1ced
71
71
72 $ echo > dir.file
72 $ echo > dir.file
73 $ hg add
73 $ hg add
74 adding dir.file
74 adding dir.file
75 $ hg commit -m commit-10 dir dir.file
75 $ hg commit -m commit-10 dir dir.file
76 abort: dir: no match under directory!
76 abort: dir: no match under directory!
77 [255]
77 [255]
78
78
79 $ echo >> dir/file
79 $ echo >> dir/file
80 $ mkdir bleh
80 $ mkdir bleh
81 $ mkdir dir2
81 $ mkdir dir2
82 $ cd bleh
82 $ cd bleh
83 $ hg commit -m commit-11 .
83 $ hg commit -m commit-11 .
84 abort: bleh: no match under directory!
84 abort: bleh: no match under directory!
85 [255]
85 [255]
86 $ hg commit -m commit-12 ../dir ../dir2
86 $ hg commit -m commit-12 ../dir ../dir2
87 abort: dir2: no match under directory!
87 abort: dir2: no match under directory!
88 [255]
88 [255]
89 $ hg -v commit -m commit-13 ../dir
89 $ hg -v commit -m commit-13 ../dir
90 committing files:
90 committing files:
91 dir/file
91 dir/file
92 committing manifest
92 committing manifest
93 committing changelog
93 committing changelog
94 committed changeset 5:a31d8f87544a
94 committed changeset 5:a31d8f87544a
95 $ cd ..
95 $ cd ..
96
96
97 $ hg commit -m commit-14 does-not-exist
97 $ hg commit -m commit-14 does-not-exist
98 abort: does-not-exist: * (glob)
98 abort: does-not-exist: * (glob)
99 [255]
99 [255]
100
100
101 #if symlink
101 #if symlink
102 $ ln -s foo baz
102 $ ln -s foo baz
103 $ hg commit -m commit-15 baz
103 $ hg commit -m commit-15 baz
104 abort: baz: file not tracked!
104 abort: baz: file not tracked!
105 [255]
105 [255]
106 #endif
106 #endif
107
107
108 $ touch quux
108 $ touch quux
109 $ hg commit -m commit-16 quux
109 $ hg commit -m commit-16 quux
110 abort: quux: file not tracked!
110 abort: quux: file not tracked!
111 [255]
111 [255]
112 $ echo >> dir/file
112 $ echo >> dir/file
113 $ hg -v commit -m commit-17 dir/file
113 $ hg -v commit -m commit-17 dir/file
114 committing files:
114 committing files:
115 dir/file
115 dir/file
116 committing manifest
116 committing manifest
117 committing changelog
117 committing changelog
118 committed changeset 6:32d054c9d085
118 committed changeset 6:32d054c9d085
119
119
120 An empty date was interpreted as epoch origin
120 An empty date was interpreted as epoch origin
121
121
122 $ echo foo >> foo
122 $ echo foo >> foo
123 $ hg commit -d '' -m commit-no-date
123 $ hg commit -d '' -m commit-no-date
124 $ hg tip --template '{date|isodate}\n' | grep '1970'
124 $ hg tip --template '{date|isodate}\n' | grep '1970'
125 [1]
125 [1]
126
126
127 Make sure we do not obscure unknown requires file entries (issue2649)
127 Make sure we do not obscure unknown requires file entries (issue2649)
128
128
129 $ echo foo >> foo
129 $ echo foo >> foo
130 $ echo fake >> .hg/requires
130 $ echo fake >> .hg/requires
131 $ hg commit -m bla
131 $ hg commit -m bla
132 abort: repository requires features unknown to this Mercurial: fake!
132 abort: repository requires features unknown to this Mercurial: fake!
133 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
133 (see https://mercurial-scm.org/wiki/MissingRequirement for more information)
134 [255]
134 [255]
135
135
136 $ cd ..
136 $ cd ..
137
137
138
138
139 partial subdir commit test
139 partial subdir commit test
140
140
141 $ hg init test2
141 $ hg init test2
142 $ cd test2
142 $ cd test2
143 $ mkdir foo
143 $ mkdir foo
144 $ echo foo > foo/foo
144 $ echo foo > foo/foo
145 $ mkdir bar
145 $ mkdir bar
146 $ echo bar > bar/bar
146 $ echo bar > bar/bar
147 $ hg add
147 $ hg add
148 adding bar/bar (glob)
148 adding bar/bar (glob)
149 adding foo/foo (glob)
149 adding foo/foo (glob)
150 $ HGEDITOR=cat hg ci -e -m commit-subdir-1 foo
150 $ HGEDITOR=cat hg ci -e -m commit-subdir-1 foo
151 commit-subdir-1
151 commit-subdir-1
152
152
153
153
154 HG: Enter commit message. Lines beginning with 'HG:' are removed.
154 HG: Enter commit message. Lines beginning with 'HG:' are removed.
155 HG: Leave message empty to abort commit.
155 HG: Leave message empty to abort commit.
156 HG: --
156 HG: --
157 HG: user: test
157 HG: user: test
158 HG: branch 'default'
158 HG: branch 'default'
159 HG: added foo/foo
159 HG: added foo/foo
160
160
161
161
162 $ hg ci -m commit-subdir-2 bar
162 $ hg ci -m commit-subdir-2 bar
163
163
164 subdir log 1
164 subdir log 1
165
165
166 $ hg log -v foo
166 $ hg log -v foo
167 changeset: 0:f97e73a25882
167 changeset: 0:f97e73a25882
168 user: test
168 user: test
169 date: Thu Jan 01 00:00:00 1970 +0000
169 date: Thu Jan 01 00:00:00 1970 +0000
170 files: foo/foo
170 files: foo/foo
171 description:
171 description:
172 commit-subdir-1
172 commit-subdir-1
173
173
174
174
175
175
176 subdir log 2
176 subdir log 2
177
177
178 $ hg log -v bar
178 $ hg log -v bar
179 changeset: 1:aa809156d50d
179 changeset: 1:aa809156d50d
180 tag: tip
180 tag: tip
181 user: test
181 user: test
182 date: Thu Jan 01 00:00:00 1970 +0000
182 date: Thu Jan 01 00:00:00 1970 +0000
183 files: bar/bar
183 files: bar/bar
184 description:
184 description:
185 commit-subdir-2
185 commit-subdir-2
186
186
187
187
188
188
189 full log
189 full log
190
190
191 $ hg log -v
191 $ hg log -v
192 changeset: 1:aa809156d50d
192 changeset: 1:aa809156d50d
193 tag: tip
193 tag: tip
194 user: test
194 user: test
195 date: Thu Jan 01 00:00:00 1970 +0000
195 date: Thu Jan 01 00:00:00 1970 +0000
196 files: bar/bar
196 files: bar/bar
197 description:
197 description:
198 commit-subdir-2
198 commit-subdir-2
199
199
200
200
201 changeset: 0:f97e73a25882
201 changeset: 0:f97e73a25882
202 user: test
202 user: test
203 date: Thu Jan 01 00:00:00 1970 +0000
203 date: Thu Jan 01 00:00:00 1970 +0000
204 files: foo/foo
204 files: foo/foo
205 description:
205 description:
206 commit-subdir-1
206 commit-subdir-1
207
207
208
208
209 $ cd ..
209 $ cd ..
210
210
211
211
212 dot and subdir commit test
212 dot and subdir commit test
213
213
214 $ hg init test3
214 $ hg init test3
215 $ echo commit-foo-subdir > commit-log-test
215 $ echo commit-foo-subdir > commit-log-test
216 $ cd test3
216 $ cd test3
217 $ mkdir foo
217 $ mkdir foo
218 $ echo foo content > foo/plain-file
218 $ echo foo content > foo/plain-file
219 $ hg add foo/plain-file
219 $ hg add foo/plain-file
220 $ HGEDITOR=cat hg ci --edit -l ../commit-log-test foo
220 $ HGEDITOR=cat hg ci --edit -l ../commit-log-test foo
221 commit-foo-subdir
221 commit-foo-subdir
222
222
223
223
224 HG: Enter commit message. Lines beginning with 'HG:' are removed.
224 HG: Enter commit message. Lines beginning with 'HG:' are removed.
225 HG: Leave message empty to abort commit.
225 HG: Leave message empty to abort commit.
226 HG: --
226 HG: --
227 HG: user: test
227 HG: user: test
228 HG: branch 'default'
228 HG: branch 'default'
229 HG: added foo/plain-file
229 HG: added foo/plain-file
230
230
231
231
232 $ echo modified foo content > foo/plain-file
232 $ echo modified foo content > foo/plain-file
233 $ hg ci -m commit-foo-dot .
233 $ hg ci -m commit-foo-dot .
234
234
235 full log
235 full log
236
236
237 $ hg log -v
237 $ hg log -v
238 changeset: 1:95b38e3a5b2e
238 changeset: 1:95b38e3a5b2e
239 tag: tip
239 tag: tip
240 user: test
240 user: test
241 date: Thu Jan 01 00:00:00 1970 +0000
241 date: Thu Jan 01 00:00:00 1970 +0000
242 files: foo/plain-file
242 files: foo/plain-file
243 description:
243 description:
244 commit-foo-dot
244 commit-foo-dot
245
245
246
246
247 changeset: 0:65d4e9386227
247 changeset: 0:65d4e9386227
248 user: test
248 user: test
249 date: Thu Jan 01 00:00:00 1970 +0000
249 date: Thu Jan 01 00:00:00 1970 +0000
250 files: foo/plain-file
250 files: foo/plain-file
251 description:
251 description:
252 commit-foo-subdir
252 commit-foo-subdir
253
253
254
254
255
255
256 subdir log
256 subdir log
257
257
258 $ cd foo
258 $ cd foo
259 $ hg log .
259 $ hg log .
260 changeset: 1:95b38e3a5b2e
260 changeset: 1:95b38e3a5b2e
261 tag: tip
261 tag: tip
262 user: test
262 user: test
263 date: Thu Jan 01 00:00:00 1970 +0000
263 date: Thu Jan 01 00:00:00 1970 +0000
264 summary: commit-foo-dot
264 summary: commit-foo-dot
265
265
266 changeset: 0:65d4e9386227
266 changeset: 0:65d4e9386227
267 user: test
267 user: test
268 date: Thu Jan 01 00:00:00 1970 +0000
268 date: Thu Jan 01 00:00:00 1970 +0000
269 summary: commit-foo-subdir
269 summary: commit-foo-subdir
270
270
271 $ cd ..
271 $ cd ..
272 $ cd ..
272 $ cd ..
273
273
274 Issue1049: Hg permits partial commit of merge without warning
274 Issue1049: Hg permits partial commit of merge without warning
275
275
276 $ hg init issue1049
276 $ hg init issue1049
277 $ cd issue1049
277 $ cd issue1049
278 $ echo a > a
278 $ echo a > a
279 $ hg ci -Ama
279 $ hg ci -Ama
280 adding a
280 adding a
281 $ echo a >> a
281 $ echo a >> a
282 $ hg ci -mb
282 $ hg ci -mb
283 $ hg up 0
283 $ hg up 0
284 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
284 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 $ echo b >> a
285 $ echo b >> a
286 $ hg ci -mc
286 $ hg ci -mc
287 created new head
287 created new head
288 $ HGMERGE=true hg merge
288 $ HGMERGE=true hg merge
289 merging a
289 merging a
290 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
290 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
291 (branch merge, don't forget to commit)
291 (branch merge, don't forget to commit)
292
292
293 should fail because we are specifying a file name
293 should fail because we are specifying a file name
294
294
295 $ hg ci -mmerge a
295 $ hg ci -mmerge a
296 abort: cannot partially commit a merge (do not specify files or patterns)
296 abort: cannot partially commit a merge (do not specify files or patterns)
297 [255]
297 [255]
298
298
299 should fail because we are specifying a pattern
299 should fail because we are specifying a pattern
300
300
301 $ hg ci -mmerge -I a
301 $ hg ci -mmerge -I a
302 abort: cannot partially commit a merge (do not specify files or patterns)
302 abort: cannot partially commit a merge (do not specify files or patterns)
303 [255]
303 [255]
304
304
305 should succeed
305 should succeed
306
306
307 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg ci -mmerge --edit
307 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg ci -mmerge --edit
308 HGEDITFORM=commit.normal.merge
308 HGEDITFORM=commit.normal.merge
309 $ cd ..
309 $ cd ..
310
310
311
311
312 test commit message content
312 test commit message content
313
313
314 $ hg init commitmsg
314 $ hg init commitmsg
315 $ cd commitmsg
315 $ cd commitmsg
316 $ echo changed > changed
316 $ echo changed > changed
317 $ echo removed > removed
317 $ echo removed > removed
318 $ hg book activebookmark
318 $ hg book activebookmark
319 $ hg ci -qAm init
319 $ hg ci -qAm init
320
320
321 $ hg rm removed
321 $ hg rm removed
322 $ echo changed >> changed
322 $ echo changed >> changed
323 $ echo added > added
323 $ echo added > added
324 $ hg add added
324 $ hg add added
325 $ HGEDITOR=cat hg ci -A
325 $ HGEDITOR=cat hg ci -A
326
326
327
327
328 HG: Enter commit message. Lines beginning with 'HG:' are removed.
328 HG: Enter commit message. Lines beginning with 'HG:' are removed.
329 HG: Leave message empty to abort commit.
329 HG: Leave message empty to abort commit.
330 HG: --
330 HG: --
331 HG: user: test
331 HG: user: test
332 HG: branch 'default'
332 HG: branch 'default'
333 HG: bookmark 'activebookmark'
333 HG: bookmark 'activebookmark'
334 HG: added added
334 HG: added added
335 HG: changed changed
335 HG: changed changed
336 HG: removed removed
336 HG: removed removed
337 abort: empty commit message
337 abort: empty commit message
338 [255]
338 [255]
339
339
340 test saving last-message.txt
340 test saving last-message.txt
341
341
342 $ hg init sub
342 $ hg init sub
343 $ echo a > sub/a
343 $ echo a > sub/a
344 $ hg -R sub add sub/a
344 $ hg -R sub add sub/a
345 $ cat > sub/.hg/hgrc <<EOF
345 $ cat > sub/.hg/hgrc <<EOF
346 > [hooks]
346 > [hooks]
347 > precommit.test-saving-last-message = false
347 > precommit.test-saving-last-message = false
348 > EOF
348 > EOF
349
349
350 $ echo 'sub = sub' > .hgsub
350 $ echo 'sub = sub' > .hgsub
351 $ hg add .hgsub
351 $ hg add .hgsub
352
352
353 $ cat > $TESTTMP/editor.sh <<EOF
353 $ cat > $TESTTMP/editor.sh <<EOF
354 > echo "==== before editing:"
354 > echo "==== before editing:"
355 > cat \$1
355 > cat \$1
356 > echo "===="
356 > echo "===="
357 > echo "test saving last-message.txt" >> \$1
357 > echo "test saving last-message.txt" >> \$1
358 > EOF
358 > EOF
359
359
360 $ rm -f .hg/last-message.txt
360 $ rm -f .hg/last-message.txt
361 $ HGEDITOR="sh $TESTTMP/editor.sh" hg commit -S -q
361 $ HGEDITOR="sh $TESTTMP/editor.sh" hg commit -S -q
362 ==== before editing:
362 ==== before editing:
363
363
364
364
365 HG: Enter commit message. Lines beginning with 'HG:' are removed.
365 HG: Enter commit message. Lines beginning with 'HG:' are removed.
366 HG: Leave message empty to abort commit.
366 HG: Leave message empty to abort commit.
367 HG: --
367 HG: --
368 HG: user: test
368 HG: user: test
369 HG: branch 'default'
369 HG: branch 'default'
370 HG: bookmark 'activebookmark'
370 HG: bookmark 'activebookmark'
371 HG: subrepo sub
371 HG: subrepo sub
372 HG: added .hgsub
372 HG: added .hgsub
373 HG: added added
373 HG: added added
374 HG: changed .hgsubstate
374 HG: changed .hgsubstate
375 HG: changed changed
375 HG: changed changed
376 HG: removed removed
376 HG: removed removed
377 ====
377 ====
378 abort: precommit.test-saving-last-message hook exited with status 1 (in subrepo sub)
378 abort: precommit.test-saving-last-message hook exited with status 1 (in subrepo sub)
379 [255]
379 [255]
380 $ cat .hg/last-message.txt
380 $ cat .hg/last-message.txt
381
381
382
382
383 test saving last-message.txt
383 test saving last-message.txt
384
384
385 test that '[committemplate] changeset' definition and commit log
385 test that '[committemplate] changeset' definition and commit log
386 specific template keywords work well
386 specific template keywords work well
387
387
388 $ cat >> .hg/hgrc <<EOF
388 $ cat >> .hg/hgrc <<EOF
389 > [committemplate]
389 > [committemplate]
390 > changeset.commit.normal = HG: this is "commit.normal" template
390 > changeset.commit.normal = HG: this is "commit.normal" template
391 > HG: {extramsg}
391 > HG: {extramsg}
392 > {if(activebookmark,
392 > {if(activebookmark,
393 > "HG: bookmark '{activebookmark}' is activated\n",
393 > "HG: bookmark '{activebookmark}' is activated\n",
394 > "HG: no bookmark is activated\n")}{subrepos %
394 > "HG: no bookmark is activated\n")}{subrepos %
395 > "HG: subrepo '{subrepo}' is changed\n"}
395 > "HG: subrepo '{subrepo}' is changed\n"}
396 >
396 >
397 > changeset.commit = HG: this is "commit" template
397 > changeset.commit = HG: this is "commit" template
398 > HG: {extramsg}
398 > HG: {extramsg}
399 > {if(activebookmark,
399 > {if(activebookmark,
400 > "HG: bookmark '{activebookmark}' is activated\n",
400 > "HG: bookmark '{activebookmark}' is activated\n",
401 > "HG: no bookmark is activated\n")}{subrepos %
401 > "HG: no bookmark is activated\n")}{subrepos %
402 > "HG: subrepo '{subrepo}' is changed\n"}
402 > "HG: subrepo '{subrepo}' is changed\n"}
403 >
403 >
404 > changeset = HG: this is customized commit template
404 > changeset = HG: this is customized commit template
405 > HG: {extramsg}
405 > HG: {extramsg}
406 > {if(activebookmark,
406 > {if(activebookmark,
407 > "HG: bookmark '{activebookmark}' is activated\n",
407 > "HG: bookmark '{activebookmark}' is activated\n",
408 > "HG: no bookmark is activated\n")}{subrepos %
408 > "HG: no bookmark is activated\n")}{subrepos %
409 > "HG: subrepo '{subrepo}' is changed\n"}
409 > "HG: subrepo '{subrepo}' is changed\n"}
410 > EOF
410 > EOF
411
411
412 $ hg init sub2
412 $ hg init sub2
413 $ echo a > sub2/a
413 $ echo a > sub2/a
414 $ hg -R sub2 add sub2/a
414 $ hg -R sub2 add sub2/a
415 $ echo 'sub2 = sub2' >> .hgsub
415 $ echo 'sub2 = sub2' >> .hgsub
416
416
417 $ HGEDITOR=cat hg commit -S -q
417 $ HGEDITOR=cat hg commit -S -q
418 HG: this is "commit.normal" template
418 HG: this is "commit.normal" template
419 HG: Leave message empty to abort commit.
419 HG: Leave message empty to abort commit.
420 HG: bookmark 'activebookmark' is activated
420 HG: bookmark 'activebookmark' is activated
421 HG: subrepo 'sub' is changed
421 HG: subrepo 'sub' is changed
422 HG: subrepo 'sub2' is changed
422 HG: subrepo 'sub2' is changed
423 abort: empty commit message
423 abort: empty commit message
424 [255]
424 [255]
425
425
426 $ cat >> .hg/hgrc <<EOF
426 $ cat >> .hg/hgrc <<EOF
427 > [committemplate]
427 > [committemplate]
428 > changeset.commit.normal =
428 > changeset.commit.normal =
429 > # now, "changeset.commit" should be chosen for "hg commit"
429 > # now, "changeset.commit" should be chosen for "hg commit"
430 > EOF
430 > EOF
431
431
432 $ hg bookmark --inactive activebookmark
432 $ hg bookmark --inactive activebookmark
433 $ hg forget .hgsub
433 $ hg forget .hgsub
434 $ HGEDITOR=cat hg commit -q
434 $ HGEDITOR=cat hg commit -q
435 HG: this is "commit" template
435 HG: this is "commit" template
436 HG: Leave message empty to abort commit.
436 HG: Leave message empty to abort commit.
437 HG: no bookmark is activated
437 HG: no bookmark is activated
438 abort: empty commit message
438 abort: empty commit message
439 [255]
439 [255]
440
440
441 $ cat >> .hg/hgrc <<EOF
441 $ cat >> .hg/hgrc <<EOF
442 > [committemplate]
442 > [committemplate]
443 > changeset.commit =
443 > changeset.commit =
444 > # now, "changeset" should be chosen for "hg commit"
444 > # now, "changeset" should be chosen for "hg commit"
445 > EOF
445 > EOF
446
446
447 $ HGEDITOR=cat hg commit -q
447 $ HGEDITOR=cat hg commit -q
448 HG: this is customized commit template
448 HG: this is customized commit template
449 HG: Leave message empty to abort commit.
449 HG: Leave message empty to abort commit.
450 HG: no bookmark is activated
450 HG: no bookmark is activated
451 abort: empty commit message
451 abort: empty commit message
452 [255]
452 [255]
453
453
454 $ cat >> .hg/hgrc <<EOF
454 $ cat >> .hg/hgrc <<EOF
455 > [committemplate]
455 > [committemplate]
456 > changeset = {desc}
456 > changeset = {desc}
457 > HG: mods={file_mods}
457 > HG: mods={file_mods}
458 > HG: adds={file_adds}
458 > HG: adds={file_adds}
459 > HG: dels={file_dels}
459 > HG: dels={file_dels}
460 > HG: files={files}
460 > HG: files={files}
461 > HG:
461 > HG:
462 > {splitlines(diff()) % 'HG: {line}\n'
462 > {splitlines(diff()) % 'HG: {line}\n'
463 > }HG:
463 > }HG:
464 > HG: mods={file_mods}
464 > HG: mods={file_mods}
465 > HG: adds={file_adds}
465 > HG: adds={file_adds}
466 > HG: dels={file_dels}
466 > HG: dels={file_dels}
467 > HG: files={files}\n
467 > HG: files={files}\n
468 > EOF
468 > EOF
469 $ hg status -amr
469 $ hg status -amr
470 M changed
470 M changed
471 A added
471 A added
472 R removed
472 R removed
473 $ HGEDITOR=cat hg commit -q -e -m "foo bar" changed
473 $ HGEDITOR=cat hg commit -q -e -m "foo bar" changed
474 foo bar
474 foo bar
475 HG: mods=changed
475 HG: mods=changed
476 HG: adds=
476 HG: adds=
477 HG: dels=
477 HG: dels=
478 HG: files=changed
478 HG: files=changed
479 HG:
479 HG:
480 HG: --- a/changed Thu Jan 01 00:00:00 1970 +0000
480 HG: --- a/changed Thu Jan 01 00:00:00 1970 +0000
481 HG: +++ b/changed Thu Jan 01 00:00:00 1970 +0000
481 HG: +++ b/changed Thu Jan 01 00:00:00 1970 +0000
482 HG: @@ -1,1 +1,2 @@
482 HG: @@ -1,1 +1,2 @@
483 HG: changed
483 HG: changed
484 HG: +changed
484 HG: +changed
485 HG:
485 HG:
486 HG: mods=changed
486 HG: mods=changed
487 HG: adds=
487 HG: adds=
488 HG: dels=
488 HG: dels=
489 HG: files=changed
489 HG: files=changed
490 $ hg status -amr
490 $ hg status -amr
491 A added
491 A added
492 R removed
492 R removed
493 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
493 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
494 M changed
494 M changed
495 A
495 A
496 R
496 R
497 $ hg rollback -q
497 $ hg rollback -q
498
498
499 $ cat >> .hg/hgrc <<EOF
499 $ cat >> .hg/hgrc <<EOF
500 > [committemplate]
500 > [committemplate]
501 > changeset = {desc}
501 > changeset = {desc}
502 > HG: mods={file_mods}
502 > HG: mods={file_mods}
503 > HG: adds={file_adds}
503 > HG: adds={file_adds}
504 > HG: dels={file_dels}
504 > HG: dels={file_dels}
505 > HG: files={files}
505 > HG: files={files}
506 > HG:
506 > HG:
507 > {splitlines(diff("changed")) % 'HG: {line}\n'
507 > {splitlines(diff("changed")) % 'HG: {line}\n'
508 > }HG:
508 > }HG:
509 > HG: mods={file_mods}
509 > HG: mods={file_mods}
510 > HG: adds={file_adds}
510 > HG: adds={file_adds}
511 > HG: dels={file_dels}
511 > HG: dels={file_dels}
512 > HG: files={files}
512 > HG: files={files}
513 > HG:
513 > HG:
514 > {splitlines(diff("added")) % 'HG: {line}\n'
514 > {splitlines(diff("added")) % 'HG: {line}\n'
515 > }HG:
515 > }HG:
516 > HG: mods={file_mods}
516 > HG: mods={file_mods}
517 > HG: adds={file_adds}
517 > HG: adds={file_adds}
518 > HG: dels={file_dels}
518 > HG: dels={file_dels}
519 > HG: files={files}
519 > HG: files={files}
520 > HG:
520 > HG:
521 > {splitlines(diff("removed")) % 'HG: {line}\n'
521 > {splitlines(diff("removed")) % 'HG: {line}\n'
522 > }HG:
522 > }HG:
523 > HG: mods={file_mods}
523 > HG: mods={file_mods}
524 > HG: adds={file_adds}
524 > HG: adds={file_adds}
525 > HG: dels={file_dels}
525 > HG: dels={file_dels}
526 > HG: files={files}\n
526 > HG: files={files}\n
527 > EOF
527 > EOF
528 $ HGEDITOR=cat hg commit -q -e -m "foo bar" added removed
528 $ HGEDITOR=cat hg commit -q -e -m "foo bar" added removed
529 foo bar
529 foo bar
530 HG: mods=
530 HG: mods=
531 HG: adds=added
531 HG: adds=added
532 HG: dels=removed
532 HG: dels=removed
533 HG: files=added removed
533 HG: files=added removed
534 HG:
534 HG:
535 HG:
535 HG:
536 HG: mods=
536 HG: mods=
537 HG: adds=added
537 HG: adds=added
538 HG: dels=removed
538 HG: dels=removed
539 HG: files=added removed
539 HG: files=added removed
540 HG:
540 HG:
541 HG: --- /dev/null Thu Jan 01 00:00:00 1970 +0000
541 HG: --- /dev/null Thu Jan 01 00:00:00 1970 +0000
542 HG: +++ b/added Thu Jan 01 00:00:00 1970 +0000
542 HG: +++ b/added Thu Jan 01 00:00:00 1970 +0000
543 HG: @@ -0,0 +1,1 @@
543 HG: @@ -0,0 +1,1 @@
544 HG: +added
544 HG: +added
545 HG:
545 HG:
546 HG: mods=
546 HG: mods=
547 HG: adds=added
547 HG: adds=added
548 HG: dels=removed
548 HG: dels=removed
549 HG: files=added removed
549 HG: files=added removed
550 HG:
550 HG:
551 HG: --- a/removed Thu Jan 01 00:00:00 1970 +0000
551 HG: --- a/removed Thu Jan 01 00:00:00 1970 +0000
552 HG: +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
552 HG: +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
553 HG: @@ -1,1 +0,0 @@
553 HG: @@ -1,1 +0,0 @@
554 HG: -removed
554 HG: -removed
555 HG:
555 HG:
556 HG: mods=
556 HG: mods=
557 HG: adds=added
557 HG: adds=added
558 HG: dels=removed
558 HG: dels=removed
559 HG: files=added removed
559 HG: files=added removed
560 $ hg status -amr
560 $ hg status -amr
561 M changed
561 M changed
562 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
562 $ hg parents --template "M {file_mods}\nA {file_adds}\nR {file_dels}\n"
563 M
563 M
564 A added
564 A added
565 R removed
565 R removed
566 $ hg rollback -q
566 $ hg rollback -q
567
567
568 $ cat >> .hg/hgrc <<EOF
568 $ cat >> .hg/hgrc <<EOF
569 > # disable customizing for subsequent tests
569 > # disable customizing for subsequent tests
570 > [committemplate]
570 > [committemplate]
571 > changeset =
571 > changeset =
572 > EOF
572 > EOF
573
573
574 $ cd ..
574 $ cd ..
575
575
576
576
577 commit copy
577 commit copy
578
578
579 $ hg init dir2
579 $ hg init dir2
580 $ cd dir2
580 $ cd dir2
581 $ echo bleh > bar
581 $ echo bleh > bar
582 $ hg add bar
582 $ hg add bar
583 $ hg ci -m 'add bar'
583 $ hg ci -m 'add bar'
584
584
585 $ hg cp bar foo
585 $ hg cp bar foo
586 $ echo >> bar
586 $ echo >> bar
587 $ hg ci -m 'cp bar foo; change bar'
587 $ hg ci -m 'cp bar foo; change bar'
588
588
589 $ hg debugrename foo
589 $ hg debugrename foo
590 foo renamed from bar:26d3ca0dfd18e44d796b564e38dd173c9668d3a9
590 foo renamed from bar:26d3ca0dfd18e44d796b564e38dd173c9668d3a9
591 $ hg debugindex bar
591 $ hg debugindex bar
592 rev offset length ..... linkrev nodeid p1 p2 (re)
592 rev offset length ..... linkrev nodeid p1 p2 (re)
593 0 0 6 ..... 0 26d3ca0dfd18 000000000000 000000000000 (re)
593 0 0 6 ..... 0 26d3ca0dfd18 000000000000 000000000000 (re)
594 1 6 7 ..... 1 d267bddd54f7 26d3ca0dfd18 000000000000 (re)
594 1 6 7 ..... 1 d267bddd54f7 26d3ca0dfd18 000000000000 (re)
595
595
596 Test making empty commits
596 Test making empty commits
597 $ hg commit --config ui.allowemptycommit=True -m "empty commit"
597 $ hg commit --config ui.allowemptycommit=True -m "empty commit"
598 $ hg log -r . -v --stat
598 $ hg log -r . -v --stat
599 changeset: 2:d809f3644287
599 changeset: 2:d809f3644287
600 tag: tip
600 tag: tip
601 user: test
601 user: test
602 date: Thu Jan 01 00:00:00 1970 +0000
602 date: Thu Jan 01 00:00:00 1970 +0000
603 description:
603 description:
604 empty commit
604 empty commit
605
605
606
606
607
607
608 verify pathauditor blocks evil filepaths
608 verify pathauditor blocks evil filepaths
609 $ cat > evil-commit.py <<EOF
609 $ cat > evil-commit.py <<EOF
610 > from mercurial import ui, hg, context, node
610 > from mercurial import ui, hg, context, node
611 > notrc = u".h\u200cg".encode('utf-8') + '/hgrc'
611 > notrc = u".h\u200cg".encode('utf-8') + '/hgrc'
612 > u = ui.ui.load()
612 > u = ui.ui.load()
613 > r = hg.repository(u, '.')
613 > r = hg.repository(u, '.')
614 > def filectxfn(repo, memctx, path):
614 > def filectxfn(repo, memctx, path):
615 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
615 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
616 > c = context.memctx(r, [r['tip'].node(), node.nullid],
616 > c = context.memctx(r, [r['tip'].node(), node.nullid],
617 > 'evil', [notrc], filectxfn, 0)
617 > 'evil', [notrc], filectxfn, 0)
618 > r.commitctx(c)
618 > r.commitctx(c)
619 > EOF
619 > EOF
620 $ $PYTHON evil-commit.py
620 $ $PYTHON evil-commit.py
621 #if windows
621 #if windows
622 $ hg co --clean tip
622 $ hg co --clean tip
623 abort: path contains illegal component: .h\xe2\x80\x8cg\\hgrc (esc)
623 abort: path contains illegal component: .h\xe2\x80\x8cg\\hgrc (esc)
624 [255]
624 [255]
625 #else
625 #else
626 $ hg co --clean tip
626 $ hg co --clean tip
627 abort: path contains illegal component: .h\xe2\x80\x8cg/hgrc (esc)
627 abort: path contains illegal component: .h\xe2\x80\x8cg/hgrc (esc)
628 [255]
628 [255]
629 #endif
629 #endif
630
630
631 $ hg rollback -f
631 $ hg rollback -f
632 repository tip rolled back to revision 2 (undo commit)
632 repository tip rolled back to revision 2 (undo commit)
633 $ cat > evil-commit.py <<EOF
633 $ cat > evil-commit.py <<EOF
634 > from mercurial import ui, hg, context, node
634 > from mercurial import ui, hg, context, node
635 > notrc = "HG~1/hgrc"
635 > notrc = "HG~1/hgrc"
636 > u = ui.ui.load()
636 > u = ui.ui.load()
637 > r = hg.repository(u, '.')
637 > r = hg.repository(u, '.')
638 > def filectxfn(repo, memctx, path):
638 > def filectxfn(repo, memctx, path):
639 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
639 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
640 > c = context.memctx(r, [r['tip'].node(), node.nullid],
640 > c = context.memctx(r, [r['tip'].node(), node.nullid],
641 > 'evil', [notrc], filectxfn, 0)
641 > 'evil', [notrc], filectxfn, 0)
642 > r.commitctx(c)
642 > r.commitctx(c)
643 > EOF
643 > EOF
644 $ $PYTHON evil-commit.py
644 $ $PYTHON evil-commit.py
645 $ hg co --clean tip
645 $ hg co --clean tip
646 abort: path contains illegal component: HG~1/hgrc (glob)
646 abort: path contains illegal component: HG~1/hgrc (glob)
647 [255]
647 [255]
648
648
649 $ hg rollback -f
649 $ hg rollback -f
650 repository tip rolled back to revision 2 (undo commit)
650 repository tip rolled back to revision 2 (undo commit)
651 $ cat > evil-commit.py <<EOF
651 $ cat > evil-commit.py <<EOF
652 > from mercurial import ui, hg, context, node
652 > from mercurial import ui, hg, context, node
653 > notrc = "HG8B6C~2/hgrc"
653 > notrc = "HG8B6C~2/hgrc"
654 > u = ui.ui.load()
654 > u = ui.ui.load()
655 > r = hg.repository(u, '.')
655 > r = hg.repository(u, '.')
656 > def filectxfn(repo, memctx, path):
656 > def filectxfn(repo, memctx, path):
657 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
657 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
658 > c = context.memctx(r, [r['tip'].node(), node.nullid],
658 > c = context.memctx(r, [r['tip'].node(), node.nullid],
659 > 'evil', [notrc], filectxfn, 0)
659 > 'evil', [notrc], filectxfn, 0)
660 > r.commitctx(c)
660 > r.commitctx(c)
661 > EOF
661 > EOF
662 $ $PYTHON evil-commit.py
662 $ $PYTHON evil-commit.py
663 $ hg co --clean tip
663 $ hg co --clean tip
664 abort: path contains illegal component: HG8B6C~2/hgrc (glob)
664 abort: path contains illegal component: HG8B6C~2/hgrc (glob)
665 [255]
665 [255]
666
666
667 # test that an unmodified commit template message aborts
667 # test that an unmodified commit template message aborts
668
668
669 $ hg init unmodified_commit_template
669 $ hg init unmodified_commit_template
670 $ cd unmodified_commit_template
670 $ cd unmodified_commit_template
671 $ echo foo > foo
671 $ echo foo > foo
672 $ hg add foo
672 $ hg add foo
673 $ hg commit -m "foo"
673 $ hg commit -m "foo"
674 $ cat >> .hg/hgrc <<EOF
674 $ cat >> .hg/hgrc <<EOF
675 > [committemplate]
675 > [committemplate]
676 > changeset.commit = HI THIS IS NOT STRIPPED
676 > changeset.commit = HI THIS IS NOT STRIPPED
677 > HG: this is customized commit template
677 > HG: this is customized commit template
678 > HG: {extramsg}
678 > HG: {extramsg}
679 > {if(activebookmark,
679 > {if(activebookmark,
680 > "HG: bookmark '{activebookmark}' is activated\n",
680 > "HG: bookmark '{activebookmark}' is activated\n",
681 > "HG: no bookmark is activated\n")}{subrepos %
681 > "HG: no bookmark is activated\n")}{subrepos %
682 > "HG: subrepo '{subrepo}' is changed\n"}
682 > "HG: subrepo '{subrepo}' is changed\n"}
683 > EOF
683 > EOF
684 $ cat > $TESTTMP/notouching.sh <<EOF
684 $ cat > $TESTTMP/notouching.sh <<EOF
685 > true
685 > true
686 > EOF
686 > EOF
687 $ echo foo2 > foo2
687 $ echo foo2 > foo2
688 $ hg add foo2
688 $ hg add foo2
689 $ HGEDITOR="sh $TESTTMP/notouching.sh" hg commit
689 $ HGEDITOR="sh $TESTTMP/notouching.sh" hg commit
690 abort: commit message unchanged
690 abort: commit message unchanged
691 [255]
691 [255]
692
692
693 test that text below the --- >8 --- special string is ignored
693 test that text below the --- >8 --- special string is ignored
694
694
695 $ cat <<'EOF' > $TESTTMP/lowercaseline.sh
695 $ cat <<'EOF' > $TESTTMP/lowercaseline.sh
696 > cat $1 | sed s/LINE/line/ | tee $1.new
696 > cat $1 | sed s/LINE/line/ | tee $1.new
697 > mv $1.new $1
697 > mv $1.new $1
698 > EOF
698 > EOF
699
699
700 $ hg init ignore_below_special_string
700 $ hg init ignore_below_special_string
701 $ cd ignore_below_special_string
701 $ cd ignore_below_special_string
702 $ echo foo > foo
702 $ echo foo > foo
703 $ hg add foo
703 $ hg add foo
704 $ hg commit -m "foo"
704 $ hg commit -m "foo"
705 $ cat >> .hg/hgrc <<EOF
705 $ cat >> .hg/hgrc <<EOF
706 > [committemplate]
706 > [committemplate]
707 > changeset.commit = first LINE
707 > changeset.commit = first LINE
708 > HG: this is customized commit template
708 > HG: this is customized commit template
709 > HG: {extramsg}
709 > HG: {extramsg}
710 > HG: ------------------------ >8 ------------------------
710 > HG: ------------------------ >8 ------------------------
711 > {diff()}
711 > {diff()}
712 > EOF
712 > EOF
713 $ echo foo2 > foo2
713 $ echo foo2 > foo2
714 $ hg add foo2
714 $ hg add foo2
715 $ HGEDITOR="sh $TESTTMP/notouching.sh" hg ci
716 abort: commit message unchanged
717 [255]
715 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
718 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
716 first line
719 first line
717 HG: this is customized commit template
720 HG: this is customized commit template
718 HG: Leave message empty to abort commit.
721 HG: Leave message empty to abort commit.
719 HG: ------------------------ >8 ------------------------
722 HG: ------------------------ >8 ------------------------
720 diff -r e63c23eaa88a foo2
723 diff -r e63c23eaa88a foo2
721 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
724 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
722 +++ b/foo2 Thu Jan 01 00:00:00 1970 +0000
725 +++ b/foo2 Thu Jan 01 00:00:00 1970 +0000
723 @@ -0,0 +1,1 @@
726 @@ -0,0 +1,1 @@
724 +foo2
727 +foo2
725 $ hg log -T '{desc}\n' -r .
728 $ hg log -T '{desc}\n' -r .
726 first line
729 first line
727
730
728 test that the special string --- >8 --- isn't used when not at the beginning of
731 test that the special string --- >8 --- isn't used when not at the beginning of
729 a line
732 a line
730
733
731 $ cat >> .hg/hgrc <<EOF
734 $ cat >> .hg/hgrc <<EOF
732 > [committemplate]
735 > [committemplate]
733 > changeset.commit = first LINE2
736 > changeset.commit = first LINE2
734 > another line HG: ------------------------ >8 ------------------------
737 > another line HG: ------------------------ >8 ------------------------
735 > HG: this is customized commit template
738 > HG: this is customized commit template
736 > HG: {extramsg}
739 > HG: {extramsg}
737 > HG: ------------------------ >8 ------------------------
740 > HG: ------------------------ >8 ------------------------
738 > {diff()}
741 > {diff()}
739 > EOF
742 > EOF
740 $ echo foo >> foo
743 $ echo foo >> foo
741 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
744 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
742 first line2
745 first line2
743 another line HG: ------------------------ >8 ------------------------
746 another line HG: ------------------------ >8 ------------------------
744 HG: this is customized commit template
747 HG: this is customized commit template
745 HG: Leave message empty to abort commit.
748 HG: Leave message empty to abort commit.
746 HG: ------------------------ >8 ------------------------
749 HG: ------------------------ >8 ------------------------
747 diff -r 3661b22b0702 foo
750 diff -r 3661b22b0702 foo
748 --- a/foo Thu Jan 01 00:00:00 1970 +0000
751 --- a/foo Thu Jan 01 00:00:00 1970 +0000
749 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
752 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
750 @@ -1,1 +1,2 @@
753 @@ -1,1 +1,2 @@
751 foo
754 foo
752 +foo
755 +foo
753 $ hg log -T '{desc}\n' -r .
756 $ hg log -T '{desc}\n' -r .
754 first line2
757 first line2
755 another line HG: ------------------------ >8 ------------------------
758 another line HG: ------------------------ >8 ------------------------
756
759
757 also test that this special string isn't accepted when there is some extra text
760 also test that this special string isn't accepted when there is some extra text
758 at the end
761 at the end
759
762
760 $ cat >> .hg/hgrc <<EOF
763 $ cat >> .hg/hgrc <<EOF
761 > [committemplate]
764 > [committemplate]
762 > changeset.commit = first LINE3
765 > changeset.commit = first LINE3
763 > HG: ------------------------ >8 ------------------------foobar
766 > HG: ------------------------ >8 ------------------------foobar
764 > second line
767 > second line
765 > HG: this is customized commit template
768 > HG: this is customized commit template
766 > HG: {extramsg}
769 > HG: {extramsg}
767 > HG: ------------------------ >8 ------------------------
770 > HG: ------------------------ >8 ------------------------
768 > {diff()}
771 > {diff()}
769 > EOF
772 > EOF
770 $ echo foo >> foo
773 $ echo foo >> foo
771 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
774 $ HGEDITOR="sh $TESTTMP/lowercaseline.sh" hg ci
772 first line3
775 first line3
773 HG: ------------------------ >8 ------------------------foobar
776 HG: ------------------------ >8 ------------------------foobar
774 second line
777 second line
775 HG: this is customized commit template
778 HG: this is customized commit template
776 HG: Leave message empty to abort commit.
779 HG: Leave message empty to abort commit.
777 HG: ------------------------ >8 ------------------------
780 HG: ------------------------ >8 ------------------------
778 diff -r ce648f5f066f foo
781 diff -r ce648f5f066f foo
779 --- a/foo Thu Jan 01 00:00:00 1970 +0000
782 --- a/foo Thu Jan 01 00:00:00 1970 +0000
780 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
783 +++ b/foo Thu Jan 01 00:00:00 1970 +0000
781 @@ -1,2 +1,3 @@
784 @@ -1,2 +1,3 @@
782 foo
785 foo
783 foo
786 foo
784 +foo
787 +foo
785 $ hg log -T '{desc}\n' -r .
788 $ hg log -T '{desc}\n' -r .
786 first line3
789 first line3
787 second line
790 second line
788
791
789 $ cd ..
792 $ cd ..
790
793
General Comments 0
You need to be logged in to leave comments. Login now