##// END OF EJS Templates
sparse: move post commit actions into core...
Gregory Szorc -
r33353:160efb55 default
parent child Browse files
Show More
@@ -1,567 +1,542
1 # sparse.py - allow sparse checkouts of the working directory
1 # sparse.py - allow sparse checkouts of the working directory
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 Facebook, Inc.
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 """allow sparse checkouts of the working directory (EXPERIMENTAL)
8 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9
9
10 (This extension is not yet protected by backwards compatibility
10 (This extension is not yet protected by backwards compatibility
11 guarantees. Any aspect may break in future releases until this
11 guarantees. Any aspect may break in future releases until this
12 notice is removed.)
12 notice is removed.)
13
13
14 This extension allows the working directory to only consist of a
14 This extension allows the working directory to only consist of a
15 subset of files for the revision. This allows specific files or
15 subset of files for the revision. This allows specific files or
16 directories to be explicitly included or excluded. Many repository
16 directories to be explicitly included or excluded. Many repository
17 operations have performance proportional to the number of files in
17 operations have performance proportional to the number of files in
18 the working directory. So only realizing a subset of files in the
18 the working directory. So only realizing a subset of files in the
19 working directory can improve performance.
19 working directory can improve performance.
20
20
21 Sparse Config Files
21 Sparse Config Files
22 -------------------
22 -------------------
23
23
24 The set of files that are part of a sparse checkout are defined by
24 The set of files that are part of a sparse checkout are defined by
25 a sparse config file. The file defines 3 things: includes (files to
25 a sparse config file. The file defines 3 things: includes (files to
26 include in the sparse checkout), excludes (files to exclude from the
26 include in the sparse checkout), excludes (files to exclude from the
27 sparse checkout), and profiles (links to other config files).
27 sparse checkout), and profiles (links to other config files).
28
28
29 The file format is newline delimited. Empty lines and lines beginning
29 The file format is newline delimited. Empty lines and lines beginning
30 with ``#`` are ignored.
30 with ``#`` are ignored.
31
31
32 Lines beginning with ``%include `` denote another sparse config file
32 Lines beginning with ``%include `` denote another sparse config file
33 to include. e.g. ``%include tests.sparse``. The filename is relative
33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 to the repository root.
34 to the repository root.
35
35
36 The special lines ``[include]`` and ``[exclude]`` denote the section
36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 for includes and excludes that follow, respectively. It is illegal to
37 for includes and excludes that follow, respectively. It is illegal to
38 have ``[include]`` after ``[exclude]``. If no sections are defined,
38 have ``[include]`` after ``[exclude]``. If no sections are defined,
39 entries are assumed to be in the ``[include]`` section.
39 entries are assumed to be in the ``[include]`` section.
40
40
41 Non-special lines resemble file patterns to be added to either includes
41 Non-special lines resemble file patterns to be added to either includes
42 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
42 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
43 Patterns are interpreted as ``glob:`` by default and match against the
43 Patterns are interpreted as ``glob:`` by default and match against the
44 root of the repository.
44 root of the repository.
45
45
46 Exclusion patterns take precedence over inclusion patterns. So even
46 Exclusion patterns take precedence over inclusion patterns. So even
47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
48
48
49 For example, say you have a repository with 3 directories, ``frontend/``,
49 For example, say you have a repository with 3 directories, ``frontend/``,
50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
51 to different projects and it is uncommon for someone working on one
51 to different projects and it is uncommon for someone working on one
52 to need the files for the other. But ``tools/`` contains files shared
52 to need the files for the other. But ``tools/`` contains files shared
53 between both projects. Your sparse config files may resemble::
53 between both projects. Your sparse config files may resemble::
54
54
55 # frontend.sparse
55 # frontend.sparse
56 frontend/**
56 frontend/**
57 tools/**
57 tools/**
58
58
59 # backend.sparse
59 # backend.sparse
60 backend/**
60 backend/**
61 tools/**
61 tools/**
62
62
63 Say the backend grows in size. Or there's a directory with thousands
63 Say the backend grows in size. Or there's a directory with thousands
64 of files you wish to exclude. You can modify the profile to exclude
64 of files you wish to exclude. You can modify the profile to exclude
65 certain files::
65 certain files::
66
66
67 [include]
67 [include]
68 backend/**
68 backend/**
69 tools/**
69 tools/**
70
70
71 [exclude]
71 [exclude]
72 tools/tests/**
72 tools/tests/**
73 """
73 """
74
74
75 from __future__ import absolute_import
75 from __future__ import absolute_import
76
76
77 from mercurial.i18n import _
77 from mercurial.i18n import _
78 from mercurial.node import nullid
78 from mercurial.node import nullid
79 from mercurial import (
79 from mercurial import (
80 cmdutil,
80 cmdutil,
81 commands,
81 commands,
82 context,
83 dirstate,
82 dirstate,
84 error,
83 error,
85 extensions,
84 extensions,
86 hg,
85 hg,
87 localrepo,
86 localrepo,
88 match as matchmod,
87 match as matchmod,
89 registrar,
88 registrar,
90 sparse,
89 sparse,
91 util,
90 util,
92 )
91 )
93
92
94 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
95 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
96 # be specifying the version(s) of Mercurial they are tested with, or
95 # be specifying the version(s) of Mercurial they are tested with, or
97 # leave the attribute unspecified.
96 # leave the attribute unspecified.
98 testedwith = 'ships-with-hg-core'
97 testedwith = 'ships-with-hg-core'
99
98
100 cmdtable = {}
99 cmdtable = {}
101 command = registrar.command(cmdtable)
100 command = registrar.command(cmdtable)
102
101
103 def uisetup(ui):
104 _setupcommit(ui)
105
106 def extsetup(ui):
102 def extsetup(ui):
107 sparse.enabled = True
103 sparse.enabled = True
108
104
109 _setupclone(ui)
105 _setupclone(ui)
110 _setuplog(ui)
106 _setuplog(ui)
111 _setupadd(ui)
107 _setupadd(ui)
112 _setupdirstate(ui)
108 _setupdirstate(ui)
113
109
114 def reposetup(ui, repo):
110 def reposetup(ui, repo):
115 if not util.safehasattr(repo, 'dirstate'):
111 if not util.safehasattr(repo, 'dirstate'):
116 return
112 return
117
113
118 if 'dirstate' in repo._filecache:
114 if 'dirstate' in repo._filecache:
119 repo.dirstate.repo = repo
115 repo.dirstate.repo = repo
120
116
121 def replacefilecache(cls, propname, replacement):
117 def replacefilecache(cls, propname, replacement):
122 """Replace a filecache property with a new class. This allows changing the
118 """Replace a filecache property with a new class. This allows changing the
123 cache invalidation condition."""
119 cache invalidation condition."""
124 origcls = cls
120 origcls = cls
125 assert callable(replacement)
121 assert callable(replacement)
126 while cls is not object:
122 while cls is not object:
127 if propname in cls.__dict__:
123 if propname in cls.__dict__:
128 orig = cls.__dict__[propname]
124 orig = cls.__dict__[propname]
129 setattr(cls, propname, replacement(orig))
125 setattr(cls, propname, replacement(orig))
130 break
126 break
131 cls = cls.__bases__[0]
127 cls = cls.__bases__[0]
132
128
133 if cls is object:
129 if cls is object:
134 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
130 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
135 propname))
131 propname))
136
132
137 def _setupcommit(ui):
138 def _refreshoncommit(orig, self, node):
139 """Refresh the checkout when commits touch .hgsparse
140 """
141 orig(self, node)
142 repo = self._repo
143
144 ctx = repo[node]
145 profiles = sparse.patternsforrev(repo, ctx.rev())[2]
146
147 # profiles will only have data if sparse is enabled.
148 if set(profiles) & set(ctx.files()):
149 origstatus = repo.status()
150 origsparsematch = sparse.matcher(repo)
151 sparse.refreshwdir(repo, origstatus, origsparsematch, force=True)
152
153 sparse.prunetemporaryincludes(repo)
154
155 extensions.wrapfunction(context.committablectx, 'markcommitted',
156 _refreshoncommit)
157
158 def _setuplog(ui):
133 def _setuplog(ui):
159 entry = commands.table['^log|history']
134 entry = commands.table['^log|history']
160 entry[1].append(('', 'sparse', None,
135 entry[1].append(('', 'sparse', None,
161 "limit to changesets affecting the sparse checkout"))
136 "limit to changesets affecting the sparse checkout"))
162
137
163 def _logrevs(orig, repo, opts):
138 def _logrevs(orig, repo, opts):
164 revs = orig(repo, opts)
139 revs = orig(repo, opts)
165 if opts.get('sparse'):
140 if opts.get('sparse'):
166 sparsematch = sparse.matcher(repo)
141 sparsematch = sparse.matcher(repo)
167 def ctxmatch(rev):
142 def ctxmatch(rev):
168 ctx = repo[rev]
143 ctx = repo[rev]
169 return any(f for f in ctx.files() if sparsematch(f))
144 return any(f for f in ctx.files() if sparsematch(f))
170 revs = revs.filter(ctxmatch)
145 revs = revs.filter(ctxmatch)
171 return revs
146 return revs
172 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
147 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
173
148
174 def _clonesparsecmd(orig, ui, repo, *args, **opts):
149 def _clonesparsecmd(orig, ui, repo, *args, **opts):
175 include_pat = opts.get('include')
150 include_pat = opts.get('include')
176 exclude_pat = opts.get('exclude')
151 exclude_pat = opts.get('exclude')
177 enableprofile_pat = opts.get('enable_profile')
152 enableprofile_pat = opts.get('enable_profile')
178 include = exclude = enableprofile = False
153 include = exclude = enableprofile = False
179 if include_pat:
154 if include_pat:
180 pat = include_pat
155 pat = include_pat
181 include = True
156 include = True
182 if exclude_pat:
157 if exclude_pat:
183 pat = exclude_pat
158 pat = exclude_pat
184 exclude = True
159 exclude = True
185 if enableprofile_pat:
160 if enableprofile_pat:
186 pat = enableprofile_pat
161 pat = enableprofile_pat
187 enableprofile = True
162 enableprofile = True
188 if sum([include, exclude, enableprofile]) > 1:
163 if sum([include, exclude, enableprofile]) > 1:
189 raise error.Abort(_("too many flags specified."))
164 raise error.Abort(_("too many flags specified."))
190 if include or exclude or enableprofile:
165 if include or exclude or enableprofile:
191 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
166 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
192 _config(self.ui, self.unfiltered(), pat, {}, include=include,
167 _config(self.ui, self.unfiltered(), pat, {}, include=include,
193 exclude=exclude, enableprofile=enableprofile)
168 exclude=exclude, enableprofile=enableprofile)
194 return orig(self, node, overwrite, *args, **kwargs)
169 return orig(self, node, overwrite, *args, **kwargs)
195 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
170 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
196 return orig(ui, repo, *args, **opts)
171 return orig(ui, repo, *args, **opts)
197
172
198 def _setupclone(ui):
173 def _setupclone(ui):
199 entry = commands.table['^clone']
174 entry = commands.table['^clone']
200 entry[1].append(('', 'enable-profile', [],
175 entry[1].append(('', 'enable-profile', [],
201 'enable a sparse profile'))
176 'enable a sparse profile'))
202 entry[1].append(('', 'include', [],
177 entry[1].append(('', 'include', [],
203 'include sparse pattern'))
178 'include sparse pattern'))
204 entry[1].append(('', 'exclude', [],
179 entry[1].append(('', 'exclude', [],
205 'exclude sparse pattern'))
180 'exclude sparse pattern'))
206 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
181 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
207
182
208 def _setupadd(ui):
183 def _setupadd(ui):
209 entry = commands.table['^add']
184 entry = commands.table['^add']
210 entry[1].append(('s', 'sparse', None,
185 entry[1].append(('s', 'sparse', None,
211 'also include directories of added files in sparse config'))
186 'also include directories of added files in sparse config'))
212
187
213 def _add(orig, ui, repo, *pats, **opts):
188 def _add(orig, ui, repo, *pats, **opts):
214 if opts.get('sparse'):
189 if opts.get('sparse'):
215 dirs = set()
190 dirs = set()
216 for pat in pats:
191 for pat in pats:
217 dirname, basename = util.split(pat)
192 dirname, basename = util.split(pat)
218 dirs.add(dirname)
193 dirs.add(dirname)
219 _config(ui, repo, list(dirs), opts, include=True)
194 _config(ui, repo, list(dirs), opts, include=True)
220 return orig(ui, repo, *pats, **opts)
195 return orig(ui, repo, *pats, **opts)
221
196
222 extensions.wrapcommand(commands.table, 'add', _add)
197 extensions.wrapcommand(commands.table, 'add', _add)
223
198
224 def _setupdirstate(ui):
199 def _setupdirstate(ui):
225 """Modify the dirstate to prevent stat'ing excluded files,
200 """Modify the dirstate to prevent stat'ing excluded files,
226 and to prevent modifications to files outside the checkout.
201 and to prevent modifications to files outside the checkout.
227 """
202 """
228
203
229 def _dirstate(orig, repo):
204 def _dirstate(orig, repo):
230 dirstate = orig(repo)
205 dirstate = orig(repo)
231 dirstate.repo = repo
206 dirstate.repo = repo
232 return dirstate
207 return dirstate
233 extensions.wrapfunction(
208 extensions.wrapfunction(
234 localrepo.localrepository.dirstate, 'func', _dirstate)
209 localrepo.localrepository.dirstate, 'func', _dirstate)
235
210
236 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
211 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
237 # property, which means normal function wrapping doesn't work.
212 # property, which means normal function wrapping doesn't work.
238 class ignorewrapper(object):
213 class ignorewrapper(object):
239 def __init__(self, orig):
214 def __init__(self, orig):
240 self.orig = orig
215 self.orig = orig
241 self.origignore = None
216 self.origignore = None
242 self.func = None
217 self.func = None
243 self.sparsematch = None
218 self.sparsematch = None
244
219
245 def __get__(self, obj, type=None):
220 def __get__(self, obj, type=None):
246 repo = obj.repo
221 repo = obj.repo
247 origignore = self.orig.__get__(obj)
222 origignore = self.orig.__get__(obj)
248
223
249 sparsematch = sparse.matcher(repo)
224 sparsematch = sparse.matcher(repo)
250 if sparsematch.always():
225 if sparsematch.always():
251 return origignore
226 return origignore
252
227
253 if self.sparsematch != sparsematch or self.origignore != origignore:
228 if self.sparsematch != sparsematch or self.origignore != origignore:
254 self.func = matchmod.unionmatcher([
229 self.func = matchmod.unionmatcher([
255 origignore, matchmod.negatematcher(sparsematch)])
230 origignore, matchmod.negatematcher(sparsematch)])
256 self.sparsematch = sparsematch
231 self.sparsematch = sparsematch
257 self.origignore = origignore
232 self.origignore = origignore
258 return self.func
233 return self.func
259
234
260 def __set__(self, obj, value):
235 def __set__(self, obj, value):
261 return self.orig.__set__(obj, value)
236 return self.orig.__set__(obj, value)
262
237
263 def __delete__(self, obj):
238 def __delete__(self, obj):
264 return self.orig.__delete__(obj)
239 return self.orig.__delete__(obj)
265
240
266 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
241 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
267
242
268 # dirstate.rebuild should not add non-matching files
243 # dirstate.rebuild should not add non-matching files
269 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
244 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
270 matcher = sparse.matcher(self.repo)
245 matcher = sparse.matcher(self.repo)
271 if not matcher.always():
246 if not matcher.always():
272 allfiles = allfiles.matches(matcher)
247 allfiles = allfiles.matches(matcher)
273 if changedfiles:
248 if changedfiles:
274 changedfiles = [f for f in changedfiles if matcher(f)]
249 changedfiles = [f for f in changedfiles if matcher(f)]
275
250
276 if changedfiles is not None:
251 if changedfiles is not None:
277 # In _rebuild, these files will be deleted from the dirstate
252 # In _rebuild, these files will be deleted from the dirstate
278 # when they are not found to be in allfiles
253 # when they are not found to be in allfiles
279 dirstatefilestoremove = set(f for f in self if not matcher(f))
254 dirstatefilestoremove = set(f for f in self if not matcher(f))
280 changedfiles = dirstatefilestoremove.union(changedfiles)
255 changedfiles = dirstatefilestoremove.union(changedfiles)
281
256
282 return orig(self, parent, allfiles, changedfiles)
257 return orig(self, parent, allfiles, changedfiles)
283 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
258 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
284
259
285 # Prevent adding files that are outside the sparse checkout
260 # Prevent adding files that are outside the sparse checkout
286 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
261 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
287 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
262 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
288 '`hg add -s <file>` to include file directory while adding')
263 '`hg add -s <file>` to include file directory while adding')
289 for func in editfuncs:
264 for func in editfuncs:
290 def _wrapper(orig, self, *args):
265 def _wrapper(orig, self, *args):
291 repo = self.repo
266 repo = self.repo
292 sparsematch = sparse.matcher(repo)
267 sparsematch = sparse.matcher(repo)
293 if not sparsematch.always():
268 if not sparsematch.always():
294 dirstate = repo.dirstate
269 dirstate = repo.dirstate
295 for f in args:
270 for f in args:
296 if (f is not None and not sparsematch(f) and
271 if (f is not None and not sparsematch(f) and
297 f not in dirstate):
272 f not in dirstate):
298 raise error.Abort(_("cannot add '%s' - it is outside "
273 raise error.Abort(_("cannot add '%s' - it is outside "
299 "the sparse checkout") % f,
274 "the sparse checkout") % f,
300 hint=hint)
275 hint=hint)
301 return orig(self, *args)
276 return orig(self, *args)
302 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
277 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
303
278
304 @command('^debugsparse', [
279 @command('^debugsparse', [
305 ('I', 'include', False, _('include files in the sparse checkout')),
280 ('I', 'include', False, _('include files in the sparse checkout')),
306 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
281 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
307 ('d', 'delete', False, _('delete an include/exclude rule')),
282 ('d', 'delete', False, _('delete an include/exclude rule')),
308 ('f', 'force', False, _('allow changing rules even with pending changes')),
283 ('f', 'force', False, _('allow changing rules even with pending changes')),
309 ('', 'enable-profile', False, _('enables the specified profile')),
284 ('', 'enable-profile', False, _('enables the specified profile')),
310 ('', 'disable-profile', False, _('disables the specified profile')),
285 ('', 'disable-profile', False, _('disables the specified profile')),
311 ('', 'import-rules', False, _('imports rules from a file')),
286 ('', 'import-rules', False, _('imports rules from a file')),
312 ('', 'clear-rules', False, _('clears local include/exclude rules')),
287 ('', 'clear-rules', False, _('clears local include/exclude rules')),
313 ('', 'refresh', False, _('updates the working after sparseness changes')),
288 ('', 'refresh', False, _('updates the working after sparseness changes')),
314 ('', 'reset', False, _('makes the repo full again')),
289 ('', 'reset', False, _('makes the repo full again')),
315 ] + commands.templateopts,
290 ] + commands.templateopts,
316 _('[--OPTION] PATTERN...'))
291 _('[--OPTION] PATTERN...'))
317 def debugsparse(ui, repo, *pats, **opts):
292 def debugsparse(ui, repo, *pats, **opts):
318 """make the current checkout sparse, or edit the existing checkout
293 """make the current checkout sparse, or edit the existing checkout
319
294
320 The sparse command is used to make the current checkout sparse.
295 The sparse command is used to make the current checkout sparse.
321 This means files that don't meet the sparse condition will not be
296 This means files that don't meet the sparse condition will not be
322 written to disk, or show up in any working copy operations. It does
297 written to disk, or show up in any working copy operations. It does
323 not affect files in history in any way.
298 not affect files in history in any way.
324
299
325 Passing no arguments prints the currently applied sparse rules.
300 Passing no arguments prints the currently applied sparse rules.
326
301
327 --include and --exclude are used to add and remove files from the sparse
302 --include and --exclude are used to add and remove files from the sparse
328 checkout. The effects of adding an include or exclude rule are applied
303 checkout. The effects of adding an include or exclude rule are applied
329 immediately. If applying the new rule would cause a file with pending
304 immediately. If applying the new rule would cause a file with pending
330 changes to be added or removed, the command will fail. Pass --force to
305 changes to be added or removed, the command will fail. Pass --force to
331 force a rule change even with pending changes (the changes on disk will
306 force a rule change even with pending changes (the changes on disk will
332 be preserved).
307 be preserved).
333
308
334 --delete removes an existing include/exclude rule. The effects are
309 --delete removes an existing include/exclude rule. The effects are
335 immediate.
310 immediate.
336
311
337 --refresh refreshes the files on disk based on the sparse rules. This is
312 --refresh refreshes the files on disk based on the sparse rules. This is
338 only necessary if .hg/sparse was changed by hand.
313 only necessary if .hg/sparse was changed by hand.
339
314
340 --enable-profile and --disable-profile accept a path to a .hgsparse file.
315 --enable-profile and --disable-profile accept a path to a .hgsparse file.
341 This allows defining sparse checkouts and tracking them inside the
316 This allows defining sparse checkouts and tracking them inside the
342 repository. This is useful for defining commonly used sparse checkouts for
317 repository. This is useful for defining commonly used sparse checkouts for
343 many people to use. As the profile definition changes over time, the sparse
318 many people to use. As the profile definition changes over time, the sparse
344 checkout will automatically be updated appropriately, depending on which
319 checkout will automatically be updated appropriately, depending on which
345 changeset is checked out. Changes to .hgsparse are not applied until they
320 changeset is checked out. Changes to .hgsparse are not applied until they
346 have been committed.
321 have been committed.
347
322
348 --import-rules accepts a path to a file containing rules in the .hgsparse
323 --import-rules accepts a path to a file containing rules in the .hgsparse
349 format, allowing you to add --include, --exclude and --enable-profile rules
324 format, allowing you to add --include, --exclude and --enable-profile rules
350 in bulk. Like the --include, --exclude and --enable-profile switches, the
325 in bulk. Like the --include, --exclude and --enable-profile switches, the
351 changes are applied immediately.
326 changes are applied immediately.
352
327
353 --clear-rules removes all local include and exclude rules, while leaving
328 --clear-rules removes all local include and exclude rules, while leaving
354 any enabled profiles in place.
329 any enabled profiles in place.
355
330
356 Returns 0 if editing the sparse checkout succeeds.
331 Returns 0 if editing the sparse checkout succeeds.
357 """
332 """
358 include = opts.get('include')
333 include = opts.get('include')
359 exclude = opts.get('exclude')
334 exclude = opts.get('exclude')
360 force = opts.get('force')
335 force = opts.get('force')
361 enableprofile = opts.get('enable_profile')
336 enableprofile = opts.get('enable_profile')
362 disableprofile = opts.get('disable_profile')
337 disableprofile = opts.get('disable_profile')
363 importrules = opts.get('import_rules')
338 importrules = opts.get('import_rules')
364 clearrules = opts.get('clear_rules')
339 clearrules = opts.get('clear_rules')
365 delete = opts.get('delete')
340 delete = opts.get('delete')
366 refresh = opts.get('refresh')
341 refresh = opts.get('refresh')
367 reset = opts.get('reset')
342 reset = opts.get('reset')
368 count = sum([include, exclude, enableprofile, disableprofile, delete,
343 count = sum([include, exclude, enableprofile, disableprofile, delete,
369 importrules, refresh, clearrules, reset])
344 importrules, refresh, clearrules, reset])
370 if count > 1:
345 if count > 1:
371 raise error.Abort(_("too many flags specified"))
346 raise error.Abort(_("too many flags specified"))
372
347
373 if count == 0:
348 if count == 0:
374 if repo.vfs.exists('sparse'):
349 if repo.vfs.exists('sparse'):
375 ui.status(repo.vfs.read("sparse") + "\n")
350 ui.status(repo.vfs.read("sparse") + "\n")
376 temporaryincludes = sparse.readtemporaryincludes(repo)
351 temporaryincludes = sparse.readtemporaryincludes(repo)
377 if temporaryincludes:
352 if temporaryincludes:
378 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
353 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
379 ui.status(("\n".join(temporaryincludes) + "\n"))
354 ui.status(("\n".join(temporaryincludes) + "\n"))
380 else:
355 else:
381 ui.status(_('repo is not sparse\n'))
356 ui.status(_('repo is not sparse\n'))
382 return
357 return
383
358
384 if include or exclude or delete or reset or enableprofile or disableprofile:
359 if include or exclude or delete or reset or enableprofile or disableprofile:
385 _config(ui, repo, pats, opts, include=include, exclude=exclude,
360 _config(ui, repo, pats, opts, include=include, exclude=exclude,
386 reset=reset, delete=delete, enableprofile=enableprofile,
361 reset=reset, delete=delete, enableprofile=enableprofile,
387 disableprofile=disableprofile, force=force)
362 disableprofile=disableprofile, force=force)
388
363
389 if importrules:
364 if importrules:
390 _import(ui, repo, pats, opts, force=force)
365 _import(ui, repo, pats, opts, force=force)
391
366
392 if clearrules:
367 if clearrules:
393 _clear(ui, repo, pats, force=force)
368 _clear(ui, repo, pats, force=force)
394
369
395 if refresh:
370 if refresh:
396 try:
371 try:
397 wlock = repo.wlock()
372 wlock = repo.wlock()
398 fcounts = map(
373 fcounts = map(
399 len,
374 len,
400 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
375 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
401 force=force))
376 force=force))
402 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
377 _verbose_output(ui, opts, 0, 0, 0, *fcounts)
403 finally:
378 finally:
404 wlock.release()
379 wlock.release()
405
380
406 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
381 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
407 delete=False, enableprofile=False, disableprofile=False,
382 delete=False, enableprofile=False, disableprofile=False,
408 force=False):
383 force=False):
409 """
384 """
410 Perform a sparse config update. Only one of the kwargs may be specified.
385 Perform a sparse config update. Only one of the kwargs may be specified.
411 """
386 """
412 wlock = repo.wlock()
387 wlock = repo.wlock()
413 try:
388 try:
414 oldsparsematch = sparse.matcher(repo)
389 oldsparsematch = sparse.matcher(repo)
415
390
416 raw = repo.vfs.tryread('sparse')
391 raw = repo.vfs.tryread('sparse')
417 if raw:
392 if raw:
418 oldinclude, oldexclude, oldprofiles = map(
393 oldinclude, oldexclude, oldprofiles = map(
419 set, sparse.parseconfig(ui, raw))
394 set, sparse.parseconfig(ui, raw))
420 else:
395 else:
421 oldinclude = set()
396 oldinclude = set()
422 oldexclude = set()
397 oldexclude = set()
423 oldprofiles = set()
398 oldprofiles = set()
424
399
425 try:
400 try:
426 if reset:
401 if reset:
427 newinclude = set()
402 newinclude = set()
428 newexclude = set()
403 newexclude = set()
429 newprofiles = set()
404 newprofiles = set()
430 else:
405 else:
431 newinclude = set(oldinclude)
406 newinclude = set(oldinclude)
432 newexclude = set(oldexclude)
407 newexclude = set(oldexclude)
433 newprofiles = set(oldprofiles)
408 newprofiles = set(oldprofiles)
434
409
435 oldstatus = repo.status()
410 oldstatus = repo.status()
436
411
437 if any(pat.startswith('/') for pat in pats):
412 if any(pat.startswith('/') for pat in pats):
438 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
413 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
439 % ([pat for pat in pats if pat.startswith('/')]))
414 % ([pat for pat in pats if pat.startswith('/')]))
440 elif include:
415 elif include:
441 newinclude.update(pats)
416 newinclude.update(pats)
442 elif exclude:
417 elif exclude:
443 newexclude.update(pats)
418 newexclude.update(pats)
444 elif enableprofile:
419 elif enableprofile:
445 newprofiles.update(pats)
420 newprofiles.update(pats)
446 elif disableprofile:
421 elif disableprofile:
447 newprofiles.difference_update(pats)
422 newprofiles.difference_update(pats)
448 elif delete:
423 elif delete:
449 newinclude.difference_update(pats)
424 newinclude.difference_update(pats)
450 newexclude.difference_update(pats)
425 newexclude.difference_update(pats)
451
426
452 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
427 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
453
428
454 fcounts = map(
429 fcounts = map(
455 len,
430 len,
456 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
431 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
457 force=force))
432 force=force))
458
433
459 profilecount = (len(newprofiles - oldprofiles) -
434 profilecount = (len(newprofiles - oldprofiles) -
460 len(oldprofiles - newprofiles))
435 len(oldprofiles - newprofiles))
461 includecount = (len(newinclude - oldinclude) -
436 includecount = (len(newinclude - oldinclude) -
462 len(oldinclude - newinclude))
437 len(oldinclude - newinclude))
463 excludecount = (len(newexclude - oldexclude) -
438 excludecount = (len(newexclude - oldexclude) -
464 len(oldexclude - newexclude))
439 len(oldexclude - newexclude))
465 _verbose_output(
440 _verbose_output(
466 ui, opts, profilecount, includecount, excludecount, *fcounts)
441 ui, opts, profilecount, includecount, excludecount, *fcounts)
467 except Exception:
442 except Exception:
468 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
443 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
469 raise
444 raise
470 finally:
445 finally:
471 wlock.release()
446 wlock.release()
472
447
473 def _import(ui, repo, files, opts, force=False):
448 def _import(ui, repo, files, opts, force=False):
474 with repo.wlock():
449 with repo.wlock():
475 # load union of current active profile
450 # load union of current active profile
476 revs = [repo.changelog.rev(node) for node in
451 revs = [repo.changelog.rev(node) for node in
477 repo.dirstate.parents() if node != nullid]
452 repo.dirstate.parents() if node != nullid]
478
453
479 # read current configuration
454 # read current configuration
480 raw = repo.vfs.tryread('sparse')
455 raw = repo.vfs.tryread('sparse')
481 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
456 oincludes, oexcludes, oprofiles = sparse.parseconfig(ui, raw)
482 includes, excludes, profiles = map(
457 includes, excludes, profiles = map(
483 set, (oincludes, oexcludes, oprofiles))
458 set, (oincludes, oexcludes, oprofiles))
484
459
485 # all active rules
460 # all active rules
486 aincludes, aexcludes, aprofiles = set(), set(), set()
461 aincludes, aexcludes, aprofiles = set(), set(), set()
487 for rev in revs:
462 for rev in revs:
488 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
463 rincludes, rexcludes, rprofiles = sparse.patternsforrev(repo, rev)
489 aincludes.update(rincludes)
464 aincludes.update(rincludes)
490 aexcludes.update(rexcludes)
465 aexcludes.update(rexcludes)
491 aprofiles.update(rprofiles)
466 aprofiles.update(rprofiles)
492
467
493 # import rules on top; only take in rules that are not yet
468 # import rules on top; only take in rules that are not yet
494 # part of the active rules.
469 # part of the active rules.
495 changed = False
470 changed = False
496 for file in files:
471 for file in files:
497 with util.posixfile(util.expandpath(file)) as importfile:
472 with util.posixfile(util.expandpath(file)) as importfile:
498 iincludes, iexcludes, iprofiles = sparse.parseconfig(
473 iincludes, iexcludes, iprofiles = sparse.parseconfig(
499 ui, importfile.read())
474 ui, importfile.read())
500 oldsize = len(includes) + len(excludes) + len(profiles)
475 oldsize = len(includes) + len(excludes) + len(profiles)
501 includes.update(iincludes - aincludes)
476 includes.update(iincludes - aincludes)
502 excludes.update(iexcludes - aexcludes)
477 excludes.update(iexcludes - aexcludes)
503 profiles.update(set(iprofiles) - aprofiles)
478 profiles.update(set(iprofiles) - aprofiles)
504 if len(includes) + len(excludes) + len(profiles) > oldsize:
479 if len(includes) + len(excludes) + len(profiles) > oldsize:
505 changed = True
480 changed = True
506
481
507 profilecount = includecount = excludecount = 0
482 profilecount = includecount = excludecount = 0
508 fcounts = (0, 0, 0)
483 fcounts = (0, 0, 0)
509
484
510 if changed:
485 if changed:
511 profilecount = len(profiles - aprofiles)
486 profilecount = len(profiles - aprofiles)
512 includecount = len(includes - aincludes)
487 includecount = len(includes - aincludes)
513 excludecount = len(excludes - aexcludes)
488 excludecount = len(excludes - aexcludes)
514
489
515 oldstatus = repo.status()
490 oldstatus = repo.status()
516 oldsparsematch = sparse.matcher(repo)
491 oldsparsematch = sparse.matcher(repo)
517 sparse.writeconfig(repo, includes, excludes, profiles)
492 sparse.writeconfig(repo, includes, excludes, profiles)
518
493
519 try:
494 try:
520 fcounts = map(
495 fcounts = map(
521 len,
496 len,
522 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
497 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
523 force=force))
498 force=force))
524 except Exception:
499 except Exception:
525 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
500 sparse.writeconfig(repo, oincludes, oexcludes, oprofiles)
526 raise
501 raise
527
502
528 _verbose_output(ui, opts, profilecount, includecount, excludecount,
503 _verbose_output(ui, opts, profilecount, includecount, excludecount,
529 *fcounts)
504 *fcounts)
530
505
531 def _clear(ui, repo, files, force=False):
506 def _clear(ui, repo, files, force=False):
532 with repo.wlock():
507 with repo.wlock():
533 raw = repo.vfs.tryread('sparse')
508 raw = repo.vfs.tryread('sparse')
534 includes, excludes, profiles = sparse.parseconfig(ui, raw)
509 includes, excludes, profiles = sparse.parseconfig(ui, raw)
535
510
536 if includes or excludes:
511 if includes or excludes:
537 oldstatus = repo.status()
512 oldstatus = repo.status()
538 oldsparsematch = sparse.matcher(repo)
513 oldsparsematch = sparse.matcher(repo)
539 sparse.writeconfig(repo, set(), set(), profiles)
514 sparse.writeconfig(repo, set(), set(), profiles)
540 sparse.refreshwdir(repo, oldstatus, oldsparsematch, force)
515 sparse.refreshwdir(repo, oldstatus, oldsparsematch, force)
541
516
542 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
517 def _verbose_output(ui, opts, profilecount, includecount, excludecount, added,
543 dropped, lookup):
518 dropped, lookup):
544 """Produce --verbose and templatable output
519 """Produce --verbose and templatable output
545
520
546 This specifically enables -Tjson, providing machine-readable stats on how
521 This specifically enables -Tjson, providing machine-readable stats on how
547 the sparse profile changed.
522 the sparse profile changed.
548
523
549 """
524 """
550 with ui.formatter('sparse', opts) as fm:
525 with ui.formatter('sparse', opts) as fm:
551 fm.startitem()
526 fm.startitem()
552 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
527 fm.condwrite(ui.verbose, 'profiles_added', 'Profile # change: %d\n',
553 profilecount)
528 profilecount)
554 fm.condwrite(ui.verbose, 'include_rules_added',
529 fm.condwrite(ui.verbose, 'include_rules_added',
555 'Include rule # change: %d\n', includecount)
530 'Include rule # change: %d\n', includecount)
556 fm.condwrite(ui.verbose, 'exclude_rules_added',
531 fm.condwrite(ui.verbose, 'exclude_rules_added',
557 'Exclude rule # change: %d\n', excludecount)
532 'Exclude rule # change: %d\n', excludecount)
558 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
533 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
559 # files are added or removed outside of the templating formatter
534 # files are added or removed outside of the templating formatter
560 # framework. No point in repeating ourselves in that case.
535 # framework. No point in repeating ourselves in that case.
561 if not fm.isplain():
536 if not fm.isplain():
562 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
537 fm.condwrite(ui.verbose, 'files_added', 'Files added: %d\n',
563 added)
538 added)
564 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
539 fm.condwrite(ui.verbose, 'files_dropped', 'Files dropped: %d\n',
565 dropped)
540 dropped)
566 fm.condwrite(ui.verbose, 'files_conflicting',
541 fm.condwrite(ui.verbose, 'files_conflicting',
567 'Files conflicting: %d\n', lookup)
542 'Files conflicting: %d\n', lookup)
@@ -1,2316 +1,2322
1 # context.py - changeset and file context objects for mercurial
1 # context.py - changeset and file context objects for mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import os
11 import os
12 import re
12 import re
13 import stat
13 import stat
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 addednodeid,
17 addednodeid,
18 bin,
18 bin,
19 hex,
19 hex,
20 modifiednodeid,
20 modifiednodeid,
21 nullid,
21 nullid,
22 nullrev,
22 nullrev,
23 short,
23 short,
24 wdirid,
24 wdirid,
25 wdirnodes,
25 wdirnodes,
26 wdirrev,
26 wdirrev,
27 )
27 )
28 from . import (
28 from . import (
29 encoding,
29 encoding,
30 error,
30 error,
31 fileset,
31 fileset,
32 match as matchmod,
32 match as matchmod,
33 mdiff,
33 mdiff,
34 obsolete as obsmod,
34 obsolete as obsmod,
35 patch,
35 patch,
36 phases,
36 phases,
37 pycompat,
37 pycompat,
38 repoview,
38 repoview,
39 revlog,
39 revlog,
40 scmutil,
40 scmutil,
41 sparse,
41 subrepo,
42 subrepo,
42 util,
43 util,
43 )
44 )
44
45
45 propertycache = util.propertycache
46 propertycache = util.propertycache
46
47
47 nonascii = re.compile(r'[^\x21-\x7f]').search
48 nonascii = re.compile(r'[^\x21-\x7f]').search
48
49
49 class basectx(object):
50 class basectx(object):
50 """A basectx object represents the common logic for its children:
51 """A basectx object represents the common logic for its children:
51 changectx: read-only context that is already present in the repo,
52 changectx: read-only context that is already present in the repo,
52 workingctx: a context that represents the working directory and can
53 workingctx: a context that represents the working directory and can
53 be committed,
54 be committed,
54 memctx: a context that represents changes in-memory and can also
55 memctx: a context that represents changes in-memory and can also
55 be committed."""
56 be committed."""
56 def __new__(cls, repo, changeid='', *args, **kwargs):
57 def __new__(cls, repo, changeid='', *args, **kwargs):
57 if isinstance(changeid, basectx):
58 if isinstance(changeid, basectx):
58 return changeid
59 return changeid
59
60
60 o = super(basectx, cls).__new__(cls)
61 o = super(basectx, cls).__new__(cls)
61
62
62 o._repo = repo
63 o._repo = repo
63 o._rev = nullrev
64 o._rev = nullrev
64 o._node = nullid
65 o._node = nullid
65
66
66 return o
67 return o
67
68
68 def __bytes__(self):
69 def __bytes__(self):
69 return short(self.node())
70 return short(self.node())
70
71
71 __str__ = encoding.strmethod(__bytes__)
72 __str__ = encoding.strmethod(__bytes__)
72
73
73 def __int__(self):
74 def __int__(self):
74 return self.rev()
75 return self.rev()
75
76
76 def __repr__(self):
77 def __repr__(self):
77 return r"<%s %s>" % (type(self).__name__, str(self))
78 return r"<%s %s>" % (type(self).__name__, str(self))
78
79
79 def __eq__(self, other):
80 def __eq__(self, other):
80 try:
81 try:
81 return type(self) == type(other) and self._rev == other._rev
82 return type(self) == type(other) and self._rev == other._rev
82 except AttributeError:
83 except AttributeError:
83 return False
84 return False
84
85
85 def __ne__(self, other):
86 def __ne__(self, other):
86 return not (self == other)
87 return not (self == other)
87
88
88 def __contains__(self, key):
89 def __contains__(self, key):
89 return key in self._manifest
90 return key in self._manifest
90
91
91 def __getitem__(self, key):
92 def __getitem__(self, key):
92 return self.filectx(key)
93 return self.filectx(key)
93
94
94 def __iter__(self):
95 def __iter__(self):
95 return iter(self._manifest)
96 return iter(self._manifest)
96
97
97 def _buildstatusmanifest(self, status):
98 def _buildstatusmanifest(self, status):
98 """Builds a manifest that includes the given status results, if this is
99 """Builds a manifest that includes the given status results, if this is
99 a working copy context. For non-working copy contexts, it just returns
100 a working copy context. For non-working copy contexts, it just returns
100 the normal manifest."""
101 the normal manifest."""
101 return self.manifest()
102 return self.manifest()
102
103
103 def _matchstatus(self, other, match):
104 def _matchstatus(self, other, match):
104 """return match.always if match is none
105 """return match.always if match is none
105
106
106 This internal method provides a way for child objects to override the
107 This internal method provides a way for child objects to override the
107 match operator.
108 match operator.
108 """
109 """
109 return match or matchmod.always(self._repo.root, self._repo.getcwd())
110 return match or matchmod.always(self._repo.root, self._repo.getcwd())
110
111
111 def _buildstatus(self, other, s, match, listignored, listclean,
112 def _buildstatus(self, other, s, match, listignored, listclean,
112 listunknown):
113 listunknown):
113 """build a status with respect to another context"""
114 """build a status with respect to another context"""
114 # Load earliest manifest first for caching reasons. More specifically,
115 # Load earliest manifest first for caching reasons. More specifically,
115 # if you have revisions 1000 and 1001, 1001 is probably stored as a
116 # if you have revisions 1000 and 1001, 1001 is probably stored as a
116 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
117 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
117 # 1000 and cache it so that when you read 1001, we just need to apply a
118 # 1000 and cache it so that when you read 1001, we just need to apply a
118 # delta to what's in the cache. So that's one full reconstruction + one
119 # delta to what's in the cache. So that's one full reconstruction + one
119 # delta application.
120 # delta application.
120 mf2 = None
121 mf2 = None
121 if self.rev() is not None and self.rev() < other.rev():
122 if self.rev() is not None and self.rev() < other.rev():
122 mf2 = self._buildstatusmanifest(s)
123 mf2 = self._buildstatusmanifest(s)
123 mf1 = other._buildstatusmanifest(s)
124 mf1 = other._buildstatusmanifest(s)
124 if mf2 is None:
125 if mf2 is None:
125 mf2 = self._buildstatusmanifest(s)
126 mf2 = self._buildstatusmanifest(s)
126
127
127 modified, added = [], []
128 modified, added = [], []
128 removed = []
129 removed = []
129 clean = []
130 clean = []
130 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
131 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
131 deletedset = set(deleted)
132 deletedset = set(deleted)
132 d = mf1.diff(mf2, match=match, clean=listclean)
133 d = mf1.diff(mf2, match=match, clean=listclean)
133 for fn, value in d.iteritems():
134 for fn, value in d.iteritems():
134 if fn in deletedset:
135 if fn in deletedset:
135 continue
136 continue
136 if value is None:
137 if value is None:
137 clean.append(fn)
138 clean.append(fn)
138 continue
139 continue
139 (node1, flag1), (node2, flag2) = value
140 (node1, flag1), (node2, flag2) = value
140 if node1 is None:
141 if node1 is None:
141 added.append(fn)
142 added.append(fn)
142 elif node2 is None:
143 elif node2 is None:
143 removed.append(fn)
144 removed.append(fn)
144 elif flag1 != flag2:
145 elif flag1 != flag2:
145 modified.append(fn)
146 modified.append(fn)
146 elif node2 not in wdirnodes:
147 elif node2 not in wdirnodes:
147 # When comparing files between two commits, we save time by
148 # When comparing files between two commits, we save time by
148 # not comparing the file contents when the nodeids differ.
149 # not comparing the file contents when the nodeids differ.
149 # Note that this means we incorrectly report a reverted change
150 # Note that this means we incorrectly report a reverted change
150 # to a file as a modification.
151 # to a file as a modification.
151 modified.append(fn)
152 modified.append(fn)
152 elif self[fn].cmp(other[fn]):
153 elif self[fn].cmp(other[fn]):
153 modified.append(fn)
154 modified.append(fn)
154 else:
155 else:
155 clean.append(fn)
156 clean.append(fn)
156
157
157 if removed:
158 if removed:
158 # need to filter files if they are already reported as removed
159 # need to filter files if they are already reported as removed
159 unknown = [fn for fn in unknown if fn not in mf1 and
160 unknown = [fn for fn in unknown if fn not in mf1 and
160 (not match or match(fn))]
161 (not match or match(fn))]
161 ignored = [fn for fn in ignored if fn not in mf1 and
162 ignored = [fn for fn in ignored if fn not in mf1 and
162 (not match or match(fn))]
163 (not match or match(fn))]
163 # if they're deleted, don't report them as removed
164 # if they're deleted, don't report them as removed
164 removed = [fn for fn in removed if fn not in deletedset]
165 removed = [fn for fn in removed if fn not in deletedset]
165
166
166 return scmutil.status(modified, added, removed, deleted, unknown,
167 return scmutil.status(modified, added, removed, deleted, unknown,
167 ignored, clean)
168 ignored, clean)
168
169
169 @propertycache
170 @propertycache
170 def substate(self):
171 def substate(self):
171 return subrepo.state(self, self._repo.ui)
172 return subrepo.state(self, self._repo.ui)
172
173
173 def subrev(self, subpath):
174 def subrev(self, subpath):
174 return self.substate[subpath][1]
175 return self.substate[subpath][1]
175
176
176 def rev(self):
177 def rev(self):
177 return self._rev
178 return self._rev
178 def node(self):
179 def node(self):
179 return self._node
180 return self._node
180 def hex(self):
181 def hex(self):
181 return hex(self.node())
182 return hex(self.node())
182 def manifest(self):
183 def manifest(self):
183 return self._manifest
184 return self._manifest
184 def manifestctx(self):
185 def manifestctx(self):
185 return self._manifestctx
186 return self._manifestctx
186 def repo(self):
187 def repo(self):
187 return self._repo
188 return self._repo
188 def phasestr(self):
189 def phasestr(self):
189 return phases.phasenames[self.phase()]
190 return phases.phasenames[self.phase()]
190 def mutable(self):
191 def mutable(self):
191 return self.phase() > phases.public
192 return self.phase() > phases.public
192
193
193 def getfileset(self, expr):
194 def getfileset(self, expr):
194 return fileset.getfileset(self, expr)
195 return fileset.getfileset(self, expr)
195
196
196 def obsolete(self):
197 def obsolete(self):
197 """True if the changeset is obsolete"""
198 """True if the changeset is obsolete"""
198 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
199 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
199
200
200 def extinct(self):
201 def extinct(self):
201 """True if the changeset is extinct"""
202 """True if the changeset is extinct"""
202 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
203 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
203
204
204 def unstable(self):
205 def unstable(self):
205 """True if the changeset is not obsolete but it's ancestor are"""
206 """True if the changeset is not obsolete but it's ancestor are"""
206 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
207 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
207
208
208 def bumped(self):
209 def bumped(self):
209 """True if the changeset try to be a successor of a public changeset
210 """True if the changeset try to be a successor of a public changeset
210
211
211 Only non-public and non-obsolete changesets may be bumped.
212 Only non-public and non-obsolete changesets may be bumped.
212 """
213 """
213 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
214 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
214
215
215 def divergent(self):
216 def divergent(self):
216 """Is a successors of a changeset with multiple possible successors set
217 """Is a successors of a changeset with multiple possible successors set
217
218
218 Only non-public and non-obsolete changesets may be divergent.
219 Only non-public and non-obsolete changesets may be divergent.
219 """
220 """
220 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
221 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
221
222
222 def troubled(self):
223 def troubled(self):
223 """True if the changeset is either unstable, bumped or divergent"""
224 """True if the changeset is either unstable, bumped or divergent"""
224 return self.unstable() or self.bumped() or self.divergent()
225 return self.unstable() or self.bumped() or self.divergent()
225
226
226 def troubles(self):
227 def troubles(self):
227 """return the list of troubles affecting this changesets.
228 """return the list of troubles affecting this changesets.
228
229
229 Troubles are returned as strings. possible values are:
230 Troubles are returned as strings. possible values are:
230 - unstable,
231 - unstable,
231 - bumped,
232 - bumped,
232 - divergent.
233 - divergent.
233 """
234 """
234 troubles = []
235 troubles = []
235 if self.unstable():
236 if self.unstable():
236 troubles.append('unstable')
237 troubles.append('unstable')
237 if self.bumped():
238 if self.bumped():
238 troubles.append('bumped')
239 troubles.append('bumped')
239 if self.divergent():
240 if self.divergent():
240 troubles.append('divergent')
241 troubles.append('divergent')
241 return troubles
242 return troubles
242
243
243 def parents(self):
244 def parents(self):
244 """return contexts for each parent changeset"""
245 """return contexts for each parent changeset"""
245 return self._parents
246 return self._parents
246
247
247 def p1(self):
248 def p1(self):
248 return self._parents[0]
249 return self._parents[0]
249
250
250 def p2(self):
251 def p2(self):
251 parents = self._parents
252 parents = self._parents
252 if len(parents) == 2:
253 if len(parents) == 2:
253 return parents[1]
254 return parents[1]
254 return changectx(self._repo, nullrev)
255 return changectx(self._repo, nullrev)
255
256
256 def _fileinfo(self, path):
257 def _fileinfo(self, path):
257 if r'_manifest' in self.__dict__:
258 if r'_manifest' in self.__dict__:
258 try:
259 try:
259 return self._manifest[path], self._manifest.flags(path)
260 return self._manifest[path], self._manifest.flags(path)
260 except KeyError:
261 except KeyError:
261 raise error.ManifestLookupError(self._node, path,
262 raise error.ManifestLookupError(self._node, path,
262 _('not found in manifest'))
263 _('not found in manifest'))
263 if r'_manifestdelta' in self.__dict__ or path in self.files():
264 if r'_manifestdelta' in self.__dict__ or path in self.files():
264 if path in self._manifestdelta:
265 if path in self._manifestdelta:
265 return (self._manifestdelta[path],
266 return (self._manifestdelta[path],
266 self._manifestdelta.flags(path))
267 self._manifestdelta.flags(path))
267 mfl = self._repo.manifestlog
268 mfl = self._repo.manifestlog
268 try:
269 try:
269 node, flag = mfl[self._changeset.manifest].find(path)
270 node, flag = mfl[self._changeset.manifest].find(path)
270 except KeyError:
271 except KeyError:
271 raise error.ManifestLookupError(self._node, path,
272 raise error.ManifestLookupError(self._node, path,
272 _('not found in manifest'))
273 _('not found in manifest'))
273
274
274 return node, flag
275 return node, flag
275
276
276 def filenode(self, path):
277 def filenode(self, path):
277 return self._fileinfo(path)[0]
278 return self._fileinfo(path)[0]
278
279
279 def flags(self, path):
280 def flags(self, path):
280 try:
281 try:
281 return self._fileinfo(path)[1]
282 return self._fileinfo(path)[1]
282 except error.LookupError:
283 except error.LookupError:
283 return ''
284 return ''
284
285
285 def sub(self, path, allowcreate=True):
286 def sub(self, path, allowcreate=True):
286 '''return a subrepo for the stored revision of path, never wdir()'''
287 '''return a subrepo for the stored revision of path, never wdir()'''
287 return subrepo.subrepo(self, path, allowcreate=allowcreate)
288 return subrepo.subrepo(self, path, allowcreate=allowcreate)
288
289
289 def nullsub(self, path, pctx):
290 def nullsub(self, path, pctx):
290 return subrepo.nullsubrepo(self, path, pctx)
291 return subrepo.nullsubrepo(self, path, pctx)
291
292
292 def workingsub(self, path):
293 def workingsub(self, path):
293 '''return a subrepo for the stored revision, or wdir if this is a wdir
294 '''return a subrepo for the stored revision, or wdir if this is a wdir
294 context.
295 context.
295 '''
296 '''
296 return subrepo.subrepo(self, path, allowwdir=True)
297 return subrepo.subrepo(self, path, allowwdir=True)
297
298
298 def match(self, pats=None, include=None, exclude=None, default='glob',
299 def match(self, pats=None, include=None, exclude=None, default='glob',
299 listsubrepos=False, badfn=None):
300 listsubrepos=False, badfn=None):
300 r = self._repo
301 r = self._repo
301 return matchmod.match(r.root, r.getcwd(), pats,
302 return matchmod.match(r.root, r.getcwd(), pats,
302 include, exclude, default,
303 include, exclude, default,
303 auditor=r.nofsauditor, ctx=self,
304 auditor=r.nofsauditor, ctx=self,
304 listsubrepos=listsubrepos, badfn=badfn)
305 listsubrepos=listsubrepos, badfn=badfn)
305
306
306 def diff(self, ctx2=None, match=None, **opts):
307 def diff(self, ctx2=None, match=None, **opts):
307 """Returns a diff generator for the given contexts and matcher"""
308 """Returns a diff generator for the given contexts and matcher"""
308 if ctx2 is None:
309 if ctx2 is None:
309 ctx2 = self.p1()
310 ctx2 = self.p1()
310 if ctx2 is not None:
311 if ctx2 is not None:
311 ctx2 = self._repo[ctx2]
312 ctx2 = self._repo[ctx2]
312 diffopts = patch.diffopts(self._repo.ui, opts)
313 diffopts = patch.diffopts(self._repo.ui, opts)
313 return patch.diff(self._repo, ctx2, self, match=match, opts=diffopts)
314 return patch.diff(self._repo, ctx2, self, match=match, opts=diffopts)
314
315
315 def dirs(self):
316 def dirs(self):
316 return self._manifest.dirs()
317 return self._manifest.dirs()
317
318
318 def hasdir(self, dir):
319 def hasdir(self, dir):
319 return self._manifest.hasdir(dir)
320 return self._manifest.hasdir(dir)
320
321
321 def status(self, other=None, match=None, listignored=False,
322 def status(self, other=None, match=None, listignored=False,
322 listclean=False, listunknown=False, listsubrepos=False):
323 listclean=False, listunknown=False, listsubrepos=False):
323 """return status of files between two nodes or node and working
324 """return status of files between two nodes or node and working
324 directory.
325 directory.
325
326
326 If other is None, compare this node with working directory.
327 If other is None, compare this node with working directory.
327
328
328 returns (modified, added, removed, deleted, unknown, ignored, clean)
329 returns (modified, added, removed, deleted, unknown, ignored, clean)
329 """
330 """
330
331
331 ctx1 = self
332 ctx1 = self
332 ctx2 = self._repo[other]
333 ctx2 = self._repo[other]
333
334
334 # This next code block is, admittedly, fragile logic that tests for
335 # This next code block is, admittedly, fragile logic that tests for
335 # reversing the contexts and wouldn't need to exist if it weren't for
336 # reversing the contexts and wouldn't need to exist if it weren't for
336 # the fast (and common) code path of comparing the working directory
337 # the fast (and common) code path of comparing the working directory
337 # with its first parent.
338 # with its first parent.
338 #
339 #
339 # What we're aiming for here is the ability to call:
340 # What we're aiming for here is the ability to call:
340 #
341 #
341 # workingctx.status(parentctx)
342 # workingctx.status(parentctx)
342 #
343 #
343 # If we always built the manifest for each context and compared those,
344 # If we always built the manifest for each context and compared those,
344 # then we'd be done. But the special case of the above call means we
345 # then we'd be done. But the special case of the above call means we
345 # just copy the manifest of the parent.
346 # just copy the manifest of the parent.
346 reversed = False
347 reversed = False
347 if (not isinstance(ctx1, changectx)
348 if (not isinstance(ctx1, changectx)
348 and isinstance(ctx2, changectx)):
349 and isinstance(ctx2, changectx)):
349 reversed = True
350 reversed = True
350 ctx1, ctx2 = ctx2, ctx1
351 ctx1, ctx2 = ctx2, ctx1
351
352
352 match = ctx2._matchstatus(ctx1, match)
353 match = ctx2._matchstatus(ctx1, match)
353 r = scmutil.status([], [], [], [], [], [], [])
354 r = scmutil.status([], [], [], [], [], [], [])
354 r = ctx2._buildstatus(ctx1, r, match, listignored, listclean,
355 r = ctx2._buildstatus(ctx1, r, match, listignored, listclean,
355 listunknown)
356 listunknown)
356
357
357 if reversed:
358 if reversed:
358 # Reverse added and removed. Clear deleted, unknown and ignored as
359 # Reverse added and removed. Clear deleted, unknown and ignored as
359 # these make no sense to reverse.
360 # these make no sense to reverse.
360 r = scmutil.status(r.modified, r.removed, r.added, [], [], [],
361 r = scmutil.status(r.modified, r.removed, r.added, [], [], [],
361 r.clean)
362 r.clean)
362
363
363 if listsubrepos:
364 if listsubrepos:
364 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
365 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
365 try:
366 try:
366 rev2 = ctx2.subrev(subpath)
367 rev2 = ctx2.subrev(subpath)
367 except KeyError:
368 except KeyError:
368 # A subrepo that existed in node1 was deleted between
369 # A subrepo that existed in node1 was deleted between
369 # node1 and node2 (inclusive). Thus, ctx2's substate
370 # node1 and node2 (inclusive). Thus, ctx2's substate
370 # won't contain that subpath. The best we can do ignore it.
371 # won't contain that subpath. The best we can do ignore it.
371 rev2 = None
372 rev2 = None
372 submatch = matchmod.subdirmatcher(subpath, match)
373 submatch = matchmod.subdirmatcher(subpath, match)
373 s = sub.status(rev2, match=submatch, ignored=listignored,
374 s = sub.status(rev2, match=submatch, ignored=listignored,
374 clean=listclean, unknown=listunknown,
375 clean=listclean, unknown=listunknown,
375 listsubrepos=True)
376 listsubrepos=True)
376 for rfiles, sfiles in zip(r, s):
377 for rfiles, sfiles in zip(r, s):
377 rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
378 rfiles.extend("%s/%s" % (subpath, f) for f in sfiles)
378
379
379 for l in r:
380 for l in r:
380 l.sort()
381 l.sort()
381
382
382 return r
383 return r
383
384
384 def _filterederror(repo, changeid):
385 def _filterederror(repo, changeid):
385 """build an exception to be raised about a filtered changeid
386 """build an exception to be raised about a filtered changeid
386
387
387 This is extracted in a function to help extensions (eg: evolve) to
388 This is extracted in a function to help extensions (eg: evolve) to
388 experiment with various message variants."""
389 experiment with various message variants."""
389 if repo.filtername.startswith('visible'):
390 if repo.filtername.startswith('visible'):
390 msg = _("hidden revision '%s'") % changeid
391 msg = _("hidden revision '%s'") % changeid
391 hint = _('use --hidden to access hidden revisions')
392 hint = _('use --hidden to access hidden revisions')
392 return error.FilteredRepoLookupError(msg, hint=hint)
393 return error.FilteredRepoLookupError(msg, hint=hint)
393 msg = _("filtered revision '%s' (not in '%s' subset)")
394 msg = _("filtered revision '%s' (not in '%s' subset)")
394 msg %= (changeid, repo.filtername)
395 msg %= (changeid, repo.filtername)
395 return error.FilteredRepoLookupError(msg)
396 return error.FilteredRepoLookupError(msg)
396
397
397 class changectx(basectx):
398 class changectx(basectx):
398 """A changecontext object makes access to data related to a particular
399 """A changecontext object makes access to data related to a particular
399 changeset convenient. It represents a read-only context already present in
400 changeset convenient. It represents a read-only context already present in
400 the repo."""
401 the repo."""
401 def __init__(self, repo, changeid=''):
402 def __init__(self, repo, changeid=''):
402 """changeid is a revision number, node, or tag"""
403 """changeid is a revision number, node, or tag"""
403
404
404 # since basectx.__new__ already took care of copying the object, we
405 # since basectx.__new__ already took care of copying the object, we
405 # don't need to do anything in __init__, so we just exit here
406 # don't need to do anything in __init__, so we just exit here
406 if isinstance(changeid, basectx):
407 if isinstance(changeid, basectx):
407 return
408 return
408
409
409 if changeid == '':
410 if changeid == '':
410 changeid = '.'
411 changeid = '.'
411 self._repo = repo
412 self._repo = repo
412
413
413 try:
414 try:
414 if isinstance(changeid, int):
415 if isinstance(changeid, int):
415 self._node = repo.changelog.node(changeid)
416 self._node = repo.changelog.node(changeid)
416 self._rev = changeid
417 self._rev = changeid
417 return
418 return
418 if not pycompat.ispy3 and isinstance(changeid, long):
419 if not pycompat.ispy3 and isinstance(changeid, long):
419 changeid = str(changeid)
420 changeid = str(changeid)
420 if changeid == 'null':
421 if changeid == 'null':
421 self._node = nullid
422 self._node = nullid
422 self._rev = nullrev
423 self._rev = nullrev
423 return
424 return
424 if changeid == 'tip':
425 if changeid == 'tip':
425 self._node = repo.changelog.tip()
426 self._node = repo.changelog.tip()
426 self._rev = repo.changelog.rev(self._node)
427 self._rev = repo.changelog.rev(self._node)
427 return
428 return
428 if changeid == '.' or changeid == repo.dirstate.p1():
429 if changeid == '.' or changeid == repo.dirstate.p1():
429 # this is a hack to delay/avoid loading obsmarkers
430 # this is a hack to delay/avoid loading obsmarkers
430 # when we know that '.' won't be hidden
431 # when we know that '.' won't be hidden
431 self._node = repo.dirstate.p1()
432 self._node = repo.dirstate.p1()
432 self._rev = repo.unfiltered().changelog.rev(self._node)
433 self._rev = repo.unfiltered().changelog.rev(self._node)
433 return
434 return
434 if len(changeid) == 20:
435 if len(changeid) == 20:
435 try:
436 try:
436 self._node = changeid
437 self._node = changeid
437 self._rev = repo.changelog.rev(changeid)
438 self._rev = repo.changelog.rev(changeid)
438 return
439 return
439 except error.FilteredRepoLookupError:
440 except error.FilteredRepoLookupError:
440 raise
441 raise
441 except LookupError:
442 except LookupError:
442 pass
443 pass
443
444
444 try:
445 try:
445 r = int(changeid)
446 r = int(changeid)
446 if '%d' % r != changeid:
447 if '%d' % r != changeid:
447 raise ValueError
448 raise ValueError
448 l = len(repo.changelog)
449 l = len(repo.changelog)
449 if r < 0:
450 if r < 0:
450 r += l
451 r += l
451 if r < 0 or r >= l and r != wdirrev:
452 if r < 0 or r >= l and r != wdirrev:
452 raise ValueError
453 raise ValueError
453 self._rev = r
454 self._rev = r
454 self._node = repo.changelog.node(r)
455 self._node = repo.changelog.node(r)
455 return
456 return
456 except error.FilteredIndexError:
457 except error.FilteredIndexError:
457 raise
458 raise
458 except (ValueError, OverflowError, IndexError):
459 except (ValueError, OverflowError, IndexError):
459 pass
460 pass
460
461
461 if len(changeid) == 40:
462 if len(changeid) == 40:
462 try:
463 try:
463 self._node = bin(changeid)
464 self._node = bin(changeid)
464 self._rev = repo.changelog.rev(self._node)
465 self._rev = repo.changelog.rev(self._node)
465 return
466 return
466 except error.FilteredLookupError:
467 except error.FilteredLookupError:
467 raise
468 raise
468 except (TypeError, LookupError):
469 except (TypeError, LookupError):
469 pass
470 pass
470
471
471 # lookup bookmarks through the name interface
472 # lookup bookmarks through the name interface
472 try:
473 try:
473 self._node = repo.names.singlenode(repo, changeid)
474 self._node = repo.names.singlenode(repo, changeid)
474 self._rev = repo.changelog.rev(self._node)
475 self._rev = repo.changelog.rev(self._node)
475 return
476 return
476 except KeyError:
477 except KeyError:
477 pass
478 pass
478 except error.FilteredRepoLookupError:
479 except error.FilteredRepoLookupError:
479 raise
480 raise
480 except error.RepoLookupError:
481 except error.RepoLookupError:
481 pass
482 pass
482
483
483 self._node = repo.unfiltered().changelog._partialmatch(changeid)
484 self._node = repo.unfiltered().changelog._partialmatch(changeid)
484 if self._node is not None:
485 if self._node is not None:
485 self._rev = repo.changelog.rev(self._node)
486 self._rev = repo.changelog.rev(self._node)
486 return
487 return
487
488
488 # lookup failed
489 # lookup failed
489 # check if it might have come from damaged dirstate
490 # check if it might have come from damaged dirstate
490 #
491 #
491 # XXX we could avoid the unfiltered if we had a recognizable
492 # XXX we could avoid the unfiltered if we had a recognizable
492 # exception for filtered changeset access
493 # exception for filtered changeset access
493 if changeid in repo.unfiltered().dirstate.parents():
494 if changeid in repo.unfiltered().dirstate.parents():
494 msg = _("working directory has unknown parent '%s'!")
495 msg = _("working directory has unknown parent '%s'!")
495 raise error.Abort(msg % short(changeid))
496 raise error.Abort(msg % short(changeid))
496 try:
497 try:
497 if len(changeid) == 20 and nonascii(changeid):
498 if len(changeid) == 20 and nonascii(changeid):
498 changeid = hex(changeid)
499 changeid = hex(changeid)
499 except TypeError:
500 except TypeError:
500 pass
501 pass
501 except (error.FilteredIndexError, error.FilteredLookupError,
502 except (error.FilteredIndexError, error.FilteredLookupError,
502 error.FilteredRepoLookupError):
503 error.FilteredRepoLookupError):
503 raise _filterederror(repo, changeid)
504 raise _filterederror(repo, changeid)
504 except IndexError:
505 except IndexError:
505 pass
506 pass
506 raise error.RepoLookupError(
507 raise error.RepoLookupError(
507 _("unknown revision '%s'") % changeid)
508 _("unknown revision '%s'") % changeid)
508
509
509 def __hash__(self):
510 def __hash__(self):
510 try:
511 try:
511 return hash(self._rev)
512 return hash(self._rev)
512 except AttributeError:
513 except AttributeError:
513 return id(self)
514 return id(self)
514
515
515 def __nonzero__(self):
516 def __nonzero__(self):
516 return self._rev != nullrev
517 return self._rev != nullrev
517
518
518 __bool__ = __nonzero__
519 __bool__ = __nonzero__
519
520
520 @propertycache
521 @propertycache
521 def _changeset(self):
522 def _changeset(self):
522 return self._repo.changelog.changelogrevision(self.rev())
523 return self._repo.changelog.changelogrevision(self.rev())
523
524
524 @propertycache
525 @propertycache
525 def _manifest(self):
526 def _manifest(self):
526 return self._manifestctx.read()
527 return self._manifestctx.read()
527
528
528 @property
529 @property
529 def _manifestctx(self):
530 def _manifestctx(self):
530 return self._repo.manifestlog[self._changeset.manifest]
531 return self._repo.manifestlog[self._changeset.manifest]
531
532
532 @propertycache
533 @propertycache
533 def _manifestdelta(self):
534 def _manifestdelta(self):
534 return self._manifestctx.readdelta()
535 return self._manifestctx.readdelta()
535
536
536 @propertycache
537 @propertycache
537 def _parents(self):
538 def _parents(self):
538 repo = self._repo
539 repo = self._repo
539 p1, p2 = repo.changelog.parentrevs(self._rev)
540 p1, p2 = repo.changelog.parentrevs(self._rev)
540 if p2 == nullrev:
541 if p2 == nullrev:
541 return [changectx(repo, p1)]
542 return [changectx(repo, p1)]
542 return [changectx(repo, p1), changectx(repo, p2)]
543 return [changectx(repo, p1), changectx(repo, p2)]
543
544
544 def changeset(self):
545 def changeset(self):
545 c = self._changeset
546 c = self._changeset
546 return (
547 return (
547 c.manifest,
548 c.manifest,
548 c.user,
549 c.user,
549 c.date,
550 c.date,
550 c.files,
551 c.files,
551 c.description,
552 c.description,
552 c.extra,
553 c.extra,
553 )
554 )
554 def manifestnode(self):
555 def manifestnode(self):
555 return self._changeset.manifest
556 return self._changeset.manifest
556
557
557 def user(self):
558 def user(self):
558 return self._changeset.user
559 return self._changeset.user
559 def date(self):
560 def date(self):
560 return self._changeset.date
561 return self._changeset.date
561 def files(self):
562 def files(self):
562 return self._changeset.files
563 return self._changeset.files
563 def description(self):
564 def description(self):
564 return self._changeset.description
565 return self._changeset.description
565 def branch(self):
566 def branch(self):
566 return encoding.tolocal(self._changeset.extra.get("branch"))
567 return encoding.tolocal(self._changeset.extra.get("branch"))
567 def closesbranch(self):
568 def closesbranch(self):
568 return 'close' in self._changeset.extra
569 return 'close' in self._changeset.extra
569 def extra(self):
570 def extra(self):
570 return self._changeset.extra
571 return self._changeset.extra
571 def tags(self):
572 def tags(self):
572 return self._repo.nodetags(self._node)
573 return self._repo.nodetags(self._node)
573 def bookmarks(self):
574 def bookmarks(self):
574 return self._repo.nodebookmarks(self._node)
575 return self._repo.nodebookmarks(self._node)
575 def phase(self):
576 def phase(self):
576 return self._repo._phasecache.phase(self._repo, self._rev)
577 return self._repo._phasecache.phase(self._repo, self._rev)
577 def hidden(self):
578 def hidden(self):
578 return self._rev in repoview.filterrevs(self._repo, 'visible')
579 return self._rev in repoview.filterrevs(self._repo, 'visible')
579
580
580 def children(self):
581 def children(self):
581 """return contexts for each child changeset"""
582 """return contexts for each child changeset"""
582 c = self._repo.changelog.children(self._node)
583 c = self._repo.changelog.children(self._node)
583 return [changectx(self._repo, x) for x in c]
584 return [changectx(self._repo, x) for x in c]
584
585
585 def ancestors(self):
586 def ancestors(self):
586 for a in self._repo.changelog.ancestors([self._rev]):
587 for a in self._repo.changelog.ancestors([self._rev]):
587 yield changectx(self._repo, a)
588 yield changectx(self._repo, a)
588
589
589 def descendants(self):
590 def descendants(self):
590 for d in self._repo.changelog.descendants([self._rev]):
591 for d in self._repo.changelog.descendants([self._rev]):
591 yield changectx(self._repo, d)
592 yield changectx(self._repo, d)
592
593
593 def filectx(self, path, fileid=None, filelog=None):
594 def filectx(self, path, fileid=None, filelog=None):
594 """get a file context from this changeset"""
595 """get a file context from this changeset"""
595 if fileid is None:
596 if fileid is None:
596 fileid = self.filenode(path)
597 fileid = self.filenode(path)
597 return filectx(self._repo, path, fileid=fileid,
598 return filectx(self._repo, path, fileid=fileid,
598 changectx=self, filelog=filelog)
599 changectx=self, filelog=filelog)
599
600
600 def ancestor(self, c2, warn=False):
601 def ancestor(self, c2, warn=False):
601 """return the "best" ancestor context of self and c2
602 """return the "best" ancestor context of self and c2
602
603
603 If there are multiple candidates, it will show a message and check
604 If there are multiple candidates, it will show a message and check
604 merge.preferancestor configuration before falling back to the
605 merge.preferancestor configuration before falling back to the
605 revlog ancestor."""
606 revlog ancestor."""
606 # deal with workingctxs
607 # deal with workingctxs
607 n2 = c2._node
608 n2 = c2._node
608 if n2 is None:
609 if n2 is None:
609 n2 = c2._parents[0]._node
610 n2 = c2._parents[0]._node
610 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
611 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
611 if not cahs:
612 if not cahs:
612 anc = nullid
613 anc = nullid
613 elif len(cahs) == 1:
614 elif len(cahs) == 1:
614 anc = cahs[0]
615 anc = cahs[0]
615 else:
616 else:
616 # experimental config: merge.preferancestor
617 # experimental config: merge.preferancestor
617 for r in self._repo.ui.configlist('merge', 'preferancestor', ['*']):
618 for r in self._repo.ui.configlist('merge', 'preferancestor', ['*']):
618 try:
619 try:
619 ctx = changectx(self._repo, r)
620 ctx = changectx(self._repo, r)
620 except error.RepoLookupError:
621 except error.RepoLookupError:
621 continue
622 continue
622 anc = ctx.node()
623 anc = ctx.node()
623 if anc in cahs:
624 if anc in cahs:
624 break
625 break
625 else:
626 else:
626 anc = self._repo.changelog.ancestor(self._node, n2)
627 anc = self._repo.changelog.ancestor(self._node, n2)
627 if warn:
628 if warn:
628 self._repo.ui.status(
629 self._repo.ui.status(
629 (_("note: using %s as ancestor of %s and %s\n") %
630 (_("note: using %s as ancestor of %s and %s\n") %
630 (short(anc), short(self._node), short(n2))) +
631 (short(anc), short(self._node), short(n2))) +
631 ''.join(_(" alternatively, use --config "
632 ''.join(_(" alternatively, use --config "
632 "merge.preferancestor=%s\n") %
633 "merge.preferancestor=%s\n") %
633 short(n) for n in sorted(cahs) if n != anc))
634 short(n) for n in sorted(cahs) if n != anc))
634 return changectx(self._repo, anc)
635 return changectx(self._repo, anc)
635
636
636 def descendant(self, other):
637 def descendant(self, other):
637 """True if other is descendant of this changeset"""
638 """True if other is descendant of this changeset"""
638 return self._repo.changelog.descendant(self._rev, other._rev)
639 return self._repo.changelog.descendant(self._rev, other._rev)
639
640
640 def walk(self, match):
641 def walk(self, match):
641 '''Generates matching file names.'''
642 '''Generates matching file names.'''
642
643
643 # Wrap match.bad method to have message with nodeid
644 # Wrap match.bad method to have message with nodeid
644 def bad(fn, msg):
645 def bad(fn, msg):
645 # The manifest doesn't know about subrepos, so don't complain about
646 # The manifest doesn't know about subrepos, so don't complain about
646 # paths into valid subrepos.
647 # paths into valid subrepos.
647 if any(fn == s or fn.startswith(s + '/')
648 if any(fn == s or fn.startswith(s + '/')
648 for s in self.substate):
649 for s in self.substate):
649 return
650 return
650 match.bad(fn, _('no such file in rev %s') % self)
651 match.bad(fn, _('no such file in rev %s') % self)
651
652
652 m = matchmod.badmatch(match, bad)
653 m = matchmod.badmatch(match, bad)
653 return self._manifest.walk(m)
654 return self._manifest.walk(m)
654
655
655 def matches(self, match):
656 def matches(self, match):
656 return self.walk(match)
657 return self.walk(match)
657
658
658 class basefilectx(object):
659 class basefilectx(object):
659 """A filecontext object represents the common logic for its children:
660 """A filecontext object represents the common logic for its children:
660 filectx: read-only access to a filerevision that is already present
661 filectx: read-only access to a filerevision that is already present
661 in the repo,
662 in the repo,
662 workingfilectx: a filecontext that represents files from the working
663 workingfilectx: a filecontext that represents files from the working
663 directory,
664 directory,
664 memfilectx: a filecontext that represents files in-memory,
665 memfilectx: a filecontext that represents files in-memory,
665 overlayfilectx: duplicate another filecontext with some fields overridden.
666 overlayfilectx: duplicate another filecontext with some fields overridden.
666 """
667 """
667 @propertycache
668 @propertycache
668 def _filelog(self):
669 def _filelog(self):
669 return self._repo.file(self._path)
670 return self._repo.file(self._path)
670
671
671 @propertycache
672 @propertycache
672 def _changeid(self):
673 def _changeid(self):
673 if r'_changeid' in self.__dict__:
674 if r'_changeid' in self.__dict__:
674 return self._changeid
675 return self._changeid
675 elif r'_changectx' in self.__dict__:
676 elif r'_changectx' in self.__dict__:
676 return self._changectx.rev()
677 return self._changectx.rev()
677 elif r'_descendantrev' in self.__dict__:
678 elif r'_descendantrev' in self.__dict__:
678 # this file context was created from a revision with a known
679 # this file context was created from a revision with a known
679 # descendant, we can (lazily) correct for linkrev aliases
680 # descendant, we can (lazily) correct for linkrev aliases
680 return self._adjustlinkrev(self._descendantrev)
681 return self._adjustlinkrev(self._descendantrev)
681 else:
682 else:
682 return self._filelog.linkrev(self._filerev)
683 return self._filelog.linkrev(self._filerev)
683
684
684 @propertycache
685 @propertycache
685 def _filenode(self):
686 def _filenode(self):
686 if r'_fileid' in self.__dict__:
687 if r'_fileid' in self.__dict__:
687 return self._filelog.lookup(self._fileid)
688 return self._filelog.lookup(self._fileid)
688 else:
689 else:
689 return self._changectx.filenode(self._path)
690 return self._changectx.filenode(self._path)
690
691
691 @propertycache
692 @propertycache
692 def _filerev(self):
693 def _filerev(self):
693 return self._filelog.rev(self._filenode)
694 return self._filelog.rev(self._filenode)
694
695
695 @propertycache
696 @propertycache
696 def _repopath(self):
697 def _repopath(self):
697 return self._path
698 return self._path
698
699
699 def __nonzero__(self):
700 def __nonzero__(self):
700 try:
701 try:
701 self._filenode
702 self._filenode
702 return True
703 return True
703 except error.LookupError:
704 except error.LookupError:
704 # file is missing
705 # file is missing
705 return False
706 return False
706
707
707 __bool__ = __nonzero__
708 __bool__ = __nonzero__
708
709
709 def __bytes__(self):
710 def __bytes__(self):
710 try:
711 try:
711 return "%s@%s" % (self.path(), self._changectx)
712 return "%s@%s" % (self.path(), self._changectx)
712 except error.LookupError:
713 except error.LookupError:
713 return "%s@???" % self.path()
714 return "%s@???" % self.path()
714
715
715 __str__ = encoding.strmethod(__bytes__)
716 __str__ = encoding.strmethod(__bytes__)
716
717
717 def __repr__(self):
718 def __repr__(self):
718 return "<%s %s>" % (type(self).__name__, str(self))
719 return "<%s %s>" % (type(self).__name__, str(self))
719
720
720 def __hash__(self):
721 def __hash__(self):
721 try:
722 try:
722 return hash((self._path, self._filenode))
723 return hash((self._path, self._filenode))
723 except AttributeError:
724 except AttributeError:
724 return id(self)
725 return id(self)
725
726
726 def __eq__(self, other):
727 def __eq__(self, other):
727 try:
728 try:
728 return (type(self) == type(other) and self._path == other._path
729 return (type(self) == type(other) and self._path == other._path
729 and self._filenode == other._filenode)
730 and self._filenode == other._filenode)
730 except AttributeError:
731 except AttributeError:
731 return False
732 return False
732
733
733 def __ne__(self, other):
734 def __ne__(self, other):
734 return not (self == other)
735 return not (self == other)
735
736
736 def filerev(self):
737 def filerev(self):
737 return self._filerev
738 return self._filerev
738 def filenode(self):
739 def filenode(self):
739 return self._filenode
740 return self._filenode
740 @propertycache
741 @propertycache
741 def _flags(self):
742 def _flags(self):
742 return self._changectx.flags(self._path)
743 return self._changectx.flags(self._path)
743 def flags(self):
744 def flags(self):
744 return self._flags
745 return self._flags
745 def filelog(self):
746 def filelog(self):
746 return self._filelog
747 return self._filelog
747 def rev(self):
748 def rev(self):
748 return self._changeid
749 return self._changeid
749 def linkrev(self):
750 def linkrev(self):
750 return self._filelog.linkrev(self._filerev)
751 return self._filelog.linkrev(self._filerev)
751 def node(self):
752 def node(self):
752 return self._changectx.node()
753 return self._changectx.node()
753 def hex(self):
754 def hex(self):
754 return self._changectx.hex()
755 return self._changectx.hex()
755 def user(self):
756 def user(self):
756 return self._changectx.user()
757 return self._changectx.user()
757 def date(self):
758 def date(self):
758 return self._changectx.date()
759 return self._changectx.date()
759 def files(self):
760 def files(self):
760 return self._changectx.files()
761 return self._changectx.files()
761 def description(self):
762 def description(self):
762 return self._changectx.description()
763 return self._changectx.description()
763 def branch(self):
764 def branch(self):
764 return self._changectx.branch()
765 return self._changectx.branch()
765 def extra(self):
766 def extra(self):
766 return self._changectx.extra()
767 return self._changectx.extra()
767 def phase(self):
768 def phase(self):
768 return self._changectx.phase()
769 return self._changectx.phase()
769 def phasestr(self):
770 def phasestr(self):
770 return self._changectx.phasestr()
771 return self._changectx.phasestr()
771 def manifest(self):
772 def manifest(self):
772 return self._changectx.manifest()
773 return self._changectx.manifest()
773 def changectx(self):
774 def changectx(self):
774 return self._changectx
775 return self._changectx
775 def renamed(self):
776 def renamed(self):
776 return self._copied
777 return self._copied
777 def repo(self):
778 def repo(self):
778 return self._repo
779 return self._repo
779 def size(self):
780 def size(self):
780 return len(self.data())
781 return len(self.data())
781
782
782 def path(self):
783 def path(self):
783 return self._path
784 return self._path
784
785
785 def isbinary(self):
786 def isbinary(self):
786 try:
787 try:
787 return util.binary(self.data())
788 return util.binary(self.data())
788 except IOError:
789 except IOError:
789 return False
790 return False
790 def isexec(self):
791 def isexec(self):
791 return 'x' in self.flags()
792 return 'x' in self.flags()
792 def islink(self):
793 def islink(self):
793 return 'l' in self.flags()
794 return 'l' in self.flags()
794
795
795 def isabsent(self):
796 def isabsent(self):
796 """whether this filectx represents a file not in self._changectx
797 """whether this filectx represents a file not in self._changectx
797
798
798 This is mainly for merge code to detect change/delete conflicts. This is
799 This is mainly for merge code to detect change/delete conflicts. This is
799 expected to be True for all subclasses of basectx."""
800 expected to be True for all subclasses of basectx."""
800 return False
801 return False
801
802
802 _customcmp = False
803 _customcmp = False
803 def cmp(self, fctx):
804 def cmp(self, fctx):
804 """compare with other file context
805 """compare with other file context
805
806
806 returns True if different than fctx.
807 returns True if different than fctx.
807 """
808 """
808 if fctx._customcmp:
809 if fctx._customcmp:
809 return fctx.cmp(self)
810 return fctx.cmp(self)
810
811
811 if (fctx._filenode is None
812 if (fctx._filenode is None
812 and (self._repo._encodefilterpats
813 and (self._repo._encodefilterpats
813 # if file data starts with '\1\n', empty metadata block is
814 # if file data starts with '\1\n', empty metadata block is
814 # prepended, which adds 4 bytes to filelog.size().
815 # prepended, which adds 4 bytes to filelog.size().
815 or self.size() - 4 == fctx.size())
816 or self.size() - 4 == fctx.size())
816 or self.size() == fctx.size()):
817 or self.size() == fctx.size()):
817 return self._filelog.cmp(self._filenode, fctx.data())
818 return self._filelog.cmp(self._filenode, fctx.data())
818
819
819 return True
820 return True
820
821
821 def _adjustlinkrev(self, srcrev, inclusive=False):
822 def _adjustlinkrev(self, srcrev, inclusive=False):
822 """return the first ancestor of <srcrev> introducing <fnode>
823 """return the first ancestor of <srcrev> introducing <fnode>
823
824
824 If the linkrev of the file revision does not point to an ancestor of
825 If the linkrev of the file revision does not point to an ancestor of
825 srcrev, we'll walk down the ancestors until we find one introducing
826 srcrev, we'll walk down the ancestors until we find one introducing
826 this file revision.
827 this file revision.
827
828
828 :srcrev: the changeset revision we search ancestors from
829 :srcrev: the changeset revision we search ancestors from
829 :inclusive: if true, the src revision will also be checked
830 :inclusive: if true, the src revision will also be checked
830 """
831 """
831 repo = self._repo
832 repo = self._repo
832 cl = repo.unfiltered().changelog
833 cl = repo.unfiltered().changelog
833 mfl = repo.manifestlog
834 mfl = repo.manifestlog
834 # fetch the linkrev
835 # fetch the linkrev
835 lkr = self.linkrev()
836 lkr = self.linkrev()
836 # hack to reuse ancestor computation when searching for renames
837 # hack to reuse ancestor computation when searching for renames
837 memberanc = getattr(self, '_ancestrycontext', None)
838 memberanc = getattr(self, '_ancestrycontext', None)
838 iteranc = None
839 iteranc = None
839 if srcrev is None:
840 if srcrev is None:
840 # wctx case, used by workingfilectx during mergecopy
841 # wctx case, used by workingfilectx during mergecopy
841 revs = [p.rev() for p in self._repo[None].parents()]
842 revs = [p.rev() for p in self._repo[None].parents()]
842 inclusive = True # we skipped the real (revless) source
843 inclusive = True # we skipped the real (revless) source
843 else:
844 else:
844 revs = [srcrev]
845 revs = [srcrev]
845 if memberanc is None:
846 if memberanc is None:
846 memberanc = iteranc = cl.ancestors(revs, lkr,
847 memberanc = iteranc = cl.ancestors(revs, lkr,
847 inclusive=inclusive)
848 inclusive=inclusive)
848 # check if this linkrev is an ancestor of srcrev
849 # check if this linkrev is an ancestor of srcrev
849 if lkr not in memberanc:
850 if lkr not in memberanc:
850 if iteranc is None:
851 if iteranc is None:
851 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
852 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
852 fnode = self._filenode
853 fnode = self._filenode
853 path = self._path
854 path = self._path
854 for a in iteranc:
855 for a in iteranc:
855 ac = cl.read(a) # get changeset data (we avoid object creation)
856 ac = cl.read(a) # get changeset data (we avoid object creation)
856 if path in ac[3]: # checking the 'files' field.
857 if path in ac[3]: # checking the 'files' field.
857 # The file has been touched, check if the content is
858 # The file has been touched, check if the content is
858 # similar to the one we search for.
859 # similar to the one we search for.
859 if fnode == mfl[ac[0]].readfast().get(path):
860 if fnode == mfl[ac[0]].readfast().get(path):
860 return a
861 return a
861 # In theory, we should never get out of that loop without a result.
862 # In theory, we should never get out of that loop without a result.
862 # But if manifest uses a buggy file revision (not children of the
863 # But if manifest uses a buggy file revision (not children of the
863 # one it replaces) we could. Such a buggy situation will likely
864 # one it replaces) we could. Such a buggy situation will likely
864 # result is crash somewhere else at to some point.
865 # result is crash somewhere else at to some point.
865 return lkr
866 return lkr
866
867
867 def introrev(self):
868 def introrev(self):
868 """return the rev of the changeset which introduced this file revision
869 """return the rev of the changeset which introduced this file revision
869
870
870 This method is different from linkrev because it take into account the
871 This method is different from linkrev because it take into account the
871 changeset the filectx was created from. It ensures the returned
872 changeset the filectx was created from. It ensures the returned
872 revision is one of its ancestors. This prevents bugs from
873 revision is one of its ancestors. This prevents bugs from
873 'linkrev-shadowing' when a file revision is used by multiple
874 'linkrev-shadowing' when a file revision is used by multiple
874 changesets.
875 changesets.
875 """
876 """
876 lkr = self.linkrev()
877 lkr = self.linkrev()
877 attrs = vars(self)
878 attrs = vars(self)
878 noctx = not ('_changeid' in attrs or '_changectx' in attrs)
879 noctx = not ('_changeid' in attrs or '_changectx' in attrs)
879 if noctx or self.rev() == lkr:
880 if noctx or self.rev() == lkr:
880 return self.linkrev()
881 return self.linkrev()
881 return self._adjustlinkrev(self.rev(), inclusive=True)
882 return self._adjustlinkrev(self.rev(), inclusive=True)
882
883
883 def _parentfilectx(self, path, fileid, filelog):
884 def _parentfilectx(self, path, fileid, filelog):
884 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
885 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
885 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
886 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
886 if '_changeid' in vars(self) or '_changectx' in vars(self):
887 if '_changeid' in vars(self) or '_changectx' in vars(self):
887 # If self is associated with a changeset (probably explicitly
888 # If self is associated with a changeset (probably explicitly
888 # fed), ensure the created filectx is associated with a
889 # fed), ensure the created filectx is associated with a
889 # changeset that is an ancestor of self.changectx.
890 # changeset that is an ancestor of self.changectx.
890 # This lets us later use _adjustlinkrev to get a correct link.
891 # This lets us later use _adjustlinkrev to get a correct link.
891 fctx._descendantrev = self.rev()
892 fctx._descendantrev = self.rev()
892 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
893 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
893 elif '_descendantrev' in vars(self):
894 elif '_descendantrev' in vars(self):
894 # Otherwise propagate _descendantrev if we have one associated.
895 # Otherwise propagate _descendantrev if we have one associated.
895 fctx._descendantrev = self._descendantrev
896 fctx._descendantrev = self._descendantrev
896 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
897 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
897 return fctx
898 return fctx
898
899
899 def parents(self):
900 def parents(self):
900 _path = self._path
901 _path = self._path
901 fl = self._filelog
902 fl = self._filelog
902 parents = self._filelog.parents(self._filenode)
903 parents = self._filelog.parents(self._filenode)
903 pl = [(_path, node, fl) for node in parents if node != nullid]
904 pl = [(_path, node, fl) for node in parents if node != nullid]
904
905
905 r = fl.renamed(self._filenode)
906 r = fl.renamed(self._filenode)
906 if r:
907 if r:
907 # - In the simple rename case, both parent are nullid, pl is empty.
908 # - In the simple rename case, both parent are nullid, pl is empty.
908 # - In case of merge, only one of the parent is null id and should
909 # - In case of merge, only one of the parent is null id and should
909 # be replaced with the rename information. This parent is -always-
910 # be replaced with the rename information. This parent is -always-
910 # the first one.
911 # the first one.
911 #
912 #
912 # As null id have always been filtered out in the previous list
913 # As null id have always been filtered out in the previous list
913 # comprehension, inserting to 0 will always result in "replacing
914 # comprehension, inserting to 0 will always result in "replacing
914 # first nullid parent with rename information.
915 # first nullid parent with rename information.
915 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
916 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
916
917
917 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
918 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
918
919
919 def p1(self):
920 def p1(self):
920 return self.parents()[0]
921 return self.parents()[0]
921
922
922 def p2(self):
923 def p2(self):
923 p = self.parents()
924 p = self.parents()
924 if len(p) == 2:
925 if len(p) == 2:
925 return p[1]
926 return p[1]
926 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
927 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
927
928
928 def annotate(self, follow=False, linenumber=False, skiprevs=None,
929 def annotate(self, follow=False, linenumber=False, skiprevs=None,
929 diffopts=None):
930 diffopts=None):
930 '''returns a list of tuples of ((ctx, number), line) for each line
931 '''returns a list of tuples of ((ctx, number), line) for each line
931 in the file, where ctx is the filectx of the node where
932 in the file, where ctx is the filectx of the node where
932 that line was last changed; if linenumber parameter is true, number is
933 that line was last changed; if linenumber parameter is true, number is
933 the line number at the first appearance in the managed file, otherwise,
934 the line number at the first appearance in the managed file, otherwise,
934 number has a fixed value of False.
935 number has a fixed value of False.
935 '''
936 '''
936
937
937 def lines(text):
938 def lines(text):
938 if text.endswith("\n"):
939 if text.endswith("\n"):
939 return text.count("\n")
940 return text.count("\n")
940 return text.count("\n") + int(bool(text))
941 return text.count("\n") + int(bool(text))
941
942
942 if linenumber:
943 if linenumber:
943 def decorate(text, rev):
944 def decorate(text, rev):
944 return ([(rev, i) for i in xrange(1, lines(text) + 1)], text)
945 return ([(rev, i) for i in xrange(1, lines(text) + 1)], text)
945 else:
946 else:
946 def decorate(text, rev):
947 def decorate(text, rev):
947 return ([(rev, False)] * lines(text), text)
948 return ([(rev, False)] * lines(text), text)
948
949
949 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
950 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
950
951
951 def parents(f):
952 def parents(f):
952 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
953 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
953 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
954 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
954 # from the topmost introrev (= srcrev) down to p.linkrev() if it
955 # from the topmost introrev (= srcrev) down to p.linkrev() if it
955 # isn't an ancestor of the srcrev.
956 # isn't an ancestor of the srcrev.
956 f._changeid
957 f._changeid
957 pl = f.parents()
958 pl = f.parents()
958
959
959 # Don't return renamed parents if we aren't following.
960 # Don't return renamed parents if we aren't following.
960 if not follow:
961 if not follow:
961 pl = [p for p in pl if p.path() == f.path()]
962 pl = [p for p in pl if p.path() == f.path()]
962
963
963 # renamed filectx won't have a filelog yet, so set it
964 # renamed filectx won't have a filelog yet, so set it
964 # from the cache to save time
965 # from the cache to save time
965 for p in pl:
966 for p in pl:
966 if not '_filelog' in p.__dict__:
967 if not '_filelog' in p.__dict__:
967 p._filelog = getlog(p.path())
968 p._filelog = getlog(p.path())
968
969
969 return pl
970 return pl
970
971
971 # use linkrev to find the first changeset where self appeared
972 # use linkrev to find the first changeset where self appeared
972 base = self
973 base = self
973 introrev = self.introrev()
974 introrev = self.introrev()
974 if self.rev() != introrev:
975 if self.rev() != introrev:
975 base = self.filectx(self.filenode(), changeid=introrev)
976 base = self.filectx(self.filenode(), changeid=introrev)
976 if getattr(base, '_ancestrycontext', None) is None:
977 if getattr(base, '_ancestrycontext', None) is None:
977 cl = self._repo.changelog
978 cl = self._repo.changelog
978 if introrev is None:
979 if introrev is None:
979 # wctx is not inclusive, but works because _ancestrycontext
980 # wctx is not inclusive, but works because _ancestrycontext
980 # is used to test filelog revisions
981 # is used to test filelog revisions
981 ac = cl.ancestors([p.rev() for p in base.parents()],
982 ac = cl.ancestors([p.rev() for p in base.parents()],
982 inclusive=True)
983 inclusive=True)
983 else:
984 else:
984 ac = cl.ancestors([introrev], inclusive=True)
985 ac = cl.ancestors([introrev], inclusive=True)
985 base._ancestrycontext = ac
986 base._ancestrycontext = ac
986
987
987 # This algorithm would prefer to be recursive, but Python is a
988 # This algorithm would prefer to be recursive, but Python is a
988 # bit recursion-hostile. Instead we do an iterative
989 # bit recursion-hostile. Instead we do an iterative
989 # depth-first search.
990 # depth-first search.
990
991
991 # 1st DFS pre-calculates pcache and needed
992 # 1st DFS pre-calculates pcache and needed
992 visit = [base]
993 visit = [base]
993 pcache = {}
994 pcache = {}
994 needed = {base: 1}
995 needed = {base: 1}
995 while visit:
996 while visit:
996 f = visit.pop()
997 f = visit.pop()
997 if f in pcache:
998 if f in pcache:
998 continue
999 continue
999 pl = parents(f)
1000 pl = parents(f)
1000 pcache[f] = pl
1001 pcache[f] = pl
1001 for p in pl:
1002 for p in pl:
1002 needed[p] = needed.get(p, 0) + 1
1003 needed[p] = needed.get(p, 0) + 1
1003 if p not in pcache:
1004 if p not in pcache:
1004 visit.append(p)
1005 visit.append(p)
1005
1006
1006 # 2nd DFS does the actual annotate
1007 # 2nd DFS does the actual annotate
1007 visit[:] = [base]
1008 visit[:] = [base]
1008 hist = {}
1009 hist = {}
1009 while visit:
1010 while visit:
1010 f = visit[-1]
1011 f = visit[-1]
1011 if f in hist:
1012 if f in hist:
1012 visit.pop()
1013 visit.pop()
1013 continue
1014 continue
1014
1015
1015 ready = True
1016 ready = True
1016 pl = pcache[f]
1017 pl = pcache[f]
1017 for p in pl:
1018 for p in pl:
1018 if p not in hist:
1019 if p not in hist:
1019 ready = False
1020 ready = False
1020 visit.append(p)
1021 visit.append(p)
1021 if ready:
1022 if ready:
1022 visit.pop()
1023 visit.pop()
1023 curr = decorate(f.data(), f)
1024 curr = decorate(f.data(), f)
1024 skipchild = False
1025 skipchild = False
1025 if skiprevs is not None:
1026 if skiprevs is not None:
1026 skipchild = f._changeid in skiprevs
1027 skipchild = f._changeid in skiprevs
1027 curr = _annotatepair([hist[p] for p in pl], f, curr, skipchild,
1028 curr = _annotatepair([hist[p] for p in pl], f, curr, skipchild,
1028 diffopts)
1029 diffopts)
1029 for p in pl:
1030 for p in pl:
1030 if needed[p] == 1:
1031 if needed[p] == 1:
1031 del hist[p]
1032 del hist[p]
1032 del needed[p]
1033 del needed[p]
1033 else:
1034 else:
1034 needed[p] -= 1
1035 needed[p] -= 1
1035
1036
1036 hist[f] = curr
1037 hist[f] = curr
1037 del pcache[f]
1038 del pcache[f]
1038
1039
1039 return zip(hist[base][0], hist[base][1].splitlines(True))
1040 return zip(hist[base][0], hist[base][1].splitlines(True))
1040
1041
1041 def ancestors(self, followfirst=False):
1042 def ancestors(self, followfirst=False):
1042 visit = {}
1043 visit = {}
1043 c = self
1044 c = self
1044 if followfirst:
1045 if followfirst:
1045 cut = 1
1046 cut = 1
1046 else:
1047 else:
1047 cut = None
1048 cut = None
1048
1049
1049 while True:
1050 while True:
1050 for parent in c.parents()[:cut]:
1051 for parent in c.parents()[:cut]:
1051 visit[(parent.linkrev(), parent.filenode())] = parent
1052 visit[(parent.linkrev(), parent.filenode())] = parent
1052 if not visit:
1053 if not visit:
1053 break
1054 break
1054 c = visit.pop(max(visit))
1055 c = visit.pop(max(visit))
1055 yield c
1056 yield c
1056
1057
1057 def _annotatepair(parents, childfctx, child, skipchild, diffopts):
1058 def _annotatepair(parents, childfctx, child, skipchild, diffopts):
1058 r'''
1059 r'''
1059 Given parent and child fctxes and annotate data for parents, for all lines
1060 Given parent and child fctxes and annotate data for parents, for all lines
1060 in either parent that match the child, annotate the child with the parent's
1061 in either parent that match the child, annotate the child with the parent's
1061 data.
1062 data.
1062
1063
1063 Additionally, if `skipchild` is True, replace all other lines with parent
1064 Additionally, if `skipchild` is True, replace all other lines with parent
1064 annotate data as well such that child is never blamed for any lines.
1065 annotate data as well such that child is never blamed for any lines.
1065
1066
1066 >>> oldfctx = 'old'
1067 >>> oldfctx = 'old'
1067 >>> p1fctx, p2fctx, childfctx = 'p1', 'p2', 'c'
1068 >>> p1fctx, p2fctx, childfctx = 'p1', 'p2', 'c'
1068 >>> olddata = 'a\nb\n'
1069 >>> olddata = 'a\nb\n'
1069 >>> p1data = 'a\nb\nc\n'
1070 >>> p1data = 'a\nb\nc\n'
1070 >>> p2data = 'a\nc\nd\n'
1071 >>> p2data = 'a\nc\nd\n'
1071 >>> childdata = 'a\nb2\nc\nc2\nd\n'
1072 >>> childdata = 'a\nb2\nc\nc2\nd\n'
1072 >>> diffopts = mdiff.diffopts()
1073 >>> diffopts = mdiff.diffopts()
1073
1074
1074 >>> def decorate(text, rev):
1075 >>> def decorate(text, rev):
1075 ... return ([(rev, i) for i in xrange(1, text.count('\n') + 1)], text)
1076 ... return ([(rev, i) for i in xrange(1, text.count('\n') + 1)], text)
1076
1077
1077 Basic usage:
1078 Basic usage:
1078
1079
1079 >>> oldann = decorate(olddata, oldfctx)
1080 >>> oldann = decorate(olddata, oldfctx)
1080 >>> p1ann = decorate(p1data, p1fctx)
1081 >>> p1ann = decorate(p1data, p1fctx)
1081 >>> p1ann = _annotatepair([oldann], p1fctx, p1ann, False, diffopts)
1082 >>> p1ann = _annotatepair([oldann], p1fctx, p1ann, False, diffopts)
1082 >>> p1ann[0]
1083 >>> p1ann[0]
1083 [('old', 1), ('old', 2), ('p1', 3)]
1084 [('old', 1), ('old', 2), ('p1', 3)]
1084 >>> p2ann = decorate(p2data, p2fctx)
1085 >>> p2ann = decorate(p2data, p2fctx)
1085 >>> p2ann = _annotatepair([oldann], p2fctx, p2ann, False, diffopts)
1086 >>> p2ann = _annotatepair([oldann], p2fctx, p2ann, False, diffopts)
1086 >>> p2ann[0]
1087 >>> p2ann[0]
1087 [('old', 1), ('p2', 2), ('p2', 3)]
1088 [('old', 1), ('p2', 2), ('p2', 3)]
1088
1089
1089 Test with multiple parents (note the difference caused by ordering):
1090 Test with multiple parents (note the difference caused by ordering):
1090
1091
1091 >>> childann = decorate(childdata, childfctx)
1092 >>> childann = decorate(childdata, childfctx)
1092 >>> childann = _annotatepair([p1ann, p2ann], childfctx, childann, False,
1093 >>> childann = _annotatepair([p1ann, p2ann], childfctx, childann, False,
1093 ... diffopts)
1094 ... diffopts)
1094 >>> childann[0]
1095 >>> childann[0]
1095 [('old', 1), ('c', 2), ('p2', 2), ('c', 4), ('p2', 3)]
1096 [('old', 1), ('c', 2), ('p2', 2), ('c', 4), ('p2', 3)]
1096
1097
1097 >>> childann = decorate(childdata, childfctx)
1098 >>> childann = decorate(childdata, childfctx)
1098 >>> childann = _annotatepair([p2ann, p1ann], childfctx, childann, False,
1099 >>> childann = _annotatepair([p2ann, p1ann], childfctx, childann, False,
1099 ... diffopts)
1100 ... diffopts)
1100 >>> childann[0]
1101 >>> childann[0]
1101 [('old', 1), ('c', 2), ('p1', 3), ('c', 4), ('p2', 3)]
1102 [('old', 1), ('c', 2), ('p1', 3), ('c', 4), ('p2', 3)]
1102
1103
1103 Test with skipchild (note the difference caused by ordering):
1104 Test with skipchild (note the difference caused by ordering):
1104
1105
1105 >>> childann = decorate(childdata, childfctx)
1106 >>> childann = decorate(childdata, childfctx)
1106 >>> childann = _annotatepair([p1ann, p2ann], childfctx, childann, True,
1107 >>> childann = _annotatepair([p1ann, p2ann], childfctx, childann, True,
1107 ... diffopts)
1108 ... diffopts)
1108 >>> childann[0]
1109 >>> childann[0]
1109 [('old', 1), ('old', 2), ('p2', 2), ('p2', 2), ('p2', 3)]
1110 [('old', 1), ('old', 2), ('p2', 2), ('p2', 2), ('p2', 3)]
1110
1111
1111 >>> childann = decorate(childdata, childfctx)
1112 >>> childann = decorate(childdata, childfctx)
1112 >>> childann = _annotatepair([p2ann, p1ann], childfctx, childann, True,
1113 >>> childann = _annotatepair([p2ann, p1ann], childfctx, childann, True,
1113 ... diffopts)
1114 ... diffopts)
1114 >>> childann[0]
1115 >>> childann[0]
1115 [('old', 1), ('old', 2), ('p1', 3), ('p1', 3), ('p2', 3)]
1116 [('old', 1), ('old', 2), ('p1', 3), ('p1', 3), ('p2', 3)]
1116 '''
1117 '''
1117 pblocks = [(parent, mdiff.allblocks(parent[1], child[1], opts=diffopts))
1118 pblocks = [(parent, mdiff.allblocks(parent[1], child[1], opts=diffopts))
1118 for parent in parents]
1119 for parent in parents]
1119
1120
1120 if skipchild:
1121 if skipchild:
1121 # Need to iterate over the blocks twice -- make it a list
1122 # Need to iterate over the blocks twice -- make it a list
1122 pblocks = [(p, list(blocks)) for (p, blocks) in pblocks]
1123 pblocks = [(p, list(blocks)) for (p, blocks) in pblocks]
1123 # Mercurial currently prefers p2 over p1 for annotate.
1124 # Mercurial currently prefers p2 over p1 for annotate.
1124 # TODO: change this?
1125 # TODO: change this?
1125 for parent, blocks in pblocks:
1126 for parent, blocks in pblocks:
1126 for (a1, a2, b1, b2), t in blocks:
1127 for (a1, a2, b1, b2), t in blocks:
1127 # Changed blocks ('!') or blocks made only of blank lines ('~')
1128 # Changed blocks ('!') or blocks made only of blank lines ('~')
1128 # belong to the child.
1129 # belong to the child.
1129 if t == '=':
1130 if t == '=':
1130 child[0][b1:b2] = parent[0][a1:a2]
1131 child[0][b1:b2] = parent[0][a1:a2]
1131
1132
1132 if skipchild:
1133 if skipchild:
1133 # Now try and match up anything that couldn't be matched,
1134 # Now try and match up anything that couldn't be matched,
1134 # Reversing pblocks maintains bias towards p2, matching above
1135 # Reversing pblocks maintains bias towards p2, matching above
1135 # behavior.
1136 # behavior.
1136 pblocks.reverse()
1137 pblocks.reverse()
1137
1138
1138 # The heuristics are:
1139 # The heuristics are:
1139 # * Work on blocks of changed lines (effectively diff hunks with -U0).
1140 # * Work on blocks of changed lines (effectively diff hunks with -U0).
1140 # This could potentially be smarter but works well enough.
1141 # This could potentially be smarter but works well enough.
1141 # * For a non-matching section, do a best-effort fit. Match lines in
1142 # * For a non-matching section, do a best-effort fit. Match lines in
1142 # diff hunks 1:1, dropping lines as necessary.
1143 # diff hunks 1:1, dropping lines as necessary.
1143 # * Repeat the last line as a last resort.
1144 # * Repeat the last line as a last resort.
1144
1145
1145 # First, replace as much as possible without repeating the last line.
1146 # First, replace as much as possible without repeating the last line.
1146 remaining = [(parent, []) for parent, _blocks in pblocks]
1147 remaining = [(parent, []) for parent, _blocks in pblocks]
1147 for idx, (parent, blocks) in enumerate(pblocks):
1148 for idx, (parent, blocks) in enumerate(pblocks):
1148 for (a1, a2, b1, b2), _t in blocks:
1149 for (a1, a2, b1, b2), _t in blocks:
1149 if a2 - a1 >= b2 - b1:
1150 if a2 - a1 >= b2 - b1:
1150 for bk in xrange(b1, b2):
1151 for bk in xrange(b1, b2):
1151 if child[0][bk][0] == childfctx:
1152 if child[0][bk][0] == childfctx:
1152 ak = min(a1 + (bk - b1), a2 - 1)
1153 ak = min(a1 + (bk - b1), a2 - 1)
1153 child[0][bk] = parent[0][ak]
1154 child[0][bk] = parent[0][ak]
1154 else:
1155 else:
1155 remaining[idx][1].append((a1, a2, b1, b2))
1156 remaining[idx][1].append((a1, a2, b1, b2))
1156
1157
1157 # Then, look at anything left, which might involve repeating the last
1158 # Then, look at anything left, which might involve repeating the last
1158 # line.
1159 # line.
1159 for parent, blocks in remaining:
1160 for parent, blocks in remaining:
1160 for a1, a2, b1, b2 in blocks:
1161 for a1, a2, b1, b2 in blocks:
1161 for bk in xrange(b1, b2):
1162 for bk in xrange(b1, b2):
1162 if child[0][bk][0] == childfctx:
1163 if child[0][bk][0] == childfctx:
1163 ak = min(a1 + (bk - b1), a2 - 1)
1164 ak = min(a1 + (bk - b1), a2 - 1)
1164 child[0][bk] = parent[0][ak]
1165 child[0][bk] = parent[0][ak]
1165 return child
1166 return child
1166
1167
1167 class filectx(basefilectx):
1168 class filectx(basefilectx):
1168 """A filecontext object makes access to data related to a particular
1169 """A filecontext object makes access to data related to a particular
1169 filerevision convenient."""
1170 filerevision convenient."""
1170 def __init__(self, repo, path, changeid=None, fileid=None,
1171 def __init__(self, repo, path, changeid=None, fileid=None,
1171 filelog=None, changectx=None):
1172 filelog=None, changectx=None):
1172 """changeid can be a changeset revision, node, or tag.
1173 """changeid can be a changeset revision, node, or tag.
1173 fileid can be a file revision or node."""
1174 fileid can be a file revision or node."""
1174 self._repo = repo
1175 self._repo = repo
1175 self._path = path
1176 self._path = path
1176
1177
1177 assert (changeid is not None
1178 assert (changeid is not None
1178 or fileid is not None
1179 or fileid is not None
1179 or changectx is not None), \
1180 or changectx is not None), \
1180 ("bad args: changeid=%r, fileid=%r, changectx=%r"
1181 ("bad args: changeid=%r, fileid=%r, changectx=%r"
1181 % (changeid, fileid, changectx))
1182 % (changeid, fileid, changectx))
1182
1183
1183 if filelog is not None:
1184 if filelog is not None:
1184 self._filelog = filelog
1185 self._filelog = filelog
1185
1186
1186 if changeid is not None:
1187 if changeid is not None:
1187 self._changeid = changeid
1188 self._changeid = changeid
1188 if changectx is not None:
1189 if changectx is not None:
1189 self._changectx = changectx
1190 self._changectx = changectx
1190 if fileid is not None:
1191 if fileid is not None:
1191 self._fileid = fileid
1192 self._fileid = fileid
1192
1193
1193 @propertycache
1194 @propertycache
1194 def _changectx(self):
1195 def _changectx(self):
1195 try:
1196 try:
1196 return changectx(self._repo, self._changeid)
1197 return changectx(self._repo, self._changeid)
1197 except error.FilteredRepoLookupError:
1198 except error.FilteredRepoLookupError:
1198 # Linkrev may point to any revision in the repository. When the
1199 # Linkrev may point to any revision in the repository. When the
1199 # repository is filtered this may lead to `filectx` trying to build
1200 # repository is filtered this may lead to `filectx` trying to build
1200 # `changectx` for filtered revision. In such case we fallback to
1201 # `changectx` for filtered revision. In such case we fallback to
1201 # creating `changectx` on the unfiltered version of the reposition.
1202 # creating `changectx` on the unfiltered version of the reposition.
1202 # This fallback should not be an issue because `changectx` from
1203 # This fallback should not be an issue because `changectx` from
1203 # `filectx` are not used in complex operations that care about
1204 # `filectx` are not used in complex operations that care about
1204 # filtering.
1205 # filtering.
1205 #
1206 #
1206 # This fallback is a cheap and dirty fix that prevent several
1207 # This fallback is a cheap and dirty fix that prevent several
1207 # crashes. It does not ensure the behavior is correct. However the
1208 # crashes. It does not ensure the behavior is correct. However the
1208 # behavior was not correct before filtering either and "incorrect
1209 # behavior was not correct before filtering either and "incorrect
1209 # behavior" is seen as better as "crash"
1210 # behavior" is seen as better as "crash"
1210 #
1211 #
1211 # Linkrevs have several serious troubles with filtering that are
1212 # Linkrevs have several serious troubles with filtering that are
1212 # complicated to solve. Proper handling of the issue here should be
1213 # complicated to solve. Proper handling of the issue here should be
1213 # considered when solving linkrev issue are on the table.
1214 # considered when solving linkrev issue are on the table.
1214 return changectx(self._repo.unfiltered(), self._changeid)
1215 return changectx(self._repo.unfiltered(), self._changeid)
1215
1216
1216 def filectx(self, fileid, changeid=None):
1217 def filectx(self, fileid, changeid=None):
1217 '''opens an arbitrary revision of the file without
1218 '''opens an arbitrary revision of the file without
1218 opening a new filelog'''
1219 opening a new filelog'''
1219 return filectx(self._repo, self._path, fileid=fileid,
1220 return filectx(self._repo, self._path, fileid=fileid,
1220 filelog=self._filelog, changeid=changeid)
1221 filelog=self._filelog, changeid=changeid)
1221
1222
1222 def rawdata(self):
1223 def rawdata(self):
1223 return self._filelog.revision(self._filenode, raw=True)
1224 return self._filelog.revision(self._filenode, raw=True)
1224
1225
1225 def rawflags(self):
1226 def rawflags(self):
1226 """low-level revlog flags"""
1227 """low-level revlog flags"""
1227 return self._filelog.flags(self._filerev)
1228 return self._filelog.flags(self._filerev)
1228
1229
1229 def data(self):
1230 def data(self):
1230 try:
1231 try:
1231 return self._filelog.read(self._filenode)
1232 return self._filelog.read(self._filenode)
1232 except error.CensoredNodeError:
1233 except error.CensoredNodeError:
1233 if self._repo.ui.config("censor", "policy", "abort") == "ignore":
1234 if self._repo.ui.config("censor", "policy", "abort") == "ignore":
1234 return ""
1235 return ""
1235 raise error.Abort(_("censored node: %s") % short(self._filenode),
1236 raise error.Abort(_("censored node: %s") % short(self._filenode),
1236 hint=_("set censor.policy to ignore errors"))
1237 hint=_("set censor.policy to ignore errors"))
1237
1238
1238 def size(self):
1239 def size(self):
1239 return self._filelog.size(self._filerev)
1240 return self._filelog.size(self._filerev)
1240
1241
1241 @propertycache
1242 @propertycache
1242 def _copied(self):
1243 def _copied(self):
1243 """check if file was actually renamed in this changeset revision
1244 """check if file was actually renamed in this changeset revision
1244
1245
1245 If rename logged in file revision, we report copy for changeset only
1246 If rename logged in file revision, we report copy for changeset only
1246 if file revisions linkrev points back to the changeset in question
1247 if file revisions linkrev points back to the changeset in question
1247 or both changeset parents contain different file revisions.
1248 or both changeset parents contain different file revisions.
1248 """
1249 """
1249
1250
1250 renamed = self._filelog.renamed(self._filenode)
1251 renamed = self._filelog.renamed(self._filenode)
1251 if not renamed:
1252 if not renamed:
1252 return renamed
1253 return renamed
1253
1254
1254 if self.rev() == self.linkrev():
1255 if self.rev() == self.linkrev():
1255 return renamed
1256 return renamed
1256
1257
1257 name = self.path()
1258 name = self.path()
1258 fnode = self._filenode
1259 fnode = self._filenode
1259 for p in self._changectx.parents():
1260 for p in self._changectx.parents():
1260 try:
1261 try:
1261 if fnode == p.filenode(name):
1262 if fnode == p.filenode(name):
1262 return None
1263 return None
1263 except error.LookupError:
1264 except error.LookupError:
1264 pass
1265 pass
1265 return renamed
1266 return renamed
1266
1267
1267 def children(self):
1268 def children(self):
1268 # hard for renames
1269 # hard for renames
1269 c = self._filelog.children(self._filenode)
1270 c = self._filelog.children(self._filenode)
1270 return [filectx(self._repo, self._path, fileid=x,
1271 return [filectx(self._repo, self._path, fileid=x,
1271 filelog=self._filelog) for x in c]
1272 filelog=self._filelog) for x in c]
1272
1273
1273 class committablectx(basectx):
1274 class committablectx(basectx):
1274 """A committablectx object provides common functionality for a context that
1275 """A committablectx object provides common functionality for a context that
1275 wants the ability to commit, e.g. workingctx or memctx."""
1276 wants the ability to commit, e.g. workingctx or memctx."""
1276 def __init__(self, repo, text="", user=None, date=None, extra=None,
1277 def __init__(self, repo, text="", user=None, date=None, extra=None,
1277 changes=None):
1278 changes=None):
1278 self._repo = repo
1279 self._repo = repo
1279 self._rev = None
1280 self._rev = None
1280 self._node = None
1281 self._node = None
1281 self._text = text
1282 self._text = text
1282 if date:
1283 if date:
1283 self._date = util.parsedate(date)
1284 self._date = util.parsedate(date)
1284 if user:
1285 if user:
1285 self._user = user
1286 self._user = user
1286 if changes:
1287 if changes:
1287 self._status = changes
1288 self._status = changes
1288
1289
1289 self._extra = {}
1290 self._extra = {}
1290 if extra:
1291 if extra:
1291 self._extra = extra.copy()
1292 self._extra = extra.copy()
1292 if 'branch' not in self._extra:
1293 if 'branch' not in self._extra:
1293 try:
1294 try:
1294 branch = encoding.fromlocal(self._repo.dirstate.branch())
1295 branch = encoding.fromlocal(self._repo.dirstate.branch())
1295 except UnicodeDecodeError:
1296 except UnicodeDecodeError:
1296 raise error.Abort(_('branch name not in UTF-8!'))
1297 raise error.Abort(_('branch name not in UTF-8!'))
1297 self._extra['branch'] = branch
1298 self._extra['branch'] = branch
1298 if self._extra['branch'] == '':
1299 if self._extra['branch'] == '':
1299 self._extra['branch'] = 'default'
1300 self._extra['branch'] = 'default'
1300
1301
1301 def __bytes__(self):
1302 def __bytes__(self):
1302 return bytes(self._parents[0]) + "+"
1303 return bytes(self._parents[0]) + "+"
1303
1304
1304 __str__ = encoding.strmethod(__bytes__)
1305 __str__ = encoding.strmethod(__bytes__)
1305
1306
1306 def __nonzero__(self):
1307 def __nonzero__(self):
1307 return True
1308 return True
1308
1309
1309 __bool__ = __nonzero__
1310 __bool__ = __nonzero__
1310
1311
1311 def _buildflagfunc(self):
1312 def _buildflagfunc(self):
1312 # Create a fallback function for getting file flags when the
1313 # Create a fallback function for getting file flags when the
1313 # filesystem doesn't support them
1314 # filesystem doesn't support them
1314
1315
1315 copiesget = self._repo.dirstate.copies().get
1316 copiesget = self._repo.dirstate.copies().get
1316 parents = self.parents()
1317 parents = self.parents()
1317 if len(parents) < 2:
1318 if len(parents) < 2:
1318 # when we have one parent, it's easy: copy from parent
1319 # when we have one parent, it's easy: copy from parent
1319 man = parents[0].manifest()
1320 man = parents[0].manifest()
1320 def func(f):
1321 def func(f):
1321 f = copiesget(f, f)
1322 f = copiesget(f, f)
1322 return man.flags(f)
1323 return man.flags(f)
1323 else:
1324 else:
1324 # merges are tricky: we try to reconstruct the unstored
1325 # merges are tricky: we try to reconstruct the unstored
1325 # result from the merge (issue1802)
1326 # result from the merge (issue1802)
1326 p1, p2 = parents
1327 p1, p2 = parents
1327 pa = p1.ancestor(p2)
1328 pa = p1.ancestor(p2)
1328 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1329 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1329
1330
1330 def func(f):
1331 def func(f):
1331 f = copiesget(f, f) # may be wrong for merges with copies
1332 f = copiesget(f, f) # may be wrong for merges with copies
1332 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1333 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1333 if fl1 == fl2:
1334 if fl1 == fl2:
1334 return fl1
1335 return fl1
1335 if fl1 == fla:
1336 if fl1 == fla:
1336 return fl2
1337 return fl2
1337 if fl2 == fla:
1338 if fl2 == fla:
1338 return fl1
1339 return fl1
1339 return '' # punt for conflicts
1340 return '' # punt for conflicts
1340
1341
1341 return func
1342 return func
1342
1343
1343 @propertycache
1344 @propertycache
1344 def _flagfunc(self):
1345 def _flagfunc(self):
1345 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1346 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1346
1347
1347 @propertycache
1348 @propertycache
1348 def _status(self):
1349 def _status(self):
1349 return self._repo.status()
1350 return self._repo.status()
1350
1351
1351 @propertycache
1352 @propertycache
1352 def _user(self):
1353 def _user(self):
1353 return self._repo.ui.username()
1354 return self._repo.ui.username()
1354
1355
1355 @propertycache
1356 @propertycache
1356 def _date(self):
1357 def _date(self):
1357 ui = self._repo.ui
1358 ui = self._repo.ui
1358 date = ui.configdate('devel', 'default-date')
1359 date = ui.configdate('devel', 'default-date')
1359 if date is None:
1360 if date is None:
1360 date = util.makedate()
1361 date = util.makedate()
1361 return date
1362 return date
1362
1363
1363 def subrev(self, subpath):
1364 def subrev(self, subpath):
1364 return None
1365 return None
1365
1366
1366 def manifestnode(self):
1367 def manifestnode(self):
1367 return None
1368 return None
1368 def user(self):
1369 def user(self):
1369 return self._user or self._repo.ui.username()
1370 return self._user or self._repo.ui.username()
1370 def date(self):
1371 def date(self):
1371 return self._date
1372 return self._date
1372 def description(self):
1373 def description(self):
1373 return self._text
1374 return self._text
1374 def files(self):
1375 def files(self):
1375 return sorted(self._status.modified + self._status.added +
1376 return sorted(self._status.modified + self._status.added +
1376 self._status.removed)
1377 self._status.removed)
1377
1378
1378 def modified(self):
1379 def modified(self):
1379 return self._status.modified
1380 return self._status.modified
1380 def added(self):
1381 def added(self):
1381 return self._status.added
1382 return self._status.added
1382 def removed(self):
1383 def removed(self):
1383 return self._status.removed
1384 return self._status.removed
1384 def deleted(self):
1385 def deleted(self):
1385 return self._status.deleted
1386 return self._status.deleted
1386 def branch(self):
1387 def branch(self):
1387 return encoding.tolocal(self._extra['branch'])
1388 return encoding.tolocal(self._extra['branch'])
1388 def closesbranch(self):
1389 def closesbranch(self):
1389 return 'close' in self._extra
1390 return 'close' in self._extra
1390 def extra(self):
1391 def extra(self):
1391 return self._extra
1392 return self._extra
1392
1393
1393 def tags(self):
1394 def tags(self):
1394 return []
1395 return []
1395
1396
1396 def bookmarks(self):
1397 def bookmarks(self):
1397 b = []
1398 b = []
1398 for p in self.parents():
1399 for p in self.parents():
1399 b.extend(p.bookmarks())
1400 b.extend(p.bookmarks())
1400 return b
1401 return b
1401
1402
1402 def phase(self):
1403 def phase(self):
1403 phase = phases.draft # default phase to draft
1404 phase = phases.draft # default phase to draft
1404 for p in self.parents():
1405 for p in self.parents():
1405 phase = max(phase, p.phase())
1406 phase = max(phase, p.phase())
1406 return phase
1407 return phase
1407
1408
1408 def hidden(self):
1409 def hidden(self):
1409 return False
1410 return False
1410
1411
1411 def children(self):
1412 def children(self):
1412 return []
1413 return []
1413
1414
1414 def flags(self, path):
1415 def flags(self, path):
1415 if r'_manifest' in self.__dict__:
1416 if r'_manifest' in self.__dict__:
1416 try:
1417 try:
1417 return self._manifest.flags(path)
1418 return self._manifest.flags(path)
1418 except KeyError:
1419 except KeyError:
1419 return ''
1420 return ''
1420
1421
1421 try:
1422 try:
1422 return self._flagfunc(path)
1423 return self._flagfunc(path)
1423 except OSError:
1424 except OSError:
1424 return ''
1425 return ''
1425
1426
1426 def ancestor(self, c2):
1427 def ancestor(self, c2):
1427 """return the "best" ancestor context of self and c2"""
1428 """return the "best" ancestor context of self and c2"""
1428 return self._parents[0].ancestor(c2) # punt on two parents for now
1429 return self._parents[0].ancestor(c2) # punt on two parents for now
1429
1430
1430 def walk(self, match):
1431 def walk(self, match):
1431 '''Generates matching file names.'''
1432 '''Generates matching file names.'''
1432 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1433 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1433 True, False))
1434 True, False))
1434
1435
1435 def matches(self, match):
1436 def matches(self, match):
1436 return sorted(self._repo.dirstate.matches(match))
1437 return sorted(self._repo.dirstate.matches(match))
1437
1438
1438 def ancestors(self):
1439 def ancestors(self):
1439 for p in self._parents:
1440 for p in self._parents:
1440 yield p
1441 yield p
1441 for a in self._repo.changelog.ancestors(
1442 for a in self._repo.changelog.ancestors(
1442 [p.rev() for p in self._parents]):
1443 [p.rev() for p in self._parents]):
1443 yield changectx(self._repo, a)
1444 yield changectx(self._repo, a)
1444
1445
1445 def markcommitted(self, node):
1446 def markcommitted(self, node):
1446 """Perform post-commit cleanup necessary after committing this ctx
1447 """Perform post-commit cleanup necessary after committing this ctx
1447
1448
1448 Specifically, this updates backing stores this working context
1449 Specifically, this updates backing stores this working context
1449 wraps to reflect the fact that the changes reflected by this
1450 wraps to reflect the fact that the changes reflected by this
1450 workingctx have been committed. For example, it marks
1451 workingctx have been committed. For example, it marks
1451 modified and added files as normal in the dirstate.
1452 modified and added files as normal in the dirstate.
1452
1453
1453 """
1454 """
1454
1455
1455 with self._repo.dirstate.parentchange():
1456 with self._repo.dirstate.parentchange():
1456 for f in self.modified() + self.added():
1457 for f in self.modified() + self.added():
1457 self._repo.dirstate.normal(f)
1458 self._repo.dirstate.normal(f)
1458 for f in self.removed():
1459 for f in self.removed():
1459 self._repo.dirstate.drop(f)
1460 self._repo.dirstate.drop(f)
1460 self._repo.dirstate.setparents(node)
1461 self._repo.dirstate.setparents(node)
1461
1462
1462 # write changes out explicitly, because nesting wlock at
1463 # write changes out explicitly, because nesting wlock at
1463 # runtime may prevent 'wlock.release()' in 'repo.commit()'
1464 # runtime may prevent 'wlock.release()' in 'repo.commit()'
1464 # from immediately doing so for subsequent changing files
1465 # from immediately doing so for subsequent changing files
1465 self._repo.dirstate.write(self._repo.currenttransaction())
1466 self._repo.dirstate.write(self._repo.currenttransaction())
1466
1467
1467 def dirty(self, missing=False, merge=True, branch=True):
1468 def dirty(self, missing=False, merge=True, branch=True):
1468 return False
1469 return False
1469
1470
1470 class workingctx(committablectx):
1471 class workingctx(committablectx):
1471 """A workingctx object makes access to data related to
1472 """A workingctx object makes access to data related to
1472 the current working directory convenient.
1473 the current working directory convenient.
1473 date - any valid date string or (unixtime, offset), or None.
1474 date - any valid date string or (unixtime, offset), or None.
1474 user - username string, or None.
1475 user - username string, or None.
1475 extra - a dictionary of extra values, or None.
1476 extra - a dictionary of extra values, or None.
1476 changes - a list of file lists as returned by localrepo.status()
1477 changes - a list of file lists as returned by localrepo.status()
1477 or None to use the repository status.
1478 or None to use the repository status.
1478 """
1479 """
1479 def __init__(self, repo, text="", user=None, date=None, extra=None,
1480 def __init__(self, repo, text="", user=None, date=None, extra=None,
1480 changes=None):
1481 changes=None):
1481 super(workingctx, self).__init__(repo, text, user, date, extra, changes)
1482 super(workingctx, self).__init__(repo, text, user, date, extra, changes)
1482
1483
1483 def __iter__(self):
1484 def __iter__(self):
1484 d = self._repo.dirstate
1485 d = self._repo.dirstate
1485 for f in d:
1486 for f in d:
1486 if d[f] != 'r':
1487 if d[f] != 'r':
1487 yield f
1488 yield f
1488
1489
1489 def __contains__(self, key):
1490 def __contains__(self, key):
1490 return self._repo.dirstate[key] not in "?r"
1491 return self._repo.dirstate[key] not in "?r"
1491
1492
1492 def hex(self):
1493 def hex(self):
1493 return hex(wdirid)
1494 return hex(wdirid)
1494
1495
1495 @propertycache
1496 @propertycache
1496 def _parents(self):
1497 def _parents(self):
1497 p = self._repo.dirstate.parents()
1498 p = self._repo.dirstate.parents()
1498 if p[1] == nullid:
1499 if p[1] == nullid:
1499 p = p[:-1]
1500 p = p[:-1]
1500 return [changectx(self._repo, x) for x in p]
1501 return [changectx(self._repo, x) for x in p]
1501
1502
1502 def filectx(self, path, filelog=None):
1503 def filectx(self, path, filelog=None):
1503 """get a file context from the working directory"""
1504 """get a file context from the working directory"""
1504 return workingfilectx(self._repo, path, workingctx=self,
1505 return workingfilectx(self._repo, path, workingctx=self,
1505 filelog=filelog)
1506 filelog=filelog)
1506
1507
1507 def dirty(self, missing=False, merge=True, branch=True):
1508 def dirty(self, missing=False, merge=True, branch=True):
1508 "check whether a working directory is modified"
1509 "check whether a working directory is modified"
1509 # check subrepos first
1510 # check subrepos first
1510 for s in sorted(self.substate):
1511 for s in sorted(self.substate):
1511 if self.sub(s).dirty():
1512 if self.sub(s).dirty():
1512 return True
1513 return True
1513 # check current working dir
1514 # check current working dir
1514 return ((merge and self.p2()) or
1515 return ((merge and self.p2()) or
1515 (branch and self.branch() != self.p1().branch()) or
1516 (branch and self.branch() != self.p1().branch()) or
1516 self.modified() or self.added() or self.removed() or
1517 self.modified() or self.added() or self.removed() or
1517 (missing and self.deleted()))
1518 (missing and self.deleted()))
1518
1519
1519 def add(self, list, prefix=""):
1520 def add(self, list, prefix=""):
1520 join = lambda f: os.path.join(prefix, f)
1521 join = lambda f: os.path.join(prefix, f)
1521 with self._repo.wlock():
1522 with self._repo.wlock():
1522 ui, ds = self._repo.ui, self._repo.dirstate
1523 ui, ds = self._repo.ui, self._repo.dirstate
1523 rejected = []
1524 rejected = []
1524 lstat = self._repo.wvfs.lstat
1525 lstat = self._repo.wvfs.lstat
1525 for f in list:
1526 for f in list:
1526 scmutil.checkportable(ui, join(f))
1527 scmutil.checkportable(ui, join(f))
1527 try:
1528 try:
1528 st = lstat(f)
1529 st = lstat(f)
1529 except OSError:
1530 except OSError:
1530 ui.warn(_("%s does not exist!\n") % join(f))
1531 ui.warn(_("%s does not exist!\n") % join(f))
1531 rejected.append(f)
1532 rejected.append(f)
1532 continue
1533 continue
1533 if st.st_size > 10000000:
1534 if st.st_size > 10000000:
1534 ui.warn(_("%s: up to %d MB of RAM may be required "
1535 ui.warn(_("%s: up to %d MB of RAM may be required "
1535 "to manage this file\n"
1536 "to manage this file\n"
1536 "(use 'hg revert %s' to cancel the "
1537 "(use 'hg revert %s' to cancel the "
1537 "pending addition)\n")
1538 "pending addition)\n")
1538 % (f, 3 * st.st_size // 1000000, join(f)))
1539 % (f, 3 * st.st_size // 1000000, join(f)))
1539 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1540 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1540 ui.warn(_("%s not added: only files and symlinks "
1541 ui.warn(_("%s not added: only files and symlinks "
1541 "supported currently\n") % join(f))
1542 "supported currently\n") % join(f))
1542 rejected.append(f)
1543 rejected.append(f)
1543 elif ds[f] in 'amn':
1544 elif ds[f] in 'amn':
1544 ui.warn(_("%s already tracked!\n") % join(f))
1545 ui.warn(_("%s already tracked!\n") % join(f))
1545 elif ds[f] == 'r':
1546 elif ds[f] == 'r':
1546 ds.normallookup(f)
1547 ds.normallookup(f)
1547 else:
1548 else:
1548 ds.add(f)
1549 ds.add(f)
1549 return rejected
1550 return rejected
1550
1551
1551 def forget(self, files, prefix=""):
1552 def forget(self, files, prefix=""):
1552 join = lambda f: os.path.join(prefix, f)
1553 join = lambda f: os.path.join(prefix, f)
1553 with self._repo.wlock():
1554 with self._repo.wlock():
1554 rejected = []
1555 rejected = []
1555 for f in files:
1556 for f in files:
1556 if f not in self._repo.dirstate:
1557 if f not in self._repo.dirstate:
1557 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1558 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1558 rejected.append(f)
1559 rejected.append(f)
1559 elif self._repo.dirstate[f] != 'a':
1560 elif self._repo.dirstate[f] != 'a':
1560 self._repo.dirstate.remove(f)
1561 self._repo.dirstate.remove(f)
1561 else:
1562 else:
1562 self._repo.dirstate.drop(f)
1563 self._repo.dirstate.drop(f)
1563 return rejected
1564 return rejected
1564
1565
1565 def undelete(self, list):
1566 def undelete(self, list):
1566 pctxs = self.parents()
1567 pctxs = self.parents()
1567 with self._repo.wlock():
1568 with self._repo.wlock():
1568 for f in list:
1569 for f in list:
1569 if self._repo.dirstate[f] != 'r':
1570 if self._repo.dirstate[f] != 'r':
1570 self._repo.ui.warn(_("%s not removed!\n") % f)
1571 self._repo.ui.warn(_("%s not removed!\n") % f)
1571 else:
1572 else:
1572 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1573 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1573 t = fctx.data()
1574 t = fctx.data()
1574 self._repo.wwrite(f, t, fctx.flags())
1575 self._repo.wwrite(f, t, fctx.flags())
1575 self._repo.dirstate.normal(f)
1576 self._repo.dirstate.normal(f)
1576
1577
1577 def copy(self, source, dest):
1578 def copy(self, source, dest):
1578 try:
1579 try:
1579 st = self._repo.wvfs.lstat(dest)
1580 st = self._repo.wvfs.lstat(dest)
1580 except OSError as err:
1581 except OSError as err:
1581 if err.errno != errno.ENOENT:
1582 if err.errno != errno.ENOENT:
1582 raise
1583 raise
1583 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1584 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1584 return
1585 return
1585 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1586 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1586 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1587 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1587 "symbolic link\n") % dest)
1588 "symbolic link\n") % dest)
1588 else:
1589 else:
1589 with self._repo.wlock():
1590 with self._repo.wlock():
1590 if self._repo.dirstate[dest] in '?':
1591 if self._repo.dirstate[dest] in '?':
1591 self._repo.dirstate.add(dest)
1592 self._repo.dirstate.add(dest)
1592 elif self._repo.dirstate[dest] in 'r':
1593 elif self._repo.dirstate[dest] in 'r':
1593 self._repo.dirstate.normallookup(dest)
1594 self._repo.dirstate.normallookup(dest)
1594 self._repo.dirstate.copy(source, dest)
1595 self._repo.dirstate.copy(source, dest)
1595
1596
1596 def match(self, pats=None, include=None, exclude=None, default='glob',
1597 def match(self, pats=None, include=None, exclude=None, default='glob',
1597 listsubrepos=False, badfn=None):
1598 listsubrepos=False, badfn=None):
1598 r = self._repo
1599 r = self._repo
1599
1600
1600 # Only a case insensitive filesystem needs magic to translate user input
1601 # Only a case insensitive filesystem needs magic to translate user input
1601 # to actual case in the filesystem.
1602 # to actual case in the filesystem.
1602 icasefs = not util.fscasesensitive(r.root)
1603 icasefs = not util.fscasesensitive(r.root)
1603 return matchmod.match(r.root, r.getcwd(), pats, include, exclude,
1604 return matchmod.match(r.root, r.getcwd(), pats, include, exclude,
1604 default, auditor=r.auditor, ctx=self,
1605 default, auditor=r.auditor, ctx=self,
1605 listsubrepos=listsubrepos, badfn=badfn,
1606 listsubrepos=listsubrepos, badfn=badfn,
1606 icasefs=icasefs)
1607 icasefs=icasefs)
1607
1608
1608 def _filtersuspectsymlink(self, files):
1609 def _filtersuspectsymlink(self, files):
1609 if not files or self._repo.dirstate._checklink:
1610 if not files or self._repo.dirstate._checklink:
1610 return files
1611 return files
1611
1612
1612 # Symlink placeholders may get non-symlink-like contents
1613 # Symlink placeholders may get non-symlink-like contents
1613 # via user error or dereferencing by NFS or Samba servers,
1614 # via user error or dereferencing by NFS or Samba servers,
1614 # so we filter out any placeholders that don't look like a
1615 # so we filter out any placeholders that don't look like a
1615 # symlink
1616 # symlink
1616 sane = []
1617 sane = []
1617 for f in files:
1618 for f in files:
1618 if self.flags(f) == 'l':
1619 if self.flags(f) == 'l':
1619 d = self[f].data()
1620 d = self[f].data()
1620 if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
1621 if d == '' or len(d) >= 1024 or '\n' in d or util.binary(d):
1621 self._repo.ui.debug('ignoring suspect symlink placeholder'
1622 self._repo.ui.debug('ignoring suspect symlink placeholder'
1622 ' "%s"\n' % f)
1623 ' "%s"\n' % f)
1623 continue
1624 continue
1624 sane.append(f)
1625 sane.append(f)
1625 return sane
1626 return sane
1626
1627
1627 def _checklookup(self, files):
1628 def _checklookup(self, files):
1628 # check for any possibly clean files
1629 # check for any possibly clean files
1629 if not files:
1630 if not files:
1630 return [], [], []
1631 return [], [], []
1631
1632
1632 modified = []
1633 modified = []
1633 deleted = []
1634 deleted = []
1634 fixup = []
1635 fixup = []
1635 pctx = self._parents[0]
1636 pctx = self._parents[0]
1636 # do a full compare of any files that might have changed
1637 # do a full compare of any files that might have changed
1637 for f in sorted(files):
1638 for f in sorted(files):
1638 try:
1639 try:
1639 # This will return True for a file that got replaced by a
1640 # This will return True for a file that got replaced by a
1640 # directory in the interim, but fixing that is pretty hard.
1641 # directory in the interim, but fixing that is pretty hard.
1641 if (f not in pctx or self.flags(f) != pctx.flags(f)
1642 if (f not in pctx or self.flags(f) != pctx.flags(f)
1642 or pctx[f].cmp(self[f])):
1643 or pctx[f].cmp(self[f])):
1643 modified.append(f)
1644 modified.append(f)
1644 else:
1645 else:
1645 fixup.append(f)
1646 fixup.append(f)
1646 except (IOError, OSError):
1647 except (IOError, OSError):
1647 # A file become inaccessible in between? Mark it as deleted,
1648 # A file become inaccessible in between? Mark it as deleted,
1648 # matching dirstate behavior (issue5584).
1649 # matching dirstate behavior (issue5584).
1649 # The dirstate has more complex behavior around whether a
1650 # The dirstate has more complex behavior around whether a
1650 # missing file matches a directory, etc, but we don't need to
1651 # missing file matches a directory, etc, but we don't need to
1651 # bother with that: if f has made it to this point, we're sure
1652 # bother with that: if f has made it to this point, we're sure
1652 # it's in the dirstate.
1653 # it's in the dirstate.
1653 deleted.append(f)
1654 deleted.append(f)
1654
1655
1655 return modified, deleted, fixup
1656 return modified, deleted, fixup
1656
1657
1657 def _poststatusfixup(self, status, fixup):
1658 def _poststatusfixup(self, status, fixup):
1658 """update dirstate for files that are actually clean"""
1659 """update dirstate for files that are actually clean"""
1659 poststatus = self._repo.postdsstatus()
1660 poststatus = self._repo.postdsstatus()
1660 if fixup or poststatus:
1661 if fixup or poststatus:
1661 try:
1662 try:
1662 oldid = self._repo.dirstate.identity()
1663 oldid = self._repo.dirstate.identity()
1663
1664
1664 # updating the dirstate is optional
1665 # updating the dirstate is optional
1665 # so we don't wait on the lock
1666 # so we don't wait on the lock
1666 # wlock can invalidate the dirstate, so cache normal _after_
1667 # wlock can invalidate the dirstate, so cache normal _after_
1667 # taking the lock
1668 # taking the lock
1668 with self._repo.wlock(False):
1669 with self._repo.wlock(False):
1669 if self._repo.dirstate.identity() == oldid:
1670 if self._repo.dirstate.identity() == oldid:
1670 if fixup:
1671 if fixup:
1671 normal = self._repo.dirstate.normal
1672 normal = self._repo.dirstate.normal
1672 for f in fixup:
1673 for f in fixup:
1673 normal(f)
1674 normal(f)
1674 # write changes out explicitly, because nesting
1675 # write changes out explicitly, because nesting
1675 # wlock at runtime may prevent 'wlock.release()'
1676 # wlock at runtime may prevent 'wlock.release()'
1676 # after this block from doing so for subsequent
1677 # after this block from doing so for subsequent
1677 # changing files
1678 # changing files
1678 tr = self._repo.currenttransaction()
1679 tr = self._repo.currenttransaction()
1679 self._repo.dirstate.write(tr)
1680 self._repo.dirstate.write(tr)
1680
1681
1681 if poststatus:
1682 if poststatus:
1682 for ps in poststatus:
1683 for ps in poststatus:
1683 ps(self, status)
1684 ps(self, status)
1684 else:
1685 else:
1685 # in this case, writing changes out breaks
1686 # in this case, writing changes out breaks
1686 # consistency, because .hg/dirstate was
1687 # consistency, because .hg/dirstate was
1687 # already changed simultaneously after last
1688 # already changed simultaneously after last
1688 # caching (see also issue5584 for detail)
1689 # caching (see also issue5584 for detail)
1689 self._repo.ui.debug('skip updating dirstate: '
1690 self._repo.ui.debug('skip updating dirstate: '
1690 'identity mismatch\n')
1691 'identity mismatch\n')
1691 except error.LockError:
1692 except error.LockError:
1692 pass
1693 pass
1693 finally:
1694 finally:
1694 # Even if the wlock couldn't be grabbed, clear out the list.
1695 # Even if the wlock couldn't be grabbed, clear out the list.
1695 self._repo.clearpostdsstatus()
1696 self._repo.clearpostdsstatus()
1696
1697
1697 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1698 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1698 unknown=False):
1699 unknown=False):
1699 '''Gets the status from the dirstate -- internal use only.'''
1700 '''Gets the status from the dirstate -- internal use only.'''
1700 listignored, listclean, listunknown = ignored, clean, unknown
1701 listignored, listclean, listunknown = ignored, clean, unknown
1701 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1702 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1702 subrepos = []
1703 subrepos = []
1703 if '.hgsub' in self:
1704 if '.hgsub' in self:
1704 subrepos = sorted(self.substate)
1705 subrepos = sorted(self.substate)
1705 cmp, s = self._repo.dirstate.status(match, subrepos, listignored,
1706 cmp, s = self._repo.dirstate.status(match, subrepos, listignored,
1706 listclean, listunknown)
1707 listclean, listunknown)
1707
1708
1708 # check for any possibly clean files
1709 # check for any possibly clean files
1709 fixup = []
1710 fixup = []
1710 if cmp:
1711 if cmp:
1711 modified2, deleted2, fixup = self._checklookup(cmp)
1712 modified2, deleted2, fixup = self._checklookup(cmp)
1712 s.modified.extend(modified2)
1713 s.modified.extend(modified2)
1713 s.deleted.extend(deleted2)
1714 s.deleted.extend(deleted2)
1714
1715
1715 if fixup and listclean:
1716 if fixup and listclean:
1716 s.clean.extend(fixup)
1717 s.clean.extend(fixup)
1717
1718
1718 self._poststatusfixup(s, fixup)
1719 self._poststatusfixup(s, fixup)
1719
1720
1720 if match.always():
1721 if match.always():
1721 # cache for performance
1722 # cache for performance
1722 if s.unknown or s.ignored or s.clean:
1723 if s.unknown or s.ignored or s.clean:
1723 # "_status" is cached with list*=False in the normal route
1724 # "_status" is cached with list*=False in the normal route
1724 self._status = scmutil.status(s.modified, s.added, s.removed,
1725 self._status = scmutil.status(s.modified, s.added, s.removed,
1725 s.deleted, [], [], [])
1726 s.deleted, [], [], [])
1726 else:
1727 else:
1727 self._status = s
1728 self._status = s
1728
1729
1729 return s
1730 return s
1730
1731
1731 @propertycache
1732 @propertycache
1732 def _manifest(self):
1733 def _manifest(self):
1733 """generate a manifest corresponding to the values in self._status
1734 """generate a manifest corresponding to the values in self._status
1734
1735
1735 This reuse the file nodeid from parent, but we use special node
1736 This reuse the file nodeid from parent, but we use special node
1736 identifiers for added and modified files. This is used by manifests
1737 identifiers for added and modified files. This is used by manifests
1737 merge to see that files are different and by update logic to avoid
1738 merge to see that files are different and by update logic to avoid
1738 deleting newly added files.
1739 deleting newly added files.
1739 """
1740 """
1740 return self._buildstatusmanifest(self._status)
1741 return self._buildstatusmanifest(self._status)
1741
1742
1742 def _buildstatusmanifest(self, status):
1743 def _buildstatusmanifest(self, status):
1743 """Builds a manifest that includes the given status results."""
1744 """Builds a manifest that includes the given status results."""
1744 parents = self.parents()
1745 parents = self.parents()
1745
1746
1746 man = parents[0].manifest().copy()
1747 man = parents[0].manifest().copy()
1747
1748
1748 ff = self._flagfunc
1749 ff = self._flagfunc
1749 for i, l in ((addednodeid, status.added),
1750 for i, l in ((addednodeid, status.added),
1750 (modifiednodeid, status.modified)):
1751 (modifiednodeid, status.modified)):
1751 for f in l:
1752 for f in l:
1752 man[f] = i
1753 man[f] = i
1753 try:
1754 try:
1754 man.setflag(f, ff(f))
1755 man.setflag(f, ff(f))
1755 except OSError:
1756 except OSError:
1756 pass
1757 pass
1757
1758
1758 for f in status.deleted + status.removed:
1759 for f in status.deleted + status.removed:
1759 if f in man:
1760 if f in man:
1760 del man[f]
1761 del man[f]
1761
1762
1762 return man
1763 return man
1763
1764
1764 def _buildstatus(self, other, s, match, listignored, listclean,
1765 def _buildstatus(self, other, s, match, listignored, listclean,
1765 listunknown):
1766 listunknown):
1766 """build a status with respect to another context
1767 """build a status with respect to another context
1767
1768
1768 This includes logic for maintaining the fast path of status when
1769 This includes logic for maintaining the fast path of status when
1769 comparing the working directory against its parent, which is to skip
1770 comparing the working directory against its parent, which is to skip
1770 building a new manifest if self (working directory) is not comparing
1771 building a new manifest if self (working directory) is not comparing
1771 against its parent (repo['.']).
1772 against its parent (repo['.']).
1772 """
1773 """
1773 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1774 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1774 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1775 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1775 # might have accidentally ended up with the entire contents of the file
1776 # might have accidentally ended up with the entire contents of the file
1776 # they are supposed to be linking to.
1777 # they are supposed to be linking to.
1777 s.modified[:] = self._filtersuspectsymlink(s.modified)
1778 s.modified[:] = self._filtersuspectsymlink(s.modified)
1778 if other != self._repo['.']:
1779 if other != self._repo['.']:
1779 s = super(workingctx, self)._buildstatus(other, s, match,
1780 s = super(workingctx, self)._buildstatus(other, s, match,
1780 listignored, listclean,
1781 listignored, listclean,
1781 listunknown)
1782 listunknown)
1782 return s
1783 return s
1783
1784
1784 def _matchstatus(self, other, match):
1785 def _matchstatus(self, other, match):
1785 """override the match method with a filter for directory patterns
1786 """override the match method with a filter for directory patterns
1786
1787
1787 We use inheritance to customize the match.bad method only in cases of
1788 We use inheritance to customize the match.bad method only in cases of
1788 workingctx since it belongs only to the working directory when
1789 workingctx since it belongs only to the working directory when
1789 comparing against the parent changeset.
1790 comparing against the parent changeset.
1790
1791
1791 If we aren't comparing against the working directory's parent, then we
1792 If we aren't comparing against the working directory's parent, then we
1792 just use the default match object sent to us.
1793 just use the default match object sent to us.
1793 """
1794 """
1794 superself = super(workingctx, self)
1795 superself = super(workingctx, self)
1795 match = superself._matchstatus(other, match)
1796 match = superself._matchstatus(other, match)
1796 if other != self._repo['.']:
1797 if other != self._repo['.']:
1797 def bad(f, msg):
1798 def bad(f, msg):
1798 # 'f' may be a directory pattern from 'match.files()',
1799 # 'f' may be a directory pattern from 'match.files()',
1799 # so 'f not in ctx1' is not enough
1800 # so 'f not in ctx1' is not enough
1800 if f not in other and not other.hasdir(f):
1801 if f not in other and not other.hasdir(f):
1801 self._repo.ui.warn('%s: %s\n' %
1802 self._repo.ui.warn('%s: %s\n' %
1802 (self._repo.dirstate.pathto(f), msg))
1803 (self._repo.dirstate.pathto(f), msg))
1803 match.bad = bad
1804 match.bad = bad
1804 return match
1805 return match
1805
1806
1807 def markcommitted(self, node):
1808 super(workingctx, self).markcommitted(node)
1809
1810 sparse.aftercommit(self._repo, node)
1811
1806 class committablefilectx(basefilectx):
1812 class committablefilectx(basefilectx):
1807 """A committablefilectx provides common functionality for a file context
1813 """A committablefilectx provides common functionality for a file context
1808 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1814 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1809 def __init__(self, repo, path, filelog=None, ctx=None):
1815 def __init__(self, repo, path, filelog=None, ctx=None):
1810 self._repo = repo
1816 self._repo = repo
1811 self._path = path
1817 self._path = path
1812 self._changeid = None
1818 self._changeid = None
1813 self._filerev = self._filenode = None
1819 self._filerev = self._filenode = None
1814
1820
1815 if filelog is not None:
1821 if filelog is not None:
1816 self._filelog = filelog
1822 self._filelog = filelog
1817 if ctx:
1823 if ctx:
1818 self._changectx = ctx
1824 self._changectx = ctx
1819
1825
1820 def __nonzero__(self):
1826 def __nonzero__(self):
1821 return True
1827 return True
1822
1828
1823 __bool__ = __nonzero__
1829 __bool__ = __nonzero__
1824
1830
1825 def linkrev(self):
1831 def linkrev(self):
1826 # linked to self._changectx no matter if file is modified or not
1832 # linked to self._changectx no matter if file is modified or not
1827 return self.rev()
1833 return self.rev()
1828
1834
1829 def parents(self):
1835 def parents(self):
1830 '''return parent filectxs, following copies if necessary'''
1836 '''return parent filectxs, following copies if necessary'''
1831 def filenode(ctx, path):
1837 def filenode(ctx, path):
1832 return ctx._manifest.get(path, nullid)
1838 return ctx._manifest.get(path, nullid)
1833
1839
1834 path = self._path
1840 path = self._path
1835 fl = self._filelog
1841 fl = self._filelog
1836 pcl = self._changectx._parents
1842 pcl = self._changectx._parents
1837 renamed = self.renamed()
1843 renamed = self.renamed()
1838
1844
1839 if renamed:
1845 if renamed:
1840 pl = [renamed + (None,)]
1846 pl = [renamed + (None,)]
1841 else:
1847 else:
1842 pl = [(path, filenode(pcl[0], path), fl)]
1848 pl = [(path, filenode(pcl[0], path), fl)]
1843
1849
1844 for pc in pcl[1:]:
1850 for pc in pcl[1:]:
1845 pl.append((path, filenode(pc, path), fl))
1851 pl.append((path, filenode(pc, path), fl))
1846
1852
1847 return [self._parentfilectx(p, fileid=n, filelog=l)
1853 return [self._parentfilectx(p, fileid=n, filelog=l)
1848 for p, n, l in pl if n != nullid]
1854 for p, n, l in pl if n != nullid]
1849
1855
1850 def children(self):
1856 def children(self):
1851 return []
1857 return []
1852
1858
1853 class workingfilectx(committablefilectx):
1859 class workingfilectx(committablefilectx):
1854 """A workingfilectx object makes access to data related to a particular
1860 """A workingfilectx object makes access to data related to a particular
1855 file in the working directory convenient."""
1861 file in the working directory convenient."""
1856 def __init__(self, repo, path, filelog=None, workingctx=None):
1862 def __init__(self, repo, path, filelog=None, workingctx=None):
1857 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
1863 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
1858
1864
1859 @propertycache
1865 @propertycache
1860 def _changectx(self):
1866 def _changectx(self):
1861 return workingctx(self._repo)
1867 return workingctx(self._repo)
1862
1868
1863 def data(self):
1869 def data(self):
1864 return self._repo.wread(self._path)
1870 return self._repo.wread(self._path)
1865 def renamed(self):
1871 def renamed(self):
1866 rp = self._repo.dirstate.copied(self._path)
1872 rp = self._repo.dirstate.copied(self._path)
1867 if not rp:
1873 if not rp:
1868 return None
1874 return None
1869 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1875 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1870
1876
1871 def size(self):
1877 def size(self):
1872 return self._repo.wvfs.lstat(self._path).st_size
1878 return self._repo.wvfs.lstat(self._path).st_size
1873 def date(self):
1879 def date(self):
1874 t, tz = self._changectx.date()
1880 t, tz = self._changectx.date()
1875 try:
1881 try:
1876 return (self._repo.wvfs.lstat(self._path).st_mtime, tz)
1882 return (self._repo.wvfs.lstat(self._path).st_mtime, tz)
1877 except OSError as err:
1883 except OSError as err:
1878 if err.errno != errno.ENOENT:
1884 if err.errno != errno.ENOENT:
1879 raise
1885 raise
1880 return (t, tz)
1886 return (t, tz)
1881
1887
1882 def exists(self):
1888 def exists(self):
1883 return self._repo.wvfs.exists(self._path)
1889 return self._repo.wvfs.exists(self._path)
1884
1890
1885 def lexists(self):
1891 def lexists(self):
1886 return self._repo.wvfs.lexists(self._path)
1892 return self._repo.wvfs.lexists(self._path)
1887
1893
1888 def audit(self):
1894 def audit(self):
1889 return self._repo.wvfs.audit(self._path)
1895 return self._repo.wvfs.audit(self._path)
1890
1896
1891 def cmp(self, fctx):
1897 def cmp(self, fctx):
1892 """compare with other file context
1898 """compare with other file context
1893
1899
1894 returns True if different than fctx.
1900 returns True if different than fctx.
1895 """
1901 """
1896 # fctx should be a filectx (not a workingfilectx)
1902 # fctx should be a filectx (not a workingfilectx)
1897 # invert comparison to reuse the same code path
1903 # invert comparison to reuse the same code path
1898 return fctx.cmp(self)
1904 return fctx.cmp(self)
1899
1905
1900 def remove(self, ignoremissing=False):
1906 def remove(self, ignoremissing=False):
1901 """wraps unlink for a repo's working directory"""
1907 """wraps unlink for a repo's working directory"""
1902 self._repo.wvfs.unlinkpath(self._path, ignoremissing=ignoremissing)
1908 self._repo.wvfs.unlinkpath(self._path, ignoremissing=ignoremissing)
1903
1909
1904 def write(self, data, flags, backgroundclose=False):
1910 def write(self, data, flags, backgroundclose=False):
1905 """wraps repo.wwrite"""
1911 """wraps repo.wwrite"""
1906 self._repo.wwrite(self._path, data, flags,
1912 self._repo.wwrite(self._path, data, flags,
1907 backgroundclose=backgroundclose)
1913 backgroundclose=backgroundclose)
1908
1914
1909 def setflags(self, l, x):
1915 def setflags(self, l, x):
1910 self._repo.wvfs.setflags(self._path, l, x)
1916 self._repo.wvfs.setflags(self._path, l, x)
1911
1917
1912 class workingcommitctx(workingctx):
1918 class workingcommitctx(workingctx):
1913 """A workingcommitctx object makes access to data related to
1919 """A workingcommitctx object makes access to data related to
1914 the revision being committed convenient.
1920 the revision being committed convenient.
1915
1921
1916 This hides changes in the working directory, if they aren't
1922 This hides changes in the working directory, if they aren't
1917 committed in this context.
1923 committed in this context.
1918 """
1924 """
1919 def __init__(self, repo, changes,
1925 def __init__(self, repo, changes,
1920 text="", user=None, date=None, extra=None):
1926 text="", user=None, date=None, extra=None):
1921 super(workingctx, self).__init__(repo, text, user, date, extra,
1927 super(workingctx, self).__init__(repo, text, user, date, extra,
1922 changes)
1928 changes)
1923
1929
1924 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1930 def _dirstatestatus(self, match=None, ignored=False, clean=False,
1925 unknown=False):
1931 unknown=False):
1926 """Return matched files only in ``self._status``
1932 """Return matched files only in ``self._status``
1927
1933
1928 Uncommitted files appear "clean" via this context, even if
1934 Uncommitted files appear "clean" via this context, even if
1929 they aren't actually so in the working directory.
1935 they aren't actually so in the working directory.
1930 """
1936 """
1931 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1937 match = match or matchmod.always(self._repo.root, self._repo.getcwd())
1932 if clean:
1938 if clean:
1933 clean = [f for f in self._manifest if f not in self._changedset]
1939 clean = [f for f in self._manifest if f not in self._changedset]
1934 else:
1940 else:
1935 clean = []
1941 clean = []
1936 return scmutil.status([f for f in self._status.modified if match(f)],
1942 return scmutil.status([f for f in self._status.modified if match(f)],
1937 [f for f in self._status.added if match(f)],
1943 [f for f in self._status.added if match(f)],
1938 [f for f in self._status.removed if match(f)],
1944 [f for f in self._status.removed if match(f)],
1939 [], [], [], clean)
1945 [], [], [], clean)
1940
1946
1941 @propertycache
1947 @propertycache
1942 def _changedset(self):
1948 def _changedset(self):
1943 """Return the set of files changed in this context
1949 """Return the set of files changed in this context
1944 """
1950 """
1945 changed = set(self._status.modified)
1951 changed = set(self._status.modified)
1946 changed.update(self._status.added)
1952 changed.update(self._status.added)
1947 changed.update(self._status.removed)
1953 changed.update(self._status.removed)
1948 return changed
1954 return changed
1949
1955
1950 def makecachingfilectxfn(func):
1956 def makecachingfilectxfn(func):
1951 """Create a filectxfn that caches based on the path.
1957 """Create a filectxfn that caches based on the path.
1952
1958
1953 We can't use util.cachefunc because it uses all arguments as the cache
1959 We can't use util.cachefunc because it uses all arguments as the cache
1954 key and this creates a cycle since the arguments include the repo and
1960 key and this creates a cycle since the arguments include the repo and
1955 memctx.
1961 memctx.
1956 """
1962 """
1957 cache = {}
1963 cache = {}
1958
1964
1959 def getfilectx(repo, memctx, path):
1965 def getfilectx(repo, memctx, path):
1960 if path not in cache:
1966 if path not in cache:
1961 cache[path] = func(repo, memctx, path)
1967 cache[path] = func(repo, memctx, path)
1962 return cache[path]
1968 return cache[path]
1963
1969
1964 return getfilectx
1970 return getfilectx
1965
1971
1966 def memfilefromctx(ctx):
1972 def memfilefromctx(ctx):
1967 """Given a context return a memfilectx for ctx[path]
1973 """Given a context return a memfilectx for ctx[path]
1968
1974
1969 This is a convenience method for building a memctx based on another
1975 This is a convenience method for building a memctx based on another
1970 context.
1976 context.
1971 """
1977 """
1972 def getfilectx(repo, memctx, path):
1978 def getfilectx(repo, memctx, path):
1973 fctx = ctx[path]
1979 fctx = ctx[path]
1974 # this is weird but apparently we only keep track of one parent
1980 # this is weird but apparently we only keep track of one parent
1975 # (why not only store that instead of a tuple?)
1981 # (why not only store that instead of a tuple?)
1976 copied = fctx.renamed()
1982 copied = fctx.renamed()
1977 if copied:
1983 if copied:
1978 copied = copied[0]
1984 copied = copied[0]
1979 return memfilectx(repo, path, fctx.data(),
1985 return memfilectx(repo, path, fctx.data(),
1980 islink=fctx.islink(), isexec=fctx.isexec(),
1986 islink=fctx.islink(), isexec=fctx.isexec(),
1981 copied=copied, memctx=memctx)
1987 copied=copied, memctx=memctx)
1982
1988
1983 return getfilectx
1989 return getfilectx
1984
1990
1985 def memfilefrompatch(patchstore):
1991 def memfilefrompatch(patchstore):
1986 """Given a patch (e.g. patchstore object) return a memfilectx
1992 """Given a patch (e.g. patchstore object) return a memfilectx
1987
1993
1988 This is a convenience method for building a memctx based on a patchstore.
1994 This is a convenience method for building a memctx based on a patchstore.
1989 """
1995 """
1990 def getfilectx(repo, memctx, path):
1996 def getfilectx(repo, memctx, path):
1991 data, mode, copied = patchstore.getfile(path)
1997 data, mode, copied = patchstore.getfile(path)
1992 if data is None:
1998 if data is None:
1993 return None
1999 return None
1994 islink, isexec = mode
2000 islink, isexec = mode
1995 return memfilectx(repo, path, data, islink=islink,
2001 return memfilectx(repo, path, data, islink=islink,
1996 isexec=isexec, copied=copied,
2002 isexec=isexec, copied=copied,
1997 memctx=memctx)
2003 memctx=memctx)
1998
2004
1999 return getfilectx
2005 return getfilectx
2000
2006
2001 class memctx(committablectx):
2007 class memctx(committablectx):
2002 """Use memctx to perform in-memory commits via localrepo.commitctx().
2008 """Use memctx to perform in-memory commits via localrepo.commitctx().
2003
2009
2004 Revision information is supplied at initialization time while
2010 Revision information is supplied at initialization time while
2005 related files data and is made available through a callback
2011 related files data and is made available through a callback
2006 mechanism. 'repo' is the current localrepo, 'parents' is a
2012 mechanism. 'repo' is the current localrepo, 'parents' is a
2007 sequence of two parent revisions identifiers (pass None for every
2013 sequence of two parent revisions identifiers (pass None for every
2008 missing parent), 'text' is the commit message and 'files' lists
2014 missing parent), 'text' is the commit message and 'files' lists
2009 names of files touched by the revision (normalized and relative to
2015 names of files touched by the revision (normalized and relative to
2010 repository root).
2016 repository root).
2011
2017
2012 filectxfn(repo, memctx, path) is a callable receiving the
2018 filectxfn(repo, memctx, path) is a callable receiving the
2013 repository, the current memctx object and the normalized path of
2019 repository, the current memctx object and the normalized path of
2014 requested file, relative to repository root. It is fired by the
2020 requested file, relative to repository root. It is fired by the
2015 commit function for every file in 'files', but calls order is
2021 commit function for every file in 'files', but calls order is
2016 undefined. If the file is available in the revision being
2022 undefined. If the file is available in the revision being
2017 committed (updated or added), filectxfn returns a memfilectx
2023 committed (updated or added), filectxfn returns a memfilectx
2018 object. If the file was removed, filectxfn return None for recent
2024 object. If the file was removed, filectxfn return None for recent
2019 Mercurial. Moved files are represented by marking the source file
2025 Mercurial. Moved files are represented by marking the source file
2020 removed and the new file added with copy information (see
2026 removed and the new file added with copy information (see
2021 memfilectx).
2027 memfilectx).
2022
2028
2023 user receives the committer name and defaults to current
2029 user receives the committer name and defaults to current
2024 repository username, date is the commit date in any format
2030 repository username, date is the commit date in any format
2025 supported by util.parsedate() and defaults to current date, extra
2031 supported by util.parsedate() and defaults to current date, extra
2026 is a dictionary of metadata or is left empty.
2032 is a dictionary of metadata or is left empty.
2027 """
2033 """
2028
2034
2029 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2035 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2030 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2036 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2031 # this field to determine what to do in filectxfn.
2037 # this field to determine what to do in filectxfn.
2032 _returnnoneformissingfiles = True
2038 _returnnoneformissingfiles = True
2033
2039
2034 def __init__(self, repo, parents, text, files, filectxfn, user=None,
2040 def __init__(self, repo, parents, text, files, filectxfn, user=None,
2035 date=None, extra=None, branch=None, editor=False):
2041 date=None, extra=None, branch=None, editor=False):
2036 super(memctx, self).__init__(repo, text, user, date, extra)
2042 super(memctx, self).__init__(repo, text, user, date, extra)
2037 self._rev = None
2043 self._rev = None
2038 self._node = None
2044 self._node = None
2039 parents = [(p or nullid) for p in parents]
2045 parents = [(p or nullid) for p in parents]
2040 p1, p2 = parents
2046 p1, p2 = parents
2041 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
2047 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
2042 files = sorted(set(files))
2048 files = sorted(set(files))
2043 self._files = files
2049 self._files = files
2044 if branch is not None:
2050 if branch is not None:
2045 self._extra['branch'] = encoding.fromlocal(branch)
2051 self._extra['branch'] = encoding.fromlocal(branch)
2046 self.substate = {}
2052 self.substate = {}
2047
2053
2048 if isinstance(filectxfn, patch.filestore):
2054 if isinstance(filectxfn, patch.filestore):
2049 filectxfn = memfilefrompatch(filectxfn)
2055 filectxfn = memfilefrompatch(filectxfn)
2050 elif not callable(filectxfn):
2056 elif not callable(filectxfn):
2051 # if store is not callable, wrap it in a function
2057 # if store is not callable, wrap it in a function
2052 filectxfn = memfilefromctx(filectxfn)
2058 filectxfn = memfilefromctx(filectxfn)
2053
2059
2054 # memoizing increases performance for e.g. vcs convert scenarios.
2060 # memoizing increases performance for e.g. vcs convert scenarios.
2055 self._filectxfn = makecachingfilectxfn(filectxfn)
2061 self._filectxfn = makecachingfilectxfn(filectxfn)
2056
2062
2057 if editor:
2063 if editor:
2058 self._text = editor(self._repo, self, [])
2064 self._text = editor(self._repo, self, [])
2059 self._repo.savecommitmessage(self._text)
2065 self._repo.savecommitmessage(self._text)
2060
2066
2061 def filectx(self, path, filelog=None):
2067 def filectx(self, path, filelog=None):
2062 """get a file context from the working directory
2068 """get a file context from the working directory
2063
2069
2064 Returns None if file doesn't exist and should be removed."""
2070 Returns None if file doesn't exist and should be removed."""
2065 return self._filectxfn(self._repo, self, path)
2071 return self._filectxfn(self._repo, self, path)
2066
2072
2067 def commit(self):
2073 def commit(self):
2068 """commit context to the repo"""
2074 """commit context to the repo"""
2069 return self._repo.commitctx(self)
2075 return self._repo.commitctx(self)
2070
2076
2071 @propertycache
2077 @propertycache
2072 def _manifest(self):
2078 def _manifest(self):
2073 """generate a manifest based on the return values of filectxfn"""
2079 """generate a manifest based on the return values of filectxfn"""
2074
2080
2075 # keep this simple for now; just worry about p1
2081 # keep this simple for now; just worry about p1
2076 pctx = self._parents[0]
2082 pctx = self._parents[0]
2077 man = pctx.manifest().copy()
2083 man = pctx.manifest().copy()
2078
2084
2079 for f in self._status.modified:
2085 for f in self._status.modified:
2080 p1node = nullid
2086 p1node = nullid
2081 p2node = nullid
2087 p2node = nullid
2082 p = pctx[f].parents() # if file isn't in pctx, check p2?
2088 p = pctx[f].parents() # if file isn't in pctx, check p2?
2083 if len(p) > 0:
2089 if len(p) > 0:
2084 p1node = p[0].filenode()
2090 p1node = p[0].filenode()
2085 if len(p) > 1:
2091 if len(p) > 1:
2086 p2node = p[1].filenode()
2092 p2node = p[1].filenode()
2087 man[f] = revlog.hash(self[f].data(), p1node, p2node)
2093 man[f] = revlog.hash(self[f].data(), p1node, p2node)
2088
2094
2089 for f in self._status.added:
2095 for f in self._status.added:
2090 man[f] = revlog.hash(self[f].data(), nullid, nullid)
2096 man[f] = revlog.hash(self[f].data(), nullid, nullid)
2091
2097
2092 for f in self._status.removed:
2098 for f in self._status.removed:
2093 if f in man:
2099 if f in man:
2094 del man[f]
2100 del man[f]
2095
2101
2096 return man
2102 return man
2097
2103
2098 @propertycache
2104 @propertycache
2099 def _status(self):
2105 def _status(self):
2100 """Calculate exact status from ``files`` specified at construction
2106 """Calculate exact status from ``files`` specified at construction
2101 """
2107 """
2102 man1 = self.p1().manifest()
2108 man1 = self.p1().manifest()
2103 p2 = self._parents[1]
2109 p2 = self._parents[1]
2104 # "1 < len(self._parents)" can't be used for checking
2110 # "1 < len(self._parents)" can't be used for checking
2105 # existence of the 2nd parent, because "memctx._parents" is
2111 # existence of the 2nd parent, because "memctx._parents" is
2106 # explicitly initialized by the list, of which length is 2.
2112 # explicitly initialized by the list, of which length is 2.
2107 if p2.node() != nullid:
2113 if p2.node() != nullid:
2108 man2 = p2.manifest()
2114 man2 = p2.manifest()
2109 managing = lambda f: f in man1 or f in man2
2115 managing = lambda f: f in man1 or f in man2
2110 else:
2116 else:
2111 managing = lambda f: f in man1
2117 managing = lambda f: f in man1
2112
2118
2113 modified, added, removed = [], [], []
2119 modified, added, removed = [], [], []
2114 for f in self._files:
2120 for f in self._files:
2115 if not managing(f):
2121 if not managing(f):
2116 added.append(f)
2122 added.append(f)
2117 elif self[f]:
2123 elif self[f]:
2118 modified.append(f)
2124 modified.append(f)
2119 else:
2125 else:
2120 removed.append(f)
2126 removed.append(f)
2121
2127
2122 return scmutil.status(modified, added, removed, [], [], [], [])
2128 return scmutil.status(modified, added, removed, [], [], [], [])
2123
2129
2124 class memfilectx(committablefilectx):
2130 class memfilectx(committablefilectx):
2125 """memfilectx represents an in-memory file to commit.
2131 """memfilectx represents an in-memory file to commit.
2126
2132
2127 See memctx and committablefilectx for more details.
2133 See memctx and committablefilectx for more details.
2128 """
2134 """
2129 def __init__(self, repo, path, data, islink=False,
2135 def __init__(self, repo, path, data, islink=False,
2130 isexec=False, copied=None, memctx=None):
2136 isexec=False, copied=None, memctx=None):
2131 """
2137 """
2132 path is the normalized file path relative to repository root.
2138 path is the normalized file path relative to repository root.
2133 data is the file content as a string.
2139 data is the file content as a string.
2134 islink is True if the file is a symbolic link.
2140 islink is True if the file is a symbolic link.
2135 isexec is True if the file is executable.
2141 isexec is True if the file is executable.
2136 copied is the source file path if current file was copied in the
2142 copied is the source file path if current file was copied in the
2137 revision being committed, or None."""
2143 revision being committed, or None."""
2138 super(memfilectx, self).__init__(repo, path, None, memctx)
2144 super(memfilectx, self).__init__(repo, path, None, memctx)
2139 self._data = data
2145 self._data = data
2140 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
2146 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
2141 self._copied = None
2147 self._copied = None
2142 if copied:
2148 if copied:
2143 self._copied = (copied, nullid)
2149 self._copied = (copied, nullid)
2144
2150
2145 def data(self):
2151 def data(self):
2146 return self._data
2152 return self._data
2147
2153
2148 def remove(self, ignoremissing=False):
2154 def remove(self, ignoremissing=False):
2149 """wraps unlink for a repo's working directory"""
2155 """wraps unlink for a repo's working directory"""
2150 # need to figure out what to do here
2156 # need to figure out what to do here
2151 del self._changectx[self._path]
2157 del self._changectx[self._path]
2152
2158
2153 def write(self, data, flags):
2159 def write(self, data, flags):
2154 """wraps repo.wwrite"""
2160 """wraps repo.wwrite"""
2155 self._data = data
2161 self._data = data
2156
2162
2157 class overlayfilectx(committablefilectx):
2163 class overlayfilectx(committablefilectx):
2158 """Like memfilectx but take an original filectx and optional parameters to
2164 """Like memfilectx but take an original filectx and optional parameters to
2159 override parts of it. This is useful when fctx.data() is expensive (i.e.
2165 override parts of it. This is useful when fctx.data() is expensive (i.e.
2160 flag processor is expensive) and raw data, flags, and filenode could be
2166 flag processor is expensive) and raw data, flags, and filenode could be
2161 reused (ex. rebase or mode-only amend a REVIDX_EXTSTORED file).
2167 reused (ex. rebase or mode-only amend a REVIDX_EXTSTORED file).
2162 """
2168 """
2163
2169
2164 def __init__(self, originalfctx, datafunc=None, path=None, flags=None,
2170 def __init__(self, originalfctx, datafunc=None, path=None, flags=None,
2165 copied=None, ctx=None):
2171 copied=None, ctx=None):
2166 """originalfctx: filecontext to duplicate
2172 """originalfctx: filecontext to duplicate
2167
2173
2168 datafunc: None or a function to override data (file content). It is a
2174 datafunc: None or a function to override data (file content). It is a
2169 function to be lazy. path, flags, copied, ctx: None or overridden value
2175 function to be lazy. path, flags, copied, ctx: None or overridden value
2170
2176
2171 copied could be (path, rev), or False. copied could also be just path,
2177 copied could be (path, rev), or False. copied could also be just path,
2172 and will be converted to (path, nullid). This simplifies some callers.
2178 and will be converted to (path, nullid). This simplifies some callers.
2173 """
2179 """
2174
2180
2175 if path is None:
2181 if path is None:
2176 path = originalfctx.path()
2182 path = originalfctx.path()
2177 if ctx is None:
2183 if ctx is None:
2178 ctx = originalfctx.changectx()
2184 ctx = originalfctx.changectx()
2179 ctxmatch = lambda: True
2185 ctxmatch = lambda: True
2180 else:
2186 else:
2181 ctxmatch = lambda: ctx == originalfctx.changectx()
2187 ctxmatch = lambda: ctx == originalfctx.changectx()
2182
2188
2183 repo = originalfctx.repo()
2189 repo = originalfctx.repo()
2184 flog = originalfctx.filelog()
2190 flog = originalfctx.filelog()
2185 super(overlayfilectx, self).__init__(repo, path, flog, ctx)
2191 super(overlayfilectx, self).__init__(repo, path, flog, ctx)
2186
2192
2187 if copied is None:
2193 if copied is None:
2188 copied = originalfctx.renamed()
2194 copied = originalfctx.renamed()
2189 copiedmatch = lambda: True
2195 copiedmatch = lambda: True
2190 else:
2196 else:
2191 if copied and not isinstance(copied, tuple):
2197 if copied and not isinstance(copied, tuple):
2192 # repo._filecommit will recalculate copyrev so nullid is okay
2198 # repo._filecommit will recalculate copyrev so nullid is okay
2193 copied = (copied, nullid)
2199 copied = (copied, nullid)
2194 copiedmatch = lambda: copied == originalfctx.renamed()
2200 copiedmatch = lambda: copied == originalfctx.renamed()
2195
2201
2196 # When data, copied (could affect data), ctx (could affect filelog
2202 # When data, copied (could affect data), ctx (could affect filelog
2197 # parents) are not overridden, rawdata, rawflags, and filenode may be
2203 # parents) are not overridden, rawdata, rawflags, and filenode may be
2198 # reused (repo._filecommit should double check filelog parents).
2204 # reused (repo._filecommit should double check filelog parents).
2199 #
2205 #
2200 # path, flags are not hashed in filelog (but in manifestlog) so they do
2206 # path, flags are not hashed in filelog (but in manifestlog) so they do
2201 # not affect reusable here.
2207 # not affect reusable here.
2202 #
2208 #
2203 # If ctx or copied is overridden to a same value with originalfctx,
2209 # If ctx or copied is overridden to a same value with originalfctx,
2204 # still consider it's reusable. originalfctx.renamed() may be a bit
2210 # still consider it's reusable. originalfctx.renamed() may be a bit
2205 # expensive so it's not called unless necessary. Assuming datafunc is
2211 # expensive so it's not called unless necessary. Assuming datafunc is
2206 # always expensive, do not call it for this "reusable" test.
2212 # always expensive, do not call it for this "reusable" test.
2207 reusable = datafunc is None and ctxmatch() and copiedmatch()
2213 reusable = datafunc is None and ctxmatch() and copiedmatch()
2208
2214
2209 if datafunc is None:
2215 if datafunc is None:
2210 datafunc = originalfctx.data
2216 datafunc = originalfctx.data
2211 if flags is None:
2217 if flags is None:
2212 flags = originalfctx.flags()
2218 flags = originalfctx.flags()
2213
2219
2214 self._datafunc = datafunc
2220 self._datafunc = datafunc
2215 self._flags = flags
2221 self._flags = flags
2216 self._copied = copied
2222 self._copied = copied
2217
2223
2218 if reusable:
2224 if reusable:
2219 # copy extra fields from originalfctx
2225 # copy extra fields from originalfctx
2220 attrs = ['rawdata', 'rawflags', '_filenode', '_filerev']
2226 attrs = ['rawdata', 'rawflags', '_filenode', '_filerev']
2221 for attr in attrs:
2227 for attr in attrs:
2222 if util.safehasattr(originalfctx, attr):
2228 if util.safehasattr(originalfctx, attr):
2223 setattr(self, attr, getattr(originalfctx, attr))
2229 setattr(self, attr, getattr(originalfctx, attr))
2224
2230
2225 def data(self):
2231 def data(self):
2226 return self._datafunc()
2232 return self._datafunc()
2227
2233
2228 class metadataonlyctx(committablectx):
2234 class metadataonlyctx(committablectx):
2229 """Like memctx but it's reusing the manifest of different commit.
2235 """Like memctx but it's reusing the manifest of different commit.
2230 Intended to be used by lightweight operations that are creating
2236 Intended to be used by lightweight operations that are creating
2231 metadata-only changes.
2237 metadata-only changes.
2232
2238
2233 Revision information is supplied at initialization time. 'repo' is the
2239 Revision information is supplied at initialization time. 'repo' is the
2234 current localrepo, 'ctx' is original revision which manifest we're reuisng
2240 current localrepo, 'ctx' is original revision which manifest we're reuisng
2235 'parents' is a sequence of two parent revisions identifiers (pass None for
2241 'parents' is a sequence of two parent revisions identifiers (pass None for
2236 every missing parent), 'text' is the commit.
2242 every missing parent), 'text' is the commit.
2237
2243
2238 user receives the committer name and defaults to current repository
2244 user receives the committer name and defaults to current repository
2239 username, date is the commit date in any format supported by
2245 username, date is the commit date in any format supported by
2240 util.parsedate() and defaults to current date, extra is a dictionary of
2246 util.parsedate() and defaults to current date, extra is a dictionary of
2241 metadata or is left empty.
2247 metadata or is left empty.
2242 """
2248 """
2243 def __new__(cls, repo, originalctx, *args, **kwargs):
2249 def __new__(cls, repo, originalctx, *args, **kwargs):
2244 return super(metadataonlyctx, cls).__new__(cls, repo)
2250 return super(metadataonlyctx, cls).__new__(cls, repo)
2245
2251
2246 def __init__(self, repo, originalctx, parents, text, user=None, date=None,
2252 def __init__(self, repo, originalctx, parents, text, user=None, date=None,
2247 extra=None, editor=False):
2253 extra=None, editor=False):
2248 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
2254 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
2249 self._rev = None
2255 self._rev = None
2250 self._node = None
2256 self._node = None
2251 self._originalctx = originalctx
2257 self._originalctx = originalctx
2252 self._manifestnode = originalctx.manifestnode()
2258 self._manifestnode = originalctx.manifestnode()
2253 parents = [(p or nullid) for p in parents]
2259 parents = [(p or nullid) for p in parents]
2254 p1, p2 = self._parents = [changectx(self._repo, p) for p in parents]
2260 p1, p2 = self._parents = [changectx(self._repo, p) for p in parents]
2255
2261
2256 # sanity check to ensure that the reused manifest parents are
2262 # sanity check to ensure that the reused manifest parents are
2257 # manifests of our commit parents
2263 # manifests of our commit parents
2258 mp1, mp2 = self.manifestctx().parents
2264 mp1, mp2 = self.manifestctx().parents
2259 if p1 != nullid and p1.manifestnode() != mp1:
2265 if p1 != nullid and p1.manifestnode() != mp1:
2260 raise RuntimeError('can\'t reuse the manifest: '
2266 raise RuntimeError('can\'t reuse the manifest: '
2261 'its p1 doesn\'t match the new ctx p1')
2267 'its p1 doesn\'t match the new ctx p1')
2262 if p2 != nullid and p2.manifestnode() != mp2:
2268 if p2 != nullid and p2.manifestnode() != mp2:
2263 raise RuntimeError('can\'t reuse the manifest: '
2269 raise RuntimeError('can\'t reuse the manifest: '
2264 'its p2 doesn\'t match the new ctx p2')
2270 'its p2 doesn\'t match the new ctx p2')
2265
2271
2266 self._files = originalctx.files()
2272 self._files = originalctx.files()
2267 self.substate = {}
2273 self.substate = {}
2268
2274
2269 if editor:
2275 if editor:
2270 self._text = editor(self._repo, self, [])
2276 self._text = editor(self._repo, self, [])
2271 self._repo.savecommitmessage(self._text)
2277 self._repo.savecommitmessage(self._text)
2272
2278
2273 def manifestnode(self):
2279 def manifestnode(self):
2274 return self._manifestnode
2280 return self._manifestnode
2275
2281
2276 @property
2282 @property
2277 def _manifestctx(self):
2283 def _manifestctx(self):
2278 return self._repo.manifestlog[self._manifestnode]
2284 return self._repo.manifestlog[self._manifestnode]
2279
2285
2280 def filectx(self, path, filelog=None):
2286 def filectx(self, path, filelog=None):
2281 return self._originalctx.filectx(path, filelog=filelog)
2287 return self._originalctx.filectx(path, filelog=filelog)
2282
2288
2283 def commit(self):
2289 def commit(self):
2284 """commit context to the repo"""
2290 """commit context to the repo"""
2285 return self._repo.commitctx(self)
2291 return self._repo.commitctx(self)
2286
2292
2287 @property
2293 @property
2288 def _manifest(self):
2294 def _manifest(self):
2289 return self._originalctx.manifest()
2295 return self._originalctx.manifest()
2290
2296
2291 @propertycache
2297 @propertycache
2292 def _status(self):
2298 def _status(self):
2293 """Calculate exact status from ``files`` specified in the ``origctx``
2299 """Calculate exact status from ``files`` specified in the ``origctx``
2294 and parents manifests.
2300 and parents manifests.
2295 """
2301 """
2296 man1 = self.p1().manifest()
2302 man1 = self.p1().manifest()
2297 p2 = self._parents[1]
2303 p2 = self._parents[1]
2298 # "1 < len(self._parents)" can't be used for checking
2304 # "1 < len(self._parents)" can't be used for checking
2299 # existence of the 2nd parent, because "metadataonlyctx._parents" is
2305 # existence of the 2nd parent, because "metadataonlyctx._parents" is
2300 # explicitly initialized by the list, of which length is 2.
2306 # explicitly initialized by the list, of which length is 2.
2301 if p2.node() != nullid:
2307 if p2.node() != nullid:
2302 man2 = p2.manifest()
2308 man2 = p2.manifest()
2303 managing = lambda f: f in man1 or f in man2
2309 managing = lambda f: f in man1 or f in man2
2304 else:
2310 else:
2305 managing = lambda f: f in man1
2311 managing = lambda f: f in man1
2306
2312
2307 modified, added, removed = [], [], []
2313 modified, added, removed = [], [], []
2308 for f in self._files:
2314 for f in self._files:
2309 if not managing(f):
2315 if not managing(f):
2310 added.append(f)
2316 added.append(f)
2311 elif self[f]:
2317 elif self[f]:
2312 modified.append(f)
2318 modified.append(f)
2313 else:
2319 else:
2314 removed.append(f)
2320 removed.append(f)
2315
2321
2316 return scmutil.status(modified, added, removed, [], [], [], [])
2322 return scmutil.status(modified, added, removed, [], [], [], [])
@@ -1,480 +1,496
1 # sparse.py - functionality for sparse checkouts
1 # sparse.py - functionality for sparse checkouts
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 Facebook, Inc.
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 collections
10 import collections
11 import hashlib
11 import hashlib
12 import os
12 import os
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import nullid
15 from .node import nullid
16 from . import (
16 from . import (
17 error,
17 error,
18 match as matchmod,
18 match as matchmod,
19 merge as mergemod,
19 merge as mergemod,
20 pycompat,
20 pycompat,
21 )
21 )
22
22
23 # Whether sparse features are enabled. This variable is intended to be
23 # Whether sparse features are enabled. This variable is intended to be
24 # temporary to facilitate porting sparse to core. It should eventually be
24 # temporary to facilitate porting sparse to core. It should eventually be
25 # a per-repo option, possibly a repo requirement.
25 # a per-repo option, possibly a repo requirement.
26 enabled = False
26 enabled = False
27
27
28 def parseconfig(ui, raw):
28 def parseconfig(ui, raw):
29 """Parse sparse config file content.
29 """Parse sparse config file content.
30
30
31 Returns a tuple of includes, excludes, and profiles.
31 Returns a tuple of includes, excludes, and profiles.
32 """
32 """
33 includes = set()
33 includes = set()
34 excludes = set()
34 excludes = set()
35 current = includes
35 current = includes
36 profiles = []
36 profiles = []
37 for line in raw.split('\n'):
37 for line in raw.split('\n'):
38 line = line.strip()
38 line = line.strip()
39 if not line or line.startswith('#'):
39 if not line or line.startswith('#'):
40 # empty or comment line, skip
40 # empty or comment line, skip
41 continue
41 continue
42 elif line.startswith('%include '):
42 elif line.startswith('%include '):
43 line = line[9:].strip()
43 line = line[9:].strip()
44 if line:
44 if line:
45 profiles.append(line)
45 profiles.append(line)
46 elif line == '[include]':
46 elif line == '[include]':
47 if current != includes:
47 if current != includes:
48 # TODO pass filename into this API so we can report it.
48 # TODO pass filename into this API so we can report it.
49 raise error.Abort(_('sparse config cannot have includes ' +
49 raise error.Abort(_('sparse config cannot have includes ' +
50 'after excludes'))
50 'after excludes'))
51 continue
51 continue
52 elif line == '[exclude]':
52 elif line == '[exclude]':
53 current = excludes
53 current = excludes
54 elif line:
54 elif line:
55 if line.strip().startswith('/'):
55 if line.strip().startswith('/'):
56 ui.warn(_('warning: sparse profile cannot use' +
56 ui.warn(_('warning: sparse profile cannot use' +
57 ' paths starting with /, ignoring %s\n') % line)
57 ' paths starting with /, ignoring %s\n') % line)
58 continue
58 continue
59 current.add(line)
59 current.add(line)
60
60
61 return includes, excludes, profiles
61 return includes, excludes, profiles
62
62
63 # Exists as separate function to facilitate monkeypatching.
63 # Exists as separate function to facilitate monkeypatching.
64 def readprofile(repo, profile, changeid):
64 def readprofile(repo, profile, changeid):
65 """Resolve the raw content of a sparse profile file."""
65 """Resolve the raw content of a sparse profile file."""
66 # TODO add some kind of cache here because this incurs a manifest
66 # TODO add some kind of cache here because this incurs a manifest
67 # resolve and can be slow.
67 # resolve and can be slow.
68 return repo.filectx(profile, changeid=changeid).data()
68 return repo.filectx(profile, changeid=changeid).data()
69
69
70 def patternsforrev(repo, rev):
70 def patternsforrev(repo, rev):
71 """Obtain sparse checkout patterns for the given rev.
71 """Obtain sparse checkout patterns for the given rev.
72
72
73 Returns a tuple of iterables representing includes, excludes, and
73 Returns a tuple of iterables representing includes, excludes, and
74 patterns.
74 patterns.
75 """
75 """
76 # Feature isn't enabled. No-op.
76 # Feature isn't enabled. No-op.
77 if not enabled:
77 if not enabled:
78 return set(), set(), []
78 return set(), set(), []
79
79
80 raw = repo.vfs.tryread('sparse')
80 raw = repo.vfs.tryread('sparse')
81 if not raw:
81 if not raw:
82 return set(), set(), []
82 return set(), set(), []
83
83
84 if rev is None:
84 if rev is None:
85 raise error.Abort(_('cannot parse sparse patterns from working '
85 raise error.Abort(_('cannot parse sparse patterns from working '
86 'directory'))
86 'directory'))
87
87
88 includes, excludes, profiles = parseconfig(repo.ui, raw)
88 includes, excludes, profiles = parseconfig(repo.ui, raw)
89 ctx = repo[rev]
89 ctx = repo[rev]
90
90
91 if profiles:
91 if profiles:
92 visited = set()
92 visited = set()
93 while profiles:
93 while profiles:
94 profile = profiles.pop()
94 profile = profiles.pop()
95 if profile in visited:
95 if profile in visited:
96 continue
96 continue
97
97
98 visited.add(profile)
98 visited.add(profile)
99
99
100 try:
100 try:
101 raw = readprofile(repo, profile, rev)
101 raw = readprofile(repo, profile, rev)
102 except error.ManifestLookupError:
102 except error.ManifestLookupError:
103 msg = (
103 msg = (
104 "warning: sparse profile '%s' not found "
104 "warning: sparse profile '%s' not found "
105 "in rev %s - ignoring it\n" % (profile, ctx))
105 "in rev %s - ignoring it\n" % (profile, ctx))
106 # experimental config: sparse.missingwarning
106 # experimental config: sparse.missingwarning
107 if repo.ui.configbool(
107 if repo.ui.configbool(
108 'sparse', 'missingwarning', True):
108 'sparse', 'missingwarning', True):
109 repo.ui.warn(msg)
109 repo.ui.warn(msg)
110 else:
110 else:
111 repo.ui.debug(msg)
111 repo.ui.debug(msg)
112 continue
112 continue
113
113
114 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
114 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
115 includes.update(pincludes)
115 includes.update(pincludes)
116 excludes.update(pexcludes)
116 excludes.update(pexcludes)
117 for subprofile in subprofs:
117 for subprofile in subprofs:
118 profiles.append(subprofile)
118 profiles.append(subprofile)
119
119
120 profiles = visited
120 profiles = visited
121
121
122 if includes:
122 if includes:
123 includes.add('.hg*')
123 includes.add('.hg*')
124
124
125 return includes, excludes, profiles
125 return includes, excludes, profiles
126
126
127 def activeprofiles(repo):
127 def activeprofiles(repo):
128 revs = [repo.changelog.rev(node) for node in
128 revs = [repo.changelog.rev(node) for node in
129 repo.dirstate.parents() if node != nullid]
129 repo.dirstate.parents() if node != nullid]
130
130
131 profiles = set()
131 profiles = set()
132 for rev in revs:
132 for rev in revs:
133 profiles.update(patternsforrev(repo, rev)[2])
133 profiles.update(patternsforrev(repo, rev)[2])
134
134
135 return profiles
135 return profiles
136
136
137 def configsignature(repo, includetemp=True):
137 def configsignature(repo, includetemp=True):
138 """Obtain the signature string for the current sparse configuration.
138 """Obtain the signature string for the current sparse configuration.
139
139
140 This is used to construct a cache key for matchers.
140 This is used to construct a cache key for matchers.
141 """
141 """
142 cache = repo._sparsesignaturecache
142 cache = repo._sparsesignaturecache
143
143
144 signature = cache.get('signature')
144 signature = cache.get('signature')
145
145
146 if includetemp:
146 if includetemp:
147 tempsignature = cache.get('tempsignature')
147 tempsignature = cache.get('tempsignature')
148 else:
148 else:
149 tempsignature = '0'
149 tempsignature = '0'
150
150
151 if signature is None or (includetemp and tempsignature is None):
151 if signature is None or (includetemp and tempsignature is None):
152 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
152 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
153 cache['signature'] = signature
153 cache['signature'] = signature
154
154
155 if includetemp:
155 if includetemp:
156 raw = repo.vfs.tryread('tempsparse')
156 raw = repo.vfs.tryread('tempsparse')
157 tempsignature = hashlib.sha1(raw).hexdigest()
157 tempsignature = hashlib.sha1(raw).hexdigest()
158 cache['tempsignature'] = tempsignature
158 cache['tempsignature'] = tempsignature
159
159
160 return '%s %s' % (signature, tempsignature)
160 return '%s %s' % (signature, tempsignature)
161
161
162 def writeconfig(repo, includes, excludes, profiles):
162 def writeconfig(repo, includes, excludes, profiles):
163 """Write the sparse config file given a sparse configuration."""
163 """Write the sparse config file given a sparse configuration."""
164 with repo.vfs('sparse', 'wb') as fh:
164 with repo.vfs('sparse', 'wb') as fh:
165 for p in sorted(profiles):
165 for p in sorted(profiles):
166 fh.write('%%include %s\n' % p)
166 fh.write('%%include %s\n' % p)
167
167
168 if includes:
168 if includes:
169 fh.write('[include]\n')
169 fh.write('[include]\n')
170 for i in sorted(includes):
170 for i in sorted(includes):
171 fh.write(i)
171 fh.write(i)
172 fh.write('\n')
172 fh.write('\n')
173
173
174 if excludes:
174 if excludes:
175 fh.write('[exclude]\n')
175 fh.write('[exclude]\n')
176 for e in sorted(excludes):
176 for e in sorted(excludes):
177 fh.write(e)
177 fh.write(e)
178 fh.write('\n')
178 fh.write('\n')
179
179
180 repo._sparsesignaturecache.clear()
180 repo._sparsesignaturecache.clear()
181
181
182 def readtemporaryincludes(repo):
182 def readtemporaryincludes(repo):
183 raw = repo.vfs.tryread('tempsparse')
183 raw = repo.vfs.tryread('tempsparse')
184 if not raw:
184 if not raw:
185 return set()
185 return set()
186
186
187 return set(raw.split('\n'))
187 return set(raw.split('\n'))
188
188
189 def writetemporaryincludes(repo, includes):
189 def writetemporaryincludes(repo, includes):
190 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
190 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
191 repo._sparsesignaturecache.clear()
191 repo._sparsesignaturecache.clear()
192
192
193 def addtemporaryincludes(repo, additional):
193 def addtemporaryincludes(repo, additional):
194 includes = readtemporaryincludes(repo)
194 includes = readtemporaryincludes(repo)
195 for i in additional:
195 for i in additional:
196 includes.add(i)
196 includes.add(i)
197 writetemporaryincludes(repo, includes)
197 writetemporaryincludes(repo, includes)
198
198
199 def prunetemporaryincludes(repo):
199 def prunetemporaryincludes(repo):
200 if not enabled or not repo.vfs.exists('tempsparse'):
200 if not enabled or not repo.vfs.exists('tempsparse'):
201 return
201 return
202
202
203 origstatus = repo.status()
203 origstatus = repo.status()
204 modified, added, removed, deleted, a, b, c = origstatus
204 modified, added, removed, deleted, a, b, c = origstatus
205 if modified or added or removed or deleted:
205 if modified or added or removed or deleted:
206 # Still have pending changes. Don't bother trying to prune.
206 # Still have pending changes. Don't bother trying to prune.
207 return
207 return
208
208
209 sparsematch = matcher(repo, includetemp=False)
209 sparsematch = matcher(repo, includetemp=False)
210 dirstate = repo.dirstate
210 dirstate = repo.dirstate
211 actions = []
211 actions = []
212 dropped = []
212 dropped = []
213 tempincludes = readtemporaryincludes(repo)
213 tempincludes = readtemporaryincludes(repo)
214 for file in tempincludes:
214 for file in tempincludes:
215 if file in dirstate and not sparsematch(file):
215 if file in dirstate and not sparsematch(file):
216 message = _('dropping temporarily included sparse files')
216 message = _('dropping temporarily included sparse files')
217 actions.append((file, None, message))
217 actions.append((file, None, message))
218 dropped.append(file)
218 dropped.append(file)
219
219
220 typeactions = collections.defaultdict(list)
220 typeactions = collections.defaultdict(list)
221 typeactions['r'] = actions
221 typeactions['r'] = actions
222 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
222 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
223
223
224 # Fix dirstate
224 # Fix dirstate
225 for file in dropped:
225 for file in dropped:
226 dirstate.drop(file)
226 dirstate.drop(file)
227
227
228 repo.vfs.unlink('tempsparse')
228 repo.vfs.unlink('tempsparse')
229 repo._sparsesignaturecache.clear()
229 repo._sparsesignaturecache.clear()
230 msg = _('cleaned up %d temporarily added file(s) from the '
230 msg = _('cleaned up %d temporarily added file(s) from the '
231 'sparse checkout\n')
231 'sparse checkout\n')
232 repo.ui.status(msg % len(tempincludes))
232 repo.ui.status(msg % len(tempincludes))
233
233
234 def matcher(repo, revs=None, includetemp=True):
234 def matcher(repo, revs=None, includetemp=True):
235 """Obtain a matcher for sparse working directories for the given revs.
235 """Obtain a matcher for sparse working directories for the given revs.
236
236
237 If multiple revisions are specified, the matcher is the union of all
237 If multiple revisions are specified, the matcher is the union of all
238 revs.
238 revs.
239
239
240 ``includetemp`` indicates whether to use the temporary sparse profile.
240 ``includetemp`` indicates whether to use the temporary sparse profile.
241 """
241 """
242 # If sparse isn't enabled, sparse matcher matches everything.
242 # If sparse isn't enabled, sparse matcher matches everything.
243 if not enabled:
243 if not enabled:
244 return matchmod.always(repo.root, '')
244 return matchmod.always(repo.root, '')
245
245
246 if not revs or revs == [None]:
246 if not revs or revs == [None]:
247 revs = [repo.changelog.rev(node)
247 revs = [repo.changelog.rev(node)
248 for node in repo.dirstate.parents() if node != nullid]
248 for node in repo.dirstate.parents() if node != nullid]
249
249
250 signature = configsignature(repo, includetemp=includetemp)
250 signature = configsignature(repo, includetemp=includetemp)
251
251
252 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
252 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
253
253
254 result = repo._sparsematchercache.get(key)
254 result = repo._sparsematchercache.get(key)
255 if result:
255 if result:
256 return result
256 return result
257
257
258 matchers = []
258 matchers = []
259 for rev in revs:
259 for rev in revs:
260 try:
260 try:
261 includes, excludes, profiles = patternsforrev(repo, rev)
261 includes, excludes, profiles = patternsforrev(repo, rev)
262
262
263 if includes or excludes:
263 if includes or excludes:
264 # Explicitly include subdirectories of includes so
264 # Explicitly include subdirectories of includes so
265 # status will walk them down to the actual include.
265 # status will walk them down to the actual include.
266 subdirs = set()
266 subdirs = set()
267 for include in includes:
267 for include in includes:
268 # TODO consider using posix path functions here so Windows
268 # TODO consider using posix path functions here so Windows
269 # \ directory separators don't come into play.
269 # \ directory separators don't come into play.
270 dirname = os.path.dirname(include)
270 dirname = os.path.dirname(include)
271 # basename is used to avoid issues with absolute
271 # basename is used to avoid issues with absolute
272 # paths (which on Windows can include the drive).
272 # paths (which on Windows can include the drive).
273 while os.path.basename(dirname):
273 while os.path.basename(dirname):
274 subdirs.add(dirname)
274 subdirs.add(dirname)
275 dirname = os.path.dirname(dirname)
275 dirname = os.path.dirname(dirname)
276
276
277 matcher = matchmod.match(repo.root, '', [],
277 matcher = matchmod.match(repo.root, '', [],
278 include=includes, exclude=excludes,
278 include=includes, exclude=excludes,
279 default='relpath')
279 default='relpath')
280 if subdirs:
280 if subdirs:
281 matcher = matchmod.forceincludematcher(matcher, subdirs)
281 matcher = matchmod.forceincludematcher(matcher, subdirs)
282 matchers.append(matcher)
282 matchers.append(matcher)
283 except IOError:
283 except IOError:
284 pass
284 pass
285
285
286 if not matchers:
286 if not matchers:
287 result = matchmod.always(repo.root, '')
287 result = matchmod.always(repo.root, '')
288 elif len(matchers) == 1:
288 elif len(matchers) == 1:
289 result = matchers[0]
289 result = matchers[0]
290 else:
290 else:
291 result = matchmod.unionmatcher(matchers)
291 result = matchmod.unionmatcher(matchers)
292
292
293 if includetemp:
293 if includetemp:
294 tempincludes = readtemporaryincludes(repo)
294 tempincludes = readtemporaryincludes(repo)
295 result = matchmod.forceincludematcher(result, tempincludes)
295 result = matchmod.forceincludematcher(result, tempincludes)
296
296
297 repo._sparsematchercache[key] = result
297 repo._sparsematchercache[key] = result
298
298
299 return result
299 return result
300
300
301 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
301 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
302 """Filter updates to only lay out files that match the sparse rules."""
302 """Filter updates to only lay out files that match the sparse rules."""
303 if not enabled:
303 if not enabled:
304 return actions
304 return actions
305
305
306 oldrevs = [pctx.rev() for pctx in wctx.parents()]
306 oldrevs = [pctx.rev() for pctx in wctx.parents()]
307 oldsparsematch = matcher(repo, oldrevs)
307 oldsparsematch = matcher(repo, oldrevs)
308
308
309 if oldsparsematch.always():
309 if oldsparsematch.always():
310 return actions
310 return actions
311
311
312 files = set()
312 files = set()
313 prunedactions = {}
313 prunedactions = {}
314
314
315 if branchmerge:
315 if branchmerge:
316 # If we're merging, use the wctx filter, since we're merging into
316 # If we're merging, use the wctx filter, since we're merging into
317 # the wctx.
317 # the wctx.
318 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
318 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
319 else:
319 else:
320 # If we're updating, use the target context's filter, since we're
320 # If we're updating, use the target context's filter, since we're
321 # moving to the target context.
321 # moving to the target context.
322 sparsematch = matcher(repo, [mctx.rev()])
322 sparsematch = matcher(repo, [mctx.rev()])
323
323
324 temporaryfiles = []
324 temporaryfiles = []
325 for file, action in actions.iteritems():
325 for file, action in actions.iteritems():
326 type, args, msg = action
326 type, args, msg = action
327 files.add(file)
327 files.add(file)
328 if sparsematch(file):
328 if sparsematch(file):
329 prunedactions[file] = action
329 prunedactions[file] = action
330 elif type == 'm':
330 elif type == 'm':
331 temporaryfiles.append(file)
331 temporaryfiles.append(file)
332 prunedactions[file] = action
332 prunedactions[file] = action
333 elif branchmerge:
333 elif branchmerge:
334 if type != 'k':
334 if type != 'k':
335 temporaryfiles.append(file)
335 temporaryfiles.append(file)
336 prunedactions[file] = action
336 prunedactions[file] = action
337 elif type == 'f':
337 elif type == 'f':
338 prunedactions[file] = action
338 prunedactions[file] = action
339 elif file in wctx:
339 elif file in wctx:
340 prunedactions[file] = ('r', args, msg)
340 prunedactions[file] = ('r', args, msg)
341
341
342 if len(temporaryfiles) > 0:
342 if len(temporaryfiles) > 0:
343 repo.ui.status(_('temporarily included %d file(s) in the sparse '
343 repo.ui.status(_('temporarily included %d file(s) in the sparse '
344 'checkout for merging\n') % len(temporaryfiles))
344 'checkout for merging\n') % len(temporaryfiles))
345 addtemporaryincludes(repo, temporaryfiles)
345 addtemporaryincludes(repo, temporaryfiles)
346
346
347 # Add the new files to the working copy so they can be merged, etc
347 # Add the new files to the working copy so they can be merged, etc
348 actions = []
348 actions = []
349 message = 'temporarily adding to sparse checkout'
349 message = 'temporarily adding to sparse checkout'
350 wctxmanifest = repo[None].manifest()
350 wctxmanifest = repo[None].manifest()
351 for file in temporaryfiles:
351 for file in temporaryfiles:
352 if file in wctxmanifest:
352 if file in wctxmanifest:
353 fctx = repo[None][file]
353 fctx = repo[None][file]
354 actions.append((file, (fctx.flags(), False), message))
354 actions.append((file, (fctx.flags(), False), message))
355
355
356 typeactions = collections.defaultdict(list)
356 typeactions = collections.defaultdict(list)
357 typeactions['g'] = actions
357 typeactions['g'] = actions
358 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
358 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
359 False)
359 False)
360
360
361 dirstate = repo.dirstate
361 dirstate = repo.dirstate
362 for file, flags, msg in actions:
362 for file, flags, msg in actions:
363 dirstate.normal(file)
363 dirstate.normal(file)
364
364
365 profiles = activeprofiles(repo)
365 profiles = activeprofiles(repo)
366 changedprofiles = profiles & files
366 changedprofiles = profiles & files
367 # If an active profile changed during the update, refresh the checkout.
367 # If an active profile changed during the update, refresh the checkout.
368 # Don't do this during a branch merge, since all incoming changes should
368 # Don't do this during a branch merge, since all incoming changes should
369 # have been handled by the temporary includes above.
369 # have been handled by the temporary includes above.
370 if changedprofiles and not branchmerge:
370 if changedprofiles and not branchmerge:
371 mf = mctx.manifest()
371 mf = mctx.manifest()
372 for file in mf:
372 for file in mf:
373 old = oldsparsematch(file)
373 old = oldsparsematch(file)
374 new = sparsematch(file)
374 new = sparsematch(file)
375 if not old and new:
375 if not old and new:
376 flags = mf.flags(file)
376 flags = mf.flags(file)
377 prunedactions[file] = ('g', (flags, False), '')
377 prunedactions[file] = ('g', (flags, False), '')
378 elif old and not new:
378 elif old and not new:
379 prunedactions[file] = ('r', [], '')
379 prunedactions[file] = ('r', [], '')
380
380
381 return prunedactions
381 return prunedactions
382
382
383 def refreshwdir(repo, origstatus, origsparsematch, force=False):
383 def refreshwdir(repo, origstatus, origsparsematch, force=False):
384 """Refreshes working directory by taking sparse config into account.
384 """Refreshes working directory by taking sparse config into account.
385
385
386 The old status and sparse matcher is compared against the current sparse
386 The old status and sparse matcher is compared against the current sparse
387 matcher.
387 matcher.
388
388
389 Will abort if a file with pending changes is being excluded or included
389 Will abort if a file with pending changes is being excluded or included
390 unless ``force`` is True.
390 unless ``force`` is True.
391 """
391 """
392 modified, added, removed, deleted, unknown, ignored, clean = origstatus
392 modified, added, removed, deleted, unknown, ignored, clean = origstatus
393
393
394 # Verify there are no pending changes
394 # Verify there are no pending changes
395 pending = set()
395 pending = set()
396 pending.update(modified)
396 pending.update(modified)
397 pending.update(added)
397 pending.update(added)
398 pending.update(removed)
398 pending.update(removed)
399 sparsematch = matcher(repo)
399 sparsematch = matcher(repo)
400 abort = False
400 abort = False
401
401
402 for f in pending:
402 for f in pending:
403 if not sparsematch(f):
403 if not sparsematch(f):
404 repo.ui.warn(_("pending changes to '%s'\n") % f)
404 repo.ui.warn(_("pending changes to '%s'\n") % f)
405 abort = not force
405 abort = not force
406
406
407 if abort:
407 if abort:
408 raise error.Abort(_('could not update sparseness due to pending '
408 raise error.Abort(_('could not update sparseness due to pending '
409 'changes'))
409 'changes'))
410
410
411 # Calculate actions
411 # Calculate actions
412 dirstate = repo.dirstate
412 dirstate = repo.dirstate
413 ctx = repo['.']
413 ctx = repo['.']
414 added = []
414 added = []
415 lookup = []
415 lookup = []
416 dropped = []
416 dropped = []
417 mf = ctx.manifest()
417 mf = ctx.manifest()
418 files = set(mf)
418 files = set(mf)
419
419
420 actions = {}
420 actions = {}
421
421
422 for file in files:
422 for file in files:
423 old = origsparsematch(file)
423 old = origsparsematch(file)
424 new = sparsematch(file)
424 new = sparsematch(file)
425 # Add files that are newly included, or that don't exist in
425 # Add files that are newly included, or that don't exist in
426 # the dirstate yet.
426 # the dirstate yet.
427 if (new and not old) or (old and new and not file in dirstate):
427 if (new and not old) or (old and new and not file in dirstate):
428 fl = mf.flags(file)
428 fl = mf.flags(file)
429 if repo.wvfs.exists(file):
429 if repo.wvfs.exists(file):
430 actions[file] = ('e', (fl,), '')
430 actions[file] = ('e', (fl,), '')
431 lookup.append(file)
431 lookup.append(file)
432 else:
432 else:
433 actions[file] = ('g', (fl, False), '')
433 actions[file] = ('g', (fl, False), '')
434 added.append(file)
434 added.append(file)
435 # Drop files that are newly excluded, or that still exist in
435 # Drop files that are newly excluded, or that still exist in
436 # the dirstate.
436 # the dirstate.
437 elif (old and not new) or (not old and not new and file in dirstate):
437 elif (old and not new) or (not old and not new and file in dirstate):
438 dropped.append(file)
438 dropped.append(file)
439 if file not in pending:
439 if file not in pending:
440 actions[file] = ('r', [], '')
440 actions[file] = ('r', [], '')
441
441
442 # Verify there are no pending changes in newly included files
442 # Verify there are no pending changes in newly included files
443 abort = False
443 abort = False
444 for file in lookup:
444 for file in lookup:
445 repo.ui.warn(_("pending changes to '%s'\n") % file)
445 repo.ui.warn(_("pending changes to '%s'\n") % file)
446 abort = not force
446 abort = not force
447 if abort:
447 if abort:
448 raise error.Abort(_('cannot change sparseness due to pending '
448 raise error.Abort(_('cannot change sparseness due to pending '
449 'changes (delete the files or use '
449 'changes (delete the files or use '
450 '--force to bring them back dirty)'))
450 '--force to bring them back dirty)'))
451
451
452 # Check for files that were only in the dirstate.
452 # Check for files that were only in the dirstate.
453 for file, state in dirstate.iteritems():
453 for file, state in dirstate.iteritems():
454 if not file in files:
454 if not file in files:
455 old = origsparsematch(file)
455 old = origsparsematch(file)
456 new = sparsematch(file)
456 new = sparsematch(file)
457 if old and not new:
457 if old and not new:
458 dropped.append(file)
458 dropped.append(file)
459
459
460 # Apply changes to disk
460 # Apply changes to disk
461 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
461 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
462 for f, (m, args, msg) in actions.iteritems():
462 for f, (m, args, msg) in actions.iteritems():
463 if m not in typeactions:
463 if m not in typeactions:
464 typeactions[m] = []
464 typeactions[m] = []
465 typeactions[m].append((f, args, msg))
465 typeactions[m].append((f, args, msg))
466
466
467 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
467 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
468
468
469 # Fix dirstate
469 # Fix dirstate
470 for file in added:
470 for file in added:
471 dirstate.normal(file)
471 dirstate.normal(file)
472
472
473 for file in dropped:
473 for file in dropped:
474 dirstate.drop(file)
474 dirstate.drop(file)
475
475
476 for file in lookup:
476 for file in lookup:
477 # File exists on disk, and we're bringing it back in an unknown state.
477 # File exists on disk, and we're bringing it back in an unknown state.
478 dirstate.normallookup(file)
478 dirstate.normallookup(file)
479
479
480 return added, dropped, lookup
480 return added, dropped, lookup
481
482 def aftercommit(repo, node):
483 """Perform actions after a working directory commit."""
484 # This function is called unconditionally, even if sparse isn't
485 # enabled.
486 ctx = repo[node]
487
488 profiles = patternsforrev(repo, ctx.rev())[2]
489
490 # profiles will only have data if sparse is enabled.
491 if set(profiles) & set(ctx.files()):
492 origstatus = repo.status()
493 origsparsematch = matcher(repo)
494 refreshwdir(repo, origstatus, origsparsematch, force=True)
495
496 prunetemporaryincludes(repo)
General Comments 0
You need to be logged in to leave comments. Login now