##// END OF EJS Templates
fastannotate: slice strings to get single character...
Augie Fackler -
r41298:e40b7a50 default
parent child Browse files
Show More
@@ -1,164 +1,164
1 1 # Copyright 2016-present Facebook. All Rights Reserved.
2 2 #
3 3 # format: defines the format used to output annotate result
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 from mercurial import (
10 10 encoding,
11 11 node,
12 12 pycompat,
13 13 templatefilters,
14 14 util,
15 15 )
16 16 from mercurial.utils import (
17 17 dateutil,
18 18 )
19 19
20 20 # imitating mercurial.commands.annotate, not using the vanilla formatter since
21 21 # the data structures are a bit different, and we have some fast paths.
22 22 class defaultformatter(object):
23 23 """the default formatter that does leftpad and support some common flags"""
24 24
25 25 def __init__(self, ui, repo, opts):
26 26 self.ui = ui
27 27 self.opts = opts
28 28
29 29 if ui.quiet:
30 30 datefunc = dateutil.shortdate
31 31 else:
32 32 datefunc = dateutil.datestr
33 33 datefunc = util.cachefunc(datefunc)
34 34 getctx = util.cachefunc(lambda x: repo[x[0]])
35 35 hexfunc = self._hexfunc
36 36
37 37 # special handling working copy "changeset" and "rev" functions
38 38 if self.opts.get('rev') == 'wdir()':
39 39 orig = hexfunc
40 40 hexfunc = lambda x: None if x is None else orig(x)
41 41 wnode = hexfunc(repo[None].p1().node()) + '+'
42 42 wrev = '%d' % repo[None].p1().rev()
43 43 wrevpad = ''
44 44 if not opts.get('changeset'): # only show + if changeset is hidden
45 45 wrev += '+'
46 46 wrevpad = ' '
47 47 revenc = lambda x: wrev if x is None else ('%d' % x) + wrevpad
48 48 def csetenc(x):
49 49 if x is None:
50 50 return wnode
51 51 return pycompat.bytestr(x) + ' '
52 52 else:
53 53 revenc = csetenc = pycompat.bytestr
54 54
55 55 # opt name, separator, raw value (for json/plain), encoder (for plain)
56 56 opmap = [('user', ' ', lambda x: getctx(x).user(), ui.shortuser),
57 57 ('number', ' ', lambda x: getctx(x).rev(), revenc),
58 58 ('changeset', ' ', lambda x: hexfunc(x[0]), csetenc),
59 59 ('date', ' ', lambda x: getctx(x).date(), datefunc),
60 60 ('file', ' ', lambda x: x[2], pycompat.bytestr),
61 61 ('line_number', ':', lambda x: x[1] + 1, pycompat.bytestr)]
62 62 fieldnamemap = {'number': 'rev', 'changeset': 'node'}
63 63 funcmap = [(get, sep, fieldnamemap.get(op, op), enc)
64 64 for op, sep, get, enc in opmap
65 65 if opts.get(op)]
66 66 # no separator for first column
67 67 funcmap[0] = list(funcmap[0])
68 68 funcmap[0][1] = ''
69 69 self.funcmap = funcmap
70 70
71 71 def write(self, annotatedresult, lines=None, existinglines=None):
72 72 """(annotateresult, [str], set([rev, linenum])) -> None. write output.
73 73 annotateresult can be [(node, linenum, path)], or [(node, linenum)]
74 74 """
75 75 pieces = [] # [[str]]
76 76 maxwidths = [] # [int]
77 77
78 78 # calculate padding
79 79 for f, sep, name, enc in self.funcmap:
80 80 l = [enc(f(x)) for x in annotatedresult]
81 81 pieces.append(l)
82 82 if name in ['node', 'date']: # node and date has fixed size
83 83 l = l[:1]
84 84 widths = pycompat.maplist(encoding.colwidth, set(l))
85 85 maxwidth = (max(widths) if widths else 0)
86 86 maxwidths.append(maxwidth)
87 87
88 88 # buffered output
89 89 result = ''
90 90 for i in pycompat.xrange(len(annotatedresult)):
91 91 for j, p in enumerate(pieces):
92 92 sep = self.funcmap[j][1]
93 93 padding = ' ' * (maxwidths[j] - len(p[i]))
94 94 result += sep + padding + p[i]
95 95 if lines:
96 96 if existinglines is None:
97 97 result += ': ' + lines[i]
98 98 else: # extra formatting showing whether a line exists
99 99 key = (annotatedresult[i][0], annotatedresult[i][1])
100 100 if key in existinglines:
101 101 result += ': ' + lines[i]
102 102 else:
103 103 result += ': ' + self.ui.label('-' + lines[i],
104 104 'diff.deleted')
105 105
106 if result[-1] != '\n':
106 if result[-1:] != '\n':
107 107 result += '\n'
108 108
109 109 self.ui.write(result)
110 110
111 111 @util.propertycache
112 112 def _hexfunc(self):
113 113 if self.ui.debugflag or self.opts.get('long_hash'):
114 114 return node.hex
115 115 else:
116 116 return node.short
117 117
118 118 def end(self):
119 119 pass
120 120
121 121 class jsonformatter(defaultformatter):
122 122 def __init__(self, ui, repo, opts):
123 123 super(jsonformatter, self).__init__(ui, repo, opts)
124 124 self.ui.write('[')
125 125 self.needcomma = False
126 126
127 127 def write(self, annotatedresult, lines=None, existinglines=None):
128 128 if annotatedresult:
129 129 self._writecomma()
130 130
131 131 pieces = [(name, map(f, annotatedresult))
132 132 for f, sep, name, enc in self.funcmap]
133 133 if lines is not None:
134 134 pieces.append(('line', lines))
135 135 pieces.sort()
136 136
137 137 seps = [','] * len(pieces[:-1]) + ['']
138 138
139 139 result = ''
140 140 lasti = len(annotatedresult) - 1
141 141 for i in pycompat.xrange(len(annotatedresult)):
142 142 result += '\n {\n'
143 143 for j, p in enumerate(pieces):
144 144 k, vs = p
145 145 result += (' "%s": %s%s\n'
146 146 % (k, templatefilters.json(vs[i], paranoid=False),
147 147 seps[j]))
148 148 result += ' }%s' % ('' if i == lasti else ',')
149 149 if lasti >= 0:
150 150 self.needcomma = True
151 151
152 152 self.ui.write(result)
153 153
154 154 def _writecomma(self):
155 155 if self.needcomma:
156 156 self.ui.write(',')
157 157 self.needcomma = False
158 158
159 159 @util.propertycache
160 160 def _hexfunc(self):
161 161 return node.hex
162 162
163 163 def end(self):
164 164 self.ui.write('\n]\n')
@@ -1,228 +1,228
1 1 # Copyright 2016-present Facebook. All Rights Reserved.
2 2 #
3 3 # protocol: logic for a server providing fastannotate support
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 error,
15 15 extensions,
16 16 hg,
17 17 util,
18 18 wireprotov1peer,
19 19 wireprotov1server,
20 20 )
21 21 from . import context
22 22
23 23 # common
24 24
25 25 def _getmaster(ui):
26 26 """get the mainbranch, and enforce it is set"""
27 27 master = ui.config('fastannotate', 'mainbranch')
28 28 if not master:
29 29 raise error.Abort(_('fastannotate.mainbranch is required '
30 30 'for both the client and the server'))
31 31 return master
32 32
33 33 # server-side
34 34
35 35 def _capabilities(orig, repo, proto):
36 36 result = orig(repo, proto)
37 37 result.append('getannotate')
38 38 return result
39 39
40 40 def _getannotate(repo, proto, path, lastnode):
41 41 # output:
42 42 # FILE := vfspath + '\0' + str(size) + '\0' + content
43 43 # OUTPUT := '' | FILE + OUTPUT
44 44 result = ''
45 45 buildondemand = repo.ui.configbool('fastannotate', 'serverbuildondemand',
46 46 True)
47 47 with context.annotatecontext(repo, path) as actx:
48 48 if buildondemand:
49 49 # update before responding to the client
50 50 master = _getmaster(repo.ui)
51 51 try:
52 52 if not actx.isuptodate(master):
53 53 actx.annotate(master, master)
54 54 except Exception:
55 55 # non-fast-forward move or corrupted. rebuild automically.
56 56 actx.rebuild()
57 57 try:
58 58 actx.annotate(master, master)
59 59 except Exception:
60 60 actx.rebuild() # delete files
61 61 finally:
62 62 # although the "with" context will also do a close/flush, we
63 63 # need to do it early so we can send the correct respond to
64 64 # client.
65 65 actx.close()
66 66 # send back the full content of revmap and linelog, in the future we
67 67 # may want to do some rsync-like fancy updating.
68 68 # the lastnode check is not necessary if the client and the server
69 69 # agree where the main branch is.
70 70 if actx.lastnode != lastnode:
71 71 for p in [actx.revmappath, actx.linelogpath]:
72 72 if not os.path.exists(p):
73 73 continue
74 74 content = ''
75 75 with open(p, 'rb') as f:
76 76 content = f.read()
77 77 vfsbaselen = len(repo.vfs.base + '/')
78 78 relpath = p[vfsbaselen:]
79 79 result += '%s\0%d\0%s' % (relpath, len(content), content)
80 80 return result
81 81
82 82 def _registerwireprotocommand():
83 83 if 'getannotate' in wireprotov1server.commands:
84 84 return
85 85 wireprotov1server.wireprotocommand(
86 86 'getannotate', 'path lastnode')(_getannotate)
87 87
88 88 def serveruisetup(ui):
89 89 _registerwireprotocommand()
90 90 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
91 91
92 92 # client-side
93 93
94 94 def _parseresponse(payload):
95 95 result = {}
96 96 i = 0
97 97 l = len(payload) - 1
98 98 state = 0 # 0: vfspath, 1: size
99 99 vfspath = size = ''
100 100 while i < l:
101 ch = payload[i]
101 ch = payload[i:i + 1]
102 102 if ch == '\0':
103 103 if state == 1:
104 104 result[vfspath] = buffer(payload, i + 1, int(size))
105 105 i += int(size)
106 106 state = 0
107 107 vfspath = size = ''
108 108 elif state == 0:
109 109 state = 1
110 110 else:
111 111 if state == 1:
112 112 size += ch
113 113 elif state == 0:
114 114 vfspath += ch
115 115 i += 1
116 116 return result
117 117
118 118 def peersetup(ui, peer):
119 119 class fastannotatepeer(peer.__class__):
120 120 @wireprotov1peer.batchable
121 121 def getannotate(self, path, lastnode=None):
122 122 if not self.capable('getannotate'):
123 123 ui.warn(_('remote peer cannot provide annotate cache\n'))
124 124 yield None, None
125 125 else:
126 126 args = {'path': path, 'lastnode': lastnode or ''}
127 127 f = wireprotov1peer.future()
128 128 yield args, f
129 129 yield _parseresponse(f.value)
130 130 peer.__class__ = fastannotatepeer
131 131
132 132 @contextlib.contextmanager
133 133 def annotatepeer(repo):
134 134 ui = repo.ui
135 135
136 136 remotepath = ui.expandpath(
137 137 ui.config('fastannotate', 'remotepath', 'default'))
138 138 peer = hg.peer(ui, {}, remotepath)
139 139
140 140 try:
141 141 yield peer
142 142 finally:
143 143 peer.close()
144 144
145 145 def clientfetch(repo, paths, lastnodemap=None, peer=None):
146 146 """download annotate cache from the server for paths"""
147 147 if not paths:
148 148 return
149 149
150 150 if peer is None:
151 151 with annotatepeer(repo) as peer:
152 152 return clientfetch(repo, paths, lastnodemap, peer)
153 153
154 154 if lastnodemap is None:
155 155 lastnodemap = {}
156 156
157 157 ui = repo.ui
158 158 results = []
159 159 with peer.commandexecutor() as batcher:
160 160 ui.debug('fastannotate: requesting %d files\n' % len(paths))
161 161 for p in paths:
162 162 results.append(batcher.callcommand(
163 163 'getannotate',
164 164 {'path': p, 'lastnode':lastnodemap.get(p)}))
165 165
166 166 for result in results:
167 167 r = result.result()
168 168 # TODO: pconvert these paths on the server?
169 169 r = {util.pconvert(p): v for p, v in r.iteritems()}
170 170 for path in sorted(r):
171 171 # ignore malicious paths
172 172 if (not path.startswith('fastannotate/')
173 173 or '/../' in (path + '/')):
174 174 ui.debug('fastannotate: ignored malicious path %s\n' % path)
175 175 continue
176 176 content = r[path]
177 177 if ui.debugflag:
178 178 ui.debug('fastannotate: writing %d bytes to %s\n'
179 179 % (len(content), path))
180 180 repo.vfs.makedirs(os.path.dirname(path))
181 181 with repo.vfs(path, 'wb') as f:
182 182 f.write(content)
183 183
184 184 def _filterfetchpaths(repo, paths):
185 185 """return a subset of paths whose history is long and need to fetch linelog
186 186 from the server. works with remotefilelog and non-remotefilelog repos.
187 187 """
188 188 threshold = repo.ui.configint('fastannotate', 'clientfetchthreshold', 10)
189 189 if threshold <= 0:
190 190 return paths
191 191
192 192 result = []
193 193 for path in paths:
194 194 try:
195 195 if len(repo.file(path)) >= threshold:
196 196 result.append(path)
197 197 except Exception: # file not found etc.
198 198 result.append(path)
199 199
200 200 return result
201 201
202 202 def localreposetup(ui, repo):
203 203 class fastannotaterepo(repo.__class__):
204 204 def prefetchfastannotate(self, paths, peer=None):
205 205 master = _getmaster(self.ui)
206 206 needupdatepaths = []
207 207 lastnodemap = {}
208 208 try:
209 209 for path in _filterfetchpaths(self, paths):
210 210 with context.annotatecontext(self, path) as actx:
211 211 if not actx.isuptodate(master, strict=False):
212 212 needupdatepaths.append(path)
213 213 lastnodemap[path] = actx.lastnode
214 214 if needupdatepaths:
215 215 clientfetch(self, needupdatepaths, lastnodemap, peer)
216 216 except Exception as ex:
217 217 # could be directory not writable or so, not fatal
218 218 self.ui.debug('fastannotate: prefetch failed: %r\n' % ex)
219 219 repo.__class__ = fastannotaterepo
220 220
221 221 def clientreposetup(ui, repo):
222 222 _registerwireprotocommand()
223 223 if repo.local():
224 224 localreposetup(ui, repo)
225 225 # TODO: this mutates global state, but only if at least one repo
226 226 # has the extension enabled. This is probably bad for hgweb.
227 227 if peersetup not in hg.wirepeersetupfuncs:
228 228 hg.wirepeersetupfuncs.append(peersetup)
General Comments 0
You need to be logged in to leave comments. Login now