##// END OF EJS Templates
fastannotate: initial import from Facebook's hg-experimental...
Augie Fackler -
r39243:1ddb296e default
parent child Browse files
Show More
@@ -0,0 +1,185 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # fastannotate: faster annotate implementation using linelog
4 #
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.
7 """yet another annotate implementation that might be faster (EXPERIMENTAL)
8
9 The fastannotate extension provides a 'fastannotate' command that makes
10 use of the linelog data structure as a cache layer and is expected to
11 be faster than the vanilla 'annotate' if the cache is present.
12
13 In most cases, fastannotate requires a setup that mainbranch is some pointer
14 that always moves forward, to be most efficient.
15
16 Using fastannotate together with linkrevcache would speed up building the
17 annotate cache greatly. Run "debugbuildlinkrevcache" before
18 "debugbuildannotatecache".
19
20 ::
21
22 [fastannotate]
23 # specify the main branch head. the internal linelog will only contain
24 # the linear (ignoring p2) "mainbranch". since linelog cannot move
25 # backwards without a rebuild, this should be something that always moves
26 # forward, usually it is "master" or "@".
27 mainbranch = master
28
29 # fastannotate supports different modes to expose its feature.
30 # a list of combination:
31 # - fastannotate: expose the feature via the "fastannotate" command which
32 # deals with everything in a most efficient way, and provides extra
33 # features like --deleted etc.
34 # - fctx: replace fctx.annotate implementation. note:
35 # a. it is less efficient than the "fastannotate" command
36 # b. it will make it practically impossible to access the old (disk
37 # side-effect free) annotate implementation
38 # c. it implies "hgweb".
39 # - hgweb: replace hgweb's annotate implementation. conflict with "fctx".
40 # (default: fastannotate)
41 modes = fastannotate
42
43 # default format when no format flags are used (default: number)
44 defaultformat = changeset, user, date
45
46 # serve the annotate cache via wire protocol (default: False)
47 # tip: the .hg/fastannotate directory is portable - can be rsynced
48 server = True
49
50 # build annotate cache on demand for every client request (default: True)
51 # disabling it could make server response faster, useful when there is a
52 # cronjob building the cache.
53 serverbuildondemand = True
54
55 # update local annotate cache from remote on demand
56 # (default: True for remotefilelog repo, False otherwise)
57 client = True
58
59 # path to use when connecting to the remote server (default: default)
60 remotepath = default
61
62 # share sshpeer with remotefilelog. this would allow fastannotate to peek
63 # into remotefilelog internals, and steal its sshpeer, or in the reversed
64 # direction: donate its sshpeer to remotefilelog. disable this if
65 # fastannotate and remotefilelog should not share a sshpeer when their
66 # endpoints are different and incompatible. (default: True)
67 clientsharepeer = True
68
69 # minimal length of the history of a file required to fetch linelog from
70 # the server. (default: 10)
71 clientfetchthreshold = 10
72
73 # use flock instead of the file existence lock
74 # flock may not work well on some network filesystems, but they avoid
75 # creating and deleting files frequently, which is faster when updating
76 # the annotate cache in batch. if you have issues with this option, set it
77 # to False. (default: True if flock is supported, False otherwise)
78 useflock = True
79
80 # for "fctx" mode, always follow renames regardless of command line option.
81 # this is a BC with the original command but will reduced the space needed
82 # for annotate cache, and is useful for client-server setup since the
83 # server will only provide annotate cache with default options (i.e. with
84 # follow). do not affect "fastannotate" mode. (default: True)
85 forcefollow = True
86
87 # for "fctx" mode, always treat file as text files, to skip the "isbinary"
88 # check. this is consistent with the "fastannotate" command and could help
89 # to avoid a file fetch if remotefilelog is used. (default: True)
90 forcetext = True
91
92 # use unfiltered repo for better performance.
93 unfilteredrepo = True
94
95 # sacrifice correctness in some corner cases for performance. it does not
96 # affect the correctness of the annotate cache being built. the option
97 # is experimental and may disappear in the future (default: False)
98 perfhack = True
99 """
100
101 from __future__ import absolute_import
102
103 from mercurial.i18n import _
104 from mercurial import (
105 error as hgerror,
106 localrepo,
107 registrar,
108 util,
109 )
110
111 from . import (
112 commands,
113 context,
114 protocol,
115 )
116
117 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
118 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
119 # be specifying the version(s) of Mercurial they are tested with, or
120 # leave the attribute unspecified.
121 testedwith = 'ships-with-hg-core'
122
123 cmdtable = commands.cmdtable
124
125 configtable = {}
126 configitem = registrar.configitem(configtable)
127
128 configitem('fastannotate', 'modes', default=['fastannotate'])
129 configitem('fastannotate', 'server', default=False)
130 configitem('fastannotate', 'useflock', default=True)
131 configitem('fastannotate', 'client')
132 configitem('fastannotate', 'unfilteredrepo', default=True)
133 configitem('fastannotate', 'defaultformat', default=['number'])
134 configitem('fastannotate', 'perfhack', default=False)
135 configitem('fastannotate', 'mainbranch')
136 configitem('fastannotate', 'forcetext', default=True)
137 configitem('fastannotate', 'forcefollow', default=True)
138 configitem('fastannotate', 'clientfetchthreshold', default=10)
139 configitem('fastannotate', 'clientsharepeer', default=True)
140 configitem('fastannotate', 'serverbuildondemand', default=True)
141 configitem('fastannotate', 'remotepath', default='default')
142
143 def _flockavailable():
144 try:
145 import fcntl
146 fcntl.flock
147 except StandardError:
148 return False
149 else:
150 return True
151
152 def uisetup(ui):
153 modes = set(ui.configlist('fastannotate', 'modes'))
154 if 'fctx' in modes:
155 modes.discard('hgweb')
156 for name in modes:
157 if name == 'fastannotate':
158 commands.registercommand()
159 elif name == 'hgweb':
160 from . import support
161 support.replacehgwebannotate()
162 elif name == 'fctx':
163 from . import support
164 support.replacefctxannotate()
165 support.replaceremotefctxannotate()
166 commands.wrapdefault()
167 else:
168 raise hgerror.Abort(_('fastannotate: invalid mode: %s') % name)
169
170 if ui.configbool('fastannotate', 'server'):
171 protocol.serveruisetup(ui)
172
173 if ui.configbool('fastannotate', 'useflock', _flockavailable()):
174 context.pathhelper.lock = context.pathhelper._lockflock
175
176 # fastannotate has its own locking, without depending on repo lock
177 localrepo.localrepository._wlockfreeprefix.add('fastannotate/')
178
179 def reposetup(ui, repo):
180 client = ui.configbool('fastannotate', 'client', default=None)
181 if client is None:
182 if util.safehasattr(repo, 'requirements'):
183 client = 'remotefilelog' in repo.requirements
184 if client:
185 protocol.clientreposetup(ui, repo)
@@ -0,0 +1,281 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # commands: fastannotate commands
4 #
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.
7
8 from __future__ import absolute_import
9
10 import os
11
12 from mercurial.i18n import _
13 from mercurial import (
14 commands,
15 error,
16 extensions,
17 patch,
18 pycompat,
19 registrar,
20 scmutil,
21 util,
22 )
23
24 from . import (
25 context as facontext,
26 error as faerror,
27 formatter as faformatter,
28 )
29
30 cmdtable = {}
31 command = registrar.command(cmdtable)
32
33 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
34 """generate paths matching given patterns"""
35 perfhack = repo.ui.configbool('fastannotate', 'perfhack')
36
37 # disable perfhack if:
38 # a) any walkopt is used
39 # b) if we treat pats as plain file names, some of them do not have
40 # corresponding linelog files
41 if perfhack:
42 # cwd related to reporoot
43 reporoot = os.path.dirname(repo.path)
44 reldir = os.path.relpath(pycompat.getcwd(), reporoot)
45 if reldir == '.':
46 reldir = ''
47 if any(opts.get(o[1]) for o in commands.walkopts): # a)
48 perfhack = False
49 else: # b)
50 relpats = [os.path.relpath(p, reporoot) if os.path.isabs(p) else p
51 for p in pats]
52 # disable perfhack on '..' since it allows escaping from the repo
53 if any(('..' in f or
54 not os.path.isfile(
55 facontext.pathhelper(repo, f, aopts).linelogpath))
56 for f in relpats):
57 perfhack = False
58
59 # perfhack: emit paths directory without checking with manifest
60 # this can be incorrect if the rev dos not have file.
61 if perfhack:
62 for p in relpats:
63 yield os.path.join(reldir, p)
64 else:
65 def bad(x, y):
66 raise error.Abort("%s: %s" % (x, y))
67 ctx = scmutil.revsingle(repo, rev)
68 m = scmutil.match(ctx, pats, opts, badfn=bad)
69 for p in ctx.walk(m):
70 yield p
71
72 fastannotatecommandargs = {
73 'options': [
74 ('r', 'rev', '.', _('annotate the specified revision'), _('REV')),
75 ('u', 'user', None, _('list the author (long with -v)')),
76 ('f', 'file', None, _('list the filename')),
77 ('d', 'date', None, _('list the date (short with -q)')),
78 ('n', 'number', None, _('list the revision number (default)')),
79 ('c', 'changeset', None, _('list the changeset')),
80 ('l', 'line-number', None, _('show line number at the first '
81 'appearance')),
82 ('e', 'deleted', None, _('show deleted lines (slow) (EXPERIMENTAL)')),
83 ('', 'no-content', None, _('do not show file content (EXPERIMENTAL)')),
84 ('', 'no-follow', None, _("don't follow copies and renames")),
85 ('', 'linear', None, _('enforce linear history, ignore second parent '
86 'of merges (EXPERIMENTAL)')),
87 ('', 'long-hash', None, _('show long changeset hash (EXPERIMENTAL)')),
88 ('', 'rebuild', None, _('rebuild cache even if it exists '
89 '(EXPERIMENTAL)')),
90 ] + commands.diffwsopts + commands.walkopts + commands.formatteropts,
91 'synopsis': _('[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
92 'inferrepo': True,
93 }
94
95 def fastannotate(ui, repo, *pats, **opts):
96 """show changeset information by line for each file
97
98 List changes in files, showing the revision id responsible for each line.
99
100 This command is useful for discovering when a change was made and by whom.
101
102 By default this command prints revision numbers. If you include --file,
103 --user, or --date, the revision number is suppressed unless you also
104 include --number. The default format can also be customized by setting
105 fastannotate.defaultformat.
106
107 Returns 0 on success.
108
109 .. container:: verbose
110
111 This command uses an implementation different from the vanilla annotate
112 command, which may produce slightly different (while still reasonable)
113 outputs for some cases.
114
115 Unlike the vanilla anootate, fastannotate follows rename regardless of
116 the existence of --file.
117
118 For the best performance when running on a full repo, use -c, -l,
119 avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
120
121 For the best performance when running on a shallow (remotefilelog)
122 repo, avoid --linear, --no-follow, or any diff options. As the server
123 won't be able to populate annotate cache when non-default options
124 affecting results are used.
125 """
126 if not pats:
127 raise error.Abort(_('at least one filename or pattern is required'))
128
129 # performance hack: filtered repo can be slow. unfilter by default.
130 if ui.configbool('fastannotate', 'unfilteredrepo'):
131 repo = repo.unfiltered()
132
133 rev = opts.get('rev', '.')
134 rebuild = opts.get('rebuild', False)
135
136 diffopts = patch.difffeatureopts(ui, opts, section='annotate',
137 whitespace=True)
138 aopts = facontext.annotateopts(
139 diffopts=diffopts,
140 followmerge=not opts.get('linear', False),
141 followrename=not opts.get('no_follow', False))
142
143 if not any(opts.get(s)
144 for s in ['user', 'date', 'file', 'number', 'changeset']):
145 # default 'number' for compatibility. but fastannotate is more
146 # efficient with "changeset", "line-number" and "no-content".
147 for name in ui.configlist('fastannotate', 'defaultformat', ['number']):
148 opts[name] = True
149
150 ui.pager('fastannotate')
151 template = opts.get('template')
152 if template == 'json':
153 formatter = faformatter.jsonformatter(ui, repo, opts)
154 else:
155 formatter = faformatter.defaultformatter(ui, repo, opts)
156 showdeleted = opts.get('deleted', False)
157 showlines = not bool(opts.get('no_content'))
158 showpath = opts.get('file', False)
159
160 # find the head of the main (master) branch
161 master = ui.config('fastannotate', 'mainbranch') or rev
162
163 # paths will be used for prefetching and the real annotating
164 paths = list(_matchpaths(repo, rev, pats, opts, aopts))
165
166 # for client, prefetch from the server
167 if util.safehasattr(repo, 'prefetchfastannotate'):
168 repo.prefetchfastannotate(paths)
169
170 for path in paths:
171 result = lines = existinglines = None
172 while True:
173 try:
174 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
175 result = a.annotate(rev, master=master, showpath=showpath,
176 showlines=(showlines and
177 not showdeleted))
178 if showdeleted:
179 existinglines = set((l[0], l[1]) for l in result)
180 result = a.annotatealllines(
181 rev, showpath=showpath, showlines=showlines)
182 break
183 except (faerror.CannotReuseError, faerror.CorruptedFileError):
184 # happens if master moves backwards, or the file was deleted
185 # and readded, or renamed to an existing name, or corrupted.
186 if rebuild: # give up since we have tried rebuild already
187 raise
188 else: # try a second time rebuilding the cache (slow)
189 rebuild = True
190 continue
191
192 if showlines:
193 result, lines = result
194
195 formatter.write(result, lines, existinglines=existinglines)
196 formatter.end()
197
198 _newopts = set([])
199 _knownopts = set([opt[1].replace('-', '_') for opt in
200 (fastannotatecommandargs['options'] + commands.globalopts)])
201
202 def _annotatewrapper(orig, ui, repo, *pats, **opts):
203 """used by wrapdefault"""
204 # we need this hack until the obsstore has 0.0 seconds perf impact
205 if ui.configbool('fastannotate', 'unfilteredrepo'):
206 repo = repo.unfiltered()
207
208 # treat the file as text (skip the isbinary check)
209 if ui.configbool('fastannotate', 'forcetext'):
210 opts['text'] = True
211
212 # check if we need to do prefetch (client-side)
213 rev = opts.get('rev')
214 if util.safehasattr(repo, 'prefetchfastannotate') and rev is not None:
215 paths = list(_matchpaths(repo, rev, pats, opts))
216 repo.prefetchfastannotate(paths)
217
218 return orig(ui, repo, *pats, **opts)
219
220 def registercommand():
221 """register the fastannotate command"""
222 name = '^fastannotate|fastblame|fa'
223 command(name, **fastannotatecommandargs)(fastannotate)
224
225 def wrapdefault():
226 """wrap the default annotate command, to be aware of the protocol"""
227 extensions.wrapcommand(commands.table, 'annotate', _annotatewrapper)
228
229 @command('debugbuildannotatecache',
230 [('r', 'rev', '', _('build up to the specific revision'), _('REV'))
231 ] + commands.walkopts,
232 _('[-r REV] FILE...'))
233 def debugbuildannotatecache(ui, repo, *pats, **opts):
234 """incrementally build fastannotate cache up to REV for specified files
235
236 If REV is not specified, use the config 'fastannotate.mainbranch'.
237
238 If fastannotate.client is True, download the annotate cache from the
239 server. Otherwise, build the annotate cache locally.
240
241 The annotate cache will be built using the default diff and follow
242 options and lives in '.hg/fastannotate/default'.
243 """
244 rev = opts.get('REV') or ui.config('fastannotate', 'mainbranch')
245 if not rev:
246 raise error.Abort(_('you need to provide a revision'),
247 hint=_('set fastannotate.mainbranch or use --rev'))
248 if ui.configbool('fastannotate', 'unfilteredrepo'):
249 repo = repo.unfiltered()
250 ctx = scmutil.revsingle(repo, rev)
251 m = scmutil.match(ctx, pats, opts)
252 paths = list(ctx.walk(m))
253 if util.safehasattr(repo, 'prefetchfastannotate'):
254 # client
255 if opts.get('REV'):
256 raise error.Abort(_('--rev cannot be used for client'))
257 repo.prefetchfastannotate(paths)
258 else:
259 # server, or full repo
260 for i, path in enumerate(paths):
261 ui.progress(_('building'), i, total=len(paths))
262 with facontext.annotatecontext(repo, path) as actx:
263 try:
264 if actx.isuptodate(rev):
265 continue
266 actx.annotate(rev, rev)
267 except (faerror.CannotReuseError, faerror.CorruptedFileError):
268 # the cache is broken (could happen with renaming so the
269 # file history gets invalidated). rebuild and try again.
270 ui.debug('fastannotate: %s: rebuilding broken cache\n'
271 % path)
272 actx.rebuild()
273 try:
274 actx.annotate(rev, rev)
275 except Exception as ex:
276 # possibly a bug, but should not stop us from building
277 # cache for other files.
278 ui.warn(_('fastannotate: %s: failed to '
279 'build cache: %r\n') % (path, ex))
280 # clear the progress bar
281 ui.write()
This diff has been collapsed as it changes many lines, (823 lines changed) Show them Hide them
@@ -0,0 +1,823 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # context: context needed to annotate a file
4 #
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.
7
8 from __future__ import absolute_import
9
10 import collections
11 import contextlib
12 import hashlib
13 import os
14
15 from mercurial.i18n import _
16 from mercurial import (
17 error,
18 linelog as linelogmod,
19 lock as lockmod,
20 mdiff,
21 node,
22 pycompat,
23 scmutil,
24 util,
25 )
26
27 from . import (
28 error as faerror,
29 revmap as revmapmod,
30 )
31
32 # given path, get filelog, cached
33 @util.lrucachefunc
34 def _getflog(repo, path):
35 return repo.file(path)
36
37 # extracted from mercurial.context.basefilectx.annotate
38 def _parents(f, follow=True):
39 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
40 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
41 # from the topmost introrev (= srcrev) down to p.linkrev() if it
42 # isn't an ancestor of the srcrev.
43 f._changeid
44 pl = f.parents()
45
46 # Don't return renamed parents if we aren't following.
47 if not follow:
48 pl = [p for p in pl if p.path() == f.path()]
49
50 # renamed filectx won't have a filelog yet, so set it
51 # from the cache to save time
52 for p in pl:
53 if not '_filelog' in p.__dict__:
54 p._filelog = _getflog(f._repo, p.path())
55
56 return pl
57
58 # extracted from mercurial.context.basefilectx.annotate. slightly modified
59 # so it takes a fctx instead of a pair of text and fctx.
60 def _decorate(fctx):
61 text = fctx.data()
62 linecount = text.count('\n')
63 if text and not text.endswith('\n'):
64 linecount += 1
65 return ([(fctx, i) for i in pycompat.xrange(linecount)], text)
66
67 # extracted from mercurial.context.basefilectx.annotate. slightly modified
68 # so it takes an extra "blocks" parameter calculated elsewhere, instead of
69 # calculating diff here.
70 def _pair(parent, child, blocks):
71 for (a1, a2, b1, b2), t in blocks:
72 # Changed blocks ('!') or blocks made only of blank lines ('~')
73 # belong to the child.
74 if t == '=':
75 child[0][b1:b2] = parent[0][a1:a2]
76 return child
77
78 # like scmutil.revsingle, but with lru cache, so their states (like manifests)
79 # could be reused
80 _revsingle = util.lrucachefunc(scmutil.revsingle)
81
82 def resolvefctx(repo, rev, path, resolverev=False, adjustctx=None):
83 """(repo, str, str) -> fctx
84
85 get the filectx object from repo, rev, path, in an efficient way.
86
87 if resolverev is True, "rev" is a revision specified by the revset
88 language, otherwise "rev" is a nodeid, or a revision number that can
89 be consumed by repo.__getitem__.
90
91 if adjustctx is not None, the returned fctx will point to a changeset
92 that introduces the change (last modified the file). if adjustctx
93 is 'linkrev', trust the linkrev and do not adjust it. this is noticeably
94 faster for big repos but is incorrect for some cases.
95 """
96 if resolverev and not isinstance(rev, int) and rev is not None:
97 ctx = _revsingle(repo, rev)
98 else:
99 ctx = repo[rev]
100
101 # If we don't need to adjust the linkrev, create the filectx using the
102 # changectx instead of using ctx[path]. This means it already has the
103 # changectx information, so blame -u will be able to look directly at the
104 # commitctx object instead of having to resolve it by going through the
105 # manifest. In a lazy-manifest world this can prevent us from downloading a
106 # lot of data.
107 if adjustctx is None:
108 # ctx.rev() is None means it's the working copy, which is a special
109 # case.
110 if ctx.rev() is None:
111 fctx = ctx[path]
112 else:
113 fctx = repo.filectx(path, changeid=ctx.rev())
114 else:
115 fctx = ctx[path]
116 if adjustctx == 'linkrev':
117 introrev = fctx.linkrev()
118 else:
119 introrev = fctx.introrev()
120 if introrev != ctx.rev():
121 fctx._changeid = introrev
122 fctx._changectx = repo[introrev]
123 return fctx
124
125 # like mercurial.store.encodedir, but use linelog suffixes: .m, .l, .lock
126 def encodedir(path):
127 return (path
128 .replace('.hg/', '.hg.hg/')
129 .replace('.l/', '.l.hg/')
130 .replace('.m/', '.m.hg/')
131 .replace('.lock/', '.lock.hg/'))
132
133 def hashdiffopts(diffopts):
134 diffoptstr = str(sorted(
135 (k, getattr(diffopts, k))
136 for k in mdiff.diffopts.defaults.iterkeys()
137 ))
138 return hashlib.sha1(diffoptstr).hexdigest()[:6]
139
140 _defaultdiffopthash = hashdiffopts(mdiff.defaultopts)
141
142 class annotateopts(object):
143 """like mercurial.mdiff.diffopts, but is for annotate
144
145 followrename: follow renames, like "hg annotate -f"
146 followmerge: follow p2 of a merge changeset, otherwise p2 is ignored
147 """
148
149 defaults = {
150 'diffopts': None,
151 'followrename': True,
152 'followmerge': True,
153 }
154
155 def __init__(self, **opts):
156 for k, v in self.defaults.iteritems():
157 setattr(self, k, opts.get(k, v))
158
159 @util.propertycache
160 def shortstr(self):
161 """represent opts in a short string, suitable for a directory name"""
162 result = ''
163 if not self.followrename:
164 result += 'r0'
165 if not self.followmerge:
166 result += 'm0'
167 if self.diffopts is not None:
168 assert isinstance(self.diffopts, mdiff.diffopts)
169 diffopthash = hashdiffopts(self.diffopts)
170 if diffopthash != _defaultdiffopthash:
171 result += 'i' + diffopthash
172 return result or 'default'
173
174 defaultopts = annotateopts()
175
176 class _annotatecontext(object):
177 """do not use this class directly as it does not use lock to protect
178 writes. use "with annotatecontext(...)" instead.
179 """
180
181 def __init__(self, repo, path, linelogpath, revmappath, opts):
182 self.repo = repo
183 self.ui = repo.ui
184 self.path = path
185 self.opts = opts
186 self.linelogpath = linelogpath
187 self.revmappath = revmappath
188 self._linelog = None
189 self._revmap = None
190 self._node2path = {} # {str: str}
191
192 @property
193 def linelog(self):
194 if self._linelog is None:
195 if os.path.exists(self.linelogpath):
196 with open(self.linelogpath, 'rb') as f:
197 try:
198 self._linelog = linelogmod.linelog.fromdata(f.read())
199 except linelogmod.LineLogError:
200 self._linelog = linelogmod.linelog()
201 else:
202 self._linelog = linelogmod.linelog()
203 return self._linelog
204
205 @property
206 def revmap(self):
207 if self._revmap is None:
208 self._revmap = revmapmod.revmap(self.revmappath)
209 return self._revmap
210
211 def close(self):
212 if self._revmap is not None:
213 self._revmap.flush()
214 self._revmap = None
215 if self._linelog is not None:
216 with open(self.linelogpath, 'wb') as f:
217 f.write(self._linelog.encode())
218 self._linelog = None
219
220 __del__ = close
221
222 def rebuild(self):
223 """delete linelog and revmap, useful for rebuilding"""
224 self.close()
225 self._node2path.clear()
226 _unlinkpaths([self.revmappath, self.linelogpath])
227
228 @property
229 def lastnode(self):
230 """return last node in revmap, or None if revmap is empty"""
231 if self._revmap is None:
232 # fast path, read revmap without loading its full content
233 return revmapmod.getlastnode(self.revmappath)
234 else:
235 return self._revmap.rev2hsh(self._revmap.maxrev)
236
237 def isuptodate(self, master, strict=True):
238 """return True if the revmap / linelog is up-to-date, or the file
239 does not exist in the master revision. False otherwise.
240
241 it tries to be fast and could return false negatives, because of the
242 use of linkrev instead of introrev.
243
244 useful for both server and client to decide whether to update
245 fastannotate cache or not.
246
247 if strict is True, even if fctx exists in the revmap, but is not the
248 last node, isuptodate will return False. it's good for performance - no
249 expensive check was done.
250
251 if strict is False, if fctx exists in the revmap, this function may
252 return True. this is useful for the client to skip downloading the
253 cache if the client's master is behind the server's.
254 """
255 lastnode = self.lastnode
256 try:
257 f = self._resolvefctx(master, resolverev=True)
258 # choose linkrev instead of introrev as the check is meant to be
259 # *fast*.
260 linknode = self.repo.changelog.node(f.linkrev())
261 if not strict and lastnode and linknode != lastnode:
262 # check if f.node() is in the revmap. note: this loads the
263 # revmap and can be slow.
264 return self.revmap.hsh2rev(linknode) is not None
265 # avoid resolving old manifest, or slow adjustlinkrev to be fast,
266 # false negatives are acceptable in this case.
267 return linknode == lastnode
268 except LookupError:
269 # master does not have the file, or the revmap is ahead
270 return True
271
272 def annotate(self, rev, master=None, showpath=False, showlines=False):
273 """incrementally update the cache so it includes revisions in the main
274 branch till 'master'. and run annotate on 'rev', which may or may not be
275 included in the main branch.
276
277 if master is None, do not update linelog.
278
279 the first value returned is the annotate result, it is [(node, linenum)]
280 by default. [(node, linenum, path)] if showpath is True.
281
282 if showlines is True, a second value will be returned, it is a list of
283 corresponding line contents.
284 """
285
286 # the fast path test requires commit hash, convert rev number to hash,
287 # so it may hit the fast path. note: in the "fctx" mode, the "annotate"
288 # command could give us a revision number even if the user passes a
289 # commit hash.
290 if isinstance(rev, int):
291 rev = node.hex(self.repo.changelog.node(rev))
292
293 # fast path: if rev is in the main branch already
294 directly, revfctx = self.canannotatedirectly(rev)
295 if directly:
296 if self.ui.debugflag:
297 self.ui.debug('fastannotate: %s: using fast path '
298 '(resolved fctx: %s)\n'
299 % (self.path, util.safehasattr(revfctx, 'node')))
300 return self.annotatedirectly(revfctx, showpath, showlines)
301
302 # resolve master
303 masterfctx = None
304 if master:
305 try:
306 masterfctx = self._resolvefctx(master, resolverev=True,
307 adjustctx=True)
308 except LookupError: # master does not have the file
309 pass
310 else:
311 if masterfctx in self.revmap: # no need to update linelog
312 masterfctx = None
313
314 # ... - @ <- rev (can be an arbitrary changeset,
315 # / not necessarily a descendant
316 # master -> o of master)
317 # |
318 # a merge -> o 'o': new changesets in the main branch
319 # |\ '#': revisions in the main branch that
320 # o * exist in linelog / revmap
321 # | . '*': changesets in side branches, or
322 # last master -> # . descendants of master
323 # | .
324 # # * joint: '#', and is a parent of a '*'
325 # |/
326 # a joint -> # ^^^^ --- side branches
327 # |
328 # ^ --- main branch (in linelog)
329
330 # these DFSes are similar to the traditional annotate algorithm.
331 # we cannot really reuse the code for perf reason.
332
333 # 1st DFS calculates merges, joint points, and needed.
334 # "needed" is a simple reference counting dict to free items in
335 # "hist", reducing its memory usage otherwise could be huge.
336 initvisit = [revfctx]
337 if masterfctx:
338 if masterfctx.rev() is None:
339 raise error.Abort(_('cannot update linelog to wdir()'),
340 hint=_('set fastannotate.mainbranch'))
341 initvisit.append(masterfctx)
342 visit = initvisit[:]
343 pcache = {}
344 needed = {revfctx: 1}
345 hist = {} # {fctx: ([(llrev or fctx, linenum)], text)}
346 while visit:
347 f = visit.pop()
348 if f in pcache or f in hist:
349 continue
350 if f in self.revmap: # in the old main branch, it's a joint
351 llrev = self.revmap.hsh2rev(f.node())
352 self.linelog.annotate(llrev)
353 result = self.linelog.annotateresult
354 hist[f] = (result, f.data())
355 continue
356 pl = self._parentfunc(f)
357 pcache[f] = pl
358 for p in pl:
359 needed[p] = needed.get(p, 0) + 1
360 if p not in pcache:
361 visit.append(p)
362
363 # 2nd (simple) DFS calculates new changesets in the main branch
364 # ('o' nodes in # the above graph), so we know when to update linelog.
365 newmainbranch = set()
366 f = masterfctx
367 while f and f not in self.revmap:
368 newmainbranch.add(f)
369 pl = pcache[f]
370 if pl:
371 f = pl[0]
372 else:
373 f = None
374 break
375
376 # f, if present, is the position where the last build stopped at, and
377 # should be the "master" last time. check to see if we can continue
378 # building the linelog incrementally. (we cannot if diverged)
379 if masterfctx is not None:
380 self._checklastmasterhead(f)
381
382 if self.ui.debugflag:
383 if newmainbranch:
384 self.ui.debug('fastannotate: %s: %d new changesets in the main'
385 ' branch\n' % (self.path, len(newmainbranch)))
386 elif not hist: # no joints, no updates
387 self.ui.debug('fastannotate: %s: linelog cannot help in '
388 'annotating this revision\n' % self.path)
389
390 # prepare annotateresult so we can update linelog incrementally
391 self.linelog.annotate(self.linelog.maxrev)
392
393 # 3rd DFS does the actual annotate
394 visit = initvisit[:]
395 progress = 0
396 while visit:
397 f = visit[-1]
398 if f in hist:
399 visit.pop()
400 continue
401
402 ready = True
403 pl = pcache[f]
404 for p in pl:
405 if p not in hist:
406 ready = False
407 visit.append(p)
408 if not ready:
409 continue
410
411 visit.pop()
412 blocks = None # mdiff blocks, used for appending linelog
413 ismainbranch = (f in newmainbranch)
414 # curr is the same as the traditional annotate algorithm,
415 # if we only care about linear history (do not follow merge),
416 # then curr is not actually used.
417 assert f not in hist
418 curr = _decorate(f)
419 for i, p in enumerate(pl):
420 bs = list(self._diffblocks(hist[p][1], curr[1]))
421 if i == 0 and ismainbranch:
422 blocks = bs
423 curr = _pair(hist[p], curr, bs)
424 if needed[p] == 1:
425 del hist[p]
426 del needed[p]
427 else:
428 needed[p] -= 1
429
430 hist[f] = curr
431 del pcache[f]
432
433 if ismainbranch: # need to write to linelog
434 if not self.ui.quiet:
435 progress += 1
436 self.ui.progress(_('building cache'), progress,
437 total=len(newmainbranch))
438 bannotated = None
439 if len(pl) == 2 and self.opts.followmerge: # merge
440 bannotated = curr[0]
441 if blocks is None: # no parents, add an empty one
442 blocks = list(self._diffblocks('', curr[1]))
443 self._appendrev(f, blocks, bannotated)
444 elif showpath: # not append linelog, but we need to record path
445 self._node2path[f.node()] = f.path()
446
447 if progress: # clean progress bar
448 self.ui.write()
449
450 result = [
451 ((self.revmap.rev2hsh(fr) if isinstance(fr, int) else fr.node()), l)
452 for fr, l in hist[revfctx][0]] # [(node, linenumber)]
453 return self._refineannotateresult(result, revfctx, showpath, showlines)
454
455 def canannotatedirectly(self, rev):
456 """(str) -> bool, fctx or node.
457 return (True, f) if we can annotate without updating the linelog, pass
458 f to annotatedirectly.
459 return (False, f) if we need extra calculation. f is the fctx resolved
460 from rev.
461 """
462 result = True
463 f = None
464 if not isinstance(rev, int) and rev is not None:
465 hsh = {20: bytes, 40: node.bin}.get(len(rev), lambda x: None)(rev)
466 if hsh is not None and (hsh, self.path) in self.revmap:
467 f = hsh
468 if f is None:
469 adjustctx = 'linkrev' if self._perfhack else True
470 f = self._resolvefctx(rev, adjustctx=adjustctx, resolverev=True)
471 result = f in self.revmap
472 if not result and self._perfhack:
473 # redo the resolution without perfhack - as we are going to
474 # do write operations, we need a correct fctx.
475 f = self._resolvefctx(rev, adjustctx=True, resolverev=True)
476 return result, f
477
478 def annotatealllines(self, rev, showpath=False, showlines=False):
479 """(rev : str) -> [(node : str, linenum : int, path : str)]
480
481 the result has the same format with annotate, but include all (including
482 deleted) lines up to rev. call this after calling annotate(rev, ...) for
483 better performance and accuracy.
484 """
485 revfctx = self._resolvefctx(rev, resolverev=True, adjustctx=True)
486
487 # find a chain from rev to anything in the mainbranch
488 if revfctx not in self.revmap:
489 chain = [revfctx]
490 a = ''
491 while True:
492 f = chain[-1]
493 pl = self._parentfunc(f)
494 if not pl:
495 break
496 if pl[0] in self.revmap:
497 a = pl[0].data()
498 break
499 chain.append(pl[0])
500
501 # both self.linelog and self.revmap is backed by filesystem. now
502 # we want to modify them but do not want to write changes back to
503 # files. so we create in-memory objects and copy them. it's like
504 # a "fork".
505 linelog = linelogmod.linelog()
506 linelog.copyfrom(self.linelog)
507 linelog.annotate(linelog.maxrev)
508 revmap = revmapmod.revmap()
509 revmap.copyfrom(self.revmap)
510
511 for f in reversed(chain):
512 b = f.data()
513 blocks = list(self._diffblocks(a, b))
514 self._doappendrev(linelog, revmap, f, blocks)
515 a = b
516 else:
517 # fastpath: use existing linelog, revmap as we don't write to them
518 linelog = self.linelog
519 revmap = self.revmap
520
521 lines = linelog.getalllines()
522 hsh = revfctx.node()
523 llrev = revmap.hsh2rev(hsh)
524 result = [(revmap.rev2hsh(r), l) for r, l in lines if r <= llrev]
525 # cannot use _refineannotateresult since we need custom logic for
526 # resolving line contents
527 if showpath:
528 result = self._addpathtoresult(result, revmap)
529 if showlines:
530 linecontents = self._resolvelines(result, revmap, linelog)
531 result = (result, linecontents)
532 return result
533
534 def _resolvelines(self, annotateresult, revmap, linelog):
535 """(annotateresult) -> [line]. designed for annotatealllines.
536 this is probably the most inefficient code in the whole fastannotate
537 directory. but we have made a decision that the linelog does not
538 store line contents. so getting them requires random accesses to
539 the revlog data, since they can be many, it can be very slow.
540 """
541 # [llrev]
542 revs = [revmap.hsh2rev(l[0]) for l in annotateresult]
543 result = [None] * len(annotateresult)
544 # {(rev, linenum): [lineindex]}
545 key2idxs = collections.defaultdict(list)
546 for i in pycompat.xrange(len(result)):
547 key2idxs[(revs[i], annotateresult[i][1])].append(i)
548 while key2idxs:
549 # find an unresolved line and its linelog rev to annotate
550 hsh = None
551 try:
552 for (rev, _linenum), idxs in key2idxs.iteritems():
553 if revmap.rev2flag(rev) & revmapmod.sidebranchflag:
554 continue
555 hsh = annotateresult[idxs[0]][0]
556 break
557 except StopIteration: # no more unresolved lines
558 return result
559 if hsh is None:
560 # the remaining key2idxs are not in main branch, resolving them
561 # using the hard way...
562 revlines = {}
563 for (rev, linenum), idxs in key2idxs.iteritems():
564 if rev not in revlines:
565 hsh = annotateresult[idxs[0]][0]
566 if self.ui.debugflag:
567 self.ui.debug('fastannotate: reading %s line #%d '
568 'to resolve lines %r\n'
569 % (node.short(hsh), linenum, idxs))
570 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
571 lines = mdiff.splitnewlines(fctx.data())
572 revlines[rev] = lines
573 for idx in idxs:
574 result[idx] = revlines[rev][linenum]
575 assert all(x is not None for x in result)
576 return result
577
578 # run the annotate and the lines should match to the file content
579 self.ui.debug('fastannotate: annotate %s to resolve lines\n'
580 % node.short(hsh))
581 linelog.annotate(rev)
582 fctx = self._resolvefctx(hsh, revmap.rev2path(rev))
583 annotated = linelog.annotateresult
584 lines = mdiff.splitnewlines(fctx.data())
585 if len(lines) != len(annotated):
586 raise faerror.CorruptedFileError('unexpected annotated lines')
587 # resolve lines from the annotate result
588 for i, line in enumerate(lines):
589 k = annotated[i]
590 if k in key2idxs:
591 for idx in key2idxs[k]:
592 result[idx] = line
593 del key2idxs[k]
594 return result
595
596 def annotatedirectly(self, f, showpath, showlines):
597 """like annotate, but when we know that f is in linelog.
598 f can be either a 20-char str (node) or a fctx. this is for perf - in
599 the best case, the user provides a node and we don't need to read the
600 filelog or construct any filecontext.
601 """
602 if isinstance(f, str):
603 hsh = f
604 else:
605 hsh = f.node()
606 llrev = self.revmap.hsh2rev(hsh)
607 if not llrev:
608 raise faerror.CorruptedFileError('%s is not in revmap'
609 % node.hex(hsh))
610 if (self.revmap.rev2flag(llrev) & revmapmod.sidebranchflag) != 0:
611 raise faerror.CorruptedFileError('%s is not in revmap mainbranch'
612 % node.hex(hsh))
613 self.linelog.annotate(llrev)
614 result = [(self.revmap.rev2hsh(r), l)
615 for r, l in self.linelog.annotateresult]
616 return self._refineannotateresult(result, f, showpath, showlines)
617
618 def _refineannotateresult(self, result, f, showpath, showlines):
619 """add the missing path or line contents, they can be expensive.
620 f could be either node or fctx.
621 """
622 if showpath:
623 result = self._addpathtoresult(result)
624 if showlines:
625 if isinstance(f, str): # f: node or fctx
626 llrev = self.revmap.hsh2rev(f)
627 fctx = self._resolvefctx(f, self.revmap.rev2path(llrev))
628 else:
629 fctx = f
630 lines = mdiff.splitnewlines(fctx.data())
631 if len(lines) != len(result): # linelog is probably corrupted
632 raise faerror.CorruptedFileError()
633 result = (result, lines)
634 return result
635
636 def _appendrev(self, fctx, blocks, bannotated=None):
637 self._doappendrev(self.linelog, self.revmap, fctx, blocks, bannotated)
638
639 def _diffblocks(self, a, b):
640 return mdiff.allblocks(a, b, self.opts.diffopts)
641
642 @staticmethod
643 def _doappendrev(linelog, revmap, fctx, blocks, bannotated=None):
644 """append a revision to linelog and revmap"""
645
646 def getllrev(f):
647 """(fctx) -> int"""
648 # f should not be a linelog revision
649 if isinstance(f, int):
650 raise error.ProgrammingError('f should not be an int')
651 # f is a fctx, allocate linelog rev on demand
652 hsh = f.node()
653 rev = revmap.hsh2rev(hsh)
654 if rev is None:
655 rev = revmap.append(hsh, sidebranch=True, path=f.path())
656 return rev
657
658 # append sidebranch revisions to revmap
659 siderevs = []
660 siderevmap = {} # node: int
661 if bannotated is not None:
662 for (a1, a2, b1, b2), op in blocks:
663 if op != '=':
664 # f could be either linelong rev, or fctx.
665 siderevs += [f for f, l in bannotated[b1:b2]
666 if not isinstance(f, int)]
667 siderevs = set(siderevs)
668 if fctx in siderevs: # mainnode must be appended seperately
669 siderevs.remove(fctx)
670 for f in siderevs:
671 siderevmap[f] = getllrev(f)
672
673 # the changeset in the main branch, could be a merge
674 llrev = revmap.append(fctx.node(), path=fctx.path())
675 siderevmap[fctx] = llrev
676
677 for (a1, a2, b1, b2), op in reversed(blocks):
678 if op == '=':
679 continue
680 if bannotated is None:
681 linelog.replacelines(llrev, a1, a2, b1, b2)
682 else:
683 blines = [((r if isinstance(r, int) else siderevmap[r]), l)
684 for r, l in bannotated[b1:b2]]
685 linelog.replacelines_vec(llrev, a1, a2, blines)
686
687 def _addpathtoresult(self, annotateresult, revmap=None):
688 """(revmap, [(node, linenum)]) -> [(node, linenum, path)]"""
689 if revmap is None:
690 revmap = self.revmap
691
692 def _getpath(nodeid):
693 path = self._node2path.get(nodeid)
694 if path is None:
695 path = revmap.rev2path(revmap.hsh2rev(nodeid))
696 self._node2path[nodeid] = path
697 return path
698
699 return [(n, l, _getpath(n)) for n, l in annotateresult]
700
701 def _checklastmasterhead(self, fctx):
702 """check if fctx is the master's head last time, raise if not"""
703 if fctx is None:
704 llrev = 0
705 else:
706 llrev = self.revmap.hsh2rev(fctx.node())
707 if not llrev:
708 raise faerror.CannotReuseError()
709 if self.linelog.maxrev != llrev:
710 raise faerror.CannotReuseError()
711
712 @util.propertycache
713 def _parentfunc(self):
714 """-> (fctx) -> [fctx]"""
715 followrename = self.opts.followrename
716 followmerge = self.opts.followmerge
717 def parents(f):
718 pl = _parents(f, follow=followrename)
719 if not followmerge:
720 pl = pl[:1]
721 return pl
722 return parents
723
724 @util.propertycache
725 def _perfhack(self):
726 return self.ui.configbool('fastannotate', 'perfhack')
727
728 def _resolvefctx(self, rev, path=None, **kwds):
729 return resolvefctx(self.repo, rev, (path or self.path), **kwds)
730
731 def _unlinkpaths(paths):
732 """silent, best-effort unlink"""
733 for path in paths:
734 try:
735 util.unlink(path)
736 except OSError:
737 pass
738
739 class pathhelper(object):
740 """helper for getting paths for lockfile, linelog and revmap"""
741
742 def __init__(self, repo, path, opts=defaultopts):
743 # different options use different directories
744 self._vfspath = os.path.join('fastannotate',
745 opts.shortstr, encodedir(path))
746 self._repo = repo
747
748 @property
749 def dirname(self):
750 return os.path.dirname(self._repo.vfs.join(self._vfspath))
751
752 @property
753 def linelogpath(self):
754 return self._repo.vfs.join(self._vfspath + '.l')
755
756 def lock(self):
757 return lockmod.lock(self._repo.vfs, self._vfspath + '.lock')
758
759 @contextlib.contextmanager
760 def _lockflock(self):
761 """the same as 'lock' but use flock instead of lockmod.lock, to avoid
762 creating temporary symlinks."""
763 import fcntl
764 lockpath = self.linelogpath
765 util.makedirs(os.path.dirname(lockpath))
766 lockfd = os.open(lockpath, os.O_RDONLY | os.O_CREAT, 0o664)
767 fcntl.flock(lockfd, fcntl.LOCK_EX)
768 try:
769 yield
770 finally:
771 fcntl.flock(lockfd, fcntl.LOCK_UN)
772 os.close(lockfd)
773
774 @property
775 def revmappath(self):
776 return self._repo.vfs.join(self._vfspath + '.m')
777
778 @contextlib.contextmanager
779 def annotatecontext(repo, path, opts=defaultopts, rebuild=False):
780 """context needed to perform (fast) annotate on a file
781
782 an annotatecontext of a single file consists of two structures: the
783 linelog and the revmap. this function takes care of locking. only 1
784 process is allowed to write that file's linelog and revmap at a time.
785
786 when something goes wrong, this function will assume the linelog and the
787 revmap are in a bad state, and remove them from disk.
788
789 use this function in the following way:
790
791 with annotatecontext(...) as actx:
792 actx. ....
793 """
794 helper = pathhelper(repo, path, opts)
795 util.makedirs(helper.dirname)
796 revmappath = helper.revmappath
797 linelogpath = helper.linelogpath
798 actx = None
799 try:
800 with helper.lock():
801 actx = _annotatecontext(repo, path, linelogpath, revmappath, opts)
802 if rebuild:
803 actx.rebuild()
804 yield actx
805 except Exception:
806 if actx is not None:
807 actx.rebuild()
808 repo.ui.debug('fastannotate: %s: cache broken and deleted\n' % path)
809 raise
810 finally:
811 if actx is not None:
812 actx.close()
813
814 def fctxannotatecontext(fctx, follow=True, diffopts=None, rebuild=False):
815 """like annotatecontext but get the context from a fctx. convenient when
816 used in fctx.annotate
817 """
818 repo = fctx._repo
819 path = fctx._path
820 if repo.ui.configbool('fastannotate', 'forcefollow', True):
821 follow = True
822 aopts = annotateopts(diffopts=diffopts, followrename=follow)
823 return annotatecontext(repo, path, aopts, rebuild)
@@ -0,0 +1,13 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # error: errors used in fastannotate
4 #
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.
7 from __future__ import absolute_import
8
9 class CorruptedFileError(Exception):
10 pass
11
12 class CannotReuseError(Exception):
13 """cannot reuse or update the cache incrementally"""
@@ -0,0 +1,161 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # format: defines the format used to output annotate result
4 #
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.
7 from __future__ import absolute_import
8
9 from mercurial import (
10 encoding,
11 node,
12 pycompat,
13 templatefilters,
14 util,
15 )
16 from mercurial.utils import (
17 dateutil,
18 )
19
20 # imitating mercurial.commands.annotate, not using the vanilla formatter since
21 # the data structures are a bit different, and we have some fast paths.
22 class defaultformatter(object):
23 """the default formatter that does leftpad and support some common flags"""
24
25 def __init__(self, ui, repo, opts):
26 self.ui = ui
27 self.opts = opts
28
29 if ui.quiet:
30 datefunc = dateutil.shortdate
31 else:
32 datefunc = dateutil.datestr
33 datefunc = util.cachefunc(datefunc)
34 getctx = util.cachefunc(lambda x: repo[x[0]])
35 hexfunc = self._hexfunc
36
37 # special handling working copy "changeset" and "rev" functions
38 if self.opts.get('rev') == 'wdir()':
39 orig = hexfunc
40 hexfunc = lambda x: None if x is None else orig(x)
41 wnode = hexfunc(repo[None].p1().node()) + '+'
42 wrev = str(repo[None].p1().rev())
43 wrevpad = ''
44 if not opts.get('changeset'): # only show + if changeset is hidden
45 wrev += '+'
46 wrevpad = ' '
47 revenc = lambda x: wrev if x is None else str(x) + wrevpad
48 csetenc = lambda x: wnode if x is None else str(x) + ' '
49 else:
50 revenc = csetenc = str
51
52 # opt name, separator, raw value (for json/plain), encoder (for plain)
53 opmap = [('user', ' ', lambda x: getctx(x).user(), ui.shortuser),
54 ('number', ' ', lambda x: getctx(x).rev(), revenc),
55 ('changeset', ' ', lambda x: hexfunc(x[0]), csetenc),
56 ('date', ' ', lambda x: getctx(x).date(), datefunc),
57 ('file', ' ', lambda x: x[2], str),
58 ('line_number', ':', lambda x: x[1] + 1, str)]
59 fieldnamemap = {'number': 'rev', 'changeset': 'node'}
60 funcmap = [(get, sep, fieldnamemap.get(op, op), enc)
61 for op, sep, get, enc in opmap
62 if opts.get(op)]
63 # no separator for first column
64 funcmap[0] = list(funcmap[0])
65 funcmap[0][1] = ''
66 self.funcmap = funcmap
67
68 def write(self, annotatedresult, lines=None, existinglines=None):
69 """(annotateresult, [str], set([rev, linenum])) -> None. write output.
70 annotateresult can be [(node, linenum, path)], or [(node, linenum)]
71 """
72 pieces = [] # [[str]]
73 maxwidths = [] # [int]
74
75 # calculate padding
76 for f, sep, name, enc in self.funcmap:
77 l = [enc(f(x)) for x in annotatedresult]
78 pieces.append(l)
79 if name in ['node', 'date']: # node and date has fixed size
80 l = l[:1]
81 widths = map(encoding.colwidth, set(l))
82 maxwidth = (max(widths) if widths else 0)
83 maxwidths.append(maxwidth)
84
85 # buffered output
86 result = ''
87 for i in pycompat.xrange(len(annotatedresult)):
88 for j, p in enumerate(pieces):
89 sep = self.funcmap[j][1]
90 padding = ' ' * (maxwidths[j] - len(p[i]))
91 result += sep + padding + p[i]
92 if lines:
93 if existinglines is None:
94 result += ': ' + lines[i]
95 else: # extra formatting showing whether a line exists
96 key = (annotatedresult[i][0], annotatedresult[i][1])
97 if key in existinglines:
98 result += ': ' + lines[i]
99 else:
100 result += ': ' + self.ui.label('-' + lines[i],
101 'diff.deleted')
102
103 if result[-1] != '\n':
104 result += '\n'
105
106 self.ui.write(result)
107
108 @util.propertycache
109 def _hexfunc(self):
110 if self.ui.debugflag or self.opts.get('long_hash'):
111 return node.hex
112 else:
113 return node.short
114
115 def end(self):
116 pass
117
118 class jsonformatter(defaultformatter):
119 def __init__(self, ui, repo, opts):
120 super(jsonformatter, self).__init__(ui, repo, opts)
121 self.ui.write('[')
122 self.needcomma = False
123
124 def write(self, annotatedresult, lines=None, existinglines=None):
125 if annotatedresult:
126 self._writecomma()
127
128 pieces = [(name, map(f, annotatedresult))
129 for f, sep, name, enc in self.funcmap]
130 if lines is not None:
131 pieces.append(('line', lines))
132 pieces.sort()
133
134 seps = [','] * len(pieces[:-1]) + ['']
135
136 result = ''
137 lasti = len(annotatedresult) - 1
138 for i in pycompat.xrange(len(annotatedresult)):
139 result += '\n {\n'
140 for j, p in enumerate(pieces):
141 k, vs = p
142 result += (' "%s": %s%s\n'
143 % (k, templatefilters.json(vs[i], paranoid=False),
144 seps[j]))
145 result += ' }%s' % ('' if i == lasti else ',')
146 if lasti >= 0:
147 self.needcomma = True
148
149 self.ui.write(result)
150
151 def _writecomma(self):
152 if self.needcomma:
153 self.ui.write(',')
154 self.needcomma = False
155
156 @util.propertycache
157 def _hexfunc(self):
158 return node.hex
159
160 def end(self):
161 self.ui.write('\n]\n')
@@ -0,0 +1,250 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # protocol: logic for a server providing fastannotate support
4 #
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.
7 from __future__ import absolute_import
8
9 import contextlib
10 import os
11
12 from mercurial.i18n import _
13 from mercurial import (
14 error,
15 extensions,
16 hg,
17 localrepo,
18 scmutil,
19 wireprotov1peer,
20 wireprotov1server,
21 )
22 from . import context
23
24 # common
25
26 def _getmaster(ui):
27 """get the mainbranch, and enforce it is set"""
28 master = ui.config('fastannotate', 'mainbranch')
29 if not master:
30 raise error.Abort(_('fastannotate.mainbranch is required '
31 'for both the client and the server'))
32 return master
33
34 # server-side
35
36 def _capabilities(orig, repo, proto):
37 result = orig(repo, proto)
38 result.append('getannotate')
39 return result
40
41 def _getannotate(repo, proto, path, lastnode):
42 # output:
43 # FILE := vfspath + '\0' + str(size) + '\0' + content
44 # OUTPUT := '' | FILE + OUTPUT
45 result = ''
46 buildondemand = repo.ui.configbool('fastannotate', 'serverbuildondemand',
47 True)
48 with context.annotatecontext(repo, path) as actx:
49 if buildondemand:
50 # update before responding to the client
51 master = _getmaster(repo.ui)
52 try:
53 if not actx.isuptodate(master):
54 actx.annotate(master, master)
55 except Exception:
56 # non-fast-forward move or corrupted. rebuild automically.
57 actx.rebuild()
58 try:
59 actx.annotate(master, master)
60 except Exception:
61 actx.rebuild() # delete files
62 finally:
63 # although the "with" context will also do a close/flush, we
64 # need to do it early so we can send the correct respond to
65 # client.
66 actx.close()
67 # send back the full content of revmap and linelog, in the future we
68 # may want to do some rsync-like fancy updating.
69 # the lastnode check is not necessary if the client and the server
70 # agree where the main branch is.
71 if actx.lastnode != lastnode:
72 for p in [actx.revmappath, actx.linelogpath]:
73 if not os.path.exists(p):
74 continue
75 content = ''
76 with open(p, 'rb') as f:
77 content = f.read()
78 vfsbaselen = len(repo.vfs.base + '/')
79 relpath = p[vfsbaselen:]
80 result += '%s\0%s\0%s' % (relpath, len(content), content)
81 return result
82
83 def _registerwireprotocommand():
84 if 'getannotate' in wireprotov1server.commands:
85 return
86 wireprotov1server.wireprotocommand(
87 'getannotate', 'path lastnode')(_getannotate)
88
89 def serveruisetup(ui):
90 _registerwireprotocommand()
91 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
92
93 # client-side
94
95 def _parseresponse(payload):
96 result = {}
97 i = 0
98 l = len(payload) - 1
99 state = 0 # 0: vfspath, 1: size
100 vfspath = size = ''
101 while i < l:
102 ch = payload[i]
103 if ch == '\0':
104 if state == 1:
105 result[vfspath] = buffer(payload, i + 1, int(size))
106 i += int(size)
107 state = 0
108 vfspath = size = ''
109 elif state == 0:
110 state = 1
111 else:
112 if state == 1:
113 size += ch
114 elif state == 0:
115 vfspath += ch
116 i += 1
117 return result
118
119 def peersetup(ui, peer):
120 class fastannotatepeer(peer.__class__):
121 @wireprotov1peer.batchable
122 def getannotate(self, path, lastnode=None):
123 if not self.capable('getannotate'):
124 ui.warn(_('remote peer cannot provide annotate cache\n'))
125 yield None, None
126 else:
127 args = {'path': path, 'lastnode': lastnode or ''}
128 f = wireprotov1peer.future()
129 yield args, f
130 yield _parseresponse(f.value)
131 peer.__class__ = fastannotatepeer
132
133 @contextlib.contextmanager
134 def annotatepeer(repo):
135 ui = repo.ui
136
137 # fileservice belongs to remotefilelog
138 fileservice = getattr(repo, 'fileservice', None)
139 sharepeer = ui.configbool('fastannotate', 'clientsharepeer', True)
140
141 if sharepeer and fileservice:
142 ui.debug('fastannotate: using remotefilelog connection pool\n')
143 conn = repo.connectionpool.get(repo.fallbackpath)
144 peer = conn.peer
145 stolen = True
146 else:
147 remotepath = ui.expandpath(
148 ui.config('fastannotate', 'remotepath', 'default'))
149 peer = hg.peer(ui, {}, remotepath)
150 stolen = False
151
152 try:
153 # Note: fastannotate requests should never trigger a remotefilelog
154 # "getfiles" request, because "getfiles" puts the stream into a state
155 # that does not exit. See "clientfetch": it does "getannotate" before
156 # any hg stuff that could potentially trigger a "getfiles".
157 yield peer
158 finally:
159 if not stolen:
160 for i in ['close', 'cleanup']:
161 getattr(peer, i, lambda: None)()
162 else:
163 conn.__exit__(None, None, None)
164
165 def clientfetch(repo, paths, lastnodemap=None, peer=None):
166 """download annotate cache from the server for paths"""
167 if not paths:
168 return
169
170 if peer is None:
171 with annotatepeer(repo) as peer:
172 return clientfetch(repo, paths, lastnodemap, peer)
173
174 if lastnodemap is None:
175 lastnodemap = {}
176
177 ui = repo.ui
178 results = []
179 with peer.commandexecutor() as batcher:
180 ui.debug('fastannotate: requesting %d files\n' % len(paths))
181 for p in paths:
182 results.append(batcher.callcommand(
183 'getannotate',
184 {'path': p, 'lastnode':lastnodemap.get(p)}))
185
186 ui.debug('fastannotate: server returned\n')
187 for result in results:
188 for path, content in result.result().iteritems():
189 # ignore malicious paths
190 if not path.startswith('fastannotate/') or '/../' in (path + '/'):
191 ui.debug('fastannotate: ignored malicious path %s\n' % path)
192 continue
193 if ui.debugflag:
194 ui.debug('fastannotate: writing %d bytes to %s\n'
195 % (len(content), path))
196 repo.vfs.makedirs(os.path.dirname(path))
197 with repo.vfs(path, 'wb') as f:
198 f.write(content)
199
200 def _filterfetchpaths(repo, paths):
201 """return a subset of paths whose history is long and need to fetch linelog
202 from the server. works with remotefilelog and non-remotefilelog repos.
203 """
204 threshold = repo.ui.configint('fastannotate', 'clientfetchthreshold', 10)
205 if threshold <= 0:
206 return paths
207
208 master = repo.ui.config('fastannotate', 'mainbranch') or 'default'
209
210 if 'remotefilelog' in repo.requirements:
211 ctx = scmutil.revsingle(repo, master)
212 f = lambda path: len(ctx[path].ancestormap())
213 else:
214 f = lambda path: len(repo.file(path))
215
216 result = []
217 for path in paths:
218 try:
219 if f(path) >= threshold:
220 result.append(path)
221 except Exception: # file not found etc.
222 result.append(path)
223
224 return result
225
226 def localreposetup(ui, repo):
227 class fastannotaterepo(repo.__class__):
228 def prefetchfastannotate(self, paths, peer=None):
229 master = _getmaster(self.ui)
230 needupdatepaths = []
231 lastnodemap = {}
232 try:
233 for path in _filterfetchpaths(self, paths):
234 with context.annotatecontext(self, path) as actx:
235 if not actx.isuptodate(master, strict=False):
236 needupdatepaths.append(path)
237 lastnodemap[path] = actx.lastnode
238 if needupdatepaths:
239 clientfetch(self, needupdatepaths, lastnodemap, peer)
240 except Exception as ex:
241 # could be directory not writable or so, not fatal
242 self.ui.debug('fastannotate: prefetch failed: %r\n' % ex)
243 repo.__class__ = fastannotaterepo
244
245 def clientreposetup(ui, repo):
246 _registerwireprotocommand()
247 if isinstance(repo, localrepo.localrepository):
248 localreposetup(ui, repo)
249 if peersetup not in hg.wirepeersetupfuncs:
250 hg.wirepeersetupfuncs.append(peersetup)
@@ -0,0 +1,254 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # revmap: trivial hg hash - linelog rev bidirectional map
4 #
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.
7
8 from __future__ import absolute_import
9
10 import bisect
11 import os
12 import struct
13
14 from mercurial.node import hex
15 from mercurial import (
16 error as hgerror,
17 pycompat,
18 )
19 from . import error
20
21 # the revmap file format is straightforward:
22 #
23 # 8 bytes: header
24 # 1 byte : flag for linelog revision 1
25 # ? bytes: (optional) '\0'-terminated path string
26 # only exists if (flag & renameflag) != 0
27 # 20 bytes: hg hash for linelog revision 1
28 # 1 byte : flag for linelog revision 2
29 # ? bytes: (optional) '\0'-terminated path string
30 # 20 bytes: hg hash for linelog revision 2
31 # ....
32 #
33 # the implementation is kinda stupid: __init__ loads the whole revmap.
34 # no laziness. benchmark shows loading 10000 revisions is about 0.015
35 # seconds, which looks enough for our use-case. if this implementation
36 # becomes a bottleneck, we can change it to lazily read the file
37 # from the end.
38
39 # whether the changeset is in the side branch. i.e. not in the linear main
40 # branch but only got referenced by lines in merge changesets.
41 sidebranchflag = 1
42
43 # whether the changeset changes the file path (ie. is a rename)
44 renameflag = 2
45
46 # len(mercurial.node.nullid)
47 _hshlen = 20
48
49 class revmap(object):
50 """trivial hg bin hash - linelog rev bidirectional map
51
52 also stores a flag (uint8) for each revision, and track renames.
53 """
54
55 HEADER = b'REVMAP1\0'
56
57 def __init__(self, path=None):
58 """create or load the revmap, optionally associate to a file
59
60 if path is None, the revmap is entirely in-memory. the caller is
61 responsible for locking. concurrent writes to a same file is unsafe.
62 the caller needs to make sure one file is associated to at most one
63 revmap object at a time."""
64 self.path = path
65 self._rev2hsh = [None]
66 self._rev2flag = [None]
67 self._hsh2rev = {}
68 # since rename does not happen frequently, do not store path for every
69 # revision. self._renamerevs can be used for bisecting.
70 self._renamerevs = [0]
71 self._renamepaths = ['']
72 self._lastmaxrev = -1
73 if path:
74 if os.path.exists(path):
75 self._load()
76 else:
77 # write the header so "append" can do incremental updates
78 self.flush()
79
80 def copyfrom(self, rhs):
81 """copy the map data from another revmap. do not affect self.path"""
82 self._rev2hsh = rhs._rev2hsh[:]
83 self._rev2flag = rhs._rev2flag[:]
84 self._hsh2rev = rhs._hsh2rev.copy()
85 self._renamerevs = rhs._renamerevs[:]
86 self._renamepaths = rhs._renamepaths[:]
87 self._lastmaxrev = -1
88
89 @property
90 def maxrev(self):
91 """return max linelog revision number"""
92 return len(self._rev2hsh) - 1
93
94 def append(self, hsh, sidebranch=False, path=None, flush=False):
95 """add a binary hg hash and return the mapped linelog revision.
96 if flush is True, incrementally update the file.
97 """
98 if hsh in self._hsh2rev:
99 raise error.CorruptedFileError('%r is in revmap already' % hex(hsh))
100 if len(hsh) != _hshlen:
101 raise hgerror.ProgrammingError('hsh must be %d-char long' % _hshlen)
102 idx = len(self._rev2hsh)
103 flag = 0
104 if sidebranch:
105 flag |= sidebranchflag
106 if path is not None and path != self._renamepaths[-1]:
107 flag |= renameflag
108 self._renamerevs.append(idx)
109 self._renamepaths.append(path)
110 self._rev2hsh.append(hsh)
111 self._rev2flag.append(flag)
112 self._hsh2rev[hsh] = idx
113 if flush:
114 self.flush()
115 return idx
116
117 def rev2hsh(self, rev):
118 """convert linelog revision to hg hash. return None if not found."""
119 if rev > self.maxrev or rev < 0:
120 return None
121 return self._rev2hsh[rev]
122
123 def rev2flag(self, rev):
124 """get the flag (uint8) for a given linelog revision.
125 return None if revision does not exist.
126 """
127 if rev > self.maxrev or rev < 0:
128 return None
129 return self._rev2flag[rev]
130
131 def rev2path(self, rev):
132 """get the path for a given linelog revision.
133 return None if revision does not exist.
134 """
135 if rev > self.maxrev or rev < 0:
136 return None
137 idx = bisect.bisect_right(self._renamerevs, rev) - 1
138 return self._renamepaths[idx]
139
140 def hsh2rev(self, hsh):
141 """convert hg hash to linelog revision. return None if not found."""
142 return self._hsh2rev.get(hsh)
143
144 def clear(self, flush=False):
145 """make the map empty. if flush is True, write to disk"""
146 # rev 0 is reserved, real rev starts from 1
147 self._rev2hsh = [None]
148 self._rev2flag = [None]
149 self._hsh2rev = {}
150 self._rev2path = ['']
151 self._lastmaxrev = -1
152 if flush:
153 self.flush()
154
155 def flush(self):
156 """write the state down to the file"""
157 if not self.path:
158 return
159 if self._lastmaxrev == -1: # write the entire file
160 with open(self.path, 'wb') as f:
161 f.write(self.HEADER)
162 for i in pycompat.xrange(1, len(self._rev2hsh)):
163 self._writerev(i, f)
164 else: # append incrementally
165 with open(self.path, 'ab') as f:
166 for i in pycompat.xrange(self._lastmaxrev + 1,
167 len(self._rev2hsh)):
168 self._writerev(i, f)
169 self._lastmaxrev = self.maxrev
170
171 def _load(self):
172 """load state from file"""
173 if not self.path:
174 return
175 # use local variables in a loop. CPython uses LOAD_FAST for them,
176 # which is faster than both LOAD_CONST and LOAD_GLOBAL.
177 flaglen = 1
178 hshlen = _hshlen
179 with open(self.path, 'rb') as f:
180 if f.read(len(self.HEADER)) != self.HEADER:
181 raise error.CorruptedFileError()
182 self.clear(flush=False)
183 while True:
184 buf = f.read(flaglen)
185 if not buf:
186 break
187 flag = ord(buf)
188 rev = len(self._rev2hsh)
189 if flag & renameflag:
190 path = self._readcstr(f)
191 self._renamerevs.append(rev)
192 self._renamepaths.append(path)
193 hsh = f.read(hshlen)
194 if len(hsh) != hshlen:
195 raise error.CorruptedFileError()
196 self._hsh2rev[hsh] = rev
197 self._rev2flag.append(flag)
198 self._rev2hsh.append(hsh)
199 self._lastmaxrev = self.maxrev
200
201 def _writerev(self, rev, f):
202 """append a revision data to file"""
203 flag = self._rev2flag[rev]
204 hsh = self._rev2hsh[rev]
205 f.write(struct.pack('B', flag))
206 if flag & renameflag:
207 path = self.rev2path(rev)
208 if path is None:
209 raise error.CorruptedFileError('cannot find path for %s' % rev)
210 f.write(path + '\0')
211 f.write(hsh)
212
213 @staticmethod
214 def _readcstr(f):
215 """read a C-language-like '\0'-terminated string"""
216 buf = ''
217 while True:
218 ch = f.read(1)
219 if not ch: # unexpected eof
220 raise error.CorruptedFileError()
221 if ch == '\0':
222 break
223 buf += ch
224 return buf
225
226 def __contains__(self, f):
227 """(fctx or (node, path)) -> bool.
228 test if (node, path) is in the map, and is not in a side branch.
229 f can be either a tuple of (node, path), or a fctx.
230 """
231 if isinstance(f, tuple): # f: (node, path)
232 hsh, path = f
233 else: # f: fctx
234 hsh, path = f.node(), f.path()
235 rev = self.hsh2rev(hsh)
236 if rev is None:
237 return False
238 if path is not None and path != self.rev2path(rev):
239 return False
240 return (self.rev2flag(rev) & sidebranchflag) == 0
241
242 def getlastnode(path):
243 """return the last hash in a revmap, without loading its full content.
244 this is equivalent to `m = revmap(path); m.rev2hsh(m.maxrev)`, but faster.
245 """
246 hsh = None
247 try:
248 with open(path, 'rb') as f:
249 f.seek(-_hshlen, 2)
250 if f.tell() > len(revmap.HEADER):
251 hsh = f.read(_hshlen)
252 except IOError:
253 pass
254 return hsh
@@ -0,0 +1,131 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # support: fastannotate support for hgweb, and filectx
4 #
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.
7
8 from __future__ import absolute_import
9
10 from mercurial import (
11 context as hgcontext,
12 dagop,
13 extensions,
14 hgweb,
15 patch,
16 util,
17 )
18
19 from . import (
20 context,
21 revmap,
22 )
23
24 class _lazyfctx(object):
25 """delegates to fctx but do not construct fctx when unnecessary"""
26
27 def __init__(self, repo, node, path):
28 self._node = node
29 self._path = path
30 self._repo = repo
31
32 def node(self):
33 return self._node
34
35 def path(self):
36 return self._path
37
38 @util.propertycache
39 def _fctx(self):
40 return context.resolvefctx(self._repo, self._node, self._path)
41
42 def __getattr__(self, name):
43 return getattr(self._fctx, name)
44
45 def _convertoutputs(repo, annotated, contents):
46 """convert fastannotate outputs to vanilla annotate format"""
47 # fastannotate returns: [(nodeid, linenum, path)], [linecontent]
48 # convert to what fctx.annotate returns: [annotateline]
49 results = []
50 fctxmap = {}
51 annotateline = dagop.annotateline
52 for i, (hsh, linenum, path) in enumerate(annotated):
53 if (hsh, path) not in fctxmap:
54 fctxmap[(hsh, path)] = _lazyfctx(repo, hsh, path)
55 # linenum: the user wants 1-based, we have 0-based.
56 lineno = linenum + 1
57 fctx = fctxmap[(hsh, path)]
58 line = contents[i]
59 results.append(annotateline(fctx=fctx, lineno=lineno, text=line))
60 return results
61
62 def _getmaster(fctx):
63 """(fctx) -> str"""
64 return fctx._repo.ui.config('fastannotate', 'mainbranch') or 'default'
65
66 def _doannotate(fctx, follow=True, diffopts=None):
67 """like the vanilla fctx.annotate, but do it via fastannotate, and make
68 the output format compatible with the vanilla fctx.annotate.
69 may raise Exception, and always return line numbers.
70 """
71 master = _getmaster(fctx)
72 annotated = contents = None
73
74 with context.fctxannotatecontext(fctx, follow, diffopts) as ac:
75 try:
76 annotated, contents = ac.annotate(fctx.rev(), master=master,
77 showpath=True, showlines=True)
78 except Exception:
79 ac.rebuild() # try rebuild once
80 fctx._repo.ui.debug('fastannotate: %s: rebuilding broken cache\n'
81 % fctx._path)
82 try:
83 annotated, contents = ac.annotate(fctx.rev(), master=master,
84 showpath=True, showlines=True)
85 except Exception:
86 raise
87
88 assert annotated and contents
89 return _convertoutputs(fctx._repo, annotated, contents)
90
91 def _hgwebannotate(orig, fctx, ui):
92 diffopts = patch.difffeatureopts(ui, untrusted=True,
93 section='annotate', whitespace=True)
94 return _doannotate(fctx, diffopts=diffopts)
95
96 def _fctxannotate(orig, self, follow=False, linenumber=False, skiprevs=None,
97 diffopts=None):
98 if skiprevs:
99 # skiprevs is not supported yet
100 return orig(self, follow, linenumber, skiprevs=skiprevs,
101 diffopts=diffopts)
102 try:
103 return _doannotate(self, follow, diffopts)
104 except Exception as ex:
105 self._repo.ui.debug('fastannotate: falling back to the vanilla '
106 'annotate: %r\n' % ex)
107 return orig(self, follow=follow, skiprevs=skiprevs,
108 diffopts=diffopts)
109
110 def _remotefctxannotate(orig, self, follow=False, skiprevs=None, diffopts=None):
111 # skipset: a set-like used to test if a fctx needs to be downloaded
112 skipset = None
113 with context.fctxannotatecontext(self, follow, diffopts) as ac:
114 skipset = revmap.revmap(ac.revmappath)
115 return orig(self, follow, skiprevs=skiprevs, diffopts=diffopts,
116 prefetchskip=skipset)
117
118 def replacehgwebannotate():
119 extensions.wrapfunction(hgweb.webutil, 'annotate', _hgwebannotate)
120
121 def replacefctxannotate():
122 extensions.wrapfunction(hgcontext.basefilectx, 'annotate', _fctxannotate)
123
124 def replaceremotefctxannotate():
125 try:
126 r = extensions.find('remotefilelog')
127 except KeyError:
128 return
129 else:
130 extensions.wrapfunction(r.remotefilectx.remotefilectx, 'annotate',
131 _remotefctxannotate)
@@ -0,0 +1,83 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > fastannotate=
4 > EOF
5
6 $ hg init repo
7 $ cd repo
8 $ for i in 0 1 2 3 4; do
9 > echo $i >> a
10 > echo $i >> b
11 > hg commit -A -m $i a b
12 > done
13
14 use the "debugbuildannotatecache" command to build annotate cache at rev 0
15
16 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=0
17 fastannotate: a: 1 new changesets in the main branch
18 fastannotate: b: 1 new changesets in the main branch
19
20 "debugbuildannotatecache" should work with broken cache (and other files would
21 be built without being affected). note: linelog being broken is only noticed
22 when we try to append to it.
23
24 $ echo 'CORRUPT!' >> .hg/fastannotate/default/a.m
25 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=1
26 fastannotate: a: rebuilding broken cache
27 fastannotate: a: 2 new changesets in the main branch
28 fastannotate: b: 1 new changesets in the main branch
29
30 $ echo 'CANNOT REUSE!' > .hg/fastannotate/default/a.l
31 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=2
32 fastannotate: a: rebuilding broken cache
33 fastannotate: a: 3 new changesets in the main branch
34 fastannotate: b: 1 new changesets in the main branch
35
36 $ rm .hg/fastannotate/default/a.m
37 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=3
38 fastannotate: a: rebuilding broken cache
39 fastannotate: a: 4 new changesets in the main branch
40 fastannotate: b: 1 new changesets in the main branch
41
42 $ rm .hg/fastannotate/default/a.l
43 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=3
44 $ hg debugbuildannotatecache --debug --config fastannotate.mainbranch=4
45 fastannotate: a: rebuilding broken cache
46 fastannotate: a: 5 new changesets in the main branch
47 fastannotate: b: 1 new changesets in the main branch
48
49 "fastannotate" should deal with file corruption as well
50
51 $ rm -rf .hg/fastannotate
52 $ hg fastannotate --debug -r 0 a
53 fastannotate: a: 1 new changesets in the main branch
54 0: 0
55
56 $ echo 'CORRUPT!' >> .hg/fastannotate/default/a.m
57 $ hg fastannotate --debug -r 0 a
58 fastannotate: a: cache broken and deleted
59 fastannotate: a: 1 new changesets in the main branch
60 0: 0
61
62 $ echo 'CORRUPT!' > .hg/fastannotate/default/a.l
63 $ hg fastannotate --debug -r 1 a
64 fastannotate: a: cache broken and deleted
65 fastannotate: a: 2 new changesets in the main branch
66 0: 0
67 1: 1
68
69 $ rm .hg/fastannotate/default/a.l
70 $ hg fastannotate --debug -r 1 a
71 fastannotate: a: using fast path (resolved fctx: True)
72 fastannotate: a: cache broken and deleted
73 fastannotate: a: 2 new changesets in the main branch
74 0: 0
75 1: 1
76
77 $ rm .hg/fastannotate/default/a.m
78 $ hg fastannotate --debug -r 2 a
79 fastannotate: a: cache broken and deleted
80 fastannotate: a: 3 new changesets in the main branch
81 0: 0
82 1: 1
83 2: 2
@@ -0,0 +1,33 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > fastannotate=
4 > EOF
5
6 $ hg init repo
7 $ cd repo
8
9 changes to whitespaces
10
11 $ cat >> a << EOF
12 > 1
13 >
14 >
15 > 2
16 > EOF
17 $ hg commit -qAm '1'
18 $ cat > a << EOF
19 > 1
20 >
21 > 2
22 >
23 >
24 > 3
25 > EOF
26 $ hg commit -m 2
27 $ hg fastannotate -wB a
28 0: 1
29 0:
30 1: 2
31 0:
32 1:
33 1: 3
This diff has been collapsed as it changes many lines, (777 lines changed) Show them Hide them
@@ -0,0 +1,777 b''
1 (this file is backported from core hg tests/test-annotate.t)
2
3 $ cat >> $HGRCPATH << EOF
4 > [diff]
5 > git=1
6 > [extensions]
7 > fastannotate=
8 > [fastannotate]
9 > modes=fctx
10 > forcefollow=False
11 > mainbranch=.
12 > EOF
13
14 $ HGMERGE=true; export HGMERGE
15
16 init
17
18 $ hg init repo
19 $ cd repo
20
21 commit
22
23 $ echo 'a' > a
24 $ hg ci -A -m test -u nobody -d '1 0'
25 adding a
26
27 annotate -c
28
29 $ hg annotate -c a
30 8435f90966e4: a
31
32 annotate -cl
33
34 $ hg annotate -cl a
35 8435f90966e4:1: a
36
37 annotate -d
38
39 $ hg annotate -d a
40 Thu Jan 01 00:00:01 1970 +0000: a
41
42 annotate -n
43
44 $ hg annotate -n a
45 0: a
46
47 annotate -nl
48
49 $ hg annotate -nl a
50 0:1: a
51
52 annotate -u
53
54 $ hg annotate -u a
55 nobody: a
56
57 annotate -cdnu
58
59 $ hg annotate -cdnu a
60 nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000: a
61
62 annotate -cdnul
63
64 $ hg annotate -cdnul a
65 nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000:1: a
66
67 annotate (JSON)
68
69 $ hg annotate -Tjson a
70 [
71 {
72 "abspath": "a",
73 "lines": [{"line": "a\n", "rev": 0}],
74 "path": "a"
75 }
76 ]
77
78 $ hg annotate -Tjson -cdfnul a
79 [
80 {
81 "abspath": "a",
82 "lines": [{"date": [1.0, 0], "file": "a", "line": "a\n", "line_number": 1, "node": "8435f90966e442695d2ded29fdade2bac5ad8065", "rev": 0, "user": "nobody"}],
83 "path": "a"
84 }
85 ]
86
87 $ cat <<EOF >>a
88 > a
89 > a
90 > EOF
91 $ hg ci -ma1 -d '1 0'
92 $ hg cp a b
93 $ hg ci -mb -d '1 0'
94 $ cat <<EOF >> b
95 > b4
96 > b5
97 > b6
98 > EOF
99 $ hg ci -mb2 -d '2 0'
100
101 annotate -n b
102
103 $ hg annotate -n b
104 0: a
105 1: a
106 1: a
107 3: b4
108 3: b5
109 3: b6
110
111 annotate --no-follow b
112
113 $ hg annotate --no-follow b
114 2: a
115 2: a
116 2: a
117 3: b4
118 3: b5
119 3: b6
120
121 annotate -nl b
122
123 $ hg annotate -nl b
124 0:1: a
125 1:2: a
126 1:3: a
127 3:4: b4
128 3:5: b5
129 3:6: b6
130
131 annotate -nf b
132
133 $ hg annotate -nf b
134 0 a: a
135 1 a: a
136 1 a: a
137 3 b: b4
138 3 b: b5
139 3 b: b6
140
141 annotate -nlf b
142
143 $ hg annotate -nlf b
144 0 a:1: a
145 1 a:2: a
146 1 a:3: a
147 3 b:4: b4
148 3 b:5: b5
149 3 b:6: b6
150
151 $ hg up -C 2
152 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
153 $ cat <<EOF >> b
154 > b4
155 > c
156 > b5
157 > EOF
158 $ hg ci -mb2.1 -d '2 0'
159 created new head
160 $ hg merge
161 merging b
162 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
163 (branch merge, don't forget to commit)
164 $ hg ci -mmergeb -d '3 0'
165
166 annotate after merge
167 (note: the first one falls back to the vanilla annotate which does not use linelog)
168
169 $ hg annotate -nf b --debug
170 fastannotate: b: rebuilding broken cache
171 fastannotate: b: 5 new changesets in the main branch
172 0 a: a
173 1 a: a
174 1 a: a
175 3 b: b4
176 4 b: c
177 3 b: b5
178
179 (difference explained below)
180
181 $ hg annotate -nf b --debug
182 fastannotate: b: using fast path (resolved fctx: False)
183 0 a: a
184 1 a: a
185 1 a: a
186 4 b: b4
187 4 b: c
188 4 b: b5
189
190 annotate after merge with -l
191 (fastannotate differs from annotate)
192
193 $ hg log -Gp -T '{rev}:{node}' -r '2..5'
194 @ 5:64afcdf8e29e063c635be123d8d2fb160af00f7e
195 |\
196 | o 4:5fbdc1152d97597717021ad9e063061b200f146bdiff --git a/b b/b
197 | | --- a/b
198 | | +++ b/b
199 | | @@ -1,3 +1,6 @@
200 | | a
201 | | a
202 | | a
203 | | +b4
204 | | +c
205 | | +b5
206 | |
207 o | 3:37ec9f5c3d1f99572d7075971cb4876e2139b52fdiff --git a/b b/b
208 |/ --- a/b
209 | +++ b/b
210 | @@ -1,3 +1,6 @@
211 | a
212 | a
213 | a
214 | +b4
215 | +b5
216 | +b6
217 |
218 o 2:3086dbafde1ce745abfc8d2d367847280aabae9ddiff --git a/a b/b
219 | copy from a
220 ~ copy to b
221
222
223 (in this case, "b4", "b5" could be considered introduced by either rev 3, or rev 4.
224 and that causes the rev number difference)
225
226 $ hg annotate -nlf b --config fastannotate.modes=
227 0 a:1: a
228 1 a:2: a
229 1 a:3: a
230 3 b:4: b4
231 4 b:5: c
232 3 b:5: b5
233
234 $ hg annotate -nlf b
235 0 a:1: a
236 1 a:2: a
237 1 a:3: a
238 4 b:4: b4
239 4 b:5: c
240 4 b:6: b5
241
242 $ hg up -C 1
243 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
244 $ hg cp a b
245 $ cat <<EOF > b
246 > a
247 > z
248 > a
249 > EOF
250 $ hg ci -mc -d '3 0'
251 created new head
252 $ hg merge
253 merging b
254 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
255 (branch merge, don't forget to commit)
256 $ cat <<EOF >> b
257 > b4
258 > c
259 > b5
260 > EOF
261 $ echo d >> b
262 $ hg ci -mmerge2 -d '4 0'
263
264 annotate after rename merge
265
266 $ hg annotate -nf b
267 0 a: a
268 6 b: z
269 1 a: a
270 3 b: b4
271 4 b: c
272 3 b: b5
273 7 b: d
274
275 annotate after rename merge with -l
276 (fastannotate differs from annotate)
277
278 $ hg log -Gp -T '{rev}:{node}' -r '0+1+6+7'
279 @ 7:6284bb6c38fef984a929862a53bbc71ce9eafa81diff --git a/b b/b
280 |\ --- a/b
281 | : +++ b/b
282 | : @@ -1,3 +1,7 @@
283 | : a
284 | : z
285 | : a
286 | : +b4
287 | : +c
288 | : +b5
289 | : +d
290 | :
291 o : 6:b80e3e32f75a6a67cd4ac85496a11511e9112816diff --git a/a b/b
292 :/ copy from a
293 : copy to b
294 : --- a/a
295 : +++ b/b
296 : @@ -1,3 +1,3 @@
297 : -a (?)
298 : a
299 : +z
300 : a
301 : -a (?)
302 :
303 o 1:762f04898e6684ff713415f7b8a8d53d33f96c92diff --git a/a b/a
304 | --- a/a
305 | +++ b/a
306 | @@ -1,1 +1,3 @@
307 | a
308 | +a
309 | +a
310 |
311 o 0:8435f90966e442695d2ded29fdade2bac5ad8065diff --git a/a b/a
312 new file mode 100644
313 --- /dev/null
314 +++ b/a
315 @@ -0,0 +1,1 @@
316 +a
317
318
319 (note on question marks:
320 the upstream bdiff change (96f2f50d923f+3633403888ae+8c0c75aa3ff4+5c4e2636c1a9
321 +38ed54888617) alters the output so deletion is not always at the end of the
322 output. for example:
323 | a | b | old | new | # old: e1d6aa0e4c3a, new: 8836f13e3c5b
324 |-------------------|
325 | a | a | a | -a |
326 | a | z | +z | a |
327 | a | a | a | +z |
328 | | | -a | a |
329 |-------------------|
330 | a | a | a |
331 | a | a | a |
332 | a | | -a |
333 this leads to more question marks below)
334
335 (rev 1 adds two "a"s and rev 6 deletes one "a".
336 the "a" that rev 6 deletes could be either the first or the second "a" of those two "a"s added by rev 1.
337 and that causes the line number difference)
338
339 $ hg annotate -nlf b --config fastannotate.modes=
340 0 a:1: a
341 6 b:2: z
342 1 a:3: a
343 3 b:4: b4
344 4 b:5: c
345 3 b:5: b5
346 7 b:7: d
347
348 $ hg annotate -nlf b
349 0 a:1: a (?)
350 1 a:2: a (?)
351 6 b:2: z
352 1 a:2: a (?)
353 1 a:3: a (?)
354 3 b:4: b4
355 4 b:5: c
356 3 b:5: b5
357 7 b:7: d
358
359 Issue2807: alignment of line numbers with -l
360 (fastannotate differs from annotate, same reason as above)
361
362 $ echo more >> b
363 $ hg ci -mmore -d '5 0'
364 $ echo more >> b
365 $ hg ci -mmore -d '6 0'
366 $ echo more >> b
367 $ hg ci -mmore -d '7 0'
368 $ hg annotate -nlf b
369 0 a: 1: a (?)
370 1 a: 2: a (?)
371 6 b: 2: z
372 1 a: 2: a (?)
373 1 a: 3: a (?)
374 3 b: 4: b4
375 4 b: 5: c
376 3 b: 5: b5
377 7 b: 7: d
378 8 b: 8: more
379 9 b: 9: more
380 10 b:10: more
381
382 linkrev vs rev
383
384 $ hg annotate -r tip -n a
385 0: a
386 1: a
387 1: a
388
389 linkrev vs rev with -l
390
391 $ hg annotate -r tip -nl a
392 0:1: a
393 1:2: a
394 1:3: a
395
396 Issue589: "undelete" sequence leads to crash
397
398 annotate was crashing when trying to --follow something
399
400 like A -> B -> A
401
402 generate ABA rename configuration
403
404 $ echo foo > foo
405 $ hg add foo
406 $ hg ci -m addfoo
407 $ hg rename foo bar
408 $ hg ci -m renamefoo
409 $ hg rename bar foo
410 $ hg ci -m renamebar
411
412 annotate after ABA with follow
413
414 $ hg annotate --follow foo
415 foo: foo
416
417 missing file
418
419 $ hg ann nosuchfile
420 abort: nosuchfile: no such file in rev e9e6b4fa872f
421 [255]
422
423 annotate file without '\n' on last line
424
425 $ printf "" > c
426 $ hg ci -A -m test -u nobody -d '1 0'
427 adding c
428 $ hg annotate c
429 $ printf "a\nb" > c
430 $ hg ci -m test
431 $ hg annotate c
432 [0-9]+: a (re)
433 [0-9]+: b (re)
434
435 Issue3841: check annotation of the file of which filelog includes
436 merging between the revision and its ancestor
437
438 to reproduce the situation with recent Mercurial, this script uses (1)
439 "hg debugsetparents" to merge without ancestor check by "hg merge",
440 and (2) the extension to allow filelog merging between the revision
441 and its ancestor by overriding "repo._filecommit".
442
443 $ cat > ../legacyrepo.py <<EOF
444 > from mercurial import node, error
445 > def reposetup(ui, repo):
446 > class legacyrepo(repo.__class__):
447 > def _filecommit(self, fctx, manifest1, manifest2,
448 > linkrev, tr, changelist):
449 > fname = fctx.path()
450 > text = fctx.data()
451 > flog = self.file(fname)
452 > fparent1 = manifest1.get(fname, node.nullid)
453 > fparent2 = manifest2.get(fname, node.nullid)
454 > meta = {}
455 > copy = fctx.renamed()
456 > if copy and copy[0] != fname:
457 > raise error.Abort('copying is not supported')
458 > if fparent2 != node.nullid:
459 > changelist.append(fname)
460 > return flog.add(text, meta, tr, linkrev,
461 > fparent1, fparent2)
462 > raise error.Abort('only merging is supported')
463 > repo.__class__ = legacyrepo
464 > EOF
465
466 $ cat > baz <<EOF
467 > 1
468 > 2
469 > 3
470 > 4
471 > 5
472 > EOF
473 $ hg add baz
474 $ hg commit -m "baz:0"
475
476 $ cat > baz <<EOF
477 > 1 baz:1
478 > 2
479 > 3
480 > 4
481 > 5
482 > EOF
483 $ hg commit -m "baz:1"
484
485 $ cat > baz <<EOF
486 > 1 baz:1
487 > 2 baz:2
488 > 3
489 > 4
490 > 5
491 > EOF
492 $ hg debugsetparents 17 17
493 $ hg --config extensions.legacyrepo=../legacyrepo.py commit -m "baz:2"
494 $ hg debugindexdot .hg/store/data/baz.i
495 digraph G {
496 -1 -> 0
497 0 -> 1
498 1 -> 2
499 1 -> 2
500 }
501 $ hg annotate baz
502 17: 1 baz:1
503 18: 2 baz:2
504 16: 3
505 16: 4
506 16: 5
507
508 $ cat > baz <<EOF
509 > 1 baz:1
510 > 2 baz:2
511 > 3 baz:3
512 > 4
513 > 5
514 > EOF
515 $ hg commit -m "baz:3"
516
517 $ cat > baz <<EOF
518 > 1 baz:1
519 > 2 baz:2
520 > 3 baz:3
521 > 4 baz:4
522 > 5
523 > EOF
524 $ hg debugsetparents 19 18
525 $ hg --config extensions.legacyrepo=../legacyrepo.py commit -m "baz:4"
526 $ hg debugindexdot .hg/store/data/baz.i
527 digraph G {
528 -1 -> 0
529 0 -> 1
530 1 -> 2
531 1 -> 2
532 2 -> 3
533 3 -> 4
534 2 -> 4
535 }
536 $ hg annotate baz
537 17: 1 baz:1
538 18: 2 baz:2
539 19: 3 baz:3
540 20: 4 baz:4
541 16: 5
542
543 annotate clean file
544
545 $ hg annotate -ncr "wdir()" foo
546 11 472b18db256d : foo
547
548 annotate modified file
549
550 $ echo foofoo >> foo
551 $ hg annotate -r "wdir()" foo
552 11 : foo
553 20+: foofoo
554
555 $ hg annotate -cr "wdir()" foo
556 472b18db256d : foo
557 b6bedd5477e7+: foofoo
558
559 $ hg annotate -ncr "wdir()" foo
560 11 472b18db256d : foo
561 20 b6bedd5477e7+: foofoo
562
563 $ hg annotate --debug -ncr "wdir()" foo
564 11 472b18db256d1e8282064eab4bfdaf48cbfe83cd : foo
565 20 b6bedd5477e797f25e568a6402d4697f3f895a72+: foofoo
566
567 $ hg annotate -udr "wdir()" foo
568 test Thu Jan 01 00:00:00 1970 +0000: foo
569 test [A-Za-z0-9:+ ]+: foofoo (re)
570
571 $ hg annotate -ncr "wdir()" -Tjson foo
572 [
573 {
574 "abspath": "foo",
575 "lines": [{"line": "foo\n", "node": "472b18db256d1e8282064eab4bfdaf48cbfe83cd", "rev": 11}, {"line": "foofoo\n", "node": null, "rev": null}],
576 "path": "foo"
577 }
578 ]
579
580 annotate added file
581
582 $ echo bar > bar
583 $ hg add bar
584 $ hg annotate -ncr "wdir()" bar
585 20 b6bedd5477e7+: bar
586
587 annotate renamed file
588
589 $ hg rename foo renamefoo2
590 $ hg annotate -ncr "wdir()" renamefoo2
591 11 472b18db256d : foo
592 20 b6bedd5477e7+: foofoo
593
594 annotate missing file
595
596 $ rm baz
597 #if windows
598 $ hg annotate -ncr "wdir()" baz
599 abort: $TESTTMP\repo\baz: The system cannot find the file specified
600 [255]
601 #else
602 $ hg annotate -ncr "wdir()" baz
603 abort: $ENOENT$: $TESTTMP/repo/baz
604 [255]
605 #endif
606
607 annotate removed file
608
609 $ hg rm baz
610 #if windows
611 $ hg annotate -ncr "wdir()" baz
612 abort: $TESTTMP\repo\baz: The system cannot find the file specified
613 [255]
614 #else
615 $ hg annotate -ncr "wdir()" baz
616 abort: $ENOENT$: $TESTTMP/repo/baz
617 [255]
618 #endif
619
620 Test annotate with whitespace options
621
622 $ cd ..
623 $ hg init repo-ws
624 $ cd repo-ws
625 $ cat > a <<EOF
626 > aa
627 >
628 > b b
629 > EOF
630 $ hg ci -Am "adda"
631 adding a
632 $ sed 's/EOL$//g' > a <<EOF
633 > a a
634 >
635 > EOL
636 > b b
637 > EOF
638 $ hg ci -m "changea"
639
640 Annotate with no option
641
642 $ hg annotate a
643 1: a a
644 0:
645 1:
646 1: b b
647
648 Annotate with --ignore-space-change
649
650 $ hg annotate --ignore-space-change a
651 1: a a
652 1:
653 0:
654 0: b b
655
656 Annotate with --ignore-all-space
657
658 $ hg annotate --ignore-all-space a
659 0: a a
660 0:
661 1:
662 0: b b
663
664 Annotate with --ignore-blank-lines (similar to no options case)
665
666 $ hg annotate --ignore-blank-lines a
667 1: a a
668 0:
669 1:
670 1: b b
671
672 $ cd ..
673
674 Annotate with linkrev pointing to another branch
675 ------------------------------------------------
676
677 create history with a filerev whose linkrev points to another branch
678
679 $ hg init branchedlinkrev
680 $ cd branchedlinkrev
681 $ echo A > a
682 $ hg commit -Am 'contentA'
683 adding a
684 $ echo B >> a
685 $ hg commit -m 'contentB'
686 $ hg up --rev 'desc(contentA)'
687 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
688 $ echo unrelated > unrelated
689 $ hg commit -Am 'unrelated'
690 adding unrelated
691 created new head
692 $ hg graft -r 'desc(contentB)'
693 grafting 1:fd27c222e3e6 "contentB"
694 $ echo C >> a
695 $ hg commit -m 'contentC'
696 $ echo W >> a
697 $ hg log -G
698 @ changeset: 4:072f1e8df249
699 | tag: tip
700 | user: test
701 | date: Thu Jan 01 00:00:00 1970 +0000
702 | summary: contentC
703 |
704 o changeset: 3:ff38df03cc4b
705 | user: test
706 | date: Thu Jan 01 00:00:00 1970 +0000
707 | summary: contentB
708 |
709 o changeset: 2:62aaf3f6fc06
710 | parent: 0:f0932f74827e
711 | user: test
712 | date: Thu Jan 01 00:00:00 1970 +0000
713 | summary: unrelated
714 |
715 | o changeset: 1:fd27c222e3e6
716 |/ user: test
717 | date: Thu Jan 01 00:00:00 1970 +0000
718 | summary: contentB
719 |
720 o changeset: 0:f0932f74827e
721 user: test
722 date: Thu Jan 01 00:00:00 1970 +0000
723 summary: contentA
724
725
726 Annotate should list ancestor of starting revision only
727
728 $ hg annotate a
729 0: A
730 3: B
731 4: C
732
733 $ hg annotate a -r 'wdir()'
734 0 : A
735 3 : B
736 4 : C
737 4+: W
738
739 Even when the starting revision is the linkrev-shadowed one:
740
741 $ hg annotate a -r 3
742 0: A
743 3: B
744
745 $ cd ..
746
747 Issue5360: Deleted chunk in p1 of a merge changeset
748
749 $ hg init repo-5360
750 $ cd repo-5360
751 $ echo 1 > a
752 $ hg commit -A a -m 1
753 $ echo 2 >> a
754 $ hg commit -m 2
755 $ echo a > a
756 $ hg commit -m a
757 $ hg update '.^' -q
758 $ echo 3 >> a
759 $ hg commit -m 3 -q
760 $ hg merge 2 -q
761 $ cat > a << EOF
762 > b
763 > 1
764 > 2
765 > 3
766 > a
767 > EOF
768 $ hg resolve --mark -q
769 $ hg commit -m m
770 $ hg annotate a
771 4: b
772 0: 1
773 1: 2
774 3: 3
775 2: a
776
777 $ cd ..
@@ -0,0 +1,182 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > fastannotate=
4 > [fastannotate]
5 > perfhack=1
6 > EOF
7
8 $ HGMERGE=true; export HGMERGE
9
10 $ hg init repo
11 $ cd repo
12
13 a simple merge case
14
15 $ echo 1 > a
16 $ hg commit -qAm 'append 1'
17 $ echo 2 >> a
18 $ hg commit -m 'append 2'
19 $ echo 3 >> a
20 $ hg commit -m 'append 3'
21 $ hg up 1 -q
22 $ cat > a << EOF
23 > 0
24 > 1
25 > 2
26 > EOF
27 $ hg commit -qm 'insert 0'
28 $ hg merge 2 -q
29 $ echo 4 >> a
30 $ hg commit -m merge
31 $ hg log -G -T '{rev}: {desc}'
32 @ 4: merge
33 |\
34 | o 3: insert 0
35 | |
36 o | 2: append 3
37 |/
38 o 1: append 2
39 |
40 o 0: append 1
41
42 $ hg fastannotate a
43 3: 0
44 0: 1
45 1: 2
46 2: 3
47 4: 4
48 $ hg fastannotate -r 0 a
49 0: 1
50 $ hg fastannotate -r 1 a
51 0: 1
52 1: 2
53 $ hg fastannotate -udnclf a
54 test 3 d641cb51f61e Thu Jan 01 00:00:00 1970 +0000 a:1: 0
55 test 0 4994017376d3 Thu Jan 01 00:00:00 1970 +0000 a:1: 1
56 test 1 e940cb6d9a06 Thu Jan 01 00:00:00 1970 +0000 a:2: 2
57 test 2 26162a884ba6 Thu Jan 01 00:00:00 1970 +0000 a:3: 3
58 test 4 3ad7bcd2815f Thu Jan 01 00:00:00 1970 +0000 a:5: 4
59 $ hg fastannotate --linear a
60 3: 0
61 0: 1
62 1: 2
63 4: 3
64 4: 4
65
66 incrementally updating
67
68 $ hg fastannotate -r 0 a --debug
69 fastannotate: a: using fast path (resolved fctx: True)
70 0: 1
71 $ hg fastannotate -r 0 a --debug --rebuild
72 fastannotate: a: 1 new changesets in the main branch
73 0: 1
74 $ hg fastannotate -r 1 a --debug
75 fastannotate: a: 1 new changesets in the main branch
76 0: 1
77 1: 2
78 $ hg fastannotate -r 3 a --debug
79 fastannotate: a: 1 new changesets in the main branch
80 3: 0
81 0: 1
82 1: 2
83 $ hg fastannotate -r 4 a --debug
84 fastannotate: a: 1 new changesets in the main branch
85 3: 0
86 0: 1
87 1: 2
88 2: 3
89 4: 4
90 $ hg fastannotate -r 1 a --debug
91 fastannotate: a: using fast path (resolved fctx: True)
92 0: 1
93 1: 2
94
95 rebuild happens automatically if unable to update
96
97 $ hg fastannotate -r 2 a --debug
98 fastannotate: a: cache broken and deleted
99 fastannotate: a: 3 new changesets in the main branch
100 0: 1
101 1: 2
102 2: 3
103
104 config option "fastannotate.mainbranch"
105
106 $ hg fastannotate -r 1 --rebuild --config fastannotate.mainbranch=tip a --debug
107 fastannotate: a: 4 new changesets in the main branch
108 0: 1
109 1: 2
110 $ hg fastannotate -r 4 a --debug
111 fastannotate: a: using fast path (resolved fctx: True)
112 3: 0
113 0: 1
114 1: 2
115 2: 3
116 4: 4
117
118 rename
119
120 $ hg mv a b
121 $ cat > b << EOF
122 > 0
123 > 11
124 > 3
125 > 44
126 > EOF
127 $ hg commit -m b -q
128 $ hg fastannotate -ncf --long-hash b
129 3 d641cb51f61e331c44654104301f8154d7865c89 a: 0
130 5 d44dade239915bc82b91e4556b1257323f8e5824 b: 11
131 2 26162a884ba60e8c87bf4e0d6bb8efcc6f711a4e a: 3
132 5 d44dade239915bc82b91e4556b1257323f8e5824 b: 44
133 $ hg fastannotate -r 26162a884ba60e8c87bf4e0d6bb8efcc6f711a4e a
134 0: 1
135 1: 2
136 2: 3
137
138 fastannotate --deleted
139
140 $ hg fastannotate --deleted -nf b
141 3 a: 0
142 5 b: 11
143 0 a: -1
144 1 a: -2
145 2 a: 3
146 5 b: 44
147 4 a: -4
148 $ hg fastannotate --deleted -r 3 -nf a
149 3 a: 0
150 0 a: 1
151 1 a: 2
152
153 file and directories with ".l", ".m" suffixes
154
155 $ cd ..
156 $ hg init repo2
157 $ cd repo2
158
159 $ mkdir a.l b.m c.lock a.l.hg b.hg
160 $ for i in a b c d d.l d.m a.l/a b.m/a c.lock/a a.l.hg/a b.hg/a; do
161 > echo $i > $i
162 > done
163 $ hg add . -q
164 $ hg commit -m init
165 $ hg fastannotate a.l/a b.m/a c.lock/a a.l.hg/a b.hg/a d.l d.m a b c d
166 0: a
167 0: a.l.hg/a
168 0: a.l/a
169 0: b
170 0: b.hg/a
171 0: b.m/a
172 0: c
173 0: c.lock/a
174 0: d
175 0: d.l
176 0: d.m
177
178 empty file
179
180 $ touch empty
181 $ hg commit -A empty -m empty
182 $ hg fastannotate empty
@@ -0,0 +1,216 b''
1 $ cat >> $HGRCPATH << EOF
2 > [ui]
3 > ssh = $PYTHON "$TESTDIR/dummyssh"
4 > [extensions]
5 > fastannotate=
6 > [fastannotate]
7 > mainbranch=@
8 > EOF
9
10 $ HGMERGE=true; export HGMERGE
11
12 setup the server repo
13
14 $ hg init repo-server
15 $ cd repo-server
16 $ cat >> .hg/hgrc << EOF
17 > [fastannotate]
18 > server=1
19 > EOF
20 $ for i in 1 2 3 4; do
21 > echo $i >> a
22 > hg commit -A -m $i a
23 > done
24 $ [ -d .hg/fastannotate ]
25 [1]
26 $ hg bookmark @
27 $ cd ..
28
29 setup the local repo
30
31 $ hg clone 'ssh://user@dummy/repo-server' repo-local -q
32 $ cd repo-local
33 $ cat >> .hg/hgrc << EOF
34 > [fastannotate]
35 > client=1
36 > clientfetchthreshold=0
37 > EOF
38 $ [ -d .hg/fastannotate ]
39 [1]
40 $ hg fastannotate a --debug
41 running * (glob)
42 sending hello command
43 sending between command
44 remote: * (glob) (?)
45 remote: capabilities: * (glob)
46 remote: * (glob) (?)
47 sending protocaps command
48 fastannotate: requesting 1 files
49 sending getannotate command
50 fastannotate: writing 112 bytes to fastannotate/default/a.l (?)
51 fastannotate: server returned
52 fastannotate: writing 112 bytes to fastannotate/default/a.l (?)
53 fastannotate: writing 94 bytes to fastannotate/default/a.m
54 fastannotate: a: using fast path (resolved fctx: True)
55 0: 1
56 1: 2
57 2: 3
58 3: 4
59
60 the cache could be reused and no download is necessary
61
62 $ hg fastannotate a --debug
63 fastannotate: a: using fast path (resolved fctx: True)
64 0: 1
65 1: 2
66 2: 3
67 3: 4
68
69 if the client agrees where the head of the master branch is, no re-download
70 happens even if the client has more commits
71
72 $ echo 5 >> a
73 $ hg commit -m 5
74 $ hg bookmark -r 3 @ -f
75 $ hg fastannotate a --debug
76 0: 1
77 1: 2
78 2: 3
79 3: 4
80 4: 5
81
82 if the client has a different "@" (head of the master branch) and "@" is ahead
83 of the server, the server can detect things are unchanged and does not return
84 full contents (not that there is no "writing ... to fastannotate"), but the
85 client can also build things up on its own (causing diverge)
86
87 $ hg bookmark -r 4 @ -f
88 $ hg fastannotate a --debug
89 running * (glob)
90 sending hello command
91 sending between command
92 remote: * (glob) (?)
93 remote: capabilities: * (glob)
94 remote: * (glob) (?)
95 sending protocaps command
96 fastannotate: requesting 1 files
97 sending getannotate command
98 fastannotate: server returned
99 fastannotate: a: 1 new changesets in the main branch
100 0: 1
101 1: 2
102 2: 3
103 3: 4
104 4: 5
105
106 if the client has a different "@" which is behind the server. no download is
107 necessary
108
109 $ hg fastannotate a --debug --config fastannotate.mainbranch=2
110 fastannotate: a: using fast path (resolved fctx: True)
111 0: 1
112 1: 2
113 2: 3
114 3: 4
115 4: 5
116
117 define fastannotate on-disk paths
118
119 $ p1=.hg/fastannotate/default
120 $ p2=../repo-server/.hg/fastannotate/default
121
122 revert bookmark change so the client is behind the server
123
124 $ hg bookmark -r 2 @ -f
125
126 in the "fctx" mode with the "annotate" command, the client also downloads the
127 cache. but not in the (default) "fastannotate" mode.
128
129 $ rm $p1/a.l $p1/a.m
130 $ hg annotate a --debug | grep 'fastannotate: writing'
131 [1]
132 $ hg annotate a --config fastannotate.modes=fctx --debug | grep 'fastannotate: writing' | sort
133 fastannotate: writing 112 bytes to fastannotate/default/a.l
134 fastannotate: writing 94 bytes to fastannotate/default/a.m
135
136 the fastannotate cache (built server-side, downloaded client-side) in two repos
137 have the same content (because the client downloads from the server)
138
139 $ diff $p1/a.l $p2/a.l
140 $ diff $p1/a.m $p2/a.m
141
142 in the "fctx" mode, the client could also build the cache locally
143
144 $ hg annotate a --config fastannotate.modes=fctx --debug --config fastannotate.mainbranch=4 | grep fastannotate
145 fastannotate: requesting 1 files
146 fastannotate: server returned
147 fastannotate: a: 1 new changesets in the main branch
148
149 the server would rebuild broken cache automatically
150
151 $ cp $p2/a.m $p2/a.m.bak
152 $ echo BROKEN1 > $p1/a.m
153 $ echo BROKEN2 > $p2/a.m
154 $ hg fastannotate a --debug | grep 'fastannotate: writing' | sort
155 fastannotate: writing 112 bytes to fastannotate/default/a.l
156 fastannotate: writing 94 bytes to fastannotate/default/a.m
157 $ diff $p1/a.m $p2/a.m
158 $ diff $p2/a.m $p2/a.m.bak
159
160 use the "debugbuildannotatecache" command to build annotate cache
161
162 $ rm -rf $p1 $p2
163 $ hg --cwd ../repo-server debugbuildannotatecache a --debug
164 fastannotate: a: 4 new changesets in the main branch
165 $ hg --cwd ../repo-local debugbuildannotatecache a --debug
166 running * (glob)
167 sending hello command
168 sending between command
169 remote: * (glob) (?)
170 remote: capabilities: * (glob)
171 remote: * (glob) (?)
172 sending protocaps command
173 fastannotate: requesting 1 files
174 sending getannotate command
175 fastannotate: server returned
176 fastannotate: writing * (glob)
177 fastannotate: writing * (glob)
178 $ diff $p1/a.l $p2/a.l
179 $ diff $p1/a.m $p2/a.m
180
181 with the clientfetchthreshold config option, the client can build up the cache
182 without downloading from the server
183
184 $ rm -rf $p1
185 $ hg fastannotate a --debug --config fastannotate.clientfetchthreshold=10
186 fastannotate: a: 3 new changesets in the main branch
187 0: 1
188 1: 2
189 2: 3
190 3: 4
191 4: 5
192
193 if the fastannotate directory is not writable, the fctx mode still works
194
195 $ rm -rf $p1
196 $ touch $p1
197 $ hg annotate a --debug --traceback --config fastannotate.modes=fctx
198 fastannotate: a: cache broken and deleted
199 fastannotate: prefetch failed: * (glob)
200 fastannotate: a: cache broken and deleted
201 fastannotate: falling back to the vanilla annotate: * (glob)
202 0: 1
203 1: 2
204 2: 3
205 3: 4
206 4: 5
207
208 with serverbuildondemand=False, the server will not build anything
209
210 $ cat >> ../repo-server/.hg/hgrc <<EOF
211 > [fastannotate]
212 > serverbuildondemand=False
213 > EOF
214 $ rm -rf $p1 $p2
215 $ hg fastannotate a --debug | grep 'fastannotate: writing'
216 [1]
@@ -0,0 +1,168 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > fastannotate=
4 > [fastannotate]
5 > mainbranch=main
6 > EOF
7
8 $ hg init repo
9 $ cd repo
10
11 add or rename files on top of the master branch
12
13 $ echo a1 > a
14 $ echo b1 > b
15 $ hg commit -qAm 1
16 $ hg bookmark -i main
17 $ hg fastannotate --debug -nf b
18 fastannotate: b: 1 new changesets in the main branch
19 0 b: b1
20 $ hg fastannotate --debug -nf a
21 fastannotate: a: 1 new changesets in the main branch
22 0 a: a1
23 $ echo a2 >> a
24 $ cat > b << EOF
25 > b0
26 > b1
27 > EOF
28 $ hg mv a t
29 $ hg mv b a
30 $ hg mv t b
31 $ hg commit -m 'swap names'
32
33 existing linelogs are not helpful with such renames in side branches
34
35 $ hg fastannotate --debug -nf a
36 fastannotate: a: linelog cannot help in annotating this revision
37 1 a: b0
38 0 b: b1
39 $ hg fastannotate --debug -nf b
40 fastannotate: b: linelog cannot help in annotating this revision
41 0 a: a1
42 1 b: a2
43
44 move main branch forward, rebuild should happen
45
46 $ hg bookmark -i main -r . -q
47 $ hg fastannotate --debug -nf b
48 fastannotate: b: cache broken and deleted
49 fastannotate: b: 2 new changesets in the main branch
50 0 a: a1
51 1 b: a2
52 $ hg fastannotate --debug -nf b
53 fastannotate: b: using fast path (resolved fctx: True)
54 0 a: a1
55 1 b: a2
56
57 for rev 0, the existing linelog is still useful for a, but not for b
58
59 $ hg fastannotate --debug -nf a -r 0
60 fastannotate: a: using fast path (resolved fctx: True)
61 0 a: a1
62 $ hg fastannotate --debug -nf b -r 0
63 fastannotate: b: linelog cannot help in annotating this revision
64 0 b: b1
65
66 a rebuild can also be triggered if "the main branch last time" mismatches
67
68 $ echo a3 >> a
69 $ hg commit -m a3
70 $ cat >> b << EOF
71 > b3
72 > b4
73 > EOF
74 $ hg commit -m b4
75 $ hg bookmark -i main -q
76 $ hg fastannotate --debug -nf a
77 fastannotate: a: cache broken and deleted
78 fastannotate: a: 3 new changesets in the main branch
79 1 a: b0
80 0 b: b1
81 2 a: a3
82 $ hg fastannotate --debug -nf a
83 fastannotate: a: using fast path (resolved fctx: True)
84 1 a: b0
85 0 b: b1
86 2 a: a3
87
88 linelog can be updated without being helpful
89
90 $ hg mv a t
91 $ hg mv b a
92 $ hg mv t b
93 $ hg commit -m 'swap names again'
94 $ hg fastannotate --debug -nf b
95 fastannotate: b: 1 new changesets in the main branch
96 1 a: b0
97 0 b: b1
98 2 a: a3
99 $ hg fastannotate --debug -nf b
100 fastannotate: b: linelog cannot help in annotating this revision
101 1 a: b0
102 0 b: b1
103 2 a: a3
104
105 move main branch forward again, rebuilds are one-time
106
107 $ hg bookmark -i main -q
108 $ hg fastannotate --debug -nf a
109 fastannotate: a: cache broken and deleted
110 fastannotate: a: 4 new changesets in the main branch
111 0 a: a1
112 1 b: a2
113 3 b: b3
114 3 b: b4
115 $ hg fastannotate --debug -nf b
116 fastannotate: b: cache broken and deleted
117 fastannotate: b: 4 new changesets in the main branch
118 1 a: b0
119 0 b: b1
120 2 a: a3
121 $ hg fastannotate --debug -nf a
122 fastannotate: a: using fast path (resolved fctx: True)
123 0 a: a1
124 1 b: a2
125 3 b: b3
126 3 b: b4
127 $ hg fastannotate --debug -nf b
128 fastannotate: b: using fast path (resolved fctx: True)
129 1 a: b0
130 0 b: b1
131 2 a: a3
132
133 list changeset hashes to improve readability
134
135 $ hg log -T '{rev}:{node}\n'
136 4:980e1ab8c516350172928fba95b49ede3b643dca
137 3:14e123fedad9f491f5dde0beca2a767625a0a93a
138 2:96495c41e4c12218766f78cdf244e768d7718b0f
139 1:35c2b781234c994896aba36bd3245d3104e023df
140 0:653e95416ebb5dbcc25bbc7f75568c9e01f7bd2f
141
142 annotate a revision not in the linelog. linelog cannot be used, but does not get rebuilt either
143
144 $ hg fastannotate --debug -nf a -r 96495c41e4c12218766f78cdf244e768d7718b0f
145 fastannotate: a: linelog cannot help in annotating this revision
146 1 a: b0
147 0 b: b1
148 2 a: a3
149 $ hg fastannotate --debug -nf a -r 2
150 fastannotate: a: linelog cannot help in annotating this revision
151 1 a: b0
152 0 b: b1
153 2 a: a3
154 $ hg fastannotate --debug -nf a -r .
155 fastannotate: a: using fast path (resolved fctx: True)
156 0 a: a1
157 1 b: a2
158 3 b: b3
159 3 b: b4
160
161 annotate an ancient revision where the path matches. linelog can be used
162
163 $ hg fastannotate --debug -nf a -r 0
164 fastannotate: a: using fast path (resolved fctx: True)
165 0 a: a1
166 $ hg fastannotate --debug -nf a -r 653e95416ebb5dbcc25bbc7f75568c9e01f7bd2f
167 fastannotate: a: using fast path (resolved fctx: False)
168 0 a: a1
@@ -0,0 +1,191 b''
1 from __future__ import absolute_import, print_function
2
3 import os
4 import tempfile
5
6 from mercurial import util
7 from hgext.fastannotate import error, revmap
8
9 def genhsh(i):
10 return chr(i) + b'\0' * 19
11
12 def gettemppath():
13 fd, path = tempfile.mkstemp()
14 os.unlink(path)
15 os.close(fd)
16 return path
17
18 def ensure(condition):
19 if not condition:
20 raise RuntimeError('Unexpected')
21
22 def testbasicreadwrite():
23 path = gettemppath()
24
25 rm = revmap.revmap(path)
26 ensure(rm.maxrev == 0)
27 for i in xrange(5):
28 ensure(rm.rev2hsh(i) is None)
29 ensure(rm.hsh2rev(b'\0' * 20) is None)
30
31 paths = ['', 'a', None, 'b', 'b', 'c', 'c', None, 'a', 'b', 'a', 'a']
32 for i in xrange(1, 5):
33 ensure(rm.append(genhsh(i), sidebranch=(i & 1), path=paths[i]) == i)
34
35 ensure(rm.maxrev == 4)
36 for i in xrange(1, 5):
37 ensure(rm.hsh2rev(genhsh(i)) == i)
38 ensure(rm.rev2hsh(i) == genhsh(i))
39
40 # re-load and verify
41 rm.flush()
42 rm = revmap.revmap(path)
43 ensure(rm.maxrev == 4)
44 for i in xrange(1, 5):
45 ensure(rm.hsh2rev(genhsh(i)) == i)
46 ensure(rm.rev2hsh(i) == genhsh(i))
47 ensure(bool(rm.rev2flag(i) & revmap.sidebranchflag) == bool(i & 1))
48
49 # append without calling save() explicitly
50 for i in xrange(5, 12):
51 ensure(rm.append(genhsh(i), sidebranch=(i & 1), path=paths[i],
52 flush=True) == i)
53
54 # re-load and verify
55 rm = revmap.revmap(path)
56 ensure(rm.maxrev == 11)
57 for i in xrange(1, 12):
58 ensure(rm.hsh2rev(genhsh(i)) == i)
59 ensure(rm.rev2hsh(i) == genhsh(i))
60 ensure(rm.rev2path(i) == paths[i] or paths[i - 1])
61 ensure(bool(rm.rev2flag(i) & revmap.sidebranchflag) == bool(i & 1))
62
63 os.unlink(path)
64
65 # missing keys
66 ensure(rm.rev2hsh(12) is None)
67 ensure(rm.rev2hsh(0) is None)
68 ensure(rm.rev2hsh(-1) is None)
69 ensure(rm.rev2flag(12) is None)
70 ensure(rm.rev2path(12) is None)
71 ensure(rm.hsh2rev(b'\1' * 20) is None)
72
73 # illformed hash (not 20 bytes)
74 try:
75 rm.append(b'\0')
76 ensure(False)
77 except Exception:
78 pass
79
80 def testcorruptformat():
81 path = gettemppath()
82
83 # incorrect header
84 with open(path, 'w') as f:
85 f.write(b'NOT A VALID HEADER')
86 try:
87 revmap.revmap(path)
88 ensure(False)
89 except error.CorruptedFileError:
90 pass
91
92 # rewrite the file
93 os.unlink(path)
94 rm = revmap.revmap(path)
95 rm.append(genhsh(0), flush=True)
96
97 rm = revmap.revmap(path)
98 ensure(rm.maxrev == 1)
99
100 # corrupt the file by appending a byte
101 size = os.stat(path).st_size
102 with open(path, 'a') as f:
103 f.write('\xff')
104 try:
105 revmap.revmap(path)
106 ensure(False)
107 except error.CorruptedFileError:
108 pass
109
110 # corrupt the file by removing the last byte
111 ensure(size > 0)
112 with open(path, 'w') as f:
113 f.truncate(size - 1)
114 try:
115 revmap.revmap(path)
116 ensure(False)
117 except error.CorruptedFileError:
118 pass
119
120 os.unlink(path)
121
122 def testcopyfrom():
123 path = gettemppath()
124 rm = revmap.revmap(path)
125 for i in xrange(1, 10):
126 ensure(rm.append(genhsh(i), sidebranch=(i & 1), path=str(i // 3)) == i)
127 rm.flush()
128
129 # copy rm to rm2
130 rm2 = revmap.revmap()
131 rm2.copyfrom(rm)
132 path2 = gettemppath()
133 rm2.path = path2
134 rm2.flush()
135
136 # two files should be the same
137 ensure(len(set(util.readfile(p) for p in [path, path2])) == 1)
138
139 os.unlink(path)
140 os.unlink(path2)
141
142 class fakefctx(object):
143 def __init__(self, node, path=None):
144 self._node = node
145 self._path = path
146
147 def node(self):
148 return self._node
149
150 def path(self):
151 return self._path
152
153 def testcontains():
154 path = gettemppath()
155
156 rm = revmap.revmap(path)
157 for i in xrange(1, 5):
158 ensure(rm.append(genhsh(i), sidebranch=(i & 1)) == i)
159
160 for i in xrange(1, 5):
161 ensure(((genhsh(i), None) in rm) == ((i & 1) == 0))
162 ensure((fakefctx(genhsh(i)) in rm) == ((i & 1) == 0))
163 for i in xrange(5, 10):
164 ensure(fakefctx(genhsh(i)) not in rm)
165 ensure((genhsh(i), None) not in rm)
166
167 # "contains" checks paths
168 rm = revmap.revmap()
169 for i in xrange(1, 5):
170 ensure(rm.append(genhsh(i), path=str(i // 2)) == i)
171 for i in xrange(1, 5):
172 ensure(fakefctx(genhsh(i), path=str(i // 2)) in rm)
173 ensure(fakefctx(genhsh(i), path='a') not in rm)
174
175 def testlastnode():
176 path = gettemppath()
177 ensure(revmap.getlastnode(path) is None)
178 rm = revmap.revmap(path)
179 ensure(revmap.getlastnode(path) is None)
180 for i in xrange(1, 10):
181 hsh = genhsh(i)
182 rm.append(hsh, path=str(i // 2), flush=True)
183 ensure(revmap.getlastnode(path) == hsh)
184 rm2 = revmap.revmap(path)
185 ensure(rm2.rev2hsh(rm2.maxrev) == hsh)
186
187 testbasicreadwrite()
188 testcorruptformat()
189 testcopyfrom()
190 testcontains()
191 testlastnode()
@@ -0,0 +1,263 b''
1 $ cat >> $HGRCPATH << EOF
2 > [extensions]
3 > fastannotate=
4 > EOF
5
6 $ HGMERGE=true; export HGMERGE
7
8 $ hg init repo
9 $ cd repo
10
11 a simple merge case
12
13 $ echo 1 > a
14 $ hg commit -qAm 'append 1'
15 $ echo 2 >> a
16 $ hg commit -m 'append 2'
17 $ echo 3 >> a
18 $ hg commit -m 'append 3'
19 $ hg up 1 -q
20 $ cat > a << EOF
21 > 0
22 > 1
23 > 2
24 > EOF
25 $ hg commit -qm 'insert 0'
26 $ hg merge 2 -q
27 $ echo 4 >> a
28 $ hg commit -m merge
29 $ hg log -G -T '{rev}: {desc}'
30 @ 4: merge
31 |\
32 | o 3: insert 0
33 | |
34 o | 2: append 3
35 |/
36 o 1: append 2
37 |
38 o 0: append 1
39
40 $ hg fastannotate a
41 3: 0
42 0: 1
43 1: 2
44 2: 3
45 4: 4
46 $ hg fastannotate -r 0 a
47 0: 1
48 $ hg fastannotate -r 1 a
49 0: 1
50 1: 2
51 $ hg fastannotate -udnclf a
52 test 3 d641cb51f61e Thu Jan 01 00:00:00 1970 +0000 a:1: 0
53 test 0 4994017376d3 Thu Jan 01 00:00:00 1970 +0000 a:1: 1
54 test 1 e940cb6d9a06 Thu Jan 01 00:00:00 1970 +0000 a:2: 2
55 test 2 26162a884ba6 Thu Jan 01 00:00:00 1970 +0000 a:3: 3
56 test 4 3ad7bcd2815f Thu Jan 01 00:00:00 1970 +0000 a:5: 4
57 $ hg fastannotate --linear a
58 3: 0
59 0: 1
60 1: 2
61 4: 3
62 4: 4
63
64 incrementally updating
65
66 $ hg fastannotate -r 0 a --debug
67 fastannotate: a: using fast path (resolved fctx: True)
68 0: 1
69 $ hg fastannotate -r 0 a --debug --rebuild
70 fastannotate: a: 1 new changesets in the main branch
71 0: 1
72 $ hg fastannotate -r 1 a --debug
73 fastannotate: a: 1 new changesets in the main branch
74 0: 1
75 1: 2
76 $ hg fastannotate -r 3 a --debug
77 fastannotate: a: 1 new changesets in the main branch
78 3: 0
79 0: 1
80 1: 2
81 $ hg fastannotate -r 4 a --debug
82 fastannotate: a: 1 new changesets in the main branch
83 3: 0
84 0: 1
85 1: 2
86 2: 3
87 4: 4
88 $ hg fastannotate -r 1 a --debug
89 fastannotate: a: using fast path (resolved fctx: True)
90 0: 1
91 1: 2
92
93 rebuild happens automatically if unable to update
94
95 $ hg fastannotate -r 2 a --debug
96 fastannotate: a: cache broken and deleted
97 fastannotate: a: 3 new changesets in the main branch
98 0: 1
99 1: 2
100 2: 3
101
102 config option "fastannotate.mainbranch"
103
104 $ hg fastannotate -r 1 --rebuild --config fastannotate.mainbranch=tip a --debug
105 fastannotate: a: 4 new changesets in the main branch
106 0: 1
107 1: 2
108 $ hg fastannotate -r 4 a --debug
109 fastannotate: a: using fast path (resolved fctx: True)
110 3: 0
111 0: 1
112 1: 2
113 2: 3
114 4: 4
115
116 config option "fastannotate.modes"
117
118 $ hg annotate -r 1 --debug a
119 0: 1
120 1: 2
121 $ hg annotate --config fastannotate.modes=fctx -r 1 --debug a
122 fastannotate: a: using fast path (resolved fctx: False)
123 0: 1
124 1: 2
125 $ hg fastannotate --config fastannotate.modes=fctx -h -q
126 hg: unknown command 'fastannotate'
127 (did you mean *) (glob)
128 [255]
129
130 rename
131
132 $ hg mv a b
133 $ cat > b << EOF
134 > 0
135 > 11
136 > 3
137 > 44
138 > EOF
139 $ hg commit -m b -q
140 $ hg fastannotate -ncf --long-hash b
141 3 d641cb51f61e331c44654104301f8154d7865c89 a: 0
142 5 d44dade239915bc82b91e4556b1257323f8e5824 b: 11
143 2 26162a884ba60e8c87bf4e0d6bb8efcc6f711a4e a: 3
144 5 d44dade239915bc82b91e4556b1257323f8e5824 b: 44
145 $ hg fastannotate -r 26162a884ba60e8c87bf4e0d6bb8efcc6f711a4e a
146 0: 1
147 1: 2
148 2: 3
149
150 fastannotate --deleted
151
152 $ hg fastannotate --deleted -nf b
153 3 a: 0
154 5 b: 11
155 0 a: -1
156 1 a: -2
157 2 a: 3
158 5 b: 44
159 4 a: -4
160 $ hg fastannotate --deleted -r 3 -nf a
161 3 a: 0
162 0 a: 1
163 1 a: 2
164
165 file and directories with ".l", ".m" suffixes
166
167 $ cd ..
168 $ hg init repo2
169 $ cd repo2
170
171 $ mkdir a.l b.m c.lock a.l.hg b.hg
172 $ for i in a b c d d.l d.m a.l/a b.m/a c.lock/a a.l.hg/a b.hg/a; do
173 > echo $i > $i
174 > done
175 $ hg add . -q
176 $ hg commit -m init
177 $ hg fastannotate a.l/a b.m/a c.lock/a a.l.hg/a b.hg/a d.l d.m a b c d
178 0: a
179 0: a.l.hg/a
180 0: a.l/a
181 0: b
182 0: b.hg/a
183 0: b.m/a
184 0: c
185 0: c.lock/a
186 0: d
187 0: d.l
188 0: d.m
189
190 empty file
191
192 $ touch empty
193 $ hg commit -A empty -m empty
194 $ hg fastannotate empty
195
196 json format
197
198 $ hg fastannotate -Tjson -cludn b a empty
199 [
200 {
201 "date": [0.0, 0],
202 "line": "a\n",
203 "line_number": 1,
204 "node": "1fd620b16252aecb54c6aa530dff5ed6e6ec3d21",
205 "rev": 0,
206 "user": "test"
207 },
208 {
209 "date": [0.0, 0],
210 "line": "b\n",
211 "line_number": 1,
212 "node": "1fd620b16252aecb54c6aa530dff5ed6e6ec3d21",
213 "rev": 0,
214 "user": "test"
215 }
216 ]
217
218 $ hg fastannotate -Tjson -cludn empty
219 [
220 ]
221 $ hg fastannotate -Tjson --no-content -n a
222 [
223 {
224 "rev": 0
225 }
226 ]
227
228 working copy
229
230 $ echo a >> a
231 $ hg fastannotate -r 'wdir()' a
232 abort: cannot update linelog to wdir()
233 (set fastannotate.mainbranch)
234 [255]
235 $ cat >> $HGRCPATH << EOF
236 > [fastannotate]
237 > mainbranch = .
238 > EOF
239 $ hg fastannotate -r 'wdir()' a
240 0 : a
241 1+: a
242 $ hg fastannotate -cludn -r 'wdir()' a
243 test 0 1fd620b16252 Thu Jan 01 00:00:00 1970 +0000:1: a
244 test 1 720582f5bdb6+ *:2: a (glob)
245 $ hg fastannotate -cludn -r 'wdir()' -Tjson a
246 [
247 {
248 "date": [0.0, 0],
249 "line": "a\n",
250 "line_number": 1,
251 "node": "1fd620b16252aecb54c6aa530dff5ed6e6ec3d21",
252 "rev": 0,
253 "user": "test"
254 },
255 {
256 "date": [*, 0], (glob)
257 "line": "a\n",
258 "line_number": 2,
259 "node": null,
260 "rev": null,
261 "user": "test"
262 }
263 ]
@@ -818,6 +818,7 b" packages = ['mercurial',"
818 'mercurial.thirdparty.zope.interface',
818 'mercurial.thirdparty.zope.interface',
819 'mercurial.utils',
819 'mercurial.utils',
820 'hgext', 'hgext.convert', 'hgext.fsmonitor',
820 'hgext', 'hgext.convert', 'hgext.fsmonitor',
821 'hgext.fastannotate',
821 'hgext.fsmonitor.pywatchman',
822 'hgext.fsmonitor.pywatchman',
822 'hgext.infinitepush',
823 'hgext.infinitepush',
823 'hgext.highlight',
824 'hgext.highlight',
General Comments 0
You need to be logged in to leave comments. Login now