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