##// END OF EJS Templates
subrepo: drop the 'ui' parameter to forget()...
Matt Harbison -
r23577:597b071a default
parent child Browse files
Show More
@@ -1,2974 +1,2974 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 node import hex, nullid, nullrev, short
8 from node import hex, nullid, nullrev, short
9 from i18n import _
9 from i18n import _
10 import os, sys, errno, re, tempfile
10 import os, sys, errno, re, tempfile
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 import match as matchmod
12 import match as matchmod
13 import context, repair, graphmod, revset, phases, obsolete, pathutil
13 import context, repair, graphmod, revset, phases, obsolete, pathutil
14 import changelog
14 import changelog
15 import bookmarks
15 import bookmarks
16 import encoding
16 import encoding
17 import lock as lockmod
17 import lock as lockmod
18
18
19 def parsealiases(cmd):
19 def parsealiases(cmd):
20 return cmd.lstrip("^").split("|")
20 return cmd.lstrip("^").split("|")
21
21
22 def findpossible(cmd, table, strict=False):
22 def findpossible(cmd, table, strict=False):
23 """
23 """
24 Return cmd -> (aliases, command table entry)
24 Return cmd -> (aliases, command table entry)
25 for each matching command.
25 for each matching command.
26 Return debug commands (or their aliases) only if no normal command matches.
26 Return debug commands (or their aliases) only if no normal command matches.
27 """
27 """
28 choice = {}
28 choice = {}
29 debugchoice = {}
29 debugchoice = {}
30
30
31 if cmd in table:
31 if cmd in table:
32 # short-circuit exact matches, "log" alias beats "^log|history"
32 # short-circuit exact matches, "log" alias beats "^log|history"
33 keys = [cmd]
33 keys = [cmd]
34 else:
34 else:
35 keys = table.keys()
35 keys = table.keys()
36
36
37 for e in keys:
37 for e in keys:
38 aliases = parsealiases(e)
38 aliases = parsealiases(e)
39 found = None
39 found = None
40 if cmd in aliases:
40 if cmd in aliases:
41 found = cmd
41 found = cmd
42 elif not strict:
42 elif not strict:
43 for a in aliases:
43 for a in aliases:
44 if a.startswith(cmd):
44 if a.startswith(cmd):
45 found = a
45 found = a
46 break
46 break
47 if found is not None:
47 if found is not None:
48 if aliases[0].startswith("debug") or found.startswith("debug"):
48 if aliases[0].startswith("debug") or found.startswith("debug"):
49 debugchoice[found] = (aliases, table[e])
49 debugchoice[found] = (aliases, table[e])
50 else:
50 else:
51 choice[found] = (aliases, table[e])
51 choice[found] = (aliases, table[e])
52
52
53 if not choice and debugchoice:
53 if not choice and debugchoice:
54 choice = debugchoice
54 choice = debugchoice
55
55
56 return choice
56 return choice
57
57
58 def findcmd(cmd, table, strict=True):
58 def findcmd(cmd, table, strict=True):
59 """Return (aliases, command table entry) for command string."""
59 """Return (aliases, command table entry) for command string."""
60 choice = findpossible(cmd, table, strict)
60 choice = findpossible(cmd, table, strict)
61
61
62 if cmd in choice:
62 if cmd in choice:
63 return choice[cmd]
63 return choice[cmd]
64
64
65 if len(choice) > 1:
65 if len(choice) > 1:
66 clist = choice.keys()
66 clist = choice.keys()
67 clist.sort()
67 clist.sort()
68 raise error.AmbiguousCommand(cmd, clist)
68 raise error.AmbiguousCommand(cmd, clist)
69
69
70 if choice:
70 if choice:
71 return choice.values()[0]
71 return choice.values()[0]
72
72
73 raise error.UnknownCommand(cmd)
73 raise error.UnknownCommand(cmd)
74
74
75 def findrepo(p):
75 def findrepo(p):
76 while not os.path.isdir(os.path.join(p, ".hg")):
76 while not os.path.isdir(os.path.join(p, ".hg")):
77 oldp, p = p, os.path.dirname(p)
77 oldp, p = p, os.path.dirname(p)
78 if p == oldp:
78 if p == oldp:
79 return None
79 return None
80
80
81 return p
81 return p
82
82
83 def bailifchanged(repo):
83 def bailifchanged(repo):
84 if repo.dirstate.p2() != nullid:
84 if repo.dirstate.p2() != nullid:
85 raise util.Abort(_('outstanding uncommitted merge'))
85 raise util.Abort(_('outstanding uncommitted merge'))
86 modified, added, removed, deleted = repo.status()[:4]
86 modified, added, removed, deleted = repo.status()[:4]
87 if modified or added or removed or deleted:
87 if modified or added or removed or deleted:
88 raise util.Abort(_('uncommitted changes'))
88 raise util.Abort(_('uncommitted changes'))
89 ctx = repo[None]
89 ctx = repo[None]
90 for s in sorted(ctx.substate):
90 for s in sorted(ctx.substate):
91 if ctx.sub(s).dirty():
91 if ctx.sub(s).dirty():
92 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
92 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
93
93
94 def logmessage(ui, opts):
94 def logmessage(ui, opts):
95 """ get the log message according to -m and -l option """
95 """ get the log message according to -m and -l option """
96 message = opts.get('message')
96 message = opts.get('message')
97 logfile = opts.get('logfile')
97 logfile = opts.get('logfile')
98
98
99 if message and logfile:
99 if message and logfile:
100 raise util.Abort(_('options --message and --logfile are mutually '
100 raise util.Abort(_('options --message and --logfile are mutually '
101 'exclusive'))
101 'exclusive'))
102 if not message and logfile:
102 if not message and logfile:
103 try:
103 try:
104 if logfile == '-':
104 if logfile == '-':
105 message = ui.fin.read()
105 message = ui.fin.read()
106 else:
106 else:
107 message = '\n'.join(util.readfile(logfile).splitlines())
107 message = '\n'.join(util.readfile(logfile).splitlines())
108 except IOError, inst:
108 except IOError, inst:
109 raise util.Abort(_("can't read commit message '%s': %s") %
109 raise util.Abort(_("can't read commit message '%s': %s") %
110 (logfile, inst.strerror))
110 (logfile, inst.strerror))
111 return message
111 return message
112
112
113 def mergeeditform(ctxorbool, baseform):
113 def mergeeditform(ctxorbool, baseform):
114 """build appropriate editform from ctxorbool and baseform
114 """build appropriate editform from ctxorbool and baseform
115
115
116 'ctxorbool' is one of a ctx to be committed, or a bool whether
116 'ctxorbool' is one of a ctx to be committed, or a bool whether
117 merging is committed.
117 merging is committed.
118
118
119 This returns editform 'baseform' with '.merge' if merging is
119 This returns editform 'baseform' with '.merge' if merging is
120 committed, or one with '.normal' suffix otherwise.
120 committed, or one with '.normal' suffix otherwise.
121 """
121 """
122 if isinstance(ctxorbool, bool):
122 if isinstance(ctxorbool, bool):
123 if ctxorbool:
123 if ctxorbool:
124 return baseform + ".merge"
124 return baseform + ".merge"
125 elif 1 < len(ctxorbool.parents()):
125 elif 1 < len(ctxorbool.parents()):
126 return baseform + ".merge"
126 return baseform + ".merge"
127
127
128 return baseform + ".normal"
128 return baseform + ".normal"
129
129
130 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
130 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
131 editform='', **opts):
131 editform='', **opts):
132 """get appropriate commit message editor according to '--edit' option
132 """get appropriate commit message editor according to '--edit' option
133
133
134 'finishdesc' is a function to be called with edited commit message
134 'finishdesc' is a function to be called with edited commit message
135 (= 'description' of the new changeset) just after editing, but
135 (= 'description' of the new changeset) just after editing, but
136 before checking empty-ness. It should return actual text to be
136 before checking empty-ness. It should return actual text to be
137 stored into history. This allows to change description before
137 stored into history. This allows to change description before
138 storing.
138 storing.
139
139
140 'extramsg' is a extra message to be shown in the editor instead of
140 'extramsg' is a extra message to be shown in the editor instead of
141 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
141 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
142 is automatically added.
142 is automatically added.
143
143
144 'editform' is a dot-separated list of names, to distinguish
144 'editform' is a dot-separated list of names, to distinguish
145 the purpose of commit text editing.
145 the purpose of commit text editing.
146
146
147 'getcommiteditor' returns 'commitforceeditor' regardless of
147 'getcommiteditor' returns 'commitforceeditor' regardless of
148 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
148 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
149 they are specific for usage in MQ.
149 they are specific for usage in MQ.
150 """
150 """
151 if edit or finishdesc or extramsg:
151 if edit or finishdesc or extramsg:
152 return lambda r, c, s: commitforceeditor(r, c, s,
152 return lambda r, c, s: commitforceeditor(r, c, s,
153 finishdesc=finishdesc,
153 finishdesc=finishdesc,
154 extramsg=extramsg,
154 extramsg=extramsg,
155 editform=editform)
155 editform=editform)
156 elif editform:
156 elif editform:
157 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
157 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
158 else:
158 else:
159 return commiteditor
159 return commiteditor
160
160
161 def loglimit(opts):
161 def loglimit(opts):
162 """get the log limit according to option -l/--limit"""
162 """get the log limit according to option -l/--limit"""
163 limit = opts.get('limit')
163 limit = opts.get('limit')
164 if limit:
164 if limit:
165 try:
165 try:
166 limit = int(limit)
166 limit = int(limit)
167 except ValueError:
167 except ValueError:
168 raise util.Abort(_('limit must be a positive integer'))
168 raise util.Abort(_('limit must be a positive integer'))
169 if limit <= 0:
169 if limit <= 0:
170 raise util.Abort(_('limit must be positive'))
170 raise util.Abort(_('limit must be positive'))
171 else:
171 else:
172 limit = None
172 limit = None
173 return limit
173 return limit
174
174
175 def makefilename(repo, pat, node, desc=None,
175 def makefilename(repo, pat, node, desc=None,
176 total=None, seqno=None, revwidth=None, pathname=None):
176 total=None, seqno=None, revwidth=None, pathname=None):
177 node_expander = {
177 node_expander = {
178 'H': lambda: hex(node),
178 'H': lambda: hex(node),
179 'R': lambda: str(repo.changelog.rev(node)),
179 'R': lambda: str(repo.changelog.rev(node)),
180 'h': lambda: short(node),
180 'h': lambda: short(node),
181 'm': lambda: re.sub('[^\w]', '_', str(desc))
181 'm': lambda: re.sub('[^\w]', '_', str(desc))
182 }
182 }
183 expander = {
183 expander = {
184 '%': lambda: '%',
184 '%': lambda: '%',
185 'b': lambda: os.path.basename(repo.root),
185 'b': lambda: os.path.basename(repo.root),
186 }
186 }
187
187
188 try:
188 try:
189 if node:
189 if node:
190 expander.update(node_expander)
190 expander.update(node_expander)
191 if node:
191 if node:
192 expander['r'] = (lambda:
192 expander['r'] = (lambda:
193 str(repo.changelog.rev(node)).zfill(revwidth or 0))
193 str(repo.changelog.rev(node)).zfill(revwidth or 0))
194 if total is not None:
194 if total is not None:
195 expander['N'] = lambda: str(total)
195 expander['N'] = lambda: str(total)
196 if seqno is not None:
196 if seqno is not None:
197 expander['n'] = lambda: str(seqno)
197 expander['n'] = lambda: str(seqno)
198 if total is not None and seqno is not None:
198 if total is not None and seqno is not None:
199 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
199 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
200 if pathname is not None:
200 if pathname is not None:
201 expander['s'] = lambda: os.path.basename(pathname)
201 expander['s'] = lambda: os.path.basename(pathname)
202 expander['d'] = lambda: os.path.dirname(pathname) or '.'
202 expander['d'] = lambda: os.path.dirname(pathname) or '.'
203 expander['p'] = lambda: pathname
203 expander['p'] = lambda: pathname
204
204
205 newname = []
205 newname = []
206 patlen = len(pat)
206 patlen = len(pat)
207 i = 0
207 i = 0
208 while i < patlen:
208 while i < patlen:
209 c = pat[i]
209 c = pat[i]
210 if c == '%':
210 if c == '%':
211 i += 1
211 i += 1
212 c = pat[i]
212 c = pat[i]
213 c = expander[c]()
213 c = expander[c]()
214 newname.append(c)
214 newname.append(c)
215 i += 1
215 i += 1
216 return ''.join(newname)
216 return ''.join(newname)
217 except KeyError, inst:
217 except KeyError, inst:
218 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
218 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
219 inst.args[0])
219 inst.args[0])
220
220
221 def makefileobj(repo, pat, node=None, desc=None, total=None,
221 def makefileobj(repo, pat, node=None, desc=None, total=None,
222 seqno=None, revwidth=None, mode='wb', modemap=None,
222 seqno=None, revwidth=None, mode='wb', modemap=None,
223 pathname=None):
223 pathname=None):
224
224
225 writable = mode not in ('r', 'rb')
225 writable = mode not in ('r', 'rb')
226
226
227 if not pat or pat == '-':
227 if not pat or pat == '-':
228 fp = writable and repo.ui.fout or repo.ui.fin
228 fp = writable and repo.ui.fout or repo.ui.fin
229 if util.safehasattr(fp, 'fileno'):
229 if util.safehasattr(fp, 'fileno'):
230 return os.fdopen(os.dup(fp.fileno()), mode)
230 return os.fdopen(os.dup(fp.fileno()), mode)
231 else:
231 else:
232 # if this fp can't be duped properly, return
232 # if this fp can't be duped properly, return
233 # a dummy object that can be closed
233 # a dummy object that can be closed
234 class wrappedfileobj(object):
234 class wrappedfileobj(object):
235 noop = lambda x: None
235 noop = lambda x: None
236 def __init__(self, f):
236 def __init__(self, f):
237 self.f = f
237 self.f = f
238 def __getattr__(self, attr):
238 def __getattr__(self, attr):
239 if attr == 'close':
239 if attr == 'close':
240 return self.noop
240 return self.noop
241 else:
241 else:
242 return getattr(self.f, attr)
242 return getattr(self.f, attr)
243
243
244 return wrappedfileobj(fp)
244 return wrappedfileobj(fp)
245 if util.safehasattr(pat, 'write') and writable:
245 if util.safehasattr(pat, 'write') and writable:
246 return pat
246 return pat
247 if util.safehasattr(pat, 'read') and 'r' in mode:
247 if util.safehasattr(pat, 'read') and 'r' in mode:
248 return pat
248 return pat
249 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
249 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
250 if modemap is not None:
250 if modemap is not None:
251 mode = modemap.get(fn, mode)
251 mode = modemap.get(fn, mode)
252 if mode == 'wb':
252 if mode == 'wb':
253 modemap[fn] = 'ab'
253 modemap[fn] = 'ab'
254 return open(fn, mode)
254 return open(fn, mode)
255
255
256 def openrevlog(repo, cmd, file_, opts):
256 def openrevlog(repo, cmd, file_, opts):
257 """opens the changelog, manifest, a filelog or a given revlog"""
257 """opens the changelog, manifest, a filelog or a given revlog"""
258 cl = opts['changelog']
258 cl = opts['changelog']
259 mf = opts['manifest']
259 mf = opts['manifest']
260 msg = None
260 msg = None
261 if cl and mf:
261 if cl and mf:
262 msg = _('cannot specify --changelog and --manifest at the same time')
262 msg = _('cannot specify --changelog and --manifest at the same time')
263 elif cl or mf:
263 elif cl or mf:
264 if file_:
264 if file_:
265 msg = _('cannot specify filename with --changelog or --manifest')
265 msg = _('cannot specify filename with --changelog or --manifest')
266 elif not repo:
266 elif not repo:
267 msg = _('cannot specify --changelog or --manifest '
267 msg = _('cannot specify --changelog or --manifest '
268 'without a repository')
268 'without a repository')
269 if msg:
269 if msg:
270 raise util.Abort(msg)
270 raise util.Abort(msg)
271
271
272 r = None
272 r = None
273 if repo:
273 if repo:
274 if cl:
274 if cl:
275 r = repo.unfiltered().changelog
275 r = repo.unfiltered().changelog
276 elif mf:
276 elif mf:
277 r = repo.manifest
277 r = repo.manifest
278 elif file_:
278 elif file_:
279 filelog = repo.file(file_)
279 filelog = repo.file(file_)
280 if len(filelog):
280 if len(filelog):
281 r = filelog
281 r = filelog
282 if not r:
282 if not r:
283 if not file_:
283 if not file_:
284 raise error.CommandError(cmd, _('invalid arguments'))
284 raise error.CommandError(cmd, _('invalid arguments'))
285 if not os.path.isfile(file_):
285 if not os.path.isfile(file_):
286 raise util.Abort(_("revlog '%s' not found") % file_)
286 raise util.Abort(_("revlog '%s' not found") % file_)
287 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
287 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
288 file_[:-2] + ".i")
288 file_[:-2] + ".i")
289 return r
289 return r
290
290
291 def copy(ui, repo, pats, opts, rename=False):
291 def copy(ui, repo, pats, opts, rename=False):
292 # called with the repo lock held
292 # called with the repo lock held
293 #
293 #
294 # hgsep => pathname that uses "/" to separate directories
294 # hgsep => pathname that uses "/" to separate directories
295 # ossep => pathname that uses os.sep to separate directories
295 # ossep => pathname that uses os.sep to separate directories
296 cwd = repo.getcwd()
296 cwd = repo.getcwd()
297 targets = {}
297 targets = {}
298 after = opts.get("after")
298 after = opts.get("after")
299 dryrun = opts.get("dry_run")
299 dryrun = opts.get("dry_run")
300 wctx = repo[None]
300 wctx = repo[None]
301
301
302 def walkpat(pat):
302 def walkpat(pat):
303 srcs = []
303 srcs = []
304 badstates = after and '?' or '?r'
304 badstates = after and '?' or '?r'
305 m = scmutil.match(repo[None], [pat], opts, globbed=True)
305 m = scmutil.match(repo[None], [pat], opts, globbed=True)
306 for abs in repo.walk(m):
306 for abs in repo.walk(m):
307 state = repo.dirstate[abs]
307 state = repo.dirstate[abs]
308 rel = m.rel(abs)
308 rel = m.rel(abs)
309 exact = m.exact(abs)
309 exact = m.exact(abs)
310 if state in badstates:
310 if state in badstates:
311 if exact and state == '?':
311 if exact and state == '?':
312 ui.warn(_('%s: not copying - file is not managed\n') % rel)
312 ui.warn(_('%s: not copying - file is not managed\n') % rel)
313 if exact and state == 'r':
313 if exact and state == 'r':
314 ui.warn(_('%s: not copying - file has been marked for'
314 ui.warn(_('%s: not copying - file has been marked for'
315 ' remove\n') % rel)
315 ' remove\n') % rel)
316 continue
316 continue
317 # abs: hgsep
317 # abs: hgsep
318 # rel: ossep
318 # rel: ossep
319 srcs.append((abs, rel, exact))
319 srcs.append((abs, rel, exact))
320 return srcs
320 return srcs
321
321
322 # abssrc: hgsep
322 # abssrc: hgsep
323 # relsrc: ossep
323 # relsrc: ossep
324 # otarget: ossep
324 # otarget: ossep
325 def copyfile(abssrc, relsrc, otarget, exact):
325 def copyfile(abssrc, relsrc, otarget, exact):
326 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
326 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
327 if '/' in abstarget:
327 if '/' in abstarget:
328 # We cannot normalize abstarget itself, this would prevent
328 # We cannot normalize abstarget itself, this would prevent
329 # case only renames, like a => A.
329 # case only renames, like a => A.
330 abspath, absname = abstarget.rsplit('/', 1)
330 abspath, absname = abstarget.rsplit('/', 1)
331 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
331 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
332 reltarget = repo.pathto(abstarget, cwd)
332 reltarget = repo.pathto(abstarget, cwd)
333 target = repo.wjoin(abstarget)
333 target = repo.wjoin(abstarget)
334 src = repo.wjoin(abssrc)
334 src = repo.wjoin(abssrc)
335 state = repo.dirstate[abstarget]
335 state = repo.dirstate[abstarget]
336
336
337 scmutil.checkportable(ui, abstarget)
337 scmutil.checkportable(ui, abstarget)
338
338
339 # check for collisions
339 # check for collisions
340 prevsrc = targets.get(abstarget)
340 prevsrc = targets.get(abstarget)
341 if prevsrc is not None:
341 if prevsrc is not None:
342 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
342 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
343 (reltarget, repo.pathto(abssrc, cwd),
343 (reltarget, repo.pathto(abssrc, cwd),
344 repo.pathto(prevsrc, cwd)))
344 repo.pathto(prevsrc, cwd)))
345 return
345 return
346
346
347 # check for overwrites
347 # check for overwrites
348 exists = os.path.lexists(target)
348 exists = os.path.lexists(target)
349 samefile = False
349 samefile = False
350 if exists and abssrc != abstarget:
350 if exists and abssrc != abstarget:
351 if (repo.dirstate.normalize(abssrc) ==
351 if (repo.dirstate.normalize(abssrc) ==
352 repo.dirstate.normalize(abstarget)):
352 repo.dirstate.normalize(abstarget)):
353 if not rename:
353 if not rename:
354 ui.warn(_("%s: can't copy - same file\n") % reltarget)
354 ui.warn(_("%s: can't copy - same file\n") % reltarget)
355 return
355 return
356 exists = False
356 exists = False
357 samefile = True
357 samefile = True
358
358
359 if not after and exists or after and state in 'mn':
359 if not after and exists or after and state in 'mn':
360 if not opts['force']:
360 if not opts['force']:
361 ui.warn(_('%s: not overwriting - file exists\n') %
361 ui.warn(_('%s: not overwriting - file exists\n') %
362 reltarget)
362 reltarget)
363 return
363 return
364
364
365 if after:
365 if after:
366 if not exists:
366 if not exists:
367 if rename:
367 if rename:
368 ui.warn(_('%s: not recording move - %s does not exist\n') %
368 ui.warn(_('%s: not recording move - %s does not exist\n') %
369 (relsrc, reltarget))
369 (relsrc, reltarget))
370 else:
370 else:
371 ui.warn(_('%s: not recording copy - %s does not exist\n') %
371 ui.warn(_('%s: not recording copy - %s does not exist\n') %
372 (relsrc, reltarget))
372 (relsrc, reltarget))
373 return
373 return
374 elif not dryrun:
374 elif not dryrun:
375 try:
375 try:
376 if exists:
376 if exists:
377 os.unlink(target)
377 os.unlink(target)
378 targetdir = os.path.dirname(target) or '.'
378 targetdir = os.path.dirname(target) or '.'
379 if not os.path.isdir(targetdir):
379 if not os.path.isdir(targetdir):
380 os.makedirs(targetdir)
380 os.makedirs(targetdir)
381 if samefile:
381 if samefile:
382 tmp = target + "~hgrename"
382 tmp = target + "~hgrename"
383 os.rename(src, tmp)
383 os.rename(src, tmp)
384 os.rename(tmp, target)
384 os.rename(tmp, target)
385 else:
385 else:
386 util.copyfile(src, target)
386 util.copyfile(src, target)
387 srcexists = True
387 srcexists = True
388 except IOError, inst:
388 except IOError, inst:
389 if inst.errno == errno.ENOENT:
389 if inst.errno == errno.ENOENT:
390 ui.warn(_('%s: deleted in working copy\n') % relsrc)
390 ui.warn(_('%s: deleted in working copy\n') % relsrc)
391 srcexists = False
391 srcexists = False
392 else:
392 else:
393 ui.warn(_('%s: cannot copy - %s\n') %
393 ui.warn(_('%s: cannot copy - %s\n') %
394 (relsrc, inst.strerror))
394 (relsrc, inst.strerror))
395 return True # report a failure
395 return True # report a failure
396
396
397 if ui.verbose or not exact:
397 if ui.verbose or not exact:
398 if rename:
398 if rename:
399 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
399 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
400 else:
400 else:
401 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
401 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
402
402
403 targets[abstarget] = abssrc
403 targets[abstarget] = abssrc
404
404
405 # fix up dirstate
405 # fix up dirstate
406 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
406 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
407 dryrun=dryrun, cwd=cwd)
407 dryrun=dryrun, cwd=cwd)
408 if rename and not dryrun:
408 if rename and not dryrun:
409 if not after and srcexists and not samefile:
409 if not after and srcexists and not samefile:
410 util.unlinkpath(repo.wjoin(abssrc))
410 util.unlinkpath(repo.wjoin(abssrc))
411 wctx.forget([abssrc])
411 wctx.forget([abssrc])
412
412
413 # pat: ossep
413 # pat: ossep
414 # dest ossep
414 # dest ossep
415 # srcs: list of (hgsep, hgsep, ossep, bool)
415 # srcs: list of (hgsep, hgsep, ossep, bool)
416 # return: function that takes hgsep and returns ossep
416 # return: function that takes hgsep and returns ossep
417 def targetpathfn(pat, dest, srcs):
417 def targetpathfn(pat, dest, srcs):
418 if os.path.isdir(pat):
418 if os.path.isdir(pat):
419 abspfx = pathutil.canonpath(repo.root, cwd, pat)
419 abspfx = pathutil.canonpath(repo.root, cwd, pat)
420 abspfx = util.localpath(abspfx)
420 abspfx = util.localpath(abspfx)
421 if destdirexists:
421 if destdirexists:
422 striplen = len(os.path.split(abspfx)[0])
422 striplen = len(os.path.split(abspfx)[0])
423 else:
423 else:
424 striplen = len(abspfx)
424 striplen = len(abspfx)
425 if striplen:
425 if striplen:
426 striplen += len(os.sep)
426 striplen += len(os.sep)
427 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
427 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
428 elif destdirexists:
428 elif destdirexists:
429 res = lambda p: os.path.join(dest,
429 res = lambda p: os.path.join(dest,
430 os.path.basename(util.localpath(p)))
430 os.path.basename(util.localpath(p)))
431 else:
431 else:
432 res = lambda p: dest
432 res = lambda p: dest
433 return res
433 return res
434
434
435 # pat: ossep
435 # pat: ossep
436 # dest ossep
436 # dest ossep
437 # srcs: list of (hgsep, hgsep, ossep, bool)
437 # srcs: list of (hgsep, hgsep, ossep, bool)
438 # return: function that takes hgsep and returns ossep
438 # return: function that takes hgsep and returns ossep
439 def targetpathafterfn(pat, dest, srcs):
439 def targetpathafterfn(pat, dest, srcs):
440 if matchmod.patkind(pat):
440 if matchmod.patkind(pat):
441 # a mercurial pattern
441 # a mercurial pattern
442 res = lambda p: os.path.join(dest,
442 res = lambda p: os.path.join(dest,
443 os.path.basename(util.localpath(p)))
443 os.path.basename(util.localpath(p)))
444 else:
444 else:
445 abspfx = pathutil.canonpath(repo.root, cwd, pat)
445 abspfx = pathutil.canonpath(repo.root, cwd, pat)
446 if len(abspfx) < len(srcs[0][0]):
446 if len(abspfx) < len(srcs[0][0]):
447 # A directory. Either the target path contains the last
447 # A directory. Either the target path contains the last
448 # component of the source path or it does not.
448 # component of the source path or it does not.
449 def evalpath(striplen):
449 def evalpath(striplen):
450 score = 0
450 score = 0
451 for s in srcs:
451 for s in srcs:
452 t = os.path.join(dest, util.localpath(s[0])[striplen:])
452 t = os.path.join(dest, util.localpath(s[0])[striplen:])
453 if os.path.lexists(t):
453 if os.path.lexists(t):
454 score += 1
454 score += 1
455 return score
455 return score
456
456
457 abspfx = util.localpath(abspfx)
457 abspfx = util.localpath(abspfx)
458 striplen = len(abspfx)
458 striplen = len(abspfx)
459 if striplen:
459 if striplen:
460 striplen += len(os.sep)
460 striplen += len(os.sep)
461 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
461 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
462 score = evalpath(striplen)
462 score = evalpath(striplen)
463 striplen1 = len(os.path.split(abspfx)[0])
463 striplen1 = len(os.path.split(abspfx)[0])
464 if striplen1:
464 if striplen1:
465 striplen1 += len(os.sep)
465 striplen1 += len(os.sep)
466 if evalpath(striplen1) > score:
466 if evalpath(striplen1) > score:
467 striplen = striplen1
467 striplen = striplen1
468 res = lambda p: os.path.join(dest,
468 res = lambda p: os.path.join(dest,
469 util.localpath(p)[striplen:])
469 util.localpath(p)[striplen:])
470 else:
470 else:
471 # a file
471 # a file
472 if destdirexists:
472 if destdirexists:
473 res = lambda p: os.path.join(dest,
473 res = lambda p: os.path.join(dest,
474 os.path.basename(util.localpath(p)))
474 os.path.basename(util.localpath(p)))
475 else:
475 else:
476 res = lambda p: dest
476 res = lambda p: dest
477 return res
477 return res
478
478
479
479
480 pats = scmutil.expandpats(pats)
480 pats = scmutil.expandpats(pats)
481 if not pats:
481 if not pats:
482 raise util.Abort(_('no source or destination specified'))
482 raise util.Abort(_('no source or destination specified'))
483 if len(pats) == 1:
483 if len(pats) == 1:
484 raise util.Abort(_('no destination specified'))
484 raise util.Abort(_('no destination specified'))
485 dest = pats.pop()
485 dest = pats.pop()
486 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
486 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
487 if not destdirexists:
487 if not destdirexists:
488 if len(pats) > 1 or matchmod.patkind(pats[0]):
488 if len(pats) > 1 or matchmod.patkind(pats[0]):
489 raise util.Abort(_('with multiple sources, destination must be an '
489 raise util.Abort(_('with multiple sources, destination must be an '
490 'existing directory'))
490 'existing directory'))
491 if util.endswithsep(dest):
491 if util.endswithsep(dest):
492 raise util.Abort(_('destination %s is not a directory') % dest)
492 raise util.Abort(_('destination %s is not a directory') % dest)
493
493
494 tfn = targetpathfn
494 tfn = targetpathfn
495 if after:
495 if after:
496 tfn = targetpathafterfn
496 tfn = targetpathafterfn
497 copylist = []
497 copylist = []
498 for pat in pats:
498 for pat in pats:
499 srcs = walkpat(pat)
499 srcs = walkpat(pat)
500 if not srcs:
500 if not srcs:
501 continue
501 continue
502 copylist.append((tfn(pat, dest, srcs), srcs))
502 copylist.append((tfn(pat, dest, srcs), srcs))
503 if not copylist:
503 if not copylist:
504 raise util.Abort(_('no files to copy'))
504 raise util.Abort(_('no files to copy'))
505
505
506 errors = 0
506 errors = 0
507 for targetpath, srcs in copylist:
507 for targetpath, srcs in copylist:
508 for abssrc, relsrc, exact in srcs:
508 for abssrc, relsrc, exact in srcs:
509 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
509 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
510 errors += 1
510 errors += 1
511
511
512 if errors:
512 if errors:
513 ui.warn(_('(consider using --after)\n'))
513 ui.warn(_('(consider using --after)\n'))
514
514
515 return errors != 0
515 return errors != 0
516
516
517 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
517 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
518 runargs=None, appendpid=False):
518 runargs=None, appendpid=False):
519 '''Run a command as a service.'''
519 '''Run a command as a service.'''
520
520
521 def writepid(pid):
521 def writepid(pid):
522 if opts['pid_file']:
522 if opts['pid_file']:
523 mode = appendpid and 'a' or 'w'
523 mode = appendpid and 'a' or 'w'
524 fp = open(opts['pid_file'], mode)
524 fp = open(opts['pid_file'], mode)
525 fp.write(str(pid) + '\n')
525 fp.write(str(pid) + '\n')
526 fp.close()
526 fp.close()
527
527
528 if opts['daemon'] and not opts['daemon_pipefds']:
528 if opts['daemon'] and not opts['daemon_pipefds']:
529 # Signal child process startup with file removal
529 # Signal child process startup with file removal
530 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
530 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
531 os.close(lockfd)
531 os.close(lockfd)
532 try:
532 try:
533 if not runargs:
533 if not runargs:
534 runargs = util.hgcmd() + sys.argv[1:]
534 runargs = util.hgcmd() + sys.argv[1:]
535 runargs.append('--daemon-pipefds=%s' % lockpath)
535 runargs.append('--daemon-pipefds=%s' % lockpath)
536 # Don't pass --cwd to the child process, because we've already
536 # Don't pass --cwd to the child process, because we've already
537 # changed directory.
537 # changed directory.
538 for i in xrange(1, len(runargs)):
538 for i in xrange(1, len(runargs)):
539 if runargs[i].startswith('--cwd='):
539 if runargs[i].startswith('--cwd='):
540 del runargs[i]
540 del runargs[i]
541 break
541 break
542 elif runargs[i].startswith('--cwd'):
542 elif runargs[i].startswith('--cwd'):
543 del runargs[i:i + 2]
543 del runargs[i:i + 2]
544 break
544 break
545 def condfn():
545 def condfn():
546 return not os.path.exists(lockpath)
546 return not os.path.exists(lockpath)
547 pid = util.rundetached(runargs, condfn)
547 pid = util.rundetached(runargs, condfn)
548 if pid < 0:
548 if pid < 0:
549 raise util.Abort(_('child process failed to start'))
549 raise util.Abort(_('child process failed to start'))
550 writepid(pid)
550 writepid(pid)
551 finally:
551 finally:
552 try:
552 try:
553 os.unlink(lockpath)
553 os.unlink(lockpath)
554 except OSError, e:
554 except OSError, e:
555 if e.errno != errno.ENOENT:
555 if e.errno != errno.ENOENT:
556 raise
556 raise
557 if parentfn:
557 if parentfn:
558 return parentfn(pid)
558 return parentfn(pid)
559 else:
559 else:
560 return
560 return
561
561
562 if initfn:
562 if initfn:
563 initfn()
563 initfn()
564
564
565 if not opts['daemon']:
565 if not opts['daemon']:
566 writepid(os.getpid())
566 writepid(os.getpid())
567
567
568 if opts['daemon_pipefds']:
568 if opts['daemon_pipefds']:
569 lockpath = opts['daemon_pipefds']
569 lockpath = opts['daemon_pipefds']
570 try:
570 try:
571 os.setsid()
571 os.setsid()
572 except AttributeError:
572 except AttributeError:
573 pass
573 pass
574 os.unlink(lockpath)
574 os.unlink(lockpath)
575 util.hidewindow()
575 util.hidewindow()
576 sys.stdout.flush()
576 sys.stdout.flush()
577 sys.stderr.flush()
577 sys.stderr.flush()
578
578
579 nullfd = os.open(os.devnull, os.O_RDWR)
579 nullfd = os.open(os.devnull, os.O_RDWR)
580 logfilefd = nullfd
580 logfilefd = nullfd
581 if logfile:
581 if logfile:
582 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
582 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
583 os.dup2(nullfd, 0)
583 os.dup2(nullfd, 0)
584 os.dup2(logfilefd, 1)
584 os.dup2(logfilefd, 1)
585 os.dup2(logfilefd, 2)
585 os.dup2(logfilefd, 2)
586 if nullfd not in (0, 1, 2):
586 if nullfd not in (0, 1, 2):
587 os.close(nullfd)
587 os.close(nullfd)
588 if logfile and logfilefd not in (0, 1, 2):
588 if logfile and logfilefd not in (0, 1, 2):
589 os.close(logfilefd)
589 os.close(logfilefd)
590
590
591 if runfn:
591 if runfn:
592 return runfn()
592 return runfn()
593
593
594 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
594 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
595 """Utility function used by commands.import to import a single patch
595 """Utility function used by commands.import to import a single patch
596
596
597 This function is explicitly defined here to help the evolve extension to
597 This function is explicitly defined here to help the evolve extension to
598 wrap this part of the import logic.
598 wrap this part of the import logic.
599
599
600 The API is currently a bit ugly because it a simple code translation from
600 The API is currently a bit ugly because it a simple code translation from
601 the import command. Feel free to make it better.
601 the import command. Feel free to make it better.
602
602
603 :hunk: a patch (as a binary string)
603 :hunk: a patch (as a binary string)
604 :parents: nodes that will be parent of the created commit
604 :parents: nodes that will be parent of the created commit
605 :opts: the full dict of option passed to the import command
605 :opts: the full dict of option passed to the import command
606 :msgs: list to save commit message to.
606 :msgs: list to save commit message to.
607 (used in case we need to save it when failing)
607 (used in case we need to save it when failing)
608 :updatefunc: a function that update a repo to a given node
608 :updatefunc: a function that update a repo to a given node
609 updatefunc(<repo>, <node>)
609 updatefunc(<repo>, <node>)
610 """
610 """
611 tmpname, message, user, date, branch, nodeid, p1, p2 = \
611 tmpname, message, user, date, branch, nodeid, p1, p2 = \
612 patch.extract(ui, hunk)
612 patch.extract(ui, hunk)
613
613
614 update = not opts.get('bypass')
614 update = not opts.get('bypass')
615 strip = opts["strip"]
615 strip = opts["strip"]
616 sim = float(opts.get('similarity') or 0)
616 sim = float(opts.get('similarity') or 0)
617 if not tmpname:
617 if not tmpname:
618 return (None, None, False)
618 return (None, None, False)
619 msg = _('applied to working directory')
619 msg = _('applied to working directory')
620
620
621 rejects = False
621 rejects = False
622
622
623 try:
623 try:
624 cmdline_message = logmessage(ui, opts)
624 cmdline_message = logmessage(ui, opts)
625 if cmdline_message:
625 if cmdline_message:
626 # pickup the cmdline msg
626 # pickup the cmdline msg
627 message = cmdline_message
627 message = cmdline_message
628 elif message:
628 elif message:
629 # pickup the patch msg
629 # pickup the patch msg
630 message = message.strip()
630 message = message.strip()
631 else:
631 else:
632 # launch the editor
632 # launch the editor
633 message = None
633 message = None
634 ui.debug('message:\n%s\n' % message)
634 ui.debug('message:\n%s\n' % message)
635
635
636 if len(parents) == 1:
636 if len(parents) == 1:
637 parents.append(repo[nullid])
637 parents.append(repo[nullid])
638 if opts.get('exact'):
638 if opts.get('exact'):
639 if not nodeid or not p1:
639 if not nodeid or not p1:
640 raise util.Abort(_('not a Mercurial patch'))
640 raise util.Abort(_('not a Mercurial patch'))
641 p1 = repo[p1]
641 p1 = repo[p1]
642 p2 = repo[p2 or nullid]
642 p2 = repo[p2 or nullid]
643 elif p2:
643 elif p2:
644 try:
644 try:
645 p1 = repo[p1]
645 p1 = repo[p1]
646 p2 = repo[p2]
646 p2 = repo[p2]
647 # Without any options, consider p2 only if the
647 # Without any options, consider p2 only if the
648 # patch is being applied on top of the recorded
648 # patch is being applied on top of the recorded
649 # first parent.
649 # first parent.
650 if p1 != parents[0]:
650 if p1 != parents[0]:
651 p1 = parents[0]
651 p1 = parents[0]
652 p2 = repo[nullid]
652 p2 = repo[nullid]
653 except error.RepoError:
653 except error.RepoError:
654 p1, p2 = parents
654 p1, p2 = parents
655 if p2.node() == nullid:
655 if p2.node() == nullid:
656 ui.warn(_("warning: import the patch as a normal revision\n"
656 ui.warn(_("warning: import the patch as a normal revision\n"
657 "(use --exact to import the patch as a merge)\n"))
657 "(use --exact to import the patch as a merge)\n"))
658 else:
658 else:
659 p1, p2 = parents
659 p1, p2 = parents
660
660
661 n = None
661 n = None
662 if update:
662 if update:
663 repo.dirstate.beginparentchange()
663 repo.dirstate.beginparentchange()
664 if p1 != parents[0]:
664 if p1 != parents[0]:
665 updatefunc(repo, p1.node())
665 updatefunc(repo, p1.node())
666 if p2 != parents[1]:
666 if p2 != parents[1]:
667 repo.setparents(p1.node(), p2.node())
667 repo.setparents(p1.node(), p2.node())
668
668
669 if opts.get('exact') or opts.get('import_branch'):
669 if opts.get('exact') or opts.get('import_branch'):
670 repo.dirstate.setbranch(branch or 'default')
670 repo.dirstate.setbranch(branch or 'default')
671
671
672 partial = opts.get('partial', False)
672 partial = opts.get('partial', False)
673 files = set()
673 files = set()
674 try:
674 try:
675 patch.patch(ui, repo, tmpname, strip=strip, files=files,
675 patch.patch(ui, repo, tmpname, strip=strip, files=files,
676 eolmode=None, similarity=sim / 100.0)
676 eolmode=None, similarity=sim / 100.0)
677 except patch.PatchError, e:
677 except patch.PatchError, e:
678 if not partial:
678 if not partial:
679 raise util.Abort(str(e))
679 raise util.Abort(str(e))
680 if partial:
680 if partial:
681 rejects = True
681 rejects = True
682
682
683 files = list(files)
683 files = list(files)
684 if opts.get('no_commit'):
684 if opts.get('no_commit'):
685 if message:
685 if message:
686 msgs.append(message)
686 msgs.append(message)
687 else:
687 else:
688 if opts.get('exact') or p2:
688 if opts.get('exact') or p2:
689 # If you got here, you either use --force and know what
689 # If you got here, you either use --force and know what
690 # you are doing or used --exact or a merge patch while
690 # you are doing or used --exact or a merge patch while
691 # being updated to its first parent.
691 # being updated to its first parent.
692 m = None
692 m = None
693 else:
693 else:
694 m = scmutil.matchfiles(repo, files or [])
694 m = scmutil.matchfiles(repo, files or [])
695 editform = mergeeditform(repo[None], 'import.normal')
695 editform = mergeeditform(repo[None], 'import.normal')
696 if opts.get('exact'):
696 if opts.get('exact'):
697 editor = None
697 editor = None
698 else:
698 else:
699 editor = getcommiteditor(editform=editform, **opts)
699 editor = getcommiteditor(editform=editform, **opts)
700 n = repo.commit(message, opts.get('user') or user,
700 n = repo.commit(message, opts.get('user') or user,
701 opts.get('date') or date, match=m,
701 opts.get('date') or date, match=m,
702 editor=editor, force=partial)
702 editor=editor, force=partial)
703 repo.dirstate.endparentchange()
703 repo.dirstate.endparentchange()
704 else:
704 else:
705 if opts.get('exact') or opts.get('import_branch'):
705 if opts.get('exact') or opts.get('import_branch'):
706 branch = branch or 'default'
706 branch = branch or 'default'
707 else:
707 else:
708 branch = p1.branch()
708 branch = p1.branch()
709 store = patch.filestore()
709 store = patch.filestore()
710 try:
710 try:
711 files = set()
711 files = set()
712 try:
712 try:
713 patch.patchrepo(ui, repo, p1, store, tmpname, strip,
713 patch.patchrepo(ui, repo, p1, store, tmpname, strip,
714 files, eolmode=None)
714 files, eolmode=None)
715 except patch.PatchError, e:
715 except patch.PatchError, e:
716 raise util.Abort(str(e))
716 raise util.Abort(str(e))
717 if opts.get('exact'):
717 if opts.get('exact'):
718 editor = None
718 editor = None
719 else:
719 else:
720 editor = getcommiteditor(editform='import.bypass')
720 editor = getcommiteditor(editform='import.bypass')
721 memctx = context.makememctx(repo, (p1.node(), p2.node()),
721 memctx = context.makememctx(repo, (p1.node(), p2.node()),
722 message,
722 message,
723 opts.get('user') or user,
723 opts.get('user') or user,
724 opts.get('date') or date,
724 opts.get('date') or date,
725 branch, files, store,
725 branch, files, store,
726 editor=editor)
726 editor=editor)
727 n = memctx.commit()
727 n = memctx.commit()
728 finally:
728 finally:
729 store.close()
729 store.close()
730 if opts.get('exact') and opts.get('no_commit'):
730 if opts.get('exact') and opts.get('no_commit'):
731 # --exact with --no-commit is still useful in that it does merge
731 # --exact with --no-commit is still useful in that it does merge
732 # and branch bits
732 # and branch bits
733 ui.warn(_("warning: can't check exact import with --no-commit\n"))
733 ui.warn(_("warning: can't check exact import with --no-commit\n"))
734 elif opts.get('exact') and hex(n) != nodeid:
734 elif opts.get('exact') and hex(n) != nodeid:
735 raise util.Abort(_('patch is damaged or loses information'))
735 raise util.Abort(_('patch is damaged or loses information'))
736 if n:
736 if n:
737 # i18n: refers to a short changeset id
737 # i18n: refers to a short changeset id
738 msg = _('created %s') % short(n)
738 msg = _('created %s') % short(n)
739 return (msg, n, rejects)
739 return (msg, n, rejects)
740 finally:
740 finally:
741 os.unlink(tmpname)
741 os.unlink(tmpname)
742
742
743 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
743 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
744 opts=None):
744 opts=None):
745 '''export changesets as hg patches.'''
745 '''export changesets as hg patches.'''
746
746
747 total = len(revs)
747 total = len(revs)
748 revwidth = max([len(str(rev)) for rev in revs])
748 revwidth = max([len(str(rev)) for rev in revs])
749 filemode = {}
749 filemode = {}
750
750
751 def single(rev, seqno, fp):
751 def single(rev, seqno, fp):
752 ctx = repo[rev]
752 ctx = repo[rev]
753 node = ctx.node()
753 node = ctx.node()
754 parents = [p.node() for p in ctx.parents() if p]
754 parents = [p.node() for p in ctx.parents() if p]
755 branch = ctx.branch()
755 branch = ctx.branch()
756 if switch_parent:
756 if switch_parent:
757 parents.reverse()
757 parents.reverse()
758 prev = (parents and parents[0]) or nullid
758 prev = (parents and parents[0]) or nullid
759
759
760 shouldclose = False
760 shouldclose = False
761 if not fp and len(template) > 0:
761 if not fp and len(template) > 0:
762 desc_lines = ctx.description().rstrip().split('\n')
762 desc_lines = ctx.description().rstrip().split('\n')
763 desc = desc_lines[0] #Commit always has a first line.
763 desc = desc_lines[0] #Commit always has a first line.
764 fp = makefileobj(repo, template, node, desc=desc, total=total,
764 fp = makefileobj(repo, template, node, desc=desc, total=total,
765 seqno=seqno, revwidth=revwidth, mode='wb',
765 seqno=seqno, revwidth=revwidth, mode='wb',
766 modemap=filemode)
766 modemap=filemode)
767 if fp != template:
767 if fp != template:
768 shouldclose = True
768 shouldclose = True
769 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
769 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
770 repo.ui.note("%s\n" % fp.name)
770 repo.ui.note("%s\n" % fp.name)
771
771
772 if not fp:
772 if not fp:
773 write = repo.ui.write
773 write = repo.ui.write
774 else:
774 else:
775 def write(s, **kw):
775 def write(s, **kw):
776 fp.write(s)
776 fp.write(s)
777
777
778
778
779 write("# HG changeset patch\n")
779 write("# HG changeset patch\n")
780 write("# User %s\n" % ctx.user())
780 write("# User %s\n" % ctx.user())
781 write("# Date %d %d\n" % ctx.date())
781 write("# Date %d %d\n" % ctx.date())
782 write("# %s\n" % util.datestr(ctx.date()))
782 write("# %s\n" % util.datestr(ctx.date()))
783 if branch and branch != 'default':
783 if branch and branch != 'default':
784 write("# Branch %s\n" % branch)
784 write("# Branch %s\n" % branch)
785 write("# Node ID %s\n" % hex(node))
785 write("# Node ID %s\n" % hex(node))
786 write("# Parent %s\n" % hex(prev))
786 write("# Parent %s\n" % hex(prev))
787 if len(parents) > 1:
787 if len(parents) > 1:
788 write("# Parent %s\n" % hex(parents[1]))
788 write("# Parent %s\n" % hex(parents[1]))
789 write(ctx.description().rstrip())
789 write(ctx.description().rstrip())
790 write("\n\n")
790 write("\n\n")
791
791
792 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
792 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
793 write(chunk, label=label)
793 write(chunk, label=label)
794
794
795 if shouldclose:
795 if shouldclose:
796 fp.close()
796 fp.close()
797
797
798 for seqno, rev in enumerate(revs):
798 for seqno, rev in enumerate(revs):
799 single(rev, seqno + 1, fp)
799 single(rev, seqno + 1, fp)
800
800
801 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
801 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
802 changes=None, stat=False, fp=None, prefix='',
802 changes=None, stat=False, fp=None, prefix='',
803 listsubrepos=False):
803 listsubrepos=False):
804 '''show diff or diffstat.'''
804 '''show diff or diffstat.'''
805 if fp is None:
805 if fp is None:
806 write = ui.write
806 write = ui.write
807 else:
807 else:
808 def write(s, **kw):
808 def write(s, **kw):
809 fp.write(s)
809 fp.write(s)
810
810
811 if stat:
811 if stat:
812 diffopts = diffopts.copy(context=0)
812 diffopts = diffopts.copy(context=0)
813 width = 80
813 width = 80
814 if not ui.plain():
814 if not ui.plain():
815 width = ui.termwidth()
815 width = ui.termwidth()
816 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
816 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
817 prefix=prefix)
817 prefix=prefix)
818 for chunk, label in patch.diffstatui(util.iterlines(chunks),
818 for chunk, label in patch.diffstatui(util.iterlines(chunks),
819 width=width,
819 width=width,
820 git=diffopts.git):
820 git=diffopts.git):
821 write(chunk, label=label)
821 write(chunk, label=label)
822 else:
822 else:
823 for chunk, label in patch.diffui(repo, node1, node2, match,
823 for chunk, label in patch.diffui(repo, node1, node2, match,
824 changes, diffopts, prefix=prefix):
824 changes, diffopts, prefix=prefix):
825 write(chunk, label=label)
825 write(chunk, label=label)
826
826
827 if listsubrepos:
827 if listsubrepos:
828 ctx1 = repo[node1]
828 ctx1 = repo[node1]
829 ctx2 = repo[node2]
829 ctx2 = repo[node2]
830 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
830 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
831 tempnode2 = node2
831 tempnode2 = node2
832 try:
832 try:
833 if node2 is not None:
833 if node2 is not None:
834 tempnode2 = ctx2.substate[subpath][1]
834 tempnode2 = ctx2.substate[subpath][1]
835 except KeyError:
835 except KeyError:
836 # A subrepo that existed in node1 was deleted between node1 and
836 # A subrepo that existed in node1 was deleted between node1 and
837 # node2 (inclusive). Thus, ctx2's substate won't contain that
837 # node2 (inclusive). Thus, ctx2's substate won't contain that
838 # subpath. The best we can do is to ignore it.
838 # subpath. The best we can do is to ignore it.
839 tempnode2 = None
839 tempnode2 = None
840 submatch = matchmod.narrowmatcher(subpath, match)
840 submatch = matchmod.narrowmatcher(subpath, match)
841 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
841 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
842 stat=stat, fp=fp, prefix=prefix)
842 stat=stat, fp=fp, prefix=prefix)
843
843
844 class changeset_printer(object):
844 class changeset_printer(object):
845 '''show changeset information when templating not requested.'''
845 '''show changeset information when templating not requested.'''
846
846
847 def __init__(self, ui, repo, matchfn, diffopts, buffered):
847 def __init__(self, ui, repo, matchfn, diffopts, buffered):
848 self.ui = ui
848 self.ui = ui
849 self.repo = repo
849 self.repo = repo
850 self.buffered = buffered
850 self.buffered = buffered
851 self.matchfn = matchfn
851 self.matchfn = matchfn
852 self.diffopts = diffopts
852 self.diffopts = diffopts
853 self.header = {}
853 self.header = {}
854 self.hunk = {}
854 self.hunk = {}
855 self.lastheader = None
855 self.lastheader = None
856 self.footer = None
856 self.footer = None
857
857
858 def flush(self, rev):
858 def flush(self, rev):
859 if rev in self.header:
859 if rev in self.header:
860 h = self.header[rev]
860 h = self.header[rev]
861 if h != self.lastheader:
861 if h != self.lastheader:
862 self.lastheader = h
862 self.lastheader = h
863 self.ui.write(h)
863 self.ui.write(h)
864 del self.header[rev]
864 del self.header[rev]
865 if rev in self.hunk:
865 if rev in self.hunk:
866 self.ui.write(self.hunk[rev])
866 self.ui.write(self.hunk[rev])
867 del self.hunk[rev]
867 del self.hunk[rev]
868 return 1
868 return 1
869 return 0
869 return 0
870
870
871 def close(self):
871 def close(self):
872 if self.footer:
872 if self.footer:
873 self.ui.write(self.footer)
873 self.ui.write(self.footer)
874
874
875 def show(self, ctx, copies=None, matchfn=None, **props):
875 def show(self, ctx, copies=None, matchfn=None, **props):
876 if self.buffered:
876 if self.buffered:
877 self.ui.pushbuffer()
877 self.ui.pushbuffer()
878 self._show(ctx, copies, matchfn, props)
878 self._show(ctx, copies, matchfn, props)
879 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
879 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
880 else:
880 else:
881 self._show(ctx, copies, matchfn, props)
881 self._show(ctx, copies, matchfn, props)
882
882
883 def _show(self, ctx, copies, matchfn, props):
883 def _show(self, ctx, copies, matchfn, props):
884 '''show a single changeset or file revision'''
884 '''show a single changeset or file revision'''
885 changenode = ctx.node()
885 changenode = ctx.node()
886 rev = ctx.rev()
886 rev = ctx.rev()
887
887
888 if self.ui.quiet:
888 if self.ui.quiet:
889 self.ui.write("%d:%s\n" % (rev, short(changenode)),
889 self.ui.write("%d:%s\n" % (rev, short(changenode)),
890 label='log.node')
890 label='log.node')
891 return
891 return
892
892
893 log = self.repo.changelog
893 log = self.repo.changelog
894 date = util.datestr(ctx.date())
894 date = util.datestr(ctx.date())
895
895
896 hexfunc = self.ui.debugflag and hex or short
896 hexfunc = self.ui.debugflag and hex or short
897
897
898 parents = [(p, hexfunc(log.node(p)))
898 parents = [(p, hexfunc(log.node(p)))
899 for p in self._meaningful_parentrevs(log, rev)]
899 for p in self._meaningful_parentrevs(log, rev)]
900
900
901 # i18n: column positioning for "hg log"
901 # i18n: column positioning for "hg log"
902 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
902 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
903 label='log.changeset changeset.%s' % ctx.phasestr())
903 label='log.changeset changeset.%s' % ctx.phasestr())
904
904
905 branch = ctx.branch()
905 branch = ctx.branch()
906 # don't show the default branch name
906 # don't show the default branch name
907 if branch != 'default':
907 if branch != 'default':
908 # i18n: column positioning for "hg log"
908 # i18n: column positioning for "hg log"
909 self.ui.write(_("branch: %s\n") % branch,
909 self.ui.write(_("branch: %s\n") % branch,
910 label='log.branch')
910 label='log.branch')
911 for bookmark in self.repo.nodebookmarks(changenode):
911 for bookmark in self.repo.nodebookmarks(changenode):
912 # i18n: column positioning for "hg log"
912 # i18n: column positioning for "hg log"
913 self.ui.write(_("bookmark: %s\n") % bookmark,
913 self.ui.write(_("bookmark: %s\n") % bookmark,
914 label='log.bookmark')
914 label='log.bookmark')
915 for tag in self.repo.nodetags(changenode):
915 for tag in self.repo.nodetags(changenode):
916 # i18n: column positioning for "hg log"
916 # i18n: column positioning for "hg log"
917 self.ui.write(_("tag: %s\n") % tag,
917 self.ui.write(_("tag: %s\n") % tag,
918 label='log.tag')
918 label='log.tag')
919 if self.ui.debugflag:
919 if self.ui.debugflag:
920 # i18n: column positioning for "hg log"
920 # i18n: column positioning for "hg log"
921 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
921 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
922 label='log.phase')
922 label='log.phase')
923 for parent in parents:
923 for parent in parents:
924 label = 'log.parent changeset.%s' % self.repo[parent[0]].phasestr()
924 label = 'log.parent changeset.%s' % self.repo[parent[0]].phasestr()
925 # i18n: column positioning for "hg log"
925 # i18n: column positioning for "hg log"
926 self.ui.write(_("parent: %d:%s\n") % parent,
926 self.ui.write(_("parent: %d:%s\n") % parent,
927 label=label)
927 label=label)
928
928
929 if self.ui.debugflag:
929 if self.ui.debugflag:
930 mnode = ctx.manifestnode()
930 mnode = ctx.manifestnode()
931 # i18n: column positioning for "hg log"
931 # i18n: column positioning for "hg log"
932 self.ui.write(_("manifest: %d:%s\n") %
932 self.ui.write(_("manifest: %d:%s\n") %
933 (self.repo.manifest.rev(mnode), hex(mnode)),
933 (self.repo.manifest.rev(mnode), hex(mnode)),
934 label='ui.debug log.manifest')
934 label='ui.debug log.manifest')
935 # i18n: column positioning for "hg log"
935 # i18n: column positioning for "hg log"
936 self.ui.write(_("user: %s\n") % ctx.user(),
936 self.ui.write(_("user: %s\n") % ctx.user(),
937 label='log.user')
937 label='log.user')
938 # i18n: column positioning for "hg log"
938 # i18n: column positioning for "hg log"
939 self.ui.write(_("date: %s\n") % date,
939 self.ui.write(_("date: %s\n") % date,
940 label='log.date')
940 label='log.date')
941
941
942 if self.ui.debugflag:
942 if self.ui.debugflag:
943 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
943 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
944 for key, value in zip([# i18n: column positioning for "hg log"
944 for key, value in zip([# i18n: column positioning for "hg log"
945 _("files:"),
945 _("files:"),
946 # i18n: column positioning for "hg log"
946 # i18n: column positioning for "hg log"
947 _("files+:"),
947 _("files+:"),
948 # i18n: column positioning for "hg log"
948 # i18n: column positioning for "hg log"
949 _("files-:")], files):
949 _("files-:")], files):
950 if value:
950 if value:
951 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
951 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
952 label='ui.debug log.files')
952 label='ui.debug log.files')
953 elif ctx.files() and self.ui.verbose:
953 elif ctx.files() and self.ui.verbose:
954 # i18n: column positioning for "hg log"
954 # i18n: column positioning for "hg log"
955 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
955 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
956 label='ui.note log.files')
956 label='ui.note log.files')
957 if copies and self.ui.verbose:
957 if copies and self.ui.verbose:
958 copies = ['%s (%s)' % c for c in copies]
958 copies = ['%s (%s)' % c for c in copies]
959 # i18n: column positioning for "hg log"
959 # i18n: column positioning for "hg log"
960 self.ui.write(_("copies: %s\n") % ' '.join(copies),
960 self.ui.write(_("copies: %s\n") % ' '.join(copies),
961 label='ui.note log.copies')
961 label='ui.note log.copies')
962
962
963 extra = ctx.extra()
963 extra = ctx.extra()
964 if extra and self.ui.debugflag:
964 if extra and self.ui.debugflag:
965 for key, value in sorted(extra.items()):
965 for key, value in sorted(extra.items()):
966 # i18n: column positioning for "hg log"
966 # i18n: column positioning for "hg log"
967 self.ui.write(_("extra: %s=%s\n")
967 self.ui.write(_("extra: %s=%s\n")
968 % (key, value.encode('string_escape')),
968 % (key, value.encode('string_escape')),
969 label='ui.debug log.extra')
969 label='ui.debug log.extra')
970
970
971 description = ctx.description().strip()
971 description = ctx.description().strip()
972 if description:
972 if description:
973 if self.ui.verbose:
973 if self.ui.verbose:
974 self.ui.write(_("description:\n"),
974 self.ui.write(_("description:\n"),
975 label='ui.note log.description')
975 label='ui.note log.description')
976 self.ui.write(description,
976 self.ui.write(description,
977 label='ui.note log.description')
977 label='ui.note log.description')
978 self.ui.write("\n\n")
978 self.ui.write("\n\n")
979 else:
979 else:
980 # i18n: column positioning for "hg log"
980 # i18n: column positioning for "hg log"
981 self.ui.write(_("summary: %s\n") %
981 self.ui.write(_("summary: %s\n") %
982 description.splitlines()[0],
982 description.splitlines()[0],
983 label='log.summary')
983 label='log.summary')
984 self.ui.write("\n")
984 self.ui.write("\n")
985
985
986 self.showpatch(changenode, matchfn)
986 self.showpatch(changenode, matchfn)
987
987
988 def showpatch(self, node, matchfn):
988 def showpatch(self, node, matchfn):
989 if not matchfn:
989 if not matchfn:
990 matchfn = self.matchfn
990 matchfn = self.matchfn
991 if matchfn:
991 if matchfn:
992 stat = self.diffopts.get('stat')
992 stat = self.diffopts.get('stat')
993 diff = self.diffopts.get('patch')
993 diff = self.diffopts.get('patch')
994 diffopts = patch.diffopts(self.ui, self.diffopts)
994 diffopts = patch.diffopts(self.ui, self.diffopts)
995 prev = self.repo.changelog.parents(node)[0]
995 prev = self.repo.changelog.parents(node)[0]
996 if stat:
996 if stat:
997 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
997 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
998 match=matchfn, stat=True)
998 match=matchfn, stat=True)
999 if diff:
999 if diff:
1000 if stat:
1000 if stat:
1001 self.ui.write("\n")
1001 self.ui.write("\n")
1002 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1002 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1003 match=matchfn, stat=False)
1003 match=matchfn, stat=False)
1004 self.ui.write("\n")
1004 self.ui.write("\n")
1005
1005
1006 def _meaningful_parentrevs(self, log, rev):
1006 def _meaningful_parentrevs(self, log, rev):
1007 """Return list of meaningful (or all if debug) parentrevs for rev.
1007 """Return list of meaningful (or all if debug) parentrevs for rev.
1008
1008
1009 For merges (two non-nullrev revisions) both parents are meaningful.
1009 For merges (two non-nullrev revisions) both parents are meaningful.
1010 Otherwise the first parent revision is considered meaningful if it
1010 Otherwise the first parent revision is considered meaningful if it
1011 is not the preceding revision.
1011 is not the preceding revision.
1012 """
1012 """
1013 parents = log.parentrevs(rev)
1013 parents = log.parentrevs(rev)
1014 if not self.ui.debugflag and parents[1] == nullrev:
1014 if not self.ui.debugflag and parents[1] == nullrev:
1015 if parents[0] >= rev - 1:
1015 if parents[0] >= rev - 1:
1016 parents = []
1016 parents = []
1017 else:
1017 else:
1018 parents = [parents[0]]
1018 parents = [parents[0]]
1019 return parents
1019 return parents
1020
1020
1021 class jsonchangeset(changeset_printer):
1021 class jsonchangeset(changeset_printer):
1022 '''format changeset information.'''
1022 '''format changeset information.'''
1023
1023
1024 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1024 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1025 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1025 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1026 self.cache = {}
1026 self.cache = {}
1027 self._first = True
1027 self._first = True
1028
1028
1029 def close(self):
1029 def close(self):
1030 if not self._first:
1030 if not self._first:
1031 self.ui.write("\n]\n")
1031 self.ui.write("\n]\n")
1032 else:
1032 else:
1033 self.ui.write("[]\n")
1033 self.ui.write("[]\n")
1034
1034
1035 def _show(self, ctx, copies, matchfn, props):
1035 def _show(self, ctx, copies, matchfn, props):
1036 '''show a single changeset or file revision'''
1036 '''show a single changeset or file revision'''
1037 hexnode = hex(ctx.node())
1037 hexnode = hex(ctx.node())
1038 rev = ctx.rev()
1038 rev = ctx.rev()
1039 j = encoding.jsonescape
1039 j = encoding.jsonescape
1040
1040
1041 if self._first:
1041 if self._first:
1042 self.ui.write("[\n {")
1042 self.ui.write("[\n {")
1043 self._first = False
1043 self._first = False
1044 else:
1044 else:
1045 self.ui.write(",\n {")
1045 self.ui.write(",\n {")
1046
1046
1047 if self.ui.quiet:
1047 if self.ui.quiet:
1048 self.ui.write('\n "rev": %d' % rev)
1048 self.ui.write('\n "rev": %d' % rev)
1049 self.ui.write(',\n "node": "%s"' % hexnode)
1049 self.ui.write(',\n "node": "%s"' % hexnode)
1050 self.ui.write('\n }')
1050 self.ui.write('\n }')
1051 return
1051 return
1052
1052
1053 self.ui.write('\n "rev": %d' % rev)
1053 self.ui.write('\n "rev": %d' % rev)
1054 self.ui.write(',\n "node": "%s"' % hexnode)
1054 self.ui.write(',\n "node": "%s"' % hexnode)
1055 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1055 self.ui.write(',\n "branch": "%s"' % j(ctx.branch()))
1056 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1056 self.ui.write(',\n "phase": "%s"' % ctx.phasestr())
1057 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1057 self.ui.write(',\n "user": "%s"' % j(ctx.user()))
1058 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1058 self.ui.write(',\n "date": [%d, %d]' % ctx.date())
1059 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1059 self.ui.write(',\n "desc": "%s"' % j(ctx.description()))
1060
1060
1061 self.ui.write(',\n "bookmarks": [%s]' %
1061 self.ui.write(',\n "bookmarks": [%s]' %
1062 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1062 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1063 self.ui.write(',\n "tags": [%s]' %
1063 self.ui.write(',\n "tags": [%s]' %
1064 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1064 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1065 self.ui.write(',\n "parents": [%s]' %
1065 self.ui.write(',\n "parents": [%s]' %
1066 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1066 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1067
1067
1068 if self.ui.debugflag:
1068 if self.ui.debugflag:
1069 self.ui.write(',\n "manifest": "%s"' % hex(ctx.manifestnode()))
1069 self.ui.write(',\n "manifest": "%s"' % hex(ctx.manifestnode()))
1070
1070
1071 self.ui.write(',\n "extra": {%s}' %
1071 self.ui.write(',\n "extra": {%s}' %
1072 ", ".join('"%s": "%s"' % (j(k), j(v))
1072 ", ".join('"%s": "%s"' % (j(k), j(v))
1073 for k, v in ctx.extra().items()))
1073 for k, v in ctx.extra().items()))
1074
1074
1075 files = ctx.status(ctx.p1())
1075 files = ctx.status(ctx.p1())
1076 self.ui.write(',\n "modified": [%s]' %
1076 self.ui.write(',\n "modified": [%s]' %
1077 ", ".join('"%s"' % j(f) for f in files[0]))
1077 ", ".join('"%s"' % j(f) for f in files[0]))
1078 self.ui.write(',\n "added": [%s]' %
1078 self.ui.write(',\n "added": [%s]' %
1079 ", ".join('"%s"' % j(f) for f in files[1]))
1079 ", ".join('"%s"' % j(f) for f in files[1]))
1080 self.ui.write(',\n "removed": [%s]' %
1080 self.ui.write(',\n "removed": [%s]' %
1081 ", ".join('"%s"' % j(f) for f in files[2]))
1081 ", ".join('"%s"' % j(f) for f in files[2]))
1082
1082
1083 elif self.ui.verbose:
1083 elif self.ui.verbose:
1084 self.ui.write(',\n "files": [%s]' %
1084 self.ui.write(',\n "files": [%s]' %
1085 ", ".join('"%s"' % j(f) for f in ctx.files()))
1085 ", ".join('"%s"' % j(f) for f in ctx.files()))
1086
1086
1087 if copies:
1087 if copies:
1088 self.ui.write(',\n "copies": {%s}' %
1088 self.ui.write(',\n "copies": {%s}' %
1089 ", ".join('"%s": %s' % (j(k), j(copies[k]))
1089 ", ".join('"%s": %s' % (j(k), j(copies[k]))
1090 for k in copies))
1090 for k in copies))
1091
1091
1092 matchfn = self.matchfn
1092 matchfn = self.matchfn
1093 if matchfn:
1093 if matchfn:
1094 stat = self.diffopts.get('stat')
1094 stat = self.diffopts.get('stat')
1095 diff = self.diffopts.get('patch')
1095 diff = self.diffopts.get('patch')
1096 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1096 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1097 node, prev = ctx.node(), ctx.p1().node()
1097 node, prev = ctx.node(), ctx.p1().node()
1098 if stat:
1098 if stat:
1099 self.ui.pushbuffer()
1099 self.ui.pushbuffer()
1100 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1100 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1101 match=matchfn, stat=True)
1101 match=matchfn, stat=True)
1102 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1102 self.ui.write(',\n "diffstat": "%s"' % j(self.ui.popbuffer()))
1103 if diff:
1103 if diff:
1104 self.ui.pushbuffer()
1104 self.ui.pushbuffer()
1105 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1105 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1106 match=matchfn, stat=False)
1106 match=matchfn, stat=False)
1107 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1107 self.ui.write(',\n "diff": "%s"' % j(self.ui.popbuffer()))
1108
1108
1109 self.ui.write("\n }")
1109 self.ui.write("\n }")
1110
1110
1111 class changeset_templater(changeset_printer):
1111 class changeset_templater(changeset_printer):
1112 '''format changeset information.'''
1112 '''format changeset information.'''
1113
1113
1114 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1114 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1115 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1115 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1116 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1116 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1117 defaulttempl = {
1117 defaulttempl = {
1118 'parent': '{rev}:{node|formatnode} ',
1118 'parent': '{rev}:{node|formatnode} ',
1119 'manifest': '{rev}:{node|formatnode}',
1119 'manifest': '{rev}:{node|formatnode}',
1120 'file_copy': '{name} ({source})',
1120 'file_copy': '{name} ({source})',
1121 'extra': '{key}={value|stringescape}'
1121 'extra': '{key}={value|stringescape}'
1122 }
1122 }
1123 # filecopy is preserved for compatibility reasons
1123 # filecopy is preserved for compatibility reasons
1124 defaulttempl['filecopy'] = defaulttempl['file_copy']
1124 defaulttempl['filecopy'] = defaulttempl['file_copy']
1125 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1125 self.t = templater.templater(mapfile, {'formatnode': formatnode},
1126 cache=defaulttempl)
1126 cache=defaulttempl)
1127 if tmpl:
1127 if tmpl:
1128 self.t.cache['changeset'] = tmpl
1128 self.t.cache['changeset'] = tmpl
1129
1129
1130 self.cache = {}
1130 self.cache = {}
1131
1131
1132 def _meaningful_parentrevs(self, ctx):
1132 def _meaningful_parentrevs(self, ctx):
1133 """Return list of meaningful (or all if debug) parentrevs for rev.
1133 """Return list of meaningful (or all if debug) parentrevs for rev.
1134 """
1134 """
1135 parents = ctx.parents()
1135 parents = ctx.parents()
1136 if len(parents) > 1:
1136 if len(parents) > 1:
1137 return parents
1137 return parents
1138 if self.ui.debugflag:
1138 if self.ui.debugflag:
1139 return [parents[0], self.repo['null']]
1139 return [parents[0], self.repo['null']]
1140 if parents[0].rev() >= ctx.rev() - 1:
1140 if parents[0].rev() >= ctx.rev() - 1:
1141 return []
1141 return []
1142 return parents
1142 return parents
1143
1143
1144 def _show(self, ctx, copies, matchfn, props):
1144 def _show(self, ctx, copies, matchfn, props):
1145 '''show a single changeset or file revision'''
1145 '''show a single changeset or file revision'''
1146
1146
1147 showlist = templatekw.showlist
1147 showlist = templatekw.showlist
1148
1148
1149 # showparents() behaviour depends on ui trace level which
1149 # showparents() behaviour depends on ui trace level which
1150 # causes unexpected behaviours at templating level and makes
1150 # causes unexpected behaviours at templating level and makes
1151 # it harder to extract it in a standalone function. Its
1151 # it harder to extract it in a standalone function. Its
1152 # behaviour cannot be changed so leave it here for now.
1152 # behaviour cannot be changed so leave it here for now.
1153 def showparents(**args):
1153 def showparents(**args):
1154 ctx = args['ctx']
1154 ctx = args['ctx']
1155 parents = [[('rev', p.rev()),
1155 parents = [[('rev', p.rev()),
1156 ('node', p.hex()),
1156 ('node', p.hex()),
1157 ('phase', p.phasestr())]
1157 ('phase', p.phasestr())]
1158 for p in self._meaningful_parentrevs(ctx)]
1158 for p in self._meaningful_parentrevs(ctx)]
1159 return showlist('parent', parents, **args)
1159 return showlist('parent', parents, **args)
1160
1160
1161 props = props.copy()
1161 props = props.copy()
1162 props.update(templatekw.keywords)
1162 props.update(templatekw.keywords)
1163 props['parents'] = showparents
1163 props['parents'] = showparents
1164 props['templ'] = self.t
1164 props['templ'] = self.t
1165 props['ctx'] = ctx
1165 props['ctx'] = ctx
1166 props['repo'] = self.repo
1166 props['repo'] = self.repo
1167 props['revcache'] = {'copies': copies}
1167 props['revcache'] = {'copies': copies}
1168 props['cache'] = self.cache
1168 props['cache'] = self.cache
1169
1169
1170 # find correct templates for current mode
1170 # find correct templates for current mode
1171
1171
1172 tmplmodes = [
1172 tmplmodes = [
1173 (True, None),
1173 (True, None),
1174 (self.ui.verbose, 'verbose'),
1174 (self.ui.verbose, 'verbose'),
1175 (self.ui.quiet, 'quiet'),
1175 (self.ui.quiet, 'quiet'),
1176 (self.ui.debugflag, 'debug'),
1176 (self.ui.debugflag, 'debug'),
1177 ]
1177 ]
1178
1178
1179 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1179 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1180 for mode, postfix in tmplmodes:
1180 for mode, postfix in tmplmodes:
1181 for type in types:
1181 for type in types:
1182 cur = postfix and ('%s_%s' % (type, postfix)) or type
1182 cur = postfix and ('%s_%s' % (type, postfix)) or type
1183 if mode and cur in self.t:
1183 if mode and cur in self.t:
1184 types[type] = cur
1184 types[type] = cur
1185
1185
1186 try:
1186 try:
1187
1187
1188 # write header
1188 # write header
1189 if types['header']:
1189 if types['header']:
1190 h = templater.stringify(self.t(types['header'], **props))
1190 h = templater.stringify(self.t(types['header'], **props))
1191 if self.buffered:
1191 if self.buffered:
1192 self.header[ctx.rev()] = h
1192 self.header[ctx.rev()] = h
1193 else:
1193 else:
1194 if self.lastheader != h:
1194 if self.lastheader != h:
1195 self.lastheader = h
1195 self.lastheader = h
1196 self.ui.write(h)
1196 self.ui.write(h)
1197
1197
1198 # write changeset metadata, then patch if requested
1198 # write changeset metadata, then patch if requested
1199 key = types['changeset']
1199 key = types['changeset']
1200 self.ui.write(templater.stringify(self.t(key, **props)))
1200 self.ui.write(templater.stringify(self.t(key, **props)))
1201 self.showpatch(ctx.node(), matchfn)
1201 self.showpatch(ctx.node(), matchfn)
1202
1202
1203 if types['footer']:
1203 if types['footer']:
1204 if not self.footer:
1204 if not self.footer:
1205 self.footer = templater.stringify(self.t(types['footer'],
1205 self.footer = templater.stringify(self.t(types['footer'],
1206 **props))
1206 **props))
1207
1207
1208 except KeyError, inst:
1208 except KeyError, inst:
1209 msg = _("%s: no key named '%s'")
1209 msg = _("%s: no key named '%s'")
1210 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1210 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1211 except SyntaxError, inst:
1211 except SyntaxError, inst:
1212 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1212 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1213
1213
1214 def gettemplate(ui, tmpl, style):
1214 def gettemplate(ui, tmpl, style):
1215 """
1215 """
1216 Find the template matching the given template spec or style.
1216 Find the template matching the given template spec or style.
1217 """
1217 """
1218
1218
1219 # ui settings
1219 # ui settings
1220 if not tmpl and not style: # template are stronger than style
1220 if not tmpl and not style: # template are stronger than style
1221 tmpl = ui.config('ui', 'logtemplate')
1221 tmpl = ui.config('ui', 'logtemplate')
1222 if tmpl:
1222 if tmpl:
1223 try:
1223 try:
1224 tmpl = templater.parsestring(tmpl)
1224 tmpl = templater.parsestring(tmpl)
1225 except SyntaxError:
1225 except SyntaxError:
1226 tmpl = templater.parsestring(tmpl, quoted=False)
1226 tmpl = templater.parsestring(tmpl, quoted=False)
1227 return tmpl, None
1227 return tmpl, None
1228 else:
1228 else:
1229 style = util.expandpath(ui.config('ui', 'style', ''))
1229 style = util.expandpath(ui.config('ui', 'style', ''))
1230
1230
1231 if not tmpl and style:
1231 if not tmpl and style:
1232 mapfile = style
1232 mapfile = style
1233 if not os.path.split(mapfile)[0]:
1233 if not os.path.split(mapfile)[0]:
1234 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1234 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1235 or templater.templatepath(mapfile))
1235 or templater.templatepath(mapfile))
1236 if mapname:
1236 if mapname:
1237 mapfile = mapname
1237 mapfile = mapname
1238 return None, mapfile
1238 return None, mapfile
1239
1239
1240 if not tmpl:
1240 if not tmpl:
1241 return None, None
1241 return None, None
1242
1242
1243 # looks like a literal template?
1243 # looks like a literal template?
1244 if '{' in tmpl:
1244 if '{' in tmpl:
1245 return tmpl, None
1245 return tmpl, None
1246
1246
1247 # perhaps a stock style?
1247 # perhaps a stock style?
1248 if not os.path.split(tmpl)[0]:
1248 if not os.path.split(tmpl)[0]:
1249 mapname = (templater.templatepath('map-cmdline.' + tmpl)
1249 mapname = (templater.templatepath('map-cmdline.' + tmpl)
1250 or templater.templatepath(tmpl))
1250 or templater.templatepath(tmpl))
1251 if mapname and os.path.isfile(mapname):
1251 if mapname and os.path.isfile(mapname):
1252 return None, mapname
1252 return None, mapname
1253
1253
1254 # perhaps it's a reference to [templates]
1254 # perhaps it's a reference to [templates]
1255 t = ui.config('templates', tmpl)
1255 t = ui.config('templates', tmpl)
1256 if t:
1256 if t:
1257 try:
1257 try:
1258 tmpl = templater.parsestring(t)
1258 tmpl = templater.parsestring(t)
1259 except SyntaxError:
1259 except SyntaxError:
1260 tmpl = templater.parsestring(t, quoted=False)
1260 tmpl = templater.parsestring(t, quoted=False)
1261 return tmpl, None
1261 return tmpl, None
1262
1262
1263 if tmpl == 'list':
1263 if tmpl == 'list':
1264 ui.write(_("available styles: %s\n") % templater.stylelist())
1264 ui.write(_("available styles: %s\n") % templater.stylelist())
1265 raise util.Abort(_("specify a template"))
1265 raise util.Abort(_("specify a template"))
1266
1266
1267 # perhaps it's a path to a map or a template
1267 # perhaps it's a path to a map or a template
1268 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
1268 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
1269 # is it a mapfile for a style?
1269 # is it a mapfile for a style?
1270 if os.path.basename(tmpl).startswith("map-"):
1270 if os.path.basename(tmpl).startswith("map-"):
1271 return None, os.path.realpath(tmpl)
1271 return None, os.path.realpath(tmpl)
1272 tmpl = open(tmpl).read()
1272 tmpl = open(tmpl).read()
1273 return tmpl, None
1273 return tmpl, None
1274
1274
1275 # constant string?
1275 # constant string?
1276 return tmpl, None
1276 return tmpl, None
1277
1277
1278 def show_changeset(ui, repo, opts, buffered=False):
1278 def show_changeset(ui, repo, opts, buffered=False):
1279 """show one changeset using template or regular display.
1279 """show one changeset using template or regular display.
1280
1280
1281 Display format will be the first non-empty hit of:
1281 Display format will be the first non-empty hit of:
1282 1. option 'template'
1282 1. option 'template'
1283 2. option 'style'
1283 2. option 'style'
1284 3. [ui] setting 'logtemplate'
1284 3. [ui] setting 'logtemplate'
1285 4. [ui] setting 'style'
1285 4. [ui] setting 'style'
1286 If all of these values are either the unset or the empty string,
1286 If all of these values are either the unset or the empty string,
1287 regular display via changeset_printer() is done.
1287 regular display via changeset_printer() is done.
1288 """
1288 """
1289 # options
1289 # options
1290 matchfn = None
1290 matchfn = None
1291 if opts.get('patch') or opts.get('stat'):
1291 if opts.get('patch') or opts.get('stat'):
1292 matchfn = scmutil.matchall(repo)
1292 matchfn = scmutil.matchall(repo)
1293
1293
1294 if opts.get('template') == 'json':
1294 if opts.get('template') == 'json':
1295 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1295 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1296
1296
1297 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1297 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1298
1298
1299 if not tmpl and not mapfile:
1299 if not tmpl and not mapfile:
1300 return changeset_printer(ui, repo, matchfn, opts, buffered)
1300 return changeset_printer(ui, repo, matchfn, opts, buffered)
1301
1301
1302 try:
1302 try:
1303 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1303 t = changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile,
1304 buffered)
1304 buffered)
1305 except SyntaxError, inst:
1305 except SyntaxError, inst:
1306 raise util.Abort(inst.args[0])
1306 raise util.Abort(inst.args[0])
1307 return t
1307 return t
1308
1308
1309 def showmarker(ui, marker):
1309 def showmarker(ui, marker):
1310 """utility function to display obsolescence marker in a readable way
1310 """utility function to display obsolescence marker in a readable way
1311
1311
1312 To be used by debug function."""
1312 To be used by debug function."""
1313 ui.write(hex(marker.precnode()))
1313 ui.write(hex(marker.precnode()))
1314 for repl in marker.succnodes():
1314 for repl in marker.succnodes():
1315 ui.write(' ')
1315 ui.write(' ')
1316 ui.write(hex(repl))
1316 ui.write(hex(repl))
1317 ui.write(' %X ' % marker.flags())
1317 ui.write(' %X ' % marker.flags())
1318 parents = marker.parentnodes()
1318 parents = marker.parentnodes()
1319 if parents is not None:
1319 if parents is not None:
1320 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1320 ui.write('{%s} ' % ', '.join(hex(p) for p in parents))
1321 ui.write('(%s) ' % util.datestr(marker.date()))
1321 ui.write('(%s) ' % util.datestr(marker.date()))
1322 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1322 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1323 sorted(marker.metadata().items())
1323 sorted(marker.metadata().items())
1324 if t[0] != 'date')))
1324 if t[0] != 'date')))
1325 ui.write('\n')
1325 ui.write('\n')
1326
1326
1327 def finddate(ui, repo, date):
1327 def finddate(ui, repo, date):
1328 """Find the tipmost changeset that matches the given date spec"""
1328 """Find the tipmost changeset that matches the given date spec"""
1329
1329
1330 df = util.matchdate(date)
1330 df = util.matchdate(date)
1331 m = scmutil.matchall(repo)
1331 m = scmutil.matchall(repo)
1332 results = {}
1332 results = {}
1333
1333
1334 def prep(ctx, fns):
1334 def prep(ctx, fns):
1335 d = ctx.date()
1335 d = ctx.date()
1336 if df(d[0]):
1336 if df(d[0]):
1337 results[ctx.rev()] = d
1337 results[ctx.rev()] = d
1338
1338
1339 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1339 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1340 rev = ctx.rev()
1340 rev = ctx.rev()
1341 if rev in results:
1341 if rev in results:
1342 ui.status(_("found revision %s from %s\n") %
1342 ui.status(_("found revision %s from %s\n") %
1343 (rev, util.datestr(results[rev])))
1343 (rev, util.datestr(results[rev])))
1344 return str(rev)
1344 return str(rev)
1345
1345
1346 raise util.Abort(_("revision matching date not found"))
1346 raise util.Abort(_("revision matching date not found"))
1347
1347
1348 def increasingwindows(windowsize=8, sizelimit=512):
1348 def increasingwindows(windowsize=8, sizelimit=512):
1349 while True:
1349 while True:
1350 yield windowsize
1350 yield windowsize
1351 if windowsize < sizelimit:
1351 if windowsize < sizelimit:
1352 windowsize *= 2
1352 windowsize *= 2
1353
1353
1354 class FileWalkError(Exception):
1354 class FileWalkError(Exception):
1355 pass
1355 pass
1356
1356
1357 def walkfilerevs(repo, match, follow, revs, fncache):
1357 def walkfilerevs(repo, match, follow, revs, fncache):
1358 '''Walks the file history for the matched files.
1358 '''Walks the file history for the matched files.
1359
1359
1360 Returns the changeset revs that are involved in the file history.
1360 Returns the changeset revs that are involved in the file history.
1361
1361
1362 Throws FileWalkError if the file history can't be walked using
1362 Throws FileWalkError if the file history can't be walked using
1363 filelogs alone.
1363 filelogs alone.
1364 '''
1364 '''
1365 wanted = set()
1365 wanted = set()
1366 copies = []
1366 copies = []
1367 minrev, maxrev = min(revs), max(revs)
1367 minrev, maxrev = min(revs), max(revs)
1368 def filerevgen(filelog, last):
1368 def filerevgen(filelog, last):
1369 """
1369 """
1370 Only files, no patterns. Check the history of each file.
1370 Only files, no patterns. Check the history of each file.
1371
1371
1372 Examines filelog entries within minrev, maxrev linkrev range
1372 Examines filelog entries within minrev, maxrev linkrev range
1373 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1373 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1374 tuples in backwards order
1374 tuples in backwards order
1375 """
1375 """
1376 cl_count = len(repo)
1376 cl_count = len(repo)
1377 revs = []
1377 revs = []
1378 for j in xrange(0, last + 1):
1378 for j in xrange(0, last + 1):
1379 linkrev = filelog.linkrev(j)
1379 linkrev = filelog.linkrev(j)
1380 if linkrev < minrev:
1380 if linkrev < minrev:
1381 continue
1381 continue
1382 # only yield rev for which we have the changelog, it can
1382 # only yield rev for which we have the changelog, it can
1383 # happen while doing "hg log" during a pull or commit
1383 # happen while doing "hg log" during a pull or commit
1384 if linkrev >= cl_count:
1384 if linkrev >= cl_count:
1385 break
1385 break
1386
1386
1387 parentlinkrevs = []
1387 parentlinkrevs = []
1388 for p in filelog.parentrevs(j):
1388 for p in filelog.parentrevs(j):
1389 if p != nullrev:
1389 if p != nullrev:
1390 parentlinkrevs.append(filelog.linkrev(p))
1390 parentlinkrevs.append(filelog.linkrev(p))
1391 n = filelog.node(j)
1391 n = filelog.node(j)
1392 revs.append((linkrev, parentlinkrevs,
1392 revs.append((linkrev, parentlinkrevs,
1393 follow and filelog.renamed(n)))
1393 follow and filelog.renamed(n)))
1394
1394
1395 return reversed(revs)
1395 return reversed(revs)
1396 def iterfiles():
1396 def iterfiles():
1397 pctx = repo['.']
1397 pctx = repo['.']
1398 for filename in match.files():
1398 for filename in match.files():
1399 if follow:
1399 if follow:
1400 if filename not in pctx:
1400 if filename not in pctx:
1401 raise util.Abort(_('cannot follow file not in parent '
1401 raise util.Abort(_('cannot follow file not in parent '
1402 'revision: "%s"') % filename)
1402 'revision: "%s"') % filename)
1403 yield filename, pctx[filename].filenode()
1403 yield filename, pctx[filename].filenode()
1404 else:
1404 else:
1405 yield filename, None
1405 yield filename, None
1406 for filename_node in copies:
1406 for filename_node in copies:
1407 yield filename_node
1407 yield filename_node
1408
1408
1409 for file_, node in iterfiles():
1409 for file_, node in iterfiles():
1410 filelog = repo.file(file_)
1410 filelog = repo.file(file_)
1411 if not len(filelog):
1411 if not len(filelog):
1412 if node is None:
1412 if node is None:
1413 # A zero count may be a directory or deleted file, so
1413 # A zero count may be a directory or deleted file, so
1414 # try to find matching entries on the slow path.
1414 # try to find matching entries on the slow path.
1415 if follow:
1415 if follow:
1416 raise util.Abort(
1416 raise util.Abort(
1417 _('cannot follow nonexistent file: "%s"') % file_)
1417 _('cannot follow nonexistent file: "%s"') % file_)
1418 raise FileWalkError("Cannot walk via filelog")
1418 raise FileWalkError("Cannot walk via filelog")
1419 else:
1419 else:
1420 continue
1420 continue
1421
1421
1422 if node is None:
1422 if node is None:
1423 last = len(filelog) - 1
1423 last = len(filelog) - 1
1424 else:
1424 else:
1425 last = filelog.rev(node)
1425 last = filelog.rev(node)
1426
1426
1427
1427
1428 # keep track of all ancestors of the file
1428 # keep track of all ancestors of the file
1429 ancestors = set([filelog.linkrev(last)])
1429 ancestors = set([filelog.linkrev(last)])
1430
1430
1431 # iterate from latest to oldest revision
1431 # iterate from latest to oldest revision
1432 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1432 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1433 if not follow:
1433 if not follow:
1434 if rev > maxrev:
1434 if rev > maxrev:
1435 continue
1435 continue
1436 else:
1436 else:
1437 # Note that last might not be the first interesting
1437 # Note that last might not be the first interesting
1438 # rev to us:
1438 # rev to us:
1439 # if the file has been changed after maxrev, we'll
1439 # if the file has been changed after maxrev, we'll
1440 # have linkrev(last) > maxrev, and we still need
1440 # have linkrev(last) > maxrev, and we still need
1441 # to explore the file graph
1441 # to explore the file graph
1442 if rev not in ancestors:
1442 if rev not in ancestors:
1443 continue
1443 continue
1444 # XXX insert 1327 fix here
1444 # XXX insert 1327 fix here
1445 if flparentlinkrevs:
1445 if flparentlinkrevs:
1446 ancestors.update(flparentlinkrevs)
1446 ancestors.update(flparentlinkrevs)
1447
1447
1448 fncache.setdefault(rev, []).append(file_)
1448 fncache.setdefault(rev, []).append(file_)
1449 wanted.add(rev)
1449 wanted.add(rev)
1450 if copied:
1450 if copied:
1451 copies.append(copied)
1451 copies.append(copied)
1452
1452
1453 return wanted
1453 return wanted
1454
1454
1455 def walkchangerevs(repo, match, opts, prepare):
1455 def walkchangerevs(repo, match, opts, prepare):
1456 '''Iterate over files and the revs in which they changed.
1456 '''Iterate over files and the revs in which they changed.
1457
1457
1458 Callers most commonly need to iterate backwards over the history
1458 Callers most commonly need to iterate backwards over the history
1459 in which they are interested. Doing so has awful (quadratic-looking)
1459 in which they are interested. Doing so has awful (quadratic-looking)
1460 performance, so we use iterators in a "windowed" way.
1460 performance, so we use iterators in a "windowed" way.
1461
1461
1462 We walk a window of revisions in the desired order. Within the
1462 We walk a window of revisions in the desired order. Within the
1463 window, we first walk forwards to gather data, then in the desired
1463 window, we first walk forwards to gather data, then in the desired
1464 order (usually backwards) to display it.
1464 order (usually backwards) to display it.
1465
1465
1466 This function returns an iterator yielding contexts. Before
1466 This function returns an iterator yielding contexts. Before
1467 yielding each context, the iterator will first call the prepare
1467 yielding each context, the iterator will first call the prepare
1468 function on each context in the window in forward order.'''
1468 function on each context in the window in forward order.'''
1469
1469
1470 follow = opts.get('follow') or opts.get('follow_first')
1470 follow = opts.get('follow') or opts.get('follow_first')
1471
1471
1472 if opts.get('rev'):
1472 if opts.get('rev'):
1473 revs = scmutil.revrange(repo, opts.get('rev'))
1473 revs = scmutil.revrange(repo, opts.get('rev'))
1474 elif follow:
1474 elif follow:
1475 revs = repo.revs('reverse(:.)')
1475 revs = repo.revs('reverse(:.)')
1476 else:
1476 else:
1477 revs = revset.spanset(repo)
1477 revs = revset.spanset(repo)
1478 revs.reverse()
1478 revs.reverse()
1479 if not revs:
1479 if not revs:
1480 return []
1480 return []
1481 wanted = set()
1481 wanted = set()
1482 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1482 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1483 fncache = {}
1483 fncache = {}
1484 change = repo.changectx
1484 change = repo.changectx
1485
1485
1486 # First step is to fill wanted, the set of revisions that we want to yield.
1486 # First step is to fill wanted, the set of revisions that we want to yield.
1487 # When it does not induce extra cost, we also fill fncache for revisions in
1487 # When it does not induce extra cost, we also fill fncache for revisions in
1488 # wanted: a cache of filenames that were changed (ctx.files()) and that
1488 # wanted: a cache of filenames that were changed (ctx.files()) and that
1489 # match the file filtering conditions.
1489 # match the file filtering conditions.
1490
1490
1491 if not slowpath and not match.files():
1491 if not slowpath and not match.files():
1492 # No files, no patterns. Display all revs.
1492 # No files, no patterns. Display all revs.
1493 wanted = revs
1493 wanted = revs
1494
1494
1495 if not slowpath and match.files():
1495 if not slowpath and match.files():
1496 # We only have to read through the filelog to find wanted revisions
1496 # We only have to read through the filelog to find wanted revisions
1497
1497
1498 try:
1498 try:
1499 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1499 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1500 except FileWalkError:
1500 except FileWalkError:
1501 slowpath = True
1501 slowpath = True
1502
1502
1503 # We decided to fall back to the slowpath because at least one
1503 # We decided to fall back to the slowpath because at least one
1504 # of the paths was not a file. Check to see if at least one of them
1504 # of the paths was not a file. Check to see if at least one of them
1505 # existed in history, otherwise simply return
1505 # existed in history, otherwise simply return
1506 for path in match.files():
1506 for path in match.files():
1507 if path == '.' or path in repo.store:
1507 if path == '.' or path in repo.store:
1508 break
1508 break
1509 else:
1509 else:
1510 return []
1510 return []
1511
1511
1512 if slowpath:
1512 if slowpath:
1513 # We have to read the changelog to match filenames against
1513 # We have to read the changelog to match filenames against
1514 # changed files
1514 # changed files
1515
1515
1516 if follow:
1516 if follow:
1517 raise util.Abort(_('can only follow copies/renames for explicit '
1517 raise util.Abort(_('can only follow copies/renames for explicit '
1518 'filenames'))
1518 'filenames'))
1519
1519
1520 # The slow path checks files modified in every changeset.
1520 # The slow path checks files modified in every changeset.
1521 # This is really slow on large repos, so compute the set lazily.
1521 # This is really slow on large repos, so compute the set lazily.
1522 class lazywantedset(object):
1522 class lazywantedset(object):
1523 def __init__(self):
1523 def __init__(self):
1524 self.set = set()
1524 self.set = set()
1525 self.revs = set(revs)
1525 self.revs = set(revs)
1526
1526
1527 # No need to worry about locality here because it will be accessed
1527 # No need to worry about locality here because it will be accessed
1528 # in the same order as the increasing window below.
1528 # in the same order as the increasing window below.
1529 def __contains__(self, value):
1529 def __contains__(self, value):
1530 if value in self.set:
1530 if value in self.set:
1531 return True
1531 return True
1532 elif not value in self.revs:
1532 elif not value in self.revs:
1533 return False
1533 return False
1534 else:
1534 else:
1535 self.revs.discard(value)
1535 self.revs.discard(value)
1536 ctx = change(value)
1536 ctx = change(value)
1537 matches = filter(match, ctx.files())
1537 matches = filter(match, ctx.files())
1538 if matches:
1538 if matches:
1539 fncache[value] = matches
1539 fncache[value] = matches
1540 self.set.add(value)
1540 self.set.add(value)
1541 return True
1541 return True
1542 return False
1542 return False
1543
1543
1544 def discard(self, value):
1544 def discard(self, value):
1545 self.revs.discard(value)
1545 self.revs.discard(value)
1546 self.set.discard(value)
1546 self.set.discard(value)
1547
1547
1548 wanted = lazywantedset()
1548 wanted = lazywantedset()
1549
1549
1550 class followfilter(object):
1550 class followfilter(object):
1551 def __init__(self, onlyfirst=False):
1551 def __init__(self, onlyfirst=False):
1552 self.startrev = nullrev
1552 self.startrev = nullrev
1553 self.roots = set()
1553 self.roots = set()
1554 self.onlyfirst = onlyfirst
1554 self.onlyfirst = onlyfirst
1555
1555
1556 def match(self, rev):
1556 def match(self, rev):
1557 def realparents(rev):
1557 def realparents(rev):
1558 if self.onlyfirst:
1558 if self.onlyfirst:
1559 return repo.changelog.parentrevs(rev)[0:1]
1559 return repo.changelog.parentrevs(rev)[0:1]
1560 else:
1560 else:
1561 return filter(lambda x: x != nullrev,
1561 return filter(lambda x: x != nullrev,
1562 repo.changelog.parentrevs(rev))
1562 repo.changelog.parentrevs(rev))
1563
1563
1564 if self.startrev == nullrev:
1564 if self.startrev == nullrev:
1565 self.startrev = rev
1565 self.startrev = rev
1566 return True
1566 return True
1567
1567
1568 if rev > self.startrev:
1568 if rev > self.startrev:
1569 # forward: all descendants
1569 # forward: all descendants
1570 if not self.roots:
1570 if not self.roots:
1571 self.roots.add(self.startrev)
1571 self.roots.add(self.startrev)
1572 for parent in realparents(rev):
1572 for parent in realparents(rev):
1573 if parent in self.roots:
1573 if parent in self.roots:
1574 self.roots.add(rev)
1574 self.roots.add(rev)
1575 return True
1575 return True
1576 else:
1576 else:
1577 # backwards: all parents
1577 # backwards: all parents
1578 if not self.roots:
1578 if not self.roots:
1579 self.roots.update(realparents(self.startrev))
1579 self.roots.update(realparents(self.startrev))
1580 if rev in self.roots:
1580 if rev in self.roots:
1581 self.roots.remove(rev)
1581 self.roots.remove(rev)
1582 self.roots.update(realparents(rev))
1582 self.roots.update(realparents(rev))
1583 return True
1583 return True
1584
1584
1585 return False
1585 return False
1586
1586
1587 # it might be worthwhile to do this in the iterator if the rev range
1587 # it might be worthwhile to do this in the iterator if the rev range
1588 # is descending and the prune args are all within that range
1588 # is descending and the prune args are all within that range
1589 for rev in opts.get('prune', ()):
1589 for rev in opts.get('prune', ()):
1590 rev = repo[rev].rev()
1590 rev = repo[rev].rev()
1591 ff = followfilter()
1591 ff = followfilter()
1592 stop = min(revs[0], revs[-1])
1592 stop = min(revs[0], revs[-1])
1593 for x in xrange(rev, stop - 1, -1):
1593 for x in xrange(rev, stop - 1, -1):
1594 if ff.match(x):
1594 if ff.match(x):
1595 wanted = wanted - [x]
1595 wanted = wanted - [x]
1596
1596
1597 # Now that wanted is correctly initialized, we can iterate over the
1597 # Now that wanted is correctly initialized, we can iterate over the
1598 # revision range, yielding only revisions in wanted.
1598 # revision range, yielding only revisions in wanted.
1599 def iterate():
1599 def iterate():
1600 if follow and not match.files():
1600 if follow and not match.files():
1601 ff = followfilter(onlyfirst=opts.get('follow_first'))
1601 ff = followfilter(onlyfirst=opts.get('follow_first'))
1602 def want(rev):
1602 def want(rev):
1603 return ff.match(rev) and rev in wanted
1603 return ff.match(rev) and rev in wanted
1604 else:
1604 else:
1605 def want(rev):
1605 def want(rev):
1606 return rev in wanted
1606 return rev in wanted
1607
1607
1608 it = iter(revs)
1608 it = iter(revs)
1609 stopiteration = False
1609 stopiteration = False
1610 for windowsize in increasingwindows():
1610 for windowsize in increasingwindows():
1611 nrevs = []
1611 nrevs = []
1612 for i in xrange(windowsize):
1612 for i in xrange(windowsize):
1613 try:
1613 try:
1614 rev = it.next()
1614 rev = it.next()
1615 if want(rev):
1615 if want(rev):
1616 nrevs.append(rev)
1616 nrevs.append(rev)
1617 except (StopIteration):
1617 except (StopIteration):
1618 stopiteration = True
1618 stopiteration = True
1619 break
1619 break
1620 for rev in sorted(nrevs):
1620 for rev in sorted(nrevs):
1621 fns = fncache.get(rev)
1621 fns = fncache.get(rev)
1622 ctx = change(rev)
1622 ctx = change(rev)
1623 if not fns:
1623 if not fns:
1624 def fns_generator():
1624 def fns_generator():
1625 for f in ctx.files():
1625 for f in ctx.files():
1626 if match(f):
1626 if match(f):
1627 yield f
1627 yield f
1628 fns = fns_generator()
1628 fns = fns_generator()
1629 prepare(ctx, fns)
1629 prepare(ctx, fns)
1630 for rev in nrevs:
1630 for rev in nrevs:
1631 yield change(rev)
1631 yield change(rev)
1632
1632
1633 if stopiteration:
1633 if stopiteration:
1634 break
1634 break
1635
1635
1636 return iterate()
1636 return iterate()
1637
1637
1638 def _makefollowlogfilematcher(repo, files, followfirst):
1638 def _makefollowlogfilematcher(repo, files, followfirst):
1639 # When displaying a revision with --patch --follow FILE, we have
1639 # When displaying a revision with --patch --follow FILE, we have
1640 # to know which file of the revision must be diffed. With
1640 # to know which file of the revision must be diffed. With
1641 # --follow, we want the names of the ancestors of FILE in the
1641 # --follow, we want the names of the ancestors of FILE in the
1642 # revision, stored in "fcache". "fcache" is populated by
1642 # revision, stored in "fcache". "fcache" is populated by
1643 # reproducing the graph traversal already done by --follow revset
1643 # reproducing the graph traversal already done by --follow revset
1644 # and relating linkrevs to file names (which is not "correct" but
1644 # and relating linkrevs to file names (which is not "correct" but
1645 # good enough).
1645 # good enough).
1646 fcache = {}
1646 fcache = {}
1647 fcacheready = [False]
1647 fcacheready = [False]
1648 pctx = repo['.']
1648 pctx = repo['.']
1649
1649
1650 def populate():
1650 def populate():
1651 for fn in files:
1651 for fn in files:
1652 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1652 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1653 for c in i:
1653 for c in i:
1654 fcache.setdefault(c.linkrev(), set()).add(c.path())
1654 fcache.setdefault(c.linkrev(), set()).add(c.path())
1655
1655
1656 def filematcher(rev):
1656 def filematcher(rev):
1657 if not fcacheready[0]:
1657 if not fcacheready[0]:
1658 # Lazy initialization
1658 # Lazy initialization
1659 fcacheready[0] = True
1659 fcacheready[0] = True
1660 populate()
1660 populate()
1661 return scmutil.matchfiles(repo, fcache.get(rev, []))
1661 return scmutil.matchfiles(repo, fcache.get(rev, []))
1662
1662
1663 return filematcher
1663 return filematcher
1664
1664
1665 def _makenofollowlogfilematcher(repo, pats, opts):
1665 def _makenofollowlogfilematcher(repo, pats, opts):
1666 '''hook for extensions to override the filematcher for non-follow cases'''
1666 '''hook for extensions to override the filematcher for non-follow cases'''
1667 return None
1667 return None
1668
1668
1669 def _makelogrevset(repo, pats, opts, revs):
1669 def _makelogrevset(repo, pats, opts, revs):
1670 """Return (expr, filematcher) where expr is a revset string built
1670 """Return (expr, filematcher) where expr is a revset string built
1671 from log options and file patterns or None. If --stat or --patch
1671 from log options and file patterns or None. If --stat or --patch
1672 are not passed filematcher is None. Otherwise it is a callable
1672 are not passed filematcher is None. Otherwise it is a callable
1673 taking a revision number and returning a match objects filtering
1673 taking a revision number and returning a match objects filtering
1674 the files to be detailed when displaying the revision.
1674 the files to be detailed when displaying the revision.
1675 """
1675 """
1676 opt2revset = {
1676 opt2revset = {
1677 'no_merges': ('not merge()', None),
1677 'no_merges': ('not merge()', None),
1678 'only_merges': ('merge()', None),
1678 'only_merges': ('merge()', None),
1679 '_ancestors': ('ancestors(%(val)s)', None),
1679 '_ancestors': ('ancestors(%(val)s)', None),
1680 '_fancestors': ('_firstancestors(%(val)s)', None),
1680 '_fancestors': ('_firstancestors(%(val)s)', None),
1681 '_descendants': ('descendants(%(val)s)', None),
1681 '_descendants': ('descendants(%(val)s)', None),
1682 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1682 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1683 '_matchfiles': ('_matchfiles(%(val)s)', None),
1683 '_matchfiles': ('_matchfiles(%(val)s)', None),
1684 'date': ('date(%(val)r)', None),
1684 'date': ('date(%(val)r)', None),
1685 'branch': ('branch(%(val)r)', ' or '),
1685 'branch': ('branch(%(val)r)', ' or '),
1686 '_patslog': ('filelog(%(val)r)', ' or '),
1686 '_patslog': ('filelog(%(val)r)', ' or '),
1687 '_patsfollow': ('follow(%(val)r)', ' or '),
1687 '_patsfollow': ('follow(%(val)r)', ' or '),
1688 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1688 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1689 'keyword': ('keyword(%(val)r)', ' or '),
1689 'keyword': ('keyword(%(val)r)', ' or '),
1690 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1690 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1691 'user': ('user(%(val)r)', ' or '),
1691 'user': ('user(%(val)r)', ' or '),
1692 }
1692 }
1693
1693
1694 opts = dict(opts)
1694 opts = dict(opts)
1695 # follow or not follow?
1695 # follow or not follow?
1696 follow = opts.get('follow') or opts.get('follow_first')
1696 follow = opts.get('follow') or opts.get('follow_first')
1697 followfirst = opts.get('follow_first') and 1 or 0
1697 followfirst = opts.get('follow_first') and 1 or 0
1698 # --follow with FILE behaviour depends on revs...
1698 # --follow with FILE behaviour depends on revs...
1699 it = iter(revs)
1699 it = iter(revs)
1700 startrev = it.next()
1700 startrev = it.next()
1701 try:
1701 try:
1702 followdescendants = startrev < it.next()
1702 followdescendants = startrev < it.next()
1703 except (StopIteration):
1703 except (StopIteration):
1704 followdescendants = False
1704 followdescendants = False
1705
1705
1706 # branch and only_branch are really aliases and must be handled at
1706 # branch and only_branch are really aliases and must be handled at
1707 # the same time
1707 # the same time
1708 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1708 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1709 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1709 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1710 # pats/include/exclude are passed to match.match() directly in
1710 # pats/include/exclude are passed to match.match() directly in
1711 # _matchfiles() revset but walkchangerevs() builds its matcher with
1711 # _matchfiles() revset but walkchangerevs() builds its matcher with
1712 # scmutil.match(). The difference is input pats are globbed on
1712 # scmutil.match(). The difference is input pats are globbed on
1713 # platforms without shell expansion (windows).
1713 # platforms without shell expansion (windows).
1714 pctx = repo[None]
1714 pctx = repo[None]
1715 match, pats = scmutil.matchandpats(pctx, pats, opts)
1715 match, pats = scmutil.matchandpats(pctx, pats, opts)
1716 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1716 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1717 if not slowpath:
1717 if not slowpath:
1718 for f in match.files():
1718 for f in match.files():
1719 if follow and f not in pctx:
1719 if follow and f not in pctx:
1720 # If the file exists, it may be a directory, so let it
1720 # If the file exists, it may be a directory, so let it
1721 # take the slow path.
1721 # take the slow path.
1722 if os.path.exists(repo.wjoin(f)):
1722 if os.path.exists(repo.wjoin(f)):
1723 slowpath = True
1723 slowpath = True
1724 continue
1724 continue
1725 else:
1725 else:
1726 raise util.Abort(_('cannot follow file not in parent '
1726 raise util.Abort(_('cannot follow file not in parent '
1727 'revision: "%s"') % f)
1727 'revision: "%s"') % f)
1728 filelog = repo.file(f)
1728 filelog = repo.file(f)
1729 if not filelog:
1729 if not filelog:
1730 # A zero count may be a directory or deleted file, so
1730 # A zero count may be a directory or deleted file, so
1731 # try to find matching entries on the slow path.
1731 # try to find matching entries on the slow path.
1732 if follow:
1732 if follow:
1733 raise util.Abort(
1733 raise util.Abort(
1734 _('cannot follow nonexistent file: "%s"') % f)
1734 _('cannot follow nonexistent file: "%s"') % f)
1735 slowpath = True
1735 slowpath = True
1736
1736
1737 # We decided to fall back to the slowpath because at least one
1737 # We decided to fall back to the slowpath because at least one
1738 # of the paths was not a file. Check to see if at least one of them
1738 # of the paths was not a file. Check to see if at least one of them
1739 # existed in history - in that case, we'll continue down the
1739 # existed in history - in that case, we'll continue down the
1740 # slowpath; otherwise, we can turn off the slowpath
1740 # slowpath; otherwise, we can turn off the slowpath
1741 if slowpath:
1741 if slowpath:
1742 for path in match.files():
1742 for path in match.files():
1743 if path == '.' or path in repo.store:
1743 if path == '.' or path in repo.store:
1744 break
1744 break
1745 else:
1745 else:
1746 slowpath = False
1746 slowpath = False
1747
1747
1748 fpats = ('_patsfollow', '_patsfollowfirst')
1748 fpats = ('_patsfollow', '_patsfollowfirst')
1749 fnopats = (('_ancestors', '_fancestors'),
1749 fnopats = (('_ancestors', '_fancestors'),
1750 ('_descendants', '_fdescendants'))
1750 ('_descendants', '_fdescendants'))
1751 if slowpath:
1751 if slowpath:
1752 # See walkchangerevs() slow path.
1752 # See walkchangerevs() slow path.
1753 #
1753 #
1754 # pats/include/exclude cannot be represented as separate
1754 # pats/include/exclude cannot be represented as separate
1755 # revset expressions as their filtering logic applies at file
1755 # revset expressions as their filtering logic applies at file
1756 # level. For instance "-I a -X a" matches a revision touching
1756 # level. For instance "-I a -X a" matches a revision touching
1757 # "a" and "b" while "file(a) and not file(b)" does
1757 # "a" and "b" while "file(a) and not file(b)" does
1758 # not. Besides, filesets are evaluated against the working
1758 # not. Besides, filesets are evaluated against the working
1759 # directory.
1759 # directory.
1760 matchargs = ['r:', 'd:relpath']
1760 matchargs = ['r:', 'd:relpath']
1761 for p in pats:
1761 for p in pats:
1762 matchargs.append('p:' + p)
1762 matchargs.append('p:' + p)
1763 for p in opts.get('include', []):
1763 for p in opts.get('include', []):
1764 matchargs.append('i:' + p)
1764 matchargs.append('i:' + p)
1765 for p in opts.get('exclude', []):
1765 for p in opts.get('exclude', []):
1766 matchargs.append('x:' + p)
1766 matchargs.append('x:' + p)
1767 matchargs = ','.join(('%r' % p) for p in matchargs)
1767 matchargs = ','.join(('%r' % p) for p in matchargs)
1768 opts['_matchfiles'] = matchargs
1768 opts['_matchfiles'] = matchargs
1769 if follow:
1769 if follow:
1770 opts[fnopats[0][followfirst]] = '.'
1770 opts[fnopats[0][followfirst]] = '.'
1771 else:
1771 else:
1772 if follow:
1772 if follow:
1773 if pats:
1773 if pats:
1774 # follow() revset interprets its file argument as a
1774 # follow() revset interprets its file argument as a
1775 # manifest entry, so use match.files(), not pats.
1775 # manifest entry, so use match.files(), not pats.
1776 opts[fpats[followfirst]] = list(match.files())
1776 opts[fpats[followfirst]] = list(match.files())
1777 else:
1777 else:
1778 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1778 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1779 else:
1779 else:
1780 opts['_patslog'] = list(pats)
1780 opts['_patslog'] = list(pats)
1781
1781
1782 filematcher = None
1782 filematcher = None
1783 if opts.get('patch') or opts.get('stat'):
1783 if opts.get('patch') or opts.get('stat'):
1784 # When following files, track renames via a special matcher.
1784 # When following files, track renames via a special matcher.
1785 # If we're forced to take the slowpath it means we're following
1785 # If we're forced to take the slowpath it means we're following
1786 # at least one pattern/directory, so don't bother with rename tracking.
1786 # at least one pattern/directory, so don't bother with rename tracking.
1787 if follow and not match.always() and not slowpath:
1787 if follow and not match.always() and not slowpath:
1788 # _makefollowlogfilematcher expects its files argument to be
1788 # _makefollowlogfilematcher expects its files argument to be
1789 # relative to the repo root, so use match.files(), not pats.
1789 # relative to the repo root, so use match.files(), not pats.
1790 filematcher = _makefollowlogfilematcher(repo, match.files(),
1790 filematcher = _makefollowlogfilematcher(repo, match.files(),
1791 followfirst)
1791 followfirst)
1792 else:
1792 else:
1793 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
1793 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
1794 if filematcher is None:
1794 if filematcher is None:
1795 filematcher = lambda rev: match
1795 filematcher = lambda rev: match
1796
1796
1797 expr = []
1797 expr = []
1798 for op, val in sorted(opts.iteritems()):
1798 for op, val in sorted(opts.iteritems()):
1799 if not val:
1799 if not val:
1800 continue
1800 continue
1801 if op not in opt2revset:
1801 if op not in opt2revset:
1802 continue
1802 continue
1803 revop, andor = opt2revset[op]
1803 revop, andor = opt2revset[op]
1804 if '%(val)' not in revop:
1804 if '%(val)' not in revop:
1805 expr.append(revop)
1805 expr.append(revop)
1806 else:
1806 else:
1807 if not isinstance(val, list):
1807 if not isinstance(val, list):
1808 e = revop % {'val': val}
1808 e = revop % {'val': val}
1809 else:
1809 else:
1810 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1810 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1811 expr.append(e)
1811 expr.append(e)
1812
1812
1813 if expr:
1813 if expr:
1814 expr = '(' + ' and '.join(expr) + ')'
1814 expr = '(' + ' and '.join(expr) + ')'
1815 else:
1815 else:
1816 expr = None
1816 expr = None
1817 return expr, filematcher
1817 return expr, filematcher
1818
1818
1819 def getgraphlogrevs(repo, pats, opts):
1819 def getgraphlogrevs(repo, pats, opts):
1820 """Return (revs, expr, filematcher) where revs is an iterable of
1820 """Return (revs, expr, filematcher) where revs is an iterable of
1821 revision numbers, expr is a revset string built from log options
1821 revision numbers, expr is a revset string built from log options
1822 and file patterns or None, and used to filter 'revs'. If --stat or
1822 and file patterns or None, and used to filter 'revs'. If --stat or
1823 --patch are not passed filematcher is None. Otherwise it is a
1823 --patch are not passed filematcher is None. Otherwise it is a
1824 callable taking a revision number and returning a match objects
1824 callable taking a revision number and returning a match objects
1825 filtering the files to be detailed when displaying the revision.
1825 filtering the files to be detailed when displaying the revision.
1826 """
1826 """
1827 if not len(repo):
1827 if not len(repo):
1828 return [], None, None
1828 return [], None, None
1829 limit = loglimit(opts)
1829 limit = loglimit(opts)
1830 # Default --rev value depends on --follow but --follow behaviour
1830 # Default --rev value depends on --follow but --follow behaviour
1831 # depends on revisions resolved from --rev...
1831 # depends on revisions resolved from --rev...
1832 follow = opts.get('follow') or opts.get('follow_first')
1832 follow = opts.get('follow') or opts.get('follow_first')
1833 possiblyunsorted = False # whether revs might need sorting
1833 possiblyunsorted = False # whether revs might need sorting
1834 if opts.get('rev'):
1834 if opts.get('rev'):
1835 revs = scmutil.revrange(repo, opts['rev'])
1835 revs = scmutil.revrange(repo, opts['rev'])
1836 # Don't sort here because _makelogrevset might depend on the
1836 # Don't sort here because _makelogrevset might depend on the
1837 # order of revs
1837 # order of revs
1838 possiblyunsorted = True
1838 possiblyunsorted = True
1839 else:
1839 else:
1840 if follow and len(repo) > 0:
1840 if follow and len(repo) > 0:
1841 revs = repo.revs('reverse(:.)')
1841 revs = repo.revs('reverse(:.)')
1842 else:
1842 else:
1843 revs = revset.spanset(repo)
1843 revs = revset.spanset(repo)
1844 revs.reverse()
1844 revs.reverse()
1845 if not revs:
1845 if not revs:
1846 return revset.baseset(), None, None
1846 return revset.baseset(), None, None
1847 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
1847 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
1848 if possiblyunsorted:
1848 if possiblyunsorted:
1849 revs.sort(reverse=True)
1849 revs.sort(reverse=True)
1850 if expr:
1850 if expr:
1851 # Revset matchers often operate faster on revisions in changelog
1851 # Revset matchers often operate faster on revisions in changelog
1852 # order, because most filters deal with the changelog.
1852 # order, because most filters deal with the changelog.
1853 revs.reverse()
1853 revs.reverse()
1854 matcher = revset.match(repo.ui, expr)
1854 matcher = revset.match(repo.ui, expr)
1855 # Revset matches can reorder revisions. "A or B" typically returns
1855 # Revset matches can reorder revisions. "A or B" typically returns
1856 # returns the revision matching A then the revision matching B. Sort
1856 # returns the revision matching A then the revision matching B. Sort
1857 # again to fix that.
1857 # again to fix that.
1858 revs = matcher(repo, revs)
1858 revs = matcher(repo, revs)
1859 revs.sort(reverse=True)
1859 revs.sort(reverse=True)
1860 if limit is not None:
1860 if limit is not None:
1861 limitedrevs = []
1861 limitedrevs = []
1862 for idx, rev in enumerate(revs):
1862 for idx, rev in enumerate(revs):
1863 if idx >= limit:
1863 if idx >= limit:
1864 break
1864 break
1865 limitedrevs.append(rev)
1865 limitedrevs.append(rev)
1866 revs = revset.baseset(limitedrevs)
1866 revs = revset.baseset(limitedrevs)
1867
1867
1868 return revs, expr, filematcher
1868 return revs, expr, filematcher
1869
1869
1870 def getlogrevs(repo, pats, opts):
1870 def getlogrevs(repo, pats, opts):
1871 """Return (revs, expr, filematcher) where revs is an iterable of
1871 """Return (revs, expr, filematcher) where revs is an iterable of
1872 revision numbers, expr is a revset string built from log options
1872 revision numbers, expr is a revset string built from log options
1873 and file patterns or None, and used to filter 'revs'. If --stat or
1873 and file patterns or None, and used to filter 'revs'. If --stat or
1874 --patch are not passed filematcher is None. Otherwise it is a
1874 --patch are not passed filematcher is None. Otherwise it is a
1875 callable taking a revision number and returning a match objects
1875 callable taking a revision number and returning a match objects
1876 filtering the files to be detailed when displaying the revision.
1876 filtering the files to be detailed when displaying the revision.
1877 """
1877 """
1878 limit = loglimit(opts)
1878 limit = loglimit(opts)
1879 # Default --rev value depends on --follow but --follow behaviour
1879 # Default --rev value depends on --follow but --follow behaviour
1880 # depends on revisions resolved from --rev...
1880 # depends on revisions resolved from --rev...
1881 follow = opts.get('follow') or opts.get('follow_first')
1881 follow = opts.get('follow') or opts.get('follow_first')
1882 if opts.get('rev'):
1882 if opts.get('rev'):
1883 revs = scmutil.revrange(repo, opts['rev'])
1883 revs = scmutil.revrange(repo, opts['rev'])
1884 elif follow:
1884 elif follow:
1885 revs = repo.revs('reverse(:.)')
1885 revs = repo.revs('reverse(:.)')
1886 else:
1886 else:
1887 revs = revset.spanset(repo)
1887 revs = revset.spanset(repo)
1888 revs.reverse()
1888 revs.reverse()
1889 if not revs:
1889 if not revs:
1890 return revset.baseset([]), None, None
1890 return revset.baseset([]), None, None
1891 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
1891 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
1892 if expr:
1892 if expr:
1893 # Revset matchers often operate faster on revisions in changelog
1893 # Revset matchers often operate faster on revisions in changelog
1894 # order, because most filters deal with the changelog.
1894 # order, because most filters deal with the changelog.
1895 if not opts.get('rev'):
1895 if not opts.get('rev'):
1896 revs.reverse()
1896 revs.reverse()
1897 matcher = revset.match(repo.ui, expr)
1897 matcher = revset.match(repo.ui, expr)
1898 # Revset matches can reorder revisions. "A or B" typically returns
1898 # Revset matches can reorder revisions. "A or B" typically returns
1899 # returns the revision matching A then the revision matching B. Sort
1899 # returns the revision matching A then the revision matching B. Sort
1900 # again to fix that.
1900 # again to fix that.
1901 revs = matcher(repo, revs)
1901 revs = matcher(repo, revs)
1902 if not opts.get('rev'):
1902 if not opts.get('rev'):
1903 revs.sort(reverse=True)
1903 revs.sort(reverse=True)
1904 if limit is not None:
1904 if limit is not None:
1905 count = 0
1905 count = 0
1906 limitedrevs = []
1906 limitedrevs = []
1907 it = iter(revs)
1907 it = iter(revs)
1908 while count < limit:
1908 while count < limit:
1909 try:
1909 try:
1910 limitedrevs.append(it.next())
1910 limitedrevs.append(it.next())
1911 except (StopIteration):
1911 except (StopIteration):
1912 break
1912 break
1913 count += 1
1913 count += 1
1914 revs = revset.baseset(limitedrevs)
1914 revs = revset.baseset(limitedrevs)
1915
1915
1916 return revs, expr, filematcher
1916 return revs, expr, filematcher
1917
1917
1918 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1918 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1919 filematcher=None):
1919 filematcher=None):
1920 seen, state = [], graphmod.asciistate()
1920 seen, state = [], graphmod.asciistate()
1921 for rev, type, ctx, parents in dag:
1921 for rev, type, ctx, parents in dag:
1922 char = 'o'
1922 char = 'o'
1923 if ctx.node() in showparents:
1923 if ctx.node() in showparents:
1924 char = '@'
1924 char = '@'
1925 elif ctx.obsolete():
1925 elif ctx.obsolete():
1926 char = 'x'
1926 char = 'x'
1927 copies = None
1927 copies = None
1928 if getrenamed and ctx.rev():
1928 if getrenamed and ctx.rev():
1929 copies = []
1929 copies = []
1930 for fn in ctx.files():
1930 for fn in ctx.files():
1931 rename = getrenamed(fn, ctx.rev())
1931 rename = getrenamed(fn, ctx.rev())
1932 if rename:
1932 if rename:
1933 copies.append((fn, rename[0]))
1933 copies.append((fn, rename[0]))
1934 revmatchfn = None
1934 revmatchfn = None
1935 if filematcher is not None:
1935 if filematcher is not None:
1936 revmatchfn = filematcher(ctx.rev())
1936 revmatchfn = filematcher(ctx.rev())
1937 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1937 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1938 lines = displayer.hunk.pop(rev).split('\n')
1938 lines = displayer.hunk.pop(rev).split('\n')
1939 if not lines[-1]:
1939 if not lines[-1]:
1940 del lines[-1]
1940 del lines[-1]
1941 displayer.flush(rev)
1941 displayer.flush(rev)
1942 edges = edgefn(type, char, lines, seen, rev, parents)
1942 edges = edgefn(type, char, lines, seen, rev, parents)
1943 for type, char, lines, coldata in edges:
1943 for type, char, lines, coldata in edges:
1944 graphmod.ascii(ui, state, type, char, lines, coldata)
1944 graphmod.ascii(ui, state, type, char, lines, coldata)
1945 displayer.close()
1945 displayer.close()
1946
1946
1947 def graphlog(ui, repo, *pats, **opts):
1947 def graphlog(ui, repo, *pats, **opts):
1948 # Parameters are identical to log command ones
1948 # Parameters are identical to log command ones
1949 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1949 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1950 revdag = graphmod.dagwalker(repo, revs)
1950 revdag = graphmod.dagwalker(repo, revs)
1951
1951
1952 getrenamed = None
1952 getrenamed = None
1953 if opts.get('copies'):
1953 if opts.get('copies'):
1954 endrev = None
1954 endrev = None
1955 if opts.get('rev'):
1955 if opts.get('rev'):
1956 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
1956 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
1957 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1957 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1958 displayer = show_changeset(ui, repo, opts, buffered=True)
1958 displayer = show_changeset(ui, repo, opts, buffered=True)
1959 showparents = [ctx.node() for ctx in repo[None].parents()]
1959 showparents = [ctx.node() for ctx in repo[None].parents()]
1960 displaygraph(ui, revdag, displayer, showparents,
1960 displaygraph(ui, revdag, displayer, showparents,
1961 graphmod.asciiedges, getrenamed, filematcher)
1961 graphmod.asciiedges, getrenamed, filematcher)
1962
1962
1963 def checkunsupportedgraphflags(pats, opts):
1963 def checkunsupportedgraphflags(pats, opts):
1964 for op in ["newest_first"]:
1964 for op in ["newest_first"]:
1965 if op in opts and opts[op]:
1965 if op in opts and opts[op]:
1966 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1966 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1967 % op.replace("_", "-"))
1967 % op.replace("_", "-"))
1968
1968
1969 def graphrevs(repo, nodes, opts):
1969 def graphrevs(repo, nodes, opts):
1970 limit = loglimit(opts)
1970 limit = loglimit(opts)
1971 nodes.reverse()
1971 nodes.reverse()
1972 if limit is not None:
1972 if limit is not None:
1973 nodes = nodes[:limit]
1973 nodes = nodes[:limit]
1974 return graphmod.nodes(repo, nodes)
1974 return graphmod.nodes(repo, nodes)
1975
1975
1976 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1976 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1977 join = lambda f: os.path.join(prefix, f)
1977 join = lambda f: os.path.join(prefix, f)
1978 bad = []
1978 bad = []
1979 oldbad = match.bad
1979 oldbad = match.bad
1980 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1980 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1981 names = []
1981 names = []
1982 wctx = repo[None]
1982 wctx = repo[None]
1983 cca = None
1983 cca = None
1984 abort, warn = scmutil.checkportabilityalert(ui)
1984 abort, warn = scmutil.checkportabilityalert(ui)
1985 if abort or warn:
1985 if abort or warn:
1986 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1986 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1987 for f in wctx.walk(match):
1987 for f in wctx.walk(match):
1988 exact = match.exact(f)
1988 exact = match.exact(f)
1989 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
1989 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
1990 if cca:
1990 if cca:
1991 cca(f)
1991 cca(f)
1992 names.append(f)
1992 names.append(f)
1993 if ui.verbose or not exact:
1993 if ui.verbose or not exact:
1994 ui.status(_('adding %s\n') % match.rel(join(f)))
1994 ui.status(_('adding %s\n') % match.rel(join(f)))
1995
1995
1996 for subpath in sorted(wctx.substate):
1996 for subpath in sorted(wctx.substate):
1997 sub = wctx.sub(subpath)
1997 sub = wctx.sub(subpath)
1998 try:
1998 try:
1999 submatch = matchmod.narrowmatcher(subpath, match)
1999 submatch = matchmod.narrowmatcher(subpath, match)
2000 if listsubrepos:
2000 if listsubrepos:
2001 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
2001 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
2002 False))
2002 False))
2003 else:
2003 else:
2004 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
2004 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
2005 True))
2005 True))
2006 except error.LookupError:
2006 except error.LookupError:
2007 ui.status(_("skipping missing subrepository: %s\n")
2007 ui.status(_("skipping missing subrepository: %s\n")
2008 % join(subpath))
2008 % join(subpath))
2009
2009
2010 if not dryrun:
2010 if not dryrun:
2011 rejected = wctx.add(names, prefix)
2011 rejected = wctx.add(names, prefix)
2012 bad.extend(f for f in rejected if f in match.files())
2012 bad.extend(f for f in rejected if f in match.files())
2013 return bad
2013 return bad
2014
2014
2015 def forget(ui, repo, match, prefix, explicitonly):
2015 def forget(ui, repo, match, prefix, explicitonly):
2016 join = lambda f: os.path.join(prefix, f)
2016 join = lambda f: os.path.join(prefix, f)
2017 bad = []
2017 bad = []
2018 oldbad = match.bad
2018 oldbad = match.bad
2019 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2019 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
2020 wctx = repo[None]
2020 wctx = repo[None]
2021 forgot = []
2021 forgot = []
2022 s = repo.status(match=match, clean=True)
2022 s = repo.status(match=match, clean=True)
2023 forget = sorted(s[0] + s[1] + s[3] + s[6])
2023 forget = sorted(s[0] + s[1] + s[3] + s[6])
2024 if explicitonly:
2024 if explicitonly:
2025 forget = [f for f in forget if match.exact(f)]
2025 forget = [f for f in forget if match.exact(f)]
2026
2026
2027 for subpath in sorted(wctx.substate):
2027 for subpath in sorted(wctx.substate):
2028 sub = wctx.sub(subpath)
2028 sub = wctx.sub(subpath)
2029 try:
2029 try:
2030 submatch = matchmod.narrowmatcher(subpath, match)
2030 submatch = matchmod.narrowmatcher(subpath, match)
2031 subbad, subforgot = sub.forget(ui, submatch, prefix)
2031 subbad, subforgot = sub.forget(submatch, prefix)
2032 bad.extend([subpath + '/' + f for f in subbad])
2032 bad.extend([subpath + '/' + f for f in subbad])
2033 forgot.extend([subpath + '/' + f for f in subforgot])
2033 forgot.extend([subpath + '/' + f for f in subforgot])
2034 except error.LookupError:
2034 except error.LookupError:
2035 ui.status(_("skipping missing subrepository: %s\n")
2035 ui.status(_("skipping missing subrepository: %s\n")
2036 % join(subpath))
2036 % join(subpath))
2037
2037
2038 if not explicitonly:
2038 if not explicitonly:
2039 for f in match.files():
2039 for f in match.files():
2040 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
2040 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
2041 if f not in forgot:
2041 if f not in forgot:
2042 if os.path.exists(match.rel(join(f))):
2042 if os.path.exists(match.rel(join(f))):
2043 ui.warn(_('not removing %s: '
2043 ui.warn(_('not removing %s: '
2044 'file is already untracked\n')
2044 'file is already untracked\n')
2045 % match.rel(join(f)))
2045 % match.rel(join(f)))
2046 bad.append(f)
2046 bad.append(f)
2047
2047
2048 for f in forget:
2048 for f in forget:
2049 if ui.verbose or not match.exact(f):
2049 if ui.verbose or not match.exact(f):
2050 ui.status(_('removing %s\n') % match.rel(join(f)))
2050 ui.status(_('removing %s\n') % match.rel(join(f)))
2051
2051
2052 rejected = wctx.forget(forget, prefix)
2052 rejected = wctx.forget(forget, prefix)
2053 bad.extend(f for f in rejected if f in match.files())
2053 bad.extend(f for f in rejected if f in match.files())
2054 forgot.extend(forget)
2054 forgot.extend(forget)
2055 return bad, forgot
2055 return bad, forgot
2056
2056
2057 def remove(ui, repo, m, prefix, after, force, subrepos):
2057 def remove(ui, repo, m, prefix, after, force, subrepos):
2058 join = lambda f: os.path.join(prefix, f)
2058 join = lambda f: os.path.join(prefix, f)
2059 ret = 0
2059 ret = 0
2060 s = repo.status(match=m, clean=True)
2060 s = repo.status(match=m, clean=True)
2061 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2061 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2062
2062
2063 wctx = repo[None]
2063 wctx = repo[None]
2064
2064
2065 for subpath in sorted(wctx.substate):
2065 for subpath in sorted(wctx.substate):
2066 def matchessubrepo(matcher, subpath):
2066 def matchessubrepo(matcher, subpath):
2067 if matcher.exact(subpath):
2067 if matcher.exact(subpath):
2068 return True
2068 return True
2069 for f in matcher.files():
2069 for f in matcher.files():
2070 if f.startswith(subpath):
2070 if f.startswith(subpath):
2071 return True
2071 return True
2072 return False
2072 return False
2073
2073
2074 if subrepos or matchessubrepo(m, subpath):
2074 if subrepos or matchessubrepo(m, subpath):
2075 sub = wctx.sub(subpath)
2075 sub = wctx.sub(subpath)
2076 try:
2076 try:
2077 submatch = matchmod.narrowmatcher(subpath, m)
2077 submatch = matchmod.narrowmatcher(subpath, m)
2078 if sub.removefiles(ui, submatch, prefix, after, force,
2078 if sub.removefiles(ui, submatch, prefix, after, force,
2079 subrepos):
2079 subrepos):
2080 ret = 1
2080 ret = 1
2081 except error.LookupError:
2081 except error.LookupError:
2082 ui.status(_("skipping missing subrepository: %s\n")
2082 ui.status(_("skipping missing subrepository: %s\n")
2083 % join(subpath))
2083 % join(subpath))
2084
2084
2085 # warn about failure to delete explicit files/dirs
2085 # warn about failure to delete explicit files/dirs
2086 for f in m.files():
2086 for f in m.files():
2087 def insubrepo():
2087 def insubrepo():
2088 for subpath in wctx.substate:
2088 for subpath in wctx.substate:
2089 if f.startswith(subpath):
2089 if f.startswith(subpath):
2090 return True
2090 return True
2091 return False
2091 return False
2092
2092
2093 if f in repo.dirstate or f in wctx.dirs() or f == '.' or insubrepo():
2093 if f in repo.dirstate or f in wctx.dirs() or f == '.' or insubrepo():
2094 continue
2094 continue
2095
2095
2096 if os.path.exists(m.rel(join(f))):
2096 if os.path.exists(m.rel(join(f))):
2097 if os.path.isdir(m.rel(join(f))):
2097 if os.path.isdir(m.rel(join(f))):
2098 ui.warn(_('not removing %s: no tracked files\n')
2098 ui.warn(_('not removing %s: no tracked files\n')
2099 % m.rel(join(f)))
2099 % m.rel(join(f)))
2100 else:
2100 else:
2101 ui.warn(_('not removing %s: file is untracked\n')
2101 ui.warn(_('not removing %s: file is untracked\n')
2102 % m.rel(join(f)))
2102 % m.rel(join(f)))
2103 # missing files will generate a warning elsewhere
2103 # missing files will generate a warning elsewhere
2104 ret = 1
2104 ret = 1
2105
2105
2106 if force:
2106 if force:
2107 list = modified + deleted + clean + added
2107 list = modified + deleted + clean + added
2108 elif after:
2108 elif after:
2109 list = deleted
2109 list = deleted
2110 for f in modified + added + clean:
2110 for f in modified + added + clean:
2111 ui.warn(_('not removing %s: file still exists\n') % m.rel(join(f)))
2111 ui.warn(_('not removing %s: file still exists\n') % m.rel(join(f)))
2112 ret = 1
2112 ret = 1
2113 else:
2113 else:
2114 list = deleted + clean
2114 list = deleted + clean
2115 for f in modified:
2115 for f in modified:
2116 ui.warn(_('not removing %s: file is modified (use -f'
2116 ui.warn(_('not removing %s: file is modified (use -f'
2117 ' to force removal)\n') % m.rel(join(f)))
2117 ' to force removal)\n') % m.rel(join(f)))
2118 ret = 1
2118 ret = 1
2119 for f in added:
2119 for f in added:
2120 ui.warn(_('not removing %s: file has been marked for add'
2120 ui.warn(_('not removing %s: file has been marked for add'
2121 ' (use forget to undo)\n') % m.rel(join(f)))
2121 ' (use forget to undo)\n') % m.rel(join(f)))
2122 ret = 1
2122 ret = 1
2123
2123
2124 for f in sorted(list):
2124 for f in sorted(list):
2125 if ui.verbose or not m.exact(f):
2125 if ui.verbose or not m.exact(f):
2126 ui.status(_('removing %s\n') % m.rel(join(f)))
2126 ui.status(_('removing %s\n') % m.rel(join(f)))
2127
2127
2128 wlock = repo.wlock()
2128 wlock = repo.wlock()
2129 try:
2129 try:
2130 if not after:
2130 if not after:
2131 for f in list:
2131 for f in list:
2132 if f in added:
2132 if f in added:
2133 continue # we never unlink added files on remove
2133 continue # we never unlink added files on remove
2134 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2134 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2135 repo[None].forget(list)
2135 repo[None].forget(list)
2136 finally:
2136 finally:
2137 wlock.release()
2137 wlock.release()
2138
2138
2139 return ret
2139 return ret
2140
2140
2141 def cat(ui, repo, ctx, matcher, prefix, **opts):
2141 def cat(ui, repo, ctx, matcher, prefix, **opts):
2142 err = 1
2142 err = 1
2143
2143
2144 def write(path):
2144 def write(path):
2145 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2145 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2146 pathname=os.path.join(prefix, path))
2146 pathname=os.path.join(prefix, path))
2147 data = ctx[path].data()
2147 data = ctx[path].data()
2148 if opts.get('decode'):
2148 if opts.get('decode'):
2149 data = repo.wwritedata(path, data)
2149 data = repo.wwritedata(path, data)
2150 fp.write(data)
2150 fp.write(data)
2151 fp.close()
2151 fp.close()
2152
2152
2153 # Automation often uses hg cat on single files, so special case it
2153 # Automation often uses hg cat on single files, so special case it
2154 # for performance to avoid the cost of parsing the manifest.
2154 # for performance to avoid the cost of parsing the manifest.
2155 if len(matcher.files()) == 1 and not matcher.anypats():
2155 if len(matcher.files()) == 1 and not matcher.anypats():
2156 file = matcher.files()[0]
2156 file = matcher.files()[0]
2157 mf = repo.manifest
2157 mf = repo.manifest
2158 mfnode = ctx._changeset[0]
2158 mfnode = ctx._changeset[0]
2159 if mf.find(mfnode, file)[0]:
2159 if mf.find(mfnode, file)[0]:
2160 write(file)
2160 write(file)
2161 return 0
2161 return 0
2162
2162
2163 # Don't warn about "missing" files that are really in subrepos
2163 # Don't warn about "missing" files that are really in subrepos
2164 bad = matcher.bad
2164 bad = matcher.bad
2165
2165
2166 def badfn(path, msg):
2166 def badfn(path, msg):
2167 for subpath in ctx.substate:
2167 for subpath in ctx.substate:
2168 if path.startswith(subpath):
2168 if path.startswith(subpath):
2169 return
2169 return
2170 bad(path, msg)
2170 bad(path, msg)
2171
2171
2172 matcher.bad = badfn
2172 matcher.bad = badfn
2173
2173
2174 for abs in ctx.walk(matcher):
2174 for abs in ctx.walk(matcher):
2175 write(abs)
2175 write(abs)
2176 err = 0
2176 err = 0
2177
2177
2178 matcher.bad = bad
2178 matcher.bad = bad
2179
2179
2180 for subpath in sorted(ctx.substate):
2180 for subpath in sorted(ctx.substate):
2181 sub = ctx.sub(subpath)
2181 sub = ctx.sub(subpath)
2182 try:
2182 try:
2183 submatch = matchmod.narrowmatcher(subpath, matcher)
2183 submatch = matchmod.narrowmatcher(subpath, matcher)
2184
2184
2185 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2185 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2186 **opts):
2186 **opts):
2187 err = 0
2187 err = 0
2188 except error.RepoLookupError:
2188 except error.RepoLookupError:
2189 ui.status(_("skipping missing subrepository: %s\n")
2189 ui.status(_("skipping missing subrepository: %s\n")
2190 % os.path.join(prefix, subpath))
2190 % os.path.join(prefix, subpath))
2191
2191
2192 return err
2192 return err
2193
2193
2194 def commit(ui, repo, commitfunc, pats, opts):
2194 def commit(ui, repo, commitfunc, pats, opts):
2195 '''commit the specified files or all outstanding changes'''
2195 '''commit the specified files or all outstanding changes'''
2196 date = opts.get('date')
2196 date = opts.get('date')
2197 if date:
2197 if date:
2198 opts['date'] = util.parsedate(date)
2198 opts['date'] = util.parsedate(date)
2199 message = logmessage(ui, opts)
2199 message = logmessage(ui, opts)
2200 matcher = scmutil.match(repo[None], pats, opts)
2200 matcher = scmutil.match(repo[None], pats, opts)
2201
2201
2202 # extract addremove carefully -- this function can be called from a command
2202 # extract addremove carefully -- this function can be called from a command
2203 # that doesn't support addremove
2203 # that doesn't support addremove
2204 if opts.get('addremove'):
2204 if opts.get('addremove'):
2205 if scmutil.addremove(repo, matcher, "", opts) != 0:
2205 if scmutil.addremove(repo, matcher, "", opts) != 0:
2206 raise util.Abort(
2206 raise util.Abort(
2207 _("failed to mark all new/missing files as added/removed"))
2207 _("failed to mark all new/missing files as added/removed"))
2208
2208
2209 return commitfunc(ui, repo, message, matcher, opts)
2209 return commitfunc(ui, repo, message, matcher, opts)
2210
2210
2211 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2211 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2212 # amend will reuse the existing user if not specified, but the obsolete
2212 # amend will reuse the existing user if not specified, but the obsolete
2213 # marker creation requires that the current user's name is specified.
2213 # marker creation requires that the current user's name is specified.
2214 if obsolete._enabled:
2214 if obsolete._enabled:
2215 ui.username() # raise exception if username not set
2215 ui.username() # raise exception if username not set
2216
2216
2217 ui.note(_('amending changeset %s\n') % old)
2217 ui.note(_('amending changeset %s\n') % old)
2218 base = old.p1()
2218 base = old.p1()
2219
2219
2220 wlock = lock = newid = None
2220 wlock = lock = newid = None
2221 try:
2221 try:
2222 wlock = repo.wlock()
2222 wlock = repo.wlock()
2223 lock = repo.lock()
2223 lock = repo.lock()
2224 tr = repo.transaction('amend')
2224 tr = repo.transaction('amend')
2225 try:
2225 try:
2226 # See if we got a message from -m or -l, if not, open the editor
2226 # See if we got a message from -m or -l, if not, open the editor
2227 # with the message of the changeset to amend
2227 # with the message of the changeset to amend
2228 message = logmessage(ui, opts)
2228 message = logmessage(ui, opts)
2229 # ensure logfile does not conflict with later enforcement of the
2229 # ensure logfile does not conflict with later enforcement of the
2230 # message. potential logfile content has been processed by
2230 # message. potential logfile content has been processed by
2231 # `logmessage` anyway.
2231 # `logmessage` anyway.
2232 opts.pop('logfile')
2232 opts.pop('logfile')
2233 # First, do a regular commit to record all changes in the working
2233 # First, do a regular commit to record all changes in the working
2234 # directory (if there are any)
2234 # directory (if there are any)
2235 ui.callhooks = False
2235 ui.callhooks = False
2236 currentbookmark = repo._bookmarkcurrent
2236 currentbookmark = repo._bookmarkcurrent
2237 try:
2237 try:
2238 repo._bookmarkcurrent = None
2238 repo._bookmarkcurrent = None
2239 opts['message'] = 'temporary amend commit for %s' % old
2239 opts['message'] = 'temporary amend commit for %s' % old
2240 node = commit(ui, repo, commitfunc, pats, opts)
2240 node = commit(ui, repo, commitfunc, pats, opts)
2241 finally:
2241 finally:
2242 repo._bookmarkcurrent = currentbookmark
2242 repo._bookmarkcurrent = currentbookmark
2243 ui.callhooks = True
2243 ui.callhooks = True
2244 ctx = repo[node]
2244 ctx = repo[node]
2245
2245
2246 # Participating changesets:
2246 # Participating changesets:
2247 #
2247 #
2248 # node/ctx o - new (intermediate) commit that contains changes
2248 # node/ctx o - new (intermediate) commit that contains changes
2249 # | from working dir to go into amending commit
2249 # | from working dir to go into amending commit
2250 # | (or a workingctx if there were no changes)
2250 # | (or a workingctx if there were no changes)
2251 # |
2251 # |
2252 # old o - changeset to amend
2252 # old o - changeset to amend
2253 # |
2253 # |
2254 # base o - parent of amending changeset
2254 # base o - parent of amending changeset
2255
2255
2256 # Update extra dict from amended commit (e.g. to preserve graft
2256 # Update extra dict from amended commit (e.g. to preserve graft
2257 # source)
2257 # source)
2258 extra.update(old.extra())
2258 extra.update(old.extra())
2259
2259
2260 # Also update it from the intermediate commit or from the wctx
2260 # Also update it from the intermediate commit or from the wctx
2261 extra.update(ctx.extra())
2261 extra.update(ctx.extra())
2262
2262
2263 if len(old.parents()) > 1:
2263 if len(old.parents()) > 1:
2264 # ctx.files() isn't reliable for merges, so fall back to the
2264 # ctx.files() isn't reliable for merges, so fall back to the
2265 # slower repo.status() method
2265 # slower repo.status() method
2266 files = set([fn for st in repo.status(base, old)[:3]
2266 files = set([fn for st in repo.status(base, old)[:3]
2267 for fn in st])
2267 for fn in st])
2268 else:
2268 else:
2269 files = set(old.files())
2269 files = set(old.files())
2270
2270
2271 # Second, we use either the commit we just did, or if there were no
2271 # Second, we use either the commit we just did, or if there were no
2272 # changes the parent of the working directory as the version of the
2272 # changes the parent of the working directory as the version of the
2273 # files in the final amend commit
2273 # files in the final amend commit
2274 if node:
2274 if node:
2275 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2275 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2276
2276
2277 user = ctx.user()
2277 user = ctx.user()
2278 date = ctx.date()
2278 date = ctx.date()
2279 # Recompute copies (avoid recording a -> b -> a)
2279 # Recompute copies (avoid recording a -> b -> a)
2280 copied = copies.pathcopies(base, ctx)
2280 copied = copies.pathcopies(base, ctx)
2281
2281
2282 # Prune files which were reverted by the updates: if old
2282 # Prune files which were reverted by the updates: if old
2283 # introduced file X and our intermediate commit, node,
2283 # introduced file X and our intermediate commit, node,
2284 # renamed that file, then those two files are the same and
2284 # renamed that file, then those two files are the same and
2285 # we can discard X from our list of files. Likewise if X
2285 # we can discard X from our list of files. Likewise if X
2286 # was deleted, it's no longer relevant
2286 # was deleted, it's no longer relevant
2287 files.update(ctx.files())
2287 files.update(ctx.files())
2288
2288
2289 def samefile(f):
2289 def samefile(f):
2290 if f in ctx.manifest():
2290 if f in ctx.manifest():
2291 a = ctx.filectx(f)
2291 a = ctx.filectx(f)
2292 if f in base.manifest():
2292 if f in base.manifest():
2293 b = base.filectx(f)
2293 b = base.filectx(f)
2294 return (not a.cmp(b)
2294 return (not a.cmp(b)
2295 and a.flags() == b.flags())
2295 and a.flags() == b.flags())
2296 else:
2296 else:
2297 return False
2297 return False
2298 else:
2298 else:
2299 return f not in base.manifest()
2299 return f not in base.manifest()
2300 files = [f for f in files if not samefile(f)]
2300 files = [f for f in files if not samefile(f)]
2301
2301
2302 def filectxfn(repo, ctx_, path):
2302 def filectxfn(repo, ctx_, path):
2303 try:
2303 try:
2304 fctx = ctx[path]
2304 fctx = ctx[path]
2305 flags = fctx.flags()
2305 flags = fctx.flags()
2306 mctx = context.memfilectx(repo,
2306 mctx = context.memfilectx(repo,
2307 fctx.path(), fctx.data(),
2307 fctx.path(), fctx.data(),
2308 islink='l' in flags,
2308 islink='l' in flags,
2309 isexec='x' in flags,
2309 isexec='x' in flags,
2310 copied=copied.get(path))
2310 copied=copied.get(path))
2311 return mctx
2311 return mctx
2312 except KeyError:
2312 except KeyError:
2313 return None
2313 return None
2314 else:
2314 else:
2315 ui.note(_('copying changeset %s to %s\n') % (old, base))
2315 ui.note(_('copying changeset %s to %s\n') % (old, base))
2316
2316
2317 # Use version of files as in the old cset
2317 # Use version of files as in the old cset
2318 def filectxfn(repo, ctx_, path):
2318 def filectxfn(repo, ctx_, path):
2319 try:
2319 try:
2320 return old.filectx(path)
2320 return old.filectx(path)
2321 except KeyError:
2321 except KeyError:
2322 return None
2322 return None
2323
2323
2324 user = opts.get('user') or old.user()
2324 user = opts.get('user') or old.user()
2325 date = opts.get('date') or old.date()
2325 date = opts.get('date') or old.date()
2326 editform = mergeeditform(old, 'commit.amend')
2326 editform = mergeeditform(old, 'commit.amend')
2327 editor = getcommiteditor(editform=editform, **opts)
2327 editor = getcommiteditor(editform=editform, **opts)
2328 if not message:
2328 if not message:
2329 editor = getcommiteditor(edit=True, editform=editform)
2329 editor = getcommiteditor(edit=True, editform=editform)
2330 message = old.description()
2330 message = old.description()
2331
2331
2332 pureextra = extra.copy()
2332 pureextra = extra.copy()
2333 extra['amend_source'] = old.hex()
2333 extra['amend_source'] = old.hex()
2334
2334
2335 new = context.memctx(repo,
2335 new = context.memctx(repo,
2336 parents=[base.node(), old.p2().node()],
2336 parents=[base.node(), old.p2().node()],
2337 text=message,
2337 text=message,
2338 files=files,
2338 files=files,
2339 filectxfn=filectxfn,
2339 filectxfn=filectxfn,
2340 user=user,
2340 user=user,
2341 date=date,
2341 date=date,
2342 extra=extra,
2342 extra=extra,
2343 editor=editor)
2343 editor=editor)
2344
2344
2345 newdesc = changelog.stripdesc(new.description())
2345 newdesc = changelog.stripdesc(new.description())
2346 if ((not node)
2346 if ((not node)
2347 and newdesc == old.description()
2347 and newdesc == old.description()
2348 and user == old.user()
2348 and user == old.user()
2349 and date == old.date()
2349 and date == old.date()
2350 and pureextra == old.extra()):
2350 and pureextra == old.extra()):
2351 # nothing changed. continuing here would create a new node
2351 # nothing changed. continuing here would create a new node
2352 # anyway because of the amend_source noise.
2352 # anyway because of the amend_source noise.
2353 #
2353 #
2354 # This not what we expect from amend.
2354 # This not what we expect from amend.
2355 return old.node()
2355 return old.node()
2356
2356
2357 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2357 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2358 try:
2358 try:
2359 if opts.get('secret'):
2359 if opts.get('secret'):
2360 commitphase = 'secret'
2360 commitphase = 'secret'
2361 else:
2361 else:
2362 commitphase = old.phase()
2362 commitphase = old.phase()
2363 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2363 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2364 newid = repo.commitctx(new)
2364 newid = repo.commitctx(new)
2365 finally:
2365 finally:
2366 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2366 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2367 if newid != old.node():
2367 if newid != old.node():
2368 # Reroute the working copy parent to the new changeset
2368 # Reroute the working copy parent to the new changeset
2369 repo.setparents(newid, nullid)
2369 repo.setparents(newid, nullid)
2370
2370
2371 # Move bookmarks from old parent to amend commit
2371 # Move bookmarks from old parent to amend commit
2372 bms = repo.nodebookmarks(old.node())
2372 bms = repo.nodebookmarks(old.node())
2373 if bms:
2373 if bms:
2374 marks = repo._bookmarks
2374 marks = repo._bookmarks
2375 for bm in bms:
2375 for bm in bms:
2376 marks[bm] = newid
2376 marks[bm] = newid
2377 marks.write()
2377 marks.write()
2378 #commit the whole amend process
2378 #commit the whole amend process
2379 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2379 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2380 if createmarkers and newid != old.node():
2380 if createmarkers and newid != old.node():
2381 # mark the new changeset as successor of the rewritten one
2381 # mark the new changeset as successor of the rewritten one
2382 new = repo[newid]
2382 new = repo[newid]
2383 obs = [(old, (new,))]
2383 obs = [(old, (new,))]
2384 if node:
2384 if node:
2385 obs.append((ctx, ()))
2385 obs.append((ctx, ()))
2386
2386
2387 obsolete.createmarkers(repo, obs)
2387 obsolete.createmarkers(repo, obs)
2388 tr.close()
2388 tr.close()
2389 finally:
2389 finally:
2390 tr.release()
2390 tr.release()
2391 if not createmarkers and newid != old.node():
2391 if not createmarkers and newid != old.node():
2392 # Strip the intermediate commit (if there was one) and the amended
2392 # Strip the intermediate commit (if there was one) and the amended
2393 # commit
2393 # commit
2394 if node:
2394 if node:
2395 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2395 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2396 ui.note(_('stripping amended changeset %s\n') % old)
2396 ui.note(_('stripping amended changeset %s\n') % old)
2397 repair.strip(ui, repo, old.node(), topic='amend-backup')
2397 repair.strip(ui, repo, old.node(), topic='amend-backup')
2398 finally:
2398 finally:
2399 if newid is None:
2399 if newid is None:
2400 repo.dirstate.invalidate()
2400 repo.dirstate.invalidate()
2401 lockmod.release(lock, wlock)
2401 lockmod.release(lock, wlock)
2402 return newid
2402 return newid
2403
2403
2404 def commiteditor(repo, ctx, subs, editform=''):
2404 def commiteditor(repo, ctx, subs, editform=''):
2405 if ctx.description():
2405 if ctx.description():
2406 return ctx.description()
2406 return ctx.description()
2407 return commitforceeditor(repo, ctx, subs, editform=editform)
2407 return commitforceeditor(repo, ctx, subs, editform=editform)
2408
2408
2409 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2409 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2410 editform=''):
2410 editform=''):
2411 if not extramsg:
2411 if not extramsg:
2412 extramsg = _("Leave message empty to abort commit.")
2412 extramsg = _("Leave message empty to abort commit.")
2413
2413
2414 forms = [e for e in editform.split('.') if e]
2414 forms = [e for e in editform.split('.') if e]
2415 forms.insert(0, 'changeset')
2415 forms.insert(0, 'changeset')
2416 while forms:
2416 while forms:
2417 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2417 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2418 if tmpl:
2418 if tmpl:
2419 committext = buildcommittemplate(repo, ctx, subs, extramsg, tmpl)
2419 committext = buildcommittemplate(repo, ctx, subs, extramsg, tmpl)
2420 break
2420 break
2421 forms.pop()
2421 forms.pop()
2422 else:
2422 else:
2423 committext = buildcommittext(repo, ctx, subs, extramsg)
2423 committext = buildcommittext(repo, ctx, subs, extramsg)
2424
2424
2425 # run editor in the repository root
2425 # run editor in the repository root
2426 olddir = os.getcwd()
2426 olddir = os.getcwd()
2427 os.chdir(repo.root)
2427 os.chdir(repo.root)
2428 text = repo.ui.edit(committext, ctx.user(), ctx.extra(), editform=editform)
2428 text = repo.ui.edit(committext, ctx.user(), ctx.extra(), editform=editform)
2429 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2429 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2430 os.chdir(olddir)
2430 os.chdir(olddir)
2431
2431
2432 if finishdesc:
2432 if finishdesc:
2433 text = finishdesc(text)
2433 text = finishdesc(text)
2434 if not text.strip():
2434 if not text.strip():
2435 raise util.Abort(_("empty commit message"))
2435 raise util.Abort(_("empty commit message"))
2436
2436
2437 return text
2437 return text
2438
2438
2439 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2439 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2440 ui = repo.ui
2440 ui = repo.ui
2441 tmpl, mapfile = gettemplate(ui, tmpl, None)
2441 tmpl, mapfile = gettemplate(ui, tmpl, None)
2442
2442
2443 try:
2443 try:
2444 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2444 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2445 except SyntaxError, inst:
2445 except SyntaxError, inst:
2446 raise util.Abort(inst.args[0])
2446 raise util.Abort(inst.args[0])
2447
2447
2448 for k, v in repo.ui.configitems('committemplate'):
2448 for k, v in repo.ui.configitems('committemplate'):
2449 if k != 'changeset':
2449 if k != 'changeset':
2450 t.t.cache[k] = v
2450 t.t.cache[k] = v
2451
2451
2452 if not extramsg:
2452 if not extramsg:
2453 extramsg = '' # ensure that extramsg is string
2453 extramsg = '' # ensure that extramsg is string
2454
2454
2455 ui.pushbuffer()
2455 ui.pushbuffer()
2456 t.show(ctx, extramsg=extramsg)
2456 t.show(ctx, extramsg=extramsg)
2457 return ui.popbuffer()
2457 return ui.popbuffer()
2458
2458
2459 def buildcommittext(repo, ctx, subs, extramsg):
2459 def buildcommittext(repo, ctx, subs, extramsg):
2460 edittext = []
2460 edittext = []
2461 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2461 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2462 if ctx.description():
2462 if ctx.description():
2463 edittext.append(ctx.description())
2463 edittext.append(ctx.description())
2464 edittext.append("")
2464 edittext.append("")
2465 edittext.append("") # Empty line between message and comments.
2465 edittext.append("") # Empty line between message and comments.
2466 edittext.append(_("HG: Enter commit message."
2466 edittext.append(_("HG: Enter commit message."
2467 " Lines beginning with 'HG:' are removed."))
2467 " Lines beginning with 'HG:' are removed."))
2468 edittext.append("HG: %s" % extramsg)
2468 edittext.append("HG: %s" % extramsg)
2469 edittext.append("HG: --")
2469 edittext.append("HG: --")
2470 edittext.append(_("HG: user: %s") % ctx.user())
2470 edittext.append(_("HG: user: %s") % ctx.user())
2471 if ctx.p2():
2471 if ctx.p2():
2472 edittext.append(_("HG: branch merge"))
2472 edittext.append(_("HG: branch merge"))
2473 if ctx.branch():
2473 if ctx.branch():
2474 edittext.append(_("HG: branch '%s'") % ctx.branch())
2474 edittext.append(_("HG: branch '%s'") % ctx.branch())
2475 if bookmarks.iscurrent(repo):
2475 if bookmarks.iscurrent(repo):
2476 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2476 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2477 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2477 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2478 edittext.extend([_("HG: added %s") % f for f in added])
2478 edittext.extend([_("HG: added %s") % f for f in added])
2479 edittext.extend([_("HG: changed %s") % f for f in modified])
2479 edittext.extend([_("HG: changed %s") % f for f in modified])
2480 edittext.extend([_("HG: removed %s") % f for f in removed])
2480 edittext.extend([_("HG: removed %s") % f for f in removed])
2481 if not added and not modified and not removed:
2481 if not added and not modified and not removed:
2482 edittext.append(_("HG: no files changed"))
2482 edittext.append(_("HG: no files changed"))
2483 edittext.append("")
2483 edittext.append("")
2484
2484
2485 return "\n".join(edittext)
2485 return "\n".join(edittext)
2486
2486
2487 def commitstatus(repo, node, branch, bheads=None, opts={}):
2487 def commitstatus(repo, node, branch, bheads=None, opts={}):
2488 ctx = repo[node]
2488 ctx = repo[node]
2489 parents = ctx.parents()
2489 parents = ctx.parents()
2490
2490
2491 if (not opts.get('amend') and bheads and node not in bheads and not
2491 if (not opts.get('amend') and bheads and node not in bheads and not
2492 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2492 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2493 repo.ui.status(_('created new head\n'))
2493 repo.ui.status(_('created new head\n'))
2494 # The message is not printed for initial roots. For the other
2494 # The message is not printed for initial roots. For the other
2495 # changesets, it is printed in the following situations:
2495 # changesets, it is printed in the following situations:
2496 #
2496 #
2497 # Par column: for the 2 parents with ...
2497 # Par column: for the 2 parents with ...
2498 # N: null or no parent
2498 # N: null or no parent
2499 # B: parent is on another named branch
2499 # B: parent is on another named branch
2500 # C: parent is a regular non head changeset
2500 # C: parent is a regular non head changeset
2501 # H: parent was a branch head of the current branch
2501 # H: parent was a branch head of the current branch
2502 # Msg column: whether we print "created new head" message
2502 # Msg column: whether we print "created new head" message
2503 # In the following, it is assumed that there already exists some
2503 # In the following, it is assumed that there already exists some
2504 # initial branch heads of the current branch, otherwise nothing is
2504 # initial branch heads of the current branch, otherwise nothing is
2505 # printed anyway.
2505 # printed anyway.
2506 #
2506 #
2507 # Par Msg Comment
2507 # Par Msg Comment
2508 # N N y additional topo root
2508 # N N y additional topo root
2509 #
2509 #
2510 # B N y additional branch root
2510 # B N y additional branch root
2511 # C N y additional topo head
2511 # C N y additional topo head
2512 # H N n usual case
2512 # H N n usual case
2513 #
2513 #
2514 # B B y weird additional branch root
2514 # B B y weird additional branch root
2515 # C B y branch merge
2515 # C B y branch merge
2516 # H B n merge with named branch
2516 # H B n merge with named branch
2517 #
2517 #
2518 # C C y additional head from merge
2518 # C C y additional head from merge
2519 # C H n merge with a head
2519 # C H n merge with a head
2520 #
2520 #
2521 # H H n head merge: head count decreases
2521 # H H n head merge: head count decreases
2522
2522
2523 if not opts.get('close_branch'):
2523 if not opts.get('close_branch'):
2524 for r in parents:
2524 for r in parents:
2525 if r.closesbranch() and r.branch() == branch:
2525 if r.closesbranch() and r.branch() == branch:
2526 repo.ui.status(_('reopening closed branch head %d\n') % r)
2526 repo.ui.status(_('reopening closed branch head %d\n') % r)
2527
2527
2528 if repo.ui.debugflag:
2528 if repo.ui.debugflag:
2529 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2529 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2530 elif repo.ui.verbose:
2530 elif repo.ui.verbose:
2531 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2531 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2532
2532
2533 def revert(ui, repo, ctx, parents, *pats, **opts):
2533 def revert(ui, repo, ctx, parents, *pats, **opts):
2534 parent, p2 = parents
2534 parent, p2 = parents
2535 node = ctx.node()
2535 node = ctx.node()
2536
2536
2537 mf = ctx.manifest()
2537 mf = ctx.manifest()
2538 if node == p2:
2538 if node == p2:
2539 parent = p2
2539 parent = p2
2540 if node == parent:
2540 if node == parent:
2541 pmf = mf
2541 pmf = mf
2542 else:
2542 else:
2543 pmf = None
2543 pmf = None
2544
2544
2545 # need all matching names in dirstate and manifest of target rev,
2545 # need all matching names in dirstate and manifest of target rev,
2546 # so have to walk both. do not print errors if files exist in one
2546 # so have to walk both. do not print errors if files exist in one
2547 # but not other.
2547 # but not other.
2548
2548
2549 # `names` is a mapping for all elements in working copy and target revision
2549 # `names` is a mapping for all elements in working copy and target revision
2550 # The mapping is in the form:
2550 # The mapping is in the form:
2551 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2551 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2552 names = {}
2552 names = {}
2553
2553
2554 wlock = repo.wlock()
2554 wlock = repo.wlock()
2555 try:
2555 try:
2556 ## filling of the `names` mapping
2556 ## filling of the `names` mapping
2557 # walk dirstate to fill `names`
2557 # walk dirstate to fill `names`
2558
2558
2559 m = scmutil.match(repo[None], pats, opts)
2559 m = scmutil.match(repo[None], pats, opts)
2560 if not m.always() or node != parent:
2560 if not m.always() or node != parent:
2561 m.bad = lambda x, y: False
2561 m.bad = lambda x, y: False
2562 for abs in repo.walk(m):
2562 for abs in repo.walk(m):
2563 names[abs] = m.rel(abs), m.exact(abs)
2563 names[abs] = m.rel(abs), m.exact(abs)
2564
2564
2565 # walk target manifest to fill `names`
2565 # walk target manifest to fill `names`
2566
2566
2567 def badfn(path, msg):
2567 def badfn(path, msg):
2568 if path in names:
2568 if path in names:
2569 return
2569 return
2570 if path in ctx.substate:
2570 if path in ctx.substate:
2571 return
2571 return
2572 path_ = path + '/'
2572 path_ = path + '/'
2573 for f in names:
2573 for f in names:
2574 if f.startswith(path_):
2574 if f.startswith(path_):
2575 return
2575 return
2576 ui.warn("%s: %s\n" % (m.rel(path), msg))
2576 ui.warn("%s: %s\n" % (m.rel(path), msg))
2577
2577
2578 m = scmutil.match(ctx, pats, opts)
2578 m = scmutil.match(ctx, pats, opts)
2579 m.bad = badfn
2579 m.bad = badfn
2580 for abs in ctx.walk(m):
2580 for abs in ctx.walk(m):
2581 if abs not in names:
2581 if abs not in names:
2582 names[abs] = m.rel(abs), m.exact(abs)
2582 names[abs] = m.rel(abs), m.exact(abs)
2583
2583
2584 # Find status of all file in `names`.
2584 # Find status of all file in `names`.
2585 m = scmutil.matchfiles(repo, names)
2585 m = scmutil.matchfiles(repo, names)
2586
2586
2587 changes = repo.status(node1=node, match=m,
2587 changes = repo.status(node1=node, match=m,
2588 unknown=True, ignored=True, clean=True)
2588 unknown=True, ignored=True, clean=True)
2589 else:
2589 else:
2590 changes = repo.status(match=m)
2590 changes = repo.status(match=m)
2591 for kind in changes:
2591 for kind in changes:
2592 for abs in kind:
2592 for abs in kind:
2593 names[abs] = m.rel(abs), m.exact(abs)
2593 names[abs] = m.rel(abs), m.exact(abs)
2594
2594
2595 m = scmutil.matchfiles(repo, names)
2595 m = scmutil.matchfiles(repo, names)
2596
2596
2597 modified = set(changes.modified)
2597 modified = set(changes.modified)
2598 added = set(changes.added)
2598 added = set(changes.added)
2599 removed = set(changes.removed)
2599 removed = set(changes.removed)
2600 _deleted = set(changes.deleted)
2600 _deleted = set(changes.deleted)
2601 unknown = set(changes.unknown)
2601 unknown = set(changes.unknown)
2602 unknown.update(changes.ignored)
2602 unknown.update(changes.ignored)
2603 clean = set(changes.clean)
2603 clean = set(changes.clean)
2604 modadded = set()
2604 modadded = set()
2605
2605
2606 # split between files known in target manifest and the others
2606 # split between files known in target manifest and the others
2607 smf = set(mf)
2607 smf = set(mf)
2608
2608
2609 # determine the exact nature of the deleted changesets
2609 # determine the exact nature of the deleted changesets
2610 deladded = _deleted - smf
2610 deladded = _deleted - smf
2611 deleted = _deleted - deladded
2611 deleted = _deleted - deladded
2612
2612
2613 # We need to account for the state of file in the dirstate.
2613 # We need to account for the state of file in the dirstate.
2614 #
2614 #
2615 # Even, when we revert against something else than parent. This will
2615 # Even, when we revert against something else than parent. This will
2616 # slightly alter the behavior of revert (doing back up or not, delete
2616 # slightly alter the behavior of revert (doing back up or not, delete
2617 # or just forget etc).
2617 # or just forget etc).
2618 if parent == node:
2618 if parent == node:
2619 dsmodified = modified
2619 dsmodified = modified
2620 dsadded = added
2620 dsadded = added
2621 dsremoved = removed
2621 dsremoved = removed
2622 # store all local modifications, useful later for rename detection
2622 # store all local modifications, useful later for rename detection
2623 localchanges = dsmodified | dsadded
2623 localchanges = dsmodified | dsadded
2624 modified, added, removed = set(), set(), set()
2624 modified, added, removed = set(), set(), set()
2625 else:
2625 else:
2626 changes = repo.status(node1=parent, match=m)
2626 changes = repo.status(node1=parent, match=m)
2627 dsmodified = set(changes.modified)
2627 dsmodified = set(changes.modified)
2628 dsadded = set(changes.added)
2628 dsadded = set(changes.added)
2629 dsremoved = set(changes.removed)
2629 dsremoved = set(changes.removed)
2630 # store all local modifications, useful later for rename detection
2630 # store all local modifications, useful later for rename detection
2631 localchanges = dsmodified | dsadded
2631 localchanges = dsmodified | dsadded
2632
2632
2633 # only take into account for removes between wc and target
2633 # only take into account for removes between wc and target
2634 clean |= dsremoved - removed
2634 clean |= dsremoved - removed
2635 dsremoved &= removed
2635 dsremoved &= removed
2636 # distinct between dirstate remove and other
2636 # distinct between dirstate remove and other
2637 removed -= dsremoved
2637 removed -= dsremoved
2638
2638
2639 modadded = added & dsmodified
2639 modadded = added & dsmodified
2640 added -= modadded
2640 added -= modadded
2641
2641
2642 # tell newly modified apart.
2642 # tell newly modified apart.
2643 dsmodified &= modified
2643 dsmodified &= modified
2644 dsmodified |= modified & dsadded # dirstate added may needs backup
2644 dsmodified |= modified & dsadded # dirstate added may needs backup
2645 modified -= dsmodified
2645 modified -= dsmodified
2646
2646
2647 # We need to wait for some post-processing to update this set
2647 # We need to wait for some post-processing to update this set
2648 # before making the distinction. The dirstate will be used for
2648 # before making the distinction. The dirstate will be used for
2649 # that purpose.
2649 # that purpose.
2650 dsadded = added
2650 dsadded = added
2651
2651
2652 # in case of merge, files that are actually added can be reported as
2652 # in case of merge, files that are actually added can be reported as
2653 # modified, we need to post process the result
2653 # modified, we need to post process the result
2654 if p2 != nullid:
2654 if p2 != nullid:
2655 if pmf is None:
2655 if pmf is None:
2656 # only need parent manifest in the merge case,
2656 # only need parent manifest in the merge case,
2657 # so do not read by default
2657 # so do not read by default
2658 pmf = repo[parent].manifest()
2658 pmf = repo[parent].manifest()
2659 mergeadd = dsmodified - set(pmf)
2659 mergeadd = dsmodified - set(pmf)
2660 dsadded |= mergeadd
2660 dsadded |= mergeadd
2661 dsmodified -= mergeadd
2661 dsmodified -= mergeadd
2662
2662
2663 # if f is a rename, update `names` to also revert the source
2663 # if f is a rename, update `names` to also revert the source
2664 cwd = repo.getcwd()
2664 cwd = repo.getcwd()
2665 for f in localchanges:
2665 for f in localchanges:
2666 src = repo.dirstate.copied(f)
2666 src = repo.dirstate.copied(f)
2667 # XXX should we check for rename down to target node?
2667 # XXX should we check for rename down to target node?
2668 if src and src not in names and repo.dirstate[src] == 'r':
2668 if src and src not in names and repo.dirstate[src] == 'r':
2669 dsremoved.add(src)
2669 dsremoved.add(src)
2670 names[src] = (repo.pathto(src, cwd), True)
2670 names[src] = (repo.pathto(src, cwd), True)
2671
2671
2672 # distinguish between file to forget and the other
2672 # distinguish between file to forget and the other
2673 added = set()
2673 added = set()
2674 for abs in dsadded:
2674 for abs in dsadded:
2675 if repo.dirstate[abs] != 'a':
2675 if repo.dirstate[abs] != 'a':
2676 added.add(abs)
2676 added.add(abs)
2677 dsadded -= added
2677 dsadded -= added
2678
2678
2679 for abs in deladded:
2679 for abs in deladded:
2680 if repo.dirstate[abs] == 'a':
2680 if repo.dirstate[abs] == 'a':
2681 dsadded.add(abs)
2681 dsadded.add(abs)
2682 deladded -= dsadded
2682 deladded -= dsadded
2683
2683
2684 # For files marked as removed, we check if an unknown file is present at
2684 # For files marked as removed, we check if an unknown file is present at
2685 # the same path. If a such file exists it may need to be backed up.
2685 # the same path. If a such file exists it may need to be backed up.
2686 # Making the distinction at this stage helps have simpler backup
2686 # Making the distinction at this stage helps have simpler backup
2687 # logic.
2687 # logic.
2688 removunk = set()
2688 removunk = set()
2689 for abs in removed:
2689 for abs in removed:
2690 target = repo.wjoin(abs)
2690 target = repo.wjoin(abs)
2691 if os.path.lexists(target):
2691 if os.path.lexists(target):
2692 removunk.add(abs)
2692 removunk.add(abs)
2693 removed -= removunk
2693 removed -= removunk
2694
2694
2695 dsremovunk = set()
2695 dsremovunk = set()
2696 for abs in dsremoved:
2696 for abs in dsremoved:
2697 target = repo.wjoin(abs)
2697 target = repo.wjoin(abs)
2698 if os.path.lexists(target):
2698 if os.path.lexists(target):
2699 dsremovunk.add(abs)
2699 dsremovunk.add(abs)
2700 dsremoved -= dsremovunk
2700 dsremoved -= dsremovunk
2701
2701
2702 # action to be actually performed by revert
2702 # action to be actually performed by revert
2703 # (<list of file>, message>) tuple
2703 # (<list of file>, message>) tuple
2704 actions = {'revert': ([], _('reverting %s\n')),
2704 actions = {'revert': ([], _('reverting %s\n')),
2705 'add': ([], _('adding %s\n')),
2705 'add': ([], _('adding %s\n')),
2706 'remove': ([], _('removing %s\n')),
2706 'remove': ([], _('removing %s\n')),
2707 'drop': ([], _('removing %s\n')),
2707 'drop': ([], _('removing %s\n')),
2708 'forget': ([], _('forgetting %s\n')),
2708 'forget': ([], _('forgetting %s\n')),
2709 'undelete': ([], _('undeleting %s\n')),
2709 'undelete': ([], _('undeleting %s\n')),
2710 'noop': (None, _('no changes needed to %s\n')),
2710 'noop': (None, _('no changes needed to %s\n')),
2711 'unknown': (None, _('file not managed: %s\n')),
2711 'unknown': (None, _('file not managed: %s\n')),
2712 }
2712 }
2713
2713
2714 # "constant" that convey the backup strategy.
2714 # "constant" that convey the backup strategy.
2715 # All set to `discard` if `no-backup` is set do avoid checking
2715 # All set to `discard` if `no-backup` is set do avoid checking
2716 # no_backup lower in the code.
2716 # no_backup lower in the code.
2717 # These values are ordered for comparison purposes
2717 # These values are ordered for comparison purposes
2718 backup = 2 # unconditionally do backup
2718 backup = 2 # unconditionally do backup
2719 check = 1 # check if the existing file differs from target
2719 check = 1 # check if the existing file differs from target
2720 discard = 0 # never do backup
2720 discard = 0 # never do backup
2721 if opts.get('no_backup'):
2721 if opts.get('no_backup'):
2722 backup = check = discard
2722 backup = check = discard
2723
2723
2724 backupanddel = actions['remove']
2724 backupanddel = actions['remove']
2725 if not opts.get('no_backup'):
2725 if not opts.get('no_backup'):
2726 backupanddel = actions['drop']
2726 backupanddel = actions['drop']
2727
2727
2728 disptable = (
2728 disptable = (
2729 # dispatch table:
2729 # dispatch table:
2730 # file state
2730 # file state
2731 # action
2731 # action
2732 # make backup
2732 # make backup
2733
2733
2734 ## Sets that results that will change file on disk
2734 ## Sets that results that will change file on disk
2735 # Modified compared to target, no local change
2735 # Modified compared to target, no local change
2736 (modified, actions['revert'], discard),
2736 (modified, actions['revert'], discard),
2737 # Modified compared to target, but local file is deleted
2737 # Modified compared to target, but local file is deleted
2738 (deleted, actions['revert'], discard),
2738 (deleted, actions['revert'], discard),
2739 # Modified compared to target, local change
2739 # Modified compared to target, local change
2740 (dsmodified, actions['revert'], backup),
2740 (dsmodified, actions['revert'], backup),
2741 # Added since target
2741 # Added since target
2742 (added, actions['remove'], discard),
2742 (added, actions['remove'], discard),
2743 # Added in working directory
2743 # Added in working directory
2744 (dsadded, actions['forget'], discard),
2744 (dsadded, actions['forget'], discard),
2745 # Added since target, have local modification
2745 # Added since target, have local modification
2746 (modadded, backupanddel, backup),
2746 (modadded, backupanddel, backup),
2747 # Added since target but file is missing in working directory
2747 # Added since target but file is missing in working directory
2748 (deladded, actions['drop'], discard),
2748 (deladded, actions['drop'], discard),
2749 # Removed since target, before working copy parent
2749 # Removed since target, before working copy parent
2750 (removed, actions['add'], discard),
2750 (removed, actions['add'], discard),
2751 # Same as `removed` but an unknown file exists at the same path
2751 # Same as `removed` but an unknown file exists at the same path
2752 (removunk, actions['add'], check),
2752 (removunk, actions['add'], check),
2753 # Removed since targe, marked as such in working copy parent
2753 # Removed since targe, marked as such in working copy parent
2754 (dsremoved, actions['undelete'], discard),
2754 (dsremoved, actions['undelete'], discard),
2755 # Same as `dsremoved` but an unknown file exists at the same path
2755 # Same as `dsremoved` but an unknown file exists at the same path
2756 (dsremovunk, actions['undelete'], check),
2756 (dsremovunk, actions['undelete'], check),
2757 ## the following sets does not result in any file changes
2757 ## the following sets does not result in any file changes
2758 # File with no modification
2758 # File with no modification
2759 (clean, actions['noop'], discard),
2759 (clean, actions['noop'], discard),
2760 # Existing file, not tracked anywhere
2760 # Existing file, not tracked anywhere
2761 (unknown, actions['unknown'], discard),
2761 (unknown, actions['unknown'], discard),
2762 )
2762 )
2763
2763
2764 needdata = ('revert', 'add', 'undelete')
2764 needdata = ('revert', 'add', 'undelete')
2765 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
2765 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
2766
2766
2767 wctx = repo[None]
2767 wctx = repo[None]
2768 for abs, (rel, exact) in sorted(names.items()):
2768 for abs, (rel, exact) in sorted(names.items()):
2769 # target file to be touch on disk (relative to cwd)
2769 # target file to be touch on disk (relative to cwd)
2770 target = repo.wjoin(abs)
2770 target = repo.wjoin(abs)
2771 # search the entry in the dispatch table.
2771 # search the entry in the dispatch table.
2772 # if the file is in any of these sets, it was touched in the working
2772 # if the file is in any of these sets, it was touched in the working
2773 # directory parent and we are sure it needs to be reverted.
2773 # directory parent and we are sure it needs to be reverted.
2774 for table, (xlist, msg), dobackup in disptable:
2774 for table, (xlist, msg), dobackup in disptable:
2775 if abs not in table:
2775 if abs not in table:
2776 continue
2776 continue
2777 if xlist is not None:
2777 if xlist is not None:
2778 xlist.append(abs)
2778 xlist.append(abs)
2779 if dobackup and (backup <= dobackup
2779 if dobackup and (backup <= dobackup
2780 or wctx[abs].cmp(ctx[abs])):
2780 or wctx[abs].cmp(ctx[abs])):
2781 bakname = "%s.orig" % rel
2781 bakname = "%s.orig" % rel
2782 ui.note(_('saving current version of %s as %s\n') %
2782 ui.note(_('saving current version of %s as %s\n') %
2783 (rel, bakname))
2783 (rel, bakname))
2784 if not opts.get('dry_run'):
2784 if not opts.get('dry_run'):
2785 util.rename(target, bakname)
2785 util.rename(target, bakname)
2786 if ui.verbose or not exact:
2786 if ui.verbose or not exact:
2787 if not isinstance(msg, basestring):
2787 if not isinstance(msg, basestring):
2788 msg = msg(abs)
2788 msg = msg(abs)
2789 ui.status(msg % rel)
2789 ui.status(msg % rel)
2790 elif exact:
2790 elif exact:
2791 ui.warn(msg % rel)
2791 ui.warn(msg % rel)
2792 break
2792 break
2793
2793
2794
2794
2795 if not opts.get('dry_run'):
2795 if not opts.get('dry_run'):
2796 _performrevert(repo, parents, ctx, actions)
2796 _performrevert(repo, parents, ctx, actions)
2797
2797
2798 # get the list of subrepos that must be reverted
2798 # get the list of subrepos that must be reverted
2799 subrepomatch = scmutil.match(ctx, pats, opts)
2799 subrepomatch = scmutil.match(ctx, pats, opts)
2800 targetsubs = sorted(s for s in ctx.substate if subrepomatch(s))
2800 targetsubs = sorted(s for s in ctx.substate if subrepomatch(s))
2801
2801
2802 if targetsubs:
2802 if targetsubs:
2803 # Revert the subrepos on the revert list
2803 # Revert the subrepos on the revert list
2804 for sub in targetsubs:
2804 for sub in targetsubs:
2805 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2805 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2806 finally:
2806 finally:
2807 wlock.release()
2807 wlock.release()
2808
2808
2809 def _revertprefetch(repo, ctx, *files):
2809 def _revertprefetch(repo, ctx, *files):
2810 """Let extension changing the storage layer prefetch content"""
2810 """Let extension changing the storage layer prefetch content"""
2811 pass
2811 pass
2812
2812
2813 def _performrevert(repo, parents, ctx, actions):
2813 def _performrevert(repo, parents, ctx, actions):
2814 """function that actually perform all the actions computed for revert
2814 """function that actually perform all the actions computed for revert
2815
2815
2816 This is an independent function to let extension to plug in and react to
2816 This is an independent function to let extension to plug in and react to
2817 the imminent revert.
2817 the imminent revert.
2818
2818
2819 Make sure you have the working directory locked when calling this function.
2819 Make sure you have the working directory locked when calling this function.
2820 """
2820 """
2821 parent, p2 = parents
2821 parent, p2 = parents
2822 node = ctx.node()
2822 node = ctx.node()
2823 def checkout(f):
2823 def checkout(f):
2824 fc = ctx[f]
2824 fc = ctx[f]
2825 repo.wwrite(f, fc.data(), fc.flags())
2825 repo.wwrite(f, fc.data(), fc.flags())
2826
2826
2827 audit_path = pathutil.pathauditor(repo.root)
2827 audit_path = pathutil.pathauditor(repo.root)
2828 for f in actions['forget'][0]:
2828 for f in actions['forget'][0]:
2829 repo.dirstate.drop(f)
2829 repo.dirstate.drop(f)
2830 for f in actions['remove'][0]:
2830 for f in actions['remove'][0]:
2831 audit_path(f)
2831 audit_path(f)
2832 util.unlinkpath(repo.wjoin(f))
2832 util.unlinkpath(repo.wjoin(f))
2833 repo.dirstate.remove(f)
2833 repo.dirstate.remove(f)
2834 for f in actions['drop'][0]:
2834 for f in actions['drop'][0]:
2835 audit_path(f)
2835 audit_path(f)
2836 repo.dirstate.remove(f)
2836 repo.dirstate.remove(f)
2837
2837
2838 normal = None
2838 normal = None
2839 if node == parent:
2839 if node == parent:
2840 # We're reverting to our parent. If possible, we'd like status
2840 # We're reverting to our parent. If possible, we'd like status
2841 # to report the file as clean. We have to use normallookup for
2841 # to report the file as clean. We have to use normallookup for
2842 # merges to avoid losing information about merged/dirty files.
2842 # merges to avoid losing information about merged/dirty files.
2843 if p2 != nullid:
2843 if p2 != nullid:
2844 normal = repo.dirstate.normallookup
2844 normal = repo.dirstate.normallookup
2845 else:
2845 else:
2846 normal = repo.dirstate.normal
2846 normal = repo.dirstate.normal
2847 for f in actions['revert'][0]:
2847 for f in actions['revert'][0]:
2848 checkout(f)
2848 checkout(f)
2849 if normal:
2849 if normal:
2850 normal(f)
2850 normal(f)
2851
2851
2852 for f in actions['add'][0]:
2852 for f in actions['add'][0]:
2853 checkout(f)
2853 checkout(f)
2854 repo.dirstate.add(f)
2854 repo.dirstate.add(f)
2855
2855
2856 normal = repo.dirstate.normallookup
2856 normal = repo.dirstate.normallookup
2857 if node == parent and p2 == nullid:
2857 if node == parent and p2 == nullid:
2858 normal = repo.dirstate.normal
2858 normal = repo.dirstate.normal
2859 for f in actions['undelete'][0]:
2859 for f in actions['undelete'][0]:
2860 checkout(f)
2860 checkout(f)
2861 normal(f)
2861 normal(f)
2862
2862
2863 copied = copies.pathcopies(repo[parent], ctx)
2863 copied = copies.pathcopies(repo[parent], ctx)
2864
2864
2865 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
2865 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
2866 if f in copied:
2866 if f in copied:
2867 repo.dirstate.copy(copied[f], f)
2867 repo.dirstate.copy(copied[f], f)
2868
2868
2869 def command(table):
2869 def command(table):
2870 """Returns a function object to be used as a decorator for making commands.
2870 """Returns a function object to be used as a decorator for making commands.
2871
2871
2872 This function receives a command table as its argument. The table should
2872 This function receives a command table as its argument. The table should
2873 be a dict.
2873 be a dict.
2874
2874
2875 The returned function can be used as a decorator for adding commands
2875 The returned function can be used as a decorator for adding commands
2876 to that command table. This function accepts multiple arguments to define
2876 to that command table. This function accepts multiple arguments to define
2877 a command.
2877 a command.
2878
2878
2879 The first argument is the command name.
2879 The first argument is the command name.
2880
2880
2881 The options argument is an iterable of tuples defining command arguments.
2881 The options argument is an iterable of tuples defining command arguments.
2882 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
2882 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
2883
2883
2884 The synopsis argument defines a short, one line summary of how to use the
2884 The synopsis argument defines a short, one line summary of how to use the
2885 command. This shows up in the help output.
2885 command. This shows up in the help output.
2886
2886
2887 The norepo argument defines whether the command does not require a
2887 The norepo argument defines whether the command does not require a
2888 local repository. Most commands operate against a repository, thus the
2888 local repository. Most commands operate against a repository, thus the
2889 default is False.
2889 default is False.
2890
2890
2891 The optionalrepo argument defines whether the command optionally requires
2891 The optionalrepo argument defines whether the command optionally requires
2892 a local repository.
2892 a local repository.
2893
2893
2894 The inferrepo argument defines whether to try to find a repository from the
2894 The inferrepo argument defines whether to try to find a repository from the
2895 command line arguments. If True, arguments will be examined for potential
2895 command line arguments. If True, arguments will be examined for potential
2896 repository locations. See ``findrepo()``. If a repository is found, it
2896 repository locations. See ``findrepo()``. If a repository is found, it
2897 will be used.
2897 will be used.
2898 """
2898 """
2899 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
2899 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
2900 inferrepo=False):
2900 inferrepo=False):
2901 def decorator(func):
2901 def decorator(func):
2902 if synopsis:
2902 if synopsis:
2903 table[name] = func, list(options), synopsis
2903 table[name] = func, list(options), synopsis
2904 else:
2904 else:
2905 table[name] = func, list(options)
2905 table[name] = func, list(options)
2906
2906
2907 if norepo:
2907 if norepo:
2908 # Avoid import cycle.
2908 # Avoid import cycle.
2909 import commands
2909 import commands
2910 commands.norepo += ' %s' % ' '.join(parsealiases(name))
2910 commands.norepo += ' %s' % ' '.join(parsealiases(name))
2911
2911
2912 if optionalrepo:
2912 if optionalrepo:
2913 import commands
2913 import commands
2914 commands.optionalrepo += ' %s' % ' '.join(parsealiases(name))
2914 commands.optionalrepo += ' %s' % ' '.join(parsealiases(name))
2915
2915
2916 if inferrepo:
2916 if inferrepo:
2917 import commands
2917 import commands
2918 commands.inferrepo += ' %s' % ' '.join(parsealiases(name))
2918 commands.inferrepo += ' %s' % ' '.join(parsealiases(name))
2919
2919
2920 return func
2920 return func
2921 return decorator
2921 return decorator
2922
2922
2923 return cmd
2923 return cmd
2924
2924
2925 # a list of (ui, repo, otherpeer, opts, missing) functions called by
2925 # a list of (ui, repo, otherpeer, opts, missing) functions called by
2926 # commands.outgoing. "missing" is "missing" of the result of
2926 # commands.outgoing. "missing" is "missing" of the result of
2927 # "findcommonoutgoing()"
2927 # "findcommonoutgoing()"
2928 outgoinghooks = util.hooks()
2928 outgoinghooks = util.hooks()
2929
2929
2930 # a list of (ui, repo) functions called by commands.summary
2930 # a list of (ui, repo) functions called by commands.summary
2931 summaryhooks = util.hooks()
2931 summaryhooks = util.hooks()
2932
2932
2933 # a list of (ui, repo, opts, changes) functions called by commands.summary.
2933 # a list of (ui, repo, opts, changes) functions called by commands.summary.
2934 #
2934 #
2935 # functions should return tuple of booleans below, if 'changes' is None:
2935 # functions should return tuple of booleans below, if 'changes' is None:
2936 # (whether-incomings-are-needed, whether-outgoings-are-needed)
2936 # (whether-incomings-are-needed, whether-outgoings-are-needed)
2937 #
2937 #
2938 # otherwise, 'changes' is a tuple of tuples below:
2938 # otherwise, 'changes' is a tuple of tuples below:
2939 # - (sourceurl, sourcebranch, sourcepeer, incoming)
2939 # - (sourceurl, sourcebranch, sourcepeer, incoming)
2940 # - (desturl, destbranch, destpeer, outgoing)
2940 # - (desturl, destbranch, destpeer, outgoing)
2941 summaryremotehooks = util.hooks()
2941 summaryremotehooks = util.hooks()
2942
2942
2943 # A list of state files kept by multistep operations like graft.
2943 # A list of state files kept by multistep operations like graft.
2944 # Since graft cannot be aborted, it is considered 'clearable' by update.
2944 # Since graft cannot be aborted, it is considered 'clearable' by update.
2945 # note: bisect is intentionally excluded
2945 # note: bisect is intentionally excluded
2946 # (state file, clearable, allowcommit, error, hint)
2946 # (state file, clearable, allowcommit, error, hint)
2947 unfinishedstates = [
2947 unfinishedstates = [
2948 ('graftstate', True, False, _('graft in progress'),
2948 ('graftstate', True, False, _('graft in progress'),
2949 _("use 'hg graft --continue' or 'hg update' to abort")),
2949 _("use 'hg graft --continue' or 'hg update' to abort")),
2950 ('updatestate', True, False, _('last update was interrupted'),
2950 ('updatestate', True, False, _('last update was interrupted'),
2951 _("use 'hg update' to get a consistent checkout"))
2951 _("use 'hg update' to get a consistent checkout"))
2952 ]
2952 ]
2953
2953
2954 def checkunfinished(repo, commit=False):
2954 def checkunfinished(repo, commit=False):
2955 '''Look for an unfinished multistep operation, like graft, and abort
2955 '''Look for an unfinished multistep operation, like graft, and abort
2956 if found. It's probably good to check this right before
2956 if found. It's probably good to check this right before
2957 bailifchanged().
2957 bailifchanged().
2958 '''
2958 '''
2959 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2959 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2960 if commit and allowcommit:
2960 if commit and allowcommit:
2961 continue
2961 continue
2962 if repo.vfs.exists(f):
2962 if repo.vfs.exists(f):
2963 raise util.Abort(msg, hint=hint)
2963 raise util.Abort(msg, hint=hint)
2964
2964
2965 def clearunfinished(repo):
2965 def clearunfinished(repo):
2966 '''Check for unfinished operations (as above), and clear the ones
2966 '''Check for unfinished operations (as above), and clear the ones
2967 that are clearable.
2967 that are clearable.
2968 '''
2968 '''
2969 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2969 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2970 if not clearable and repo.vfs.exists(f):
2970 if not clearable and repo.vfs.exists(f):
2971 raise util.Abort(msg, hint=hint)
2971 raise util.Abort(msg, hint=hint)
2972 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2972 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2973 if clearable and repo.vfs.exists(f):
2973 if clearable and repo.vfs.exists(f):
2974 util.unlink(repo.join(f))
2974 util.unlink(repo.join(f))
@@ -1,1676 +1,1676 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import copy
8 import copy
9 import errno, os, re, shutil, posixpath, sys
9 import errno, os, re, shutil, posixpath, sys
10 import xml.dom.minidom
10 import xml.dom.minidom
11 import stat, subprocess, tarfile
11 import stat, subprocess, tarfile
12 from i18n import _
12 from i18n import _
13 import config, util, node, error, cmdutil, scmutil, match as matchmod
13 import config, util, node, error, cmdutil, scmutil, match as matchmod
14 import phases
14 import phases
15 import pathutil
15 import pathutil
16 import exchange
16 import exchange
17 hg = None
17 hg = None
18 propertycache = util.propertycache
18 propertycache = util.propertycache
19
19
20 nullstate = ('', '', 'empty')
20 nullstate = ('', '', 'empty')
21
21
22 def _expandedabspath(path):
22 def _expandedabspath(path):
23 '''
23 '''
24 get a path or url and if it is a path expand it and return an absolute path
24 get a path or url and if it is a path expand it and return an absolute path
25 '''
25 '''
26 expandedpath = util.urllocalpath(util.expandpath(path))
26 expandedpath = util.urllocalpath(util.expandpath(path))
27 u = util.url(expandedpath)
27 u = util.url(expandedpath)
28 if not u.scheme:
28 if not u.scheme:
29 path = util.normpath(os.path.abspath(u.path))
29 path = util.normpath(os.path.abspath(u.path))
30 return path
30 return path
31
31
32 def _getstorehashcachename(remotepath):
32 def _getstorehashcachename(remotepath):
33 '''get a unique filename for the store hash cache of a remote repository'''
33 '''get a unique filename for the store hash cache of a remote repository'''
34 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
34 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
35
35
36 class SubrepoAbort(error.Abort):
36 class SubrepoAbort(error.Abort):
37 """Exception class used to avoid handling a subrepo error more than once"""
37 """Exception class used to avoid handling a subrepo error more than once"""
38 def __init__(self, *args, **kw):
38 def __init__(self, *args, **kw):
39 error.Abort.__init__(self, *args, **kw)
39 error.Abort.__init__(self, *args, **kw)
40 self.subrepo = kw.get('subrepo')
40 self.subrepo = kw.get('subrepo')
41 self.cause = kw.get('cause')
41 self.cause = kw.get('cause')
42
42
43 def annotatesubrepoerror(func):
43 def annotatesubrepoerror(func):
44 def decoratedmethod(self, *args, **kargs):
44 def decoratedmethod(self, *args, **kargs):
45 try:
45 try:
46 res = func(self, *args, **kargs)
46 res = func(self, *args, **kargs)
47 except SubrepoAbort, ex:
47 except SubrepoAbort, ex:
48 # This exception has already been handled
48 # This exception has already been handled
49 raise ex
49 raise ex
50 except error.Abort, ex:
50 except error.Abort, ex:
51 subrepo = subrelpath(self)
51 subrepo = subrelpath(self)
52 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
52 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
53 # avoid handling this exception by raising a SubrepoAbort exception
53 # avoid handling this exception by raising a SubrepoAbort exception
54 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
54 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
55 cause=sys.exc_info())
55 cause=sys.exc_info())
56 return res
56 return res
57 return decoratedmethod
57 return decoratedmethod
58
58
59 def state(ctx, ui):
59 def state(ctx, ui):
60 """return a state dict, mapping subrepo paths configured in .hgsub
60 """return a state dict, mapping subrepo paths configured in .hgsub
61 to tuple: (source from .hgsub, revision from .hgsubstate, kind
61 to tuple: (source from .hgsub, revision from .hgsubstate, kind
62 (key in types dict))
62 (key in types dict))
63 """
63 """
64 p = config.config()
64 p = config.config()
65 def read(f, sections=None, remap=None):
65 def read(f, sections=None, remap=None):
66 if f in ctx:
66 if f in ctx:
67 try:
67 try:
68 data = ctx[f].data()
68 data = ctx[f].data()
69 except IOError, err:
69 except IOError, err:
70 if err.errno != errno.ENOENT:
70 if err.errno != errno.ENOENT:
71 raise
71 raise
72 # handle missing subrepo spec files as removed
72 # handle missing subrepo spec files as removed
73 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
73 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
74 return
74 return
75 p.parse(f, data, sections, remap, read)
75 p.parse(f, data, sections, remap, read)
76 else:
76 else:
77 raise util.Abort(_("subrepo spec file %s not found") % f)
77 raise util.Abort(_("subrepo spec file %s not found") % f)
78
78
79 if '.hgsub' in ctx:
79 if '.hgsub' in ctx:
80 read('.hgsub')
80 read('.hgsub')
81
81
82 for path, src in ui.configitems('subpaths'):
82 for path, src in ui.configitems('subpaths'):
83 p.set('subpaths', path, src, ui.configsource('subpaths', path))
83 p.set('subpaths', path, src, ui.configsource('subpaths', path))
84
84
85 rev = {}
85 rev = {}
86 if '.hgsubstate' in ctx:
86 if '.hgsubstate' in ctx:
87 try:
87 try:
88 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
88 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
89 l = l.lstrip()
89 l = l.lstrip()
90 if not l:
90 if not l:
91 continue
91 continue
92 try:
92 try:
93 revision, path = l.split(" ", 1)
93 revision, path = l.split(" ", 1)
94 except ValueError:
94 except ValueError:
95 raise util.Abort(_("invalid subrepository revision "
95 raise util.Abort(_("invalid subrepository revision "
96 "specifier in .hgsubstate line %d")
96 "specifier in .hgsubstate line %d")
97 % (i + 1))
97 % (i + 1))
98 rev[path] = revision
98 rev[path] = revision
99 except IOError, err:
99 except IOError, err:
100 if err.errno != errno.ENOENT:
100 if err.errno != errno.ENOENT:
101 raise
101 raise
102
102
103 def remap(src):
103 def remap(src):
104 for pattern, repl in p.items('subpaths'):
104 for pattern, repl in p.items('subpaths'):
105 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
105 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
106 # does a string decode.
106 # does a string decode.
107 repl = repl.encode('string-escape')
107 repl = repl.encode('string-escape')
108 # However, we still want to allow back references to go
108 # However, we still want to allow back references to go
109 # through unharmed, so we turn r'\\1' into r'\1'. Again,
109 # through unharmed, so we turn r'\\1' into r'\1'. Again,
110 # extra escapes are needed because re.sub string decodes.
110 # extra escapes are needed because re.sub string decodes.
111 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
111 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
112 try:
112 try:
113 src = re.sub(pattern, repl, src, 1)
113 src = re.sub(pattern, repl, src, 1)
114 except re.error, e:
114 except re.error, e:
115 raise util.Abort(_("bad subrepository pattern in %s: %s")
115 raise util.Abort(_("bad subrepository pattern in %s: %s")
116 % (p.source('subpaths', pattern), e))
116 % (p.source('subpaths', pattern), e))
117 return src
117 return src
118
118
119 state = {}
119 state = {}
120 for path, src in p[''].items():
120 for path, src in p[''].items():
121 kind = 'hg'
121 kind = 'hg'
122 if src.startswith('['):
122 if src.startswith('['):
123 if ']' not in src:
123 if ']' not in src:
124 raise util.Abort(_('missing ] in subrepo source'))
124 raise util.Abort(_('missing ] in subrepo source'))
125 kind, src = src.split(']', 1)
125 kind, src = src.split(']', 1)
126 kind = kind[1:]
126 kind = kind[1:]
127 src = src.lstrip() # strip any extra whitespace after ']'
127 src = src.lstrip() # strip any extra whitespace after ']'
128
128
129 if not util.url(src).isabs():
129 if not util.url(src).isabs():
130 parent = _abssource(ctx._repo, abort=False)
130 parent = _abssource(ctx._repo, abort=False)
131 if parent:
131 if parent:
132 parent = util.url(parent)
132 parent = util.url(parent)
133 parent.path = posixpath.join(parent.path or '', src)
133 parent.path = posixpath.join(parent.path or '', src)
134 parent.path = posixpath.normpath(parent.path)
134 parent.path = posixpath.normpath(parent.path)
135 joined = str(parent)
135 joined = str(parent)
136 # Remap the full joined path and use it if it changes,
136 # Remap the full joined path and use it if it changes,
137 # else remap the original source.
137 # else remap the original source.
138 remapped = remap(joined)
138 remapped = remap(joined)
139 if remapped == joined:
139 if remapped == joined:
140 src = remap(src)
140 src = remap(src)
141 else:
141 else:
142 src = remapped
142 src = remapped
143
143
144 src = remap(src)
144 src = remap(src)
145 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
145 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
146
146
147 return state
147 return state
148
148
149 def writestate(repo, state):
149 def writestate(repo, state):
150 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
150 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
151 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
151 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
152 repo.wwrite('.hgsubstate', ''.join(lines), '')
152 repo.wwrite('.hgsubstate', ''.join(lines), '')
153
153
154 def submerge(repo, wctx, mctx, actx, overwrite):
154 def submerge(repo, wctx, mctx, actx, overwrite):
155 """delegated from merge.applyupdates: merging of .hgsubstate file
155 """delegated from merge.applyupdates: merging of .hgsubstate file
156 in working context, merging context and ancestor context"""
156 in working context, merging context and ancestor context"""
157 if mctx == actx: # backwards?
157 if mctx == actx: # backwards?
158 actx = wctx.p1()
158 actx = wctx.p1()
159 s1 = wctx.substate
159 s1 = wctx.substate
160 s2 = mctx.substate
160 s2 = mctx.substate
161 sa = actx.substate
161 sa = actx.substate
162 sm = {}
162 sm = {}
163
163
164 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
164 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
165
165
166 def debug(s, msg, r=""):
166 def debug(s, msg, r=""):
167 if r:
167 if r:
168 r = "%s:%s:%s" % r
168 r = "%s:%s:%s" % r
169 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
169 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
170
170
171 for s, l in sorted(s1.iteritems()):
171 for s, l in sorted(s1.iteritems()):
172 a = sa.get(s, nullstate)
172 a = sa.get(s, nullstate)
173 ld = l # local state with possible dirty flag for compares
173 ld = l # local state with possible dirty flag for compares
174 if wctx.sub(s).dirty():
174 if wctx.sub(s).dirty():
175 ld = (l[0], l[1] + "+")
175 ld = (l[0], l[1] + "+")
176 if wctx == actx: # overwrite
176 if wctx == actx: # overwrite
177 a = ld
177 a = ld
178
178
179 if s in s2:
179 if s in s2:
180 r = s2[s]
180 r = s2[s]
181 if ld == r or r == a: # no change or local is newer
181 if ld == r or r == a: # no change or local is newer
182 sm[s] = l
182 sm[s] = l
183 continue
183 continue
184 elif ld == a: # other side changed
184 elif ld == a: # other side changed
185 debug(s, "other changed, get", r)
185 debug(s, "other changed, get", r)
186 wctx.sub(s).get(r, overwrite)
186 wctx.sub(s).get(r, overwrite)
187 sm[s] = r
187 sm[s] = r
188 elif ld[0] != r[0]: # sources differ
188 elif ld[0] != r[0]: # sources differ
189 if repo.ui.promptchoice(
189 if repo.ui.promptchoice(
190 _(' subrepository sources for %s differ\n'
190 _(' subrepository sources for %s differ\n'
191 'use (l)ocal source (%s) or (r)emote source (%s)?'
191 'use (l)ocal source (%s) or (r)emote source (%s)?'
192 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
192 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
193 debug(s, "prompt changed, get", r)
193 debug(s, "prompt changed, get", r)
194 wctx.sub(s).get(r, overwrite)
194 wctx.sub(s).get(r, overwrite)
195 sm[s] = r
195 sm[s] = r
196 elif ld[1] == a[1]: # local side is unchanged
196 elif ld[1] == a[1]: # local side is unchanged
197 debug(s, "other side changed, get", r)
197 debug(s, "other side changed, get", r)
198 wctx.sub(s).get(r, overwrite)
198 wctx.sub(s).get(r, overwrite)
199 sm[s] = r
199 sm[s] = r
200 else:
200 else:
201 debug(s, "both sides changed")
201 debug(s, "both sides changed")
202 srepo = wctx.sub(s)
202 srepo = wctx.sub(s)
203 option = repo.ui.promptchoice(
203 option = repo.ui.promptchoice(
204 _(' subrepository %s diverged (local revision: %s, '
204 _(' subrepository %s diverged (local revision: %s, '
205 'remote revision: %s)\n'
205 'remote revision: %s)\n'
206 '(M)erge, keep (l)ocal or keep (r)emote?'
206 '(M)erge, keep (l)ocal or keep (r)emote?'
207 '$$ &Merge $$ &Local $$ &Remote')
207 '$$ &Merge $$ &Local $$ &Remote')
208 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
208 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
209 if option == 0:
209 if option == 0:
210 wctx.sub(s).merge(r)
210 wctx.sub(s).merge(r)
211 sm[s] = l
211 sm[s] = l
212 debug(s, "merge with", r)
212 debug(s, "merge with", r)
213 elif option == 1:
213 elif option == 1:
214 sm[s] = l
214 sm[s] = l
215 debug(s, "keep local subrepo revision", l)
215 debug(s, "keep local subrepo revision", l)
216 else:
216 else:
217 wctx.sub(s).get(r, overwrite)
217 wctx.sub(s).get(r, overwrite)
218 sm[s] = r
218 sm[s] = r
219 debug(s, "get remote subrepo revision", r)
219 debug(s, "get remote subrepo revision", r)
220 elif ld == a: # remote removed, local unchanged
220 elif ld == a: # remote removed, local unchanged
221 debug(s, "remote removed, remove")
221 debug(s, "remote removed, remove")
222 wctx.sub(s).remove()
222 wctx.sub(s).remove()
223 elif a == nullstate: # not present in remote or ancestor
223 elif a == nullstate: # not present in remote or ancestor
224 debug(s, "local added, keep")
224 debug(s, "local added, keep")
225 sm[s] = l
225 sm[s] = l
226 continue
226 continue
227 else:
227 else:
228 if repo.ui.promptchoice(
228 if repo.ui.promptchoice(
229 _(' local changed subrepository %s which remote removed\n'
229 _(' local changed subrepository %s which remote removed\n'
230 'use (c)hanged version or (d)elete?'
230 'use (c)hanged version or (d)elete?'
231 '$$ &Changed $$ &Delete') % s, 0):
231 '$$ &Changed $$ &Delete') % s, 0):
232 debug(s, "prompt remove")
232 debug(s, "prompt remove")
233 wctx.sub(s).remove()
233 wctx.sub(s).remove()
234
234
235 for s, r in sorted(s2.items()):
235 for s, r in sorted(s2.items()):
236 if s in s1:
236 if s in s1:
237 continue
237 continue
238 elif s not in sa:
238 elif s not in sa:
239 debug(s, "remote added, get", r)
239 debug(s, "remote added, get", r)
240 mctx.sub(s).get(r)
240 mctx.sub(s).get(r)
241 sm[s] = r
241 sm[s] = r
242 elif r != sa[s]:
242 elif r != sa[s]:
243 if repo.ui.promptchoice(
243 if repo.ui.promptchoice(
244 _(' remote changed subrepository %s which local removed\n'
244 _(' remote changed subrepository %s which local removed\n'
245 'use (c)hanged version or (d)elete?'
245 'use (c)hanged version or (d)elete?'
246 '$$ &Changed $$ &Delete') % s, 0) == 0:
246 '$$ &Changed $$ &Delete') % s, 0) == 0:
247 debug(s, "prompt recreate", r)
247 debug(s, "prompt recreate", r)
248 wctx.sub(s).get(r)
248 wctx.sub(s).get(r)
249 sm[s] = r
249 sm[s] = r
250
250
251 # record merged .hgsubstate
251 # record merged .hgsubstate
252 writestate(repo, sm)
252 writestate(repo, sm)
253 return sm
253 return sm
254
254
255 def _updateprompt(ui, sub, dirty, local, remote):
255 def _updateprompt(ui, sub, dirty, local, remote):
256 if dirty:
256 if dirty:
257 msg = (_(' subrepository sources for %s differ\n'
257 msg = (_(' subrepository sources for %s differ\n'
258 'use (l)ocal source (%s) or (r)emote source (%s)?'
258 'use (l)ocal source (%s) or (r)emote source (%s)?'
259 '$$ &Local $$ &Remote')
259 '$$ &Local $$ &Remote')
260 % (subrelpath(sub), local, remote))
260 % (subrelpath(sub), local, remote))
261 else:
261 else:
262 msg = (_(' subrepository sources for %s differ (in checked out '
262 msg = (_(' subrepository sources for %s differ (in checked out '
263 'version)\n'
263 'version)\n'
264 'use (l)ocal source (%s) or (r)emote source (%s)?'
264 'use (l)ocal source (%s) or (r)emote source (%s)?'
265 '$$ &Local $$ &Remote')
265 '$$ &Local $$ &Remote')
266 % (subrelpath(sub), local, remote))
266 % (subrelpath(sub), local, remote))
267 return ui.promptchoice(msg, 0)
267 return ui.promptchoice(msg, 0)
268
268
269 def reporelpath(repo):
269 def reporelpath(repo):
270 """return path to this (sub)repo as seen from outermost repo"""
270 """return path to this (sub)repo as seen from outermost repo"""
271 parent = repo
271 parent = repo
272 while util.safehasattr(parent, '_subparent'):
272 while util.safehasattr(parent, '_subparent'):
273 parent = parent._subparent
273 parent = parent._subparent
274 return repo.root[len(pathutil.normasprefix(parent.root)):]
274 return repo.root[len(pathutil.normasprefix(parent.root)):]
275
275
276 def subrelpath(sub):
276 def subrelpath(sub):
277 """return path to this subrepo as seen from outermost repo"""
277 """return path to this subrepo as seen from outermost repo"""
278 if util.safehasattr(sub, '_relpath'):
278 if util.safehasattr(sub, '_relpath'):
279 return sub._relpath
279 return sub._relpath
280 if not util.safehasattr(sub, '_repo'):
280 if not util.safehasattr(sub, '_repo'):
281 return sub._path
281 return sub._path
282 return reporelpath(sub._repo)
282 return reporelpath(sub._repo)
283
283
284 def _abssource(repo, push=False, abort=True):
284 def _abssource(repo, push=False, abort=True):
285 """return pull/push path of repo - either based on parent repo .hgsub info
285 """return pull/push path of repo - either based on parent repo .hgsub info
286 or on the top repo config. Abort or return None if no source found."""
286 or on the top repo config. Abort or return None if no source found."""
287 if util.safehasattr(repo, '_subparent'):
287 if util.safehasattr(repo, '_subparent'):
288 source = util.url(repo._subsource)
288 source = util.url(repo._subsource)
289 if source.isabs():
289 if source.isabs():
290 return str(source)
290 return str(source)
291 source.path = posixpath.normpath(source.path)
291 source.path = posixpath.normpath(source.path)
292 parent = _abssource(repo._subparent, push, abort=False)
292 parent = _abssource(repo._subparent, push, abort=False)
293 if parent:
293 if parent:
294 parent = util.url(util.pconvert(parent))
294 parent = util.url(util.pconvert(parent))
295 parent.path = posixpath.join(parent.path or '', source.path)
295 parent.path = posixpath.join(parent.path or '', source.path)
296 parent.path = posixpath.normpath(parent.path)
296 parent.path = posixpath.normpath(parent.path)
297 return str(parent)
297 return str(parent)
298 else: # recursion reached top repo
298 else: # recursion reached top repo
299 if util.safehasattr(repo, '_subtoppath'):
299 if util.safehasattr(repo, '_subtoppath'):
300 return repo._subtoppath
300 return repo._subtoppath
301 if push and repo.ui.config('paths', 'default-push'):
301 if push and repo.ui.config('paths', 'default-push'):
302 return repo.ui.config('paths', 'default-push')
302 return repo.ui.config('paths', 'default-push')
303 if repo.ui.config('paths', 'default'):
303 if repo.ui.config('paths', 'default'):
304 return repo.ui.config('paths', 'default')
304 return repo.ui.config('paths', 'default')
305 if repo.sharedpath != repo.path:
305 if repo.sharedpath != repo.path:
306 # chop off the .hg component to get the default path form
306 # chop off the .hg component to get the default path form
307 return os.path.dirname(repo.sharedpath)
307 return os.path.dirname(repo.sharedpath)
308 if abort:
308 if abort:
309 raise util.Abort(_("default path for subrepository not found"))
309 raise util.Abort(_("default path for subrepository not found"))
310
310
311 def _sanitize(ui, path, ignore):
311 def _sanitize(ui, path, ignore):
312 for dirname, dirs, names in os.walk(path):
312 for dirname, dirs, names in os.walk(path):
313 for i, d in enumerate(dirs):
313 for i, d in enumerate(dirs):
314 if d.lower() == ignore:
314 if d.lower() == ignore:
315 del dirs[i]
315 del dirs[i]
316 break
316 break
317 if os.path.basename(dirname).lower() != '.hg':
317 if os.path.basename(dirname).lower() != '.hg':
318 continue
318 continue
319 for f in names:
319 for f in names:
320 if f.lower() == 'hgrc':
320 if f.lower() == 'hgrc':
321 ui.warn(_("warning: removing potentially hostile 'hgrc' "
321 ui.warn(_("warning: removing potentially hostile 'hgrc' "
322 "in '%s'\n") % dirname)
322 "in '%s'\n") % dirname)
323 os.unlink(os.path.join(dirname, f))
323 os.unlink(os.path.join(dirname, f))
324
324
325 def subrepo(ctx, path):
325 def subrepo(ctx, path):
326 """return instance of the right subrepo class for subrepo in path"""
326 """return instance of the right subrepo class for subrepo in path"""
327 # subrepo inherently violates our import layering rules
327 # subrepo inherently violates our import layering rules
328 # because it wants to make repo objects from deep inside the stack
328 # because it wants to make repo objects from deep inside the stack
329 # so we manually delay the circular imports to not break
329 # so we manually delay the circular imports to not break
330 # scripts that don't use our demand-loading
330 # scripts that don't use our demand-loading
331 global hg
331 global hg
332 import hg as h
332 import hg as h
333 hg = h
333 hg = h
334
334
335 pathutil.pathauditor(ctx._repo.root)(path)
335 pathutil.pathauditor(ctx._repo.root)(path)
336 state = ctx.substate[path]
336 state = ctx.substate[path]
337 if state[2] not in types:
337 if state[2] not in types:
338 raise util.Abort(_('unknown subrepo type %s') % state[2])
338 raise util.Abort(_('unknown subrepo type %s') % state[2])
339 return types[state[2]](ctx, path, state[:2])
339 return types[state[2]](ctx, path, state[:2])
340
340
341 def newcommitphase(ui, ctx):
341 def newcommitphase(ui, ctx):
342 commitphase = phases.newcommitphase(ui)
342 commitphase = phases.newcommitphase(ui)
343 substate = getattr(ctx, "substate", None)
343 substate = getattr(ctx, "substate", None)
344 if not substate:
344 if not substate:
345 return commitphase
345 return commitphase
346 check = ui.config('phases', 'checksubrepos', 'follow')
346 check = ui.config('phases', 'checksubrepos', 'follow')
347 if check not in ('ignore', 'follow', 'abort'):
347 if check not in ('ignore', 'follow', 'abort'):
348 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
348 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
349 % (check))
349 % (check))
350 if check == 'ignore':
350 if check == 'ignore':
351 return commitphase
351 return commitphase
352 maxphase = phases.public
352 maxphase = phases.public
353 maxsub = None
353 maxsub = None
354 for s in sorted(substate):
354 for s in sorted(substate):
355 sub = ctx.sub(s)
355 sub = ctx.sub(s)
356 subphase = sub.phase(substate[s][1])
356 subphase = sub.phase(substate[s][1])
357 if maxphase < subphase:
357 if maxphase < subphase:
358 maxphase = subphase
358 maxphase = subphase
359 maxsub = s
359 maxsub = s
360 if commitphase < maxphase:
360 if commitphase < maxphase:
361 if check == 'abort':
361 if check == 'abort':
362 raise util.Abort(_("can't commit in %s phase"
362 raise util.Abort(_("can't commit in %s phase"
363 " conflicting %s from subrepository %s") %
363 " conflicting %s from subrepository %s") %
364 (phases.phasenames[commitphase],
364 (phases.phasenames[commitphase],
365 phases.phasenames[maxphase], maxsub))
365 phases.phasenames[maxphase], maxsub))
366 ui.warn(_("warning: changes are committed in"
366 ui.warn(_("warning: changes are committed in"
367 " %s phase from subrepository %s\n") %
367 " %s phase from subrepository %s\n") %
368 (phases.phasenames[maxphase], maxsub))
368 (phases.phasenames[maxphase], maxsub))
369 return maxphase
369 return maxphase
370 return commitphase
370 return commitphase
371
371
372 # subrepo classes need to implement the following abstract class:
372 # subrepo classes need to implement the following abstract class:
373
373
374 class abstractsubrepo(object):
374 class abstractsubrepo(object):
375
375
376 def __init__(self, ui):
376 def __init__(self, ui):
377 self.ui = ui
377 self.ui = ui
378
378
379 def storeclean(self, path):
379 def storeclean(self, path):
380 """
380 """
381 returns true if the repository has not changed since it was last
381 returns true if the repository has not changed since it was last
382 cloned from or pushed to a given repository.
382 cloned from or pushed to a given repository.
383 """
383 """
384 return False
384 return False
385
385
386 def dirty(self, ignoreupdate=False):
386 def dirty(self, ignoreupdate=False):
387 """returns true if the dirstate of the subrepo is dirty or does not
387 """returns true if the dirstate of the subrepo is dirty or does not
388 match current stored state. If ignoreupdate is true, only check
388 match current stored state. If ignoreupdate is true, only check
389 whether the subrepo has uncommitted changes in its dirstate.
389 whether the subrepo has uncommitted changes in its dirstate.
390 """
390 """
391 raise NotImplementedError
391 raise NotImplementedError
392
392
393 def basestate(self):
393 def basestate(self):
394 """current working directory base state, disregarding .hgsubstate
394 """current working directory base state, disregarding .hgsubstate
395 state and working directory modifications"""
395 state and working directory modifications"""
396 raise NotImplementedError
396 raise NotImplementedError
397
397
398 def checknested(self, path):
398 def checknested(self, path):
399 """check if path is a subrepository within this repository"""
399 """check if path is a subrepository within this repository"""
400 return False
400 return False
401
401
402 def commit(self, text, user, date):
402 def commit(self, text, user, date):
403 """commit the current changes to the subrepo with the given
403 """commit the current changes to the subrepo with the given
404 log message. Use given user and date if possible. Return the
404 log message. Use given user and date if possible. Return the
405 new state of the subrepo.
405 new state of the subrepo.
406 """
406 """
407 raise NotImplementedError
407 raise NotImplementedError
408
408
409 def phase(self, state):
409 def phase(self, state):
410 """returns phase of specified state in the subrepository.
410 """returns phase of specified state in the subrepository.
411 """
411 """
412 return phases.public
412 return phases.public
413
413
414 def remove(self):
414 def remove(self):
415 """remove the subrepo
415 """remove the subrepo
416
416
417 (should verify the dirstate is not dirty first)
417 (should verify the dirstate is not dirty first)
418 """
418 """
419 raise NotImplementedError
419 raise NotImplementedError
420
420
421 def get(self, state, overwrite=False):
421 def get(self, state, overwrite=False):
422 """run whatever commands are needed to put the subrepo into
422 """run whatever commands are needed to put the subrepo into
423 this state
423 this state
424 """
424 """
425 raise NotImplementedError
425 raise NotImplementedError
426
426
427 def merge(self, state):
427 def merge(self, state):
428 """merge currently-saved state with the new state."""
428 """merge currently-saved state with the new state."""
429 raise NotImplementedError
429 raise NotImplementedError
430
430
431 def push(self, opts):
431 def push(self, opts):
432 """perform whatever action is analogous to 'hg push'
432 """perform whatever action is analogous to 'hg push'
433
433
434 This may be a no-op on some systems.
434 This may be a no-op on some systems.
435 """
435 """
436 raise NotImplementedError
436 raise NotImplementedError
437
437
438 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
438 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
439 return []
439 return []
440
440
441 def addremove(self, matcher, prefix, opts, dry_run, similarity):
441 def addremove(self, matcher, prefix, opts, dry_run, similarity):
442 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
442 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
443 return 1
443 return 1
444
444
445 def cat(self, match, prefix, **opts):
445 def cat(self, match, prefix, **opts):
446 return 1
446 return 1
447
447
448 def status(self, rev2, **opts):
448 def status(self, rev2, **opts):
449 return scmutil.status([], [], [], [], [], [], [])
449 return scmutil.status([], [], [], [], [], [], [])
450
450
451 def diff(self, ui, diffopts, node2, match, prefix, **opts):
451 def diff(self, ui, diffopts, node2, match, prefix, **opts):
452 pass
452 pass
453
453
454 def outgoing(self, ui, dest, opts):
454 def outgoing(self, ui, dest, opts):
455 return 1
455 return 1
456
456
457 def incoming(self, ui, source, opts):
457 def incoming(self, ui, source, opts):
458 return 1
458 return 1
459
459
460 def files(self):
460 def files(self):
461 """return filename iterator"""
461 """return filename iterator"""
462 raise NotImplementedError
462 raise NotImplementedError
463
463
464 def filedata(self, name):
464 def filedata(self, name):
465 """return file data"""
465 """return file data"""
466 raise NotImplementedError
466 raise NotImplementedError
467
467
468 def fileflags(self, name):
468 def fileflags(self, name):
469 """return file flags"""
469 """return file flags"""
470 return ''
470 return ''
471
471
472 def archive(self, archiver, prefix, match=None):
472 def archive(self, archiver, prefix, match=None):
473 if match is not None:
473 if match is not None:
474 files = [f for f in self.files() if match(f)]
474 files = [f for f in self.files() if match(f)]
475 else:
475 else:
476 files = self.files()
476 files = self.files()
477 total = len(files)
477 total = len(files)
478 relpath = subrelpath(self)
478 relpath = subrelpath(self)
479 self.ui.progress(_('archiving (%s)') % relpath, 0,
479 self.ui.progress(_('archiving (%s)') % relpath, 0,
480 unit=_('files'), total=total)
480 unit=_('files'), total=total)
481 for i, name in enumerate(files):
481 for i, name in enumerate(files):
482 flags = self.fileflags(name)
482 flags = self.fileflags(name)
483 mode = 'x' in flags and 0755 or 0644
483 mode = 'x' in flags and 0755 or 0644
484 symlink = 'l' in flags
484 symlink = 'l' in flags
485 archiver.addfile(os.path.join(prefix, self._path, name),
485 archiver.addfile(os.path.join(prefix, self._path, name),
486 mode, symlink, self.filedata(name))
486 mode, symlink, self.filedata(name))
487 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
487 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
488 unit=_('files'), total=total)
488 unit=_('files'), total=total)
489 self.ui.progress(_('archiving (%s)') % relpath, None)
489 self.ui.progress(_('archiving (%s)') % relpath, None)
490 return total
490 return total
491
491
492 def walk(self, match):
492 def walk(self, match):
493 '''
493 '''
494 walk recursively through the directory tree, finding all files
494 walk recursively through the directory tree, finding all files
495 matched by the match function
495 matched by the match function
496 '''
496 '''
497 pass
497 pass
498
498
499 def forget(self, ui, match, prefix):
499 def forget(self, match, prefix):
500 return ([], [])
500 return ([], [])
501
501
502 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
502 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
503 """remove the matched files from the subrepository and the filesystem,
503 """remove the matched files from the subrepository and the filesystem,
504 possibly by force and/or after the file has been removed from the
504 possibly by force and/or after the file has been removed from the
505 filesystem. Return 0 on success, 1 on any warning.
505 filesystem. Return 0 on success, 1 on any warning.
506 """
506 """
507 return 1
507 return 1
508
508
509 def revert(self, ui, substate, *pats, **opts):
509 def revert(self, ui, substate, *pats, **opts):
510 ui.warn('%s: reverting %s subrepos is unsupported\n' \
510 ui.warn('%s: reverting %s subrepos is unsupported\n' \
511 % (substate[0], substate[2]))
511 % (substate[0], substate[2]))
512 return []
512 return []
513
513
514 def shortid(self, revid):
514 def shortid(self, revid):
515 return revid
515 return revid
516
516
517 class hgsubrepo(abstractsubrepo):
517 class hgsubrepo(abstractsubrepo):
518 def __init__(self, ctx, path, state):
518 def __init__(self, ctx, path, state):
519 super(hgsubrepo, self).__init__(ctx._repo.ui)
519 super(hgsubrepo, self).__init__(ctx._repo.ui)
520 self._path = path
520 self._path = path
521 self._state = state
521 self._state = state
522 r = ctx._repo
522 r = ctx._repo
523 root = r.wjoin(path)
523 root = r.wjoin(path)
524 create = not r.wvfs.exists('%s/.hg' % path)
524 create = not r.wvfs.exists('%s/.hg' % path)
525 self._repo = hg.repository(r.baseui, root, create=create)
525 self._repo = hg.repository(r.baseui, root, create=create)
526 self.ui = self._repo.ui
526 self.ui = self._repo.ui
527 for s, k in [('ui', 'commitsubrepos')]:
527 for s, k in [('ui', 'commitsubrepos')]:
528 v = r.ui.config(s, k)
528 v = r.ui.config(s, k)
529 if v:
529 if v:
530 self.ui.setconfig(s, k, v, 'subrepo')
530 self.ui.setconfig(s, k, v, 'subrepo')
531 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
531 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
532 self._initrepo(r, state[0], create)
532 self._initrepo(r, state[0], create)
533
533
534 def storeclean(self, path):
534 def storeclean(self, path):
535 lock = self._repo.lock()
535 lock = self._repo.lock()
536 try:
536 try:
537 return self._storeclean(path)
537 return self._storeclean(path)
538 finally:
538 finally:
539 lock.release()
539 lock.release()
540
540
541 def _storeclean(self, path):
541 def _storeclean(self, path):
542 clean = True
542 clean = True
543 itercache = self._calcstorehash(path)
543 itercache = self._calcstorehash(path)
544 try:
544 try:
545 for filehash in self._readstorehashcache(path):
545 for filehash in self._readstorehashcache(path):
546 if filehash != itercache.next():
546 if filehash != itercache.next():
547 clean = False
547 clean = False
548 break
548 break
549 except StopIteration:
549 except StopIteration:
550 # the cached and current pull states have a different size
550 # the cached and current pull states have a different size
551 clean = False
551 clean = False
552 if clean:
552 if clean:
553 try:
553 try:
554 itercache.next()
554 itercache.next()
555 # the cached and current pull states have a different size
555 # the cached and current pull states have a different size
556 clean = False
556 clean = False
557 except StopIteration:
557 except StopIteration:
558 pass
558 pass
559 return clean
559 return clean
560
560
561 def _calcstorehash(self, remotepath):
561 def _calcstorehash(self, remotepath):
562 '''calculate a unique "store hash"
562 '''calculate a unique "store hash"
563
563
564 This method is used to to detect when there are changes that may
564 This method is used to to detect when there are changes that may
565 require a push to a given remote path.'''
565 require a push to a given remote path.'''
566 # sort the files that will be hashed in increasing (likely) file size
566 # sort the files that will be hashed in increasing (likely) file size
567 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
567 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
568 yield '# %s\n' % _expandedabspath(remotepath)
568 yield '# %s\n' % _expandedabspath(remotepath)
569 vfs = self._repo.vfs
569 vfs = self._repo.vfs
570 for relname in filelist:
570 for relname in filelist:
571 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
571 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
572 yield '%s = %s\n' % (relname, filehash)
572 yield '%s = %s\n' % (relname, filehash)
573
573
574 @propertycache
574 @propertycache
575 def _cachestorehashvfs(self):
575 def _cachestorehashvfs(self):
576 return scmutil.vfs(self._repo.join('cache/storehash'))
576 return scmutil.vfs(self._repo.join('cache/storehash'))
577
577
578 def _readstorehashcache(self, remotepath):
578 def _readstorehashcache(self, remotepath):
579 '''read the store hash cache for a given remote repository'''
579 '''read the store hash cache for a given remote repository'''
580 cachefile = _getstorehashcachename(remotepath)
580 cachefile = _getstorehashcachename(remotepath)
581 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
581 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
582
582
583 def _cachestorehash(self, remotepath):
583 def _cachestorehash(self, remotepath):
584 '''cache the current store hash
584 '''cache the current store hash
585
585
586 Each remote repo requires its own store hash cache, because a subrepo
586 Each remote repo requires its own store hash cache, because a subrepo
587 store may be "clean" versus a given remote repo, but not versus another
587 store may be "clean" versus a given remote repo, but not versus another
588 '''
588 '''
589 cachefile = _getstorehashcachename(remotepath)
589 cachefile = _getstorehashcachename(remotepath)
590 lock = self._repo.lock()
590 lock = self._repo.lock()
591 try:
591 try:
592 storehash = list(self._calcstorehash(remotepath))
592 storehash = list(self._calcstorehash(remotepath))
593 vfs = self._cachestorehashvfs
593 vfs = self._cachestorehashvfs
594 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
594 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
595 finally:
595 finally:
596 lock.release()
596 lock.release()
597
597
598 @annotatesubrepoerror
598 @annotatesubrepoerror
599 def _initrepo(self, parentrepo, source, create):
599 def _initrepo(self, parentrepo, source, create):
600 self._repo._subparent = parentrepo
600 self._repo._subparent = parentrepo
601 self._repo._subsource = source
601 self._repo._subsource = source
602
602
603 if create:
603 if create:
604 lines = ['[paths]\n']
604 lines = ['[paths]\n']
605
605
606 def addpathconfig(key, value):
606 def addpathconfig(key, value):
607 if value:
607 if value:
608 lines.append('%s = %s\n' % (key, value))
608 lines.append('%s = %s\n' % (key, value))
609 self.ui.setconfig('paths', key, value, 'subrepo')
609 self.ui.setconfig('paths', key, value, 'subrepo')
610
610
611 defpath = _abssource(self._repo, abort=False)
611 defpath = _abssource(self._repo, abort=False)
612 defpushpath = _abssource(self._repo, True, abort=False)
612 defpushpath = _abssource(self._repo, True, abort=False)
613 addpathconfig('default', defpath)
613 addpathconfig('default', defpath)
614 if defpath != defpushpath:
614 if defpath != defpushpath:
615 addpathconfig('default-push', defpushpath)
615 addpathconfig('default-push', defpushpath)
616
616
617 fp = self._repo.opener("hgrc", "w", text=True)
617 fp = self._repo.opener("hgrc", "w", text=True)
618 try:
618 try:
619 fp.write(''.join(lines))
619 fp.write(''.join(lines))
620 finally:
620 finally:
621 fp.close()
621 fp.close()
622
622
623 @annotatesubrepoerror
623 @annotatesubrepoerror
624 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
624 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
625 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
625 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
626 os.path.join(prefix, self._path), explicitonly)
626 os.path.join(prefix, self._path), explicitonly)
627
627
628 def addremove(self, m, prefix, opts, dry_run, similarity):
628 def addremove(self, m, prefix, opts, dry_run, similarity):
629 # In the same way as sub directories are processed, once in a subrepo,
629 # In the same way as sub directories are processed, once in a subrepo,
630 # always entry any of its subrepos. Don't corrupt the options that will
630 # always entry any of its subrepos. Don't corrupt the options that will
631 # be used to process sibling subrepos however.
631 # be used to process sibling subrepos however.
632 opts = copy.copy(opts)
632 opts = copy.copy(opts)
633 opts['subrepos'] = True
633 opts['subrepos'] = True
634 return scmutil.addremove(self._repo, m,
634 return scmutil.addremove(self._repo, m,
635 os.path.join(prefix, self._path), opts,
635 os.path.join(prefix, self._path), opts,
636 dry_run, similarity)
636 dry_run, similarity)
637
637
638 @annotatesubrepoerror
638 @annotatesubrepoerror
639 def cat(self, match, prefix, **opts):
639 def cat(self, match, prefix, **opts):
640 rev = self._state[1]
640 rev = self._state[1]
641 ctx = self._repo[rev]
641 ctx = self._repo[rev]
642 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
642 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
643
643
644 @annotatesubrepoerror
644 @annotatesubrepoerror
645 def status(self, rev2, **opts):
645 def status(self, rev2, **opts):
646 try:
646 try:
647 rev1 = self._state[1]
647 rev1 = self._state[1]
648 ctx1 = self._repo[rev1]
648 ctx1 = self._repo[rev1]
649 ctx2 = self._repo[rev2]
649 ctx2 = self._repo[rev2]
650 return self._repo.status(ctx1, ctx2, **opts)
650 return self._repo.status(ctx1, ctx2, **opts)
651 except error.RepoLookupError, inst:
651 except error.RepoLookupError, inst:
652 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
652 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
653 % (inst, subrelpath(self)))
653 % (inst, subrelpath(self)))
654 return scmutil.status([], [], [], [], [], [], [])
654 return scmutil.status([], [], [], [], [], [], [])
655
655
656 @annotatesubrepoerror
656 @annotatesubrepoerror
657 def diff(self, ui, diffopts, node2, match, prefix, **opts):
657 def diff(self, ui, diffopts, node2, match, prefix, **opts):
658 try:
658 try:
659 node1 = node.bin(self._state[1])
659 node1 = node.bin(self._state[1])
660 # We currently expect node2 to come from substate and be
660 # We currently expect node2 to come from substate and be
661 # in hex format
661 # in hex format
662 if node2 is not None:
662 if node2 is not None:
663 node2 = node.bin(node2)
663 node2 = node.bin(node2)
664 cmdutil.diffordiffstat(ui, self._repo, diffopts,
664 cmdutil.diffordiffstat(ui, self._repo, diffopts,
665 node1, node2, match,
665 node1, node2, match,
666 prefix=posixpath.join(prefix, self._path),
666 prefix=posixpath.join(prefix, self._path),
667 listsubrepos=True, **opts)
667 listsubrepos=True, **opts)
668 except error.RepoLookupError, inst:
668 except error.RepoLookupError, inst:
669 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
669 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
670 % (inst, subrelpath(self)))
670 % (inst, subrelpath(self)))
671
671
672 @annotatesubrepoerror
672 @annotatesubrepoerror
673 def archive(self, archiver, prefix, match=None):
673 def archive(self, archiver, prefix, match=None):
674 self._get(self._state + ('hg',))
674 self._get(self._state + ('hg',))
675 total = abstractsubrepo.archive(self, archiver, prefix, match)
675 total = abstractsubrepo.archive(self, archiver, prefix, match)
676 rev = self._state[1]
676 rev = self._state[1]
677 ctx = self._repo[rev]
677 ctx = self._repo[rev]
678 for subpath in ctx.substate:
678 for subpath in ctx.substate:
679 s = subrepo(ctx, subpath)
679 s = subrepo(ctx, subpath)
680 submatch = matchmod.narrowmatcher(subpath, match)
680 submatch = matchmod.narrowmatcher(subpath, match)
681 total += s.archive(
681 total += s.archive(
682 archiver, os.path.join(prefix, self._path), submatch)
682 archiver, os.path.join(prefix, self._path), submatch)
683 return total
683 return total
684
684
685 @annotatesubrepoerror
685 @annotatesubrepoerror
686 def dirty(self, ignoreupdate=False):
686 def dirty(self, ignoreupdate=False):
687 r = self._state[1]
687 r = self._state[1]
688 if r == '' and not ignoreupdate: # no state recorded
688 if r == '' and not ignoreupdate: # no state recorded
689 return True
689 return True
690 w = self._repo[None]
690 w = self._repo[None]
691 if r != w.p1().hex() and not ignoreupdate:
691 if r != w.p1().hex() and not ignoreupdate:
692 # different version checked out
692 # different version checked out
693 return True
693 return True
694 return w.dirty() # working directory changed
694 return w.dirty() # working directory changed
695
695
696 def basestate(self):
696 def basestate(self):
697 return self._repo['.'].hex()
697 return self._repo['.'].hex()
698
698
699 def checknested(self, path):
699 def checknested(self, path):
700 return self._repo._checknested(self._repo.wjoin(path))
700 return self._repo._checknested(self._repo.wjoin(path))
701
701
702 @annotatesubrepoerror
702 @annotatesubrepoerror
703 def commit(self, text, user, date):
703 def commit(self, text, user, date):
704 # don't bother committing in the subrepo if it's only been
704 # don't bother committing in the subrepo if it's only been
705 # updated
705 # updated
706 if not self.dirty(True):
706 if not self.dirty(True):
707 return self._repo['.'].hex()
707 return self._repo['.'].hex()
708 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
708 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
709 n = self._repo.commit(text, user, date)
709 n = self._repo.commit(text, user, date)
710 if not n:
710 if not n:
711 return self._repo['.'].hex() # different version checked out
711 return self._repo['.'].hex() # different version checked out
712 return node.hex(n)
712 return node.hex(n)
713
713
714 @annotatesubrepoerror
714 @annotatesubrepoerror
715 def phase(self, state):
715 def phase(self, state):
716 return self._repo[state].phase()
716 return self._repo[state].phase()
717
717
718 @annotatesubrepoerror
718 @annotatesubrepoerror
719 def remove(self):
719 def remove(self):
720 # we can't fully delete the repository as it may contain
720 # we can't fully delete the repository as it may contain
721 # local-only history
721 # local-only history
722 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
722 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
723 hg.clean(self._repo, node.nullid, False)
723 hg.clean(self._repo, node.nullid, False)
724
724
725 def _get(self, state):
725 def _get(self, state):
726 source, revision, kind = state
726 source, revision, kind = state
727 if revision in self._repo.unfiltered():
727 if revision in self._repo.unfiltered():
728 return True
728 return True
729 self._repo._subsource = source
729 self._repo._subsource = source
730 srcurl = _abssource(self._repo)
730 srcurl = _abssource(self._repo)
731 other = hg.peer(self._repo, {}, srcurl)
731 other = hg.peer(self._repo, {}, srcurl)
732 if len(self._repo) == 0:
732 if len(self._repo) == 0:
733 self.ui.status(_('cloning subrepo %s from %s\n')
733 self.ui.status(_('cloning subrepo %s from %s\n')
734 % (subrelpath(self), srcurl))
734 % (subrelpath(self), srcurl))
735 parentrepo = self._repo._subparent
735 parentrepo = self._repo._subparent
736 shutil.rmtree(self._repo.path)
736 shutil.rmtree(self._repo.path)
737 other, cloned = hg.clone(self._repo._subparent.baseui, {},
737 other, cloned = hg.clone(self._repo._subparent.baseui, {},
738 other, self._repo.root,
738 other, self._repo.root,
739 update=False)
739 update=False)
740 self._repo = cloned.local()
740 self._repo = cloned.local()
741 self._initrepo(parentrepo, source, create=True)
741 self._initrepo(parentrepo, source, create=True)
742 self._cachestorehash(srcurl)
742 self._cachestorehash(srcurl)
743 else:
743 else:
744 self.ui.status(_('pulling subrepo %s from %s\n')
744 self.ui.status(_('pulling subrepo %s from %s\n')
745 % (subrelpath(self), srcurl))
745 % (subrelpath(self), srcurl))
746 cleansub = self.storeclean(srcurl)
746 cleansub = self.storeclean(srcurl)
747 exchange.pull(self._repo, other)
747 exchange.pull(self._repo, other)
748 if cleansub:
748 if cleansub:
749 # keep the repo clean after pull
749 # keep the repo clean after pull
750 self._cachestorehash(srcurl)
750 self._cachestorehash(srcurl)
751 return False
751 return False
752
752
753 @annotatesubrepoerror
753 @annotatesubrepoerror
754 def get(self, state, overwrite=False):
754 def get(self, state, overwrite=False):
755 inrepo = self._get(state)
755 inrepo = self._get(state)
756 source, revision, kind = state
756 source, revision, kind = state
757 repo = self._repo
757 repo = self._repo
758 repo.ui.debug("getting subrepo %s\n" % self._path)
758 repo.ui.debug("getting subrepo %s\n" % self._path)
759 if inrepo:
759 if inrepo:
760 urepo = repo.unfiltered()
760 urepo = repo.unfiltered()
761 ctx = urepo[revision]
761 ctx = urepo[revision]
762 if ctx.hidden():
762 if ctx.hidden():
763 urepo.ui.warn(
763 urepo.ui.warn(
764 _('revision %s in subrepo %s is hidden\n') \
764 _('revision %s in subrepo %s is hidden\n') \
765 % (revision[0:12], self._path))
765 % (revision[0:12], self._path))
766 repo = urepo
766 repo = urepo
767 hg.updaterepo(repo, revision, overwrite)
767 hg.updaterepo(repo, revision, overwrite)
768
768
769 @annotatesubrepoerror
769 @annotatesubrepoerror
770 def merge(self, state):
770 def merge(self, state):
771 self._get(state)
771 self._get(state)
772 cur = self._repo['.']
772 cur = self._repo['.']
773 dst = self._repo[state[1]]
773 dst = self._repo[state[1]]
774 anc = dst.ancestor(cur)
774 anc = dst.ancestor(cur)
775
775
776 def mergefunc():
776 def mergefunc():
777 if anc == cur and dst.branch() == cur.branch():
777 if anc == cur and dst.branch() == cur.branch():
778 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
778 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
779 hg.update(self._repo, state[1])
779 hg.update(self._repo, state[1])
780 elif anc == dst:
780 elif anc == dst:
781 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
781 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
782 else:
782 else:
783 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
783 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
784 hg.merge(self._repo, state[1], remind=False)
784 hg.merge(self._repo, state[1], remind=False)
785
785
786 wctx = self._repo[None]
786 wctx = self._repo[None]
787 if self.dirty():
787 if self.dirty():
788 if anc != dst:
788 if anc != dst:
789 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
789 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
790 mergefunc()
790 mergefunc()
791 else:
791 else:
792 mergefunc()
792 mergefunc()
793 else:
793 else:
794 mergefunc()
794 mergefunc()
795
795
796 @annotatesubrepoerror
796 @annotatesubrepoerror
797 def push(self, opts):
797 def push(self, opts):
798 force = opts.get('force')
798 force = opts.get('force')
799 newbranch = opts.get('new_branch')
799 newbranch = opts.get('new_branch')
800 ssh = opts.get('ssh')
800 ssh = opts.get('ssh')
801
801
802 # push subrepos depth-first for coherent ordering
802 # push subrepos depth-first for coherent ordering
803 c = self._repo['']
803 c = self._repo['']
804 subs = c.substate # only repos that are committed
804 subs = c.substate # only repos that are committed
805 for s in sorted(subs):
805 for s in sorted(subs):
806 if c.sub(s).push(opts) == 0:
806 if c.sub(s).push(opts) == 0:
807 return False
807 return False
808
808
809 dsturl = _abssource(self._repo, True)
809 dsturl = _abssource(self._repo, True)
810 if not force:
810 if not force:
811 if self.storeclean(dsturl):
811 if self.storeclean(dsturl):
812 self.ui.status(
812 self.ui.status(
813 _('no changes made to subrepo %s since last push to %s\n')
813 _('no changes made to subrepo %s since last push to %s\n')
814 % (subrelpath(self), dsturl))
814 % (subrelpath(self), dsturl))
815 return None
815 return None
816 self.ui.status(_('pushing subrepo %s to %s\n') %
816 self.ui.status(_('pushing subrepo %s to %s\n') %
817 (subrelpath(self), dsturl))
817 (subrelpath(self), dsturl))
818 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
818 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
819 res = exchange.push(self._repo, other, force, newbranch=newbranch)
819 res = exchange.push(self._repo, other, force, newbranch=newbranch)
820
820
821 # the repo is now clean
821 # the repo is now clean
822 self._cachestorehash(dsturl)
822 self._cachestorehash(dsturl)
823 return res.cgresult
823 return res.cgresult
824
824
825 @annotatesubrepoerror
825 @annotatesubrepoerror
826 def outgoing(self, ui, dest, opts):
826 def outgoing(self, ui, dest, opts):
827 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
827 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
828
828
829 @annotatesubrepoerror
829 @annotatesubrepoerror
830 def incoming(self, ui, source, opts):
830 def incoming(self, ui, source, opts):
831 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
831 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
832
832
833 @annotatesubrepoerror
833 @annotatesubrepoerror
834 def files(self):
834 def files(self):
835 rev = self._state[1]
835 rev = self._state[1]
836 ctx = self._repo[rev]
836 ctx = self._repo[rev]
837 return ctx.manifest()
837 return ctx.manifest()
838
838
839 def filedata(self, name):
839 def filedata(self, name):
840 rev = self._state[1]
840 rev = self._state[1]
841 return self._repo[rev][name].data()
841 return self._repo[rev][name].data()
842
842
843 def fileflags(self, name):
843 def fileflags(self, name):
844 rev = self._state[1]
844 rev = self._state[1]
845 ctx = self._repo[rev]
845 ctx = self._repo[rev]
846 return ctx.flags(name)
846 return ctx.flags(name)
847
847
848 def walk(self, match):
848 def walk(self, match):
849 ctx = self._repo[None]
849 ctx = self._repo[None]
850 return ctx.walk(match)
850 return ctx.walk(match)
851
851
852 @annotatesubrepoerror
852 @annotatesubrepoerror
853 def forget(self, ui, match, prefix):
853 def forget(self, match, prefix):
854 return cmdutil.forget(ui, self._repo, match,
854 return cmdutil.forget(self.ui, self._repo, match,
855 os.path.join(prefix, self._path), True)
855 os.path.join(prefix, self._path), True)
856
856
857 @annotatesubrepoerror
857 @annotatesubrepoerror
858 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
858 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
859 return cmdutil.remove(ui, self._repo, matcher,
859 return cmdutil.remove(ui, self._repo, matcher,
860 os.path.join(prefix, self._path), after, force,
860 os.path.join(prefix, self._path), after, force,
861 subrepos)
861 subrepos)
862
862
863 @annotatesubrepoerror
863 @annotatesubrepoerror
864 def revert(self, ui, substate, *pats, **opts):
864 def revert(self, ui, substate, *pats, **opts):
865 # reverting a subrepo is a 2 step process:
865 # reverting a subrepo is a 2 step process:
866 # 1. if the no_backup is not set, revert all modified
866 # 1. if the no_backup is not set, revert all modified
867 # files inside the subrepo
867 # files inside the subrepo
868 # 2. update the subrepo to the revision specified in
868 # 2. update the subrepo to the revision specified in
869 # the corresponding substate dictionary
869 # the corresponding substate dictionary
870 ui.status(_('reverting subrepo %s\n') % substate[0])
870 ui.status(_('reverting subrepo %s\n') % substate[0])
871 if not opts.get('no_backup'):
871 if not opts.get('no_backup'):
872 # Revert all files on the subrepo, creating backups
872 # Revert all files on the subrepo, creating backups
873 # Note that this will not recursively revert subrepos
873 # Note that this will not recursively revert subrepos
874 # We could do it if there was a set:subrepos() predicate
874 # We could do it if there was a set:subrepos() predicate
875 opts = opts.copy()
875 opts = opts.copy()
876 opts['date'] = None
876 opts['date'] = None
877 opts['rev'] = substate[1]
877 opts['rev'] = substate[1]
878
878
879 pats = []
879 pats = []
880 if not opts.get('all'):
880 if not opts.get('all'):
881 pats = ['set:modified()']
881 pats = ['set:modified()']
882 self.filerevert(ui, *pats, **opts)
882 self.filerevert(ui, *pats, **opts)
883
883
884 # Update the repo to the revision specified in the given substate
884 # Update the repo to the revision specified in the given substate
885 self.get(substate, overwrite=True)
885 self.get(substate, overwrite=True)
886
886
887 def filerevert(self, ui, *pats, **opts):
887 def filerevert(self, ui, *pats, **opts):
888 ctx = self._repo[opts['rev']]
888 ctx = self._repo[opts['rev']]
889 parents = self._repo.dirstate.parents()
889 parents = self._repo.dirstate.parents()
890 if opts.get('all'):
890 if opts.get('all'):
891 pats = ['set:modified()']
891 pats = ['set:modified()']
892 else:
892 else:
893 pats = []
893 pats = []
894 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
894 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
895
895
896 def shortid(self, revid):
896 def shortid(self, revid):
897 return revid[:12]
897 return revid[:12]
898
898
899 class svnsubrepo(abstractsubrepo):
899 class svnsubrepo(abstractsubrepo):
900 def __init__(self, ctx, path, state):
900 def __init__(self, ctx, path, state):
901 super(svnsubrepo, self).__init__(ctx._repo.ui)
901 super(svnsubrepo, self).__init__(ctx._repo.ui)
902 self._path = path
902 self._path = path
903 self._state = state
903 self._state = state
904 self._ctx = ctx
904 self._ctx = ctx
905 self._exe = util.findexe('svn')
905 self._exe = util.findexe('svn')
906 if not self._exe:
906 if not self._exe:
907 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
907 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
908 % self._path)
908 % self._path)
909
909
910 def _svncommand(self, commands, filename='', failok=False):
910 def _svncommand(self, commands, filename='', failok=False):
911 cmd = [self._exe]
911 cmd = [self._exe]
912 extrakw = {}
912 extrakw = {}
913 if not self.ui.interactive():
913 if not self.ui.interactive():
914 # Making stdin be a pipe should prevent svn from behaving
914 # Making stdin be a pipe should prevent svn from behaving
915 # interactively even if we can't pass --non-interactive.
915 # interactively even if we can't pass --non-interactive.
916 extrakw['stdin'] = subprocess.PIPE
916 extrakw['stdin'] = subprocess.PIPE
917 # Starting in svn 1.5 --non-interactive is a global flag
917 # Starting in svn 1.5 --non-interactive is a global flag
918 # instead of being per-command, but we need to support 1.4 so
918 # instead of being per-command, but we need to support 1.4 so
919 # we have to be intelligent about what commands take
919 # we have to be intelligent about what commands take
920 # --non-interactive.
920 # --non-interactive.
921 if commands[0] in ('update', 'checkout', 'commit'):
921 if commands[0] in ('update', 'checkout', 'commit'):
922 cmd.append('--non-interactive')
922 cmd.append('--non-interactive')
923 cmd.extend(commands)
923 cmd.extend(commands)
924 if filename is not None:
924 if filename is not None:
925 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
925 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
926 cmd.append(path)
926 cmd.append(path)
927 env = dict(os.environ)
927 env = dict(os.environ)
928 # Avoid localized output, preserve current locale for everything else.
928 # Avoid localized output, preserve current locale for everything else.
929 lc_all = env.get('LC_ALL')
929 lc_all = env.get('LC_ALL')
930 if lc_all:
930 if lc_all:
931 env['LANG'] = lc_all
931 env['LANG'] = lc_all
932 del env['LC_ALL']
932 del env['LC_ALL']
933 env['LC_MESSAGES'] = 'C'
933 env['LC_MESSAGES'] = 'C'
934 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
934 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
935 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
935 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
936 universal_newlines=True, env=env, **extrakw)
936 universal_newlines=True, env=env, **extrakw)
937 stdout, stderr = p.communicate()
937 stdout, stderr = p.communicate()
938 stderr = stderr.strip()
938 stderr = stderr.strip()
939 if not failok:
939 if not failok:
940 if p.returncode:
940 if p.returncode:
941 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
941 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
942 if stderr:
942 if stderr:
943 self.ui.warn(stderr + '\n')
943 self.ui.warn(stderr + '\n')
944 return stdout, stderr
944 return stdout, stderr
945
945
946 @propertycache
946 @propertycache
947 def _svnversion(self):
947 def _svnversion(self):
948 output, err = self._svncommand(['--version', '--quiet'], filename=None)
948 output, err = self._svncommand(['--version', '--quiet'], filename=None)
949 m = re.search(r'^(\d+)\.(\d+)', output)
949 m = re.search(r'^(\d+)\.(\d+)', output)
950 if not m:
950 if not m:
951 raise util.Abort(_('cannot retrieve svn tool version'))
951 raise util.Abort(_('cannot retrieve svn tool version'))
952 return (int(m.group(1)), int(m.group(2)))
952 return (int(m.group(1)), int(m.group(2)))
953
953
954 def _wcrevs(self):
954 def _wcrevs(self):
955 # Get the working directory revision as well as the last
955 # Get the working directory revision as well as the last
956 # commit revision so we can compare the subrepo state with
956 # commit revision so we can compare the subrepo state with
957 # both. We used to store the working directory one.
957 # both. We used to store the working directory one.
958 output, err = self._svncommand(['info', '--xml'])
958 output, err = self._svncommand(['info', '--xml'])
959 doc = xml.dom.minidom.parseString(output)
959 doc = xml.dom.minidom.parseString(output)
960 entries = doc.getElementsByTagName('entry')
960 entries = doc.getElementsByTagName('entry')
961 lastrev, rev = '0', '0'
961 lastrev, rev = '0', '0'
962 if entries:
962 if entries:
963 rev = str(entries[0].getAttribute('revision')) or '0'
963 rev = str(entries[0].getAttribute('revision')) or '0'
964 commits = entries[0].getElementsByTagName('commit')
964 commits = entries[0].getElementsByTagName('commit')
965 if commits:
965 if commits:
966 lastrev = str(commits[0].getAttribute('revision')) or '0'
966 lastrev = str(commits[0].getAttribute('revision')) or '0'
967 return (lastrev, rev)
967 return (lastrev, rev)
968
968
969 def _wcrev(self):
969 def _wcrev(self):
970 return self._wcrevs()[0]
970 return self._wcrevs()[0]
971
971
972 def _wcchanged(self):
972 def _wcchanged(self):
973 """Return (changes, extchanges, missing) where changes is True
973 """Return (changes, extchanges, missing) where changes is True
974 if the working directory was changed, extchanges is
974 if the working directory was changed, extchanges is
975 True if any of these changes concern an external entry and missing
975 True if any of these changes concern an external entry and missing
976 is True if any change is a missing entry.
976 is True if any change is a missing entry.
977 """
977 """
978 output, err = self._svncommand(['status', '--xml'])
978 output, err = self._svncommand(['status', '--xml'])
979 externals, changes, missing = [], [], []
979 externals, changes, missing = [], [], []
980 doc = xml.dom.minidom.parseString(output)
980 doc = xml.dom.minidom.parseString(output)
981 for e in doc.getElementsByTagName('entry'):
981 for e in doc.getElementsByTagName('entry'):
982 s = e.getElementsByTagName('wc-status')
982 s = e.getElementsByTagName('wc-status')
983 if not s:
983 if not s:
984 continue
984 continue
985 item = s[0].getAttribute('item')
985 item = s[0].getAttribute('item')
986 props = s[0].getAttribute('props')
986 props = s[0].getAttribute('props')
987 path = e.getAttribute('path')
987 path = e.getAttribute('path')
988 if item == 'external':
988 if item == 'external':
989 externals.append(path)
989 externals.append(path)
990 elif item == 'missing':
990 elif item == 'missing':
991 missing.append(path)
991 missing.append(path)
992 if (item not in ('', 'normal', 'unversioned', 'external')
992 if (item not in ('', 'normal', 'unversioned', 'external')
993 or props not in ('', 'none', 'normal')):
993 or props not in ('', 'none', 'normal')):
994 changes.append(path)
994 changes.append(path)
995 for path in changes:
995 for path in changes:
996 for ext in externals:
996 for ext in externals:
997 if path == ext or path.startswith(ext + os.sep):
997 if path == ext or path.startswith(ext + os.sep):
998 return True, True, bool(missing)
998 return True, True, bool(missing)
999 return bool(changes), False, bool(missing)
999 return bool(changes), False, bool(missing)
1000
1000
1001 def dirty(self, ignoreupdate=False):
1001 def dirty(self, ignoreupdate=False):
1002 if not self._wcchanged()[0]:
1002 if not self._wcchanged()[0]:
1003 if self._state[1] in self._wcrevs() or ignoreupdate:
1003 if self._state[1] in self._wcrevs() or ignoreupdate:
1004 return False
1004 return False
1005 return True
1005 return True
1006
1006
1007 def basestate(self):
1007 def basestate(self):
1008 lastrev, rev = self._wcrevs()
1008 lastrev, rev = self._wcrevs()
1009 if lastrev != rev:
1009 if lastrev != rev:
1010 # Last committed rev is not the same than rev. We would
1010 # Last committed rev is not the same than rev. We would
1011 # like to take lastrev but we do not know if the subrepo
1011 # like to take lastrev but we do not know if the subrepo
1012 # URL exists at lastrev. Test it and fallback to rev it
1012 # URL exists at lastrev. Test it and fallback to rev it
1013 # is not there.
1013 # is not there.
1014 try:
1014 try:
1015 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1015 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1016 return lastrev
1016 return lastrev
1017 except error.Abort:
1017 except error.Abort:
1018 pass
1018 pass
1019 return rev
1019 return rev
1020
1020
1021 @annotatesubrepoerror
1021 @annotatesubrepoerror
1022 def commit(self, text, user, date):
1022 def commit(self, text, user, date):
1023 # user and date are out of our hands since svn is centralized
1023 # user and date are out of our hands since svn is centralized
1024 changed, extchanged, missing = self._wcchanged()
1024 changed, extchanged, missing = self._wcchanged()
1025 if not changed:
1025 if not changed:
1026 return self.basestate()
1026 return self.basestate()
1027 if extchanged:
1027 if extchanged:
1028 # Do not try to commit externals
1028 # Do not try to commit externals
1029 raise util.Abort(_('cannot commit svn externals'))
1029 raise util.Abort(_('cannot commit svn externals'))
1030 if missing:
1030 if missing:
1031 # svn can commit with missing entries but aborting like hg
1031 # svn can commit with missing entries but aborting like hg
1032 # seems a better approach.
1032 # seems a better approach.
1033 raise util.Abort(_('cannot commit missing svn entries'))
1033 raise util.Abort(_('cannot commit missing svn entries'))
1034 commitinfo, err = self._svncommand(['commit', '-m', text])
1034 commitinfo, err = self._svncommand(['commit', '-m', text])
1035 self.ui.status(commitinfo)
1035 self.ui.status(commitinfo)
1036 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1036 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1037 if not newrev:
1037 if not newrev:
1038 if not commitinfo.strip():
1038 if not commitinfo.strip():
1039 # Sometimes, our definition of "changed" differs from
1039 # Sometimes, our definition of "changed" differs from
1040 # svn one. For instance, svn ignores missing files
1040 # svn one. For instance, svn ignores missing files
1041 # when committing. If there are only missing files, no
1041 # when committing. If there are only missing files, no
1042 # commit is made, no output and no error code.
1042 # commit is made, no output and no error code.
1043 raise util.Abort(_('failed to commit svn changes'))
1043 raise util.Abort(_('failed to commit svn changes'))
1044 raise util.Abort(commitinfo.splitlines()[-1])
1044 raise util.Abort(commitinfo.splitlines()[-1])
1045 newrev = newrev.groups()[0]
1045 newrev = newrev.groups()[0]
1046 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1046 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1047 return newrev
1047 return newrev
1048
1048
1049 @annotatesubrepoerror
1049 @annotatesubrepoerror
1050 def remove(self):
1050 def remove(self):
1051 if self.dirty():
1051 if self.dirty():
1052 self.ui.warn(_('not removing repo %s because '
1052 self.ui.warn(_('not removing repo %s because '
1053 'it has changes.\n') % self._path)
1053 'it has changes.\n') % self._path)
1054 return
1054 return
1055 self.ui.note(_('removing subrepo %s\n') % self._path)
1055 self.ui.note(_('removing subrepo %s\n') % self._path)
1056
1056
1057 def onerror(function, path, excinfo):
1057 def onerror(function, path, excinfo):
1058 if function is not os.remove:
1058 if function is not os.remove:
1059 raise
1059 raise
1060 # read-only files cannot be unlinked under Windows
1060 # read-only files cannot be unlinked under Windows
1061 s = os.stat(path)
1061 s = os.stat(path)
1062 if (s.st_mode & stat.S_IWRITE) != 0:
1062 if (s.st_mode & stat.S_IWRITE) != 0:
1063 raise
1063 raise
1064 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1064 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1065 os.remove(path)
1065 os.remove(path)
1066
1066
1067 path = self._ctx._repo.wjoin(self._path)
1067 path = self._ctx._repo.wjoin(self._path)
1068 shutil.rmtree(path, onerror=onerror)
1068 shutil.rmtree(path, onerror=onerror)
1069 try:
1069 try:
1070 os.removedirs(os.path.dirname(path))
1070 os.removedirs(os.path.dirname(path))
1071 except OSError:
1071 except OSError:
1072 pass
1072 pass
1073
1073
1074 @annotatesubrepoerror
1074 @annotatesubrepoerror
1075 def get(self, state, overwrite=False):
1075 def get(self, state, overwrite=False):
1076 if overwrite:
1076 if overwrite:
1077 self._svncommand(['revert', '--recursive'])
1077 self._svncommand(['revert', '--recursive'])
1078 args = ['checkout']
1078 args = ['checkout']
1079 if self._svnversion >= (1, 5):
1079 if self._svnversion >= (1, 5):
1080 args.append('--force')
1080 args.append('--force')
1081 # The revision must be specified at the end of the URL to properly
1081 # The revision must be specified at the end of the URL to properly
1082 # update to a directory which has since been deleted and recreated.
1082 # update to a directory which has since been deleted and recreated.
1083 args.append('%s@%s' % (state[0], state[1]))
1083 args.append('%s@%s' % (state[0], state[1]))
1084 status, err = self._svncommand(args, failok=True)
1084 status, err = self._svncommand(args, failok=True)
1085 _sanitize(self.ui, self._ctx._repo.wjoin(self._path), '.svn')
1085 _sanitize(self.ui, self._ctx._repo.wjoin(self._path), '.svn')
1086 if not re.search('Checked out revision [0-9]+.', status):
1086 if not re.search('Checked out revision [0-9]+.', status):
1087 if ('is already a working copy for a different URL' in err
1087 if ('is already a working copy for a different URL' in err
1088 and (self._wcchanged()[:2] == (False, False))):
1088 and (self._wcchanged()[:2] == (False, False))):
1089 # obstructed but clean working copy, so just blow it away.
1089 # obstructed but clean working copy, so just blow it away.
1090 self.remove()
1090 self.remove()
1091 self.get(state, overwrite=False)
1091 self.get(state, overwrite=False)
1092 return
1092 return
1093 raise util.Abort((status or err).splitlines()[-1])
1093 raise util.Abort((status or err).splitlines()[-1])
1094 self.ui.status(status)
1094 self.ui.status(status)
1095
1095
1096 @annotatesubrepoerror
1096 @annotatesubrepoerror
1097 def merge(self, state):
1097 def merge(self, state):
1098 old = self._state[1]
1098 old = self._state[1]
1099 new = state[1]
1099 new = state[1]
1100 wcrev = self._wcrev()
1100 wcrev = self._wcrev()
1101 if new != wcrev:
1101 if new != wcrev:
1102 dirty = old == wcrev or self._wcchanged()[0]
1102 dirty = old == wcrev or self._wcchanged()[0]
1103 if _updateprompt(self.ui, self, dirty, wcrev, new):
1103 if _updateprompt(self.ui, self, dirty, wcrev, new):
1104 self.get(state, False)
1104 self.get(state, False)
1105
1105
1106 def push(self, opts):
1106 def push(self, opts):
1107 # push is a no-op for SVN
1107 # push is a no-op for SVN
1108 return True
1108 return True
1109
1109
1110 @annotatesubrepoerror
1110 @annotatesubrepoerror
1111 def files(self):
1111 def files(self):
1112 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1112 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1113 doc = xml.dom.minidom.parseString(output)
1113 doc = xml.dom.minidom.parseString(output)
1114 paths = []
1114 paths = []
1115 for e in doc.getElementsByTagName('entry'):
1115 for e in doc.getElementsByTagName('entry'):
1116 kind = str(e.getAttribute('kind'))
1116 kind = str(e.getAttribute('kind'))
1117 if kind != 'file':
1117 if kind != 'file':
1118 continue
1118 continue
1119 name = ''.join(c.data for c
1119 name = ''.join(c.data for c
1120 in e.getElementsByTagName('name')[0].childNodes
1120 in e.getElementsByTagName('name')[0].childNodes
1121 if c.nodeType == c.TEXT_NODE)
1121 if c.nodeType == c.TEXT_NODE)
1122 paths.append(name.encode('utf-8'))
1122 paths.append(name.encode('utf-8'))
1123 return paths
1123 return paths
1124
1124
1125 def filedata(self, name):
1125 def filedata(self, name):
1126 return self._svncommand(['cat'], name)[0]
1126 return self._svncommand(['cat'], name)[0]
1127
1127
1128
1128
1129 class gitsubrepo(abstractsubrepo):
1129 class gitsubrepo(abstractsubrepo):
1130 def __init__(self, ctx, path, state):
1130 def __init__(self, ctx, path, state):
1131 super(gitsubrepo, self).__init__(ctx._repo.ui)
1131 super(gitsubrepo, self).__init__(ctx._repo.ui)
1132 self._state = state
1132 self._state = state
1133 self._ctx = ctx
1133 self._ctx = ctx
1134 self._path = path
1134 self._path = path
1135 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1135 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1136 self._abspath = ctx._repo.wjoin(path)
1136 self._abspath = ctx._repo.wjoin(path)
1137 self._subparent = ctx._repo
1137 self._subparent = ctx._repo
1138 self._ensuregit()
1138 self._ensuregit()
1139
1139
1140 def _ensuregit(self):
1140 def _ensuregit(self):
1141 try:
1141 try:
1142 self._gitexecutable = 'git'
1142 self._gitexecutable = 'git'
1143 out, err = self._gitnodir(['--version'])
1143 out, err = self._gitnodir(['--version'])
1144 except OSError, e:
1144 except OSError, e:
1145 if e.errno != 2 or os.name != 'nt':
1145 if e.errno != 2 or os.name != 'nt':
1146 raise
1146 raise
1147 self._gitexecutable = 'git.cmd'
1147 self._gitexecutable = 'git.cmd'
1148 out, err = self._gitnodir(['--version'])
1148 out, err = self._gitnodir(['--version'])
1149 versionstatus = self._checkversion(out)
1149 versionstatus = self._checkversion(out)
1150 if versionstatus == 'unknown':
1150 if versionstatus == 'unknown':
1151 self.ui.warn(_('cannot retrieve git version\n'))
1151 self.ui.warn(_('cannot retrieve git version\n'))
1152 elif versionstatus == 'abort':
1152 elif versionstatus == 'abort':
1153 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1153 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1154 elif versionstatus == 'warning':
1154 elif versionstatus == 'warning':
1155 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1155 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1156
1156
1157 @staticmethod
1157 @staticmethod
1158 def _gitversion(out):
1158 def _gitversion(out):
1159 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1159 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1160 if m:
1160 if m:
1161 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1161 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1162
1162
1163 m = re.search(r'^git version (\d+)\.(\d+)', out)
1163 m = re.search(r'^git version (\d+)\.(\d+)', out)
1164 if m:
1164 if m:
1165 return (int(m.group(1)), int(m.group(2)), 0)
1165 return (int(m.group(1)), int(m.group(2)), 0)
1166
1166
1167 return -1
1167 return -1
1168
1168
1169 @staticmethod
1169 @staticmethod
1170 def _checkversion(out):
1170 def _checkversion(out):
1171 '''ensure git version is new enough
1171 '''ensure git version is new enough
1172
1172
1173 >>> _checkversion = gitsubrepo._checkversion
1173 >>> _checkversion = gitsubrepo._checkversion
1174 >>> _checkversion('git version 1.6.0')
1174 >>> _checkversion('git version 1.6.0')
1175 'ok'
1175 'ok'
1176 >>> _checkversion('git version 1.8.5')
1176 >>> _checkversion('git version 1.8.5')
1177 'ok'
1177 'ok'
1178 >>> _checkversion('git version 1.4.0')
1178 >>> _checkversion('git version 1.4.0')
1179 'abort'
1179 'abort'
1180 >>> _checkversion('git version 1.5.0')
1180 >>> _checkversion('git version 1.5.0')
1181 'warning'
1181 'warning'
1182 >>> _checkversion('git version 1.9-rc0')
1182 >>> _checkversion('git version 1.9-rc0')
1183 'ok'
1183 'ok'
1184 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1184 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1185 'ok'
1185 'ok'
1186 >>> _checkversion('git version 1.9.0.GIT')
1186 >>> _checkversion('git version 1.9.0.GIT')
1187 'ok'
1187 'ok'
1188 >>> _checkversion('git version 12345')
1188 >>> _checkversion('git version 12345')
1189 'unknown'
1189 'unknown'
1190 >>> _checkversion('no')
1190 >>> _checkversion('no')
1191 'unknown'
1191 'unknown'
1192 '''
1192 '''
1193 version = gitsubrepo._gitversion(out)
1193 version = gitsubrepo._gitversion(out)
1194 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1194 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1195 # despite the docstring comment. For now, error on 1.4.0, warn on
1195 # despite the docstring comment. For now, error on 1.4.0, warn on
1196 # 1.5.0 but attempt to continue.
1196 # 1.5.0 but attempt to continue.
1197 if version == -1:
1197 if version == -1:
1198 return 'unknown'
1198 return 'unknown'
1199 if version < (1, 5, 0):
1199 if version < (1, 5, 0):
1200 return 'abort'
1200 return 'abort'
1201 elif version < (1, 6, 0):
1201 elif version < (1, 6, 0):
1202 return 'warning'
1202 return 'warning'
1203 return 'ok'
1203 return 'ok'
1204
1204
1205 def _gitcommand(self, commands, env=None, stream=False):
1205 def _gitcommand(self, commands, env=None, stream=False):
1206 return self._gitdir(commands, env=env, stream=stream)[0]
1206 return self._gitdir(commands, env=env, stream=stream)[0]
1207
1207
1208 def _gitdir(self, commands, env=None, stream=False):
1208 def _gitdir(self, commands, env=None, stream=False):
1209 return self._gitnodir(commands, env=env, stream=stream,
1209 return self._gitnodir(commands, env=env, stream=stream,
1210 cwd=self._abspath)
1210 cwd=self._abspath)
1211
1211
1212 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1212 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1213 """Calls the git command
1213 """Calls the git command
1214
1214
1215 The methods tries to call the git command. versions prior to 1.6.0
1215 The methods tries to call the git command. versions prior to 1.6.0
1216 are not supported and very probably fail.
1216 are not supported and very probably fail.
1217 """
1217 """
1218 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1218 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1219 # unless ui.quiet is set, print git's stderr,
1219 # unless ui.quiet is set, print git's stderr,
1220 # which is mostly progress and useful info
1220 # which is mostly progress and useful info
1221 errpipe = None
1221 errpipe = None
1222 if self.ui.quiet:
1222 if self.ui.quiet:
1223 errpipe = open(os.devnull, 'w')
1223 errpipe = open(os.devnull, 'w')
1224 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1224 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1225 cwd=cwd, env=env, close_fds=util.closefds,
1225 cwd=cwd, env=env, close_fds=util.closefds,
1226 stdout=subprocess.PIPE, stderr=errpipe)
1226 stdout=subprocess.PIPE, stderr=errpipe)
1227 if stream:
1227 if stream:
1228 return p.stdout, None
1228 return p.stdout, None
1229
1229
1230 retdata = p.stdout.read().strip()
1230 retdata = p.stdout.read().strip()
1231 # wait for the child to exit to avoid race condition.
1231 # wait for the child to exit to avoid race condition.
1232 p.wait()
1232 p.wait()
1233
1233
1234 if p.returncode != 0 and p.returncode != 1:
1234 if p.returncode != 0 and p.returncode != 1:
1235 # there are certain error codes that are ok
1235 # there are certain error codes that are ok
1236 command = commands[0]
1236 command = commands[0]
1237 if command in ('cat-file', 'symbolic-ref'):
1237 if command in ('cat-file', 'symbolic-ref'):
1238 return retdata, p.returncode
1238 return retdata, p.returncode
1239 # for all others, abort
1239 # for all others, abort
1240 raise util.Abort('git %s error %d in %s' %
1240 raise util.Abort('git %s error %d in %s' %
1241 (command, p.returncode, self._relpath))
1241 (command, p.returncode, self._relpath))
1242
1242
1243 return retdata, p.returncode
1243 return retdata, p.returncode
1244
1244
1245 def _gitmissing(self):
1245 def _gitmissing(self):
1246 return not os.path.exists(os.path.join(self._abspath, '.git'))
1246 return not os.path.exists(os.path.join(self._abspath, '.git'))
1247
1247
1248 def _gitstate(self):
1248 def _gitstate(self):
1249 return self._gitcommand(['rev-parse', 'HEAD'])
1249 return self._gitcommand(['rev-parse', 'HEAD'])
1250
1250
1251 def _gitcurrentbranch(self):
1251 def _gitcurrentbranch(self):
1252 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1252 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1253 if err:
1253 if err:
1254 current = None
1254 current = None
1255 return current
1255 return current
1256
1256
1257 def _gitremote(self, remote):
1257 def _gitremote(self, remote):
1258 out = self._gitcommand(['remote', 'show', '-n', remote])
1258 out = self._gitcommand(['remote', 'show', '-n', remote])
1259 line = out.split('\n')[1]
1259 line = out.split('\n')[1]
1260 i = line.index('URL: ') + len('URL: ')
1260 i = line.index('URL: ') + len('URL: ')
1261 return line[i:]
1261 return line[i:]
1262
1262
1263 def _githavelocally(self, revision):
1263 def _githavelocally(self, revision):
1264 out, code = self._gitdir(['cat-file', '-e', revision])
1264 out, code = self._gitdir(['cat-file', '-e', revision])
1265 return code == 0
1265 return code == 0
1266
1266
1267 def _gitisancestor(self, r1, r2):
1267 def _gitisancestor(self, r1, r2):
1268 base = self._gitcommand(['merge-base', r1, r2])
1268 base = self._gitcommand(['merge-base', r1, r2])
1269 return base == r1
1269 return base == r1
1270
1270
1271 def _gitisbare(self):
1271 def _gitisbare(self):
1272 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1272 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1273
1273
1274 def _gitupdatestat(self):
1274 def _gitupdatestat(self):
1275 """This must be run before git diff-index.
1275 """This must be run before git diff-index.
1276 diff-index only looks at changes to file stat;
1276 diff-index only looks at changes to file stat;
1277 this command looks at file contents and updates the stat."""
1277 this command looks at file contents and updates the stat."""
1278 self._gitcommand(['update-index', '-q', '--refresh'])
1278 self._gitcommand(['update-index', '-q', '--refresh'])
1279
1279
1280 def _gitbranchmap(self):
1280 def _gitbranchmap(self):
1281 '''returns 2 things:
1281 '''returns 2 things:
1282 a map from git branch to revision
1282 a map from git branch to revision
1283 a map from revision to branches'''
1283 a map from revision to branches'''
1284 branch2rev = {}
1284 branch2rev = {}
1285 rev2branch = {}
1285 rev2branch = {}
1286
1286
1287 out = self._gitcommand(['for-each-ref', '--format',
1287 out = self._gitcommand(['for-each-ref', '--format',
1288 '%(objectname) %(refname)'])
1288 '%(objectname) %(refname)'])
1289 for line in out.split('\n'):
1289 for line in out.split('\n'):
1290 revision, ref = line.split(' ')
1290 revision, ref = line.split(' ')
1291 if (not ref.startswith('refs/heads/') and
1291 if (not ref.startswith('refs/heads/') and
1292 not ref.startswith('refs/remotes/')):
1292 not ref.startswith('refs/remotes/')):
1293 continue
1293 continue
1294 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1294 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1295 continue # ignore remote/HEAD redirects
1295 continue # ignore remote/HEAD redirects
1296 branch2rev[ref] = revision
1296 branch2rev[ref] = revision
1297 rev2branch.setdefault(revision, []).append(ref)
1297 rev2branch.setdefault(revision, []).append(ref)
1298 return branch2rev, rev2branch
1298 return branch2rev, rev2branch
1299
1299
1300 def _gittracking(self, branches):
1300 def _gittracking(self, branches):
1301 'return map of remote branch to local tracking branch'
1301 'return map of remote branch to local tracking branch'
1302 # assumes no more than one local tracking branch for each remote
1302 # assumes no more than one local tracking branch for each remote
1303 tracking = {}
1303 tracking = {}
1304 for b in branches:
1304 for b in branches:
1305 if b.startswith('refs/remotes/'):
1305 if b.startswith('refs/remotes/'):
1306 continue
1306 continue
1307 bname = b.split('/', 2)[2]
1307 bname = b.split('/', 2)[2]
1308 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1308 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1309 if remote:
1309 if remote:
1310 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1310 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1311 tracking['refs/remotes/%s/%s' %
1311 tracking['refs/remotes/%s/%s' %
1312 (remote, ref.split('/', 2)[2])] = b
1312 (remote, ref.split('/', 2)[2])] = b
1313 return tracking
1313 return tracking
1314
1314
1315 def _abssource(self, source):
1315 def _abssource(self, source):
1316 if '://' not in source:
1316 if '://' not in source:
1317 # recognize the scp syntax as an absolute source
1317 # recognize the scp syntax as an absolute source
1318 colon = source.find(':')
1318 colon = source.find(':')
1319 if colon != -1 and '/' not in source[:colon]:
1319 if colon != -1 and '/' not in source[:colon]:
1320 return source
1320 return source
1321 self._subsource = source
1321 self._subsource = source
1322 return _abssource(self)
1322 return _abssource(self)
1323
1323
1324 def _fetch(self, source, revision):
1324 def _fetch(self, source, revision):
1325 if self._gitmissing():
1325 if self._gitmissing():
1326 source = self._abssource(source)
1326 source = self._abssource(source)
1327 self.ui.status(_('cloning subrepo %s from %s\n') %
1327 self.ui.status(_('cloning subrepo %s from %s\n') %
1328 (self._relpath, source))
1328 (self._relpath, source))
1329 self._gitnodir(['clone', source, self._abspath])
1329 self._gitnodir(['clone', source, self._abspath])
1330 if self._githavelocally(revision):
1330 if self._githavelocally(revision):
1331 return
1331 return
1332 self.ui.status(_('pulling subrepo %s from %s\n') %
1332 self.ui.status(_('pulling subrepo %s from %s\n') %
1333 (self._relpath, self._gitremote('origin')))
1333 (self._relpath, self._gitremote('origin')))
1334 # try only origin: the originally cloned repo
1334 # try only origin: the originally cloned repo
1335 self._gitcommand(['fetch'])
1335 self._gitcommand(['fetch'])
1336 if not self._githavelocally(revision):
1336 if not self._githavelocally(revision):
1337 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1337 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1338 (revision, self._relpath))
1338 (revision, self._relpath))
1339
1339
1340 @annotatesubrepoerror
1340 @annotatesubrepoerror
1341 def dirty(self, ignoreupdate=False):
1341 def dirty(self, ignoreupdate=False):
1342 if self._gitmissing():
1342 if self._gitmissing():
1343 return self._state[1] != ''
1343 return self._state[1] != ''
1344 if self._gitisbare():
1344 if self._gitisbare():
1345 return True
1345 return True
1346 if not ignoreupdate and self._state[1] != self._gitstate():
1346 if not ignoreupdate and self._state[1] != self._gitstate():
1347 # different version checked out
1347 # different version checked out
1348 return True
1348 return True
1349 # check for staged changes or modified files; ignore untracked files
1349 # check for staged changes or modified files; ignore untracked files
1350 self._gitupdatestat()
1350 self._gitupdatestat()
1351 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1351 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1352 return code == 1
1352 return code == 1
1353
1353
1354 def basestate(self):
1354 def basestate(self):
1355 return self._gitstate()
1355 return self._gitstate()
1356
1356
1357 @annotatesubrepoerror
1357 @annotatesubrepoerror
1358 def get(self, state, overwrite=False):
1358 def get(self, state, overwrite=False):
1359 source, revision, kind = state
1359 source, revision, kind = state
1360 if not revision:
1360 if not revision:
1361 self.remove()
1361 self.remove()
1362 return
1362 return
1363 self._fetch(source, revision)
1363 self._fetch(source, revision)
1364 # if the repo was set to be bare, unbare it
1364 # if the repo was set to be bare, unbare it
1365 if self._gitisbare():
1365 if self._gitisbare():
1366 self._gitcommand(['config', 'core.bare', 'false'])
1366 self._gitcommand(['config', 'core.bare', 'false'])
1367 if self._gitstate() == revision:
1367 if self._gitstate() == revision:
1368 self._gitcommand(['reset', '--hard', 'HEAD'])
1368 self._gitcommand(['reset', '--hard', 'HEAD'])
1369 return
1369 return
1370 elif self._gitstate() == revision:
1370 elif self._gitstate() == revision:
1371 if overwrite:
1371 if overwrite:
1372 # first reset the index to unmark new files for commit, because
1372 # first reset the index to unmark new files for commit, because
1373 # reset --hard will otherwise throw away files added for commit,
1373 # reset --hard will otherwise throw away files added for commit,
1374 # not just unmark them.
1374 # not just unmark them.
1375 self._gitcommand(['reset', 'HEAD'])
1375 self._gitcommand(['reset', 'HEAD'])
1376 self._gitcommand(['reset', '--hard', 'HEAD'])
1376 self._gitcommand(['reset', '--hard', 'HEAD'])
1377 return
1377 return
1378 branch2rev, rev2branch = self._gitbranchmap()
1378 branch2rev, rev2branch = self._gitbranchmap()
1379
1379
1380 def checkout(args):
1380 def checkout(args):
1381 cmd = ['checkout']
1381 cmd = ['checkout']
1382 if overwrite:
1382 if overwrite:
1383 # first reset the index to unmark new files for commit, because
1383 # first reset the index to unmark new files for commit, because
1384 # the -f option will otherwise throw away files added for
1384 # the -f option will otherwise throw away files added for
1385 # commit, not just unmark them.
1385 # commit, not just unmark them.
1386 self._gitcommand(['reset', 'HEAD'])
1386 self._gitcommand(['reset', 'HEAD'])
1387 cmd.append('-f')
1387 cmd.append('-f')
1388 self._gitcommand(cmd + args)
1388 self._gitcommand(cmd + args)
1389 _sanitize(self.ui, self._abspath, '.git')
1389 _sanitize(self.ui, self._abspath, '.git')
1390
1390
1391 def rawcheckout():
1391 def rawcheckout():
1392 # no branch to checkout, check it out with no branch
1392 # no branch to checkout, check it out with no branch
1393 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1393 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1394 self._relpath)
1394 self._relpath)
1395 self.ui.warn(_('check out a git branch if you intend '
1395 self.ui.warn(_('check out a git branch if you intend '
1396 'to make changes\n'))
1396 'to make changes\n'))
1397 checkout(['-q', revision])
1397 checkout(['-q', revision])
1398
1398
1399 if revision not in rev2branch:
1399 if revision not in rev2branch:
1400 rawcheckout()
1400 rawcheckout()
1401 return
1401 return
1402 branches = rev2branch[revision]
1402 branches = rev2branch[revision]
1403 firstlocalbranch = None
1403 firstlocalbranch = None
1404 for b in branches:
1404 for b in branches:
1405 if b == 'refs/heads/master':
1405 if b == 'refs/heads/master':
1406 # master trumps all other branches
1406 # master trumps all other branches
1407 checkout(['refs/heads/master'])
1407 checkout(['refs/heads/master'])
1408 return
1408 return
1409 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1409 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1410 firstlocalbranch = b
1410 firstlocalbranch = b
1411 if firstlocalbranch:
1411 if firstlocalbranch:
1412 checkout([firstlocalbranch])
1412 checkout([firstlocalbranch])
1413 return
1413 return
1414
1414
1415 tracking = self._gittracking(branch2rev.keys())
1415 tracking = self._gittracking(branch2rev.keys())
1416 # choose a remote branch already tracked if possible
1416 # choose a remote branch already tracked if possible
1417 remote = branches[0]
1417 remote = branches[0]
1418 if remote not in tracking:
1418 if remote not in tracking:
1419 for b in branches:
1419 for b in branches:
1420 if b in tracking:
1420 if b in tracking:
1421 remote = b
1421 remote = b
1422 break
1422 break
1423
1423
1424 if remote not in tracking:
1424 if remote not in tracking:
1425 # create a new local tracking branch
1425 # create a new local tracking branch
1426 local = remote.split('/', 3)[3]
1426 local = remote.split('/', 3)[3]
1427 checkout(['-b', local, remote])
1427 checkout(['-b', local, remote])
1428 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1428 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1429 # When updating to a tracked remote branch,
1429 # When updating to a tracked remote branch,
1430 # if the local tracking branch is downstream of it,
1430 # if the local tracking branch is downstream of it,
1431 # a normal `git pull` would have performed a "fast-forward merge"
1431 # a normal `git pull` would have performed a "fast-forward merge"
1432 # which is equivalent to updating the local branch to the remote.
1432 # which is equivalent to updating the local branch to the remote.
1433 # Since we are only looking at branching at update, we need to
1433 # Since we are only looking at branching at update, we need to
1434 # detect this situation and perform this action lazily.
1434 # detect this situation and perform this action lazily.
1435 if tracking[remote] != self._gitcurrentbranch():
1435 if tracking[remote] != self._gitcurrentbranch():
1436 checkout([tracking[remote]])
1436 checkout([tracking[remote]])
1437 self._gitcommand(['merge', '--ff', remote])
1437 self._gitcommand(['merge', '--ff', remote])
1438 _sanitize(self.ui, self._abspath, '.git')
1438 _sanitize(self.ui, self._abspath, '.git')
1439 else:
1439 else:
1440 # a real merge would be required, just checkout the revision
1440 # a real merge would be required, just checkout the revision
1441 rawcheckout()
1441 rawcheckout()
1442
1442
1443 @annotatesubrepoerror
1443 @annotatesubrepoerror
1444 def commit(self, text, user, date):
1444 def commit(self, text, user, date):
1445 if self._gitmissing():
1445 if self._gitmissing():
1446 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1446 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1447 cmd = ['commit', '-a', '-m', text]
1447 cmd = ['commit', '-a', '-m', text]
1448 env = os.environ.copy()
1448 env = os.environ.copy()
1449 if user:
1449 if user:
1450 cmd += ['--author', user]
1450 cmd += ['--author', user]
1451 if date:
1451 if date:
1452 # git's date parser silently ignores when seconds < 1e9
1452 # git's date parser silently ignores when seconds < 1e9
1453 # convert to ISO8601
1453 # convert to ISO8601
1454 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1454 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1455 '%Y-%m-%dT%H:%M:%S %1%2')
1455 '%Y-%m-%dT%H:%M:%S %1%2')
1456 self._gitcommand(cmd, env=env)
1456 self._gitcommand(cmd, env=env)
1457 # make sure commit works otherwise HEAD might not exist under certain
1457 # make sure commit works otherwise HEAD might not exist under certain
1458 # circumstances
1458 # circumstances
1459 return self._gitstate()
1459 return self._gitstate()
1460
1460
1461 @annotatesubrepoerror
1461 @annotatesubrepoerror
1462 def merge(self, state):
1462 def merge(self, state):
1463 source, revision, kind = state
1463 source, revision, kind = state
1464 self._fetch(source, revision)
1464 self._fetch(source, revision)
1465 base = self._gitcommand(['merge-base', revision, self._state[1]])
1465 base = self._gitcommand(['merge-base', revision, self._state[1]])
1466 self._gitupdatestat()
1466 self._gitupdatestat()
1467 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1467 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1468
1468
1469 def mergefunc():
1469 def mergefunc():
1470 if base == revision:
1470 if base == revision:
1471 self.get(state) # fast forward merge
1471 self.get(state) # fast forward merge
1472 elif base != self._state[1]:
1472 elif base != self._state[1]:
1473 self._gitcommand(['merge', '--no-commit', revision])
1473 self._gitcommand(['merge', '--no-commit', revision])
1474 _sanitize(self.ui, self._abspath, '.git')
1474 _sanitize(self.ui, self._abspath, '.git')
1475
1475
1476 if self.dirty():
1476 if self.dirty():
1477 if self._gitstate() != revision:
1477 if self._gitstate() != revision:
1478 dirty = self._gitstate() == self._state[1] or code != 0
1478 dirty = self._gitstate() == self._state[1] or code != 0
1479 if _updateprompt(self.ui, self, dirty,
1479 if _updateprompt(self.ui, self, dirty,
1480 self._state[1][:7], revision[:7]):
1480 self._state[1][:7], revision[:7]):
1481 mergefunc()
1481 mergefunc()
1482 else:
1482 else:
1483 mergefunc()
1483 mergefunc()
1484
1484
1485 @annotatesubrepoerror
1485 @annotatesubrepoerror
1486 def push(self, opts):
1486 def push(self, opts):
1487 force = opts.get('force')
1487 force = opts.get('force')
1488
1488
1489 if not self._state[1]:
1489 if not self._state[1]:
1490 return True
1490 return True
1491 if self._gitmissing():
1491 if self._gitmissing():
1492 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1492 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1493 # if a branch in origin contains the revision, nothing to do
1493 # if a branch in origin contains the revision, nothing to do
1494 branch2rev, rev2branch = self._gitbranchmap()
1494 branch2rev, rev2branch = self._gitbranchmap()
1495 if self._state[1] in rev2branch:
1495 if self._state[1] in rev2branch:
1496 for b in rev2branch[self._state[1]]:
1496 for b in rev2branch[self._state[1]]:
1497 if b.startswith('refs/remotes/origin/'):
1497 if b.startswith('refs/remotes/origin/'):
1498 return True
1498 return True
1499 for b, revision in branch2rev.iteritems():
1499 for b, revision in branch2rev.iteritems():
1500 if b.startswith('refs/remotes/origin/'):
1500 if b.startswith('refs/remotes/origin/'):
1501 if self._gitisancestor(self._state[1], revision):
1501 if self._gitisancestor(self._state[1], revision):
1502 return True
1502 return True
1503 # otherwise, try to push the currently checked out branch
1503 # otherwise, try to push the currently checked out branch
1504 cmd = ['push']
1504 cmd = ['push']
1505 if force:
1505 if force:
1506 cmd.append('--force')
1506 cmd.append('--force')
1507
1507
1508 current = self._gitcurrentbranch()
1508 current = self._gitcurrentbranch()
1509 if current:
1509 if current:
1510 # determine if the current branch is even useful
1510 # determine if the current branch is even useful
1511 if not self._gitisancestor(self._state[1], current):
1511 if not self._gitisancestor(self._state[1], current):
1512 self.ui.warn(_('unrelated git branch checked out '
1512 self.ui.warn(_('unrelated git branch checked out '
1513 'in subrepo %s\n') % self._relpath)
1513 'in subrepo %s\n') % self._relpath)
1514 return False
1514 return False
1515 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1515 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1516 (current.split('/', 2)[2], self._relpath))
1516 (current.split('/', 2)[2], self._relpath))
1517 ret = self._gitdir(cmd + ['origin', current])
1517 ret = self._gitdir(cmd + ['origin', current])
1518 return ret[1] == 0
1518 return ret[1] == 0
1519 else:
1519 else:
1520 self.ui.warn(_('no branch checked out in subrepo %s\n'
1520 self.ui.warn(_('no branch checked out in subrepo %s\n'
1521 'cannot push revision %s\n') %
1521 'cannot push revision %s\n') %
1522 (self._relpath, self._state[1]))
1522 (self._relpath, self._state[1]))
1523 return False
1523 return False
1524
1524
1525 @annotatesubrepoerror
1525 @annotatesubrepoerror
1526 def remove(self):
1526 def remove(self):
1527 if self._gitmissing():
1527 if self._gitmissing():
1528 return
1528 return
1529 if self.dirty():
1529 if self.dirty():
1530 self.ui.warn(_('not removing repo %s because '
1530 self.ui.warn(_('not removing repo %s because '
1531 'it has changes.\n') % self._relpath)
1531 'it has changes.\n') % self._relpath)
1532 return
1532 return
1533 # we can't fully delete the repository as it may contain
1533 # we can't fully delete the repository as it may contain
1534 # local-only history
1534 # local-only history
1535 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1535 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1536 self._gitcommand(['config', 'core.bare', 'true'])
1536 self._gitcommand(['config', 'core.bare', 'true'])
1537 for f in os.listdir(self._abspath):
1537 for f in os.listdir(self._abspath):
1538 if f == '.git':
1538 if f == '.git':
1539 continue
1539 continue
1540 path = os.path.join(self._abspath, f)
1540 path = os.path.join(self._abspath, f)
1541 if os.path.isdir(path) and not os.path.islink(path):
1541 if os.path.isdir(path) and not os.path.islink(path):
1542 shutil.rmtree(path)
1542 shutil.rmtree(path)
1543 else:
1543 else:
1544 os.remove(path)
1544 os.remove(path)
1545
1545
1546 def archive(self, archiver, prefix, match=None):
1546 def archive(self, archiver, prefix, match=None):
1547 total = 0
1547 total = 0
1548 source, revision = self._state
1548 source, revision = self._state
1549 if not revision:
1549 if not revision:
1550 return total
1550 return total
1551 self._fetch(source, revision)
1551 self._fetch(source, revision)
1552
1552
1553 # Parse git's native archive command.
1553 # Parse git's native archive command.
1554 # This should be much faster than manually traversing the trees
1554 # This should be much faster than manually traversing the trees
1555 # and objects with many subprocess calls.
1555 # and objects with many subprocess calls.
1556 tarstream = self._gitcommand(['archive', revision], stream=True)
1556 tarstream = self._gitcommand(['archive', revision], stream=True)
1557 tar = tarfile.open(fileobj=tarstream, mode='r|')
1557 tar = tarfile.open(fileobj=tarstream, mode='r|')
1558 relpath = subrelpath(self)
1558 relpath = subrelpath(self)
1559 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1559 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1560 for i, info in enumerate(tar):
1560 for i, info in enumerate(tar):
1561 if info.isdir():
1561 if info.isdir():
1562 continue
1562 continue
1563 if match and not match(info.name):
1563 if match and not match(info.name):
1564 continue
1564 continue
1565 if info.issym():
1565 if info.issym():
1566 data = info.linkname
1566 data = info.linkname
1567 else:
1567 else:
1568 data = tar.extractfile(info).read()
1568 data = tar.extractfile(info).read()
1569 archiver.addfile(os.path.join(prefix, self._path, info.name),
1569 archiver.addfile(os.path.join(prefix, self._path, info.name),
1570 info.mode, info.issym(), data)
1570 info.mode, info.issym(), data)
1571 total += 1
1571 total += 1
1572 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1572 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1573 unit=_('files'))
1573 unit=_('files'))
1574 self.ui.progress(_('archiving (%s)') % relpath, None)
1574 self.ui.progress(_('archiving (%s)') % relpath, None)
1575 return total
1575 return total
1576
1576
1577
1577
1578 @annotatesubrepoerror
1578 @annotatesubrepoerror
1579 def status(self, rev2, **opts):
1579 def status(self, rev2, **opts):
1580 rev1 = self._state[1]
1580 rev1 = self._state[1]
1581 if self._gitmissing() or not rev1:
1581 if self._gitmissing() or not rev1:
1582 # if the repo is missing, return no results
1582 # if the repo is missing, return no results
1583 return [], [], [], [], [], [], []
1583 return [], [], [], [], [], [], []
1584 modified, added, removed = [], [], []
1584 modified, added, removed = [], [], []
1585 self._gitupdatestat()
1585 self._gitupdatestat()
1586 if rev2:
1586 if rev2:
1587 command = ['diff-tree', rev1, rev2]
1587 command = ['diff-tree', rev1, rev2]
1588 else:
1588 else:
1589 command = ['diff-index', rev1]
1589 command = ['diff-index', rev1]
1590 out = self._gitcommand(command)
1590 out = self._gitcommand(command)
1591 for line in out.split('\n'):
1591 for line in out.split('\n'):
1592 tab = line.find('\t')
1592 tab = line.find('\t')
1593 if tab == -1:
1593 if tab == -1:
1594 continue
1594 continue
1595 status, f = line[tab - 1], line[tab + 1:]
1595 status, f = line[tab - 1], line[tab + 1:]
1596 if status == 'M':
1596 if status == 'M':
1597 modified.append(f)
1597 modified.append(f)
1598 elif status == 'A':
1598 elif status == 'A':
1599 added.append(f)
1599 added.append(f)
1600 elif status == 'D':
1600 elif status == 'D':
1601 removed.append(f)
1601 removed.append(f)
1602
1602
1603 deleted, unknown, ignored, clean = [], [], [], []
1603 deleted, unknown, ignored, clean = [], [], [], []
1604
1604
1605 if not rev2:
1605 if not rev2:
1606 command = ['ls-files', '--others', '--exclude-standard']
1606 command = ['ls-files', '--others', '--exclude-standard']
1607 out = self._gitcommand(command)
1607 out = self._gitcommand(command)
1608 for line in out.split('\n'):
1608 for line in out.split('\n'):
1609 if len(line) == 0:
1609 if len(line) == 0:
1610 continue
1610 continue
1611 unknown.append(line)
1611 unknown.append(line)
1612
1612
1613 return scmutil.status(modified, added, removed, deleted,
1613 return scmutil.status(modified, added, removed, deleted,
1614 unknown, ignored, clean)
1614 unknown, ignored, clean)
1615
1615
1616 @annotatesubrepoerror
1616 @annotatesubrepoerror
1617 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1617 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1618 node1 = self._state[1]
1618 node1 = self._state[1]
1619 cmd = ['diff']
1619 cmd = ['diff']
1620 if opts['stat']:
1620 if opts['stat']:
1621 cmd.append('--stat')
1621 cmd.append('--stat')
1622 else:
1622 else:
1623 # for Git, this also implies '-p'
1623 # for Git, this also implies '-p'
1624 cmd.append('-U%d' % diffopts.context)
1624 cmd.append('-U%d' % diffopts.context)
1625
1625
1626 gitprefix = os.path.join(prefix, self._path)
1626 gitprefix = os.path.join(prefix, self._path)
1627
1627
1628 if diffopts.noprefix:
1628 if diffopts.noprefix:
1629 cmd.extend(['--src-prefix=%s/' % gitprefix,
1629 cmd.extend(['--src-prefix=%s/' % gitprefix,
1630 '--dst-prefix=%s/' % gitprefix])
1630 '--dst-prefix=%s/' % gitprefix])
1631 else:
1631 else:
1632 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1632 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1633 '--dst-prefix=b/%s/' % gitprefix])
1633 '--dst-prefix=b/%s/' % gitprefix])
1634
1634
1635 if diffopts.ignorews:
1635 if diffopts.ignorews:
1636 cmd.append('--ignore-all-space')
1636 cmd.append('--ignore-all-space')
1637 if diffopts.ignorewsamount:
1637 if diffopts.ignorewsamount:
1638 cmd.append('--ignore-space-change')
1638 cmd.append('--ignore-space-change')
1639 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1639 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1640 and diffopts.ignoreblanklines:
1640 and diffopts.ignoreblanklines:
1641 cmd.append('--ignore-blank-lines')
1641 cmd.append('--ignore-blank-lines')
1642
1642
1643 cmd.append(node1)
1643 cmd.append(node1)
1644 if node2:
1644 if node2:
1645 cmd.append(node2)
1645 cmd.append(node2)
1646
1646
1647 if match.anypats():
1647 if match.anypats():
1648 return #No support for include/exclude yet
1648 return #No support for include/exclude yet
1649
1649
1650 if match.always():
1650 if match.always():
1651 ui.write(self._gitcommand(cmd))
1651 ui.write(self._gitcommand(cmd))
1652 elif match.files():
1652 elif match.files():
1653 for f in match.files():
1653 for f in match.files():
1654 ui.write(self._gitcommand(cmd + [f]))
1654 ui.write(self._gitcommand(cmd + [f]))
1655 elif match(gitprefix): #Subrepo is matched
1655 elif match(gitprefix): #Subrepo is matched
1656 ui.write(self._gitcommand(cmd))
1656 ui.write(self._gitcommand(cmd))
1657
1657
1658 def revert(self, ui, substate, *pats, **opts):
1658 def revert(self, ui, substate, *pats, **opts):
1659 ui.status(_('reverting subrepo %s\n') % substate[0])
1659 ui.status(_('reverting subrepo %s\n') % substate[0])
1660 if not opts.get('no_backup'):
1660 if not opts.get('no_backup'):
1661 ui.warn('%s: reverting %s subrepos without '
1661 ui.warn('%s: reverting %s subrepos without '
1662 '--no-backup is unsupported\n'
1662 '--no-backup is unsupported\n'
1663 % (substate[0], substate[2]))
1663 % (substate[0], substate[2]))
1664 return []
1664 return []
1665
1665
1666 self.get(substate, overwrite=True)
1666 self.get(substate, overwrite=True)
1667 return []
1667 return []
1668
1668
1669 def shortid(self, revid):
1669 def shortid(self, revid):
1670 return revid[:7]
1670 return revid[:7]
1671
1671
1672 types = {
1672 types = {
1673 'hg': hgsubrepo,
1673 'hg': hgsubrepo,
1674 'svn': svnsubrepo,
1674 'svn': svnsubrepo,
1675 'git': gitsubrepo,
1675 'git': gitsubrepo,
1676 }
1676 }
General Comments 0
You need to be logged in to leave comments. Login now