##// END OF EJS Templates
subrepo: add argument to "diff()" to pass "ui" of caller side (issue3712) (API)...
FUJIWARA Katsunori -
r18006:0c10cf81 2.4.1 stable
parent child Browse files
Show More
@@ -1,2022 +1,2022
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 subrepo, context, repair, bookmarks, graphmod, revset, phases, obsolete
13 import subrepo, context, repair, bookmarks, graphmod, revset, phases, obsolete
14 import changelog
14 import changelog
15 import lock as lockmod
15 import lock as lockmod
16
16
17 def parsealiases(cmd):
17 def parsealiases(cmd):
18 return cmd.lstrip("^").split("|")
18 return cmd.lstrip("^").split("|")
19
19
20 def findpossible(cmd, table, strict=False):
20 def findpossible(cmd, table, strict=False):
21 """
21 """
22 Return cmd -> (aliases, command table entry)
22 Return cmd -> (aliases, command table entry)
23 for each matching command.
23 for each matching command.
24 Return debug commands (or their aliases) only if no normal command matches.
24 Return debug commands (or their aliases) only if no normal command matches.
25 """
25 """
26 choice = {}
26 choice = {}
27 debugchoice = {}
27 debugchoice = {}
28
28
29 if cmd in table:
29 if cmd in table:
30 # short-circuit exact matches, "log" alias beats "^log|history"
30 # short-circuit exact matches, "log" alias beats "^log|history"
31 keys = [cmd]
31 keys = [cmd]
32 else:
32 else:
33 keys = table.keys()
33 keys = table.keys()
34
34
35 for e in keys:
35 for e in keys:
36 aliases = parsealiases(e)
36 aliases = parsealiases(e)
37 found = None
37 found = None
38 if cmd in aliases:
38 if cmd in aliases:
39 found = cmd
39 found = cmd
40 elif not strict:
40 elif not strict:
41 for a in aliases:
41 for a in aliases:
42 if a.startswith(cmd):
42 if a.startswith(cmd):
43 found = a
43 found = a
44 break
44 break
45 if found is not None:
45 if found is not None:
46 if aliases[0].startswith("debug") or found.startswith("debug"):
46 if aliases[0].startswith("debug") or found.startswith("debug"):
47 debugchoice[found] = (aliases, table[e])
47 debugchoice[found] = (aliases, table[e])
48 else:
48 else:
49 choice[found] = (aliases, table[e])
49 choice[found] = (aliases, table[e])
50
50
51 if not choice and debugchoice:
51 if not choice and debugchoice:
52 choice = debugchoice
52 choice = debugchoice
53
53
54 return choice
54 return choice
55
55
56 def findcmd(cmd, table, strict=True):
56 def findcmd(cmd, table, strict=True):
57 """Return (aliases, command table entry) for command string."""
57 """Return (aliases, command table entry) for command string."""
58 choice = findpossible(cmd, table, strict)
58 choice = findpossible(cmd, table, strict)
59
59
60 if cmd in choice:
60 if cmd in choice:
61 return choice[cmd]
61 return choice[cmd]
62
62
63 if len(choice) > 1:
63 if len(choice) > 1:
64 clist = choice.keys()
64 clist = choice.keys()
65 clist.sort()
65 clist.sort()
66 raise error.AmbiguousCommand(cmd, clist)
66 raise error.AmbiguousCommand(cmd, clist)
67
67
68 if choice:
68 if choice:
69 return choice.values()[0]
69 return choice.values()[0]
70
70
71 raise error.UnknownCommand(cmd)
71 raise error.UnknownCommand(cmd)
72
72
73 def findrepo(p):
73 def findrepo(p):
74 while not os.path.isdir(os.path.join(p, ".hg")):
74 while not os.path.isdir(os.path.join(p, ".hg")):
75 oldp, p = p, os.path.dirname(p)
75 oldp, p = p, os.path.dirname(p)
76 if p == oldp:
76 if p == oldp:
77 return None
77 return None
78
78
79 return p
79 return p
80
80
81 def bailifchanged(repo):
81 def bailifchanged(repo):
82 if repo.dirstate.p2() != nullid:
82 if repo.dirstate.p2() != nullid:
83 raise util.Abort(_('outstanding uncommitted merge'))
83 raise util.Abort(_('outstanding uncommitted merge'))
84 modified, added, removed, deleted = repo.status()[:4]
84 modified, added, removed, deleted = repo.status()[:4]
85 if modified or added or removed or deleted:
85 if modified or added or removed or deleted:
86 raise util.Abort(_("outstanding uncommitted changes"))
86 raise util.Abort(_("outstanding uncommitted changes"))
87 ctx = repo[None]
87 ctx = repo[None]
88 for s in ctx.substate:
88 for s in ctx.substate:
89 if ctx.sub(s).dirty():
89 if ctx.sub(s).dirty():
90 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
90 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
91
91
92 def logmessage(ui, opts):
92 def logmessage(ui, opts):
93 """ get the log message according to -m and -l option """
93 """ get the log message according to -m and -l option """
94 message = opts.get('message')
94 message = opts.get('message')
95 logfile = opts.get('logfile')
95 logfile = opts.get('logfile')
96
96
97 if message and logfile:
97 if message and logfile:
98 raise util.Abort(_('options --message and --logfile are mutually '
98 raise util.Abort(_('options --message and --logfile are mutually '
99 'exclusive'))
99 'exclusive'))
100 if not message and logfile:
100 if not message and logfile:
101 try:
101 try:
102 if logfile == '-':
102 if logfile == '-':
103 message = ui.fin.read()
103 message = ui.fin.read()
104 else:
104 else:
105 message = '\n'.join(util.readfile(logfile).splitlines())
105 message = '\n'.join(util.readfile(logfile).splitlines())
106 except IOError, inst:
106 except IOError, inst:
107 raise util.Abort(_("can't read commit message '%s': %s") %
107 raise util.Abort(_("can't read commit message '%s': %s") %
108 (logfile, inst.strerror))
108 (logfile, inst.strerror))
109 return message
109 return message
110
110
111 def loglimit(opts):
111 def loglimit(opts):
112 """get the log limit according to option -l/--limit"""
112 """get the log limit according to option -l/--limit"""
113 limit = opts.get('limit')
113 limit = opts.get('limit')
114 if limit:
114 if limit:
115 try:
115 try:
116 limit = int(limit)
116 limit = int(limit)
117 except ValueError:
117 except ValueError:
118 raise util.Abort(_('limit must be a positive integer'))
118 raise util.Abort(_('limit must be a positive integer'))
119 if limit <= 0:
119 if limit <= 0:
120 raise util.Abort(_('limit must be positive'))
120 raise util.Abort(_('limit must be positive'))
121 else:
121 else:
122 limit = None
122 limit = None
123 return limit
123 return limit
124
124
125 def makefilename(repo, pat, node, desc=None,
125 def makefilename(repo, pat, node, desc=None,
126 total=None, seqno=None, revwidth=None, pathname=None):
126 total=None, seqno=None, revwidth=None, pathname=None):
127 node_expander = {
127 node_expander = {
128 'H': lambda: hex(node),
128 'H': lambda: hex(node),
129 'R': lambda: str(repo.changelog.rev(node)),
129 'R': lambda: str(repo.changelog.rev(node)),
130 'h': lambda: short(node),
130 'h': lambda: short(node),
131 'm': lambda: re.sub('[^\w]', '_', str(desc))
131 'm': lambda: re.sub('[^\w]', '_', str(desc))
132 }
132 }
133 expander = {
133 expander = {
134 '%': lambda: '%',
134 '%': lambda: '%',
135 'b': lambda: os.path.basename(repo.root),
135 'b': lambda: os.path.basename(repo.root),
136 }
136 }
137
137
138 try:
138 try:
139 if node:
139 if node:
140 expander.update(node_expander)
140 expander.update(node_expander)
141 if node:
141 if node:
142 expander['r'] = (lambda:
142 expander['r'] = (lambda:
143 str(repo.changelog.rev(node)).zfill(revwidth or 0))
143 str(repo.changelog.rev(node)).zfill(revwidth or 0))
144 if total is not None:
144 if total is not None:
145 expander['N'] = lambda: str(total)
145 expander['N'] = lambda: str(total)
146 if seqno is not None:
146 if seqno is not None:
147 expander['n'] = lambda: str(seqno)
147 expander['n'] = lambda: str(seqno)
148 if total is not None and seqno is not None:
148 if total is not None and seqno is not None:
149 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
149 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
150 if pathname is not None:
150 if pathname is not None:
151 expander['s'] = lambda: os.path.basename(pathname)
151 expander['s'] = lambda: os.path.basename(pathname)
152 expander['d'] = lambda: os.path.dirname(pathname) or '.'
152 expander['d'] = lambda: os.path.dirname(pathname) or '.'
153 expander['p'] = lambda: pathname
153 expander['p'] = lambda: pathname
154
154
155 newname = []
155 newname = []
156 patlen = len(pat)
156 patlen = len(pat)
157 i = 0
157 i = 0
158 while i < patlen:
158 while i < patlen:
159 c = pat[i]
159 c = pat[i]
160 if c == '%':
160 if c == '%':
161 i += 1
161 i += 1
162 c = pat[i]
162 c = pat[i]
163 c = expander[c]()
163 c = expander[c]()
164 newname.append(c)
164 newname.append(c)
165 i += 1
165 i += 1
166 return ''.join(newname)
166 return ''.join(newname)
167 except KeyError, inst:
167 except KeyError, inst:
168 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
168 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
169 inst.args[0])
169 inst.args[0])
170
170
171 def makefileobj(repo, pat, node=None, desc=None, total=None,
171 def makefileobj(repo, pat, node=None, desc=None, total=None,
172 seqno=None, revwidth=None, mode='wb', pathname=None):
172 seqno=None, revwidth=None, mode='wb', pathname=None):
173
173
174 writable = mode not in ('r', 'rb')
174 writable = mode not in ('r', 'rb')
175
175
176 if not pat or pat == '-':
176 if not pat or pat == '-':
177 fp = writable and repo.ui.fout or repo.ui.fin
177 fp = writable and repo.ui.fout or repo.ui.fin
178 if util.safehasattr(fp, 'fileno'):
178 if util.safehasattr(fp, 'fileno'):
179 return os.fdopen(os.dup(fp.fileno()), mode)
179 return os.fdopen(os.dup(fp.fileno()), mode)
180 else:
180 else:
181 # if this fp can't be duped properly, return
181 # if this fp can't be duped properly, return
182 # a dummy object that can be closed
182 # a dummy object that can be closed
183 class wrappedfileobj(object):
183 class wrappedfileobj(object):
184 noop = lambda x: None
184 noop = lambda x: None
185 def __init__(self, f):
185 def __init__(self, f):
186 self.f = f
186 self.f = f
187 def __getattr__(self, attr):
187 def __getattr__(self, attr):
188 if attr == 'close':
188 if attr == 'close':
189 return self.noop
189 return self.noop
190 else:
190 else:
191 return getattr(self.f, attr)
191 return getattr(self.f, attr)
192
192
193 return wrappedfileobj(fp)
193 return wrappedfileobj(fp)
194 if util.safehasattr(pat, 'write') and writable:
194 if util.safehasattr(pat, 'write') and writable:
195 return pat
195 return pat
196 if util.safehasattr(pat, 'read') and 'r' in mode:
196 if util.safehasattr(pat, 'read') and 'r' in mode:
197 return pat
197 return pat
198 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
198 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
199 pathname),
199 pathname),
200 mode)
200 mode)
201
201
202 def openrevlog(repo, cmd, file_, opts):
202 def openrevlog(repo, cmd, file_, opts):
203 """opens the changelog, manifest, a filelog or a given revlog"""
203 """opens the changelog, manifest, a filelog or a given revlog"""
204 cl = opts['changelog']
204 cl = opts['changelog']
205 mf = opts['manifest']
205 mf = opts['manifest']
206 msg = None
206 msg = None
207 if cl and mf:
207 if cl and mf:
208 msg = _('cannot specify --changelog and --manifest at the same time')
208 msg = _('cannot specify --changelog and --manifest at the same time')
209 elif cl or mf:
209 elif cl or mf:
210 if file_:
210 if file_:
211 msg = _('cannot specify filename with --changelog or --manifest')
211 msg = _('cannot specify filename with --changelog or --manifest')
212 elif not repo:
212 elif not repo:
213 msg = _('cannot specify --changelog or --manifest '
213 msg = _('cannot specify --changelog or --manifest '
214 'without a repository')
214 'without a repository')
215 if msg:
215 if msg:
216 raise util.Abort(msg)
216 raise util.Abort(msg)
217
217
218 r = None
218 r = None
219 if repo:
219 if repo:
220 if cl:
220 if cl:
221 r = repo.changelog
221 r = repo.changelog
222 elif mf:
222 elif mf:
223 r = repo.manifest
223 r = repo.manifest
224 elif file_:
224 elif file_:
225 filelog = repo.file(file_)
225 filelog = repo.file(file_)
226 if len(filelog):
226 if len(filelog):
227 r = filelog
227 r = filelog
228 if not r:
228 if not r:
229 if not file_:
229 if not file_:
230 raise error.CommandError(cmd, _('invalid arguments'))
230 raise error.CommandError(cmd, _('invalid arguments'))
231 if not os.path.isfile(file_):
231 if not os.path.isfile(file_):
232 raise util.Abort(_("revlog '%s' not found") % file_)
232 raise util.Abort(_("revlog '%s' not found") % file_)
233 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
233 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
234 file_[:-2] + ".i")
234 file_[:-2] + ".i")
235 return r
235 return r
236
236
237 def copy(ui, repo, pats, opts, rename=False):
237 def copy(ui, repo, pats, opts, rename=False):
238 # called with the repo lock held
238 # called with the repo lock held
239 #
239 #
240 # hgsep => pathname that uses "/" to separate directories
240 # hgsep => pathname that uses "/" to separate directories
241 # ossep => pathname that uses os.sep to separate directories
241 # ossep => pathname that uses os.sep to separate directories
242 cwd = repo.getcwd()
242 cwd = repo.getcwd()
243 targets = {}
243 targets = {}
244 after = opts.get("after")
244 after = opts.get("after")
245 dryrun = opts.get("dry_run")
245 dryrun = opts.get("dry_run")
246 wctx = repo[None]
246 wctx = repo[None]
247
247
248 def walkpat(pat):
248 def walkpat(pat):
249 srcs = []
249 srcs = []
250 badstates = after and '?' or '?r'
250 badstates = after and '?' or '?r'
251 m = scmutil.match(repo[None], [pat], opts, globbed=True)
251 m = scmutil.match(repo[None], [pat], opts, globbed=True)
252 for abs in repo.walk(m):
252 for abs in repo.walk(m):
253 state = repo.dirstate[abs]
253 state = repo.dirstate[abs]
254 rel = m.rel(abs)
254 rel = m.rel(abs)
255 exact = m.exact(abs)
255 exact = m.exact(abs)
256 if state in badstates:
256 if state in badstates:
257 if exact and state == '?':
257 if exact and state == '?':
258 ui.warn(_('%s: not copying - file is not managed\n') % rel)
258 ui.warn(_('%s: not copying - file is not managed\n') % rel)
259 if exact and state == 'r':
259 if exact and state == 'r':
260 ui.warn(_('%s: not copying - file has been marked for'
260 ui.warn(_('%s: not copying - file has been marked for'
261 ' remove\n') % rel)
261 ' remove\n') % rel)
262 continue
262 continue
263 # abs: hgsep
263 # abs: hgsep
264 # rel: ossep
264 # rel: ossep
265 srcs.append((abs, rel, exact))
265 srcs.append((abs, rel, exact))
266 return srcs
266 return srcs
267
267
268 # abssrc: hgsep
268 # abssrc: hgsep
269 # relsrc: ossep
269 # relsrc: ossep
270 # otarget: ossep
270 # otarget: ossep
271 def copyfile(abssrc, relsrc, otarget, exact):
271 def copyfile(abssrc, relsrc, otarget, exact):
272 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
272 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
273 if '/' in abstarget:
273 if '/' in abstarget:
274 # We cannot normalize abstarget itself, this would prevent
274 # We cannot normalize abstarget itself, this would prevent
275 # case only renames, like a => A.
275 # case only renames, like a => A.
276 abspath, absname = abstarget.rsplit('/', 1)
276 abspath, absname = abstarget.rsplit('/', 1)
277 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
277 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
278 reltarget = repo.pathto(abstarget, cwd)
278 reltarget = repo.pathto(abstarget, cwd)
279 target = repo.wjoin(abstarget)
279 target = repo.wjoin(abstarget)
280 src = repo.wjoin(abssrc)
280 src = repo.wjoin(abssrc)
281 state = repo.dirstate[abstarget]
281 state = repo.dirstate[abstarget]
282
282
283 scmutil.checkportable(ui, abstarget)
283 scmutil.checkportable(ui, abstarget)
284
284
285 # check for collisions
285 # check for collisions
286 prevsrc = targets.get(abstarget)
286 prevsrc = targets.get(abstarget)
287 if prevsrc is not None:
287 if prevsrc is not None:
288 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
288 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
289 (reltarget, repo.pathto(abssrc, cwd),
289 (reltarget, repo.pathto(abssrc, cwd),
290 repo.pathto(prevsrc, cwd)))
290 repo.pathto(prevsrc, cwd)))
291 return
291 return
292
292
293 # check for overwrites
293 # check for overwrites
294 exists = os.path.lexists(target)
294 exists = os.path.lexists(target)
295 samefile = False
295 samefile = False
296 if exists and abssrc != abstarget:
296 if exists and abssrc != abstarget:
297 if (repo.dirstate.normalize(abssrc) ==
297 if (repo.dirstate.normalize(abssrc) ==
298 repo.dirstate.normalize(abstarget)):
298 repo.dirstate.normalize(abstarget)):
299 if not rename:
299 if not rename:
300 ui.warn(_("%s: can't copy - same file\n") % reltarget)
300 ui.warn(_("%s: can't copy - same file\n") % reltarget)
301 return
301 return
302 exists = False
302 exists = False
303 samefile = True
303 samefile = True
304
304
305 if not after and exists or after and state in 'mn':
305 if not after and exists or after and state in 'mn':
306 if not opts['force']:
306 if not opts['force']:
307 ui.warn(_('%s: not overwriting - file exists\n') %
307 ui.warn(_('%s: not overwriting - file exists\n') %
308 reltarget)
308 reltarget)
309 return
309 return
310
310
311 if after:
311 if after:
312 if not exists:
312 if not exists:
313 if rename:
313 if rename:
314 ui.warn(_('%s: not recording move - %s does not exist\n') %
314 ui.warn(_('%s: not recording move - %s does not exist\n') %
315 (relsrc, reltarget))
315 (relsrc, reltarget))
316 else:
316 else:
317 ui.warn(_('%s: not recording copy - %s does not exist\n') %
317 ui.warn(_('%s: not recording copy - %s does not exist\n') %
318 (relsrc, reltarget))
318 (relsrc, reltarget))
319 return
319 return
320 elif not dryrun:
320 elif not dryrun:
321 try:
321 try:
322 if exists:
322 if exists:
323 os.unlink(target)
323 os.unlink(target)
324 targetdir = os.path.dirname(target) or '.'
324 targetdir = os.path.dirname(target) or '.'
325 if not os.path.isdir(targetdir):
325 if not os.path.isdir(targetdir):
326 os.makedirs(targetdir)
326 os.makedirs(targetdir)
327 if samefile:
327 if samefile:
328 tmp = target + "~hgrename"
328 tmp = target + "~hgrename"
329 os.rename(src, tmp)
329 os.rename(src, tmp)
330 os.rename(tmp, target)
330 os.rename(tmp, target)
331 else:
331 else:
332 util.copyfile(src, target)
332 util.copyfile(src, target)
333 srcexists = True
333 srcexists = True
334 except IOError, inst:
334 except IOError, inst:
335 if inst.errno == errno.ENOENT:
335 if inst.errno == errno.ENOENT:
336 ui.warn(_('%s: deleted in working copy\n') % relsrc)
336 ui.warn(_('%s: deleted in working copy\n') % relsrc)
337 srcexists = False
337 srcexists = False
338 else:
338 else:
339 ui.warn(_('%s: cannot copy - %s\n') %
339 ui.warn(_('%s: cannot copy - %s\n') %
340 (relsrc, inst.strerror))
340 (relsrc, inst.strerror))
341 return True # report a failure
341 return True # report a failure
342
342
343 if ui.verbose or not exact:
343 if ui.verbose or not exact:
344 if rename:
344 if rename:
345 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
345 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
346 else:
346 else:
347 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
347 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
348
348
349 targets[abstarget] = abssrc
349 targets[abstarget] = abssrc
350
350
351 # fix up dirstate
351 # fix up dirstate
352 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
352 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
353 dryrun=dryrun, cwd=cwd)
353 dryrun=dryrun, cwd=cwd)
354 if rename and not dryrun:
354 if rename and not dryrun:
355 if not after and srcexists and not samefile:
355 if not after and srcexists and not samefile:
356 util.unlinkpath(repo.wjoin(abssrc))
356 util.unlinkpath(repo.wjoin(abssrc))
357 wctx.forget([abssrc])
357 wctx.forget([abssrc])
358
358
359 # pat: ossep
359 # pat: ossep
360 # dest ossep
360 # dest ossep
361 # srcs: list of (hgsep, hgsep, ossep, bool)
361 # srcs: list of (hgsep, hgsep, ossep, bool)
362 # return: function that takes hgsep and returns ossep
362 # return: function that takes hgsep and returns ossep
363 def targetpathfn(pat, dest, srcs):
363 def targetpathfn(pat, dest, srcs):
364 if os.path.isdir(pat):
364 if os.path.isdir(pat):
365 abspfx = scmutil.canonpath(repo.root, cwd, pat)
365 abspfx = scmutil.canonpath(repo.root, cwd, pat)
366 abspfx = util.localpath(abspfx)
366 abspfx = util.localpath(abspfx)
367 if destdirexists:
367 if destdirexists:
368 striplen = len(os.path.split(abspfx)[0])
368 striplen = len(os.path.split(abspfx)[0])
369 else:
369 else:
370 striplen = len(abspfx)
370 striplen = len(abspfx)
371 if striplen:
371 if striplen:
372 striplen += len(os.sep)
372 striplen += len(os.sep)
373 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
373 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
374 elif destdirexists:
374 elif destdirexists:
375 res = lambda p: os.path.join(dest,
375 res = lambda p: os.path.join(dest,
376 os.path.basename(util.localpath(p)))
376 os.path.basename(util.localpath(p)))
377 else:
377 else:
378 res = lambda p: dest
378 res = lambda p: dest
379 return res
379 return res
380
380
381 # pat: ossep
381 # pat: ossep
382 # dest ossep
382 # dest ossep
383 # srcs: list of (hgsep, hgsep, ossep, bool)
383 # srcs: list of (hgsep, hgsep, ossep, bool)
384 # return: function that takes hgsep and returns ossep
384 # return: function that takes hgsep and returns ossep
385 def targetpathafterfn(pat, dest, srcs):
385 def targetpathafterfn(pat, dest, srcs):
386 if matchmod.patkind(pat):
386 if matchmod.patkind(pat):
387 # a mercurial pattern
387 # a mercurial pattern
388 res = lambda p: os.path.join(dest,
388 res = lambda p: os.path.join(dest,
389 os.path.basename(util.localpath(p)))
389 os.path.basename(util.localpath(p)))
390 else:
390 else:
391 abspfx = scmutil.canonpath(repo.root, cwd, pat)
391 abspfx = scmutil.canonpath(repo.root, cwd, pat)
392 if len(abspfx) < len(srcs[0][0]):
392 if len(abspfx) < len(srcs[0][0]):
393 # A directory. Either the target path contains the last
393 # A directory. Either the target path contains the last
394 # component of the source path or it does not.
394 # component of the source path or it does not.
395 def evalpath(striplen):
395 def evalpath(striplen):
396 score = 0
396 score = 0
397 for s in srcs:
397 for s in srcs:
398 t = os.path.join(dest, util.localpath(s[0])[striplen:])
398 t = os.path.join(dest, util.localpath(s[0])[striplen:])
399 if os.path.lexists(t):
399 if os.path.lexists(t):
400 score += 1
400 score += 1
401 return score
401 return score
402
402
403 abspfx = util.localpath(abspfx)
403 abspfx = util.localpath(abspfx)
404 striplen = len(abspfx)
404 striplen = len(abspfx)
405 if striplen:
405 if striplen:
406 striplen += len(os.sep)
406 striplen += len(os.sep)
407 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
407 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
408 score = evalpath(striplen)
408 score = evalpath(striplen)
409 striplen1 = len(os.path.split(abspfx)[0])
409 striplen1 = len(os.path.split(abspfx)[0])
410 if striplen1:
410 if striplen1:
411 striplen1 += len(os.sep)
411 striplen1 += len(os.sep)
412 if evalpath(striplen1) > score:
412 if evalpath(striplen1) > score:
413 striplen = striplen1
413 striplen = striplen1
414 res = lambda p: os.path.join(dest,
414 res = lambda p: os.path.join(dest,
415 util.localpath(p)[striplen:])
415 util.localpath(p)[striplen:])
416 else:
416 else:
417 # a file
417 # a file
418 if destdirexists:
418 if destdirexists:
419 res = lambda p: os.path.join(dest,
419 res = lambda p: os.path.join(dest,
420 os.path.basename(util.localpath(p)))
420 os.path.basename(util.localpath(p)))
421 else:
421 else:
422 res = lambda p: dest
422 res = lambda p: dest
423 return res
423 return res
424
424
425
425
426 pats = scmutil.expandpats(pats)
426 pats = scmutil.expandpats(pats)
427 if not pats:
427 if not pats:
428 raise util.Abort(_('no source or destination specified'))
428 raise util.Abort(_('no source or destination specified'))
429 if len(pats) == 1:
429 if len(pats) == 1:
430 raise util.Abort(_('no destination specified'))
430 raise util.Abort(_('no destination specified'))
431 dest = pats.pop()
431 dest = pats.pop()
432 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
432 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
433 if not destdirexists:
433 if not destdirexists:
434 if len(pats) > 1 or matchmod.patkind(pats[0]):
434 if len(pats) > 1 or matchmod.patkind(pats[0]):
435 raise util.Abort(_('with multiple sources, destination must be an '
435 raise util.Abort(_('with multiple sources, destination must be an '
436 'existing directory'))
436 'existing directory'))
437 if util.endswithsep(dest):
437 if util.endswithsep(dest):
438 raise util.Abort(_('destination %s is not a directory') % dest)
438 raise util.Abort(_('destination %s is not a directory') % dest)
439
439
440 tfn = targetpathfn
440 tfn = targetpathfn
441 if after:
441 if after:
442 tfn = targetpathafterfn
442 tfn = targetpathafterfn
443 copylist = []
443 copylist = []
444 for pat in pats:
444 for pat in pats:
445 srcs = walkpat(pat)
445 srcs = walkpat(pat)
446 if not srcs:
446 if not srcs:
447 continue
447 continue
448 copylist.append((tfn(pat, dest, srcs), srcs))
448 copylist.append((tfn(pat, dest, srcs), srcs))
449 if not copylist:
449 if not copylist:
450 raise util.Abort(_('no files to copy'))
450 raise util.Abort(_('no files to copy'))
451
451
452 errors = 0
452 errors = 0
453 for targetpath, srcs in copylist:
453 for targetpath, srcs in copylist:
454 for abssrc, relsrc, exact in srcs:
454 for abssrc, relsrc, exact in srcs:
455 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
455 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
456 errors += 1
456 errors += 1
457
457
458 if errors:
458 if errors:
459 ui.warn(_('(consider using --after)\n'))
459 ui.warn(_('(consider using --after)\n'))
460
460
461 return errors != 0
461 return errors != 0
462
462
463 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
463 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
464 runargs=None, appendpid=False):
464 runargs=None, appendpid=False):
465 '''Run a command as a service.'''
465 '''Run a command as a service.'''
466
466
467 if opts['daemon'] and not opts['daemon_pipefds']:
467 if opts['daemon'] and not opts['daemon_pipefds']:
468 # Signal child process startup with file removal
468 # Signal child process startup with file removal
469 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
469 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
470 os.close(lockfd)
470 os.close(lockfd)
471 try:
471 try:
472 if not runargs:
472 if not runargs:
473 runargs = util.hgcmd() + sys.argv[1:]
473 runargs = util.hgcmd() + sys.argv[1:]
474 runargs.append('--daemon-pipefds=%s' % lockpath)
474 runargs.append('--daemon-pipefds=%s' % lockpath)
475 # Don't pass --cwd to the child process, because we've already
475 # Don't pass --cwd to the child process, because we've already
476 # changed directory.
476 # changed directory.
477 for i in xrange(1, len(runargs)):
477 for i in xrange(1, len(runargs)):
478 if runargs[i].startswith('--cwd='):
478 if runargs[i].startswith('--cwd='):
479 del runargs[i]
479 del runargs[i]
480 break
480 break
481 elif runargs[i].startswith('--cwd'):
481 elif runargs[i].startswith('--cwd'):
482 del runargs[i:i + 2]
482 del runargs[i:i + 2]
483 break
483 break
484 def condfn():
484 def condfn():
485 return not os.path.exists(lockpath)
485 return not os.path.exists(lockpath)
486 pid = util.rundetached(runargs, condfn)
486 pid = util.rundetached(runargs, condfn)
487 if pid < 0:
487 if pid < 0:
488 raise util.Abort(_('child process failed to start'))
488 raise util.Abort(_('child process failed to start'))
489 finally:
489 finally:
490 try:
490 try:
491 os.unlink(lockpath)
491 os.unlink(lockpath)
492 except OSError, e:
492 except OSError, e:
493 if e.errno != errno.ENOENT:
493 if e.errno != errno.ENOENT:
494 raise
494 raise
495 if parentfn:
495 if parentfn:
496 return parentfn(pid)
496 return parentfn(pid)
497 else:
497 else:
498 return
498 return
499
499
500 if initfn:
500 if initfn:
501 initfn()
501 initfn()
502
502
503 if opts['pid_file']:
503 if opts['pid_file']:
504 mode = appendpid and 'a' or 'w'
504 mode = appendpid and 'a' or 'w'
505 fp = open(opts['pid_file'], mode)
505 fp = open(opts['pid_file'], mode)
506 fp.write(str(os.getpid()) + '\n')
506 fp.write(str(os.getpid()) + '\n')
507 fp.close()
507 fp.close()
508
508
509 if opts['daemon_pipefds']:
509 if opts['daemon_pipefds']:
510 lockpath = opts['daemon_pipefds']
510 lockpath = opts['daemon_pipefds']
511 try:
511 try:
512 os.setsid()
512 os.setsid()
513 except AttributeError:
513 except AttributeError:
514 pass
514 pass
515 os.unlink(lockpath)
515 os.unlink(lockpath)
516 util.hidewindow()
516 util.hidewindow()
517 sys.stdout.flush()
517 sys.stdout.flush()
518 sys.stderr.flush()
518 sys.stderr.flush()
519
519
520 nullfd = os.open(os.devnull, os.O_RDWR)
520 nullfd = os.open(os.devnull, os.O_RDWR)
521 logfilefd = nullfd
521 logfilefd = nullfd
522 if logfile:
522 if logfile:
523 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
523 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
524 os.dup2(nullfd, 0)
524 os.dup2(nullfd, 0)
525 os.dup2(logfilefd, 1)
525 os.dup2(logfilefd, 1)
526 os.dup2(logfilefd, 2)
526 os.dup2(logfilefd, 2)
527 if nullfd not in (0, 1, 2):
527 if nullfd not in (0, 1, 2):
528 os.close(nullfd)
528 os.close(nullfd)
529 if logfile and logfilefd not in (0, 1, 2):
529 if logfile and logfilefd not in (0, 1, 2):
530 os.close(logfilefd)
530 os.close(logfilefd)
531
531
532 if runfn:
532 if runfn:
533 return runfn()
533 return runfn()
534
534
535 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
535 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
536 opts=None):
536 opts=None):
537 '''export changesets as hg patches.'''
537 '''export changesets as hg patches.'''
538
538
539 total = len(revs)
539 total = len(revs)
540 revwidth = max([len(str(rev)) for rev in revs])
540 revwidth = max([len(str(rev)) for rev in revs])
541
541
542 def single(rev, seqno, fp):
542 def single(rev, seqno, fp):
543 ctx = repo[rev]
543 ctx = repo[rev]
544 node = ctx.node()
544 node = ctx.node()
545 parents = [p.node() for p in ctx.parents() if p]
545 parents = [p.node() for p in ctx.parents() if p]
546 branch = ctx.branch()
546 branch = ctx.branch()
547 if switch_parent:
547 if switch_parent:
548 parents.reverse()
548 parents.reverse()
549 prev = (parents and parents[0]) or nullid
549 prev = (parents and parents[0]) or nullid
550
550
551 shouldclose = False
551 shouldclose = False
552 if not fp and len(template) > 0:
552 if not fp and len(template) > 0:
553 desc_lines = ctx.description().rstrip().split('\n')
553 desc_lines = ctx.description().rstrip().split('\n')
554 desc = desc_lines[0] #Commit always has a first line.
554 desc = desc_lines[0] #Commit always has a first line.
555 fp = makefileobj(repo, template, node, desc=desc, total=total,
555 fp = makefileobj(repo, template, node, desc=desc, total=total,
556 seqno=seqno, revwidth=revwidth, mode='ab')
556 seqno=seqno, revwidth=revwidth, mode='ab')
557 if fp != template:
557 if fp != template:
558 shouldclose = True
558 shouldclose = True
559 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
559 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
560 repo.ui.note("%s\n" % fp.name)
560 repo.ui.note("%s\n" % fp.name)
561
561
562 if not fp:
562 if not fp:
563 write = repo.ui.write
563 write = repo.ui.write
564 else:
564 else:
565 def write(s, **kw):
565 def write(s, **kw):
566 fp.write(s)
566 fp.write(s)
567
567
568
568
569 write("# HG changeset patch\n")
569 write("# HG changeset patch\n")
570 write("# User %s\n" % ctx.user())
570 write("# User %s\n" % ctx.user())
571 write("# Date %d %d\n" % ctx.date())
571 write("# Date %d %d\n" % ctx.date())
572 if branch and branch != 'default':
572 if branch and branch != 'default':
573 write("# Branch %s\n" % branch)
573 write("# Branch %s\n" % branch)
574 write("# Node ID %s\n" % hex(node))
574 write("# Node ID %s\n" % hex(node))
575 write("# Parent %s\n" % hex(prev))
575 write("# Parent %s\n" % hex(prev))
576 if len(parents) > 1:
576 if len(parents) > 1:
577 write("# Parent %s\n" % hex(parents[1]))
577 write("# Parent %s\n" % hex(parents[1]))
578 write(ctx.description().rstrip())
578 write(ctx.description().rstrip())
579 write("\n\n")
579 write("\n\n")
580
580
581 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
581 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
582 write(chunk, label=label)
582 write(chunk, label=label)
583
583
584 if shouldclose:
584 if shouldclose:
585 fp.close()
585 fp.close()
586
586
587 for seqno, rev in enumerate(revs):
587 for seqno, rev in enumerate(revs):
588 single(rev, seqno + 1, fp)
588 single(rev, seqno + 1, fp)
589
589
590 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
590 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
591 changes=None, stat=False, fp=None, prefix='',
591 changes=None, stat=False, fp=None, prefix='',
592 listsubrepos=False):
592 listsubrepos=False):
593 '''show diff or diffstat.'''
593 '''show diff or diffstat.'''
594 if fp is None:
594 if fp is None:
595 write = ui.write
595 write = ui.write
596 else:
596 else:
597 def write(s, **kw):
597 def write(s, **kw):
598 fp.write(s)
598 fp.write(s)
599
599
600 if stat:
600 if stat:
601 diffopts = diffopts.copy(context=0)
601 diffopts = diffopts.copy(context=0)
602 width = 80
602 width = 80
603 if not ui.plain():
603 if not ui.plain():
604 width = ui.termwidth()
604 width = ui.termwidth()
605 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
605 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
606 prefix=prefix)
606 prefix=prefix)
607 for chunk, label in patch.diffstatui(util.iterlines(chunks),
607 for chunk, label in patch.diffstatui(util.iterlines(chunks),
608 width=width,
608 width=width,
609 git=diffopts.git):
609 git=diffopts.git):
610 write(chunk, label=label)
610 write(chunk, label=label)
611 else:
611 else:
612 for chunk, label in patch.diffui(repo, node1, node2, match,
612 for chunk, label in patch.diffui(repo, node1, node2, match,
613 changes, diffopts, prefix=prefix):
613 changes, diffopts, prefix=prefix):
614 write(chunk, label=label)
614 write(chunk, label=label)
615
615
616 if listsubrepos:
616 if listsubrepos:
617 ctx1 = repo[node1]
617 ctx1 = repo[node1]
618 ctx2 = repo[node2]
618 ctx2 = repo[node2]
619 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
619 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
620 tempnode2 = node2
620 tempnode2 = node2
621 try:
621 try:
622 if node2 is not None:
622 if node2 is not None:
623 tempnode2 = ctx2.substate[subpath][1]
623 tempnode2 = ctx2.substate[subpath][1]
624 except KeyError:
624 except KeyError:
625 # A subrepo that existed in node1 was deleted between node1 and
625 # A subrepo that existed in node1 was deleted between node1 and
626 # node2 (inclusive). Thus, ctx2's substate won't contain that
626 # node2 (inclusive). Thus, ctx2's substate won't contain that
627 # subpath. The best we can do is to ignore it.
627 # subpath. The best we can do is to ignore it.
628 tempnode2 = None
628 tempnode2 = None
629 submatch = matchmod.narrowmatcher(subpath, match)
629 submatch = matchmod.narrowmatcher(subpath, match)
630 sub.diff(diffopts, tempnode2, submatch, changes=changes,
630 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
631 stat=stat, fp=fp, prefix=prefix)
631 stat=stat, fp=fp, prefix=prefix)
632
632
633 class changeset_printer(object):
633 class changeset_printer(object):
634 '''show changeset information when templating not requested.'''
634 '''show changeset information when templating not requested.'''
635
635
636 def __init__(self, ui, repo, patch, diffopts, buffered):
636 def __init__(self, ui, repo, patch, diffopts, buffered):
637 self.ui = ui
637 self.ui = ui
638 self.repo = repo
638 self.repo = repo
639 self.buffered = buffered
639 self.buffered = buffered
640 self.patch = patch
640 self.patch = patch
641 self.diffopts = diffopts
641 self.diffopts = diffopts
642 self.header = {}
642 self.header = {}
643 self.hunk = {}
643 self.hunk = {}
644 self.lastheader = None
644 self.lastheader = None
645 self.footer = None
645 self.footer = None
646
646
647 def flush(self, rev):
647 def flush(self, rev):
648 if rev in self.header:
648 if rev in self.header:
649 h = self.header[rev]
649 h = self.header[rev]
650 if h != self.lastheader:
650 if h != self.lastheader:
651 self.lastheader = h
651 self.lastheader = h
652 self.ui.write(h)
652 self.ui.write(h)
653 del self.header[rev]
653 del self.header[rev]
654 if rev in self.hunk:
654 if rev in self.hunk:
655 self.ui.write(self.hunk[rev])
655 self.ui.write(self.hunk[rev])
656 del self.hunk[rev]
656 del self.hunk[rev]
657 return 1
657 return 1
658 return 0
658 return 0
659
659
660 def close(self):
660 def close(self):
661 if self.footer:
661 if self.footer:
662 self.ui.write(self.footer)
662 self.ui.write(self.footer)
663
663
664 def show(self, ctx, copies=None, matchfn=None, **props):
664 def show(self, ctx, copies=None, matchfn=None, **props):
665 if self.buffered:
665 if self.buffered:
666 self.ui.pushbuffer()
666 self.ui.pushbuffer()
667 self._show(ctx, copies, matchfn, props)
667 self._show(ctx, copies, matchfn, props)
668 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
668 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
669 else:
669 else:
670 self._show(ctx, copies, matchfn, props)
670 self._show(ctx, copies, matchfn, props)
671
671
672 def _show(self, ctx, copies, matchfn, props):
672 def _show(self, ctx, copies, matchfn, props):
673 '''show a single changeset or file revision'''
673 '''show a single changeset or file revision'''
674 changenode = ctx.node()
674 changenode = ctx.node()
675 rev = ctx.rev()
675 rev = ctx.rev()
676
676
677 if self.ui.quiet:
677 if self.ui.quiet:
678 self.ui.write("%d:%s\n" % (rev, short(changenode)),
678 self.ui.write("%d:%s\n" % (rev, short(changenode)),
679 label='log.node')
679 label='log.node')
680 return
680 return
681
681
682 log = self.repo.changelog
682 log = self.repo.changelog
683 date = util.datestr(ctx.date())
683 date = util.datestr(ctx.date())
684
684
685 hexfunc = self.ui.debugflag and hex or short
685 hexfunc = self.ui.debugflag and hex or short
686
686
687 parents = [(p, hexfunc(log.node(p)))
687 parents = [(p, hexfunc(log.node(p)))
688 for p in self._meaningful_parentrevs(log, rev)]
688 for p in self._meaningful_parentrevs(log, rev)]
689
689
690 # i18n: column positioning for "hg log"
690 # i18n: column positioning for "hg log"
691 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
691 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
692 label='log.changeset changeset.%s' % ctx.phasestr())
692 label='log.changeset changeset.%s' % ctx.phasestr())
693
693
694 branch = ctx.branch()
694 branch = ctx.branch()
695 # don't show the default branch name
695 # don't show the default branch name
696 if branch != 'default':
696 if branch != 'default':
697 # i18n: column positioning for "hg log"
697 # i18n: column positioning for "hg log"
698 self.ui.write(_("branch: %s\n") % branch,
698 self.ui.write(_("branch: %s\n") % branch,
699 label='log.branch')
699 label='log.branch')
700 for bookmark in self.repo.nodebookmarks(changenode):
700 for bookmark in self.repo.nodebookmarks(changenode):
701 # i18n: column positioning for "hg log"
701 # i18n: column positioning for "hg log"
702 self.ui.write(_("bookmark: %s\n") % bookmark,
702 self.ui.write(_("bookmark: %s\n") % bookmark,
703 label='log.bookmark')
703 label='log.bookmark')
704 for tag in self.repo.nodetags(changenode):
704 for tag in self.repo.nodetags(changenode):
705 # i18n: column positioning for "hg log"
705 # i18n: column positioning for "hg log"
706 self.ui.write(_("tag: %s\n") % tag,
706 self.ui.write(_("tag: %s\n") % tag,
707 label='log.tag')
707 label='log.tag')
708 if self.ui.debugflag and ctx.phase():
708 if self.ui.debugflag and ctx.phase():
709 # i18n: column positioning for "hg log"
709 # i18n: column positioning for "hg log"
710 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
710 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
711 label='log.phase')
711 label='log.phase')
712 for parent in parents:
712 for parent in parents:
713 # i18n: column positioning for "hg log"
713 # i18n: column positioning for "hg log"
714 self.ui.write(_("parent: %d:%s\n") % parent,
714 self.ui.write(_("parent: %d:%s\n") % parent,
715 label='log.parent changeset.%s' % ctx.phasestr())
715 label='log.parent changeset.%s' % ctx.phasestr())
716
716
717 if self.ui.debugflag:
717 if self.ui.debugflag:
718 mnode = ctx.manifestnode()
718 mnode = ctx.manifestnode()
719 # i18n: column positioning for "hg log"
719 # i18n: column positioning for "hg log"
720 self.ui.write(_("manifest: %d:%s\n") %
720 self.ui.write(_("manifest: %d:%s\n") %
721 (self.repo.manifest.rev(mnode), hex(mnode)),
721 (self.repo.manifest.rev(mnode), hex(mnode)),
722 label='ui.debug log.manifest')
722 label='ui.debug log.manifest')
723 # i18n: column positioning for "hg log"
723 # i18n: column positioning for "hg log"
724 self.ui.write(_("user: %s\n") % ctx.user(),
724 self.ui.write(_("user: %s\n") % ctx.user(),
725 label='log.user')
725 label='log.user')
726 # i18n: column positioning for "hg log"
726 # i18n: column positioning for "hg log"
727 self.ui.write(_("date: %s\n") % date,
727 self.ui.write(_("date: %s\n") % date,
728 label='log.date')
728 label='log.date')
729
729
730 if self.ui.debugflag:
730 if self.ui.debugflag:
731 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
731 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
732 for key, value in zip([# i18n: column positioning for "hg log"
732 for key, value in zip([# i18n: column positioning for "hg log"
733 _("files:"),
733 _("files:"),
734 # i18n: column positioning for "hg log"
734 # i18n: column positioning for "hg log"
735 _("files+:"),
735 _("files+:"),
736 # i18n: column positioning for "hg log"
736 # i18n: column positioning for "hg log"
737 _("files-:")], files):
737 _("files-:")], files):
738 if value:
738 if value:
739 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
739 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
740 label='ui.debug log.files')
740 label='ui.debug log.files')
741 elif ctx.files() and self.ui.verbose:
741 elif ctx.files() and self.ui.verbose:
742 # i18n: column positioning for "hg log"
742 # i18n: column positioning for "hg log"
743 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
743 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
744 label='ui.note log.files')
744 label='ui.note log.files')
745 if copies and self.ui.verbose:
745 if copies and self.ui.verbose:
746 copies = ['%s (%s)' % c for c in copies]
746 copies = ['%s (%s)' % c for c in copies]
747 # i18n: column positioning for "hg log"
747 # i18n: column positioning for "hg log"
748 self.ui.write(_("copies: %s\n") % ' '.join(copies),
748 self.ui.write(_("copies: %s\n") % ' '.join(copies),
749 label='ui.note log.copies')
749 label='ui.note log.copies')
750
750
751 extra = ctx.extra()
751 extra = ctx.extra()
752 if extra and self.ui.debugflag:
752 if extra and self.ui.debugflag:
753 for key, value in sorted(extra.items()):
753 for key, value in sorted(extra.items()):
754 # i18n: column positioning for "hg log"
754 # i18n: column positioning for "hg log"
755 self.ui.write(_("extra: %s=%s\n")
755 self.ui.write(_("extra: %s=%s\n")
756 % (key, value.encode('string_escape')),
756 % (key, value.encode('string_escape')),
757 label='ui.debug log.extra')
757 label='ui.debug log.extra')
758
758
759 description = ctx.description().strip()
759 description = ctx.description().strip()
760 if description:
760 if description:
761 if self.ui.verbose:
761 if self.ui.verbose:
762 self.ui.write(_("description:\n"),
762 self.ui.write(_("description:\n"),
763 label='ui.note log.description')
763 label='ui.note log.description')
764 self.ui.write(description,
764 self.ui.write(description,
765 label='ui.note log.description')
765 label='ui.note log.description')
766 self.ui.write("\n\n")
766 self.ui.write("\n\n")
767 else:
767 else:
768 # i18n: column positioning for "hg log"
768 # i18n: column positioning for "hg log"
769 self.ui.write(_("summary: %s\n") %
769 self.ui.write(_("summary: %s\n") %
770 description.splitlines()[0],
770 description.splitlines()[0],
771 label='log.summary')
771 label='log.summary')
772 self.ui.write("\n")
772 self.ui.write("\n")
773
773
774 self.showpatch(changenode, matchfn)
774 self.showpatch(changenode, matchfn)
775
775
776 def showpatch(self, node, matchfn):
776 def showpatch(self, node, matchfn):
777 if not matchfn:
777 if not matchfn:
778 matchfn = self.patch
778 matchfn = self.patch
779 if matchfn:
779 if matchfn:
780 stat = self.diffopts.get('stat')
780 stat = self.diffopts.get('stat')
781 diff = self.diffopts.get('patch')
781 diff = self.diffopts.get('patch')
782 diffopts = patch.diffopts(self.ui, self.diffopts)
782 diffopts = patch.diffopts(self.ui, self.diffopts)
783 prev = self.repo.changelog.parents(node)[0]
783 prev = self.repo.changelog.parents(node)[0]
784 if stat:
784 if stat:
785 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
785 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
786 match=matchfn, stat=True)
786 match=matchfn, stat=True)
787 if diff:
787 if diff:
788 if stat:
788 if stat:
789 self.ui.write("\n")
789 self.ui.write("\n")
790 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
790 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
791 match=matchfn, stat=False)
791 match=matchfn, stat=False)
792 self.ui.write("\n")
792 self.ui.write("\n")
793
793
794 def _meaningful_parentrevs(self, log, rev):
794 def _meaningful_parentrevs(self, log, rev):
795 """Return list of meaningful (or all if debug) parentrevs for rev.
795 """Return list of meaningful (or all if debug) parentrevs for rev.
796
796
797 For merges (two non-nullrev revisions) both parents are meaningful.
797 For merges (two non-nullrev revisions) both parents are meaningful.
798 Otherwise the first parent revision is considered meaningful if it
798 Otherwise the first parent revision is considered meaningful if it
799 is not the preceding revision.
799 is not the preceding revision.
800 """
800 """
801 parents = log.parentrevs(rev)
801 parents = log.parentrevs(rev)
802 if not self.ui.debugflag and parents[1] == nullrev:
802 if not self.ui.debugflag and parents[1] == nullrev:
803 if parents[0] >= rev - 1:
803 if parents[0] >= rev - 1:
804 parents = []
804 parents = []
805 else:
805 else:
806 parents = [parents[0]]
806 parents = [parents[0]]
807 return parents
807 return parents
808
808
809
809
810 class changeset_templater(changeset_printer):
810 class changeset_templater(changeset_printer):
811 '''format changeset information.'''
811 '''format changeset information.'''
812
812
813 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
813 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
814 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
814 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
815 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
815 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
816 defaulttempl = {
816 defaulttempl = {
817 'parent': '{rev}:{node|formatnode} ',
817 'parent': '{rev}:{node|formatnode} ',
818 'manifest': '{rev}:{node|formatnode}',
818 'manifest': '{rev}:{node|formatnode}',
819 'file_copy': '{name} ({source})',
819 'file_copy': '{name} ({source})',
820 'extra': '{key}={value|stringescape}'
820 'extra': '{key}={value|stringescape}'
821 }
821 }
822 # filecopy is preserved for compatibility reasons
822 # filecopy is preserved for compatibility reasons
823 defaulttempl['filecopy'] = defaulttempl['file_copy']
823 defaulttempl['filecopy'] = defaulttempl['file_copy']
824 self.t = templater.templater(mapfile, {'formatnode': formatnode},
824 self.t = templater.templater(mapfile, {'formatnode': formatnode},
825 cache=defaulttempl)
825 cache=defaulttempl)
826 self.cache = {}
826 self.cache = {}
827
827
828 def use_template(self, t):
828 def use_template(self, t):
829 '''set template string to use'''
829 '''set template string to use'''
830 self.t.cache['changeset'] = t
830 self.t.cache['changeset'] = t
831
831
832 def _meaningful_parentrevs(self, ctx):
832 def _meaningful_parentrevs(self, ctx):
833 """Return list of meaningful (or all if debug) parentrevs for rev.
833 """Return list of meaningful (or all if debug) parentrevs for rev.
834 """
834 """
835 parents = ctx.parents()
835 parents = ctx.parents()
836 if len(parents) > 1:
836 if len(parents) > 1:
837 return parents
837 return parents
838 if self.ui.debugflag:
838 if self.ui.debugflag:
839 return [parents[0], self.repo['null']]
839 return [parents[0], self.repo['null']]
840 if parents[0].rev() >= ctx.rev() - 1:
840 if parents[0].rev() >= ctx.rev() - 1:
841 return []
841 return []
842 return parents
842 return parents
843
843
844 def _show(self, ctx, copies, matchfn, props):
844 def _show(self, ctx, copies, matchfn, props):
845 '''show a single changeset or file revision'''
845 '''show a single changeset or file revision'''
846
846
847 showlist = templatekw.showlist
847 showlist = templatekw.showlist
848
848
849 # showparents() behaviour depends on ui trace level which
849 # showparents() behaviour depends on ui trace level which
850 # causes unexpected behaviours at templating level and makes
850 # causes unexpected behaviours at templating level and makes
851 # it harder to extract it in a standalone function. Its
851 # it harder to extract it in a standalone function. Its
852 # behaviour cannot be changed so leave it here for now.
852 # behaviour cannot be changed so leave it here for now.
853 def showparents(**args):
853 def showparents(**args):
854 ctx = args['ctx']
854 ctx = args['ctx']
855 parents = [[('rev', p.rev()), ('node', p.hex())]
855 parents = [[('rev', p.rev()), ('node', p.hex())]
856 for p in self._meaningful_parentrevs(ctx)]
856 for p in self._meaningful_parentrevs(ctx)]
857 return showlist('parent', parents, **args)
857 return showlist('parent', parents, **args)
858
858
859 props = props.copy()
859 props = props.copy()
860 props.update(templatekw.keywords)
860 props.update(templatekw.keywords)
861 props['parents'] = showparents
861 props['parents'] = showparents
862 props['templ'] = self.t
862 props['templ'] = self.t
863 props['ctx'] = ctx
863 props['ctx'] = ctx
864 props['repo'] = self.repo
864 props['repo'] = self.repo
865 props['revcache'] = {'copies': copies}
865 props['revcache'] = {'copies': copies}
866 props['cache'] = self.cache
866 props['cache'] = self.cache
867
867
868 # find correct templates for current mode
868 # find correct templates for current mode
869
869
870 tmplmodes = [
870 tmplmodes = [
871 (True, None),
871 (True, None),
872 (self.ui.verbose, 'verbose'),
872 (self.ui.verbose, 'verbose'),
873 (self.ui.quiet, 'quiet'),
873 (self.ui.quiet, 'quiet'),
874 (self.ui.debugflag, 'debug'),
874 (self.ui.debugflag, 'debug'),
875 ]
875 ]
876
876
877 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
877 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
878 for mode, postfix in tmplmodes:
878 for mode, postfix in tmplmodes:
879 for type in types:
879 for type in types:
880 cur = postfix and ('%s_%s' % (type, postfix)) or type
880 cur = postfix and ('%s_%s' % (type, postfix)) or type
881 if mode and cur in self.t:
881 if mode and cur in self.t:
882 types[type] = cur
882 types[type] = cur
883
883
884 try:
884 try:
885
885
886 # write header
886 # write header
887 if types['header']:
887 if types['header']:
888 h = templater.stringify(self.t(types['header'], **props))
888 h = templater.stringify(self.t(types['header'], **props))
889 if self.buffered:
889 if self.buffered:
890 self.header[ctx.rev()] = h
890 self.header[ctx.rev()] = h
891 else:
891 else:
892 if self.lastheader != h:
892 if self.lastheader != h:
893 self.lastheader = h
893 self.lastheader = h
894 self.ui.write(h)
894 self.ui.write(h)
895
895
896 # write changeset metadata, then patch if requested
896 # write changeset metadata, then patch if requested
897 key = types['changeset']
897 key = types['changeset']
898 self.ui.write(templater.stringify(self.t(key, **props)))
898 self.ui.write(templater.stringify(self.t(key, **props)))
899 self.showpatch(ctx.node(), matchfn)
899 self.showpatch(ctx.node(), matchfn)
900
900
901 if types['footer']:
901 if types['footer']:
902 if not self.footer:
902 if not self.footer:
903 self.footer = templater.stringify(self.t(types['footer'],
903 self.footer = templater.stringify(self.t(types['footer'],
904 **props))
904 **props))
905
905
906 except KeyError, inst:
906 except KeyError, inst:
907 msg = _("%s: no key named '%s'")
907 msg = _("%s: no key named '%s'")
908 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
908 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
909 except SyntaxError, inst:
909 except SyntaxError, inst:
910 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
910 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
911
911
912 def show_changeset(ui, repo, opts, buffered=False):
912 def show_changeset(ui, repo, opts, buffered=False):
913 """show one changeset using template or regular display.
913 """show one changeset using template or regular display.
914
914
915 Display format will be the first non-empty hit of:
915 Display format will be the first non-empty hit of:
916 1. option 'template'
916 1. option 'template'
917 2. option 'style'
917 2. option 'style'
918 3. [ui] setting 'logtemplate'
918 3. [ui] setting 'logtemplate'
919 4. [ui] setting 'style'
919 4. [ui] setting 'style'
920 If all of these values are either the unset or the empty string,
920 If all of these values are either the unset or the empty string,
921 regular display via changeset_printer() is done.
921 regular display via changeset_printer() is done.
922 """
922 """
923 # options
923 # options
924 patch = False
924 patch = False
925 if opts.get('patch') or opts.get('stat'):
925 if opts.get('patch') or opts.get('stat'):
926 patch = scmutil.matchall(repo)
926 patch = scmutil.matchall(repo)
927
927
928 tmpl = opts.get('template')
928 tmpl = opts.get('template')
929 style = None
929 style = None
930 if tmpl:
930 if tmpl:
931 tmpl = templater.parsestring(tmpl, quoted=False)
931 tmpl = templater.parsestring(tmpl, quoted=False)
932 else:
932 else:
933 style = opts.get('style')
933 style = opts.get('style')
934
934
935 # ui settings
935 # ui settings
936 if not (tmpl or style):
936 if not (tmpl or style):
937 tmpl = ui.config('ui', 'logtemplate')
937 tmpl = ui.config('ui', 'logtemplate')
938 if tmpl:
938 if tmpl:
939 try:
939 try:
940 tmpl = templater.parsestring(tmpl)
940 tmpl = templater.parsestring(tmpl)
941 except SyntaxError:
941 except SyntaxError:
942 tmpl = templater.parsestring(tmpl, quoted=False)
942 tmpl = templater.parsestring(tmpl, quoted=False)
943 else:
943 else:
944 style = util.expandpath(ui.config('ui', 'style', ''))
944 style = util.expandpath(ui.config('ui', 'style', ''))
945
945
946 if not (tmpl or style):
946 if not (tmpl or style):
947 return changeset_printer(ui, repo, patch, opts, buffered)
947 return changeset_printer(ui, repo, patch, opts, buffered)
948
948
949 mapfile = None
949 mapfile = None
950 if style and not tmpl:
950 if style and not tmpl:
951 mapfile = style
951 mapfile = style
952 if not os.path.split(mapfile)[0]:
952 if not os.path.split(mapfile)[0]:
953 mapname = (templater.templatepath('map-cmdline.' + mapfile)
953 mapname = (templater.templatepath('map-cmdline.' + mapfile)
954 or templater.templatepath(mapfile))
954 or templater.templatepath(mapfile))
955 if mapname:
955 if mapname:
956 mapfile = mapname
956 mapfile = mapname
957
957
958 try:
958 try:
959 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
959 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
960 except SyntaxError, inst:
960 except SyntaxError, inst:
961 raise util.Abort(inst.args[0])
961 raise util.Abort(inst.args[0])
962 if tmpl:
962 if tmpl:
963 t.use_template(tmpl)
963 t.use_template(tmpl)
964 return t
964 return t
965
965
966 def finddate(ui, repo, date):
966 def finddate(ui, repo, date):
967 """Find the tipmost changeset that matches the given date spec"""
967 """Find the tipmost changeset that matches the given date spec"""
968
968
969 df = util.matchdate(date)
969 df = util.matchdate(date)
970 m = scmutil.matchall(repo)
970 m = scmutil.matchall(repo)
971 results = {}
971 results = {}
972
972
973 def prep(ctx, fns):
973 def prep(ctx, fns):
974 d = ctx.date()
974 d = ctx.date()
975 if df(d[0]):
975 if df(d[0]):
976 results[ctx.rev()] = d
976 results[ctx.rev()] = d
977
977
978 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
978 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
979 rev = ctx.rev()
979 rev = ctx.rev()
980 if rev in results:
980 if rev in results:
981 ui.status(_("found revision %s from %s\n") %
981 ui.status(_("found revision %s from %s\n") %
982 (rev, util.datestr(results[rev])))
982 (rev, util.datestr(results[rev])))
983 return str(rev)
983 return str(rev)
984
984
985 raise util.Abort(_("revision matching date not found"))
985 raise util.Abort(_("revision matching date not found"))
986
986
987 def increasingwindows(start, end, windowsize=8, sizelimit=512):
987 def increasingwindows(start, end, windowsize=8, sizelimit=512):
988 if start < end:
988 if start < end:
989 while start < end:
989 while start < end:
990 yield start, min(windowsize, end - start)
990 yield start, min(windowsize, end - start)
991 start += windowsize
991 start += windowsize
992 if windowsize < sizelimit:
992 if windowsize < sizelimit:
993 windowsize *= 2
993 windowsize *= 2
994 else:
994 else:
995 while start > end:
995 while start > end:
996 yield start, min(windowsize, start - end - 1)
996 yield start, min(windowsize, start - end - 1)
997 start -= windowsize
997 start -= windowsize
998 if windowsize < sizelimit:
998 if windowsize < sizelimit:
999 windowsize *= 2
999 windowsize *= 2
1000
1000
1001 def walkchangerevs(repo, match, opts, prepare):
1001 def walkchangerevs(repo, match, opts, prepare):
1002 '''Iterate over files and the revs in which they changed.
1002 '''Iterate over files and the revs in which they changed.
1003
1003
1004 Callers most commonly need to iterate backwards over the history
1004 Callers most commonly need to iterate backwards over the history
1005 in which they are interested. Doing so has awful (quadratic-looking)
1005 in which they are interested. Doing so has awful (quadratic-looking)
1006 performance, so we use iterators in a "windowed" way.
1006 performance, so we use iterators in a "windowed" way.
1007
1007
1008 We walk a window of revisions in the desired order. Within the
1008 We walk a window of revisions in the desired order. Within the
1009 window, we first walk forwards to gather data, then in the desired
1009 window, we first walk forwards to gather data, then in the desired
1010 order (usually backwards) to display it.
1010 order (usually backwards) to display it.
1011
1011
1012 This function returns an iterator yielding contexts. Before
1012 This function returns an iterator yielding contexts. Before
1013 yielding each context, the iterator will first call the prepare
1013 yielding each context, the iterator will first call the prepare
1014 function on each context in the window in forward order.'''
1014 function on each context in the window in forward order.'''
1015
1015
1016 follow = opts.get('follow') or opts.get('follow_first')
1016 follow = opts.get('follow') or opts.get('follow_first')
1017
1017
1018 if not len(repo):
1018 if not len(repo):
1019 return []
1019 return []
1020 if opts.get('rev'):
1020 if opts.get('rev'):
1021 revs = scmutil.revrange(repo, opts.get('rev'))
1021 revs = scmutil.revrange(repo, opts.get('rev'))
1022 elif follow:
1022 elif follow:
1023 revs = repo.revs('reverse(:.)')
1023 revs = repo.revs('reverse(:.)')
1024 else:
1024 else:
1025 revs = list(repo)
1025 revs = list(repo)
1026 revs.reverse()
1026 revs.reverse()
1027 if not revs:
1027 if not revs:
1028 return []
1028 return []
1029 wanted = set()
1029 wanted = set()
1030 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1030 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1031 fncache = {}
1031 fncache = {}
1032 change = repo.changectx
1032 change = repo.changectx
1033
1033
1034 # First step is to fill wanted, the set of revisions that we want to yield.
1034 # First step is to fill wanted, the set of revisions that we want to yield.
1035 # When it does not induce extra cost, we also fill fncache for revisions in
1035 # When it does not induce extra cost, we also fill fncache for revisions in
1036 # wanted: a cache of filenames that were changed (ctx.files()) and that
1036 # wanted: a cache of filenames that were changed (ctx.files()) and that
1037 # match the file filtering conditions.
1037 # match the file filtering conditions.
1038
1038
1039 if not slowpath and not match.files():
1039 if not slowpath and not match.files():
1040 # No files, no patterns. Display all revs.
1040 # No files, no patterns. Display all revs.
1041 wanted = set(revs)
1041 wanted = set(revs)
1042 copies = []
1042 copies = []
1043
1043
1044 if not slowpath and match.files():
1044 if not slowpath and match.files():
1045 # We only have to read through the filelog to find wanted revisions
1045 # We only have to read through the filelog to find wanted revisions
1046
1046
1047 minrev, maxrev = min(revs), max(revs)
1047 minrev, maxrev = min(revs), max(revs)
1048 def filerevgen(filelog, last):
1048 def filerevgen(filelog, last):
1049 """
1049 """
1050 Only files, no patterns. Check the history of each file.
1050 Only files, no patterns. Check the history of each file.
1051
1051
1052 Examines filelog entries within minrev, maxrev linkrev range
1052 Examines filelog entries within minrev, maxrev linkrev range
1053 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1053 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1054 tuples in backwards order
1054 tuples in backwards order
1055 """
1055 """
1056 cl_count = len(repo)
1056 cl_count = len(repo)
1057 revs = []
1057 revs = []
1058 for j in xrange(0, last + 1):
1058 for j in xrange(0, last + 1):
1059 linkrev = filelog.linkrev(j)
1059 linkrev = filelog.linkrev(j)
1060 if linkrev < minrev:
1060 if linkrev < minrev:
1061 continue
1061 continue
1062 # only yield rev for which we have the changelog, it can
1062 # only yield rev for which we have the changelog, it can
1063 # happen while doing "hg log" during a pull or commit
1063 # happen while doing "hg log" during a pull or commit
1064 if linkrev >= cl_count:
1064 if linkrev >= cl_count:
1065 break
1065 break
1066
1066
1067 parentlinkrevs = []
1067 parentlinkrevs = []
1068 for p in filelog.parentrevs(j):
1068 for p in filelog.parentrevs(j):
1069 if p != nullrev:
1069 if p != nullrev:
1070 parentlinkrevs.append(filelog.linkrev(p))
1070 parentlinkrevs.append(filelog.linkrev(p))
1071 n = filelog.node(j)
1071 n = filelog.node(j)
1072 revs.append((linkrev, parentlinkrevs,
1072 revs.append((linkrev, parentlinkrevs,
1073 follow and filelog.renamed(n)))
1073 follow and filelog.renamed(n)))
1074
1074
1075 return reversed(revs)
1075 return reversed(revs)
1076 def iterfiles():
1076 def iterfiles():
1077 pctx = repo['.']
1077 pctx = repo['.']
1078 for filename in match.files():
1078 for filename in match.files():
1079 if follow:
1079 if follow:
1080 if filename not in pctx:
1080 if filename not in pctx:
1081 raise util.Abort(_('cannot follow file not in parent '
1081 raise util.Abort(_('cannot follow file not in parent '
1082 'revision: "%s"') % filename)
1082 'revision: "%s"') % filename)
1083 yield filename, pctx[filename].filenode()
1083 yield filename, pctx[filename].filenode()
1084 else:
1084 else:
1085 yield filename, None
1085 yield filename, None
1086 for filename_node in copies:
1086 for filename_node in copies:
1087 yield filename_node
1087 yield filename_node
1088 for file_, node in iterfiles():
1088 for file_, node in iterfiles():
1089 filelog = repo.file(file_)
1089 filelog = repo.file(file_)
1090 if not len(filelog):
1090 if not len(filelog):
1091 if node is None:
1091 if node is None:
1092 # A zero count may be a directory or deleted file, so
1092 # A zero count may be a directory or deleted file, so
1093 # try to find matching entries on the slow path.
1093 # try to find matching entries on the slow path.
1094 if follow:
1094 if follow:
1095 raise util.Abort(
1095 raise util.Abort(
1096 _('cannot follow nonexistent file: "%s"') % file_)
1096 _('cannot follow nonexistent file: "%s"') % file_)
1097 slowpath = True
1097 slowpath = True
1098 break
1098 break
1099 else:
1099 else:
1100 continue
1100 continue
1101
1101
1102 if node is None:
1102 if node is None:
1103 last = len(filelog) - 1
1103 last = len(filelog) - 1
1104 else:
1104 else:
1105 last = filelog.rev(node)
1105 last = filelog.rev(node)
1106
1106
1107
1107
1108 # keep track of all ancestors of the file
1108 # keep track of all ancestors of the file
1109 ancestors = set([filelog.linkrev(last)])
1109 ancestors = set([filelog.linkrev(last)])
1110
1110
1111 # iterate from latest to oldest revision
1111 # iterate from latest to oldest revision
1112 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1112 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1113 if not follow:
1113 if not follow:
1114 if rev > maxrev:
1114 if rev > maxrev:
1115 continue
1115 continue
1116 else:
1116 else:
1117 # Note that last might not be the first interesting
1117 # Note that last might not be the first interesting
1118 # rev to us:
1118 # rev to us:
1119 # if the file has been changed after maxrev, we'll
1119 # if the file has been changed after maxrev, we'll
1120 # have linkrev(last) > maxrev, and we still need
1120 # have linkrev(last) > maxrev, and we still need
1121 # to explore the file graph
1121 # to explore the file graph
1122 if rev not in ancestors:
1122 if rev not in ancestors:
1123 continue
1123 continue
1124 # XXX insert 1327 fix here
1124 # XXX insert 1327 fix here
1125 if flparentlinkrevs:
1125 if flparentlinkrevs:
1126 ancestors.update(flparentlinkrevs)
1126 ancestors.update(flparentlinkrevs)
1127
1127
1128 fncache.setdefault(rev, []).append(file_)
1128 fncache.setdefault(rev, []).append(file_)
1129 wanted.add(rev)
1129 wanted.add(rev)
1130 if copied:
1130 if copied:
1131 copies.append(copied)
1131 copies.append(copied)
1132
1132
1133 # We decided to fall back to the slowpath because at least one
1133 # We decided to fall back to the slowpath because at least one
1134 # of the paths was not a file. Check to see if at least one of them
1134 # of the paths was not a file. Check to see if at least one of them
1135 # existed in history, otherwise simply return
1135 # existed in history, otherwise simply return
1136 if slowpath:
1136 if slowpath:
1137 for path in match.files():
1137 for path in match.files():
1138 if path == '.' or path in repo.store:
1138 if path == '.' or path in repo.store:
1139 break
1139 break
1140 else:
1140 else:
1141 return []
1141 return []
1142
1142
1143 if slowpath:
1143 if slowpath:
1144 # We have to read the changelog to match filenames against
1144 # We have to read the changelog to match filenames against
1145 # changed files
1145 # changed files
1146
1146
1147 if follow:
1147 if follow:
1148 raise util.Abort(_('can only follow copies/renames for explicit '
1148 raise util.Abort(_('can only follow copies/renames for explicit '
1149 'filenames'))
1149 'filenames'))
1150
1150
1151 # The slow path checks files modified in every changeset.
1151 # The slow path checks files modified in every changeset.
1152 for i in sorted(revs):
1152 for i in sorted(revs):
1153 ctx = change(i)
1153 ctx = change(i)
1154 matches = filter(match, ctx.files())
1154 matches = filter(match, ctx.files())
1155 if matches:
1155 if matches:
1156 fncache[i] = matches
1156 fncache[i] = matches
1157 wanted.add(i)
1157 wanted.add(i)
1158
1158
1159 class followfilter(object):
1159 class followfilter(object):
1160 def __init__(self, onlyfirst=False):
1160 def __init__(self, onlyfirst=False):
1161 self.startrev = nullrev
1161 self.startrev = nullrev
1162 self.roots = set()
1162 self.roots = set()
1163 self.onlyfirst = onlyfirst
1163 self.onlyfirst = onlyfirst
1164
1164
1165 def match(self, rev):
1165 def match(self, rev):
1166 def realparents(rev):
1166 def realparents(rev):
1167 if self.onlyfirst:
1167 if self.onlyfirst:
1168 return repo.changelog.parentrevs(rev)[0:1]
1168 return repo.changelog.parentrevs(rev)[0:1]
1169 else:
1169 else:
1170 return filter(lambda x: x != nullrev,
1170 return filter(lambda x: x != nullrev,
1171 repo.changelog.parentrevs(rev))
1171 repo.changelog.parentrevs(rev))
1172
1172
1173 if self.startrev == nullrev:
1173 if self.startrev == nullrev:
1174 self.startrev = rev
1174 self.startrev = rev
1175 return True
1175 return True
1176
1176
1177 if rev > self.startrev:
1177 if rev > self.startrev:
1178 # forward: all descendants
1178 # forward: all descendants
1179 if not self.roots:
1179 if not self.roots:
1180 self.roots.add(self.startrev)
1180 self.roots.add(self.startrev)
1181 for parent in realparents(rev):
1181 for parent in realparents(rev):
1182 if parent in self.roots:
1182 if parent in self.roots:
1183 self.roots.add(rev)
1183 self.roots.add(rev)
1184 return True
1184 return True
1185 else:
1185 else:
1186 # backwards: all parents
1186 # backwards: all parents
1187 if not self.roots:
1187 if not self.roots:
1188 self.roots.update(realparents(self.startrev))
1188 self.roots.update(realparents(self.startrev))
1189 if rev in self.roots:
1189 if rev in self.roots:
1190 self.roots.remove(rev)
1190 self.roots.remove(rev)
1191 self.roots.update(realparents(rev))
1191 self.roots.update(realparents(rev))
1192 return True
1192 return True
1193
1193
1194 return False
1194 return False
1195
1195
1196 # it might be worthwhile to do this in the iterator if the rev range
1196 # it might be worthwhile to do this in the iterator if the rev range
1197 # is descending and the prune args are all within that range
1197 # is descending and the prune args are all within that range
1198 for rev in opts.get('prune', ()):
1198 for rev in opts.get('prune', ()):
1199 rev = repo[rev].rev()
1199 rev = repo[rev].rev()
1200 ff = followfilter()
1200 ff = followfilter()
1201 stop = min(revs[0], revs[-1])
1201 stop = min(revs[0], revs[-1])
1202 for x in xrange(rev, stop - 1, -1):
1202 for x in xrange(rev, stop - 1, -1):
1203 if ff.match(x):
1203 if ff.match(x):
1204 wanted.discard(x)
1204 wanted.discard(x)
1205
1205
1206 # Now that wanted is correctly initialized, we can iterate over the
1206 # Now that wanted is correctly initialized, we can iterate over the
1207 # revision range, yielding only revisions in wanted.
1207 # revision range, yielding only revisions in wanted.
1208 def iterate():
1208 def iterate():
1209 if follow and not match.files():
1209 if follow and not match.files():
1210 ff = followfilter(onlyfirst=opts.get('follow_first'))
1210 ff = followfilter(onlyfirst=opts.get('follow_first'))
1211 def want(rev):
1211 def want(rev):
1212 return ff.match(rev) and rev in wanted
1212 return ff.match(rev) and rev in wanted
1213 else:
1213 else:
1214 def want(rev):
1214 def want(rev):
1215 return rev in wanted
1215 return rev in wanted
1216
1216
1217 for i, window in increasingwindows(0, len(revs)):
1217 for i, window in increasingwindows(0, len(revs)):
1218 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1218 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1219 for rev in sorted(nrevs):
1219 for rev in sorted(nrevs):
1220 fns = fncache.get(rev)
1220 fns = fncache.get(rev)
1221 ctx = change(rev)
1221 ctx = change(rev)
1222 if not fns:
1222 if not fns:
1223 def fns_generator():
1223 def fns_generator():
1224 for f in ctx.files():
1224 for f in ctx.files():
1225 if match(f):
1225 if match(f):
1226 yield f
1226 yield f
1227 fns = fns_generator()
1227 fns = fns_generator()
1228 prepare(ctx, fns)
1228 prepare(ctx, fns)
1229 for rev in nrevs:
1229 for rev in nrevs:
1230 yield change(rev)
1230 yield change(rev)
1231 return iterate()
1231 return iterate()
1232
1232
1233 def _makegraphfilematcher(repo, pats, followfirst):
1233 def _makegraphfilematcher(repo, pats, followfirst):
1234 # When displaying a revision with --patch --follow FILE, we have
1234 # When displaying a revision with --patch --follow FILE, we have
1235 # to know which file of the revision must be diffed. With
1235 # to know which file of the revision must be diffed. With
1236 # --follow, we want the names of the ancestors of FILE in the
1236 # --follow, we want the names of the ancestors of FILE in the
1237 # revision, stored in "fcache". "fcache" is populated by
1237 # revision, stored in "fcache". "fcache" is populated by
1238 # reproducing the graph traversal already done by --follow revset
1238 # reproducing the graph traversal already done by --follow revset
1239 # and relating linkrevs to file names (which is not "correct" but
1239 # and relating linkrevs to file names (which is not "correct" but
1240 # good enough).
1240 # good enough).
1241 fcache = {}
1241 fcache = {}
1242 fcacheready = [False]
1242 fcacheready = [False]
1243 pctx = repo['.']
1243 pctx = repo['.']
1244 wctx = repo[None]
1244 wctx = repo[None]
1245
1245
1246 def populate():
1246 def populate():
1247 for fn in pats:
1247 for fn in pats:
1248 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1248 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1249 for c in i:
1249 for c in i:
1250 fcache.setdefault(c.linkrev(), set()).add(c.path())
1250 fcache.setdefault(c.linkrev(), set()).add(c.path())
1251
1251
1252 def filematcher(rev):
1252 def filematcher(rev):
1253 if not fcacheready[0]:
1253 if not fcacheready[0]:
1254 # Lazy initialization
1254 # Lazy initialization
1255 fcacheready[0] = True
1255 fcacheready[0] = True
1256 populate()
1256 populate()
1257 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1257 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1258
1258
1259 return filematcher
1259 return filematcher
1260
1260
1261 def _makegraphlogrevset(repo, pats, opts, revs):
1261 def _makegraphlogrevset(repo, pats, opts, revs):
1262 """Return (expr, filematcher) where expr is a revset string built
1262 """Return (expr, filematcher) where expr is a revset string built
1263 from log options and file patterns or None. If --stat or --patch
1263 from log options and file patterns or None. If --stat or --patch
1264 are not passed filematcher is None. Otherwise it is a callable
1264 are not passed filematcher is None. Otherwise it is a callable
1265 taking a revision number and returning a match objects filtering
1265 taking a revision number and returning a match objects filtering
1266 the files to be detailed when displaying the revision.
1266 the files to be detailed when displaying the revision.
1267 """
1267 """
1268 opt2revset = {
1268 opt2revset = {
1269 'no_merges': ('not merge()', None),
1269 'no_merges': ('not merge()', None),
1270 'only_merges': ('merge()', None),
1270 'only_merges': ('merge()', None),
1271 '_ancestors': ('ancestors(%(val)s)', None),
1271 '_ancestors': ('ancestors(%(val)s)', None),
1272 '_fancestors': ('_firstancestors(%(val)s)', None),
1272 '_fancestors': ('_firstancestors(%(val)s)', None),
1273 '_descendants': ('descendants(%(val)s)', None),
1273 '_descendants': ('descendants(%(val)s)', None),
1274 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1274 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1275 '_matchfiles': ('_matchfiles(%(val)s)', None),
1275 '_matchfiles': ('_matchfiles(%(val)s)', None),
1276 'date': ('date(%(val)r)', None),
1276 'date': ('date(%(val)r)', None),
1277 'branch': ('branch(%(val)r)', ' or '),
1277 'branch': ('branch(%(val)r)', ' or '),
1278 '_patslog': ('filelog(%(val)r)', ' or '),
1278 '_patslog': ('filelog(%(val)r)', ' or '),
1279 '_patsfollow': ('follow(%(val)r)', ' or '),
1279 '_patsfollow': ('follow(%(val)r)', ' or '),
1280 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1280 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1281 'keyword': ('keyword(%(val)r)', ' or '),
1281 'keyword': ('keyword(%(val)r)', ' or '),
1282 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1282 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1283 'user': ('user(%(val)r)', ' or '),
1283 'user': ('user(%(val)r)', ' or '),
1284 }
1284 }
1285
1285
1286 opts = dict(opts)
1286 opts = dict(opts)
1287 # follow or not follow?
1287 # follow or not follow?
1288 follow = opts.get('follow') or opts.get('follow_first')
1288 follow = opts.get('follow') or opts.get('follow_first')
1289 followfirst = opts.get('follow_first') and 1 or 0
1289 followfirst = opts.get('follow_first') and 1 or 0
1290 # --follow with FILE behaviour depends on revs...
1290 # --follow with FILE behaviour depends on revs...
1291 startrev = revs[0]
1291 startrev = revs[0]
1292 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1292 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1293
1293
1294 # branch and only_branch are really aliases and must be handled at
1294 # branch and only_branch are really aliases and must be handled at
1295 # the same time
1295 # the same time
1296 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1296 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1297 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1297 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1298 # pats/include/exclude are passed to match.match() directly in
1298 # pats/include/exclude are passed to match.match() directly in
1299 # _matchfiles() revset but walkchangerevs() builds its matcher with
1299 # _matchfiles() revset but walkchangerevs() builds its matcher with
1300 # scmutil.match(). The difference is input pats are globbed on
1300 # scmutil.match(). The difference is input pats are globbed on
1301 # platforms without shell expansion (windows).
1301 # platforms without shell expansion (windows).
1302 pctx = repo[None]
1302 pctx = repo[None]
1303 match, pats = scmutil.matchandpats(pctx, pats, opts)
1303 match, pats = scmutil.matchandpats(pctx, pats, opts)
1304 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1304 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1305 if not slowpath:
1305 if not slowpath:
1306 for f in match.files():
1306 for f in match.files():
1307 if follow and f not in pctx:
1307 if follow and f not in pctx:
1308 raise util.Abort(_('cannot follow file not in parent '
1308 raise util.Abort(_('cannot follow file not in parent '
1309 'revision: "%s"') % f)
1309 'revision: "%s"') % f)
1310 filelog = repo.file(f)
1310 filelog = repo.file(f)
1311 if not len(filelog):
1311 if not len(filelog):
1312 # A zero count may be a directory or deleted file, so
1312 # A zero count may be a directory or deleted file, so
1313 # try to find matching entries on the slow path.
1313 # try to find matching entries on the slow path.
1314 if follow:
1314 if follow:
1315 raise util.Abort(
1315 raise util.Abort(
1316 _('cannot follow nonexistent file: "%s"') % f)
1316 _('cannot follow nonexistent file: "%s"') % f)
1317 slowpath = True
1317 slowpath = True
1318
1318
1319 # We decided to fall back to the slowpath because at least one
1319 # We decided to fall back to the slowpath because at least one
1320 # of the paths was not a file. Check to see if at least one of them
1320 # of the paths was not a file. Check to see if at least one of them
1321 # existed in history - in that case, we'll continue down the
1321 # existed in history - in that case, we'll continue down the
1322 # slowpath; otherwise, we can turn off the slowpath
1322 # slowpath; otherwise, we can turn off the slowpath
1323 if slowpath:
1323 if slowpath:
1324 for path in match.files():
1324 for path in match.files():
1325 if path == '.' or path in repo.store:
1325 if path == '.' or path in repo.store:
1326 break
1326 break
1327 else:
1327 else:
1328 slowpath = False
1328 slowpath = False
1329
1329
1330 if slowpath:
1330 if slowpath:
1331 # See walkchangerevs() slow path.
1331 # See walkchangerevs() slow path.
1332 #
1332 #
1333 if follow:
1333 if follow:
1334 raise util.Abort(_('can only follow copies/renames for explicit '
1334 raise util.Abort(_('can only follow copies/renames for explicit '
1335 'filenames'))
1335 'filenames'))
1336 # pats/include/exclude cannot be represented as separate
1336 # pats/include/exclude cannot be represented as separate
1337 # revset expressions as their filtering logic applies at file
1337 # revset expressions as their filtering logic applies at file
1338 # level. For instance "-I a -X a" matches a revision touching
1338 # level. For instance "-I a -X a" matches a revision touching
1339 # "a" and "b" while "file(a) and not file(b)" does
1339 # "a" and "b" while "file(a) and not file(b)" does
1340 # not. Besides, filesets are evaluated against the working
1340 # not. Besides, filesets are evaluated against the working
1341 # directory.
1341 # directory.
1342 matchargs = ['r:', 'd:relpath']
1342 matchargs = ['r:', 'd:relpath']
1343 for p in pats:
1343 for p in pats:
1344 matchargs.append('p:' + p)
1344 matchargs.append('p:' + p)
1345 for p in opts.get('include', []):
1345 for p in opts.get('include', []):
1346 matchargs.append('i:' + p)
1346 matchargs.append('i:' + p)
1347 for p in opts.get('exclude', []):
1347 for p in opts.get('exclude', []):
1348 matchargs.append('x:' + p)
1348 matchargs.append('x:' + p)
1349 matchargs = ','.join(('%r' % p) for p in matchargs)
1349 matchargs = ','.join(('%r' % p) for p in matchargs)
1350 opts['_matchfiles'] = matchargs
1350 opts['_matchfiles'] = matchargs
1351 else:
1351 else:
1352 if follow:
1352 if follow:
1353 fpats = ('_patsfollow', '_patsfollowfirst')
1353 fpats = ('_patsfollow', '_patsfollowfirst')
1354 fnopats = (('_ancestors', '_fancestors'),
1354 fnopats = (('_ancestors', '_fancestors'),
1355 ('_descendants', '_fdescendants'))
1355 ('_descendants', '_fdescendants'))
1356 if pats:
1356 if pats:
1357 # follow() revset interprets its file argument as a
1357 # follow() revset interprets its file argument as a
1358 # manifest entry, so use match.files(), not pats.
1358 # manifest entry, so use match.files(), not pats.
1359 opts[fpats[followfirst]] = list(match.files())
1359 opts[fpats[followfirst]] = list(match.files())
1360 else:
1360 else:
1361 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1361 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1362 else:
1362 else:
1363 opts['_patslog'] = list(pats)
1363 opts['_patslog'] = list(pats)
1364
1364
1365 filematcher = None
1365 filematcher = None
1366 if opts.get('patch') or opts.get('stat'):
1366 if opts.get('patch') or opts.get('stat'):
1367 if follow:
1367 if follow:
1368 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1368 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1369 else:
1369 else:
1370 filematcher = lambda rev: match
1370 filematcher = lambda rev: match
1371
1371
1372 expr = []
1372 expr = []
1373 for op, val in opts.iteritems():
1373 for op, val in opts.iteritems():
1374 if not val:
1374 if not val:
1375 continue
1375 continue
1376 if op not in opt2revset:
1376 if op not in opt2revset:
1377 continue
1377 continue
1378 revop, andor = opt2revset[op]
1378 revop, andor = opt2revset[op]
1379 if '%(val)' not in revop:
1379 if '%(val)' not in revop:
1380 expr.append(revop)
1380 expr.append(revop)
1381 else:
1381 else:
1382 if not isinstance(val, list):
1382 if not isinstance(val, list):
1383 e = revop % {'val': val}
1383 e = revop % {'val': val}
1384 else:
1384 else:
1385 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1385 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1386 expr.append(e)
1386 expr.append(e)
1387
1387
1388 if expr:
1388 if expr:
1389 expr = '(' + ' and '.join(expr) + ')'
1389 expr = '(' + ' and '.join(expr) + ')'
1390 else:
1390 else:
1391 expr = None
1391 expr = None
1392 return expr, filematcher
1392 return expr, filematcher
1393
1393
1394 def getgraphlogrevs(repo, pats, opts):
1394 def getgraphlogrevs(repo, pats, opts):
1395 """Return (revs, expr, filematcher) where revs is an iterable of
1395 """Return (revs, expr, filematcher) where revs is an iterable of
1396 revision numbers, expr is a revset string built from log options
1396 revision numbers, expr is a revset string built from log options
1397 and file patterns or None, and used to filter 'revs'. If --stat or
1397 and file patterns or None, and used to filter 'revs'. If --stat or
1398 --patch are not passed filematcher is None. Otherwise it is a
1398 --patch are not passed filematcher is None. Otherwise it is a
1399 callable taking a revision number and returning a match objects
1399 callable taking a revision number and returning a match objects
1400 filtering the files to be detailed when displaying the revision.
1400 filtering the files to be detailed when displaying the revision.
1401 """
1401 """
1402 def increasingrevs(repo, revs, matcher):
1402 def increasingrevs(repo, revs, matcher):
1403 # The sorted input rev sequence is chopped in sub-sequences
1403 # The sorted input rev sequence is chopped in sub-sequences
1404 # which are sorted in ascending order and passed to the
1404 # which are sorted in ascending order and passed to the
1405 # matcher. The filtered revs are sorted again as they were in
1405 # matcher. The filtered revs are sorted again as they were in
1406 # the original sub-sequence. This achieve several things:
1406 # the original sub-sequence. This achieve several things:
1407 #
1407 #
1408 # - getlogrevs() now returns a generator which behaviour is
1408 # - getlogrevs() now returns a generator which behaviour is
1409 # adapted to log need. First results come fast, last ones
1409 # adapted to log need. First results come fast, last ones
1410 # are batched for performances.
1410 # are batched for performances.
1411 #
1411 #
1412 # - revset matchers often operate faster on revision in
1412 # - revset matchers often operate faster on revision in
1413 # changelog order, because most filters deal with the
1413 # changelog order, because most filters deal with the
1414 # changelog.
1414 # changelog.
1415 #
1415 #
1416 # - revset matchers can reorder revisions. "A or B" typically
1416 # - revset matchers can reorder revisions. "A or B" typically
1417 # returns returns the revision matching A then the revision
1417 # returns returns the revision matching A then the revision
1418 # matching B. We want to hide this internal implementation
1418 # matching B. We want to hide this internal implementation
1419 # detail from the caller, and sorting the filtered revision
1419 # detail from the caller, and sorting the filtered revision
1420 # again achieves this.
1420 # again achieves this.
1421 for i, window in increasingwindows(0, len(revs), windowsize=1):
1421 for i, window in increasingwindows(0, len(revs), windowsize=1):
1422 orevs = revs[i:i + window]
1422 orevs = revs[i:i + window]
1423 nrevs = set(matcher(repo, sorted(orevs)))
1423 nrevs = set(matcher(repo, sorted(orevs)))
1424 for rev in orevs:
1424 for rev in orevs:
1425 if rev in nrevs:
1425 if rev in nrevs:
1426 yield rev
1426 yield rev
1427
1427
1428 if not len(repo):
1428 if not len(repo):
1429 return iter([]), None, None
1429 return iter([]), None, None
1430 # Default --rev value depends on --follow but --follow behaviour
1430 # Default --rev value depends on --follow but --follow behaviour
1431 # depends on revisions resolved from --rev...
1431 # depends on revisions resolved from --rev...
1432 follow = opts.get('follow') or opts.get('follow_first')
1432 follow = opts.get('follow') or opts.get('follow_first')
1433 if opts.get('rev'):
1433 if opts.get('rev'):
1434 revs = scmutil.revrange(repo, opts['rev'])
1434 revs = scmutil.revrange(repo, opts['rev'])
1435 else:
1435 else:
1436 if follow and len(repo) > 0:
1436 if follow and len(repo) > 0:
1437 revs = repo.revs('reverse(:.)')
1437 revs = repo.revs('reverse(:.)')
1438 else:
1438 else:
1439 revs = list(repo.changelog)
1439 revs = list(repo.changelog)
1440 revs.reverse()
1440 revs.reverse()
1441 if not revs:
1441 if not revs:
1442 return iter([]), None, None
1442 return iter([]), None, None
1443 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1443 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1444 if expr:
1444 if expr:
1445 matcher = revset.match(repo.ui, expr)
1445 matcher = revset.match(repo.ui, expr)
1446 revs = increasingrevs(repo, revs, matcher)
1446 revs = increasingrevs(repo, revs, matcher)
1447 if not opts.get('hidden'):
1447 if not opts.get('hidden'):
1448 # --hidden is still experimental and not worth a dedicated revset
1448 # --hidden is still experimental and not worth a dedicated revset
1449 # yet. Fortunately, filtering revision number is fast.
1449 # yet. Fortunately, filtering revision number is fast.
1450 revs = (r for r in revs if r not in repo.hiddenrevs)
1450 revs = (r for r in revs if r not in repo.hiddenrevs)
1451 else:
1451 else:
1452 revs = iter(revs)
1452 revs = iter(revs)
1453 return revs, expr, filematcher
1453 return revs, expr, filematcher
1454
1454
1455 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1455 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1456 filematcher=None):
1456 filematcher=None):
1457 seen, state = [], graphmod.asciistate()
1457 seen, state = [], graphmod.asciistate()
1458 for rev, type, ctx, parents in dag:
1458 for rev, type, ctx, parents in dag:
1459 char = 'o'
1459 char = 'o'
1460 if ctx.node() in showparents:
1460 if ctx.node() in showparents:
1461 char = '@'
1461 char = '@'
1462 elif ctx.obsolete():
1462 elif ctx.obsolete():
1463 char = 'x'
1463 char = 'x'
1464 copies = None
1464 copies = None
1465 if getrenamed and ctx.rev():
1465 if getrenamed and ctx.rev():
1466 copies = []
1466 copies = []
1467 for fn in ctx.files():
1467 for fn in ctx.files():
1468 rename = getrenamed(fn, ctx.rev())
1468 rename = getrenamed(fn, ctx.rev())
1469 if rename:
1469 if rename:
1470 copies.append((fn, rename[0]))
1470 copies.append((fn, rename[0]))
1471 revmatchfn = None
1471 revmatchfn = None
1472 if filematcher is not None:
1472 if filematcher is not None:
1473 revmatchfn = filematcher(ctx.rev())
1473 revmatchfn = filematcher(ctx.rev())
1474 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1474 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1475 lines = displayer.hunk.pop(rev).split('\n')
1475 lines = displayer.hunk.pop(rev).split('\n')
1476 if not lines[-1]:
1476 if not lines[-1]:
1477 del lines[-1]
1477 del lines[-1]
1478 displayer.flush(rev)
1478 displayer.flush(rev)
1479 edges = edgefn(type, char, lines, seen, rev, parents)
1479 edges = edgefn(type, char, lines, seen, rev, parents)
1480 for type, char, lines, coldata in edges:
1480 for type, char, lines, coldata in edges:
1481 graphmod.ascii(ui, state, type, char, lines, coldata)
1481 graphmod.ascii(ui, state, type, char, lines, coldata)
1482 displayer.close()
1482 displayer.close()
1483
1483
1484 def graphlog(ui, repo, *pats, **opts):
1484 def graphlog(ui, repo, *pats, **opts):
1485 # Parameters are identical to log command ones
1485 # Parameters are identical to log command ones
1486 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1486 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1487 revs = sorted(revs, reverse=1)
1487 revs = sorted(revs, reverse=1)
1488 limit = loglimit(opts)
1488 limit = loglimit(opts)
1489 if limit is not None:
1489 if limit is not None:
1490 revs = revs[:limit]
1490 revs = revs[:limit]
1491 revdag = graphmod.dagwalker(repo, revs)
1491 revdag = graphmod.dagwalker(repo, revs)
1492
1492
1493 getrenamed = None
1493 getrenamed = None
1494 if opts.get('copies'):
1494 if opts.get('copies'):
1495 endrev = None
1495 endrev = None
1496 if opts.get('rev'):
1496 if opts.get('rev'):
1497 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1497 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1498 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1498 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1499 displayer = show_changeset(ui, repo, opts, buffered=True)
1499 displayer = show_changeset(ui, repo, opts, buffered=True)
1500 showparents = [ctx.node() for ctx in repo[None].parents()]
1500 showparents = [ctx.node() for ctx in repo[None].parents()]
1501 displaygraph(ui, revdag, displayer, showparents,
1501 displaygraph(ui, revdag, displayer, showparents,
1502 graphmod.asciiedges, getrenamed, filematcher)
1502 graphmod.asciiedges, getrenamed, filematcher)
1503
1503
1504 def checkunsupportedgraphflags(pats, opts):
1504 def checkunsupportedgraphflags(pats, opts):
1505 for op in ["newest_first"]:
1505 for op in ["newest_first"]:
1506 if op in opts and opts[op]:
1506 if op in opts and opts[op]:
1507 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1507 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1508 % op.replace("_", "-"))
1508 % op.replace("_", "-"))
1509
1509
1510 def graphrevs(repo, nodes, opts):
1510 def graphrevs(repo, nodes, opts):
1511 limit = loglimit(opts)
1511 limit = loglimit(opts)
1512 nodes.reverse()
1512 nodes.reverse()
1513 if limit is not None:
1513 if limit is not None:
1514 nodes = nodes[:limit]
1514 nodes = nodes[:limit]
1515 return graphmod.nodes(repo, nodes)
1515 return graphmod.nodes(repo, nodes)
1516
1516
1517 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1517 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1518 join = lambda f: os.path.join(prefix, f)
1518 join = lambda f: os.path.join(prefix, f)
1519 bad = []
1519 bad = []
1520 oldbad = match.bad
1520 oldbad = match.bad
1521 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1521 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1522 names = []
1522 names = []
1523 wctx = repo[None]
1523 wctx = repo[None]
1524 cca = None
1524 cca = None
1525 abort, warn = scmutil.checkportabilityalert(ui)
1525 abort, warn = scmutil.checkportabilityalert(ui)
1526 if abort or warn:
1526 if abort or warn:
1527 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1527 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1528 for f in repo.walk(match):
1528 for f in repo.walk(match):
1529 exact = match.exact(f)
1529 exact = match.exact(f)
1530 if exact or not explicitonly and f not in repo.dirstate:
1530 if exact or not explicitonly and f not in repo.dirstate:
1531 if cca:
1531 if cca:
1532 cca(f)
1532 cca(f)
1533 names.append(f)
1533 names.append(f)
1534 if ui.verbose or not exact:
1534 if ui.verbose or not exact:
1535 ui.status(_('adding %s\n') % match.rel(join(f)))
1535 ui.status(_('adding %s\n') % match.rel(join(f)))
1536
1536
1537 for subpath in wctx.substate:
1537 for subpath in wctx.substate:
1538 sub = wctx.sub(subpath)
1538 sub = wctx.sub(subpath)
1539 try:
1539 try:
1540 submatch = matchmod.narrowmatcher(subpath, match)
1540 submatch = matchmod.narrowmatcher(subpath, match)
1541 if listsubrepos:
1541 if listsubrepos:
1542 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1542 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1543 False))
1543 False))
1544 else:
1544 else:
1545 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1545 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1546 True))
1546 True))
1547 except error.LookupError:
1547 except error.LookupError:
1548 ui.status(_("skipping missing subrepository: %s\n")
1548 ui.status(_("skipping missing subrepository: %s\n")
1549 % join(subpath))
1549 % join(subpath))
1550
1550
1551 if not dryrun:
1551 if not dryrun:
1552 rejected = wctx.add(names, prefix)
1552 rejected = wctx.add(names, prefix)
1553 bad.extend(f for f in rejected if f in match.files())
1553 bad.extend(f for f in rejected if f in match.files())
1554 return bad
1554 return bad
1555
1555
1556 def forget(ui, repo, match, prefix, explicitonly):
1556 def forget(ui, repo, match, prefix, explicitonly):
1557 join = lambda f: os.path.join(prefix, f)
1557 join = lambda f: os.path.join(prefix, f)
1558 bad = []
1558 bad = []
1559 oldbad = match.bad
1559 oldbad = match.bad
1560 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1560 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1561 wctx = repo[None]
1561 wctx = repo[None]
1562 forgot = []
1562 forgot = []
1563 s = repo.status(match=match, clean=True)
1563 s = repo.status(match=match, clean=True)
1564 forget = sorted(s[0] + s[1] + s[3] + s[6])
1564 forget = sorted(s[0] + s[1] + s[3] + s[6])
1565 if explicitonly:
1565 if explicitonly:
1566 forget = [f for f in forget if match.exact(f)]
1566 forget = [f for f in forget if match.exact(f)]
1567
1567
1568 for subpath in wctx.substate:
1568 for subpath in wctx.substate:
1569 sub = wctx.sub(subpath)
1569 sub = wctx.sub(subpath)
1570 try:
1570 try:
1571 submatch = matchmod.narrowmatcher(subpath, match)
1571 submatch = matchmod.narrowmatcher(subpath, match)
1572 subbad, subforgot = sub.forget(ui, submatch, prefix)
1572 subbad, subforgot = sub.forget(ui, submatch, prefix)
1573 bad.extend([subpath + '/' + f for f in subbad])
1573 bad.extend([subpath + '/' + f for f in subbad])
1574 forgot.extend([subpath + '/' + f for f in subforgot])
1574 forgot.extend([subpath + '/' + f for f in subforgot])
1575 except error.LookupError:
1575 except error.LookupError:
1576 ui.status(_("skipping missing subrepository: %s\n")
1576 ui.status(_("skipping missing subrepository: %s\n")
1577 % join(subpath))
1577 % join(subpath))
1578
1578
1579 if not explicitonly:
1579 if not explicitonly:
1580 for f in match.files():
1580 for f in match.files():
1581 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1581 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1582 if f not in forgot:
1582 if f not in forgot:
1583 if os.path.exists(match.rel(join(f))):
1583 if os.path.exists(match.rel(join(f))):
1584 ui.warn(_('not removing %s: '
1584 ui.warn(_('not removing %s: '
1585 'file is already untracked\n')
1585 'file is already untracked\n')
1586 % match.rel(join(f)))
1586 % match.rel(join(f)))
1587 bad.append(f)
1587 bad.append(f)
1588
1588
1589 for f in forget:
1589 for f in forget:
1590 if ui.verbose or not match.exact(f):
1590 if ui.verbose or not match.exact(f):
1591 ui.status(_('removing %s\n') % match.rel(join(f)))
1591 ui.status(_('removing %s\n') % match.rel(join(f)))
1592
1592
1593 rejected = wctx.forget(forget, prefix)
1593 rejected = wctx.forget(forget, prefix)
1594 bad.extend(f for f in rejected if f in match.files())
1594 bad.extend(f for f in rejected if f in match.files())
1595 forgot.extend(forget)
1595 forgot.extend(forget)
1596 return bad, forgot
1596 return bad, forgot
1597
1597
1598 def duplicatecopies(repo, rev, p1):
1598 def duplicatecopies(repo, rev, p1):
1599 "Reproduce copies found in the source revision in the dirstate for grafts"
1599 "Reproduce copies found in the source revision in the dirstate for grafts"
1600 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1600 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1601 repo.dirstate.copy(src, dst)
1601 repo.dirstate.copy(src, dst)
1602
1602
1603 def commit(ui, repo, commitfunc, pats, opts):
1603 def commit(ui, repo, commitfunc, pats, opts):
1604 '''commit the specified files or all outstanding changes'''
1604 '''commit the specified files or all outstanding changes'''
1605 date = opts.get('date')
1605 date = opts.get('date')
1606 if date:
1606 if date:
1607 opts['date'] = util.parsedate(date)
1607 opts['date'] = util.parsedate(date)
1608 message = logmessage(ui, opts)
1608 message = logmessage(ui, opts)
1609
1609
1610 # extract addremove carefully -- this function can be called from a command
1610 # extract addremove carefully -- this function can be called from a command
1611 # that doesn't support addremove
1611 # that doesn't support addremove
1612 if opts.get('addremove'):
1612 if opts.get('addremove'):
1613 scmutil.addremove(repo, pats, opts)
1613 scmutil.addremove(repo, pats, opts)
1614
1614
1615 return commitfunc(ui, repo, message,
1615 return commitfunc(ui, repo, message,
1616 scmutil.match(repo[None], pats, opts), opts)
1616 scmutil.match(repo[None], pats, opts), opts)
1617
1617
1618 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1618 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1619 ui.note(_('amending changeset %s\n') % old)
1619 ui.note(_('amending changeset %s\n') % old)
1620 base = old.p1()
1620 base = old.p1()
1621
1621
1622 wlock = lock = None
1622 wlock = lock = None
1623 try:
1623 try:
1624 wlock = repo.wlock()
1624 wlock = repo.wlock()
1625 lock = repo.lock()
1625 lock = repo.lock()
1626 tr = repo.transaction('amend')
1626 tr = repo.transaction('amend')
1627 try:
1627 try:
1628 # See if we got a message from -m or -l, if not, open the editor
1628 # See if we got a message from -m or -l, if not, open the editor
1629 # with the message of the changeset to amend
1629 # with the message of the changeset to amend
1630 message = logmessage(ui, opts)
1630 message = logmessage(ui, opts)
1631 # ensure logfile does not conflict with later enforcement of the
1631 # ensure logfile does not conflict with later enforcement of the
1632 # message. potential logfile content has been processed by
1632 # message. potential logfile content has been processed by
1633 # `logmessage` anyway.
1633 # `logmessage` anyway.
1634 opts.pop('logfile')
1634 opts.pop('logfile')
1635 # First, do a regular commit to record all changes in the working
1635 # First, do a regular commit to record all changes in the working
1636 # directory (if there are any)
1636 # directory (if there are any)
1637 ui.callhooks = False
1637 ui.callhooks = False
1638 try:
1638 try:
1639 opts['message'] = 'temporary amend commit for %s' % old
1639 opts['message'] = 'temporary amend commit for %s' % old
1640 node = commit(ui, repo, commitfunc, pats, opts)
1640 node = commit(ui, repo, commitfunc, pats, opts)
1641 finally:
1641 finally:
1642 ui.callhooks = True
1642 ui.callhooks = True
1643 ctx = repo[node]
1643 ctx = repo[node]
1644
1644
1645 # Participating changesets:
1645 # Participating changesets:
1646 #
1646 #
1647 # node/ctx o - new (intermediate) commit that contains changes
1647 # node/ctx o - new (intermediate) commit that contains changes
1648 # | from working dir to go into amending commit
1648 # | from working dir to go into amending commit
1649 # | (or a workingctx if there were no changes)
1649 # | (or a workingctx if there were no changes)
1650 # |
1650 # |
1651 # old o - changeset to amend
1651 # old o - changeset to amend
1652 # |
1652 # |
1653 # base o - parent of amending changeset
1653 # base o - parent of amending changeset
1654
1654
1655 # Update extra dict from amended commit (e.g. to preserve graft
1655 # Update extra dict from amended commit (e.g. to preserve graft
1656 # source)
1656 # source)
1657 extra.update(old.extra())
1657 extra.update(old.extra())
1658
1658
1659 # Also update it from the intermediate commit or from the wctx
1659 # Also update it from the intermediate commit or from the wctx
1660 extra.update(ctx.extra())
1660 extra.update(ctx.extra())
1661
1661
1662 files = set(old.files())
1662 files = set(old.files())
1663
1663
1664 # Second, we use either the commit we just did, or if there were no
1664 # Second, we use either the commit we just did, or if there were no
1665 # changes the parent of the working directory as the version of the
1665 # changes the parent of the working directory as the version of the
1666 # files in the final amend commit
1666 # files in the final amend commit
1667 if node:
1667 if node:
1668 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1668 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1669
1669
1670 user = ctx.user()
1670 user = ctx.user()
1671 date = ctx.date()
1671 date = ctx.date()
1672 # Recompute copies (avoid recording a -> b -> a)
1672 # Recompute copies (avoid recording a -> b -> a)
1673 copied = copies.pathcopies(base, ctx)
1673 copied = copies.pathcopies(base, ctx)
1674
1674
1675 # Prune files which were reverted by the updates: if old
1675 # Prune files which were reverted by the updates: if old
1676 # introduced file X and our intermediate commit, node,
1676 # introduced file X and our intermediate commit, node,
1677 # renamed that file, then those two files are the same and
1677 # renamed that file, then those two files are the same and
1678 # we can discard X from our list of files. Likewise if X
1678 # we can discard X from our list of files. Likewise if X
1679 # was deleted, it's no longer relevant
1679 # was deleted, it's no longer relevant
1680 files.update(ctx.files())
1680 files.update(ctx.files())
1681
1681
1682 def samefile(f):
1682 def samefile(f):
1683 if f in ctx.manifest():
1683 if f in ctx.manifest():
1684 a = ctx.filectx(f)
1684 a = ctx.filectx(f)
1685 if f in base.manifest():
1685 if f in base.manifest():
1686 b = base.filectx(f)
1686 b = base.filectx(f)
1687 return (not a.cmp(b)
1687 return (not a.cmp(b)
1688 and a.flags() == b.flags())
1688 and a.flags() == b.flags())
1689 else:
1689 else:
1690 return False
1690 return False
1691 else:
1691 else:
1692 return f not in base.manifest()
1692 return f not in base.manifest()
1693 files = [f for f in files if not samefile(f)]
1693 files = [f for f in files if not samefile(f)]
1694
1694
1695 def filectxfn(repo, ctx_, path):
1695 def filectxfn(repo, ctx_, path):
1696 try:
1696 try:
1697 fctx = ctx[path]
1697 fctx = ctx[path]
1698 flags = fctx.flags()
1698 flags = fctx.flags()
1699 mctx = context.memfilectx(fctx.path(), fctx.data(),
1699 mctx = context.memfilectx(fctx.path(), fctx.data(),
1700 islink='l' in flags,
1700 islink='l' in flags,
1701 isexec='x' in flags,
1701 isexec='x' in flags,
1702 copied=copied.get(path))
1702 copied=copied.get(path))
1703 return mctx
1703 return mctx
1704 except KeyError:
1704 except KeyError:
1705 raise IOError
1705 raise IOError
1706 else:
1706 else:
1707 ui.note(_('copying changeset %s to %s\n') % (old, base))
1707 ui.note(_('copying changeset %s to %s\n') % (old, base))
1708
1708
1709 # Use version of files as in the old cset
1709 # Use version of files as in the old cset
1710 def filectxfn(repo, ctx_, path):
1710 def filectxfn(repo, ctx_, path):
1711 try:
1711 try:
1712 return old.filectx(path)
1712 return old.filectx(path)
1713 except KeyError:
1713 except KeyError:
1714 raise IOError
1714 raise IOError
1715
1715
1716 user = opts.get('user') or old.user()
1716 user = opts.get('user') or old.user()
1717 date = opts.get('date') or old.date()
1717 date = opts.get('date') or old.date()
1718 editmsg = False
1718 editmsg = False
1719 if not message:
1719 if not message:
1720 editmsg = True
1720 editmsg = True
1721 message = old.description()
1721 message = old.description()
1722
1722
1723 pureextra = extra.copy()
1723 pureextra = extra.copy()
1724 extra['amend_source'] = old.hex()
1724 extra['amend_source'] = old.hex()
1725
1725
1726 new = context.memctx(repo,
1726 new = context.memctx(repo,
1727 parents=[base.node(), nullid],
1727 parents=[base.node(), nullid],
1728 text=message,
1728 text=message,
1729 files=files,
1729 files=files,
1730 filectxfn=filectxfn,
1730 filectxfn=filectxfn,
1731 user=user,
1731 user=user,
1732 date=date,
1732 date=date,
1733 extra=extra)
1733 extra=extra)
1734 if editmsg:
1734 if editmsg:
1735 new._text = commitforceeditor(repo, new, [])
1735 new._text = commitforceeditor(repo, new, [])
1736
1736
1737 newdesc = changelog.stripdesc(new.description())
1737 newdesc = changelog.stripdesc(new.description())
1738 if ((not node)
1738 if ((not node)
1739 and newdesc == old.description()
1739 and newdesc == old.description()
1740 and user == old.user()
1740 and user == old.user()
1741 and date == old.date()
1741 and date == old.date()
1742 and pureextra == old.extra()):
1742 and pureextra == old.extra()):
1743 # nothing changed. continuing here would create a new node
1743 # nothing changed. continuing here would create a new node
1744 # anyway because of the amend_source noise.
1744 # anyway because of the amend_source noise.
1745 #
1745 #
1746 # This not what we expect from amend.
1746 # This not what we expect from amend.
1747 return old.node()
1747 return old.node()
1748
1748
1749 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1749 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1750 try:
1750 try:
1751 repo.ui.setconfig('phases', 'new-commit', old.phase())
1751 repo.ui.setconfig('phases', 'new-commit', old.phase())
1752 newid = repo.commitctx(new)
1752 newid = repo.commitctx(new)
1753 finally:
1753 finally:
1754 repo.ui.setconfig('phases', 'new-commit', ph)
1754 repo.ui.setconfig('phases', 'new-commit', ph)
1755 if newid != old.node():
1755 if newid != old.node():
1756 # Reroute the working copy parent to the new changeset
1756 # Reroute the working copy parent to the new changeset
1757 repo.setparents(newid, nullid)
1757 repo.setparents(newid, nullid)
1758
1758
1759 # Move bookmarks from old parent to amend commit
1759 # Move bookmarks from old parent to amend commit
1760 bms = repo.nodebookmarks(old.node())
1760 bms = repo.nodebookmarks(old.node())
1761 if bms:
1761 if bms:
1762 for bm in bms:
1762 for bm in bms:
1763 repo._bookmarks[bm] = newid
1763 repo._bookmarks[bm] = newid
1764 bookmarks.write(repo)
1764 bookmarks.write(repo)
1765 #commit the whole amend process
1765 #commit the whole amend process
1766 if obsolete._enabled and newid != old.node():
1766 if obsolete._enabled and newid != old.node():
1767 # mark the new changeset as successor of the rewritten one
1767 # mark the new changeset as successor of the rewritten one
1768 new = repo[newid]
1768 new = repo[newid]
1769 obs = [(old, (new,))]
1769 obs = [(old, (new,))]
1770 if node:
1770 if node:
1771 obs.append((ctx, ()))
1771 obs.append((ctx, ()))
1772
1772
1773 obsolete.createmarkers(repo, obs)
1773 obsolete.createmarkers(repo, obs)
1774 tr.close()
1774 tr.close()
1775 finally:
1775 finally:
1776 tr.release()
1776 tr.release()
1777 if (not obsolete._enabled) and newid != old.node():
1777 if (not obsolete._enabled) and newid != old.node():
1778 # Strip the intermediate commit (if there was one) and the amended
1778 # Strip the intermediate commit (if there was one) and the amended
1779 # commit
1779 # commit
1780 if node:
1780 if node:
1781 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1781 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1782 ui.note(_('stripping amended changeset %s\n') % old)
1782 ui.note(_('stripping amended changeset %s\n') % old)
1783 repair.strip(ui, repo, old.node(), topic='amend-backup')
1783 repair.strip(ui, repo, old.node(), topic='amend-backup')
1784 finally:
1784 finally:
1785 lockmod.release(wlock, lock)
1785 lockmod.release(wlock, lock)
1786 return newid
1786 return newid
1787
1787
1788 def commiteditor(repo, ctx, subs):
1788 def commiteditor(repo, ctx, subs):
1789 if ctx.description():
1789 if ctx.description():
1790 return ctx.description()
1790 return ctx.description()
1791 return commitforceeditor(repo, ctx, subs)
1791 return commitforceeditor(repo, ctx, subs)
1792
1792
1793 def commitforceeditor(repo, ctx, subs):
1793 def commitforceeditor(repo, ctx, subs):
1794 edittext = []
1794 edittext = []
1795 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1795 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1796 if ctx.description():
1796 if ctx.description():
1797 edittext.append(ctx.description())
1797 edittext.append(ctx.description())
1798 edittext.append("")
1798 edittext.append("")
1799 edittext.append("") # Empty line between message and comments.
1799 edittext.append("") # Empty line between message and comments.
1800 edittext.append(_("HG: Enter commit message."
1800 edittext.append(_("HG: Enter commit message."
1801 " Lines beginning with 'HG:' are removed."))
1801 " Lines beginning with 'HG:' are removed."))
1802 edittext.append(_("HG: Leave message empty to abort commit."))
1802 edittext.append(_("HG: Leave message empty to abort commit."))
1803 edittext.append("HG: --")
1803 edittext.append("HG: --")
1804 edittext.append(_("HG: user: %s") % ctx.user())
1804 edittext.append(_("HG: user: %s") % ctx.user())
1805 if ctx.p2():
1805 if ctx.p2():
1806 edittext.append(_("HG: branch merge"))
1806 edittext.append(_("HG: branch merge"))
1807 if ctx.branch():
1807 if ctx.branch():
1808 edittext.append(_("HG: branch '%s'") % ctx.branch())
1808 edittext.append(_("HG: branch '%s'") % ctx.branch())
1809 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1809 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1810 edittext.extend([_("HG: added %s") % f for f in added])
1810 edittext.extend([_("HG: added %s") % f for f in added])
1811 edittext.extend([_("HG: changed %s") % f for f in modified])
1811 edittext.extend([_("HG: changed %s") % f for f in modified])
1812 edittext.extend([_("HG: removed %s") % f for f in removed])
1812 edittext.extend([_("HG: removed %s") % f for f in removed])
1813 if not added and not modified and not removed:
1813 if not added and not modified and not removed:
1814 edittext.append(_("HG: no files changed"))
1814 edittext.append(_("HG: no files changed"))
1815 edittext.append("")
1815 edittext.append("")
1816 # run editor in the repository root
1816 # run editor in the repository root
1817 olddir = os.getcwd()
1817 olddir = os.getcwd()
1818 os.chdir(repo.root)
1818 os.chdir(repo.root)
1819 text = repo.ui.edit("\n".join(edittext), ctx.user())
1819 text = repo.ui.edit("\n".join(edittext), ctx.user())
1820 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1820 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1821 os.chdir(olddir)
1821 os.chdir(olddir)
1822
1822
1823 if not text.strip():
1823 if not text.strip():
1824 raise util.Abort(_("empty commit message"))
1824 raise util.Abort(_("empty commit message"))
1825
1825
1826 return text
1826 return text
1827
1827
1828 def revert(ui, repo, ctx, parents, *pats, **opts):
1828 def revert(ui, repo, ctx, parents, *pats, **opts):
1829 parent, p2 = parents
1829 parent, p2 = parents
1830 node = ctx.node()
1830 node = ctx.node()
1831
1831
1832 mf = ctx.manifest()
1832 mf = ctx.manifest()
1833 if node == parent:
1833 if node == parent:
1834 pmf = mf
1834 pmf = mf
1835 else:
1835 else:
1836 pmf = None
1836 pmf = None
1837
1837
1838 # need all matching names in dirstate and manifest of target rev,
1838 # need all matching names in dirstate and manifest of target rev,
1839 # so have to walk both. do not print errors if files exist in one
1839 # so have to walk both. do not print errors if files exist in one
1840 # but not other.
1840 # but not other.
1841
1841
1842 names = {}
1842 names = {}
1843
1843
1844 wlock = repo.wlock()
1844 wlock = repo.wlock()
1845 try:
1845 try:
1846 # walk dirstate.
1846 # walk dirstate.
1847
1847
1848 m = scmutil.match(repo[None], pats, opts)
1848 m = scmutil.match(repo[None], pats, opts)
1849 m.bad = lambda x, y: False
1849 m.bad = lambda x, y: False
1850 for abs in repo.walk(m):
1850 for abs in repo.walk(m):
1851 names[abs] = m.rel(abs), m.exact(abs)
1851 names[abs] = m.rel(abs), m.exact(abs)
1852
1852
1853 # walk target manifest.
1853 # walk target manifest.
1854
1854
1855 def badfn(path, msg):
1855 def badfn(path, msg):
1856 if path in names:
1856 if path in names:
1857 return
1857 return
1858 if path in ctx.substate:
1858 if path in ctx.substate:
1859 return
1859 return
1860 path_ = path + '/'
1860 path_ = path + '/'
1861 for f in names:
1861 for f in names:
1862 if f.startswith(path_):
1862 if f.startswith(path_):
1863 return
1863 return
1864 ui.warn("%s: %s\n" % (m.rel(path), msg))
1864 ui.warn("%s: %s\n" % (m.rel(path), msg))
1865
1865
1866 m = scmutil.match(ctx, pats, opts)
1866 m = scmutil.match(ctx, pats, opts)
1867 m.bad = badfn
1867 m.bad = badfn
1868 for abs in ctx.walk(m):
1868 for abs in ctx.walk(m):
1869 if abs not in names:
1869 if abs not in names:
1870 names[abs] = m.rel(abs), m.exact(abs)
1870 names[abs] = m.rel(abs), m.exact(abs)
1871
1871
1872 # get the list of subrepos that must be reverted
1872 # get the list of subrepos that must be reverted
1873 targetsubs = [s for s in ctx.substate if m(s)]
1873 targetsubs = [s for s in ctx.substate if m(s)]
1874 m = scmutil.matchfiles(repo, names)
1874 m = scmutil.matchfiles(repo, names)
1875 changes = repo.status(match=m)[:4]
1875 changes = repo.status(match=m)[:4]
1876 modified, added, removed, deleted = map(set, changes)
1876 modified, added, removed, deleted = map(set, changes)
1877
1877
1878 # if f is a rename, also revert the source
1878 # if f is a rename, also revert the source
1879 cwd = repo.getcwd()
1879 cwd = repo.getcwd()
1880 for f in added:
1880 for f in added:
1881 src = repo.dirstate.copied(f)
1881 src = repo.dirstate.copied(f)
1882 if src and src not in names and repo.dirstate[src] == 'r':
1882 if src and src not in names and repo.dirstate[src] == 'r':
1883 removed.add(src)
1883 removed.add(src)
1884 names[src] = (repo.pathto(src, cwd), True)
1884 names[src] = (repo.pathto(src, cwd), True)
1885
1885
1886 def removeforget(abs):
1886 def removeforget(abs):
1887 if repo.dirstate[abs] == 'a':
1887 if repo.dirstate[abs] == 'a':
1888 return _('forgetting %s\n')
1888 return _('forgetting %s\n')
1889 return _('removing %s\n')
1889 return _('removing %s\n')
1890
1890
1891 revert = ([], _('reverting %s\n'))
1891 revert = ([], _('reverting %s\n'))
1892 add = ([], _('adding %s\n'))
1892 add = ([], _('adding %s\n'))
1893 remove = ([], removeforget)
1893 remove = ([], removeforget)
1894 undelete = ([], _('undeleting %s\n'))
1894 undelete = ([], _('undeleting %s\n'))
1895
1895
1896 disptable = (
1896 disptable = (
1897 # dispatch table:
1897 # dispatch table:
1898 # file state
1898 # file state
1899 # action if in target manifest
1899 # action if in target manifest
1900 # action if not in target manifest
1900 # action if not in target manifest
1901 # make backup if in target manifest
1901 # make backup if in target manifest
1902 # make backup if not in target manifest
1902 # make backup if not in target manifest
1903 (modified, revert, remove, True, True),
1903 (modified, revert, remove, True, True),
1904 (added, revert, remove, True, False),
1904 (added, revert, remove, True, False),
1905 (removed, undelete, None, False, False),
1905 (removed, undelete, None, False, False),
1906 (deleted, revert, remove, False, False),
1906 (deleted, revert, remove, False, False),
1907 )
1907 )
1908
1908
1909 for abs, (rel, exact) in sorted(names.items()):
1909 for abs, (rel, exact) in sorted(names.items()):
1910 mfentry = mf.get(abs)
1910 mfentry = mf.get(abs)
1911 target = repo.wjoin(abs)
1911 target = repo.wjoin(abs)
1912 def handle(xlist, dobackup):
1912 def handle(xlist, dobackup):
1913 xlist[0].append(abs)
1913 xlist[0].append(abs)
1914 if (dobackup and not opts.get('no_backup') and
1914 if (dobackup and not opts.get('no_backup') and
1915 os.path.lexists(target)):
1915 os.path.lexists(target)):
1916 bakname = "%s.orig" % rel
1916 bakname = "%s.orig" % rel
1917 ui.note(_('saving current version of %s as %s\n') %
1917 ui.note(_('saving current version of %s as %s\n') %
1918 (rel, bakname))
1918 (rel, bakname))
1919 if not opts.get('dry_run'):
1919 if not opts.get('dry_run'):
1920 util.rename(target, bakname)
1920 util.rename(target, bakname)
1921 if ui.verbose or not exact:
1921 if ui.verbose or not exact:
1922 msg = xlist[1]
1922 msg = xlist[1]
1923 if not isinstance(msg, basestring):
1923 if not isinstance(msg, basestring):
1924 msg = msg(abs)
1924 msg = msg(abs)
1925 ui.status(msg % rel)
1925 ui.status(msg % rel)
1926 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1926 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1927 if abs not in table:
1927 if abs not in table:
1928 continue
1928 continue
1929 # file has changed in dirstate
1929 # file has changed in dirstate
1930 if mfentry:
1930 if mfentry:
1931 handle(hitlist, backuphit)
1931 handle(hitlist, backuphit)
1932 elif misslist is not None:
1932 elif misslist is not None:
1933 handle(misslist, backupmiss)
1933 handle(misslist, backupmiss)
1934 break
1934 break
1935 else:
1935 else:
1936 if abs not in repo.dirstate:
1936 if abs not in repo.dirstate:
1937 if mfentry:
1937 if mfentry:
1938 handle(add, True)
1938 handle(add, True)
1939 elif exact:
1939 elif exact:
1940 ui.warn(_('file not managed: %s\n') % rel)
1940 ui.warn(_('file not managed: %s\n') % rel)
1941 continue
1941 continue
1942 # file has not changed in dirstate
1942 # file has not changed in dirstate
1943 if node == parent:
1943 if node == parent:
1944 if exact:
1944 if exact:
1945 ui.warn(_('no changes needed to %s\n') % rel)
1945 ui.warn(_('no changes needed to %s\n') % rel)
1946 continue
1946 continue
1947 if pmf is None:
1947 if pmf is None:
1948 # only need parent manifest in this unlikely case,
1948 # only need parent manifest in this unlikely case,
1949 # so do not read by default
1949 # so do not read by default
1950 pmf = repo[parent].manifest()
1950 pmf = repo[parent].manifest()
1951 if abs in pmf and mfentry:
1951 if abs in pmf and mfentry:
1952 # if version of file is same in parent and target
1952 # if version of file is same in parent and target
1953 # manifests, do nothing
1953 # manifests, do nothing
1954 if (pmf[abs] != mfentry or
1954 if (pmf[abs] != mfentry or
1955 pmf.flags(abs) != mf.flags(abs)):
1955 pmf.flags(abs) != mf.flags(abs)):
1956 handle(revert, False)
1956 handle(revert, False)
1957 else:
1957 else:
1958 handle(remove, False)
1958 handle(remove, False)
1959
1959
1960 if not opts.get('dry_run'):
1960 if not opts.get('dry_run'):
1961 def checkout(f):
1961 def checkout(f):
1962 fc = ctx[f]
1962 fc = ctx[f]
1963 repo.wwrite(f, fc.data(), fc.flags())
1963 repo.wwrite(f, fc.data(), fc.flags())
1964
1964
1965 audit_path = scmutil.pathauditor(repo.root)
1965 audit_path = scmutil.pathauditor(repo.root)
1966 for f in remove[0]:
1966 for f in remove[0]:
1967 if repo.dirstate[f] == 'a':
1967 if repo.dirstate[f] == 'a':
1968 repo.dirstate.drop(f)
1968 repo.dirstate.drop(f)
1969 continue
1969 continue
1970 audit_path(f)
1970 audit_path(f)
1971 try:
1971 try:
1972 util.unlinkpath(repo.wjoin(f))
1972 util.unlinkpath(repo.wjoin(f))
1973 except OSError:
1973 except OSError:
1974 pass
1974 pass
1975 repo.dirstate.remove(f)
1975 repo.dirstate.remove(f)
1976
1976
1977 normal = None
1977 normal = None
1978 if node == parent:
1978 if node == parent:
1979 # We're reverting to our parent. If possible, we'd like status
1979 # We're reverting to our parent. If possible, we'd like status
1980 # to report the file as clean. We have to use normallookup for
1980 # to report the file as clean. We have to use normallookup for
1981 # merges to avoid losing information about merged/dirty files.
1981 # merges to avoid losing information about merged/dirty files.
1982 if p2 != nullid:
1982 if p2 != nullid:
1983 normal = repo.dirstate.normallookup
1983 normal = repo.dirstate.normallookup
1984 else:
1984 else:
1985 normal = repo.dirstate.normal
1985 normal = repo.dirstate.normal
1986 for f in revert[0]:
1986 for f in revert[0]:
1987 checkout(f)
1987 checkout(f)
1988 if normal:
1988 if normal:
1989 normal(f)
1989 normal(f)
1990
1990
1991 for f in add[0]:
1991 for f in add[0]:
1992 checkout(f)
1992 checkout(f)
1993 repo.dirstate.add(f)
1993 repo.dirstate.add(f)
1994
1994
1995 normal = repo.dirstate.normallookup
1995 normal = repo.dirstate.normallookup
1996 if node == parent and p2 == nullid:
1996 if node == parent and p2 == nullid:
1997 normal = repo.dirstate.normal
1997 normal = repo.dirstate.normal
1998 for f in undelete[0]:
1998 for f in undelete[0]:
1999 checkout(f)
1999 checkout(f)
2000 normal(f)
2000 normal(f)
2001
2001
2002 if targetsubs:
2002 if targetsubs:
2003 # Revert the subrepos on the revert list
2003 # Revert the subrepos on the revert list
2004 for sub in targetsubs:
2004 for sub in targetsubs:
2005 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2005 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2006 finally:
2006 finally:
2007 wlock.release()
2007 wlock.release()
2008
2008
2009 def command(table):
2009 def command(table):
2010 '''returns a function object bound to table which can be used as
2010 '''returns a function object bound to table which can be used as
2011 a decorator for populating table as a command table'''
2011 a decorator for populating table as a command table'''
2012
2012
2013 def cmd(name, options, synopsis=None):
2013 def cmd(name, options, synopsis=None):
2014 def decorator(func):
2014 def decorator(func):
2015 if synopsis:
2015 if synopsis:
2016 table[name] = func, options[:], synopsis
2016 table[name] = func, options[:], synopsis
2017 else:
2017 else:
2018 table[name] = func, options[:]
2018 table[name] = func, options[:]
2019 return func
2019 return func
2020 return decorator
2020 return decorator
2021
2021
2022 return cmd
2022 return cmd
@@ -1,1281 +1,1281
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 hg = None
12 hg = None
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 nullstate = ('', '', 'empty')
15 nullstate = ('', '', 'empty')
16
16
17 def state(ctx, ui):
17 def state(ctx, ui):
18 """return a state dict, mapping subrepo paths configured in .hgsub
18 """return a state dict, mapping subrepo paths configured in .hgsub
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 (key in types dict))
20 (key in types dict))
21 """
21 """
22 p = config.config()
22 p = config.config()
23 def read(f, sections=None, remap=None):
23 def read(f, sections=None, remap=None):
24 if f in ctx:
24 if f in ctx:
25 try:
25 try:
26 data = ctx[f].data()
26 data = ctx[f].data()
27 except IOError, err:
27 except IOError, err:
28 if err.errno != errno.ENOENT:
28 if err.errno != errno.ENOENT:
29 raise
29 raise
30 # handle missing subrepo spec files as removed
30 # handle missing subrepo spec files as removed
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 return
32 return
33 p.parse(f, data, sections, remap, read)
33 p.parse(f, data, sections, remap, read)
34 else:
34 else:
35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 raise util.Abort(_("subrepo spec file %s not found") % f)
36
36
37 if '.hgsub' in ctx:
37 if '.hgsub' in ctx:
38 read('.hgsub')
38 read('.hgsub')
39
39
40 for path, src in ui.configitems('subpaths'):
40 for path, src in ui.configitems('subpaths'):
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42
42
43 rev = {}
43 rev = {}
44 if '.hgsubstate' in ctx:
44 if '.hgsubstate' in ctx:
45 try:
45 try:
46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 l = l.lstrip()
47 l = l.lstrip()
48 if not l:
48 if not l:
49 continue
49 continue
50 try:
50 try:
51 revision, path = l.split(" ", 1)
51 revision, path = l.split(" ", 1)
52 except ValueError:
52 except ValueError:
53 raise util.Abort(_("invalid subrepository revision "
53 raise util.Abort(_("invalid subrepository revision "
54 "specifier in .hgsubstate line %d")
54 "specifier in .hgsubstate line %d")
55 % (i + 1))
55 % (i + 1))
56 rev[path] = revision
56 rev[path] = revision
57 except IOError, err:
57 except IOError, err:
58 if err.errno != errno.ENOENT:
58 if err.errno != errno.ENOENT:
59 raise
59 raise
60
60
61 def remap(src):
61 def remap(src):
62 for pattern, repl in p.items('subpaths'):
62 for pattern, repl in p.items('subpaths'):
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 # does a string decode.
64 # does a string decode.
65 repl = repl.encode('string-escape')
65 repl = repl.encode('string-escape')
66 # However, we still want to allow back references to go
66 # However, we still want to allow back references to go
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 # extra escapes are needed because re.sub string decodes.
68 # extra escapes are needed because re.sub string decodes.
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 try:
70 try:
71 src = re.sub(pattern, repl, src, 1)
71 src = re.sub(pattern, repl, src, 1)
72 except re.error, e:
72 except re.error, e:
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 % (p.source('subpaths', pattern), e))
74 % (p.source('subpaths', pattern), e))
75 return src
75 return src
76
76
77 state = {}
77 state = {}
78 for path, src in p[''].items():
78 for path, src in p[''].items():
79 kind = 'hg'
79 kind = 'hg'
80 if src.startswith('['):
80 if src.startswith('['):
81 if ']' not in src:
81 if ']' not in src:
82 raise util.Abort(_('missing ] in subrepo source'))
82 raise util.Abort(_('missing ] in subrepo source'))
83 kind, src = src.split(']', 1)
83 kind, src = src.split(']', 1)
84 kind = kind[1:]
84 kind = kind[1:]
85 src = src.lstrip() # strip any extra whitespace after ']'
85 src = src.lstrip() # strip any extra whitespace after ']'
86
86
87 if not util.url(src).isabs():
87 if not util.url(src).isabs():
88 parent = _abssource(ctx._repo, abort=False)
88 parent = _abssource(ctx._repo, abort=False)
89 if parent:
89 if parent:
90 parent = util.url(parent)
90 parent = util.url(parent)
91 parent.path = posixpath.join(parent.path or '', src)
91 parent.path = posixpath.join(parent.path or '', src)
92 parent.path = posixpath.normpath(parent.path)
92 parent.path = posixpath.normpath(parent.path)
93 joined = str(parent)
93 joined = str(parent)
94 # Remap the full joined path and use it if it changes,
94 # Remap the full joined path and use it if it changes,
95 # else remap the original source.
95 # else remap the original source.
96 remapped = remap(joined)
96 remapped = remap(joined)
97 if remapped == joined:
97 if remapped == joined:
98 src = remap(src)
98 src = remap(src)
99 else:
99 else:
100 src = remapped
100 src = remapped
101
101
102 src = remap(src)
102 src = remap(src)
103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104
104
105 return state
105 return state
106
106
107 def writestate(repo, state):
107 def writestate(repo, state):
108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 repo.wwrite('.hgsubstate', ''.join(lines), '')
110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111
111
112 def submerge(repo, wctx, mctx, actx, overwrite):
112 def submerge(repo, wctx, mctx, actx, overwrite):
113 """delegated from merge.applyupdates: merging of .hgsubstate file
113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 in working context, merging context and ancestor context"""
114 in working context, merging context and ancestor context"""
115 if mctx == actx: # backwards?
115 if mctx == actx: # backwards?
116 actx = wctx.p1()
116 actx = wctx.p1()
117 s1 = wctx.substate
117 s1 = wctx.substate
118 s2 = mctx.substate
118 s2 = mctx.substate
119 sa = actx.substate
119 sa = actx.substate
120 sm = {}
120 sm = {}
121
121
122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123
123
124 def debug(s, msg, r=""):
124 def debug(s, msg, r=""):
125 if r:
125 if r:
126 r = "%s:%s:%s" % r
126 r = "%s:%s:%s" % r
127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128
128
129 for s, l in s1.items():
129 for s, l in s1.items():
130 a = sa.get(s, nullstate)
130 a = sa.get(s, nullstate)
131 ld = l # local state with possible dirty flag for compares
131 ld = l # local state with possible dirty flag for compares
132 if wctx.sub(s).dirty():
132 if wctx.sub(s).dirty():
133 ld = (l[0], l[1] + "+")
133 ld = (l[0], l[1] + "+")
134 if wctx == actx: # overwrite
134 if wctx == actx: # overwrite
135 a = ld
135 a = ld
136
136
137 if s in s2:
137 if s in s2:
138 r = s2[s]
138 r = s2[s]
139 if ld == r or r == a: # no change or local is newer
139 if ld == r or r == a: # no change or local is newer
140 sm[s] = l
140 sm[s] = l
141 continue
141 continue
142 elif ld == a: # other side changed
142 elif ld == a: # other side changed
143 debug(s, "other changed, get", r)
143 debug(s, "other changed, get", r)
144 wctx.sub(s).get(r, overwrite)
144 wctx.sub(s).get(r, overwrite)
145 sm[s] = r
145 sm[s] = r
146 elif ld[0] != r[0]: # sources differ
146 elif ld[0] != r[0]: # sources differ
147 if repo.ui.promptchoice(
147 if repo.ui.promptchoice(
148 _(' subrepository sources for %s differ\n'
148 _(' subrepository sources for %s differ\n'
149 'use (l)ocal source (%s) or (r)emote source (%s)?')
149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 % (s, l[0], r[0]),
150 % (s, l[0], r[0]),
151 (_('&Local'), _('&Remote')), 0):
151 (_('&Local'), _('&Remote')), 0):
152 debug(s, "prompt changed, get", r)
152 debug(s, "prompt changed, get", r)
153 wctx.sub(s).get(r, overwrite)
153 wctx.sub(s).get(r, overwrite)
154 sm[s] = r
154 sm[s] = r
155 elif ld[1] == a[1]: # local side is unchanged
155 elif ld[1] == a[1]: # local side is unchanged
156 debug(s, "other side changed, get", r)
156 debug(s, "other side changed, get", r)
157 wctx.sub(s).get(r, overwrite)
157 wctx.sub(s).get(r, overwrite)
158 sm[s] = r
158 sm[s] = r
159 else:
159 else:
160 debug(s, "both sides changed, merge with", r)
160 debug(s, "both sides changed, merge with", r)
161 wctx.sub(s).merge(r)
161 wctx.sub(s).merge(r)
162 sm[s] = l
162 sm[s] = l
163 elif ld == a: # remote removed, local unchanged
163 elif ld == a: # remote removed, local unchanged
164 debug(s, "remote removed, remove")
164 debug(s, "remote removed, remove")
165 wctx.sub(s).remove()
165 wctx.sub(s).remove()
166 elif a == nullstate: # not present in remote or ancestor
166 elif a == nullstate: # not present in remote or ancestor
167 debug(s, "local added, keep")
167 debug(s, "local added, keep")
168 sm[s] = l
168 sm[s] = l
169 continue
169 continue
170 else:
170 else:
171 if repo.ui.promptchoice(
171 if repo.ui.promptchoice(
172 _(' local changed subrepository %s which remote removed\n'
172 _(' local changed subrepository %s which remote removed\n'
173 'use (c)hanged version or (d)elete?') % s,
173 'use (c)hanged version or (d)elete?') % s,
174 (_('&Changed'), _('&Delete')), 0):
174 (_('&Changed'), _('&Delete')), 0):
175 debug(s, "prompt remove")
175 debug(s, "prompt remove")
176 wctx.sub(s).remove()
176 wctx.sub(s).remove()
177
177
178 for s, r in sorted(s2.items()):
178 for s, r in sorted(s2.items()):
179 if s in s1:
179 if s in s1:
180 continue
180 continue
181 elif s not in sa:
181 elif s not in sa:
182 debug(s, "remote added, get", r)
182 debug(s, "remote added, get", r)
183 mctx.sub(s).get(r)
183 mctx.sub(s).get(r)
184 sm[s] = r
184 sm[s] = r
185 elif r != sa[s]:
185 elif r != sa[s]:
186 if repo.ui.promptchoice(
186 if repo.ui.promptchoice(
187 _(' remote changed subrepository %s which local removed\n'
187 _(' remote changed subrepository %s which local removed\n'
188 'use (c)hanged version or (d)elete?') % s,
188 'use (c)hanged version or (d)elete?') % s,
189 (_('&Changed'), _('&Delete')), 0) == 0:
189 (_('&Changed'), _('&Delete')), 0) == 0:
190 debug(s, "prompt recreate", r)
190 debug(s, "prompt recreate", r)
191 wctx.sub(s).get(r)
191 wctx.sub(s).get(r)
192 sm[s] = r
192 sm[s] = r
193
193
194 # record merged .hgsubstate
194 # record merged .hgsubstate
195 writestate(repo, sm)
195 writestate(repo, sm)
196
196
197 def _updateprompt(ui, sub, dirty, local, remote):
197 def _updateprompt(ui, sub, dirty, local, remote):
198 if dirty:
198 if dirty:
199 msg = (_(' subrepository sources for %s differ\n'
199 msg = (_(' subrepository sources for %s differ\n'
200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 % (subrelpath(sub), local, remote))
201 % (subrelpath(sub), local, remote))
202 else:
202 else:
203 msg = (_(' subrepository sources for %s differ (in checked out '
203 msg = (_(' subrepository sources for %s differ (in checked out '
204 'version)\n'
204 'version)\n'
205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 % (subrelpath(sub), local, remote))
206 % (subrelpath(sub), local, remote))
207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208
208
209 def reporelpath(repo):
209 def reporelpath(repo):
210 """return path to this (sub)repo as seen from outermost repo"""
210 """return path to this (sub)repo as seen from outermost repo"""
211 parent = repo
211 parent = repo
212 while util.safehasattr(parent, '_subparent'):
212 while util.safehasattr(parent, '_subparent'):
213 parent = parent._subparent
213 parent = parent._subparent
214 p = parent.root.rstrip(os.sep)
214 p = parent.root.rstrip(os.sep)
215 return repo.root[len(p) + 1:]
215 return repo.root[len(p) + 1:]
216
216
217 def subrelpath(sub):
217 def subrelpath(sub):
218 """return path to this subrepo as seen from outermost repo"""
218 """return path to this subrepo as seen from outermost repo"""
219 if util.safehasattr(sub, '_relpath'):
219 if util.safehasattr(sub, '_relpath'):
220 return sub._relpath
220 return sub._relpath
221 if not util.safehasattr(sub, '_repo'):
221 if not util.safehasattr(sub, '_repo'):
222 return sub._path
222 return sub._path
223 return reporelpath(sub._repo)
223 return reporelpath(sub._repo)
224
224
225 def _abssource(repo, push=False, abort=True):
225 def _abssource(repo, push=False, abort=True):
226 """return pull/push path of repo - either based on parent repo .hgsub info
226 """return pull/push path of repo - either based on parent repo .hgsub info
227 or on the top repo config. Abort or return None if no source found."""
227 or on the top repo config. Abort or return None if no source found."""
228 if util.safehasattr(repo, '_subparent'):
228 if util.safehasattr(repo, '_subparent'):
229 source = util.url(repo._subsource)
229 source = util.url(repo._subsource)
230 if source.isabs():
230 if source.isabs():
231 return str(source)
231 return str(source)
232 source.path = posixpath.normpath(source.path)
232 source.path = posixpath.normpath(source.path)
233 parent = _abssource(repo._subparent, push, abort=False)
233 parent = _abssource(repo._subparent, push, abort=False)
234 if parent:
234 if parent:
235 parent = util.url(util.pconvert(parent))
235 parent = util.url(util.pconvert(parent))
236 parent.path = posixpath.join(parent.path or '', source.path)
236 parent.path = posixpath.join(parent.path or '', source.path)
237 parent.path = posixpath.normpath(parent.path)
237 parent.path = posixpath.normpath(parent.path)
238 return str(parent)
238 return str(parent)
239 else: # recursion reached top repo
239 else: # recursion reached top repo
240 if util.safehasattr(repo, '_subtoppath'):
240 if util.safehasattr(repo, '_subtoppath'):
241 return repo._subtoppath
241 return repo._subtoppath
242 if push and repo.ui.config('paths', 'default-push'):
242 if push and repo.ui.config('paths', 'default-push'):
243 return repo.ui.config('paths', 'default-push')
243 return repo.ui.config('paths', 'default-push')
244 if repo.ui.config('paths', 'default'):
244 if repo.ui.config('paths', 'default'):
245 return repo.ui.config('paths', 'default')
245 return repo.ui.config('paths', 'default')
246 if abort:
246 if abort:
247 raise util.Abort(_("default path for subrepository %s not found") %
247 raise util.Abort(_("default path for subrepository %s not found") %
248 reporelpath(repo))
248 reporelpath(repo))
249
249
250 def itersubrepos(ctx1, ctx2):
250 def itersubrepos(ctx1, ctx2):
251 """find subrepos in ctx1 or ctx2"""
251 """find subrepos in ctx1 or ctx2"""
252 # Create a (subpath, ctx) mapping where we prefer subpaths from
252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 # has been modified (in ctx2) but not yet committed (in ctx1).
254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 for subpath, ctx in sorted(subpaths.iteritems()):
257 for subpath, ctx in sorted(subpaths.iteritems()):
258 yield subpath, ctx.sub(subpath)
258 yield subpath, ctx.sub(subpath)
259
259
260 def subrepo(ctx, path):
260 def subrepo(ctx, path):
261 """return instance of the right subrepo class for subrepo in path"""
261 """return instance of the right subrepo class for subrepo in path"""
262 # subrepo inherently violates our import layering rules
262 # subrepo inherently violates our import layering rules
263 # because it wants to make repo objects from deep inside the stack
263 # because it wants to make repo objects from deep inside the stack
264 # so we manually delay the circular imports to not break
264 # so we manually delay the circular imports to not break
265 # scripts that don't use our demand-loading
265 # scripts that don't use our demand-loading
266 global hg
266 global hg
267 import hg as h
267 import hg as h
268 hg = h
268 hg = h
269
269
270 scmutil.pathauditor(ctx._repo.root)(path)
270 scmutil.pathauditor(ctx._repo.root)(path)
271 state = ctx.substate[path]
271 state = ctx.substate[path]
272 if state[2] not in types:
272 if state[2] not in types:
273 raise util.Abort(_('unknown subrepo type %s') % state[2])
273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 return types[state[2]](ctx, path, state[:2])
274 return types[state[2]](ctx, path, state[:2])
275
275
276 # subrepo classes need to implement the following abstract class:
276 # subrepo classes need to implement the following abstract class:
277
277
278 class abstractsubrepo(object):
278 class abstractsubrepo(object):
279
279
280 def dirty(self, ignoreupdate=False):
280 def dirty(self, ignoreupdate=False):
281 """returns true if the dirstate of the subrepo is dirty or does not
281 """returns true if the dirstate of the subrepo is dirty or does not
282 match current stored state. If ignoreupdate is true, only check
282 match current stored state. If ignoreupdate is true, only check
283 whether the subrepo has uncommitted changes in its dirstate.
283 whether the subrepo has uncommitted changes in its dirstate.
284 """
284 """
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def basestate(self):
287 def basestate(self):
288 """current working directory base state, disregarding .hgsubstate
288 """current working directory base state, disregarding .hgsubstate
289 state and working directory modifications"""
289 state and working directory modifications"""
290 raise NotImplementedError
290 raise NotImplementedError
291
291
292 def checknested(self, path):
292 def checknested(self, path):
293 """check if path is a subrepository within this repository"""
293 """check if path is a subrepository within this repository"""
294 return False
294 return False
295
295
296 def commit(self, text, user, date):
296 def commit(self, text, user, date):
297 """commit the current changes to the subrepo with the given
297 """commit the current changes to the subrepo with the given
298 log message. Use given user and date if possible. Return the
298 log message. Use given user and date if possible. Return the
299 new state of the subrepo.
299 new state of the subrepo.
300 """
300 """
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 def remove(self):
303 def remove(self):
304 """remove the subrepo
304 """remove the subrepo
305
305
306 (should verify the dirstate is not dirty first)
306 (should verify the dirstate is not dirty first)
307 """
307 """
308 raise NotImplementedError
308 raise NotImplementedError
309
309
310 def get(self, state, overwrite=False):
310 def get(self, state, overwrite=False):
311 """run whatever commands are needed to put the subrepo into
311 """run whatever commands are needed to put the subrepo into
312 this state
312 this state
313 """
313 """
314 raise NotImplementedError
314 raise NotImplementedError
315
315
316 def merge(self, state):
316 def merge(self, state):
317 """merge currently-saved state with the new state."""
317 """merge currently-saved state with the new state."""
318 raise NotImplementedError
318 raise NotImplementedError
319
319
320 def push(self, opts):
320 def push(self, opts):
321 """perform whatever action is analogous to 'hg push'
321 """perform whatever action is analogous to 'hg push'
322
322
323 This may be a no-op on some systems.
323 This may be a no-op on some systems.
324 """
324 """
325 raise NotImplementedError
325 raise NotImplementedError
326
326
327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 return []
328 return []
329
329
330 def status(self, rev2, **opts):
330 def status(self, rev2, **opts):
331 return [], [], [], [], [], [], []
331 return [], [], [], [], [], [], []
332
332
333 def diff(self, diffopts, node2, match, prefix, **opts):
333 def diff(self, ui, diffopts, node2, match, prefix, **opts):
334 pass
334 pass
335
335
336 def outgoing(self, ui, dest, opts):
336 def outgoing(self, ui, dest, opts):
337 return 1
337 return 1
338
338
339 def incoming(self, ui, source, opts):
339 def incoming(self, ui, source, opts):
340 return 1
340 return 1
341
341
342 def files(self):
342 def files(self):
343 """return filename iterator"""
343 """return filename iterator"""
344 raise NotImplementedError
344 raise NotImplementedError
345
345
346 def filedata(self, name):
346 def filedata(self, name):
347 """return file data"""
347 """return file data"""
348 raise NotImplementedError
348 raise NotImplementedError
349
349
350 def fileflags(self, name):
350 def fileflags(self, name):
351 """return file flags"""
351 """return file flags"""
352 return ''
352 return ''
353
353
354 def archive(self, ui, archiver, prefix, match=None):
354 def archive(self, ui, archiver, prefix, match=None):
355 if match is not None:
355 if match is not None:
356 files = [f for f in self.files() if match(f)]
356 files = [f for f in self.files() if match(f)]
357 else:
357 else:
358 files = self.files()
358 files = self.files()
359 total = len(files)
359 total = len(files)
360 relpath = subrelpath(self)
360 relpath = subrelpath(self)
361 ui.progress(_('archiving (%s)') % relpath, 0,
361 ui.progress(_('archiving (%s)') % relpath, 0,
362 unit=_('files'), total=total)
362 unit=_('files'), total=total)
363 for i, name in enumerate(files):
363 for i, name in enumerate(files):
364 flags = self.fileflags(name)
364 flags = self.fileflags(name)
365 mode = 'x' in flags and 0755 or 0644
365 mode = 'x' in flags and 0755 or 0644
366 symlink = 'l' in flags
366 symlink = 'l' in flags
367 archiver.addfile(os.path.join(prefix, self._path, name),
367 archiver.addfile(os.path.join(prefix, self._path, name),
368 mode, symlink, self.filedata(name))
368 mode, symlink, self.filedata(name))
369 ui.progress(_('archiving (%s)') % relpath, i + 1,
369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 unit=_('files'), total=total)
370 unit=_('files'), total=total)
371 ui.progress(_('archiving (%s)') % relpath, None)
371 ui.progress(_('archiving (%s)') % relpath, None)
372
372
373 def walk(self, match):
373 def walk(self, match):
374 '''
374 '''
375 walk recursively through the directory tree, finding all files
375 walk recursively through the directory tree, finding all files
376 matched by the match function
376 matched by the match function
377 '''
377 '''
378 pass
378 pass
379
379
380 def forget(self, ui, match, prefix):
380 def forget(self, ui, match, prefix):
381 return ([], [])
381 return ([], [])
382
382
383 def revert(self, ui, substate, *pats, **opts):
383 def revert(self, ui, substate, *pats, **opts):
384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 % (substate[0], substate[2]))
385 % (substate[0], substate[2]))
386 return []
386 return []
387
387
388 class hgsubrepo(abstractsubrepo):
388 class hgsubrepo(abstractsubrepo):
389 def __init__(self, ctx, path, state):
389 def __init__(self, ctx, path, state):
390 self._path = path
390 self._path = path
391 self._state = state
391 self._state = state
392 r = ctx._repo
392 r = ctx._repo
393 root = r.wjoin(path)
393 root = r.wjoin(path)
394 create = False
394 create = False
395 if not os.path.exists(os.path.join(root, '.hg')):
395 if not os.path.exists(os.path.join(root, '.hg')):
396 create = True
396 create = True
397 util.makedirs(root)
397 util.makedirs(root)
398 self._repo = hg.repository(r.baseui, root, create=create)
398 self._repo = hg.repository(r.baseui, root, create=create)
399 for s, k in [('ui', 'commitsubrepos')]:
399 for s, k in [('ui', 'commitsubrepos')]:
400 v = r.ui.config(s, k)
400 v = r.ui.config(s, k)
401 if v:
401 if v:
402 self._repo.ui.setconfig(s, k, v)
402 self._repo.ui.setconfig(s, k, v)
403 self._initrepo(r, state[0], create)
403 self._initrepo(r, state[0], create)
404
404
405 def _initrepo(self, parentrepo, source, create):
405 def _initrepo(self, parentrepo, source, create):
406 self._repo._subparent = parentrepo
406 self._repo._subparent = parentrepo
407 self._repo._subsource = source
407 self._repo._subsource = source
408
408
409 if create:
409 if create:
410 fp = self._repo.opener("hgrc", "w", text=True)
410 fp = self._repo.opener("hgrc", "w", text=True)
411 fp.write('[paths]\n')
411 fp.write('[paths]\n')
412
412
413 def addpathconfig(key, value):
413 def addpathconfig(key, value):
414 if value:
414 if value:
415 fp.write('%s = %s\n' % (key, value))
415 fp.write('%s = %s\n' % (key, value))
416 self._repo.ui.setconfig('paths', key, value)
416 self._repo.ui.setconfig('paths', key, value)
417
417
418 defpath = _abssource(self._repo, abort=False)
418 defpath = _abssource(self._repo, abort=False)
419 defpushpath = _abssource(self._repo, True, abort=False)
419 defpushpath = _abssource(self._repo, True, abort=False)
420 addpathconfig('default', defpath)
420 addpathconfig('default', defpath)
421 if defpath != defpushpath:
421 if defpath != defpushpath:
422 addpathconfig('default-push', defpushpath)
422 addpathconfig('default-push', defpushpath)
423 fp.close()
423 fp.close()
424
424
425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
425 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
426 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
427 os.path.join(prefix, self._path), explicitonly)
427 os.path.join(prefix, self._path), explicitonly)
428
428
429 def status(self, rev2, **opts):
429 def status(self, rev2, **opts):
430 try:
430 try:
431 rev1 = self._state[1]
431 rev1 = self._state[1]
432 ctx1 = self._repo[rev1]
432 ctx1 = self._repo[rev1]
433 ctx2 = self._repo[rev2]
433 ctx2 = self._repo[rev2]
434 return self._repo.status(ctx1, ctx2, **opts)
434 return self._repo.status(ctx1, ctx2, **opts)
435 except error.RepoLookupError, inst:
435 except error.RepoLookupError, inst:
436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
436 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
437 % (inst, subrelpath(self)))
437 % (inst, subrelpath(self)))
438 return [], [], [], [], [], [], []
438 return [], [], [], [], [], [], []
439
439
440 def diff(self, diffopts, node2, match, prefix, **opts):
440 def diff(self, ui, diffopts, node2, match, prefix, **opts):
441 try:
441 try:
442 node1 = node.bin(self._state[1])
442 node1 = node.bin(self._state[1])
443 # We currently expect node2 to come from substate and be
443 # We currently expect node2 to come from substate and be
444 # in hex format
444 # in hex format
445 if node2 is not None:
445 if node2 is not None:
446 node2 = node.bin(node2)
446 node2 = node.bin(node2)
447 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
447 cmdutil.diffordiffstat(ui, self._repo, diffopts,
448 node1, node2, match,
448 node1, node2, match,
449 prefix=os.path.join(prefix, self._path),
449 prefix=os.path.join(prefix, self._path),
450 listsubrepos=True, **opts)
450 listsubrepos=True, **opts)
451 except error.RepoLookupError, inst:
451 except error.RepoLookupError, inst:
452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
452 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
453 % (inst, subrelpath(self)))
453 % (inst, subrelpath(self)))
454
454
455 def archive(self, ui, archiver, prefix, match=None):
455 def archive(self, ui, archiver, prefix, match=None):
456 self._get(self._state + ('hg',))
456 self._get(self._state + ('hg',))
457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
457 abstractsubrepo.archive(self, ui, archiver, prefix, match)
458
458
459 rev = self._state[1]
459 rev = self._state[1]
460 ctx = self._repo[rev]
460 ctx = self._repo[rev]
461 for subpath in ctx.substate:
461 for subpath in ctx.substate:
462 s = subrepo(ctx, subpath)
462 s = subrepo(ctx, subpath)
463 submatch = matchmod.narrowmatcher(subpath, match)
463 submatch = matchmod.narrowmatcher(subpath, match)
464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
464 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
465
465
466 def dirty(self, ignoreupdate=False):
466 def dirty(self, ignoreupdate=False):
467 r = self._state[1]
467 r = self._state[1]
468 if r == '' and not ignoreupdate: # no state recorded
468 if r == '' and not ignoreupdate: # no state recorded
469 return True
469 return True
470 w = self._repo[None]
470 w = self._repo[None]
471 if r != w.p1().hex() and not ignoreupdate:
471 if r != w.p1().hex() and not ignoreupdate:
472 # different version checked out
472 # different version checked out
473 return True
473 return True
474 return w.dirty() # working directory changed
474 return w.dirty() # working directory changed
475
475
476 def basestate(self):
476 def basestate(self):
477 return self._repo['.'].hex()
477 return self._repo['.'].hex()
478
478
479 def checknested(self, path):
479 def checknested(self, path):
480 return self._repo._checknested(self._repo.wjoin(path))
480 return self._repo._checknested(self._repo.wjoin(path))
481
481
482 def commit(self, text, user, date):
482 def commit(self, text, user, date):
483 # don't bother committing in the subrepo if it's only been
483 # don't bother committing in the subrepo if it's only been
484 # updated
484 # updated
485 if not self.dirty(True):
485 if not self.dirty(True):
486 return self._repo['.'].hex()
486 return self._repo['.'].hex()
487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
487 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
488 n = self._repo.commit(text, user, date)
488 n = self._repo.commit(text, user, date)
489 if not n:
489 if not n:
490 return self._repo['.'].hex() # different version checked out
490 return self._repo['.'].hex() # different version checked out
491 return node.hex(n)
491 return node.hex(n)
492
492
493 def remove(self):
493 def remove(self):
494 # we can't fully delete the repository as it may contain
494 # we can't fully delete the repository as it may contain
495 # local-only history
495 # local-only history
496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
496 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
497 hg.clean(self._repo, node.nullid, False)
497 hg.clean(self._repo, node.nullid, False)
498
498
499 def _get(self, state):
499 def _get(self, state):
500 source, revision, kind = state
500 source, revision, kind = state
501 if revision not in self._repo:
501 if revision not in self._repo:
502 self._repo._subsource = source
502 self._repo._subsource = source
503 srcurl = _abssource(self._repo)
503 srcurl = _abssource(self._repo)
504 other = hg.peer(self._repo, {}, srcurl)
504 other = hg.peer(self._repo, {}, srcurl)
505 if len(self._repo) == 0:
505 if len(self._repo) == 0:
506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
506 self._repo.ui.status(_('cloning subrepo %s from %s\n')
507 % (subrelpath(self), srcurl))
507 % (subrelpath(self), srcurl))
508 parentrepo = self._repo._subparent
508 parentrepo = self._repo._subparent
509 shutil.rmtree(self._repo.path)
509 shutil.rmtree(self._repo.path)
510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
510 other, cloned = hg.clone(self._repo._subparent.baseui, {},
511 other, self._repo.root,
511 other, self._repo.root,
512 update=False)
512 update=False)
513 self._repo = cloned.local()
513 self._repo = cloned.local()
514 self._initrepo(parentrepo, source, create=True)
514 self._initrepo(parentrepo, source, create=True)
515 else:
515 else:
516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
516 self._repo.ui.status(_('pulling subrepo %s from %s\n')
517 % (subrelpath(self), srcurl))
517 % (subrelpath(self), srcurl))
518 self._repo.pull(other)
518 self._repo.pull(other)
519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
519 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
520 srcurl)
520 srcurl)
521
521
522 def get(self, state, overwrite=False):
522 def get(self, state, overwrite=False):
523 self._get(state)
523 self._get(state)
524 source, revision, kind = state
524 source, revision, kind = state
525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
525 self._repo.ui.debug("getting subrepo %s\n" % self._path)
526 hg.updaterepo(self._repo, revision, overwrite)
526 hg.updaterepo(self._repo, revision, overwrite)
527
527
528 def merge(self, state):
528 def merge(self, state):
529 self._get(state)
529 self._get(state)
530 cur = self._repo['.']
530 cur = self._repo['.']
531 dst = self._repo[state[1]]
531 dst = self._repo[state[1]]
532 anc = dst.ancestor(cur)
532 anc = dst.ancestor(cur)
533
533
534 def mergefunc():
534 def mergefunc():
535 if anc == cur and dst.branch() == cur.branch():
535 if anc == cur and dst.branch() == cur.branch():
536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
536 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
537 hg.update(self._repo, state[1])
537 hg.update(self._repo, state[1])
538 elif anc == dst:
538 elif anc == dst:
539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
539 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
540 else:
540 else:
541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
541 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
542 hg.merge(self._repo, state[1], remind=False)
542 hg.merge(self._repo, state[1], remind=False)
543
543
544 wctx = self._repo[None]
544 wctx = self._repo[None]
545 if self.dirty():
545 if self.dirty():
546 if anc != dst:
546 if anc != dst:
547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
547 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
548 mergefunc()
548 mergefunc()
549 else:
549 else:
550 mergefunc()
550 mergefunc()
551 else:
551 else:
552 mergefunc()
552 mergefunc()
553
553
554 def push(self, opts):
554 def push(self, opts):
555 force = opts.get('force')
555 force = opts.get('force')
556 newbranch = opts.get('new_branch')
556 newbranch = opts.get('new_branch')
557 ssh = opts.get('ssh')
557 ssh = opts.get('ssh')
558
558
559 # push subrepos depth-first for coherent ordering
559 # push subrepos depth-first for coherent ordering
560 c = self._repo['']
560 c = self._repo['']
561 subs = c.substate # only repos that are committed
561 subs = c.substate # only repos that are committed
562 for s in sorted(subs):
562 for s in sorted(subs):
563 if c.sub(s).push(opts) == 0:
563 if c.sub(s).push(opts) == 0:
564 return False
564 return False
565
565
566 dsturl = _abssource(self._repo, True)
566 dsturl = _abssource(self._repo, True)
567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
567 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
568 (subrelpath(self), dsturl))
568 (subrelpath(self), dsturl))
569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
569 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
570 return self._repo.push(other, force, newbranch=newbranch)
570 return self._repo.push(other, force, newbranch=newbranch)
571
571
572 def outgoing(self, ui, dest, opts):
572 def outgoing(self, ui, dest, opts):
573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
573 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
574
574
575 def incoming(self, ui, source, opts):
575 def incoming(self, ui, source, opts):
576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
576 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
577
577
578 def files(self):
578 def files(self):
579 rev = self._state[1]
579 rev = self._state[1]
580 ctx = self._repo[rev]
580 ctx = self._repo[rev]
581 return ctx.manifest()
581 return ctx.manifest()
582
582
583 def filedata(self, name):
583 def filedata(self, name):
584 rev = self._state[1]
584 rev = self._state[1]
585 return self._repo[rev][name].data()
585 return self._repo[rev][name].data()
586
586
587 def fileflags(self, name):
587 def fileflags(self, name):
588 rev = self._state[1]
588 rev = self._state[1]
589 ctx = self._repo[rev]
589 ctx = self._repo[rev]
590 return ctx.flags(name)
590 return ctx.flags(name)
591
591
592 def walk(self, match):
592 def walk(self, match):
593 ctx = self._repo[None]
593 ctx = self._repo[None]
594 return ctx.walk(match)
594 return ctx.walk(match)
595
595
596 def forget(self, ui, match, prefix):
596 def forget(self, ui, match, prefix):
597 return cmdutil.forget(ui, self._repo, match,
597 return cmdutil.forget(ui, self._repo, match,
598 os.path.join(prefix, self._path), True)
598 os.path.join(prefix, self._path), True)
599
599
600 def revert(self, ui, substate, *pats, **opts):
600 def revert(self, ui, substate, *pats, **opts):
601 # reverting a subrepo is a 2 step process:
601 # reverting a subrepo is a 2 step process:
602 # 1. if the no_backup is not set, revert all modified
602 # 1. if the no_backup is not set, revert all modified
603 # files inside the subrepo
603 # files inside the subrepo
604 # 2. update the subrepo to the revision specified in
604 # 2. update the subrepo to the revision specified in
605 # the corresponding substate dictionary
605 # the corresponding substate dictionary
606 ui.status(_('reverting subrepo %s\n') % substate[0])
606 ui.status(_('reverting subrepo %s\n') % substate[0])
607 if not opts.get('no_backup'):
607 if not opts.get('no_backup'):
608 # Revert all files on the subrepo, creating backups
608 # Revert all files on the subrepo, creating backups
609 # Note that this will not recursively revert subrepos
609 # Note that this will not recursively revert subrepos
610 # We could do it if there was a set:subrepos() predicate
610 # We could do it if there was a set:subrepos() predicate
611 opts = opts.copy()
611 opts = opts.copy()
612 opts['date'] = None
612 opts['date'] = None
613 opts['rev'] = substate[1]
613 opts['rev'] = substate[1]
614
614
615 pats = []
615 pats = []
616 if not opts['all']:
616 if not opts['all']:
617 pats = ['set:modified()']
617 pats = ['set:modified()']
618 self.filerevert(ui, *pats, **opts)
618 self.filerevert(ui, *pats, **opts)
619
619
620 # Update the repo to the revision specified in the given substate
620 # Update the repo to the revision specified in the given substate
621 self.get(substate, overwrite=True)
621 self.get(substate, overwrite=True)
622
622
623 def filerevert(self, ui, *pats, **opts):
623 def filerevert(self, ui, *pats, **opts):
624 ctx = self._repo[opts['rev']]
624 ctx = self._repo[opts['rev']]
625 parents = self._repo.dirstate.parents()
625 parents = self._repo.dirstate.parents()
626 if opts['all']:
626 if opts['all']:
627 pats = ['set:modified()']
627 pats = ['set:modified()']
628 else:
628 else:
629 pats = []
629 pats = []
630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
630 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
631
631
632 class svnsubrepo(abstractsubrepo):
632 class svnsubrepo(abstractsubrepo):
633 def __init__(self, ctx, path, state):
633 def __init__(self, ctx, path, state):
634 self._path = path
634 self._path = path
635 self._state = state
635 self._state = state
636 self._ctx = ctx
636 self._ctx = ctx
637 self._ui = ctx._repo.ui
637 self._ui = ctx._repo.ui
638 self._exe = util.findexe('svn')
638 self._exe = util.findexe('svn')
639 if not self._exe:
639 if not self._exe:
640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
640 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
641 % self._path)
641 % self._path)
642
642
643 def _svncommand(self, commands, filename='', failok=False):
643 def _svncommand(self, commands, filename='', failok=False):
644 cmd = [self._exe]
644 cmd = [self._exe]
645 extrakw = {}
645 extrakw = {}
646 if not self._ui.interactive():
646 if not self._ui.interactive():
647 # Making stdin be a pipe should prevent svn from behaving
647 # Making stdin be a pipe should prevent svn from behaving
648 # interactively even if we can't pass --non-interactive.
648 # interactively even if we can't pass --non-interactive.
649 extrakw['stdin'] = subprocess.PIPE
649 extrakw['stdin'] = subprocess.PIPE
650 # Starting in svn 1.5 --non-interactive is a global flag
650 # Starting in svn 1.5 --non-interactive is a global flag
651 # instead of being per-command, but we need to support 1.4 so
651 # instead of being per-command, but we need to support 1.4 so
652 # we have to be intelligent about what commands take
652 # we have to be intelligent about what commands take
653 # --non-interactive.
653 # --non-interactive.
654 if commands[0] in ('update', 'checkout', 'commit'):
654 if commands[0] in ('update', 'checkout', 'commit'):
655 cmd.append('--non-interactive')
655 cmd.append('--non-interactive')
656 cmd.extend(commands)
656 cmd.extend(commands)
657 if filename is not None:
657 if filename is not None:
658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
658 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
659 cmd.append(path)
659 cmd.append(path)
660 env = dict(os.environ)
660 env = dict(os.environ)
661 # Avoid localized output, preserve current locale for everything else.
661 # Avoid localized output, preserve current locale for everything else.
662 lc_all = env.get('LC_ALL')
662 lc_all = env.get('LC_ALL')
663 if lc_all:
663 if lc_all:
664 env['LANG'] = lc_all
664 env['LANG'] = lc_all
665 del env['LC_ALL']
665 del env['LC_ALL']
666 env['LC_MESSAGES'] = 'C'
666 env['LC_MESSAGES'] = 'C'
667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
667 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
668 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
669 universal_newlines=True, env=env, **extrakw)
669 universal_newlines=True, env=env, **extrakw)
670 stdout, stderr = p.communicate()
670 stdout, stderr = p.communicate()
671 stderr = stderr.strip()
671 stderr = stderr.strip()
672 if not failok:
672 if not failok:
673 if p.returncode:
673 if p.returncode:
674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
674 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
675 if stderr:
675 if stderr:
676 self._ui.warn(stderr + '\n')
676 self._ui.warn(stderr + '\n')
677 return stdout, stderr
677 return stdout, stderr
678
678
679 @propertycache
679 @propertycache
680 def _svnversion(self):
680 def _svnversion(self):
681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
681 output, err = self._svncommand(['--version', '--quiet'], filename=None)
682 m = re.search(r'^(\d+)\.(\d+)', output)
682 m = re.search(r'^(\d+)\.(\d+)', output)
683 if not m:
683 if not m:
684 raise util.Abort(_('cannot retrieve svn tool version'))
684 raise util.Abort(_('cannot retrieve svn tool version'))
685 return (int(m.group(1)), int(m.group(2)))
685 return (int(m.group(1)), int(m.group(2)))
686
686
687 def _wcrevs(self):
687 def _wcrevs(self):
688 # Get the working directory revision as well as the last
688 # Get the working directory revision as well as the last
689 # commit revision so we can compare the subrepo state with
689 # commit revision so we can compare the subrepo state with
690 # both. We used to store the working directory one.
690 # both. We used to store the working directory one.
691 output, err = self._svncommand(['info', '--xml'])
691 output, err = self._svncommand(['info', '--xml'])
692 doc = xml.dom.minidom.parseString(output)
692 doc = xml.dom.minidom.parseString(output)
693 entries = doc.getElementsByTagName('entry')
693 entries = doc.getElementsByTagName('entry')
694 lastrev, rev = '0', '0'
694 lastrev, rev = '0', '0'
695 if entries:
695 if entries:
696 rev = str(entries[0].getAttribute('revision')) or '0'
696 rev = str(entries[0].getAttribute('revision')) or '0'
697 commits = entries[0].getElementsByTagName('commit')
697 commits = entries[0].getElementsByTagName('commit')
698 if commits:
698 if commits:
699 lastrev = str(commits[0].getAttribute('revision')) or '0'
699 lastrev = str(commits[0].getAttribute('revision')) or '0'
700 return (lastrev, rev)
700 return (lastrev, rev)
701
701
702 def _wcrev(self):
702 def _wcrev(self):
703 return self._wcrevs()[0]
703 return self._wcrevs()[0]
704
704
705 def _wcchanged(self):
705 def _wcchanged(self):
706 """Return (changes, extchanges, missing) where changes is True
706 """Return (changes, extchanges, missing) where changes is True
707 if the working directory was changed, extchanges is
707 if the working directory was changed, extchanges is
708 True if any of these changes concern an external entry and missing
708 True if any of these changes concern an external entry and missing
709 is True if any change is a missing entry.
709 is True if any change is a missing entry.
710 """
710 """
711 output, err = self._svncommand(['status', '--xml'])
711 output, err = self._svncommand(['status', '--xml'])
712 externals, changes, missing = [], [], []
712 externals, changes, missing = [], [], []
713 doc = xml.dom.minidom.parseString(output)
713 doc = xml.dom.minidom.parseString(output)
714 for e in doc.getElementsByTagName('entry'):
714 for e in doc.getElementsByTagName('entry'):
715 s = e.getElementsByTagName('wc-status')
715 s = e.getElementsByTagName('wc-status')
716 if not s:
716 if not s:
717 continue
717 continue
718 item = s[0].getAttribute('item')
718 item = s[0].getAttribute('item')
719 props = s[0].getAttribute('props')
719 props = s[0].getAttribute('props')
720 path = e.getAttribute('path')
720 path = e.getAttribute('path')
721 if item == 'external':
721 if item == 'external':
722 externals.append(path)
722 externals.append(path)
723 elif item == 'missing':
723 elif item == 'missing':
724 missing.append(path)
724 missing.append(path)
725 if (item not in ('', 'normal', 'unversioned', 'external')
725 if (item not in ('', 'normal', 'unversioned', 'external')
726 or props not in ('', 'none', 'normal')):
726 or props not in ('', 'none', 'normal')):
727 changes.append(path)
727 changes.append(path)
728 for path in changes:
728 for path in changes:
729 for ext in externals:
729 for ext in externals:
730 if path == ext or path.startswith(ext + os.sep):
730 if path == ext or path.startswith(ext + os.sep):
731 return True, True, bool(missing)
731 return True, True, bool(missing)
732 return bool(changes), False, bool(missing)
732 return bool(changes), False, bool(missing)
733
733
734 def dirty(self, ignoreupdate=False):
734 def dirty(self, ignoreupdate=False):
735 if not self._wcchanged()[0]:
735 if not self._wcchanged()[0]:
736 if self._state[1] in self._wcrevs() or ignoreupdate:
736 if self._state[1] in self._wcrevs() or ignoreupdate:
737 return False
737 return False
738 return True
738 return True
739
739
740 def basestate(self):
740 def basestate(self):
741 lastrev, rev = self._wcrevs()
741 lastrev, rev = self._wcrevs()
742 if lastrev != rev:
742 if lastrev != rev:
743 # Last committed rev is not the same than rev. We would
743 # Last committed rev is not the same than rev. We would
744 # like to take lastrev but we do not know if the subrepo
744 # like to take lastrev but we do not know if the subrepo
745 # URL exists at lastrev. Test it and fallback to rev it
745 # URL exists at lastrev. Test it and fallback to rev it
746 # is not there.
746 # is not there.
747 try:
747 try:
748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
748 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
749 return lastrev
749 return lastrev
750 except error.Abort:
750 except error.Abort:
751 pass
751 pass
752 return rev
752 return rev
753
753
754 def commit(self, text, user, date):
754 def commit(self, text, user, date):
755 # user and date are out of our hands since svn is centralized
755 # user and date are out of our hands since svn is centralized
756 changed, extchanged, missing = self._wcchanged()
756 changed, extchanged, missing = self._wcchanged()
757 if not changed:
757 if not changed:
758 return self.basestate()
758 return self.basestate()
759 if extchanged:
759 if extchanged:
760 # Do not try to commit externals
760 # Do not try to commit externals
761 raise util.Abort(_('cannot commit svn externals'))
761 raise util.Abort(_('cannot commit svn externals'))
762 if missing:
762 if missing:
763 # svn can commit with missing entries but aborting like hg
763 # svn can commit with missing entries but aborting like hg
764 # seems a better approach.
764 # seems a better approach.
765 raise util.Abort(_('cannot commit missing svn entries'))
765 raise util.Abort(_('cannot commit missing svn entries'))
766 commitinfo, err = self._svncommand(['commit', '-m', text])
766 commitinfo, err = self._svncommand(['commit', '-m', text])
767 self._ui.status(commitinfo)
767 self._ui.status(commitinfo)
768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
768 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
769 if not newrev:
769 if not newrev:
770 if not commitinfo.strip():
770 if not commitinfo.strip():
771 # Sometimes, our definition of "changed" differs from
771 # Sometimes, our definition of "changed" differs from
772 # svn one. For instance, svn ignores missing files
772 # svn one. For instance, svn ignores missing files
773 # when committing. If there are only missing files, no
773 # when committing. If there are only missing files, no
774 # commit is made, no output and no error code.
774 # commit is made, no output and no error code.
775 raise util.Abort(_('failed to commit svn changes'))
775 raise util.Abort(_('failed to commit svn changes'))
776 raise util.Abort(commitinfo.splitlines()[-1])
776 raise util.Abort(commitinfo.splitlines()[-1])
777 newrev = newrev.groups()[0]
777 newrev = newrev.groups()[0]
778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
778 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
779 return newrev
779 return newrev
780
780
781 def remove(self):
781 def remove(self):
782 if self.dirty():
782 if self.dirty():
783 self._ui.warn(_('not removing repo %s because '
783 self._ui.warn(_('not removing repo %s because '
784 'it has changes.\n' % self._path))
784 'it has changes.\n' % self._path))
785 return
785 return
786 self._ui.note(_('removing subrepo %s\n') % self._path)
786 self._ui.note(_('removing subrepo %s\n') % self._path)
787
787
788 def onerror(function, path, excinfo):
788 def onerror(function, path, excinfo):
789 if function is not os.remove:
789 if function is not os.remove:
790 raise
790 raise
791 # read-only files cannot be unlinked under Windows
791 # read-only files cannot be unlinked under Windows
792 s = os.stat(path)
792 s = os.stat(path)
793 if (s.st_mode & stat.S_IWRITE) != 0:
793 if (s.st_mode & stat.S_IWRITE) != 0:
794 raise
794 raise
795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
795 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
796 os.remove(path)
796 os.remove(path)
797
797
798 path = self._ctx._repo.wjoin(self._path)
798 path = self._ctx._repo.wjoin(self._path)
799 shutil.rmtree(path, onerror=onerror)
799 shutil.rmtree(path, onerror=onerror)
800 try:
800 try:
801 os.removedirs(os.path.dirname(path))
801 os.removedirs(os.path.dirname(path))
802 except OSError:
802 except OSError:
803 pass
803 pass
804
804
805 def get(self, state, overwrite=False):
805 def get(self, state, overwrite=False):
806 if overwrite:
806 if overwrite:
807 self._svncommand(['revert', '--recursive'])
807 self._svncommand(['revert', '--recursive'])
808 args = ['checkout']
808 args = ['checkout']
809 if self._svnversion >= (1, 5):
809 if self._svnversion >= (1, 5):
810 args.append('--force')
810 args.append('--force')
811 # The revision must be specified at the end of the URL to properly
811 # The revision must be specified at the end of the URL to properly
812 # update to a directory which has since been deleted and recreated.
812 # update to a directory which has since been deleted and recreated.
813 args.append('%s@%s' % (state[0], state[1]))
813 args.append('%s@%s' % (state[0], state[1]))
814 status, err = self._svncommand(args, failok=True)
814 status, err = self._svncommand(args, failok=True)
815 if not re.search('Checked out revision [0-9]+.', status):
815 if not re.search('Checked out revision [0-9]+.', status):
816 if ('is already a working copy for a different URL' in err
816 if ('is already a working copy for a different URL' in err
817 and (self._wcchanged()[:2] == (False, False))):
817 and (self._wcchanged()[:2] == (False, False))):
818 # obstructed but clean working copy, so just blow it away.
818 # obstructed but clean working copy, so just blow it away.
819 self.remove()
819 self.remove()
820 self.get(state, overwrite=False)
820 self.get(state, overwrite=False)
821 return
821 return
822 raise util.Abort((status or err).splitlines()[-1])
822 raise util.Abort((status or err).splitlines()[-1])
823 self._ui.status(status)
823 self._ui.status(status)
824
824
825 def merge(self, state):
825 def merge(self, state):
826 old = self._state[1]
826 old = self._state[1]
827 new = state[1]
827 new = state[1]
828 wcrev = self._wcrev()
828 wcrev = self._wcrev()
829 if new != wcrev:
829 if new != wcrev:
830 dirty = old == wcrev or self._wcchanged()[0]
830 dirty = old == wcrev or self._wcchanged()[0]
831 if _updateprompt(self._ui, self, dirty, wcrev, new):
831 if _updateprompt(self._ui, self, dirty, wcrev, new):
832 self.get(state, False)
832 self.get(state, False)
833
833
834 def push(self, opts):
834 def push(self, opts):
835 # push is a no-op for SVN
835 # push is a no-op for SVN
836 return True
836 return True
837
837
838 def files(self):
838 def files(self):
839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
839 output = self._svncommand(['list', '--recursive', '--xml'])[0]
840 doc = xml.dom.minidom.parseString(output)
840 doc = xml.dom.minidom.parseString(output)
841 paths = []
841 paths = []
842 for e in doc.getElementsByTagName('entry'):
842 for e in doc.getElementsByTagName('entry'):
843 kind = str(e.getAttribute('kind'))
843 kind = str(e.getAttribute('kind'))
844 if kind != 'file':
844 if kind != 'file':
845 continue
845 continue
846 name = ''.join(c.data for c
846 name = ''.join(c.data for c
847 in e.getElementsByTagName('name')[0].childNodes
847 in e.getElementsByTagName('name')[0].childNodes
848 if c.nodeType == c.TEXT_NODE)
848 if c.nodeType == c.TEXT_NODE)
849 paths.append(name.encode('utf-8'))
849 paths.append(name.encode('utf-8'))
850 return paths
850 return paths
851
851
852 def filedata(self, name):
852 def filedata(self, name):
853 return self._svncommand(['cat'], name)[0]
853 return self._svncommand(['cat'], name)[0]
854
854
855
855
856 class gitsubrepo(abstractsubrepo):
856 class gitsubrepo(abstractsubrepo):
857 def __init__(self, ctx, path, state):
857 def __init__(self, ctx, path, state):
858 self._state = state
858 self._state = state
859 self._ctx = ctx
859 self._ctx = ctx
860 self._path = path
860 self._path = path
861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
861 self._relpath = os.path.join(reporelpath(ctx._repo), path)
862 self._abspath = ctx._repo.wjoin(path)
862 self._abspath = ctx._repo.wjoin(path)
863 self._subparent = ctx._repo
863 self._subparent = ctx._repo
864 self._ui = ctx._repo.ui
864 self._ui = ctx._repo.ui
865 self._ensuregit()
865 self._ensuregit()
866
866
867 def _ensuregit(self):
867 def _ensuregit(self):
868 try:
868 try:
869 self._gitexecutable = 'git'
869 self._gitexecutable = 'git'
870 out, err = self._gitnodir(['--version'])
870 out, err = self._gitnodir(['--version'])
871 except OSError, e:
871 except OSError, e:
872 if e.errno != 2 or os.name != 'nt':
872 if e.errno != 2 or os.name != 'nt':
873 raise
873 raise
874 self._gitexecutable = 'git.cmd'
874 self._gitexecutable = 'git.cmd'
875 out, err = self._gitnodir(['--version'])
875 out, err = self._gitnodir(['--version'])
876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
876 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
877 if not m:
877 if not m:
878 self._ui.warn(_('cannot retrieve git version'))
878 self._ui.warn(_('cannot retrieve git version'))
879 return
879 return
880 version = (int(m.group(1)), m.group(2), m.group(3))
880 version = (int(m.group(1)), m.group(2), m.group(3))
881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
881 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
882 # despite the docstring comment. For now, error on 1.4.0, warn on
882 # despite the docstring comment. For now, error on 1.4.0, warn on
883 # 1.5.0 but attempt to continue.
883 # 1.5.0 but attempt to continue.
884 if version < (1, 5, 0):
884 if version < (1, 5, 0):
885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
885 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
886 elif version < (1, 6, 0):
886 elif version < (1, 6, 0):
887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
887 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
888
888
889 def _gitcommand(self, commands, env=None, stream=False):
889 def _gitcommand(self, commands, env=None, stream=False):
890 return self._gitdir(commands, env=env, stream=stream)[0]
890 return self._gitdir(commands, env=env, stream=stream)[0]
891
891
892 def _gitdir(self, commands, env=None, stream=False):
892 def _gitdir(self, commands, env=None, stream=False):
893 return self._gitnodir(commands, env=env, stream=stream,
893 return self._gitnodir(commands, env=env, stream=stream,
894 cwd=self._abspath)
894 cwd=self._abspath)
895
895
896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
896 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
897 """Calls the git command
897 """Calls the git command
898
898
899 The methods tries to call the git command. versions prior to 1.6.0
899 The methods tries to call the git command. versions prior to 1.6.0
900 are not supported and very probably fail.
900 are not supported and very probably fail.
901 """
901 """
902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
902 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
903 # unless ui.quiet is set, print git's stderr,
903 # unless ui.quiet is set, print git's stderr,
904 # which is mostly progress and useful info
904 # which is mostly progress and useful info
905 errpipe = None
905 errpipe = None
906 if self._ui.quiet:
906 if self._ui.quiet:
907 errpipe = open(os.devnull, 'w')
907 errpipe = open(os.devnull, 'w')
908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
908 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
909 cwd=cwd, env=env, close_fds=util.closefds,
909 cwd=cwd, env=env, close_fds=util.closefds,
910 stdout=subprocess.PIPE, stderr=errpipe)
910 stdout=subprocess.PIPE, stderr=errpipe)
911 if stream:
911 if stream:
912 return p.stdout, None
912 return p.stdout, None
913
913
914 retdata = p.stdout.read().strip()
914 retdata = p.stdout.read().strip()
915 # wait for the child to exit to avoid race condition.
915 # wait for the child to exit to avoid race condition.
916 p.wait()
916 p.wait()
917
917
918 if p.returncode != 0 and p.returncode != 1:
918 if p.returncode != 0 and p.returncode != 1:
919 # there are certain error codes that are ok
919 # there are certain error codes that are ok
920 command = commands[0]
920 command = commands[0]
921 if command in ('cat-file', 'symbolic-ref'):
921 if command in ('cat-file', 'symbolic-ref'):
922 return retdata, p.returncode
922 return retdata, p.returncode
923 # for all others, abort
923 # for all others, abort
924 raise util.Abort('git %s error %d in %s' %
924 raise util.Abort('git %s error %d in %s' %
925 (command, p.returncode, self._relpath))
925 (command, p.returncode, self._relpath))
926
926
927 return retdata, p.returncode
927 return retdata, p.returncode
928
928
929 def _gitmissing(self):
929 def _gitmissing(self):
930 return not os.path.exists(os.path.join(self._abspath, '.git'))
930 return not os.path.exists(os.path.join(self._abspath, '.git'))
931
931
932 def _gitstate(self):
932 def _gitstate(self):
933 return self._gitcommand(['rev-parse', 'HEAD'])
933 return self._gitcommand(['rev-parse', 'HEAD'])
934
934
935 def _gitcurrentbranch(self):
935 def _gitcurrentbranch(self):
936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
936 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
937 if err:
937 if err:
938 current = None
938 current = None
939 return current
939 return current
940
940
941 def _gitremote(self, remote):
941 def _gitremote(self, remote):
942 out = self._gitcommand(['remote', 'show', '-n', remote])
942 out = self._gitcommand(['remote', 'show', '-n', remote])
943 line = out.split('\n')[1]
943 line = out.split('\n')[1]
944 i = line.index('URL: ') + len('URL: ')
944 i = line.index('URL: ') + len('URL: ')
945 return line[i:]
945 return line[i:]
946
946
947 def _githavelocally(self, revision):
947 def _githavelocally(self, revision):
948 out, code = self._gitdir(['cat-file', '-e', revision])
948 out, code = self._gitdir(['cat-file', '-e', revision])
949 return code == 0
949 return code == 0
950
950
951 def _gitisancestor(self, r1, r2):
951 def _gitisancestor(self, r1, r2):
952 base = self._gitcommand(['merge-base', r1, r2])
952 base = self._gitcommand(['merge-base', r1, r2])
953 return base == r1
953 return base == r1
954
954
955 def _gitisbare(self):
955 def _gitisbare(self):
956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
956 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
957
957
958 def _gitupdatestat(self):
958 def _gitupdatestat(self):
959 """This must be run before git diff-index.
959 """This must be run before git diff-index.
960 diff-index only looks at changes to file stat;
960 diff-index only looks at changes to file stat;
961 this command looks at file contents and updates the stat."""
961 this command looks at file contents and updates the stat."""
962 self._gitcommand(['update-index', '-q', '--refresh'])
962 self._gitcommand(['update-index', '-q', '--refresh'])
963
963
964 def _gitbranchmap(self):
964 def _gitbranchmap(self):
965 '''returns 2 things:
965 '''returns 2 things:
966 a map from git branch to revision
966 a map from git branch to revision
967 a map from revision to branches'''
967 a map from revision to branches'''
968 branch2rev = {}
968 branch2rev = {}
969 rev2branch = {}
969 rev2branch = {}
970
970
971 out = self._gitcommand(['for-each-ref', '--format',
971 out = self._gitcommand(['for-each-ref', '--format',
972 '%(objectname) %(refname)'])
972 '%(objectname) %(refname)'])
973 for line in out.split('\n'):
973 for line in out.split('\n'):
974 revision, ref = line.split(' ')
974 revision, ref = line.split(' ')
975 if (not ref.startswith('refs/heads/') and
975 if (not ref.startswith('refs/heads/') and
976 not ref.startswith('refs/remotes/')):
976 not ref.startswith('refs/remotes/')):
977 continue
977 continue
978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
978 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
979 continue # ignore remote/HEAD redirects
979 continue # ignore remote/HEAD redirects
980 branch2rev[ref] = revision
980 branch2rev[ref] = revision
981 rev2branch.setdefault(revision, []).append(ref)
981 rev2branch.setdefault(revision, []).append(ref)
982 return branch2rev, rev2branch
982 return branch2rev, rev2branch
983
983
984 def _gittracking(self, branches):
984 def _gittracking(self, branches):
985 'return map of remote branch to local tracking branch'
985 'return map of remote branch to local tracking branch'
986 # assumes no more than one local tracking branch for each remote
986 # assumes no more than one local tracking branch for each remote
987 tracking = {}
987 tracking = {}
988 for b in branches:
988 for b in branches:
989 if b.startswith('refs/remotes/'):
989 if b.startswith('refs/remotes/'):
990 continue
990 continue
991 bname = b.split('/', 2)[2]
991 bname = b.split('/', 2)[2]
992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
992 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
993 if remote:
993 if remote:
994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
994 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
995 tracking['refs/remotes/%s/%s' %
995 tracking['refs/remotes/%s/%s' %
996 (remote, ref.split('/', 2)[2])] = b
996 (remote, ref.split('/', 2)[2])] = b
997 return tracking
997 return tracking
998
998
999 def _abssource(self, source):
999 def _abssource(self, source):
1000 if '://' not in source:
1000 if '://' not in source:
1001 # recognize the scp syntax as an absolute source
1001 # recognize the scp syntax as an absolute source
1002 colon = source.find(':')
1002 colon = source.find(':')
1003 if colon != -1 and '/' not in source[:colon]:
1003 if colon != -1 and '/' not in source[:colon]:
1004 return source
1004 return source
1005 self._subsource = source
1005 self._subsource = source
1006 return _abssource(self)
1006 return _abssource(self)
1007
1007
1008 def _fetch(self, source, revision):
1008 def _fetch(self, source, revision):
1009 if self._gitmissing():
1009 if self._gitmissing():
1010 source = self._abssource(source)
1010 source = self._abssource(source)
1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1011 self._ui.status(_('cloning subrepo %s from %s\n') %
1012 (self._relpath, source))
1012 (self._relpath, source))
1013 self._gitnodir(['clone', source, self._abspath])
1013 self._gitnodir(['clone', source, self._abspath])
1014 if self._githavelocally(revision):
1014 if self._githavelocally(revision):
1015 return
1015 return
1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1016 self._ui.status(_('pulling subrepo %s from %s\n') %
1017 (self._relpath, self._gitremote('origin')))
1017 (self._relpath, self._gitremote('origin')))
1018 # try only origin: the originally cloned repo
1018 # try only origin: the originally cloned repo
1019 self._gitcommand(['fetch'])
1019 self._gitcommand(['fetch'])
1020 if not self._githavelocally(revision):
1020 if not self._githavelocally(revision):
1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1021 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1022 (revision, self._relpath))
1022 (revision, self._relpath))
1023
1023
1024 def dirty(self, ignoreupdate=False):
1024 def dirty(self, ignoreupdate=False):
1025 if self._gitmissing():
1025 if self._gitmissing():
1026 return self._state[1] != ''
1026 return self._state[1] != ''
1027 if self._gitisbare():
1027 if self._gitisbare():
1028 return True
1028 return True
1029 if not ignoreupdate and self._state[1] != self._gitstate():
1029 if not ignoreupdate and self._state[1] != self._gitstate():
1030 # different version checked out
1030 # different version checked out
1031 return True
1031 return True
1032 # check for staged changes or modified files; ignore untracked files
1032 # check for staged changes or modified files; ignore untracked files
1033 self._gitupdatestat()
1033 self._gitupdatestat()
1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1034 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1035 return code == 1
1035 return code == 1
1036
1036
1037 def basestate(self):
1037 def basestate(self):
1038 return self._gitstate()
1038 return self._gitstate()
1039
1039
1040 def get(self, state, overwrite=False):
1040 def get(self, state, overwrite=False):
1041 source, revision, kind = state
1041 source, revision, kind = state
1042 if not revision:
1042 if not revision:
1043 self.remove()
1043 self.remove()
1044 return
1044 return
1045 self._fetch(source, revision)
1045 self._fetch(source, revision)
1046 # if the repo was set to be bare, unbare it
1046 # if the repo was set to be bare, unbare it
1047 if self._gitisbare():
1047 if self._gitisbare():
1048 self._gitcommand(['config', 'core.bare', 'false'])
1048 self._gitcommand(['config', 'core.bare', 'false'])
1049 if self._gitstate() == revision:
1049 if self._gitstate() == revision:
1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1050 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 return
1051 return
1052 elif self._gitstate() == revision:
1052 elif self._gitstate() == revision:
1053 if overwrite:
1053 if overwrite:
1054 # first reset the index to unmark new files for commit, because
1054 # first reset the index to unmark new files for commit, because
1055 # reset --hard will otherwise throw away files added for commit,
1055 # reset --hard will otherwise throw away files added for commit,
1056 # not just unmark them.
1056 # not just unmark them.
1057 self._gitcommand(['reset', 'HEAD'])
1057 self._gitcommand(['reset', 'HEAD'])
1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1058 self._gitcommand(['reset', '--hard', 'HEAD'])
1059 return
1059 return
1060 branch2rev, rev2branch = self._gitbranchmap()
1060 branch2rev, rev2branch = self._gitbranchmap()
1061
1061
1062 def checkout(args):
1062 def checkout(args):
1063 cmd = ['checkout']
1063 cmd = ['checkout']
1064 if overwrite:
1064 if overwrite:
1065 # first reset the index to unmark new files for commit, because
1065 # first reset the index to unmark new files for commit, because
1066 # the -f option will otherwise throw away files added for
1066 # the -f option will otherwise throw away files added for
1067 # commit, not just unmark them.
1067 # commit, not just unmark them.
1068 self._gitcommand(['reset', 'HEAD'])
1068 self._gitcommand(['reset', 'HEAD'])
1069 cmd.append('-f')
1069 cmd.append('-f')
1070 self._gitcommand(cmd + args)
1070 self._gitcommand(cmd + args)
1071
1071
1072 def rawcheckout():
1072 def rawcheckout():
1073 # no branch to checkout, check it out with no branch
1073 # no branch to checkout, check it out with no branch
1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1074 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1075 self._relpath)
1075 self._relpath)
1076 self._ui.warn(_('check out a git branch if you intend '
1076 self._ui.warn(_('check out a git branch if you intend '
1077 'to make changes\n'))
1077 'to make changes\n'))
1078 checkout(['-q', revision])
1078 checkout(['-q', revision])
1079
1079
1080 if revision not in rev2branch:
1080 if revision not in rev2branch:
1081 rawcheckout()
1081 rawcheckout()
1082 return
1082 return
1083 branches = rev2branch[revision]
1083 branches = rev2branch[revision]
1084 firstlocalbranch = None
1084 firstlocalbranch = None
1085 for b in branches:
1085 for b in branches:
1086 if b == 'refs/heads/master':
1086 if b == 'refs/heads/master':
1087 # master trumps all other branches
1087 # master trumps all other branches
1088 checkout(['refs/heads/master'])
1088 checkout(['refs/heads/master'])
1089 return
1089 return
1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1090 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1091 firstlocalbranch = b
1091 firstlocalbranch = b
1092 if firstlocalbranch:
1092 if firstlocalbranch:
1093 checkout([firstlocalbranch])
1093 checkout([firstlocalbranch])
1094 return
1094 return
1095
1095
1096 tracking = self._gittracking(branch2rev.keys())
1096 tracking = self._gittracking(branch2rev.keys())
1097 # choose a remote branch already tracked if possible
1097 # choose a remote branch already tracked if possible
1098 remote = branches[0]
1098 remote = branches[0]
1099 if remote not in tracking:
1099 if remote not in tracking:
1100 for b in branches:
1100 for b in branches:
1101 if b in tracking:
1101 if b in tracking:
1102 remote = b
1102 remote = b
1103 break
1103 break
1104
1104
1105 if remote not in tracking:
1105 if remote not in tracking:
1106 # create a new local tracking branch
1106 # create a new local tracking branch
1107 local = remote.split('/', 2)[2]
1107 local = remote.split('/', 2)[2]
1108 checkout(['-b', local, remote])
1108 checkout(['-b', local, remote])
1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1109 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1110 # When updating to a tracked remote branch,
1110 # When updating to a tracked remote branch,
1111 # if the local tracking branch is downstream of it,
1111 # if the local tracking branch is downstream of it,
1112 # a normal `git pull` would have performed a "fast-forward merge"
1112 # a normal `git pull` would have performed a "fast-forward merge"
1113 # which is equivalent to updating the local branch to the remote.
1113 # which is equivalent to updating the local branch to the remote.
1114 # Since we are only looking at branching at update, we need to
1114 # Since we are only looking at branching at update, we need to
1115 # detect this situation and perform this action lazily.
1115 # detect this situation and perform this action lazily.
1116 if tracking[remote] != self._gitcurrentbranch():
1116 if tracking[remote] != self._gitcurrentbranch():
1117 checkout([tracking[remote]])
1117 checkout([tracking[remote]])
1118 self._gitcommand(['merge', '--ff', remote])
1118 self._gitcommand(['merge', '--ff', remote])
1119 else:
1119 else:
1120 # a real merge would be required, just checkout the revision
1120 # a real merge would be required, just checkout the revision
1121 rawcheckout()
1121 rawcheckout()
1122
1122
1123 def commit(self, text, user, date):
1123 def commit(self, text, user, date):
1124 if self._gitmissing():
1124 if self._gitmissing():
1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1125 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1126 cmd = ['commit', '-a', '-m', text]
1126 cmd = ['commit', '-a', '-m', text]
1127 env = os.environ.copy()
1127 env = os.environ.copy()
1128 if user:
1128 if user:
1129 cmd += ['--author', user]
1129 cmd += ['--author', user]
1130 if date:
1130 if date:
1131 # git's date parser silently ignores when seconds < 1e9
1131 # git's date parser silently ignores when seconds < 1e9
1132 # convert to ISO8601
1132 # convert to ISO8601
1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1133 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1134 '%Y-%m-%dT%H:%M:%S %1%2')
1134 '%Y-%m-%dT%H:%M:%S %1%2')
1135 self._gitcommand(cmd, env=env)
1135 self._gitcommand(cmd, env=env)
1136 # make sure commit works otherwise HEAD might not exist under certain
1136 # make sure commit works otherwise HEAD might not exist under certain
1137 # circumstances
1137 # circumstances
1138 return self._gitstate()
1138 return self._gitstate()
1139
1139
1140 def merge(self, state):
1140 def merge(self, state):
1141 source, revision, kind = state
1141 source, revision, kind = state
1142 self._fetch(source, revision)
1142 self._fetch(source, revision)
1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1143 base = self._gitcommand(['merge-base', revision, self._state[1]])
1144 self._gitupdatestat()
1144 self._gitupdatestat()
1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1145 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1146
1146
1147 def mergefunc():
1147 def mergefunc():
1148 if base == revision:
1148 if base == revision:
1149 self.get(state) # fast forward merge
1149 self.get(state) # fast forward merge
1150 elif base != self._state[1]:
1150 elif base != self._state[1]:
1151 self._gitcommand(['merge', '--no-commit', revision])
1151 self._gitcommand(['merge', '--no-commit', revision])
1152
1152
1153 if self.dirty():
1153 if self.dirty():
1154 if self._gitstate() != revision:
1154 if self._gitstate() != revision:
1155 dirty = self._gitstate() == self._state[1] or code != 0
1155 dirty = self._gitstate() == self._state[1] or code != 0
1156 if _updateprompt(self._ui, self, dirty,
1156 if _updateprompt(self._ui, self, dirty,
1157 self._state[1][:7], revision[:7]):
1157 self._state[1][:7], revision[:7]):
1158 mergefunc()
1158 mergefunc()
1159 else:
1159 else:
1160 mergefunc()
1160 mergefunc()
1161
1161
1162 def push(self, opts):
1162 def push(self, opts):
1163 force = opts.get('force')
1163 force = opts.get('force')
1164
1164
1165 if not self._state[1]:
1165 if not self._state[1]:
1166 return True
1166 return True
1167 if self._gitmissing():
1167 if self._gitmissing():
1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1168 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1169 # if a branch in origin contains the revision, nothing to do
1169 # if a branch in origin contains the revision, nothing to do
1170 branch2rev, rev2branch = self._gitbranchmap()
1170 branch2rev, rev2branch = self._gitbranchmap()
1171 if self._state[1] in rev2branch:
1171 if self._state[1] in rev2branch:
1172 for b in rev2branch[self._state[1]]:
1172 for b in rev2branch[self._state[1]]:
1173 if b.startswith('refs/remotes/origin/'):
1173 if b.startswith('refs/remotes/origin/'):
1174 return True
1174 return True
1175 for b, revision in branch2rev.iteritems():
1175 for b, revision in branch2rev.iteritems():
1176 if b.startswith('refs/remotes/origin/'):
1176 if b.startswith('refs/remotes/origin/'):
1177 if self._gitisancestor(self._state[1], revision):
1177 if self._gitisancestor(self._state[1], revision):
1178 return True
1178 return True
1179 # otherwise, try to push the currently checked out branch
1179 # otherwise, try to push the currently checked out branch
1180 cmd = ['push']
1180 cmd = ['push']
1181 if force:
1181 if force:
1182 cmd.append('--force')
1182 cmd.append('--force')
1183
1183
1184 current = self._gitcurrentbranch()
1184 current = self._gitcurrentbranch()
1185 if current:
1185 if current:
1186 # determine if the current branch is even useful
1186 # determine if the current branch is even useful
1187 if not self._gitisancestor(self._state[1], current):
1187 if not self._gitisancestor(self._state[1], current):
1188 self._ui.warn(_('unrelated git branch checked out '
1188 self._ui.warn(_('unrelated git branch checked out '
1189 'in subrepo %s\n') % self._relpath)
1189 'in subrepo %s\n') % self._relpath)
1190 return False
1190 return False
1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1191 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1192 (current.split('/', 2)[2], self._relpath))
1192 (current.split('/', 2)[2], self._relpath))
1193 self._gitcommand(cmd + ['origin', current])
1193 self._gitcommand(cmd + ['origin', current])
1194 return True
1194 return True
1195 else:
1195 else:
1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1196 self._ui.warn(_('no branch checked out in subrepo %s\n'
1197 'cannot push revision %s\n') %
1197 'cannot push revision %s\n') %
1198 (self._relpath, self._state[1]))
1198 (self._relpath, self._state[1]))
1199 return False
1199 return False
1200
1200
1201 def remove(self):
1201 def remove(self):
1202 if self._gitmissing():
1202 if self._gitmissing():
1203 return
1203 return
1204 if self.dirty():
1204 if self.dirty():
1205 self._ui.warn(_('not removing repo %s because '
1205 self._ui.warn(_('not removing repo %s because '
1206 'it has changes.\n') % self._relpath)
1206 'it has changes.\n') % self._relpath)
1207 return
1207 return
1208 # we can't fully delete the repository as it may contain
1208 # we can't fully delete the repository as it may contain
1209 # local-only history
1209 # local-only history
1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1210 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1211 self._gitcommand(['config', 'core.bare', 'true'])
1211 self._gitcommand(['config', 'core.bare', 'true'])
1212 for f in os.listdir(self._abspath):
1212 for f in os.listdir(self._abspath):
1213 if f == '.git':
1213 if f == '.git':
1214 continue
1214 continue
1215 path = os.path.join(self._abspath, f)
1215 path = os.path.join(self._abspath, f)
1216 if os.path.isdir(path) and not os.path.islink(path):
1216 if os.path.isdir(path) and not os.path.islink(path):
1217 shutil.rmtree(path)
1217 shutil.rmtree(path)
1218 else:
1218 else:
1219 os.remove(path)
1219 os.remove(path)
1220
1220
1221 def archive(self, ui, archiver, prefix, match=None):
1221 def archive(self, ui, archiver, prefix, match=None):
1222 source, revision = self._state
1222 source, revision = self._state
1223 if not revision:
1223 if not revision:
1224 return
1224 return
1225 self._fetch(source, revision)
1225 self._fetch(source, revision)
1226
1226
1227 # Parse git's native archive command.
1227 # Parse git's native archive command.
1228 # This should be much faster than manually traversing the trees
1228 # This should be much faster than manually traversing the trees
1229 # and objects with many subprocess calls.
1229 # and objects with many subprocess calls.
1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1230 tarstream = self._gitcommand(['archive', revision], stream=True)
1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1231 tar = tarfile.open(fileobj=tarstream, mode='r|')
1232 relpath = subrelpath(self)
1232 relpath = subrelpath(self)
1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1233 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1234 for i, info in enumerate(tar):
1234 for i, info in enumerate(tar):
1235 if info.isdir():
1235 if info.isdir():
1236 continue
1236 continue
1237 if match and not match(info.name):
1237 if match and not match(info.name):
1238 continue
1238 continue
1239 if info.issym():
1239 if info.issym():
1240 data = info.linkname
1240 data = info.linkname
1241 else:
1241 else:
1242 data = tar.extractfile(info).read()
1242 data = tar.extractfile(info).read()
1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1243 archiver.addfile(os.path.join(prefix, self._path, info.name),
1244 info.mode, info.issym(), data)
1244 info.mode, info.issym(), data)
1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1245 ui.progress(_('archiving (%s)') % relpath, i + 1,
1246 unit=_('files'))
1246 unit=_('files'))
1247 ui.progress(_('archiving (%s)') % relpath, None)
1247 ui.progress(_('archiving (%s)') % relpath, None)
1248
1248
1249
1249
1250 def status(self, rev2, **opts):
1250 def status(self, rev2, **opts):
1251 rev1 = self._state[1]
1251 rev1 = self._state[1]
1252 if self._gitmissing() or not rev1:
1252 if self._gitmissing() or not rev1:
1253 # if the repo is missing, return no results
1253 # if the repo is missing, return no results
1254 return [], [], [], [], [], [], []
1254 return [], [], [], [], [], [], []
1255 modified, added, removed = [], [], []
1255 modified, added, removed = [], [], []
1256 self._gitupdatestat()
1256 self._gitupdatestat()
1257 if rev2:
1257 if rev2:
1258 command = ['diff-tree', rev1, rev2]
1258 command = ['diff-tree', rev1, rev2]
1259 else:
1259 else:
1260 command = ['diff-index', rev1]
1260 command = ['diff-index', rev1]
1261 out = self._gitcommand(command)
1261 out = self._gitcommand(command)
1262 for line in out.split('\n'):
1262 for line in out.split('\n'):
1263 tab = line.find('\t')
1263 tab = line.find('\t')
1264 if tab == -1:
1264 if tab == -1:
1265 continue
1265 continue
1266 status, f = line[tab - 1], line[tab + 1:]
1266 status, f = line[tab - 1], line[tab + 1:]
1267 if status == 'M':
1267 if status == 'M':
1268 modified.append(f)
1268 modified.append(f)
1269 elif status == 'A':
1269 elif status == 'A':
1270 added.append(f)
1270 added.append(f)
1271 elif status == 'D':
1271 elif status == 'D':
1272 removed.append(f)
1272 removed.append(f)
1273
1273
1274 deleted = unknown = ignored = clean = []
1274 deleted = unknown = ignored = clean = []
1275 return modified, added, removed, deleted, unknown, ignored, clean
1275 return modified, added, removed, deleted, unknown, ignored, clean
1276
1276
1277 types = {
1277 types = {
1278 'hg': hgsubrepo,
1278 'hg': hgsubrepo,
1279 'svn': svnsubrepo,
1279 'svn': svnsubrepo,
1280 'git': gitsubrepo,
1280 'git': gitsubrepo,
1281 }
1281 }
@@ -1,130 +1,162
1 Setup
1 Setup
2
2
3 $ echo "[color]" >> $HGRCPATH
3 $ echo "[color]" >> $HGRCPATH
4 $ echo "mode = ansi" >> $HGRCPATH
4 $ echo "mode = ansi" >> $HGRCPATH
5 $ echo "[extensions]" >> $HGRCPATH
5 $ echo "[extensions]" >> $HGRCPATH
6 $ echo "color=" >> $HGRCPATH
6 $ echo "color=" >> $HGRCPATH
7 $ hg init repo
7 $ hg init repo
8 $ cd repo
8 $ cd repo
9 $ cat > a <<EOF
9 $ cat > a <<EOF
10 > c
10 > c
11 > c
11 > c
12 > a
12 > a
13 > a
13 > a
14 > b
14 > b
15 > a
15 > a
16 > a
16 > a
17 > c
17 > c
18 > c
18 > c
19 > EOF
19 > EOF
20 $ hg ci -Am adda
20 $ hg ci -Am adda
21 adding a
21 adding a
22 $ cat > a <<EOF
22 $ cat > a <<EOF
23 > c
23 > c
24 > c
24 > c
25 > a
25 > a
26 > a
26 > a
27 > dd
27 > dd
28 > a
28 > a
29 > a
29 > a
30 > c
30 > c
31 > c
31 > c
32 > EOF
32 > EOF
33
33
34 default context
34 default context
35
35
36 $ hg diff --nodates --color=always
36 $ hg diff --nodates --color=always
37 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
37 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
38 \x1b[0;31;1m--- a/a\x1b[0m (esc)
38 \x1b[0;31;1m--- a/a\x1b[0m (esc)
39 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
39 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
40 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
40 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
41 c
41 c
42 a
42 a
43 a
43 a
44 \x1b[0;31m-b\x1b[0m (esc)
44 \x1b[0;31m-b\x1b[0m (esc)
45 \x1b[0;32m+dd\x1b[0m (esc)
45 \x1b[0;32m+dd\x1b[0m (esc)
46 a
46 a
47 a
47 a
48 c
48 c
49
49
50 --unified=2
50 --unified=2
51
51
52 $ hg diff --nodates -U 2 --color=always
52 $ hg diff --nodates -U 2 --color=always
53 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
53 \x1b[0;1mdiff -r cf9f4ba66af2 a\x1b[0m (esc)
54 \x1b[0;31;1m--- a/a\x1b[0m (esc)
54 \x1b[0;31;1m--- a/a\x1b[0m (esc)
55 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
55 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
56 \x1b[0;35m@@ -3,5 +3,5 @@\x1b[0m (esc)
56 \x1b[0;35m@@ -3,5 +3,5 @@\x1b[0m (esc)
57 a
57 a
58 a
58 a
59 \x1b[0;31m-b\x1b[0m (esc)
59 \x1b[0;31m-b\x1b[0m (esc)
60 \x1b[0;32m+dd\x1b[0m (esc)
60 \x1b[0;32m+dd\x1b[0m (esc)
61 a
61 a
62 a
62 a
63
63
64 diffstat
64 diffstat
65
65
66 $ hg diff --stat --color=always
66 $ hg diff --stat --color=always
67 a | 2 \x1b[0;32m+\x1b[0m\x1b[0;31m-\x1b[0m (esc)
67 a | 2 \x1b[0;32m+\x1b[0m\x1b[0;31m-\x1b[0m (esc)
68 1 files changed, 1 insertions(+), 1 deletions(-)
68 1 files changed, 1 insertions(+), 1 deletions(-)
69 $ echo "record=" >> $HGRCPATH
69 $ echo "record=" >> $HGRCPATH
70 $ echo "[ui]" >> $HGRCPATH
70 $ echo "[ui]" >> $HGRCPATH
71 $ echo "interactive=true" >> $HGRCPATH
71 $ echo "interactive=true" >> $HGRCPATH
72 $ echo "[diff]" >> $HGRCPATH
72 $ echo "[diff]" >> $HGRCPATH
73 $ echo "git=True" >> $HGRCPATH
73 $ echo "git=True" >> $HGRCPATH
74
74
75 #if execbit
75 #if execbit
76
76
77 record
77 record
78
78
79 $ chmod +x a
79 $ chmod +x a
80 $ hg record --color=always -m moda a <<EOF
80 $ hg record --color=always -m moda a <<EOF
81 > y
81 > y
82 > y
82 > y
83 > EOF
83 > EOF
84 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
84 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
85 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
85 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
86 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
86 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
87 1 hunks, 1 lines changed
87 1 hunks, 1 lines changed
88 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
88 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
89 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
89 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
90 c
90 c
91 a
91 a
92 a
92 a
93 \x1b[0;31m-b\x1b[0m (esc)
93 \x1b[0;31m-b\x1b[0m (esc)
94 \x1b[0;32m+dd\x1b[0m (esc)
94 \x1b[0;32m+dd\x1b[0m (esc)
95 a
95 a
96 a
96 a
97 c
97 c
98 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
98 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
99
99
100 $ echo "[extensions]" >> $HGRCPATH
100 $ echo "[extensions]" >> $HGRCPATH
101 $ echo "mq=" >> $HGRCPATH
101 $ echo "mq=" >> $HGRCPATH
102 $ hg rollback
102 $ hg rollback
103 repository tip rolled back to revision 0 (undo commit)
103 repository tip rolled back to revision 0 (undo commit)
104 working directory now based on revision 0
104 working directory now based on revision 0
105
105
106 qrecord
106 qrecord
107
107
108 $ hg qrecord --color=always -m moda patch <<EOF
108 $ hg qrecord --color=always -m moda patch <<EOF
109 > y
109 > y
110 > y
110 > y
111 > EOF
111 > EOF
112 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
112 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
113 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
113 \x1b[0;36;1mold mode 100644\x1b[0m (esc)
114 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
114 \x1b[0;36;1mnew mode 100755\x1b[0m (esc)
115 1 hunks, 1 lines changed
115 1 hunks, 1 lines changed
116 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
116 \x1b[0;33mexamine changes to 'a'? [Ynesfdaq?]\x1b[0m (esc)
117 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
117 \x1b[0;35m@@ -2,7 +2,7 @@\x1b[0m (esc)
118 c
118 c
119 a
119 a
120 a
120 a
121 \x1b[0;31m-b\x1b[0m (esc)
121 \x1b[0;31m-b\x1b[0m (esc)
122 \x1b[0;32m+dd\x1b[0m (esc)
122 \x1b[0;32m+dd\x1b[0m (esc)
123 a
123 a
124 a
124 a
125 c
125 c
126 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
126 \x1b[0;33mrecord this change to 'a'? [Ynesfdaq?]\x1b[0m (esc)
127
127
128 $ hg qpop -a
129 popping patch
130 patch queue now empty
131
128 #endif
132 #endif
129
133
134 issue3712: test colorization of subrepo diff
135
136 $ hg init sub
137 $ echo b > sub/b
138 $ hg -R sub commit -Am 'create sub'
139 adding b
140 $ echo 'sub = sub' > .hgsub
141 $ hg add .hgsub
142 $ hg commit -m 'add subrepo sub'
143 $ echo aa >> a
144 $ echo bb >> sub/b
145
146 $ hg diff --color=always -S
147 \x1b[0;1mdiff --git a/a b/a\x1b[0m (esc)
148 \x1b[0;31;1m--- a/a\x1b[0m (esc)
149 \x1b[0;32;1m+++ b/a\x1b[0m (esc)
150 \x1b[0;35m@@ -7,3 +7,4 @@\x1b[0m (esc)
151 a
152 c
153 c
154 \x1b[0;32m+aa\x1b[0m (esc)
155 \x1b[0;1mdiff --git a/sub/b b/sub/b\x1b[0m (esc)
156 \x1b[0;31;1m--- a/sub/b\x1b[0m (esc)
157 \x1b[0;32;1m+++ b/sub/b\x1b[0m (esc)
158 \x1b[0;35m@@ -1,1 +1,2 @@\x1b[0m (esc)
159 b
160 \x1b[0;32m+bb\x1b[0m (esc)
161
130 $ cd ..
162 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now