##// END OF EJS Templates
subrepo: adjust subrepo prefix before calling subrepo.diff() (API)...
Martin von Zweigbergk -
r41779:3d094bfa default
parent child Browse files
Show More
@@ -1,919 +1,920 b''
1 # logcmdutil.py - utility for log-like commands
1 # logcmdutil.py - utility for log-like commands
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import itertools
10 import itertools
11 import os
11 import os
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 nullid,
15 nullid,
16 wdirid,
16 wdirid,
17 wdirrev,
17 wdirrev,
18 )
18 )
19
19
20 from . import (
20 from . import (
21 dagop,
21 dagop,
22 error,
22 error,
23 formatter,
23 formatter,
24 graphmod,
24 graphmod,
25 match as matchmod,
25 match as matchmod,
26 mdiff,
26 mdiff,
27 patch,
27 patch,
28 pathutil,
28 pathutil,
29 pycompat,
29 pycompat,
30 revset,
30 revset,
31 revsetlang,
31 revsetlang,
32 scmutil,
32 scmutil,
33 smartset,
33 smartset,
34 templatekw,
34 templatekw,
35 templater,
35 templater,
36 util,
36 util,
37 )
37 )
38 from .utils import (
38 from .utils import (
39 dateutil,
39 dateutil,
40 stringutil,
40 stringutil,
41 )
41 )
42
42
43 def getlimit(opts):
43 def getlimit(opts):
44 """get the log limit according to option -l/--limit"""
44 """get the log limit according to option -l/--limit"""
45 limit = opts.get('limit')
45 limit = opts.get('limit')
46 if limit:
46 if limit:
47 try:
47 try:
48 limit = int(limit)
48 limit = int(limit)
49 except ValueError:
49 except ValueError:
50 raise error.Abort(_('limit must be a positive integer'))
50 raise error.Abort(_('limit must be a positive integer'))
51 if limit <= 0:
51 if limit <= 0:
52 raise error.Abort(_('limit must be positive'))
52 raise error.Abort(_('limit must be positive'))
53 else:
53 else:
54 limit = None
54 limit = None
55 return limit
55 return limit
56
56
57 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
57 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
58 changes=None, stat=False, fp=None, graphwidth=0,
58 changes=None, stat=False, fp=None, graphwidth=0,
59 prefix='', root='', listsubrepos=False, hunksfilterfn=None):
59 prefix='', root='', listsubrepos=False, hunksfilterfn=None):
60 '''show diff or diffstat.'''
60 '''show diff or diffstat.'''
61 ctx1 = repo[node1]
61 ctx1 = repo[node1]
62 ctx2 = repo[node2]
62 ctx2 = repo[node2]
63 if root:
63 if root:
64 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
64 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
65 else:
65 else:
66 relroot = ''
66 relroot = ''
67 copysourcematch = None
67 copysourcematch = None
68 if relroot != '':
68 if relroot != '':
69 # XXX relative roots currently don't work if the root is within a
69 # XXX relative roots currently don't work if the root is within a
70 # subrepo
70 # subrepo
71 uirelroot = match.uipath(relroot)
71 uirelroot = match.uipath(relroot)
72 relroot += '/'
72 relroot += '/'
73 for matchroot in match.files():
73 for matchroot in match.files():
74 if not matchroot.startswith(relroot):
74 if not matchroot.startswith(relroot):
75 ui.warn(_('warning: %s not inside relative root %s\n') % (
75 ui.warn(_('warning: %s not inside relative root %s\n') % (
76 match.uipath(matchroot), uirelroot))
76 match.uipath(matchroot), uirelroot))
77
77
78 relrootmatch = scmutil.match(ctx2, pats=[relroot], default='path')
78 relrootmatch = scmutil.match(ctx2, pats=[relroot], default='path')
79 match = matchmod.intersectmatchers(match, relrootmatch)
79 match = matchmod.intersectmatchers(match, relrootmatch)
80 copysourcematch = relrootmatch
80 copysourcematch = relrootmatch
81
81
82 if stat:
82 if stat:
83 diffopts = diffopts.copy(context=0, noprefix=False)
83 diffopts = diffopts.copy(context=0, noprefix=False)
84 width = 80
84 width = 80
85 if not ui.plain():
85 if not ui.plain():
86 width = ui.termwidth() - graphwidth
86 width = ui.termwidth() - graphwidth
87
87
88 chunks = ctx2.diff(ctx1, match, changes, opts=diffopts, prefix=prefix,
88 chunks = ctx2.diff(ctx1, match, changes, opts=diffopts, prefix=prefix,
89 relroot=relroot, copysourcematch=copysourcematch,
89 relroot=relroot, copysourcematch=copysourcematch,
90 hunksfilterfn=hunksfilterfn)
90 hunksfilterfn=hunksfilterfn)
91
91
92 if fp is not None or ui.canwritewithoutlabels():
92 if fp is not None or ui.canwritewithoutlabels():
93 out = fp or ui
93 out = fp or ui
94 if stat:
94 if stat:
95 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
95 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
96 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
96 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
97 out.write(chunk)
97 out.write(chunk)
98 else:
98 else:
99 if stat:
99 if stat:
100 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
100 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
101 else:
101 else:
102 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
102 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
103 opts=diffopts)
103 opts=diffopts)
104 if ui.canbatchlabeledwrites():
104 if ui.canbatchlabeledwrites():
105 def gen():
105 def gen():
106 for chunk, label in chunks:
106 for chunk, label in chunks:
107 yield ui.label(chunk, label=label)
107 yield ui.label(chunk, label=label)
108 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
108 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
109 ui.write(chunk)
109 ui.write(chunk)
110 else:
110 else:
111 for chunk, label in chunks:
111 for chunk, label in chunks:
112 ui.write(chunk, label=label)
112 ui.write(chunk, label=label)
113
113
114 if listsubrepos:
114 if listsubrepos:
115 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
115 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
116 tempnode2 = node2
116 tempnode2 = node2
117 try:
117 try:
118 if node2 is not None:
118 if node2 is not None:
119 tempnode2 = ctx2.substate[subpath][1]
119 tempnode2 = ctx2.substate[subpath][1]
120 except KeyError:
120 except KeyError:
121 # A subrepo that existed in node1 was deleted between node1 and
121 # A subrepo that existed in node1 was deleted between node1 and
122 # node2 (inclusive). Thus, ctx2's substate won't contain that
122 # node2 (inclusive). Thus, ctx2's substate won't contain that
123 # subpath. The best we can do is to ignore it.
123 # subpath. The best we can do is to ignore it.
124 tempnode2 = None
124 tempnode2 = None
125 submatch = matchmod.subdirmatcher(subpath, match)
125 submatch = matchmod.subdirmatcher(subpath, match)
126 subprefix = repo.wvfs.reljoin(prefix, subpath)
126 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
127 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
127 stat=stat, fp=fp, prefix=prefix)
128 stat=stat, fp=fp, prefix=subprefix)
128
129
129 class changesetdiffer(object):
130 class changesetdiffer(object):
130 """Generate diff of changeset with pre-configured filtering functions"""
131 """Generate diff of changeset with pre-configured filtering functions"""
131
132
132 def _makefilematcher(self, ctx):
133 def _makefilematcher(self, ctx):
133 return scmutil.matchall(ctx.repo())
134 return scmutil.matchall(ctx.repo())
134
135
135 def _makehunksfilter(self, ctx):
136 def _makehunksfilter(self, ctx):
136 return None
137 return None
137
138
138 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
139 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
139 repo = ctx.repo()
140 repo = ctx.repo()
140 node = ctx.node()
141 node = ctx.node()
141 prev = ctx.p1().node()
142 prev = ctx.p1().node()
142 diffordiffstat(ui, repo, diffopts, prev, node,
143 diffordiffstat(ui, repo, diffopts, prev, node,
143 match=self._makefilematcher(ctx), stat=stat,
144 match=self._makefilematcher(ctx), stat=stat,
144 graphwidth=graphwidth,
145 graphwidth=graphwidth,
145 hunksfilterfn=self._makehunksfilter(ctx))
146 hunksfilterfn=self._makehunksfilter(ctx))
146
147
147 def changesetlabels(ctx):
148 def changesetlabels(ctx):
148 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
149 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
149 if ctx.obsolete():
150 if ctx.obsolete():
150 labels.append('changeset.obsolete')
151 labels.append('changeset.obsolete')
151 if ctx.isunstable():
152 if ctx.isunstable():
152 labels.append('changeset.unstable')
153 labels.append('changeset.unstable')
153 for instability in ctx.instabilities():
154 for instability in ctx.instabilities():
154 labels.append('instability.%s' % instability)
155 labels.append('instability.%s' % instability)
155 return ' '.join(labels)
156 return ' '.join(labels)
156
157
157 class changesetprinter(object):
158 class changesetprinter(object):
158 '''show changeset information when templating not requested.'''
159 '''show changeset information when templating not requested.'''
159
160
160 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
161 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
161 self.ui = ui
162 self.ui = ui
162 self.repo = repo
163 self.repo = repo
163 self.buffered = buffered
164 self.buffered = buffered
164 self._differ = differ or changesetdiffer()
165 self._differ = differ or changesetdiffer()
165 self._diffopts = patch.diffallopts(ui, diffopts)
166 self._diffopts = patch.diffallopts(ui, diffopts)
166 self._includestat = diffopts and diffopts.get('stat')
167 self._includestat = diffopts and diffopts.get('stat')
167 self._includediff = diffopts and diffopts.get('patch')
168 self._includediff = diffopts and diffopts.get('patch')
168 self.header = {}
169 self.header = {}
169 self.hunk = {}
170 self.hunk = {}
170 self.lastheader = None
171 self.lastheader = None
171 self.footer = None
172 self.footer = None
172 self._columns = templatekw.getlogcolumns()
173 self._columns = templatekw.getlogcolumns()
173
174
174 def flush(self, ctx):
175 def flush(self, ctx):
175 rev = ctx.rev()
176 rev = ctx.rev()
176 if rev in self.header:
177 if rev in self.header:
177 h = self.header[rev]
178 h = self.header[rev]
178 if h != self.lastheader:
179 if h != self.lastheader:
179 self.lastheader = h
180 self.lastheader = h
180 self.ui.write(h)
181 self.ui.write(h)
181 del self.header[rev]
182 del self.header[rev]
182 if rev in self.hunk:
183 if rev in self.hunk:
183 self.ui.write(self.hunk[rev])
184 self.ui.write(self.hunk[rev])
184 del self.hunk[rev]
185 del self.hunk[rev]
185
186
186 def close(self):
187 def close(self):
187 if self.footer:
188 if self.footer:
188 self.ui.write(self.footer)
189 self.ui.write(self.footer)
189
190
190 def show(self, ctx, copies=None, **props):
191 def show(self, ctx, copies=None, **props):
191 props = pycompat.byteskwargs(props)
192 props = pycompat.byteskwargs(props)
192 if self.buffered:
193 if self.buffered:
193 self.ui.pushbuffer(labeled=True)
194 self.ui.pushbuffer(labeled=True)
194 self._show(ctx, copies, props)
195 self._show(ctx, copies, props)
195 self.hunk[ctx.rev()] = self.ui.popbuffer()
196 self.hunk[ctx.rev()] = self.ui.popbuffer()
196 else:
197 else:
197 self._show(ctx, copies, props)
198 self._show(ctx, copies, props)
198
199
199 def _show(self, ctx, copies, props):
200 def _show(self, ctx, copies, props):
200 '''show a single changeset or file revision'''
201 '''show a single changeset or file revision'''
201 changenode = ctx.node()
202 changenode = ctx.node()
202 graphwidth = props.get('graphwidth', 0)
203 graphwidth = props.get('graphwidth', 0)
203
204
204 if self.ui.quiet:
205 if self.ui.quiet:
205 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
206 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
206 label='log.node')
207 label='log.node')
207 return
208 return
208
209
209 columns = self._columns
210 columns = self._columns
210 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
211 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
211 label=changesetlabels(ctx))
212 label=changesetlabels(ctx))
212
213
213 # branches are shown first before any other names due to backwards
214 # branches are shown first before any other names due to backwards
214 # compatibility
215 # compatibility
215 branch = ctx.branch()
216 branch = ctx.branch()
216 # don't show the default branch name
217 # don't show the default branch name
217 if branch != 'default':
218 if branch != 'default':
218 self.ui.write(columns['branch'] % branch, label='log.branch')
219 self.ui.write(columns['branch'] % branch, label='log.branch')
219
220
220 for nsname, ns in self.repo.names.iteritems():
221 for nsname, ns in self.repo.names.iteritems():
221 # branches has special logic already handled above, so here we just
222 # branches has special logic already handled above, so here we just
222 # skip it
223 # skip it
223 if nsname == 'branches':
224 if nsname == 'branches':
224 continue
225 continue
225 # we will use the templatename as the color name since those two
226 # we will use the templatename as the color name since those two
226 # should be the same
227 # should be the same
227 for name in ns.names(self.repo, changenode):
228 for name in ns.names(self.repo, changenode):
228 self.ui.write(ns.logfmt % name,
229 self.ui.write(ns.logfmt % name,
229 label='log.%s' % ns.colorname)
230 label='log.%s' % ns.colorname)
230 if self.ui.debugflag:
231 if self.ui.debugflag:
231 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
232 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
232 for pctx in scmutil.meaningfulparents(self.repo, ctx):
233 for pctx in scmutil.meaningfulparents(self.repo, ctx):
233 label = 'log.parent changeset.%s' % pctx.phasestr()
234 label = 'log.parent changeset.%s' % pctx.phasestr()
234 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
235 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
235 label=label)
236 label=label)
236
237
237 if self.ui.debugflag:
238 if self.ui.debugflag:
238 mnode = ctx.manifestnode()
239 mnode = ctx.manifestnode()
239 if mnode is None:
240 if mnode is None:
240 mnode = wdirid
241 mnode = wdirid
241 mrev = wdirrev
242 mrev = wdirrev
242 else:
243 else:
243 mrev = self.repo.manifestlog.rev(mnode)
244 mrev = self.repo.manifestlog.rev(mnode)
244 self.ui.write(columns['manifest']
245 self.ui.write(columns['manifest']
245 % scmutil.formatrevnode(self.ui, mrev, mnode),
246 % scmutil.formatrevnode(self.ui, mrev, mnode),
246 label='ui.debug log.manifest')
247 label='ui.debug log.manifest')
247 self.ui.write(columns['user'] % ctx.user(), label='log.user')
248 self.ui.write(columns['user'] % ctx.user(), label='log.user')
248 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
249 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
249 label='log.date')
250 label='log.date')
250
251
251 if ctx.isunstable():
252 if ctx.isunstable():
252 instabilities = ctx.instabilities()
253 instabilities = ctx.instabilities()
253 self.ui.write(columns['instability'] % ', '.join(instabilities),
254 self.ui.write(columns['instability'] % ', '.join(instabilities),
254 label='log.instability')
255 label='log.instability')
255
256
256 elif ctx.obsolete():
257 elif ctx.obsolete():
257 self._showobsfate(ctx)
258 self._showobsfate(ctx)
258
259
259 self._exthook(ctx)
260 self._exthook(ctx)
260
261
261 if self.ui.debugflag:
262 if self.ui.debugflag:
262 files = ctx.p1().status(ctx)[:3]
263 files = ctx.p1().status(ctx)[:3]
263 for key, value in zip(['files', 'files+', 'files-'], files):
264 for key, value in zip(['files', 'files+', 'files-'], files):
264 if value:
265 if value:
265 self.ui.write(columns[key] % " ".join(value),
266 self.ui.write(columns[key] % " ".join(value),
266 label='ui.debug log.files')
267 label='ui.debug log.files')
267 elif ctx.files() and self.ui.verbose:
268 elif ctx.files() and self.ui.verbose:
268 self.ui.write(columns['files'] % " ".join(ctx.files()),
269 self.ui.write(columns['files'] % " ".join(ctx.files()),
269 label='ui.note log.files')
270 label='ui.note log.files')
270 if copies and self.ui.verbose:
271 if copies and self.ui.verbose:
271 copies = ['%s (%s)' % c for c in copies]
272 copies = ['%s (%s)' % c for c in copies]
272 self.ui.write(columns['copies'] % ' '.join(copies),
273 self.ui.write(columns['copies'] % ' '.join(copies),
273 label='ui.note log.copies')
274 label='ui.note log.copies')
274
275
275 extra = ctx.extra()
276 extra = ctx.extra()
276 if extra and self.ui.debugflag:
277 if extra and self.ui.debugflag:
277 for key, value in sorted(extra.items()):
278 for key, value in sorted(extra.items()):
278 self.ui.write(columns['extra']
279 self.ui.write(columns['extra']
279 % (key, stringutil.escapestr(value)),
280 % (key, stringutil.escapestr(value)),
280 label='ui.debug log.extra')
281 label='ui.debug log.extra')
281
282
282 description = ctx.description().strip()
283 description = ctx.description().strip()
283 if description:
284 if description:
284 if self.ui.verbose:
285 if self.ui.verbose:
285 self.ui.write(_("description:\n"),
286 self.ui.write(_("description:\n"),
286 label='ui.note log.description')
287 label='ui.note log.description')
287 self.ui.write(description,
288 self.ui.write(description,
288 label='ui.note log.description')
289 label='ui.note log.description')
289 self.ui.write("\n\n")
290 self.ui.write("\n\n")
290 else:
291 else:
291 self.ui.write(columns['summary'] % description.splitlines()[0],
292 self.ui.write(columns['summary'] % description.splitlines()[0],
292 label='log.summary')
293 label='log.summary')
293 self.ui.write("\n")
294 self.ui.write("\n")
294
295
295 self._showpatch(ctx, graphwidth)
296 self._showpatch(ctx, graphwidth)
296
297
297 def _showobsfate(self, ctx):
298 def _showobsfate(self, ctx):
298 # TODO: do not depend on templater
299 # TODO: do not depend on templater
299 tres = formatter.templateresources(self.repo.ui, self.repo)
300 tres = formatter.templateresources(self.repo.ui, self.repo)
300 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
301 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
301 defaults=templatekw.keywords,
302 defaults=templatekw.keywords,
302 resources=tres)
303 resources=tres)
303 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
304 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
304
305
305 if obsfate:
306 if obsfate:
306 for obsfateline in obsfate:
307 for obsfateline in obsfate:
307 self.ui.write(self._columns['obsolete'] % obsfateline,
308 self.ui.write(self._columns['obsolete'] % obsfateline,
308 label='log.obsfate')
309 label='log.obsfate')
309
310
310 def _exthook(self, ctx):
311 def _exthook(self, ctx):
311 '''empty method used by extension as a hook point
312 '''empty method used by extension as a hook point
312 '''
313 '''
313
314
314 def _showpatch(self, ctx, graphwidth=0):
315 def _showpatch(self, ctx, graphwidth=0):
315 if self._includestat:
316 if self._includestat:
316 self._differ.showdiff(self.ui, ctx, self._diffopts,
317 self._differ.showdiff(self.ui, ctx, self._diffopts,
317 graphwidth, stat=True)
318 graphwidth, stat=True)
318 if self._includestat and self._includediff:
319 if self._includestat and self._includediff:
319 self.ui.write("\n")
320 self.ui.write("\n")
320 if self._includediff:
321 if self._includediff:
321 self._differ.showdiff(self.ui, ctx, self._diffopts,
322 self._differ.showdiff(self.ui, ctx, self._diffopts,
322 graphwidth, stat=False)
323 graphwidth, stat=False)
323 if self._includestat or self._includediff:
324 if self._includestat or self._includediff:
324 self.ui.write("\n")
325 self.ui.write("\n")
325
326
326 class changesetformatter(changesetprinter):
327 class changesetformatter(changesetprinter):
327 """Format changeset information by generic formatter"""
328 """Format changeset information by generic formatter"""
328
329
329 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
330 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
330 buffered=False):
331 buffered=False):
331 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
332 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
332 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
333 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
333 self._fm = fm
334 self._fm = fm
334
335
335 def close(self):
336 def close(self):
336 self._fm.end()
337 self._fm.end()
337
338
338 def _show(self, ctx, copies, props):
339 def _show(self, ctx, copies, props):
339 '''show a single changeset or file revision'''
340 '''show a single changeset or file revision'''
340 fm = self._fm
341 fm = self._fm
341 fm.startitem()
342 fm.startitem()
342 fm.context(ctx=ctx)
343 fm.context(ctx=ctx)
343 fm.data(rev=scmutil.intrev(ctx),
344 fm.data(rev=scmutil.intrev(ctx),
344 node=fm.hexfunc(scmutil.binnode(ctx)))
345 node=fm.hexfunc(scmutil.binnode(ctx)))
345
346
346 if self.ui.quiet:
347 if self.ui.quiet:
347 return
348 return
348
349
349 fm.data(branch=ctx.branch(),
350 fm.data(branch=ctx.branch(),
350 phase=ctx.phasestr(),
351 phase=ctx.phasestr(),
351 user=ctx.user(),
352 user=ctx.user(),
352 date=fm.formatdate(ctx.date()),
353 date=fm.formatdate(ctx.date()),
353 desc=ctx.description(),
354 desc=ctx.description(),
354 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
355 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
355 tags=fm.formatlist(ctx.tags(), name='tag'),
356 tags=fm.formatlist(ctx.tags(), name='tag'),
356 parents=fm.formatlist([fm.hexfunc(c.node())
357 parents=fm.formatlist([fm.hexfunc(c.node())
357 for c in ctx.parents()], name='node'))
358 for c in ctx.parents()], name='node'))
358
359
359 if self.ui.debugflag:
360 if self.ui.debugflag:
360 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid),
361 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid),
361 extra=fm.formatdict(ctx.extra()))
362 extra=fm.formatdict(ctx.extra()))
362
363
363 files = ctx.p1().status(ctx)
364 files = ctx.p1().status(ctx)
364 fm.data(modified=fm.formatlist(files[0], name='file'),
365 fm.data(modified=fm.formatlist(files[0], name='file'),
365 added=fm.formatlist(files[1], name='file'),
366 added=fm.formatlist(files[1], name='file'),
366 removed=fm.formatlist(files[2], name='file'))
367 removed=fm.formatlist(files[2], name='file'))
367
368
368 elif self.ui.verbose:
369 elif self.ui.verbose:
369 fm.data(files=fm.formatlist(ctx.files(), name='file'))
370 fm.data(files=fm.formatlist(ctx.files(), name='file'))
370 if copies:
371 if copies:
371 fm.data(copies=fm.formatdict(copies,
372 fm.data(copies=fm.formatdict(copies,
372 key='name', value='source'))
373 key='name', value='source'))
373
374
374 if self._includestat:
375 if self._includestat:
375 self.ui.pushbuffer()
376 self.ui.pushbuffer()
376 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
377 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
377 fm.data(diffstat=self.ui.popbuffer())
378 fm.data(diffstat=self.ui.popbuffer())
378 if self._includediff:
379 if self._includediff:
379 self.ui.pushbuffer()
380 self.ui.pushbuffer()
380 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
381 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
381 fm.data(diff=self.ui.popbuffer())
382 fm.data(diff=self.ui.popbuffer())
382
383
383 class changesettemplater(changesetprinter):
384 class changesettemplater(changesetprinter):
384 '''format changeset information.
385 '''format changeset information.
385
386
386 Note: there are a variety of convenience functions to build a
387 Note: there are a variety of convenience functions to build a
387 changesettemplater for common cases. See functions such as:
388 changesettemplater for common cases. See functions such as:
388 maketemplater, changesetdisplayer, buildcommittemplate, or other
389 maketemplater, changesetdisplayer, buildcommittemplate, or other
389 functions that use changesest_templater.
390 functions that use changesest_templater.
390 '''
391 '''
391
392
392 # Arguments before "buffered" used to be positional. Consider not
393 # Arguments before "buffered" used to be positional. Consider not
393 # adding/removing arguments before "buffered" to not break callers.
394 # adding/removing arguments before "buffered" to not break callers.
394 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
395 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
395 buffered=False):
396 buffered=False):
396 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
397 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
397 # tres is shared with _graphnodeformatter()
398 # tres is shared with _graphnodeformatter()
398 self._tresources = tres = formatter.templateresources(ui, repo)
399 self._tresources = tres = formatter.templateresources(ui, repo)
399 self.t = formatter.loadtemplater(ui, tmplspec,
400 self.t = formatter.loadtemplater(ui, tmplspec,
400 defaults=templatekw.keywords,
401 defaults=templatekw.keywords,
401 resources=tres,
402 resources=tres,
402 cache=templatekw.defaulttempl)
403 cache=templatekw.defaulttempl)
403 self._counter = itertools.count()
404 self._counter = itertools.count()
404
405
405 self._tref = tmplspec.ref
406 self._tref = tmplspec.ref
406 self._parts = {'header': '', 'footer': '',
407 self._parts = {'header': '', 'footer': '',
407 tmplspec.ref: tmplspec.ref,
408 tmplspec.ref: tmplspec.ref,
408 'docheader': '', 'docfooter': '',
409 'docheader': '', 'docfooter': '',
409 'separator': ''}
410 'separator': ''}
410 if tmplspec.mapfile:
411 if tmplspec.mapfile:
411 # find correct templates for current mode, for backward
412 # find correct templates for current mode, for backward
412 # compatibility with 'log -v/-q/--debug' using a mapfile
413 # compatibility with 'log -v/-q/--debug' using a mapfile
413 tmplmodes = [
414 tmplmodes = [
414 (True, ''),
415 (True, ''),
415 (self.ui.verbose, '_verbose'),
416 (self.ui.verbose, '_verbose'),
416 (self.ui.quiet, '_quiet'),
417 (self.ui.quiet, '_quiet'),
417 (self.ui.debugflag, '_debug'),
418 (self.ui.debugflag, '_debug'),
418 ]
419 ]
419 for mode, postfix in tmplmodes:
420 for mode, postfix in tmplmodes:
420 for t in self._parts:
421 for t in self._parts:
421 cur = t + postfix
422 cur = t + postfix
422 if mode and cur in self.t:
423 if mode and cur in self.t:
423 self._parts[t] = cur
424 self._parts[t] = cur
424 else:
425 else:
425 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
426 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
426 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
427 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
427 self._parts.update(m)
428 self._parts.update(m)
428
429
429 if self._parts['docheader']:
430 if self._parts['docheader']:
430 self.ui.write(self.t.render(self._parts['docheader'], {}))
431 self.ui.write(self.t.render(self._parts['docheader'], {}))
431
432
432 def close(self):
433 def close(self):
433 if self._parts['docfooter']:
434 if self._parts['docfooter']:
434 if not self.footer:
435 if not self.footer:
435 self.footer = ""
436 self.footer = ""
436 self.footer += self.t.render(self._parts['docfooter'], {})
437 self.footer += self.t.render(self._parts['docfooter'], {})
437 return super(changesettemplater, self).close()
438 return super(changesettemplater, self).close()
438
439
439 def _show(self, ctx, copies, props):
440 def _show(self, ctx, copies, props):
440 '''show a single changeset or file revision'''
441 '''show a single changeset or file revision'''
441 props = props.copy()
442 props = props.copy()
442 props['ctx'] = ctx
443 props['ctx'] = ctx
443 props['index'] = index = next(self._counter)
444 props['index'] = index = next(self._counter)
444 props['revcache'] = {'copies': copies}
445 props['revcache'] = {'copies': copies}
445 graphwidth = props.get('graphwidth', 0)
446 graphwidth = props.get('graphwidth', 0)
446
447
447 # write separator, which wouldn't work well with the header part below
448 # write separator, which wouldn't work well with the header part below
448 # since there's inherently a conflict between header (across items) and
449 # since there's inherently a conflict between header (across items) and
449 # separator (per item)
450 # separator (per item)
450 if self._parts['separator'] and index > 0:
451 if self._parts['separator'] and index > 0:
451 self.ui.write(self.t.render(self._parts['separator'], {}))
452 self.ui.write(self.t.render(self._parts['separator'], {}))
452
453
453 # write header
454 # write header
454 if self._parts['header']:
455 if self._parts['header']:
455 h = self.t.render(self._parts['header'], props)
456 h = self.t.render(self._parts['header'], props)
456 if self.buffered:
457 if self.buffered:
457 self.header[ctx.rev()] = h
458 self.header[ctx.rev()] = h
458 else:
459 else:
459 if self.lastheader != h:
460 if self.lastheader != h:
460 self.lastheader = h
461 self.lastheader = h
461 self.ui.write(h)
462 self.ui.write(h)
462
463
463 # write changeset metadata, then patch if requested
464 # write changeset metadata, then patch if requested
464 key = self._parts[self._tref]
465 key = self._parts[self._tref]
465 self.ui.write(self.t.render(key, props))
466 self.ui.write(self.t.render(key, props))
466 self._showpatch(ctx, graphwidth)
467 self._showpatch(ctx, graphwidth)
467
468
468 if self._parts['footer']:
469 if self._parts['footer']:
469 if not self.footer:
470 if not self.footer:
470 self.footer = self.t.render(self._parts['footer'], props)
471 self.footer = self.t.render(self._parts['footer'], props)
471
472
472 def templatespec(tmpl, mapfile):
473 def templatespec(tmpl, mapfile):
473 if pycompat.ispy3:
474 if pycompat.ispy3:
474 assert not isinstance(tmpl, str), 'tmpl must not be a str'
475 assert not isinstance(tmpl, str), 'tmpl must not be a str'
475 if mapfile:
476 if mapfile:
476 return formatter.templatespec('changeset', tmpl, mapfile)
477 return formatter.templatespec('changeset', tmpl, mapfile)
477 else:
478 else:
478 return formatter.templatespec('', tmpl, None)
479 return formatter.templatespec('', tmpl, None)
479
480
480 def _lookuptemplate(ui, tmpl, style):
481 def _lookuptemplate(ui, tmpl, style):
481 """Find the template matching the given template spec or style
482 """Find the template matching the given template spec or style
482
483
483 See formatter.lookuptemplate() for details.
484 See formatter.lookuptemplate() for details.
484 """
485 """
485
486
486 # ui settings
487 # ui settings
487 if not tmpl and not style: # template are stronger than style
488 if not tmpl and not style: # template are stronger than style
488 tmpl = ui.config('ui', 'logtemplate')
489 tmpl = ui.config('ui', 'logtemplate')
489 if tmpl:
490 if tmpl:
490 return templatespec(templater.unquotestring(tmpl), None)
491 return templatespec(templater.unquotestring(tmpl), None)
491 else:
492 else:
492 style = util.expandpath(ui.config('ui', 'style'))
493 style = util.expandpath(ui.config('ui', 'style'))
493
494
494 if not tmpl and style:
495 if not tmpl and style:
495 mapfile = style
496 mapfile = style
496 if not os.path.split(mapfile)[0]:
497 if not os.path.split(mapfile)[0]:
497 mapname = (templater.templatepath('map-cmdline.' + mapfile)
498 mapname = (templater.templatepath('map-cmdline.' + mapfile)
498 or templater.templatepath(mapfile))
499 or templater.templatepath(mapfile))
499 if mapname:
500 if mapname:
500 mapfile = mapname
501 mapfile = mapname
501 return templatespec(None, mapfile)
502 return templatespec(None, mapfile)
502
503
503 if not tmpl:
504 if not tmpl:
504 return templatespec(None, None)
505 return templatespec(None, None)
505
506
506 return formatter.lookuptemplate(ui, 'changeset', tmpl)
507 return formatter.lookuptemplate(ui, 'changeset', tmpl)
507
508
508 def maketemplater(ui, repo, tmpl, buffered=False):
509 def maketemplater(ui, repo, tmpl, buffered=False):
509 """Create a changesettemplater from a literal template 'tmpl'
510 """Create a changesettemplater from a literal template 'tmpl'
510 byte-string."""
511 byte-string."""
511 spec = templatespec(tmpl, None)
512 spec = templatespec(tmpl, None)
512 return changesettemplater(ui, repo, spec, buffered=buffered)
513 return changesettemplater(ui, repo, spec, buffered=buffered)
513
514
514 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
515 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
515 """show one changeset using template or regular display.
516 """show one changeset using template or regular display.
516
517
517 Display format will be the first non-empty hit of:
518 Display format will be the first non-empty hit of:
518 1. option 'template'
519 1. option 'template'
519 2. option 'style'
520 2. option 'style'
520 3. [ui] setting 'logtemplate'
521 3. [ui] setting 'logtemplate'
521 4. [ui] setting 'style'
522 4. [ui] setting 'style'
522 If all of these values are either the unset or the empty string,
523 If all of these values are either the unset or the empty string,
523 regular display via changesetprinter() is done.
524 regular display via changesetprinter() is done.
524 """
525 """
525 postargs = (differ, opts, buffered)
526 postargs = (differ, opts, buffered)
526 if opts.get('template') == 'json':
527 if opts.get('template') == 'json':
527 fm = ui.formatter('log', opts)
528 fm = ui.formatter('log', opts)
528 return changesetformatter(ui, repo, fm, *postargs)
529 return changesetformatter(ui, repo, fm, *postargs)
529
530
530 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
531 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
531
532
532 if not spec.ref and not spec.tmpl and not spec.mapfile:
533 if not spec.ref and not spec.tmpl and not spec.mapfile:
533 return changesetprinter(ui, repo, *postargs)
534 return changesetprinter(ui, repo, *postargs)
534
535
535 return changesettemplater(ui, repo, spec, *postargs)
536 return changesettemplater(ui, repo, spec, *postargs)
536
537
537 def _makematcher(repo, revs, pats, opts):
538 def _makematcher(repo, revs, pats, opts):
538 """Build matcher and expanded patterns from log options
539 """Build matcher and expanded patterns from log options
539
540
540 If --follow, revs are the revisions to follow from.
541 If --follow, revs are the revisions to follow from.
541
542
542 Returns (match, pats, slowpath) where
543 Returns (match, pats, slowpath) where
543 - match: a matcher built from the given pats and -I/-X opts
544 - match: a matcher built from the given pats and -I/-X opts
544 - pats: patterns used (globs are expanded on Windows)
545 - pats: patterns used (globs are expanded on Windows)
545 - slowpath: True if patterns aren't as simple as scanning filelogs
546 - slowpath: True if patterns aren't as simple as scanning filelogs
546 """
547 """
547 # pats/include/exclude are passed to match.match() directly in
548 # pats/include/exclude are passed to match.match() directly in
548 # _matchfiles() revset but walkchangerevs() builds its matcher with
549 # _matchfiles() revset but walkchangerevs() builds its matcher with
549 # scmutil.match(). The difference is input pats are globbed on
550 # scmutil.match(). The difference is input pats are globbed on
550 # platforms without shell expansion (windows).
551 # platforms without shell expansion (windows).
551 wctx = repo[None]
552 wctx = repo[None]
552 match, pats = scmutil.matchandpats(wctx, pats, opts)
553 match, pats = scmutil.matchandpats(wctx, pats, opts)
553 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
554 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
554 if not slowpath:
555 if not slowpath:
555 follow = opts.get('follow') or opts.get('follow_first')
556 follow = opts.get('follow') or opts.get('follow_first')
556 startctxs = []
557 startctxs = []
557 if follow and opts.get('rev'):
558 if follow and opts.get('rev'):
558 startctxs = [repo[r] for r in revs]
559 startctxs = [repo[r] for r in revs]
559 for f in match.files():
560 for f in match.files():
560 if follow and startctxs:
561 if follow and startctxs:
561 # No idea if the path was a directory at that revision, so
562 # No idea if the path was a directory at that revision, so
562 # take the slow path.
563 # take the slow path.
563 if any(f not in c for c in startctxs):
564 if any(f not in c for c in startctxs):
564 slowpath = True
565 slowpath = True
565 continue
566 continue
566 elif follow and f not in wctx:
567 elif follow and f not in wctx:
567 # If the file exists, it may be a directory, so let it
568 # If the file exists, it may be a directory, so let it
568 # take the slow path.
569 # take the slow path.
569 if os.path.exists(repo.wjoin(f)):
570 if os.path.exists(repo.wjoin(f)):
570 slowpath = True
571 slowpath = True
571 continue
572 continue
572 else:
573 else:
573 raise error.Abort(_('cannot follow file not in parent '
574 raise error.Abort(_('cannot follow file not in parent '
574 'revision: "%s"') % f)
575 'revision: "%s"') % f)
575 filelog = repo.file(f)
576 filelog = repo.file(f)
576 if not filelog:
577 if not filelog:
577 # A zero count may be a directory or deleted file, so
578 # A zero count may be a directory or deleted file, so
578 # try to find matching entries on the slow path.
579 # try to find matching entries on the slow path.
579 if follow:
580 if follow:
580 raise error.Abort(
581 raise error.Abort(
581 _('cannot follow nonexistent file: "%s"') % f)
582 _('cannot follow nonexistent file: "%s"') % f)
582 slowpath = True
583 slowpath = True
583
584
584 # We decided to fall back to the slowpath because at least one
585 # We decided to fall back to the slowpath because at least one
585 # of the paths was not a file. Check to see if at least one of them
586 # of the paths was not a file. Check to see if at least one of them
586 # existed in history - in that case, we'll continue down the
587 # existed in history - in that case, we'll continue down the
587 # slowpath; otherwise, we can turn off the slowpath
588 # slowpath; otherwise, we can turn off the slowpath
588 if slowpath:
589 if slowpath:
589 for path in match.files():
590 for path in match.files():
590 if path == '.' or path in repo.store:
591 if path == '.' or path in repo.store:
591 break
592 break
592 else:
593 else:
593 slowpath = False
594 slowpath = False
594
595
595 return match, pats, slowpath
596 return match, pats, slowpath
596
597
597 def _fileancestors(repo, revs, match, followfirst):
598 def _fileancestors(repo, revs, match, followfirst):
598 fctxs = []
599 fctxs = []
599 for r in revs:
600 for r in revs:
600 ctx = repo[r]
601 ctx = repo[r]
601 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
602 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
602
603
603 # When displaying a revision with --patch --follow FILE, we have
604 # When displaying a revision with --patch --follow FILE, we have
604 # to know which file of the revision must be diffed. With
605 # to know which file of the revision must be diffed. With
605 # --follow, we want the names of the ancestors of FILE in the
606 # --follow, we want the names of the ancestors of FILE in the
606 # revision, stored in "fcache". "fcache" is populated as a side effect
607 # revision, stored in "fcache". "fcache" is populated as a side effect
607 # of the graph traversal.
608 # of the graph traversal.
608 fcache = {}
609 fcache = {}
609 def filematcher(ctx):
610 def filematcher(ctx):
610 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
611 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
611
612
612 def revgen():
613 def revgen():
613 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
614 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
614 fcache[rev] = [c.path() for c in cs]
615 fcache[rev] = [c.path() for c in cs]
615 yield rev
616 yield rev
616 return smartset.generatorset(revgen(), iterasc=False), filematcher
617 return smartset.generatorset(revgen(), iterasc=False), filematcher
617
618
618 def _makenofollowfilematcher(repo, pats, opts):
619 def _makenofollowfilematcher(repo, pats, opts):
619 '''hook for extensions to override the filematcher for non-follow cases'''
620 '''hook for extensions to override the filematcher for non-follow cases'''
620 return None
621 return None
621
622
622 _opt2logrevset = {
623 _opt2logrevset = {
623 'no_merges': ('not merge()', None),
624 'no_merges': ('not merge()', None),
624 'only_merges': ('merge()', None),
625 'only_merges': ('merge()', None),
625 '_matchfiles': (None, '_matchfiles(%ps)'),
626 '_matchfiles': (None, '_matchfiles(%ps)'),
626 'date': ('date(%s)', None),
627 'date': ('date(%s)', None),
627 'branch': ('branch(%s)', '%lr'),
628 'branch': ('branch(%s)', '%lr'),
628 '_patslog': ('filelog(%s)', '%lr'),
629 '_patslog': ('filelog(%s)', '%lr'),
629 'keyword': ('keyword(%s)', '%lr'),
630 'keyword': ('keyword(%s)', '%lr'),
630 'prune': ('ancestors(%s)', 'not %lr'),
631 'prune': ('ancestors(%s)', 'not %lr'),
631 'user': ('user(%s)', '%lr'),
632 'user': ('user(%s)', '%lr'),
632 }
633 }
633
634
634 def _makerevset(repo, match, pats, slowpath, opts):
635 def _makerevset(repo, match, pats, slowpath, opts):
635 """Return a revset string built from log options and file patterns"""
636 """Return a revset string built from log options and file patterns"""
636 opts = dict(opts)
637 opts = dict(opts)
637 # follow or not follow?
638 # follow or not follow?
638 follow = opts.get('follow') or opts.get('follow_first')
639 follow = opts.get('follow') or opts.get('follow_first')
639
640
640 # branch and only_branch are really aliases and must be handled at
641 # branch and only_branch are really aliases and must be handled at
641 # the same time
642 # the same time
642 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
643 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
643 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
644 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
644
645
645 if slowpath:
646 if slowpath:
646 # See walkchangerevs() slow path.
647 # See walkchangerevs() slow path.
647 #
648 #
648 # pats/include/exclude cannot be represented as separate
649 # pats/include/exclude cannot be represented as separate
649 # revset expressions as their filtering logic applies at file
650 # revset expressions as their filtering logic applies at file
650 # level. For instance "-I a -X b" matches a revision touching
651 # level. For instance "-I a -X b" matches a revision touching
651 # "a" and "b" while "file(a) and not file(b)" does
652 # "a" and "b" while "file(a) and not file(b)" does
652 # not. Besides, filesets are evaluated against the working
653 # not. Besides, filesets are evaluated against the working
653 # directory.
654 # directory.
654 matchargs = ['r:', 'd:relpath']
655 matchargs = ['r:', 'd:relpath']
655 for p in pats:
656 for p in pats:
656 matchargs.append('p:' + p)
657 matchargs.append('p:' + p)
657 for p in opts.get('include', []):
658 for p in opts.get('include', []):
658 matchargs.append('i:' + p)
659 matchargs.append('i:' + p)
659 for p in opts.get('exclude', []):
660 for p in opts.get('exclude', []):
660 matchargs.append('x:' + p)
661 matchargs.append('x:' + p)
661 opts['_matchfiles'] = matchargs
662 opts['_matchfiles'] = matchargs
662 elif not follow:
663 elif not follow:
663 opts['_patslog'] = list(pats)
664 opts['_patslog'] = list(pats)
664
665
665 expr = []
666 expr = []
666 for op, val in sorted(opts.iteritems()):
667 for op, val in sorted(opts.iteritems()):
667 if not val:
668 if not val:
668 continue
669 continue
669 if op not in _opt2logrevset:
670 if op not in _opt2logrevset:
670 continue
671 continue
671 revop, listop = _opt2logrevset[op]
672 revop, listop = _opt2logrevset[op]
672 if revop and '%' not in revop:
673 if revop and '%' not in revop:
673 expr.append(revop)
674 expr.append(revop)
674 elif not listop:
675 elif not listop:
675 expr.append(revsetlang.formatspec(revop, val))
676 expr.append(revsetlang.formatspec(revop, val))
676 else:
677 else:
677 if revop:
678 if revop:
678 val = [revsetlang.formatspec(revop, v) for v in val]
679 val = [revsetlang.formatspec(revop, v) for v in val]
679 expr.append(revsetlang.formatspec(listop, val))
680 expr.append(revsetlang.formatspec(listop, val))
680
681
681 if expr:
682 if expr:
682 expr = '(' + ' and '.join(expr) + ')'
683 expr = '(' + ' and '.join(expr) + ')'
683 else:
684 else:
684 expr = None
685 expr = None
685 return expr
686 return expr
686
687
687 def _initialrevs(repo, opts):
688 def _initialrevs(repo, opts):
688 """Return the initial set of revisions to be filtered or followed"""
689 """Return the initial set of revisions to be filtered or followed"""
689 follow = opts.get('follow') or opts.get('follow_first')
690 follow = opts.get('follow') or opts.get('follow_first')
690 if opts.get('rev'):
691 if opts.get('rev'):
691 revs = scmutil.revrange(repo, opts['rev'])
692 revs = scmutil.revrange(repo, opts['rev'])
692 elif follow and repo.dirstate.p1() == nullid:
693 elif follow and repo.dirstate.p1() == nullid:
693 revs = smartset.baseset()
694 revs = smartset.baseset()
694 elif follow:
695 elif follow:
695 revs = repo.revs('.')
696 revs = repo.revs('.')
696 else:
697 else:
697 revs = smartset.spanset(repo)
698 revs = smartset.spanset(repo)
698 revs.reverse()
699 revs.reverse()
699 return revs
700 return revs
700
701
701 def getrevs(repo, pats, opts):
702 def getrevs(repo, pats, opts):
702 """Return (revs, differ) where revs is a smartset
703 """Return (revs, differ) where revs is a smartset
703
704
704 differ is a changesetdiffer with pre-configured file matcher.
705 differ is a changesetdiffer with pre-configured file matcher.
705 """
706 """
706 follow = opts.get('follow') or opts.get('follow_first')
707 follow = opts.get('follow') or opts.get('follow_first')
707 followfirst = opts.get('follow_first')
708 followfirst = opts.get('follow_first')
708 limit = getlimit(opts)
709 limit = getlimit(opts)
709 revs = _initialrevs(repo, opts)
710 revs = _initialrevs(repo, opts)
710 if not revs:
711 if not revs:
711 return smartset.baseset(), None
712 return smartset.baseset(), None
712 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
713 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
713 filematcher = None
714 filematcher = None
714 if follow:
715 if follow:
715 if slowpath or match.always():
716 if slowpath or match.always():
716 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
717 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
717 else:
718 else:
718 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
719 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
719 revs.reverse()
720 revs.reverse()
720 if filematcher is None:
721 if filematcher is None:
721 filematcher = _makenofollowfilematcher(repo, pats, opts)
722 filematcher = _makenofollowfilematcher(repo, pats, opts)
722 if filematcher is None:
723 if filematcher is None:
723 def filematcher(ctx):
724 def filematcher(ctx):
724 return match
725 return match
725
726
726 expr = _makerevset(repo, match, pats, slowpath, opts)
727 expr = _makerevset(repo, match, pats, slowpath, opts)
727 if opts.get('graph') and opts.get('rev'):
728 if opts.get('graph') and opts.get('rev'):
728 # User-specified revs might be unsorted, but don't sort before
729 # User-specified revs might be unsorted, but don't sort before
729 # _makerevset because it might depend on the order of revs
730 # _makerevset because it might depend on the order of revs
730 if not (revs.isdescending() or revs.istopo()):
731 if not (revs.isdescending() or revs.istopo()):
731 revs.sort(reverse=True)
732 revs.sort(reverse=True)
732 if expr:
733 if expr:
733 matcher = revset.match(None, expr)
734 matcher = revset.match(None, expr)
734 revs = matcher(repo, revs)
735 revs = matcher(repo, revs)
735 if limit is not None:
736 if limit is not None:
736 revs = revs.slice(0, limit)
737 revs = revs.slice(0, limit)
737
738
738 differ = changesetdiffer()
739 differ = changesetdiffer()
739 differ._makefilematcher = filematcher
740 differ._makefilematcher = filematcher
740 return revs, differ
741 return revs, differ
741
742
742 def _parselinerangeopt(repo, opts):
743 def _parselinerangeopt(repo, opts):
743 """Parse --line-range log option and return a list of tuples (filename,
744 """Parse --line-range log option and return a list of tuples (filename,
744 (fromline, toline)).
745 (fromline, toline)).
745 """
746 """
746 linerangebyfname = []
747 linerangebyfname = []
747 for pat in opts.get('line_range', []):
748 for pat in opts.get('line_range', []):
748 try:
749 try:
749 pat, linerange = pat.rsplit(',', 1)
750 pat, linerange = pat.rsplit(',', 1)
750 except ValueError:
751 except ValueError:
751 raise error.Abort(_('malformatted line-range pattern %s') % pat)
752 raise error.Abort(_('malformatted line-range pattern %s') % pat)
752 try:
753 try:
753 fromline, toline = map(int, linerange.split(':'))
754 fromline, toline = map(int, linerange.split(':'))
754 except ValueError:
755 except ValueError:
755 raise error.Abort(_("invalid line range for %s") % pat)
756 raise error.Abort(_("invalid line range for %s") % pat)
756 msg = _("line range pattern '%s' must match exactly one file") % pat
757 msg = _("line range pattern '%s' must match exactly one file") % pat
757 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
758 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
758 linerangebyfname.append(
759 linerangebyfname.append(
759 (fname, util.processlinerange(fromline, toline)))
760 (fname, util.processlinerange(fromline, toline)))
760 return linerangebyfname
761 return linerangebyfname
761
762
762 def getlinerangerevs(repo, userrevs, opts):
763 def getlinerangerevs(repo, userrevs, opts):
763 """Return (revs, differ).
764 """Return (revs, differ).
764
765
765 "revs" are revisions obtained by processing "line-range" log options and
766 "revs" are revisions obtained by processing "line-range" log options and
766 walking block ancestors of each specified file/line-range.
767 walking block ancestors of each specified file/line-range.
767
768
768 "differ" is a changesetdiffer with pre-configured file matcher and hunks
769 "differ" is a changesetdiffer with pre-configured file matcher and hunks
769 filter.
770 filter.
770 """
771 """
771 wctx = repo[None]
772 wctx = repo[None]
772
773
773 # Two-levels map of "rev -> file ctx -> [line range]".
774 # Two-levels map of "rev -> file ctx -> [line range]".
774 linerangesbyrev = {}
775 linerangesbyrev = {}
775 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
776 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
776 if fname not in wctx:
777 if fname not in wctx:
777 raise error.Abort(_('cannot follow file not in parent '
778 raise error.Abort(_('cannot follow file not in parent '
778 'revision: "%s"') % fname)
779 'revision: "%s"') % fname)
779 fctx = wctx.filectx(fname)
780 fctx = wctx.filectx(fname)
780 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
781 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
781 rev = fctx.introrev()
782 rev = fctx.introrev()
782 if rev not in userrevs:
783 if rev not in userrevs:
783 continue
784 continue
784 linerangesbyrev.setdefault(
785 linerangesbyrev.setdefault(
785 rev, {}).setdefault(
786 rev, {}).setdefault(
786 fctx.path(), []).append(linerange)
787 fctx.path(), []).append(linerange)
787
788
788 def nofilterhunksfn(fctx, hunks):
789 def nofilterhunksfn(fctx, hunks):
789 return hunks
790 return hunks
790
791
791 def hunksfilter(ctx):
792 def hunksfilter(ctx):
792 fctxlineranges = linerangesbyrev.get(ctx.rev())
793 fctxlineranges = linerangesbyrev.get(ctx.rev())
793 if fctxlineranges is None:
794 if fctxlineranges is None:
794 return nofilterhunksfn
795 return nofilterhunksfn
795
796
796 def filterfn(fctx, hunks):
797 def filterfn(fctx, hunks):
797 lineranges = fctxlineranges.get(fctx.path())
798 lineranges = fctxlineranges.get(fctx.path())
798 if lineranges is not None:
799 if lineranges is not None:
799 for hr, lines in hunks:
800 for hr, lines in hunks:
800 if hr is None: # binary
801 if hr is None: # binary
801 yield hr, lines
802 yield hr, lines
802 continue
803 continue
803 if any(mdiff.hunkinrange(hr[2:], lr)
804 if any(mdiff.hunkinrange(hr[2:], lr)
804 for lr in lineranges):
805 for lr in lineranges):
805 yield hr, lines
806 yield hr, lines
806 else:
807 else:
807 for hunk in hunks:
808 for hunk in hunks:
808 yield hunk
809 yield hunk
809
810
810 return filterfn
811 return filterfn
811
812
812 def filematcher(ctx):
813 def filematcher(ctx):
813 files = list(linerangesbyrev.get(ctx.rev(), []))
814 files = list(linerangesbyrev.get(ctx.rev(), []))
814 return scmutil.matchfiles(repo, files)
815 return scmutil.matchfiles(repo, files)
815
816
816 revs = sorted(linerangesbyrev, reverse=True)
817 revs = sorted(linerangesbyrev, reverse=True)
817
818
818 differ = changesetdiffer()
819 differ = changesetdiffer()
819 differ._makefilematcher = filematcher
820 differ._makefilematcher = filematcher
820 differ._makehunksfilter = hunksfilter
821 differ._makehunksfilter = hunksfilter
821 return revs, differ
822 return revs, differ
822
823
823 def _graphnodeformatter(ui, displayer):
824 def _graphnodeformatter(ui, displayer):
824 spec = ui.config('ui', 'graphnodetemplate')
825 spec = ui.config('ui', 'graphnodetemplate')
825 if not spec:
826 if not spec:
826 return templatekw.getgraphnode # fast path for "{graphnode}"
827 return templatekw.getgraphnode # fast path for "{graphnode}"
827
828
828 spec = templater.unquotestring(spec)
829 spec = templater.unquotestring(spec)
829 if isinstance(displayer, changesettemplater):
830 if isinstance(displayer, changesettemplater):
830 # reuse cache of slow templates
831 # reuse cache of slow templates
831 tres = displayer._tresources
832 tres = displayer._tresources
832 else:
833 else:
833 tres = formatter.templateresources(ui)
834 tres = formatter.templateresources(ui)
834 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
835 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
835 resources=tres)
836 resources=tres)
836 def formatnode(repo, ctx):
837 def formatnode(repo, ctx):
837 props = {'ctx': ctx, 'repo': repo}
838 props = {'ctx': ctx, 'repo': repo}
838 return templ.renderdefault(props)
839 return templ.renderdefault(props)
839 return formatnode
840 return formatnode
840
841
841 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
842 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
842 props = props or {}
843 props = props or {}
843 formatnode = _graphnodeformatter(ui, displayer)
844 formatnode = _graphnodeformatter(ui, displayer)
844 state = graphmod.asciistate()
845 state = graphmod.asciistate()
845 styles = state['styles']
846 styles = state['styles']
846
847
847 # only set graph styling if HGPLAIN is not set.
848 # only set graph styling if HGPLAIN is not set.
848 if ui.plain('graph'):
849 if ui.plain('graph'):
849 # set all edge styles to |, the default pre-3.8 behaviour
850 # set all edge styles to |, the default pre-3.8 behaviour
850 styles.update(dict.fromkeys(styles, '|'))
851 styles.update(dict.fromkeys(styles, '|'))
851 else:
852 else:
852 edgetypes = {
853 edgetypes = {
853 'parent': graphmod.PARENT,
854 'parent': graphmod.PARENT,
854 'grandparent': graphmod.GRANDPARENT,
855 'grandparent': graphmod.GRANDPARENT,
855 'missing': graphmod.MISSINGPARENT
856 'missing': graphmod.MISSINGPARENT
856 }
857 }
857 for name, key in edgetypes.items():
858 for name, key in edgetypes.items():
858 # experimental config: experimental.graphstyle.*
859 # experimental config: experimental.graphstyle.*
859 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
860 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
860 styles[key])
861 styles[key])
861 if not styles[key]:
862 if not styles[key]:
862 styles[key] = None
863 styles[key] = None
863
864
864 # experimental config: experimental.graphshorten
865 # experimental config: experimental.graphshorten
865 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
866 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
866
867
867 for rev, type, ctx, parents in dag:
868 for rev, type, ctx, parents in dag:
868 char = formatnode(repo, ctx)
869 char = formatnode(repo, ctx)
869 copies = None
870 copies = None
870 if getrenamed and ctx.rev():
871 if getrenamed and ctx.rev():
871 copies = []
872 copies = []
872 for fn in ctx.files():
873 for fn in ctx.files():
873 rename = getrenamed(fn, ctx.rev())
874 rename = getrenamed(fn, ctx.rev())
874 if rename:
875 if rename:
875 copies.append((fn, rename))
876 copies.append((fn, rename))
876 edges = edgefn(type, char, state, rev, parents)
877 edges = edgefn(type, char, state, rev, parents)
877 firstedge = next(edges)
878 firstedge = next(edges)
878 width = firstedge[2]
879 width = firstedge[2]
879 displayer.show(ctx, copies=copies,
880 displayer.show(ctx, copies=copies,
880 graphwidth=width, **pycompat.strkwargs(props))
881 graphwidth=width, **pycompat.strkwargs(props))
881 lines = displayer.hunk.pop(rev).split('\n')
882 lines = displayer.hunk.pop(rev).split('\n')
882 if not lines[-1]:
883 if not lines[-1]:
883 del lines[-1]
884 del lines[-1]
884 displayer.flush(ctx)
885 displayer.flush(ctx)
885 for type, char, width, coldata in itertools.chain([firstedge], edges):
886 for type, char, width, coldata in itertools.chain([firstedge], edges):
886 graphmod.ascii(ui, state, type, char, lines, coldata)
887 graphmod.ascii(ui, state, type, char, lines, coldata)
887 lines = []
888 lines = []
888 displayer.close()
889 displayer.close()
889
890
890 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
891 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
891 revdag = graphmod.dagwalker(repo, revs)
892 revdag = graphmod.dagwalker(repo, revs)
892 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
893 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
893
894
894 def displayrevs(ui, repo, revs, displayer, getrenamed):
895 def displayrevs(ui, repo, revs, displayer, getrenamed):
895 for rev in revs:
896 for rev in revs:
896 ctx = repo[rev]
897 ctx = repo[rev]
897 copies = None
898 copies = None
898 if getrenamed is not None and rev:
899 if getrenamed is not None and rev:
899 copies = []
900 copies = []
900 for fn in ctx.files():
901 for fn in ctx.files():
901 rename = getrenamed(fn, rev)
902 rename = getrenamed(fn, rev)
902 if rename:
903 if rename:
903 copies.append((fn, rename))
904 copies.append((fn, rename))
904 displayer.show(ctx, copies=copies)
905 displayer.show(ctx, copies=copies)
905 displayer.flush(ctx)
906 displayer.flush(ctx)
906 displayer.close()
907 displayer.close()
907
908
908 def checkunsupportedgraphflags(pats, opts):
909 def checkunsupportedgraphflags(pats, opts):
909 for op in ["newest_first"]:
910 for op in ["newest_first"]:
910 if op in opts and opts[op]:
911 if op in opts and opts[op]:
911 raise error.Abort(_("-G/--graph option is incompatible with --%s")
912 raise error.Abort(_("-G/--graph option is incompatible with --%s")
912 % op.replace("_", "-"))
913 % op.replace("_", "-"))
913
914
914 def graphrevs(repo, nodes, opts):
915 def graphrevs(repo, nodes, opts):
915 limit = getlimit(opts)
916 limit = getlimit(opts)
916 nodes.reverse()
917 nodes.reverse()
917 if limit is not None:
918 if limit is not None:
918 nodes = nodes[:limit]
919 nodes = nodes[:limit]
919 return graphmod.nodes(repo, nodes)
920 return graphmod.nodes(repo, nodes)
@@ -1,1843 +1,1839 b''
1 # subrepo.py - sub-repository classes and factory
1 # subrepo.py - sub-repository classes and factory
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
15 import re
14 import re
16 import stat
15 import stat
17 import subprocess
16 import subprocess
18 import sys
17 import sys
19 import tarfile
18 import tarfile
20 import xml.dom.minidom
19 import xml.dom.minidom
21
20
22 from .i18n import _
21 from .i18n import _
23 from . import (
22 from . import (
24 cmdutil,
23 cmdutil,
25 encoding,
24 encoding,
26 error,
25 error,
27 exchange,
26 exchange,
28 logcmdutil,
27 logcmdutil,
29 match as matchmod,
28 match as matchmod,
30 node,
29 node,
31 pathutil,
30 pathutil,
32 phases,
31 phases,
33 pycompat,
32 pycompat,
34 scmutil,
33 scmutil,
35 subrepoutil,
34 subrepoutil,
36 util,
35 util,
37 vfs as vfsmod,
36 vfs as vfsmod,
38 )
37 )
39 from .utils import (
38 from .utils import (
40 dateutil,
39 dateutil,
41 procutil,
40 procutil,
42 stringutil,
41 stringutil,
43 )
42 )
44
43
45 hg = None
44 hg = None
46 reporelpath = subrepoutil.reporelpath
45 reporelpath = subrepoutil.reporelpath
47 subrelpath = subrepoutil.subrelpath
46 subrelpath = subrepoutil.subrelpath
48 _abssource = subrepoutil._abssource
47 _abssource = subrepoutil._abssource
49 propertycache = util.propertycache
48 propertycache = util.propertycache
50
49
51 def _expandedabspath(path):
50 def _expandedabspath(path):
52 '''
51 '''
53 get a path or url and if it is a path expand it and return an absolute path
52 get a path or url and if it is a path expand it and return an absolute path
54 '''
53 '''
55 expandedpath = util.urllocalpath(util.expandpath(path))
54 expandedpath = util.urllocalpath(util.expandpath(path))
56 u = util.url(expandedpath)
55 u = util.url(expandedpath)
57 if not u.scheme:
56 if not u.scheme:
58 path = util.normpath(os.path.abspath(u.path))
57 path = util.normpath(os.path.abspath(u.path))
59 return path
58 return path
60
59
61 def _getstorehashcachename(remotepath):
60 def _getstorehashcachename(remotepath):
62 '''get a unique filename for the store hash cache of a remote repository'''
61 '''get a unique filename for the store hash cache of a remote repository'''
63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
62 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64
63
65 class SubrepoAbort(error.Abort):
64 class SubrepoAbort(error.Abort):
66 """Exception class used to avoid handling a subrepo error more than once"""
65 """Exception class used to avoid handling a subrepo error more than once"""
67 def __init__(self, *args, **kw):
66 def __init__(self, *args, **kw):
68 self.subrepo = kw.pop(r'subrepo', None)
67 self.subrepo = kw.pop(r'subrepo', None)
69 self.cause = kw.pop(r'cause', None)
68 self.cause = kw.pop(r'cause', None)
70 error.Abort.__init__(self, *args, **kw)
69 error.Abort.__init__(self, *args, **kw)
71
70
72 def annotatesubrepoerror(func):
71 def annotatesubrepoerror(func):
73 def decoratedmethod(self, *args, **kargs):
72 def decoratedmethod(self, *args, **kargs):
74 try:
73 try:
75 res = func(self, *args, **kargs)
74 res = func(self, *args, **kargs)
76 except SubrepoAbort as ex:
75 except SubrepoAbort as ex:
77 # This exception has already been handled
76 # This exception has already been handled
78 raise ex
77 raise ex
79 except error.Abort as ex:
78 except error.Abort as ex:
80 subrepo = subrelpath(self)
79 subrepo = subrelpath(self)
81 errormsg = (stringutil.forcebytestr(ex) + ' '
80 errormsg = (stringutil.forcebytestr(ex) + ' '
82 + _('(in subrepository "%s")') % subrepo)
81 + _('(in subrepository "%s")') % subrepo)
83 # avoid handling this exception by raising a SubrepoAbort exception
82 # avoid handling this exception by raising a SubrepoAbort exception
84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
83 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 cause=sys.exc_info())
84 cause=sys.exc_info())
86 return res
85 return res
87 return decoratedmethod
86 return decoratedmethod
88
87
89 def _updateprompt(ui, sub, dirty, local, remote):
88 def _updateprompt(ui, sub, dirty, local, remote):
90 if dirty:
89 if dirty:
91 msg = (_(' subrepository sources for %s differ\n'
90 msg = (_(' subrepository sources for %s differ\n'
92 'use (l)ocal source (%s) or (r)emote source (%s)?'
91 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 '$$ &Local $$ &Remote')
92 '$$ &Local $$ &Remote')
94 % (subrelpath(sub), local, remote))
93 % (subrelpath(sub), local, remote))
95 else:
94 else:
96 msg = (_(' subrepository sources for %s differ (in checked out '
95 msg = (_(' subrepository sources for %s differ (in checked out '
97 'version)\n'
96 'version)\n'
98 'use (l)ocal source (%s) or (r)emote source (%s)?'
97 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 '$$ &Local $$ &Remote')
98 '$$ &Local $$ &Remote')
100 % (subrelpath(sub), local, remote))
99 % (subrelpath(sub), local, remote))
101 return ui.promptchoice(msg, 0)
100 return ui.promptchoice(msg, 0)
102
101
103 def _sanitize(ui, vfs, ignore):
102 def _sanitize(ui, vfs, ignore):
104 for dirname, dirs, names in vfs.walk():
103 for dirname, dirs, names in vfs.walk():
105 for i, d in enumerate(dirs):
104 for i, d in enumerate(dirs):
106 if d.lower() == ignore:
105 if d.lower() == ignore:
107 del dirs[i]
106 del dirs[i]
108 break
107 break
109 if vfs.basename(dirname).lower() != '.hg':
108 if vfs.basename(dirname).lower() != '.hg':
110 continue
109 continue
111 for f in names:
110 for f in names:
112 if f.lower() == 'hgrc':
111 if f.lower() == 'hgrc':
113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
112 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 "in '%s'\n") % vfs.join(dirname))
113 "in '%s'\n") % vfs.join(dirname))
115 vfs.unlink(vfs.reljoin(dirname, f))
114 vfs.unlink(vfs.reljoin(dirname, f))
116
115
117 def _auditsubrepopath(repo, path):
116 def _auditsubrepopath(repo, path):
118 # sanity check for potentially unsafe paths such as '~' and '$FOO'
117 # sanity check for potentially unsafe paths such as '~' and '$FOO'
119 if path.startswith('~') or '$' in path or util.expandpath(path) != path:
118 if path.startswith('~') or '$' in path or util.expandpath(path) != path:
120 raise error.Abort(_('subrepo path contains illegal component: %s')
119 raise error.Abort(_('subrepo path contains illegal component: %s')
121 % path)
120 % path)
122 # auditor doesn't check if the path itself is a symlink
121 # auditor doesn't check if the path itself is a symlink
123 pathutil.pathauditor(repo.root)(path)
122 pathutil.pathauditor(repo.root)(path)
124 if repo.wvfs.islink(path):
123 if repo.wvfs.islink(path):
125 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
124 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
126
125
127 SUBREPO_ALLOWED_DEFAULTS = {
126 SUBREPO_ALLOWED_DEFAULTS = {
128 'hg': True,
127 'hg': True,
129 'git': False,
128 'git': False,
130 'svn': False,
129 'svn': False,
131 }
130 }
132
131
133 def _checktype(ui, kind):
132 def _checktype(ui, kind):
134 # subrepos.allowed is a master kill switch. If disabled, subrepos are
133 # subrepos.allowed is a master kill switch. If disabled, subrepos are
135 # disabled period.
134 # disabled period.
136 if not ui.configbool('subrepos', 'allowed', True):
135 if not ui.configbool('subrepos', 'allowed', True):
137 raise error.Abort(_('subrepos not enabled'),
136 raise error.Abort(_('subrepos not enabled'),
138 hint=_("see 'hg help config.subrepos' for details"))
137 hint=_("see 'hg help config.subrepos' for details"))
139
138
140 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
139 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
141 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
140 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
142 raise error.Abort(_('%s subrepos not allowed') % kind,
141 raise error.Abort(_('%s subrepos not allowed') % kind,
143 hint=_("see 'hg help config.subrepos' for details"))
142 hint=_("see 'hg help config.subrepos' for details"))
144
143
145 if kind not in types:
144 if kind not in types:
146 raise error.Abort(_('unknown subrepo type %s') % kind)
145 raise error.Abort(_('unknown subrepo type %s') % kind)
147
146
148 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
147 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
149 """return instance of the right subrepo class for subrepo in path"""
148 """return instance of the right subrepo class for subrepo in path"""
150 # subrepo inherently violates our import layering rules
149 # subrepo inherently violates our import layering rules
151 # because it wants to make repo objects from deep inside the stack
150 # because it wants to make repo objects from deep inside the stack
152 # so we manually delay the circular imports to not break
151 # so we manually delay the circular imports to not break
153 # scripts that don't use our demand-loading
152 # scripts that don't use our demand-loading
154 global hg
153 global hg
155 from . import hg as h
154 from . import hg as h
156 hg = h
155 hg = h
157
156
158 repo = ctx.repo()
157 repo = ctx.repo()
159 _auditsubrepopath(repo, path)
158 _auditsubrepopath(repo, path)
160 state = ctx.substate[path]
159 state = ctx.substate[path]
161 _checktype(repo.ui, state[2])
160 _checktype(repo.ui, state[2])
162 if allowwdir:
161 if allowwdir:
163 state = (state[0], ctx.subrev(path), state[2])
162 state = (state[0], ctx.subrev(path), state[2])
164 return types[state[2]](ctx, path, state[:2], allowcreate)
163 return types[state[2]](ctx, path, state[:2], allowcreate)
165
164
166 def nullsubrepo(ctx, path, pctx):
165 def nullsubrepo(ctx, path, pctx):
167 """return an empty subrepo in pctx for the extant subrepo in ctx"""
166 """return an empty subrepo in pctx for the extant subrepo in ctx"""
168 # subrepo inherently violates our import layering rules
167 # subrepo inherently violates our import layering rules
169 # because it wants to make repo objects from deep inside the stack
168 # because it wants to make repo objects from deep inside the stack
170 # so we manually delay the circular imports to not break
169 # so we manually delay the circular imports to not break
171 # scripts that don't use our demand-loading
170 # scripts that don't use our demand-loading
172 global hg
171 global hg
173 from . import hg as h
172 from . import hg as h
174 hg = h
173 hg = h
175
174
176 repo = ctx.repo()
175 repo = ctx.repo()
177 _auditsubrepopath(repo, path)
176 _auditsubrepopath(repo, path)
178 state = ctx.substate[path]
177 state = ctx.substate[path]
179 _checktype(repo.ui, state[2])
178 _checktype(repo.ui, state[2])
180 subrev = ''
179 subrev = ''
181 if state[2] == 'hg':
180 if state[2] == 'hg':
182 subrev = "0" * 40
181 subrev = "0" * 40
183 return types[state[2]](pctx, path, (state[0], subrev), True)
182 return types[state[2]](pctx, path, (state[0], subrev), True)
184
183
185 # subrepo classes need to implement the following abstract class:
184 # subrepo classes need to implement the following abstract class:
186
185
187 class abstractsubrepo(object):
186 class abstractsubrepo(object):
188
187
189 def __init__(self, ctx, path):
188 def __init__(self, ctx, path):
190 """Initialize abstractsubrepo part
189 """Initialize abstractsubrepo part
191
190
192 ``ctx`` is the context referring this subrepository in the
191 ``ctx`` is the context referring this subrepository in the
193 parent repository.
192 parent repository.
194
193
195 ``path`` is the path to this subrepository as seen from
194 ``path`` is the path to this subrepository as seen from
196 innermost repository.
195 innermost repository.
197 """
196 """
198 self.ui = ctx.repo().ui
197 self.ui = ctx.repo().ui
199 self._ctx = ctx
198 self._ctx = ctx
200 self._path = path
199 self._path = path
201
200
202 def addwebdirpath(self, serverpath, webconf):
201 def addwebdirpath(self, serverpath, webconf):
203 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
202 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
204
203
205 ``serverpath`` is the path component of the URL for this repo.
204 ``serverpath`` is the path component of the URL for this repo.
206
205
207 ``webconf`` is the dictionary of hgwebdir entries.
206 ``webconf`` is the dictionary of hgwebdir entries.
208 """
207 """
209 pass
208 pass
210
209
211 def storeclean(self, path):
210 def storeclean(self, path):
212 """
211 """
213 returns true if the repository has not changed since it was last
212 returns true if the repository has not changed since it was last
214 cloned from or pushed to a given repository.
213 cloned from or pushed to a given repository.
215 """
214 """
216 return False
215 return False
217
216
218 def dirty(self, ignoreupdate=False, missing=False):
217 def dirty(self, ignoreupdate=False, missing=False):
219 """returns true if the dirstate of the subrepo is dirty or does not
218 """returns true if the dirstate of the subrepo is dirty or does not
220 match current stored state. If ignoreupdate is true, only check
219 match current stored state. If ignoreupdate is true, only check
221 whether the subrepo has uncommitted changes in its dirstate. If missing
220 whether the subrepo has uncommitted changes in its dirstate. If missing
222 is true, check for deleted files.
221 is true, check for deleted files.
223 """
222 """
224 raise NotImplementedError
223 raise NotImplementedError
225
224
226 def dirtyreason(self, ignoreupdate=False, missing=False):
225 def dirtyreason(self, ignoreupdate=False, missing=False):
227 """return reason string if it is ``dirty()``
226 """return reason string if it is ``dirty()``
228
227
229 Returned string should have enough information for the message
228 Returned string should have enough information for the message
230 of exception.
229 of exception.
231
230
232 This returns None, otherwise.
231 This returns None, otherwise.
233 """
232 """
234 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
233 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
235 return _('uncommitted changes in subrepository "%s"'
234 return _('uncommitted changes in subrepository "%s"'
236 ) % subrelpath(self)
235 ) % subrelpath(self)
237
236
238 def bailifchanged(self, ignoreupdate=False, hint=None):
237 def bailifchanged(self, ignoreupdate=False, hint=None):
239 """raise Abort if subrepository is ``dirty()``
238 """raise Abort if subrepository is ``dirty()``
240 """
239 """
241 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
240 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
242 missing=True)
241 missing=True)
243 if dirtyreason:
242 if dirtyreason:
244 raise error.Abort(dirtyreason, hint=hint)
243 raise error.Abort(dirtyreason, hint=hint)
245
244
246 def basestate(self):
245 def basestate(self):
247 """current working directory base state, disregarding .hgsubstate
246 """current working directory base state, disregarding .hgsubstate
248 state and working directory modifications"""
247 state and working directory modifications"""
249 raise NotImplementedError
248 raise NotImplementedError
250
249
251 def checknested(self, path):
250 def checknested(self, path):
252 """check if path is a subrepository within this repository"""
251 """check if path is a subrepository within this repository"""
253 return False
252 return False
254
253
255 def commit(self, text, user, date):
254 def commit(self, text, user, date):
256 """commit the current changes to the subrepo with the given
255 """commit the current changes to the subrepo with the given
257 log message. Use given user and date if possible. Return the
256 log message. Use given user and date if possible. Return the
258 new state of the subrepo.
257 new state of the subrepo.
259 """
258 """
260 raise NotImplementedError
259 raise NotImplementedError
261
260
262 def phase(self, state):
261 def phase(self, state):
263 """returns phase of specified state in the subrepository.
262 """returns phase of specified state in the subrepository.
264 """
263 """
265 return phases.public
264 return phases.public
266
265
267 def remove(self):
266 def remove(self):
268 """remove the subrepo
267 """remove the subrepo
269
268
270 (should verify the dirstate is not dirty first)
269 (should verify the dirstate is not dirty first)
271 """
270 """
272 raise NotImplementedError
271 raise NotImplementedError
273
272
274 def get(self, state, overwrite=False):
273 def get(self, state, overwrite=False):
275 """run whatever commands are needed to put the subrepo into
274 """run whatever commands are needed to put the subrepo into
276 this state
275 this state
277 """
276 """
278 raise NotImplementedError
277 raise NotImplementedError
279
278
280 def merge(self, state):
279 def merge(self, state):
281 """merge currently-saved state with the new state."""
280 """merge currently-saved state with the new state."""
282 raise NotImplementedError
281 raise NotImplementedError
283
282
284 def push(self, opts):
283 def push(self, opts):
285 """perform whatever action is analogous to 'hg push'
284 """perform whatever action is analogous to 'hg push'
286
285
287 This may be a no-op on some systems.
286 This may be a no-op on some systems.
288 """
287 """
289 raise NotImplementedError
288 raise NotImplementedError
290
289
291 def add(self, ui, match, prefix, explicitonly, **opts):
290 def add(self, ui, match, prefix, explicitonly, **opts):
292 return []
291 return []
293
292
294 def addremove(self, matcher, prefix, opts):
293 def addremove(self, matcher, prefix, opts):
295 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
294 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
296 return 1
295 return 1
297
296
298 def cat(self, match, fm, fntemplate, prefix, **opts):
297 def cat(self, match, fm, fntemplate, prefix, **opts):
299 return 1
298 return 1
300
299
301 def status(self, rev2, **opts):
300 def status(self, rev2, **opts):
302 return scmutil.status([], [], [], [], [], [], [])
301 return scmutil.status([], [], [], [], [], [], [])
303
302
304 def diff(self, ui, diffopts, node2, match, prefix, **opts):
303 def diff(self, ui, diffopts, node2, match, prefix, **opts):
305 pass
304 pass
306
305
307 def outgoing(self, ui, dest, opts):
306 def outgoing(self, ui, dest, opts):
308 return 1
307 return 1
309
308
310 def incoming(self, ui, source, opts):
309 def incoming(self, ui, source, opts):
311 return 1
310 return 1
312
311
313 def files(self):
312 def files(self):
314 """return filename iterator"""
313 """return filename iterator"""
315 raise NotImplementedError
314 raise NotImplementedError
316
315
317 def filedata(self, name, decode):
316 def filedata(self, name, decode):
318 """return file data, optionally passed through repo decoders"""
317 """return file data, optionally passed through repo decoders"""
319 raise NotImplementedError
318 raise NotImplementedError
320
319
321 def fileflags(self, name):
320 def fileflags(self, name):
322 """return file flags"""
321 """return file flags"""
323 return ''
322 return ''
324
323
325 def matchfileset(self, expr, badfn=None):
324 def matchfileset(self, expr, badfn=None):
326 """Resolve the fileset expression for this repo"""
325 """Resolve the fileset expression for this repo"""
327 return matchmod.nevermatcher(self.wvfs.base, '', badfn=badfn)
326 return matchmod.nevermatcher(self.wvfs.base, '', badfn=badfn)
328
327
329 def printfiles(self, ui, m, fm, fmt, subrepos):
328 def printfiles(self, ui, m, fm, fmt, subrepos):
330 """handle the files command for this subrepo"""
329 """handle the files command for this subrepo"""
331 return 1
330 return 1
332
331
333 def archive(self, archiver, prefix, match=None, decode=True):
332 def archive(self, archiver, prefix, match=None, decode=True):
334 if match is not None:
333 if match is not None:
335 files = [f for f in self.files() if match(f)]
334 files = [f for f in self.files() if match(f)]
336 else:
335 else:
337 files = self.files()
336 files = self.files()
338 total = len(files)
337 total = len(files)
339 relpath = subrelpath(self)
338 relpath = subrelpath(self)
340 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
339 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
341 unit=_('files'), total=total)
340 unit=_('files'), total=total)
342 progress.update(0)
341 progress.update(0)
343 for name in files:
342 for name in files:
344 flags = self.fileflags(name)
343 flags = self.fileflags(name)
345 mode = 'x' in flags and 0o755 or 0o644
344 mode = 'x' in flags and 0o755 or 0o644
346 symlink = 'l' in flags
345 symlink = 'l' in flags
347 archiver.addfile(prefix + self._path + '/' + name,
346 archiver.addfile(prefix + self._path + '/' + name,
348 mode, symlink, self.filedata(name, decode))
347 mode, symlink, self.filedata(name, decode))
349 progress.increment()
348 progress.increment()
350 progress.complete()
349 progress.complete()
351 return total
350 return total
352
351
353 def walk(self, match):
352 def walk(self, match):
354 '''
353 '''
355 walk recursively through the directory tree, finding all files
354 walk recursively through the directory tree, finding all files
356 matched by the match function
355 matched by the match function
357 '''
356 '''
358
357
359 def forget(self, match, prefix, dryrun, interactive):
358 def forget(self, match, prefix, dryrun, interactive):
360 return ([], [])
359 return ([], [])
361
360
362 def removefiles(self, matcher, prefix, after, force, subrepos,
361 def removefiles(self, matcher, prefix, after, force, subrepos,
363 dryrun, warnings):
362 dryrun, warnings):
364 """remove the matched files from the subrepository and the filesystem,
363 """remove the matched files from the subrepository and the filesystem,
365 possibly by force and/or after the file has been removed from the
364 possibly by force and/or after the file has been removed from the
366 filesystem. Return 0 on success, 1 on any warning.
365 filesystem. Return 0 on success, 1 on any warning.
367 """
366 """
368 warnings.append(_("warning: removefiles not implemented (%s)")
367 warnings.append(_("warning: removefiles not implemented (%s)")
369 % self._path)
368 % self._path)
370 return 1
369 return 1
371
370
372 def revert(self, substate, *pats, **opts):
371 def revert(self, substate, *pats, **opts):
373 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
372 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
374 % (substate[0], substate[2]))
373 % (substate[0], substate[2]))
375 return []
374 return []
376
375
377 def shortid(self, revid):
376 def shortid(self, revid):
378 return revid
377 return revid
379
378
380 def unshare(self):
379 def unshare(self):
381 '''
380 '''
382 convert this repository from shared to normal storage.
381 convert this repository from shared to normal storage.
383 '''
382 '''
384
383
385 def verify(self):
384 def verify(self):
386 '''verify the integrity of the repository. Return 0 on success or
385 '''verify the integrity of the repository. Return 0 on success or
387 warning, 1 on any error.
386 warning, 1 on any error.
388 '''
387 '''
389 return 0
388 return 0
390
389
391 @propertycache
390 @propertycache
392 def wvfs(self):
391 def wvfs(self):
393 """return vfs to access the working directory of this subrepository
392 """return vfs to access the working directory of this subrepository
394 """
393 """
395 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
394 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
396
395
397 @propertycache
396 @propertycache
398 def _relpath(self):
397 def _relpath(self):
399 """return path to this subrepository as seen from outermost repository
398 """return path to this subrepository as seen from outermost repository
400 """
399 """
401 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
400 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
402
401
403 class hgsubrepo(abstractsubrepo):
402 class hgsubrepo(abstractsubrepo):
404 def __init__(self, ctx, path, state, allowcreate):
403 def __init__(self, ctx, path, state, allowcreate):
405 super(hgsubrepo, self).__init__(ctx, path)
404 super(hgsubrepo, self).__init__(ctx, path)
406 self._state = state
405 self._state = state
407 r = ctx.repo()
406 r = ctx.repo()
408 root = r.wjoin(path)
407 root = r.wjoin(path)
409 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
408 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
410 # repository constructor does expand variables in path, which is
409 # repository constructor does expand variables in path, which is
411 # unsafe since subrepo path might come from untrusted source.
410 # unsafe since subrepo path might come from untrusted source.
412 if os.path.realpath(util.expandpath(root)) != root:
411 if os.path.realpath(util.expandpath(root)) != root:
413 raise error.Abort(_('subrepo path contains illegal component: %s')
412 raise error.Abort(_('subrepo path contains illegal component: %s')
414 % path)
413 % path)
415 self._repo = hg.repository(r.baseui, root, create=create)
414 self._repo = hg.repository(r.baseui, root, create=create)
416 if self._repo.root != root:
415 if self._repo.root != root:
417 raise error.ProgrammingError('failed to reject unsafe subrepo '
416 raise error.ProgrammingError('failed to reject unsafe subrepo '
418 'path: %s (expanded to %s)'
417 'path: %s (expanded to %s)'
419 % (root, self._repo.root))
418 % (root, self._repo.root))
420
419
421 # Propagate the parent's --hidden option
420 # Propagate the parent's --hidden option
422 if r is r.unfiltered():
421 if r is r.unfiltered():
423 self._repo = self._repo.unfiltered()
422 self._repo = self._repo.unfiltered()
424
423
425 self.ui = self._repo.ui
424 self.ui = self._repo.ui
426 for s, k in [('ui', 'commitsubrepos')]:
425 for s, k in [('ui', 'commitsubrepos')]:
427 v = r.ui.config(s, k)
426 v = r.ui.config(s, k)
428 if v:
427 if v:
429 self.ui.setconfig(s, k, v, 'subrepo')
428 self.ui.setconfig(s, k, v, 'subrepo')
430 # internal config: ui._usedassubrepo
429 # internal config: ui._usedassubrepo
431 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
430 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
432 self._initrepo(r, state[0], create)
431 self._initrepo(r, state[0], create)
433
432
434 @annotatesubrepoerror
433 @annotatesubrepoerror
435 def addwebdirpath(self, serverpath, webconf):
434 def addwebdirpath(self, serverpath, webconf):
436 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
435 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
437
436
438 def storeclean(self, path):
437 def storeclean(self, path):
439 with self._repo.lock():
438 with self._repo.lock():
440 return self._storeclean(path)
439 return self._storeclean(path)
441
440
442 def _storeclean(self, path):
441 def _storeclean(self, path):
443 clean = True
442 clean = True
444 itercache = self._calcstorehash(path)
443 itercache = self._calcstorehash(path)
445 for filehash in self._readstorehashcache(path):
444 for filehash in self._readstorehashcache(path):
446 if filehash != next(itercache, None):
445 if filehash != next(itercache, None):
447 clean = False
446 clean = False
448 break
447 break
449 if clean:
448 if clean:
450 # if not empty:
449 # if not empty:
451 # the cached and current pull states have a different size
450 # the cached and current pull states have a different size
452 clean = next(itercache, None) is None
451 clean = next(itercache, None) is None
453 return clean
452 return clean
454
453
455 def _calcstorehash(self, remotepath):
454 def _calcstorehash(self, remotepath):
456 '''calculate a unique "store hash"
455 '''calculate a unique "store hash"
457
456
458 This method is used to to detect when there are changes that may
457 This method is used to to detect when there are changes that may
459 require a push to a given remote path.'''
458 require a push to a given remote path.'''
460 # sort the files that will be hashed in increasing (likely) file size
459 # sort the files that will be hashed in increasing (likely) file size
461 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
460 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
462 yield '# %s\n' % _expandedabspath(remotepath)
461 yield '# %s\n' % _expandedabspath(remotepath)
463 vfs = self._repo.vfs
462 vfs = self._repo.vfs
464 for relname in filelist:
463 for relname in filelist:
465 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
464 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
466 yield '%s = %s\n' % (relname, filehash)
465 yield '%s = %s\n' % (relname, filehash)
467
466
468 @propertycache
467 @propertycache
469 def _cachestorehashvfs(self):
468 def _cachestorehashvfs(self):
470 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
469 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
471
470
472 def _readstorehashcache(self, remotepath):
471 def _readstorehashcache(self, remotepath):
473 '''read the store hash cache for a given remote repository'''
472 '''read the store hash cache for a given remote repository'''
474 cachefile = _getstorehashcachename(remotepath)
473 cachefile = _getstorehashcachename(remotepath)
475 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
474 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
476
475
477 def _cachestorehash(self, remotepath):
476 def _cachestorehash(self, remotepath):
478 '''cache the current store hash
477 '''cache the current store hash
479
478
480 Each remote repo requires its own store hash cache, because a subrepo
479 Each remote repo requires its own store hash cache, because a subrepo
481 store may be "clean" versus a given remote repo, but not versus another
480 store may be "clean" versus a given remote repo, but not versus another
482 '''
481 '''
483 cachefile = _getstorehashcachename(remotepath)
482 cachefile = _getstorehashcachename(remotepath)
484 with self._repo.lock():
483 with self._repo.lock():
485 storehash = list(self._calcstorehash(remotepath))
484 storehash = list(self._calcstorehash(remotepath))
486 vfs = self._cachestorehashvfs
485 vfs = self._cachestorehashvfs
487 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
486 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
488
487
489 def _getctx(self):
488 def _getctx(self):
490 '''fetch the context for this subrepo revision, possibly a workingctx
489 '''fetch the context for this subrepo revision, possibly a workingctx
491 '''
490 '''
492 if self._ctx.rev() is None:
491 if self._ctx.rev() is None:
493 return self._repo[None] # workingctx if parent is workingctx
492 return self._repo[None] # workingctx if parent is workingctx
494 else:
493 else:
495 rev = self._state[1]
494 rev = self._state[1]
496 return self._repo[rev]
495 return self._repo[rev]
497
496
498 @annotatesubrepoerror
497 @annotatesubrepoerror
499 def _initrepo(self, parentrepo, source, create):
498 def _initrepo(self, parentrepo, source, create):
500 self._repo._subparent = parentrepo
499 self._repo._subparent = parentrepo
501 self._repo._subsource = source
500 self._repo._subsource = source
502
501
503 if create:
502 if create:
504 lines = ['[paths]\n']
503 lines = ['[paths]\n']
505
504
506 def addpathconfig(key, value):
505 def addpathconfig(key, value):
507 if value:
506 if value:
508 lines.append('%s = %s\n' % (key, value))
507 lines.append('%s = %s\n' % (key, value))
509 self.ui.setconfig('paths', key, value, 'subrepo')
508 self.ui.setconfig('paths', key, value, 'subrepo')
510
509
511 defpath = _abssource(self._repo, abort=False)
510 defpath = _abssource(self._repo, abort=False)
512 defpushpath = _abssource(self._repo, True, abort=False)
511 defpushpath = _abssource(self._repo, True, abort=False)
513 addpathconfig('default', defpath)
512 addpathconfig('default', defpath)
514 if defpath != defpushpath:
513 if defpath != defpushpath:
515 addpathconfig('default-push', defpushpath)
514 addpathconfig('default-push', defpushpath)
516
515
517 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
516 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
518
517
519 @annotatesubrepoerror
518 @annotatesubrepoerror
520 def add(self, ui, match, prefix, explicitonly, **opts):
519 def add(self, ui, match, prefix, explicitonly, **opts):
521 return cmdutil.add(ui, self._repo, match, prefix, explicitonly, **opts)
520 return cmdutil.add(ui, self._repo, match, prefix, explicitonly, **opts)
522
521
523 @annotatesubrepoerror
522 @annotatesubrepoerror
524 def addremove(self, m, prefix, opts):
523 def addremove(self, m, prefix, opts):
525 # In the same way as sub directories are processed, once in a subrepo,
524 # In the same way as sub directories are processed, once in a subrepo,
526 # always entry any of its subrepos. Don't corrupt the options that will
525 # always entry any of its subrepos. Don't corrupt the options that will
527 # be used to process sibling subrepos however.
526 # be used to process sibling subrepos however.
528 opts = copy.copy(opts)
527 opts = copy.copy(opts)
529 opts['subrepos'] = True
528 opts['subrepos'] = True
530 return scmutil.addremove(self._repo, m, prefix, opts)
529 return scmutil.addremove(self._repo, m, prefix, opts)
531
530
532 @annotatesubrepoerror
531 @annotatesubrepoerror
533 def cat(self, match, fm, fntemplate, prefix, **opts):
532 def cat(self, match, fm, fntemplate, prefix, **opts):
534 rev = self._state[1]
533 rev = self._state[1]
535 ctx = self._repo[rev]
534 ctx = self._repo[rev]
536 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
535 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
537 prefix, **opts)
536 prefix, **opts)
538
537
539 @annotatesubrepoerror
538 @annotatesubrepoerror
540 def status(self, rev2, **opts):
539 def status(self, rev2, **opts):
541 try:
540 try:
542 rev1 = self._state[1]
541 rev1 = self._state[1]
543 ctx1 = self._repo[rev1]
542 ctx1 = self._repo[rev1]
544 ctx2 = self._repo[rev2]
543 ctx2 = self._repo[rev2]
545 return self._repo.status(ctx1, ctx2, **opts)
544 return self._repo.status(ctx1, ctx2, **opts)
546 except error.RepoLookupError as inst:
545 except error.RepoLookupError as inst:
547 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
546 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
548 % (inst, subrelpath(self)))
547 % (inst, subrelpath(self)))
549 return scmutil.status([], [], [], [], [], [], [])
548 return scmutil.status([], [], [], [], [], [], [])
550
549
551 @annotatesubrepoerror
550 @annotatesubrepoerror
552 def diff(self, ui, diffopts, node2, match, prefix, **opts):
551 def diff(self, ui, diffopts, node2, match, prefix, **opts):
553 try:
552 try:
554 node1 = node.bin(self._state[1])
553 node1 = node.bin(self._state[1])
555 # We currently expect node2 to come from substate and be
554 # We currently expect node2 to come from substate and be
556 # in hex format
555 # in hex format
557 if node2 is not None:
556 if node2 is not None:
558 node2 = node.bin(node2)
557 node2 = node.bin(node2)
559 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
558 logcmdutil.diffordiffstat(ui, self._repo, diffopts, node1, node2,
560 node1, node2, match,
559 match, prefix=prefix, listsubrepos=True,
561 prefix=posixpath.join(prefix, self._path),
560 **opts)
562 listsubrepos=True, **opts)
563 except error.RepoLookupError as inst:
561 except error.RepoLookupError as inst:
564 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
562 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
565 % (inst, subrelpath(self)))
563 % (inst, subrelpath(self)))
566
564
567 @annotatesubrepoerror
565 @annotatesubrepoerror
568 def archive(self, archiver, prefix, match=None, decode=True):
566 def archive(self, archiver, prefix, match=None, decode=True):
569 self._get(self._state + ('hg',))
567 self._get(self._state + ('hg',))
570 files = self.files()
568 files = self.files()
571 if match:
569 if match:
572 files = [f for f in files if match(f)]
570 files = [f for f in files if match(f)]
573 rev = self._state[1]
571 rev = self._state[1]
574 ctx = self._repo[rev]
572 ctx = self._repo[rev]
575 scmutil.prefetchfiles(self._repo, [ctx.rev()],
573 scmutil.prefetchfiles(self._repo, [ctx.rev()],
576 scmutil.matchfiles(self._repo, files))
574 scmutil.matchfiles(self._repo, files))
577 total = abstractsubrepo.archive(self, archiver, prefix, match)
575 total = abstractsubrepo.archive(self, archiver, prefix, match)
578 for subpath in ctx.substate:
576 for subpath in ctx.substate:
579 s = subrepo(ctx, subpath, True)
577 s = subrepo(ctx, subpath, True)
580 submatch = matchmod.subdirmatcher(subpath, match)
578 submatch = matchmod.subdirmatcher(subpath, match)
581 total += s.archive(archiver, prefix + self._path + '/', submatch,
579 total += s.archive(archiver, prefix + self._path + '/', submatch,
582 decode)
580 decode)
583 return total
581 return total
584
582
585 @annotatesubrepoerror
583 @annotatesubrepoerror
586 def dirty(self, ignoreupdate=False, missing=False):
584 def dirty(self, ignoreupdate=False, missing=False):
587 r = self._state[1]
585 r = self._state[1]
588 if r == '' and not ignoreupdate: # no state recorded
586 if r == '' and not ignoreupdate: # no state recorded
589 return True
587 return True
590 w = self._repo[None]
588 w = self._repo[None]
591 if r != w.p1().hex() and not ignoreupdate:
589 if r != w.p1().hex() and not ignoreupdate:
592 # different version checked out
590 # different version checked out
593 return True
591 return True
594 return w.dirty(missing=missing) # working directory changed
592 return w.dirty(missing=missing) # working directory changed
595
593
596 def basestate(self):
594 def basestate(self):
597 return self._repo['.'].hex()
595 return self._repo['.'].hex()
598
596
599 def checknested(self, path):
597 def checknested(self, path):
600 return self._repo._checknested(self._repo.wjoin(path))
598 return self._repo._checknested(self._repo.wjoin(path))
601
599
602 @annotatesubrepoerror
600 @annotatesubrepoerror
603 def commit(self, text, user, date):
601 def commit(self, text, user, date):
604 # don't bother committing in the subrepo if it's only been
602 # don't bother committing in the subrepo if it's only been
605 # updated
603 # updated
606 if not self.dirty(True):
604 if not self.dirty(True):
607 return self._repo['.'].hex()
605 return self._repo['.'].hex()
608 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
606 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
609 n = self._repo.commit(text, user, date)
607 n = self._repo.commit(text, user, date)
610 if not n:
608 if not n:
611 return self._repo['.'].hex() # different version checked out
609 return self._repo['.'].hex() # different version checked out
612 return node.hex(n)
610 return node.hex(n)
613
611
614 @annotatesubrepoerror
612 @annotatesubrepoerror
615 def phase(self, state):
613 def phase(self, state):
616 return self._repo[state or '.'].phase()
614 return self._repo[state or '.'].phase()
617
615
618 @annotatesubrepoerror
616 @annotatesubrepoerror
619 def remove(self):
617 def remove(self):
620 # we can't fully delete the repository as it may contain
618 # we can't fully delete the repository as it may contain
621 # local-only history
619 # local-only history
622 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
620 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
623 hg.clean(self._repo, node.nullid, False)
621 hg.clean(self._repo, node.nullid, False)
624
622
625 def _get(self, state):
623 def _get(self, state):
626 source, revision, kind = state
624 source, revision, kind = state
627 parentrepo = self._repo._subparent
625 parentrepo = self._repo._subparent
628
626
629 if revision in self._repo.unfiltered():
627 if revision in self._repo.unfiltered():
630 # Allow shared subrepos tracked at null to setup the sharedpath
628 # Allow shared subrepos tracked at null to setup the sharedpath
631 if len(self._repo) != 0 or not parentrepo.shared():
629 if len(self._repo) != 0 or not parentrepo.shared():
632 return True
630 return True
633 self._repo._subsource = source
631 self._repo._subsource = source
634 srcurl = _abssource(self._repo)
632 srcurl = _abssource(self._repo)
635
633
636 # Defer creating the peer until after the status message is logged, in
634 # Defer creating the peer until after the status message is logged, in
637 # case there are network problems.
635 # case there are network problems.
638 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
636 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
639
637
640 if len(self._repo) == 0:
638 if len(self._repo) == 0:
641 # use self._repo.vfs instead of self.wvfs to remove .hg only
639 # use self._repo.vfs instead of self.wvfs to remove .hg only
642 self._repo.vfs.rmtree()
640 self._repo.vfs.rmtree()
643
641
644 # A remote subrepo could be shared if there is a local copy
642 # A remote subrepo could be shared if there is a local copy
645 # relative to the parent's share source. But clone pooling doesn't
643 # relative to the parent's share source. But clone pooling doesn't
646 # assemble the repos in a tree, so that can't be consistently done.
644 # assemble the repos in a tree, so that can't be consistently done.
647 # A simpler option is for the user to configure clone pooling, and
645 # A simpler option is for the user to configure clone pooling, and
648 # work with that.
646 # work with that.
649 if parentrepo.shared() and hg.islocal(srcurl):
647 if parentrepo.shared() and hg.islocal(srcurl):
650 self.ui.status(_('sharing subrepo %s from %s\n')
648 self.ui.status(_('sharing subrepo %s from %s\n')
651 % (subrelpath(self), srcurl))
649 % (subrelpath(self), srcurl))
652 shared = hg.share(self._repo._subparent.baseui,
650 shared = hg.share(self._repo._subparent.baseui,
653 getpeer(), self._repo.root,
651 getpeer(), self._repo.root,
654 update=False, bookmarks=False)
652 update=False, bookmarks=False)
655 self._repo = shared.local()
653 self._repo = shared.local()
656 else:
654 else:
657 # TODO: find a common place for this and this code in the
655 # TODO: find a common place for this and this code in the
658 # share.py wrap of the clone command.
656 # share.py wrap of the clone command.
659 if parentrepo.shared():
657 if parentrepo.shared():
660 pool = self.ui.config('share', 'pool')
658 pool = self.ui.config('share', 'pool')
661 if pool:
659 if pool:
662 pool = util.expandpath(pool)
660 pool = util.expandpath(pool)
663
661
664 shareopts = {
662 shareopts = {
665 'pool': pool,
663 'pool': pool,
666 'mode': self.ui.config('share', 'poolnaming'),
664 'mode': self.ui.config('share', 'poolnaming'),
667 }
665 }
668 else:
666 else:
669 shareopts = {}
667 shareopts = {}
670
668
671 self.ui.status(_('cloning subrepo %s from %s\n')
669 self.ui.status(_('cloning subrepo %s from %s\n')
672 % (subrelpath(self), util.hidepassword(srcurl)))
670 % (subrelpath(self), util.hidepassword(srcurl)))
673 other, cloned = hg.clone(self._repo._subparent.baseui, {},
671 other, cloned = hg.clone(self._repo._subparent.baseui, {},
674 getpeer(), self._repo.root,
672 getpeer(), self._repo.root,
675 update=False, shareopts=shareopts)
673 update=False, shareopts=shareopts)
676 self._repo = cloned.local()
674 self._repo = cloned.local()
677 self._initrepo(parentrepo, source, create=True)
675 self._initrepo(parentrepo, source, create=True)
678 self._cachestorehash(srcurl)
676 self._cachestorehash(srcurl)
679 else:
677 else:
680 self.ui.status(_('pulling subrepo %s from %s\n')
678 self.ui.status(_('pulling subrepo %s from %s\n')
681 % (subrelpath(self), util.hidepassword(srcurl)))
679 % (subrelpath(self), util.hidepassword(srcurl)))
682 cleansub = self.storeclean(srcurl)
680 cleansub = self.storeclean(srcurl)
683 exchange.pull(self._repo, getpeer())
681 exchange.pull(self._repo, getpeer())
684 if cleansub:
682 if cleansub:
685 # keep the repo clean after pull
683 # keep the repo clean after pull
686 self._cachestorehash(srcurl)
684 self._cachestorehash(srcurl)
687 return False
685 return False
688
686
689 @annotatesubrepoerror
687 @annotatesubrepoerror
690 def get(self, state, overwrite=False):
688 def get(self, state, overwrite=False):
691 inrepo = self._get(state)
689 inrepo = self._get(state)
692 source, revision, kind = state
690 source, revision, kind = state
693 repo = self._repo
691 repo = self._repo
694 repo.ui.debug("getting subrepo %s\n" % self._path)
692 repo.ui.debug("getting subrepo %s\n" % self._path)
695 if inrepo:
693 if inrepo:
696 urepo = repo.unfiltered()
694 urepo = repo.unfiltered()
697 ctx = urepo[revision]
695 ctx = urepo[revision]
698 if ctx.hidden():
696 if ctx.hidden():
699 urepo.ui.warn(
697 urepo.ui.warn(
700 _('revision %s in subrepository "%s" is hidden\n') \
698 _('revision %s in subrepository "%s" is hidden\n') \
701 % (revision[0:12], self._path))
699 % (revision[0:12], self._path))
702 repo = urepo
700 repo = urepo
703 hg.updaterepo(repo, revision, overwrite)
701 hg.updaterepo(repo, revision, overwrite)
704
702
705 @annotatesubrepoerror
703 @annotatesubrepoerror
706 def merge(self, state):
704 def merge(self, state):
707 self._get(state)
705 self._get(state)
708 cur = self._repo['.']
706 cur = self._repo['.']
709 dst = self._repo[state[1]]
707 dst = self._repo[state[1]]
710 anc = dst.ancestor(cur)
708 anc = dst.ancestor(cur)
711
709
712 def mergefunc():
710 def mergefunc():
713 if anc == cur and dst.branch() == cur.branch():
711 if anc == cur and dst.branch() == cur.branch():
714 self.ui.debug('updating subrepository "%s"\n'
712 self.ui.debug('updating subrepository "%s"\n'
715 % subrelpath(self))
713 % subrelpath(self))
716 hg.update(self._repo, state[1])
714 hg.update(self._repo, state[1])
717 elif anc == dst:
715 elif anc == dst:
718 self.ui.debug('skipping subrepository "%s"\n'
716 self.ui.debug('skipping subrepository "%s"\n'
719 % subrelpath(self))
717 % subrelpath(self))
720 else:
718 else:
721 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
719 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
722 hg.merge(self._repo, state[1], remind=False)
720 hg.merge(self._repo, state[1], remind=False)
723
721
724 wctx = self._repo[None]
722 wctx = self._repo[None]
725 if self.dirty():
723 if self.dirty():
726 if anc != dst:
724 if anc != dst:
727 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
725 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
728 mergefunc()
726 mergefunc()
729 else:
727 else:
730 mergefunc()
728 mergefunc()
731 else:
729 else:
732 mergefunc()
730 mergefunc()
733
731
734 @annotatesubrepoerror
732 @annotatesubrepoerror
735 def push(self, opts):
733 def push(self, opts):
736 force = opts.get('force')
734 force = opts.get('force')
737 newbranch = opts.get('new_branch')
735 newbranch = opts.get('new_branch')
738 ssh = opts.get('ssh')
736 ssh = opts.get('ssh')
739
737
740 # push subrepos depth-first for coherent ordering
738 # push subrepos depth-first for coherent ordering
741 c = self._repo['.']
739 c = self._repo['.']
742 subs = c.substate # only repos that are committed
740 subs = c.substate # only repos that are committed
743 for s in sorted(subs):
741 for s in sorted(subs):
744 if c.sub(s).push(opts) == 0:
742 if c.sub(s).push(opts) == 0:
745 return False
743 return False
746
744
747 dsturl = _abssource(self._repo, True)
745 dsturl = _abssource(self._repo, True)
748 if not force:
746 if not force:
749 if self.storeclean(dsturl):
747 if self.storeclean(dsturl):
750 self.ui.status(
748 self.ui.status(
751 _('no changes made to subrepo %s since last push to %s\n')
749 _('no changes made to subrepo %s since last push to %s\n')
752 % (subrelpath(self), util.hidepassword(dsturl)))
750 % (subrelpath(self), util.hidepassword(dsturl)))
753 return None
751 return None
754 self.ui.status(_('pushing subrepo %s to %s\n') %
752 self.ui.status(_('pushing subrepo %s to %s\n') %
755 (subrelpath(self), util.hidepassword(dsturl)))
753 (subrelpath(self), util.hidepassword(dsturl)))
756 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
754 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
757 res = exchange.push(self._repo, other, force, newbranch=newbranch)
755 res = exchange.push(self._repo, other, force, newbranch=newbranch)
758
756
759 # the repo is now clean
757 # the repo is now clean
760 self._cachestorehash(dsturl)
758 self._cachestorehash(dsturl)
761 return res.cgresult
759 return res.cgresult
762
760
763 @annotatesubrepoerror
761 @annotatesubrepoerror
764 def outgoing(self, ui, dest, opts):
762 def outgoing(self, ui, dest, opts):
765 if 'rev' in opts or 'branch' in opts:
763 if 'rev' in opts or 'branch' in opts:
766 opts = copy.copy(opts)
764 opts = copy.copy(opts)
767 opts.pop('rev', None)
765 opts.pop('rev', None)
768 opts.pop('branch', None)
766 opts.pop('branch', None)
769 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
767 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
770
768
771 @annotatesubrepoerror
769 @annotatesubrepoerror
772 def incoming(self, ui, source, opts):
770 def incoming(self, ui, source, opts):
773 if 'rev' in opts or 'branch' in opts:
771 if 'rev' in opts or 'branch' in opts:
774 opts = copy.copy(opts)
772 opts = copy.copy(opts)
775 opts.pop('rev', None)
773 opts.pop('rev', None)
776 opts.pop('branch', None)
774 opts.pop('branch', None)
777 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
775 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
778
776
779 @annotatesubrepoerror
777 @annotatesubrepoerror
780 def files(self):
778 def files(self):
781 rev = self._state[1]
779 rev = self._state[1]
782 ctx = self._repo[rev]
780 ctx = self._repo[rev]
783 return ctx.manifest().keys()
781 return ctx.manifest().keys()
784
782
785 def filedata(self, name, decode):
783 def filedata(self, name, decode):
786 rev = self._state[1]
784 rev = self._state[1]
787 data = self._repo[rev][name].data()
785 data = self._repo[rev][name].data()
788 if decode:
786 if decode:
789 data = self._repo.wwritedata(name, data)
787 data = self._repo.wwritedata(name, data)
790 return data
788 return data
791
789
792 def fileflags(self, name):
790 def fileflags(self, name):
793 rev = self._state[1]
791 rev = self._state[1]
794 ctx = self._repo[rev]
792 ctx = self._repo[rev]
795 return ctx.flags(name)
793 return ctx.flags(name)
796
794
797 @annotatesubrepoerror
795 @annotatesubrepoerror
798 def printfiles(self, ui, m, fm, fmt, subrepos):
796 def printfiles(self, ui, m, fm, fmt, subrepos):
799 # If the parent context is a workingctx, use the workingctx here for
797 # If the parent context is a workingctx, use the workingctx here for
800 # consistency.
798 # consistency.
801 if self._ctx.rev() is None:
799 if self._ctx.rev() is None:
802 ctx = self._repo[None]
800 ctx = self._repo[None]
803 else:
801 else:
804 rev = self._state[1]
802 rev = self._state[1]
805 ctx = self._repo[rev]
803 ctx = self._repo[rev]
806 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
804 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
807
805
808 @annotatesubrepoerror
806 @annotatesubrepoerror
809 def matchfileset(self, expr, badfn=None):
807 def matchfileset(self, expr, badfn=None):
810 repo = self._repo
808 repo = self._repo
811 if self._ctx.rev() is None:
809 if self._ctx.rev() is None:
812 ctx = repo[None]
810 ctx = repo[None]
813 else:
811 else:
814 rev = self._state[1]
812 rev = self._state[1]
815 ctx = repo[rev]
813 ctx = repo[rev]
816
814
817 matchers = [ctx.matchfileset(expr, badfn=badfn)]
815 matchers = [ctx.matchfileset(expr, badfn=badfn)]
818
816
819 for subpath in ctx.substate:
817 for subpath in ctx.substate:
820 sub = ctx.sub(subpath)
818 sub = ctx.sub(subpath)
821
819
822 try:
820 try:
823 sm = sub.matchfileset(expr, badfn=badfn)
821 sm = sub.matchfileset(expr, badfn=badfn)
824 pm = matchmod.prefixdirmatcher(repo.root, repo.getcwd(),
822 pm = matchmod.prefixdirmatcher(repo.root, repo.getcwd(),
825 subpath, sm, badfn=badfn)
823 subpath, sm, badfn=badfn)
826 matchers.append(pm)
824 matchers.append(pm)
827 except error.LookupError:
825 except error.LookupError:
828 self.ui.status(_("skipping missing subrepository: %s\n")
826 self.ui.status(_("skipping missing subrepository: %s\n")
829 % self.wvfs.reljoin(reporelpath(self), subpath))
827 % self.wvfs.reljoin(reporelpath(self), subpath))
830 if len(matchers) == 1:
828 if len(matchers) == 1:
831 return matchers[0]
829 return matchers[0]
832 return matchmod.unionmatcher(matchers)
830 return matchmod.unionmatcher(matchers)
833
831
834 def walk(self, match):
832 def walk(self, match):
835 ctx = self._repo[None]
833 ctx = self._repo[None]
836 return ctx.walk(match)
834 return ctx.walk(match)
837
835
838 @annotatesubrepoerror
836 @annotatesubrepoerror
839 def forget(self, match, prefix, dryrun, interactive):
837 def forget(self, match, prefix, dryrun, interactive):
840 return cmdutil.forget(self.ui, self._repo, match, prefix,
838 return cmdutil.forget(self.ui, self._repo, match, prefix,
841 True, dryrun=dryrun, interactive=interactive)
839 True, dryrun=dryrun, interactive=interactive)
842
840
843 @annotatesubrepoerror
841 @annotatesubrepoerror
844 def removefiles(self, matcher, prefix, after, force, subrepos,
842 def removefiles(self, matcher, prefix, after, force, subrepos,
845 dryrun, warnings):
843 dryrun, warnings):
846 return cmdutil.remove(self.ui, self._repo, matcher, prefix,
844 return cmdutil.remove(self.ui, self._repo, matcher, prefix,
847 after, force, subrepos, dryrun)
845 after, force, subrepos, dryrun)
848
846
849 @annotatesubrepoerror
847 @annotatesubrepoerror
850 def revert(self, substate, *pats, **opts):
848 def revert(self, substate, *pats, **opts):
851 # reverting a subrepo is a 2 step process:
849 # reverting a subrepo is a 2 step process:
852 # 1. if the no_backup is not set, revert all modified
850 # 1. if the no_backup is not set, revert all modified
853 # files inside the subrepo
851 # files inside the subrepo
854 # 2. update the subrepo to the revision specified in
852 # 2. update the subrepo to the revision specified in
855 # the corresponding substate dictionary
853 # the corresponding substate dictionary
856 self.ui.status(_('reverting subrepo %s\n') % substate[0])
854 self.ui.status(_('reverting subrepo %s\n') % substate[0])
857 if not opts.get(r'no_backup'):
855 if not opts.get(r'no_backup'):
858 # Revert all files on the subrepo, creating backups
856 # Revert all files on the subrepo, creating backups
859 # Note that this will not recursively revert subrepos
857 # Note that this will not recursively revert subrepos
860 # We could do it if there was a set:subrepos() predicate
858 # We could do it if there was a set:subrepos() predicate
861 opts = opts.copy()
859 opts = opts.copy()
862 opts[r'date'] = None
860 opts[r'date'] = None
863 opts[r'rev'] = substate[1]
861 opts[r'rev'] = substate[1]
864
862
865 self.filerevert(*pats, **opts)
863 self.filerevert(*pats, **opts)
866
864
867 # Update the repo to the revision specified in the given substate
865 # Update the repo to the revision specified in the given substate
868 if not opts.get(r'dry_run'):
866 if not opts.get(r'dry_run'):
869 self.get(substate, overwrite=True)
867 self.get(substate, overwrite=True)
870
868
871 def filerevert(self, *pats, **opts):
869 def filerevert(self, *pats, **opts):
872 ctx = self._repo[opts[r'rev']]
870 ctx = self._repo[opts[r'rev']]
873 parents = self._repo.dirstate.parents()
871 parents = self._repo.dirstate.parents()
874 if opts.get(r'all'):
872 if opts.get(r'all'):
875 pats = ['set:modified()']
873 pats = ['set:modified()']
876 else:
874 else:
877 pats = []
875 pats = []
878 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
876 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
879
877
880 def shortid(self, revid):
878 def shortid(self, revid):
881 return revid[:12]
879 return revid[:12]
882
880
883 @annotatesubrepoerror
881 @annotatesubrepoerror
884 def unshare(self):
882 def unshare(self):
885 # subrepo inherently violates our import layering rules
883 # subrepo inherently violates our import layering rules
886 # because it wants to make repo objects from deep inside the stack
884 # because it wants to make repo objects from deep inside the stack
887 # so we manually delay the circular imports to not break
885 # so we manually delay the circular imports to not break
888 # scripts that don't use our demand-loading
886 # scripts that don't use our demand-loading
889 global hg
887 global hg
890 from . import hg as h
888 from . import hg as h
891 hg = h
889 hg = h
892
890
893 # Nothing prevents a user from sharing in a repo, and then making that a
891 # Nothing prevents a user from sharing in a repo, and then making that a
894 # subrepo. Alternately, the previous unshare attempt may have failed
892 # subrepo. Alternately, the previous unshare attempt may have failed
895 # part way through. So recurse whether or not this layer is shared.
893 # part way through. So recurse whether or not this layer is shared.
896 if self._repo.shared():
894 if self._repo.shared():
897 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
895 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
898
896
899 hg.unshare(self.ui, self._repo)
897 hg.unshare(self.ui, self._repo)
900
898
901 def verify(self):
899 def verify(self):
902 try:
900 try:
903 rev = self._state[1]
901 rev = self._state[1]
904 ctx = self._repo.unfiltered()[rev]
902 ctx = self._repo.unfiltered()[rev]
905 if ctx.hidden():
903 if ctx.hidden():
906 # Since hidden revisions aren't pushed/pulled, it seems worth an
904 # Since hidden revisions aren't pushed/pulled, it seems worth an
907 # explicit warning.
905 # explicit warning.
908 ui = self._repo.ui
906 ui = self._repo.ui
909 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
907 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
910 (self._relpath, node.short(self._ctx.node())))
908 (self._relpath, node.short(self._ctx.node())))
911 return 0
909 return 0
912 except error.RepoLookupError:
910 except error.RepoLookupError:
913 # A missing subrepo revision may be a case of needing to pull it, so
911 # A missing subrepo revision may be a case of needing to pull it, so
914 # don't treat this as an error.
912 # don't treat this as an error.
915 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
913 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
916 (self._relpath, node.short(self._ctx.node())))
914 (self._relpath, node.short(self._ctx.node())))
917 return 0
915 return 0
918
916
919 @propertycache
917 @propertycache
920 def wvfs(self):
918 def wvfs(self):
921 """return own wvfs for efficiency and consistency
919 """return own wvfs for efficiency and consistency
922 """
920 """
923 return self._repo.wvfs
921 return self._repo.wvfs
924
922
925 @propertycache
923 @propertycache
926 def _relpath(self):
924 def _relpath(self):
927 """return path to this subrepository as seen from outermost repository
925 """return path to this subrepository as seen from outermost repository
928 """
926 """
929 # Keep consistent dir separators by avoiding vfs.join(self._path)
927 # Keep consistent dir separators by avoiding vfs.join(self._path)
930 return reporelpath(self._repo)
928 return reporelpath(self._repo)
931
929
932 class svnsubrepo(abstractsubrepo):
930 class svnsubrepo(abstractsubrepo):
933 def __init__(self, ctx, path, state, allowcreate):
931 def __init__(self, ctx, path, state, allowcreate):
934 super(svnsubrepo, self).__init__(ctx, path)
932 super(svnsubrepo, self).__init__(ctx, path)
935 self._state = state
933 self._state = state
936 self._exe = procutil.findexe('svn')
934 self._exe = procutil.findexe('svn')
937 if not self._exe:
935 if not self._exe:
938 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
936 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
939 % self._path)
937 % self._path)
940
938
941 def _svncommand(self, commands, filename='', failok=False):
939 def _svncommand(self, commands, filename='', failok=False):
942 cmd = [self._exe]
940 cmd = [self._exe]
943 extrakw = {}
941 extrakw = {}
944 if not self.ui.interactive():
942 if not self.ui.interactive():
945 # Making stdin be a pipe should prevent svn from behaving
943 # Making stdin be a pipe should prevent svn from behaving
946 # interactively even if we can't pass --non-interactive.
944 # interactively even if we can't pass --non-interactive.
947 extrakw[r'stdin'] = subprocess.PIPE
945 extrakw[r'stdin'] = subprocess.PIPE
948 # Starting in svn 1.5 --non-interactive is a global flag
946 # Starting in svn 1.5 --non-interactive is a global flag
949 # instead of being per-command, but we need to support 1.4 so
947 # instead of being per-command, but we need to support 1.4 so
950 # we have to be intelligent about what commands take
948 # we have to be intelligent about what commands take
951 # --non-interactive.
949 # --non-interactive.
952 if commands[0] in ('update', 'checkout', 'commit'):
950 if commands[0] in ('update', 'checkout', 'commit'):
953 cmd.append('--non-interactive')
951 cmd.append('--non-interactive')
954 cmd.extend(commands)
952 cmd.extend(commands)
955 if filename is not None:
953 if filename is not None:
956 path = self.wvfs.reljoin(self._ctx.repo().origroot,
954 path = self.wvfs.reljoin(self._ctx.repo().origroot,
957 self._path, filename)
955 self._path, filename)
958 cmd.append(path)
956 cmd.append(path)
959 env = dict(encoding.environ)
957 env = dict(encoding.environ)
960 # Avoid localized output, preserve current locale for everything else.
958 # Avoid localized output, preserve current locale for everything else.
961 lc_all = env.get('LC_ALL')
959 lc_all = env.get('LC_ALL')
962 if lc_all:
960 if lc_all:
963 env['LANG'] = lc_all
961 env['LANG'] = lc_all
964 del env['LC_ALL']
962 del env['LC_ALL']
965 env['LC_MESSAGES'] = 'C'
963 env['LC_MESSAGES'] = 'C'
966 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr, cmd),
964 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr, cmd),
967 bufsize=-1, close_fds=procutil.closefds,
965 bufsize=-1, close_fds=procutil.closefds,
968 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
966 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
969 env=procutil.tonativeenv(env), **extrakw)
967 env=procutil.tonativeenv(env), **extrakw)
970 stdout, stderr = map(util.fromnativeeol, p.communicate())
968 stdout, stderr = map(util.fromnativeeol, p.communicate())
971 stderr = stderr.strip()
969 stderr = stderr.strip()
972 if not failok:
970 if not failok:
973 if p.returncode:
971 if p.returncode:
974 raise error.Abort(stderr or 'exited with code %d'
972 raise error.Abort(stderr or 'exited with code %d'
975 % p.returncode)
973 % p.returncode)
976 if stderr:
974 if stderr:
977 self.ui.warn(stderr + '\n')
975 self.ui.warn(stderr + '\n')
978 return stdout, stderr
976 return stdout, stderr
979
977
980 @propertycache
978 @propertycache
981 def _svnversion(self):
979 def _svnversion(self):
982 output, err = self._svncommand(['--version', '--quiet'], filename=None)
980 output, err = self._svncommand(['--version', '--quiet'], filename=None)
983 m = re.search(br'^(\d+)\.(\d+)', output)
981 m = re.search(br'^(\d+)\.(\d+)', output)
984 if not m:
982 if not m:
985 raise error.Abort(_('cannot retrieve svn tool version'))
983 raise error.Abort(_('cannot retrieve svn tool version'))
986 return (int(m.group(1)), int(m.group(2)))
984 return (int(m.group(1)), int(m.group(2)))
987
985
988 def _svnmissing(self):
986 def _svnmissing(self):
989 return not self.wvfs.exists('.svn')
987 return not self.wvfs.exists('.svn')
990
988
991 def _wcrevs(self):
989 def _wcrevs(self):
992 # Get the working directory revision as well as the last
990 # Get the working directory revision as well as the last
993 # commit revision so we can compare the subrepo state with
991 # commit revision so we can compare the subrepo state with
994 # both. We used to store the working directory one.
992 # both. We used to store the working directory one.
995 output, err = self._svncommand(['info', '--xml'])
993 output, err = self._svncommand(['info', '--xml'])
996 doc = xml.dom.minidom.parseString(output)
994 doc = xml.dom.minidom.parseString(output)
997 entries = doc.getElementsByTagName(r'entry')
995 entries = doc.getElementsByTagName(r'entry')
998 lastrev, rev = '0', '0'
996 lastrev, rev = '0', '0'
999 if entries:
997 if entries:
1000 rev = pycompat.bytestr(entries[0].getAttribute(r'revision')) or '0'
998 rev = pycompat.bytestr(entries[0].getAttribute(r'revision')) or '0'
1001 commits = entries[0].getElementsByTagName(r'commit')
999 commits = entries[0].getElementsByTagName(r'commit')
1002 if commits:
1000 if commits:
1003 lastrev = pycompat.bytestr(
1001 lastrev = pycompat.bytestr(
1004 commits[0].getAttribute(r'revision')) or '0'
1002 commits[0].getAttribute(r'revision')) or '0'
1005 return (lastrev, rev)
1003 return (lastrev, rev)
1006
1004
1007 def _wcrev(self):
1005 def _wcrev(self):
1008 return self._wcrevs()[0]
1006 return self._wcrevs()[0]
1009
1007
1010 def _wcchanged(self):
1008 def _wcchanged(self):
1011 """Return (changes, extchanges, missing) where changes is True
1009 """Return (changes, extchanges, missing) where changes is True
1012 if the working directory was changed, extchanges is
1010 if the working directory was changed, extchanges is
1013 True if any of these changes concern an external entry and missing
1011 True if any of these changes concern an external entry and missing
1014 is True if any change is a missing entry.
1012 is True if any change is a missing entry.
1015 """
1013 """
1016 output, err = self._svncommand(['status', '--xml'])
1014 output, err = self._svncommand(['status', '--xml'])
1017 externals, changes, missing = [], [], []
1015 externals, changes, missing = [], [], []
1018 doc = xml.dom.minidom.parseString(output)
1016 doc = xml.dom.minidom.parseString(output)
1019 for e in doc.getElementsByTagName(r'entry'):
1017 for e in doc.getElementsByTagName(r'entry'):
1020 s = e.getElementsByTagName(r'wc-status')
1018 s = e.getElementsByTagName(r'wc-status')
1021 if not s:
1019 if not s:
1022 continue
1020 continue
1023 item = s[0].getAttribute(r'item')
1021 item = s[0].getAttribute(r'item')
1024 props = s[0].getAttribute(r'props')
1022 props = s[0].getAttribute(r'props')
1025 path = e.getAttribute(r'path').encode('utf8')
1023 path = e.getAttribute(r'path').encode('utf8')
1026 if item == r'external':
1024 if item == r'external':
1027 externals.append(path)
1025 externals.append(path)
1028 elif item == r'missing':
1026 elif item == r'missing':
1029 missing.append(path)
1027 missing.append(path)
1030 if (item not in (r'', r'normal', r'unversioned', r'external')
1028 if (item not in (r'', r'normal', r'unversioned', r'external')
1031 or props not in (r'', r'none', r'normal')):
1029 or props not in (r'', r'none', r'normal')):
1032 changes.append(path)
1030 changes.append(path)
1033 for path in changes:
1031 for path in changes:
1034 for ext in externals:
1032 for ext in externals:
1035 if path == ext or path.startswith(ext + pycompat.ossep):
1033 if path == ext or path.startswith(ext + pycompat.ossep):
1036 return True, True, bool(missing)
1034 return True, True, bool(missing)
1037 return bool(changes), False, bool(missing)
1035 return bool(changes), False, bool(missing)
1038
1036
1039 @annotatesubrepoerror
1037 @annotatesubrepoerror
1040 def dirty(self, ignoreupdate=False, missing=False):
1038 def dirty(self, ignoreupdate=False, missing=False):
1041 if self._svnmissing():
1039 if self._svnmissing():
1042 return self._state[1] != ''
1040 return self._state[1] != ''
1043 wcchanged = self._wcchanged()
1041 wcchanged = self._wcchanged()
1044 changed = wcchanged[0] or (missing and wcchanged[2])
1042 changed = wcchanged[0] or (missing and wcchanged[2])
1045 if not changed:
1043 if not changed:
1046 if self._state[1] in self._wcrevs() or ignoreupdate:
1044 if self._state[1] in self._wcrevs() or ignoreupdate:
1047 return False
1045 return False
1048 return True
1046 return True
1049
1047
1050 def basestate(self):
1048 def basestate(self):
1051 lastrev, rev = self._wcrevs()
1049 lastrev, rev = self._wcrevs()
1052 if lastrev != rev:
1050 if lastrev != rev:
1053 # Last committed rev is not the same than rev. We would
1051 # Last committed rev is not the same than rev. We would
1054 # like to take lastrev but we do not know if the subrepo
1052 # like to take lastrev but we do not know if the subrepo
1055 # URL exists at lastrev. Test it and fallback to rev it
1053 # URL exists at lastrev. Test it and fallback to rev it
1056 # is not there.
1054 # is not there.
1057 try:
1055 try:
1058 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1056 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1059 return lastrev
1057 return lastrev
1060 except error.Abort:
1058 except error.Abort:
1061 pass
1059 pass
1062 return rev
1060 return rev
1063
1061
1064 @annotatesubrepoerror
1062 @annotatesubrepoerror
1065 def commit(self, text, user, date):
1063 def commit(self, text, user, date):
1066 # user and date are out of our hands since svn is centralized
1064 # user and date are out of our hands since svn is centralized
1067 changed, extchanged, missing = self._wcchanged()
1065 changed, extchanged, missing = self._wcchanged()
1068 if not changed:
1066 if not changed:
1069 return self.basestate()
1067 return self.basestate()
1070 if extchanged:
1068 if extchanged:
1071 # Do not try to commit externals
1069 # Do not try to commit externals
1072 raise error.Abort(_('cannot commit svn externals'))
1070 raise error.Abort(_('cannot commit svn externals'))
1073 if missing:
1071 if missing:
1074 # svn can commit with missing entries but aborting like hg
1072 # svn can commit with missing entries but aborting like hg
1075 # seems a better approach.
1073 # seems a better approach.
1076 raise error.Abort(_('cannot commit missing svn entries'))
1074 raise error.Abort(_('cannot commit missing svn entries'))
1077 commitinfo, err = self._svncommand(['commit', '-m', text])
1075 commitinfo, err = self._svncommand(['commit', '-m', text])
1078 self.ui.status(commitinfo)
1076 self.ui.status(commitinfo)
1079 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1077 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1080 if not newrev:
1078 if not newrev:
1081 if not commitinfo.strip():
1079 if not commitinfo.strip():
1082 # Sometimes, our definition of "changed" differs from
1080 # Sometimes, our definition of "changed" differs from
1083 # svn one. For instance, svn ignores missing files
1081 # svn one. For instance, svn ignores missing files
1084 # when committing. If there are only missing files, no
1082 # when committing. If there are only missing files, no
1085 # commit is made, no output and no error code.
1083 # commit is made, no output and no error code.
1086 raise error.Abort(_('failed to commit svn changes'))
1084 raise error.Abort(_('failed to commit svn changes'))
1087 raise error.Abort(commitinfo.splitlines()[-1])
1085 raise error.Abort(commitinfo.splitlines()[-1])
1088 newrev = newrev.groups()[0]
1086 newrev = newrev.groups()[0]
1089 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1087 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1090 return newrev
1088 return newrev
1091
1089
1092 @annotatesubrepoerror
1090 @annotatesubrepoerror
1093 def remove(self):
1091 def remove(self):
1094 if self.dirty():
1092 if self.dirty():
1095 self.ui.warn(_('not removing repo %s because '
1093 self.ui.warn(_('not removing repo %s because '
1096 'it has changes.\n') % self._path)
1094 'it has changes.\n') % self._path)
1097 return
1095 return
1098 self.ui.note(_('removing subrepo %s\n') % self._path)
1096 self.ui.note(_('removing subrepo %s\n') % self._path)
1099
1097
1100 self.wvfs.rmtree(forcibly=True)
1098 self.wvfs.rmtree(forcibly=True)
1101 try:
1099 try:
1102 pwvfs = self._ctx.repo().wvfs
1100 pwvfs = self._ctx.repo().wvfs
1103 pwvfs.removedirs(pwvfs.dirname(self._path))
1101 pwvfs.removedirs(pwvfs.dirname(self._path))
1104 except OSError:
1102 except OSError:
1105 pass
1103 pass
1106
1104
1107 @annotatesubrepoerror
1105 @annotatesubrepoerror
1108 def get(self, state, overwrite=False):
1106 def get(self, state, overwrite=False):
1109 if overwrite:
1107 if overwrite:
1110 self._svncommand(['revert', '--recursive'])
1108 self._svncommand(['revert', '--recursive'])
1111 args = ['checkout']
1109 args = ['checkout']
1112 if self._svnversion >= (1, 5):
1110 if self._svnversion >= (1, 5):
1113 args.append('--force')
1111 args.append('--force')
1114 # The revision must be specified at the end of the URL to properly
1112 # The revision must be specified at the end of the URL to properly
1115 # update to a directory which has since been deleted and recreated.
1113 # update to a directory which has since been deleted and recreated.
1116 args.append('%s@%s' % (state[0], state[1]))
1114 args.append('%s@%s' % (state[0], state[1]))
1117
1115
1118 # SEC: check that the ssh url is safe
1116 # SEC: check that the ssh url is safe
1119 util.checksafessh(state[0])
1117 util.checksafessh(state[0])
1120
1118
1121 status, err = self._svncommand(args, failok=True)
1119 status, err = self._svncommand(args, failok=True)
1122 _sanitize(self.ui, self.wvfs, '.svn')
1120 _sanitize(self.ui, self.wvfs, '.svn')
1123 if not re.search('Checked out revision [0-9]+.', status):
1121 if not re.search('Checked out revision [0-9]+.', status):
1124 if ('is already a working copy for a different URL' in err
1122 if ('is already a working copy for a different URL' in err
1125 and (self._wcchanged()[:2] == (False, False))):
1123 and (self._wcchanged()[:2] == (False, False))):
1126 # obstructed but clean working copy, so just blow it away.
1124 # obstructed but clean working copy, so just blow it away.
1127 self.remove()
1125 self.remove()
1128 self.get(state, overwrite=False)
1126 self.get(state, overwrite=False)
1129 return
1127 return
1130 raise error.Abort((status or err).splitlines()[-1])
1128 raise error.Abort((status or err).splitlines()[-1])
1131 self.ui.status(status)
1129 self.ui.status(status)
1132
1130
1133 @annotatesubrepoerror
1131 @annotatesubrepoerror
1134 def merge(self, state):
1132 def merge(self, state):
1135 old = self._state[1]
1133 old = self._state[1]
1136 new = state[1]
1134 new = state[1]
1137 wcrev = self._wcrev()
1135 wcrev = self._wcrev()
1138 if new != wcrev:
1136 if new != wcrev:
1139 dirty = old == wcrev or self._wcchanged()[0]
1137 dirty = old == wcrev or self._wcchanged()[0]
1140 if _updateprompt(self.ui, self, dirty, wcrev, new):
1138 if _updateprompt(self.ui, self, dirty, wcrev, new):
1141 self.get(state, False)
1139 self.get(state, False)
1142
1140
1143 def push(self, opts):
1141 def push(self, opts):
1144 # push is a no-op for SVN
1142 # push is a no-op for SVN
1145 return True
1143 return True
1146
1144
1147 @annotatesubrepoerror
1145 @annotatesubrepoerror
1148 def files(self):
1146 def files(self):
1149 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1147 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1150 doc = xml.dom.minidom.parseString(output)
1148 doc = xml.dom.minidom.parseString(output)
1151 paths = []
1149 paths = []
1152 for e in doc.getElementsByTagName(r'entry'):
1150 for e in doc.getElementsByTagName(r'entry'):
1153 kind = pycompat.bytestr(e.getAttribute(r'kind'))
1151 kind = pycompat.bytestr(e.getAttribute(r'kind'))
1154 if kind != 'file':
1152 if kind != 'file':
1155 continue
1153 continue
1156 name = r''.join(c.data for c
1154 name = r''.join(c.data for c
1157 in e.getElementsByTagName(r'name')[0].childNodes
1155 in e.getElementsByTagName(r'name')[0].childNodes
1158 if c.nodeType == c.TEXT_NODE)
1156 if c.nodeType == c.TEXT_NODE)
1159 paths.append(name.encode('utf8'))
1157 paths.append(name.encode('utf8'))
1160 return paths
1158 return paths
1161
1159
1162 def filedata(self, name, decode):
1160 def filedata(self, name, decode):
1163 return self._svncommand(['cat'], name)[0]
1161 return self._svncommand(['cat'], name)[0]
1164
1162
1165
1163
1166 class gitsubrepo(abstractsubrepo):
1164 class gitsubrepo(abstractsubrepo):
1167 def __init__(self, ctx, path, state, allowcreate):
1165 def __init__(self, ctx, path, state, allowcreate):
1168 super(gitsubrepo, self).__init__(ctx, path)
1166 super(gitsubrepo, self).__init__(ctx, path)
1169 self._state = state
1167 self._state = state
1170 self._abspath = ctx.repo().wjoin(path)
1168 self._abspath = ctx.repo().wjoin(path)
1171 self._subparent = ctx.repo()
1169 self._subparent = ctx.repo()
1172 self._ensuregit()
1170 self._ensuregit()
1173
1171
1174 def _ensuregit(self):
1172 def _ensuregit(self):
1175 try:
1173 try:
1176 self._gitexecutable = 'git'
1174 self._gitexecutable = 'git'
1177 out, err = self._gitnodir(['--version'])
1175 out, err = self._gitnodir(['--version'])
1178 except OSError as e:
1176 except OSError as e:
1179 genericerror = _("error executing git for subrepo '%s': %s")
1177 genericerror = _("error executing git for subrepo '%s': %s")
1180 notfoundhint = _("check git is installed and in your PATH")
1178 notfoundhint = _("check git is installed and in your PATH")
1181 if e.errno != errno.ENOENT:
1179 if e.errno != errno.ENOENT:
1182 raise error.Abort(genericerror % (
1180 raise error.Abort(genericerror % (
1183 self._path, encoding.strtolocal(e.strerror)))
1181 self._path, encoding.strtolocal(e.strerror)))
1184 elif pycompat.iswindows:
1182 elif pycompat.iswindows:
1185 try:
1183 try:
1186 self._gitexecutable = 'git.cmd'
1184 self._gitexecutable = 'git.cmd'
1187 out, err = self._gitnodir(['--version'])
1185 out, err = self._gitnodir(['--version'])
1188 except OSError as e2:
1186 except OSError as e2:
1189 if e2.errno == errno.ENOENT:
1187 if e2.errno == errno.ENOENT:
1190 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1188 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1191 " for subrepo '%s'") % self._path,
1189 " for subrepo '%s'") % self._path,
1192 hint=notfoundhint)
1190 hint=notfoundhint)
1193 else:
1191 else:
1194 raise error.Abort(genericerror % (self._path,
1192 raise error.Abort(genericerror % (self._path,
1195 encoding.strtolocal(e2.strerror)))
1193 encoding.strtolocal(e2.strerror)))
1196 else:
1194 else:
1197 raise error.Abort(_("couldn't find git for subrepo '%s'")
1195 raise error.Abort(_("couldn't find git for subrepo '%s'")
1198 % self._path, hint=notfoundhint)
1196 % self._path, hint=notfoundhint)
1199 versionstatus = self._checkversion(out)
1197 versionstatus = self._checkversion(out)
1200 if versionstatus == 'unknown':
1198 if versionstatus == 'unknown':
1201 self.ui.warn(_('cannot retrieve git version\n'))
1199 self.ui.warn(_('cannot retrieve git version\n'))
1202 elif versionstatus == 'abort':
1200 elif versionstatus == 'abort':
1203 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1201 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1204 elif versionstatus == 'warning':
1202 elif versionstatus == 'warning':
1205 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1203 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1206
1204
1207 @staticmethod
1205 @staticmethod
1208 def _gitversion(out):
1206 def _gitversion(out):
1209 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1207 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1210 if m:
1208 if m:
1211 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1209 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1212
1210
1213 m = re.search(br'^git version (\d+)\.(\d+)', out)
1211 m = re.search(br'^git version (\d+)\.(\d+)', out)
1214 if m:
1212 if m:
1215 return (int(m.group(1)), int(m.group(2)), 0)
1213 return (int(m.group(1)), int(m.group(2)), 0)
1216
1214
1217 return -1
1215 return -1
1218
1216
1219 @staticmethod
1217 @staticmethod
1220 def _checkversion(out):
1218 def _checkversion(out):
1221 '''ensure git version is new enough
1219 '''ensure git version is new enough
1222
1220
1223 >>> _checkversion = gitsubrepo._checkversion
1221 >>> _checkversion = gitsubrepo._checkversion
1224 >>> _checkversion(b'git version 1.6.0')
1222 >>> _checkversion(b'git version 1.6.0')
1225 'ok'
1223 'ok'
1226 >>> _checkversion(b'git version 1.8.5')
1224 >>> _checkversion(b'git version 1.8.5')
1227 'ok'
1225 'ok'
1228 >>> _checkversion(b'git version 1.4.0')
1226 >>> _checkversion(b'git version 1.4.0')
1229 'abort'
1227 'abort'
1230 >>> _checkversion(b'git version 1.5.0')
1228 >>> _checkversion(b'git version 1.5.0')
1231 'warning'
1229 'warning'
1232 >>> _checkversion(b'git version 1.9-rc0')
1230 >>> _checkversion(b'git version 1.9-rc0')
1233 'ok'
1231 'ok'
1234 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1232 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1235 'ok'
1233 'ok'
1236 >>> _checkversion(b'git version 1.9.0.GIT')
1234 >>> _checkversion(b'git version 1.9.0.GIT')
1237 'ok'
1235 'ok'
1238 >>> _checkversion(b'git version 12345')
1236 >>> _checkversion(b'git version 12345')
1239 'unknown'
1237 'unknown'
1240 >>> _checkversion(b'no')
1238 >>> _checkversion(b'no')
1241 'unknown'
1239 'unknown'
1242 '''
1240 '''
1243 version = gitsubrepo._gitversion(out)
1241 version = gitsubrepo._gitversion(out)
1244 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1242 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1245 # despite the docstring comment. For now, error on 1.4.0, warn on
1243 # despite the docstring comment. For now, error on 1.4.0, warn on
1246 # 1.5.0 but attempt to continue.
1244 # 1.5.0 but attempt to continue.
1247 if version == -1:
1245 if version == -1:
1248 return 'unknown'
1246 return 'unknown'
1249 if version < (1, 5, 0):
1247 if version < (1, 5, 0):
1250 return 'abort'
1248 return 'abort'
1251 elif version < (1, 6, 0):
1249 elif version < (1, 6, 0):
1252 return 'warning'
1250 return 'warning'
1253 return 'ok'
1251 return 'ok'
1254
1252
1255 def _gitcommand(self, commands, env=None, stream=False):
1253 def _gitcommand(self, commands, env=None, stream=False):
1256 return self._gitdir(commands, env=env, stream=stream)[0]
1254 return self._gitdir(commands, env=env, stream=stream)[0]
1257
1255
1258 def _gitdir(self, commands, env=None, stream=False):
1256 def _gitdir(self, commands, env=None, stream=False):
1259 return self._gitnodir(commands, env=env, stream=stream,
1257 return self._gitnodir(commands, env=env, stream=stream,
1260 cwd=self._abspath)
1258 cwd=self._abspath)
1261
1259
1262 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1260 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1263 """Calls the git command
1261 """Calls the git command
1264
1262
1265 The methods tries to call the git command. versions prior to 1.6.0
1263 The methods tries to call the git command. versions prior to 1.6.0
1266 are not supported and very probably fail.
1264 are not supported and very probably fail.
1267 """
1265 """
1268 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1266 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1269 if env is None:
1267 if env is None:
1270 env = encoding.environ.copy()
1268 env = encoding.environ.copy()
1271 # disable localization for Git output (issue5176)
1269 # disable localization for Git output (issue5176)
1272 env['LC_ALL'] = 'C'
1270 env['LC_ALL'] = 'C'
1273 # fix for Git CVE-2015-7545
1271 # fix for Git CVE-2015-7545
1274 if 'GIT_ALLOW_PROTOCOL' not in env:
1272 if 'GIT_ALLOW_PROTOCOL' not in env:
1275 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1273 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1276 # unless ui.quiet is set, print git's stderr,
1274 # unless ui.quiet is set, print git's stderr,
1277 # which is mostly progress and useful info
1275 # which is mostly progress and useful info
1278 errpipe = None
1276 errpipe = None
1279 if self.ui.quiet:
1277 if self.ui.quiet:
1280 errpipe = open(os.devnull, 'w')
1278 errpipe = open(os.devnull, 'w')
1281 if self.ui._colormode and len(commands) and commands[0] == "diff":
1279 if self.ui._colormode and len(commands) and commands[0] == "diff":
1282 # insert the argument in the front,
1280 # insert the argument in the front,
1283 # the end of git diff arguments is used for paths
1281 # the end of git diff arguments is used for paths
1284 commands.insert(1, '--color')
1282 commands.insert(1, '--color')
1285 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr,
1283 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr,
1286 [self._gitexecutable] + commands),
1284 [self._gitexecutable] + commands),
1287 bufsize=-1,
1285 bufsize=-1,
1288 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1286 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1289 env=procutil.tonativeenv(env),
1287 env=procutil.tonativeenv(env),
1290 close_fds=procutil.closefds,
1288 close_fds=procutil.closefds,
1291 stdout=subprocess.PIPE, stderr=errpipe)
1289 stdout=subprocess.PIPE, stderr=errpipe)
1292 if stream:
1290 if stream:
1293 return p.stdout, None
1291 return p.stdout, None
1294
1292
1295 retdata = p.stdout.read().strip()
1293 retdata = p.stdout.read().strip()
1296 # wait for the child to exit to avoid race condition.
1294 # wait for the child to exit to avoid race condition.
1297 p.wait()
1295 p.wait()
1298
1296
1299 if p.returncode != 0 and p.returncode != 1:
1297 if p.returncode != 0 and p.returncode != 1:
1300 # there are certain error codes that are ok
1298 # there are certain error codes that are ok
1301 command = commands[0]
1299 command = commands[0]
1302 if command in ('cat-file', 'symbolic-ref'):
1300 if command in ('cat-file', 'symbolic-ref'):
1303 return retdata, p.returncode
1301 return retdata, p.returncode
1304 # for all others, abort
1302 # for all others, abort
1305 raise error.Abort(_('git %s error %d in %s') %
1303 raise error.Abort(_('git %s error %d in %s') %
1306 (command, p.returncode, self._relpath))
1304 (command, p.returncode, self._relpath))
1307
1305
1308 return retdata, p.returncode
1306 return retdata, p.returncode
1309
1307
1310 def _gitmissing(self):
1308 def _gitmissing(self):
1311 return not self.wvfs.exists('.git')
1309 return not self.wvfs.exists('.git')
1312
1310
1313 def _gitstate(self):
1311 def _gitstate(self):
1314 return self._gitcommand(['rev-parse', 'HEAD'])
1312 return self._gitcommand(['rev-parse', 'HEAD'])
1315
1313
1316 def _gitcurrentbranch(self):
1314 def _gitcurrentbranch(self):
1317 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1315 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1318 if err:
1316 if err:
1319 current = None
1317 current = None
1320 return current
1318 return current
1321
1319
1322 def _gitremote(self, remote):
1320 def _gitremote(self, remote):
1323 out = self._gitcommand(['remote', 'show', '-n', remote])
1321 out = self._gitcommand(['remote', 'show', '-n', remote])
1324 line = out.split('\n')[1]
1322 line = out.split('\n')[1]
1325 i = line.index('URL: ') + len('URL: ')
1323 i = line.index('URL: ') + len('URL: ')
1326 return line[i:]
1324 return line[i:]
1327
1325
1328 def _githavelocally(self, revision):
1326 def _githavelocally(self, revision):
1329 out, code = self._gitdir(['cat-file', '-e', revision])
1327 out, code = self._gitdir(['cat-file', '-e', revision])
1330 return code == 0
1328 return code == 0
1331
1329
1332 def _gitisancestor(self, r1, r2):
1330 def _gitisancestor(self, r1, r2):
1333 base = self._gitcommand(['merge-base', r1, r2])
1331 base = self._gitcommand(['merge-base', r1, r2])
1334 return base == r1
1332 return base == r1
1335
1333
1336 def _gitisbare(self):
1334 def _gitisbare(self):
1337 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1335 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1338
1336
1339 def _gitupdatestat(self):
1337 def _gitupdatestat(self):
1340 """This must be run before git diff-index.
1338 """This must be run before git diff-index.
1341 diff-index only looks at changes to file stat;
1339 diff-index only looks at changes to file stat;
1342 this command looks at file contents and updates the stat."""
1340 this command looks at file contents and updates the stat."""
1343 self._gitcommand(['update-index', '-q', '--refresh'])
1341 self._gitcommand(['update-index', '-q', '--refresh'])
1344
1342
1345 def _gitbranchmap(self):
1343 def _gitbranchmap(self):
1346 '''returns 2 things:
1344 '''returns 2 things:
1347 a map from git branch to revision
1345 a map from git branch to revision
1348 a map from revision to branches'''
1346 a map from revision to branches'''
1349 branch2rev = {}
1347 branch2rev = {}
1350 rev2branch = {}
1348 rev2branch = {}
1351
1349
1352 out = self._gitcommand(['for-each-ref', '--format',
1350 out = self._gitcommand(['for-each-ref', '--format',
1353 '%(objectname) %(refname)'])
1351 '%(objectname) %(refname)'])
1354 for line in out.split('\n'):
1352 for line in out.split('\n'):
1355 revision, ref = line.split(' ')
1353 revision, ref = line.split(' ')
1356 if (not ref.startswith('refs/heads/') and
1354 if (not ref.startswith('refs/heads/') and
1357 not ref.startswith('refs/remotes/')):
1355 not ref.startswith('refs/remotes/')):
1358 continue
1356 continue
1359 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1357 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1360 continue # ignore remote/HEAD redirects
1358 continue # ignore remote/HEAD redirects
1361 branch2rev[ref] = revision
1359 branch2rev[ref] = revision
1362 rev2branch.setdefault(revision, []).append(ref)
1360 rev2branch.setdefault(revision, []).append(ref)
1363 return branch2rev, rev2branch
1361 return branch2rev, rev2branch
1364
1362
1365 def _gittracking(self, branches):
1363 def _gittracking(self, branches):
1366 'return map of remote branch to local tracking branch'
1364 'return map of remote branch to local tracking branch'
1367 # assumes no more than one local tracking branch for each remote
1365 # assumes no more than one local tracking branch for each remote
1368 tracking = {}
1366 tracking = {}
1369 for b in branches:
1367 for b in branches:
1370 if b.startswith('refs/remotes/'):
1368 if b.startswith('refs/remotes/'):
1371 continue
1369 continue
1372 bname = b.split('/', 2)[2]
1370 bname = b.split('/', 2)[2]
1373 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1371 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1374 if remote:
1372 if remote:
1375 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1373 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1376 tracking['refs/remotes/%s/%s' %
1374 tracking['refs/remotes/%s/%s' %
1377 (remote, ref.split('/', 2)[2])] = b
1375 (remote, ref.split('/', 2)[2])] = b
1378 return tracking
1376 return tracking
1379
1377
1380 def _abssource(self, source):
1378 def _abssource(self, source):
1381 if '://' not in source:
1379 if '://' not in source:
1382 # recognize the scp syntax as an absolute source
1380 # recognize the scp syntax as an absolute source
1383 colon = source.find(':')
1381 colon = source.find(':')
1384 if colon != -1 and '/' not in source[:colon]:
1382 if colon != -1 and '/' not in source[:colon]:
1385 return source
1383 return source
1386 self._subsource = source
1384 self._subsource = source
1387 return _abssource(self)
1385 return _abssource(self)
1388
1386
1389 def _fetch(self, source, revision):
1387 def _fetch(self, source, revision):
1390 if self._gitmissing():
1388 if self._gitmissing():
1391 # SEC: check for safe ssh url
1389 # SEC: check for safe ssh url
1392 util.checksafessh(source)
1390 util.checksafessh(source)
1393
1391
1394 source = self._abssource(source)
1392 source = self._abssource(source)
1395 self.ui.status(_('cloning subrepo %s from %s\n') %
1393 self.ui.status(_('cloning subrepo %s from %s\n') %
1396 (self._relpath, source))
1394 (self._relpath, source))
1397 self._gitnodir(['clone', source, self._abspath])
1395 self._gitnodir(['clone', source, self._abspath])
1398 if self._githavelocally(revision):
1396 if self._githavelocally(revision):
1399 return
1397 return
1400 self.ui.status(_('pulling subrepo %s from %s\n') %
1398 self.ui.status(_('pulling subrepo %s from %s\n') %
1401 (self._relpath, self._gitremote('origin')))
1399 (self._relpath, self._gitremote('origin')))
1402 # try only origin: the originally cloned repo
1400 # try only origin: the originally cloned repo
1403 self._gitcommand(['fetch'])
1401 self._gitcommand(['fetch'])
1404 if not self._githavelocally(revision):
1402 if not self._githavelocally(revision):
1405 raise error.Abort(_('revision %s does not exist in subrepository '
1403 raise error.Abort(_('revision %s does not exist in subrepository '
1406 '"%s"\n') % (revision, self._relpath))
1404 '"%s"\n') % (revision, self._relpath))
1407
1405
1408 @annotatesubrepoerror
1406 @annotatesubrepoerror
1409 def dirty(self, ignoreupdate=False, missing=False):
1407 def dirty(self, ignoreupdate=False, missing=False):
1410 if self._gitmissing():
1408 if self._gitmissing():
1411 return self._state[1] != ''
1409 return self._state[1] != ''
1412 if self._gitisbare():
1410 if self._gitisbare():
1413 return True
1411 return True
1414 if not ignoreupdate and self._state[1] != self._gitstate():
1412 if not ignoreupdate and self._state[1] != self._gitstate():
1415 # different version checked out
1413 # different version checked out
1416 return True
1414 return True
1417 # check for staged changes or modified files; ignore untracked files
1415 # check for staged changes or modified files; ignore untracked files
1418 self._gitupdatestat()
1416 self._gitupdatestat()
1419 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1417 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1420 return code == 1
1418 return code == 1
1421
1419
1422 def basestate(self):
1420 def basestate(self):
1423 return self._gitstate()
1421 return self._gitstate()
1424
1422
1425 @annotatesubrepoerror
1423 @annotatesubrepoerror
1426 def get(self, state, overwrite=False):
1424 def get(self, state, overwrite=False):
1427 source, revision, kind = state
1425 source, revision, kind = state
1428 if not revision:
1426 if not revision:
1429 self.remove()
1427 self.remove()
1430 return
1428 return
1431 self._fetch(source, revision)
1429 self._fetch(source, revision)
1432 # if the repo was set to be bare, unbare it
1430 # if the repo was set to be bare, unbare it
1433 if self._gitisbare():
1431 if self._gitisbare():
1434 self._gitcommand(['config', 'core.bare', 'false'])
1432 self._gitcommand(['config', 'core.bare', 'false'])
1435 if self._gitstate() == revision:
1433 if self._gitstate() == revision:
1436 self._gitcommand(['reset', '--hard', 'HEAD'])
1434 self._gitcommand(['reset', '--hard', 'HEAD'])
1437 return
1435 return
1438 elif self._gitstate() == revision:
1436 elif self._gitstate() == revision:
1439 if overwrite:
1437 if overwrite:
1440 # first reset the index to unmark new files for commit, because
1438 # first reset the index to unmark new files for commit, because
1441 # reset --hard will otherwise throw away files added for commit,
1439 # reset --hard will otherwise throw away files added for commit,
1442 # not just unmark them.
1440 # not just unmark them.
1443 self._gitcommand(['reset', 'HEAD'])
1441 self._gitcommand(['reset', 'HEAD'])
1444 self._gitcommand(['reset', '--hard', 'HEAD'])
1442 self._gitcommand(['reset', '--hard', 'HEAD'])
1445 return
1443 return
1446 branch2rev, rev2branch = self._gitbranchmap()
1444 branch2rev, rev2branch = self._gitbranchmap()
1447
1445
1448 def checkout(args):
1446 def checkout(args):
1449 cmd = ['checkout']
1447 cmd = ['checkout']
1450 if overwrite:
1448 if overwrite:
1451 # first reset the index to unmark new files for commit, because
1449 # first reset the index to unmark new files for commit, because
1452 # the -f option will otherwise throw away files added for
1450 # the -f option will otherwise throw away files added for
1453 # commit, not just unmark them.
1451 # commit, not just unmark them.
1454 self._gitcommand(['reset', 'HEAD'])
1452 self._gitcommand(['reset', 'HEAD'])
1455 cmd.append('-f')
1453 cmd.append('-f')
1456 self._gitcommand(cmd + args)
1454 self._gitcommand(cmd + args)
1457 _sanitize(self.ui, self.wvfs, '.git')
1455 _sanitize(self.ui, self.wvfs, '.git')
1458
1456
1459 def rawcheckout():
1457 def rawcheckout():
1460 # no branch to checkout, check it out with no branch
1458 # no branch to checkout, check it out with no branch
1461 self.ui.warn(_('checking out detached HEAD in '
1459 self.ui.warn(_('checking out detached HEAD in '
1462 'subrepository "%s"\n') % self._relpath)
1460 'subrepository "%s"\n') % self._relpath)
1463 self.ui.warn(_('check out a git branch if you intend '
1461 self.ui.warn(_('check out a git branch if you intend '
1464 'to make changes\n'))
1462 'to make changes\n'))
1465 checkout(['-q', revision])
1463 checkout(['-q', revision])
1466
1464
1467 if revision not in rev2branch:
1465 if revision not in rev2branch:
1468 rawcheckout()
1466 rawcheckout()
1469 return
1467 return
1470 branches = rev2branch[revision]
1468 branches = rev2branch[revision]
1471 firstlocalbranch = None
1469 firstlocalbranch = None
1472 for b in branches:
1470 for b in branches:
1473 if b == 'refs/heads/master':
1471 if b == 'refs/heads/master':
1474 # master trumps all other branches
1472 # master trumps all other branches
1475 checkout(['refs/heads/master'])
1473 checkout(['refs/heads/master'])
1476 return
1474 return
1477 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1475 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1478 firstlocalbranch = b
1476 firstlocalbranch = b
1479 if firstlocalbranch:
1477 if firstlocalbranch:
1480 checkout([firstlocalbranch])
1478 checkout([firstlocalbranch])
1481 return
1479 return
1482
1480
1483 tracking = self._gittracking(branch2rev.keys())
1481 tracking = self._gittracking(branch2rev.keys())
1484 # choose a remote branch already tracked if possible
1482 # choose a remote branch already tracked if possible
1485 remote = branches[0]
1483 remote = branches[0]
1486 if remote not in tracking:
1484 if remote not in tracking:
1487 for b in branches:
1485 for b in branches:
1488 if b in tracking:
1486 if b in tracking:
1489 remote = b
1487 remote = b
1490 break
1488 break
1491
1489
1492 if remote not in tracking:
1490 if remote not in tracking:
1493 # create a new local tracking branch
1491 # create a new local tracking branch
1494 local = remote.split('/', 3)[3]
1492 local = remote.split('/', 3)[3]
1495 checkout(['-b', local, remote])
1493 checkout(['-b', local, remote])
1496 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1494 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1497 # When updating to a tracked remote branch,
1495 # When updating to a tracked remote branch,
1498 # if the local tracking branch is downstream of it,
1496 # if the local tracking branch is downstream of it,
1499 # a normal `git pull` would have performed a "fast-forward merge"
1497 # a normal `git pull` would have performed a "fast-forward merge"
1500 # which is equivalent to updating the local branch to the remote.
1498 # which is equivalent to updating the local branch to the remote.
1501 # Since we are only looking at branching at update, we need to
1499 # Since we are only looking at branching at update, we need to
1502 # detect this situation and perform this action lazily.
1500 # detect this situation and perform this action lazily.
1503 if tracking[remote] != self._gitcurrentbranch():
1501 if tracking[remote] != self._gitcurrentbranch():
1504 checkout([tracking[remote]])
1502 checkout([tracking[remote]])
1505 self._gitcommand(['merge', '--ff', remote])
1503 self._gitcommand(['merge', '--ff', remote])
1506 _sanitize(self.ui, self.wvfs, '.git')
1504 _sanitize(self.ui, self.wvfs, '.git')
1507 else:
1505 else:
1508 # a real merge would be required, just checkout the revision
1506 # a real merge would be required, just checkout the revision
1509 rawcheckout()
1507 rawcheckout()
1510
1508
1511 @annotatesubrepoerror
1509 @annotatesubrepoerror
1512 def commit(self, text, user, date):
1510 def commit(self, text, user, date):
1513 if self._gitmissing():
1511 if self._gitmissing():
1514 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1512 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1515 cmd = ['commit', '-a', '-m', text]
1513 cmd = ['commit', '-a', '-m', text]
1516 env = encoding.environ.copy()
1514 env = encoding.environ.copy()
1517 if user:
1515 if user:
1518 cmd += ['--author', user]
1516 cmd += ['--author', user]
1519 if date:
1517 if date:
1520 # git's date parser silently ignores when seconds < 1e9
1518 # git's date parser silently ignores when seconds < 1e9
1521 # convert to ISO8601
1519 # convert to ISO8601
1522 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1520 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1523 '%Y-%m-%dT%H:%M:%S %1%2')
1521 '%Y-%m-%dT%H:%M:%S %1%2')
1524 self._gitcommand(cmd, env=env)
1522 self._gitcommand(cmd, env=env)
1525 # make sure commit works otherwise HEAD might not exist under certain
1523 # make sure commit works otherwise HEAD might not exist under certain
1526 # circumstances
1524 # circumstances
1527 return self._gitstate()
1525 return self._gitstate()
1528
1526
1529 @annotatesubrepoerror
1527 @annotatesubrepoerror
1530 def merge(self, state):
1528 def merge(self, state):
1531 source, revision, kind = state
1529 source, revision, kind = state
1532 self._fetch(source, revision)
1530 self._fetch(source, revision)
1533 base = self._gitcommand(['merge-base', revision, self._state[1]])
1531 base = self._gitcommand(['merge-base', revision, self._state[1]])
1534 self._gitupdatestat()
1532 self._gitupdatestat()
1535 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1533 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1536
1534
1537 def mergefunc():
1535 def mergefunc():
1538 if base == revision:
1536 if base == revision:
1539 self.get(state) # fast forward merge
1537 self.get(state) # fast forward merge
1540 elif base != self._state[1]:
1538 elif base != self._state[1]:
1541 self._gitcommand(['merge', '--no-commit', revision])
1539 self._gitcommand(['merge', '--no-commit', revision])
1542 _sanitize(self.ui, self.wvfs, '.git')
1540 _sanitize(self.ui, self.wvfs, '.git')
1543
1541
1544 if self.dirty():
1542 if self.dirty():
1545 if self._gitstate() != revision:
1543 if self._gitstate() != revision:
1546 dirty = self._gitstate() == self._state[1] or code != 0
1544 dirty = self._gitstate() == self._state[1] or code != 0
1547 if _updateprompt(self.ui, self, dirty,
1545 if _updateprompt(self.ui, self, dirty,
1548 self._state[1][:7], revision[:7]):
1546 self._state[1][:7], revision[:7]):
1549 mergefunc()
1547 mergefunc()
1550 else:
1548 else:
1551 mergefunc()
1549 mergefunc()
1552
1550
1553 @annotatesubrepoerror
1551 @annotatesubrepoerror
1554 def push(self, opts):
1552 def push(self, opts):
1555 force = opts.get('force')
1553 force = opts.get('force')
1556
1554
1557 if not self._state[1]:
1555 if not self._state[1]:
1558 return True
1556 return True
1559 if self._gitmissing():
1557 if self._gitmissing():
1560 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1558 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1561 # if a branch in origin contains the revision, nothing to do
1559 # if a branch in origin contains the revision, nothing to do
1562 branch2rev, rev2branch = self._gitbranchmap()
1560 branch2rev, rev2branch = self._gitbranchmap()
1563 if self._state[1] in rev2branch:
1561 if self._state[1] in rev2branch:
1564 for b in rev2branch[self._state[1]]:
1562 for b in rev2branch[self._state[1]]:
1565 if b.startswith('refs/remotes/origin/'):
1563 if b.startswith('refs/remotes/origin/'):
1566 return True
1564 return True
1567 for b, revision in branch2rev.iteritems():
1565 for b, revision in branch2rev.iteritems():
1568 if b.startswith('refs/remotes/origin/'):
1566 if b.startswith('refs/remotes/origin/'):
1569 if self._gitisancestor(self._state[1], revision):
1567 if self._gitisancestor(self._state[1], revision):
1570 return True
1568 return True
1571 # otherwise, try to push the currently checked out branch
1569 # otherwise, try to push the currently checked out branch
1572 cmd = ['push']
1570 cmd = ['push']
1573 if force:
1571 if force:
1574 cmd.append('--force')
1572 cmd.append('--force')
1575
1573
1576 current = self._gitcurrentbranch()
1574 current = self._gitcurrentbranch()
1577 if current:
1575 if current:
1578 # determine if the current branch is even useful
1576 # determine if the current branch is even useful
1579 if not self._gitisancestor(self._state[1], current):
1577 if not self._gitisancestor(self._state[1], current):
1580 self.ui.warn(_('unrelated git branch checked out '
1578 self.ui.warn(_('unrelated git branch checked out '
1581 'in subrepository "%s"\n') % self._relpath)
1579 'in subrepository "%s"\n') % self._relpath)
1582 return False
1580 return False
1583 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1581 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1584 (current.split('/', 2)[2], self._relpath))
1582 (current.split('/', 2)[2], self._relpath))
1585 ret = self._gitdir(cmd + ['origin', current])
1583 ret = self._gitdir(cmd + ['origin', current])
1586 return ret[1] == 0
1584 return ret[1] == 0
1587 else:
1585 else:
1588 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1586 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1589 'cannot push revision %s\n') %
1587 'cannot push revision %s\n') %
1590 (self._relpath, self._state[1]))
1588 (self._relpath, self._state[1]))
1591 return False
1589 return False
1592
1590
1593 @annotatesubrepoerror
1591 @annotatesubrepoerror
1594 def add(self, ui, match, prefix, explicitonly, **opts):
1592 def add(self, ui, match, prefix, explicitonly, **opts):
1595 if self._gitmissing():
1593 if self._gitmissing():
1596 return []
1594 return []
1597
1595
1598 s = self.status(None, unknown=True, clean=True)
1596 s = self.status(None, unknown=True, clean=True)
1599
1597
1600 tracked = set()
1598 tracked = set()
1601 # dirstates 'amn' warn, 'r' is added again
1599 # dirstates 'amn' warn, 'r' is added again
1602 for l in (s.modified, s.added, s.deleted, s.clean):
1600 for l in (s.modified, s.added, s.deleted, s.clean):
1603 tracked.update(l)
1601 tracked.update(l)
1604
1602
1605 # Unknown files not of interest will be rejected by the matcher
1603 # Unknown files not of interest will be rejected by the matcher
1606 files = s.unknown
1604 files = s.unknown
1607 files.extend(match.files())
1605 files.extend(match.files())
1608
1606
1609 rejected = []
1607 rejected = []
1610
1608
1611 files = [f for f in sorted(set(files)) if match(f)]
1609 files = [f for f in sorted(set(files)) if match(f)]
1612 for f in files:
1610 for f in files:
1613 exact = match.exact(f)
1611 exact = match.exact(f)
1614 command = ["add"]
1612 command = ["add"]
1615 if exact:
1613 if exact:
1616 command.append("-f") #should be added, even if ignored
1614 command.append("-f") #should be added, even if ignored
1617 if ui.verbose or not exact:
1615 if ui.verbose or not exact:
1618 ui.status(_('adding %s\n') % match.rel(f))
1616 ui.status(_('adding %s\n') % match.rel(f))
1619
1617
1620 if f in tracked: # hg prints 'adding' even if already tracked
1618 if f in tracked: # hg prints 'adding' even if already tracked
1621 if exact:
1619 if exact:
1622 rejected.append(f)
1620 rejected.append(f)
1623 continue
1621 continue
1624 if not opts.get(r'dry_run'):
1622 if not opts.get(r'dry_run'):
1625 self._gitcommand(command + [f])
1623 self._gitcommand(command + [f])
1626
1624
1627 for f in rejected:
1625 for f in rejected:
1628 ui.warn(_("%s already tracked!\n") % match.abs(f))
1626 ui.warn(_("%s already tracked!\n") % match.abs(f))
1629
1627
1630 return rejected
1628 return rejected
1631
1629
1632 @annotatesubrepoerror
1630 @annotatesubrepoerror
1633 def remove(self):
1631 def remove(self):
1634 if self._gitmissing():
1632 if self._gitmissing():
1635 return
1633 return
1636 if self.dirty():
1634 if self.dirty():
1637 self.ui.warn(_('not removing repo %s because '
1635 self.ui.warn(_('not removing repo %s because '
1638 'it has changes.\n') % self._relpath)
1636 'it has changes.\n') % self._relpath)
1639 return
1637 return
1640 # we can't fully delete the repository as it may contain
1638 # we can't fully delete the repository as it may contain
1641 # local-only history
1639 # local-only history
1642 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1640 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1643 self._gitcommand(['config', 'core.bare', 'true'])
1641 self._gitcommand(['config', 'core.bare', 'true'])
1644 for f, kind in self.wvfs.readdir():
1642 for f, kind in self.wvfs.readdir():
1645 if f == '.git':
1643 if f == '.git':
1646 continue
1644 continue
1647 if kind == stat.S_IFDIR:
1645 if kind == stat.S_IFDIR:
1648 self.wvfs.rmtree(f)
1646 self.wvfs.rmtree(f)
1649 else:
1647 else:
1650 self.wvfs.unlink(f)
1648 self.wvfs.unlink(f)
1651
1649
1652 def archive(self, archiver, prefix, match=None, decode=True):
1650 def archive(self, archiver, prefix, match=None, decode=True):
1653 total = 0
1651 total = 0
1654 source, revision = self._state
1652 source, revision = self._state
1655 if not revision:
1653 if not revision:
1656 return total
1654 return total
1657 self._fetch(source, revision)
1655 self._fetch(source, revision)
1658
1656
1659 # Parse git's native archive command.
1657 # Parse git's native archive command.
1660 # This should be much faster than manually traversing the trees
1658 # This should be much faster than manually traversing the trees
1661 # and objects with many subprocess calls.
1659 # and objects with many subprocess calls.
1662 tarstream = self._gitcommand(['archive', revision], stream=True)
1660 tarstream = self._gitcommand(['archive', revision], stream=True)
1663 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1661 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1664 relpath = subrelpath(self)
1662 relpath = subrelpath(self)
1665 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
1663 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
1666 unit=_('files'))
1664 unit=_('files'))
1667 progress.update(0)
1665 progress.update(0)
1668 for info in tar:
1666 for info in tar:
1669 if info.isdir():
1667 if info.isdir():
1670 continue
1668 continue
1671 bname = pycompat.fsencode(info.name)
1669 bname = pycompat.fsencode(info.name)
1672 if match and not match(bname):
1670 if match and not match(bname):
1673 continue
1671 continue
1674 if info.issym():
1672 if info.issym():
1675 data = info.linkname
1673 data = info.linkname
1676 else:
1674 else:
1677 data = tar.extractfile(info).read()
1675 data = tar.extractfile(info).read()
1678 archiver.addfile(prefix + self._path + '/' + bname,
1676 archiver.addfile(prefix + self._path + '/' + bname,
1679 info.mode, info.issym(), data)
1677 info.mode, info.issym(), data)
1680 total += 1
1678 total += 1
1681 progress.increment()
1679 progress.increment()
1682 progress.complete()
1680 progress.complete()
1683 return total
1681 return total
1684
1682
1685
1683
1686 @annotatesubrepoerror
1684 @annotatesubrepoerror
1687 def cat(self, match, fm, fntemplate, prefix, **opts):
1685 def cat(self, match, fm, fntemplate, prefix, **opts):
1688 rev = self._state[1]
1686 rev = self._state[1]
1689 if match.anypats():
1687 if match.anypats():
1690 return 1 #No support for include/exclude yet
1688 return 1 #No support for include/exclude yet
1691
1689
1692 if not match.files():
1690 if not match.files():
1693 return 1
1691 return 1
1694
1692
1695 # TODO: add support for non-plain formatter (see cmdutil.cat())
1693 # TODO: add support for non-plain formatter (see cmdutil.cat())
1696 for f in match.files():
1694 for f in match.files():
1697 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1695 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1698 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1696 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1699 pathname=self.wvfs.reljoin(prefix, f))
1697 pathname=self.wvfs.reljoin(prefix, f))
1700 fp.write(output)
1698 fp.write(output)
1701 fp.close()
1699 fp.close()
1702 return 0
1700 return 0
1703
1701
1704
1702
1705 @annotatesubrepoerror
1703 @annotatesubrepoerror
1706 def status(self, rev2, **opts):
1704 def status(self, rev2, **opts):
1707 rev1 = self._state[1]
1705 rev1 = self._state[1]
1708 if self._gitmissing() or not rev1:
1706 if self._gitmissing() or not rev1:
1709 # if the repo is missing, return no results
1707 # if the repo is missing, return no results
1710 return scmutil.status([], [], [], [], [], [], [])
1708 return scmutil.status([], [], [], [], [], [], [])
1711 modified, added, removed = [], [], []
1709 modified, added, removed = [], [], []
1712 self._gitupdatestat()
1710 self._gitupdatestat()
1713 if rev2:
1711 if rev2:
1714 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1712 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1715 else:
1713 else:
1716 command = ['diff-index', '--no-renames', rev1]
1714 command = ['diff-index', '--no-renames', rev1]
1717 out = self._gitcommand(command)
1715 out = self._gitcommand(command)
1718 for line in out.split('\n'):
1716 for line in out.split('\n'):
1719 tab = line.find('\t')
1717 tab = line.find('\t')
1720 if tab == -1:
1718 if tab == -1:
1721 continue
1719 continue
1722 status, f = line[tab - 1:tab], line[tab + 1:]
1720 status, f = line[tab - 1:tab], line[tab + 1:]
1723 if status == 'M':
1721 if status == 'M':
1724 modified.append(f)
1722 modified.append(f)
1725 elif status == 'A':
1723 elif status == 'A':
1726 added.append(f)
1724 added.append(f)
1727 elif status == 'D':
1725 elif status == 'D':
1728 removed.append(f)
1726 removed.append(f)
1729
1727
1730 deleted, unknown, ignored, clean = [], [], [], []
1728 deleted, unknown, ignored, clean = [], [], [], []
1731
1729
1732 command = ['status', '--porcelain', '-z']
1730 command = ['status', '--porcelain', '-z']
1733 if opts.get(r'unknown'):
1731 if opts.get(r'unknown'):
1734 command += ['--untracked-files=all']
1732 command += ['--untracked-files=all']
1735 if opts.get(r'ignored'):
1733 if opts.get(r'ignored'):
1736 command += ['--ignored']
1734 command += ['--ignored']
1737 out = self._gitcommand(command)
1735 out = self._gitcommand(command)
1738
1736
1739 changedfiles = set()
1737 changedfiles = set()
1740 changedfiles.update(modified)
1738 changedfiles.update(modified)
1741 changedfiles.update(added)
1739 changedfiles.update(added)
1742 changedfiles.update(removed)
1740 changedfiles.update(removed)
1743 for line in out.split('\0'):
1741 for line in out.split('\0'):
1744 if not line:
1742 if not line:
1745 continue
1743 continue
1746 st = line[0:2]
1744 st = line[0:2]
1747 #moves and copies show 2 files on one line
1745 #moves and copies show 2 files on one line
1748 if line.find('\0') >= 0:
1746 if line.find('\0') >= 0:
1749 filename1, filename2 = line[3:].split('\0')
1747 filename1, filename2 = line[3:].split('\0')
1750 else:
1748 else:
1751 filename1 = line[3:]
1749 filename1 = line[3:]
1752 filename2 = None
1750 filename2 = None
1753
1751
1754 changedfiles.add(filename1)
1752 changedfiles.add(filename1)
1755 if filename2:
1753 if filename2:
1756 changedfiles.add(filename2)
1754 changedfiles.add(filename2)
1757
1755
1758 if st == '??':
1756 if st == '??':
1759 unknown.append(filename1)
1757 unknown.append(filename1)
1760 elif st == '!!':
1758 elif st == '!!':
1761 ignored.append(filename1)
1759 ignored.append(filename1)
1762
1760
1763 if opts.get(r'clean'):
1761 if opts.get(r'clean'):
1764 out = self._gitcommand(['ls-files'])
1762 out = self._gitcommand(['ls-files'])
1765 for f in out.split('\n'):
1763 for f in out.split('\n'):
1766 if not f in changedfiles:
1764 if not f in changedfiles:
1767 clean.append(f)
1765 clean.append(f)
1768
1766
1769 return scmutil.status(modified, added, removed, deleted,
1767 return scmutil.status(modified, added, removed, deleted,
1770 unknown, ignored, clean)
1768 unknown, ignored, clean)
1771
1769
1772 @annotatesubrepoerror
1770 @annotatesubrepoerror
1773 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1771 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1774 node1 = self._state[1]
1772 node1 = self._state[1]
1775 cmd = ['diff', '--no-renames']
1773 cmd = ['diff', '--no-renames']
1776 if opts[r'stat']:
1774 if opts[r'stat']:
1777 cmd.append('--stat')
1775 cmd.append('--stat')
1778 else:
1776 else:
1779 # for Git, this also implies '-p'
1777 # for Git, this also implies '-p'
1780 cmd.append('-U%d' % diffopts.context)
1778 cmd.append('-U%d' % diffopts.context)
1781
1779
1782 gitprefix = self.wvfs.reljoin(prefix, self._path)
1783
1784 if diffopts.noprefix:
1780 if diffopts.noprefix:
1785 cmd.extend(['--src-prefix=%s/' % gitprefix,
1781 cmd.extend(['--src-prefix=%s/' % prefix,
1786 '--dst-prefix=%s/' % gitprefix])
1782 '--dst-prefix=%s/' % prefix])
1787 else:
1783 else:
1788 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1784 cmd.extend(['--src-prefix=a/%s/' % prefix,
1789 '--dst-prefix=b/%s/' % gitprefix])
1785 '--dst-prefix=b/%s/' % prefix])
1790
1786
1791 if diffopts.ignorews:
1787 if diffopts.ignorews:
1792 cmd.append('--ignore-all-space')
1788 cmd.append('--ignore-all-space')
1793 if diffopts.ignorewsamount:
1789 if diffopts.ignorewsamount:
1794 cmd.append('--ignore-space-change')
1790 cmd.append('--ignore-space-change')
1795 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1791 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1796 and diffopts.ignoreblanklines:
1792 and diffopts.ignoreblanklines:
1797 cmd.append('--ignore-blank-lines')
1793 cmd.append('--ignore-blank-lines')
1798
1794
1799 cmd.append(node1)
1795 cmd.append(node1)
1800 if node2:
1796 if node2:
1801 cmd.append(node2)
1797 cmd.append(node2)
1802
1798
1803 output = ""
1799 output = ""
1804 if match.always():
1800 if match.always():
1805 output += self._gitcommand(cmd) + '\n'
1801 output += self._gitcommand(cmd) + '\n'
1806 else:
1802 else:
1807 st = self.status(node2)[:3]
1803 st = self.status(node2)[:3]
1808 files = [f for sublist in st for f in sublist]
1804 files = [f for sublist in st for f in sublist]
1809 for f in files:
1805 for f in files:
1810 if match(f):
1806 if match(f):
1811 output += self._gitcommand(cmd + ['--', f]) + '\n'
1807 output += self._gitcommand(cmd + ['--', f]) + '\n'
1812
1808
1813 if output.strip():
1809 if output.strip():
1814 ui.write(output)
1810 ui.write(output)
1815
1811
1816 @annotatesubrepoerror
1812 @annotatesubrepoerror
1817 def revert(self, substate, *pats, **opts):
1813 def revert(self, substate, *pats, **opts):
1818 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1814 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1819 if not opts.get(r'no_backup'):
1815 if not opts.get(r'no_backup'):
1820 status = self.status(None)
1816 status = self.status(None)
1821 names = status.modified
1817 names = status.modified
1822 for name in names:
1818 for name in names:
1823 # backuppath() expects a path relative to the parent repo (the
1819 # backuppath() expects a path relative to the parent repo (the
1824 # repo that ui.origbackuppath is relative to)
1820 # repo that ui.origbackuppath is relative to)
1825 parentname = os.path.join(self._path, name)
1821 parentname = os.path.join(self._path, name)
1826 bakname = scmutil.backuppath(self.ui, self._subparent,
1822 bakname = scmutil.backuppath(self.ui, self._subparent,
1827 parentname)
1823 parentname)
1828 self.ui.note(_('saving current version of %s as %s\n') %
1824 self.ui.note(_('saving current version of %s as %s\n') %
1829 (name, os.path.relpath(bakname)))
1825 (name, os.path.relpath(bakname)))
1830 util.rename(self.wvfs.join(name), bakname)
1826 util.rename(self.wvfs.join(name), bakname)
1831
1827
1832 if not opts.get(r'dry_run'):
1828 if not opts.get(r'dry_run'):
1833 self.get(substate, overwrite=True)
1829 self.get(substate, overwrite=True)
1834 return []
1830 return []
1835
1831
1836 def shortid(self, revid):
1832 def shortid(self, revid):
1837 return revid[:7]
1833 return revid[:7]
1838
1834
1839 types = {
1835 types = {
1840 'hg': hgsubrepo,
1836 'hg': hgsubrepo,
1841 'svn': svnsubrepo,
1837 'svn': svnsubrepo,
1842 'git': gitsubrepo,
1838 'git': gitsubrepo,
1843 }
1839 }
General Comments 0
You need to be logged in to leave comments. Login now