##// END OF EJS Templates
py3: delete b'' prefix from safehasattr arguments...
Martin von Zweigbergk -
r43385:4aa72cdf default
parent child Browse files
Show More
@@ -1,1131 +1,1131 b''
1 # absorb.py
1 # absorb.py
2 #
2 #
3 # Copyright 2016 Facebook, Inc.
3 # Copyright 2016 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """apply working directory changes to changesets (EXPERIMENTAL)
8 """apply working directory changes to changesets (EXPERIMENTAL)
9
9
10 The absorb extension provides a command to use annotate information to
10 The absorb extension provides a command to use annotate information to
11 amend modified chunks into the corresponding non-public changesets.
11 amend modified chunks into the corresponding non-public changesets.
12
12
13 ::
13 ::
14
14
15 [absorb]
15 [absorb]
16 # only check 50 recent non-public changesets at most
16 # only check 50 recent non-public changesets at most
17 max-stack-size = 50
17 max-stack-size = 50
18 # whether to add noise to new commits to avoid obsolescence cycle
18 # whether to add noise to new commits to avoid obsolescence cycle
19 add-noise = 1
19 add-noise = 1
20 # make `amend --correlated` a shortcut to the main command
20 # make `amend --correlated` a shortcut to the main command
21 amend-flag = correlated
21 amend-flag = correlated
22
22
23 [color]
23 [color]
24 absorb.description = yellow
24 absorb.description = yellow
25 absorb.node = blue bold
25 absorb.node = blue bold
26 absorb.path = bold
26 absorb.path = bold
27 """
27 """
28
28
29 # TODO:
29 # TODO:
30 # * Rename config items to [commands] namespace
30 # * Rename config items to [commands] namespace
31 # * Converge getdraftstack() with other code in core
31 # * Converge getdraftstack() with other code in core
32 # * move many attributes on fixupstate to be private
32 # * move many attributes on fixupstate to be private
33
33
34 from __future__ import absolute_import
34 from __future__ import absolute_import
35
35
36 import collections
36 import collections
37
37
38 from mercurial.i18n import _
38 from mercurial.i18n import _
39 from mercurial import (
39 from mercurial import (
40 cmdutil,
40 cmdutil,
41 commands,
41 commands,
42 context,
42 context,
43 crecord,
43 crecord,
44 error,
44 error,
45 linelog,
45 linelog,
46 mdiff,
46 mdiff,
47 node,
47 node,
48 obsolete,
48 obsolete,
49 patch,
49 patch,
50 phases,
50 phases,
51 pycompat,
51 pycompat,
52 registrar,
52 registrar,
53 scmutil,
53 scmutil,
54 util,
54 util,
55 )
55 )
56 from mercurial.utils import stringutil
56 from mercurial.utils import stringutil
57
57
58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
60 # be specifying the version(s) of Mercurial they are tested with, or
60 # be specifying the version(s) of Mercurial they are tested with, or
61 # leave the attribute unspecified.
61 # leave the attribute unspecified.
62 testedwith = b'ships-with-hg-core'
62 testedwith = b'ships-with-hg-core'
63
63
64 cmdtable = {}
64 cmdtable = {}
65 command = registrar.command(cmdtable)
65 command = registrar.command(cmdtable)
66
66
67 configtable = {}
67 configtable = {}
68 configitem = registrar.configitem(configtable)
68 configitem = registrar.configitem(configtable)
69
69
70 configitem(b'absorb', b'add-noise', default=True)
70 configitem(b'absorb', b'add-noise', default=True)
71 configitem(b'absorb', b'amend-flag', default=None)
71 configitem(b'absorb', b'amend-flag', default=None)
72 configitem(b'absorb', b'max-stack-size', default=50)
72 configitem(b'absorb', b'max-stack-size', default=50)
73
73
74 colortable = {
74 colortable = {
75 b'absorb.description': b'yellow',
75 b'absorb.description': b'yellow',
76 b'absorb.node': b'blue bold',
76 b'absorb.node': b'blue bold',
77 b'absorb.path': b'bold',
77 b'absorb.path': b'bold',
78 }
78 }
79
79
80 defaultdict = collections.defaultdict
80 defaultdict = collections.defaultdict
81
81
82
82
83 class nullui(object):
83 class nullui(object):
84 """blank ui object doing nothing"""
84 """blank ui object doing nothing"""
85
85
86 debugflag = False
86 debugflag = False
87 verbose = False
87 verbose = False
88 quiet = True
88 quiet = True
89
89
90 def __getitem__(name):
90 def __getitem__(name):
91 def nullfunc(*args, **kwds):
91 def nullfunc(*args, **kwds):
92 return
92 return
93
93
94 return nullfunc
94 return nullfunc
95
95
96
96
97 class emptyfilecontext(object):
97 class emptyfilecontext(object):
98 """minimal filecontext representing an empty file"""
98 """minimal filecontext representing an empty file"""
99
99
100 def data(self):
100 def data(self):
101 return b''
101 return b''
102
102
103 def node(self):
103 def node(self):
104 return node.nullid
104 return node.nullid
105
105
106
106
107 def uniq(lst):
107 def uniq(lst):
108 """list -> list. remove duplicated items without changing the order"""
108 """list -> list. remove duplicated items without changing the order"""
109 seen = set()
109 seen = set()
110 result = []
110 result = []
111 for x in lst:
111 for x in lst:
112 if x not in seen:
112 if x not in seen:
113 seen.add(x)
113 seen.add(x)
114 result.append(x)
114 result.append(x)
115 return result
115 return result
116
116
117
117
118 def getdraftstack(headctx, limit=None):
118 def getdraftstack(headctx, limit=None):
119 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
119 """(ctx, int?) -> [ctx]. get a linear stack of non-public changesets.
120
120
121 changesets are sorted in topo order, oldest first.
121 changesets are sorted in topo order, oldest first.
122 return at most limit items, if limit is a positive number.
122 return at most limit items, if limit is a positive number.
123
123
124 merges are considered as non-draft as well. i.e. every commit
124 merges are considered as non-draft as well. i.e. every commit
125 returned has and only has 1 parent.
125 returned has and only has 1 parent.
126 """
126 """
127 ctx = headctx
127 ctx = headctx
128 result = []
128 result = []
129 while ctx.phase() != phases.public:
129 while ctx.phase() != phases.public:
130 if limit and len(result) >= limit:
130 if limit and len(result) >= limit:
131 break
131 break
132 parents = ctx.parents()
132 parents = ctx.parents()
133 if len(parents) != 1:
133 if len(parents) != 1:
134 break
134 break
135 result.append(ctx)
135 result.append(ctx)
136 ctx = parents[0]
136 ctx = parents[0]
137 result.reverse()
137 result.reverse()
138 return result
138 return result
139
139
140
140
141 def getfilestack(stack, path, seenfctxs=None):
141 def getfilestack(stack, path, seenfctxs=None):
142 """([ctx], str, set) -> [fctx], {ctx: fctx}
142 """([ctx], str, set) -> [fctx], {ctx: fctx}
143
143
144 stack is a list of contexts, from old to new. usually they are what
144 stack is a list of contexts, from old to new. usually they are what
145 "getdraftstack" returns.
145 "getdraftstack" returns.
146
146
147 follows renames, but not copies.
147 follows renames, but not copies.
148
148
149 seenfctxs is a set of filecontexts that will be considered "immutable".
149 seenfctxs is a set of filecontexts that will be considered "immutable".
150 they are usually what this function returned in earlier calls, useful
150 they are usually what this function returned in earlier calls, useful
151 to avoid issues that a file was "moved" to multiple places and was then
151 to avoid issues that a file was "moved" to multiple places and was then
152 modified differently, like: "a" was copied to "b", "a" was also copied to
152 modified differently, like: "a" was copied to "b", "a" was also copied to
153 "c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
153 "c" and then "a" was deleted, then both "b" and "c" were "moved" from "a"
154 and we enforce only one of them to be able to affect "a"'s content.
154 and we enforce only one of them to be able to affect "a"'s content.
155
155
156 return an empty list and an empty dict, if the specified path does not
156 return an empty list and an empty dict, if the specified path does not
157 exist in stack[-1] (the top of the stack).
157 exist in stack[-1] (the top of the stack).
158
158
159 otherwise, return a list of de-duplicated filecontexts, and the map to
159 otherwise, return a list of de-duplicated filecontexts, and the map to
160 convert ctx in the stack to fctx, for possible mutable fctxs. the first item
160 convert ctx in the stack to fctx, for possible mutable fctxs. the first item
161 of the list would be outside the stack and should be considered immutable.
161 of the list would be outside the stack and should be considered immutable.
162 the remaining items are within the stack.
162 the remaining items are within the stack.
163
163
164 for example, given the following changelog and corresponding filelog
164 for example, given the following changelog and corresponding filelog
165 revisions:
165 revisions:
166
166
167 changelog: 3----4----5----6----7
167 changelog: 3----4----5----6----7
168 filelog: x 0----1----1----2 (x: no such file yet)
168 filelog: x 0----1----1----2 (x: no such file yet)
169
169
170 - if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
170 - if stack = [5, 6, 7], returns ([0, 1, 2], {5: 1, 6: 1, 7: 2})
171 - if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
171 - if stack = [3, 4, 5], returns ([e, 0, 1], {4: 0, 5: 1}), where "e" is a
172 dummy empty filecontext.
172 dummy empty filecontext.
173 - if stack = [2], returns ([], {})
173 - if stack = [2], returns ([], {})
174 - if stack = [7], returns ([1, 2], {7: 2})
174 - if stack = [7], returns ([1, 2], {7: 2})
175 - if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
175 - if stack = [6, 7], returns ([1, 2], {6: 1, 7: 2}), although {6: 1} can be
176 removed, since 1 is immutable.
176 removed, since 1 is immutable.
177 """
177 """
178 if seenfctxs is None:
178 if seenfctxs is None:
179 seenfctxs = set()
179 seenfctxs = set()
180 assert stack
180 assert stack
181
181
182 if path not in stack[-1]:
182 if path not in stack[-1]:
183 return [], {}
183 return [], {}
184
184
185 fctxs = []
185 fctxs = []
186 fctxmap = {}
186 fctxmap = {}
187
187
188 pctx = stack[0].p1() # the public (immutable) ctx we stop at
188 pctx = stack[0].p1() # the public (immutable) ctx we stop at
189 for ctx in reversed(stack):
189 for ctx in reversed(stack):
190 if path not in ctx: # the file is added in the next commit
190 if path not in ctx: # the file is added in the next commit
191 pctx = ctx
191 pctx = ctx
192 break
192 break
193 fctx = ctx[path]
193 fctx = ctx[path]
194 fctxs.append(fctx)
194 fctxs.append(fctx)
195 if fctx in seenfctxs: # treat fctx as the immutable one
195 if fctx in seenfctxs: # treat fctx as the immutable one
196 pctx = None # do not add another immutable fctx
196 pctx = None # do not add another immutable fctx
197 break
197 break
198 fctxmap[ctx] = fctx # only for mutable fctxs
198 fctxmap[ctx] = fctx # only for mutable fctxs
199 copy = fctx.copysource()
199 copy = fctx.copysource()
200 if copy:
200 if copy:
201 path = copy # follow rename
201 path = copy # follow rename
202 if path in ctx: # but do not follow copy
202 if path in ctx: # but do not follow copy
203 pctx = ctx.p1()
203 pctx = ctx.p1()
204 break
204 break
205
205
206 if pctx is not None: # need an extra immutable fctx
206 if pctx is not None: # need an extra immutable fctx
207 if path in pctx:
207 if path in pctx:
208 fctxs.append(pctx[path])
208 fctxs.append(pctx[path])
209 else:
209 else:
210 fctxs.append(emptyfilecontext())
210 fctxs.append(emptyfilecontext())
211
211
212 fctxs.reverse()
212 fctxs.reverse()
213 # note: we rely on a property of hg: filerev is not reused for linear
213 # note: we rely on a property of hg: filerev is not reused for linear
214 # history. i.e. it's impossible to have:
214 # history. i.e. it's impossible to have:
215 # changelog: 4----5----6 (linear, no merges)
215 # changelog: 4----5----6 (linear, no merges)
216 # filelog: 1----2----1
216 # filelog: 1----2----1
217 # ^ reuse filerev (impossible)
217 # ^ reuse filerev (impossible)
218 # because parents are part of the hash. if that's not true, we need to
218 # because parents are part of the hash. if that's not true, we need to
219 # remove uniq and find a different way to identify fctxs.
219 # remove uniq and find a different way to identify fctxs.
220 return uniq(fctxs), fctxmap
220 return uniq(fctxs), fctxmap
221
221
222
222
223 class overlaystore(patch.filestore):
223 class overlaystore(patch.filestore):
224 """read-only, hybrid store based on a dict and ctx.
224 """read-only, hybrid store based on a dict and ctx.
225 memworkingcopy: {path: content}, overrides file contents.
225 memworkingcopy: {path: content}, overrides file contents.
226 """
226 """
227
227
228 def __init__(self, basectx, memworkingcopy):
228 def __init__(self, basectx, memworkingcopy):
229 self.basectx = basectx
229 self.basectx = basectx
230 self.memworkingcopy = memworkingcopy
230 self.memworkingcopy = memworkingcopy
231
231
232 def getfile(self, path):
232 def getfile(self, path):
233 """comply with mercurial.patch.filestore.getfile"""
233 """comply with mercurial.patch.filestore.getfile"""
234 if path not in self.basectx:
234 if path not in self.basectx:
235 return None, None, None
235 return None, None, None
236 fctx = self.basectx[path]
236 fctx = self.basectx[path]
237 if path in self.memworkingcopy:
237 if path in self.memworkingcopy:
238 content = self.memworkingcopy[path]
238 content = self.memworkingcopy[path]
239 else:
239 else:
240 content = fctx.data()
240 content = fctx.data()
241 mode = (fctx.islink(), fctx.isexec())
241 mode = (fctx.islink(), fctx.isexec())
242 copy = fctx.copysource()
242 copy = fctx.copysource()
243 return content, mode, copy
243 return content, mode, copy
244
244
245
245
246 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
246 def overlaycontext(memworkingcopy, ctx, parents=None, extra=None):
247 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
247 """({path: content}, ctx, (p1node, p2node)?, {}?) -> memctx
248 memworkingcopy overrides file contents.
248 memworkingcopy overrides file contents.
249 """
249 """
250 # parents must contain 2 items: (node1, node2)
250 # parents must contain 2 items: (node1, node2)
251 if parents is None:
251 if parents is None:
252 parents = ctx.repo().changelog.parents(ctx.node())
252 parents = ctx.repo().changelog.parents(ctx.node())
253 if extra is None:
253 if extra is None:
254 extra = ctx.extra()
254 extra = ctx.extra()
255 date = ctx.date()
255 date = ctx.date()
256 desc = ctx.description()
256 desc = ctx.description()
257 user = ctx.user()
257 user = ctx.user()
258 files = set(ctx.files()).union(memworkingcopy)
258 files = set(ctx.files()).union(memworkingcopy)
259 store = overlaystore(ctx, memworkingcopy)
259 store = overlaystore(ctx, memworkingcopy)
260 return context.memctx(
260 return context.memctx(
261 repo=ctx.repo(),
261 repo=ctx.repo(),
262 parents=parents,
262 parents=parents,
263 text=desc,
263 text=desc,
264 files=files,
264 files=files,
265 filectxfn=store,
265 filectxfn=store,
266 user=user,
266 user=user,
267 date=date,
267 date=date,
268 branch=None,
268 branch=None,
269 extra=extra,
269 extra=extra,
270 )
270 )
271
271
272
272
273 class filefixupstate(object):
273 class filefixupstate(object):
274 """state needed to apply fixups to a single file
274 """state needed to apply fixups to a single file
275
275
276 internally, it keeps file contents of several revisions and a linelog.
276 internally, it keeps file contents of several revisions and a linelog.
277
277
278 the linelog uses odd revision numbers for original contents (fctxs passed
278 the linelog uses odd revision numbers for original contents (fctxs passed
279 to __init__), and even revision numbers for fixups, like:
279 to __init__), and even revision numbers for fixups, like:
280
280
281 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
281 linelog rev 1: self.fctxs[0] (from an immutable "public" changeset)
282 linelog rev 2: fixups made to self.fctxs[0]
282 linelog rev 2: fixups made to self.fctxs[0]
283 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
283 linelog rev 3: self.fctxs[1] (a child of fctxs[0])
284 linelog rev 4: fixups made to self.fctxs[1]
284 linelog rev 4: fixups made to self.fctxs[1]
285 ...
285 ...
286
286
287 a typical use is like:
287 a typical use is like:
288
288
289 1. call diffwith, to calculate self.fixups
289 1. call diffwith, to calculate self.fixups
290 2. (optionally), present self.fixups to the user, or change it
290 2. (optionally), present self.fixups to the user, or change it
291 3. call apply, to apply changes
291 3. call apply, to apply changes
292 4. read results from "finalcontents", or call getfinalcontent
292 4. read results from "finalcontents", or call getfinalcontent
293 """
293 """
294
294
295 def __init__(self, fctxs, path, ui=None, opts=None):
295 def __init__(self, fctxs, path, ui=None, opts=None):
296 """([fctx], ui or None) -> None
296 """([fctx], ui or None) -> None
297
297
298 fctxs should be linear, and sorted by topo order - oldest first.
298 fctxs should be linear, and sorted by topo order - oldest first.
299 fctxs[0] will be considered as "immutable" and will not be changed.
299 fctxs[0] will be considered as "immutable" and will not be changed.
300 """
300 """
301 self.fctxs = fctxs
301 self.fctxs = fctxs
302 self.path = path
302 self.path = path
303 self.ui = ui or nullui()
303 self.ui = ui or nullui()
304 self.opts = opts or {}
304 self.opts = opts or {}
305
305
306 # following fields are built from fctxs. they exist for perf reason
306 # following fields are built from fctxs. they exist for perf reason
307 self.contents = [f.data() for f in fctxs]
307 self.contents = [f.data() for f in fctxs]
308 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
308 self.contentlines = pycompat.maplist(mdiff.splitnewlines, self.contents)
309 self.linelog = self._buildlinelog()
309 self.linelog = self._buildlinelog()
310 if self.ui.debugflag:
310 if self.ui.debugflag:
311 assert self._checkoutlinelog() == self.contents
311 assert self._checkoutlinelog() == self.contents
312
312
313 # following fields will be filled later
313 # following fields will be filled later
314 self.chunkstats = [0, 0] # [adopted, total : int]
314 self.chunkstats = [0, 0] # [adopted, total : int]
315 self.targetlines = [] # [str]
315 self.targetlines = [] # [str]
316 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
316 self.fixups = [] # [(linelog rev, a1, a2, b1, b2)]
317 self.finalcontents = [] # [str]
317 self.finalcontents = [] # [str]
318 self.ctxaffected = set()
318 self.ctxaffected = set()
319
319
320 def diffwith(self, targetfctx, fm=None):
320 def diffwith(self, targetfctx, fm=None):
321 """calculate fixups needed by examining the differences between
321 """calculate fixups needed by examining the differences between
322 self.fctxs[-1] and targetfctx, chunk by chunk.
322 self.fctxs[-1] and targetfctx, chunk by chunk.
323
323
324 targetfctx is the target state we move towards. we may or may not be
324 targetfctx is the target state we move towards. we may or may not be
325 able to get there because not all modified chunks can be amended into
325 able to get there because not all modified chunks can be amended into
326 a non-public fctx unambiguously.
326 a non-public fctx unambiguously.
327
327
328 call this only once, before apply().
328 call this only once, before apply().
329
329
330 update self.fixups, self.chunkstats, and self.targetlines.
330 update self.fixups, self.chunkstats, and self.targetlines.
331 """
331 """
332 a = self.contents[-1]
332 a = self.contents[-1]
333 alines = self.contentlines[-1]
333 alines = self.contentlines[-1]
334 b = targetfctx.data()
334 b = targetfctx.data()
335 blines = mdiff.splitnewlines(b)
335 blines = mdiff.splitnewlines(b)
336 self.targetlines = blines
336 self.targetlines = blines
337
337
338 self.linelog.annotate(self.linelog.maxrev)
338 self.linelog.annotate(self.linelog.maxrev)
339 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
339 annotated = self.linelog.annotateresult # [(linelog rev, linenum)]
340 assert len(annotated) == len(alines)
340 assert len(annotated) == len(alines)
341 # add a dummy end line to make insertion at the end easier
341 # add a dummy end line to make insertion at the end easier
342 if annotated:
342 if annotated:
343 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
343 dummyendline = (annotated[-1][0], annotated[-1][1] + 1)
344 annotated.append(dummyendline)
344 annotated.append(dummyendline)
345
345
346 # analyse diff blocks
346 # analyse diff blocks
347 for chunk in self._alldiffchunks(a, b, alines, blines):
347 for chunk in self._alldiffchunks(a, b, alines, blines):
348 newfixups = self._analysediffchunk(chunk, annotated)
348 newfixups = self._analysediffchunk(chunk, annotated)
349 self.chunkstats[0] += bool(newfixups) # 1 or 0
349 self.chunkstats[0] += bool(newfixups) # 1 or 0
350 self.chunkstats[1] += 1
350 self.chunkstats[1] += 1
351 self.fixups += newfixups
351 self.fixups += newfixups
352 if fm is not None:
352 if fm is not None:
353 self._showchanges(fm, alines, blines, chunk, newfixups)
353 self._showchanges(fm, alines, blines, chunk, newfixups)
354
354
355 def apply(self):
355 def apply(self):
356 """apply self.fixups. update self.linelog, self.finalcontents.
356 """apply self.fixups. update self.linelog, self.finalcontents.
357
357
358 call this only once, before getfinalcontent(), after diffwith().
358 call this only once, before getfinalcontent(), after diffwith().
359 """
359 """
360 # the following is unnecessary, as it's done by "diffwith":
360 # the following is unnecessary, as it's done by "diffwith":
361 # self.linelog.annotate(self.linelog.maxrev)
361 # self.linelog.annotate(self.linelog.maxrev)
362 for rev, a1, a2, b1, b2 in reversed(self.fixups):
362 for rev, a1, a2, b1, b2 in reversed(self.fixups):
363 blines = self.targetlines[b1:b2]
363 blines = self.targetlines[b1:b2]
364 if self.ui.debugflag:
364 if self.ui.debugflag:
365 idx = (max(rev - 1, 0)) // 2
365 idx = (max(rev - 1, 0)) // 2
366 self.ui.write(
366 self.ui.write(
367 _(b'%s: chunk %d:%d -> %d lines\n')
367 _(b'%s: chunk %d:%d -> %d lines\n')
368 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
368 % (node.short(self.fctxs[idx].node()), a1, a2, len(blines))
369 )
369 )
370 self.linelog.replacelines(rev, a1, a2, b1, b2)
370 self.linelog.replacelines(rev, a1, a2, b1, b2)
371 if self.opts.get(b'edit_lines', False):
371 if self.opts.get(b'edit_lines', False):
372 self.finalcontents = self._checkoutlinelogwithedits()
372 self.finalcontents = self._checkoutlinelogwithedits()
373 else:
373 else:
374 self.finalcontents = self._checkoutlinelog()
374 self.finalcontents = self._checkoutlinelog()
375
375
376 def getfinalcontent(self, fctx):
376 def getfinalcontent(self, fctx):
377 """(fctx) -> str. get modified file content for a given filecontext"""
377 """(fctx) -> str. get modified file content for a given filecontext"""
378 idx = self.fctxs.index(fctx)
378 idx = self.fctxs.index(fctx)
379 return self.finalcontents[idx]
379 return self.finalcontents[idx]
380
380
381 def _analysediffchunk(self, chunk, annotated):
381 def _analysediffchunk(self, chunk, annotated):
382 """analyse a different chunk and return new fixups found
382 """analyse a different chunk and return new fixups found
383
383
384 return [] if no lines from the chunk can be safely applied.
384 return [] if no lines from the chunk can be safely applied.
385
385
386 the chunk (or lines) cannot be safely applied, if, for example:
386 the chunk (or lines) cannot be safely applied, if, for example:
387 - the modified (deleted) lines belong to a public changeset
387 - the modified (deleted) lines belong to a public changeset
388 (self.fctxs[0])
388 (self.fctxs[0])
389 - the chunk is a pure insertion and the adjacent lines (at most 2
389 - the chunk is a pure insertion and the adjacent lines (at most 2
390 lines) belong to different non-public changesets, or do not belong
390 lines) belong to different non-public changesets, or do not belong
391 to any non-public changesets.
391 to any non-public changesets.
392 - the chunk is modifying lines from different changesets.
392 - the chunk is modifying lines from different changesets.
393 in this case, if the number of lines deleted equals to the number
393 in this case, if the number of lines deleted equals to the number
394 of lines added, assume it's a simple 1:1 map (could be wrong).
394 of lines added, assume it's a simple 1:1 map (could be wrong).
395 otherwise, give up.
395 otherwise, give up.
396 - the chunk is modifying lines from a single non-public changeset,
396 - the chunk is modifying lines from a single non-public changeset,
397 but other revisions touch the area as well. i.e. the lines are
397 but other revisions touch the area as well. i.e. the lines are
398 not continuous as seen from the linelog.
398 not continuous as seen from the linelog.
399 """
399 """
400 a1, a2, b1, b2 = chunk
400 a1, a2, b1, b2 = chunk
401 # find involved indexes from annotate result
401 # find involved indexes from annotate result
402 involved = annotated[a1:a2]
402 involved = annotated[a1:a2]
403 if not involved and annotated: # a1 == a2 and a is not empty
403 if not involved and annotated: # a1 == a2 and a is not empty
404 # pure insertion, check nearby lines. ignore lines belong
404 # pure insertion, check nearby lines. ignore lines belong
405 # to the public (first) changeset (i.e. annotated[i][0] == 1)
405 # to the public (first) changeset (i.e. annotated[i][0] == 1)
406 nearbylinenums = {a2, max(0, a1 - 1)}
406 nearbylinenums = {a2, max(0, a1 - 1)}
407 involved = [
407 involved = [
408 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
408 annotated[i] for i in nearbylinenums if annotated[i][0] != 1
409 ]
409 ]
410 involvedrevs = list(set(r for r, l in involved))
410 involvedrevs = list(set(r for r, l in involved))
411 newfixups = []
411 newfixups = []
412 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
412 if len(involvedrevs) == 1 and self._iscontinuous(a1, a2 - 1, True):
413 # chunk belongs to a single revision
413 # chunk belongs to a single revision
414 rev = involvedrevs[0]
414 rev = involvedrevs[0]
415 if rev > 1:
415 if rev > 1:
416 fixuprev = rev + 1
416 fixuprev = rev + 1
417 newfixups.append((fixuprev, a1, a2, b1, b2))
417 newfixups.append((fixuprev, a1, a2, b1, b2))
418 elif a2 - a1 == b2 - b1 or b1 == b2:
418 elif a2 - a1 == b2 - b1 or b1 == b2:
419 # 1:1 line mapping, or chunk was deleted
419 # 1:1 line mapping, or chunk was deleted
420 for i in pycompat.xrange(a1, a2):
420 for i in pycompat.xrange(a1, a2):
421 rev, linenum = annotated[i]
421 rev, linenum = annotated[i]
422 if rev > 1:
422 if rev > 1:
423 if b1 == b2: # deletion, simply remove that single line
423 if b1 == b2: # deletion, simply remove that single line
424 nb1 = nb2 = 0
424 nb1 = nb2 = 0
425 else: # 1:1 line mapping, change the corresponding rev
425 else: # 1:1 line mapping, change the corresponding rev
426 nb1 = b1 + i - a1
426 nb1 = b1 + i - a1
427 nb2 = nb1 + 1
427 nb2 = nb1 + 1
428 fixuprev = rev + 1
428 fixuprev = rev + 1
429 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
429 newfixups.append((fixuprev, i, i + 1, nb1, nb2))
430 return self._optimizefixups(newfixups)
430 return self._optimizefixups(newfixups)
431
431
432 @staticmethod
432 @staticmethod
433 def _alldiffchunks(a, b, alines, blines):
433 def _alldiffchunks(a, b, alines, blines):
434 """like mdiff.allblocks, but only care about differences"""
434 """like mdiff.allblocks, but only care about differences"""
435 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
435 blocks = mdiff.allblocks(a, b, lines1=alines, lines2=blines)
436 for chunk, btype in blocks:
436 for chunk, btype in blocks:
437 if btype != b'!':
437 if btype != b'!':
438 continue
438 continue
439 yield chunk
439 yield chunk
440
440
441 def _buildlinelog(self):
441 def _buildlinelog(self):
442 """calculate the initial linelog based on self.content{,line}s.
442 """calculate the initial linelog based on self.content{,line}s.
443 this is similar to running a partial "annotate".
443 this is similar to running a partial "annotate".
444 """
444 """
445 llog = linelog.linelog()
445 llog = linelog.linelog()
446 a, alines = b'', []
446 a, alines = b'', []
447 for i in pycompat.xrange(len(self.contents)):
447 for i in pycompat.xrange(len(self.contents)):
448 b, blines = self.contents[i], self.contentlines[i]
448 b, blines = self.contents[i], self.contentlines[i]
449 llrev = i * 2 + 1
449 llrev = i * 2 + 1
450 chunks = self._alldiffchunks(a, b, alines, blines)
450 chunks = self._alldiffchunks(a, b, alines, blines)
451 for a1, a2, b1, b2 in reversed(list(chunks)):
451 for a1, a2, b1, b2 in reversed(list(chunks)):
452 llog.replacelines(llrev, a1, a2, b1, b2)
452 llog.replacelines(llrev, a1, a2, b1, b2)
453 a, alines = b, blines
453 a, alines = b, blines
454 return llog
454 return llog
455
455
456 def _checkoutlinelog(self):
456 def _checkoutlinelog(self):
457 """() -> [str]. check out file contents from linelog"""
457 """() -> [str]. check out file contents from linelog"""
458 contents = []
458 contents = []
459 for i in pycompat.xrange(len(self.contents)):
459 for i in pycompat.xrange(len(self.contents)):
460 rev = (i + 1) * 2
460 rev = (i + 1) * 2
461 self.linelog.annotate(rev)
461 self.linelog.annotate(rev)
462 content = b''.join(map(self._getline, self.linelog.annotateresult))
462 content = b''.join(map(self._getline, self.linelog.annotateresult))
463 contents.append(content)
463 contents.append(content)
464 return contents
464 return contents
465
465
466 def _checkoutlinelogwithedits(self):
466 def _checkoutlinelogwithedits(self):
467 """() -> [str]. prompt all lines for edit"""
467 """() -> [str]. prompt all lines for edit"""
468 alllines = self.linelog.getalllines()
468 alllines = self.linelog.getalllines()
469 # header
469 # header
470 editortext = (
470 editortext = (
471 _(
471 _(
472 b'HG: editing %s\nHG: "y" means the line to the right '
472 b'HG: editing %s\nHG: "y" means the line to the right '
473 b'exists in the changeset to the top\nHG:\n'
473 b'exists in the changeset to the top\nHG:\n'
474 )
474 )
475 % self.fctxs[-1].path()
475 % self.fctxs[-1].path()
476 )
476 )
477 # [(idx, fctx)]. hide the dummy emptyfilecontext
477 # [(idx, fctx)]. hide the dummy emptyfilecontext
478 visiblefctxs = [
478 visiblefctxs = [
479 (i, f)
479 (i, f)
480 for i, f in enumerate(self.fctxs)
480 for i, f in enumerate(self.fctxs)
481 if not isinstance(f, emptyfilecontext)
481 if not isinstance(f, emptyfilecontext)
482 ]
482 ]
483 for i, (j, f) in enumerate(visiblefctxs):
483 for i, (j, f) in enumerate(visiblefctxs):
484 editortext += _(b'HG: %s/%s %s %s\n') % (
484 editortext += _(b'HG: %s/%s %s %s\n') % (
485 b'|' * i,
485 b'|' * i,
486 b'-' * (len(visiblefctxs) - i + 1),
486 b'-' * (len(visiblefctxs) - i + 1),
487 node.short(f.node()),
487 node.short(f.node()),
488 f.description().split(b'\n', 1)[0],
488 f.description().split(b'\n', 1)[0],
489 )
489 )
490 editortext += _(b'HG: %s\n') % (b'|' * len(visiblefctxs))
490 editortext += _(b'HG: %s\n') % (b'|' * len(visiblefctxs))
491 # figure out the lifetime of a line, this is relatively inefficient,
491 # figure out the lifetime of a line, this is relatively inefficient,
492 # but probably fine
492 # but probably fine
493 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
493 lineset = defaultdict(lambda: set()) # {(llrev, linenum): {llrev}}
494 for i, f in visiblefctxs:
494 for i, f in visiblefctxs:
495 self.linelog.annotate((i + 1) * 2)
495 self.linelog.annotate((i + 1) * 2)
496 for l in self.linelog.annotateresult:
496 for l in self.linelog.annotateresult:
497 lineset[l].add(i)
497 lineset[l].add(i)
498 # append lines
498 # append lines
499 for l in alllines:
499 for l in alllines:
500 editortext += b' %s : %s' % (
500 editortext += b' %s : %s' % (
501 b''.join(
501 b''.join(
502 [
502 [
503 (b'y' if i in lineset[l] else b' ')
503 (b'y' if i in lineset[l] else b' ')
504 for i, _f in visiblefctxs
504 for i, _f in visiblefctxs
505 ]
505 ]
506 ),
506 ),
507 self._getline(l),
507 self._getline(l),
508 )
508 )
509 # run editor
509 # run editor
510 editedtext = self.ui.edit(editortext, b'', action=b'absorb')
510 editedtext = self.ui.edit(editortext, b'', action=b'absorb')
511 if not editedtext:
511 if not editedtext:
512 raise error.Abort(_(b'empty editor text'))
512 raise error.Abort(_(b'empty editor text'))
513 # parse edited result
513 # parse edited result
514 contents = [b'' for i in self.fctxs]
514 contents = [b'' for i in self.fctxs]
515 leftpadpos = 4
515 leftpadpos = 4
516 colonpos = leftpadpos + len(visiblefctxs) + 1
516 colonpos = leftpadpos + len(visiblefctxs) + 1
517 for l in mdiff.splitnewlines(editedtext):
517 for l in mdiff.splitnewlines(editedtext):
518 if l.startswith(b'HG:'):
518 if l.startswith(b'HG:'):
519 continue
519 continue
520 if l[colonpos - 1 : colonpos + 2] != b' : ':
520 if l[colonpos - 1 : colonpos + 2] != b' : ':
521 raise error.Abort(_(b'malformed line: %s') % l)
521 raise error.Abort(_(b'malformed line: %s') % l)
522 linecontent = l[colonpos + 2 :]
522 linecontent = l[colonpos + 2 :]
523 for i, ch in enumerate(
523 for i, ch in enumerate(
524 pycompat.bytestr(l[leftpadpos : colonpos - 1])
524 pycompat.bytestr(l[leftpadpos : colonpos - 1])
525 ):
525 ):
526 if ch == b'y':
526 if ch == b'y':
527 contents[visiblefctxs[i][0]] += linecontent
527 contents[visiblefctxs[i][0]] += linecontent
528 # chunkstats is hard to calculate if anything changes, therefore
528 # chunkstats is hard to calculate if anything changes, therefore
529 # set them to just a simple value (1, 1).
529 # set them to just a simple value (1, 1).
530 if editedtext != editortext:
530 if editedtext != editortext:
531 self.chunkstats = [1, 1]
531 self.chunkstats = [1, 1]
532 return contents
532 return contents
533
533
534 def _getline(self, lineinfo):
534 def _getline(self, lineinfo):
535 """((rev, linenum)) -> str. convert rev+line number to line content"""
535 """((rev, linenum)) -> str. convert rev+line number to line content"""
536 rev, linenum = lineinfo
536 rev, linenum = lineinfo
537 if rev & 1: # odd: original line taken from fctxs
537 if rev & 1: # odd: original line taken from fctxs
538 return self.contentlines[rev // 2][linenum]
538 return self.contentlines[rev // 2][linenum]
539 else: # even: fixup line from targetfctx
539 else: # even: fixup line from targetfctx
540 return self.targetlines[linenum]
540 return self.targetlines[linenum]
541
541
542 def _iscontinuous(self, a1, a2, closedinterval=False):
542 def _iscontinuous(self, a1, a2, closedinterval=False):
543 """(a1, a2 : int) -> bool
543 """(a1, a2 : int) -> bool
544
544
545 check if these lines are continuous. i.e. no other insertions or
545 check if these lines are continuous. i.e. no other insertions or
546 deletions (from other revisions) among these lines.
546 deletions (from other revisions) among these lines.
547
547
548 closedinterval decides whether a2 should be included or not. i.e. is
548 closedinterval decides whether a2 should be included or not. i.e. is
549 it [a1, a2), or [a1, a2] ?
549 it [a1, a2), or [a1, a2] ?
550 """
550 """
551 if a1 >= a2:
551 if a1 >= a2:
552 return True
552 return True
553 llog = self.linelog
553 llog = self.linelog
554 offset1 = llog.getoffset(a1)
554 offset1 = llog.getoffset(a1)
555 offset2 = llog.getoffset(a2) + int(closedinterval)
555 offset2 = llog.getoffset(a2) + int(closedinterval)
556 linesinbetween = llog.getalllines(offset1, offset2)
556 linesinbetween = llog.getalllines(offset1, offset2)
557 return len(linesinbetween) == a2 - a1 + int(closedinterval)
557 return len(linesinbetween) == a2 - a1 + int(closedinterval)
558
558
559 def _optimizefixups(self, fixups):
559 def _optimizefixups(self, fixups):
560 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
560 """[(rev, a1, a2, b1, b2)] -> [(rev, a1, a2, b1, b2)].
561 merge adjacent fixups to make them less fragmented.
561 merge adjacent fixups to make them less fragmented.
562 """
562 """
563 result = []
563 result = []
564 pcurrentchunk = [[-1, -1, -1, -1, -1]]
564 pcurrentchunk = [[-1, -1, -1, -1, -1]]
565
565
566 def pushchunk():
566 def pushchunk():
567 if pcurrentchunk[0][0] != -1:
567 if pcurrentchunk[0][0] != -1:
568 result.append(tuple(pcurrentchunk[0]))
568 result.append(tuple(pcurrentchunk[0]))
569
569
570 for i, chunk in enumerate(fixups):
570 for i, chunk in enumerate(fixups):
571 rev, a1, a2, b1, b2 = chunk
571 rev, a1, a2, b1, b2 = chunk
572 lastrev = pcurrentchunk[0][0]
572 lastrev = pcurrentchunk[0][0]
573 lasta2 = pcurrentchunk[0][2]
573 lasta2 = pcurrentchunk[0][2]
574 lastb2 = pcurrentchunk[0][4]
574 lastb2 = pcurrentchunk[0][4]
575 if (
575 if (
576 a1 == lasta2
576 a1 == lasta2
577 and b1 == lastb2
577 and b1 == lastb2
578 and rev == lastrev
578 and rev == lastrev
579 and self._iscontinuous(max(a1 - 1, 0), a1)
579 and self._iscontinuous(max(a1 - 1, 0), a1)
580 ):
580 ):
581 # merge into currentchunk
581 # merge into currentchunk
582 pcurrentchunk[0][2] = a2
582 pcurrentchunk[0][2] = a2
583 pcurrentchunk[0][4] = b2
583 pcurrentchunk[0][4] = b2
584 else:
584 else:
585 pushchunk()
585 pushchunk()
586 pcurrentchunk[0] = list(chunk)
586 pcurrentchunk[0] = list(chunk)
587 pushchunk()
587 pushchunk()
588 return result
588 return result
589
589
590 def _showchanges(self, fm, alines, blines, chunk, fixups):
590 def _showchanges(self, fm, alines, blines, chunk, fixups):
591 def trim(line):
591 def trim(line):
592 if line.endswith(b'\n'):
592 if line.endswith(b'\n'):
593 line = line[:-1]
593 line = line[:-1]
594 return line
594 return line
595
595
596 # this is not optimized for perf but _showchanges only gets executed
596 # this is not optimized for perf but _showchanges only gets executed
597 # with an extra command-line flag.
597 # with an extra command-line flag.
598 a1, a2, b1, b2 = chunk
598 a1, a2, b1, b2 = chunk
599 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
599 aidxs, bidxs = [0] * (a2 - a1), [0] * (b2 - b1)
600 for idx, fa1, fa2, fb1, fb2 in fixups:
600 for idx, fa1, fa2, fb1, fb2 in fixups:
601 for i in pycompat.xrange(fa1, fa2):
601 for i in pycompat.xrange(fa1, fa2):
602 aidxs[i - a1] = (max(idx, 1) - 1) // 2
602 aidxs[i - a1] = (max(idx, 1) - 1) // 2
603 for i in pycompat.xrange(fb1, fb2):
603 for i in pycompat.xrange(fb1, fb2):
604 bidxs[i - b1] = (max(idx, 1) - 1) // 2
604 bidxs[i - b1] = (max(idx, 1) - 1) // 2
605
605
606 fm.startitem()
606 fm.startitem()
607 fm.write(
607 fm.write(
608 b'hunk',
608 b'hunk',
609 b' %s\n',
609 b' %s\n',
610 b'@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
610 b'@@ -%d,%d +%d,%d @@' % (a1, a2 - a1, b1, b2 - b1),
611 label=b'diff.hunk',
611 label=b'diff.hunk',
612 )
612 )
613 fm.data(path=self.path, linetype=b'hunk')
613 fm.data(path=self.path, linetype=b'hunk')
614
614
615 def writeline(idx, diffchar, line, linetype, linelabel):
615 def writeline(idx, diffchar, line, linetype, linelabel):
616 fm.startitem()
616 fm.startitem()
617 node = b''
617 node = b''
618 if idx:
618 if idx:
619 ctx = self.fctxs[idx]
619 ctx = self.fctxs[idx]
620 fm.context(fctx=ctx)
620 fm.context(fctx=ctx)
621 node = ctx.hex()
621 node = ctx.hex()
622 self.ctxaffected.add(ctx.changectx())
622 self.ctxaffected.add(ctx.changectx())
623 fm.write(b'node', b'%-7.7s ', node, label=b'absorb.node')
623 fm.write(b'node', b'%-7.7s ', node, label=b'absorb.node')
624 fm.write(
624 fm.write(
625 b'diffchar ' + linetype,
625 b'diffchar ' + linetype,
626 b'%s%s\n',
626 b'%s%s\n',
627 diffchar,
627 diffchar,
628 line,
628 line,
629 label=linelabel,
629 label=linelabel,
630 )
630 )
631 fm.data(path=self.path, linetype=linetype)
631 fm.data(path=self.path, linetype=linetype)
632
632
633 for i in pycompat.xrange(a1, a2):
633 for i in pycompat.xrange(a1, a2):
634 writeline(
634 writeline(
635 aidxs[i - a1],
635 aidxs[i - a1],
636 b'-',
636 b'-',
637 trim(alines[i]),
637 trim(alines[i]),
638 b'deleted',
638 b'deleted',
639 b'diff.deleted',
639 b'diff.deleted',
640 )
640 )
641 for i in pycompat.xrange(b1, b2):
641 for i in pycompat.xrange(b1, b2):
642 writeline(
642 writeline(
643 bidxs[i - b1],
643 bidxs[i - b1],
644 b'+',
644 b'+',
645 trim(blines[i]),
645 trim(blines[i]),
646 b'inserted',
646 b'inserted',
647 b'diff.inserted',
647 b'diff.inserted',
648 )
648 )
649
649
650
650
651 class fixupstate(object):
651 class fixupstate(object):
652 """state needed to run absorb
652 """state needed to run absorb
653
653
654 internally, it keeps paths and filefixupstates.
654 internally, it keeps paths and filefixupstates.
655
655
656 a typical use is like filefixupstates:
656 a typical use is like filefixupstates:
657
657
658 1. call diffwith, to calculate fixups
658 1. call diffwith, to calculate fixups
659 2. (optionally), present fixups to the user, or edit fixups
659 2. (optionally), present fixups to the user, or edit fixups
660 3. call apply, to apply changes to memory
660 3. call apply, to apply changes to memory
661 4. call commit, to commit changes to hg database
661 4. call commit, to commit changes to hg database
662 """
662 """
663
663
664 def __init__(self, stack, ui=None, opts=None):
664 def __init__(self, stack, ui=None, opts=None):
665 """([ctx], ui or None) -> None
665 """([ctx], ui or None) -> None
666
666
667 stack: should be linear, and sorted by topo order - oldest first.
667 stack: should be linear, and sorted by topo order - oldest first.
668 all commits in stack are considered mutable.
668 all commits in stack are considered mutable.
669 """
669 """
670 assert stack
670 assert stack
671 self.ui = ui or nullui()
671 self.ui = ui or nullui()
672 self.opts = opts or {}
672 self.opts = opts or {}
673 self.stack = stack
673 self.stack = stack
674 self.repo = stack[-1].repo().unfiltered()
674 self.repo = stack[-1].repo().unfiltered()
675
675
676 # following fields will be filled later
676 # following fields will be filled later
677 self.paths = [] # [str]
677 self.paths = [] # [str]
678 self.status = None # ctx.status output
678 self.status = None # ctx.status output
679 self.fctxmap = {} # {path: {ctx: fctx}}
679 self.fctxmap = {} # {path: {ctx: fctx}}
680 self.fixupmap = {} # {path: filefixupstate}
680 self.fixupmap = {} # {path: filefixupstate}
681 self.replacemap = {} # {oldnode: newnode or None}
681 self.replacemap = {} # {oldnode: newnode or None}
682 self.finalnode = None # head after all fixups
682 self.finalnode = None # head after all fixups
683 self.ctxaffected = set() # ctx that will be absorbed into
683 self.ctxaffected = set() # ctx that will be absorbed into
684
684
685 def diffwith(self, targetctx, match=None, fm=None):
685 def diffwith(self, targetctx, match=None, fm=None):
686 """diff and prepare fixups. update self.fixupmap, self.paths"""
686 """diff and prepare fixups. update self.fixupmap, self.paths"""
687 # only care about modified files
687 # only care about modified files
688 self.status = self.stack[-1].status(targetctx, match)
688 self.status = self.stack[-1].status(targetctx, match)
689 self.paths = []
689 self.paths = []
690 # but if --edit-lines is used, the user may want to edit files
690 # but if --edit-lines is used, the user may want to edit files
691 # even if they are not modified
691 # even if they are not modified
692 editopt = self.opts.get(b'edit_lines')
692 editopt = self.opts.get(b'edit_lines')
693 if not self.status.modified and editopt and match:
693 if not self.status.modified and editopt and match:
694 interestingpaths = match.files()
694 interestingpaths = match.files()
695 else:
695 else:
696 interestingpaths = self.status.modified
696 interestingpaths = self.status.modified
697 # prepare the filefixupstate
697 # prepare the filefixupstate
698 seenfctxs = set()
698 seenfctxs = set()
699 # sorting is necessary to eliminate ambiguity for the "double move"
699 # sorting is necessary to eliminate ambiguity for the "double move"
700 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
700 # case: "hg cp A B; hg cp A C; hg rm A", then only "B" can affect "A".
701 for path in sorted(interestingpaths):
701 for path in sorted(interestingpaths):
702 self.ui.debug(b'calculating fixups for %s\n' % path)
702 self.ui.debug(b'calculating fixups for %s\n' % path)
703 targetfctx = targetctx[path]
703 targetfctx = targetctx[path]
704 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
704 fctxs, ctx2fctx = getfilestack(self.stack, path, seenfctxs)
705 # ignore symbolic links or binary, or unchanged files
705 # ignore symbolic links or binary, or unchanged files
706 if any(
706 if any(
707 f.islink() or stringutil.binary(f.data())
707 f.islink() or stringutil.binary(f.data())
708 for f in [targetfctx] + fctxs
708 for f in [targetfctx] + fctxs
709 if not isinstance(f, emptyfilecontext)
709 if not isinstance(f, emptyfilecontext)
710 ):
710 ):
711 continue
711 continue
712 if targetfctx.data() == fctxs[-1].data() and not editopt:
712 if targetfctx.data() == fctxs[-1].data() and not editopt:
713 continue
713 continue
714 seenfctxs.update(fctxs[1:])
714 seenfctxs.update(fctxs[1:])
715 self.fctxmap[path] = ctx2fctx
715 self.fctxmap[path] = ctx2fctx
716 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
716 fstate = filefixupstate(fctxs, path, ui=self.ui, opts=self.opts)
717 if fm is not None:
717 if fm is not None:
718 fm.startitem()
718 fm.startitem()
719 fm.plain(b'showing changes for ')
719 fm.plain(b'showing changes for ')
720 fm.write(b'path', b'%s\n', path, label=b'absorb.path')
720 fm.write(b'path', b'%s\n', path, label=b'absorb.path')
721 fm.data(linetype=b'path')
721 fm.data(linetype=b'path')
722 fstate.diffwith(targetfctx, fm)
722 fstate.diffwith(targetfctx, fm)
723 self.fixupmap[path] = fstate
723 self.fixupmap[path] = fstate
724 self.paths.append(path)
724 self.paths.append(path)
725 self.ctxaffected.update(fstate.ctxaffected)
725 self.ctxaffected.update(fstate.ctxaffected)
726
726
727 def apply(self):
727 def apply(self):
728 """apply fixups to individual filefixupstates"""
728 """apply fixups to individual filefixupstates"""
729 for path, state in pycompat.iteritems(self.fixupmap):
729 for path, state in pycompat.iteritems(self.fixupmap):
730 if self.ui.debugflag:
730 if self.ui.debugflag:
731 self.ui.write(_(b'applying fixups to %s\n') % path)
731 self.ui.write(_(b'applying fixups to %s\n') % path)
732 state.apply()
732 state.apply()
733
733
734 @property
734 @property
735 def chunkstats(self):
735 def chunkstats(self):
736 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
736 """-> {path: chunkstats}. collect chunkstats from filefixupstates"""
737 return dict(
737 return dict(
738 (path, state.chunkstats)
738 (path, state.chunkstats)
739 for path, state in pycompat.iteritems(self.fixupmap)
739 for path, state in pycompat.iteritems(self.fixupmap)
740 )
740 )
741
741
742 def commit(self):
742 def commit(self):
743 """commit changes. update self.finalnode, self.replacemap"""
743 """commit changes. update self.finalnode, self.replacemap"""
744 with self.repo.transaction(b'absorb') as tr:
744 with self.repo.transaction(b'absorb') as tr:
745 self._commitstack()
745 self._commitstack()
746 self._movebookmarks(tr)
746 self._movebookmarks(tr)
747 if self.repo[b'.'].node() in self.replacemap:
747 if self.repo[b'.'].node() in self.replacemap:
748 self._moveworkingdirectoryparent()
748 self._moveworkingdirectoryparent()
749 self._cleanupoldcommits()
749 self._cleanupoldcommits()
750 return self.finalnode
750 return self.finalnode
751
751
752 def printchunkstats(self):
752 def printchunkstats(self):
753 """print things like '1 of 2 chunk(s) applied'"""
753 """print things like '1 of 2 chunk(s) applied'"""
754 ui = self.ui
754 ui = self.ui
755 chunkstats = self.chunkstats
755 chunkstats = self.chunkstats
756 if ui.verbose:
756 if ui.verbose:
757 # chunkstats for each file
757 # chunkstats for each file
758 for path, stat in pycompat.iteritems(chunkstats):
758 for path, stat in pycompat.iteritems(chunkstats):
759 if stat[0]:
759 if stat[0]:
760 ui.write(
760 ui.write(
761 _(b'%s: %d of %d chunk(s) applied\n')
761 _(b'%s: %d of %d chunk(s) applied\n')
762 % (path, stat[0], stat[1])
762 % (path, stat[0], stat[1])
763 )
763 )
764 elif not ui.quiet:
764 elif not ui.quiet:
765 # a summary for all files
765 # a summary for all files
766 stats = chunkstats.values()
766 stats = chunkstats.values()
767 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
767 applied, total = (sum(s[i] for s in stats) for i in (0, 1))
768 ui.write(_(b'%d of %d chunk(s) applied\n') % (applied, total))
768 ui.write(_(b'%d of %d chunk(s) applied\n') % (applied, total))
769
769
770 def _commitstack(self):
770 def _commitstack(self):
771 """make new commits. update self.finalnode, self.replacemap.
771 """make new commits. update self.finalnode, self.replacemap.
772 it is splitted from "commit" to avoid too much indentation.
772 it is splitted from "commit" to avoid too much indentation.
773 """
773 """
774 # last node (20-char) committed by us
774 # last node (20-char) committed by us
775 lastcommitted = None
775 lastcommitted = None
776 # p1 which overrides the parent of the next commit, "None" means use
776 # p1 which overrides the parent of the next commit, "None" means use
777 # the original parent unchanged
777 # the original parent unchanged
778 nextp1 = None
778 nextp1 = None
779 for ctx in self.stack:
779 for ctx in self.stack:
780 memworkingcopy = self._getnewfilecontents(ctx)
780 memworkingcopy = self._getnewfilecontents(ctx)
781 if not memworkingcopy and not lastcommitted:
781 if not memworkingcopy and not lastcommitted:
782 # nothing changed, nothing commited
782 # nothing changed, nothing commited
783 nextp1 = ctx
783 nextp1 = ctx
784 continue
784 continue
785 if self._willbecomenoop(memworkingcopy, ctx, nextp1):
785 if self._willbecomenoop(memworkingcopy, ctx, nextp1):
786 # changeset is no longer necessary
786 # changeset is no longer necessary
787 self.replacemap[ctx.node()] = None
787 self.replacemap[ctx.node()] = None
788 msg = _(b'became empty and was dropped')
788 msg = _(b'became empty and was dropped')
789 else:
789 else:
790 # changeset needs re-commit
790 # changeset needs re-commit
791 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
791 nodestr = self._commitsingle(memworkingcopy, ctx, p1=nextp1)
792 lastcommitted = self.repo[nodestr]
792 lastcommitted = self.repo[nodestr]
793 nextp1 = lastcommitted
793 nextp1 = lastcommitted
794 self.replacemap[ctx.node()] = lastcommitted.node()
794 self.replacemap[ctx.node()] = lastcommitted.node()
795 if memworkingcopy:
795 if memworkingcopy:
796 msg = _(b'%d file(s) changed, became %s') % (
796 msg = _(b'%d file(s) changed, became %s') % (
797 len(memworkingcopy),
797 len(memworkingcopy),
798 self._ctx2str(lastcommitted),
798 self._ctx2str(lastcommitted),
799 )
799 )
800 else:
800 else:
801 msg = _(b'became %s') % self._ctx2str(lastcommitted)
801 msg = _(b'became %s') % self._ctx2str(lastcommitted)
802 if self.ui.verbose and msg:
802 if self.ui.verbose and msg:
803 self.ui.write(_(b'%s: %s\n') % (self._ctx2str(ctx), msg))
803 self.ui.write(_(b'%s: %s\n') % (self._ctx2str(ctx), msg))
804 self.finalnode = lastcommitted and lastcommitted.node()
804 self.finalnode = lastcommitted and lastcommitted.node()
805
805
806 def _ctx2str(self, ctx):
806 def _ctx2str(self, ctx):
807 if self.ui.debugflag:
807 if self.ui.debugflag:
808 return b'%d:%s' % (ctx.rev(), ctx.hex())
808 return b'%d:%s' % (ctx.rev(), ctx.hex())
809 else:
809 else:
810 return b'%d:%s' % (ctx.rev(), node.short(ctx.node()))
810 return b'%d:%s' % (ctx.rev(), node.short(ctx.node()))
811
811
812 def _getnewfilecontents(self, ctx):
812 def _getnewfilecontents(self, ctx):
813 """(ctx) -> {path: str}
813 """(ctx) -> {path: str}
814
814
815 fetch file contents from filefixupstates.
815 fetch file contents from filefixupstates.
816 return the working copy overrides - files different from ctx.
816 return the working copy overrides - files different from ctx.
817 """
817 """
818 result = {}
818 result = {}
819 for path in self.paths:
819 for path in self.paths:
820 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
820 ctx2fctx = self.fctxmap[path] # {ctx: fctx}
821 if ctx not in ctx2fctx:
821 if ctx not in ctx2fctx:
822 continue
822 continue
823 fctx = ctx2fctx[ctx]
823 fctx = ctx2fctx[ctx]
824 content = fctx.data()
824 content = fctx.data()
825 newcontent = self.fixupmap[path].getfinalcontent(fctx)
825 newcontent = self.fixupmap[path].getfinalcontent(fctx)
826 if content != newcontent:
826 if content != newcontent:
827 result[fctx.path()] = newcontent
827 result[fctx.path()] = newcontent
828 return result
828 return result
829
829
830 def _movebookmarks(self, tr):
830 def _movebookmarks(self, tr):
831 repo = self.repo
831 repo = self.repo
832 needupdate = [
832 needupdate = [
833 (name, self.replacemap[hsh])
833 (name, self.replacemap[hsh])
834 for name, hsh in pycompat.iteritems(repo._bookmarks)
834 for name, hsh in pycompat.iteritems(repo._bookmarks)
835 if hsh in self.replacemap
835 if hsh in self.replacemap
836 ]
836 ]
837 changes = []
837 changes = []
838 for name, hsh in needupdate:
838 for name, hsh in needupdate:
839 if hsh:
839 if hsh:
840 changes.append((name, hsh))
840 changes.append((name, hsh))
841 if self.ui.verbose:
841 if self.ui.verbose:
842 self.ui.write(
842 self.ui.write(
843 _(b'moving bookmark %s to %s\n') % (name, node.hex(hsh))
843 _(b'moving bookmark %s to %s\n') % (name, node.hex(hsh))
844 )
844 )
845 else:
845 else:
846 changes.append((name, None))
846 changes.append((name, None))
847 if self.ui.verbose:
847 if self.ui.verbose:
848 self.ui.write(_(b'deleting bookmark %s\n') % name)
848 self.ui.write(_(b'deleting bookmark %s\n') % name)
849 repo._bookmarks.applychanges(repo, tr, changes)
849 repo._bookmarks.applychanges(repo, tr, changes)
850
850
851 def _moveworkingdirectoryparent(self):
851 def _moveworkingdirectoryparent(self):
852 if not self.finalnode:
852 if not self.finalnode:
853 # Find the latest not-{obsoleted,stripped} parent.
853 # Find the latest not-{obsoleted,stripped} parent.
854 revs = self.repo.revs(b'max(::. - %ln)', self.replacemap.keys())
854 revs = self.repo.revs(b'max(::. - %ln)', self.replacemap.keys())
855 ctx = self.repo[revs.first()]
855 ctx = self.repo[revs.first()]
856 self.finalnode = ctx.node()
856 self.finalnode = ctx.node()
857 else:
857 else:
858 ctx = self.repo[self.finalnode]
858 ctx = self.repo[self.finalnode]
859
859
860 dirstate = self.repo.dirstate
860 dirstate = self.repo.dirstate
861 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
861 # dirstate.rebuild invalidates fsmonitorstate, causing "hg status" to
862 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
862 # be slow. in absorb's case, no need to invalidate fsmonitorstate.
863 noop = lambda: 0
863 noop = lambda: 0
864 restore = noop
864 restore = noop
865 if util.safehasattr(dirstate, b'_fsmonitorstate'):
865 if util.safehasattr(dirstate, '_fsmonitorstate'):
866 bak = dirstate._fsmonitorstate.invalidate
866 bak = dirstate._fsmonitorstate.invalidate
867
867
868 def restore():
868 def restore():
869 dirstate._fsmonitorstate.invalidate = bak
869 dirstate._fsmonitorstate.invalidate = bak
870
870
871 dirstate._fsmonitorstate.invalidate = noop
871 dirstate._fsmonitorstate.invalidate = noop
872 try:
872 try:
873 with dirstate.parentchange():
873 with dirstate.parentchange():
874 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
874 dirstate.rebuild(ctx.node(), ctx.manifest(), self.paths)
875 finally:
875 finally:
876 restore()
876 restore()
877
877
878 @staticmethod
878 @staticmethod
879 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
879 def _willbecomenoop(memworkingcopy, ctx, pctx=None):
880 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
880 """({path: content}, ctx, ctx) -> bool. test if a commit will be noop
881
881
882 if it will become an empty commit (does not change anything, after the
882 if it will become an empty commit (does not change anything, after the
883 memworkingcopy overrides), return True. otherwise return False.
883 memworkingcopy overrides), return True. otherwise return False.
884 """
884 """
885 if not pctx:
885 if not pctx:
886 parents = ctx.parents()
886 parents = ctx.parents()
887 if len(parents) != 1:
887 if len(parents) != 1:
888 return False
888 return False
889 pctx = parents[0]
889 pctx = parents[0]
890 # ctx changes more files (not a subset of memworkingcopy)
890 # ctx changes more files (not a subset of memworkingcopy)
891 if not set(ctx.files()).issubset(set(memworkingcopy)):
891 if not set(ctx.files()).issubset(set(memworkingcopy)):
892 return False
892 return False
893 for path, content in pycompat.iteritems(memworkingcopy):
893 for path, content in pycompat.iteritems(memworkingcopy):
894 if path not in pctx or path not in ctx:
894 if path not in pctx or path not in ctx:
895 return False
895 return False
896 fctx = ctx[path]
896 fctx = ctx[path]
897 pfctx = pctx[path]
897 pfctx = pctx[path]
898 if pfctx.flags() != fctx.flags():
898 if pfctx.flags() != fctx.flags():
899 return False
899 return False
900 if pfctx.data() != content:
900 if pfctx.data() != content:
901 return False
901 return False
902 return True
902 return True
903
903
904 def _commitsingle(self, memworkingcopy, ctx, p1=None):
904 def _commitsingle(self, memworkingcopy, ctx, p1=None):
905 """(ctx, {path: content}, node) -> node. make a single commit
905 """(ctx, {path: content}, node) -> node. make a single commit
906
906
907 the commit is a clone from ctx, with a (optionally) different p1, and
907 the commit is a clone from ctx, with a (optionally) different p1, and
908 different file contents replaced by memworkingcopy.
908 different file contents replaced by memworkingcopy.
909 """
909 """
910 parents = p1 and (p1, node.nullid)
910 parents = p1 and (p1, node.nullid)
911 extra = ctx.extra()
911 extra = ctx.extra()
912 if self._useobsolete and self.ui.configbool(b'absorb', b'add-noise'):
912 if self._useobsolete and self.ui.configbool(b'absorb', b'add-noise'):
913 extra[b'absorb_source'] = ctx.hex()
913 extra[b'absorb_source'] = ctx.hex()
914 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
914 mctx = overlaycontext(memworkingcopy, ctx, parents, extra=extra)
915 return mctx.commit()
915 return mctx.commit()
916
916
917 @util.propertycache
917 @util.propertycache
918 def _useobsolete(self):
918 def _useobsolete(self):
919 """() -> bool"""
919 """() -> bool"""
920 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
920 return obsolete.isenabled(self.repo, obsolete.createmarkersopt)
921
921
922 def _cleanupoldcommits(self):
922 def _cleanupoldcommits(self):
923 replacements = {
923 replacements = {
924 k: ([v] if v is not None else [])
924 k: ([v] if v is not None else [])
925 for k, v in pycompat.iteritems(self.replacemap)
925 for k, v in pycompat.iteritems(self.replacemap)
926 }
926 }
927 if replacements:
927 if replacements:
928 scmutil.cleanupnodes(
928 scmutil.cleanupnodes(
929 self.repo, replacements, operation=b'absorb', fixphase=True
929 self.repo, replacements, operation=b'absorb', fixphase=True
930 )
930 )
931
931
932
932
933 def _parsechunk(hunk):
933 def _parsechunk(hunk):
934 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
934 """(crecord.uihunk or patch.recordhunk) -> (path, (a1, a2, [bline]))"""
935 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
935 if type(hunk) not in (crecord.uihunk, patch.recordhunk):
936 return None, None
936 return None, None
937 path = hunk.header.filename()
937 path = hunk.header.filename()
938 a1 = hunk.fromline + len(hunk.before) - 1
938 a1 = hunk.fromline + len(hunk.before) - 1
939 # remove before and after context
939 # remove before and after context
940 hunk.before = hunk.after = []
940 hunk.before = hunk.after = []
941 buf = util.stringio()
941 buf = util.stringio()
942 hunk.write(buf)
942 hunk.write(buf)
943 patchlines = mdiff.splitnewlines(buf.getvalue())
943 patchlines = mdiff.splitnewlines(buf.getvalue())
944 # hunk.prettystr() will update hunk.removed
944 # hunk.prettystr() will update hunk.removed
945 a2 = a1 + hunk.removed
945 a2 = a1 + hunk.removed
946 blines = [l[1:] for l in patchlines[1:] if not l.startswith(b'-')]
946 blines = [l[1:] for l in patchlines[1:] if not l.startswith(b'-')]
947 return path, (a1, a2, blines)
947 return path, (a1, a2, blines)
948
948
949
949
950 def overlaydiffcontext(ctx, chunks):
950 def overlaydiffcontext(ctx, chunks):
951 """(ctx, [crecord.uihunk]) -> memctx
951 """(ctx, [crecord.uihunk]) -> memctx
952
952
953 return a memctx with some [1] patches (chunks) applied to ctx.
953 return a memctx with some [1] patches (chunks) applied to ctx.
954 [1]: modifications are handled. renames, mode changes, etc. are ignored.
954 [1]: modifications are handled. renames, mode changes, etc. are ignored.
955 """
955 """
956 # sadly the applying-patch logic is hardly reusable, and messy:
956 # sadly the applying-patch logic is hardly reusable, and messy:
957 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
957 # 1. the core logic "_applydiff" is too heavy - it writes .rej files, it
958 # needs a file stream of a patch and will re-parse it, while we have
958 # needs a file stream of a patch and will re-parse it, while we have
959 # structured hunk objects at hand.
959 # structured hunk objects at hand.
960 # 2. a lot of different implementations about "chunk" (patch.hunk,
960 # 2. a lot of different implementations about "chunk" (patch.hunk,
961 # patch.recordhunk, crecord.uihunk)
961 # patch.recordhunk, crecord.uihunk)
962 # as we only care about applying changes to modified files, no mode
962 # as we only care about applying changes to modified files, no mode
963 # change, no binary diff, and no renames, it's probably okay to
963 # change, no binary diff, and no renames, it's probably okay to
964 # re-invent the logic using much simpler code here.
964 # re-invent the logic using much simpler code here.
965 memworkingcopy = {} # {path: content}
965 memworkingcopy = {} # {path: content}
966 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
966 patchmap = defaultdict(lambda: []) # {path: [(a1, a2, [bline])]}
967 for path, info in map(_parsechunk, chunks):
967 for path, info in map(_parsechunk, chunks):
968 if not path or not info:
968 if not path or not info:
969 continue
969 continue
970 patchmap[path].append(info)
970 patchmap[path].append(info)
971 for path, patches in pycompat.iteritems(patchmap):
971 for path, patches in pycompat.iteritems(patchmap):
972 if path not in ctx or not patches:
972 if path not in ctx or not patches:
973 continue
973 continue
974 patches.sort(reverse=True)
974 patches.sort(reverse=True)
975 lines = mdiff.splitnewlines(ctx[path].data())
975 lines = mdiff.splitnewlines(ctx[path].data())
976 for a1, a2, blines in patches:
976 for a1, a2, blines in patches:
977 lines[a1:a2] = blines
977 lines[a1:a2] = blines
978 memworkingcopy[path] = b''.join(lines)
978 memworkingcopy[path] = b''.join(lines)
979 return overlaycontext(memworkingcopy, ctx)
979 return overlaycontext(memworkingcopy, ctx)
980
980
981
981
982 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
982 def absorb(ui, repo, stack=None, targetctx=None, pats=None, opts=None):
983 """pick fixup chunks from targetctx, apply them to stack.
983 """pick fixup chunks from targetctx, apply them to stack.
984
984
985 if targetctx is None, the working copy context will be used.
985 if targetctx is None, the working copy context will be used.
986 if stack is None, the current draft stack will be used.
986 if stack is None, the current draft stack will be used.
987 return fixupstate.
987 return fixupstate.
988 """
988 """
989 if stack is None:
989 if stack is None:
990 limit = ui.configint(b'absorb', b'max-stack-size')
990 limit = ui.configint(b'absorb', b'max-stack-size')
991 headctx = repo[b'.']
991 headctx = repo[b'.']
992 if len(headctx.parents()) > 1:
992 if len(headctx.parents()) > 1:
993 raise error.Abort(_(b'cannot absorb into a merge'))
993 raise error.Abort(_(b'cannot absorb into a merge'))
994 stack = getdraftstack(headctx, limit)
994 stack = getdraftstack(headctx, limit)
995 if limit and len(stack) >= limit:
995 if limit and len(stack) >= limit:
996 ui.warn(
996 ui.warn(
997 _(
997 _(
998 b'absorb: only the recent %d changesets will '
998 b'absorb: only the recent %d changesets will '
999 b'be analysed\n'
999 b'be analysed\n'
1000 )
1000 )
1001 % limit
1001 % limit
1002 )
1002 )
1003 if not stack:
1003 if not stack:
1004 raise error.Abort(_(b'no mutable changeset to change'))
1004 raise error.Abort(_(b'no mutable changeset to change'))
1005 if targetctx is None: # default to working copy
1005 if targetctx is None: # default to working copy
1006 targetctx = repo[None]
1006 targetctx = repo[None]
1007 if pats is None:
1007 if pats is None:
1008 pats = ()
1008 pats = ()
1009 if opts is None:
1009 if opts is None:
1010 opts = {}
1010 opts = {}
1011 state = fixupstate(stack, ui=ui, opts=opts)
1011 state = fixupstate(stack, ui=ui, opts=opts)
1012 matcher = scmutil.match(targetctx, pats, opts)
1012 matcher = scmutil.match(targetctx, pats, opts)
1013 if opts.get(b'interactive'):
1013 if opts.get(b'interactive'):
1014 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
1014 diff = patch.diff(repo, stack[-1].node(), targetctx.node(), matcher)
1015 origchunks = patch.parsepatch(diff)
1015 origchunks = patch.parsepatch(diff)
1016 chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
1016 chunks = cmdutil.recordfilter(ui, origchunks, matcher)[0]
1017 targetctx = overlaydiffcontext(stack[-1], chunks)
1017 targetctx = overlaydiffcontext(stack[-1], chunks)
1018 fm = None
1018 fm = None
1019 if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
1019 if opts.get(b'print_changes') or not opts.get(b'apply_changes'):
1020 fm = ui.formatter(b'absorb', opts)
1020 fm = ui.formatter(b'absorb', opts)
1021 state.diffwith(targetctx, matcher, fm)
1021 state.diffwith(targetctx, matcher, fm)
1022 if fm is not None:
1022 if fm is not None:
1023 fm.startitem()
1023 fm.startitem()
1024 fm.write(
1024 fm.write(
1025 b"count", b"\n%d changesets affected\n", len(state.ctxaffected)
1025 b"count", b"\n%d changesets affected\n", len(state.ctxaffected)
1026 )
1026 )
1027 fm.data(linetype=b'summary')
1027 fm.data(linetype=b'summary')
1028 for ctx in reversed(stack):
1028 for ctx in reversed(stack):
1029 if ctx not in state.ctxaffected:
1029 if ctx not in state.ctxaffected:
1030 continue
1030 continue
1031 fm.startitem()
1031 fm.startitem()
1032 fm.context(ctx=ctx)
1032 fm.context(ctx=ctx)
1033 fm.data(linetype=b'changeset')
1033 fm.data(linetype=b'changeset')
1034 fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
1034 fm.write(b'node', b'%-7.7s ', ctx.hex(), label=b'absorb.node')
1035 descfirstline = ctx.description().splitlines()[0]
1035 descfirstline = ctx.description().splitlines()[0]
1036 fm.write(
1036 fm.write(
1037 b'descfirstline',
1037 b'descfirstline',
1038 b'%s\n',
1038 b'%s\n',
1039 descfirstline,
1039 descfirstline,
1040 label=b'absorb.description',
1040 label=b'absorb.description',
1041 )
1041 )
1042 fm.end()
1042 fm.end()
1043 if not opts.get(b'dry_run'):
1043 if not opts.get(b'dry_run'):
1044 if (
1044 if (
1045 not opts.get(b'apply_changes')
1045 not opts.get(b'apply_changes')
1046 and state.ctxaffected
1046 and state.ctxaffected
1047 and ui.promptchoice(
1047 and ui.promptchoice(
1048 b"apply changes (yn)? $$ &Yes $$ &No", default=1
1048 b"apply changes (yn)? $$ &Yes $$ &No", default=1
1049 )
1049 )
1050 ):
1050 ):
1051 raise error.Abort(_(b'absorb cancelled\n'))
1051 raise error.Abort(_(b'absorb cancelled\n'))
1052
1052
1053 state.apply()
1053 state.apply()
1054 if state.commit():
1054 if state.commit():
1055 state.printchunkstats()
1055 state.printchunkstats()
1056 elif not ui.quiet:
1056 elif not ui.quiet:
1057 ui.write(_(b'nothing applied\n'))
1057 ui.write(_(b'nothing applied\n'))
1058 return state
1058 return state
1059
1059
1060
1060
1061 @command(
1061 @command(
1062 b'absorb',
1062 b'absorb',
1063 [
1063 [
1064 (
1064 (
1065 b'a',
1065 b'a',
1066 b'apply-changes',
1066 b'apply-changes',
1067 None,
1067 None,
1068 _(b'apply changes without prompting for confirmation'),
1068 _(b'apply changes without prompting for confirmation'),
1069 ),
1069 ),
1070 (
1070 (
1071 b'p',
1071 b'p',
1072 b'print-changes',
1072 b'print-changes',
1073 None,
1073 None,
1074 _(b'always print which changesets are modified by which changes'),
1074 _(b'always print which changesets are modified by which changes'),
1075 ),
1075 ),
1076 (
1076 (
1077 b'i',
1077 b'i',
1078 b'interactive',
1078 b'interactive',
1079 None,
1079 None,
1080 _(b'interactively select which chunks to apply (EXPERIMENTAL)'),
1080 _(b'interactively select which chunks to apply (EXPERIMENTAL)'),
1081 ),
1081 ),
1082 (
1082 (
1083 b'e',
1083 b'e',
1084 b'edit-lines',
1084 b'edit-lines',
1085 None,
1085 None,
1086 _(
1086 _(
1087 b'edit what lines belong to which changesets before commit '
1087 b'edit what lines belong to which changesets before commit '
1088 b'(EXPERIMENTAL)'
1088 b'(EXPERIMENTAL)'
1089 ),
1089 ),
1090 ),
1090 ),
1091 ]
1091 ]
1092 + commands.dryrunopts
1092 + commands.dryrunopts
1093 + commands.templateopts
1093 + commands.templateopts
1094 + commands.walkopts,
1094 + commands.walkopts,
1095 _(b'hg absorb [OPTION] [FILE]...'),
1095 _(b'hg absorb [OPTION] [FILE]...'),
1096 helpcategory=command.CATEGORY_COMMITTING,
1096 helpcategory=command.CATEGORY_COMMITTING,
1097 helpbasic=True,
1097 helpbasic=True,
1098 )
1098 )
1099 def absorbcmd(ui, repo, *pats, **opts):
1099 def absorbcmd(ui, repo, *pats, **opts):
1100 """incorporate corrections into the stack of draft changesets
1100 """incorporate corrections into the stack of draft changesets
1101
1101
1102 absorb analyzes each change in your working directory and attempts to
1102 absorb analyzes each change in your working directory and attempts to
1103 amend the changed lines into the changesets in your stack that first
1103 amend the changed lines into the changesets in your stack that first
1104 introduced those lines.
1104 introduced those lines.
1105
1105
1106 If absorb cannot find an unambiguous changeset to amend for a change,
1106 If absorb cannot find an unambiguous changeset to amend for a change,
1107 that change will be left in the working directory, untouched. They can be
1107 that change will be left in the working directory, untouched. They can be
1108 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1108 observed by :hg:`status` or :hg:`diff` afterwards. In other words,
1109 absorb does not write to the working directory.
1109 absorb does not write to the working directory.
1110
1110
1111 Changesets outside the revset `::. and not public() and not merge()` will
1111 Changesets outside the revset `::. and not public() and not merge()` will
1112 not be changed.
1112 not be changed.
1113
1113
1114 Changesets that become empty after applying the changes will be deleted.
1114 Changesets that become empty after applying the changes will be deleted.
1115
1115
1116 By default, absorb will show what it plans to do and prompt for
1116 By default, absorb will show what it plans to do and prompt for
1117 confirmation. If you are confident that the changes will be absorbed
1117 confirmation. If you are confident that the changes will be absorbed
1118 to the correct place, run :hg:`absorb -a` to apply the changes
1118 to the correct place, run :hg:`absorb -a` to apply the changes
1119 immediately.
1119 immediately.
1120
1120
1121 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1121 Returns 0 on success, 1 if all chunks were ignored and nothing amended.
1122 """
1122 """
1123 opts = pycompat.byteskwargs(opts)
1123 opts = pycompat.byteskwargs(opts)
1124
1124
1125 with repo.wlock(), repo.lock():
1125 with repo.wlock(), repo.lock():
1126 if not opts[b'dry_run']:
1126 if not opts[b'dry_run']:
1127 cmdutil.checkunfinished(repo)
1127 cmdutil.checkunfinished(repo)
1128
1128
1129 state = absorb(ui, repo, pats=pats, opts=opts)
1129 state = absorb(ui, repo, pats=pats, opts=opts)
1130 if sum(s[0] for s in state.chunkstats.values()) == 0:
1130 if sum(s[0] for s in state.chunkstats.values()) == 0:
1131 return 1
1131 return 1
@@ -1,1215 +1,1215 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''hooks for integrating with the Bugzilla bug tracker
9 '''hooks for integrating with the Bugzilla bug tracker
10
10
11 This hook extension adds comments on bugs in Bugzilla when changesets
11 This hook extension adds comments on bugs in Bugzilla when changesets
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 the Mercurial template mechanism.
13 the Mercurial template mechanism.
14
14
15 The bug references can optionally include an update for Bugzilla of the
15 The bug references can optionally include an update for Bugzilla of the
16 hours spent working on the bug. Bugs can also be marked fixed.
16 hours spent working on the bug. Bugs can also be marked fixed.
17
17
18 Four basic modes of access to Bugzilla are provided:
18 Four basic modes of access to Bugzilla are provided:
19
19
20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21
21
22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23
23
24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26
26
27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 using MySQL are supported. Requires Python MySQLdb.
28 using MySQL are supported. Requires Python MySQLdb.
29
29
30 Writing directly to the database is susceptible to schema changes, and
30 Writing directly to the database is susceptible to schema changes, and
31 relies on a Bugzilla contrib script to send out bug change
31 relies on a Bugzilla contrib script to send out bug change
32 notification emails. This script runs as the user running Mercurial,
32 notification emails. This script runs as the user running Mercurial,
33 must be run on the host with the Bugzilla install, and requires
33 must be run on the host with the Bugzilla install, and requires
34 permission to read Bugzilla configuration details and the necessary
34 permission to read Bugzilla configuration details and the necessary
35 MySQL user and password to have full access rights to the Bugzilla
35 MySQL user and password to have full access rights to the Bugzilla
36 database. For these reasons this access mode is now considered
36 database. For these reasons this access mode is now considered
37 deprecated, and will not be updated for new Bugzilla versions going
37 deprecated, and will not be updated for new Bugzilla versions going
38 forward. Only adding comments is supported in this access mode.
38 forward. Only adding comments is supported in this access mode.
39
39
40 Access via XMLRPC needs a Bugzilla username and password to be specified
40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 in the configuration. Comments are added under that username. Since the
41 in the configuration. Comments are added under that username. Since the
42 configuration must be readable by all Mercurial users, it is recommended
42 configuration must be readable by all Mercurial users, it is recommended
43 that the rights of that user are restricted in Bugzilla to the minimum
43 that the rights of that user are restricted in Bugzilla to the minimum
44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45
45
46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 email to the Bugzilla email interface to submit comments to bugs.
47 email to the Bugzilla email interface to submit comments to bugs.
48 The From: address in the email is set to the email address of the Mercurial
48 The From: address in the email is set to the email address of the Mercurial
49 user, so the comment appears to come from the Mercurial user. In the event
49 user, so the comment appears to come from the Mercurial user. In the event
50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 user, the email associated with the Bugzilla username used to log into
51 user, the email associated with the Bugzilla username used to log into
52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 works on all supported Bugzilla versions.
53 works on all supported Bugzilla versions.
54
54
55 Access via the REST-API needs either a Bugzilla username and password
55 Access via the REST-API needs either a Bugzilla username and password
56 or an apikey specified in the configuration. Comments are made under
56 or an apikey specified in the configuration. Comments are made under
57 the given username or the user associated with the apikey in Bugzilla.
57 the given username or the user associated with the apikey in Bugzilla.
58
58
59 Configuration items common to all access modes:
59 Configuration items common to all access modes:
60
60
61 bugzilla.version
61 bugzilla.version
62 The access type to use. Values recognized are:
62 The access type to use. Values recognized are:
63
63
64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 :``xmlrpc``: Bugzilla XMLRPC interface.
65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 including 3.0.
69 including 3.0.
70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 including 2.18.
71 including 2.18.
72
72
73 bugzilla.regexp
73 bugzilla.regexp
74 Regular expression to match bug IDs for update in changeset commit message.
74 Regular expression to match bug IDs for update in changeset commit message.
75 It must contain one "()" named group ``<ids>`` containing the bug
75 It must contain one "()" named group ``<ids>`` containing the bug
76 IDs separated by non-digit characters. It may also contain
76 IDs separated by non-digit characters. It may also contain
77 a named group ``<hours>`` with a floating-point number giving the
77 a named group ``<hours>`` with a floating-point number giving the
78 hours worked on the bug. If no named groups are present, the first
78 hours worked on the bug. If no named groups are present, the first
79 "()" group is assumed to contain the bug IDs, and work time is not
79 "()" group is assumed to contain the bug IDs, and work time is not
80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 variations thereof, followed by an hours number prefixed by ``h`` or
82 variations thereof, followed by an hours number prefixed by ``h`` or
83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84
84
85 bugzilla.fixregexp
85 bugzilla.fixregexp
86 Regular expression to match bug IDs for marking fixed in changeset
86 Regular expression to match bug IDs for marking fixed in changeset
87 commit message. This must contain a "()" named group ``<ids>` containing
87 commit message. This must contain a "()" named group ``<ids>` containing
88 the bug IDs separated by non-digit characters. It may also contain
88 the bug IDs separated by non-digit characters. It may also contain
89 a named group ``<hours>`` with a floating-point number giving the
89 a named group ``<hours>`` with a floating-point number giving the
90 hours worked on the bug. If no named groups are present, the first
90 hours worked on the bug. If no named groups are present, the first
91 "()" group is assumed to contain the bug IDs, and work time is not
91 "()" group is assumed to contain the bug IDs, and work time is not
92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 variations thereof, followed by an hours number prefixed by ``h`` or
94 variations thereof, followed by an hours number prefixed by ``h`` or
95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96
96
97 bugzilla.fixstatus
97 bugzilla.fixstatus
98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99
99
100 bugzilla.fixresolution
100 bugzilla.fixresolution
101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102
102
103 bugzilla.style
103 bugzilla.style
104 The style file to use when formatting comments.
104 The style file to use when formatting comments.
105
105
106 bugzilla.template
106 bugzilla.template
107 Template to use when formatting comments. Overrides style if
107 Template to use when formatting comments. Overrides style if
108 specified. In addition to the usual Mercurial keywords, the
108 specified. In addition to the usual Mercurial keywords, the
109 extension specifies:
109 extension specifies:
110
110
111 :``{bug}``: The Bugzilla bug ID.
111 :``{bug}``: The Bugzilla bug ID.
112 :``{root}``: The full pathname of the Mercurial repository.
112 :``{root}``: The full pathname of the Mercurial repository.
113 :``{webroot}``: Stripped pathname of the Mercurial repository.
113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115
115
116 Default ``changeset {node|short} in repo {root} refers to bug
116 Default ``changeset {node|short} in repo {root} refers to bug
117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118
118
119 bugzilla.strip
119 bugzilla.strip
120 The number of path separator characters to strip from the front of
120 The number of path separator characters to strip from the front of
121 the Mercurial repository path (``{root}`` in templates) to produce
121 the Mercurial repository path (``{root}`` in templates) to produce
122 ``{webroot}``. For example, a repository with ``{root}``
122 ``{webroot}``. For example, a repository with ``{root}``
123 ``/var/local/my-project`` with a strip of 2 gives a value for
123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 ``{webroot}`` of ``my-project``. Default 0.
124 ``{webroot}`` of ``my-project``. Default 0.
125
125
126 web.baseurl
126 web.baseurl
127 Base URL for browsing Mercurial repositories. Referenced from
127 Base URL for browsing Mercurial repositories. Referenced from
128 templates as ``{hgweb}``.
128 templates as ``{hgweb}``.
129
129
130 Configuration items common to XMLRPC+email and MySQL access modes:
130 Configuration items common to XMLRPC+email and MySQL access modes:
131
131
132 bugzilla.usermap
132 bugzilla.usermap
133 Path of file containing Mercurial committer email to Bugzilla user email
133 Path of file containing Mercurial committer email to Bugzilla user email
134 mappings. If specified, the file should contain one mapping per
134 mappings. If specified, the file should contain one mapping per
135 line::
135 line::
136
136
137 committer = Bugzilla user
137 committer = Bugzilla user
138
138
139 See also the ``[usermap]`` section.
139 See also the ``[usermap]`` section.
140
140
141 The ``[usermap]`` section is used to specify mappings of Mercurial
141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 Contains entries of the form ``committer = Bugzilla user``.
143 Contains entries of the form ``committer = Bugzilla user``.
144
144
145 XMLRPC and REST-API access mode configuration:
145 XMLRPC and REST-API access mode configuration:
146
146
147 bugzilla.bzurl
147 bugzilla.bzurl
148 The base URL for the Bugzilla installation.
148 The base URL for the Bugzilla installation.
149 Default ``http://localhost/bugzilla``.
149 Default ``http://localhost/bugzilla``.
150
150
151 bugzilla.user
151 bugzilla.user
152 The username to use to log into Bugzilla via XMLRPC. Default
152 The username to use to log into Bugzilla via XMLRPC. Default
153 ``bugs``.
153 ``bugs``.
154
154
155 bugzilla.password
155 bugzilla.password
156 The password for Bugzilla login.
156 The password for Bugzilla login.
157
157
158 REST-API access mode uses the options listed above as well as:
158 REST-API access mode uses the options listed above as well as:
159
159
160 bugzilla.apikey
160 bugzilla.apikey
161 An apikey generated on the Bugzilla instance for api access.
161 An apikey generated on the Bugzilla instance for api access.
162 Using an apikey removes the need to store the user and password
162 Using an apikey removes the need to store the user and password
163 options.
163 options.
164
164
165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 and also:
166 and also:
167
167
168 bugzilla.bzemail
168 bugzilla.bzemail
169 The Bugzilla email address.
169 The Bugzilla email address.
170
170
171 In addition, the Mercurial email settings must be configured. See the
171 In addition, the Mercurial email settings must be configured. See the
172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173
173
174 MySQL access mode configuration:
174 MySQL access mode configuration:
175
175
176 bugzilla.host
176 bugzilla.host
177 Hostname of the MySQL server holding the Bugzilla database.
177 Hostname of the MySQL server holding the Bugzilla database.
178 Default ``localhost``.
178 Default ``localhost``.
179
179
180 bugzilla.db
180 bugzilla.db
181 Name of the Bugzilla database in MySQL. Default ``bugs``.
181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182
182
183 bugzilla.user
183 bugzilla.user
184 Username to use to access MySQL server. Default ``bugs``.
184 Username to use to access MySQL server. Default ``bugs``.
185
185
186 bugzilla.password
186 bugzilla.password
187 Password to use to access MySQL server.
187 Password to use to access MySQL server.
188
188
189 bugzilla.timeout
189 bugzilla.timeout
190 Database connection timeout (seconds). Default 5.
190 Database connection timeout (seconds). Default 5.
191
191
192 bugzilla.bzuser
192 bugzilla.bzuser
193 Fallback Bugzilla user name to record comments with, if changeset
193 Fallback Bugzilla user name to record comments with, if changeset
194 committer cannot be found as a Bugzilla user.
194 committer cannot be found as a Bugzilla user.
195
195
196 bugzilla.bzdir
196 bugzilla.bzdir
197 Bugzilla install directory. Used by default notify. Default
197 Bugzilla install directory. Used by default notify. Default
198 ``/var/www/html/bugzilla``.
198 ``/var/www/html/bugzilla``.
199
199
200 bugzilla.notify
200 bugzilla.notify
201 The command to run to get Bugzilla to send bug change notification
201 The command to run to get Bugzilla to send bug change notification
202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 id) and ``user`` (committer bugzilla email). Default depends on
203 id) and ``user`` (committer bugzilla email). Default depends on
204 version; from 2.18 it is "cd %(bzdir)s && perl -T
204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 contrib/sendbugmail.pl %(id)s %(user)s".
205 contrib/sendbugmail.pl %(id)s %(user)s".
206
206
207 Activating the extension::
207 Activating the extension::
208
208
209 [extensions]
209 [extensions]
210 bugzilla =
210 bugzilla =
211
211
212 [hooks]
212 [hooks]
213 # run bugzilla hook on every change pulled or pushed in here
213 # run bugzilla hook on every change pulled or pushed in here
214 incoming.bugzilla = python:hgext.bugzilla.hook
214 incoming.bugzilla = python:hgext.bugzilla.hook
215
215
216 Example configurations:
216 Example configurations:
217
217
218 XMLRPC example configuration. This uses the Bugzilla at
218 XMLRPC example configuration. This uses the Bugzilla at
219 ``http://my-project.org/bugzilla``, logging in as user
219 ``http://my-project.org/bugzilla``, logging in as user
220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 with a web interface at ``http://my-project.org/hg``. ::
222 with a web interface at ``http://my-project.org/hg``. ::
223
223
224 [bugzilla]
224 [bugzilla]
225 bzurl=http://my-project.org/bugzilla
225 bzurl=http://my-project.org/bugzilla
226 user=bugmail@my-project.org
226 user=bugmail@my-project.org
227 password=plugh
227 password=plugh
228 version=xmlrpc
228 version=xmlrpc
229 template=Changeset {node|short} in {root|basename}.
229 template=Changeset {node|short} in {root|basename}.
230 {hgweb}/{webroot}/rev/{node|short}\\n
230 {hgweb}/{webroot}/rev/{node|short}\\n
231 {desc}\\n
231 {desc}\\n
232 strip=5
232 strip=5
233
233
234 [web]
234 [web]
235 baseurl=http://my-project.org/hg
235 baseurl=http://my-project.org/hg
236
236
237 XMLRPC+email example configuration. This uses the Bugzilla at
237 XMLRPC+email example configuration. This uses the Bugzilla at
238 ``http://my-project.org/bugzilla``, logging in as user
238 ``http://my-project.org/bugzilla``, logging in as user
239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 with a web interface at ``http://my-project.org/hg``. Bug comments
241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 are sent to the Bugzilla email address
242 are sent to the Bugzilla email address
243 ``bugzilla@my-project.org``. ::
243 ``bugzilla@my-project.org``. ::
244
244
245 [bugzilla]
245 [bugzilla]
246 bzurl=http://my-project.org/bugzilla
246 bzurl=http://my-project.org/bugzilla
247 user=bugmail@my-project.org
247 user=bugmail@my-project.org
248 password=plugh
248 password=plugh
249 version=xmlrpc+email
249 version=xmlrpc+email
250 bzemail=bugzilla@my-project.org
250 bzemail=bugzilla@my-project.org
251 template=Changeset {node|short} in {root|basename}.
251 template=Changeset {node|short} in {root|basename}.
252 {hgweb}/{webroot}/rev/{node|short}\\n
252 {hgweb}/{webroot}/rev/{node|short}\\n
253 {desc}\\n
253 {desc}\\n
254 strip=5
254 strip=5
255
255
256 [web]
256 [web]
257 baseurl=http://my-project.org/hg
257 baseurl=http://my-project.org/hg
258
258
259 [usermap]
259 [usermap]
260 user@emaildomain.com=user.name@bugzilladomain.com
260 user@emaildomain.com=user.name@bugzilladomain.com
261
261
262 MySQL example configuration. This has a local Bugzilla 3.2 installation
262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 the Bugzilla database name is ``bugs`` and MySQL is
264 the Bugzilla database name is ``bugs`` and MySQL is
265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 with a web interface at ``http://my-project.org/hg``. ::
267 with a web interface at ``http://my-project.org/hg``. ::
268
268
269 [bugzilla]
269 [bugzilla]
270 host=localhost
270 host=localhost
271 password=XYZZY
271 password=XYZZY
272 version=3.0
272 version=3.0
273 bzuser=unknown@domain.com
273 bzuser=unknown@domain.com
274 bzdir=/opt/bugzilla-3.2
274 bzdir=/opt/bugzilla-3.2
275 template=Changeset {node|short} in {root|basename}.
275 template=Changeset {node|short} in {root|basename}.
276 {hgweb}/{webroot}/rev/{node|short}\\n
276 {hgweb}/{webroot}/rev/{node|short}\\n
277 {desc}\\n
277 {desc}\\n
278 strip=5
278 strip=5
279
279
280 [web]
280 [web]
281 baseurl=http://my-project.org/hg
281 baseurl=http://my-project.org/hg
282
282
283 [usermap]
283 [usermap]
284 user@emaildomain.com=user.name@bugzilladomain.com
284 user@emaildomain.com=user.name@bugzilladomain.com
285
285
286 All the above add a comment to the Bugzilla bug record of the form::
286 All the above add a comment to the Bugzilla bug record of the form::
287
287
288 Changeset 3b16791d6642 in repository-name.
288 Changeset 3b16791d6642 in repository-name.
289 http://my-project.org/hg/repository-name/rev/3b16791d6642
289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290
290
291 Changeset commit comment. Bug 1234.
291 Changeset commit comment. Bug 1234.
292 '''
292 '''
293
293
294 from __future__ import absolute_import
294 from __future__ import absolute_import
295
295
296 import json
296 import json
297 import re
297 import re
298 import time
298 import time
299
299
300 from mercurial.i18n import _
300 from mercurial.i18n import _
301 from mercurial.node import short
301 from mercurial.node import short
302 from mercurial import (
302 from mercurial import (
303 error,
303 error,
304 logcmdutil,
304 logcmdutil,
305 mail,
305 mail,
306 pycompat,
306 pycompat,
307 registrar,
307 registrar,
308 url,
308 url,
309 util,
309 util,
310 )
310 )
311 from mercurial.utils import (
311 from mercurial.utils import (
312 procutil,
312 procutil,
313 stringutil,
313 stringutil,
314 )
314 )
315
315
316 xmlrpclib = util.xmlrpclib
316 xmlrpclib = util.xmlrpclib
317
317
318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
320 # be specifying the version(s) of Mercurial they are tested with, or
320 # be specifying the version(s) of Mercurial they are tested with, or
321 # leave the attribute unspecified.
321 # leave the attribute unspecified.
322 testedwith = b'ships-with-hg-core'
322 testedwith = b'ships-with-hg-core'
323
323
324 configtable = {}
324 configtable = {}
325 configitem = registrar.configitem(configtable)
325 configitem = registrar.configitem(configtable)
326
326
327 configitem(
327 configitem(
328 b'bugzilla', b'apikey', default=b'',
328 b'bugzilla', b'apikey', default=b'',
329 )
329 )
330 configitem(
330 configitem(
331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
332 )
332 )
333 configitem(
333 configitem(
334 b'bugzilla', b'bzemail', default=None,
334 b'bugzilla', b'bzemail', default=None,
335 )
335 )
336 configitem(
336 configitem(
337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
338 )
338 )
339 configitem(
339 configitem(
340 b'bugzilla', b'bzuser', default=None,
340 b'bugzilla', b'bzuser', default=None,
341 )
341 )
342 configitem(
342 configitem(
343 b'bugzilla', b'db', default=b'bugs',
343 b'bugzilla', b'db', default=b'bugs',
344 )
344 )
345 configitem(
345 configitem(
346 b'bugzilla',
346 b'bugzilla',
347 b'fixregexp',
347 b'fixregexp',
348 default=(
348 default=(
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
353 ),
353 ),
354 )
354 )
355 configitem(
355 configitem(
356 b'bugzilla', b'fixresolution', default=b'FIXED',
356 b'bugzilla', b'fixresolution', default=b'FIXED',
357 )
357 )
358 configitem(
358 configitem(
359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
360 )
360 )
361 configitem(
361 configitem(
362 b'bugzilla', b'host', default=b'localhost',
362 b'bugzilla', b'host', default=b'localhost',
363 )
363 )
364 configitem(
364 configitem(
365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
366 )
366 )
367 configitem(
367 configitem(
368 b'bugzilla', b'password', default=None,
368 b'bugzilla', b'password', default=None,
369 )
369 )
370 configitem(
370 configitem(
371 b'bugzilla',
371 b'bugzilla',
372 b'regexp',
372 b'regexp',
373 default=(
373 default=(
374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
377 ),
377 ),
378 )
378 )
379 configitem(
379 configitem(
380 b'bugzilla', b'strip', default=0,
380 b'bugzilla', b'strip', default=0,
381 )
381 )
382 configitem(
382 configitem(
383 b'bugzilla', b'style', default=None,
383 b'bugzilla', b'style', default=None,
384 )
384 )
385 configitem(
385 configitem(
386 b'bugzilla', b'template', default=None,
386 b'bugzilla', b'template', default=None,
387 )
387 )
388 configitem(
388 configitem(
389 b'bugzilla', b'timeout', default=5,
389 b'bugzilla', b'timeout', default=5,
390 )
390 )
391 configitem(
391 configitem(
392 b'bugzilla', b'user', default=b'bugs',
392 b'bugzilla', b'user', default=b'bugs',
393 )
393 )
394 configitem(
394 configitem(
395 b'bugzilla', b'usermap', default=None,
395 b'bugzilla', b'usermap', default=None,
396 )
396 )
397 configitem(
397 configitem(
398 b'bugzilla', b'version', default=None,
398 b'bugzilla', b'version', default=None,
399 )
399 )
400
400
401
401
402 class bzaccess(object):
402 class bzaccess(object):
403 '''Base class for access to Bugzilla.'''
403 '''Base class for access to Bugzilla.'''
404
404
405 def __init__(self, ui):
405 def __init__(self, ui):
406 self.ui = ui
406 self.ui = ui
407 usermap = self.ui.config(b'bugzilla', b'usermap')
407 usermap = self.ui.config(b'bugzilla', b'usermap')
408 if usermap:
408 if usermap:
409 self.ui.readconfig(usermap, sections=[b'usermap'])
409 self.ui.readconfig(usermap, sections=[b'usermap'])
410
410
411 def map_committer(self, user):
411 def map_committer(self, user):
412 '''map name of committer to Bugzilla user name.'''
412 '''map name of committer to Bugzilla user name.'''
413 for committer, bzuser in self.ui.configitems(b'usermap'):
413 for committer, bzuser in self.ui.configitems(b'usermap'):
414 if committer.lower() == user.lower():
414 if committer.lower() == user.lower():
415 return bzuser
415 return bzuser
416 return user
416 return user
417
417
418 # Methods to be implemented by access classes.
418 # Methods to be implemented by access classes.
419 #
419 #
420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
421 # updates to bug state. Recognized dict keys are:
421 # updates to bug state. Recognized dict keys are:
422 #
422 #
423 # 'hours': Value, float containing work hours to be updated.
423 # 'hours': Value, float containing work hours to be updated.
424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
425
425
426 def filter_real_bug_ids(self, bugs):
426 def filter_real_bug_ids(self, bugs):
427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
428
428
429 def filter_cset_known_bug_ids(self, node, bugs):
429 def filter_cset_known_bug_ids(self, node, bugs):
430 '''remove bug IDs where node occurs in comment text from bugs.'''
430 '''remove bug IDs where node occurs in comment text from bugs.'''
431
431
432 def updatebug(self, bugid, newstate, text, committer):
432 def updatebug(self, bugid, newstate, text, committer):
433 '''update the specified bug. Add comment text and set new states.
433 '''update the specified bug. Add comment text and set new states.
434
434
435 If possible add the comment as being from the committer of
435 If possible add the comment as being from the committer of
436 the changeset. Otherwise use the default Bugzilla user.
436 the changeset. Otherwise use the default Bugzilla user.
437 '''
437 '''
438
438
439 def notify(self, bugs, committer):
439 def notify(self, bugs, committer):
440 '''Force sending of Bugzilla notification emails.
440 '''Force sending of Bugzilla notification emails.
441
441
442 Only required if the access method does not trigger notification
442 Only required if the access method does not trigger notification
443 emails automatically.
443 emails automatically.
444 '''
444 '''
445
445
446
446
447 # Bugzilla via direct access to MySQL database.
447 # Bugzilla via direct access to MySQL database.
448 class bzmysql(bzaccess):
448 class bzmysql(bzaccess):
449 '''Support for direct MySQL access to Bugzilla.
449 '''Support for direct MySQL access to Bugzilla.
450
450
451 The earliest Bugzilla version this is tested with is version 2.16.
451 The earliest Bugzilla version this is tested with is version 2.16.
452
452
453 If your Bugzilla is version 3.4 or above, you are strongly
453 If your Bugzilla is version 3.4 or above, you are strongly
454 recommended to use the XMLRPC access method instead.
454 recommended to use the XMLRPC access method instead.
455 '''
455 '''
456
456
457 @staticmethod
457 @staticmethod
458 def sql_buglist(ids):
458 def sql_buglist(ids):
459 '''return SQL-friendly list of bug ids'''
459 '''return SQL-friendly list of bug ids'''
460 return b'(' + b','.join(map(str, ids)) + b')'
460 return b'(' + b','.join(map(str, ids)) + b')'
461
461
462 _MySQLdb = None
462 _MySQLdb = None
463
463
464 def __init__(self, ui):
464 def __init__(self, ui):
465 try:
465 try:
466 import MySQLdb as mysql
466 import MySQLdb as mysql
467
467
468 bzmysql._MySQLdb = mysql
468 bzmysql._MySQLdb = mysql
469 except ImportError as err:
469 except ImportError as err:
470 raise error.Abort(
470 raise error.Abort(
471 _(b'python mysql support not available: %s') % err
471 _(b'python mysql support not available: %s') % err
472 )
472 )
473
473
474 bzaccess.__init__(self, ui)
474 bzaccess.__init__(self, ui)
475
475
476 host = self.ui.config(b'bugzilla', b'host')
476 host = self.ui.config(b'bugzilla', b'host')
477 user = self.ui.config(b'bugzilla', b'user')
477 user = self.ui.config(b'bugzilla', b'user')
478 passwd = self.ui.config(b'bugzilla', b'password')
478 passwd = self.ui.config(b'bugzilla', b'password')
479 db = self.ui.config(b'bugzilla', b'db')
479 db = self.ui.config(b'bugzilla', b'db')
480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
481 self.ui.note(
481 self.ui.note(
482 _(b'connecting to %s:%s as %s, password %s\n')
482 _(b'connecting to %s:%s as %s, password %s\n')
483 % (host, db, user, b'*' * len(passwd))
483 % (host, db, user, b'*' * len(passwd))
484 )
484 )
485 self.conn = bzmysql._MySQLdb.connect(
485 self.conn = bzmysql._MySQLdb.connect(
486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
487 )
487 )
488 self.cursor = self.conn.cursor()
488 self.cursor = self.conn.cursor()
489 self.longdesc_id = self.get_longdesc_id()
489 self.longdesc_id = self.get_longdesc_id()
490 self.user_ids = {}
490 self.user_ids = {}
491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
492
492
493 def run(self, *args, **kwargs):
493 def run(self, *args, **kwargs):
494 '''run a query.'''
494 '''run a query.'''
495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
496 try:
496 try:
497 self.cursor.execute(*args, **kwargs)
497 self.cursor.execute(*args, **kwargs)
498 except bzmysql._MySQLdb.MySQLError:
498 except bzmysql._MySQLdb.MySQLError:
499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
500 raise
500 raise
501
501
502 def get_longdesc_id(self):
502 def get_longdesc_id(self):
503 '''get identity of longdesc field'''
503 '''get identity of longdesc field'''
504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
505 ids = self.cursor.fetchall()
505 ids = self.cursor.fetchall()
506 if len(ids) != 1:
506 if len(ids) != 1:
507 raise error.Abort(_(b'unknown database schema'))
507 raise error.Abort(_(b'unknown database schema'))
508 return ids[0][0]
508 return ids[0][0]
509
509
510 def filter_real_bug_ids(self, bugs):
510 def filter_real_bug_ids(self, bugs):
511 '''filter not-existing bugs from set.'''
511 '''filter not-existing bugs from set.'''
512 self.run(
512 self.run(
513 b'select bug_id from bugs where bug_id in %s'
513 b'select bug_id from bugs where bug_id in %s'
514 % bzmysql.sql_buglist(bugs.keys())
514 % bzmysql.sql_buglist(bugs.keys())
515 )
515 )
516 existing = [id for (id,) in self.cursor.fetchall()]
516 existing = [id for (id,) in self.cursor.fetchall()]
517 for id in bugs.keys():
517 for id in bugs.keys():
518 if id not in existing:
518 if id not in existing:
519 self.ui.status(_(b'bug %d does not exist\n') % id)
519 self.ui.status(_(b'bug %d does not exist\n') % id)
520 del bugs[id]
520 del bugs[id]
521
521
522 def filter_cset_known_bug_ids(self, node, bugs):
522 def filter_cset_known_bug_ids(self, node, bugs):
523 '''filter bug ids that already refer to this changeset from set.'''
523 '''filter bug ids that already refer to this changeset from set.'''
524 self.run(
524 self.run(
525 '''select bug_id from longdescs where
525 '''select bug_id from longdescs where
526 bug_id in %s and thetext like "%%%s%%"'''
526 bug_id in %s and thetext like "%%%s%%"'''
527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
528 )
528 )
529 for (id,) in self.cursor.fetchall():
529 for (id,) in self.cursor.fetchall():
530 self.ui.status(
530 self.ui.status(
531 _(b'bug %d already knows about changeset %s\n')
531 _(b'bug %d already knows about changeset %s\n')
532 % (id, short(node))
532 % (id, short(node))
533 )
533 )
534 del bugs[id]
534 del bugs[id]
535
535
536 def notify(self, bugs, committer):
536 def notify(self, bugs, committer):
537 '''tell bugzilla to send mail.'''
537 '''tell bugzilla to send mail.'''
538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
539 (user, userid) = self.get_bugzilla_user(committer)
539 (user, userid) = self.get_bugzilla_user(committer)
540 for id in bugs.keys():
540 for id in bugs.keys():
541 self.ui.status(_(b' bug %s\n') % id)
541 self.ui.status(_(b' bug %s\n') % id)
542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
544 try:
544 try:
545 # Backwards-compatible with old notify string, which
545 # Backwards-compatible with old notify string, which
546 # took one string. This will throw with a new format
546 # took one string. This will throw with a new format
547 # string.
547 # string.
548 cmd = cmdfmt % id
548 cmd = cmdfmt % id
549 except TypeError:
549 except TypeError:
550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
551 self.ui.note(_(b'running notify command %s\n') % cmd)
551 self.ui.note(_(b'running notify command %s\n') % cmd)
552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
553 out = util.fromnativeeol(fp.read())
553 out = util.fromnativeeol(fp.read())
554 ret = fp.close()
554 ret = fp.close()
555 if ret:
555 if ret:
556 self.ui.warn(out)
556 self.ui.warn(out)
557 raise error.Abort(
557 raise error.Abort(
558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
559 )
559 )
560 self.ui.status(_(b'done\n'))
560 self.ui.status(_(b'done\n'))
561
561
562 def get_user_id(self, user):
562 def get_user_id(self, user):
563 '''look up numeric bugzilla user id.'''
563 '''look up numeric bugzilla user id.'''
564 try:
564 try:
565 return self.user_ids[user]
565 return self.user_ids[user]
566 except KeyError:
566 except KeyError:
567 try:
567 try:
568 userid = int(user)
568 userid = int(user)
569 except ValueError:
569 except ValueError:
570 self.ui.note(_(b'looking up user %s\n') % user)
570 self.ui.note(_(b'looking up user %s\n') % user)
571 self.run(
571 self.run(
572 '''select userid from profiles
572 '''select userid from profiles
573 where login_name like %s''',
573 where login_name like %s''',
574 user,
574 user,
575 )
575 )
576 all = self.cursor.fetchall()
576 all = self.cursor.fetchall()
577 if len(all) != 1:
577 if len(all) != 1:
578 raise KeyError(user)
578 raise KeyError(user)
579 userid = int(all[0][0])
579 userid = int(all[0][0])
580 self.user_ids[user] = userid
580 self.user_ids[user] = userid
581 return userid
581 return userid
582
582
583 def get_bugzilla_user(self, committer):
583 def get_bugzilla_user(self, committer):
584 '''See if committer is a registered bugzilla user. Return
584 '''See if committer is a registered bugzilla user. Return
585 bugzilla username and userid if so. If not, return default
585 bugzilla username and userid if so. If not, return default
586 bugzilla username and userid.'''
586 bugzilla username and userid.'''
587 user = self.map_committer(committer)
587 user = self.map_committer(committer)
588 try:
588 try:
589 userid = self.get_user_id(user)
589 userid = self.get_user_id(user)
590 except KeyError:
590 except KeyError:
591 try:
591 try:
592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
593 if not defaultuser:
593 if not defaultuser:
594 raise error.Abort(
594 raise error.Abort(
595 _(b'cannot find bugzilla user id for %s') % user
595 _(b'cannot find bugzilla user id for %s') % user
596 )
596 )
597 userid = self.get_user_id(defaultuser)
597 userid = self.get_user_id(defaultuser)
598 user = defaultuser
598 user = defaultuser
599 except KeyError:
599 except KeyError:
600 raise error.Abort(
600 raise error.Abort(
601 _(b'cannot find bugzilla user id for %s or %s')
601 _(b'cannot find bugzilla user id for %s or %s')
602 % (user, defaultuser)
602 % (user, defaultuser)
603 )
603 )
604 return (user, userid)
604 return (user, userid)
605
605
606 def updatebug(self, bugid, newstate, text, committer):
606 def updatebug(self, bugid, newstate, text, committer):
607 '''update bug state with comment text.
607 '''update bug state with comment text.
608
608
609 Try adding comment as committer of changeset, otherwise as
609 Try adding comment as committer of changeset, otherwise as
610 default bugzilla user.'''
610 default bugzilla user.'''
611 if len(newstate) > 0:
611 if len(newstate) > 0:
612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
613
613
614 (user, userid) = self.get_bugzilla_user(committer)
614 (user, userid) = self.get_bugzilla_user(committer)
615 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
615 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
616 self.run(
616 self.run(
617 '''insert into longdescs
617 '''insert into longdescs
618 (bug_id, who, bug_when, thetext)
618 (bug_id, who, bug_when, thetext)
619 values (%s, %s, %s, %s)''',
619 values (%s, %s, %s, %s)''',
620 (bugid, userid, now, text),
620 (bugid, userid, now, text),
621 )
621 )
622 self.run(
622 self.run(
623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
624 values (%s, %s, %s, %s)''',
624 values (%s, %s, %s, %s)''',
625 (bugid, userid, now, self.longdesc_id),
625 (bugid, userid, now, self.longdesc_id),
626 )
626 )
627 self.conn.commit()
627 self.conn.commit()
628
628
629
629
630 class bzmysql_2_18(bzmysql):
630 class bzmysql_2_18(bzmysql):
631 '''support for bugzilla 2.18 series.'''
631 '''support for bugzilla 2.18 series.'''
632
632
633 def __init__(self, ui):
633 def __init__(self, ui):
634 bzmysql.__init__(self, ui)
634 bzmysql.__init__(self, ui)
635 self.default_notify = (
635 self.default_notify = (
636 b"cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
636 b"cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
637 )
637 )
638
638
639
639
640 class bzmysql_3_0(bzmysql_2_18):
640 class bzmysql_3_0(bzmysql_2_18):
641 '''support for bugzilla 3.0 series.'''
641 '''support for bugzilla 3.0 series.'''
642
642
643 def __init__(self, ui):
643 def __init__(self, ui):
644 bzmysql_2_18.__init__(self, ui)
644 bzmysql_2_18.__init__(self, ui)
645
645
646 def get_longdesc_id(self):
646 def get_longdesc_id(self):
647 '''get identity of longdesc field'''
647 '''get identity of longdesc field'''
648 self.run(b'select id from fielddefs where name = "longdesc"')
648 self.run(b'select id from fielddefs where name = "longdesc"')
649 ids = self.cursor.fetchall()
649 ids = self.cursor.fetchall()
650 if len(ids) != 1:
650 if len(ids) != 1:
651 raise error.Abort(_(b'unknown database schema'))
651 raise error.Abort(_(b'unknown database schema'))
652 return ids[0][0]
652 return ids[0][0]
653
653
654
654
655 # Bugzilla via XMLRPC interface.
655 # Bugzilla via XMLRPC interface.
656
656
657
657
658 class cookietransportrequest(object):
658 class cookietransportrequest(object):
659 """A Transport request method that retains cookies over its lifetime.
659 """A Transport request method that retains cookies over its lifetime.
660
660
661 The regular xmlrpclib transports ignore cookies. Which causes
661 The regular xmlrpclib transports ignore cookies. Which causes
662 a bit of a problem when you need a cookie-based login, as with
662 a bit of a problem when you need a cookie-based login, as with
663 the Bugzilla XMLRPC interface prior to 4.4.3.
663 the Bugzilla XMLRPC interface prior to 4.4.3.
664
664
665 So this is a helper for defining a Transport which looks for
665 So this is a helper for defining a Transport which looks for
666 cookies being set in responses and saves them to add to all future
666 cookies being set in responses and saves them to add to all future
667 requests.
667 requests.
668 """
668 """
669
669
670 # Inspiration drawn from
670 # Inspiration drawn from
671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
673
673
674 cookies = []
674 cookies = []
675
675
676 def send_cookies(self, connection):
676 def send_cookies(self, connection):
677 if self.cookies:
677 if self.cookies:
678 for cookie in self.cookies:
678 for cookie in self.cookies:
679 connection.putheader(b"Cookie", cookie)
679 connection.putheader(b"Cookie", cookie)
680
680
681 def request(self, host, handler, request_body, verbose=0):
681 def request(self, host, handler, request_body, verbose=0):
682 self.verbose = verbose
682 self.verbose = verbose
683 self.accept_gzip_encoding = False
683 self.accept_gzip_encoding = False
684
684
685 # issue XML-RPC request
685 # issue XML-RPC request
686 h = self.make_connection(host)
686 h = self.make_connection(host)
687 if verbose:
687 if verbose:
688 h.set_debuglevel(1)
688 h.set_debuglevel(1)
689
689
690 self.send_request(h, handler, request_body)
690 self.send_request(h, handler, request_body)
691 self.send_host(h, host)
691 self.send_host(h, host)
692 self.send_cookies(h)
692 self.send_cookies(h)
693 self.send_user_agent(h)
693 self.send_user_agent(h)
694 self.send_content(h, request_body)
694 self.send_content(h, request_body)
695
695
696 # Deal with differences between Python 2.6 and 2.7.
696 # Deal with differences between Python 2.6 and 2.7.
697 # In the former h is a HTTP(S). In the latter it's a
697 # In the former h is a HTTP(S). In the latter it's a
698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
700 # that and use it.
700 # that and use it.
701 try:
701 try:
702 response = h.getresponse()
702 response = h.getresponse()
703 except AttributeError:
703 except AttributeError:
704 response = h._conn.getresponse()
704 response = h._conn.getresponse()
705
705
706 # Add any cookie definitions to our list.
706 # Add any cookie definitions to our list.
707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
708 val = header.split(b": ", 1)[1]
708 val = header.split(b": ", 1)[1]
709 cookie = val.split(b";", 1)[0]
709 cookie = val.split(b";", 1)[0]
710 self.cookies.append(cookie)
710 self.cookies.append(cookie)
711
711
712 if response.status != 200:
712 if response.status != 200:
713 raise xmlrpclib.ProtocolError(
713 raise xmlrpclib.ProtocolError(
714 host + handler,
714 host + handler,
715 response.status,
715 response.status,
716 response.reason,
716 response.reason,
717 response.msg.headers,
717 response.msg.headers,
718 )
718 )
719
719
720 payload = response.read()
720 payload = response.read()
721 parser, unmarshaller = self.getparser()
721 parser, unmarshaller = self.getparser()
722 parser.feed(payload)
722 parser.feed(payload)
723 parser.close()
723 parser.close()
724
724
725 return unmarshaller.close()
725 return unmarshaller.close()
726
726
727
727
728 # The explicit calls to the underlying xmlrpclib __init__() methods are
728 # The explicit calls to the underlying xmlrpclib __init__() methods are
729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
730 # it turns out their __init__() doesn't get called when doing multiple
730 # it turns out their __init__() doesn't get called when doing multiple
731 # inheritance with a new-style class.
731 # inheritance with a new-style class.
732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
733 def __init__(self, use_datetime=0):
733 def __init__(self, use_datetime=0):
734 if util.safehasattr(xmlrpclib.Transport, b"__init__"):
734 if util.safehasattr(xmlrpclib.Transport, "__init__"):
735 xmlrpclib.Transport.__init__(self, use_datetime)
735 xmlrpclib.Transport.__init__(self, use_datetime)
736
736
737
737
738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
739 def __init__(self, use_datetime=0):
739 def __init__(self, use_datetime=0):
740 if util.safehasattr(xmlrpclib.Transport, b"__init__"):
740 if util.safehasattr(xmlrpclib.Transport, "__init__"):
741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
742
742
743
743
744 class bzxmlrpc(bzaccess):
744 class bzxmlrpc(bzaccess):
745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
746
746
747 Requires a minimum Bugzilla version 3.4.
747 Requires a minimum Bugzilla version 3.4.
748 """
748 """
749
749
750 def __init__(self, ui):
750 def __init__(self, ui):
751 bzaccess.__init__(self, ui)
751 bzaccess.__init__(self, ui)
752
752
753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
755
755
756 user = self.ui.config(b'bugzilla', b'user')
756 user = self.ui.config(b'bugzilla', b'user')
757 passwd = self.ui.config(b'bugzilla', b'password')
757 passwd = self.ui.config(b'bugzilla', b'password')
758
758
759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
761
761
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
764 self.bzvermajor = int(ver[0])
764 self.bzvermajor = int(ver[0])
765 self.bzverminor = int(ver[1])
765 self.bzverminor = int(ver[1])
766 login = self.bzproxy.User.login(
766 login = self.bzproxy.User.login(
767 {b'login': user, b'password': passwd, b'restrict_login': True}
767 {b'login': user, b'password': passwd, b'restrict_login': True}
768 )
768 )
769 self.bztoken = login.get(b'token', b'')
769 self.bztoken = login.get(b'token', b'')
770
770
771 def transport(self, uri):
771 def transport(self, uri):
772 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
772 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
773 return cookiesafetransport()
773 return cookiesafetransport()
774 else:
774 else:
775 return cookietransport()
775 return cookietransport()
776
776
777 def get_bug_comments(self, id):
777 def get_bug_comments(self, id):
778 """Return a string with all comment text for a bug."""
778 """Return a string with all comment text for a bug."""
779 c = self.bzproxy.Bug.comments(
779 c = self.bzproxy.Bug.comments(
780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
781 )
781 )
782 return b''.join(
782 return b''.join(
783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
784 )
784 )
785
785
786 def filter_real_bug_ids(self, bugs):
786 def filter_real_bug_ids(self, bugs):
787 probe = self.bzproxy.Bug.get(
787 probe = self.bzproxy.Bug.get(
788 {
788 {
789 b'ids': sorted(bugs.keys()),
789 b'ids': sorted(bugs.keys()),
790 b'include_fields': [],
790 b'include_fields': [],
791 b'permissive': True,
791 b'permissive': True,
792 b'token': self.bztoken,
792 b'token': self.bztoken,
793 }
793 }
794 )
794 )
795 for badbug in probe[b'faults']:
795 for badbug in probe[b'faults']:
796 id = badbug[b'id']
796 id = badbug[b'id']
797 self.ui.status(_(b'bug %d does not exist\n') % id)
797 self.ui.status(_(b'bug %d does not exist\n') % id)
798 del bugs[id]
798 del bugs[id]
799
799
800 def filter_cset_known_bug_ids(self, node, bugs):
800 def filter_cset_known_bug_ids(self, node, bugs):
801 for id in sorted(bugs.keys()):
801 for id in sorted(bugs.keys()):
802 if self.get_bug_comments(id).find(short(node)) != -1:
802 if self.get_bug_comments(id).find(short(node)) != -1:
803 self.ui.status(
803 self.ui.status(
804 _(b'bug %d already knows about changeset %s\n')
804 _(b'bug %d already knows about changeset %s\n')
805 % (id, short(node))
805 % (id, short(node))
806 )
806 )
807 del bugs[id]
807 del bugs[id]
808
808
809 def updatebug(self, bugid, newstate, text, committer):
809 def updatebug(self, bugid, newstate, text, committer):
810 args = {}
810 args = {}
811 if b'hours' in newstate:
811 if b'hours' in newstate:
812 args[b'work_time'] = newstate[b'hours']
812 args[b'work_time'] = newstate[b'hours']
813
813
814 if self.bzvermajor >= 4:
814 if self.bzvermajor >= 4:
815 args[b'ids'] = [bugid]
815 args[b'ids'] = [bugid]
816 args[b'comment'] = {b'body': text}
816 args[b'comment'] = {b'body': text}
817 if b'fix' in newstate:
817 if b'fix' in newstate:
818 args[b'status'] = self.fixstatus
818 args[b'status'] = self.fixstatus
819 args[b'resolution'] = self.fixresolution
819 args[b'resolution'] = self.fixresolution
820 args[b'token'] = self.bztoken
820 args[b'token'] = self.bztoken
821 self.bzproxy.Bug.update(args)
821 self.bzproxy.Bug.update(args)
822 else:
822 else:
823 if b'fix' in newstate:
823 if b'fix' in newstate:
824 self.ui.warn(
824 self.ui.warn(
825 _(
825 _(
826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
827 b"to mark bugs fixed\n"
827 b"to mark bugs fixed\n"
828 )
828 )
829 )
829 )
830 args[b'id'] = bugid
830 args[b'id'] = bugid
831 args[b'comment'] = text
831 args[b'comment'] = text
832 self.bzproxy.Bug.add_comment(args)
832 self.bzproxy.Bug.add_comment(args)
833
833
834
834
835 class bzxmlrpcemail(bzxmlrpc):
835 class bzxmlrpcemail(bzxmlrpc):
836 """Read data from Bugzilla via XMLRPC, send updates via email.
836 """Read data from Bugzilla via XMLRPC, send updates via email.
837
837
838 Advantages of sending updates via email:
838 Advantages of sending updates via email:
839 1. Comments can be added as any user, not just logged in user.
839 1. Comments can be added as any user, not just logged in user.
840 2. Bug statuses or other fields not accessible via XMLRPC can
840 2. Bug statuses or other fields not accessible via XMLRPC can
841 potentially be updated.
841 potentially be updated.
842
842
843 There is no XMLRPC function to change bug status before Bugzilla
843 There is no XMLRPC function to change bug status before Bugzilla
844 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
844 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
845 But bugs can be marked fixed via email from 3.4 onwards.
845 But bugs can be marked fixed via email from 3.4 onwards.
846 """
846 """
847
847
848 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
848 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
849 # in-email fields are specified as '@<fieldname> = <value>'. In
849 # in-email fields are specified as '@<fieldname> = <value>'. In
850 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
850 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
851 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
851 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
852 # compatibility, but rather than rely on this use the new format for
852 # compatibility, but rather than rely on this use the new format for
853 # 4.0 onwards.
853 # 4.0 onwards.
854
854
855 def __init__(self, ui):
855 def __init__(self, ui):
856 bzxmlrpc.__init__(self, ui)
856 bzxmlrpc.__init__(self, ui)
857
857
858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
859 if not self.bzemail:
859 if not self.bzemail:
860 raise error.Abort(_(b"configuration 'bzemail' missing"))
860 raise error.Abort(_(b"configuration 'bzemail' missing"))
861 mail.validateconfig(self.ui)
861 mail.validateconfig(self.ui)
862
862
863 def makecommandline(self, fieldname, value):
863 def makecommandline(self, fieldname, value):
864 if self.bzvermajor >= 4:
864 if self.bzvermajor >= 4:
865 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
865 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
866 else:
866 else:
867 if fieldname == b"id":
867 if fieldname == b"id":
868 fieldname = b"bug_id"
868 fieldname = b"bug_id"
869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
870
870
871 def send_bug_modify_email(self, bugid, commands, comment, committer):
871 def send_bug_modify_email(self, bugid, commands, comment, committer):
872 '''send modification message to Bugzilla bug via email.
872 '''send modification message to Bugzilla bug via email.
873
873
874 The message format is documented in the Bugzilla email_in.pl
874 The message format is documented in the Bugzilla email_in.pl
875 specification. commands is a list of command lines, comment is the
875 specification. commands is a list of command lines, comment is the
876 comment text.
876 comment text.
877
877
878 To stop users from crafting commit comments with
878 To stop users from crafting commit comments with
879 Bugzilla commands, specify the bug ID via the message body, rather
879 Bugzilla commands, specify the bug ID via the message body, rather
880 than the subject line, and leave a blank line after it.
880 than the subject line, and leave a blank line after it.
881 '''
881 '''
882 user = self.map_committer(committer)
882 user = self.map_committer(committer)
883 matches = self.bzproxy.User.get(
883 matches = self.bzproxy.User.get(
884 {b'match': [user], b'token': self.bztoken}
884 {b'match': [user], b'token': self.bztoken}
885 )
885 )
886 if not matches[b'users']:
886 if not matches[b'users']:
887 user = self.ui.config(b'bugzilla', b'user')
887 user = self.ui.config(b'bugzilla', b'user')
888 matches = self.bzproxy.User.get(
888 matches = self.bzproxy.User.get(
889 {b'match': [user], b'token': self.bztoken}
889 {b'match': [user], b'token': self.bztoken}
890 )
890 )
891 if not matches[b'users']:
891 if not matches[b'users']:
892 raise error.Abort(
892 raise error.Abort(
893 _(b"default bugzilla user %s email not found") % user
893 _(b"default bugzilla user %s email not found") % user
894 )
894 )
895 user = matches[b'users'][0][b'email']
895 user = matches[b'users'][0][b'email']
896 commands.append(self.makecommandline(b"id", bugid))
896 commands.append(self.makecommandline(b"id", bugid))
897
897
898 text = b"\n".join(commands) + b"\n\n" + comment
898 text = b"\n".join(commands) + b"\n\n" + comment
899
899
900 _charsets = mail._charsets(self.ui)
900 _charsets = mail._charsets(self.ui)
901 user = mail.addressencode(self.ui, user, _charsets)
901 user = mail.addressencode(self.ui, user, _charsets)
902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
903 msg = mail.mimeencode(self.ui, text, _charsets)
903 msg = mail.mimeencode(self.ui, text, _charsets)
904 msg[b'From'] = user
904 msg[b'From'] = user
905 msg[b'To'] = bzemail
905 msg[b'To'] = bzemail
906 msg[b'Subject'] = mail.headencode(
906 msg[b'Subject'] = mail.headencode(
907 self.ui, b"Bug modification", _charsets
907 self.ui, b"Bug modification", _charsets
908 )
908 )
909 sendmail = mail.connect(self.ui)
909 sendmail = mail.connect(self.ui)
910 sendmail(user, bzemail, msg.as_string())
910 sendmail(user, bzemail, msg.as_string())
911
911
912 def updatebug(self, bugid, newstate, text, committer):
912 def updatebug(self, bugid, newstate, text, committer):
913 cmds = []
913 cmds = []
914 if b'hours' in newstate:
914 if b'hours' in newstate:
915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
916 if b'fix' in newstate:
916 if b'fix' in newstate:
917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
918 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
918 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
919 self.send_bug_modify_email(bugid, cmds, text, committer)
919 self.send_bug_modify_email(bugid, cmds, text, committer)
920
920
921
921
922 class NotFound(LookupError):
922 class NotFound(LookupError):
923 pass
923 pass
924
924
925
925
926 class bzrestapi(bzaccess):
926 class bzrestapi(bzaccess):
927 """Read and write bugzilla data using the REST API available since
927 """Read and write bugzilla data using the REST API available since
928 Bugzilla 5.0.
928 Bugzilla 5.0.
929 """
929 """
930
930
931 def __init__(self, ui):
931 def __init__(self, ui):
932 bzaccess.__init__(self, ui)
932 bzaccess.__init__(self, ui)
933 bz = self.ui.config(b'bugzilla', b'bzurl')
933 bz = self.ui.config(b'bugzilla', b'bzurl')
934 self.bzroot = b'/'.join([bz, b'rest'])
934 self.bzroot = b'/'.join([bz, b'rest'])
935 self.apikey = self.ui.config(b'bugzilla', b'apikey')
935 self.apikey = self.ui.config(b'bugzilla', b'apikey')
936 self.user = self.ui.config(b'bugzilla', b'user')
936 self.user = self.ui.config(b'bugzilla', b'user')
937 self.passwd = self.ui.config(b'bugzilla', b'password')
937 self.passwd = self.ui.config(b'bugzilla', b'password')
938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
940
940
941 def apiurl(self, targets, include_fields=None):
941 def apiurl(self, targets, include_fields=None):
942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
943 qv = {}
943 qv = {}
944 if self.apikey:
944 if self.apikey:
945 qv[b'api_key'] = self.apikey
945 qv[b'api_key'] = self.apikey
946 elif self.user and self.passwd:
946 elif self.user and self.passwd:
947 qv[b'login'] = self.user
947 qv[b'login'] = self.user
948 qv[b'password'] = self.passwd
948 qv[b'password'] = self.passwd
949 if include_fields:
949 if include_fields:
950 qv[b'include_fields'] = include_fields
950 qv[b'include_fields'] = include_fields
951 if qv:
951 if qv:
952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
953 return url
953 return url
954
954
955 def _fetch(self, burl):
955 def _fetch(self, burl):
956 try:
956 try:
957 resp = url.open(self.ui, burl)
957 resp = url.open(self.ui, burl)
958 return json.loads(resp.read())
958 return json.loads(resp.read())
959 except util.urlerr.httperror as inst:
959 except util.urlerr.httperror as inst:
960 if inst.code == 401:
960 if inst.code == 401:
961 raise error.Abort(_(b'authorization failed'))
961 raise error.Abort(_(b'authorization failed'))
962 if inst.code == 404:
962 if inst.code == 404:
963 raise NotFound()
963 raise NotFound()
964 else:
964 else:
965 raise
965 raise
966
966
967 def _submit(self, burl, data, method=b'POST'):
967 def _submit(self, burl, data, method=b'POST'):
968 data = json.dumps(data)
968 data = json.dumps(data)
969 if method == b'PUT':
969 if method == b'PUT':
970
970
971 class putrequest(util.urlreq.request):
971 class putrequest(util.urlreq.request):
972 def get_method(self):
972 def get_method(self):
973 return b'PUT'
973 return b'PUT'
974
974
975 request_type = putrequest
975 request_type = putrequest
976 else:
976 else:
977 request_type = util.urlreq.request
977 request_type = util.urlreq.request
978 req = request_type(burl, data, {b'Content-Type': b'application/json'})
978 req = request_type(burl, data, {b'Content-Type': b'application/json'})
979 try:
979 try:
980 resp = url.opener(self.ui).open(req)
980 resp = url.opener(self.ui).open(req)
981 return json.loads(resp.read())
981 return json.loads(resp.read())
982 except util.urlerr.httperror as inst:
982 except util.urlerr.httperror as inst:
983 if inst.code == 401:
983 if inst.code == 401:
984 raise error.Abort(_(b'authorization failed'))
984 raise error.Abort(_(b'authorization failed'))
985 if inst.code == 404:
985 if inst.code == 404:
986 raise NotFound()
986 raise NotFound()
987 else:
987 else:
988 raise
988 raise
989
989
990 def filter_real_bug_ids(self, bugs):
990 def filter_real_bug_ids(self, bugs):
991 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
991 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
992 badbugs = set()
992 badbugs = set()
993 for bugid in bugs:
993 for bugid in bugs:
994 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
994 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
995 try:
995 try:
996 self._fetch(burl)
996 self._fetch(burl)
997 except NotFound:
997 except NotFound:
998 badbugs.add(bugid)
998 badbugs.add(bugid)
999 for bugid in badbugs:
999 for bugid in badbugs:
1000 del bugs[bugid]
1000 del bugs[bugid]
1001
1001
1002 def filter_cset_known_bug_ids(self, node, bugs):
1002 def filter_cset_known_bug_ids(self, node, bugs):
1003 '''remove bug IDs where node occurs in comment text from bugs.'''
1003 '''remove bug IDs where node occurs in comment text from bugs.'''
1004 sn = short(node)
1004 sn = short(node)
1005 for bugid in bugs.keys():
1005 for bugid in bugs.keys():
1006 burl = self.apiurl(
1006 burl = self.apiurl(
1007 (b'bug', bugid, b'comment'), include_fields=b'text'
1007 (b'bug', bugid, b'comment'), include_fields=b'text'
1008 )
1008 )
1009 result = self._fetch(burl)
1009 result = self._fetch(burl)
1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1011 if any(sn in c[b'text'] for c in comments):
1011 if any(sn in c[b'text'] for c in comments):
1012 self.ui.status(
1012 self.ui.status(
1013 _(b'bug %d already knows about changeset %s\n')
1013 _(b'bug %d already knows about changeset %s\n')
1014 % (bugid, sn)
1014 % (bugid, sn)
1015 )
1015 )
1016 del bugs[bugid]
1016 del bugs[bugid]
1017
1017
1018 def updatebug(self, bugid, newstate, text, committer):
1018 def updatebug(self, bugid, newstate, text, committer):
1019 '''update the specified bug. Add comment text and set new states.
1019 '''update the specified bug. Add comment text and set new states.
1020
1020
1021 If possible add the comment as being from the committer of
1021 If possible add the comment as being from the committer of
1022 the changeset. Otherwise use the default Bugzilla user.
1022 the changeset. Otherwise use the default Bugzilla user.
1023 '''
1023 '''
1024 bugmod = {}
1024 bugmod = {}
1025 if b'hours' in newstate:
1025 if b'hours' in newstate:
1026 bugmod[b'work_time'] = newstate[b'hours']
1026 bugmod[b'work_time'] = newstate[b'hours']
1027 if b'fix' in newstate:
1027 if b'fix' in newstate:
1028 bugmod[b'status'] = self.fixstatus
1028 bugmod[b'status'] = self.fixstatus
1029 bugmod[b'resolution'] = self.fixresolution
1029 bugmod[b'resolution'] = self.fixresolution
1030 if bugmod:
1030 if bugmod:
1031 # if we have to change the bugs state do it here
1031 # if we have to change the bugs state do it here
1032 bugmod[b'comment'] = {
1032 bugmod[b'comment'] = {
1033 b'comment': text,
1033 b'comment': text,
1034 b'is_private': False,
1034 b'is_private': False,
1035 b'is_markdown': False,
1035 b'is_markdown': False,
1036 }
1036 }
1037 burl = self.apiurl((b'bug', bugid))
1037 burl = self.apiurl((b'bug', bugid))
1038 self._submit(burl, bugmod, method=b'PUT')
1038 self._submit(burl, bugmod, method=b'PUT')
1039 self.ui.debug(b'updated bug %s\n' % bugid)
1039 self.ui.debug(b'updated bug %s\n' % bugid)
1040 else:
1040 else:
1041 burl = self.apiurl((b'bug', bugid, b'comment'))
1041 burl = self.apiurl((b'bug', bugid, b'comment'))
1042 self._submit(
1042 self._submit(
1043 burl,
1043 burl,
1044 {
1044 {
1045 b'comment': text,
1045 b'comment': text,
1046 b'is_private': False,
1046 b'is_private': False,
1047 b'is_markdown': False,
1047 b'is_markdown': False,
1048 },
1048 },
1049 )
1049 )
1050 self.ui.debug(b'added comment to bug %s\n' % bugid)
1050 self.ui.debug(b'added comment to bug %s\n' % bugid)
1051
1051
1052 def notify(self, bugs, committer):
1052 def notify(self, bugs, committer):
1053 '''Force sending of Bugzilla notification emails.
1053 '''Force sending of Bugzilla notification emails.
1054
1054
1055 Only required if the access method does not trigger notification
1055 Only required if the access method does not trigger notification
1056 emails automatically.
1056 emails automatically.
1057 '''
1057 '''
1058 pass
1058 pass
1059
1059
1060
1060
1061 class bugzilla(object):
1061 class bugzilla(object):
1062 # supported versions of bugzilla. different versions have
1062 # supported versions of bugzilla. different versions have
1063 # different schemas.
1063 # different schemas.
1064 _versions = {
1064 _versions = {
1065 b'2.16': bzmysql,
1065 b'2.16': bzmysql,
1066 b'2.18': bzmysql_2_18,
1066 b'2.18': bzmysql_2_18,
1067 b'3.0': bzmysql_3_0,
1067 b'3.0': bzmysql_3_0,
1068 b'xmlrpc': bzxmlrpc,
1068 b'xmlrpc': bzxmlrpc,
1069 b'xmlrpc+email': bzxmlrpcemail,
1069 b'xmlrpc+email': bzxmlrpcemail,
1070 b'restapi': bzrestapi,
1070 b'restapi': bzrestapi,
1071 }
1071 }
1072
1072
1073 def __init__(self, ui, repo):
1073 def __init__(self, ui, repo):
1074 self.ui = ui
1074 self.ui = ui
1075 self.repo = repo
1075 self.repo = repo
1076
1076
1077 bzversion = self.ui.config(b'bugzilla', b'version')
1077 bzversion = self.ui.config(b'bugzilla', b'version')
1078 try:
1078 try:
1079 bzclass = bugzilla._versions[bzversion]
1079 bzclass = bugzilla._versions[bzversion]
1080 except KeyError:
1080 except KeyError:
1081 raise error.Abort(
1081 raise error.Abort(
1082 _(b'bugzilla version %s not supported') % bzversion
1082 _(b'bugzilla version %s not supported') % bzversion
1083 )
1083 )
1084 self.bzdriver = bzclass(self.ui)
1084 self.bzdriver = bzclass(self.ui)
1085
1085
1086 self.bug_re = re.compile(
1086 self.bug_re = re.compile(
1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1088 )
1088 )
1089 self.fix_re = re.compile(
1089 self.fix_re = re.compile(
1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1091 )
1091 )
1092 self.split_re = re.compile(br'\D+')
1092 self.split_re = re.compile(br'\D+')
1093
1093
1094 def find_bugs(self, ctx):
1094 def find_bugs(self, ctx):
1095 '''return bugs dictionary created from commit comment.
1095 '''return bugs dictionary created from commit comment.
1096
1096
1097 Extract bug info from changeset comments. Filter out any that are
1097 Extract bug info from changeset comments. Filter out any that are
1098 not known to Bugzilla, and any that already have a reference to
1098 not known to Bugzilla, and any that already have a reference to
1099 the given changeset in their comments.
1099 the given changeset in their comments.
1100 '''
1100 '''
1101 start = 0
1101 start = 0
1102 hours = 0.0
1102 hours = 0.0
1103 bugs = {}
1103 bugs = {}
1104 bugmatch = self.bug_re.search(ctx.description(), start)
1104 bugmatch = self.bug_re.search(ctx.description(), start)
1105 fixmatch = self.fix_re.search(ctx.description(), start)
1105 fixmatch = self.fix_re.search(ctx.description(), start)
1106 while True:
1106 while True:
1107 bugattribs = {}
1107 bugattribs = {}
1108 if not bugmatch and not fixmatch:
1108 if not bugmatch and not fixmatch:
1109 break
1109 break
1110 if not bugmatch:
1110 if not bugmatch:
1111 m = fixmatch
1111 m = fixmatch
1112 elif not fixmatch:
1112 elif not fixmatch:
1113 m = bugmatch
1113 m = bugmatch
1114 else:
1114 else:
1115 if bugmatch.start() < fixmatch.start():
1115 if bugmatch.start() < fixmatch.start():
1116 m = bugmatch
1116 m = bugmatch
1117 else:
1117 else:
1118 m = fixmatch
1118 m = fixmatch
1119 start = m.end()
1119 start = m.end()
1120 if m is bugmatch:
1120 if m is bugmatch:
1121 bugmatch = self.bug_re.search(ctx.description(), start)
1121 bugmatch = self.bug_re.search(ctx.description(), start)
1122 if b'fix' in bugattribs:
1122 if b'fix' in bugattribs:
1123 del bugattribs[b'fix']
1123 del bugattribs[b'fix']
1124 else:
1124 else:
1125 fixmatch = self.fix_re.search(ctx.description(), start)
1125 fixmatch = self.fix_re.search(ctx.description(), start)
1126 bugattribs[b'fix'] = None
1126 bugattribs[b'fix'] = None
1127
1127
1128 try:
1128 try:
1129 ids = m.group(b'ids')
1129 ids = m.group(b'ids')
1130 except IndexError:
1130 except IndexError:
1131 ids = m.group(1)
1131 ids = m.group(1)
1132 try:
1132 try:
1133 hours = float(m.group(b'hours'))
1133 hours = float(m.group(b'hours'))
1134 bugattribs[b'hours'] = hours
1134 bugattribs[b'hours'] = hours
1135 except IndexError:
1135 except IndexError:
1136 pass
1136 pass
1137 except TypeError:
1137 except TypeError:
1138 pass
1138 pass
1139 except ValueError:
1139 except ValueError:
1140 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1140 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1141
1141
1142 for id in self.split_re.split(ids):
1142 for id in self.split_re.split(ids):
1143 if not id:
1143 if not id:
1144 continue
1144 continue
1145 bugs[int(id)] = bugattribs
1145 bugs[int(id)] = bugattribs
1146 if bugs:
1146 if bugs:
1147 self.bzdriver.filter_real_bug_ids(bugs)
1147 self.bzdriver.filter_real_bug_ids(bugs)
1148 if bugs:
1148 if bugs:
1149 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1149 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1150 return bugs
1150 return bugs
1151
1151
1152 def update(self, bugid, newstate, ctx):
1152 def update(self, bugid, newstate, ctx):
1153 '''update bugzilla bug with reference to changeset.'''
1153 '''update bugzilla bug with reference to changeset.'''
1154
1154
1155 def webroot(root):
1155 def webroot(root):
1156 '''strip leading prefix of repo root and turn into
1156 '''strip leading prefix of repo root and turn into
1157 url-safe path.'''
1157 url-safe path.'''
1158 count = int(self.ui.config(b'bugzilla', b'strip'))
1158 count = int(self.ui.config(b'bugzilla', b'strip'))
1159 root = util.pconvert(root)
1159 root = util.pconvert(root)
1160 while count > 0:
1160 while count > 0:
1161 c = root.find(b'/')
1161 c = root.find(b'/')
1162 if c == -1:
1162 if c == -1:
1163 break
1163 break
1164 root = root[c + 1 :]
1164 root = root[c + 1 :]
1165 count -= 1
1165 count -= 1
1166 return root
1166 return root
1167
1167
1168 mapfile = None
1168 mapfile = None
1169 tmpl = self.ui.config(b'bugzilla', b'template')
1169 tmpl = self.ui.config(b'bugzilla', b'template')
1170 if not tmpl:
1170 if not tmpl:
1171 mapfile = self.ui.config(b'bugzilla', b'style')
1171 mapfile = self.ui.config(b'bugzilla', b'style')
1172 if not mapfile and not tmpl:
1172 if not mapfile and not tmpl:
1173 tmpl = _(
1173 tmpl = _(
1174 b'changeset {node|short} in repo {root} refers '
1174 b'changeset {node|short} in repo {root} refers '
1175 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1175 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1176 )
1176 )
1177 spec = logcmdutil.templatespec(tmpl, mapfile)
1177 spec = logcmdutil.templatespec(tmpl, mapfile)
1178 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1178 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1179 self.ui.pushbuffer()
1179 self.ui.pushbuffer()
1180 t.show(
1180 t.show(
1181 ctx,
1181 ctx,
1182 changes=ctx.changeset(),
1182 changes=ctx.changeset(),
1183 bug=pycompat.bytestr(bugid),
1183 bug=pycompat.bytestr(bugid),
1184 hgweb=self.ui.config(b'web', b'baseurl'),
1184 hgweb=self.ui.config(b'web', b'baseurl'),
1185 root=self.repo.root,
1185 root=self.repo.root,
1186 webroot=webroot(self.repo.root),
1186 webroot=webroot(self.repo.root),
1187 )
1187 )
1188 data = self.ui.popbuffer()
1188 data = self.ui.popbuffer()
1189 self.bzdriver.updatebug(
1189 self.bzdriver.updatebug(
1190 bugid, newstate, data, stringutil.email(ctx.user())
1190 bugid, newstate, data, stringutil.email(ctx.user())
1191 )
1191 )
1192
1192
1193 def notify(self, bugs, committer):
1193 def notify(self, bugs, committer):
1194 '''ensure Bugzilla users are notified of bug change.'''
1194 '''ensure Bugzilla users are notified of bug change.'''
1195 self.bzdriver.notify(bugs, committer)
1195 self.bzdriver.notify(bugs, committer)
1196
1196
1197
1197
1198 def hook(ui, repo, hooktype, node=None, **kwargs):
1198 def hook(ui, repo, hooktype, node=None, **kwargs):
1199 '''add comment to bugzilla for each changeset that refers to a
1199 '''add comment to bugzilla for each changeset that refers to a
1200 bugzilla bug id. only add a comment once per bug, so same change
1200 bugzilla bug id. only add a comment once per bug, so same change
1201 seen multiple times does not fill bug with duplicate data.'''
1201 seen multiple times does not fill bug with duplicate data.'''
1202 if node is None:
1202 if node is None:
1203 raise error.Abort(
1203 raise error.Abort(
1204 _(b'hook type %s does not pass a changeset id') % hooktype
1204 _(b'hook type %s does not pass a changeset id') % hooktype
1205 )
1205 )
1206 try:
1206 try:
1207 bz = bugzilla(ui, repo)
1207 bz = bugzilla(ui, repo)
1208 ctx = repo[node]
1208 ctx = repo[node]
1209 bugs = bz.find_bugs(ctx)
1209 bugs = bz.find_bugs(ctx)
1210 if bugs:
1210 if bugs:
1211 for bug in bugs:
1211 for bug in bugs:
1212 bz.update(bug, bugs[bug], ctx)
1212 bz.update(bug, bugs[bug], ctx)
1213 bz.notify(bugs, stringutil.email(ctx.user()))
1213 bz.notify(bugs, stringutil.email(ctx.user()))
1214 except Exception as e:
1214 except Exception as e:
1215 raise error.Abort(_(b'Bugzilla error: %s') % e)
1215 raise error.Abort(_(b'Bugzilla error: %s') % e)
@@ -1,89 +1,89 b''
1 # commitextras.py
1 # commitextras.py
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''adds a new flag extras to commit (ADVANCED)'''
8 '''adds a new flag extras to commit (ADVANCED)'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import re
12 import re
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import (
16 commands,
16 commands,
17 error,
17 error,
18 extensions,
18 extensions,
19 registrar,
19 registrar,
20 util,
20 util,
21 )
21 )
22
22
23 cmdtable = {}
23 cmdtable = {}
24 command = registrar.command(cmdtable)
24 command = registrar.command(cmdtable)
25 testedwith = b'ships-with-hg-core'
25 testedwith = b'ships-with-hg-core'
26
26
27 usedinternally = {
27 usedinternally = {
28 b'amend_source',
28 b'amend_source',
29 b'branch',
29 b'branch',
30 b'close',
30 b'close',
31 b'histedit_source',
31 b'histedit_source',
32 b'topic',
32 b'topic',
33 b'rebase_source',
33 b'rebase_source',
34 b'intermediate-source',
34 b'intermediate-source',
35 b'__touch-noise__',
35 b'__touch-noise__',
36 b'source',
36 b'source',
37 b'transplant_source',
37 b'transplant_source',
38 }
38 }
39
39
40
40
41 def extsetup(ui):
41 def extsetup(ui):
42 entry = extensions.wrapcommand(commands.table, b'commit', _commit)
42 entry = extensions.wrapcommand(commands.table, b'commit', _commit)
43 options = entry[1]
43 options = entry[1]
44 options.append(
44 options.append(
45 (
45 (
46 b'',
46 b'',
47 b'extra',
47 b'extra',
48 [],
48 [],
49 _(b'set a changeset\'s extra values'),
49 _(b'set a changeset\'s extra values'),
50 _(b"KEY=VALUE"),
50 _(b"KEY=VALUE"),
51 )
51 )
52 )
52 )
53
53
54
54
55 def _commit(orig, ui, repo, *pats, **opts):
55 def _commit(orig, ui, repo, *pats, **opts):
56 if util.safehasattr(repo, b'unfiltered'):
56 if util.safehasattr(repo, 'unfiltered'):
57 repo = repo.unfiltered()
57 repo = repo.unfiltered()
58
58
59 class repoextra(repo.__class__):
59 class repoextra(repo.__class__):
60 def commit(self, *innerpats, **inneropts):
60 def commit(self, *innerpats, **inneropts):
61 extras = opts.get(r'extra')
61 extras = opts.get(r'extra')
62 for raw in extras:
62 for raw in extras:
63 if b'=' not in raw:
63 if b'=' not in raw:
64 msg = _(
64 msg = _(
65 b"unable to parse '%s', should follow "
65 b"unable to parse '%s', should follow "
66 b"KEY=VALUE format"
66 b"KEY=VALUE format"
67 )
67 )
68 raise error.Abort(msg % raw)
68 raise error.Abort(msg % raw)
69 k, v = raw.split(b'=', 1)
69 k, v = raw.split(b'=', 1)
70 if not k:
70 if not k:
71 msg = _(b"unable to parse '%s', keys can't be empty")
71 msg = _(b"unable to parse '%s', keys can't be empty")
72 raise error.Abort(msg % raw)
72 raise error.Abort(msg % raw)
73 if re.search(br'[^\w-]', k):
73 if re.search(br'[^\w-]', k):
74 msg = _(
74 msg = _(
75 b"keys can only contain ascii letters, digits,"
75 b"keys can only contain ascii letters, digits,"
76 b" '_' and '-'"
76 b" '_' and '-'"
77 )
77 )
78 raise error.Abort(msg)
78 raise error.Abort(msg)
79 if k in usedinternally:
79 if k in usedinternally:
80 msg = _(
80 msg = _(
81 b"key '%s' is used internally, can't be set "
81 b"key '%s' is used internally, can't be set "
82 b"manually"
82 b"manually"
83 )
83 )
84 raise error.Abort(msg % k)
84 raise error.Abort(msg % k)
85 inneropts[r'extra'][k] = v
85 inneropts[r'extra'][k] = v
86 return super(repoextra, self).commit(*innerpats, **inneropts)
86 return super(repoextra, self).commit(*innerpats, **inneropts)
87
87
88 repo.__class__ = repoextra
88 repo.__class__ = repoextra
89 return orig(ui, repo, *pats, **opts)
89 return orig(ui, repo, *pats, **opts)
@@ -1,357 +1,357 b''
1 # Copyright 2016-present Facebook. All Rights Reserved.
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
2 #
3 # commands: fastannotate commands
3 # commands: fastannotate commands
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 commands,
14 commands,
15 encoding,
15 encoding,
16 error,
16 error,
17 extensions,
17 extensions,
18 patch,
18 patch,
19 pycompat,
19 pycompat,
20 registrar,
20 registrar,
21 scmutil,
21 scmutil,
22 util,
22 util,
23 )
23 )
24
24
25 from . import (
25 from . import (
26 context as facontext,
26 context as facontext,
27 error as faerror,
27 error as faerror,
28 formatter as faformatter,
28 formatter as faformatter,
29 )
29 )
30
30
31 cmdtable = {}
31 cmdtable = {}
32 command = registrar.command(cmdtable)
32 command = registrar.command(cmdtable)
33
33
34
34
35 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
35 def _matchpaths(repo, rev, pats, opts, aopts=facontext.defaultopts):
36 """generate paths matching given patterns"""
36 """generate paths matching given patterns"""
37 perfhack = repo.ui.configbool(b'fastannotate', b'perfhack')
37 perfhack = repo.ui.configbool(b'fastannotate', b'perfhack')
38
38
39 # disable perfhack if:
39 # disable perfhack if:
40 # a) any walkopt is used
40 # a) any walkopt is used
41 # b) if we treat pats as plain file names, some of them do not have
41 # b) if we treat pats as plain file names, some of them do not have
42 # corresponding linelog files
42 # corresponding linelog files
43 if perfhack:
43 if perfhack:
44 # cwd related to reporoot
44 # cwd related to reporoot
45 reporoot = os.path.dirname(repo.path)
45 reporoot = os.path.dirname(repo.path)
46 reldir = os.path.relpath(encoding.getcwd(), reporoot)
46 reldir = os.path.relpath(encoding.getcwd(), reporoot)
47 if reldir == b'.':
47 if reldir == b'.':
48 reldir = b''
48 reldir = b''
49 if any(opts.get(o[1]) for o in commands.walkopts): # a)
49 if any(opts.get(o[1]) for o in commands.walkopts): # a)
50 perfhack = False
50 perfhack = False
51 else: # b)
51 else: # b)
52 relpats = [
52 relpats = [
53 os.path.relpath(p, reporoot) if os.path.isabs(p) else p
53 os.path.relpath(p, reporoot) if os.path.isabs(p) else p
54 for p in pats
54 for p in pats
55 ]
55 ]
56 # disable perfhack on '..' since it allows escaping from the repo
56 # disable perfhack on '..' since it allows escaping from the repo
57 if any(
57 if any(
58 (
58 (
59 b'..' in f
59 b'..' in f
60 or not os.path.isfile(
60 or not os.path.isfile(
61 facontext.pathhelper(repo, f, aopts).linelogpath
61 facontext.pathhelper(repo, f, aopts).linelogpath
62 )
62 )
63 )
63 )
64 for f in relpats
64 for f in relpats
65 ):
65 ):
66 perfhack = False
66 perfhack = False
67
67
68 # perfhack: emit paths directory without checking with manifest
68 # perfhack: emit paths directory without checking with manifest
69 # this can be incorrect if the rev dos not have file.
69 # this can be incorrect if the rev dos not have file.
70 if perfhack:
70 if perfhack:
71 for p in relpats:
71 for p in relpats:
72 yield os.path.join(reldir, p)
72 yield os.path.join(reldir, p)
73 else:
73 else:
74
74
75 def bad(x, y):
75 def bad(x, y):
76 raise error.Abort(b"%s: %s" % (x, y))
76 raise error.Abort(b"%s: %s" % (x, y))
77
77
78 ctx = scmutil.revsingle(repo, rev)
78 ctx = scmutil.revsingle(repo, rev)
79 m = scmutil.match(ctx, pats, opts, badfn=bad)
79 m = scmutil.match(ctx, pats, opts, badfn=bad)
80 for p in ctx.walk(m):
80 for p in ctx.walk(m):
81 yield p
81 yield p
82
82
83
83
84 fastannotatecommandargs = {
84 fastannotatecommandargs = {
85 r'options': [
85 r'options': [
86 (b'r', b'rev', b'.', _(b'annotate the specified revision'), _(b'REV')),
86 (b'r', b'rev', b'.', _(b'annotate the specified revision'), _(b'REV')),
87 (b'u', b'user', None, _(b'list the author (long with -v)')),
87 (b'u', b'user', None, _(b'list the author (long with -v)')),
88 (b'f', b'file', None, _(b'list the filename')),
88 (b'f', b'file', None, _(b'list the filename')),
89 (b'd', b'date', None, _(b'list the date (short with -q)')),
89 (b'd', b'date', None, _(b'list the date (short with -q)')),
90 (b'n', b'number', None, _(b'list the revision number (default)')),
90 (b'n', b'number', None, _(b'list the revision number (default)')),
91 (b'c', b'changeset', None, _(b'list the changeset')),
91 (b'c', b'changeset', None, _(b'list the changeset')),
92 (
92 (
93 b'l',
93 b'l',
94 b'line-number',
94 b'line-number',
95 None,
95 None,
96 _(b'show line number at the first ' b'appearance'),
96 _(b'show line number at the first ' b'appearance'),
97 ),
97 ),
98 (
98 (
99 b'e',
99 b'e',
100 b'deleted',
100 b'deleted',
101 None,
101 None,
102 _(b'show deleted lines (slow) (EXPERIMENTAL)'),
102 _(b'show deleted lines (slow) (EXPERIMENTAL)'),
103 ),
103 ),
104 (
104 (
105 b'',
105 b'',
106 b'no-content',
106 b'no-content',
107 None,
107 None,
108 _(b'do not show file content (EXPERIMENTAL)'),
108 _(b'do not show file content (EXPERIMENTAL)'),
109 ),
109 ),
110 (b'', b'no-follow', None, _(b"don't follow copies and renames")),
110 (b'', b'no-follow', None, _(b"don't follow copies and renames")),
111 (
111 (
112 b'',
112 b'',
113 b'linear',
113 b'linear',
114 None,
114 None,
115 _(
115 _(
116 b'enforce linear history, ignore second parent '
116 b'enforce linear history, ignore second parent '
117 b'of merges (EXPERIMENTAL)'
117 b'of merges (EXPERIMENTAL)'
118 ),
118 ),
119 ),
119 ),
120 (
120 (
121 b'',
121 b'',
122 b'long-hash',
122 b'long-hash',
123 None,
123 None,
124 _(b'show long changeset hash (EXPERIMENTAL)'),
124 _(b'show long changeset hash (EXPERIMENTAL)'),
125 ),
125 ),
126 (
126 (
127 b'',
127 b'',
128 b'rebuild',
128 b'rebuild',
129 None,
129 None,
130 _(b'rebuild cache even if it exists ' b'(EXPERIMENTAL)'),
130 _(b'rebuild cache even if it exists ' b'(EXPERIMENTAL)'),
131 ),
131 ),
132 ]
132 ]
133 + commands.diffwsopts
133 + commands.diffwsopts
134 + commands.walkopts
134 + commands.walkopts
135 + commands.formatteropts,
135 + commands.formatteropts,
136 r'synopsis': _(b'[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
136 r'synopsis': _(b'[-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...'),
137 r'inferrepo': True,
137 r'inferrepo': True,
138 }
138 }
139
139
140
140
141 def fastannotate(ui, repo, *pats, **opts):
141 def fastannotate(ui, repo, *pats, **opts):
142 """show changeset information by line for each file
142 """show changeset information by line for each file
143
143
144 List changes in files, showing the revision id responsible for each line.
144 List changes in files, showing the revision id responsible for each line.
145
145
146 This command is useful for discovering when a change was made and by whom.
146 This command is useful for discovering when a change was made and by whom.
147
147
148 By default this command prints revision numbers. If you include --file,
148 By default this command prints revision numbers. If you include --file,
149 --user, or --date, the revision number is suppressed unless you also
149 --user, or --date, the revision number is suppressed unless you also
150 include --number. The default format can also be customized by setting
150 include --number. The default format can also be customized by setting
151 fastannotate.defaultformat.
151 fastannotate.defaultformat.
152
152
153 Returns 0 on success.
153 Returns 0 on success.
154
154
155 .. container:: verbose
155 .. container:: verbose
156
156
157 This command uses an implementation different from the vanilla annotate
157 This command uses an implementation different from the vanilla annotate
158 command, which may produce slightly different (while still reasonable)
158 command, which may produce slightly different (while still reasonable)
159 outputs for some cases.
159 outputs for some cases.
160
160
161 Unlike the vanilla anootate, fastannotate follows rename regardless of
161 Unlike the vanilla anootate, fastannotate follows rename regardless of
162 the existence of --file.
162 the existence of --file.
163
163
164 For the best performance when running on a full repo, use -c, -l,
164 For the best performance when running on a full repo, use -c, -l,
165 avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
165 avoid -u, -d, -n. Use --linear and --no-content to make it even faster.
166
166
167 For the best performance when running on a shallow (remotefilelog)
167 For the best performance when running on a shallow (remotefilelog)
168 repo, avoid --linear, --no-follow, or any diff options. As the server
168 repo, avoid --linear, --no-follow, or any diff options. As the server
169 won't be able to populate annotate cache when non-default options
169 won't be able to populate annotate cache when non-default options
170 affecting results are used.
170 affecting results are used.
171 """
171 """
172 if not pats:
172 if not pats:
173 raise error.Abort(_(b'at least one filename or pattern is required'))
173 raise error.Abort(_(b'at least one filename or pattern is required'))
174
174
175 # performance hack: filtered repo can be slow. unfilter by default.
175 # performance hack: filtered repo can be slow. unfilter by default.
176 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
176 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
177 repo = repo.unfiltered()
177 repo = repo.unfiltered()
178
178
179 opts = pycompat.byteskwargs(opts)
179 opts = pycompat.byteskwargs(opts)
180
180
181 rev = opts.get(b'rev', b'.')
181 rev = opts.get(b'rev', b'.')
182 rebuild = opts.get(b'rebuild', False)
182 rebuild = opts.get(b'rebuild', False)
183
183
184 diffopts = patch.difffeatureopts(
184 diffopts = patch.difffeatureopts(
185 ui, opts, section=b'annotate', whitespace=True
185 ui, opts, section=b'annotate', whitespace=True
186 )
186 )
187 aopts = facontext.annotateopts(
187 aopts = facontext.annotateopts(
188 diffopts=diffopts,
188 diffopts=diffopts,
189 followmerge=not opts.get(b'linear', False),
189 followmerge=not opts.get(b'linear', False),
190 followrename=not opts.get(b'no_follow', False),
190 followrename=not opts.get(b'no_follow', False),
191 )
191 )
192
192
193 if not any(
193 if not any(
194 opts.get(s)
194 opts.get(s)
195 for s in [b'user', b'date', b'file', b'number', b'changeset']
195 for s in [b'user', b'date', b'file', b'number', b'changeset']
196 ):
196 ):
197 # default 'number' for compatibility. but fastannotate is more
197 # default 'number' for compatibility. but fastannotate is more
198 # efficient with "changeset", "line-number" and "no-content".
198 # efficient with "changeset", "line-number" and "no-content".
199 for name in ui.configlist(
199 for name in ui.configlist(
200 b'fastannotate', b'defaultformat', [b'number']
200 b'fastannotate', b'defaultformat', [b'number']
201 ):
201 ):
202 opts[name] = True
202 opts[name] = True
203
203
204 ui.pager(b'fastannotate')
204 ui.pager(b'fastannotate')
205 template = opts.get(b'template')
205 template = opts.get(b'template')
206 if template == b'json':
206 if template == b'json':
207 formatter = faformatter.jsonformatter(ui, repo, opts)
207 formatter = faformatter.jsonformatter(ui, repo, opts)
208 else:
208 else:
209 formatter = faformatter.defaultformatter(ui, repo, opts)
209 formatter = faformatter.defaultformatter(ui, repo, opts)
210 showdeleted = opts.get(b'deleted', False)
210 showdeleted = opts.get(b'deleted', False)
211 showlines = not bool(opts.get(b'no_content'))
211 showlines = not bool(opts.get(b'no_content'))
212 showpath = opts.get(b'file', False)
212 showpath = opts.get(b'file', False)
213
213
214 # find the head of the main (master) branch
214 # find the head of the main (master) branch
215 master = ui.config(b'fastannotate', b'mainbranch') or rev
215 master = ui.config(b'fastannotate', b'mainbranch') or rev
216
216
217 # paths will be used for prefetching and the real annotating
217 # paths will be used for prefetching and the real annotating
218 paths = list(_matchpaths(repo, rev, pats, opts, aopts))
218 paths = list(_matchpaths(repo, rev, pats, opts, aopts))
219
219
220 # for client, prefetch from the server
220 # for client, prefetch from the server
221 if util.safehasattr(repo, b'prefetchfastannotate'):
221 if util.safehasattr(repo, 'prefetchfastannotate'):
222 repo.prefetchfastannotate(paths)
222 repo.prefetchfastannotate(paths)
223
223
224 for path in paths:
224 for path in paths:
225 result = lines = existinglines = None
225 result = lines = existinglines = None
226 while True:
226 while True:
227 try:
227 try:
228 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
228 with facontext.annotatecontext(repo, path, aopts, rebuild) as a:
229 result = a.annotate(
229 result = a.annotate(
230 rev,
230 rev,
231 master=master,
231 master=master,
232 showpath=showpath,
232 showpath=showpath,
233 showlines=(showlines and not showdeleted),
233 showlines=(showlines and not showdeleted),
234 )
234 )
235 if showdeleted:
235 if showdeleted:
236 existinglines = set((l[0], l[1]) for l in result)
236 existinglines = set((l[0], l[1]) for l in result)
237 result = a.annotatealllines(
237 result = a.annotatealllines(
238 rev, showpath=showpath, showlines=showlines
238 rev, showpath=showpath, showlines=showlines
239 )
239 )
240 break
240 break
241 except (faerror.CannotReuseError, faerror.CorruptedFileError):
241 except (faerror.CannotReuseError, faerror.CorruptedFileError):
242 # happens if master moves backwards, or the file was deleted
242 # happens if master moves backwards, or the file was deleted
243 # and readded, or renamed to an existing name, or corrupted.
243 # and readded, or renamed to an existing name, or corrupted.
244 if rebuild: # give up since we have tried rebuild already
244 if rebuild: # give up since we have tried rebuild already
245 raise
245 raise
246 else: # try a second time rebuilding the cache (slow)
246 else: # try a second time rebuilding the cache (slow)
247 rebuild = True
247 rebuild = True
248 continue
248 continue
249
249
250 if showlines:
250 if showlines:
251 result, lines = result
251 result, lines = result
252
252
253 formatter.write(result, lines, existinglines=existinglines)
253 formatter.write(result, lines, existinglines=existinglines)
254 formatter.end()
254 formatter.end()
255
255
256
256
257 _newopts = set()
257 _newopts = set()
258 _knownopts = {
258 _knownopts = {
259 opt[1].replace(b'-', b'_')
259 opt[1].replace(b'-', b'_')
260 for opt in (fastannotatecommandargs[r'options'] + commands.globalopts)
260 for opt in (fastannotatecommandargs[r'options'] + commands.globalopts)
261 }
261 }
262
262
263
263
264 def _annotatewrapper(orig, ui, repo, *pats, **opts):
264 def _annotatewrapper(orig, ui, repo, *pats, **opts):
265 """used by wrapdefault"""
265 """used by wrapdefault"""
266 # we need this hack until the obsstore has 0.0 seconds perf impact
266 # we need this hack until the obsstore has 0.0 seconds perf impact
267 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
267 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
268 repo = repo.unfiltered()
268 repo = repo.unfiltered()
269
269
270 # treat the file as text (skip the isbinary check)
270 # treat the file as text (skip the isbinary check)
271 if ui.configbool(b'fastannotate', b'forcetext'):
271 if ui.configbool(b'fastannotate', b'forcetext'):
272 opts[r'text'] = True
272 opts[r'text'] = True
273
273
274 # check if we need to do prefetch (client-side)
274 # check if we need to do prefetch (client-side)
275 rev = opts.get(r'rev')
275 rev = opts.get(r'rev')
276 if util.safehasattr(repo, b'prefetchfastannotate') and rev is not None:
276 if util.safehasattr(repo, 'prefetchfastannotate') and rev is not None:
277 paths = list(_matchpaths(repo, rev, pats, pycompat.byteskwargs(opts)))
277 paths = list(_matchpaths(repo, rev, pats, pycompat.byteskwargs(opts)))
278 repo.prefetchfastannotate(paths)
278 repo.prefetchfastannotate(paths)
279
279
280 return orig(ui, repo, *pats, **opts)
280 return orig(ui, repo, *pats, **opts)
281
281
282
282
283 def registercommand():
283 def registercommand():
284 """register the fastannotate command"""
284 """register the fastannotate command"""
285 name = b'fastannotate|fastblame|fa'
285 name = b'fastannotate|fastblame|fa'
286 command(name, helpbasic=True, **fastannotatecommandargs)(fastannotate)
286 command(name, helpbasic=True, **fastannotatecommandargs)(fastannotate)
287
287
288
288
289 def wrapdefault():
289 def wrapdefault():
290 """wrap the default annotate command, to be aware of the protocol"""
290 """wrap the default annotate command, to be aware of the protocol"""
291 extensions.wrapcommand(commands.table, b'annotate', _annotatewrapper)
291 extensions.wrapcommand(commands.table, b'annotate', _annotatewrapper)
292
292
293
293
294 @command(
294 @command(
295 b'debugbuildannotatecache',
295 b'debugbuildannotatecache',
296 [(b'r', b'rev', b'', _(b'build up to the specific revision'), _(b'REV'))]
296 [(b'r', b'rev', b'', _(b'build up to the specific revision'), _(b'REV'))]
297 + commands.walkopts,
297 + commands.walkopts,
298 _(b'[-r REV] FILE...'),
298 _(b'[-r REV] FILE...'),
299 )
299 )
300 def debugbuildannotatecache(ui, repo, *pats, **opts):
300 def debugbuildannotatecache(ui, repo, *pats, **opts):
301 """incrementally build fastannotate cache up to REV for specified files
301 """incrementally build fastannotate cache up to REV for specified files
302
302
303 If REV is not specified, use the config 'fastannotate.mainbranch'.
303 If REV is not specified, use the config 'fastannotate.mainbranch'.
304
304
305 If fastannotate.client is True, download the annotate cache from the
305 If fastannotate.client is True, download the annotate cache from the
306 server. Otherwise, build the annotate cache locally.
306 server. Otherwise, build the annotate cache locally.
307
307
308 The annotate cache will be built using the default diff and follow
308 The annotate cache will be built using the default diff and follow
309 options and lives in '.hg/fastannotate/default'.
309 options and lives in '.hg/fastannotate/default'.
310 """
310 """
311 opts = pycompat.byteskwargs(opts)
311 opts = pycompat.byteskwargs(opts)
312 rev = opts.get(b'REV') or ui.config(b'fastannotate', b'mainbranch')
312 rev = opts.get(b'REV') or ui.config(b'fastannotate', b'mainbranch')
313 if not rev:
313 if not rev:
314 raise error.Abort(
314 raise error.Abort(
315 _(b'you need to provide a revision'),
315 _(b'you need to provide a revision'),
316 hint=_(b'set fastannotate.mainbranch or use --rev'),
316 hint=_(b'set fastannotate.mainbranch or use --rev'),
317 )
317 )
318 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
318 if ui.configbool(b'fastannotate', b'unfilteredrepo'):
319 repo = repo.unfiltered()
319 repo = repo.unfiltered()
320 ctx = scmutil.revsingle(repo, rev)
320 ctx = scmutil.revsingle(repo, rev)
321 m = scmutil.match(ctx, pats, opts)
321 m = scmutil.match(ctx, pats, opts)
322 paths = list(ctx.walk(m))
322 paths = list(ctx.walk(m))
323 if util.safehasattr(repo, b'prefetchfastannotate'):
323 if util.safehasattr(repo, 'prefetchfastannotate'):
324 # client
324 # client
325 if opts.get(b'REV'):
325 if opts.get(b'REV'):
326 raise error.Abort(_(b'--rev cannot be used for client'))
326 raise error.Abort(_(b'--rev cannot be used for client'))
327 repo.prefetchfastannotate(paths)
327 repo.prefetchfastannotate(paths)
328 else:
328 else:
329 # server, or full repo
329 # server, or full repo
330 progress = ui.makeprogress(_(b'building'), total=len(paths))
330 progress = ui.makeprogress(_(b'building'), total=len(paths))
331 for i, path in enumerate(paths):
331 for i, path in enumerate(paths):
332 progress.update(i)
332 progress.update(i)
333 with facontext.annotatecontext(repo, path) as actx:
333 with facontext.annotatecontext(repo, path) as actx:
334 try:
334 try:
335 if actx.isuptodate(rev):
335 if actx.isuptodate(rev):
336 continue
336 continue
337 actx.annotate(rev, rev)
337 actx.annotate(rev, rev)
338 except (faerror.CannotReuseError, faerror.CorruptedFileError):
338 except (faerror.CannotReuseError, faerror.CorruptedFileError):
339 # the cache is broken (could happen with renaming so the
339 # the cache is broken (could happen with renaming so the
340 # file history gets invalidated). rebuild and try again.
340 # file history gets invalidated). rebuild and try again.
341 ui.debug(
341 ui.debug(
342 b'fastannotate: %s: rebuilding broken cache\n' % path
342 b'fastannotate: %s: rebuilding broken cache\n' % path
343 )
343 )
344 actx.rebuild()
344 actx.rebuild()
345 try:
345 try:
346 actx.annotate(rev, rev)
346 actx.annotate(rev, rev)
347 except Exception as ex:
347 except Exception as ex:
348 # possibly a bug, but should not stop us from building
348 # possibly a bug, but should not stop us from building
349 # cache for other files.
349 # cache for other files.
350 ui.warn(
350 ui.warn(
351 _(
351 _(
352 b'fastannotate: %s: failed to '
352 b'fastannotate: %s: failed to '
353 b'build cache: %r\n'
353 b'build cache: %r\n'
354 )
354 )
355 % (path, ex)
355 % (path, ex)
356 )
356 )
357 progress.complete()
357 progress.complete()
@@ -1,118 +1,118 b''
1 # watchmanclient.py - Watchman client for the fsmonitor extension
1 # watchmanclient.py - Watchman client for the fsmonitor extension
2 #
2 #
3 # Copyright 2013-2016 Facebook, Inc.
3 # Copyright 2013-2016 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import getpass
10 import getpass
11
11
12 from mercurial import util
12 from mercurial import util
13
13
14 from . import pywatchman
14 from . import pywatchman
15
15
16
16
17 class Unavailable(Exception):
17 class Unavailable(Exception):
18 def __init__(self, msg, warn=True, invalidate=False):
18 def __init__(self, msg, warn=True, invalidate=False):
19 self.msg = msg
19 self.msg = msg
20 self.warn = warn
20 self.warn = warn
21 if self.msg == b'timed out waiting for response':
21 if self.msg == b'timed out waiting for response':
22 self.warn = False
22 self.warn = False
23 self.invalidate = invalidate
23 self.invalidate = invalidate
24
24
25 def __str__(self):
25 def __str__(self):
26 if self.warn:
26 if self.warn:
27 return b'warning: Watchman unavailable: %s' % self.msg
27 return b'warning: Watchman unavailable: %s' % self.msg
28 else:
28 else:
29 return b'Watchman unavailable: %s' % self.msg
29 return b'Watchman unavailable: %s' % self.msg
30
30
31
31
32 class WatchmanNoRoot(Unavailable):
32 class WatchmanNoRoot(Unavailable):
33 def __init__(self, root, msg):
33 def __init__(self, root, msg):
34 self.root = root
34 self.root = root
35 super(WatchmanNoRoot, self).__init__(msg)
35 super(WatchmanNoRoot, self).__init__(msg)
36
36
37
37
38 class client(object):
38 class client(object):
39 def __init__(self, ui, root, timeout=1.0):
39 def __init__(self, ui, root, timeout=1.0):
40 err = None
40 err = None
41 if not self._user:
41 if not self._user:
42 err = b"couldn't get user"
42 err = b"couldn't get user"
43 warn = True
43 warn = True
44 if self._user in ui.configlist(b'fsmonitor', b'blacklistusers'):
44 if self._user in ui.configlist(b'fsmonitor', b'blacklistusers'):
45 err = b'user %s in blacklist' % self._user
45 err = b'user %s in blacklist' % self._user
46 warn = False
46 warn = False
47
47
48 if err:
48 if err:
49 raise Unavailable(err, warn)
49 raise Unavailable(err, warn)
50
50
51 self._timeout = timeout
51 self._timeout = timeout
52 self._watchmanclient = None
52 self._watchmanclient = None
53 self._root = root
53 self._root = root
54 self._ui = ui
54 self._ui = ui
55 self._firsttime = True
55 self._firsttime = True
56
56
57 def settimeout(self, timeout):
57 def settimeout(self, timeout):
58 self._timeout = timeout
58 self._timeout = timeout
59 if self._watchmanclient is not None:
59 if self._watchmanclient is not None:
60 self._watchmanclient.setTimeout(timeout)
60 self._watchmanclient.setTimeout(timeout)
61
61
62 def getcurrentclock(self):
62 def getcurrentclock(self):
63 result = self.command(b'clock')
63 result = self.command(b'clock')
64 if not util.safehasattr(result, b'clock'):
64 if not util.safehasattr(result, 'clock'):
65 raise Unavailable(
65 raise Unavailable(
66 b'clock result is missing clock value', invalidate=True
66 b'clock result is missing clock value', invalidate=True
67 )
67 )
68 return result.clock
68 return result.clock
69
69
70 def clearconnection(self):
70 def clearconnection(self):
71 self._watchmanclient = None
71 self._watchmanclient = None
72
72
73 def available(self):
73 def available(self):
74 return self._watchmanclient is not None or self._firsttime
74 return self._watchmanclient is not None or self._firsttime
75
75
76 @util.propertycache
76 @util.propertycache
77 def _user(self):
77 def _user(self):
78 try:
78 try:
79 return getpass.getuser()
79 return getpass.getuser()
80 except KeyError:
80 except KeyError:
81 # couldn't figure out our user
81 # couldn't figure out our user
82 return None
82 return None
83
83
84 def _command(self, *args):
84 def _command(self, *args):
85 watchmanargs = (args[0], self._root) + args[1:]
85 watchmanargs = (args[0], self._root) + args[1:]
86 try:
86 try:
87 if self._watchmanclient is None:
87 if self._watchmanclient is None:
88 self._firsttime = False
88 self._firsttime = False
89 watchman_exe = self._ui.configpath(
89 watchman_exe = self._ui.configpath(
90 b'fsmonitor', b'watchman_exe'
90 b'fsmonitor', b'watchman_exe'
91 )
91 )
92 self._watchmanclient = pywatchman.client(
92 self._watchmanclient = pywatchman.client(
93 timeout=self._timeout,
93 timeout=self._timeout,
94 useImmutableBser=True,
94 useImmutableBser=True,
95 watchman_exe=watchman_exe,
95 watchman_exe=watchman_exe,
96 )
96 )
97 return self._watchmanclient.query(*watchmanargs)
97 return self._watchmanclient.query(*watchmanargs)
98 except pywatchman.CommandError as ex:
98 except pywatchman.CommandError as ex:
99 if b'unable to resolve root' in ex.msg:
99 if b'unable to resolve root' in ex.msg:
100 raise WatchmanNoRoot(self._root, ex.msg)
100 raise WatchmanNoRoot(self._root, ex.msg)
101 raise Unavailable(ex.msg)
101 raise Unavailable(ex.msg)
102 except pywatchman.WatchmanError as ex:
102 except pywatchman.WatchmanError as ex:
103 raise Unavailable(str(ex))
103 raise Unavailable(str(ex))
104
104
105 def command(self, *args):
105 def command(self, *args):
106 try:
106 try:
107 try:
107 try:
108 return self._command(*args)
108 return self._command(*args)
109 except WatchmanNoRoot:
109 except WatchmanNoRoot:
110 # this 'watch' command can also raise a WatchmanNoRoot if
110 # this 'watch' command can also raise a WatchmanNoRoot if
111 # watchman refuses to accept this root
111 # watchman refuses to accept this root
112 self._command(b'watch')
112 self._command(b'watch')
113 return self._command(*args)
113 return self._command(*args)
114 except Unavailable:
114 except Unavailable:
115 # this is in an outer scope to catch Unavailable form any of the
115 # this is in an outer scope to catch Unavailable form any of the
116 # above _command calls
116 # above _command calls
117 self._watchmanclient = None
117 self._watchmanclient = None
118 raise
118 raise
@@ -1,604 +1,604 b''
1 # journal.py
1 # journal.py
2 #
2 #
3 # Copyright 2014-2016 Facebook, Inc.
3 # Copyright 2014-2016 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """track previous positions of bookmarks (EXPERIMENTAL)
7 """track previous positions of bookmarks (EXPERIMENTAL)
8
8
9 This extension adds a new command: `hg journal`, which shows you where
9 This extension adds a new command: `hg journal`, which shows you where
10 bookmarks were previously located.
10 bookmarks were previously located.
11
11
12 """
12 """
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import collections
16 import collections
17 import errno
17 import errno
18 import os
18 import os
19 import weakref
19 import weakref
20
20
21 from mercurial.i18n import _
21 from mercurial.i18n import _
22
22
23 from mercurial import (
23 from mercurial import (
24 bookmarks,
24 bookmarks,
25 cmdutil,
25 cmdutil,
26 dispatch,
26 dispatch,
27 encoding,
27 encoding,
28 error,
28 error,
29 extensions,
29 extensions,
30 hg,
30 hg,
31 localrepo,
31 localrepo,
32 lock,
32 lock,
33 logcmdutil,
33 logcmdutil,
34 node,
34 node,
35 pycompat,
35 pycompat,
36 registrar,
36 registrar,
37 util,
37 util,
38 )
38 )
39 from mercurial.utils import (
39 from mercurial.utils import (
40 dateutil,
40 dateutil,
41 procutil,
41 procutil,
42 stringutil,
42 stringutil,
43 )
43 )
44
44
45 cmdtable = {}
45 cmdtable = {}
46 command = registrar.command(cmdtable)
46 command = registrar.command(cmdtable)
47
47
48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
48 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
49 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
50 # be specifying the version(s) of Mercurial they are tested with, or
50 # be specifying the version(s) of Mercurial they are tested with, or
51 # leave the attribute unspecified.
51 # leave the attribute unspecified.
52 testedwith = b'ships-with-hg-core'
52 testedwith = b'ships-with-hg-core'
53
53
54 # storage format version; increment when the format changes
54 # storage format version; increment when the format changes
55 storageversion = 0
55 storageversion = 0
56
56
57 # namespaces
57 # namespaces
58 bookmarktype = b'bookmark'
58 bookmarktype = b'bookmark'
59 wdirparenttype = b'wdirparent'
59 wdirparenttype = b'wdirparent'
60 # In a shared repository, what shared feature name is used
60 # In a shared repository, what shared feature name is used
61 # to indicate this namespace is shared with the source?
61 # to indicate this namespace is shared with the source?
62 sharednamespaces = {
62 sharednamespaces = {
63 bookmarktype: hg.sharedbookmarks,
63 bookmarktype: hg.sharedbookmarks,
64 }
64 }
65
65
66 # Journal recording, register hooks and storage object
66 # Journal recording, register hooks and storage object
67 def extsetup(ui):
67 def extsetup(ui):
68 extensions.wrapfunction(dispatch, b'runcommand', runcommand)
68 extensions.wrapfunction(dispatch, b'runcommand', runcommand)
69 extensions.wrapfunction(bookmarks.bmstore, b'_write', recordbookmarks)
69 extensions.wrapfunction(bookmarks.bmstore, b'_write', recordbookmarks)
70 extensions.wrapfilecache(
70 extensions.wrapfilecache(
71 localrepo.localrepository, b'dirstate', wrapdirstate
71 localrepo.localrepository, b'dirstate', wrapdirstate
72 )
72 )
73 extensions.wrapfunction(hg, b'postshare', wrappostshare)
73 extensions.wrapfunction(hg, b'postshare', wrappostshare)
74 extensions.wrapfunction(hg, b'copystore', unsharejournal)
74 extensions.wrapfunction(hg, b'copystore', unsharejournal)
75
75
76
76
77 def reposetup(ui, repo):
77 def reposetup(ui, repo):
78 if repo.local():
78 if repo.local():
79 repo.journal = journalstorage(repo)
79 repo.journal = journalstorage(repo)
80 repo._wlockfreeprefix.add(b'namejournal')
80 repo._wlockfreeprefix.add(b'namejournal')
81
81
82 dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
82 dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
83 if cached:
83 if cached:
84 # already instantiated dirstate isn't yet marked as
84 # already instantiated dirstate isn't yet marked as
85 # "journal"-ing, even though repo.dirstate() was already
85 # "journal"-ing, even though repo.dirstate() was already
86 # wrapped by own wrapdirstate()
86 # wrapped by own wrapdirstate()
87 _setupdirstate(repo, dirstate)
87 _setupdirstate(repo, dirstate)
88
88
89
89
90 def runcommand(orig, lui, repo, cmd, fullargs, *args):
90 def runcommand(orig, lui, repo, cmd, fullargs, *args):
91 """Track the command line options for recording in the journal"""
91 """Track the command line options for recording in the journal"""
92 journalstorage.recordcommand(*fullargs)
92 journalstorage.recordcommand(*fullargs)
93 return orig(lui, repo, cmd, fullargs, *args)
93 return orig(lui, repo, cmd, fullargs, *args)
94
94
95
95
96 def _setupdirstate(repo, dirstate):
96 def _setupdirstate(repo, dirstate):
97 dirstate.journalstorage = repo.journal
97 dirstate.journalstorage = repo.journal
98 dirstate.addparentchangecallback(b'journal', recorddirstateparents)
98 dirstate.addparentchangecallback(b'journal', recorddirstateparents)
99
99
100
100
101 # hooks to record dirstate changes
101 # hooks to record dirstate changes
102 def wrapdirstate(orig, repo):
102 def wrapdirstate(orig, repo):
103 """Make journal storage available to the dirstate object"""
103 """Make journal storage available to the dirstate object"""
104 dirstate = orig(repo)
104 dirstate = orig(repo)
105 if util.safehasattr(repo, b'journal'):
105 if util.safehasattr(repo, 'journal'):
106 _setupdirstate(repo, dirstate)
106 _setupdirstate(repo, dirstate)
107 return dirstate
107 return dirstate
108
108
109
109
110 def recorddirstateparents(dirstate, old, new):
110 def recorddirstateparents(dirstate, old, new):
111 """Records all dirstate parent changes in the journal."""
111 """Records all dirstate parent changes in the journal."""
112 old = list(old)
112 old = list(old)
113 new = list(new)
113 new = list(new)
114 if util.safehasattr(dirstate, b'journalstorage'):
114 if util.safehasattr(dirstate, 'journalstorage'):
115 # only record two hashes if there was a merge
115 # only record two hashes if there was a merge
116 oldhashes = old[:1] if old[1] == node.nullid else old
116 oldhashes = old[:1] if old[1] == node.nullid else old
117 newhashes = new[:1] if new[1] == node.nullid else new
117 newhashes = new[:1] if new[1] == node.nullid else new
118 dirstate.journalstorage.record(
118 dirstate.journalstorage.record(
119 wdirparenttype, b'.', oldhashes, newhashes
119 wdirparenttype, b'.', oldhashes, newhashes
120 )
120 )
121
121
122
122
123 # hooks to record bookmark changes (both local and remote)
123 # hooks to record bookmark changes (both local and remote)
124 def recordbookmarks(orig, store, fp):
124 def recordbookmarks(orig, store, fp):
125 """Records all bookmark changes in the journal."""
125 """Records all bookmark changes in the journal."""
126 repo = store._repo
126 repo = store._repo
127 if util.safehasattr(repo, b'journal'):
127 if util.safehasattr(repo, 'journal'):
128 oldmarks = bookmarks.bmstore(repo)
128 oldmarks = bookmarks.bmstore(repo)
129 for mark, value in pycompat.iteritems(store):
129 for mark, value in pycompat.iteritems(store):
130 oldvalue = oldmarks.get(mark, node.nullid)
130 oldvalue = oldmarks.get(mark, node.nullid)
131 if value != oldvalue:
131 if value != oldvalue:
132 repo.journal.record(bookmarktype, mark, oldvalue, value)
132 repo.journal.record(bookmarktype, mark, oldvalue, value)
133 return orig(store, fp)
133 return orig(store, fp)
134
134
135
135
136 # shared repository support
136 # shared repository support
137 def _readsharedfeatures(repo):
137 def _readsharedfeatures(repo):
138 """A set of shared features for this repository"""
138 """A set of shared features for this repository"""
139 try:
139 try:
140 return set(repo.vfs.read(b'shared').splitlines())
140 return set(repo.vfs.read(b'shared').splitlines())
141 except IOError as inst:
141 except IOError as inst:
142 if inst.errno != errno.ENOENT:
142 if inst.errno != errno.ENOENT:
143 raise
143 raise
144 return set()
144 return set()
145
145
146
146
147 def _mergeentriesiter(*iterables, **kwargs):
147 def _mergeentriesiter(*iterables, **kwargs):
148 """Given a set of sorted iterables, yield the next entry in merged order
148 """Given a set of sorted iterables, yield the next entry in merged order
149
149
150 Note that by default entries go from most recent to oldest.
150 Note that by default entries go from most recent to oldest.
151 """
151 """
152 order = kwargs.pop(r'order', max)
152 order = kwargs.pop(r'order', max)
153 iterables = [iter(it) for it in iterables]
153 iterables = [iter(it) for it in iterables]
154 # this tracks still active iterables; iterables are deleted as they are
154 # this tracks still active iterables; iterables are deleted as they are
155 # exhausted, which is why this is a dictionary and why each entry also
155 # exhausted, which is why this is a dictionary and why each entry also
156 # stores the key. Entries are mutable so we can store the next value each
156 # stores the key. Entries are mutable so we can store the next value each
157 # time.
157 # time.
158 iterable_map = {}
158 iterable_map = {}
159 for key, it in enumerate(iterables):
159 for key, it in enumerate(iterables):
160 try:
160 try:
161 iterable_map[key] = [next(it), key, it]
161 iterable_map[key] = [next(it), key, it]
162 except StopIteration:
162 except StopIteration:
163 # empty entry, can be ignored
163 # empty entry, can be ignored
164 pass
164 pass
165
165
166 while iterable_map:
166 while iterable_map:
167 value, key, it = order(pycompat.itervalues(iterable_map))
167 value, key, it = order(pycompat.itervalues(iterable_map))
168 yield value
168 yield value
169 try:
169 try:
170 iterable_map[key][0] = next(it)
170 iterable_map[key][0] = next(it)
171 except StopIteration:
171 except StopIteration:
172 # this iterable is empty, remove it from consideration
172 # this iterable is empty, remove it from consideration
173 del iterable_map[key]
173 del iterable_map[key]
174
174
175
175
176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
176 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
177 """Mark this shared working copy as sharing journal information"""
177 """Mark this shared working copy as sharing journal information"""
178 with destrepo.wlock():
178 with destrepo.wlock():
179 orig(sourcerepo, destrepo, **kwargs)
179 orig(sourcerepo, destrepo, **kwargs)
180 with destrepo.vfs(b'shared', b'a') as fp:
180 with destrepo.vfs(b'shared', b'a') as fp:
181 fp.write(b'journal\n')
181 fp.write(b'journal\n')
182
182
183
183
184 def unsharejournal(orig, ui, repo, repopath):
184 def unsharejournal(orig, ui, repo, repopath):
185 """Copy shared journal entries into this repo when unsharing"""
185 """Copy shared journal entries into this repo when unsharing"""
186 if (
186 if (
187 repo.path == repopath
187 repo.path == repopath
188 and repo.shared()
188 and repo.shared()
189 and util.safehasattr(repo, b'journal')
189 and util.safehasattr(repo, 'journal')
190 ):
190 ):
191 sharedrepo = hg.sharedreposource(repo)
191 sharedrepo = hg.sharedreposource(repo)
192 sharedfeatures = _readsharedfeatures(repo)
192 sharedfeatures = _readsharedfeatures(repo)
193 if sharedrepo and sharedfeatures > {b'journal'}:
193 if sharedrepo and sharedfeatures > {b'journal'}:
194 # there is a shared repository and there are shared journal entries
194 # there is a shared repository and there are shared journal entries
195 # to copy. move shared date over from source to destination but
195 # to copy. move shared date over from source to destination but
196 # move the local file first
196 # move the local file first
197 if repo.vfs.exists(b'namejournal'):
197 if repo.vfs.exists(b'namejournal'):
198 journalpath = repo.vfs.join(b'namejournal')
198 journalpath = repo.vfs.join(b'namejournal')
199 util.rename(journalpath, journalpath + b'.bak')
199 util.rename(journalpath, journalpath + b'.bak')
200 storage = repo.journal
200 storage = repo.journal
201 local = storage._open(
201 local = storage._open(
202 repo.vfs, filename=b'namejournal.bak', _newestfirst=False
202 repo.vfs, filename=b'namejournal.bak', _newestfirst=False
203 )
203 )
204 shared = (
204 shared = (
205 e
205 e
206 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
206 for e in storage._open(sharedrepo.vfs, _newestfirst=False)
207 if sharednamespaces.get(e.namespace) in sharedfeatures
207 if sharednamespaces.get(e.namespace) in sharedfeatures
208 )
208 )
209 for entry in _mergeentriesiter(local, shared, order=min):
209 for entry in _mergeentriesiter(local, shared, order=min):
210 storage._write(repo.vfs, entry)
210 storage._write(repo.vfs, entry)
211
211
212 return orig(ui, repo, repopath)
212 return orig(ui, repo, repopath)
213
213
214
214
215 class journalentry(
215 class journalentry(
216 collections.namedtuple(
216 collections.namedtuple(
217 r'journalentry',
217 r'journalentry',
218 r'timestamp user command namespace name oldhashes newhashes',
218 r'timestamp user command namespace name oldhashes newhashes',
219 )
219 )
220 ):
220 ):
221 """Individual journal entry
221 """Individual journal entry
222
222
223 * timestamp: a mercurial (time, timezone) tuple
223 * timestamp: a mercurial (time, timezone) tuple
224 * user: the username that ran the command
224 * user: the username that ran the command
225 * namespace: the entry namespace, an opaque string
225 * namespace: the entry namespace, an opaque string
226 * name: the name of the changed item, opaque string with meaning in the
226 * name: the name of the changed item, opaque string with meaning in the
227 namespace
227 namespace
228 * command: the hg command that triggered this record
228 * command: the hg command that triggered this record
229 * oldhashes: a tuple of one or more binary hashes for the old location
229 * oldhashes: a tuple of one or more binary hashes for the old location
230 * newhashes: a tuple of one or more binary hashes for the new location
230 * newhashes: a tuple of one or more binary hashes for the new location
231
231
232 Handles serialisation from and to the storage format. Fields are
232 Handles serialisation from and to the storage format. Fields are
233 separated by newlines, hashes are written out in hex separated by commas,
233 separated by newlines, hashes are written out in hex separated by commas,
234 timestamp and timezone are separated by a space.
234 timestamp and timezone are separated by a space.
235
235
236 """
236 """
237
237
238 @classmethod
238 @classmethod
239 def fromstorage(cls, line):
239 def fromstorage(cls, line):
240 (
240 (
241 time,
241 time,
242 user,
242 user,
243 command,
243 command,
244 namespace,
244 namespace,
245 name,
245 name,
246 oldhashes,
246 oldhashes,
247 newhashes,
247 newhashes,
248 ) = line.split(b'\n')
248 ) = line.split(b'\n')
249 timestamp, tz = time.split()
249 timestamp, tz = time.split()
250 timestamp, tz = float(timestamp), int(tz)
250 timestamp, tz = float(timestamp), int(tz)
251 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(b','))
251 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(b','))
252 newhashes = tuple(node.bin(hash) for hash in newhashes.split(b','))
252 newhashes = tuple(node.bin(hash) for hash in newhashes.split(b','))
253 return cls(
253 return cls(
254 (timestamp, tz),
254 (timestamp, tz),
255 user,
255 user,
256 command,
256 command,
257 namespace,
257 namespace,
258 name,
258 name,
259 oldhashes,
259 oldhashes,
260 newhashes,
260 newhashes,
261 )
261 )
262
262
263 def __bytes__(self):
263 def __bytes__(self):
264 """bytes representation for storage"""
264 """bytes representation for storage"""
265 time = b' '.join(map(pycompat.bytestr, self.timestamp))
265 time = b' '.join(map(pycompat.bytestr, self.timestamp))
266 oldhashes = b','.join([node.hex(hash) for hash in self.oldhashes])
266 oldhashes = b','.join([node.hex(hash) for hash in self.oldhashes])
267 newhashes = b','.join([node.hex(hash) for hash in self.newhashes])
267 newhashes = b','.join([node.hex(hash) for hash in self.newhashes])
268 return b'\n'.join(
268 return b'\n'.join(
269 (
269 (
270 time,
270 time,
271 self.user,
271 self.user,
272 self.command,
272 self.command,
273 self.namespace,
273 self.namespace,
274 self.name,
274 self.name,
275 oldhashes,
275 oldhashes,
276 newhashes,
276 newhashes,
277 )
277 )
278 )
278 )
279
279
280 __str__ = encoding.strmethod(__bytes__)
280 __str__ = encoding.strmethod(__bytes__)
281
281
282
282
283 class journalstorage(object):
283 class journalstorage(object):
284 """Storage for journal entries
284 """Storage for journal entries
285
285
286 Entries are divided over two files; one with entries that pertain to the
286 Entries are divided over two files; one with entries that pertain to the
287 local working copy *only*, and one with entries that are shared across
287 local working copy *only*, and one with entries that are shared across
288 multiple working copies when shared using the share extension.
288 multiple working copies when shared using the share extension.
289
289
290 Entries are stored with NUL bytes as separators. See the journalentry
290 Entries are stored with NUL bytes as separators. See the journalentry
291 class for the per-entry structure.
291 class for the per-entry structure.
292
292
293 The file format starts with an integer version, delimited by a NUL.
293 The file format starts with an integer version, delimited by a NUL.
294
294
295 This storage uses a dedicated lock; this makes it easier to avoid issues
295 This storage uses a dedicated lock; this makes it easier to avoid issues
296 with adding entries that added when the regular wlock is unlocked (e.g.
296 with adding entries that added when the regular wlock is unlocked (e.g.
297 the dirstate).
297 the dirstate).
298
298
299 """
299 """
300
300
301 _currentcommand = ()
301 _currentcommand = ()
302 _lockref = None
302 _lockref = None
303
303
304 def __init__(self, repo):
304 def __init__(self, repo):
305 self.user = procutil.getuser()
305 self.user = procutil.getuser()
306 self.ui = repo.ui
306 self.ui = repo.ui
307 self.vfs = repo.vfs
307 self.vfs = repo.vfs
308
308
309 # is this working copy using a shared storage?
309 # is this working copy using a shared storage?
310 self.sharedfeatures = self.sharedvfs = None
310 self.sharedfeatures = self.sharedvfs = None
311 if repo.shared():
311 if repo.shared():
312 features = _readsharedfeatures(repo)
312 features = _readsharedfeatures(repo)
313 sharedrepo = hg.sharedreposource(repo)
313 sharedrepo = hg.sharedreposource(repo)
314 if sharedrepo is not None and b'journal' in features:
314 if sharedrepo is not None and b'journal' in features:
315 self.sharedvfs = sharedrepo.vfs
315 self.sharedvfs = sharedrepo.vfs
316 self.sharedfeatures = features
316 self.sharedfeatures = features
317
317
318 # track the current command for recording in journal entries
318 # track the current command for recording in journal entries
319 @property
319 @property
320 def command(self):
320 def command(self):
321 commandstr = b' '.join(
321 commandstr = b' '.join(
322 map(procutil.shellquote, journalstorage._currentcommand)
322 map(procutil.shellquote, journalstorage._currentcommand)
323 )
323 )
324 if b'\n' in commandstr:
324 if b'\n' in commandstr:
325 # truncate multi-line commands
325 # truncate multi-line commands
326 commandstr = commandstr.partition(b'\n')[0] + b' ...'
326 commandstr = commandstr.partition(b'\n')[0] + b' ...'
327 return commandstr
327 return commandstr
328
328
329 @classmethod
329 @classmethod
330 def recordcommand(cls, *fullargs):
330 def recordcommand(cls, *fullargs):
331 """Set the current hg arguments, stored with recorded entries"""
331 """Set the current hg arguments, stored with recorded entries"""
332 # Set the current command on the class because we may have started
332 # Set the current command on the class because we may have started
333 # with a non-local repo (cloning for example).
333 # with a non-local repo (cloning for example).
334 cls._currentcommand = fullargs
334 cls._currentcommand = fullargs
335
335
336 def _currentlock(self, lockref):
336 def _currentlock(self, lockref):
337 """Returns the lock if it's held, or None if it's not.
337 """Returns the lock if it's held, or None if it's not.
338
338
339 (This is copied from the localrepo class)
339 (This is copied from the localrepo class)
340 """
340 """
341 if lockref is None:
341 if lockref is None:
342 return None
342 return None
343 l = lockref()
343 l = lockref()
344 if l is None or not l.held:
344 if l is None or not l.held:
345 return None
345 return None
346 return l
346 return l
347
347
348 def jlock(self, vfs):
348 def jlock(self, vfs):
349 """Create a lock for the journal file"""
349 """Create a lock for the journal file"""
350 if self._currentlock(self._lockref) is not None:
350 if self._currentlock(self._lockref) is not None:
351 raise error.Abort(_(b'journal lock does not support nesting'))
351 raise error.Abort(_(b'journal lock does not support nesting'))
352 desc = _(b'journal of %s') % vfs.base
352 desc = _(b'journal of %s') % vfs.base
353 try:
353 try:
354 l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc)
354 l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc)
355 except error.LockHeld as inst:
355 except error.LockHeld as inst:
356 self.ui.warn(
356 self.ui.warn(
357 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker)
357 _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker)
358 )
358 )
359 # default to 600 seconds timeout
359 # default to 600 seconds timeout
360 l = lock.lock(
360 l = lock.lock(
361 vfs,
361 vfs,
362 b'namejournal.lock',
362 b'namejournal.lock',
363 self.ui.configint(b"ui", b"timeout"),
363 self.ui.configint(b"ui", b"timeout"),
364 desc=desc,
364 desc=desc,
365 )
365 )
366 self.ui.warn(_(b"got lock after %s seconds\n") % l.delay)
366 self.ui.warn(_(b"got lock after %s seconds\n") % l.delay)
367 self._lockref = weakref.ref(l)
367 self._lockref = weakref.ref(l)
368 return l
368 return l
369
369
370 def record(self, namespace, name, oldhashes, newhashes):
370 def record(self, namespace, name, oldhashes, newhashes):
371 """Record a new journal entry
371 """Record a new journal entry
372
372
373 * namespace: an opaque string; this can be used to filter on the type
373 * namespace: an opaque string; this can be used to filter on the type
374 of recorded entries.
374 of recorded entries.
375 * name: the name defining this entry; for bookmarks, this is the
375 * name: the name defining this entry; for bookmarks, this is the
376 bookmark name. Can be filtered on when retrieving entries.
376 bookmark name. Can be filtered on when retrieving entries.
377 * oldhashes and newhashes: each a single binary hash, or a list of
377 * oldhashes and newhashes: each a single binary hash, or a list of
378 binary hashes. These represent the old and new position of the named
378 binary hashes. These represent the old and new position of the named
379 item.
379 item.
380
380
381 """
381 """
382 if not isinstance(oldhashes, list):
382 if not isinstance(oldhashes, list):
383 oldhashes = [oldhashes]
383 oldhashes = [oldhashes]
384 if not isinstance(newhashes, list):
384 if not isinstance(newhashes, list):
385 newhashes = [newhashes]
385 newhashes = [newhashes]
386
386
387 entry = journalentry(
387 entry = journalentry(
388 dateutil.makedate(),
388 dateutil.makedate(),
389 self.user,
389 self.user,
390 self.command,
390 self.command,
391 namespace,
391 namespace,
392 name,
392 name,
393 oldhashes,
393 oldhashes,
394 newhashes,
394 newhashes,
395 )
395 )
396
396
397 vfs = self.vfs
397 vfs = self.vfs
398 if self.sharedvfs is not None:
398 if self.sharedvfs is not None:
399 # write to the shared repository if this feature is being
399 # write to the shared repository if this feature is being
400 # shared between working copies.
400 # shared between working copies.
401 if sharednamespaces.get(namespace) in self.sharedfeatures:
401 if sharednamespaces.get(namespace) in self.sharedfeatures:
402 vfs = self.sharedvfs
402 vfs = self.sharedvfs
403
403
404 self._write(vfs, entry)
404 self._write(vfs, entry)
405
405
406 def _write(self, vfs, entry):
406 def _write(self, vfs, entry):
407 with self.jlock(vfs):
407 with self.jlock(vfs):
408 # open file in amend mode to ensure it is created if missing
408 # open file in amend mode to ensure it is created if missing
409 with vfs(b'namejournal', mode=b'a+b') as f:
409 with vfs(b'namejournal', mode=b'a+b') as f:
410 f.seek(0, os.SEEK_SET)
410 f.seek(0, os.SEEK_SET)
411 # Read just enough bytes to get a version number (up to 2
411 # Read just enough bytes to get a version number (up to 2
412 # digits plus separator)
412 # digits plus separator)
413 version = f.read(3).partition(b'\0')[0]
413 version = f.read(3).partition(b'\0')[0]
414 if version and version != b"%d" % storageversion:
414 if version and version != b"%d" % storageversion:
415 # different version of the storage. Exit early (and not
415 # different version of the storage. Exit early (and not
416 # write anything) if this is not a version we can handle or
416 # write anything) if this is not a version we can handle or
417 # the file is corrupt. In future, perhaps rotate the file
417 # the file is corrupt. In future, perhaps rotate the file
418 # instead?
418 # instead?
419 self.ui.warn(
419 self.ui.warn(
420 _(b"unsupported journal file version '%s'\n") % version
420 _(b"unsupported journal file version '%s'\n") % version
421 )
421 )
422 return
422 return
423 if not version:
423 if not version:
424 # empty file, write version first
424 # empty file, write version first
425 f.write((b"%d" % storageversion) + b'\0')
425 f.write((b"%d" % storageversion) + b'\0')
426 f.seek(0, os.SEEK_END)
426 f.seek(0, os.SEEK_END)
427 f.write(bytes(entry) + b'\0')
427 f.write(bytes(entry) + b'\0')
428
428
429 def filtered(self, namespace=None, name=None):
429 def filtered(self, namespace=None, name=None):
430 """Yield all journal entries with the given namespace or name
430 """Yield all journal entries with the given namespace or name
431
431
432 Both the namespace and the name are optional; if neither is given all
432 Both the namespace and the name are optional; if neither is given all
433 entries in the journal are produced.
433 entries in the journal are produced.
434
434
435 Matching supports regular expressions by using the `re:` prefix
435 Matching supports regular expressions by using the `re:` prefix
436 (use `literal:` to match names or namespaces that start with `re:`)
436 (use `literal:` to match names or namespaces that start with `re:`)
437
437
438 """
438 """
439 if namespace is not None:
439 if namespace is not None:
440 namespace = stringutil.stringmatcher(namespace)[-1]
440 namespace = stringutil.stringmatcher(namespace)[-1]
441 if name is not None:
441 if name is not None:
442 name = stringutil.stringmatcher(name)[-1]
442 name = stringutil.stringmatcher(name)[-1]
443 for entry in self:
443 for entry in self:
444 if namespace is not None and not namespace(entry.namespace):
444 if namespace is not None and not namespace(entry.namespace):
445 continue
445 continue
446 if name is not None and not name(entry.name):
446 if name is not None and not name(entry.name):
447 continue
447 continue
448 yield entry
448 yield entry
449
449
450 def __iter__(self):
450 def __iter__(self):
451 """Iterate over the storage
451 """Iterate over the storage
452
452
453 Yields journalentry instances for each contained journal record.
453 Yields journalentry instances for each contained journal record.
454
454
455 """
455 """
456 local = self._open(self.vfs)
456 local = self._open(self.vfs)
457
457
458 if self.sharedvfs is None:
458 if self.sharedvfs is None:
459 return local
459 return local
460
460
461 # iterate over both local and shared entries, but only those
461 # iterate over both local and shared entries, but only those
462 # shared entries that are among the currently shared features
462 # shared entries that are among the currently shared features
463 shared = (
463 shared = (
464 e
464 e
465 for e in self._open(self.sharedvfs)
465 for e in self._open(self.sharedvfs)
466 if sharednamespaces.get(e.namespace) in self.sharedfeatures
466 if sharednamespaces.get(e.namespace) in self.sharedfeatures
467 )
467 )
468 return _mergeentriesiter(local, shared)
468 return _mergeentriesiter(local, shared)
469
469
470 def _open(self, vfs, filename=b'namejournal', _newestfirst=True):
470 def _open(self, vfs, filename=b'namejournal', _newestfirst=True):
471 if not vfs.exists(filename):
471 if not vfs.exists(filename):
472 return
472 return
473
473
474 with vfs(filename) as f:
474 with vfs(filename) as f:
475 raw = f.read()
475 raw = f.read()
476
476
477 lines = raw.split(b'\0')
477 lines = raw.split(b'\0')
478 version = lines and lines[0]
478 version = lines and lines[0]
479 if version != b"%d" % storageversion:
479 if version != b"%d" % storageversion:
480 version = version or _(b'not available')
480 version = version or _(b'not available')
481 raise error.Abort(_(b"unknown journal file version '%s'") % version)
481 raise error.Abort(_(b"unknown journal file version '%s'") % version)
482
482
483 # Skip the first line, it's a version number. Normally we iterate over
483 # Skip the first line, it's a version number. Normally we iterate over
484 # these in reverse order to list newest first; only when copying across
484 # these in reverse order to list newest first; only when copying across
485 # a shared storage do we forgo reversing.
485 # a shared storage do we forgo reversing.
486 lines = lines[1:]
486 lines = lines[1:]
487 if _newestfirst:
487 if _newestfirst:
488 lines = reversed(lines)
488 lines = reversed(lines)
489 for line in lines:
489 for line in lines:
490 if not line:
490 if not line:
491 continue
491 continue
492 yield journalentry.fromstorage(line)
492 yield journalentry.fromstorage(line)
493
493
494
494
495 # journal reading
495 # journal reading
496 # log options that don't make sense for journal
496 # log options that don't make sense for journal
497 _ignoreopts = (b'no-merges', b'graph')
497 _ignoreopts = (b'no-merges', b'graph')
498
498
499
499
500 @command(
500 @command(
501 b'journal',
501 b'journal',
502 [
502 [
503 (b'', b'all', None, b'show history for all names'),
503 (b'', b'all', None, b'show history for all names'),
504 (b'c', b'commits', None, b'show commit metadata'),
504 (b'c', b'commits', None, b'show commit metadata'),
505 ]
505 ]
506 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
506 + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
507 b'[OPTION]... [BOOKMARKNAME]',
507 b'[OPTION]... [BOOKMARKNAME]',
508 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
508 helpcategory=command.CATEGORY_CHANGE_ORGANIZATION,
509 )
509 )
510 def journal(ui, repo, *args, **opts):
510 def journal(ui, repo, *args, **opts):
511 """show the previous position of bookmarks and the working copy
511 """show the previous position of bookmarks and the working copy
512
512
513 The journal is used to see the previous commits that bookmarks and the
513 The journal is used to see the previous commits that bookmarks and the
514 working copy pointed to. By default the previous locations for the working
514 working copy pointed to. By default the previous locations for the working
515 copy. Passing a bookmark name will show all the previous positions of
515 copy. Passing a bookmark name will show all the previous positions of
516 that bookmark. Use the --all switch to show previous locations for all
516 that bookmark. Use the --all switch to show previous locations for all
517 bookmarks and the working copy; each line will then include the bookmark
517 bookmarks and the working copy; each line will then include the bookmark
518 name, or '.' for the working copy, as well.
518 name, or '.' for the working copy, as well.
519
519
520 If `name` starts with `re:`, the remainder of the name is treated as
520 If `name` starts with `re:`, the remainder of the name is treated as
521 a regular expression. To match a name that actually starts with `re:`,
521 a regular expression. To match a name that actually starts with `re:`,
522 use the prefix `literal:`.
522 use the prefix `literal:`.
523
523
524 By default hg journal only shows the commit hash and the command that was
524 By default hg journal only shows the commit hash and the command that was
525 running at that time. -v/--verbose will show the prior hash, the user, and
525 running at that time. -v/--verbose will show the prior hash, the user, and
526 the time at which it happened.
526 the time at which it happened.
527
527
528 Use -c/--commits to output log information on each commit hash; at this
528 Use -c/--commits to output log information on each commit hash; at this
529 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
529 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
530 switches to alter the log output for these.
530 switches to alter the log output for these.
531
531
532 `hg journal -T json` can be used to produce machine readable output.
532 `hg journal -T json` can be used to produce machine readable output.
533
533
534 """
534 """
535 opts = pycompat.byteskwargs(opts)
535 opts = pycompat.byteskwargs(opts)
536 name = b'.'
536 name = b'.'
537 if opts.get(b'all'):
537 if opts.get(b'all'):
538 if args:
538 if args:
539 raise error.Abort(
539 raise error.Abort(
540 _(b"You can't combine --all and filtering on a name")
540 _(b"You can't combine --all and filtering on a name")
541 )
541 )
542 name = None
542 name = None
543 if args:
543 if args:
544 name = args[0]
544 name = args[0]
545
545
546 fm = ui.formatter(b'journal', opts)
546 fm = ui.formatter(b'journal', opts)
547
547
548 def formatnodes(nodes):
548 def formatnodes(nodes):
549 return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',')
549 return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',')
550
550
551 if opts.get(b"template") != b"json":
551 if opts.get(b"template") != b"json":
552 if name is None:
552 if name is None:
553 displayname = _(b'the working copy and bookmarks')
553 displayname = _(b'the working copy and bookmarks')
554 else:
554 else:
555 displayname = b"'%s'" % name
555 displayname = b"'%s'" % name
556 ui.status(_(b"previous locations of %s:\n") % displayname)
556 ui.status(_(b"previous locations of %s:\n") % displayname)
557
557
558 limit = logcmdutil.getlimit(opts)
558 limit = logcmdutil.getlimit(opts)
559 entry = None
559 entry = None
560 ui.pager(b'journal')
560 ui.pager(b'journal')
561 for count, entry in enumerate(repo.journal.filtered(name=name)):
561 for count, entry in enumerate(repo.journal.filtered(name=name)):
562 if count == limit:
562 if count == limit:
563 break
563 break
564
564
565 fm.startitem()
565 fm.startitem()
566 fm.condwrite(
566 fm.condwrite(
567 ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes)
567 ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes)
568 )
568 )
569 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
569 fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes))
570 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
570 fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user)
571 fm.condwrite(
571 fm.condwrite(
572 opts.get(b'all') or name.startswith(b're:'),
572 opts.get(b'all') or name.startswith(b're:'),
573 b'name',
573 b'name',
574 b' %-8s',
574 b' %-8s',
575 entry.name,
575 entry.name,
576 )
576 )
577
577
578 fm.condwrite(
578 fm.condwrite(
579 ui.verbose,
579 ui.verbose,
580 b'date',
580 b'date',
581 b' %s',
581 b' %s',
582 fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'),
582 fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'),
583 )
583 )
584 fm.write(b'command', b' %s\n', entry.command)
584 fm.write(b'command', b' %s\n', entry.command)
585
585
586 if opts.get(b"commits"):
586 if opts.get(b"commits"):
587 if fm.isplain():
587 if fm.isplain():
588 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
588 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
589 else:
589 else:
590 displayer = logcmdutil.changesetformatter(
590 displayer = logcmdutil.changesetformatter(
591 ui, repo, fm.nested(b'changesets'), diffopts=opts
591 ui, repo, fm.nested(b'changesets'), diffopts=opts
592 )
592 )
593 for hash in entry.newhashes:
593 for hash in entry.newhashes:
594 try:
594 try:
595 ctx = repo[hash]
595 ctx = repo[hash]
596 displayer.show(ctx)
596 displayer.show(ctx)
597 except error.RepoLookupError as e:
597 except error.RepoLookupError as e:
598 fm.plain(b"%s\n\n" % pycompat.bytestr(e))
598 fm.plain(b"%s\n\n" % pycompat.bytestr(e))
599 displayer.close()
599 displayer.close()
600
600
601 fm.end()
601 fm.end()
602
602
603 if entry is None:
603 if entry is None:
604 ui.status(_(b"no recorded locations\n"))
604 ui.status(_(b"no recorded locations\n"))
@@ -1,370 +1,370 b''
1 # wireprotolfsserver.py - lfs protocol server side implementation
1 # wireprotolfsserver.py - lfs protocol server side implementation
2 #
2 #
3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
3 # Copyright 2018 Matt Harbison <matt_harbison@yahoo.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import datetime
10 import datetime
11 import errno
11 import errno
12 import json
12 import json
13 import traceback
13 import traceback
14
14
15 from mercurial.hgweb import common as hgwebcommon
15 from mercurial.hgweb import common as hgwebcommon
16
16
17 from mercurial import (
17 from mercurial import (
18 exthelper,
18 exthelper,
19 pycompat,
19 pycompat,
20 util,
20 util,
21 wireprotoserver,
21 wireprotoserver,
22 )
22 )
23
23
24 from . import blobstore
24 from . import blobstore
25
25
26 HTTP_OK = hgwebcommon.HTTP_OK
26 HTTP_OK = hgwebcommon.HTTP_OK
27 HTTP_CREATED = hgwebcommon.HTTP_CREATED
27 HTTP_CREATED = hgwebcommon.HTTP_CREATED
28 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
28 HTTP_BAD_REQUEST = hgwebcommon.HTTP_BAD_REQUEST
29 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
29 HTTP_NOT_FOUND = hgwebcommon.HTTP_NOT_FOUND
30 HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED
30 HTTP_METHOD_NOT_ALLOWED = hgwebcommon.HTTP_METHOD_NOT_ALLOWED
31 HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE
31 HTTP_NOT_ACCEPTABLE = hgwebcommon.HTTP_NOT_ACCEPTABLE
32 HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE
32 HTTP_UNSUPPORTED_MEDIA_TYPE = hgwebcommon.HTTP_UNSUPPORTED_MEDIA_TYPE
33
33
34 eh = exthelper.exthelper()
34 eh = exthelper.exthelper()
35
35
36
36
37 @eh.wrapfunction(wireprotoserver, b'handlewsgirequest')
37 @eh.wrapfunction(wireprotoserver, b'handlewsgirequest')
38 def handlewsgirequest(orig, rctx, req, res, checkperm):
38 def handlewsgirequest(orig, rctx, req, res, checkperm):
39 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
39 """Wrap wireprotoserver.handlewsgirequest() to possibly process an LFS
40 request if it is left unprocessed by the wrapped method.
40 request if it is left unprocessed by the wrapped method.
41 """
41 """
42 if orig(rctx, req, res, checkperm):
42 if orig(rctx, req, res, checkperm):
43 return True
43 return True
44
44
45 if not rctx.repo.ui.configbool(b'experimental', b'lfs.serve'):
45 if not rctx.repo.ui.configbool(b'experimental', b'lfs.serve'):
46 return False
46 return False
47
47
48 if not util.safehasattr(rctx.repo.svfs, b'lfslocalblobstore'):
48 if not util.safehasattr(rctx.repo.svfs, 'lfslocalblobstore'):
49 return False
49 return False
50
50
51 if not req.dispatchpath:
51 if not req.dispatchpath:
52 return False
52 return False
53
53
54 try:
54 try:
55 if req.dispatchpath == b'.git/info/lfs/objects/batch':
55 if req.dispatchpath == b'.git/info/lfs/objects/batch':
56 checkperm(rctx, req, b'pull')
56 checkperm(rctx, req, b'pull')
57 return _processbatchrequest(rctx.repo, req, res)
57 return _processbatchrequest(rctx.repo, req, res)
58 # TODO: reserve and use a path in the proposed http wireprotocol /api/
58 # TODO: reserve and use a path in the proposed http wireprotocol /api/
59 # namespace?
59 # namespace?
60 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
60 elif req.dispatchpath.startswith(b'.hg/lfs/objects'):
61 return _processbasictransfer(
61 return _processbasictransfer(
62 rctx.repo, req, res, lambda perm: checkperm(rctx, req, perm)
62 rctx.repo, req, res, lambda perm: checkperm(rctx, req, perm)
63 )
63 )
64 return False
64 return False
65 except hgwebcommon.ErrorResponse as e:
65 except hgwebcommon.ErrorResponse as e:
66 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
66 # XXX: copied from the handler surrounding wireprotoserver._callhttp()
67 # in the wrapped function. Should this be moved back to hgweb to
67 # in the wrapped function. Should this be moved back to hgweb to
68 # be a common handler?
68 # be a common handler?
69 for k, v in e.headers:
69 for k, v in e.headers:
70 res.headers[k] = v
70 res.headers[k] = v
71 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
71 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
72 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
72 res.setbodybytes(b'0\n%s\n' % pycompat.bytestr(e))
73 return True
73 return True
74
74
75
75
76 def _sethttperror(res, code, message=None):
76 def _sethttperror(res, code, message=None):
77 res.status = hgwebcommon.statusmessage(code, message=message)
77 res.status = hgwebcommon.statusmessage(code, message=message)
78 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
78 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
79 res.setbodybytes(b'')
79 res.setbodybytes(b'')
80
80
81
81
82 def _logexception(req):
82 def _logexception(req):
83 """Write information about the current exception to wsgi.errors."""
83 """Write information about the current exception to wsgi.errors."""
84 tb = pycompat.sysbytes(traceback.format_exc())
84 tb = pycompat.sysbytes(traceback.format_exc())
85 errorlog = req.rawenv[b'wsgi.errors']
85 errorlog = req.rawenv[b'wsgi.errors']
86
86
87 uri = b''
87 uri = b''
88 if req.apppath:
88 if req.apppath:
89 uri += req.apppath
89 uri += req.apppath
90 uri += b'/' + req.dispatchpath
90 uri += b'/' + req.dispatchpath
91
91
92 errorlog.write(
92 errorlog.write(
93 b"Exception happened while processing request '%s':\n%s" % (uri, tb)
93 b"Exception happened while processing request '%s':\n%s" % (uri, tb)
94 )
94 )
95
95
96
96
97 def _processbatchrequest(repo, req, res):
97 def _processbatchrequest(repo, req, res):
98 """Handle a request for the Batch API, which is the gateway to granting file
98 """Handle a request for the Batch API, which is the gateway to granting file
99 access.
99 access.
100
100
101 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
101 https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
102 """
102 """
103
103
104 # Mercurial client request:
104 # Mercurial client request:
105 #
105 #
106 # HOST: localhost:$HGPORT
106 # HOST: localhost:$HGPORT
107 # ACCEPT: application/vnd.git-lfs+json
107 # ACCEPT: application/vnd.git-lfs+json
108 # ACCEPT-ENCODING: identity
108 # ACCEPT-ENCODING: identity
109 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
109 # USER-AGENT: git-lfs/2.3.4 (Mercurial 4.5.2+1114-f48b9754f04c+20180316)
110 # Content-Length: 125
110 # Content-Length: 125
111 # Content-Type: application/vnd.git-lfs+json
111 # Content-Type: application/vnd.git-lfs+json
112 #
112 #
113 # {
113 # {
114 # "objects": [
114 # "objects": [
115 # {
115 # {
116 # "oid": "31cf...8e5b"
116 # "oid": "31cf...8e5b"
117 # "size": 12
117 # "size": 12
118 # }
118 # }
119 # ]
119 # ]
120 # "operation": "upload"
120 # "operation": "upload"
121 # }
121 # }
122
122
123 if req.method != b'POST':
123 if req.method != b'POST':
124 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED)
124 _sethttperror(res, HTTP_METHOD_NOT_ALLOWED)
125 return True
125 return True
126
126
127 if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json':
127 if req.headers[b'Content-Type'] != b'application/vnd.git-lfs+json':
128 _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE)
128 _sethttperror(res, HTTP_UNSUPPORTED_MEDIA_TYPE)
129 return True
129 return True
130
130
131 if req.headers[b'Accept'] != b'application/vnd.git-lfs+json':
131 if req.headers[b'Accept'] != b'application/vnd.git-lfs+json':
132 _sethttperror(res, HTTP_NOT_ACCEPTABLE)
132 _sethttperror(res, HTTP_NOT_ACCEPTABLE)
133 return True
133 return True
134
134
135 # XXX: specify an encoding?
135 # XXX: specify an encoding?
136 lfsreq = json.loads(req.bodyfh.read())
136 lfsreq = json.loads(req.bodyfh.read())
137
137
138 # If no transfer handlers are explicitly requested, 'basic' is assumed.
138 # If no transfer handlers are explicitly requested, 'basic' is assumed.
139 if r'basic' not in lfsreq.get(r'transfers', [r'basic']):
139 if r'basic' not in lfsreq.get(r'transfers', [r'basic']):
140 _sethttperror(
140 _sethttperror(
141 res,
141 res,
142 HTTP_BAD_REQUEST,
142 HTTP_BAD_REQUEST,
143 b'Only the basic LFS transfer handler is supported',
143 b'Only the basic LFS transfer handler is supported',
144 )
144 )
145 return True
145 return True
146
146
147 operation = lfsreq.get(r'operation')
147 operation = lfsreq.get(r'operation')
148 operation = pycompat.bytestr(operation)
148 operation = pycompat.bytestr(operation)
149
149
150 if operation not in (b'upload', b'download'):
150 if operation not in (b'upload', b'download'):
151 _sethttperror(
151 _sethttperror(
152 res,
152 res,
153 HTTP_BAD_REQUEST,
153 HTTP_BAD_REQUEST,
154 b'Unsupported LFS transfer operation: %s' % operation,
154 b'Unsupported LFS transfer operation: %s' % operation,
155 )
155 )
156 return True
156 return True
157
157
158 localstore = repo.svfs.lfslocalblobstore
158 localstore = repo.svfs.lfslocalblobstore
159
159
160 objects = [
160 objects = [
161 p
161 p
162 for p in _batchresponseobjects(
162 for p in _batchresponseobjects(
163 req, lfsreq.get(r'objects', []), operation, localstore
163 req, lfsreq.get(r'objects', []), operation, localstore
164 )
164 )
165 ]
165 ]
166
166
167 rsp = {
167 rsp = {
168 r'transfer': r'basic',
168 r'transfer': r'basic',
169 r'objects': objects,
169 r'objects': objects,
170 }
170 }
171
171
172 res.status = hgwebcommon.statusmessage(HTTP_OK)
172 res.status = hgwebcommon.statusmessage(HTTP_OK)
173 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
173 res.headers[b'Content-Type'] = b'application/vnd.git-lfs+json'
174 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
174 res.setbodybytes(pycompat.bytestr(json.dumps(rsp)))
175
175
176 return True
176 return True
177
177
178
178
179 def _batchresponseobjects(req, objects, action, store):
179 def _batchresponseobjects(req, objects, action, store):
180 """Yield one dictionary of attributes for the Batch API response for each
180 """Yield one dictionary of attributes for the Batch API response for each
181 object in the list.
181 object in the list.
182
182
183 req: The parsedrequest for the Batch API request
183 req: The parsedrequest for the Batch API request
184 objects: The list of objects in the Batch API object request list
184 objects: The list of objects in the Batch API object request list
185 action: 'upload' or 'download'
185 action: 'upload' or 'download'
186 store: The local blob store for servicing requests"""
186 store: The local blob store for servicing requests"""
187
187
188 # Successful lfs-test-server response to solict an upload:
188 # Successful lfs-test-server response to solict an upload:
189 # {
189 # {
190 # u'objects': [{
190 # u'objects': [{
191 # u'size': 12,
191 # u'size': 12,
192 # u'oid': u'31cf...8e5b',
192 # u'oid': u'31cf...8e5b',
193 # u'actions': {
193 # u'actions': {
194 # u'upload': {
194 # u'upload': {
195 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
195 # u'href': u'http://localhost:$HGPORT/objects/31cf...8e5b',
196 # u'expires_at': u'0001-01-01T00:00:00Z',
196 # u'expires_at': u'0001-01-01T00:00:00Z',
197 # u'header': {
197 # u'header': {
198 # u'Accept': u'application/vnd.git-lfs'
198 # u'Accept': u'application/vnd.git-lfs'
199 # }
199 # }
200 # }
200 # }
201 # }
201 # }
202 # }]
202 # }]
203 # }
203 # }
204
204
205 # TODO: Sort out the expires_at/expires_in/authenticated keys.
205 # TODO: Sort out the expires_at/expires_in/authenticated keys.
206
206
207 for obj in objects:
207 for obj in objects:
208 # Convert unicode to ASCII to create a filesystem path
208 # Convert unicode to ASCII to create a filesystem path
209 soid = obj.get(r'oid')
209 soid = obj.get(r'oid')
210 oid = soid.encode(r'ascii')
210 oid = soid.encode(r'ascii')
211 rsp = {
211 rsp = {
212 r'oid': soid,
212 r'oid': soid,
213 r'size': obj.get(r'size'), # XXX: should this check the local size?
213 r'size': obj.get(r'size'), # XXX: should this check the local size?
214 # r'authenticated': True,
214 # r'authenticated': True,
215 }
215 }
216
216
217 exists = True
217 exists = True
218 verifies = False
218 verifies = False
219
219
220 # Verify an existing file on the upload request, so that the client is
220 # Verify an existing file on the upload request, so that the client is
221 # solicited to re-upload if it corrupt locally. Download requests are
221 # solicited to re-upload if it corrupt locally. Download requests are
222 # also verified, so the error can be flagged in the Batch API response.
222 # also verified, so the error can be flagged in the Batch API response.
223 # (Maybe we can use this to short circuit the download for `hg verify`,
223 # (Maybe we can use this to short circuit the download for `hg verify`,
224 # IFF the client can assert that the remote end is an hg server.)
224 # IFF the client can assert that the remote end is an hg server.)
225 # Otherwise, it's potentially overkill on download, since it is also
225 # Otherwise, it's potentially overkill on download, since it is also
226 # verified as the file is streamed to the caller.
226 # verified as the file is streamed to the caller.
227 try:
227 try:
228 verifies = store.verify(oid)
228 verifies = store.verify(oid)
229 if verifies and action == b'upload':
229 if verifies and action == b'upload':
230 # The client will skip this upload, but make sure it remains
230 # The client will skip this upload, but make sure it remains
231 # available locally.
231 # available locally.
232 store.linkfromusercache(oid)
232 store.linkfromusercache(oid)
233 except IOError as inst:
233 except IOError as inst:
234 if inst.errno != errno.ENOENT:
234 if inst.errno != errno.ENOENT:
235 _logexception(req)
235 _logexception(req)
236
236
237 rsp[r'error'] = {
237 rsp[r'error'] = {
238 r'code': 500,
238 r'code': 500,
239 r'message': inst.strerror or r'Internal Server Server',
239 r'message': inst.strerror or r'Internal Server Server',
240 }
240 }
241 yield rsp
241 yield rsp
242 continue
242 continue
243
243
244 exists = False
244 exists = False
245
245
246 # Items are always listed for downloads. They are dropped for uploads
246 # Items are always listed for downloads. They are dropped for uploads
247 # IFF they already exist locally.
247 # IFF they already exist locally.
248 if action == b'download':
248 if action == b'download':
249 if not exists:
249 if not exists:
250 rsp[r'error'] = {
250 rsp[r'error'] = {
251 r'code': 404,
251 r'code': 404,
252 r'message': r"The object does not exist",
252 r'message': r"The object does not exist",
253 }
253 }
254 yield rsp
254 yield rsp
255 continue
255 continue
256
256
257 elif not verifies:
257 elif not verifies:
258 rsp[r'error'] = {
258 rsp[r'error'] = {
259 r'code': 422, # XXX: is this the right code?
259 r'code': 422, # XXX: is this the right code?
260 r'message': r"The object is corrupt",
260 r'message': r"The object is corrupt",
261 }
261 }
262 yield rsp
262 yield rsp
263 continue
263 continue
264
264
265 elif verifies:
265 elif verifies:
266 yield rsp # Skip 'actions': already uploaded
266 yield rsp # Skip 'actions': already uploaded
267 continue
267 continue
268
268
269 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
269 expiresat = datetime.datetime.now() + datetime.timedelta(minutes=10)
270
270
271 def _buildheader():
271 def _buildheader():
272 # The spec doesn't mention the Accept header here, but avoid
272 # The spec doesn't mention the Accept header here, but avoid
273 # a gratuitous deviation from lfs-test-server in the test
273 # a gratuitous deviation from lfs-test-server in the test
274 # output.
274 # output.
275 hdr = {r'Accept': r'application/vnd.git-lfs'}
275 hdr = {r'Accept': r'application/vnd.git-lfs'}
276
276
277 auth = req.headers.get(b'Authorization', b'')
277 auth = req.headers.get(b'Authorization', b'')
278 if auth.startswith(b'Basic '):
278 if auth.startswith(b'Basic '):
279 hdr[r'Authorization'] = pycompat.strurl(auth)
279 hdr[r'Authorization'] = pycompat.strurl(auth)
280
280
281 return hdr
281 return hdr
282
282
283 rsp[r'actions'] = {
283 rsp[r'actions'] = {
284 r'%s'
284 r'%s'
285 % pycompat.strurl(action): {
285 % pycompat.strurl(action): {
286 r'href': pycompat.strurl(
286 r'href': pycompat.strurl(
287 b'%s%s/.hg/lfs/objects/%s' % (req.baseurl, req.apppath, oid)
287 b'%s%s/.hg/lfs/objects/%s' % (req.baseurl, req.apppath, oid)
288 ),
288 ),
289 # datetime.isoformat() doesn't include the 'Z' suffix
289 # datetime.isoformat() doesn't include the 'Z' suffix
290 r"expires_at": expiresat.strftime(r'%Y-%m-%dT%H:%M:%SZ'),
290 r"expires_at": expiresat.strftime(r'%Y-%m-%dT%H:%M:%SZ'),
291 r'header': _buildheader(),
291 r'header': _buildheader(),
292 }
292 }
293 }
293 }
294
294
295 yield rsp
295 yield rsp
296
296
297
297
298 def _processbasictransfer(repo, req, res, checkperm):
298 def _processbasictransfer(repo, req, res, checkperm):
299 """Handle a single file upload (PUT) or download (GET) action for the Basic
299 """Handle a single file upload (PUT) or download (GET) action for the Basic
300 Transfer Adapter.
300 Transfer Adapter.
301
301
302 After determining if the request is for an upload or download, the access
302 After determining if the request is for an upload or download, the access
303 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
303 must be checked by calling ``checkperm()`` with either 'pull' or 'upload'
304 before accessing the files.
304 before accessing the files.
305
305
306 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
306 https://github.com/git-lfs/git-lfs/blob/master/docs/api/basic-transfers.md
307 """
307 """
308
308
309 method = req.method
309 method = req.method
310 oid = req.dispatchparts[-1]
310 oid = req.dispatchparts[-1]
311 localstore = repo.svfs.lfslocalblobstore
311 localstore = repo.svfs.lfslocalblobstore
312
312
313 if len(req.dispatchparts) != 4:
313 if len(req.dispatchparts) != 4:
314 _sethttperror(res, HTTP_NOT_FOUND)
314 _sethttperror(res, HTTP_NOT_FOUND)
315 return True
315 return True
316
316
317 if method == b'PUT':
317 if method == b'PUT':
318 checkperm(b'upload')
318 checkperm(b'upload')
319
319
320 # TODO: verify Content-Type?
320 # TODO: verify Content-Type?
321
321
322 existed = localstore.has(oid)
322 existed = localstore.has(oid)
323
323
324 # TODO: how to handle timeouts? The body proxy handles limiting to
324 # TODO: how to handle timeouts? The body proxy handles limiting to
325 # Content-Length, but what happens if a client sends less than it
325 # Content-Length, but what happens if a client sends less than it
326 # says it will?
326 # says it will?
327
327
328 statusmessage = hgwebcommon.statusmessage
328 statusmessage = hgwebcommon.statusmessage
329 try:
329 try:
330 localstore.download(oid, req.bodyfh)
330 localstore.download(oid, req.bodyfh)
331 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
331 res.status = statusmessage(HTTP_OK if existed else HTTP_CREATED)
332 except blobstore.LfsCorruptionError:
332 except blobstore.LfsCorruptionError:
333 _logexception(req)
333 _logexception(req)
334
334
335 # XXX: Is this the right code?
335 # XXX: Is this the right code?
336 res.status = statusmessage(422, b'corrupt blob')
336 res.status = statusmessage(422, b'corrupt blob')
337
337
338 # There's no payload here, but this is the header that lfs-test-server
338 # There's no payload here, but this is the header that lfs-test-server
339 # sends back. This eliminates some gratuitous test output conditionals.
339 # sends back. This eliminates some gratuitous test output conditionals.
340 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
340 res.headers[b'Content-Type'] = b'text/plain; charset=utf-8'
341 res.setbodybytes(b'')
341 res.setbodybytes(b'')
342
342
343 return True
343 return True
344 elif method == b'GET':
344 elif method == b'GET':
345 checkperm(b'pull')
345 checkperm(b'pull')
346
346
347 res.status = hgwebcommon.statusmessage(HTTP_OK)
347 res.status = hgwebcommon.statusmessage(HTTP_OK)
348 res.headers[b'Content-Type'] = b'application/octet-stream'
348 res.headers[b'Content-Type'] = b'application/octet-stream'
349
349
350 try:
350 try:
351 # TODO: figure out how to send back the file in chunks, instead of
351 # TODO: figure out how to send back the file in chunks, instead of
352 # reading the whole thing. (Also figure out how to send back
352 # reading the whole thing. (Also figure out how to send back
353 # an error status if an IOError occurs after a partial write
353 # an error status if an IOError occurs after a partial write
354 # in that case. Here, everything is read before starting.)
354 # in that case. Here, everything is read before starting.)
355 res.setbodybytes(localstore.read(oid))
355 res.setbodybytes(localstore.read(oid))
356 except blobstore.LfsCorruptionError:
356 except blobstore.LfsCorruptionError:
357 _logexception(req)
357 _logexception(req)
358
358
359 # XXX: Is this the right code?
359 # XXX: Is this the right code?
360 res.status = hgwebcommon.statusmessage(422, b'corrupt blob')
360 res.status = hgwebcommon.statusmessage(422, b'corrupt blob')
361 res.setbodybytes(b'')
361 res.setbodybytes(b'')
362
362
363 return True
363 return True
364 else:
364 else:
365 _sethttperror(
365 _sethttperror(
366 res,
366 res,
367 HTTP_METHOD_NOT_ALLOWED,
367 HTTP_METHOD_NOT_ALLOWED,
368 message=b'Unsupported LFS transfer method: %s' % method,
368 message=b'Unsupported LFS transfer method: %s' % method,
369 )
369 )
370 return True
370 return True
@@ -1,360 +1,360 b''
1 # narrowbundle2.py - bundle2 extensions for narrow repository support
1 # narrowbundle2.py - bundle2 extensions for narrow repository support
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import struct
11 import struct
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.node import (
14 from mercurial.node import (
15 bin,
15 bin,
16 nullid,
16 nullid,
17 )
17 )
18 from mercurial import (
18 from mercurial import (
19 bundle2,
19 bundle2,
20 changegroup,
20 changegroup,
21 error,
21 error,
22 exchange,
22 exchange,
23 localrepo,
23 localrepo,
24 narrowspec,
24 narrowspec,
25 repair,
25 repair,
26 util,
26 util,
27 wireprototypes,
27 wireprototypes,
28 )
28 )
29 from mercurial.interfaces import repository
29 from mercurial.interfaces import repository
30 from mercurial.utils import stringutil
30 from mercurial.utils import stringutil
31
31
32 _NARROWACL_SECTION = b'narrowacl'
32 _NARROWACL_SECTION = b'narrowacl'
33 _CHANGESPECPART = b'narrow:changespec'
33 _CHANGESPECPART = b'narrow:changespec'
34 _RESSPECS = b'narrow:responsespec'
34 _RESSPECS = b'narrow:responsespec'
35 _SPECPART = b'narrow:spec'
35 _SPECPART = b'narrow:spec'
36 _SPECPART_INCLUDE = b'include'
36 _SPECPART_INCLUDE = b'include'
37 _SPECPART_EXCLUDE = b'exclude'
37 _SPECPART_EXCLUDE = b'exclude'
38 _KILLNODESIGNAL = b'KILL'
38 _KILLNODESIGNAL = b'KILL'
39 _DONESIGNAL = b'DONE'
39 _DONESIGNAL = b'DONE'
40 _ELIDEDCSHEADER = b'>20s20s20sl' # cset id, p1, p2, len(text)
40 _ELIDEDCSHEADER = b'>20s20s20sl' # cset id, p1, p2, len(text)
41 _ELIDEDMFHEADER = b'>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
41 _ELIDEDMFHEADER = b'>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
42 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
42 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
43 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
43 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
44
44
45 # Serve a changegroup for a client with a narrow clone.
45 # Serve a changegroup for a client with a narrow clone.
46 def getbundlechangegrouppart_narrow(
46 def getbundlechangegrouppart_narrow(
47 bundler,
47 bundler,
48 repo,
48 repo,
49 source,
49 source,
50 bundlecaps=None,
50 bundlecaps=None,
51 b2caps=None,
51 b2caps=None,
52 heads=None,
52 heads=None,
53 common=None,
53 common=None,
54 **kwargs
54 **kwargs
55 ):
55 ):
56 assert repo.ui.configbool(b'experimental', b'narrowservebrokenellipses')
56 assert repo.ui.configbool(b'experimental', b'narrowservebrokenellipses')
57
57
58 cgversions = b2caps.get(b'changegroup')
58 cgversions = b2caps.get(b'changegroup')
59 cgversions = [
59 cgversions = [
60 v
60 v
61 for v in cgversions
61 for v in cgversions
62 if v in changegroup.supportedoutgoingversions(repo)
62 if v in changegroup.supportedoutgoingversions(repo)
63 ]
63 ]
64 if not cgversions:
64 if not cgversions:
65 raise ValueError(_(b'no common changegroup version'))
65 raise ValueError(_(b'no common changegroup version'))
66 version = max(cgversions)
66 version = max(cgversions)
67
67
68 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
68 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
69 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
69 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
70 newinclude = sorted(filter(bool, kwargs.get(r'includepats', [])))
70 newinclude = sorted(filter(bool, kwargs.get(r'includepats', [])))
71 newexclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
71 newexclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
72 known = {bin(n) for n in kwargs.get(r'known', [])}
72 known = {bin(n) for n in kwargs.get(r'known', [])}
73 generateellipsesbundle2(
73 generateellipsesbundle2(
74 bundler,
74 bundler,
75 repo,
75 repo,
76 oldinclude,
76 oldinclude,
77 oldexclude,
77 oldexclude,
78 newinclude,
78 newinclude,
79 newexclude,
79 newexclude,
80 version,
80 version,
81 common,
81 common,
82 heads,
82 heads,
83 known,
83 known,
84 kwargs.get(r'depth', None),
84 kwargs.get(r'depth', None),
85 )
85 )
86
86
87
87
88 def generateellipsesbundle2(
88 def generateellipsesbundle2(
89 bundler,
89 bundler,
90 repo,
90 repo,
91 oldinclude,
91 oldinclude,
92 oldexclude,
92 oldexclude,
93 newinclude,
93 newinclude,
94 newexclude,
94 newexclude,
95 version,
95 version,
96 common,
96 common,
97 heads,
97 heads,
98 known,
98 known,
99 depth,
99 depth,
100 ):
100 ):
101 newmatch = narrowspec.match(
101 newmatch = narrowspec.match(
102 repo.root, include=newinclude, exclude=newexclude
102 repo.root, include=newinclude, exclude=newexclude
103 )
103 )
104 if depth is not None:
104 if depth is not None:
105 depth = int(depth)
105 depth = int(depth)
106 if depth < 1:
106 if depth < 1:
107 raise error.Abort(_(b'depth must be positive, got %d') % depth)
107 raise error.Abort(_(b'depth must be positive, got %d') % depth)
108
108
109 heads = set(heads or repo.heads())
109 heads = set(heads or repo.heads())
110 common = set(common or [nullid])
110 common = set(common or [nullid])
111 if known and (oldinclude != newinclude or oldexclude != newexclude):
111 if known and (oldinclude != newinclude or oldexclude != newexclude):
112 # Steps:
112 # Steps:
113 # 1. Send kill for "$known & ::common"
113 # 1. Send kill for "$known & ::common"
114 #
114 #
115 # 2. Send changegroup for ::common
115 # 2. Send changegroup for ::common
116 #
116 #
117 # 3. Proceed.
117 # 3. Proceed.
118 #
118 #
119 # In the future, we can send kills for only the specific
119 # In the future, we can send kills for only the specific
120 # nodes we know should go away or change shape, and then
120 # nodes we know should go away or change shape, and then
121 # send a data stream that tells the client something like this:
121 # send a data stream that tells the client something like this:
122 #
122 #
123 # a) apply this changegroup
123 # a) apply this changegroup
124 # b) apply nodes XXX, YYY, ZZZ that you already have
124 # b) apply nodes XXX, YYY, ZZZ that you already have
125 # c) goto a
125 # c) goto a
126 #
126 #
127 # until they've built up the full new state.
127 # until they've built up the full new state.
128 # Convert to revnums and intersect with "common". The client should
128 # Convert to revnums and intersect with "common". The client should
129 # have made it a subset of "common" already, but let's be safe.
129 # have made it a subset of "common" already, but let's be safe.
130 known = set(repo.revs(b"%ln & ::%ln", known, common))
130 known = set(repo.revs(b"%ln & ::%ln", known, common))
131 # TODO: we could send only roots() of this set, and the
131 # TODO: we could send only roots() of this set, and the
132 # list of nodes in common, and the client could work out
132 # list of nodes in common, and the client could work out
133 # what to strip, instead of us explicitly sending every
133 # what to strip, instead of us explicitly sending every
134 # single node.
134 # single node.
135 deadrevs = known
135 deadrevs = known
136
136
137 def genkills():
137 def genkills():
138 for r in deadrevs:
138 for r in deadrevs:
139 yield _KILLNODESIGNAL
139 yield _KILLNODESIGNAL
140 yield repo.changelog.node(r)
140 yield repo.changelog.node(r)
141 yield _DONESIGNAL
141 yield _DONESIGNAL
142
142
143 bundler.newpart(_CHANGESPECPART, data=genkills())
143 bundler.newpart(_CHANGESPECPART, data=genkills())
144 newvisit, newfull, newellipsis = exchange._computeellipsis(
144 newvisit, newfull, newellipsis = exchange._computeellipsis(
145 repo, set(), common, known, newmatch
145 repo, set(), common, known, newmatch
146 )
146 )
147 if newvisit:
147 if newvisit:
148 packer = changegroup.getbundler(
148 packer = changegroup.getbundler(
149 version,
149 version,
150 repo,
150 repo,
151 matcher=newmatch,
151 matcher=newmatch,
152 ellipses=True,
152 ellipses=True,
153 shallow=depth is not None,
153 shallow=depth is not None,
154 ellipsisroots=newellipsis,
154 ellipsisroots=newellipsis,
155 fullnodes=newfull,
155 fullnodes=newfull,
156 )
156 )
157 cgdata = packer.generate(common, newvisit, False, b'narrow_widen')
157 cgdata = packer.generate(common, newvisit, False, b'narrow_widen')
158
158
159 part = bundler.newpart(b'changegroup', data=cgdata)
159 part = bundler.newpart(b'changegroup', data=cgdata)
160 part.addparam(b'version', version)
160 part.addparam(b'version', version)
161 if b'treemanifest' in repo.requirements:
161 if b'treemanifest' in repo.requirements:
162 part.addparam(b'treemanifest', b'1')
162 part.addparam(b'treemanifest', b'1')
163
163
164 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
164 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
165 repo, common, heads, set(), newmatch, depth=depth
165 repo, common, heads, set(), newmatch, depth=depth
166 )
166 )
167
167
168 repo.ui.debug(b'Found %d relevant revs\n' % len(relevant_nodes))
168 repo.ui.debug(b'Found %d relevant revs\n' % len(relevant_nodes))
169 if visitnodes:
169 if visitnodes:
170 packer = changegroup.getbundler(
170 packer = changegroup.getbundler(
171 version,
171 version,
172 repo,
172 repo,
173 matcher=newmatch,
173 matcher=newmatch,
174 ellipses=True,
174 ellipses=True,
175 shallow=depth is not None,
175 shallow=depth is not None,
176 ellipsisroots=ellipsisroots,
176 ellipsisroots=ellipsisroots,
177 fullnodes=relevant_nodes,
177 fullnodes=relevant_nodes,
178 )
178 )
179 cgdata = packer.generate(common, visitnodes, False, b'narrow_widen')
179 cgdata = packer.generate(common, visitnodes, False, b'narrow_widen')
180
180
181 part = bundler.newpart(b'changegroup', data=cgdata)
181 part = bundler.newpart(b'changegroup', data=cgdata)
182 part.addparam(b'version', version)
182 part.addparam(b'version', version)
183 if b'treemanifest' in repo.requirements:
183 if b'treemanifest' in repo.requirements:
184 part.addparam(b'treemanifest', b'1')
184 part.addparam(b'treemanifest', b'1')
185
185
186
186
187 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
187 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
188 def _handlechangespec_2(op, inpart):
188 def _handlechangespec_2(op, inpart):
189 # XXX: This bundle2 handling is buggy and should be removed after hg5.2 is
189 # XXX: This bundle2 handling is buggy and should be removed after hg5.2 is
190 # released. New servers will send a mandatory bundle2 part named
190 # released. New servers will send a mandatory bundle2 part named
191 # 'Narrowspec' and will send specs as data instead of params.
191 # 'Narrowspec' and will send specs as data instead of params.
192 # Refer to issue5952 and 6019
192 # Refer to issue5952 and 6019
193 includepats = set(inpart.params.get(_SPECPART_INCLUDE, b'').splitlines())
193 includepats = set(inpart.params.get(_SPECPART_INCLUDE, b'').splitlines())
194 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, b'').splitlines())
194 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, b'').splitlines())
195 narrowspec.validatepatterns(includepats)
195 narrowspec.validatepatterns(includepats)
196 narrowspec.validatepatterns(excludepats)
196 narrowspec.validatepatterns(excludepats)
197
197
198 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
198 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
199 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
199 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
200 op.repo._writerequirements()
200 op.repo._writerequirements()
201 op.repo.setnarrowpats(includepats, excludepats)
201 op.repo.setnarrowpats(includepats, excludepats)
202 narrowspec.copytoworkingcopy(op.repo)
202 narrowspec.copytoworkingcopy(op.repo)
203
203
204
204
205 @bundle2.parthandler(_RESSPECS)
205 @bundle2.parthandler(_RESSPECS)
206 def _handlenarrowspecs(op, inpart):
206 def _handlenarrowspecs(op, inpart):
207 data = inpart.read()
207 data = inpart.read()
208 inc, exc = data.split(b'\0')
208 inc, exc = data.split(b'\0')
209 includepats = set(inc.splitlines())
209 includepats = set(inc.splitlines())
210 excludepats = set(exc.splitlines())
210 excludepats = set(exc.splitlines())
211 narrowspec.validatepatterns(includepats)
211 narrowspec.validatepatterns(includepats)
212 narrowspec.validatepatterns(excludepats)
212 narrowspec.validatepatterns(excludepats)
213
213
214 if repository.NARROW_REQUIREMENT not in op.repo.requirements:
214 if repository.NARROW_REQUIREMENT not in op.repo.requirements:
215 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
215 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
216 op.repo._writerequirements()
216 op.repo._writerequirements()
217 op.repo.setnarrowpats(includepats, excludepats)
217 op.repo.setnarrowpats(includepats, excludepats)
218 narrowspec.copytoworkingcopy(op.repo)
218 narrowspec.copytoworkingcopy(op.repo)
219
219
220
220
221 @bundle2.parthandler(_CHANGESPECPART)
221 @bundle2.parthandler(_CHANGESPECPART)
222 def _handlechangespec(op, inpart):
222 def _handlechangespec(op, inpart):
223 repo = op.repo
223 repo = op.repo
224 cl = repo.changelog
224 cl = repo.changelog
225
225
226 # changesets which need to be stripped entirely. either they're no longer
226 # changesets which need to be stripped entirely. either they're no longer
227 # needed in the new narrow spec, or the server is sending a replacement
227 # needed in the new narrow spec, or the server is sending a replacement
228 # in the changegroup part.
228 # in the changegroup part.
229 clkills = set()
229 clkills = set()
230
230
231 # A changespec part contains all the updates to ellipsis nodes
231 # A changespec part contains all the updates to ellipsis nodes
232 # that will happen as a result of widening or narrowing a
232 # that will happen as a result of widening or narrowing a
233 # repo. All the changes that this block encounters are ellipsis
233 # repo. All the changes that this block encounters are ellipsis
234 # nodes or flags to kill an existing ellipsis.
234 # nodes or flags to kill an existing ellipsis.
235 chunksignal = changegroup.readexactly(inpart, 4)
235 chunksignal = changegroup.readexactly(inpart, 4)
236 while chunksignal != _DONESIGNAL:
236 while chunksignal != _DONESIGNAL:
237 if chunksignal == _KILLNODESIGNAL:
237 if chunksignal == _KILLNODESIGNAL:
238 # a node used to be an ellipsis but isn't anymore
238 # a node used to be an ellipsis but isn't anymore
239 ck = changegroup.readexactly(inpart, 20)
239 ck = changegroup.readexactly(inpart, 20)
240 if cl.hasnode(ck):
240 if cl.hasnode(ck):
241 clkills.add(ck)
241 clkills.add(ck)
242 else:
242 else:
243 raise error.Abort(
243 raise error.Abort(
244 _(b'unexpected changespec node chunk type: %s') % chunksignal
244 _(b'unexpected changespec node chunk type: %s') % chunksignal
245 )
245 )
246 chunksignal = changegroup.readexactly(inpart, 4)
246 chunksignal = changegroup.readexactly(inpart, 4)
247
247
248 if clkills:
248 if clkills:
249 # preserve bookmarks that repair.strip() would otherwise strip
249 # preserve bookmarks that repair.strip() would otherwise strip
250 op._bookmarksbackup = repo._bookmarks
250 op._bookmarksbackup = repo._bookmarks
251
251
252 class dummybmstore(dict):
252 class dummybmstore(dict):
253 def applychanges(self, repo, tr, changes):
253 def applychanges(self, repo, tr, changes):
254 pass
254 pass
255
255
256 localrepo.localrepository._bookmarks.set(repo, dummybmstore())
256 localrepo.localrepository._bookmarks.set(repo, dummybmstore())
257 chgrpfile = repair.strip(
257 chgrpfile = repair.strip(
258 op.ui, repo, list(clkills), backup=True, topic=b'widen'
258 op.ui, repo, list(clkills), backup=True, topic=b'widen'
259 )
259 )
260 if chgrpfile:
260 if chgrpfile:
261 op._widen_uninterr = repo.ui.uninterruptible()
261 op._widen_uninterr = repo.ui.uninterruptible()
262 op._widen_uninterr.__enter__()
262 op._widen_uninterr.__enter__()
263 # presence of _widen_bundle attribute activates widen handler later
263 # presence of _widen_bundle attribute activates widen handler later
264 op._widen_bundle = chgrpfile
264 op._widen_bundle = chgrpfile
265 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
265 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
266 # will currently always be there when using the core+narrowhg server, but
266 # will currently always be there when using the core+narrowhg server, but
267 # other servers may include a changespec part even when not widening (e.g.
267 # other servers may include a changespec part even when not widening (e.g.
268 # because we're deepening a shallow repo).
268 # because we're deepening a shallow repo).
269 if util.safehasattr(repo, b'setnewnarrowpats'):
269 if util.safehasattr(repo, 'setnewnarrowpats'):
270 repo.setnewnarrowpats()
270 repo.setnewnarrowpats()
271
271
272
272
273 def handlechangegroup_widen(op, inpart):
273 def handlechangegroup_widen(op, inpart):
274 """Changegroup exchange handler which restores temporarily-stripped nodes"""
274 """Changegroup exchange handler which restores temporarily-stripped nodes"""
275 # We saved a bundle with stripped node data we must now restore.
275 # We saved a bundle with stripped node data we must now restore.
276 # This approach is based on mercurial/repair.py@6ee26a53c111.
276 # This approach is based on mercurial/repair.py@6ee26a53c111.
277 repo = op.repo
277 repo = op.repo
278 ui = op.ui
278 ui = op.ui
279
279
280 chgrpfile = op._widen_bundle
280 chgrpfile = op._widen_bundle
281 del op._widen_bundle
281 del op._widen_bundle
282 vfs = repo.vfs
282 vfs = repo.vfs
283
283
284 ui.note(_(b"adding branch\n"))
284 ui.note(_(b"adding branch\n"))
285 f = vfs.open(chgrpfile, b"rb")
285 f = vfs.open(chgrpfile, b"rb")
286 try:
286 try:
287 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
287 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
288 # silence internal shuffling chatter
288 # silence internal shuffling chatter
289 override = {(b'ui', b'quiet'): True}
289 override = {(b'ui', b'quiet'): True}
290 if ui.verbose:
290 if ui.verbose:
291 override = {}
291 override = {}
292 with ui.configoverride(override):
292 with ui.configoverride(override):
293 if isinstance(gen, bundle2.unbundle20):
293 if isinstance(gen, bundle2.unbundle20):
294 with repo.transaction(b'strip') as tr:
294 with repo.transaction(b'strip') as tr:
295 bundle2.processbundle(repo, gen, lambda: tr)
295 bundle2.processbundle(repo, gen, lambda: tr)
296 else:
296 else:
297 gen.apply(
297 gen.apply(
298 repo, b'strip', b'bundle:' + vfs.join(chgrpfile), True
298 repo, b'strip', b'bundle:' + vfs.join(chgrpfile), True
299 )
299 )
300 finally:
300 finally:
301 f.close()
301 f.close()
302
302
303 # remove undo files
303 # remove undo files
304 for undovfs, undofile in repo.undofiles():
304 for undovfs, undofile in repo.undofiles():
305 try:
305 try:
306 undovfs.unlink(undofile)
306 undovfs.unlink(undofile)
307 except OSError as e:
307 except OSError as e:
308 if e.errno != errno.ENOENT:
308 if e.errno != errno.ENOENT:
309 ui.warn(
309 ui.warn(
310 _(b'error removing %s: %s\n')
310 _(b'error removing %s: %s\n')
311 % (undovfs.join(undofile), stringutil.forcebytestr(e))
311 % (undovfs.join(undofile), stringutil.forcebytestr(e))
312 )
312 )
313
313
314 # Remove partial backup only if there were no exceptions
314 # Remove partial backup only if there were no exceptions
315 op._widen_uninterr.__exit__(None, None, None)
315 op._widen_uninterr.__exit__(None, None, None)
316 vfs.unlink(chgrpfile)
316 vfs.unlink(chgrpfile)
317
317
318
318
319 def setup():
319 def setup():
320 """Enable narrow repo support in bundle2-related extension points."""
320 """Enable narrow repo support in bundle2-related extension points."""
321 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
321 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
322
322
323 getbundleargs[b'narrow'] = b'boolean'
323 getbundleargs[b'narrow'] = b'boolean'
324 getbundleargs[b'depth'] = b'plain'
324 getbundleargs[b'depth'] = b'plain'
325 getbundleargs[b'oldincludepats'] = b'csv'
325 getbundleargs[b'oldincludepats'] = b'csv'
326 getbundleargs[b'oldexcludepats'] = b'csv'
326 getbundleargs[b'oldexcludepats'] = b'csv'
327 getbundleargs[b'known'] = b'csv'
327 getbundleargs[b'known'] = b'csv'
328
328
329 # Extend changegroup serving to handle requests from narrow clients.
329 # Extend changegroup serving to handle requests from narrow clients.
330 origcgfn = exchange.getbundle2partsmapping[b'changegroup']
330 origcgfn = exchange.getbundle2partsmapping[b'changegroup']
331
331
332 def wrappedcgfn(*args, **kwargs):
332 def wrappedcgfn(*args, **kwargs):
333 repo = args[1]
333 repo = args[1]
334 if repo.ui.has_section(_NARROWACL_SECTION):
334 if repo.ui.has_section(_NARROWACL_SECTION):
335 kwargs = exchange.applynarrowacl(repo, kwargs)
335 kwargs = exchange.applynarrowacl(repo, kwargs)
336
336
337 if kwargs.get(r'narrow', False) and repo.ui.configbool(
337 if kwargs.get(r'narrow', False) and repo.ui.configbool(
338 b'experimental', b'narrowservebrokenellipses'
338 b'experimental', b'narrowservebrokenellipses'
339 ):
339 ):
340 getbundlechangegrouppart_narrow(*args, **kwargs)
340 getbundlechangegrouppart_narrow(*args, **kwargs)
341 else:
341 else:
342 origcgfn(*args, **kwargs)
342 origcgfn(*args, **kwargs)
343
343
344 exchange.getbundle2partsmapping[b'changegroup'] = wrappedcgfn
344 exchange.getbundle2partsmapping[b'changegroup'] = wrappedcgfn
345
345
346 # Extend changegroup receiver so client can fixup after widen requests.
346 # Extend changegroup receiver so client can fixup after widen requests.
347 origcghandler = bundle2.parthandlermapping[b'changegroup']
347 origcghandler = bundle2.parthandlermapping[b'changegroup']
348
348
349 def wrappedcghandler(op, inpart):
349 def wrappedcghandler(op, inpart):
350 origcghandler(op, inpart)
350 origcghandler(op, inpart)
351 if util.safehasattr(op, b'_widen_bundle'):
351 if util.safehasattr(op, '_widen_bundle'):
352 handlechangegroup_widen(op, inpart)
352 handlechangegroup_widen(op, inpart)
353 if util.safehasattr(op, b'_bookmarksbackup'):
353 if util.safehasattr(op, '_bookmarksbackup'):
354 localrepo.localrepository._bookmarks.set(
354 localrepo.localrepository._bookmarks.set(
355 op.repo, op._bookmarksbackup
355 op.repo, op._bookmarksbackup
356 )
356 )
357 del op._bookmarksbackup
357 del op._bookmarksbackup
358
358
359 wrappedcghandler.params = origcghandler.params
359 wrappedcghandler.params = origcghandler.params
360 bundle2.parthandlermapping[b'changegroup'] = wrappedcghandler
360 bundle2.parthandlermapping[b'changegroup'] = wrappedcghandler
@@ -1,88 +1,88 b''
1 # connectionpool.py - class for pooling peer connections for reuse
1 # connectionpool.py - class for pooling peer connections for reuse
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from mercurial import (
10 from mercurial import (
11 extensions,
11 extensions,
12 hg,
12 hg,
13 pycompat,
13 pycompat,
14 sshpeer,
14 sshpeer,
15 util,
15 util,
16 )
16 )
17
17
18 _sshv1peer = sshpeer.sshv1peer
18 _sshv1peer = sshpeer.sshv1peer
19
19
20
20
21 class connectionpool(object):
21 class connectionpool(object):
22 def __init__(self, repo):
22 def __init__(self, repo):
23 self._repo = repo
23 self._repo = repo
24 self._pool = dict()
24 self._pool = dict()
25
25
26 def get(self, path):
26 def get(self, path):
27 pathpool = self._pool.get(path)
27 pathpool = self._pool.get(path)
28 if pathpool is None:
28 if pathpool is None:
29 pathpool = list()
29 pathpool = list()
30 self._pool[path] = pathpool
30 self._pool[path] = pathpool
31
31
32 conn = None
32 conn = None
33 if len(pathpool) > 0:
33 if len(pathpool) > 0:
34 try:
34 try:
35 conn = pathpool.pop()
35 conn = pathpool.pop()
36 peer = conn.peer
36 peer = conn.peer
37 # If the connection has died, drop it
37 # If the connection has died, drop it
38 if isinstance(peer, _sshv1peer):
38 if isinstance(peer, _sshv1peer):
39 if peer._subprocess.poll() is not None:
39 if peer._subprocess.poll() is not None:
40 conn = None
40 conn = None
41 except IndexError:
41 except IndexError:
42 pass
42 pass
43
43
44 if conn is None:
44 if conn is None:
45
45
46 def _cleanup(orig):
46 def _cleanup(orig):
47 # close pipee first so peer.cleanup reading it won't deadlock,
47 # close pipee first so peer.cleanup reading it won't deadlock,
48 # if there are other processes with pipeo open (i.e. us).
48 # if there are other processes with pipeo open (i.e. us).
49 peer = orig.im_self
49 peer = orig.im_self
50 if util.safehasattr(peer, b'pipee'):
50 if util.safehasattr(peer, 'pipee'):
51 peer.pipee.close()
51 peer.pipee.close()
52 return orig()
52 return orig()
53
53
54 peer = hg.peer(self._repo.ui, {}, path)
54 peer = hg.peer(self._repo.ui, {}, path)
55 if util.safehasattr(peer, b'cleanup'):
55 if util.safehasattr(peer, 'cleanup'):
56 extensions.wrapfunction(peer, b'cleanup', _cleanup)
56 extensions.wrapfunction(peer, b'cleanup', _cleanup)
57
57
58 conn = connection(pathpool, peer)
58 conn = connection(pathpool, peer)
59
59
60 return conn
60 return conn
61
61
62 def close(self):
62 def close(self):
63 for pathpool in pycompat.itervalues(self._pool):
63 for pathpool in pycompat.itervalues(self._pool):
64 for conn in pathpool:
64 for conn in pathpool:
65 conn.close()
65 conn.close()
66 del pathpool[:]
66 del pathpool[:]
67
67
68
68
69 class connection(object):
69 class connection(object):
70 def __init__(self, pool, peer):
70 def __init__(self, pool, peer):
71 self._pool = pool
71 self._pool = pool
72 self.peer = peer
72 self.peer = peer
73
73
74 def __enter__(self):
74 def __enter__(self):
75 return self
75 return self
76
76
77 def __exit__(self, type, value, traceback):
77 def __exit__(self, type, value, traceback):
78 # Only add the connection back to the pool if there was no exception,
78 # Only add the connection back to the pool if there was no exception,
79 # since an exception could mean the connection is not in a reusable
79 # since an exception could mean the connection is not in a reusable
80 # state.
80 # state.
81 if type is None:
81 if type is None:
82 self._pool.append(self)
82 self._pool.append(self)
83 else:
83 else:
84 self.close()
84 self.close()
85
85
86 def close(self):
86 def close(self):
87 if util.safehasattr(self.peer, b'cleanup'):
87 if util.safehasattr(self.peer, 'cleanup'):
88 self.peer.cleanup()
88 self.peer.cleanup()
@@ -1,667 +1,667 b''
1 # fileserverclient.py - client for communicating with the cache process
1 # fileserverclient.py - client for communicating with the cache process
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11 import io
11 import io
12 import os
12 import os
13 import threading
13 import threading
14 import time
14 import time
15 import zlib
15 import zlib
16
16
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial.node import bin, hex, nullid
18 from mercurial.node import bin, hex, nullid
19 from mercurial import (
19 from mercurial import (
20 error,
20 error,
21 node,
21 node,
22 pycompat,
22 pycompat,
23 revlog,
23 revlog,
24 sshpeer,
24 sshpeer,
25 util,
25 util,
26 wireprotov1peer,
26 wireprotov1peer,
27 )
27 )
28 from mercurial.utils import procutil
28 from mercurial.utils import procutil
29
29
30 from . import (
30 from . import (
31 constants,
31 constants,
32 contentstore,
32 contentstore,
33 metadatastore,
33 metadatastore,
34 )
34 )
35
35
36 _sshv1peer = sshpeer.sshv1peer
36 _sshv1peer = sshpeer.sshv1peer
37
37
38 # Statistics for debugging
38 # Statistics for debugging
39 fetchcost = 0
39 fetchcost = 0
40 fetches = 0
40 fetches = 0
41 fetched = 0
41 fetched = 0
42 fetchmisses = 0
42 fetchmisses = 0
43
43
44 _lfsmod = None
44 _lfsmod = None
45
45
46
46
47 def getcachekey(reponame, file, id):
47 def getcachekey(reponame, file, id):
48 pathhash = node.hex(hashlib.sha1(file).digest())
48 pathhash = node.hex(hashlib.sha1(file).digest())
49 return os.path.join(reponame, pathhash[:2], pathhash[2:], id)
49 return os.path.join(reponame, pathhash[:2], pathhash[2:], id)
50
50
51
51
52 def getlocalkey(file, id):
52 def getlocalkey(file, id):
53 pathhash = node.hex(hashlib.sha1(file).digest())
53 pathhash = node.hex(hashlib.sha1(file).digest())
54 return os.path.join(pathhash, id)
54 return os.path.join(pathhash, id)
55
55
56
56
57 def peersetup(ui, peer):
57 def peersetup(ui, peer):
58 class remotefilepeer(peer.__class__):
58 class remotefilepeer(peer.__class__):
59 @wireprotov1peer.batchable
59 @wireprotov1peer.batchable
60 def x_rfl_getfile(self, file, node):
60 def x_rfl_getfile(self, file, node):
61 if not self.capable(b'x_rfl_getfile'):
61 if not self.capable(b'x_rfl_getfile'):
62 raise error.Abort(
62 raise error.Abort(
63 b'configured remotefile server does not support getfile'
63 b'configured remotefile server does not support getfile'
64 )
64 )
65 f = wireprotov1peer.future()
65 f = wireprotov1peer.future()
66 yield {b'file': file, b'node': node}, f
66 yield {b'file': file, b'node': node}, f
67 code, data = f.value.split(b'\0', 1)
67 code, data = f.value.split(b'\0', 1)
68 if int(code):
68 if int(code):
69 raise error.LookupError(file, node, data)
69 raise error.LookupError(file, node, data)
70 yield data
70 yield data
71
71
72 @wireprotov1peer.batchable
72 @wireprotov1peer.batchable
73 def x_rfl_getflogheads(self, path):
73 def x_rfl_getflogheads(self, path):
74 if not self.capable(b'x_rfl_getflogheads'):
74 if not self.capable(b'x_rfl_getflogheads'):
75 raise error.Abort(
75 raise error.Abort(
76 b'configured remotefile server does not '
76 b'configured remotefile server does not '
77 b'support getflogheads'
77 b'support getflogheads'
78 )
78 )
79 f = wireprotov1peer.future()
79 f = wireprotov1peer.future()
80 yield {b'path': path}, f
80 yield {b'path': path}, f
81 heads = f.value.split(b'\n') if f.value else []
81 heads = f.value.split(b'\n') if f.value else []
82 yield heads
82 yield heads
83
83
84 def _updatecallstreamopts(self, command, opts):
84 def _updatecallstreamopts(self, command, opts):
85 if command != b'getbundle':
85 if command != b'getbundle':
86 return
86 return
87 if (
87 if (
88 constants.NETWORK_CAP_LEGACY_SSH_GETFILES
88 constants.NETWORK_CAP_LEGACY_SSH_GETFILES
89 not in self.capabilities()
89 not in self.capabilities()
90 ):
90 ):
91 return
91 return
92 if not util.safehasattr(self, b'_localrepo'):
92 if not util.safehasattr(self, '_localrepo'):
93 return
93 return
94 if (
94 if (
95 constants.SHALLOWREPO_REQUIREMENT
95 constants.SHALLOWREPO_REQUIREMENT
96 not in self._localrepo.requirements
96 not in self._localrepo.requirements
97 ):
97 ):
98 return
98 return
99
99
100 bundlecaps = opts.get(b'bundlecaps')
100 bundlecaps = opts.get(b'bundlecaps')
101 if bundlecaps:
101 if bundlecaps:
102 bundlecaps = [bundlecaps]
102 bundlecaps = [bundlecaps]
103 else:
103 else:
104 bundlecaps = []
104 bundlecaps = []
105
105
106 # shallow, includepattern, and excludepattern are a hacky way of
106 # shallow, includepattern, and excludepattern are a hacky way of
107 # carrying over data from the local repo to this getbundle
107 # carrying over data from the local repo to this getbundle
108 # command. We need to do it this way because bundle1 getbundle
108 # command. We need to do it this way because bundle1 getbundle
109 # doesn't provide any other place we can hook in to manipulate
109 # doesn't provide any other place we can hook in to manipulate
110 # getbundle args before it goes across the wire. Once we get rid
110 # getbundle args before it goes across the wire. Once we get rid
111 # of bundle1, we can use bundle2's _pullbundle2extraprepare to
111 # of bundle1, we can use bundle2's _pullbundle2extraprepare to
112 # do this more cleanly.
112 # do this more cleanly.
113 bundlecaps.append(constants.BUNDLE2_CAPABLITY)
113 bundlecaps.append(constants.BUNDLE2_CAPABLITY)
114 if self._localrepo.includepattern:
114 if self._localrepo.includepattern:
115 patterns = b'\0'.join(self._localrepo.includepattern)
115 patterns = b'\0'.join(self._localrepo.includepattern)
116 includecap = b"includepattern=" + patterns
116 includecap = b"includepattern=" + patterns
117 bundlecaps.append(includecap)
117 bundlecaps.append(includecap)
118 if self._localrepo.excludepattern:
118 if self._localrepo.excludepattern:
119 patterns = b'\0'.join(self._localrepo.excludepattern)
119 patterns = b'\0'.join(self._localrepo.excludepattern)
120 excludecap = b"excludepattern=" + patterns
120 excludecap = b"excludepattern=" + patterns
121 bundlecaps.append(excludecap)
121 bundlecaps.append(excludecap)
122 opts[b'bundlecaps'] = b','.join(bundlecaps)
122 opts[b'bundlecaps'] = b','.join(bundlecaps)
123
123
124 def _sendrequest(self, command, args, **opts):
124 def _sendrequest(self, command, args, **opts):
125 self._updatecallstreamopts(command, args)
125 self._updatecallstreamopts(command, args)
126 return super(remotefilepeer, self)._sendrequest(
126 return super(remotefilepeer, self)._sendrequest(
127 command, args, **opts
127 command, args, **opts
128 )
128 )
129
129
130 def _callstream(self, command, **opts):
130 def _callstream(self, command, **opts):
131 supertype = super(remotefilepeer, self)
131 supertype = super(remotefilepeer, self)
132 if not util.safehasattr(supertype, b'_sendrequest'):
132 if not util.safehasattr(supertype, '_sendrequest'):
133 self._updatecallstreamopts(command, pycompat.byteskwargs(opts))
133 self._updatecallstreamopts(command, pycompat.byteskwargs(opts))
134 return super(remotefilepeer, self)._callstream(command, **opts)
134 return super(remotefilepeer, self)._callstream(command, **opts)
135
135
136 peer.__class__ = remotefilepeer
136 peer.__class__ = remotefilepeer
137
137
138
138
139 class cacheconnection(object):
139 class cacheconnection(object):
140 """The connection for communicating with the remote cache. Performs
140 """The connection for communicating with the remote cache. Performs
141 gets and sets by communicating with an external process that has the
141 gets and sets by communicating with an external process that has the
142 cache-specific implementation.
142 cache-specific implementation.
143 """
143 """
144
144
145 def __init__(self):
145 def __init__(self):
146 self.pipeo = self.pipei = self.pipee = None
146 self.pipeo = self.pipei = self.pipee = None
147 self.subprocess = None
147 self.subprocess = None
148 self.connected = False
148 self.connected = False
149
149
150 def connect(self, cachecommand):
150 def connect(self, cachecommand):
151 if self.pipeo:
151 if self.pipeo:
152 raise error.Abort(_(b"cache connection already open"))
152 raise error.Abort(_(b"cache connection already open"))
153 self.pipei, self.pipeo, self.pipee, self.subprocess = procutil.popen4(
153 self.pipei, self.pipeo, self.pipee, self.subprocess = procutil.popen4(
154 cachecommand
154 cachecommand
155 )
155 )
156 self.connected = True
156 self.connected = True
157
157
158 def close(self):
158 def close(self):
159 def tryclose(pipe):
159 def tryclose(pipe):
160 try:
160 try:
161 pipe.close()
161 pipe.close()
162 except Exception:
162 except Exception:
163 pass
163 pass
164
164
165 if self.connected:
165 if self.connected:
166 try:
166 try:
167 self.pipei.write(b"exit\n")
167 self.pipei.write(b"exit\n")
168 except Exception:
168 except Exception:
169 pass
169 pass
170 tryclose(self.pipei)
170 tryclose(self.pipei)
171 self.pipei = None
171 self.pipei = None
172 tryclose(self.pipeo)
172 tryclose(self.pipeo)
173 self.pipeo = None
173 self.pipeo = None
174 tryclose(self.pipee)
174 tryclose(self.pipee)
175 self.pipee = None
175 self.pipee = None
176 try:
176 try:
177 # Wait for process to terminate, making sure to avoid deadlock.
177 # Wait for process to terminate, making sure to avoid deadlock.
178 # See https://docs.python.org/2/library/subprocess.html for
178 # See https://docs.python.org/2/library/subprocess.html for
179 # warnings about wait() and deadlocking.
179 # warnings about wait() and deadlocking.
180 self.subprocess.communicate()
180 self.subprocess.communicate()
181 except Exception:
181 except Exception:
182 pass
182 pass
183 self.subprocess = None
183 self.subprocess = None
184 self.connected = False
184 self.connected = False
185
185
186 def request(self, request, flush=True):
186 def request(self, request, flush=True):
187 if self.connected:
187 if self.connected:
188 try:
188 try:
189 self.pipei.write(request)
189 self.pipei.write(request)
190 if flush:
190 if flush:
191 self.pipei.flush()
191 self.pipei.flush()
192 except IOError:
192 except IOError:
193 self.close()
193 self.close()
194
194
195 def receiveline(self):
195 def receiveline(self):
196 if not self.connected:
196 if not self.connected:
197 return None
197 return None
198 try:
198 try:
199 result = self.pipeo.readline()[:-1]
199 result = self.pipeo.readline()[:-1]
200 if not result:
200 if not result:
201 self.close()
201 self.close()
202 except IOError:
202 except IOError:
203 self.close()
203 self.close()
204
204
205 return result
205 return result
206
206
207
207
208 def _getfilesbatch(
208 def _getfilesbatch(
209 remote, receivemissing, progresstick, missed, idmap, batchsize
209 remote, receivemissing, progresstick, missed, idmap, batchsize
210 ):
210 ):
211 # Over http(s), iterbatch is a streamy method and we can start
211 # Over http(s), iterbatch is a streamy method and we can start
212 # looking at results early. This means we send one (potentially
212 # looking at results early. This means we send one (potentially
213 # large) request, but then we show nice progress as we process
213 # large) request, but then we show nice progress as we process
214 # file results, rather than showing chunks of $batchsize in
214 # file results, rather than showing chunks of $batchsize in
215 # progress.
215 # progress.
216 #
216 #
217 # Over ssh, iterbatch isn't streamy because batch() wasn't
217 # Over ssh, iterbatch isn't streamy because batch() wasn't
218 # explicitly designed as a streaming method. In the future we
218 # explicitly designed as a streaming method. In the future we
219 # should probably introduce a streambatch() method upstream and
219 # should probably introduce a streambatch() method upstream and
220 # use that for this.
220 # use that for this.
221 with remote.commandexecutor() as e:
221 with remote.commandexecutor() as e:
222 futures = []
222 futures = []
223 for m in missed:
223 for m in missed:
224 futures.append(
224 futures.append(
225 e.callcommand(
225 e.callcommand(
226 b'x_rfl_getfile', {b'file': idmap[m], b'node': m[-40:]}
226 b'x_rfl_getfile', {b'file': idmap[m], b'node': m[-40:]}
227 )
227 )
228 )
228 )
229
229
230 for i, m in enumerate(missed):
230 for i, m in enumerate(missed):
231 r = futures[i].result()
231 r = futures[i].result()
232 futures[i] = None # release memory
232 futures[i] = None # release memory
233 file_ = idmap[m]
233 file_ = idmap[m]
234 node = m[-40:]
234 node = m[-40:]
235 receivemissing(io.BytesIO(b'%d\n%s' % (len(r), r)), file_, node)
235 receivemissing(io.BytesIO(b'%d\n%s' % (len(r), r)), file_, node)
236 progresstick()
236 progresstick()
237
237
238
238
239 def _getfiles_optimistic(
239 def _getfiles_optimistic(
240 remote, receivemissing, progresstick, missed, idmap, step
240 remote, receivemissing, progresstick, missed, idmap, step
241 ):
241 ):
242 remote._callstream(b"x_rfl_getfiles")
242 remote._callstream(b"x_rfl_getfiles")
243 i = 0
243 i = 0
244 pipeo = remote._pipeo
244 pipeo = remote._pipeo
245 pipei = remote._pipei
245 pipei = remote._pipei
246 while i < len(missed):
246 while i < len(missed):
247 # issue a batch of requests
247 # issue a batch of requests
248 start = i
248 start = i
249 end = min(len(missed), start + step)
249 end = min(len(missed), start + step)
250 i = end
250 i = end
251 for missingid in missed[start:end]:
251 for missingid in missed[start:end]:
252 # issue new request
252 # issue new request
253 versionid = missingid[-40:]
253 versionid = missingid[-40:]
254 file = idmap[missingid]
254 file = idmap[missingid]
255 sshrequest = b"%s%s\n" % (versionid, file)
255 sshrequest = b"%s%s\n" % (versionid, file)
256 pipeo.write(sshrequest)
256 pipeo.write(sshrequest)
257 pipeo.flush()
257 pipeo.flush()
258
258
259 # receive batch results
259 # receive batch results
260 for missingid in missed[start:end]:
260 for missingid in missed[start:end]:
261 versionid = missingid[-40:]
261 versionid = missingid[-40:]
262 file = idmap[missingid]
262 file = idmap[missingid]
263 receivemissing(pipei, file, versionid)
263 receivemissing(pipei, file, versionid)
264 progresstick()
264 progresstick()
265
265
266 # End the command
266 # End the command
267 pipeo.write(b'\n')
267 pipeo.write(b'\n')
268 pipeo.flush()
268 pipeo.flush()
269
269
270
270
271 def _getfiles_threaded(
271 def _getfiles_threaded(
272 remote, receivemissing, progresstick, missed, idmap, step
272 remote, receivemissing, progresstick, missed, idmap, step
273 ):
273 ):
274 remote._callstream(b"getfiles")
274 remote._callstream(b"getfiles")
275 pipeo = remote._pipeo
275 pipeo = remote._pipeo
276 pipei = remote._pipei
276 pipei = remote._pipei
277
277
278 def writer():
278 def writer():
279 for missingid in missed:
279 for missingid in missed:
280 versionid = missingid[-40:]
280 versionid = missingid[-40:]
281 file = idmap[missingid]
281 file = idmap[missingid]
282 sshrequest = b"%s%s\n" % (versionid, file)
282 sshrequest = b"%s%s\n" % (versionid, file)
283 pipeo.write(sshrequest)
283 pipeo.write(sshrequest)
284 pipeo.flush()
284 pipeo.flush()
285
285
286 writerthread = threading.Thread(target=writer)
286 writerthread = threading.Thread(target=writer)
287 writerthread.daemon = True
287 writerthread.daemon = True
288 writerthread.start()
288 writerthread.start()
289
289
290 for missingid in missed:
290 for missingid in missed:
291 versionid = missingid[-40:]
291 versionid = missingid[-40:]
292 file = idmap[missingid]
292 file = idmap[missingid]
293 receivemissing(pipei, file, versionid)
293 receivemissing(pipei, file, versionid)
294 progresstick()
294 progresstick()
295
295
296 writerthread.join()
296 writerthread.join()
297 # End the command
297 # End the command
298 pipeo.write(b'\n')
298 pipeo.write(b'\n')
299 pipeo.flush()
299 pipeo.flush()
300
300
301
301
302 class fileserverclient(object):
302 class fileserverclient(object):
303 """A client for requesting files from the remote file server.
303 """A client for requesting files from the remote file server.
304 """
304 """
305
305
306 def __init__(self, repo):
306 def __init__(self, repo):
307 ui = repo.ui
307 ui = repo.ui
308 self.repo = repo
308 self.repo = repo
309 self.ui = ui
309 self.ui = ui
310 self.cacheprocess = ui.config(b"remotefilelog", b"cacheprocess")
310 self.cacheprocess = ui.config(b"remotefilelog", b"cacheprocess")
311 if self.cacheprocess:
311 if self.cacheprocess:
312 self.cacheprocess = util.expandpath(self.cacheprocess)
312 self.cacheprocess = util.expandpath(self.cacheprocess)
313
313
314 # This option causes remotefilelog to pass the full file path to the
314 # This option causes remotefilelog to pass the full file path to the
315 # cacheprocess instead of a hashed key.
315 # cacheprocess instead of a hashed key.
316 self.cacheprocesspasspath = ui.configbool(
316 self.cacheprocesspasspath = ui.configbool(
317 b"remotefilelog", b"cacheprocess.includepath"
317 b"remotefilelog", b"cacheprocess.includepath"
318 )
318 )
319
319
320 self.debugoutput = ui.configbool(b"remotefilelog", b"debug")
320 self.debugoutput = ui.configbool(b"remotefilelog", b"debug")
321
321
322 self.remotecache = cacheconnection()
322 self.remotecache = cacheconnection()
323
323
324 def setstore(self, datastore, historystore, writedata, writehistory):
324 def setstore(self, datastore, historystore, writedata, writehistory):
325 self.datastore = datastore
325 self.datastore = datastore
326 self.historystore = historystore
326 self.historystore = historystore
327 self.writedata = writedata
327 self.writedata = writedata
328 self.writehistory = writehistory
328 self.writehistory = writehistory
329
329
330 def _connect(self):
330 def _connect(self):
331 return self.repo.connectionpool.get(self.repo.fallbackpath)
331 return self.repo.connectionpool.get(self.repo.fallbackpath)
332
332
333 def request(self, fileids):
333 def request(self, fileids):
334 """Takes a list of filename/node pairs and fetches them from the
334 """Takes a list of filename/node pairs and fetches them from the
335 server. Files are stored in the local cache.
335 server. Files are stored in the local cache.
336 A list of nodes that the server couldn't find is returned.
336 A list of nodes that the server couldn't find is returned.
337 If the connection fails, an exception is raised.
337 If the connection fails, an exception is raised.
338 """
338 """
339 if not self.remotecache.connected:
339 if not self.remotecache.connected:
340 self.connect()
340 self.connect()
341 cache = self.remotecache
341 cache = self.remotecache
342 writedata = self.writedata
342 writedata = self.writedata
343
343
344 repo = self.repo
344 repo = self.repo
345 total = len(fileids)
345 total = len(fileids)
346 request = b"get\n%d\n" % total
346 request = b"get\n%d\n" % total
347 idmap = {}
347 idmap = {}
348 reponame = repo.name
348 reponame = repo.name
349 for file, id in fileids:
349 for file, id in fileids:
350 fullid = getcachekey(reponame, file, id)
350 fullid = getcachekey(reponame, file, id)
351 if self.cacheprocesspasspath:
351 if self.cacheprocesspasspath:
352 request += file + b'\0'
352 request += file + b'\0'
353 request += fullid + b"\n"
353 request += fullid + b"\n"
354 idmap[fullid] = file
354 idmap[fullid] = file
355
355
356 cache.request(request)
356 cache.request(request)
357
357
358 progress = self.ui.makeprogress(_(b'downloading'), total=total)
358 progress = self.ui.makeprogress(_(b'downloading'), total=total)
359 progress.update(0)
359 progress.update(0)
360
360
361 missed = []
361 missed = []
362 while True:
362 while True:
363 missingid = cache.receiveline()
363 missingid = cache.receiveline()
364 if not missingid:
364 if not missingid:
365 missedset = set(missed)
365 missedset = set(missed)
366 for missingid in idmap:
366 for missingid in idmap:
367 if not missingid in missedset:
367 if not missingid in missedset:
368 missed.append(missingid)
368 missed.append(missingid)
369 self.ui.warn(
369 self.ui.warn(
370 _(
370 _(
371 b"warning: cache connection closed early - "
371 b"warning: cache connection closed early - "
372 + b"falling back to server\n"
372 + b"falling back to server\n"
373 )
373 )
374 )
374 )
375 break
375 break
376 if missingid == b"0":
376 if missingid == b"0":
377 break
377 break
378 if missingid.startswith(b"_hits_"):
378 if missingid.startswith(b"_hits_"):
379 # receive progress reports
379 # receive progress reports
380 parts = missingid.split(b"_")
380 parts = missingid.split(b"_")
381 progress.increment(int(parts[2]))
381 progress.increment(int(parts[2]))
382 continue
382 continue
383
383
384 missed.append(missingid)
384 missed.append(missingid)
385
385
386 global fetchmisses
386 global fetchmisses
387 fetchmisses += len(missed)
387 fetchmisses += len(missed)
388
388
389 fromcache = total - len(missed)
389 fromcache = total - len(missed)
390 progress.update(fromcache, total=total)
390 progress.update(fromcache, total=total)
391 self.ui.log(
391 self.ui.log(
392 b"remotefilelog",
392 b"remotefilelog",
393 b"remote cache hit rate is %r of %r\n",
393 b"remote cache hit rate is %r of %r\n",
394 fromcache,
394 fromcache,
395 total,
395 total,
396 hit=fromcache,
396 hit=fromcache,
397 total=total,
397 total=total,
398 )
398 )
399
399
400 oldumask = os.umask(0o002)
400 oldumask = os.umask(0o002)
401 try:
401 try:
402 # receive cache misses from master
402 # receive cache misses from master
403 if missed:
403 if missed:
404 # When verbose is true, sshpeer prints 'running ssh...'
404 # When verbose is true, sshpeer prints 'running ssh...'
405 # to stdout, which can interfere with some command
405 # to stdout, which can interfere with some command
406 # outputs
406 # outputs
407 verbose = self.ui.verbose
407 verbose = self.ui.verbose
408 self.ui.verbose = False
408 self.ui.verbose = False
409 try:
409 try:
410 with self._connect() as conn:
410 with self._connect() as conn:
411 remote = conn.peer
411 remote = conn.peer
412 if remote.capable(
412 if remote.capable(
413 constants.NETWORK_CAP_LEGACY_SSH_GETFILES
413 constants.NETWORK_CAP_LEGACY_SSH_GETFILES
414 ):
414 ):
415 if not isinstance(remote, _sshv1peer):
415 if not isinstance(remote, _sshv1peer):
416 raise error.Abort(
416 raise error.Abort(
417 b'remotefilelog requires ssh ' b'servers'
417 b'remotefilelog requires ssh ' b'servers'
418 )
418 )
419 step = self.ui.configint(
419 step = self.ui.configint(
420 b'remotefilelog', b'getfilesstep'
420 b'remotefilelog', b'getfilesstep'
421 )
421 )
422 getfilestype = self.ui.config(
422 getfilestype = self.ui.config(
423 b'remotefilelog', b'getfilestype'
423 b'remotefilelog', b'getfilestype'
424 )
424 )
425 if getfilestype == b'threaded':
425 if getfilestype == b'threaded':
426 _getfiles = _getfiles_threaded
426 _getfiles = _getfiles_threaded
427 else:
427 else:
428 _getfiles = _getfiles_optimistic
428 _getfiles = _getfiles_optimistic
429 _getfiles(
429 _getfiles(
430 remote,
430 remote,
431 self.receivemissing,
431 self.receivemissing,
432 progress.increment,
432 progress.increment,
433 missed,
433 missed,
434 idmap,
434 idmap,
435 step,
435 step,
436 )
436 )
437 elif remote.capable(b"x_rfl_getfile"):
437 elif remote.capable(b"x_rfl_getfile"):
438 if remote.capable(b'batch'):
438 if remote.capable(b'batch'):
439 batchdefault = 100
439 batchdefault = 100
440 else:
440 else:
441 batchdefault = 10
441 batchdefault = 10
442 batchsize = self.ui.configint(
442 batchsize = self.ui.configint(
443 b'remotefilelog', b'batchsize', batchdefault
443 b'remotefilelog', b'batchsize', batchdefault
444 )
444 )
445 self.ui.debug(
445 self.ui.debug(
446 b'requesting %d files from '
446 b'requesting %d files from '
447 b'remotefilelog server...\n' % len(missed)
447 b'remotefilelog server...\n' % len(missed)
448 )
448 )
449 _getfilesbatch(
449 _getfilesbatch(
450 remote,
450 remote,
451 self.receivemissing,
451 self.receivemissing,
452 progress.increment,
452 progress.increment,
453 missed,
453 missed,
454 idmap,
454 idmap,
455 batchsize,
455 batchsize,
456 )
456 )
457 else:
457 else:
458 raise error.Abort(
458 raise error.Abort(
459 b"configured remotefilelog server"
459 b"configured remotefilelog server"
460 b" does not support remotefilelog"
460 b" does not support remotefilelog"
461 )
461 )
462
462
463 self.ui.log(
463 self.ui.log(
464 b"remotefilefetchlog",
464 b"remotefilefetchlog",
465 b"Success\n",
465 b"Success\n",
466 fetched_files=progress.pos - fromcache,
466 fetched_files=progress.pos - fromcache,
467 total_to_fetch=total - fromcache,
467 total_to_fetch=total - fromcache,
468 )
468 )
469 except Exception:
469 except Exception:
470 self.ui.log(
470 self.ui.log(
471 b"remotefilefetchlog",
471 b"remotefilefetchlog",
472 b"Fail\n",
472 b"Fail\n",
473 fetched_files=progress.pos - fromcache,
473 fetched_files=progress.pos - fromcache,
474 total_to_fetch=total - fromcache,
474 total_to_fetch=total - fromcache,
475 )
475 )
476 raise
476 raise
477 finally:
477 finally:
478 self.ui.verbose = verbose
478 self.ui.verbose = verbose
479 # send to memcache
479 # send to memcache
480 request = b"set\n%d\n%s\n" % (len(missed), b"\n".join(missed))
480 request = b"set\n%d\n%s\n" % (len(missed), b"\n".join(missed))
481 cache.request(request)
481 cache.request(request)
482
482
483 progress.complete()
483 progress.complete()
484
484
485 # mark ourselves as a user of this cache
485 # mark ourselves as a user of this cache
486 writedata.markrepo(self.repo.path)
486 writedata.markrepo(self.repo.path)
487 finally:
487 finally:
488 os.umask(oldumask)
488 os.umask(oldumask)
489
489
490 def receivemissing(self, pipe, filename, node):
490 def receivemissing(self, pipe, filename, node):
491 line = pipe.readline()[:-1]
491 line = pipe.readline()[:-1]
492 if not line:
492 if not line:
493 raise error.ResponseError(
493 raise error.ResponseError(
494 _(b"error downloading file contents:"),
494 _(b"error downloading file contents:"),
495 _(b"connection closed early"),
495 _(b"connection closed early"),
496 )
496 )
497 size = int(line)
497 size = int(line)
498 data = pipe.read(size)
498 data = pipe.read(size)
499 if len(data) != size:
499 if len(data) != size:
500 raise error.ResponseError(
500 raise error.ResponseError(
501 _(b"error downloading file contents:"),
501 _(b"error downloading file contents:"),
502 _(b"only received %s of %s bytes") % (len(data), size),
502 _(b"only received %s of %s bytes") % (len(data), size),
503 )
503 )
504
504
505 self.writedata.addremotefilelognode(
505 self.writedata.addremotefilelognode(
506 filename, bin(node), zlib.decompress(data)
506 filename, bin(node), zlib.decompress(data)
507 )
507 )
508
508
509 def connect(self):
509 def connect(self):
510 if self.cacheprocess:
510 if self.cacheprocess:
511 cmd = b"%s %s" % (self.cacheprocess, self.writedata._path)
511 cmd = b"%s %s" % (self.cacheprocess, self.writedata._path)
512 self.remotecache.connect(cmd)
512 self.remotecache.connect(cmd)
513 else:
513 else:
514 # If no cache process is specified, we fake one that always
514 # If no cache process is specified, we fake one that always
515 # returns cache misses. This enables tests to run easily
515 # returns cache misses. This enables tests to run easily
516 # and may eventually allow us to be a drop in replacement
516 # and may eventually allow us to be a drop in replacement
517 # for the largefiles extension.
517 # for the largefiles extension.
518 class simplecache(object):
518 class simplecache(object):
519 def __init__(self):
519 def __init__(self):
520 self.missingids = []
520 self.missingids = []
521 self.connected = True
521 self.connected = True
522
522
523 def close(self):
523 def close(self):
524 pass
524 pass
525
525
526 def request(self, value, flush=True):
526 def request(self, value, flush=True):
527 lines = value.split(b"\n")
527 lines = value.split(b"\n")
528 if lines[0] != b"get":
528 if lines[0] != b"get":
529 return
529 return
530 self.missingids = lines[2:-1]
530 self.missingids = lines[2:-1]
531 self.missingids.append(b'0')
531 self.missingids.append(b'0')
532
532
533 def receiveline(self):
533 def receiveline(self):
534 if len(self.missingids) > 0:
534 if len(self.missingids) > 0:
535 return self.missingids.pop(0)
535 return self.missingids.pop(0)
536 return None
536 return None
537
537
538 self.remotecache = simplecache()
538 self.remotecache = simplecache()
539
539
540 def close(self):
540 def close(self):
541 if fetches:
541 if fetches:
542 msg = (
542 msg = (
543 b"%d files fetched over %d fetches - "
543 b"%d files fetched over %d fetches - "
544 + b"(%d misses, %0.2f%% hit ratio) over %0.2fs\n"
544 + b"(%d misses, %0.2f%% hit ratio) over %0.2fs\n"
545 ) % (
545 ) % (
546 fetched,
546 fetched,
547 fetches,
547 fetches,
548 fetchmisses,
548 fetchmisses,
549 float(fetched - fetchmisses) / float(fetched) * 100.0,
549 float(fetched - fetchmisses) / float(fetched) * 100.0,
550 fetchcost,
550 fetchcost,
551 )
551 )
552 if self.debugoutput:
552 if self.debugoutput:
553 self.ui.warn(msg)
553 self.ui.warn(msg)
554 self.ui.log(
554 self.ui.log(
555 b"remotefilelog.prefetch",
555 b"remotefilelog.prefetch",
556 msg.replace(b"%", b"%%"),
556 msg.replace(b"%", b"%%"),
557 remotefilelogfetched=fetched,
557 remotefilelogfetched=fetched,
558 remotefilelogfetches=fetches,
558 remotefilelogfetches=fetches,
559 remotefilelogfetchmisses=fetchmisses,
559 remotefilelogfetchmisses=fetchmisses,
560 remotefilelogfetchtime=fetchcost * 1000,
560 remotefilelogfetchtime=fetchcost * 1000,
561 )
561 )
562
562
563 if self.remotecache.connected:
563 if self.remotecache.connected:
564 self.remotecache.close()
564 self.remotecache.close()
565
565
566 def prefetch(
566 def prefetch(
567 self, fileids, force=False, fetchdata=True, fetchhistory=False
567 self, fileids, force=False, fetchdata=True, fetchhistory=False
568 ):
568 ):
569 """downloads the given file versions to the cache
569 """downloads the given file versions to the cache
570 """
570 """
571 repo = self.repo
571 repo = self.repo
572 idstocheck = []
572 idstocheck = []
573 for file, id in fileids:
573 for file, id in fileids:
574 # hack
574 # hack
575 # - we don't use .hgtags
575 # - we don't use .hgtags
576 # - workingctx produces ids with length 42,
576 # - workingctx produces ids with length 42,
577 # which we skip since they aren't in any cache
577 # which we skip since they aren't in any cache
578 if (
578 if (
579 file == b'.hgtags'
579 file == b'.hgtags'
580 or len(id) == 42
580 or len(id) == 42
581 or not repo.shallowmatch(file)
581 or not repo.shallowmatch(file)
582 ):
582 ):
583 continue
583 continue
584
584
585 idstocheck.append((file, bin(id)))
585 idstocheck.append((file, bin(id)))
586
586
587 datastore = self.datastore
587 datastore = self.datastore
588 historystore = self.historystore
588 historystore = self.historystore
589 if force:
589 if force:
590 datastore = contentstore.unioncontentstore(*repo.shareddatastores)
590 datastore = contentstore.unioncontentstore(*repo.shareddatastores)
591 historystore = metadatastore.unionmetadatastore(
591 historystore = metadatastore.unionmetadatastore(
592 *repo.sharedhistorystores
592 *repo.sharedhistorystores
593 )
593 )
594
594
595 missingids = set()
595 missingids = set()
596 if fetchdata:
596 if fetchdata:
597 missingids.update(datastore.getmissing(idstocheck))
597 missingids.update(datastore.getmissing(idstocheck))
598 if fetchhistory:
598 if fetchhistory:
599 missingids.update(historystore.getmissing(idstocheck))
599 missingids.update(historystore.getmissing(idstocheck))
600
600
601 # partition missing nodes into nullid and not-nullid so we can
601 # partition missing nodes into nullid and not-nullid so we can
602 # warn about this filtering potentially shadowing bugs.
602 # warn about this filtering potentially shadowing bugs.
603 nullids = len([None for unused, id in missingids if id == nullid])
603 nullids = len([None for unused, id in missingids if id == nullid])
604 if nullids:
604 if nullids:
605 missingids = [(f, id) for f, id in missingids if id != nullid]
605 missingids = [(f, id) for f, id in missingids if id != nullid]
606 repo.ui.develwarn(
606 repo.ui.develwarn(
607 (
607 (
608 b'remotefilelog not fetching %d null revs'
608 b'remotefilelog not fetching %d null revs'
609 b' - this is likely hiding bugs' % nullids
609 b' - this is likely hiding bugs' % nullids
610 ),
610 ),
611 config=b'remotefilelog-ext',
611 config=b'remotefilelog-ext',
612 )
612 )
613 if missingids:
613 if missingids:
614 global fetches, fetched, fetchcost
614 global fetches, fetched, fetchcost
615 fetches += 1
615 fetches += 1
616
616
617 # We want to be able to detect excess individual file downloads, so
617 # We want to be able to detect excess individual file downloads, so
618 # let's log that information for debugging.
618 # let's log that information for debugging.
619 if fetches >= 15 and fetches < 18:
619 if fetches >= 15 and fetches < 18:
620 if fetches == 15:
620 if fetches == 15:
621 fetchwarning = self.ui.config(
621 fetchwarning = self.ui.config(
622 b'remotefilelog', b'fetchwarning'
622 b'remotefilelog', b'fetchwarning'
623 )
623 )
624 if fetchwarning:
624 if fetchwarning:
625 self.ui.warn(fetchwarning + b'\n')
625 self.ui.warn(fetchwarning + b'\n')
626 self.logstacktrace()
626 self.logstacktrace()
627 missingids = [(file, hex(id)) for file, id in sorted(missingids)]
627 missingids = [(file, hex(id)) for file, id in sorted(missingids)]
628 fetched += len(missingids)
628 fetched += len(missingids)
629 start = time.time()
629 start = time.time()
630 missingids = self.request(missingids)
630 missingids = self.request(missingids)
631 if missingids:
631 if missingids:
632 raise error.Abort(
632 raise error.Abort(
633 _(b"unable to download %d files") % len(missingids)
633 _(b"unable to download %d files") % len(missingids)
634 )
634 )
635 fetchcost += time.time() - start
635 fetchcost += time.time() - start
636 self._lfsprefetch(fileids)
636 self._lfsprefetch(fileids)
637
637
638 def _lfsprefetch(self, fileids):
638 def _lfsprefetch(self, fileids):
639 if not _lfsmod or not util.safehasattr(
639 if not _lfsmod or not util.safehasattr(
640 self.repo.svfs, b'lfslocalblobstore'
640 self.repo.svfs, b'lfslocalblobstore'
641 ):
641 ):
642 return
642 return
643 if not _lfsmod.wrapper.candownload(self.repo):
643 if not _lfsmod.wrapper.candownload(self.repo):
644 return
644 return
645 pointers = []
645 pointers = []
646 store = self.repo.svfs.lfslocalblobstore
646 store = self.repo.svfs.lfslocalblobstore
647 for file, id in fileids:
647 for file, id in fileids:
648 node = bin(id)
648 node = bin(id)
649 rlog = self.repo.file(file)
649 rlog = self.repo.file(file)
650 if rlog.flags(node) & revlog.REVIDX_EXTSTORED:
650 if rlog.flags(node) & revlog.REVIDX_EXTSTORED:
651 text = rlog.rawdata(node)
651 text = rlog.rawdata(node)
652 p = _lfsmod.pointer.deserialize(text)
652 p = _lfsmod.pointer.deserialize(text)
653 oid = p.oid()
653 oid = p.oid()
654 if not store.has(oid):
654 if not store.has(oid):
655 pointers.append(p)
655 pointers.append(p)
656 if len(pointers) > 0:
656 if len(pointers) > 0:
657 self.repo.svfs.lfsremoteblobstore.readbatch(pointers, store)
657 self.repo.svfs.lfsremoteblobstore.readbatch(pointers, store)
658 assert all(store.has(p.oid()) for p in pointers)
658 assert all(store.has(p.oid()) for p in pointers)
659
659
660 def logstacktrace(self):
660 def logstacktrace(self):
661 import traceback
661 import traceback
662
662
663 self.ui.log(
663 self.ui.log(
664 b'remotefilelog',
664 b'remotefilelog',
665 b'excess remotefilelog fetching:\n%s\n',
665 b'excess remotefilelog fetching:\n%s\n',
666 b''.join(traceback.format_stack()),
666 b''.join(traceback.format_stack()),
667 )
667 )
@@ -1,912 +1,912 b''
1 from __future__ import absolute_import
1 from __future__ import absolute_import
2
2
3 import os
3 import os
4 import time
4 import time
5
5
6 from mercurial.i18n import _
6 from mercurial.i18n import _
7 from mercurial.node import (
7 from mercurial.node import (
8 nullid,
8 nullid,
9 short,
9 short,
10 )
10 )
11 from mercurial import (
11 from mercurial import (
12 encoding,
12 encoding,
13 error,
13 error,
14 lock as lockmod,
14 lock as lockmod,
15 mdiff,
15 mdiff,
16 policy,
16 policy,
17 pycompat,
17 pycompat,
18 scmutil,
18 scmutil,
19 util,
19 util,
20 vfs,
20 vfs,
21 )
21 )
22 from mercurial.utils import procutil
22 from mercurial.utils import procutil
23 from . import (
23 from . import (
24 constants,
24 constants,
25 contentstore,
25 contentstore,
26 datapack,
26 datapack,
27 historypack,
27 historypack,
28 metadatastore,
28 metadatastore,
29 shallowutil,
29 shallowutil,
30 )
30 )
31
31
32 osutil = policy.importmod(r'osutil')
32 osutil = policy.importmod(r'osutil')
33
33
34
34
35 class RepackAlreadyRunning(error.Abort):
35 class RepackAlreadyRunning(error.Abort):
36 pass
36 pass
37
37
38
38
39 def backgroundrepack(
39 def backgroundrepack(
40 repo, incremental=True, packsonly=False, ensurestart=False
40 repo, incremental=True, packsonly=False, ensurestart=False
41 ):
41 ):
42 cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'repack']
42 cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'repack']
43 msg = _(b"(running background repack)\n")
43 msg = _(b"(running background repack)\n")
44 if incremental:
44 if incremental:
45 cmd.append(b'--incremental')
45 cmd.append(b'--incremental')
46 msg = _(b"(running background incremental repack)\n")
46 msg = _(b"(running background incremental repack)\n")
47 if packsonly:
47 if packsonly:
48 cmd.append(b'--packsonly')
48 cmd.append(b'--packsonly')
49 repo.ui.warn(msg)
49 repo.ui.warn(msg)
50 # We know this command will find a binary, so don't block on it starting.
50 # We know this command will find a binary, so don't block on it starting.
51 procutil.runbgcommand(cmd, encoding.environ, ensurestart=ensurestart)
51 procutil.runbgcommand(cmd, encoding.environ, ensurestart=ensurestart)
52
52
53
53
54 def fullrepack(repo, options=None):
54 def fullrepack(repo, options=None):
55 """If ``packsonly`` is True, stores creating only loose objects are skipped.
55 """If ``packsonly`` is True, stores creating only loose objects are skipped.
56 """
56 """
57 if util.safehasattr(repo, b'shareddatastores'):
57 if util.safehasattr(repo, 'shareddatastores'):
58 datasource = contentstore.unioncontentstore(*repo.shareddatastores)
58 datasource = contentstore.unioncontentstore(*repo.shareddatastores)
59 historysource = metadatastore.unionmetadatastore(
59 historysource = metadatastore.unionmetadatastore(
60 *repo.sharedhistorystores, allowincomplete=True
60 *repo.sharedhistorystores, allowincomplete=True
61 )
61 )
62
62
63 packpath = shallowutil.getcachepackpath(
63 packpath = shallowutil.getcachepackpath(
64 repo, constants.FILEPACK_CATEGORY
64 repo, constants.FILEPACK_CATEGORY
65 )
65 )
66 _runrepack(
66 _runrepack(
67 repo,
67 repo,
68 datasource,
68 datasource,
69 historysource,
69 historysource,
70 packpath,
70 packpath,
71 constants.FILEPACK_CATEGORY,
71 constants.FILEPACK_CATEGORY,
72 options=options,
72 options=options,
73 )
73 )
74
74
75 if util.safehasattr(repo.manifestlog, b'datastore'):
75 if util.safehasattr(repo.manifestlog, 'datastore'):
76 localdata, shareddata = _getmanifeststores(repo)
76 localdata, shareddata = _getmanifeststores(repo)
77 lpackpath, ldstores, lhstores = localdata
77 lpackpath, ldstores, lhstores = localdata
78 spackpath, sdstores, shstores = shareddata
78 spackpath, sdstores, shstores = shareddata
79
79
80 # Repack the shared manifest store
80 # Repack the shared manifest store
81 datasource = contentstore.unioncontentstore(*sdstores)
81 datasource = contentstore.unioncontentstore(*sdstores)
82 historysource = metadatastore.unionmetadatastore(
82 historysource = metadatastore.unionmetadatastore(
83 *shstores, allowincomplete=True
83 *shstores, allowincomplete=True
84 )
84 )
85 _runrepack(
85 _runrepack(
86 repo,
86 repo,
87 datasource,
87 datasource,
88 historysource,
88 historysource,
89 spackpath,
89 spackpath,
90 constants.TREEPACK_CATEGORY,
90 constants.TREEPACK_CATEGORY,
91 options=options,
91 options=options,
92 )
92 )
93
93
94 # Repack the local manifest store
94 # Repack the local manifest store
95 datasource = contentstore.unioncontentstore(
95 datasource = contentstore.unioncontentstore(
96 *ldstores, allowincomplete=True
96 *ldstores, allowincomplete=True
97 )
97 )
98 historysource = metadatastore.unionmetadatastore(
98 historysource = metadatastore.unionmetadatastore(
99 *lhstores, allowincomplete=True
99 *lhstores, allowincomplete=True
100 )
100 )
101 _runrepack(
101 _runrepack(
102 repo,
102 repo,
103 datasource,
103 datasource,
104 historysource,
104 historysource,
105 lpackpath,
105 lpackpath,
106 constants.TREEPACK_CATEGORY,
106 constants.TREEPACK_CATEGORY,
107 options=options,
107 options=options,
108 )
108 )
109
109
110
110
111 def incrementalrepack(repo, options=None):
111 def incrementalrepack(repo, options=None):
112 """This repacks the repo by looking at the distribution of pack files in the
112 """This repacks the repo by looking at the distribution of pack files in the
113 repo and performing the most minimal repack to keep the repo in good shape.
113 repo and performing the most minimal repack to keep the repo in good shape.
114 """
114 """
115 if util.safehasattr(repo, b'shareddatastores'):
115 if util.safehasattr(repo, 'shareddatastores'):
116 packpath = shallowutil.getcachepackpath(
116 packpath = shallowutil.getcachepackpath(
117 repo, constants.FILEPACK_CATEGORY
117 repo, constants.FILEPACK_CATEGORY
118 )
118 )
119 _incrementalrepack(
119 _incrementalrepack(
120 repo,
120 repo,
121 repo.shareddatastores,
121 repo.shareddatastores,
122 repo.sharedhistorystores,
122 repo.sharedhistorystores,
123 packpath,
123 packpath,
124 constants.FILEPACK_CATEGORY,
124 constants.FILEPACK_CATEGORY,
125 options=options,
125 options=options,
126 )
126 )
127
127
128 if util.safehasattr(repo.manifestlog, b'datastore'):
128 if util.safehasattr(repo.manifestlog, 'datastore'):
129 localdata, shareddata = _getmanifeststores(repo)
129 localdata, shareddata = _getmanifeststores(repo)
130 lpackpath, ldstores, lhstores = localdata
130 lpackpath, ldstores, lhstores = localdata
131 spackpath, sdstores, shstores = shareddata
131 spackpath, sdstores, shstores = shareddata
132
132
133 # Repack the shared manifest store
133 # Repack the shared manifest store
134 _incrementalrepack(
134 _incrementalrepack(
135 repo,
135 repo,
136 sdstores,
136 sdstores,
137 shstores,
137 shstores,
138 spackpath,
138 spackpath,
139 constants.TREEPACK_CATEGORY,
139 constants.TREEPACK_CATEGORY,
140 options=options,
140 options=options,
141 )
141 )
142
142
143 # Repack the local manifest store
143 # Repack the local manifest store
144 _incrementalrepack(
144 _incrementalrepack(
145 repo,
145 repo,
146 ldstores,
146 ldstores,
147 lhstores,
147 lhstores,
148 lpackpath,
148 lpackpath,
149 constants.TREEPACK_CATEGORY,
149 constants.TREEPACK_CATEGORY,
150 allowincompletedata=True,
150 allowincompletedata=True,
151 options=options,
151 options=options,
152 )
152 )
153
153
154
154
155 def _getmanifeststores(repo):
155 def _getmanifeststores(repo):
156 shareddatastores = repo.manifestlog.shareddatastores
156 shareddatastores = repo.manifestlog.shareddatastores
157 localdatastores = repo.manifestlog.localdatastores
157 localdatastores = repo.manifestlog.localdatastores
158 sharedhistorystores = repo.manifestlog.sharedhistorystores
158 sharedhistorystores = repo.manifestlog.sharedhistorystores
159 localhistorystores = repo.manifestlog.localhistorystores
159 localhistorystores = repo.manifestlog.localhistorystores
160
160
161 sharedpackpath = shallowutil.getcachepackpath(
161 sharedpackpath = shallowutil.getcachepackpath(
162 repo, constants.TREEPACK_CATEGORY
162 repo, constants.TREEPACK_CATEGORY
163 )
163 )
164 localpackpath = shallowutil.getlocalpackpath(
164 localpackpath = shallowutil.getlocalpackpath(
165 repo.svfs.vfs.base, constants.TREEPACK_CATEGORY
165 repo.svfs.vfs.base, constants.TREEPACK_CATEGORY
166 )
166 )
167
167
168 return (
168 return (
169 (localpackpath, localdatastores, localhistorystores),
169 (localpackpath, localdatastores, localhistorystores),
170 (sharedpackpath, shareddatastores, sharedhistorystores),
170 (sharedpackpath, shareddatastores, sharedhistorystores),
171 )
171 )
172
172
173
173
174 def _topacks(packpath, files, constructor):
174 def _topacks(packpath, files, constructor):
175 paths = list(os.path.join(packpath, p) for p in files)
175 paths = list(os.path.join(packpath, p) for p in files)
176 packs = list(constructor(p) for p in paths)
176 packs = list(constructor(p) for p in paths)
177 return packs
177 return packs
178
178
179
179
180 def _deletebigpacks(repo, folder, files):
180 def _deletebigpacks(repo, folder, files):
181 """Deletes packfiles that are bigger than ``packs.maxpacksize``.
181 """Deletes packfiles that are bigger than ``packs.maxpacksize``.
182
182
183 Returns ``files` with the removed files omitted."""
183 Returns ``files` with the removed files omitted."""
184 maxsize = repo.ui.configbytes(b"packs", b"maxpacksize")
184 maxsize = repo.ui.configbytes(b"packs", b"maxpacksize")
185 if maxsize <= 0:
185 if maxsize <= 0:
186 return files
186 return files
187
187
188 # This only considers datapacks today, but we could broaden it to include
188 # This only considers datapacks today, but we could broaden it to include
189 # historypacks.
189 # historypacks.
190 VALIDEXTS = [b".datapack", b".dataidx"]
190 VALIDEXTS = [b".datapack", b".dataidx"]
191
191
192 # Either an oversize index or datapack will trigger cleanup of the whole
192 # Either an oversize index or datapack will trigger cleanup of the whole
193 # pack:
193 # pack:
194 oversized = {
194 oversized = {
195 os.path.splitext(path)[0]
195 os.path.splitext(path)[0]
196 for path, ftype, stat in files
196 for path, ftype, stat in files
197 if (stat.st_size > maxsize and (os.path.splitext(path)[1] in VALIDEXTS))
197 if (stat.st_size > maxsize and (os.path.splitext(path)[1] in VALIDEXTS))
198 }
198 }
199
199
200 for rootfname in oversized:
200 for rootfname in oversized:
201 rootpath = os.path.join(folder, rootfname)
201 rootpath = os.path.join(folder, rootfname)
202 for ext in VALIDEXTS:
202 for ext in VALIDEXTS:
203 path = rootpath + ext
203 path = rootpath + ext
204 repo.ui.debug(
204 repo.ui.debug(
205 b'removing oversize packfile %s (%s)\n'
205 b'removing oversize packfile %s (%s)\n'
206 % (path, util.bytecount(os.stat(path).st_size))
206 % (path, util.bytecount(os.stat(path).st_size))
207 )
207 )
208 os.unlink(path)
208 os.unlink(path)
209 return [row for row in files if os.path.basename(row[0]) not in oversized]
209 return [row for row in files if os.path.basename(row[0]) not in oversized]
210
210
211
211
212 def _incrementalrepack(
212 def _incrementalrepack(
213 repo,
213 repo,
214 datastore,
214 datastore,
215 historystore,
215 historystore,
216 packpath,
216 packpath,
217 category,
217 category,
218 allowincompletedata=False,
218 allowincompletedata=False,
219 options=None,
219 options=None,
220 ):
220 ):
221 shallowutil.mkstickygroupdir(repo.ui, packpath)
221 shallowutil.mkstickygroupdir(repo.ui, packpath)
222
222
223 files = osutil.listdir(packpath, stat=True)
223 files = osutil.listdir(packpath, stat=True)
224 files = _deletebigpacks(repo, packpath, files)
224 files = _deletebigpacks(repo, packpath, files)
225 datapacks = _topacks(
225 datapacks = _topacks(
226 packpath, _computeincrementaldatapack(repo.ui, files), datapack.datapack
226 packpath, _computeincrementaldatapack(repo.ui, files), datapack.datapack
227 )
227 )
228 datapacks.extend(
228 datapacks.extend(
229 s for s in datastore if not isinstance(s, datapack.datapackstore)
229 s for s in datastore if not isinstance(s, datapack.datapackstore)
230 )
230 )
231
231
232 historypacks = _topacks(
232 historypacks = _topacks(
233 packpath,
233 packpath,
234 _computeincrementalhistorypack(repo.ui, files),
234 _computeincrementalhistorypack(repo.ui, files),
235 historypack.historypack,
235 historypack.historypack,
236 )
236 )
237 historypacks.extend(
237 historypacks.extend(
238 s
238 s
239 for s in historystore
239 for s in historystore
240 if not isinstance(s, historypack.historypackstore)
240 if not isinstance(s, historypack.historypackstore)
241 )
241 )
242
242
243 # ``allhistory{files,packs}`` contains all known history packs, even ones we
243 # ``allhistory{files,packs}`` contains all known history packs, even ones we
244 # don't plan to repack. They are used during the datapack repack to ensure
244 # don't plan to repack. They are used during the datapack repack to ensure
245 # good ordering of nodes.
245 # good ordering of nodes.
246 allhistoryfiles = _allpackfileswithsuffix(
246 allhistoryfiles = _allpackfileswithsuffix(
247 files, historypack.PACKSUFFIX, historypack.INDEXSUFFIX
247 files, historypack.PACKSUFFIX, historypack.INDEXSUFFIX
248 )
248 )
249 allhistorypacks = _topacks(
249 allhistorypacks = _topacks(
250 packpath,
250 packpath,
251 (f for f, mode, stat in allhistoryfiles),
251 (f for f, mode, stat in allhistoryfiles),
252 historypack.historypack,
252 historypack.historypack,
253 )
253 )
254 allhistorypacks.extend(
254 allhistorypacks.extend(
255 s
255 s
256 for s in historystore
256 for s in historystore
257 if not isinstance(s, historypack.historypackstore)
257 if not isinstance(s, historypack.historypackstore)
258 )
258 )
259 _runrepack(
259 _runrepack(
260 repo,
260 repo,
261 contentstore.unioncontentstore(
261 contentstore.unioncontentstore(
262 *datapacks, allowincomplete=allowincompletedata
262 *datapacks, allowincomplete=allowincompletedata
263 ),
263 ),
264 metadatastore.unionmetadatastore(*historypacks, allowincomplete=True),
264 metadatastore.unionmetadatastore(*historypacks, allowincomplete=True),
265 packpath,
265 packpath,
266 category,
266 category,
267 fullhistory=metadatastore.unionmetadatastore(
267 fullhistory=metadatastore.unionmetadatastore(
268 *allhistorypacks, allowincomplete=True
268 *allhistorypacks, allowincomplete=True
269 ),
269 ),
270 options=options,
270 options=options,
271 )
271 )
272
272
273
273
274 def _computeincrementaldatapack(ui, files):
274 def _computeincrementaldatapack(ui, files):
275 opts = {
275 opts = {
276 b'gencountlimit': ui.configint(b'remotefilelog', b'data.gencountlimit'),
276 b'gencountlimit': ui.configint(b'remotefilelog', b'data.gencountlimit'),
277 b'generations': ui.configlist(b'remotefilelog', b'data.generations'),
277 b'generations': ui.configlist(b'remotefilelog', b'data.generations'),
278 b'maxrepackpacks': ui.configint(
278 b'maxrepackpacks': ui.configint(
279 b'remotefilelog', b'data.maxrepackpacks'
279 b'remotefilelog', b'data.maxrepackpacks'
280 ),
280 ),
281 b'repackmaxpacksize': ui.configbytes(
281 b'repackmaxpacksize': ui.configbytes(
282 b'remotefilelog', b'data.repackmaxpacksize'
282 b'remotefilelog', b'data.repackmaxpacksize'
283 ),
283 ),
284 b'repacksizelimit': ui.configbytes(
284 b'repacksizelimit': ui.configbytes(
285 b'remotefilelog', b'data.repacksizelimit'
285 b'remotefilelog', b'data.repacksizelimit'
286 ),
286 ),
287 }
287 }
288
288
289 packfiles = _allpackfileswithsuffix(
289 packfiles = _allpackfileswithsuffix(
290 files, datapack.PACKSUFFIX, datapack.INDEXSUFFIX
290 files, datapack.PACKSUFFIX, datapack.INDEXSUFFIX
291 )
291 )
292 return _computeincrementalpack(packfiles, opts)
292 return _computeincrementalpack(packfiles, opts)
293
293
294
294
295 def _computeincrementalhistorypack(ui, files):
295 def _computeincrementalhistorypack(ui, files):
296 opts = {
296 opts = {
297 b'gencountlimit': ui.configint(
297 b'gencountlimit': ui.configint(
298 b'remotefilelog', b'history.gencountlimit'
298 b'remotefilelog', b'history.gencountlimit'
299 ),
299 ),
300 b'generations': ui.configlist(
300 b'generations': ui.configlist(
301 b'remotefilelog', b'history.generations', [b'100MB']
301 b'remotefilelog', b'history.generations', [b'100MB']
302 ),
302 ),
303 b'maxrepackpacks': ui.configint(
303 b'maxrepackpacks': ui.configint(
304 b'remotefilelog', b'history.maxrepackpacks'
304 b'remotefilelog', b'history.maxrepackpacks'
305 ),
305 ),
306 b'repackmaxpacksize': ui.configbytes(
306 b'repackmaxpacksize': ui.configbytes(
307 b'remotefilelog', b'history.repackmaxpacksize', b'400MB'
307 b'remotefilelog', b'history.repackmaxpacksize', b'400MB'
308 ),
308 ),
309 b'repacksizelimit': ui.configbytes(
309 b'repacksizelimit': ui.configbytes(
310 b'remotefilelog', b'history.repacksizelimit'
310 b'remotefilelog', b'history.repacksizelimit'
311 ),
311 ),
312 }
312 }
313
313
314 packfiles = _allpackfileswithsuffix(
314 packfiles = _allpackfileswithsuffix(
315 files, historypack.PACKSUFFIX, historypack.INDEXSUFFIX
315 files, historypack.PACKSUFFIX, historypack.INDEXSUFFIX
316 )
316 )
317 return _computeincrementalpack(packfiles, opts)
317 return _computeincrementalpack(packfiles, opts)
318
318
319
319
320 def _allpackfileswithsuffix(files, packsuffix, indexsuffix):
320 def _allpackfileswithsuffix(files, packsuffix, indexsuffix):
321 result = []
321 result = []
322 fileset = set(fn for fn, mode, stat in files)
322 fileset = set(fn for fn, mode, stat in files)
323 for filename, mode, stat in files:
323 for filename, mode, stat in files:
324 if not filename.endswith(packsuffix):
324 if not filename.endswith(packsuffix):
325 continue
325 continue
326
326
327 prefix = filename[: -len(packsuffix)]
327 prefix = filename[: -len(packsuffix)]
328
328
329 # Don't process a pack if it doesn't have an index.
329 # Don't process a pack if it doesn't have an index.
330 if (prefix + indexsuffix) not in fileset:
330 if (prefix + indexsuffix) not in fileset:
331 continue
331 continue
332 result.append((prefix, mode, stat))
332 result.append((prefix, mode, stat))
333
333
334 return result
334 return result
335
335
336
336
337 def _computeincrementalpack(files, opts):
337 def _computeincrementalpack(files, opts):
338 """Given a set of pack files along with the configuration options, this
338 """Given a set of pack files along with the configuration options, this
339 function computes the list of files that should be packed as part of an
339 function computes the list of files that should be packed as part of an
340 incremental repack.
340 incremental repack.
341
341
342 It tries to strike a balance between keeping incremental repacks cheap (i.e.
342 It tries to strike a balance between keeping incremental repacks cheap (i.e.
343 packing small things when possible, and rolling the packs up to the big ones
343 packing small things when possible, and rolling the packs up to the big ones
344 over time).
344 over time).
345 """
345 """
346
346
347 limits = list(
347 limits = list(
348 sorted((util.sizetoint(s) for s in opts[b'generations']), reverse=True)
348 sorted((util.sizetoint(s) for s in opts[b'generations']), reverse=True)
349 )
349 )
350 limits.append(0)
350 limits.append(0)
351
351
352 # Group the packs by generation (i.e. by size)
352 # Group the packs by generation (i.e. by size)
353 generations = []
353 generations = []
354 for i in pycompat.xrange(len(limits)):
354 for i in pycompat.xrange(len(limits)):
355 generations.append([])
355 generations.append([])
356
356
357 sizes = {}
357 sizes = {}
358 for prefix, mode, stat in files:
358 for prefix, mode, stat in files:
359 size = stat.st_size
359 size = stat.st_size
360 if size > opts[b'repackmaxpacksize']:
360 if size > opts[b'repackmaxpacksize']:
361 continue
361 continue
362
362
363 sizes[prefix] = size
363 sizes[prefix] = size
364 for i, limit in enumerate(limits):
364 for i, limit in enumerate(limits):
365 if size > limit:
365 if size > limit:
366 generations[i].append(prefix)
366 generations[i].append(prefix)
367 break
367 break
368
368
369 # Steps for picking what packs to repack:
369 # Steps for picking what packs to repack:
370 # 1. Pick the largest generation with > gencountlimit pack files.
370 # 1. Pick the largest generation with > gencountlimit pack files.
371 # 2. Take the smallest three packs.
371 # 2. Take the smallest three packs.
372 # 3. While total-size-of-packs < repacksizelimit: add another pack
372 # 3. While total-size-of-packs < repacksizelimit: add another pack
373
373
374 # Find the largest generation with more than gencountlimit packs
374 # Find the largest generation with more than gencountlimit packs
375 genpacks = []
375 genpacks = []
376 for i, limit in enumerate(limits):
376 for i, limit in enumerate(limits):
377 if len(generations[i]) > opts[b'gencountlimit']:
377 if len(generations[i]) > opts[b'gencountlimit']:
378 # Sort to be smallest last, for easy popping later
378 # Sort to be smallest last, for easy popping later
379 genpacks.extend(
379 genpacks.extend(
380 sorted(generations[i], reverse=True, key=lambda x: sizes[x])
380 sorted(generations[i], reverse=True, key=lambda x: sizes[x])
381 )
381 )
382 break
382 break
383
383
384 # Take as many packs from the generation as we can
384 # Take as many packs from the generation as we can
385 chosenpacks = genpacks[-3:]
385 chosenpacks = genpacks[-3:]
386 genpacks = genpacks[:-3]
386 genpacks = genpacks[:-3]
387 repacksize = sum(sizes[n] for n in chosenpacks)
387 repacksize = sum(sizes[n] for n in chosenpacks)
388 while (
388 while (
389 repacksize < opts[b'repacksizelimit']
389 repacksize < opts[b'repacksizelimit']
390 and genpacks
390 and genpacks
391 and len(chosenpacks) < opts[b'maxrepackpacks']
391 and len(chosenpacks) < opts[b'maxrepackpacks']
392 ):
392 ):
393 chosenpacks.append(genpacks.pop())
393 chosenpacks.append(genpacks.pop())
394 repacksize += sizes[chosenpacks[-1]]
394 repacksize += sizes[chosenpacks[-1]]
395
395
396 return chosenpacks
396 return chosenpacks
397
397
398
398
399 def _runrepack(
399 def _runrepack(
400 repo, data, history, packpath, category, fullhistory=None, options=None
400 repo, data, history, packpath, category, fullhistory=None, options=None
401 ):
401 ):
402 shallowutil.mkstickygroupdir(repo.ui, packpath)
402 shallowutil.mkstickygroupdir(repo.ui, packpath)
403
403
404 def isold(repo, filename, node):
404 def isold(repo, filename, node):
405 """Check if the file node is older than a limit.
405 """Check if the file node is older than a limit.
406 Unless a limit is specified in the config the default limit is taken.
406 Unless a limit is specified in the config the default limit is taken.
407 """
407 """
408 filectx = repo.filectx(filename, fileid=node)
408 filectx = repo.filectx(filename, fileid=node)
409 filetime = repo[filectx.linkrev()].date()
409 filetime = repo[filectx.linkrev()].date()
410
410
411 ttl = repo.ui.configint(b'remotefilelog', b'nodettl')
411 ttl = repo.ui.configint(b'remotefilelog', b'nodettl')
412
412
413 limit = time.time() - ttl
413 limit = time.time() - ttl
414 return filetime[0] < limit
414 return filetime[0] < limit
415
415
416 garbagecollect = repo.ui.configbool(b'remotefilelog', b'gcrepack')
416 garbagecollect = repo.ui.configbool(b'remotefilelog', b'gcrepack')
417 if not fullhistory:
417 if not fullhistory:
418 fullhistory = history
418 fullhistory = history
419 packer = repacker(
419 packer = repacker(
420 repo,
420 repo,
421 data,
421 data,
422 history,
422 history,
423 fullhistory,
423 fullhistory,
424 category,
424 category,
425 gc=garbagecollect,
425 gc=garbagecollect,
426 isold=isold,
426 isold=isold,
427 options=options,
427 options=options,
428 )
428 )
429
429
430 with datapack.mutabledatapack(repo.ui, packpath) as dpack:
430 with datapack.mutabledatapack(repo.ui, packpath) as dpack:
431 with historypack.mutablehistorypack(repo.ui, packpath) as hpack:
431 with historypack.mutablehistorypack(repo.ui, packpath) as hpack:
432 try:
432 try:
433 packer.run(dpack, hpack)
433 packer.run(dpack, hpack)
434 except error.LockHeld:
434 except error.LockHeld:
435 raise RepackAlreadyRunning(
435 raise RepackAlreadyRunning(
436 _(
436 _(
437 b"skipping repack - another repack "
437 b"skipping repack - another repack "
438 b"is already running"
438 b"is already running"
439 )
439 )
440 )
440 )
441
441
442
442
443 def keepset(repo, keyfn, lastkeepkeys=None):
443 def keepset(repo, keyfn, lastkeepkeys=None):
444 """Computes a keepset which is not garbage collected.
444 """Computes a keepset which is not garbage collected.
445 'keyfn' is a function that maps filename, node to a unique key.
445 'keyfn' is a function that maps filename, node to a unique key.
446 'lastkeepkeys' is an optional argument and if provided the keepset
446 'lastkeepkeys' is an optional argument and if provided the keepset
447 function updates lastkeepkeys with more keys and returns the result.
447 function updates lastkeepkeys with more keys and returns the result.
448 """
448 """
449 if not lastkeepkeys:
449 if not lastkeepkeys:
450 keepkeys = set()
450 keepkeys = set()
451 else:
451 else:
452 keepkeys = lastkeepkeys
452 keepkeys = lastkeepkeys
453
453
454 # We want to keep:
454 # We want to keep:
455 # 1. Working copy parent
455 # 1. Working copy parent
456 # 2. Draft commits
456 # 2. Draft commits
457 # 3. Parents of draft commits
457 # 3. Parents of draft commits
458 # 4. Pullprefetch and bgprefetchrevs revsets if specified
458 # 4. Pullprefetch and bgprefetchrevs revsets if specified
459 revs = [b'.', b'draft()', b'parents(draft())']
459 revs = [b'.', b'draft()', b'parents(draft())']
460 prefetchrevs = repo.ui.config(b'remotefilelog', b'pullprefetch', None)
460 prefetchrevs = repo.ui.config(b'remotefilelog', b'pullprefetch', None)
461 if prefetchrevs:
461 if prefetchrevs:
462 revs.append(b'(%s)' % prefetchrevs)
462 revs.append(b'(%s)' % prefetchrevs)
463 prefetchrevs = repo.ui.config(b'remotefilelog', b'bgprefetchrevs', None)
463 prefetchrevs = repo.ui.config(b'remotefilelog', b'bgprefetchrevs', None)
464 if prefetchrevs:
464 if prefetchrevs:
465 revs.append(b'(%s)' % prefetchrevs)
465 revs.append(b'(%s)' % prefetchrevs)
466 revs = b'+'.join(revs)
466 revs = b'+'.join(revs)
467
467
468 revs = [b'sort((%s), "topo")' % revs]
468 revs = [b'sort((%s), "topo")' % revs]
469 keep = scmutil.revrange(repo, revs)
469 keep = scmutil.revrange(repo, revs)
470
470
471 processed = set()
471 processed = set()
472 lastmanifest = None
472 lastmanifest = None
473
473
474 # process the commits in toposorted order starting from the oldest
474 # process the commits in toposorted order starting from the oldest
475 for r in reversed(keep._list):
475 for r in reversed(keep._list):
476 if repo[r].p1().rev() in processed:
476 if repo[r].p1().rev() in processed:
477 # if the direct parent has already been processed
477 # if the direct parent has already been processed
478 # then we only need to process the delta
478 # then we only need to process the delta
479 m = repo[r].manifestctx().readdelta()
479 m = repo[r].manifestctx().readdelta()
480 else:
480 else:
481 # otherwise take the manifest and diff it
481 # otherwise take the manifest and diff it
482 # with the previous manifest if one exists
482 # with the previous manifest if one exists
483 if lastmanifest:
483 if lastmanifest:
484 m = repo[r].manifest().diff(lastmanifest)
484 m = repo[r].manifest().diff(lastmanifest)
485 else:
485 else:
486 m = repo[r].manifest()
486 m = repo[r].manifest()
487 lastmanifest = repo[r].manifest()
487 lastmanifest = repo[r].manifest()
488 processed.add(r)
488 processed.add(r)
489
489
490 # populate keepkeys with keys from the current manifest
490 # populate keepkeys with keys from the current manifest
491 if type(m) is dict:
491 if type(m) is dict:
492 # m is a result of diff of two manifests and is a dictionary that
492 # m is a result of diff of two manifests and is a dictionary that
493 # maps filename to ((newnode, newflag), (oldnode, oldflag)) tuple
493 # maps filename to ((newnode, newflag), (oldnode, oldflag)) tuple
494 for filename, diff in pycompat.iteritems(m):
494 for filename, diff in pycompat.iteritems(m):
495 if diff[0][0] is not None:
495 if diff[0][0] is not None:
496 keepkeys.add(keyfn(filename, diff[0][0]))
496 keepkeys.add(keyfn(filename, diff[0][0]))
497 else:
497 else:
498 # m is a manifest object
498 # m is a manifest object
499 for filename, filenode in pycompat.iteritems(m):
499 for filename, filenode in pycompat.iteritems(m):
500 keepkeys.add(keyfn(filename, filenode))
500 keepkeys.add(keyfn(filename, filenode))
501
501
502 return keepkeys
502 return keepkeys
503
503
504
504
505 class repacker(object):
505 class repacker(object):
506 """Class for orchestrating the repack of data and history information into a
506 """Class for orchestrating the repack of data and history information into a
507 new format.
507 new format.
508 """
508 """
509
509
510 def __init__(
510 def __init__(
511 self,
511 self,
512 repo,
512 repo,
513 data,
513 data,
514 history,
514 history,
515 fullhistory,
515 fullhistory,
516 category,
516 category,
517 gc=False,
517 gc=False,
518 isold=None,
518 isold=None,
519 options=None,
519 options=None,
520 ):
520 ):
521 self.repo = repo
521 self.repo = repo
522 self.data = data
522 self.data = data
523 self.history = history
523 self.history = history
524 self.fullhistory = fullhistory
524 self.fullhistory = fullhistory
525 self.unit = constants.getunits(category)
525 self.unit = constants.getunits(category)
526 self.garbagecollect = gc
526 self.garbagecollect = gc
527 self.options = options
527 self.options = options
528 if self.garbagecollect:
528 if self.garbagecollect:
529 if not isold:
529 if not isold:
530 raise ValueError(b"Function 'isold' is not properly specified")
530 raise ValueError(b"Function 'isold' is not properly specified")
531 # use (filename, node) tuple as a keepset key
531 # use (filename, node) tuple as a keepset key
532 self.keepkeys = keepset(repo, lambda f, n: (f, n))
532 self.keepkeys = keepset(repo, lambda f, n: (f, n))
533 self.isold = isold
533 self.isold = isold
534
534
535 def run(self, targetdata, targethistory):
535 def run(self, targetdata, targethistory):
536 ledger = repackledger()
536 ledger = repackledger()
537
537
538 with lockmod.lock(
538 with lockmod.lock(
539 repacklockvfs(self.repo), b"repacklock", desc=None, timeout=0
539 repacklockvfs(self.repo), b"repacklock", desc=None, timeout=0
540 ):
540 ):
541 self.repo.hook(b'prerepack')
541 self.repo.hook(b'prerepack')
542
542
543 # Populate ledger from source
543 # Populate ledger from source
544 self.data.markledger(ledger, options=self.options)
544 self.data.markledger(ledger, options=self.options)
545 self.history.markledger(ledger, options=self.options)
545 self.history.markledger(ledger, options=self.options)
546
546
547 # Run repack
547 # Run repack
548 self.repackdata(ledger, targetdata)
548 self.repackdata(ledger, targetdata)
549 self.repackhistory(ledger, targethistory)
549 self.repackhistory(ledger, targethistory)
550
550
551 # Call cleanup on each source
551 # Call cleanup on each source
552 for source in ledger.sources:
552 for source in ledger.sources:
553 source.cleanup(ledger)
553 source.cleanup(ledger)
554
554
555 def _chainorphans(self, ui, filename, nodes, orphans, deltabases):
555 def _chainorphans(self, ui, filename, nodes, orphans, deltabases):
556 """Reorderes ``orphans`` into a single chain inside ``nodes`` and
556 """Reorderes ``orphans`` into a single chain inside ``nodes`` and
557 ``deltabases``.
557 ``deltabases``.
558
558
559 We often have orphan entries (nodes without a base that aren't
559 We often have orphan entries (nodes without a base that aren't
560 referenced by other nodes -- i.e., part of a chain) due to gaps in
560 referenced by other nodes -- i.e., part of a chain) due to gaps in
561 history. Rather than store them as individual fulltexts, we prefer to
561 history. Rather than store them as individual fulltexts, we prefer to
562 insert them as one chain sorted by size.
562 insert them as one chain sorted by size.
563 """
563 """
564 if not orphans:
564 if not orphans:
565 return nodes
565 return nodes
566
566
567 def getsize(node, default=0):
567 def getsize(node, default=0):
568 meta = self.data.getmeta(filename, node)
568 meta = self.data.getmeta(filename, node)
569 if constants.METAKEYSIZE in meta:
569 if constants.METAKEYSIZE in meta:
570 return meta[constants.METAKEYSIZE]
570 return meta[constants.METAKEYSIZE]
571 else:
571 else:
572 return default
572 return default
573
573
574 # Sort orphans by size; biggest first is preferred, since it's more
574 # Sort orphans by size; biggest first is preferred, since it's more
575 # likely to be the newest version assuming files grow over time.
575 # likely to be the newest version assuming files grow over time.
576 # (Sort by node first to ensure the sort is stable.)
576 # (Sort by node first to ensure the sort is stable.)
577 orphans = sorted(orphans)
577 orphans = sorted(orphans)
578 orphans = list(sorted(orphans, key=getsize, reverse=True))
578 orphans = list(sorted(orphans, key=getsize, reverse=True))
579 if ui.debugflag:
579 if ui.debugflag:
580 ui.debug(
580 ui.debug(
581 b"%s: orphan chain: %s\n"
581 b"%s: orphan chain: %s\n"
582 % (filename, b", ".join([short(s) for s in orphans]))
582 % (filename, b", ".join([short(s) for s in orphans]))
583 )
583 )
584
584
585 # Create one contiguous chain and reassign deltabases.
585 # Create one contiguous chain and reassign deltabases.
586 for i, node in enumerate(orphans):
586 for i, node in enumerate(orphans):
587 if i == 0:
587 if i == 0:
588 deltabases[node] = (nullid, 0)
588 deltabases[node] = (nullid, 0)
589 else:
589 else:
590 parent = orphans[i - 1]
590 parent = orphans[i - 1]
591 deltabases[node] = (parent, deltabases[parent][1] + 1)
591 deltabases[node] = (parent, deltabases[parent][1] + 1)
592 nodes = [n for n in nodes if n not in orphans]
592 nodes = [n for n in nodes if n not in orphans]
593 nodes += orphans
593 nodes += orphans
594 return nodes
594 return nodes
595
595
596 def repackdata(self, ledger, target):
596 def repackdata(self, ledger, target):
597 ui = self.repo.ui
597 ui = self.repo.ui
598 maxchainlen = ui.configint(b'packs', b'maxchainlen', 1000)
598 maxchainlen = ui.configint(b'packs', b'maxchainlen', 1000)
599
599
600 byfile = {}
600 byfile = {}
601 for entry in pycompat.itervalues(ledger.entries):
601 for entry in pycompat.itervalues(ledger.entries):
602 if entry.datasource:
602 if entry.datasource:
603 byfile.setdefault(entry.filename, {})[entry.node] = entry
603 byfile.setdefault(entry.filename, {})[entry.node] = entry
604
604
605 count = 0
605 count = 0
606 repackprogress = ui.makeprogress(
606 repackprogress = ui.makeprogress(
607 _(b"repacking data"), unit=self.unit, total=len(byfile)
607 _(b"repacking data"), unit=self.unit, total=len(byfile)
608 )
608 )
609 for filename, entries in sorted(pycompat.iteritems(byfile)):
609 for filename, entries in sorted(pycompat.iteritems(byfile)):
610 repackprogress.update(count)
610 repackprogress.update(count)
611
611
612 ancestors = {}
612 ancestors = {}
613 nodes = list(node for node in entries)
613 nodes = list(node for node in entries)
614 nohistory = []
614 nohistory = []
615 buildprogress = ui.makeprogress(
615 buildprogress = ui.makeprogress(
616 _(b"building history"), unit=b'nodes', total=len(nodes)
616 _(b"building history"), unit=b'nodes', total=len(nodes)
617 )
617 )
618 for i, node in enumerate(nodes):
618 for i, node in enumerate(nodes):
619 if node in ancestors:
619 if node in ancestors:
620 continue
620 continue
621 buildprogress.update(i)
621 buildprogress.update(i)
622 try:
622 try:
623 ancestors.update(
623 ancestors.update(
624 self.fullhistory.getancestors(
624 self.fullhistory.getancestors(
625 filename, node, known=ancestors
625 filename, node, known=ancestors
626 )
626 )
627 )
627 )
628 except KeyError:
628 except KeyError:
629 # Since we're packing data entries, we may not have the
629 # Since we're packing data entries, we may not have the
630 # corresponding history entries for them. It's not a big
630 # corresponding history entries for them. It's not a big
631 # deal, but the entries won't be delta'd perfectly.
631 # deal, but the entries won't be delta'd perfectly.
632 nohistory.append(node)
632 nohistory.append(node)
633 buildprogress.complete()
633 buildprogress.complete()
634
634
635 # Order the nodes children first, so we can produce reverse deltas
635 # Order the nodes children first, so we can produce reverse deltas
636 orderednodes = list(reversed(self._toposort(ancestors)))
636 orderednodes = list(reversed(self._toposort(ancestors)))
637 if len(nohistory) > 0:
637 if len(nohistory) > 0:
638 ui.debug(
638 ui.debug(
639 b'repackdata: %d nodes without history\n' % len(nohistory)
639 b'repackdata: %d nodes without history\n' % len(nohistory)
640 )
640 )
641 orderednodes.extend(sorted(nohistory))
641 orderednodes.extend(sorted(nohistory))
642
642
643 # Filter orderednodes to just the nodes we want to serialize (it
643 # Filter orderednodes to just the nodes we want to serialize (it
644 # currently also has the edge nodes' ancestors).
644 # currently also has the edge nodes' ancestors).
645 orderednodes = list(
645 orderednodes = list(
646 filter(lambda node: node in nodes, orderednodes)
646 filter(lambda node: node in nodes, orderednodes)
647 )
647 )
648
648
649 # Garbage collect old nodes:
649 # Garbage collect old nodes:
650 if self.garbagecollect:
650 if self.garbagecollect:
651 neworderednodes = []
651 neworderednodes = []
652 for node in orderednodes:
652 for node in orderednodes:
653 # If the node is old and is not in the keepset, we skip it,
653 # If the node is old and is not in the keepset, we skip it,
654 # and mark as garbage collected
654 # and mark as garbage collected
655 if (filename, node) not in self.keepkeys and self.isold(
655 if (filename, node) not in self.keepkeys and self.isold(
656 self.repo, filename, node
656 self.repo, filename, node
657 ):
657 ):
658 entries[node].gced = True
658 entries[node].gced = True
659 continue
659 continue
660 neworderednodes.append(node)
660 neworderednodes.append(node)
661 orderednodes = neworderednodes
661 orderednodes = neworderednodes
662
662
663 # Compute delta bases for nodes:
663 # Compute delta bases for nodes:
664 deltabases = {}
664 deltabases = {}
665 nobase = set()
665 nobase = set()
666 referenced = set()
666 referenced = set()
667 nodes = set(nodes)
667 nodes = set(nodes)
668 processprogress = ui.makeprogress(
668 processprogress = ui.makeprogress(
669 _(b"processing nodes"), unit=b'nodes', total=len(orderednodes)
669 _(b"processing nodes"), unit=b'nodes', total=len(orderednodes)
670 )
670 )
671 for i, node in enumerate(orderednodes):
671 for i, node in enumerate(orderednodes):
672 processprogress.update(i)
672 processprogress.update(i)
673 # Find delta base
673 # Find delta base
674 # TODO: allow delta'ing against most recent descendant instead
674 # TODO: allow delta'ing against most recent descendant instead
675 # of immediate child
675 # of immediate child
676 deltatuple = deltabases.get(node, None)
676 deltatuple = deltabases.get(node, None)
677 if deltatuple is None:
677 if deltatuple is None:
678 deltabase, chainlen = nullid, 0
678 deltabase, chainlen = nullid, 0
679 deltabases[node] = (nullid, 0)
679 deltabases[node] = (nullid, 0)
680 nobase.add(node)
680 nobase.add(node)
681 else:
681 else:
682 deltabase, chainlen = deltatuple
682 deltabase, chainlen = deltatuple
683 referenced.add(deltabase)
683 referenced.add(deltabase)
684
684
685 # Use available ancestor information to inform our delta choices
685 # Use available ancestor information to inform our delta choices
686 ancestorinfo = ancestors.get(node)
686 ancestorinfo = ancestors.get(node)
687 if ancestorinfo:
687 if ancestorinfo:
688 p1, p2, linknode, copyfrom = ancestorinfo
688 p1, p2, linknode, copyfrom = ancestorinfo
689
689
690 # The presence of copyfrom means we're at a point where the
690 # The presence of copyfrom means we're at a point where the
691 # file was copied from elsewhere. So don't attempt to do any
691 # file was copied from elsewhere. So don't attempt to do any
692 # deltas with the other file.
692 # deltas with the other file.
693 if copyfrom:
693 if copyfrom:
694 p1 = nullid
694 p1 = nullid
695
695
696 if chainlen < maxchainlen:
696 if chainlen < maxchainlen:
697 # Record this child as the delta base for its parents.
697 # Record this child as the delta base for its parents.
698 # This may be non optimal, since the parents may have
698 # This may be non optimal, since the parents may have
699 # many children, and this will only choose the last one.
699 # many children, and this will only choose the last one.
700 # TODO: record all children and try all deltas to find
700 # TODO: record all children and try all deltas to find
701 # best
701 # best
702 if p1 != nullid:
702 if p1 != nullid:
703 deltabases[p1] = (node, chainlen + 1)
703 deltabases[p1] = (node, chainlen + 1)
704 if p2 != nullid:
704 if p2 != nullid:
705 deltabases[p2] = (node, chainlen + 1)
705 deltabases[p2] = (node, chainlen + 1)
706
706
707 # experimental config: repack.chainorphansbysize
707 # experimental config: repack.chainorphansbysize
708 if ui.configbool(b'repack', b'chainorphansbysize'):
708 if ui.configbool(b'repack', b'chainorphansbysize'):
709 orphans = nobase - referenced
709 orphans = nobase - referenced
710 orderednodes = self._chainorphans(
710 orderednodes = self._chainorphans(
711 ui, filename, orderednodes, orphans, deltabases
711 ui, filename, orderednodes, orphans, deltabases
712 )
712 )
713
713
714 # Compute deltas and write to the pack
714 # Compute deltas and write to the pack
715 for i, node in enumerate(orderednodes):
715 for i, node in enumerate(orderednodes):
716 deltabase, chainlen = deltabases[node]
716 deltabase, chainlen = deltabases[node]
717 # Compute delta
717 # Compute delta
718 # TODO: Optimize the deltachain fetching. Since we're
718 # TODO: Optimize the deltachain fetching. Since we're
719 # iterating over the different version of the file, we may
719 # iterating over the different version of the file, we may
720 # be fetching the same deltachain over and over again.
720 # be fetching the same deltachain over and over again.
721 if deltabase != nullid:
721 if deltabase != nullid:
722 deltaentry = self.data.getdelta(filename, node)
722 deltaentry = self.data.getdelta(filename, node)
723 delta, deltabasename, origdeltabase, meta = deltaentry
723 delta, deltabasename, origdeltabase, meta = deltaentry
724 size = meta.get(constants.METAKEYSIZE)
724 size = meta.get(constants.METAKEYSIZE)
725 if (
725 if (
726 deltabasename != filename
726 deltabasename != filename
727 or origdeltabase != deltabase
727 or origdeltabase != deltabase
728 or size is None
728 or size is None
729 ):
729 ):
730 deltabasetext = self.data.get(filename, deltabase)
730 deltabasetext = self.data.get(filename, deltabase)
731 original = self.data.get(filename, node)
731 original = self.data.get(filename, node)
732 size = len(original)
732 size = len(original)
733 delta = mdiff.textdiff(deltabasetext, original)
733 delta = mdiff.textdiff(deltabasetext, original)
734 else:
734 else:
735 delta = self.data.get(filename, node)
735 delta = self.data.get(filename, node)
736 size = len(delta)
736 size = len(delta)
737 meta = self.data.getmeta(filename, node)
737 meta = self.data.getmeta(filename, node)
738
738
739 # TODO: don't use the delta if it's larger than the fulltext
739 # TODO: don't use the delta if it's larger than the fulltext
740 if constants.METAKEYSIZE not in meta:
740 if constants.METAKEYSIZE not in meta:
741 meta[constants.METAKEYSIZE] = size
741 meta[constants.METAKEYSIZE] = size
742 target.add(filename, node, deltabase, delta, meta)
742 target.add(filename, node, deltabase, delta, meta)
743
743
744 entries[node].datarepacked = True
744 entries[node].datarepacked = True
745
745
746 processprogress.complete()
746 processprogress.complete()
747 count += 1
747 count += 1
748
748
749 repackprogress.complete()
749 repackprogress.complete()
750 target.close(ledger=ledger)
750 target.close(ledger=ledger)
751
751
752 def repackhistory(self, ledger, target):
752 def repackhistory(self, ledger, target):
753 ui = self.repo.ui
753 ui = self.repo.ui
754
754
755 byfile = {}
755 byfile = {}
756 for entry in pycompat.itervalues(ledger.entries):
756 for entry in pycompat.itervalues(ledger.entries):
757 if entry.historysource:
757 if entry.historysource:
758 byfile.setdefault(entry.filename, {})[entry.node] = entry
758 byfile.setdefault(entry.filename, {})[entry.node] = entry
759
759
760 progress = ui.makeprogress(
760 progress = ui.makeprogress(
761 _(b"repacking history"), unit=self.unit, total=len(byfile)
761 _(b"repacking history"), unit=self.unit, total=len(byfile)
762 )
762 )
763 for filename, entries in sorted(pycompat.iteritems(byfile)):
763 for filename, entries in sorted(pycompat.iteritems(byfile)):
764 ancestors = {}
764 ancestors = {}
765 nodes = list(node for node in entries)
765 nodes = list(node for node in entries)
766
766
767 for node in nodes:
767 for node in nodes:
768 if node in ancestors:
768 if node in ancestors:
769 continue
769 continue
770 ancestors.update(
770 ancestors.update(
771 self.history.getancestors(filename, node, known=ancestors)
771 self.history.getancestors(filename, node, known=ancestors)
772 )
772 )
773
773
774 # Order the nodes children first
774 # Order the nodes children first
775 orderednodes = reversed(self._toposort(ancestors))
775 orderednodes = reversed(self._toposort(ancestors))
776
776
777 # Write to the pack
777 # Write to the pack
778 dontprocess = set()
778 dontprocess = set()
779 for node in orderednodes:
779 for node in orderednodes:
780 p1, p2, linknode, copyfrom = ancestors[node]
780 p1, p2, linknode, copyfrom = ancestors[node]
781
781
782 # If the node is marked dontprocess, but it's also in the
782 # If the node is marked dontprocess, but it's also in the
783 # explicit entries set, that means the node exists both in this
783 # explicit entries set, that means the node exists both in this
784 # file and in another file that was copied to this file.
784 # file and in another file that was copied to this file.
785 # Usually this happens if the file was copied to another file,
785 # Usually this happens if the file was copied to another file,
786 # then the copy was deleted, then reintroduced without copy
786 # then the copy was deleted, then reintroduced without copy
787 # metadata. The original add and the new add have the same hash
787 # metadata. The original add and the new add have the same hash
788 # since the content is identical and the parents are null.
788 # since the content is identical and the parents are null.
789 if node in dontprocess and node not in entries:
789 if node in dontprocess and node not in entries:
790 # If copyfrom == filename, it means the copy history
790 # If copyfrom == filename, it means the copy history
791 # went to come other file, then came back to this one, so we
791 # went to come other file, then came back to this one, so we
792 # should continue processing it.
792 # should continue processing it.
793 if p1 != nullid and copyfrom != filename:
793 if p1 != nullid and copyfrom != filename:
794 dontprocess.add(p1)
794 dontprocess.add(p1)
795 if p2 != nullid:
795 if p2 != nullid:
796 dontprocess.add(p2)
796 dontprocess.add(p2)
797 continue
797 continue
798
798
799 if copyfrom:
799 if copyfrom:
800 dontprocess.add(p1)
800 dontprocess.add(p1)
801
801
802 target.add(filename, node, p1, p2, linknode, copyfrom)
802 target.add(filename, node, p1, p2, linknode, copyfrom)
803
803
804 if node in entries:
804 if node in entries:
805 entries[node].historyrepacked = True
805 entries[node].historyrepacked = True
806
806
807 progress.increment()
807 progress.increment()
808
808
809 progress.complete()
809 progress.complete()
810 target.close(ledger=ledger)
810 target.close(ledger=ledger)
811
811
812 def _toposort(self, ancestors):
812 def _toposort(self, ancestors):
813 def parentfunc(node):
813 def parentfunc(node):
814 p1, p2, linknode, copyfrom = ancestors[node]
814 p1, p2, linknode, copyfrom = ancestors[node]
815 parents = []
815 parents = []
816 if p1 != nullid:
816 if p1 != nullid:
817 parents.append(p1)
817 parents.append(p1)
818 if p2 != nullid:
818 if p2 != nullid:
819 parents.append(p2)
819 parents.append(p2)
820 return parents
820 return parents
821
821
822 sortednodes = shallowutil.sortnodes(ancestors.keys(), parentfunc)
822 sortednodes = shallowutil.sortnodes(ancestors.keys(), parentfunc)
823 return sortednodes
823 return sortednodes
824
824
825
825
826 class repackledger(object):
826 class repackledger(object):
827 """Storage for all the bookkeeping that happens during a repack. It contains
827 """Storage for all the bookkeeping that happens during a repack. It contains
828 the list of revisions being repacked, what happened to each revision, and
828 the list of revisions being repacked, what happened to each revision, and
829 which source store contained which revision originally (for later cleanup).
829 which source store contained which revision originally (for later cleanup).
830 """
830 """
831
831
832 def __init__(self):
832 def __init__(self):
833 self.entries = {}
833 self.entries = {}
834 self.sources = {}
834 self.sources = {}
835 self.created = set()
835 self.created = set()
836
836
837 def markdataentry(self, source, filename, node):
837 def markdataentry(self, source, filename, node):
838 """Mark the given filename+node revision as having a data rev in the
838 """Mark the given filename+node revision as having a data rev in the
839 given source.
839 given source.
840 """
840 """
841 entry = self._getorcreateentry(filename, node)
841 entry = self._getorcreateentry(filename, node)
842 entry.datasource = True
842 entry.datasource = True
843 entries = self.sources.get(source)
843 entries = self.sources.get(source)
844 if not entries:
844 if not entries:
845 entries = set()
845 entries = set()
846 self.sources[source] = entries
846 self.sources[source] = entries
847 entries.add(entry)
847 entries.add(entry)
848
848
849 def markhistoryentry(self, source, filename, node):
849 def markhistoryentry(self, source, filename, node):
850 """Mark the given filename+node revision as having a history rev in the
850 """Mark the given filename+node revision as having a history rev in the
851 given source.
851 given source.
852 """
852 """
853 entry = self._getorcreateentry(filename, node)
853 entry = self._getorcreateentry(filename, node)
854 entry.historysource = True
854 entry.historysource = True
855 entries = self.sources.get(source)
855 entries = self.sources.get(source)
856 if not entries:
856 if not entries:
857 entries = set()
857 entries = set()
858 self.sources[source] = entries
858 self.sources[source] = entries
859 entries.add(entry)
859 entries.add(entry)
860
860
861 def _getorcreateentry(self, filename, node):
861 def _getorcreateentry(self, filename, node):
862 key = (filename, node)
862 key = (filename, node)
863 value = self.entries.get(key)
863 value = self.entries.get(key)
864 if not value:
864 if not value:
865 value = repackentry(filename, node)
865 value = repackentry(filename, node)
866 self.entries[key] = value
866 self.entries[key] = value
867
867
868 return value
868 return value
869
869
870 def addcreated(self, value):
870 def addcreated(self, value):
871 self.created.add(value)
871 self.created.add(value)
872
872
873
873
874 class repackentry(object):
874 class repackentry(object):
875 """Simple class representing a single revision entry in the repackledger.
875 """Simple class representing a single revision entry in the repackledger.
876 """
876 """
877
877
878 __slots__ = (
878 __slots__ = (
879 r'filename',
879 r'filename',
880 r'node',
880 r'node',
881 r'datasource',
881 r'datasource',
882 r'historysource',
882 r'historysource',
883 r'datarepacked',
883 r'datarepacked',
884 r'historyrepacked',
884 r'historyrepacked',
885 r'gced',
885 r'gced',
886 )
886 )
887
887
888 def __init__(self, filename, node):
888 def __init__(self, filename, node):
889 self.filename = filename
889 self.filename = filename
890 self.node = node
890 self.node = node
891 # If the revision has a data entry in the source
891 # If the revision has a data entry in the source
892 self.datasource = False
892 self.datasource = False
893 # If the revision has a history entry in the source
893 # If the revision has a history entry in the source
894 self.historysource = False
894 self.historysource = False
895 # If the revision's data entry was repacked into the repack target
895 # If the revision's data entry was repacked into the repack target
896 self.datarepacked = False
896 self.datarepacked = False
897 # If the revision's history entry was repacked into the repack target
897 # If the revision's history entry was repacked into the repack target
898 self.historyrepacked = False
898 self.historyrepacked = False
899 # If garbage collected
899 # If garbage collected
900 self.gced = False
900 self.gced = False
901
901
902
902
903 def repacklockvfs(repo):
903 def repacklockvfs(repo):
904 if util.safehasattr(repo, b'name'):
904 if util.safehasattr(repo, 'name'):
905 # Lock in the shared cache so repacks across multiple copies of the same
905 # Lock in the shared cache so repacks across multiple copies of the same
906 # repo are coordinated.
906 # repo are coordinated.
907 sharedcachepath = shallowutil.getcachepackpath(
907 sharedcachepath = shallowutil.getcachepackpath(
908 repo, constants.FILEPACK_CATEGORY
908 repo, constants.FILEPACK_CATEGORY
909 )
909 )
910 return vfs.vfs(sharedcachepath)
910 return vfs.vfs(sharedcachepath)
911 else:
911 else:
912 return repo.svfs
912 return repo.svfs
@@ -1,354 +1,354 b''
1 # shallowrepo.py - shallow repository that uses remote filelogs
1 # shallowrepo.py - shallow repository that uses remote filelogs
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import hex, nullid, nullrev
12 from mercurial.node import hex, nullid, nullrev
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 encoding,
15 error,
15 error,
16 localrepo,
16 localrepo,
17 match,
17 match,
18 pycompat,
18 pycompat,
19 scmutil,
19 scmutil,
20 sparse,
20 sparse,
21 util,
21 util,
22 )
22 )
23 from mercurial.utils import procutil
23 from mercurial.utils import procutil
24 from . import (
24 from . import (
25 connectionpool,
25 connectionpool,
26 constants,
26 constants,
27 contentstore,
27 contentstore,
28 datapack,
28 datapack,
29 fileserverclient,
29 fileserverclient,
30 historypack,
30 historypack,
31 metadatastore,
31 metadatastore,
32 remotefilectx,
32 remotefilectx,
33 remotefilelog,
33 remotefilelog,
34 shallowutil,
34 shallowutil,
35 )
35 )
36
36
37 # These make*stores functions are global so that other extensions can replace
37 # These make*stores functions are global so that other extensions can replace
38 # them.
38 # them.
39 def makelocalstores(repo):
39 def makelocalstores(repo):
40 """In-repo stores, like .hg/store/data; can not be discarded."""
40 """In-repo stores, like .hg/store/data; can not be discarded."""
41 localpath = os.path.join(repo.svfs.vfs.base, b'data')
41 localpath = os.path.join(repo.svfs.vfs.base, b'data')
42 if not os.path.exists(localpath):
42 if not os.path.exists(localpath):
43 os.makedirs(localpath)
43 os.makedirs(localpath)
44
44
45 # Instantiate local data stores
45 # Instantiate local data stores
46 localcontent = contentstore.remotefilelogcontentstore(
46 localcontent = contentstore.remotefilelogcontentstore(
47 repo, localpath, repo.name, shared=False
47 repo, localpath, repo.name, shared=False
48 )
48 )
49 localmetadata = metadatastore.remotefilelogmetadatastore(
49 localmetadata = metadatastore.remotefilelogmetadatastore(
50 repo, localpath, repo.name, shared=False
50 repo, localpath, repo.name, shared=False
51 )
51 )
52 return localcontent, localmetadata
52 return localcontent, localmetadata
53
53
54
54
55 def makecachestores(repo):
55 def makecachestores(repo):
56 """Typically machine-wide, cache of remote data; can be discarded."""
56 """Typically machine-wide, cache of remote data; can be discarded."""
57 # Instantiate shared cache stores
57 # Instantiate shared cache stores
58 cachepath = shallowutil.getcachepath(repo.ui)
58 cachepath = shallowutil.getcachepath(repo.ui)
59 cachecontent = contentstore.remotefilelogcontentstore(
59 cachecontent = contentstore.remotefilelogcontentstore(
60 repo, cachepath, repo.name, shared=True
60 repo, cachepath, repo.name, shared=True
61 )
61 )
62 cachemetadata = metadatastore.remotefilelogmetadatastore(
62 cachemetadata = metadatastore.remotefilelogmetadatastore(
63 repo, cachepath, repo.name, shared=True
63 repo, cachepath, repo.name, shared=True
64 )
64 )
65
65
66 repo.sharedstore = cachecontent
66 repo.sharedstore = cachecontent
67 repo.shareddatastores.append(cachecontent)
67 repo.shareddatastores.append(cachecontent)
68 repo.sharedhistorystores.append(cachemetadata)
68 repo.sharedhistorystores.append(cachemetadata)
69
69
70 return cachecontent, cachemetadata
70 return cachecontent, cachemetadata
71
71
72
72
73 def makeremotestores(repo, cachecontent, cachemetadata):
73 def makeremotestores(repo, cachecontent, cachemetadata):
74 """These stores fetch data from a remote server."""
74 """These stores fetch data from a remote server."""
75 # Instantiate remote stores
75 # Instantiate remote stores
76 repo.fileservice = fileserverclient.fileserverclient(repo)
76 repo.fileservice = fileserverclient.fileserverclient(repo)
77 remotecontent = contentstore.remotecontentstore(
77 remotecontent = contentstore.remotecontentstore(
78 repo.ui, repo.fileservice, cachecontent
78 repo.ui, repo.fileservice, cachecontent
79 )
79 )
80 remotemetadata = metadatastore.remotemetadatastore(
80 remotemetadata = metadatastore.remotemetadatastore(
81 repo.ui, repo.fileservice, cachemetadata
81 repo.ui, repo.fileservice, cachemetadata
82 )
82 )
83 return remotecontent, remotemetadata
83 return remotecontent, remotemetadata
84
84
85
85
86 def makepackstores(repo):
86 def makepackstores(repo):
87 """Packs are more efficient (to read from) cache stores."""
87 """Packs are more efficient (to read from) cache stores."""
88 # Instantiate pack stores
88 # Instantiate pack stores
89 packpath = shallowutil.getcachepackpath(repo, constants.FILEPACK_CATEGORY)
89 packpath = shallowutil.getcachepackpath(repo, constants.FILEPACK_CATEGORY)
90 packcontentstore = datapack.datapackstore(repo.ui, packpath)
90 packcontentstore = datapack.datapackstore(repo.ui, packpath)
91 packmetadatastore = historypack.historypackstore(repo.ui, packpath)
91 packmetadatastore = historypack.historypackstore(repo.ui, packpath)
92
92
93 repo.shareddatastores.append(packcontentstore)
93 repo.shareddatastores.append(packcontentstore)
94 repo.sharedhistorystores.append(packmetadatastore)
94 repo.sharedhistorystores.append(packmetadatastore)
95 shallowutil.reportpackmetrics(
95 shallowutil.reportpackmetrics(
96 repo.ui, b'filestore', packcontentstore, packmetadatastore
96 repo.ui, b'filestore', packcontentstore, packmetadatastore
97 )
97 )
98 return packcontentstore, packmetadatastore
98 return packcontentstore, packmetadatastore
99
99
100
100
101 def makeunionstores(repo):
101 def makeunionstores(repo):
102 """Union stores iterate the other stores and return the first result."""
102 """Union stores iterate the other stores and return the first result."""
103 repo.shareddatastores = []
103 repo.shareddatastores = []
104 repo.sharedhistorystores = []
104 repo.sharedhistorystores = []
105
105
106 packcontentstore, packmetadatastore = makepackstores(repo)
106 packcontentstore, packmetadatastore = makepackstores(repo)
107 cachecontent, cachemetadata = makecachestores(repo)
107 cachecontent, cachemetadata = makecachestores(repo)
108 localcontent, localmetadata = makelocalstores(repo)
108 localcontent, localmetadata = makelocalstores(repo)
109 remotecontent, remotemetadata = makeremotestores(
109 remotecontent, remotemetadata = makeremotestores(
110 repo, cachecontent, cachemetadata
110 repo, cachecontent, cachemetadata
111 )
111 )
112
112
113 # Instantiate union stores
113 # Instantiate union stores
114 repo.contentstore = contentstore.unioncontentstore(
114 repo.contentstore = contentstore.unioncontentstore(
115 packcontentstore,
115 packcontentstore,
116 cachecontent,
116 cachecontent,
117 localcontent,
117 localcontent,
118 remotecontent,
118 remotecontent,
119 writestore=localcontent,
119 writestore=localcontent,
120 )
120 )
121 repo.metadatastore = metadatastore.unionmetadatastore(
121 repo.metadatastore = metadatastore.unionmetadatastore(
122 packmetadatastore,
122 packmetadatastore,
123 cachemetadata,
123 cachemetadata,
124 localmetadata,
124 localmetadata,
125 remotemetadata,
125 remotemetadata,
126 writestore=localmetadata,
126 writestore=localmetadata,
127 )
127 )
128
128
129 fileservicedatawrite = cachecontent
129 fileservicedatawrite = cachecontent
130 fileservicehistorywrite = cachemetadata
130 fileservicehistorywrite = cachemetadata
131 repo.fileservice.setstore(
131 repo.fileservice.setstore(
132 repo.contentstore,
132 repo.contentstore,
133 repo.metadatastore,
133 repo.metadatastore,
134 fileservicedatawrite,
134 fileservicedatawrite,
135 fileservicehistorywrite,
135 fileservicehistorywrite,
136 )
136 )
137 shallowutil.reportpackmetrics(
137 shallowutil.reportpackmetrics(
138 repo.ui, b'filestore', packcontentstore, packmetadatastore
138 repo.ui, b'filestore', packcontentstore, packmetadatastore
139 )
139 )
140
140
141
141
142 def wraprepo(repo):
142 def wraprepo(repo):
143 class shallowrepository(repo.__class__):
143 class shallowrepository(repo.__class__):
144 @util.propertycache
144 @util.propertycache
145 def name(self):
145 def name(self):
146 return self.ui.config(b'remotefilelog', b'reponame')
146 return self.ui.config(b'remotefilelog', b'reponame')
147
147
148 @util.propertycache
148 @util.propertycache
149 def fallbackpath(self):
149 def fallbackpath(self):
150 path = repo.ui.config(
150 path = repo.ui.config(
151 b"remotefilelog",
151 b"remotefilelog",
152 b"fallbackpath",
152 b"fallbackpath",
153 repo.ui.config(b'paths', b'default'),
153 repo.ui.config(b'paths', b'default'),
154 )
154 )
155 if not path:
155 if not path:
156 raise error.Abort(
156 raise error.Abort(
157 b"no remotefilelog server "
157 b"no remotefilelog server "
158 b"configured - is your .hg/hgrc trusted?"
158 b"configured - is your .hg/hgrc trusted?"
159 )
159 )
160
160
161 return path
161 return path
162
162
163 def maybesparsematch(self, *revs, **kwargs):
163 def maybesparsematch(self, *revs, **kwargs):
164 '''
164 '''
165 A wrapper that allows the remotefilelog to invoke sparsematch() if
165 A wrapper that allows the remotefilelog to invoke sparsematch() if
166 this is a sparse repository, or returns None if this is not a
166 this is a sparse repository, or returns None if this is not a
167 sparse repository.
167 sparse repository.
168 '''
168 '''
169 if revs:
169 if revs:
170 ret = sparse.matcher(repo, revs=revs)
170 ret = sparse.matcher(repo, revs=revs)
171 else:
171 else:
172 ret = sparse.matcher(repo)
172 ret = sparse.matcher(repo)
173
173
174 if ret.always():
174 if ret.always():
175 return None
175 return None
176 return ret
176 return ret
177
177
178 def file(self, f):
178 def file(self, f):
179 if f[0] == b'/':
179 if f[0] == b'/':
180 f = f[1:]
180 f = f[1:]
181
181
182 if self.shallowmatch(f):
182 if self.shallowmatch(f):
183 return remotefilelog.remotefilelog(self.svfs, f, self)
183 return remotefilelog.remotefilelog(self.svfs, f, self)
184 else:
184 else:
185 return super(shallowrepository, self).file(f)
185 return super(shallowrepository, self).file(f)
186
186
187 def filectx(self, path, *args, **kwargs):
187 def filectx(self, path, *args, **kwargs):
188 if self.shallowmatch(path):
188 if self.shallowmatch(path):
189 return remotefilectx.remotefilectx(self, path, *args, **kwargs)
189 return remotefilectx.remotefilectx(self, path, *args, **kwargs)
190 else:
190 else:
191 return super(shallowrepository, self).filectx(
191 return super(shallowrepository, self).filectx(
192 path, *args, **kwargs
192 path, *args, **kwargs
193 )
193 )
194
194
195 @localrepo.unfilteredmethod
195 @localrepo.unfilteredmethod
196 def commitctx(self, ctx, error=False, origctx=None):
196 def commitctx(self, ctx, error=False, origctx=None):
197 """Add a new revision to current repository.
197 """Add a new revision to current repository.
198 Revision information is passed via the context argument.
198 Revision information is passed via the context argument.
199 """
199 """
200
200
201 # some contexts already have manifest nodes, they don't need any
201 # some contexts already have manifest nodes, they don't need any
202 # prefetching (for example if we're just editing a commit message
202 # prefetching (for example if we're just editing a commit message
203 # we can reuse manifest
203 # we can reuse manifest
204 if not ctx.manifestnode():
204 if not ctx.manifestnode():
205 # prefetch files that will likely be compared
205 # prefetch files that will likely be compared
206 m1 = ctx.p1().manifest()
206 m1 = ctx.p1().manifest()
207 files = []
207 files = []
208 for f in ctx.modified() + ctx.added():
208 for f in ctx.modified() + ctx.added():
209 fparent1 = m1.get(f, nullid)
209 fparent1 = m1.get(f, nullid)
210 if fparent1 != nullid:
210 if fparent1 != nullid:
211 files.append((f, hex(fparent1)))
211 files.append((f, hex(fparent1)))
212 self.fileservice.prefetch(files)
212 self.fileservice.prefetch(files)
213 return super(shallowrepository, self).commitctx(
213 return super(shallowrepository, self).commitctx(
214 ctx, error=error, origctx=origctx
214 ctx, error=error, origctx=origctx
215 )
215 )
216
216
217 def backgroundprefetch(
217 def backgroundprefetch(
218 self,
218 self,
219 revs,
219 revs,
220 base=None,
220 base=None,
221 repack=False,
221 repack=False,
222 pats=None,
222 pats=None,
223 opts=None,
223 opts=None,
224 ensurestart=False,
224 ensurestart=False,
225 ):
225 ):
226 """Runs prefetch in background with optional repack
226 """Runs prefetch in background with optional repack
227 """
227 """
228 cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'prefetch']
228 cmd = [procutil.hgexecutable(), b'-R', repo.origroot, b'prefetch']
229 if repack:
229 if repack:
230 cmd.append(b'--repack')
230 cmd.append(b'--repack')
231 if revs:
231 if revs:
232 cmd += [b'-r', revs]
232 cmd += [b'-r', revs]
233 # We know this command will find a binary, so don't block
233 # We know this command will find a binary, so don't block
234 # on it starting.
234 # on it starting.
235 procutil.runbgcommand(
235 procutil.runbgcommand(
236 cmd, encoding.environ, ensurestart=ensurestart
236 cmd, encoding.environ, ensurestart=ensurestart
237 )
237 )
238
238
239 def prefetch(self, revs, base=None, pats=None, opts=None):
239 def prefetch(self, revs, base=None, pats=None, opts=None):
240 """Prefetches all the necessary file revisions for the given revs
240 """Prefetches all the necessary file revisions for the given revs
241 Optionally runs repack in background
241 Optionally runs repack in background
242 """
242 """
243 with repo._lock(
243 with repo._lock(
244 repo.svfs,
244 repo.svfs,
245 b'prefetchlock',
245 b'prefetchlock',
246 True,
246 True,
247 None,
247 None,
248 None,
248 None,
249 _(b'prefetching in %s') % repo.origroot,
249 _(b'prefetching in %s') % repo.origroot,
250 ):
250 ):
251 self._prefetch(revs, base, pats, opts)
251 self._prefetch(revs, base, pats, opts)
252
252
253 def _prefetch(self, revs, base=None, pats=None, opts=None):
253 def _prefetch(self, revs, base=None, pats=None, opts=None):
254 fallbackpath = self.fallbackpath
254 fallbackpath = self.fallbackpath
255 if fallbackpath:
255 if fallbackpath:
256 # If we know a rev is on the server, we should fetch the server
256 # If we know a rev is on the server, we should fetch the server
257 # version of those files, since our local file versions might
257 # version of those files, since our local file versions might
258 # become obsolete if the local commits are stripped.
258 # become obsolete if the local commits are stripped.
259 localrevs = repo.revs(b'outgoing(%s)', fallbackpath)
259 localrevs = repo.revs(b'outgoing(%s)', fallbackpath)
260 if base is not None and base != nullrev:
260 if base is not None and base != nullrev:
261 serverbase = list(
261 serverbase = list(
262 repo.revs(
262 repo.revs(
263 b'first(reverse(::%s) - %ld)', base, localrevs
263 b'first(reverse(::%s) - %ld)', base, localrevs
264 )
264 )
265 )
265 )
266 if serverbase:
266 if serverbase:
267 base = serverbase[0]
267 base = serverbase[0]
268 else:
268 else:
269 localrevs = repo
269 localrevs = repo
270
270
271 mfl = repo.manifestlog
271 mfl = repo.manifestlog
272 mfrevlog = mfl.getstorage(b'')
272 mfrevlog = mfl.getstorage(b'')
273 if base is not None:
273 if base is not None:
274 mfdict = mfl[repo[base].manifestnode()].read()
274 mfdict = mfl[repo[base].manifestnode()].read()
275 skip = set(pycompat.iteritems(mfdict))
275 skip = set(pycompat.iteritems(mfdict))
276 else:
276 else:
277 skip = set()
277 skip = set()
278
278
279 # Copy the skip set to start large and avoid constant resizing,
279 # Copy the skip set to start large and avoid constant resizing,
280 # and since it's likely to be very similar to the prefetch set.
280 # and since it's likely to be very similar to the prefetch set.
281 files = skip.copy()
281 files = skip.copy()
282 serverfiles = skip.copy()
282 serverfiles = skip.copy()
283 visited = set()
283 visited = set()
284 visited.add(nullrev)
284 visited.add(nullrev)
285 revcount = len(revs)
285 revcount = len(revs)
286 progress = self.ui.makeprogress(_(b'prefetching'), total=revcount)
286 progress = self.ui.makeprogress(_(b'prefetching'), total=revcount)
287 progress.update(0)
287 progress.update(0)
288 for rev in sorted(revs):
288 for rev in sorted(revs):
289 ctx = repo[rev]
289 ctx = repo[rev]
290 if pats:
290 if pats:
291 m = scmutil.match(ctx, pats, opts)
291 m = scmutil.match(ctx, pats, opts)
292 sparsematch = repo.maybesparsematch(rev)
292 sparsematch = repo.maybesparsematch(rev)
293
293
294 mfnode = ctx.manifestnode()
294 mfnode = ctx.manifestnode()
295 mfrev = mfrevlog.rev(mfnode)
295 mfrev = mfrevlog.rev(mfnode)
296
296
297 # Decompressing manifests is expensive.
297 # Decompressing manifests is expensive.
298 # When possible, only read the deltas.
298 # When possible, only read the deltas.
299 p1, p2 = mfrevlog.parentrevs(mfrev)
299 p1, p2 = mfrevlog.parentrevs(mfrev)
300 if p1 in visited and p2 in visited:
300 if p1 in visited and p2 in visited:
301 mfdict = mfl[mfnode].readfast()
301 mfdict = mfl[mfnode].readfast()
302 else:
302 else:
303 mfdict = mfl[mfnode].read()
303 mfdict = mfl[mfnode].read()
304
304
305 diff = pycompat.iteritems(mfdict)
305 diff = pycompat.iteritems(mfdict)
306 if pats:
306 if pats:
307 diff = (pf for pf in diff if m(pf[0]))
307 diff = (pf for pf in diff if m(pf[0]))
308 if sparsematch:
308 if sparsematch:
309 diff = (pf for pf in diff if sparsematch(pf[0]))
309 diff = (pf for pf in diff if sparsematch(pf[0]))
310 if rev not in localrevs:
310 if rev not in localrevs:
311 serverfiles.update(diff)
311 serverfiles.update(diff)
312 else:
312 else:
313 files.update(diff)
313 files.update(diff)
314
314
315 visited.add(mfrev)
315 visited.add(mfrev)
316 progress.increment()
316 progress.increment()
317
317
318 files.difference_update(skip)
318 files.difference_update(skip)
319 serverfiles.difference_update(skip)
319 serverfiles.difference_update(skip)
320 progress.complete()
320 progress.complete()
321
321
322 # Fetch files known to be on the server
322 # Fetch files known to be on the server
323 if serverfiles:
323 if serverfiles:
324 results = [(path, hex(fnode)) for (path, fnode) in serverfiles]
324 results = [(path, hex(fnode)) for (path, fnode) in serverfiles]
325 repo.fileservice.prefetch(results, force=True)
325 repo.fileservice.prefetch(results, force=True)
326
326
327 # Fetch files that may or may not be on the server
327 # Fetch files that may or may not be on the server
328 if files:
328 if files:
329 results = [(path, hex(fnode)) for (path, fnode) in files]
329 results = [(path, hex(fnode)) for (path, fnode) in files]
330 repo.fileservice.prefetch(results)
330 repo.fileservice.prefetch(results)
331
331
332 def close(self):
332 def close(self):
333 super(shallowrepository, self).close()
333 super(shallowrepository, self).close()
334 self.connectionpool.close()
334 self.connectionpool.close()
335
335
336 repo.__class__ = shallowrepository
336 repo.__class__ = shallowrepository
337
337
338 repo.shallowmatch = match.always()
338 repo.shallowmatch = match.always()
339
339
340 makeunionstores(repo)
340 makeunionstores(repo)
341
341
342 repo.includepattern = repo.ui.configlist(
342 repo.includepattern = repo.ui.configlist(
343 b"remotefilelog", b"includepattern", None
343 b"remotefilelog", b"includepattern", None
344 )
344 )
345 repo.excludepattern = repo.ui.configlist(
345 repo.excludepattern = repo.ui.configlist(
346 b"remotefilelog", b"excludepattern", None
346 b"remotefilelog", b"excludepattern", None
347 )
347 )
348 if not util.safehasattr(repo, b'connectionpool'):
348 if not util.safehasattr(repo, 'connectionpool'):
349 repo.connectionpool = connectionpool.connectionpool(repo)
349 repo.connectionpool = connectionpool.connectionpool(repo)
350
350
351 if repo.includepattern or repo.excludepattern:
351 if repo.includepattern or repo.excludepattern:
352 repo.shallowmatch = match.match(
352 repo.shallowmatch = match.match(
353 repo.root, b'', None, repo.includepattern, repo.excludepattern
353 repo.root, b'', None, repo.includepattern, repo.excludepattern
354 )
354 )
@@ -1,2555 +1,2555 b''
1 # bundle2.py - generic container format to transmit arbitrary data.
1 # bundle2.py - generic container format to transmit arbitrary data.
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """Handling of the new bundle2 format
7 """Handling of the new bundle2 format
8
8
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 payloads in an application agnostic way. It consist in a sequence of "parts"
10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 that will be handed to and processed by the application layer.
11 that will be handed to and processed by the application layer.
12
12
13
13
14 General format architecture
14 General format architecture
15 ===========================
15 ===========================
16
16
17 The format is architectured as follow
17 The format is architectured as follow
18
18
19 - magic string
19 - magic string
20 - stream level parameters
20 - stream level parameters
21 - payload parts (any number)
21 - payload parts (any number)
22 - end of stream marker.
22 - end of stream marker.
23
23
24 the Binary format
24 the Binary format
25 ============================
25 ============================
26
26
27 All numbers are unsigned and big-endian.
27 All numbers are unsigned and big-endian.
28
28
29 stream level parameters
29 stream level parameters
30 ------------------------
30 ------------------------
31
31
32 Binary format is as follow
32 Binary format is as follow
33
33
34 :params size: int32
34 :params size: int32
35
35
36 The total number of Bytes used by the parameters
36 The total number of Bytes used by the parameters
37
37
38 :params value: arbitrary number of Bytes
38 :params value: arbitrary number of Bytes
39
39
40 A blob of `params size` containing the serialized version of all stream level
40 A blob of `params size` containing the serialized version of all stream level
41 parameters.
41 parameters.
42
42
43 The blob contains a space separated list of parameters. Parameters with value
43 The blob contains a space separated list of parameters. Parameters with value
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45
45
46 Empty name are obviously forbidden.
46 Empty name are obviously forbidden.
47
47
48 Name MUST start with a letter. If this first letter is lower case, the
48 Name MUST start with a letter. If this first letter is lower case, the
49 parameter is advisory and can be safely ignored. However when the first
49 parameter is advisory and can be safely ignored. However when the first
50 letter is capital, the parameter is mandatory and the bundling process MUST
50 letter is capital, the parameter is mandatory and the bundling process MUST
51 stop if he is not able to proceed it.
51 stop if he is not able to proceed it.
52
52
53 Stream parameters use a simple textual format for two main reasons:
53 Stream parameters use a simple textual format for two main reasons:
54
54
55 - Stream level parameters should remain simple and we want to discourage any
55 - Stream level parameters should remain simple and we want to discourage any
56 crazy usage.
56 crazy usage.
57 - Textual data allow easy human inspection of a bundle2 header in case of
57 - Textual data allow easy human inspection of a bundle2 header in case of
58 troubles.
58 troubles.
59
59
60 Any Applicative level options MUST go into a bundle2 part instead.
60 Any Applicative level options MUST go into a bundle2 part instead.
61
61
62 Payload part
62 Payload part
63 ------------------------
63 ------------------------
64
64
65 Binary format is as follow
65 Binary format is as follow
66
66
67 :header size: int32
67 :header size: int32
68
68
69 The total number of Bytes used by the part header. When the header is empty
69 The total number of Bytes used by the part header. When the header is empty
70 (size = 0) this is interpreted as the end of stream marker.
70 (size = 0) this is interpreted as the end of stream marker.
71
71
72 :header:
72 :header:
73
73
74 The header defines how to interpret the part. It contains two piece of
74 The header defines how to interpret the part. It contains two piece of
75 data: the part type, and the part parameters.
75 data: the part type, and the part parameters.
76
76
77 The part type is used to route an application level handler, that can
77 The part type is used to route an application level handler, that can
78 interpret payload.
78 interpret payload.
79
79
80 Part parameters are passed to the application level handler. They are
80 Part parameters are passed to the application level handler. They are
81 meant to convey information that will help the application level object to
81 meant to convey information that will help the application level object to
82 interpret the part payload.
82 interpret the part payload.
83
83
84 The binary format of the header is has follow
84 The binary format of the header is has follow
85
85
86 :typesize: (one byte)
86 :typesize: (one byte)
87
87
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
89
89
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 to this part.
91 to this part.
92
92
93 :parameters:
93 :parameters:
94
94
95 Part's parameter may have arbitrary content, the binary structure is::
95 Part's parameter may have arbitrary content, the binary structure is::
96
96
97 <mandatory-count><advisory-count><param-sizes><param-data>
97 <mandatory-count><advisory-count><param-sizes><param-data>
98
98
99 :mandatory-count: 1 byte, number of mandatory parameters
99 :mandatory-count: 1 byte, number of mandatory parameters
100
100
101 :advisory-count: 1 byte, number of advisory parameters
101 :advisory-count: 1 byte, number of advisory parameters
102
102
103 :param-sizes:
103 :param-sizes:
104
104
105 N couple of bytes, where N is the total number of parameters. Each
105 N couple of bytes, where N is the total number of parameters. Each
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107
107
108 :param-data:
108 :param-data:
109
109
110 A blob of bytes from which each parameter key and value can be
110 A blob of bytes from which each parameter key and value can be
111 retrieved using the list of size couples stored in the previous
111 retrieved using the list of size couples stored in the previous
112 field.
112 field.
113
113
114 Mandatory parameters comes first, then the advisory ones.
114 Mandatory parameters comes first, then the advisory ones.
115
115
116 Each parameter's key MUST be unique within the part.
116 Each parameter's key MUST be unique within the part.
117
117
118 :payload:
118 :payload:
119
119
120 payload is a series of `<chunksize><chunkdata>`.
120 payload is a series of `<chunksize><chunkdata>`.
121
121
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124
124
125 The current implementation always produces either zero or one chunk.
125 The current implementation always produces either zero or one chunk.
126 This is an implementation limitation that will ultimately be lifted.
126 This is an implementation limitation that will ultimately be lifted.
127
127
128 `chunksize` can be negative to trigger special case processing. No such
128 `chunksize` can be negative to trigger special case processing. No such
129 processing is in place yet.
129 processing is in place yet.
130
130
131 Bundle processing
131 Bundle processing
132 ============================
132 ============================
133
133
134 Each part is processed in order using a "part handler". Handler are registered
134 Each part is processed in order using a "part handler". Handler are registered
135 for a certain part type.
135 for a certain part type.
136
136
137 The matching of a part to its handler is case insensitive. The case of the
137 The matching of a part to its handler is case insensitive. The case of the
138 part type is used to know if a part is mandatory or advisory. If the Part type
138 part type is used to know if a part is mandatory or advisory. If the Part type
139 contains any uppercase char it is considered mandatory. When no handler is
139 contains any uppercase char it is considered mandatory. When no handler is
140 known for a Mandatory part, the process is aborted and an exception is raised.
140 known for a Mandatory part, the process is aborted and an exception is raised.
141 If the part is advisory and no handler is known, the part is ignored. When the
141 If the part is advisory and no handler is known, the part is ignored. When the
142 process is aborted, the full bundle is still read from the stream to keep the
142 process is aborted, the full bundle is still read from the stream to keep the
143 channel usable. But none of the part read from an abort are processed. In the
143 channel usable. But none of the part read from an abort are processed. In the
144 future, dropping the stream may become an option for channel we do not care to
144 future, dropping the stream may become an option for channel we do not care to
145 preserve.
145 preserve.
146 """
146 """
147
147
148 from __future__ import absolute_import, division
148 from __future__ import absolute_import, division
149
149
150 import collections
150 import collections
151 import errno
151 import errno
152 import os
152 import os
153 import re
153 import re
154 import string
154 import string
155 import struct
155 import struct
156 import sys
156 import sys
157
157
158 from .i18n import _
158 from .i18n import _
159 from . import (
159 from . import (
160 bookmarks,
160 bookmarks,
161 changegroup,
161 changegroup,
162 encoding,
162 encoding,
163 error,
163 error,
164 node as nodemod,
164 node as nodemod,
165 obsolete,
165 obsolete,
166 phases,
166 phases,
167 pushkey,
167 pushkey,
168 pycompat,
168 pycompat,
169 streamclone,
169 streamclone,
170 tags,
170 tags,
171 url,
171 url,
172 util,
172 util,
173 )
173 )
174 from .utils import stringutil
174 from .utils import stringutil
175
175
176 urlerr = util.urlerr
176 urlerr = util.urlerr
177 urlreq = util.urlreq
177 urlreq = util.urlreq
178
178
179 _pack = struct.pack
179 _pack = struct.pack
180 _unpack = struct.unpack
180 _unpack = struct.unpack
181
181
182 _fstreamparamsize = b'>i'
182 _fstreamparamsize = b'>i'
183 _fpartheadersize = b'>i'
183 _fpartheadersize = b'>i'
184 _fparttypesize = b'>B'
184 _fparttypesize = b'>B'
185 _fpartid = b'>I'
185 _fpartid = b'>I'
186 _fpayloadsize = b'>i'
186 _fpayloadsize = b'>i'
187 _fpartparamcount = b'>BB'
187 _fpartparamcount = b'>BB'
188
188
189 preferedchunksize = 32768
189 preferedchunksize = 32768
190
190
191 _parttypeforbidden = re.compile(b'[^a-zA-Z0-9_:-]')
191 _parttypeforbidden = re.compile(b'[^a-zA-Z0-9_:-]')
192
192
193
193
194 def outdebug(ui, message):
194 def outdebug(ui, message):
195 """debug regarding output stream (bundling)"""
195 """debug regarding output stream (bundling)"""
196 if ui.configbool(b'devel', b'bundle2.debug'):
196 if ui.configbool(b'devel', b'bundle2.debug'):
197 ui.debug(b'bundle2-output: %s\n' % message)
197 ui.debug(b'bundle2-output: %s\n' % message)
198
198
199
199
200 def indebug(ui, message):
200 def indebug(ui, message):
201 """debug on input stream (unbundling)"""
201 """debug on input stream (unbundling)"""
202 if ui.configbool(b'devel', b'bundle2.debug'):
202 if ui.configbool(b'devel', b'bundle2.debug'):
203 ui.debug(b'bundle2-input: %s\n' % message)
203 ui.debug(b'bundle2-input: %s\n' % message)
204
204
205
205
206 def validateparttype(parttype):
206 def validateparttype(parttype):
207 """raise ValueError if a parttype contains invalid character"""
207 """raise ValueError if a parttype contains invalid character"""
208 if _parttypeforbidden.search(parttype):
208 if _parttypeforbidden.search(parttype):
209 raise ValueError(parttype)
209 raise ValueError(parttype)
210
210
211
211
212 def _makefpartparamsizes(nbparams):
212 def _makefpartparamsizes(nbparams):
213 """return a struct format to read part parameter sizes
213 """return a struct format to read part parameter sizes
214
214
215 The number parameters is variable so we need to build that format
215 The number parameters is variable so we need to build that format
216 dynamically.
216 dynamically.
217 """
217 """
218 return b'>' + (b'BB' * nbparams)
218 return b'>' + (b'BB' * nbparams)
219
219
220
220
221 parthandlermapping = {}
221 parthandlermapping = {}
222
222
223
223
224 def parthandler(parttype, params=()):
224 def parthandler(parttype, params=()):
225 """decorator that register a function as a bundle2 part handler
225 """decorator that register a function as a bundle2 part handler
226
226
227 eg::
227 eg::
228
228
229 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
229 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
230 def myparttypehandler(...):
230 def myparttypehandler(...):
231 '''process a part of type "my part".'''
231 '''process a part of type "my part".'''
232 ...
232 ...
233 """
233 """
234 validateparttype(parttype)
234 validateparttype(parttype)
235
235
236 def _decorator(func):
236 def _decorator(func):
237 lparttype = parttype.lower() # enforce lower case matching.
237 lparttype = parttype.lower() # enforce lower case matching.
238 assert lparttype not in parthandlermapping
238 assert lparttype not in parthandlermapping
239 parthandlermapping[lparttype] = func
239 parthandlermapping[lparttype] = func
240 func.params = frozenset(params)
240 func.params = frozenset(params)
241 return func
241 return func
242
242
243 return _decorator
243 return _decorator
244
244
245
245
246 class unbundlerecords(object):
246 class unbundlerecords(object):
247 """keep record of what happens during and unbundle
247 """keep record of what happens during and unbundle
248
248
249 New records are added using `records.add('cat', obj)`. Where 'cat' is a
249 New records are added using `records.add('cat', obj)`. Where 'cat' is a
250 category of record and obj is an arbitrary object.
250 category of record and obj is an arbitrary object.
251
251
252 `records['cat']` will return all entries of this category 'cat'.
252 `records['cat']` will return all entries of this category 'cat'.
253
253
254 Iterating on the object itself will yield `('category', obj)` tuples
254 Iterating on the object itself will yield `('category', obj)` tuples
255 for all entries.
255 for all entries.
256
256
257 All iterations happens in chronological order.
257 All iterations happens in chronological order.
258 """
258 """
259
259
260 def __init__(self):
260 def __init__(self):
261 self._categories = {}
261 self._categories = {}
262 self._sequences = []
262 self._sequences = []
263 self._replies = {}
263 self._replies = {}
264
264
265 def add(self, category, entry, inreplyto=None):
265 def add(self, category, entry, inreplyto=None):
266 """add a new record of a given category.
266 """add a new record of a given category.
267
267
268 The entry can then be retrieved in the list returned by
268 The entry can then be retrieved in the list returned by
269 self['category']."""
269 self['category']."""
270 self._categories.setdefault(category, []).append(entry)
270 self._categories.setdefault(category, []).append(entry)
271 self._sequences.append((category, entry))
271 self._sequences.append((category, entry))
272 if inreplyto is not None:
272 if inreplyto is not None:
273 self.getreplies(inreplyto).add(category, entry)
273 self.getreplies(inreplyto).add(category, entry)
274
274
275 def getreplies(self, partid):
275 def getreplies(self, partid):
276 """get the records that are replies to a specific part"""
276 """get the records that are replies to a specific part"""
277 return self._replies.setdefault(partid, unbundlerecords())
277 return self._replies.setdefault(partid, unbundlerecords())
278
278
279 def __getitem__(self, cat):
279 def __getitem__(self, cat):
280 return tuple(self._categories.get(cat, ()))
280 return tuple(self._categories.get(cat, ()))
281
281
282 def __iter__(self):
282 def __iter__(self):
283 return iter(self._sequences)
283 return iter(self._sequences)
284
284
285 def __len__(self):
285 def __len__(self):
286 return len(self._sequences)
286 return len(self._sequences)
287
287
288 def __nonzero__(self):
288 def __nonzero__(self):
289 return bool(self._sequences)
289 return bool(self._sequences)
290
290
291 __bool__ = __nonzero__
291 __bool__ = __nonzero__
292
292
293
293
294 class bundleoperation(object):
294 class bundleoperation(object):
295 """an object that represents a single bundling process
295 """an object that represents a single bundling process
296
296
297 Its purpose is to carry unbundle-related objects and states.
297 Its purpose is to carry unbundle-related objects and states.
298
298
299 A new object should be created at the beginning of each bundle processing.
299 A new object should be created at the beginning of each bundle processing.
300 The object is to be returned by the processing function.
300 The object is to be returned by the processing function.
301
301
302 The object has very little content now it will ultimately contain:
302 The object has very little content now it will ultimately contain:
303 * an access to the repo the bundle is applied to,
303 * an access to the repo the bundle is applied to,
304 * a ui object,
304 * a ui object,
305 * a way to retrieve a transaction to add changes to the repo,
305 * a way to retrieve a transaction to add changes to the repo,
306 * a way to record the result of processing each part,
306 * a way to record the result of processing each part,
307 * a way to construct a bundle response when applicable.
307 * a way to construct a bundle response when applicable.
308 """
308 """
309
309
310 def __init__(self, repo, transactiongetter, captureoutput=True, source=b''):
310 def __init__(self, repo, transactiongetter, captureoutput=True, source=b''):
311 self.repo = repo
311 self.repo = repo
312 self.ui = repo.ui
312 self.ui = repo.ui
313 self.records = unbundlerecords()
313 self.records = unbundlerecords()
314 self.reply = None
314 self.reply = None
315 self.captureoutput = captureoutput
315 self.captureoutput = captureoutput
316 self.hookargs = {}
316 self.hookargs = {}
317 self._gettransaction = transactiongetter
317 self._gettransaction = transactiongetter
318 # carries value that can modify part behavior
318 # carries value that can modify part behavior
319 self.modes = {}
319 self.modes = {}
320 self.source = source
320 self.source = source
321
321
322 def gettransaction(self):
322 def gettransaction(self):
323 transaction = self._gettransaction()
323 transaction = self._gettransaction()
324
324
325 if self.hookargs:
325 if self.hookargs:
326 # the ones added to the transaction supercede those added
326 # the ones added to the transaction supercede those added
327 # to the operation.
327 # to the operation.
328 self.hookargs.update(transaction.hookargs)
328 self.hookargs.update(transaction.hookargs)
329 transaction.hookargs = self.hookargs
329 transaction.hookargs = self.hookargs
330
330
331 # mark the hookargs as flushed. further attempts to add to
331 # mark the hookargs as flushed. further attempts to add to
332 # hookargs will result in an abort.
332 # hookargs will result in an abort.
333 self.hookargs = None
333 self.hookargs = None
334
334
335 return transaction
335 return transaction
336
336
337 def addhookargs(self, hookargs):
337 def addhookargs(self, hookargs):
338 if self.hookargs is None:
338 if self.hookargs is None:
339 raise error.ProgrammingError(
339 raise error.ProgrammingError(
340 b'attempted to add hookargs to '
340 b'attempted to add hookargs to '
341 b'operation after transaction started'
341 b'operation after transaction started'
342 )
342 )
343 self.hookargs.update(hookargs)
343 self.hookargs.update(hookargs)
344
344
345
345
346 class TransactionUnavailable(RuntimeError):
346 class TransactionUnavailable(RuntimeError):
347 pass
347 pass
348
348
349
349
350 def _notransaction():
350 def _notransaction():
351 """default method to get a transaction while processing a bundle
351 """default method to get a transaction while processing a bundle
352
352
353 Raise an exception to highlight the fact that no transaction was expected
353 Raise an exception to highlight the fact that no transaction was expected
354 to be created"""
354 to be created"""
355 raise TransactionUnavailable()
355 raise TransactionUnavailable()
356
356
357
357
358 def applybundle(repo, unbundler, tr, source, url=None, **kwargs):
358 def applybundle(repo, unbundler, tr, source, url=None, **kwargs):
359 # transform me into unbundler.apply() as soon as the freeze is lifted
359 # transform me into unbundler.apply() as soon as the freeze is lifted
360 if isinstance(unbundler, unbundle20):
360 if isinstance(unbundler, unbundle20):
361 tr.hookargs[b'bundle2'] = b'1'
361 tr.hookargs[b'bundle2'] = b'1'
362 if source is not None and b'source' not in tr.hookargs:
362 if source is not None and b'source' not in tr.hookargs:
363 tr.hookargs[b'source'] = source
363 tr.hookargs[b'source'] = source
364 if url is not None and b'url' not in tr.hookargs:
364 if url is not None and b'url' not in tr.hookargs:
365 tr.hookargs[b'url'] = url
365 tr.hookargs[b'url'] = url
366 return processbundle(repo, unbundler, lambda: tr, source=source)
366 return processbundle(repo, unbundler, lambda: tr, source=source)
367 else:
367 else:
368 # the transactiongetter won't be used, but we might as well set it
368 # the transactiongetter won't be used, but we might as well set it
369 op = bundleoperation(repo, lambda: tr, source=source)
369 op = bundleoperation(repo, lambda: tr, source=source)
370 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
370 _processchangegroup(op, unbundler, tr, source, url, **kwargs)
371 return op
371 return op
372
372
373
373
374 class partiterator(object):
374 class partiterator(object):
375 def __init__(self, repo, op, unbundler):
375 def __init__(self, repo, op, unbundler):
376 self.repo = repo
376 self.repo = repo
377 self.op = op
377 self.op = op
378 self.unbundler = unbundler
378 self.unbundler = unbundler
379 self.iterator = None
379 self.iterator = None
380 self.count = 0
380 self.count = 0
381 self.current = None
381 self.current = None
382
382
383 def __enter__(self):
383 def __enter__(self):
384 def func():
384 def func():
385 itr = enumerate(self.unbundler.iterparts(), 1)
385 itr = enumerate(self.unbundler.iterparts(), 1)
386 for count, p in itr:
386 for count, p in itr:
387 self.count = count
387 self.count = count
388 self.current = p
388 self.current = p
389 yield p
389 yield p
390 p.consume()
390 p.consume()
391 self.current = None
391 self.current = None
392
392
393 self.iterator = func()
393 self.iterator = func()
394 return self.iterator
394 return self.iterator
395
395
396 def __exit__(self, type, exc, tb):
396 def __exit__(self, type, exc, tb):
397 if not self.iterator:
397 if not self.iterator:
398 return
398 return
399
399
400 # Only gracefully abort in a normal exception situation. User aborts
400 # Only gracefully abort in a normal exception situation. User aborts
401 # like Ctrl+C throw a KeyboardInterrupt which is not a base Exception,
401 # like Ctrl+C throw a KeyboardInterrupt which is not a base Exception,
402 # and should not gracefully cleanup.
402 # and should not gracefully cleanup.
403 if isinstance(exc, Exception):
403 if isinstance(exc, Exception):
404 # Any exceptions seeking to the end of the bundle at this point are
404 # Any exceptions seeking to the end of the bundle at this point are
405 # almost certainly related to the underlying stream being bad.
405 # almost certainly related to the underlying stream being bad.
406 # And, chances are that the exception we're handling is related to
406 # And, chances are that the exception we're handling is related to
407 # getting in that bad state. So, we swallow the seeking error and
407 # getting in that bad state. So, we swallow the seeking error and
408 # re-raise the original error.
408 # re-raise the original error.
409 seekerror = False
409 seekerror = False
410 try:
410 try:
411 if self.current:
411 if self.current:
412 # consume the part content to not corrupt the stream.
412 # consume the part content to not corrupt the stream.
413 self.current.consume()
413 self.current.consume()
414
414
415 for part in self.iterator:
415 for part in self.iterator:
416 # consume the bundle content
416 # consume the bundle content
417 part.consume()
417 part.consume()
418 except Exception:
418 except Exception:
419 seekerror = True
419 seekerror = True
420
420
421 # Small hack to let caller code distinguish exceptions from bundle2
421 # Small hack to let caller code distinguish exceptions from bundle2
422 # processing from processing the old format. This is mostly needed
422 # processing from processing the old format. This is mostly needed
423 # to handle different return codes to unbundle according to the type
423 # to handle different return codes to unbundle according to the type
424 # of bundle. We should probably clean up or drop this return code
424 # of bundle. We should probably clean up or drop this return code
425 # craziness in a future version.
425 # craziness in a future version.
426 exc.duringunbundle2 = True
426 exc.duringunbundle2 = True
427 salvaged = []
427 salvaged = []
428 replycaps = None
428 replycaps = None
429 if self.op.reply is not None:
429 if self.op.reply is not None:
430 salvaged = self.op.reply.salvageoutput()
430 salvaged = self.op.reply.salvageoutput()
431 replycaps = self.op.reply.capabilities
431 replycaps = self.op.reply.capabilities
432 exc._replycaps = replycaps
432 exc._replycaps = replycaps
433 exc._bundle2salvagedoutput = salvaged
433 exc._bundle2salvagedoutput = salvaged
434
434
435 # Re-raising from a variable loses the original stack. So only use
435 # Re-raising from a variable loses the original stack. So only use
436 # that form if we need to.
436 # that form if we need to.
437 if seekerror:
437 if seekerror:
438 raise exc
438 raise exc
439
439
440 self.repo.ui.debug(
440 self.repo.ui.debug(
441 b'bundle2-input-bundle: %i parts total\n' % self.count
441 b'bundle2-input-bundle: %i parts total\n' % self.count
442 )
442 )
443
443
444
444
445 def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''):
445 def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''):
446 """This function process a bundle, apply effect to/from a repo
446 """This function process a bundle, apply effect to/from a repo
447
447
448 It iterates over each part then searches for and uses the proper handling
448 It iterates over each part then searches for and uses the proper handling
449 code to process the part. Parts are processed in order.
449 code to process the part. Parts are processed in order.
450
450
451 Unknown Mandatory part will abort the process.
451 Unknown Mandatory part will abort the process.
452
452
453 It is temporarily possible to provide a prebuilt bundleoperation to the
453 It is temporarily possible to provide a prebuilt bundleoperation to the
454 function. This is used to ensure output is properly propagated in case of
454 function. This is used to ensure output is properly propagated in case of
455 an error during the unbundling. This output capturing part will likely be
455 an error during the unbundling. This output capturing part will likely be
456 reworked and this ability will probably go away in the process.
456 reworked and this ability will probably go away in the process.
457 """
457 """
458 if op is None:
458 if op is None:
459 if transactiongetter is None:
459 if transactiongetter is None:
460 transactiongetter = _notransaction
460 transactiongetter = _notransaction
461 op = bundleoperation(repo, transactiongetter, source=source)
461 op = bundleoperation(repo, transactiongetter, source=source)
462 # todo:
462 # todo:
463 # - replace this is a init function soon.
463 # - replace this is a init function soon.
464 # - exception catching
464 # - exception catching
465 unbundler.params
465 unbundler.params
466 if repo.ui.debugflag:
466 if repo.ui.debugflag:
467 msg = [b'bundle2-input-bundle:']
467 msg = [b'bundle2-input-bundle:']
468 if unbundler.params:
468 if unbundler.params:
469 msg.append(b' %i params' % len(unbundler.params))
469 msg.append(b' %i params' % len(unbundler.params))
470 if op._gettransaction is None or op._gettransaction is _notransaction:
470 if op._gettransaction is None or op._gettransaction is _notransaction:
471 msg.append(b' no-transaction')
471 msg.append(b' no-transaction')
472 else:
472 else:
473 msg.append(b' with-transaction')
473 msg.append(b' with-transaction')
474 msg.append(b'\n')
474 msg.append(b'\n')
475 repo.ui.debug(b''.join(msg))
475 repo.ui.debug(b''.join(msg))
476
476
477 processparts(repo, op, unbundler)
477 processparts(repo, op, unbundler)
478
478
479 return op
479 return op
480
480
481
481
482 def processparts(repo, op, unbundler):
482 def processparts(repo, op, unbundler):
483 with partiterator(repo, op, unbundler) as parts:
483 with partiterator(repo, op, unbundler) as parts:
484 for part in parts:
484 for part in parts:
485 _processpart(op, part)
485 _processpart(op, part)
486
486
487
487
488 def _processchangegroup(op, cg, tr, source, url, **kwargs):
488 def _processchangegroup(op, cg, tr, source, url, **kwargs):
489 ret = cg.apply(op.repo, tr, source, url, **kwargs)
489 ret = cg.apply(op.repo, tr, source, url, **kwargs)
490 op.records.add(b'changegroup', {b'return': ret,})
490 op.records.add(b'changegroup', {b'return': ret,})
491 return ret
491 return ret
492
492
493
493
494 def _gethandler(op, part):
494 def _gethandler(op, part):
495 status = b'unknown' # used by debug output
495 status = b'unknown' # used by debug output
496 try:
496 try:
497 handler = parthandlermapping.get(part.type)
497 handler = parthandlermapping.get(part.type)
498 if handler is None:
498 if handler is None:
499 status = b'unsupported-type'
499 status = b'unsupported-type'
500 raise error.BundleUnknownFeatureError(parttype=part.type)
500 raise error.BundleUnknownFeatureError(parttype=part.type)
501 indebug(op.ui, b'found a handler for part %s' % part.type)
501 indebug(op.ui, b'found a handler for part %s' % part.type)
502 unknownparams = part.mandatorykeys - handler.params
502 unknownparams = part.mandatorykeys - handler.params
503 if unknownparams:
503 if unknownparams:
504 unknownparams = list(unknownparams)
504 unknownparams = list(unknownparams)
505 unknownparams.sort()
505 unknownparams.sort()
506 status = b'unsupported-params (%s)' % b', '.join(unknownparams)
506 status = b'unsupported-params (%s)' % b', '.join(unknownparams)
507 raise error.BundleUnknownFeatureError(
507 raise error.BundleUnknownFeatureError(
508 parttype=part.type, params=unknownparams
508 parttype=part.type, params=unknownparams
509 )
509 )
510 status = b'supported'
510 status = b'supported'
511 except error.BundleUnknownFeatureError as exc:
511 except error.BundleUnknownFeatureError as exc:
512 if part.mandatory: # mandatory parts
512 if part.mandatory: # mandatory parts
513 raise
513 raise
514 indebug(op.ui, b'ignoring unsupported advisory part %s' % exc)
514 indebug(op.ui, b'ignoring unsupported advisory part %s' % exc)
515 return # skip to part processing
515 return # skip to part processing
516 finally:
516 finally:
517 if op.ui.debugflag:
517 if op.ui.debugflag:
518 msg = [b'bundle2-input-part: "%s"' % part.type]
518 msg = [b'bundle2-input-part: "%s"' % part.type]
519 if not part.mandatory:
519 if not part.mandatory:
520 msg.append(b' (advisory)')
520 msg.append(b' (advisory)')
521 nbmp = len(part.mandatorykeys)
521 nbmp = len(part.mandatorykeys)
522 nbap = len(part.params) - nbmp
522 nbap = len(part.params) - nbmp
523 if nbmp or nbap:
523 if nbmp or nbap:
524 msg.append(b' (params:')
524 msg.append(b' (params:')
525 if nbmp:
525 if nbmp:
526 msg.append(b' %i mandatory' % nbmp)
526 msg.append(b' %i mandatory' % nbmp)
527 if nbap:
527 if nbap:
528 msg.append(b' %i advisory' % nbmp)
528 msg.append(b' %i advisory' % nbmp)
529 msg.append(b')')
529 msg.append(b')')
530 msg.append(b' %s\n' % status)
530 msg.append(b' %s\n' % status)
531 op.ui.debug(b''.join(msg))
531 op.ui.debug(b''.join(msg))
532
532
533 return handler
533 return handler
534
534
535
535
536 def _processpart(op, part):
536 def _processpart(op, part):
537 """process a single part from a bundle
537 """process a single part from a bundle
538
538
539 The part is guaranteed to have been fully consumed when the function exits
539 The part is guaranteed to have been fully consumed when the function exits
540 (even if an exception is raised)."""
540 (even if an exception is raised)."""
541 handler = _gethandler(op, part)
541 handler = _gethandler(op, part)
542 if handler is None:
542 if handler is None:
543 return
543 return
544
544
545 # handler is called outside the above try block so that we don't
545 # handler is called outside the above try block so that we don't
546 # risk catching KeyErrors from anything other than the
546 # risk catching KeyErrors from anything other than the
547 # parthandlermapping lookup (any KeyError raised by handler()
547 # parthandlermapping lookup (any KeyError raised by handler()
548 # itself represents a defect of a different variety).
548 # itself represents a defect of a different variety).
549 output = None
549 output = None
550 if op.captureoutput and op.reply is not None:
550 if op.captureoutput and op.reply is not None:
551 op.ui.pushbuffer(error=True, subproc=True)
551 op.ui.pushbuffer(error=True, subproc=True)
552 output = b''
552 output = b''
553 try:
553 try:
554 handler(op, part)
554 handler(op, part)
555 finally:
555 finally:
556 if output is not None:
556 if output is not None:
557 output = op.ui.popbuffer()
557 output = op.ui.popbuffer()
558 if output:
558 if output:
559 outpart = op.reply.newpart(b'output', data=output, mandatory=False)
559 outpart = op.reply.newpart(b'output', data=output, mandatory=False)
560 outpart.addparam(
560 outpart.addparam(
561 b'in-reply-to', pycompat.bytestr(part.id), mandatory=False
561 b'in-reply-to', pycompat.bytestr(part.id), mandatory=False
562 )
562 )
563
563
564
564
565 def decodecaps(blob):
565 def decodecaps(blob):
566 """decode a bundle2 caps bytes blob into a dictionary
566 """decode a bundle2 caps bytes blob into a dictionary
567
567
568 The blob is a list of capabilities (one per line)
568 The blob is a list of capabilities (one per line)
569 Capabilities may have values using a line of the form::
569 Capabilities may have values using a line of the form::
570
570
571 capability=value1,value2,value3
571 capability=value1,value2,value3
572
572
573 The values are always a list."""
573 The values are always a list."""
574 caps = {}
574 caps = {}
575 for line in blob.splitlines():
575 for line in blob.splitlines():
576 if not line:
576 if not line:
577 continue
577 continue
578 if b'=' not in line:
578 if b'=' not in line:
579 key, vals = line, ()
579 key, vals = line, ()
580 else:
580 else:
581 key, vals = line.split(b'=', 1)
581 key, vals = line.split(b'=', 1)
582 vals = vals.split(b',')
582 vals = vals.split(b',')
583 key = urlreq.unquote(key)
583 key = urlreq.unquote(key)
584 vals = [urlreq.unquote(v) for v in vals]
584 vals = [urlreq.unquote(v) for v in vals]
585 caps[key] = vals
585 caps[key] = vals
586 return caps
586 return caps
587
587
588
588
589 def encodecaps(caps):
589 def encodecaps(caps):
590 """encode a bundle2 caps dictionary into a bytes blob"""
590 """encode a bundle2 caps dictionary into a bytes blob"""
591 chunks = []
591 chunks = []
592 for ca in sorted(caps):
592 for ca in sorted(caps):
593 vals = caps[ca]
593 vals = caps[ca]
594 ca = urlreq.quote(ca)
594 ca = urlreq.quote(ca)
595 vals = [urlreq.quote(v) for v in vals]
595 vals = [urlreq.quote(v) for v in vals]
596 if vals:
596 if vals:
597 ca = b"%s=%s" % (ca, b','.join(vals))
597 ca = b"%s=%s" % (ca, b','.join(vals))
598 chunks.append(ca)
598 chunks.append(ca)
599 return b'\n'.join(chunks)
599 return b'\n'.join(chunks)
600
600
601
601
602 bundletypes = {
602 bundletypes = {
603 b"": (b"", b'UN'), # only when using unbundle on ssh and old http servers
603 b"": (b"", b'UN'), # only when using unbundle on ssh and old http servers
604 # since the unification ssh accepts a header but there
604 # since the unification ssh accepts a header but there
605 # is no capability signaling it.
605 # is no capability signaling it.
606 b"HG20": (), # special-cased below
606 b"HG20": (), # special-cased below
607 b"HG10UN": (b"HG10UN", b'UN'),
607 b"HG10UN": (b"HG10UN", b'UN'),
608 b"HG10BZ": (b"HG10", b'BZ'),
608 b"HG10BZ": (b"HG10", b'BZ'),
609 b"HG10GZ": (b"HG10GZ", b'GZ'),
609 b"HG10GZ": (b"HG10GZ", b'GZ'),
610 }
610 }
611
611
612 # hgweb uses this list to communicate its preferred type
612 # hgweb uses this list to communicate its preferred type
613 bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN']
613 bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN']
614
614
615
615
616 class bundle20(object):
616 class bundle20(object):
617 """represent an outgoing bundle2 container
617 """represent an outgoing bundle2 container
618
618
619 Use the `addparam` method to add stream level parameter. and `newpart` to
619 Use the `addparam` method to add stream level parameter. and `newpart` to
620 populate it. Then call `getchunks` to retrieve all the binary chunks of
620 populate it. Then call `getchunks` to retrieve all the binary chunks of
621 data that compose the bundle2 container."""
621 data that compose the bundle2 container."""
622
622
623 _magicstring = b'HG20'
623 _magicstring = b'HG20'
624
624
625 def __init__(self, ui, capabilities=()):
625 def __init__(self, ui, capabilities=()):
626 self.ui = ui
626 self.ui = ui
627 self._params = []
627 self._params = []
628 self._parts = []
628 self._parts = []
629 self.capabilities = dict(capabilities)
629 self.capabilities = dict(capabilities)
630 self._compengine = util.compengines.forbundletype(b'UN')
630 self._compengine = util.compengines.forbundletype(b'UN')
631 self._compopts = None
631 self._compopts = None
632 # If compression is being handled by a consumer of the raw
632 # If compression is being handled by a consumer of the raw
633 # data (e.g. the wire protocol), unsetting this flag tells
633 # data (e.g. the wire protocol), unsetting this flag tells
634 # consumers that the bundle is best left uncompressed.
634 # consumers that the bundle is best left uncompressed.
635 self.prefercompressed = True
635 self.prefercompressed = True
636
636
637 def setcompression(self, alg, compopts=None):
637 def setcompression(self, alg, compopts=None):
638 """setup core part compression to <alg>"""
638 """setup core part compression to <alg>"""
639 if alg in (None, b'UN'):
639 if alg in (None, b'UN'):
640 return
640 return
641 assert not any(n.lower() == b'compression' for n, v in self._params)
641 assert not any(n.lower() == b'compression' for n, v in self._params)
642 self.addparam(b'Compression', alg)
642 self.addparam(b'Compression', alg)
643 self._compengine = util.compengines.forbundletype(alg)
643 self._compengine = util.compengines.forbundletype(alg)
644 self._compopts = compopts
644 self._compopts = compopts
645
645
646 @property
646 @property
647 def nbparts(self):
647 def nbparts(self):
648 """total number of parts added to the bundler"""
648 """total number of parts added to the bundler"""
649 return len(self._parts)
649 return len(self._parts)
650
650
651 # methods used to defines the bundle2 content
651 # methods used to defines the bundle2 content
652 def addparam(self, name, value=None):
652 def addparam(self, name, value=None):
653 """add a stream level parameter"""
653 """add a stream level parameter"""
654 if not name:
654 if not name:
655 raise error.ProgrammingError(b'empty parameter name')
655 raise error.ProgrammingError(b'empty parameter name')
656 if name[0:1] not in pycompat.bytestr(string.ascii_letters):
656 if name[0:1] not in pycompat.bytestr(string.ascii_letters):
657 raise error.ProgrammingError(
657 raise error.ProgrammingError(
658 b'non letter first character: %s' % name
658 b'non letter first character: %s' % name
659 )
659 )
660 self._params.append((name, value))
660 self._params.append((name, value))
661
661
662 def addpart(self, part):
662 def addpart(self, part):
663 """add a new part to the bundle2 container
663 """add a new part to the bundle2 container
664
664
665 Parts contains the actual applicative payload."""
665 Parts contains the actual applicative payload."""
666 assert part.id is None
666 assert part.id is None
667 part.id = len(self._parts) # very cheap counter
667 part.id = len(self._parts) # very cheap counter
668 self._parts.append(part)
668 self._parts.append(part)
669
669
670 def newpart(self, typeid, *args, **kwargs):
670 def newpart(self, typeid, *args, **kwargs):
671 """create a new part and add it to the containers
671 """create a new part and add it to the containers
672
672
673 As the part is directly added to the containers. For now, this means
673 As the part is directly added to the containers. For now, this means
674 that any failure to properly initialize the part after calling
674 that any failure to properly initialize the part after calling
675 ``newpart`` should result in a failure of the whole bundling process.
675 ``newpart`` should result in a failure of the whole bundling process.
676
676
677 You can still fall back to manually create and add if you need better
677 You can still fall back to manually create and add if you need better
678 control."""
678 control."""
679 part = bundlepart(typeid, *args, **kwargs)
679 part = bundlepart(typeid, *args, **kwargs)
680 self.addpart(part)
680 self.addpart(part)
681 return part
681 return part
682
682
683 # methods used to generate the bundle2 stream
683 # methods used to generate the bundle2 stream
684 def getchunks(self):
684 def getchunks(self):
685 if self.ui.debugflag:
685 if self.ui.debugflag:
686 msg = [b'bundle2-output-bundle: "%s",' % self._magicstring]
686 msg = [b'bundle2-output-bundle: "%s",' % self._magicstring]
687 if self._params:
687 if self._params:
688 msg.append(b' (%i params)' % len(self._params))
688 msg.append(b' (%i params)' % len(self._params))
689 msg.append(b' %i parts total\n' % len(self._parts))
689 msg.append(b' %i parts total\n' % len(self._parts))
690 self.ui.debug(b''.join(msg))
690 self.ui.debug(b''.join(msg))
691 outdebug(self.ui, b'start emission of %s stream' % self._magicstring)
691 outdebug(self.ui, b'start emission of %s stream' % self._magicstring)
692 yield self._magicstring
692 yield self._magicstring
693 param = self._paramchunk()
693 param = self._paramchunk()
694 outdebug(self.ui, b'bundle parameter: %s' % param)
694 outdebug(self.ui, b'bundle parameter: %s' % param)
695 yield _pack(_fstreamparamsize, len(param))
695 yield _pack(_fstreamparamsize, len(param))
696 if param:
696 if param:
697 yield param
697 yield param
698 for chunk in self._compengine.compressstream(
698 for chunk in self._compengine.compressstream(
699 self._getcorechunk(), self._compopts
699 self._getcorechunk(), self._compopts
700 ):
700 ):
701 yield chunk
701 yield chunk
702
702
703 def _paramchunk(self):
703 def _paramchunk(self):
704 """return a encoded version of all stream parameters"""
704 """return a encoded version of all stream parameters"""
705 blocks = []
705 blocks = []
706 for par, value in self._params:
706 for par, value in self._params:
707 par = urlreq.quote(par)
707 par = urlreq.quote(par)
708 if value is not None:
708 if value is not None:
709 value = urlreq.quote(value)
709 value = urlreq.quote(value)
710 par = b'%s=%s' % (par, value)
710 par = b'%s=%s' % (par, value)
711 blocks.append(par)
711 blocks.append(par)
712 return b' '.join(blocks)
712 return b' '.join(blocks)
713
713
714 def _getcorechunk(self):
714 def _getcorechunk(self):
715 """yield chunk for the core part of the bundle
715 """yield chunk for the core part of the bundle
716
716
717 (all but headers and parameters)"""
717 (all but headers and parameters)"""
718 outdebug(self.ui, b'start of parts')
718 outdebug(self.ui, b'start of parts')
719 for part in self._parts:
719 for part in self._parts:
720 outdebug(self.ui, b'bundle part: "%s"' % part.type)
720 outdebug(self.ui, b'bundle part: "%s"' % part.type)
721 for chunk in part.getchunks(ui=self.ui):
721 for chunk in part.getchunks(ui=self.ui):
722 yield chunk
722 yield chunk
723 outdebug(self.ui, b'end of bundle')
723 outdebug(self.ui, b'end of bundle')
724 yield _pack(_fpartheadersize, 0)
724 yield _pack(_fpartheadersize, 0)
725
725
726 def salvageoutput(self):
726 def salvageoutput(self):
727 """return a list with a copy of all output parts in the bundle
727 """return a list with a copy of all output parts in the bundle
728
728
729 This is meant to be used during error handling to make sure we preserve
729 This is meant to be used during error handling to make sure we preserve
730 server output"""
730 server output"""
731 salvaged = []
731 salvaged = []
732 for part in self._parts:
732 for part in self._parts:
733 if part.type.startswith(b'output'):
733 if part.type.startswith(b'output'):
734 salvaged.append(part.copy())
734 salvaged.append(part.copy())
735 return salvaged
735 return salvaged
736
736
737
737
738 class unpackermixin(object):
738 class unpackermixin(object):
739 """A mixin to extract bytes and struct data from a stream"""
739 """A mixin to extract bytes and struct data from a stream"""
740
740
741 def __init__(self, fp):
741 def __init__(self, fp):
742 self._fp = fp
742 self._fp = fp
743
743
744 def _unpack(self, format):
744 def _unpack(self, format):
745 """unpack this struct format from the stream
745 """unpack this struct format from the stream
746
746
747 This method is meant for internal usage by the bundle2 protocol only.
747 This method is meant for internal usage by the bundle2 protocol only.
748 They directly manipulate the low level stream including bundle2 level
748 They directly manipulate the low level stream including bundle2 level
749 instruction.
749 instruction.
750
750
751 Do not use it to implement higher-level logic or methods."""
751 Do not use it to implement higher-level logic or methods."""
752 data = self._readexact(struct.calcsize(format))
752 data = self._readexact(struct.calcsize(format))
753 return _unpack(format, data)
753 return _unpack(format, data)
754
754
755 def _readexact(self, size):
755 def _readexact(self, size):
756 """read exactly <size> bytes from the stream
756 """read exactly <size> bytes from the stream
757
757
758 This method is meant for internal usage by the bundle2 protocol only.
758 This method is meant for internal usage by the bundle2 protocol only.
759 They directly manipulate the low level stream including bundle2 level
759 They directly manipulate the low level stream including bundle2 level
760 instruction.
760 instruction.
761
761
762 Do not use it to implement higher-level logic or methods."""
762 Do not use it to implement higher-level logic or methods."""
763 return changegroup.readexactly(self._fp, size)
763 return changegroup.readexactly(self._fp, size)
764
764
765
765
766 def getunbundler(ui, fp, magicstring=None):
766 def getunbundler(ui, fp, magicstring=None):
767 """return a valid unbundler object for a given magicstring"""
767 """return a valid unbundler object for a given magicstring"""
768 if magicstring is None:
768 if magicstring is None:
769 magicstring = changegroup.readexactly(fp, 4)
769 magicstring = changegroup.readexactly(fp, 4)
770 magic, version = magicstring[0:2], magicstring[2:4]
770 magic, version = magicstring[0:2], magicstring[2:4]
771 if magic != b'HG':
771 if magic != b'HG':
772 ui.debug(
772 ui.debug(
773 b"error: invalid magic: %r (version %r), should be 'HG'\n"
773 b"error: invalid magic: %r (version %r), should be 'HG'\n"
774 % (magic, version)
774 % (magic, version)
775 )
775 )
776 raise error.Abort(_(b'not a Mercurial bundle'))
776 raise error.Abort(_(b'not a Mercurial bundle'))
777 unbundlerclass = formatmap.get(version)
777 unbundlerclass = formatmap.get(version)
778 if unbundlerclass is None:
778 if unbundlerclass is None:
779 raise error.Abort(_(b'unknown bundle version %s') % version)
779 raise error.Abort(_(b'unknown bundle version %s') % version)
780 unbundler = unbundlerclass(ui, fp)
780 unbundler = unbundlerclass(ui, fp)
781 indebug(ui, b'start processing of %s stream' % magicstring)
781 indebug(ui, b'start processing of %s stream' % magicstring)
782 return unbundler
782 return unbundler
783
783
784
784
785 class unbundle20(unpackermixin):
785 class unbundle20(unpackermixin):
786 """interpret a bundle2 stream
786 """interpret a bundle2 stream
787
787
788 This class is fed with a binary stream and yields parts through its
788 This class is fed with a binary stream and yields parts through its
789 `iterparts` methods."""
789 `iterparts` methods."""
790
790
791 _magicstring = b'HG20'
791 _magicstring = b'HG20'
792
792
793 def __init__(self, ui, fp):
793 def __init__(self, ui, fp):
794 """If header is specified, we do not read it out of the stream."""
794 """If header is specified, we do not read it out of the stream."""
795 self.ui = ui
795 self.ui = ui
796 self._compengine = util.compengines.forbundletype(b'UN')
796 self._compengine = util.compengines.forbundletype(b'UN')
797 self._compressed = None
797 self._compressed = None
798 super(unbundle20, self).__init__(fp)
798 super(unbundle20, self).__init__(fp)
799
799
800 @util.propertycache
800 @util.propertycache
801 def params(self):
801 def params(self):
802 """dictionary of stream level parameters"""
802 """dictionary of stream level parameters"""
803 indebug(self.ui, b'reading bundle2 stream parameters')
803 indebug(self.ui, b'reading bundle2 stream parameters')
804 params = {}
804 params = {}
805 paramssize = self._unpack(_fstreamparamsize)[0]
805 paramssize = self._unpack(_fstreamparamsize)[0]
806 if paramssize < 0:
806 if paramssize < 0:
807 raise error.BundleValueError(
807 raise error.BundleValueError(
808 b'negative bundle param size: %i' % paramssize
808 b'negative bundle param size: %i' % paramssize
809 )
809 )
810 if paramssize:
810 if paramssize:
811 params = self._readexact(paramssize)
811 params = self._readexact(paramssize)
812 params = self._processallparams(params)
812 params = self._processallparams(params)
813 return params
813 return params
814
814
815 def _processallparams(self, paramsblock):
815 def _processallparams(self, paramsblock):
816 """"""
816 """"""
817 params = util.sortdict()
817 params = util.sortdict()
818 for p in paramsblock.split(b' '):
818 for p in paramsblock.split(b' '):
819 p = p.split(b'=', 1)
819 p = p.split(b'=', 1)
820 p = [urlreq.unquote(i) for i in p]
820 p = [urlreq.unquote(i) for i in p]
821 if len(p) < 2:
821 if len(p) < 2:
822 p.append(None)
822 p.append(None)
823 self._processparam(*p)
823 self._processparam(*p)
824 params[p[0]] = p[1]
824 params[p[0]] = p[1]
825 return params
825 return params
826
826
827 def _processparam(self, name, value):
827 def _processparam(self, name, value):
828 """process a parameter, applying its effect if needed
828 """process a parameter, applying its effect if needed
829
829
830 Parameter starting with a lower case letter are advisory and will be
830 Parameter starting with a lower case letter are advisory and will be
831 ignored when unknown. Those starting with an upper case letter are
831 ignored when unknown. Those starting with an upper case letter are
832 mandatory and will this function will raise a KeyError when unknown.
832 mandatory and will this function will raise a KeyError when unknown.
833
833
834 Note: no option are currently supported. Any input will be either
834 Note: no option are currently supported. Any input will be either
835 ignored or failing.
835 ignored or failing.
836 """
836 """
837 if not name:
837 if not name:
838 raise ValueError(r'empty parameter name')
838 raise ValueError(r'empty parameter name')
839 if name[0:1] not in pycompat.bytestr(string.ascii_letters):
839 if name[0:1] not in pycompat.bytestr(string.ascii_letters):
840 raise ValueError(r'non letter first character: %s' % name)
840 raise ValueError(r'non letter first character: %s' % name)
841 try:
841 try:
842 handler = b2streamparamsmap[name.lower()]
842 handler = b2streamparamsmap[name.lower()]
843 except KeyError:
843 except KeyError:
844 if name[0:1].islower():
844 if name[0:1].islower():
845 indebug(self.ui, b"ignoring unknown parameter %s" % name)
845 indebug(self.ui, b"ignoring unknown parameter %s" % name)
846 else:
846 else:
847 raise error.BundleUnknownFeatureError(params=(name,))
847 raise error.BundleUnknownFeatureError(params=(name,))
848 else:
848 else:
849 handler(self, name, value)
849 handler(self, name, value)
850
850
851 def _forwardchunks(self):
851 def _forwardchunks(self):
852 """utility to transfer a bundle2 as binary
852 """utility to transfer a bundle2 as binary
853
853
854 This is made necessary by the fact the 'getbundle' command over 'ssh'
854 This is made necessary by the fact the 'getbundle' command over 'ssh'
855 have no way to know then the reply end, relying on the bundle to be
855 have no way to know then the reply end, relying on the bundle to be
856 interpreted to know its end. This is terrible and we are sorry, but we
856 interpreted to know its end. This is terrible and we are sorry, but we
857 needed to move forward to get general delta enabled.
857 needed to move forward to get general delta enabled.
858 """
858 """
859 yield self._magicstring
859 yield self._magicstring
860 assert b'params' not in vars(self)
860 assert b'params' not in vars(self)
861 paramssize = self._unpack(_fstreamparamsize)[0]
861 paramssize = self._unpack(_fstreamparamsize)[0]
862 if paramssize < 0:
862 if paramssize < 0:
863 raise error.BundleValueError(
863 raise error.BundleValueError(
864 b'negative bundle param size: %i' % paramssize
864 b'negative bundle param size: %i' % paramssize
865 )
865 )
866 if paramssize:
866 if paramssize:
867 params = self._readexact(paramssize)
867 params = self._readexact(paramssize)
868 self._processallparams(params)
868 self._processallparams(params)
869 # The payload itself is decompressed below, so drop
869 # The payload itself is decompressed below, so drop
870 # the compression parameter passed down to compensate.
870 # the compression parameter passed down to compensate.
871 outparams = []
871 outparams = []
872 for p in params.split(b' '):
872 for p in params.split(b' '):
873 k, v = p.split(b'=', 1)
873 k, v = p.split(b'=', 1)
874 if k.lower() != b'compression':
874 if k.lower() != b'compression':
875 outparams.append(p)
875 outparams.append(p)
876 outparams = b' '.join(outparams)
876 outparams = b' '.join(outparams)
877 yield _pack(_fstreamparamsize, len(outparams))
877 yield _pack(_fstreamparamsize, len(outparams))
878 yield outparams
878 yield outparams
879 else:
879 else:
880 yield _pack(_fstreamparamsize, paramssize)
880 yield _pack(_fstreamparamsize, paramssize)
881 # From there, payload might need to be decompressed
881 # From there, payload might need to be decompressed
882 self._fp = self._compengine.decompressorreader(self._fp)
882 self._fp = self._compengine.decompressorreader(self._fp)
883 emptycount = 0
883 emptycount = 0
884 while emptycount < 2:
884 while emptycount < 2:
885 # so we can brainlessly loop
885 # so we can brainlessly loop
886 assert _fpartheadersize == _fpayloadsize
886 assert _fpartheadersize == _fpayloadsize
887 size = self._unpack(_fpartheadersize)[0]
887 size = self._unpack(_fpartheadersize)[0]
888 yield _pack(_fpartheadersize, size)
888 yield _pack(_fpartheadersize, size)
889 if size:
889 if size:
890 emptycount = 0
890 emptycount = 0
891 else:
891 else:
892 emptycount += 1
892 emptycount += 1
893 continue
893 continue
894 if size == flaginterrupt:
894 if size == flaginterrupt:
895 continue
895 continue
896 elif size < 0:
896 elif size < 0:
897 raise error.BundleValueError(b'negative chunk size: %i')
897 raise error.BundleValueError(b'negative chunk size: %i')
898 yield self._readexact(size)
898 yield self._readexact(size)
899
899
900 def iterparts(self, seekable=False):
900 def iterparts(self, seekable=False):
901 """yield all parts contained in the stream"""
901 """yield all parts contained in the stream"""
902 cls = seekableunbundlepart if seekable else unbundlepart
902 cls = seekableunbundlepart if seekable else unbundlepart
903 # make sure param have been loaded
903 # make sure param have been loaded
904 self.params
904 self.params
905 # From there, payload need to be decompressed
905 # From there, payload need to be decompressed
906 self._fp = self._compengine.decompressorreader(self._fp)
906 self._fp = self._compengine.decompressorreader(self._fp)
907 indebug(self.ui, b'start extraction of bundle2 parts')
907 indebug(self.ui, b'start extraction of bundle2 parts')
908 headerblock = self._readpartheader()
908 headerblock = self._readpartheader()
909 while headerblock is not None:
909 while headerblock is not None:
910 part = cls(self.ui, headerblock, self._fp)
910 part = cls(self.ui, headerblock, self._fp)
911 yield part
911 yield part
912 # Ensure part is fully consumed so we can start reading the next
912 # Ensure part is fully consumed so we can start reading the next
913 # part.
913 # part.
914 part.consume()
914 part.consume()
915
915
916 headerblock = self._readpartheader()
916 headerblock = self._readpartheader()
917 indebug(self.ui, b'end of bundle2 stream')
917 indebug(self.ui, b'end of bundle2 stream')
918
918
919 def _readpartheader(self):
919 def _readpartheader(self):
920 """reads a part header size and return the bytes blob
920 """reads a part header size and return the bytes blob
921
921
922 returns None if empty"""
922 returns None if empty"""
923 headersize = self._unpack(_fpartheadersize)[0]
923 headersize = self._unpack(_fpartheadersize)[0]
924 if headersize < 0:
924 if headersize < 0:
925 raise error.BundleValueError(
925 raise error.BundleValueError(
926 b'negative part header size: %i' % headersize
926 b'negative part header size: %i' % headersize
927 )
927 )
928 indebug(self.ui, b'part header size: %i' % headersize)
928 indebug(self.ui, b'part header size: %i' % headersize)
929 if headersize:
929 if headersize:
930 return self._readexact(headersize)
930 return self._readexact(headersize)
931 return None
931 return None
932
932
933 def compressed(self):
933 def compressed(self):
934 self.params # load params
934 self.params # load params
935 return self._compressed
935 return self._compressed
936
936
937 def close(self):
937 def close(self):
938 """close underlying file"""
938 """close underlying file"""
939 if util.safehasattr(self._fp, b'close'):
939 if util.safehasattr(self._fp, 'close'):
940 return self._fp.close()
940 return self._fp.close()
941
941
942
942
943 formatmap = {b'20': unbundle20}
943 formatmap = {b'20': unbundle20}
944
944
945 b2streamparamsmap = {}
945 b2streamparamsmap = {}
946
946
947
947
948 def b2streamparamhandler(name):
948 def b2streamparamhandler(name):
949 """register a handler for a stream level parameter"""
949 """register a handler for a stream level parameter"""
950
950
951 def decorator(func):
951 def decorator(func):
952 assert name not in formatmap
952 assert name not in formatmap
953 b2streamparamsmap[name] = func
953 b2streamparamsmap[name] = func
954 return func
954 return func
955
955
956 return decorator
956 return decorator
957
957
958
958
959 @b2streamparamhandler(b'compression')
959 @b2streamparamhandler(b'compression')
960 def processcompression(unbundler, param, value):
960 def processcompression(unbundler, param, value):
961 """read compression parameter and install payload decompression"""
961 """read compression parameter and install payload decompression"""
962 if value not in util.compengines.supportedbundletypes:
962 if value not in util.compengines.supportedbundletypes:
963 raise error.BundleUnknownFeatureError(params=(param,), values=(value,))
963 raise error.BundleUnknownFeatureError(params=(param,), values=(value,))
964 unbundler._compengine = util.compengines.forbundletype(value)
964 unbundler._compengine = util.compengines.forbundletype(value)
965 if value is not None:
965 if value is not None:
966 unbundler._compressed = True
966 unbundler._compressed = True
967
967
968
968
969 class bundlepart(object):
969 class bundlepart(object):
970 """A bundle2 part contains application level payload
970 """A bundle2 part contains application level payload
971
971
972 The part `type` is used to route the part to the application level
972 The part `type` is used to route the part to the application level
973 handler.
973 handler.
974
974
975 The part payload is contained in ``part.data``. It could be raw bytes or a
975 The part payload is contained in ``part.data``. It could be raw bytes or a
976 generator of byte chunks.
976 generator of byte chunks.
977
977
978 You can add parameters to the part using the ``addparam`` method.
978 You can add parameters to the part using the ``addparam`` method.
979 Parameters can be either mandatory (default) or advisory. Remote side
979 Parameters can be either mandatory (default) or advisory. Remote side
980 should be able to safely ignore the advisory ones.
980 should be able to safely ignore the advisory ones.
981
981
982 Both data and parameters cannot be modified after the generation has begun.
982 Both data and parameters cannot be modified after the generation has begun.
983 """
983 """
984
984
985 def __init__(
985 def __init__(
986 self,
986 self,
987 parttype,
987 parttype,
988 mandatoryparams=(),
988 mandatoryparams=(),
989 advisoryparams=(),
989 advisoryparams=(),
990 data=b'',
990 data=b'',
991 mandatory=True,
991 mandatory=True,
992 ):
992 ):
993 validateparttype(parttype)
993 validateparttype(parttype)
994 self.id = None
994 self.id = None
995 self.type = parttype
995 self.type = parttype
996 self._data = data
996 self._data = data
997 self._mandatoryparams = list(mandatoryparams)
997 self._mandatoryparams = list(mandatoryparams)
998 self._advisoryparams = list(advisoryparams)
998 self._advisoryparams = list(advisoryparams)
999 # checking for duplicated entries
999 # checking for duplicated entries
1000 self._seenparams = set()
1000 self._seenparams = set()
1001 for pname, __ in self._mandatoryparams + self._advisoryparams:
1001 for pname, __ in self._mandatoryparams + self._advisoryparams:
1002 if pname in self._seenparams:
1002 if pname in self._seenparams:
1003 raise error.ProgrammingError(b'duplicated params: %s' % pname)
1003 raise error.ProgrammingError(b'duplicated params: %s' % pname)
1004 self._seenparams.add(pname)
1004 self._seenparams.add(pname)
1005 # status of the part's generation:
1005 # status of the part's generation:
1006 # - None: not started,
1006 # - None: not started,
1007 # - False: currently generated,
1007 # - False: currently generated,
1008 # - True: generation done.
1008 # - True: generation done.
1009 self._generated = None
1009 self._generated = None
1010 self.mandatory = mandatory
1010 self.mandatory = mandatory
1011
1011
1012 def __repr__(self):
1012 def __repr__(self):
1013 cls = b"%s.%s" % (self.__class__.__module__, self.__class__.__name__)
1013 cls = b"%s.%s" % (self.__class__.__module__, self.__class__.__name__)
1014 return b'<%s object at %x; id: %s; type: %s; mandatory: %s>' % (
1014 return b'<%s object at %x; id: %s; type: %s; mandatory: %s>' % (
1015 cls,
1015 cls,
1016 id(self),
1016 id(self),
1017 self.id,
1017 self.id,
1018 self.type,
1018 self.type,
1019 self.mandatory,
1019 self.mandatory,
1020 )
1020 )
1021
1021
1022 def copy(self):
1022 def copy(self):
1023 """return a copy of the part
1023 """return a copy of the part
1024
1024
1025 The new part have the very same content but no partid assigned yet.
1025 The new part have the very same content but no partid assigned yet.
1026 Parts with generated data cannot be copied."""
1026 Parts with generated data cannot be copied."""
1027 assert not util.safehasattr(self.data, b'next')
1027 assert not util.safehasattr(self.data, 'next')
1028 return self.__class__(
1028 return self.__class__(
1029 self.type,
1029 self.type,
1030 self._mandatoryparams,
1030 self._mandatoryparams,
1031 self._advisoryparams,
1031 self._advisoryparams,
1032 self._data,
1032 self._data,
1033 self.mandatory,
1033 self.mandatory,
1034 )
1034 )
1035
1035
1036 # methods used to defines the part content
1036 # methods used to defines the part content
1037 @property
1037 @property
1038 def data(self):
1038 def data(self):
1039 return self._data
1039 return self._data
1040
1040
1041 @data.setter
1041 @data.setter
1042 def data(self, data):
1042 def data(self, data):
1043 if self._generated is not None:
1043 if self._generated is not None:
1044 raise error.ReadOnlyPartError(b'part is being generated')
1044 raise error.ReadOnlyPartError(b'part is being generated')
1045 self._data = data
1045 self._data = data
1046
1046
1047 @property
1047 @property
1048 def mandatoryparams(self):
1048 def mandatoryparams(self):
1049 # make it an immutable tuple to force people through ``addparam``
1049 # make it an immutable tuple to force people through ``addparam``
1050 return tuple(self._mandatoryparams)
1050 return tuple(self._mandatoryparams)
1051
1051
1052 @property
1052 @property
1053 def advisoryparams(self):
1053 def advisoryparams(self):
1054 # make it an immutable tuple to force people through ``addparam``
1054 # make it an immutable tuple to force people through ``addparam``
1055 return tuple(self._advisoryparams)
1055 return tuple(self._advisoryparams)
1056
1056
1057 def addparam(self, name, value=b'', mandatory=True):
1057 def addparam(self, name, value=b'', mandatory=True):
1058 """add a parameter to the part
1058 """add a parameter to the part
1059
1059
1060 If 'mandatory' is set to True, the remote handler must claim support
1060 If 'mandatory' is set to True, the remote handler must claim support
1061 for this parameter or the unbundling will be aborted.
1061 for this parameter or the unbundling will be aborted.
1062
1062
1063 The 'name' and 'value' cannot exceed 255 bytes each.
1063 The 'name' and 'value' cannot exceed 255 bytes each.
1064 """
1064 """
1065 if self._generated is not None:
1065 if self._generated is not None:
1066 raise error.ReadOnlyPartError(b'part is being generated')
1066 raise error.ReadOnlyPartError(b'part is being generated')
1067 if name in self._seenparams:
1067 if name in self._seenparams:
1068 raise ValueError(b'duplicated params: %s' % name)
1068 raise ValueError(b'duplicated params: %s' % name)
1069 self._seenparams.add(name)
1069 self._seenparams.add(name)
1070 params = self._advisoryparams
1070 params = self._advisoryparams
1071 if mandatory:
1071 if mandatory:
1072 params = self._mandatoryparams
1072 params = self._mandatoryparams
1073 params.append((name, value))
1073 params.append((name, value))
1074
1074
1075 # methods used to generates the bundle2 stream
1075 # methods used to generates the bundle2 stream
1076 def getchunks(self, ui):
1076 def getchunks(self, ui):
1077 if self._generated is not None:
1077 if self._generated is not None:
1078 raise error.ProgrammingError(b'part can only be consumed once')
1078 raise error.ProgrammingError(b'part can only be consumed once')
1079 self._generated = False
1079 self._generated = False
1080
1080
1081 if ui.debugflag:
1081 if ui.debugflag:
1082 msg = [b'bundle2-output-part: "%s"' % self.type]
1082 msg = [b'bundle2-output-part: "%s"' % self.type]
1083 if not self.mandatory:
1083 if not self.mandatory:
1084 msg.append(b' (advisory)')
1084 msg.append(b' (advisory)')
1085 nbmp = len(self.mandatoryparams)
1085 nbmp = len(self.mandatoryparams)
1086 nbap = len(self.advisoryparams)
1086 nbap = len(self.advisoryparams)
1087 if nbmp or nbap:
1087 if nbmp or nbap:
1088 msg.append(b' (params:')
1088 msg.append(b' (params:')
1089 if nbmp:
1089 if nbmp:
1090 msg.append(b' %i mandatory' % nbmp)
1090 msg.append(b' %i mandatory' % nbmp)
1091 if nbap:
1091 if nbap:
1092 msg.append(b' %i advisory' % nbmp)
1092 msg.append(b' %i advisory' % nbmp)
1093 msg.append(b')')
1093 msg.append(b')')
1094 if not self.data:
1094 if not self.data:
1095 msg.append(b' empty payload')
1095 msg.append(b' empty payload')
1096 elif util.safehasattr(self.data, b'next') or util.safehasattr(
1096 elif util.safehasattr(self.data, 'next') or util.safehasattr(
1097 self.data, b'__next__'
1097 self.data, b'__next__'
1098 ):
1098 ):
1099 msg.append(b' streamed payload')
1099 msg.append(b' streamed payload')
1100 else:
1100 else:
1101 msg.append(b' %i bytes payload' % len(self.data))
1101 msg.append(b' %i bytes payload' % len(self.data))
1102 msg.append(b'\n')
1102 msg.append(b'\n')
1103 ui.debug(b''.join(msg))
1103 ui.debug(b''.join(msg))
1104
1104
1105 #### header
1105 #### header
1106 if self.mandatory:
1106 if self.mandatory:
1107 parttype = self.type.upper()
1107 parttype = self.type.upper()
1108 else:
1108 else:
1109 parttype = self.type.lower()
1109 parttype = self.type.lower()
1110 outdebug(ui, b'part %s: "%s"' % (pycompat.bytestr(self.id), parttype))
1110 outdebug(ui, b'part %s: "%s"' % (pycompat.bytestr(self.id), parttype))
1111 ## parttype
1111 ## parttype
1112 header = [
1112 header = [
1113 _pack(_fparttypesize, len(parttype)),
1113 _pack(_fparttypesize, len(parttype)),
1114 parttype,
1114 parttype,
1115 _pack(_fpartid, self.id),
1115 _pack(_fpartid, self.id),
1116 ]
1116 ]
1117 ## parameters
1117 ## parameters
1118 # count
1118 # count
1119 manpar = self.mandatoryparams
1119 manpar = self.mandatoryparams
1120 advpar = self.advisoryparams
1120 advpar = self.advisoryparams
1121 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
1121 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
1122 # size
1122 # size
1123 parsizes = []
1123 parsizes = []
1124 for key, value in manpar:
1124 for key, value in manpar:
1125 parsizes.append(len(key))
1125 parsizes.append(len(key))
1126 parsizes.append(len(value))
1126 parsizes.append(len(value))
1127 for key, value in advpar:
1127 for key, value in advpar:
1128 parsizes.append(len(key))
1128 parsizes.append(len(key))
1129 parsizes.append(len(value))
1129 parsizes.append(len(value))
1130 paramsizes = _pack(_makefpartparamsizes(len(parsizes) // 2), *parsizes)
1130 paramsizes = _pack(_makefpartparamsizes(len(parsizes) // 2), *parsizes)
1131 header.append(paramsizes)
1131 header.append(paramsizes)
1132 # key, value
1132 # key, value
1133 for key, value in manpar:
1133 for key, value in manpar:
1134 header.append(key)
1134 header.append(key)
1135 header.append(value)
1135 header.append(value)
1136 for key, value in advpar:
1136 for key, value in advpar:
1137 header.append(key)
1137 header.append(key)
1138 header.append(value)
1138 header.append(value)
1139 ## finalize header
1139 ## finalize header
1140 try:
1140 try:
1141 headerchunk = b''.join(header)
1141 headerchunk = b''.join(header)
1142 except TypeError:
1142 except TypeError:
1143 raise TypeError(
1143 raise TypeError(
1144 r'Found a non-bytes trying to '
1144 r'Found a non-bytes trying to '
1145 r'build bundle part header: %r' % header
1145 r'build bundle part header: %r' % header
1146 )
1146 )
1147 outdebug(ui, b'header chunk size: %i' % len(headerchunk))
1147 outdebug(ui, b'header chunk size: %i' % len(headerchunk))
1148 yield _pack(_fpartheadersize, len(headerchunk))
1148 yield _pack(_fpartheadersize, len(headerchunk))
1149 yield headerchunk
1149 yield headerchunk
1150 ## payload
1150 ## payload
1151 try:
1151 try:
1152 for chunk in self._payloadchunks():
1152 for chunk in self._payloadchunks():
1153 outdebug(ui, b'payload chunk size: %i' % len(chunk))
1153 outdebug(ui, b'payload chunk size: %i' % len(chunk))
1154 yield _pack(_fpayloadsize, len(chunk))
1154 yield _pack(_fpayloadsize, len(chunk))
1155 yield chunk
1155 yield chunk
1156 except GeneratorExit:
1156 except GeneratorExit:
1157 # GeneratorExit means that nobody is listening for our
1157 # GeneratorExit means that nobody is listening for our
1158 # results anyway, so just bail quickly rather than trying
1158 # results anyway, so just bail quickly rather than trying
1159 # to produce an error part.
1159 # to produce an error part.
1160 ui.debug(b'bundle2-generatorexit\n')
1160 ui.debug(b'bundle2-generatorexit\n')
1161 raise
1161 raise
1162 except BaseException as exc:
1162 except BaseException as exc:
1163 bexc = stringutil.forcebytestr(exc)
1163 bexc = stringutil.forcebytestr(exc)
1164 # backup exception data for later
1164 # backup exception data for later
1165 ui.debug(
1165 ui.debug(
1166 b'bundle2-input-stream-interrupt: encoding exception %s' % bexc
1166 b'bundle2-input-stream-interrupt: encoding exception %s' % bexc
1167 )
1167 )
1168 tb = sys.exc_info()[2]
1168 tb = sys.exc_info()[2]
1169 msg = b'unexpected error: %s' % bexc
1169 msg = b'unexpected error: %s' % bexc
1170 interpart = bundlepart(
1170 interpart = bundlepart(
1171 b'error:abort', [(b'message', msg)], mandatory=False
1171 b'error:abort', [(b'message', msg)], mandatory=False
1172 )
1172 )
1173 interpart.id = 0
1173 interpart.id = 0
1174 yield _pack(_fpayloadsize, -1)
1174 yield _pack(_fpayloadsize, -1)
1175 for chunk in interpart.getchunks(ui=ui):
1175 for chunk in interpart.getchunks(ui=ui):
1176 yield chunk
1176 yield chunk
1177 outdebug(ui, b'closing payload chunk')
1177 outdebug(ui, b'closing payload chunk')
1178 # abort current part payload
1178 # abort current part payload
1179 yield _pack(_fpayloadsize, 0)
1179 yield _pack(_fpayloadsize, 0)
1180 pycompat.raisewithtb(exc, tb)
1180 pycompat.raisewithtb(exc, tb)
1181 # end of payload
1181 # end of payload
1182 outdebug(ui, b'closing payload chunk')
1182 outdebug(ui, b'closing payload chunk')
1183 yield _pack(_fpayloadsize, 0)
1183 yield _pack(_fpayloadsize, 0)
1184 self._generated = True
1184 self._generated = True
1185
1185
1186 def _payloadchunks(self):
1186 def _payloadchunks(self):
1187 """yield chunks of a the part payload
1187 """yield chunks of a the part payload
1188
1188
1189 Exists to handle the different methods to provide data to a part."""
1189 Exists to handle the different methods to provide data to a part."""
1190 # we only support fixed size data now.
1190 # we only support fixed size data now.
1191 # This will be improved in the future.
1191 # This will be improved in the future.
1192 if util.safehasattr(self.data, b'next') or util.safehasattr(
1192 if util.safehasattr(self.data, 'next') or util.safehasattr(
1193 self.data, b'__next__'
1193 self.data, b'__next__'
1194 ):
1194 ):
1195 buff = util.chunkbuffer(self.data)
1195 buff = util.chunkbuffer(self.data)
1196 chunk = buff.read(preferedchunksize)
1196 chunk = buff.read(preferedchunksize)
1197 while chunk:
1197 while chunk:
1198 yield chunk
1198 yield chunk
1199 chunk = buff.read(preferedchunksize)
1199 chunk = buff.read(preferedchunksize)
1200 elif len(self.data):
1200 elif len(self.data):
1201 yield self.data
1201 yield self.data
1202
1202
1203
1203
1204 flaginterrupt = -1
1204 flaginterrupt = -1
1205
1205
1206
1206
1207 class interrupthandler(unpackermixin):
1207 class interrupthandler(unpackermixin):
1208 """read one part and process it with restricted capability
1208 """read one part and process it with restricted capability
1209
1209
1210 This allows to transmit exception raised on the producer size during part
1210 This allows to transmit exception raised on the producer size during part
1211 iteration while the consumer is reading a part.
1211 iteration while the consumer is reading a part.
1212
1212
1213 Part processed in this manner only have access to a ui object,"""
1213 Part processed in this manner only have access to a ui object,"""
1214
1214
1215 def __init__(self, ui, fp):
1215 def __init__(self, ui, fp):
1216 super(interrupthandler, self).__init__(fp)
1216 super(interrupthandler, self).__init__(fp)
1217 self.ui = ui
1217 self.ui = ui
1218
1218
1219 def _readpartheader(self):
1219 def _readpartheader(self):
1220 """reads a part header size and return the bytes blob
1220 """reads a part header size and return the bytes blob
1221
1221
1222 returns None if empty"""
1222 returns None if empty"""
1223 headersize = self._unpack(_fpartheadersize)[0]
1223 headersize = self._unpack(_fpartheadersize)[0]
1224 if headersize < 0:
1224 if headersize < 0:
1225 raise error.BundleValueError(
1225 raise error.BundleValueError(
1226 b'negative part header size: %i' % headersize
1226 b'negative part header size: %i' % headersize
1227 )
1227 )
1228 indebug(self.ui, b'part header size: %i\n' % headersize)
1228 indebug(self.ui, b'part header size: %i\n' % headersize)
1229 if headersize:
1229 if headersize:
1230 return self._readexact(headersize)
1230 return self._readexact(headersize)
1231 return None
1231 return None
1232
1232
1233 def __call__(self):
1233 def __call__(self):
1234
1234
1235 self.ui.debug(
1235 self.ui.debug(
1236 b'bundle2-input-stream-interrupt:' b' opening out of band context\n'
1236 b'bundle2-input-stream-interrupt:' b' opening out of band context\n'
1237 )
1237 )
1238 indebug(self.ui, b'bundle2 stream interruption, looking for a part.')
1238 indebug(self.ui, b'bundle2 stream interruption, looking for a part.')
1239 headerblock = self._readpartheader()
1239 headerblock = self._readpartheader()
1240 if headerblock is None:
1240 if headerblock is None:
1241 indebug(self.ui, b'no part found during interruption.')
1241 indebug(self.ui, b'no part found during interruption.')
1242 return
1242 return
1243 part = unbundlepart(self.ui, headerblock, self._fp)
1243 part = unbundlepart(self.ui, headerblock, self._fp)
1244 op = interruptoperation(self.ui)
1244 op = interruptoperation(self.ui)
1245 hardabort = False
1245 hardabort = False
1246 try:
1246 try:
1247 _processpart(op, part)
1247 _processpart(op, part)
1248 except (SystemExit, KeyboardInterrupt):
1248 except (SystemExit, KeyboardInterrupt):
1249 hardabort = True
1249 hardabort = True
1250 raise
1250 raise
1251 finally:
1251 finally:
1252 if not hardabort:
1252 if not hardabort:
1253 part.consume()
1253 part.consume()
1254 self.ui.debug(
1254 self.ui.debug(
1255 b'bundle2-input-stream-interrupt:' b' closing out of band context\n'
1255 b'bundle2-input-stream-interrupt:' b' closing out of band context\n'
1256 )
1256 )
1257
1257
1258
1258
1259 class interruptoperation(object):
1259 class interruptoperation(object):
1260 """A limited operation to be use by part handler during interruption
1260 """A limited operation to be use by part handler during interruption
1261
1261
1262 It only have access to an ui object.
1262 It only have access to an ui object.
1263 """
1263 """
1264
1264
1265 def __init__(self, ui):
1265 def __init__(self, ui):
1266 self.ui = ui
1266 self.ui = ui
1267 self.reply = None
1267 self.reply = None
1268 self.captureoutput = False
1268 self.captureoutput = False
1269
1269
1270 @property
1270 @property
1271 def repo(self):
1271 def repo(self):
1272 raise error.ProgrammingError(b'no repo access from stream interruption')
1272 raise error.ProgrammingError(b'no repo access from stream interruption')
1273
1273
1274 def gettransaction(self):
1274 def gettransaction(self):
1275 raise TransactionUnavailable(b'no repo access from stream interruption')
1275 raise TransactionUnavailable(b'no repo access from stream interruption')
1276
1276
1277
1277
1278 def decodepayloadchunks(ui, fh):
1278 def decodepayloadchunks(ui, fh):
1279 """Reads bundle2 part payload data into chunks.
1279 """Reads bundle2 part payload data into chunks.
1280
1280
1281 Part payload data consists of framed chunks. This function takes
1281 Part payload data consists of framed chunks. This function takes
1282 a file handle and emits those chunks.
1282 a file handle and emits those chunks.
1283 """
1283 """
1284 dolog = ui.configbool(b'devel', b'bundle2.debug')
1284 dolog = ui.configbool(b'devel', b'bundle2.debug')
1285 debug = ui.debug
1285 debug = ui.debug
1286
1286
1287 headerstruct = struct.Struct(_fpayloadsize)
1287 headerstruct = struct.Struct(_fpayloadsize)
1288 headersize = headerstruct.size
1288 headersize = headerstruct.size
1289 unpack = headerstruct.unpack
1289 unpack = headerstruct.unpack
1290
1290
1291 readexactly = changegroup.readexactly
1291 readexactly = changegroup.readexactly
1292 read = fh.read
1292 read = fh.read
1293
1293
1294 chunksize = unpack(readexactly(fh, headersize))[0]
1294 chunksize = unpack(readexactly(fh, headersize))[0]
1295 indebug(ui, b'payload chunk size: %i' % chunksize)
1295 indebug(ui, b'payload chunk size: %i' % chunksize)
1296
1296
1297 # changegroup.readexactly() is inlined below for performance.
1297 # changegroup.readexactly() is inlined below for performance.
1298 while chunksize:
1298 while chunksize:
1299 if chunksize >= 0:
1299 if chunksize >= 0:
1300 s = read(chunksize)
1300 s = read(chunksize)
1301 if len(s) < chunksize:
1301 if len(s) < chunksize:
1302 raise error.Abort(
1302 raise error.Abort(
1303 _(
1303 _(
1304 b'stream ended unexpectedly '
1304 b'stream ended unexpectedly '
1305 b' (got %d bytes, expected %d)'
1305 b' (got %d bytes, expected %d)'
1306 )
1306 )
1307 % (len(s), chunksize)
1307 % (len(s), chunksize)
1308 )
1308 )
1309
1309
1310 yield s
1310 yield s
1311 elif chunksize == flaginterrupt:
1311 elif chunksize == flaginterrupt:
1312 # Interrupt "signal" detected. The regular stream is interrupted
1312 # Interrupt "signal" detected. The regular stream is interrupted
1313 # and a bundle2 part follows. Consume it.
1313 # and a bundle2 part follows. Consume it.
1314 interrupthandler(ui, fh)()
1314 interrupthandler(ui, fh)()
1315 else:
1315 else:
1316 raise error.BundleValueError(
1316 raise error.BundleValueError(
1317 b'negative payload chunk size: %s' % chunksize
1317 b'negative payload chunk size: %s' % chunksize
1318 )
1318 )
1319
1319
1320 s = read(headersize)
1320 s = read(headersize)
1321 if len(s) < headersize:
1321 if len(s) < headersize:
1322 raise error.Abort(
1322 raise error.Abort(
1323 _(b'stream ended unexpectedly ' b' (got %d bytes, expected %d)')
1323 _(b'stream ended unexpectedly ' b' (got %d bytes, expected %d)')
1324 % (len(s), chunksize)
1324 % (len(s), chunksize)
1325 )
1325 )
1326
1326
1327 chunksize = unpack(s)[0]
1327 chunksize = unpack(s)[0]
1328
1328
1329 # indebug() inlined for performance.
1329 # indebug() inlined for performance.
1330 if dolog:
1330 if dolog:
1331 debug(b'bundle2-input: payload chunk size: %i\n' % chunksize)
1331 debug(b'bundle2-input: payload chunk size: %i\n' % chunksize)
1332
1332
1333
1333
1334 class unbundlepart(unpackermixin):
1334 class unbundlepart(unpackermixin):
1335 """a bundle part read from a bundle"""
1335 """a bundle part read from a bundle"""
1336
1336
1337 def __init__(self, ui, header, fp):
1337 def __init__(self, ui, header, fp):
1338 super(unbundlepart, self).__init__(fp)
1338 super(unbundlepart, self).__init__(fp)
1339 self._seekable = util.safehasattr(fp, b'seek') and util.safehasattr(
1339 self._seekable = util.safehasattr(fp, 'seek') and util.safehasattr(
1340 fp, b'tell'
1340 fp, b'tell'
1341 )
1341 )
1342 self.ui = ui
1342 self.ui = ui
1343 # unbundle state attr
1343 # unbundle state attr
1344 self._headerdata = header
1344 self._headerdata = header
1345 self._headeroffset = 0
1345 self._headeroffset = 0
1346 self._initialized = False
1346 self._initialized = False
1347 self.consumed = False
1347 self.consumed = False
1348 # part data
1348 # part data
1349 self.id = None
1349 self.id = None
1350 self.type = None
1350 self.type = None
1351 self.mandatoryparams = None
1351 self.mandatoryparams = None
1352 self.advisoryparams = None
1352 self.advisoryparams = None
1353 self.params = None
1353 self.params = None
1354 self.mandatorykeys = ()
1354 self.mandatorykeys = ()
1355 self._readheader()
1355 self._readheader()
1356 self._mandatory = None
1356 self._mandatory = None
1357 self._pos = 0
1357 self._pos = 0
1358
1358
1359 def _fromheader(self, size):
1359 def _fromheader(self, size):
1360 """return the next <size> byte from the header"""
1360 """return the next <size> byte from the header"""
1361 offset = self._headeroffset
1361 offset = self._headeroffset
1362 data = self._headerdata[offset : (offset + size)]
1362 data = self._headerdata[offset : (offset + size)]
1363 self._headeroffset = offset + size
1363 self._headeroffset = offset + size
1364 return data
1364 return data
1365
1365
1366 def _unpackheader(self, format):
1366 def _unpackheader(self, format):
1367 """read given format from header
1367 """read given format from header
1368
1368
1369 This automatically compute the size of the format to read."""
1369 This automatically compute the size of the format to read."""
1370 data = self._fromheader(struct.calcsize(format))
1370 data = self._fromheader(struct.calcsize(format))
1371 return _unpack(format, data)
1371 return _unpack(format, data)
1372
1372
1373 def _initparams(self, mandatoryparams, advisoryparams):
1373 def _initparams(self, mandatoryparams, advisoryparams):
1374 """internal function to setup all logic related parameters"""
1374 """internal function to setup all logic related parameters"""
1375 # make it read only to prevent people touching it by mistake.
1375 # make it read only to prevent people touching it by mistake.
1376 self.mandatoryparams = tuple(mandatoryparams)
1376 self.mandatoryparams = tuple(mandatoryparams)
1377 self.advisoryparams = tuple(advisoryparams)
1377 self.advisoryparams = tuple(advisoryparams)
1378 # user friendly UI
1378 # user friendly UI
1379 self.params = util.sortdict(self.mandatoryparams)
1379 self.params = util.sortdict(self.mandatoryparams)
1380 self.params.update(self.advisoryparams)
1380 self.params.update(self.advisoryparams)
1381 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1381 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1382
1382
1383 def _readheader(self):
1383 def _readheader(self):
1384 """read the header and setup the object"""
1384 """read the header and setup the object"""
1385 typesize = self._unpackheader(_fparttypesize)[0]
1385 typesize = self._unpackheader(_fparttypesize)[0]
1386 self.type = self._fromheader(typesize)
1386 self.type = self._fromheader(typesize)
1387 indebug(self.ui, b'part type: "%s"' % self.type)
1387 indebug(self.ui, b'part type: "%s"' % self.type)
1388 self.id = self._unpackheader(_fpartid)[0]
1388 self.id = self._unpackheader(_fpartid)[0]
1389 indebug(self.ui, b'part id: "%s"' % pycompat.bytestr(self.id))
1389 indebug(self.ui, b'part id: "%s"' % pycompat.bytestr(self.id))
1390 # extract mandatory bit from type
1390 # extract mandatory bit from type
1391 self.mandatory = self.type != self.type.lower()
1391 self.mandatory = self.type != self.type.lower()
1392 self.type = self.type.lower()
1392 self.type = self.type.lower()
1393 ## reading parameters
1393 ## reading parameters
1394 # param count
1394 # param count
1395 mancount, advcount = self._unpackheader(_fpartparamcount)
1395 mancount, advcount = self._unpackheader(_fpartparamcount)
1396 indebug(self.ui, b'part parameters: %i' % (mancount + advcount))
1396 indebug(self.ui, b'part parameters: %i' % (mancount + advcount))
1397 # param size
1397 # param size
1398 fparamsizes = _makefpartparamsizes(mancount + advcount)
1398 fparamsizes = _makefpartparamsizes(mancount + advcount)
1399 paramsizes = self._unpackheader(fparamsizes)
1399 paramsizes = self._unpackheader(fparamsizes)
1400 # make it a list of couple again
1400 # make it a list of couple again
1401 paramsizes = list(zip(paramsizes[::2], paramsizes[1::2]))
1401 paramsizes = list(zip(paramsizes[::2], paramsizes[1::2]))
1402 # split mandatory from advisory
1402 # split mandatory from advisory
1403 mansizes = paramsizes[:mancount]
1403 mansizes = paramsizes[:mancount]
1404 advsizes = paramsizes[mancount:]
1404 advsizes = paramsizes[mancount:]
1405 # retrieve param value
1405 # retrieve param value
1406 manparams = []
1406 manparams = []
1407 for key, value in mansizes:
1407 for key, value in mansizes:
1408 manparams.append((self._fromheader(key), self._fromheader(value)))
1408 manparams.append((self._fromheader(key), self._fromheader(value)))
1409 advparams = []
1409 advparams = []
1410 for key, value in advsizes:
1410 for key, value in advsizes:
1411 advparams.append((self._fromheader(key), self._fromheader(value)))
1411 advparams.append((self._fromheader(key), self._fromheader(value)))
1412 self._initparams(manparams, advparams)
1412 self._initparams(manparams, advparams)
1413 ## part payload
1413 ## part payload
1414 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1414 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1415 # we read the data, tell it
1415 # we read the data, tell it
1416 self._initialized = True
1416 self._initialized = True
1417
1417
1418 def _payloadchunks(self):
1418 def _payloadchunks(self):
1419 """Generator of decoded chunks in the payload."""
1419 """Generator of decoded chunks in the payload."""
1420 return decodepayloadchunks(self.ui, self._fp)
1420 return decodepayloadchunks(self.ui, self._fp)
1421
1421
1422 def consume(self):
1422 def consume(self):
1423 """Read the part payload until completion.
1423 """Read the part payload until completion.
1424
1424
1425 By consuming the part data, the underlying stream read offset will
1425 By consuming the part data, the underlying stream read offset will
1426 be advanced to the next part (or end of stream).
1426 be advanced to the next part (or end of stream).
1427 """
1427 """
1428 if self.consumed:
1428 if self.consumed:
1429 return
1429 return
1430
1430
1431 chunk = self.read(32768)
1431 chunk = self.read(32768)
1432 while chunk:
1432 while chunk:
1433 self._pos += len(chunk)
1433 self._pos += len(chunk)
1434 chunk = self.read(32768)
1434 chunk = self.read(32768)
1435
1435
1436 def read(self, size=None):
1436 def read(self, size=None):
1437 """read payload data"""
1437 """read payload data"""
1438 if not self._initialized:
1438 if not self._initialized:
1439 self._readheader()
1439 self._readheader()
1440 if size is None:
1440 if size is None:
1441 data = self._payloadstream.read()
1441 data = self._payloadstream.read()
1442 else:
1442 else:
1443 data = self._payloadstream.read(size)
1443 data = self._payloadstream.read(size)
1444 self._pos += len(data)
1444 self._pos += len(data)
1445 if size is None or len(data) < size:
1445 if size is None or len(data) < size:
1446 if not self.consumed and self._pos:
1446 if not self.consumed and self._pos:
1447 self.ui.debug(
1447 self.ui.debug(
1448 b'bundle2-input-part: total payload size %i\n' % self._pos
1448 b'bundle2-input-part: total payload size %i\n' % self._pos
1449 )
1449 )
1450 self.consumed = True
1450 self.consumed = True
1451 return data
1451 return data
1452
1452
1453
1453
1454 class seekableunbundlepart(unbundlepart):
1454 class seekableunbundlepart(unbundlepart):
1455 """A bundle2 part in a bundle that is seekable.
1455 """A bundle2 part in a bundle that is seekable.
1456
1456
1457 Regular ``unbundlepart`` instances can only be read once. This class
1457 Regular ``unbundlepart`` instances can only be read once. This class
1458 extends ``unbundlepart`` to enable bi-directional seeking within the
1458 extends ``unbundlepart`` to enable bi-directional seeking within the
1459 part.
1459 part.
1460
1460
1461 Bundle2 part data consists of framed chunks. Offsets when seeking
1461 Bundle2 part data consists of framed chunks. Offsets when seeking
1462 refer to the decoded data, not the offsets in the underlying bundle2
1462 refer to the decoded data, not the offsets in the underlying bundle2
1463 stream.
1463 stream.
1464
1464
1465 To facilitate quickly seeking within the decoded data, instances of this
1465 To facilitate quickly seeking within the decoded data, instances of this
1466 class maintain a mapping between offsets in the underlying stream and
1466 class maintain a mapping between offsets in the underlying stream and
1467 the decoded payload. This mapping will consume memory in proportion
1467 the decoded payload. This mapping will consume memory in proportion
1468 to the number of chunks within the payload (which almost certainly
1468 to the number of chunks within the payload (which almost certainly
1469 increases in proportion with the size of the part).
1469 increases in proportion with the size of the part).
1470 """
1470 """
1471
1471
1472 def __init__(self, ui, header, fp):
1472 def __init__(self, ui, header, fp):
1473 # (payload, file) offsets for chunk starts.
1473 # (payload, file) offsets for chunk starts.
1474 self._chunkindex = []
1474 self._chunkindex = []
1475
1475
1476 super(seekableunbundlepart, self).__init__(ui, header, fp)
1476 super(seekableunbundlepart, self).__init__(ui, header, fp)
1477
1477
1478 def _payloadchunks(self, chunknum=0):
1478 def _payloadchunks(self, chunknum=0):
1479 '''seek to specified chunk and start yielding data'''
1479 '''seek to specified chunk and start yielding data'''
1480 if len(self._chunkindex) == 0:
1480 if len(self._chunkindex) == 0:
1481 assert chunknum == 0, b'Must start with chunk 0'
1481 assert chunknum == 0, b'Must start with chunk 0'
1482 self._chunkindex.append((0, self._tellfp()))
1482 self._chunkindex.append((0, self._tellfp()))
1483 else:
1483 else:
1484 assert chunknum < len(self._chunkindex), (
1484 assert chunknum < len(self._chunkindex), (
1485 b'Unknown chunk %d' % chunknum
1485 b'Unknown chunk %d' % chunknum
1486 )
1486 )
1487 self._seekfp(self._chunkindex[chunknum][1])
1487 self._seekfp(self._chunkindex[chunknum][1])
1488
1488
1489 pos = self._chunkindex[chunknum][0]
1489 pos = self._chunkindex[chunknum][0]
1490
1490
1491 for chunk in decodepayloadchunks(self.ui, self._fp):
1491 for chunk in decodepayloadchunks(self.ui, self._fp):
1492 chunknum += 1
1492 chunknum += 1
1493 pos += len(chunk)
1493 pos += len(chunk)
1494 if chunknum == len(self._chunkindex):
1494 if chunknum == len(self._chunkindex):
1495 self._chunkindex.append((pos, self._tellfp()))
1495 self._chunkindex.append((pos, self._tellfp()))
1496
1496
1497 yield chunk
1497 yield chunk
1498
1498
1499 def _findchunk(self, pos):
1499 def _findchunk(self, pos):
1500 '''for a given payload position, return a chunk number and offset'''
1500 '''for a given payload position, return a chunk number and offset'''
1501 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1501 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1502 if ppos == pos:
1502 if ppos == pos:
1503 return chunk, 0
1503 return chunk, 0
1504 elif ppos > pos:
1504 elif ppos > pos:
1505 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1505 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1506 raise ValueError(b'Unknown chunk')
1506 raise ValueError(b'Unknown chunk')
1507
1507
1508 def tell(self):
1508 def tell(self):
1509 return self._pos
1509 return self._pos
1510
1510
1511 def seek(self, offset, whence=os.SEEK_SET):
1511 def seek(self, offset, whence=os.SEEK_SET):
1512 if whence == os.SEEK_SET:
1512 if whence == os.SEEK_SET:
1513 newpos = offset
1513 newpos = offset
1514 elif whence == os.SEEK_CUR:
1514 elif whence == os.SEEK_CUR:
1515 newpos = self._pos + offset
1515 newpos = self._pos + offset
1516 elif whence == os.SEEK_END:
1516 elif whence == os.SEEK_END:
1517 if not self.consumed:
1517 if not self.consumed:
1518 # Can't use self.consume() here because it advances self._pos.
1518 # Can't use self.consume() here because it advances self._pos.
1519 chunk = self.read(32768)
1519 chunk = self.read(32768)
1520 while chunk:
1520 while chunk:
1521 chunk = self.read(32768)
1521 chunk = self.read(32768)
1522 newpos = self._chunkindex[-1][0] - offset
1522 newpos = self._chunkindex[-1][0] - offset
1523 else:
1523 else:
1524 raise ValueError(b'Unknown whence value: %r' % (whence,))
1524 raise ValueError(b'Unknown whence value: %r' % (whence,))
1525
1525
1526 if newpos > self._chunkindex[-1][0] and not self.consumed:
1526 if newpos > self._chunkindex[-1][0] and not self.consumed:
1527 # Can't use self.consume() here because it advances self._pos.
1527 # Can't use self.consume() here because it advances self._pos.
1528 chunk = self.read(32768)
1528 chunk = self.read(32768)
1529 while chunk:
1529 while chunk:
1530 chunk = self.read(32668)
1530 chunk = self.read(32668)
1531
1531
1532 if not 0 <= newpos <= self._chunkindex[-1][0]:
1532 if not 0 <= newpos <= self._chunkindex[-1][0]:
1533 raise ValueError(b'Offset out of range')
1533 raise ValueError(b'Offset out of range')
1534
1534
1535 if self._pos != newpos:
1535 if self._pos != newpos:
1536 chunk, internaloffset = self._findchunk(newpos)
1536 chunk, internaloffset = self._findchunk(newpos)
1537 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1537 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1538 adjust = self.read(internaloffset)
1538 adjust = self.read(internaloffset)
1539 if len(adjust) != internaloffset:
1539 if len(adjust) != internaloffset:
1540 raise error.Abort(_(b'Seek failed\n'))
1540 raise error.Abort(_(b'Seek failed\n'))
1541 self._pos = newpos
1541 self._pos = newpos
1542
1542
1543 def _seekfp(self, offset, whence=0):
1543 def _seekfp(self, offset, whence=0):
1544 """move the underlying file pointer
1544 """move the underlying file pointer
1545
1545
1546 This method is meant for internal usage by the bundle2 protocol only.
1546 This method is meant for internal usage by the bundle2 protocol only.
1547 They directly manipulate the low level stream including bundle2 level
1547 They directly manipulate the low level stream including bundle2 level
1548 instruction.
1548 instruction.
1549
1549
1550 Do not use it to implement higher-level logic or methods."""
1550 Do not use it to implement higher-level logic or methods."""
1551 if self._seekable:
1551 if self._seekable:
1552 return self._fp.seek(offset, whence)
1552 return self._fp.seek(offset, whence)
1553 else:
1553 else:
1554 raise NotImplementedError(_(b'File pointer is not seekable'))
1554 raise NotImplementedError(_(b'File pointer is not seekable'))
1555
1555
1556 def _tellfp(self):
1556 def _tellfp(self):
1557 """return the file offset, or None if file is not seekable
1557 """return the file offset, or None if file is not seekable
1558
1558
1559 This method is meant for internal usage by the bundle2 protocol only.
1559 This method is meant for internal usage by the bundle2 protocol only.
1560 They directly manipulate the low level stream including bundle2 level
1560 They directly manipulate the low level stream including bundle2 level
1561 instruction.
1561 instruction.
1562
1562
1563 Do not use it to implement higher-level logic or methods."""
1563 Do not use it to implement higher-level logic or methods."""
1564 if self._seekable:
1564 if self._seekable:
1565 try:
1565 try:
1566 return self._fp.tell()
1566 return self._fp.tell()
1567 except IOError as e:
1567 except IOError as e:
1568 if e.errno == errno.ESPIPE:
1568 if e.errno == errno.ESPIPE:
1569 self._seekable = False
1569 self._seekable = False
1570 else:
1570 else:
1571 raise
1571 raise
1572 return None
1572 return None
1573
1573
1574
1574
1575 # These are only the static capabilities.
1575 # These are only the static capabilities.
1576 # Check the 'getrepocaps' function for the rest.
1576 # Check the 'getrepocaps' function for the rest.
1577 capabilities = {
1577 capabilities = {
1578 b'HG20': (),
1578 b'HG20': (),
1579 b'bookmarks': (),
1579 b'bookmarks': (),
1580 b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'),
1580 b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'),
1581 b'listkeys': (),
1581 b'listkeys': (),
1582 b'pushkey': (),
1582 b'pushkey': (),
1583 b'digests': tuple(sorted(util.DIGESTS.keys())),
1583 b'digests': tuple(sorted(util.DIGESTS.keys())),
1584 b'remote-changegroup': (b'http', b'https'),
1584 b'remote-changegroup': (b'http', b'https'),
1585 b'hgtagsfnodes': (),
1585 b'hgtagsfnodes': (),
1586 b'rev-branch-cache': (),
1586 b'rev-branch-cache': (),
1587 b'phases': (b'heads',),
1587 b'phases': (b'heads',),
1588 b'stream': (b'v2',),
1588 b'stream': (b'v2',),
1589 }
1589 }
1590
1590
1591
1591
1592 def getrepocaps(repo, allowpushback=False, role=None):
1592 def getrepocaps(repo, allowpushback=False, role=None):
1593 """return the bundle2 capabilities for a given repo
1593 """return the bundle2 capabilities for a given repo
1594
1594
1595 Exists to allow extensions (like evolution) to mutate the capabilities.
1595 Exists to allow extensions (like evolution) to mutate the capabilities.
1596
1596
1597 The returned value is used for servers advertising their capabilities as
1597 The returned value is used for servers advertising their capabilities as
1598 well as clients advertising their capabilities to servers as part of
1598 well as clients advertising their capabilities to servers as part of
1599 bundle2 requests. The ``role`` argument specifies which is which.
1599 bundle2 requests. The ``role`` argument specifies which is which.
1600 """
1600 """
1601 if role not in (b'client', b'server'):
1601 if role not in (b'client', b'server'):
1602 raise error.ProgrammingError(b'role argument must be client or server')
1602 raise error.ProgrammingError(b'role argument must be client or server')
1603
1603
1604 caps = capabilities.copy()
1604 caps = capabilities.copy()
1605 caps[b'changegroup'] = tuple(
1605 caps[b'changegroup'] = tuple(
1606 sorted(changegroup.supportedincomingversions(repo))
1606 sorted(changegroup.supportedincomingversions(repo))
1607 )
1607 )
1608 if obsolete.isenabled(repo, obsolete.exchangeopt):
1608 if obsolete.isenabled(repo, obsolete.exchangeopt):
1609 supportedformat = tuple(b'V%i' % v for v in obsolete.formats)
1609 supportedformat = tuple(b'V%i' % v for v in obsolete.formats)
1610 caps[b'obsmarkers'] = supportedformat
1610 caps[b'obsmarkers'] = supportedformat
1611 if allowpushback:
1611 if allowpushback:
1612 caps[b'pushback'] = ()
1612 caps[b'pushback'] = ()
1613 cpmode = repo.ui.config(b'server', b'concurrent-push-mode')
1613 cpmode = repo.ui.config(b'server', b'concurrent-push-mode')
1614 if cpmode == b'check-related':
1614 if cpmode == b'check-related':
1615 caps[b'checkheads'] = (b'related',)
1615 caps[b'checkheads'] = (b'related',)
1616 if b'phases' in repo.ui.configlist(b'devel', b'legacy.exchange'):
1616 if b'phases' in repo.ui.configlist(b'devel', b'legacy.exchange'):
1617 caps.pop(b'phases')
1617 caps.pop(b'phases')
1618
1618
1619 # Don't advertise stream clone support in server mode if not configured.
1619 # Don't advertise stream clone support in server mode if not configured.
1620 if role == b'server':
1620 if role == b'server':
1621 streamsupported = repo.ui.configbool(
1621 streamsupported = repo.ui.configbool(
1622 b'server', b'uncompressed', untrusted=True
1622 b'server', b'uncompressed', untrusted=True
1623 )
1623 )
1624 featuresupported = repo.ui.configbool(b'server', b'bundle2.stream')
1624 featuresupported = repo.ui.configbool(b'server', b'bundle2.stream')
1625
1625
1626 if not streamsupported or not featuresupported:
1626 if not streamsupported or not featuresupported:
1627 caps.pop(b'stream')
1627 caps.pop(b'stream')
1628 # Else always advertise support on client, because payload support
1628 # Else always advertise support on client, because payload support
1629 # should always be advertised.
1629 # should always be advertised.
1630
1630
1631 return caps
1631 return caps
1632
1632
1633
1633
1634 def bundle2caps(remote):
1634 def bundle2caps(remote):
1635 """return the bundle capabilities of a peer as dict"""
1635 """return the bundle capabilities of a peer as dict"""
1636 raw = remote.capable(b'bundle2')
1636 raw = remote.capable(b'bundle2')
1637 if not raw and raw != b'':
1637 if not raw and raw != b'':
1638 return {}
1638 return {}
1639 capsblob = urlreq.unquote(remote.capable(b'bundle2'))
1639 capsblob = urlreq.unquote(remote.capable(b'bundle2'))
1640 return decodecaps(capsblob)
1640 return decodecaps(capsblob)
1641
1641
1642
1642
1643 def obsmarkersversion(caps):
1643 def obsmarkersversion(caps):
1644 """extract the list of supported obsmarkers versions from a bundle2caps dict
1644 """extract the list of supported obsmarkers versions from a bundle2caps dict
1645 """
1645 """
1646 obscaps = caps.get(b'obsmarkers', ())
1646 obscaps = caps.get(b'obsmarkers', ())
1647 return [int(c[1:]) for c in obscaps if c.startswith(b'V')]
1647 return [int(c[1:]) for c in obscaps if c.startswith(b'V')]
1648
1648
1649
1649
1650 def writenewbundle(
1650 def writenewbundle(
1651 ui,
1651 ui,
1652 repo,
1652 repo,
1653 source,
1653 source,
1654 filename,
1654 filename,
1655 bundletype,
1655 bundletype,
1656 outgoing,
1656 outgoing,
1657 opts,
1657 opts,
1658 vfs=None,
1658 vfs=None,
1659 compression=None,
1659 compression=None,
1660 compopts=None,
1660 compopts=None,
1661 ):
1661 ):
1662 if bundletype.startswith(b'HG10'):
1662 if bundletype.startswith(b'HG10'):
1663 cg = changegroup.makechangegroup(repo, outgoing, b'01', source)
1663 cg = changegroup.makechangegroup(repo, outgoing, b'01', source)
1664 return writebundle(
1664 return writebundle(
1665 ui,
1665 ui,
1666 cg,
1666 cg,
1667 filename,
1667 filename,
1668 bundletype,
1668 bundletype,
1669 vfs=vfs,
1669 vfs=vfs,
1670 compression=compression,
1670 compression=compression,
1671 compopts=compopts,
1671 compopts=compopts,
1672 )
1672 )
1673 elif not bundletype.startswith(b'HG20'):
1673 elif not bundletype.startswith(b'HG20'):
1674 raise error.ProgrammingError(b'unknown bundle type: %s' % bundletype)
1674 raise error.ProgrammingError(b'unknown bundle type: %s' % bundletype)
1675
1675
1676 caps = {}
1676 caps = {}
1677 if b'obsolescence' in opts:
1677 if b'obsolescence' in opts:
1678 caps[b'obsmarkers'] = (b'V1',)
1678 caps[b'obsmarkers'] = (b'V1',)
1679 bundle = bundle20(ui, caps)
1679 bundle = bundle20(ui, caps)
1680 bundle.setcompression(compression, compopts)
1680 bundle.setcompression(compression, compopts)
1681 _addpartsfromopts(ui, repo, bundle, source, outgoing, opts)
1681 _addpartsfromopts(ui, repo, bundle, source, outgoing, opts)
1682 chunkiter = bundle.getchunks()
1682 chunkiter = bundle.getchunks()
1683
1683
1684 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1684 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1685
1685
1686
1686
1687 def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts):
1687 def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts):
1688 # We should eventually reconcile this logic with the one behind
1688 # We should eventually reconcile this logic with the one behind
1689 # 'exchange.getbundle2partsgenerator'.
1689 # 'exchange.getbundle2partsgenerator'.
1690 #
1690 #
1691 # The type of input from 'getbundle' and 'writenewbundle' are a bit
1691 # The type of input from 'getbundle' and 'writenewbundle' are a bit
1692 # different right now. So we keep them separated for now for the sake of
1692 # different right now. So we keep them separated for now for the sake of
1693 # simplicity.
1693 # simplicity.
1694
1694
1695 # we might not always want a changegroup in such bundle, for example in
1695 # we might not always want a changegroup in such bundle, for example in
1696 # stream bundles
1696 # stream bundles
1697 if opts.get(b'changegroup', True):
1697 if opts.get(b'changegroup', True):
1698 cgversion = opts.get(b'cg.version')
1698 cgversion = opts.get(b'cg.version')
1699 if cgversion is None:
1699 if cgversion is None:
1700 cgversion = changegroup.safeversion(repo)
1700 cgversion = changegroup.safeversion(repo)
1701 cg = changegroup.makechangegroup(repo, outgoing, cgversion, source)
1701 cg = changegroup.makechangegroup(repo, outgoing, cgversion, source)
1702 part = bundler.newpart(b'changegroup', data=cg.getchunks())
1702 part = bundler.newpart(b'changegroup', data=cg.getchunks())
1703 part.addparam(b'version', cg.version)
1703 part.addparam(b'version', cg.version)
1704 if b'clcount' in cg.extras:
1704 if b'clcount' in cg.extras:
1705 part.addparam(
1705 part.addparam(
1706 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1706 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1707 )
1707 )
1708 if opts.get(b'phases') and repo.revs(
1708 if opts.get(b'phases') and repo.revs(
1709 b'%ln and secret()', outgoing.missingheads
1709 b'%ln and secret()', outgoing.missingheads
1710 ):
1710 ):
1711 part.addparam(
1711 part.addparam(
1712 b'targetphase', b'%d' % phases.secret, mandatory=False
1712 b'targetphase', b'%d' % phases.secret, mandatory=False
1713 )
1713 )
1714
1714
1715 if opts.get(b'streamv2', False):
1715 if opts.get(b'streamv2', False):
1716 addpartbundlestream2(bundler, repo, stream=True)
1716 addpartbundlestream2(bundler, repo, stream=True)
1717
1717
1718 if opts.get(b'tagsfnodescache', True):
1718 if opts.get(b'tagsfnodescache', True):
1719 addparttagsfnodescache(repo, bundler, outgoing)
1719 addparttagsfnodescache(repo, bundler, outgoing)
1720
1720
1721 if opts.get(b'revbranchcache', True):
1721 if opts.get(b'revbranchcache', True):
1722 addpartrevbranchcache(repo, bundler, outgoing)
1722 addpartrevbranchcache(repo, bundler, outgoing)
1723
1723
1724 if opts.get(b'obsolescence', False):
1724 if opts.get(b'obsolescence', False):
1725 obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
1725 obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing)
1726 buildobsmarkerspart(bundler, obsmarkers)
1726 buildobsmarkerspart(bundler, obsmarkers)
1727
1727
1728 if opts.get(b'phases', False):
1728 if opts.get(b'phases', False):
1729 headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
1729 headsbyphase = phases.subsetphaseheads(repo, outgoing.missing)
1730 phasedata = phases.binaryencode(headsbyphase)
1730 phasedata = phases.binaryencode(headsbyphase)
1731 bundler.newpart(b'phase-heads', data=phasedata)
1731 bundler.newpart(b'phase-heads', data=phasedata)
1732
1732
1733
1733
1734 def addparttagsfnodescache(repo, bundler, outgoing):
1734 def addparttagsfnodescache(repo, bundler, outgoing):
1735 # we include the tags fnode cache for the bundle changeset
1735 # we include the tags fnode cache for the bundle changeset
1736 # (as an optional parts)
1736 # (as an optional parts)
1737 cache = tags.hgtagsfnodescache(repo.unfiltered())
1737 cache = tags.hgtagsfnodescache(repo.unfiltered())
1738 chunks = []
1738 chunks = []
1739
1739
1740 # .hgtags fnodes are only relevant for head changesets. While we could
1740 # .hgtags fnodes are only relevant for head changesets. While we could
1741 # transfer values for all known nodes, there will likely be little to
1741 # transfer values for all known nodes, there will likely be little to
1742 # no benefit.
1742 # no benefit.
1743 #
1743 #
1744 # We don't bother using a generator to produce output data because
1744 # We don't bother using a generator to produce output data because
1745 # a) we only have 40 bytes per head and even esoteric numbers of heads
1745 # a) we only have 40 bytes per head and even esoteric numbers of heads
1746 # consume little memory (1M heads is 40MB) b) we don't want to send the
1746 # consume little memory (1M heads is 40MB) b) we don't want to send the
1747 # part if we don't have entries and knowing if we have entries requires
1747 # part if we don't have entries and knowing if we have entries requires
1748 # cache lookups.
1748 # cache lookups.
1749 for node in outgoing.missingheads:
1749 for node in outgoing.missingheads:
1750 # Don't compute missing, as this may slow down serving.
1750 # Don't compute missing, as this may slow down serving.
1751 fnode = cache.getfnode(node, computemissing=False)
1751 fnode = cache.getfnode(node, computemissing=False)
1752 if fnode is not None:
1752 if fnode is not None:
1753 chunks.extend([node, fnode])
1753 chunks.extend([node, fnode])
1754
1754
1755 if chunks:
1755 if chunks:
1756 bundler.newpart(b'hgtagsfnodes', data=b''.join(chunks))
1756 bundler.newpart(b'hgtagsfnodes', data=b''.join(chunks))
1757
1757
1758
1758
1759 def addpartrevbranchcache(repo, bundler, outgoing):
1759 def addpartrevbranchcache(repo, bundler, outgoing):
1760 # we include the rev branch cache for the bundle changeset
1760 # we include the rev branch cache for the bundle changeset
1761 # (as an optional parts)
1761 # (as an optional parts)
1762 cache = repo.revbranchcache()
1762 cache = repo.revbranchcache()
1763 cl = repo.unfiltered().changelog
1763 cl = repo.unfiltered().changelog
1764 branchesdata = collections.defaultdict(lambda: (set(), set()))
1764 branchesdata = collections.defaultdict(lambda: (set(), set()))
1765 for node in outgoing.missing:
1765 for node in outgoing.missing:
1766 branch, close = cache.branchinfo(cl.rev(node))
1766 branch, close = cache.branchinfo(cl.rev(node))
1767 branchesdata[branch][close].add(node)
1767 branchesdata[branch][close].add(node)
1768
1768
1769 def generate():
1769 def generate():
1770 for branch, (nodes, closed) in sorted(branchesdata.items()):
1770 for branch, (nodes, closed) in sorted(branchesdata.items()):
1771 utf8branch = encoding.fromlocal(branch)
1771 utf8branch = encoding.fromlocal(branch)
1772 yield rbcstruct.pack(len(utf8branch), len(nodes), len(closed))
1772 yield rbcstruct.pack(len(utf8branch), len(nodes), len(closed))
1773 yield utf8branch
1773 yield utf8branch
1774 for n in sorted(nodes):
1774 for n in sorted(nodes):
1775 yield n
1775 yield n
1776 for n in sorted(closed):
1776 for n in sorted(closed):
1777 yield n
1777 yield n
1778
1778
1779 bundler.newpart(b'cache:rev-branch-cache', data=generate(), mandatory=False)
1779 bundler.newpart(b'cache:rev-branch-cache', data=generate(), mandatory=False)
1780
1780
1781
1781
1782 def _formatrequirementsspec(requirements):
1782 def _formatrequirementsspec(requirements):
1783 requirements = [req for req in requirements if req != b"shared"]
1783 requirements = [req for req in requirements if req != b"shared"]
1784 return urlreq.quote(b','.join(sorted(requirements)))
1784 return urlreq.quote(b','.join(sorted(requirements)))
1785
1785
1786
1786
1787 def _formatrequirementsparams(requirements):
1787 def _formatrequirementsparams(requirements):
1788 requirements = _formatrequirementsspec(requirements)
1788 requirements = _formatrequirementsspec(requirements)
1789 params = b"%s%s" % (urlreq.quote(b"requirements="), requirements)
1789 params = b"%s%s" % (urlreq.quote(b"requirements="), requirements)
1790 return params
1790 return params
1791
1791
1792
1792
1793 def addpartbundlestream2(bundler, repo, **kwargs):
1793 def addpartbundlestream2(bundler, repo, **kwargs):
1794 if not kwargs.get(r'stream', False):
1794 if not kwargs.get(r'stream', False):
1795 return
1795 return
1796
1796
1797 if not streamclone.allowservergeneration(repo):
1797 if not streamclone.allowservergeneration(repo):
1798 raise error.Abort(
1798 raise error.Abort(
1799 _(
1799 _(
1800 b'stream data requested but server does not allow '
1800 b'stream data requested but server does not allow '
1801 b'this feature'
1801 b'this feature'
1802 ),
1802 ),
1803 hint=_(
1803 hint=_(
1804 b'well-behaved clients should not be '
1804 b'well-behaved clients should not be '
1805 b'requesting stream data from servers not '
1805 b'requesting stream data from servers not '
1806 b'advertising it; the client may be buggy'
1806 b'advertising it; the client may be buggy'
1807 ),
1807 ),
1808 )
1808 )
1809
1809
1810 # Stream clones don't compress well. And compression undermines a
1810 # Stream clones don't compress well. And compression undermines a
1811 # goal of stream clones, which is to be fast. Communicate the desire
1811 # goal of stream clones, which is to be fast. Communicate the desire
1812 # to avoid compression to consumers of the bundle.
1812 # to avoid compression to consumers of the bundle.
1813 bundler.prefercompressed = False
1813 bundler.prefercompressed = False
1814
1814
1815 # get the includes and excludes
1815 # get the includes and excludes
1816 includepats = kwargs.get(r'includepats')
1816 includepats = kwargs.get(r'includepats')
1817 excludepats = kwargs.get(r'excludepats')
1817 excludepats = kwargs.get(r'excludepats')
1818
1818
1819 narrowstream = repo.ui.configbool(
1819 narrowstream = repo.ui.configbool(
1820 b'experimental', b'server.stream-narrow-clones'
1820 b'experimental', b'server.stream-narrow-clones'
1821 )
1821 )
1822
1822
1823 if (includepats or excludepats) and not narrowstream:
1823 if (includepats or excludepats) and not narrowstream:
1824 raise error.Abort(_(b'server does not support narrow stream clones'))
1824 raise error.Abort(_(b'server does not support narrow stream clones'))
1825
1825
1826 includeobsmarkers = False
1826 includeobsmarkers = False
1827 if repo.obsstore:
1827 if repo.obsstore:
1828 remoteversions = obsmarkersversion(bundler.capabilities)
1828 remoteversions = obsmarkersversion(bundler.capabilities)
1829 if not remoteversions:
1829 if not remoteversions:
1830 raise error.Abort(
1830 raise error.Abort(
1831 _(
1831 _(
1832 b'server has obsolescence markers, but client '
1832 b'server has obsolescence markers, but client '
1833 b'cannot receive them via stream clone'
1833 b'cannot receive them via stream clone'
1834 )
1834 )
1835 )
1835 )
1836 elif repo.obsstore._version in remoteversions:
1836 elif repo.obsstore._version in remoteversions:
1837 includeobsmarkers = True
1837 includeobsmarkers = True
1838
1838
1839 filecount, bytecount, it = streamclone.generatev2(
1839 filecount, bytecount, it = streamclone.generatev2(
1840 repo, includepats, excludepats, includeobsmarkers
1840 repo, includepats, excludepats, includeobsmarkers
1841 )
1841 )
1842 requirements = _formatrequirementsspec(repo.requirements)
1842 requirements = _formatrequirementsspec(repo.requirements)
1843 part = bundler.newpart(b'stream2', data=it)
1843 part = bundler.newpart(b'stream2', data=it)
1844 part.addparam(b'bytecount', b'%d' % bytecount, mandatory=True)
1844 part.addparam(b'bytecount', b'%d' % bytecount, mandatory=True)
1845 part.addparam(b'filecount', b'%d' % filecount, mandatory=True)
1845 part.addparam(b'filecount', b'%d' % filecount, mandatory=True)
1846 part.addparam(b'requirements', requirements, mandatory=True)
1846 part.addparam(b'requirements', requirements, mandatory=True)
1847
1847
1848
1848
1849 def buildobsmarkerspart(bundler, markers):
1849 def buildobsmarkerspart(bundler, markers):
1850 """add an obsmarker part to the bundler with <markers>
1850 """add an obsmarker part to the bundler with <markers>
1851
1851
1852 No part is created if markers is empty.
1852 No part is created if markers is empty.
1853 Raises ValueError if the bundler doesn't support any known obsmarker format.
1853 Raises ValueError if the bundler doesn't support any known obsmarker format.
1854 """
1854 """
1855 if not markers:
1855 if not markers:
1856 return None
1856 return None
1857
1857
1858 remoteversions = obsmarkersversion(bundler.capabilities)
1858 remoteversions = obsmarkersversion(bundler.capabilities)
1859 version = obsolete.commonversion(remoteversions)
1859 version = obsolete.commonversion(remoteversions)
1860 if version is None:
1860 if version is None:
1861 raise ValueError(b'bundler does not support common obsmarker format')
1861 raise ValueError(b'bundler does not support common obsmarker format')
1862 stream = obsolete.encodemarkers(markers, True, version=version)
1862 stream = obsolete.encodemarkers(markers, True, version=version)
1863 return bundler.newpart(b'obsmarkers', data=stream)
1863 return bundler.newpart(b'obsmarkers', data=stream)
1864
1864
1865
1865
1866 def writebundle(
1866 def writebundle(
1867 ui, cg, filename, bundletype, vfs=None, compression=None, compopts=None
1867 ui, cg, filename, bundletype, vfs=None, compression=None, compopts=None
1868 ):
1868 ):
1869 """Write a bundle file and return its filename.
1869 """Write a bundle file and return its filename.
1870
1870
1871 Existing files will not be overwritten.
1871 Existing files will not be overwritten.
1872 If no filename is specified, a temporary file is created.
1872 If no filename is specified, a temporary file is created.
1873 bz2 compression can be turned off.
1873 bz2 compression can be turned off.
1874 The bundle file will be deleted in case of errors.
1874 The bundle file will be deleted in case of errors.
1875 """
1875 """
1876
1876
1877 if bundletype == b"HG20":
1877 if bundletype == b"HG20":
1878 bundle = bundle20(ui)
1878 bundle = bundle20(ui)
1879 bundle.setcompression(compression, compopts)
1879 bundle.setcompression(compression, compopts)
1880 part = bundle.newpart(b'changegroup', data=cg.getchunks())
1880 part = bundle.newpart(b'changegroup', data=cg.getchunks())
1881 part.addparam(b'version', cg.version)
1881 part.addparam(b'version', cg.version)
1882 if b'clcount' in cg.extras:
1882 if b'clcount' in cg.extras:
1883 part.addparam(
1883 part.addparam(
1884 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1884 b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False
1885 )
1885 )
1886 chunkiter = bundle.getchunks()
1886 chunkiter = bundle.getchunks()
1887 else:
1887 else:
1888 # compression argument is only for the bundle2 case
1888 # compression argument is only for the bundle2 case
1889 assert compression is None
1889 assert compression is None
1890 if cg.version != b'01':
1890 if cg.version != b'01':
1891 raise error.Abort(
1891 raise error.Abort(
1892 _(b'old bundle types only supports v1 ' b'changegroups')
1892 _(b'old bundle types only supports v1 ' b'changegroups')
1893 )
1893 )
1894 header, comp = bundletypes[bundletype]
1894 header, comp = bundletypes[bundletype]
1895 if comp not in util.compengines.supportedbundletypes:
1895 if comp not in util.compengines.supportedbundletypes:
1896 raise error.Abort(_(b'unknown stream compression type: %s') % comp)
1896 raise error.Abort(_(b'unknown stream compression type: %s') % comp)
1897 compengine = util.compengines.forbundletype(comp)
1897 compengine = util.compengines.forbundletype(comp)
1898
1898
1899 def chunkiter():
1899 def chunkiter():
1900 yield header
1900 yield header
1901 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1901 for chunk in compengine.compressstream(cg.getchunks(), compopts):
1902 yield chunk
1902 yield chunk
1903
1903
1904 chunkiter = chunkiter()
1904 chunkiter = chunkiter()
1905
1905
1906 # parse the changegroup data, otherwise we will block
1906 # parse the changegroup data, otherwise we will block
1907 # in case of sshrepo because we don't know the end of the stream
1907 # in case of sshrepo because we don't know the end of the stream
1908 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1908 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1909
1909
1910
1910
1911 def combinechangegroupresults(op):
1911 def combinechangegroupresults(op):
1912 """logic to combine 0 or more addchangegroup results into one"""
1912 """logic to combine 0 or more addchangegroup results into one"""
1913 results = [r.get(b'return', 0) for r in op.records[b'changegroup']]
1913 results = [r.get(b'return', 0) for r in op.records[b'changegroup']]
1914 changedheads = 0
1914 changedheads = 0
1915 result = 1
1915 result = 1
1916 for ret in results:
1916 for ret in results:
1917 # If any changegroup result is 0, return 0
1917 # If any changegroup result is 0, return 0
1918 if ret == 0:
1918 if ret == 0:
1919 result = 0
1919 result = 0
1920 break
1920 break
1921 if ret < -1:
1921 if ret < -1:
1922 changedheads += ret + 1
1922 changedheads += ret + 1
1923 elif ret > 1:
1923 elif ret > 1:
1924 changedheads += ret - 1
1924 changedheads += ret - 1
1925 if changedheads > 0:
1925 if changedheads > 0:
1926 result = 1 + changedheads
1926 result = 1 + changedheads
1927 elif changedheads < 0:
1927 elif changedheads < 0:
1928 result = -1 + changedheads
1928 result = -1 + changedheads
1929 return result
1929 return result
1930
1930
1931
1931
1932 @parthandler(
1932 @parthandler(
1933 b'changegroup', (b'version', b'nbchanges', b'treemanifest', b'targetphase')
1933 b'changegroup', (b'version', b'nbchanges', b'treemanifest', b'targetphase')
1934 )
1934 )
1935 def handlechangegroup(op, inpart):
1935 def handlechangegroup(op, inpart):
1936 """apply a changegroup part on the repo
1936 """apply a changegroup part on the repo
1937
1937
1938 This is a very early implementation that will massive rework before being
1938 This is a very early implementation that will massive rework before being
1939 inflicted to any end-user.
1939 inflicted to any end-user.
1940 """
1940 """
1941 from . import localrepo
1941 from . import localrepo
1942
1942
1943 tr = op.gettransaction()
1943 tr = op.gettransaction()
1944 unpackerversion = inpart.params.get(b'version', b'01')
1944 unpackerversion = inpart.params.get(b'version', b'01')
1945 # We should raise an appropriate exception here
1945 # We should raise an appropriate exception here
1946 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1946 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1947 # the source and url passed here are overwritten by the one contained in
1947 # the source and url passed here are overwritten by the one contained in
1948 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1948 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1949 nbchangesets = None
1949 nbchangesets = None
1950 if b'nbchanges' in inpart.params:
1950 if b'nbchanges' in inpart.params:
1951 nbchangesets = int(inpart.params.get(b'nbchanges'))
1951 nbchangesets = int(inpart.params.get(b'nbchanges'))
1952 if (
1952 if (
1953 b'treemanifest' in inpart.params
1953 b'treemanifest' in inpart.params
1954 and b'treemanifest' not in op.repo.requirements
1954 and b'treemanifest' not in op.repo.requirements
1955 ):
1955 ):
1956 if len(op.repo.changelog) != 0:
1956 if len(op.repo.changelog) != 0:
1957 raise error.Abort(
1957 raise error.Abort(
1958 _(
1958 _(
1959 b"bundle contains tree manifests, but local repo is "
1959 b"bundle contains tree manifests, but local repo is "
1960 b"non-empty and does not use tree manifests"
1960 b"non-empty and does not use tree manifests"
1961 )
1961 )
1962 )
1962 )
1963 op.repo.requirements.add(b'treemanifest')
1963 op.repo.requirements.add(b'treemanifest')
1964 op.repo.svfs.options = localrepo.resolvestorevfsoptions(
1964 op.repo.svfs.options = localrepo.resolvestorevfsoptions(
1965 op.repo.ui, op.repo.requirements, op.repo.features
1965 op.repo.ui, op.repo.requirements, op.repo.features
1966 )
1966 )
1967 op.repo._writerequirements()
1967 op.repo._writerequirements()
1968 extrakwargs = {}
1968 extrakwargs = {}
1969 targetphase = inpart.params.get(b'targetphase')
1969 targetphase = inpart.params.get(b'targetphase')
1970 if targetphase is not None:
1970 if targetphase is not None:
1971 extrakwargs[r'targetphase'] = int(targetphase)
1971 extrakwargs[r'targetphase'] = int(targetphase)
1972 ret = _processchangegroup(
1972 ret = _processchangegroup(
1973 op,
1973 op,
1974 cg,
1974 cg,
1975 tr,
1975 tr,
1976 b'bundle2',
1976 b'bundle2',
1977 b'bundle2',
1977 b'bundle2',
1978 expectedtotal=nbchangesets,
1978 expectedtotal=nbchangesets,
1979 **extrakwargs
1979 **extrakwargs
1980 )
1980 )
1981 if op.reply is not None:
1981 if op.reply is not None:
1982 # This is definitely not the final form of this
1982 # This is definitely not the final form of this
1983 # return. But one need to start somewhere.
1983 # return. But one need to start somewhere.
1984 part = op.reply.newpart(b'reply:changegroup', mandatory=False)
1984 part = op.reply.newpart(b'reply:changegroup', mandatory=False)
1985 part.addparam(
1985 part.addparam(
1986 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
1986 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
1987 )
1987 )
1988 part.addparam(b'return', b'%i' % ret, mandatory=False)
1988 part.addparam(b'return', b'%i' % ret, mandatory=False)
1989 assert not inpart.read()
1989 assert not inpart.read()
1990
1990
1991
1991
1992 _remotechangegroupparams = tuple(
1992 _remotechangegroupparams = tuple(
1993 [b'url', b'size', b'digests']
1993 [b'url', b'size', b'digests']
1994 + [b'digest:%s' % k for k in util.DIGESTS.keys()]
1994 + [b'digest:%s' % k for k in util.DIGESTS.keys()]
1995 )
1995 )
1996
1996
1997
1997
1998 @parthandler(b'remote-changegroup', _remotechangegroupparams)
1998 @parthandler(b'remote-changegroup', _remotechangegroupparams)
1999 def handleremotechangegroup(op, inpart):
1999 def handleremotechangegroup(op, inpart):
2000 """apply a bundle10 on the repo, given an url and validation information
2000 """apply a bundle10 on the repo, given an url and validation information
2001
2001
2002 All the information about the remote bundle to import are given as
2002 All the information about the remote bundle to import are given as
2003 parameters. The parameters include:
2003 parameters. The parameters include:
2004 - url: the url to the bundle10.
2004 - url: the url to the bundle10.
2005 - size: the bundle10 file size. It is used to validate what was
2005 - size: the bundle10 file size. It is used to validate what was
2006 retrieved by the client matches the server knowledge about the bundle.
2006 retrieved by the client matches the server knowledge about the bundle.
2007 - digests: a space separated list of the digest types provided as
2007 - digests: a space separated list of the digest types provided as
2008 parameters.
2008 parameters.
2009 - digest:<digest-type>: the hexadecimal representation of the digest with
2009 - digest:<digest-type>: the hexadecimal representation of the digest with
2010 that name. Like the size, it is used to validate what was retrieved by
2010 that name. Like the size, it is used to validate what was retrieved by
2011 the client matches what the server knows about the bundle.
2011 the client matches what the server knows about the bundle.
2012
2012
2013 When multiple digest types are given, all of them are checked.
2013 When multiple digest types are given, all of them are checked.
2014 """
2014 """
2015 try:
2015 try:
2016 raw_url = inpart.params[b'url']
2016 raw_url = inpart.params[b'url']
2017 except KeyError:
2017 except KeyError:
2018 raise error.Abort(_(b'remote-changegroup: missing "%s" param') % b'url')
2018 raise error.Abort(_(b'remote-changegroup: missing "%s" param') % b'url')
2019 parsed_url = util.url(raw_url)
2019 parsed_url = util.url(raw_url)
2020 if parsed_url.scheme not in capabilities[b'remote-changegroup']:
2020 if parsed_url.scheme not in capabilities[b'remote-changegroup']:
2021 raise error.Abort(
2021 raise error.Abort(
2022 _(b'remote-changegroup does not support %s urls')
2022 _(b'remote-changegroup does not support %s urls')
2023 % parsed_url.scheme
2023 % parsed_url.scheme
2024 )
2024 )
2025
2025
2026 try:
2026 try:
2027 size = int(inpart.params[b'size'])
2027 size = int(inpart.params[b'size'])
2028 except ValueError:
2028 except ValueError:
2029 raise error.Abort(
2029 raise error.Abort(
2030 _(b'remote-changegroup: invalid value for param "%s"') % b'size'
2030 _(b'remote-changegroup: invalid value for param "%s"') % b'size'
2031 )
2031 )
2032 except KeyError:
2032 except KeyError:
2033 raise error.Abort(
2033 raise error.Abort(
2034 _(b'remote-changegroup: missing "%s" param') % b'size'
2034 _(b'remote-changegroup: missing "%s" param') % b'size'
2035 )
2035 )
2036
2036
2037 digests = {}
2037 digests = {}
2038 for typ in inpart.params.get(b'digests', b'').split():
2038 for typ in inpart.params.get(b'digests', b'').split():
2039 param = b'digest:%s' % typ
2039 param = b'digest:%s' % typ
2040 try:
2040 try:
2041 value = inpart.params[param]
2041 value = inpart.params[param]
2042 except KeyError:
2042 except KeyError:
2043 raise error.Abort(
2043 raise error.Abort(
2044 _(b'remote-changegroup: missing "%s" param') % param
2044 _(b'remote-changegroup: missing "%s" param') % param
2045 )
2045 )
2046 digests[typ] = value
2046 digests[typ] = value
2047
2047
2048 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
2048 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
2049
2049
2050 tr = op.gettransaction()
2050 tr = op.gettransaction()
2051 from . import exchange
2051 from . import exchange
2052
2052
2053 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
2053 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
2054 if not isinstance(cg, changegroup.cg1unpacker):
2054 if not isinstance(cg, changegroup.cg1unpacker):
2055 raise error.Abort(
2055 raise error.Abort(
2056 _(b'%s: not a bundle version 1.0') % util.hidepassword(raw_url)
2056 _(b'%s: not a bundle version 1.0') % util.hidepassword(raw_url)
2057 )
2057 )
2058 ret = _processchangegroup(op, cg, tr, b'bundle2', b'bundle2')
2058 ret = _processchangegroup(op, cg, tr, b'bundle2', b'bundle2')
2059 if op.reply is not None:
2059 if op.reply is not None:
2060 # This is definitely not the final form of this
2060 # This is definitely not the final form of this
2061 # return. But one need to start somewhere.
2061 # return. But one need to start somewhere.
2062 part = op.reply.newpart(b'reply:changegroup')
2062 part = op.reply.newpart(b'reply:changegroup')
2063 part.addparam(
2063 part.addparam(
2064 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2064 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2065 )
2065 )
2066 part.addparam(b'return', b'%i' % ret, mandatory=False)
2066 part.addparam(b'return', b'%i' % ret, mandatory=False)
2067 try:
2067 try:
2068 real_part.validate()
2068 real_part.validate()
2069 except error.Abort as e:
2069 except error.Abort as e:
2070 raise error.Abort(
2070 raise error.Abort(
2071 _(b'bundle at %s is corrupted:\n%s')
2071 _(b'bundle at %s is corrupted:\n%s')
2072 % (util.hidepassword(raw_url), bytes(e))
2072 % (util.hidepassword(raw_url), bytes(e))
2073 )
2073 )
2074 assert not inpart.read()
2074 assert not inpart.read()
2075
2075
2076
2076
2077 @parthandler(b'reply:changegroup', (b'return', b'in-reply-to'))
2077 @parthandler(b'reply:changegroup', (b'return', b'in-reply-to'))
2078 def handlereplychangegroup(op, inpart):
2078 def handlereplychangegroup(op, inpart):
2079 ret = int(inpart.params[b'return'])
2079 ret = int(inpart.params[b'return'])
2080 replyto = int(inpart.params[b'in-reply-to'])
2080 replyto = int(inpart.params[b'in-reply-to'])
2081 op.records.add(b'changegroup', {b'return': ret}, replyto)
2081 op.records.add(b'changegroup', {b'return': ret}, replyto)
2082
2082
2083
2083
2084 @parthandler(b'check:bookmarks')
2084 @parthandler(b'check:bookmarks')
2085 def handlecheckbookmarks(op, inpart):
2085 def handlecheckbookmarks(op, inpart):
2086 """check location of bookmarks
2086 """check location of bookmarks
2087
2087
2088 This part is to be used to detect push race regarding bookmark, it
2088 This part is to be used to detect push race regarding bookmark, it
2089 contains binary encoded (bookmark, node) tuple. If the local state does
2089 contains binary encoded (bookmark, node) tuple. If the local state does
2090 not marks the one in the part, a PushRaced exception is raised
2090 not marks the one in the part, a PushRaced exception is raised
2091 """
2091 """
2092 bookdata = bookmarks.binarydecode(inpart)
2092 bookdata = bookmarks.binarydecode(inpart)
2093
2093
2094 msgstandard = (
2094 msgstandard = (
2095 b'remote repository changed while pushing - please try again '
2095 b'remote repository changed while pushing - please try again '
2096 b'(bookmark "%s" move from %s to %s)'
2096 b'(bookmark "%s" move from %s to %s)'
2097 )
2097 )
2098 msgmissing = (
2098 msgmissing = (
2099 b'remote repository changed while pushing - please try again '
2099 b'remote repository changed while pushing - please try again '
2100 b'(bookmark "%s" is missing, expected %s)'
2100 b'(bookmark "%s" is missing, expected %s)'
2101 )
2101 )
2102 msgexist = (
2102 msgexist = (
2103 b'remote repository changed while pushing - please try again '
2103 b'remote repository changed while pushing - please try again '
2104 b'(bookmark "%s" set on %s, expected missing)'
2104 b'(bookmark "%s" set on %s, expected missing)'
2105 )
2105 )
2106 for book, node in bookdata:
2106 for book, node in bookdata:
2107 currentnode = op.repo._bookmarks.get(book)
2107 currentnode = op.repo._bookmarks.get(book)
2108 if currentnode != node:
2108 if currentnode != node:
2109 if node is None:
2109 if node is None:
2110 finalmsg = msgexist % (book, nodemod.short(currentnode))
2110 finalmsg = msgexist % (book, nodemod.short(currentnode))
2111 elif currentnode is None:
2111 elif currentnode is None:
2112 finalmsg = msgmissing % (book, nodemod.short(node))
2112 finalmsg = msgmissing % (book, nodemod.short(node))
2113 else:
2113 else:
2114 finalmsg = msgstandard % (
2114 finalmsg = msgstandard % (
2115 book,
2115 book,
2116 nodemod.short(node),
2116 nodemod.short(node),
2117 nodemod.short(currentnode),
2117 nodemod.short(currentnode),
2118 )
2118 )
2119 raise error.PushRaced(finalmsg)
2119 raise error.PushRaced(finalmsg)
2120
2120
2121
2121
2122 @parthandler(b'check:heads')
2122 @parthandler(b'check:heads')
2123 def handlecheckheads(op, inpart):
2123 def handlecheckheads(op, inpart):
2124 """check that head of the repo did not change
2124 """check that head of the repo did not change
2125
2125
2126 This is used to detect a push race when using unbundle.
2126 This is used to detect a push race when using unbundle.
2127 This replaces the "heads" argument of unbundle."""
2127 This replaces the "heads" argument of unbundle."""
2128 h = inpart.read(20)
2128 h = inpart.read(20)
2129 heads = []
2129 heads = []
2130 while len(h) == 20:
2130 while len(h) == 20:
2131 heads.append(h)
2131 heads.append(h)
2132 h = inpart.read(20)
2132 h = inpart.read(20)
2133 assert not h
2133 assert not h
2134 # Trigger a transaction so that we are guaranteed to have the lock now.
2134 # Trigger a transaction so that we are guaranteed to have the lock now.
2135 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2135 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2136 op.gettransaction()
2136 op.gettransaction()
2137 if sorted(heads) != sorted(op.repo.heads()):
2137 if sorted(heads) != sorted(op.repo.heads()):
2138 raise error.PushRaced(
2138 raise error.PushRaced(
2139 b'remote repository changed while pushing - ' b'please try again'
2139 b'remote repository changed while pushing - ' b'please try again'
2140 )
2140 )
2141
2141
2142
2142
2143 @parthandler(b'check:updated-heads')
2143 @parthandler(b'check:updated-heads')
2144 def handlecheckupdatedheads(op, inpart):
2144 def handlecheckupdatedheads(op, inpart):
2145 """check for race on the heads touched by a push
2145 """check for race on the heads touched by a push
2146
2146
2147 This is similar to 'check:heads' but focus on the heads actually updated
2147 This is similar to 'check:heads' but focus on the heads actually updated
2148 during the push. If other activities happen on unrelated heads, it is
2148 during the push. If other activities happen on unrelated heads, it is
2149 ignored.
2149 ignored.
2150
2150
2151 This allow server with high traffic to avoid push contention as long as
2151 This allow server with high traffic to avoid push contention as long as
2152 unrelated parts of the graph are involved."""
2152 unrelated parts of the graph are involved."""
2153 h = inpart.read(20)
2153 h = inpart.read(20)
2154 heads = []
2154 heads = []
2155 while len(h) == 20:
2155 while len(h) == 20:
2156 heads.append(h)
2156 heads.append(h)
2157 h = inpart.read(20)
2157 h = inpart.read(20)
2158 assert not h
2158 assert not h
2159 # trigger a transaction so that we are guaranteed to have the lock now.
2159 # trigger a transaction so that we are guaranteed to have the lock now.
2160 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2160 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2161 op.gettransaction()
2161 op.gettransaction()
2162
2162
2163 currentheads = set()
2163 currentheads = set()
2164 for ls in op.repo.branchmap().iterheads():
2164 for ls in op.repo.branchmap().iterheads():
2165 currentheads.update(ls)
2165 currentheads.update(ls)
2166
2166
2167 for h in heads:
2167 for h in heads:
2168 if h not in currentheads:
2168 if h not in currentheads:
2169 raise error.PushRaced(
2169 raise error.PushRaced(
2170 b'remote repository changed while pushing - '
2170 b'remote repository changed while pushing - '
2171 b'please try again'
2171 b'please try again'
2172 )
2172 )
2173
2173
2174
2174
2175 @parthandler(b'check:phases')
2175 @parthandler(b'check:phases')
2176 def handlecheckphases(op, inpart):
2176 def handlecheckphases(op, inpart):
2177 """check that phase boundaries of the repository did not change
2177 """check that phase boundaries of the repository did not change
2178
2178
2179 This is used to detect a push race.
2179 This is used to detect a push race.
2180 """
2180 """
2181 phasetonodes = phases.binarydecode(inpart)
2181 phasetonodes = phases.binarydecode(inpart)
2182 unfi = op.repo.unfiltered()
2182 unfi = op.repo.unfiltered()
2183 cl = unfi.changelog
2183 cl = unfi.changelog
2184 phasecache = unfi._phasecache
2184 phasecache = unfi._phasecache
2185 msg = (
2185 msg = (
2186 b'remote repository changed while pushing - please try again '
2186 b'remote repository changed while pushing - please try again '
2187 b'(%s is %s expected %s)'
2187 b'(%s is %s expected %s)'
2188 )
2188 )
2189 for expectedphase, nodes in enumerate(phasetonodes):
2189 for expectedphase, nodes in enumerate(phasetonodes):
2190 for n in nodes:
2190 for n in nodes:
2191 actualphase = phasecache.phase(unfi, cl.rev(n))
2191 actualphase = phasecache.phase(unfi, cl.rev(n))
2192 if actualphase != expectedphase:
2192 if actualphase != expectedphase:
2193 finalmsg = msg % (
2193 finalmsg = msg % (
2194 nodemod.short(n),
2194 nodemod.short(n),
2195 phases.phasenames[actualphase],
2195 phases.phasenames[actualphase],
2196 phases.phasenames[expectedphase],
2196 phases.phasenames[expectedphase],
2197 )
2197 )
2198 raise error.PushRaced(finalmsg)
2198 raise error.PushRaced(finalmsg)
2199
2199
2200
2200
2201 @parthandler(b'output')
2201 @parthandler(b'output')
2202 def handleoutput(op, inpart):
2202 def handleoutput(op, inpart):
2203 """forward output captured on the server to the client"""
2203 """forward output captured on the server to the client"""
2204 for line in inpart.read().splitlines():
2204 for line in inpart.read().splitlines():
2205 op.ui.status(_(b'remote: %s\n') % line)
2205 op.ui.status(_(b'remote: %s\n') % line)
2206
2206
2207
2207
2208 @parthandler(b'replycaps')
2208 @parthandler(b'replycaps')
2209 def handlereplycaps(op, inpart):
2209 def handlereplycaps(op, inpart):
2210 """Notify that a reply bundle should be created
2210 """Notify that a reply bundle should be created
2211
2211
2212 The payload contains the capabilities information for the reply"""
2212 The payload contains the capabilities information for the reply"""
2213 caps = decodecaps(inpart.read())
2213 caps = decodecaps(inpart.read())
2214 if op.reply is None:
2214 if op.reply is None:
2215 op.reply = bundle20(op.ui, caps)
2215 op.reply = bundle20(op.ui, caps)
2216
2216
2217
2217
2218 class AbortFromPart(error.Abort):
2218 class AbortFromPart(error.Abort):
2219 """Sub-class of Abort that denotes an error from a bundle2 part."""
2219 """Sub-class of Abort that denotes an error from a bundle2 part."""
2220
2220
2221
2221
2222 @parthandler(b'error:abort', (b'message', b'hint'))
2222 @parthandler(b'error:abort', (b'message', b'hint'))
2223 def handleerrorabort(op, inpart):
2223 def handleerrorabort(op, inpart):
2224 """Used to transmit abort error over the wire"""
2224 """Used to transmit abort error over the wire"""
2225 raise AbortFromPart(
2225 raise AbortFromPart(
2226 inpart.params[b'message'], hint=inpart.params.get(b'hint')
2226 inpart.params[b'message'], hint=inpart.params.get(b'hint')
2227 )
2227 )
2228
2228
2229
2229
2230 @parthandler(
2230 @parthandler(
2231 b'error:pushkey',
2231 b'error:pushkey',
2232 (b'namespace', b'key', b'new', b'old', b'ret', b'in-reply-to'),
2232 (b'namespace', b'key', b'new', b'old', b'ret', b'in-reply-to'),
2233 )
2233 )
2234 def handleerrorpushkey(op, inpart):
2234 def handleerrorpushkey(op, inpart):
2235 """Used to transmit failure of a mandatory pushkey over the wire"""
2235 """Used to transmit failure of a mandatory pushkey over the wire"""
2236 kwargs = {}
2236 kwargs = {}
2237 for name in (b'namespace', b'key', b'new', b'old', b'ret'):
2237 for name in (b'namespace', b'key', b'new', b'old', b'ret'):
2238 value = inpart.params.get(name)
2238 value = inpart.params.get(name)
2239 if value is not None:
2239 if value is not None:
2240 kwargs[name] = value
2240 kwargs[name] = value
2241 raise error.PushkeyFailed(
2241 raise error.PushkeyFailed(
2242 inpart.params[b'in-reply-to'], **pycompat.strkwargs(kwargs)
2242 inpart.params[b'in-reply-to'], **pycompat.strkwargs(kwargs)
2243 )
2243 )
2244
2244
2245
2245
2246 @parthandler(b'error:unsupportedcontent', (b'parttype', b'params'))
2246 @parthandler(b'error:unsupportedcontent', (b'parttype', b'params'))
2247 def handleerrorunsupportedcontent(op, inpart):
2247 def handleerrorunsupportedcontent(op, inpart):
2248 """Used to transmit unknown content error over the wire"""
2248 """Used to transmit unknown content error over the wire"""
2249 kwargs = {}
2249 kwargs = {}
2250 parttype = inpart.params.get(b'parttype')
2250 parttype = inpart.params.get(b'parttype')
2251 if parttype is not None:
2251 if parttype is not None:
2252 kwargs[b'parttype'] = parttype
2252 kwargs[b'parttype'] = parttype
2253 params = inpart.params.get(b'params')
2253 params = inpart.params.get(b'params')
2254 if params is not None:
2254 if params is not None:
2255 kwargs[b'params'] = params.split(b'\0')
2255 kwargs[b'params'] = params.split(b'\0')
2256
2256
2257 raise error.BundleUnknownFeatureError(**pycompat.strkwargs(kwargs))
2257 raise error.BundleUnknownFeatureError(**pycompat.strkwargs(kwargs))
2258
2258
2259
2259
2260 @parthandler(b'error:pushraced', (b'message',))
2260 @parthandler(b'error:pushraced', (b'message',))
2261 def handleerrorpushraced(op, inpart):
2261 def handleerrorpushraced(op, inpart):
2262 """Used to transmit push race error over the wire"""
2262 """Used to transmit push race error over the wire"""
2263 raise error.ResponseError(_(b'push failed:'), inpart.params[b'message'])
2263 raise error.ResponseError(_(b'push failed:'), inpart.params[b'message'])
2264
2264
2265
2265
2266 @parthandler(b'listkeys', (b'namespace',))
2266 @parthandler(b'listkeys', (b'namespace',))
2267 def handlelistkeys(op, inpart):
2267 def handlelistkeys(op, inpart):
2268 """retrieve pushkey namespace content stored in a bundle2"""
2268 """retrieve pushkey namespace content stored in a bundle2"""
2269 namespace = inpart.params[b'namespace']
2269 namespace = inpart.params[b'namespace']
2270 r = pushkey.decodekeys(inpart.read())
2270 r = pushkey.decodekeys(inpart.read())
2271 op.records.add(b'listkeys', (namespace, r))
2271 op.records.add(b'listkeys', (namespace, r))
2272
2272
2273
2273
2274 @parthandler(b'pushkey', (b'namespace', b'key', b'old', b'new'))
2274 @parthandler(b'pushkey', (b'namespace', b'key', b'old', b'new'))
2275 def handlepushkey(op, inpart):
2275 def handlepushkey(op, inpart):
2276 """process a pushkey request"""
2276 """process a pushkey request"""
2277 dec = pushkey.decode
2277 dec = pushkey.decode
2278 namespace = dec(inpart.params[b'namespace'])
2278 namespace = dec(inpart.params[b'namespace'])
2279 key = dec(inpart.params[b'key'])
2279 key = dec(inpart.params[b'key'])
2280 old = dec(inpart.params[b'old'])
2280 old = dec(inpart.params[b'old'])
2281 new = dec(inpart.params[b'new'])
2281 new = dec(inpart.params[b'new'])
2282 # Grab the transaction to ensure that we have the lock before performing the
2282 # Grab the transaction to ensure that we have the lock before performing the
2283 # pushkey.
2283 # pushkey.
2284 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2284 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2285 op.gettransaction()
2285 op.gettransaction()
2286 ret = op.repo.pushkey(namespace, key, old, new)
2286 ret = op.repo.pushkey(namespace, key, old, new)
2287 record = {b'namespace': namespace, b'key': key, b'old': old, b'new': new}
2287 record = {b'namespace': namespace, b'key': key, b'old': old, b'new': new}
2288 op.records.add(b'pushkey', record)
2288 op.records.add(b'pushkey', record)
2289 if op.reply is not None:
2289 if op.reply is not None:
2290 rpart = op.reply.newpart(b'reply:pushkey')
2290 rpart = op.reply.newpart(b'reply:pushkey')
2291 rpart.addparam(
2291 rpart.addparam(
2292 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2292 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2293 )
2293 )
2294 rpart.addparam(b'return', b'%i' % ret, mandatory=False)
2294 rpart.addparam(b'return', b'%i' % ret, mandatory=False)
2295 if inpart.mandatory and not ret:
2295 if inpart.mandatory and not ret:
2296 kwargs = {}
2296 kwargs = {}
2297 for key in (b'namespace', b'key', b'new', b'old', b'ret'):
2297 for key in (b'namespace', b'key', b'new', b'old', b'ret'):
2298 if key in inpart.params:
2298 if key in inpart.params:
2299 kwargs[key] = inpart.params[key]
2299 kwargs[key] = inpart.params[key]
2300 raise error.PushkeyFailed(
2300 raise error.PushkeyFailed(
2301 partid=b'%d' % inpart.id, **pycompat.strkwargs(kwargs)
2301 partid=b'%d' % inpart.id, **pycompat.strkwargs(kwargs)
2302 )
2302 )
2303
2303
2304
2304
2305 @parthandler(b'bookmarks')
2305 @parthandler(b'bookmarks')
2306 def handlebookmark(op, inpart):
2306 def handlebookmark(op, inpart):
2307 """transmit bookmark information
2307 """transmit bookmark information
2308
2308
2309 The part contains binary encoded bookmark information.
2309 The part contains binary encoded bookmark information.
2310
2310
2311 The exact behavior of this part can be controlled by the 'bookmarks' mode
2311 The exact behavior of this part can be controlled by the 'bookmarks' mode
2312 on the bundle operation.
2312 on the bundle operation.
2313
2313
2314 When mode is 'apply' (the default) the bookmark information is applied as
2314 When mode is 'apply' (the default) the bookmark information is applied as
2315 is to the unbundling repository. Make sure a 'check:bookmarks' part is
2315 is to the unbundling repository. Make sure a 'check:bookmarks' part is
2316 issued earlier to check for push races in such update. This behavior is
2316 issued earlier to check for push races in such update. This behavior is
2317 suitable for pushing.
2317 suitable for pushing.
2318
2318
2319 When mode is 'records', the information is recorded into the 'bookmarks'
2319 When mode is 'records', the information is recorded into the 'bookmarks'
2320 records of the bundle operation. This behavior is suitable for pulling.
2320 records of the bundle operation. This behavior is suitable for pulling.
2321 """
2321 """
2322 changes = bookmarks.binarydecode(inpart)
2322 changes = bookmarks.binarydecode(inpart)
2323
2323
2324 pushkeycompat = op.repo.ui.configbool(
2324 pushkeycompat = op.repo.ui.configbool(
2325 b'server', b'bookmarks-pushkey-compat'
2325 b'server', b'bookmarks-pushkey-compat'
2326 )
2326 )
2327 bookmarksmode = op.modes.get(b'bookmarks', b'apply')
2327 bookmarksmode = op.modes.get(b'bookmarks', b'apply')
2328
2328
2329 if bookmarksmode == b'apply':
2329 if bookmarksmode == b'apply':
2330 tr = op.gettransaction()
2330 tr = op.gettransaction()
2331 bookstore = op.repo._bookmarks
2331 bookstore = op.repo._bookmarks
2332 if pushkeycompat:
2332 if pushkeycompat:
2333 allhooks = []
2333 allhooks = []
2334 for book, node in changes:
2334 for book, node in changes:
2335 hookargs = tr.hookargs.copy()
2335 hookargs = tr.hookargs.copy()
2336 hookargs[b'pushkeycompat'] = b'1'
2336 hookargs[b'pushkeycompat'] = b'1'
2337 hookargs[b'namespace'] = b'bookmarks'
2337 hookargs[b'namespace'] = b'bookmarks'
2338 hookargs[b'key'] = book
2338 hookargs[b'key'] = book
2339 hookargs[b'old'] = nodemod.hex(bookstore.get(book, b''))
2339 hookargs[b'old'] = nodemod.hex(bookstore.get(book, b''))
2340 hookargs[b'new'] = nodemod.hex(
2340 hookargs[b'new'] = nodemod.hex(
2341 node if node is not None else b''
2341 node if node is not None else b''
2342 )
2342 )
2343 allhooks.append(hookargs)
2343 allhooks.append(hookargs)
2344
2344
2345 for hookargs in allhooks:
2345 for hookargs in allhooks:
2346 op.repo.hook(
2346 op.repo.hook(
2347 b'prepushkey', throw=True, **pycompat.strkwargs(hookargs)
2347 b'prepushkey', throw=True, **pycompat.strkwargs(hookargs)
2348 )
2348 )
2349
2349
2350 bookstore.applychanges(op.repo, op.gettransaction(), changes)
2350 bookstore.applychanges(op.repo, op.gettransaction(), changes)
2351
2351
2352 if pushkeycompat:
2352 if pushkeycompat:
2353
2353
2354 def runhook():
2354 def runhook():
2355 for hookargs in allhooks:
2355 for hookargs in allhooks:
2356 op.repo.hook(b'pushkey', **pycompat.strkwargs(hookargs))
2356 op.repo.hook(b'pushkey', **pycompat.strkwargs(hookargs))
2357
2357
2358 op.repo._afterlock(runhook)
2358 op.repo._afterlock(runhook)
2359
2359
2360 elif bookmarksmode == b'records':
2360 elif bookmarksmode == b'records':
2361 for book, node in changes:
2361 for book, node in changes:
2362 record = {b'bookmark': book, b'node': node}
2362 record = {b'bookmark': book, b'node': node}
2363 op.records.add(b'bookmarks', record)
2363 op.records.add(b'bookmarks', record)
2364 else:
2364 else:
2365 raise error.ProgrammingError(
2365 raise error.ProgrammingError(
2366 b'unkown bookmark mode: %s' % bookmarksmode
2366 b'unkown bookmark mode: %s' % bookmarksmode
2367 )
2367 )
2368
2368
2369
2369
2370 @parthandler(b'phase-heads')
2370 @parthandler(b'phase-heads')
2371 def handlephases(op, inpart):
2371 def handlephases(op, inpart):
2372 """apply phases from bundle part to repo"""
2372 """apply phases from bundle part to repo"""
2373 headsbyphase = phases.binarydecode(inpart)
2373 headsbyphase = phases.binarydecode(inpart)
2374 phases.updatephases(op.repo.unfiltered(), op.gettransaction, headsbyphase)
2374 phases.updatephases(op.repo.unfiltered(), op.gettransaction, headsbyphase)
2375
2375
2376
2376
2377 @parthandler(b'reply:pushkey', (b'return', b'in-reply-to'))
2377 @parthandler(b'reply:pushkey', (b'return', b'in-reply-to'))
2378 def handlepushkeyreply(op, inpart):
2378 def handlepushkeyreply(op, inpart):
2379 """retrieve the result of a pushkey request"""
2379 """retrieve the result of a pushkey request"""
2380 ret = int(inpart.params[b'return'])
2380 ret = int(inpart.params[b'return'])
2381 partid = int(inpart.params[b'in-reply-to'])
2381 partid = int(inpart.params[b'in-reply-to'])
2382 op.records.add(b'pushkey', {b'return': ret}, partid)
2382 op.records.add(b'pushkey', {b'return': ret}, partid)
2383
2383
2384
2384
2385 @parthandler(b'obsmarkers')
2385 @parthandler(b'obsmarkers')
2386 def handleobsmarker(op, inpart):
2386 def handleobsmarker(op, inpart):
2387 """add a stream of obsmarkers to the repo"""
2387 """add a stream of obsmarkers to the repo"""
2388 tr = op.gettransaction()
2388 tr = op.gettransaction()
2389 markerdata = inpart.read()
2389 markerdata = inpart.read()
2390 if op.ui.config(b'experimental', b'obsmarkers-exchange-debug'):
2390 if op.ui.config(b'experimental', b'obsmarkers-exchange-debug'):
2391 op.ui.writenoi18n(
2391 op.ui.writenoi18n(
2392 b'obsmarker-exchange: %i bytes received\n' % len(markerdata)
2392 b'obsmarker-exchange: %i bytes received\n' % len(markerdata)
2393 )
2393 )
2394 # The mergemarkers call will crash if marker creation is not enabled.
2394 # The mergemarkers call will crash if marker creation is not enabled.
2395 # we want to avoid this if the part is advisory.
2395 # we want to avoid this if the part is advisory.
2396 if not inpart.mandatory and op.repo.obsstore.readonly:
2396 if not inpart.mandatory and op.repo.obsstore.readonly:
2397 op.repo.ui.debug(
2397 op.repo.ui.debug(
2398 b'ignoring obsolescence markers, feature not enabled\n'
2398 b'ignoring obsolescence markers, feature not enabled\n'
2399 )
2399 )
2400 return
2400 return
2401 new = op.repo.obsstore.mergemarkers(tr, markerdata)
2401 new = op.repo.obsstore.mergemarkers(tr, markerdata)
2402 op.repo.invalidatevolatilesets()
2402 op.repo.invalidatevolatilesets()
2403 op.records.add(b'obsmarkers', {b'new': new})
2403 op.records.add(b'obsmarkers', {b'new': new})
2404 if op.reply is not None:
2404 if op.reply is not None:
2405 rpart = op.reply.newpart(b'reply:obsmarkers')
2405 rpart = op.reply.newpart(b'reply:obsmarkers')
2406 rpart.addparam(
2406 rpart.addparam(
2407 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2407 b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False
2408 )
2408 )
2409 rpart.addparam(b'new', b'%i' % new, mandatory=False)
2409 rpart.addparam(b'new', b'%i' % new, mandatory=False)
2410
2410
2411
2411
2412 @parthandler(b'reply:obsmarkers', (b'new', b'in-reply-to'))
2412 @parthandler(b'reply:obsmarkers', (b'new', b'in-reply-to'))
2413 def handleobsmarkerreply(op, inpart):
2413 def handleobsmarkerreply(op, inpart):
2414 """retrieve the result of a pushkey request"""
2414 """retrieve the result of a pushkey request"""
2415 ret = int(inpart.params[b'new'])
2415 ret = int(inpart.params[b'new'])
2416 partid = int(inpart.params[b'in-reply-to'])
2416 partid = int(inpart.params[b'in-reply-to'])
2417 op.records.add(b'obsmarkers', {b'new': ret}, partid)
2417 op.records.add(b'obsmarkers', {b'new': ret}, partid)
2418
2418
2419
2419
2420 @parthandler(b'hgtagsfnodes')
2420 @parthandler(b'hgtagsfnodes')
2421 def handlehgtagsfnodes(op, inpart):
2421 def handlehgtagsfnodes(op, inpart):
2422 """Applies .hgtags fnodes cache entries to the local repo.
2422 """Applies .hgtags fnodes cache entries to the local repo.
2423
2423
2424 Payload is pairs of 20 byte changeset nodes and filenodes.
2424 Payload is pairs of 20 byte changeset nodes and filenodes.
2425 """
2425 """
2426 # Grab the transaction so we ensure that we have the lock at this point.
2426 # Grab the transaction so we ensure that we have the lock at this point.
2427 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2427 if op.ui.configbool(b'experimental', b'bundle2lazylocking'):
2428 op.gettransaction()
2428 op.gettransaction()
2429 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
2429 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
2430
2430
2431 count = 0
2431 count = 0
2432 while True:
2432 while True:
2433 node = inpart.read(20)
2433 node = inpart.read(20)
2434 fnode = inpart.read(20)
2434 fnode = inpart.read(20)
2435 if len(node) < 20 or len(fnode) < 20:
2435 if len(node) < 20 or len(fnode) < 20:
2436 op.ui.debug(b'ignoring incomplete received .hgtags fnodes data\n')
2436 op.ui.debug(b'ignoring incomplete received .hgtags fnodes data\n')
2437 break
2437 break
2438 cache.setfnode(node, fnode)
2438 cache.setfnode(node, fnode)
2439 count += 1
2439 count += 1
2440
2440
2441 cache.write()
2441 cache.write()
2442 op.ui.debug(b'applied %i hgtags fnodes cache entries\n' % count)
2442 op.ui.debug(b'applied %i hgtags fnodes cache entries\n' % count)
2443
2443
2444
2444
2445 rbcstruct = struct.Struct(b'>III')
2445 rbcstruct = struct.Struct(b'>III')
2446
2446
2447
2447
2448 @parthandler(b'cache:rev-branch-cache')
2448 @parthandler(b'cache:rev-branch-cache')
2449 def handlerbc(op, inpart):
2449 def handlerbc(op, inpart):
2450 """receive a rev-branch-cache payload and update the local cache
2450 """receive a rev-branch-cache payload and update the local cache
2451
2451
2452 The payload is a series of data related to each branch
2452 The payload is a series of data related to each branch
2453
2453
2454 1) branch name length
2454 1) branch name length
2455 2) number of open heads
2455 2) number of open heads
2456 3) number of closed heads
2456 3) number of closed heads
2457 4) open heads nodes
2457 4) open heads nodes
2458 5) closed heads nodes
2458 5) closed heads nodes
2459 """
2459 """
2460 total = 0
2460 total = 0
2461 rawheader = inpart.read(rbcstruct.size)
2461 rawheader = inpart.read(rbcstruct.size)
2462 cache = op.repo.revbranchcache()
2462 cache = op.repo.revbranchcache()
2463 cl = op.repo.unfiltered().changelog
2463 cl = op.repo.unfiltered().changelog
2464 while rawheader:
2464 while rawheader:
2465 header = rbcstruct.unpack(rawheader)
2465 header = rbcstruct.unpack(rawheader)
2466 total += header[1] + header[2]
2466 total += header[1] + header[2]
2467 utf8branch = inpart.read(header[0])
2467 utf8branch = inpart.read(header[0])
2468 branch = encoding.tolocal(utf8branch)
2468 branch = encoding.tolocal(utf8branch)
2469 for x in pycompat.xrange(header[1]):
2469 for x in pycompat.xrange(header[1]):
2470 node = inpart.read(20)
2470 node = inpart.read(20)
2471 rev = cl.rev(node)
2471 rev = cl.rev(node)
2472 cache.setdata(branch, rev, node, False)
2472 cache.setdata(branch, rev, node, False)
2473 for x in pycompat.xrange(header[2]):
2473 for x in pycompat.xrange(header[2]):
2474 node = inpart.read(20)
2474 node = inpart.read(20)
2475 rev = cl.rev(node)
2475 rev = cl.rev(node)
2476 cache.setdata(branch, rev, node, True)
2476 cache.setdata(branch, rev, node, True)
2477 rawheader = inpart.read(rbcstruct.size)
2477 rawheader = inpart.read(rbcstruct.size)
2478 cache.write()
2478 cache.write()
2479
2479
2480
2480
2481 @parthandler(b'pushvars')
2481 @parthandler(b'pushvars')
2482 def bundle2getvars(op, part):
2482 def bundle2getvars(op, part):
2483 '''unbundle a bundle2 containing shellvars on the server'''
2483 '''unbundle a bundle2 containing shellvars on the server'''
2484 # An option to disable unbundling on server-side for security reasons
2484 # An option to disable unbundling on server-side for security reasons
2485 if op.ui.configbool(b'push', b'pushvars.server'):
2485 if op.ui.configbool(b'push', b'pushvars.server'):
2486 hookargs = {}
2486 hookargs = {}
2487 for key, value in part.advisoryparams:
2487 for key, value in part.advisoryparams:
2488 key = key.upper()
2488 key = key.upper()
2489 # We want pushed variables to have USERVAR_ prepended so we know
2489 # We want pushed variables to have USERVAR_ prepended so we know
2490 # they came from the --pushvar flag.
2490 # they came from the --pushvar flag.
2491 key = b"USERVAR_" + key
2491 key = b"USERVAR_" + key
2492 hookargs[key] = value
2492 hookargs[key] = value
2493 op.addhookargs(hookargs)
2493 op.addhookargs(hookargs)
2494
2494
2495
2495
2496 @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount'))
2496 @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount'))
2497 def handlestreamv2bundle(op, part):
2497 def handlestreamv2bundle(op, part):
2498
2498
2499 requirements = urlreq.unquote(part.params[b'requirements']).split(b',')
2499 requirements = urlreq.unquote(part.params[b'requirements']).split(b',')
2500 filecount = int(part.params[b'filecount'])
2500 filecount = int(part.params[b'filecount'])
2501 bytecount = int(part.params[b'bytecount'])
2501 bytecount = int(part.params[b'bytecount'])
2502
2502
2503 repo = op.repo
2503 repo = op.repo
2504 if len(repo):
2504 if len(repo):
2505 msg = _(b'cannot apply stream clone to non empty repository')
2505 msg = _(b'cannot apply stream clone to non empty repository')
2506 raise error.Abort(msg)
2506 raise error.Abort(msg)
2507
2507
2508 repo.ui.debug(b'applying stream bundle\n')
2508 repo.ui.debug(b'applying stream bundle\n')
2509 streamclone.applybundlev2(repo, part, filecount, bytecount, requirements)
2509 streamclone.applybundlev2(repo, part, filecount, bytecount, requirements)
2510
2510
2511
2511
2512 def widen_bundle(
2512 def widen_bundle(
2513 bundler, repo, oldmatcher, newmatcher, common, known, cgversion, ellipses
2513 bundler, repo, oldmatcher, newmatcher, common, known, cgversion, ellipses
2514 ):
2514 ):
2515 """generates bundle2 for widening a narrow clone
2515 """generates bundle2 for widening a narrow clone
2516
2516
2517 bundler is the bundle to which data should be added
2517 bundler is the bundle to which data should be added
2518 repo is the localrepository instance
2518 repo is the localrepository instance
2519 oldmatcher matches what the client already has
2519 oldmatcher matches what the client already has
2520 newmatcher matches what the client needs (including what it already has)
2520 newmatcher matches what the client needs (including what it already has)
2521 common is set of common heads between server and client
2521 common is set of common heads between server and client
2522 known is a set of revs known on the client side (used in ellipses)
2522 known is a set of revs known on the client side (used in ellipses)
2523 cgversion is the changegroup version to send
2523 cgversion is the changegroup version to send
2524 ellipses is boolean value telling whether to send ellipses data or not
2524 ellipses is boolean value telling whether to send ellipses data or not
2525
2525
2526 returns bundle2 of the data required for extending
2526 returns bundle2 of the data required for extending
2527 """
2527 """
2528 commonnodes = set()
2528 commonnodes = set()
2529 cl = repo.changelog
2529 cl = repo.changelog
2530 for r in repo.revs(b"::%ln", common):
2530 for r in repo.revs(b"::%ln", common):
2531 commonnodes.add(cl.node(r))
2531 commonnodes.add(cl.node(r))
2532 if commonnodes:
2532 if commonnodes:
2533 # XXX: we should only send the filelogs (and treemanifest). user
2533 # XXX: we should only send the filelogs (and treemanifest). user
2534 # already has the changelog and manifest
2534 # already has the changelog and manifest
2535 packer = changegroup.getbundler(
2535 packer = changegroup.getbundler(
2536 cgversion,
2536 cgversion,
2537 repo,
2537 repo,
2538 oldmatcher=oldmatcher,
2538 oldmatcher=oldmatcher,
2539 matcher=newmatcher,
2539 matcher=newmatcher,
2540 fullnodes=commonnodes,
2540 fullnodes=commonnodes,
2541 )
2541 )
2542 cgdata = packer.generate(
2542 cgdata = packer.generate(
2543 {nodemod.nullid},
2543 {nodemod.nullid},
2544 list(commonnodes),
2544 list(commonnodes),
2545 False,
2545 False,
2546 b'narrow_widen',
2546 b'narrow_widen',
2547 changelog=False,
2547 changelog=False,
2548 )
2548 )
2549
2549
2550 part = bundler.newpart(b'changegroup', data=cgdata)
2550 part = bundler.newpart(b'changegroup', data=cgdata)
2551 part.addparam(b'version', cgversion)
2551 part.addparam(b'version', cgversion)
2552 if b'treemanifest' in repo.requirements:
2552 if b'treemanifest' in repo.requirements:
2553 part.addparam(b'treemanifest', b'1')
2553 part.addparam(b'treemanifest', b'1')
2554
2554
2555 return bundler
2555 return bundler
@@ -1,670 +1,670 b''
1 # bundlerepo.py - repository class for viewing uncompressed bundles
1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 #
2 #
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Repository class for viewing uncompressed bundles.
8 """Repository class for viewing uncompressed bundles.
9
9
10 This provides a read-only repository interface to bundles as if they
10 This provides a read-only repository interface to bundles as if they
11 were part of the actual repository.
11 were part of the actual repository.
12 """
12 """
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import os
16 import os
17 import shutil
17 import shutil
18
18
19 from .i18n import _
19 from .i18n import _
20 from .node import nullid, nullrev
20 from .node import nullid, nullrev
21
21
22 from . import (
22 from . import (
23 bundle2,
23 bundle2,
24 changegroup,
24 changegroup,
25 changelog,
25 changelog,
26 cmdutil,
26 cmdutil,
27 discovery,
27 discovery,
28 encoding,
28 encoding,
29 error,
29 error,
30 exchange,
30 exchange,
31 filelog,
31 filelog,
32 localrepo,
32 localrepo,
33 manifest,
33 manifest,
34 mdiff,
34 mdiff,
35 node as nodemod,
35 node as nodemod,
36 pathutil,
36 pathutil,
37 phases,
37 phases,
38 pycompat,
38 pycompat,
39 revlog,
39 revlog,
40 util,
40 util,
41 vfs as vfsmod,
41 vfs as vfsmod,
42 )
42 )
43
43
44
44
45 class bundlerevlog(revlog.revlog):
45 class bundlerevlog(revlog.revlog):
46 def __init__(self, opener, indexfile, cgunpacker, linkmapper):
46 def __init__(self, opener, indexfile, cgunpacker, linkmapper):
47 # How it works:
47 # How it works:
48 # To retrieve a revision, we need to know the offset of the revision in
48 # To retrieve a revision, we need to know the offset of the revision in
49 # the bundle (an unbundle object). We store this offset in the index
49 # the bundle (an unbundle object). We store this offset in the index
50 # (start). The base of the delta is stored in the base field.
50 # (start). The base of the delta is stored in the base field.
51 #
51 #
52 # To differentiate a rev in the bundle from a rev in the revlog, we
52 # To differentiate a rev in the bundle from a rev in the revlog, we
53 # check revision against repotiprev.
53 # check revision against repotiprev.
54 opener = vfsmod.readonlyvfs(opener)
54 opener = vfsmod.readonlyvfs(opener)
55 revlog.revlog.__init__(self, opener, indexfile)
55 revlog.revlog.__init__(self, opener, indexfile)
56 self.bundle = cgunpacker
56 self.bundle = cgunpacker
57 n = len(self)
57 n = len(self)
58 self.repotiprev = n - 1
58 self.repotiprev = n - 1
59 self.bundlerevs = set() # used by 'bundle()' revset expression
59 self.bundlerevs = set() # used by 'bundle()' revset expression
60 for deltadata in cgunpacker.deltaiter():
60 for deltadata in cgunpacker.deltaiter():
61 node, p1, p2, cs, deltabase, delta, flags = deltadata
61 node, p1, p2, cs, deltabase, delta, flags = deltadata
62
62
63 size = len(delta)
63 size = len(delta)
64 start = cgunpacker.tell() - size
64 start = cgunpacker.tell() - size
65
65
66 link = linkmapper(cs)
66 link = linkmapper(cs)
67 if node in self.nodemap:
67 if node in self.nodemap:
68 # this can happen if two branches make the same change
68 # this can happen if two branches make the same change
69 self.bundlerevs.add(self.nodemap[node])
69 self.bundlerevs.add(self.nodemap[node])
70 continue
70 continue
71
71
72 for p in (p1, p2):
72 for p in (p1, p2):
73 if p not in self.nodemap:
73 if p not in self.nodemap:
74 raise error.LookupError(
74 raise error.LookupError(
75 p, self.indexfile, _(b"unknown parent")
75 p, self.indexfile, _(b"unknown parent")
76 )
76 )
77
77
78 if deltabase not in self.nodemap:
78 if deltabase not in self.nodemap:
79 raise LookupError(
79 raise LookupError(
80 deltabase, self.indexfile, _(b'unknown delta base')
80 deltabase, self.indexfile, _(b'unknown delta base')
81 )
81 )
82
82
83 baserev = self.rev(deltabase)
83 baserev = self.rev(deltabase)
84 # start, size, full unc. size, base (unused), link, p1, p2, node
84 # start, size, full unc. size, base (unused), link, p1, p2, node
85 e = (
85 e = (
86 revlog.offset_type(start, flags),
86 revlog.offset_type(start, flags),
87 size,
87 size,
88 -1,
88 -1,
89 baserev,
89 baserev,
90 link,
90 link,
91 self.rev(p1),
91 self.rev(p1),
92 self.rev(p2),
92 self.rev(p2),
93 node,
93 node,
94 )
94 )
95 self.index.append(e)
95 self.index.append(e)
96 self.nodemap[node] = n
96 self.nodemap[node] = n
97 self.bundlerevs.add(n)
97 self.bundlerevs.add(n)
98 n += 1
98 n += 1
99
99
100 def _chunk(self, rev, df=None):
100 def _chunk(self, rev, df=None):
101 # Warning: in case of bundle, the diff is against what we stored as
101 # Warning: in case of bundle, the diff is against what we stored as
102 # delta base, not against rev - 1
102 # delta base, not against rev - 1
103 # XXX: could use some caching
103 # XXX: could use some caching
104 if rev <= self.repotiprev:
104 if rev <= self.repotiprev:
105 return revlog.revlog._chunk(self, rev)
105 return revlog.revlog._chunk(self, rev)
106 self.bundle.seek(self.start(rev))
106 self.bundle.seek(self.start(rev))
107 return self.bundle.read(self.length(rev))
107 return self.bundle.read(self.length(rev))
108
108
109 def revdiff(self, rev1, rev2):
109 def revdiff(self, rev1, rev2):
110 """return or calculate a delta between two revisions"""
110 """return or calculate a delta between two revisions"""
111 if rev1 > self.repotiprev and rev2 > self.repotiprev:
111 if rev1 > self.repotiprev and rev2 > self.repotiprev:
112 # hot path for bundle
112 # hot path for bundle
113 revb = self.index[rev2][3]
113 revb = self.index[rev2][3]
114 if revb == rev1:
114 if revb == rev1:
115 return self._chunk(rev2)
115 return self._chunk(rev2)
116 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
116 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
117 return revlog.revlog.revdiff(self, rev1, rev2)
117 return revlog.revlog.revdiff(self, rev1, rev2)
118
118
119 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
119 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
120
120
121 def _rawtext(self, node, rev, _df=None):
121 def _rawtext(self, node, rev, _df=None):
122 if rev is None:
122 if rev is None:
123 rev = self.rev(node)
123 rev = self.rev(node)
124 validated = False
124 validated = False
125 rawtext = None
125 rawtext = None
126 chain = []
126 chain = []
127 iterrev = rev
127 iterrev = rev
128 # reconstruct the revision if it is from a changegroup
128 # reconstruct the revision if it is from a changegroup
129 while iterrev > self.repotiprev:
129 while iterrev > self.repotiprev:
130 if self._revisioncache and self._revisioncache[1] == iterrev:
130 if self._revisioncache and self._revisioncache[1] == iterrev:
131 rawtext = self._revisioncache[2]
131 rawtext = self._revisioncache[2]
132 break
132 break
133 chain.append(iterrev)
133 chain.append(iterrev)
134 iterrev = self.index[iterrev][3]
134 iterrev = self.index[iterrev][3]
135 if iterrev == nullrev:
135 if iterrev == nullrev:
136 rawtext = b''
136 rawtext = b''
137 elif rawtext is None:
137 elif rawtext is None:
138 r = super(bundlerevlog, self)._rawtext(
138 r = super(bundlerevlog, self)._rawtext(
139 self.node(iterrev), iterrev, _df=_df
139 self.node(iterrev), iterrev, _df=_df
140 )
140 )
141 __, rawtext, validated = r
141 __, rawtext, validated = r
142 if chain:
142 if chain:
143 validated = False
143 validated = False
144 while chain:
144 while chain:
145 delta = self._chunk(chain.pop())
145 delta = self._chunk(chain.pop())
146 rawtext = mdiff.patches(rawtext, [delta])
146 rawtext = mdiff.patches(rawtext, [delta])
147 return rev, rawtext, validated
147 return rev, rawtext, validated
148
148
149 def addrevision(self, *args, **kwargs):
149 def addrevision(self, *args, **kwargs):
150 raise NotImplementedError
150 raise NotImplementedError
151
151
152 def addgroup(self, *args, **kwargs):
152 def addgroup(self, *args, **kwargs):
153 raise NotImplementedError
153 raise NotImplementedError
154
154
155 def strip(self, *args, **kwargs):
155 def strip(self, *args, **kwargs):
156 raise NotImplementedError
156 raise NotImplementedError
157
157
158 def checksize(self):
158 def checksize(self):
159 raise NotImplementedError
159 raise NotImplementedError
160
160
161
161
162 class bundlechangelog(bundlerevlog, changelog.changelog):
162 class bundlechangelog(bundlerevlog, changelog.changelog):
163 def __init__(self, opener, cgunpacker):
163 def __init__(self, opener, cgunpacker):
164 changelog.changelog.__init__(self, opener)
164 changelog.changelog.__init__(self, opener)
165 linkmapper = lambda x: x
165 linkmapper = lambda x: x
166 bundlerevlog.__init__(
166 bundlerevlog.__init__(
167 self, opener, self.indexfile, cgunpacker, linkmapper
167 self, opener, self.indexfile, cgunpacker, linkmapper
168 )
168 )
169
169
170
170
171 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
171 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
172 def __init__(
172 def __init__(
173 self, opener, cgunpacker, linkmapper, dirlogstarts=None, dir=b''
173 self, opener, cgunpacker, linkmapper, dirlogstarts=None, dir=b''
174 ):
174 ):
175 manifest.manifestrevlog.__init__(self, opener, tree=dir)
175 manifest.manifestrevlog.__init__(self, opener, tree=dir)
176 bundlerevlog.__init__(
176 bundlerevlog.__init__(
177 self, opener, self.indexfile, cgunpacker, linkmapper
177 self, opener, self.indexfile, cgunpacker, linkmapper
178 )
178 )
179 if dirlogstarts is None:
179 if dirlogstarts is None:
180 dirlogstarts = {}
180 dirlogstarts = {}
181 if self.bundle.version == b"03":
181 if self.bundle.version == b"03":
182 dirlogstarts = _getfilestarts(self.bundle)
182 dirlogstarts = _getfilestarts(self.bundle)
183 self._dirlogstarts = dirlogstarts
183 self._dirlogstarts = dirlogstarts
184 self._linkmapper = linkmapper
184 self._linkmapper = linkmapper
185
185
186 def dirlog(self, d):
186 def dirlog(self, d):
187 if d in self._dirlogstarts:
187 if d in self._dirlogstarts:
188 self.bundle.seek(self._dirlogstarts[d])
188 self.bundle.seek(self._dirlogstarts[d])
189 return bundlemanifest(
189 return bundlemanifest(
190 self.opener,
190 self.opener,
191 self.bundle,
191 self.bundle,
192 self._linkmapper,
192 self._linkmapper,
193 self._dirlogstarts,
193 self._dirlogstarts,
194 dir=d,
194 dir=d,
195 )
195 )
196 return super(bundlemanifest, self).dirlog(d)
196 return super(bundlemanifest, self).dirlog(d)
197
197
198
198
199 class bundlefilelog(filelog.filelog):
199 class bundlefilelog(filelog.filelog):
200 def __init__(self, opener, path, cgunpacker, linkmapper):
200 def __init__(self, opener, path, cgunpacker, linkmapper):
201 filelog.filelog.__init__(self, opener, path)
201 filelog.filelog.__init__(self, opener, path)
202 self._revlog = bundlerevlog(
202 self._revlog = bundlerevlog(
203 opener, self.indexfile, cgunpacker, linkmapper
203 opener, self.indexfile, cgunpacker, linkmapper
204 )
204 )
205
205
206
206
207 class bundlepeer(localrepo.localpeer):
207 class bundlepeer(localrepo.localpeer):
208 def canpush(self):
208 def canpush(self):
209 return False
209 return False
210
210
211
211
212 class bundlephasecache(phases.phasecache):
212 class bundlephasecache(phases.phasecache):
213 def __init__(self, *args, **kwargs):
213 def __init__(self, *args, **kwargs):
214 super(bundlephasecache, self).__init__(*args, **kwargs)
214 super(bundlephasecache, self).__init__(*args, **kwargs)
215 if util.safehasattr(self, b'opener'):
215 if util.safehasattr(self, 'opener'):
216 self.opener = vfsmod.readonlyvfs(self.opener)
216 self.opener = vfsmod.readonlyvfs(self.opener)
217
217
218 def write(self):
218 def write(self):
219 raise NotImplementedError
219 raise NotImplementedError
220
220
221 def _write(self, fp):
221 def _write(self, fp):
222 raise NotImplementedError
222 raise NotImplementedError
223
223
224 def _updateroots(self, phase, newroots, tr):
224 def _updateroots(self, phase, newroots, tr):
225 self.phaseroots[phase] = newroots
225 self.phaseroots[phase] = newroots
226 self.invalidate()
226 self.invalidate()
227 self.dirty = True
227 self.dirty = True
228
228
229
229
230 def _getfilestarts(cgunpacker):
230 def _getfilestarts(cgunpacker):
231 filespos = {}
231 filespos = {}
232 for chunkdata in iter(cgunpacker.filelogheader, {}):
232 for chunkdata in iter(cgunpacker.filelogheader, {}):
233 fname = chunkdata[b'filename']
233 fname = chunkdata[b'filename']
234 filespos[fname] = cgunpacker.tell()
234 filespos[fname] = cgunpacker.tell()
235 for chunk in iter(lambda: cgunpacker.deltachunk(None), {}):
235 for chunk in iter(lambda: cgunpacker.deltachunk(None), {}):
236 pass
236 pass
237 return filespos
237 return filespos
238
238
239
239
240 class bundlerepository(object):
240 class bundlerepository(object):
241 """A repository instance that is a union of a local repo and a bundle.
241 """A repository instance that is a union of a local repo and a bundle.
242
242
243 Instances represent a read-only repository composed of a local repository
243 Instances represent a read-only repository composed of a local repository
244 with the contents of a bundle file applied. The repository instance is
244 with the contents of a bundle file applied. The repository instance is
245 conceptually similar to the state of a repository after an
245 conceptually similar to the state of a repository after an
246 ``hg unbundle`` operation. However, the contents of the bundle are never
246 ``hg unbundle`` operation. However, the contents of the bundle are never
247 applied to the actual base repository.
247 applied to the actual base repository.
248
248
249 Instances constructed directly are not usable as repository objects.
249 Instances constructed directly are not usable as repository objects.
250 Use instance() or makebundlerepository() to create instances.
250 Use instance() or makebundlerepository() to create instances.
251 """
251 """
252
252
253 def __init__(self, bundlepath, url, tempparent):
253 def __init__(self, bundlepath, url, tempparent):
254 self._tempparent = tempparent
254 self._tempparent = tempparent
255 self._url = url
255 self._url = url
256
256
257 self.ui.setconfig(b'phases', b'publish', False, b'bundlerepo')
257 self.ui.setconfig(b'phases', b'publish', False, b'bundlerepo')
258
258
259 self.tempfile = None
259 self.tempfile = None
260 f = util.posixfile(bundlepath, b"rb")
260 f = util.posixfile(bundlepath, b"rb")
261 bundle = exchange.readbundle(self.ui, f, bundlepath)
261 bundle = exchange.readbundle(self.ui, f, bundlepath)
262
262
263 if isinstance(bundle, bundle2.unbundle20):
263 if isinstance(bundle, bundle2.unbundle20):
264 self._bundlefile = bundle
264 self._bundlefile = bundle
265 self._cgunpacker = None
265 self._cgunpacker = None
266
266
267 cgpart = None
267 cgpart = None
268 for part in bundle.iterparts(seekable=True):
268 for part in bundle.iterparts(seekable=True):
269 if part.type == b'changegroup':
269 if part.type == b'changegroup':
270 if cgpart:
270 if cgpart:
271 raise NotImplementedError(
271 raise NotImplementedError(
272 b"can't process " b"multiple changegroups"
272 b"can't process " b"multiple changegroups"
273 )
273 )
274 cgpart = part
274 cgpart = part
275
275
276 self._handlebundle2part(bundle, part)
276 self._handlebundle2part(bundle, part)
277
277
278 if not cgpart:
278 if not cgpart:
279 raise error.Abort(_(b"No changegroups found"))
279 raise error.Abort(_(b"No changegroups found"))
280
280
281 # This is required to placate a later consumer, which expects
281 # This is required to placate a later consumer, which expects
282 # the payload offset to be at the beginning of the changegroup.
282 # the payload offset to be at the beginning of the changegroup.
283 # We need to do this after the iterparts() generator advances
283 # We need to do this after the iterparts() generator advances
284 # because iterparts() will seek to end of payload after the
284 # because iterparts() will seek to end of payload after the
285 # generator returns control to iterparts().
285 # generator returns control to iterparts().
286 cgpart.seek(0, os.SEEK_SET)
286 cgpart.seek(0, os.SEEK_SET)
287
287
288 elif isinstance(bundle, changegroup.cg1unpacker):
288 elif isinstance(bundle, changegroup.cg1unpacker):
289 if bundle.compressed():
289 if bundle.compressed():
290 f = self._writetempbundle(
290 f = self._writetempbundle(
291 bundle.read, b'.hg10un', header=b'HG10UN'
291 bundle.read, b'.hg10un', header=b'HG10UN'
292 )
292 )
293 bundle = exchange.readbundle(self.ui, f, bundlepath, self.vfs)
293 bundle = exchange.readbundle(self.ui, f, bundlepath, self.vfs)
294
294
295 self._bundlefile = bundle
295 self._bundlefile = bundle
296 self._cgunpacker = bundle
296 self._cgunpacker = bundle
297 else:
297 else:
298 raise error.Abort(
298 raise error.Abort(
299 _(b'bundle type %s cannot be read') % type(bundle)
299 _(b'bundle type %s cannot be read') % type(bundle)
300 )
300 )
301
301
302 # dict with the mapping 'filename' -> position in the changegroup.
302 # dict with the mapping 'filename' -> position in the changegroup.
303 self._cgfilespos = {}
303 self._cgfilespos = {}
304
304
305 self.firstnewrev = self.changelog.repotiprev + 1
305 self.firstnewrev = self.changelog.repotiprev + 1
306 phases.retractboundary(
306 phases.retractboundary(
307 self,
307 self,
308 None,
308 None,
309 phases.draft,
309 phases.draft,
310 [ctx.node() for ctx in self[self.firstnewrev :]],
310 [ctx.node() for ctx in self[self.firstnewrev :]],
311 )
311 )
312
312
313 def _handlebundle2part(self, bundle, part):
313 def _handlebundle2part(self, bundle, part):
314 if part.type != b'changegroup':
314 if part.type != b'changegroup':
315 return
315 return
316
316
317 cgstream = part
317 cgstream = part
318 version = part.params.get(b'version', b'01')
318 version = part.params.get(b'version', b'01')
319 legalcgvers = changegroup.supportedincomingversions(self)
319 legalcgvers = changegroup.supportedincomingversions(self)
320 if version not in legalcgvers:
320 if version not in legalcgvers:
321 msg = _(b'Unsupported changegroup version: %s')
321 msg = _(b'Unsupported changegroup version: %s')
322 raise error.Abort(msg % version)
322 raise error.Abort(msg % version)
323 if bundle.compressed():
323 if bundle.compressed():
324 cgstream = self._writetempbundle(part.read, b'.cg%sun' % version)
324 cgstream = self._writetempbundle(part.read, b'.cg%sun' % version)
325
325
326 self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN')
326 self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN')
327
327
328 def _writetempbundle(self, readfn, suffix, header=b''):
328 def _writetempbundle(self, readfn, suffix, header=b''):
329 """Write a temporary file to disk
329 """Write a temporary file to disk
330 """
330 """
331 fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix)
331 fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix)
332 self.tempfile = temp
332 self.tempfile = temp
333
333
334 with os.fdopen(fdtemp, r'wb') as fptemp:
334 with os.fdopen(fdtemp, r'wb') as fptemp:
335 fptemp.write(header)
335 fptemp.write(header)
336 while True:
336 while True:
337 chunk = readfn(2 ** 18)
337 chunk = readfn(2 ** 18)
338 if not chunk:
338 if not chunk:
339 break
339 break
340 fptemp.write(chunk)
340 fptemp.write(chunk)
341
341
342 return self.vfs.open(self.tempfile, mode=b"rb")
342 return self.vfs.open(self.tempfile, mode=b"rb")
343
343
344 @localrepo.unfilteredpropertycache
344 @localrepo.unfilteredpropertycache
345 def _phasecache(self):
345 def _phasecache(self):
346 return bundlephasecache(self, self._phasedefaults)
346 return bundlephasecache(self, self._phasedefaults)
347
347
348 @localrepo.unfilteredpropertycache
348 @localrepo.unfilteredpropertycache
349 def changelog(self):
349 def changelog(self):
350 # consume the header if it exists
350 # consume the header if it exists
351 self._cgunpacker.changelogheader()
351 self._cgunpacker.changelogheader()
352 c = bundlechangelog(self.svfs, self._cgunpacker)
352 c = bundlechangelog(self.svfs, self._cgunpacker)
353 self.manstart = self._cgunpacker.tell()
353 self.manstart = self._cgunpacker.tell()
354 return c
354 return c
355
355
356 def _refreshchangelog(self):
356 def _refreshchangelog(self):
357 # changelog for bundle repo are not filecache, this method is not
357 # changelog for bundle repo are not filecache, this method is not
358 # applicable.
358 # applicable.
359 pass
359 pass
360
360
361 @localrepo.unfilteredpropertycache
361 @localrepo.unfilteredpropertycache
362 def manifestlog(self):
362 def manifestlog(self):
363 self._cgunpacker.seek(self.manstart)
363 self._cgunpacker.seek(self.manstart)
364 # consume the header if it exists
364 # consume the header if it exists
365 self._cgunpacker.manifestheader()
365 self._cgunpacker.manifestheader()
366 linkmapper = self.unfiltered().changelog.rev
366 linkmapper = self.unfiltered().changelog.rev
367 rootstore = bundlemanifest(self.svfs, self._cgunpacker, linkmapper)
367 rootstore = bundlemanifest(self.svfs, self._cgunpacker, linkmapper)
368 self.filestart = self._cgunpacker.tell()
368 self.filestart = self._cgunpacker.tell()
369
369
370 return manifest.manifestlog(
370 return manifest.manifestlog(
371 self.svfs, self, rootstore, self.narrowmatch()
371 self.svfs, self, rootstore, self.narrowmatch()
372 )
372 )
373
373
374 def _consumemanifest(self):
374 def _consumemanifest(self):
375 """Consumes the manifest portion of the bundle, setting filestart so the
375 """Consumes the manifest portion of the bundle, setting filestart so the
376 file portion can be read."""
376 file portion can be read."""
377 self._cgunpacker.seek(self.manstart)
377 self._cgunpacker.seek(self.manstart)
378 self._cgunpacker.manifestheader()
378 self._cgunpacker.manifestheader()
379 for delta in self._cgunpacker.deltaiter():
379 for delta in self._cgunpacker.deltaiter():
380 pass
380 pass
381 self.filestart = self._cgunpacker.tell()
381 self.filestart = self._cgunpacker.tell()
382
382
383 @localrepo.unfilteredpropertycache
383 @localrepo.unfilteredpropertycache
384 def manstart(self):
384 def manstart(self):
385 self.changelog
385 self.changelog
386 return self.manstart
386 return self.manstart
387
387
388 @localrepo.unfilteredpropertycache
388 @localrepo.unfilteredpropertycache
389 def filestart(self):
389 def filestart(self):
390 self.manifestlog
390 self.manifestlog
391
391
392 # If filestart was not set by self.manifestlog, that means the
392 # If filestart was not set by self.manifestlog, that means the
393 # manifestlog implementation did not consume the manifests from the
393 # manifestlog implementation did not consume the manifests from the
394 # changegroup (ex: it might be consuming trees from a separate bundle2
394 # changegroup (ex: it might be consuming trees from a separate bundle2
395 # part instead). So we need to manually consume it.
395 # part instead). So we need to manually consume it.
396 if r'filestart' not in self.__dict__:
396 if r'filestart' not in self.__dict__:
397 self._consumemanifest()
397 self._consumemanifest()
398
398
399 return self.filestart
399 return self.filestart
400
400
401 def url(self):
401 def url(self):
402 return self._url
402 return self._url
403
403
404 def file(self, f):
404 def file(self, f):
405 if not self._cgfilespos:
405 if not self._cgfilespos:
406 self._cgunpacker.seek(self.filestart)
406 self._cgunpacker.seek(self.filestart)
407 self._cgfilespos = _getfilestarts(self._cgunpacker)
407 self._cgfilespos = _getfilestarts(self._cgunpacker)
408
408
409 if f in self._cgfilespos:
409 if f in self._cgfilespos:
410 self._cgunpacker.seek(self._cgfilespos[f])
410 self._cgunpacker.seek(self._cgfilespos[f])
411 linkmapper = self.unfiltered().changelog.rev
411 linkmapper = self.unfiltered().changelog.rev
412 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
412 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
413 else:
413 else:
414 return super(bundlerepository, self).file(f)
414 return super(bundlerepository, self).file(f)
415
415
416 def close(self):
416 def close(self):
417 """Close assigned bundle file immediately."""
417 """Close assigned bundle file immediately."""
418 self._bundlefile.close()
418 self._bundlefile.close()
419 if self.tempfile is not None:
419 if self.tempfile is not None:
420 self.vfs.unlink(self.tempfile)
420 self.vfs.unlink(self.tempfile)
421 if self._tempparent:
421 if self._tempparent:
422 shutil.rmtree(self._tempparent, True)
422 shutil.rmtree(self._tempparent, True)
423
423
424 def cancopy(self):
424 def cancopy(self):
425 return False
425 return False
426
426
427 def peer(self):
427 def peer(self):
428 return bundlepeer(self)
428 return bundlepeer(self)
429
429
430 def getcwd(self):
430 def getcwd(self):
431 return encoding.getcwd() # always outside the repo
431 return encoding.getcwd() # always outside the repo
432
432
433 # Check if parents exist in localrepo before setting
433 # Check if parents exist in localrepo before setting
434 def setparents(self, p1, p2=nullid):
434 def setparents(self, p1, p2=nullid):
435 p1rev = self.changelog.rev(p1)
435 p1rev = self.changelog.rev(p1)
436 p2rev = self.changelog.rev(p2)
436 p2rev = self.changelog.rev(p2)
437 msg = _(b"setting parent to node %s that only exists in the bundle\n")
437 msg = _(b"setting parent to node %s that only exists in the bundle\n")
438 if self.changelog.repotiprev < p1rev:
438 if self.changelog.repotiprev < p1rev:
439 self.ui.warn(msg % nodemod.hex(p1))
439 self.ui.warn(msg % nodemod.hex(p1))
440 if self.changelog.repotiprev < p2rev:
440 if self.changelog.repotiprev < p2rev:
441 self.ui.warn(msg % nodemod.hex(p2))
441 self.ui.warn(msg % nodemod.hex(p2))
442 return super(bundlerepository, self).setparents(p1, p2)
442 return super(bundlerepository, self).setparents(p1, p2)
443
443
444
444
445 def instance(ui, path, create, intents=None, createopts=None):
445 def instance(ui, path, create, intents=None, createopts=None):
446 if create:
446 if create:
447 raise error.Abort(_(b'cannot create new bundle repository'))
447 raise error.Abort(_(b'cannot create new bundle repository'))
448 # internal config: bundle.mainreporoot
448 # internal config: bundle.mainreporoot
449 parentpath = ui.config(b"bundle", b"mainreporoot")
449 parentpath = ui.config(b"bundle", b"mainreporoot")
450 if not parentpath:
450 if not parentpath:
451 # try to find the correct path to the working directory repo
451 # try to find the correct path to the working directory repo
452 parentpath = cmdutil.findrepo(encoding.getcwd())
452 parentpath = cmdutil.findrepo(encoding.getcwd())
453 if parentpath is None:
453 if parentpath is None:
454 parentpath = b''
454 parentpath = b''
455 if parentpath:
455 if parentpath:
456 # Try to make the full path relative so we get a nice, short URL.
456 # Try to make the full path relative so we get a nice, short URL.
457 # In particular, we don't want temp dir names in test outputs.
457 # In particular, we don't want temp dir names in test outputs.
458 cwd = encoding.getcwd()
458 cwd = encoding.getcwd()
459 if parentpath == cwd:
459 if parentpath == cwd:
460 parentpath = b''
460 parentpath = b''
461 else:
461 else:
462 cwd = pathutil.normasprefix(cwd)
462 cwd = pathutil.normasprefix(cwd)
463 if parentpath.startswith(cwd):
463 if parentpath.startswith(cwd):
464 parentpath = parentpath[len(cwd) :]
464 parentpath = parentpath[len(cwd) :]
465 u = util.url(path)
465 u = util.url(path)
466 path = u.localpath()
466 path = u.localpath()
467 if u.scheme == b'bundle':
467 if u.scheme == b'bundle':
468 s = path.split(b"+", 1)
468 s = path.split(b"+", 1)
469 if len(s) == 1:
469 if len(s) == 1:
470 repopath, bundlename = parentpath, s[0]
470 repopath, bundlename = parentpath, s[0]
471 else:
471 else:
472 repopath, bundlename = s
472 repopath, bundlename = s
473 else:
473 else:
474 repopath, bundlename = parentpath, path
474 repopath, bundlename = parentpath, path
475
475
476 return makebundlerepository(ui, repopath, bundlename)
476 return makebundlerepository(ui, repopath, bundlename)
477
477
478
478
479 def makebundlerepository(ui, repopath, bundlepath):
479 def makebundlerepository(ui, repopath, bundlepath):
480 """Make a bundle repository object based on repo and bundle paths."""
480 """Make a bundle repository object based on repo and bundle paths."""
481 if repopath:
481 if repopath:
482 url = b'bundle:%s+%s' % (util.expandpath(repopath), bundlepath)
482 url = b'bundle:%s+%s' % (util.expandpath(repopath), bundlepath)
483 else:
483 else:
484 url = b'bundle:%s' % bundlepath
484 url = b'bundle:%s' % bundlepath
485
485
486 # Because we can't make any guarantees about the type of the base
486 # Because we can't make any guarantees about the type of the base
487 # repository, we can't have a static class representing the bundle
487 # repository, we can't have a static class representing the bundle
488 # repository. We also can't make any guarantees about how to even
488 # repository. We also can't make any guarantees about how to even
489 # call the base repository's constructor!
489 # call the base repository's constructor!
490 #
490 #
491 # So, our strategy is to go through ``localrepo.instance()`` to construct
491 # So, our strategy is to go through ``localrepo.instance()`` to construct
492 # a repo instance. Then, we dynamically create a new type derived from
492 # a repo instance. Then, we dynamically create a new type derived from
493 # both it and our ``bundlerepository`` class which overrides some
493 # both it and our ``bundlerepository`` class which overrides some
494 # functionality. We then change the type of the constructed repository
494 # functionality. We then change the type of the constructed repository
495 # to this new type and initialize the bundle-specific bits of it.
495 # to this new type and initialize the bundle-specific bits of it.
496
496
497 try:
497 try:
498 repo = localrepo.instance(ui, repopath, create=False)
498 repo = localrepo.instance(ui, repopath, create=False)
499 tempparent = None
499 tempparent = None
500 except error.RepoError:
500 except error.RepoError:
501 tempparent = pycompat.mkdtemp()
501 tempparent = pycompat.mkdtemp()
502 try:
502 try:
503 repo = localrepo.instance(ui, tempparent, create=True)
503 repo = localrepo.instance(ui, tempparent, create=True)
504 except Exception:
504 except Exception:
505 shutil.rmtree(tempparent)
505 shutil.rmtree(tempparent)
506 raise
506 raise
507
507
508 class derivedbundlerepository(bundlerepository, repo.__class__):
508 class derivedbundlerepository(bundlerepository, repo.__class__):
509 pass
509 pass
510
510
511 repo.__class__ = derivedbundlerepository
511 repo.__class__ = derivedbundlerepository
512 bundlerepository.__init__(repo, bundlepath, url, tempparent)
512 bundlerepository.__init__(repo, bundlepath, url, tempparent)
513
513
514 return repo
514 return repo
515
515
516
516
517 class bundletransactionmanager(object):
517 class bundletransactionmanager(object):
518 def transaction(self):
518 def transaction(self):
519 return None
519 return None
520
520
521 def close(self):
521 def close(self):
522 raise NotImplementedError
522 raise NotImplementedError
523
523
524 def release(self):
524 def release(self):
525 raise NotImplementedError
525 raise NotImplementedError
526
526
527
527
528 def getremotechanges(
528 def getremotechanges(
529 ui, repo, peer, onlyheads=None, bundlename=None, force=False
529 ui, repo, peer, onlyheads=None, bundlename=None, force=False
530 ):
530 ):
531 '''obtains a bundle of changes incoming from peer
531 '''obtains a bundle of changes incoming from peer
532
532
533 "onlyheads" restricts the returned changes to those reachable from the
533 "onlyheads" restricts the returned changes to those reachable from the
534 specified heads.
534 specified heads.
535 "bundlename", if given, stores the bundle to this file path permanently;
535 "bundlename", if given, stores the bundle to this file path permanently;
536 otherwise it's stored to a temp file and gets deleted again when you call
536 otherwise it's stored to a temp file and gets deleted again when you call
537 the returned "cleanupfn".
537 the returned "cleanupfn".
538 "force" indicates whether to proceed on unrelated repos.
538 "force" indicates whether to proceed on unrelated repos.
539
539
540 Returns a tuple (local, csets, cleanupfn):
540 Returns a tuple (local, csets, cleanupfn):
541
541
542 "local" is a local repo from which to obtain the actual incoming
542 "local" is a local repo from which to obtain the actual incoming
543 changesets; it is a bundlerepo for the obtained bundle when the
543 changesets; it is a bundlerepo for the obtained bundle when the
544 original "peer" is remote.
544 original "peer" is remote.
545 "csets" lists the incoming changeset node ids.
545 "csets" lists the incoming changeset node ids.
546 "cleanupfn" must be called without arguments when you're done processing
546 "cleanupfn" must be called without arguments when you're done processing
547 the changes; it closes both the original "peer" and the one returned
547 the changes; it closes both the original "peer" and the one returned
548 here.
548 here.
549 '''
549 '''
550 tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force)
550 tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force)
551 common, incoming, rheads = tmp
551 common, incoming, rheads = tmp
552 if not incoming:
552 if not incoming:
553 try:
553 try:
554 if bundlename:
554 if bundlename:
555 os.unlink(bundlename)
555 os.unlink(bundlename)
556 except OSError:
556 except OSError:
557 pass
557 pass
558 return repo, [], peer.close
558 return repo, [], peer.close
559
559
560 commonset = set(common)
560 commonset = set(common)
561 rheads = [x for x in rheads if x not in commonset]
561 rheads = [x for x in rheads if x not in commonset]
562
562
563 bundle = None
563 bundle = None
564 bundlerepo = None
564 bundlerepo = None
565 localrepo = peer.local()
565 localrepo = peer.local()
566 if bundlename or not localrepo:
566 if bundlename or not localrepo:
567 # create a bundle (uncompressed if peer repo is not local)
567 # create a bundle (uncompressed if peer repo is not local)
568
568
569 # developer config: devel.legacy.exchange
569 # developer config: devel.legacy.exchange
570 legexc = ui.configlist(b'devel', b'legacy.exchange')
570 legexc = ui.configlist(b'devel', b'legacy.exchange')
571 forcebundle1 = b'bundle2' not in legexc and b'bundle1' in legexc
571 forcebundle1 = b'bundle2' not in legexc and b'bundle1' in legexc
572 canbundle2 = (
572 canbundle2 = (
573 not forcebundle1
573 not forcebundle1
574 and peer.capable(b'getbundle')
574 and peer.capable(b'getbundle')
575 and peer.capable(b'bundle2')
575 and peer.capable(b'bundle2')
576 )
576 )
577 if canbundle2:
577 if canbundle2:
578 with peer.commandexecutor() as e:
578 with peer.commandexecutor() as e:
579 b2 = e.callcommand(
579 b2 = e.callcommand(
580 b'getbundle',
580 b'getbundle',
581 {
581 {
582 b'source': b'incoming',
582 b'source': b'incoming',
583 b'common': common,
583 b'common': common,
584 b'heads': rheads,
584 b'heads': rheads,
585 b'bundlecaps': exchange.caps20to10(
585 b'bundlecaps': exchange.caps20to10(
586 repo, role=b'client'
586 repo, role=b'client'
587 ),
587 ),
588 b'cg': True,
588 b'cg': True,
589 },
589 },
590 ).result()
590 ).result()
591
591
592 fname = bundle = changegroup.writechunks(
592 fname = bundle = changegroup.writechunks(
593 ui, b2._forwardchunks(), bundlename
593 ui, b2._forwardchunks(), bundlename
594 )
594 )
595 else:
595 else:
596 if peer.capable(b'getbundle'):
596 if peer.capable(b'getbundle'):
597 with peer.commandexecutor() as e:
597 with peer.commandexecutor() as e:
598 cg = e.callcommand(
598 cg = e.callcommand(
599 b'getbundle',
599 b'getbundle',
600 {
600 {
601 b'source': b'incoming',
601 b'source': b'incoming',
602 b'common': common,
602 b'common': common,
603 b'heads': rheads,
603 b'heads': rheads,
604 },
604 },
605 ).result()
605 ).result()
606 elif onlyheads is None and not peer.capable(b'changegroupsubset'):
606 elif onlyheads is None and not peer.capable(b'changegroupsubset'):
607 # compat with older servers when pulling all remote heads
607 # compat with older servers when pulling all remote heads
608
608
609 with peer.commandexecutor() as e:
609 with peer.commandexecutor() as e:
610 cg = e.callcommand(
610 cg = e.callcommand(
611 b'changegroup',
611 b'changegroup',
612 {b'nodes': incoming, b'source': b'incoming',},
612 {b'nodes': incoming, b'source': b'incoming',},
613 ).result()
613 ).result()
614
614
615 rheads = None
615 rheads = None
616 else:
616 else:
617 with peer.commandexecutor() as e:
617 with peer.commandexecutor() as e:
618 cg = e.callcommand(
618 cg = e.callcommand(
619 b'changegroupsubset',
619 b'changegroupsubset',
620 {
620 {
621 b'bases': incoming,
621 b'bases': incoming,
622 b'heads': rheads,
622 b'heads': rheads,
623 b'source': b'incoming',
623 b'source': b'incoming',
624 },
624 },
625 ).result()
625 ).result()
626
626
627 if localrepo:
627 if localrepo:
628 bundletype = b"HG10BZ"
628 bundletype = b"HG10BZ"
629 else:
629 else:
630 bundletype = b"HG10UN"
630 bundletype = b"HG10UN"
631 fname = bundle = bundle2.writebundle(ui, cg, bundlename, bundletype)
631 fname = bundle = bundle2.writebundle(ui, cg, bundlename, bundletype)
632 # keep written bundle?
632 # keep written bundle?
633 if bundlename:
633 if bundlename:
634 bundle = None
634 bundle = None
635 if not localrepo:
635 if not localrepo:
636 # use the created uncompressed bundlerepo
636 # use the created uncompressed bundlerepo
637 localrepo = bundlerepo = makebundlerepository(
637 localrepo = bundlerepo = makebundlerepository(
638 repo.baseui, repo.root, fname
638 repo.baseui, repo.root, fname
639 )
639 )
640
640
641 # this repo contains local and peer now, so filter out local again
641 # this repo contains local and peer now, so filter out local again
642 common = repo.heads()
642 common = repo.heads()
643 if localrepo:
643 if localrepo:
644 # Part of common may be remotely filtered
644 # Part of common may be remotely filtered
645 # So use an unfiltered version
645 # So use an unfiltered version
646 # The discovery process probably need cleanup to avoid that
646 # The discovery process probably need cleanup to avoid that
647 localrepo = localrepo.unfiltered()
647 localrepo = localrepo.unfiltered()
648
648
649 csets = localrepo.changelog.findmissing(common, rheads)
649 csets = localrepo.changelog.findmissing(common, rheads)
650
650
651 if bundlerepo:
651 if bundlerepo:
652 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]]
652 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]]
653
653
654 with peer.commandexecutor() as e:
654 with peer.commandexecutor() as e:
655 remotephases = e.callcommand(
655 remotephases = e.callcommand(
656 b'listkeys', {b'namespace': b'phases',}
656 b'listkeys', {b'namespace': b'phases',}
657 ).result()
657 ).result()
658
658
659 pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes)
659 pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes)
660 pullop.trmanager = bundletransactionmanager()
660 pullop.trmanager = bundletransactionmanager()
661 exchange._pullapplyphases(pullop, remotephases)
661 exchange._pullapplyphases(pullop, remotephases)
662
662
663 def cleanup():
663 def cleanup():
664 if bundlerepo:
664 if bundlerepo:
665 bundlerepo.close()
665 bundlerepo.close()
666 if bundle:
666 if bundle:
667 os.unlink(bundle)
667 os.unlink(bundle)
668 peer.close()
668 peer.close()
669
669
670 return (localrepo, csets, cleanup)
670 return (localrepo, csets, cleanup)
@@ -1,227 +1,227 b''
1 # pvec.py - probabilistic vector clocks for Mercurial
1 # pvec.py - probabilistic vector clocks for Mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''
8 '''
9 A "pvec" is a changeset property based on the theory of vector clocks
9 A "pvec" is a changeset property based on the theory of vector clocks
10 that can be compared to discover relatedness without consulting a
10 that can be compared to discover relatedness without consulting a
11 graph. This can be useful for tasks like determining how a
11 graph. This can be useful for tasks like determining how a
12 disconnected patch relates to a repository.
12 disconnected patch relates to a repository.
13
13
14 Currently a pvec consist of 448 bits, of which 24 are 'depth' and the
14 Currently a pvec consist of 448 bits, of which 24 are 'depth' and the
15 remainder are a bit vector. It is represented as a 70-character base85
15 remainder are a bit vector. It is represented as a 70-character base85
16 string.
16 string.
17
17
18 Construction:
18 Construction:
19
19
20 - a root changeset has a depth of 0 and a bit vector based on its hash
20 - a root changeset has a depth of 0 and a bit vector based on its hash
21 - a normal commit has a changeset where depth is increased by one and
21 - a normal commit has a changeset where depth is increased by one and
22 one bit vector bit is flipped based on its hash
22 one bit vector bit is flipped based on its hash
23 - a merge changeset pvec is constructed by copying changes from one pvec into
23 - a merge changeset pvec is constructed by copying changes from one pvec into
24 the other to balance its depth
24 the other to balance its depth
25
25
26 Properties:
26 Properties:
27
27
28 - for linear changes, difference in depth is always <= hamming distance
28 - for linear changes, difference in depth is always <= hamming distance
29 - otherwise, changes are probably divergent
29 - otherwise, changes are probably divergent
30 - when hamming distance is < 200, we can reliably detect when pvecs are near
30 - when hamming distance is < 200, we can reliably detect when pvecs are near
31
31
32 Issues:
32 Issues:
33
33
34 - hamming distance ceases to work over distances of ~ 200
34 - hamming distance ceases to work over distances of ~ 200
35 - detecting divergence is less accurate when the common ancestor is very close
35 - detecting divergence is less accurate when the common ancestor is very close
36 to either revision or total distance is high
36 to either revision or total distance is high
37 - this could probably be improved by modeling the relation between
37 - this could probably be improved by modeling the relation between
38 delta and hdist
38 delta and hdist
39
39
40 Uses:
40 Uses:
41
41
42 - a patch pvec can be used to locate the nearest available common ancestor for
42 - a patch pvec can be used to locate the nearest available common ancestor for
43 resolving conflicts
43 resolving conflicts
44 - ordering of patches can be established without a DAG
44 - ordering of patches can be established without a DAG
45 - two head pvecs can be compared to determine whether push/pull/merge is needed
45 - two head pvecs can be compared to determine whether push/pull/merge is needed
46 and approximately how many changesets are involved
46 and approximately how many changesets are involved
47 - can be used to find a heuristic divergence measure between changesets on
47 - can be used to find a heuristic divergence measure between changesets on
48 different branches
48 different branches
49 '''
49 '''
50
50
51 from __future__ import absolute_import
51 from __future__ import absolute_import
52
52
53 from .node import nullrev
53 from .node import nullrev
54 from . import (
54 from . import (
55 pycompat,
55 pycompat,
56 util,
56 util,
57 )
57 )
58
58
59 _size = 448 # 70 chars b85-encoded
59 _size = 448 # 70 chars b85-encoded
60 _bytes = _size / 8
60 _bytes = _size / 8
61 _depthbits = 24
61 _depthbits = 24
62 _depthbytes = _depthbits / 8
62 _depthbytes = _depthbits / 8
63 _vecbytes = _bytes - _depthbytes
63 _vecbytes = _bytes - _depthbytes
64 _vecbits = _vecbytes * 8
64 _vecbits = _vecbytes * 8
65 _radius = (_vecbits - 30) / 2 # high probability vectors are related
65 _radius = (_vecbits - 30) / 2 # high probability vectors are related
66
66
67
67
68 def _bin(bs):
68 def _bin(bs):
69 '''convert a bytestring to a long'''
69 '''convert a bytestring to a long'''
70 v = 0
70 v = 0
71 for b in bs:
71 for b in bs:
72 v = v * 256 + ord(b)
72 v = v * 256 + ord(b)
73 return v
73 return v
74
74
75
75
76 def _str(v, l):
76 def _str(v, l):
77 bs = b""
77 bs = b""
78 for p in pycompat.xrange(l):
78 for p in pycompat.xrange(l):
79 bs = chr(v & 255) + bs
79 bs = chr(v & 255) + bs
80 v >>= 8
80 v >>= 8
81 return bs
81 return bs
82
82
83
83
84 def _split(b):
84 def _split(b):
85 '''depth and bitvec'''
85 '''depth and bitvec'''
86 return _bin(b[:_depthbytes]), _bin(b[_depthbytes:])
86 return _bin(b[:_depthbytes]), _bin(b[_depthbytes:])
87
87
88
88
89 def _join(depth, bitvec):
89 def _join(depth, bitvec):
90 return _str(depth, _depthbytes) + _str(bitvec, _vecbytes)
90 return _str(depth, _depthbytes) + _str(bitvec, _vecbytes)
91
91
92
92
93 def _hweight(x):
93 def _hweight(x):
94 c = 0
94 c = 0
95 while x:
95 while x:
96 if x & 1:
96 if x & 1:
97 c += 1
97 c += 1
98 x >>= 1
98 x >>= 1
99 return c
99 return c
100
100
101
101
102 _htab = [_hweight(x) for x in pycompat.xrange(256)]
102 _htab = [_hweight(x) for x in pycompat.xrange(256)]
103
103
104
104
105 def _hamming(a, b):
105 def _hamming(a, b):
106 '''find the hamming distance between two longs'''
106 '''find the hamming distance between two longs'''
107 d = a ^ b
107 d = a ^ b
108 c = 0
108 c = 0
109 while d:
109 while d:
110 c += _htab[d & 0xFF]
110 c += _htab[d & 0xFF]
111 d >>= 8
111 d >>= 8
112 return c
112 return c
113
113
114
114
115 def _mergevec(x, y, c):
115 def _mergevec(x, y, c):
116 # Ideally, this function would be x ^ y ^ ancestor, but finding
116 # Ideally, this function would be x ^ y ^ ancestor, but finding
117 # ancestors is a nuisance. So instead we find the minimal number
117 # ancestors is a nuisance. So instead we find the minimal number
118 # of changes to balance the depth and hamming distance
118 # of changes to balance the depth and hamming distance
119
119
120 d1, v1 = x
120 d1, v1 = x
121 d2, v2 = y
121 d2, v2 = y
122 if d1 < d2:
122 if d1 < d2:
123 d1, d2, v1, v2 = d2, d1, v2, v1
123 d1, d2, v1, v2 = d2, d1, v2, v1
124
124
125 hdist = _hamming(v1, v2)
125 hdist = _hamming(v1, v2)
126 ddist = d1 - d2
126 ddist = d1 - d2
127 v = v1
127 v = v1
128 m = v1 ^ v2 # mask of different bits
128 m = v1 ^ v2 # mask of different bits
129 i = 1
129 i = 1
130
130
131 if hdist > ddist:
131 if hdist > ddist:
132 # if delta = 10 and hdist = 100, then we need to go up 55 steps
132 # if delta = 10 and hdist = 100, then we need to go up 55 steps
133 # to the ancestor and down 45
133 # to the ancestor and down 45
134 changes = (hdist - ddist + 1) / 2
134 changes = (hdist - ddist + 1) / 2
135 else:
135 else:
136 # must make at least one change
136 # must make at least one change
137 changes = 1
137 changes = 1
138 depth = d1 + changes
138 depth = d1 + changes
139
139
140 # copy changes from v2
140 # copy changes from v2
141 if m:
141 if m:
142 while changes:
142 while changes:
143 if m & i:
143 if m & i:
144 v ^= i
144 v ^= i
145 changes -= 1
145 changes -= 1
146 i <<= 1
146 i <<= 1
147 else:
147 else:
148 v = _flipbit(v, c)
148 v = _flipbit(v, c)
149
149
150 return depth, v
150 return depth, v
151
151
152
152
153 def _flipbit(v, node):
153 def _flipbit(v, node):
154 # converting bit strings to longs is slow
154 # converting bit strings to longs is slow
155 bit = (hash(node) & 0xFFFFFFFF) % _vecbits
155 bit = (hash(node) & 0xFFFFFFFF) % _vecbits
156 return v ^ (1 << bit)
156 return v ^ (1 << bit)
157
157
158
158
159 def ctxpvec(ctx):
159 def ctxpvec(ctx):
160 '''construct a pvec for ctx while filling in the cache'''
160 '''construct a pvec for ctx while filling in the cache'''
161 r = ctx.repo()
161 r = ctx.repo()
162 if not util.safehasattr(r, b"_pveccache"):
162 if not util.safehasattr(r, "_pveccache"):
163 r._pveccache = {}
163 r._pveccache = {}
164 pvc = r._pveccache
164 pvc = r._pveccache
165 if ctx.rev() not in pvc:
165 if ctx.rev() not in pvc:
166 cl = r.changelog
166 cl = r.changelog
167 for n in pycompat.xrange(ctx.rev() + 1):
167 for n in pycompat.xrange(ctx.rev() + 1):
168 if n not in pvc:
168 if n not in pvc:
169 node = cl.node(n)
169 node = cl.node(n)
170 p1, p2 = cl.parentrevs(n)
170 p1, p2 = cl.parentrevs(n)
171 if p1 == nullrev:
171 if p1 == nullrev:
172 # start with a 'random' vector at root
172 # start with a 'random' vector at root
173 pvc[n] = (0, _bin((node * 3)[:_vecbytes]))
173 pvc[n] = (0, _bin((node * 3)[:_vecbytes]))
174 elif p2 == nullrev:
174 elif p2 == nullrev:
175 d, v = pvc[p1]
175 d, v = pvc[p1]
176 pvc[n] = (d + 1, _flipbit(v, node))
176 pvc[n] = (d + 1, _flipbit(v, node))
177 else:
177 else:
178 pvc[n] = _mergevec(pvc[p1], pvc[p2], node)
178 pvc[n] = _mergevec(pvc[p1], pvc[p2], node)
179 bs = _join(*pvc[ctx.rev()])
179 bs = _join(*pvc[ctx.rev()])
180 return pvec(util.b85encode(bs))
180 return pvec(util.b85encode(bs))
181
181
182
182
183 class pvec(object):
183 class pvec(object):
184 def __init__(self, hashorctx):
184 def __init__(self, hashorctx):
185 if isinstance(hashorctx, str):
185 if isinstance(hashorctx, str):
186 self._bs = hashorctx
186 self._bs = hashorctx
187 self._depth, self._vec = _split(util.b85decode(hashorctx))
187 self._depth, self._vec = _split(util.b85decode(hashorctx))
188 else:
188 else:
189 self._vec = ctxpvec(hashorctx)
189 self._vec = ctxpvec(hashorctx)
190
190
191 def __str__(self):
191 def __str__(self):
192 return self._bs
192 return self._bs
193
193
194 def __eq__(self, b):
194 def __eq__(self, b):
195 return self._vec == b._vec and self._depth == b._depth
195 return self._vec == b._vec and self._depth == b._depth
196
196
197 def __lt__(self, b):
197 def __lt__(self, b):
198 delta = b._depth - self._depth
198 delta = b._depth - self._depth
199 if delta < 0:
199 if delta < 0:
200 return False # always correct
200 return False # always correct
201 if _hamming(self._vec, b._vec) > delta:
201 if _hamming(self._vec, b._vec) > delta:
202 return False
202 return False
203 return True
203 return True
204
204
205 def __gt__(self, b):
205 def __gt__(self, b):
206 return b < self
206 return b < self
207
207
208 def __or__(self, b):
208 def __or__(self, b):
209 delta = abs(b._depth - self._depth)
209 delta = abs(b._depth - self._depth)
210 if _hamming(self._vec, b._vec) <= delta:
210 if _hamming(self._vec, b._vec) <= delta:
211 return False
211 return False
212 return True
212 return True
213
213
214 def __sub__(self, b):
214 def __sub__(self, b):
215 if self | b:
215 if self | b:
216 raise ValueError(b"concurrent pvecs")
216 raise ValueError(b"concurrent pvecs")
217 return self._depth - b._depth
217 return self._depth - b._depth
218
218
219 def distance(self, b):
219 def distance(self, b):
220 d = abs(b._depth - self._depth)
220 d = abs(b._depth - self._depth)
221 h = _hamming(self._vec, b._vec)
221 h = _hamming(self._vec, b._vec)
222 return max(d, h)
222 return max(d, h)
223
223
224 def near(self, b):
224 def near(self, b):
225 dist = abs(b.depth - self._depth)
225 dist = abs(b.depth - self._depth)
226 if dist > _radius or _hamming(self._vec, b._vec) > _radius:
226 if dist > _radius or _hamming(self._vec, b._vec) > _radius:
227 return False
227 return False
@@ -1,534 +1,534 b''
1 # registrar.py - utilities to register function for specific purpose
1 # registrar.py - utilities to register function for specific purpose
2 #
2 #
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
3 # Copyright FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from . import (
10 from . import (
11 configitems,
11 configitems,
12 error,
12 error,
13 pycompat,
13 pycompat,
14 util,
14 util,
15 )
15 )
16
16
17 # unlike the other registered items, config options are neither functions or
17 # unlike the other registered items, config options are neither functions or
18 # classes. Registering the option is just small function call.
18 # classes. Registering the option is just small function call.
19 #
19 #
20 # We still add the official API to the registrar module for consistency with
20 # We still add the official API to the registrar module for consistency with
21 # the other items extensions want might to register.
21 # the other items extensions want might to register.
22 configitem = configitems.getitemregister
22 configitem = configitems.getitemregister
23
23
24
24
25 class _funcregistrarbase(object):
25 class _funcregistrarbase(object):
26 """Base of decorator to register a function for specific purpose
26 """Base of decorator to register a function for specific purpose
27
27
28 This decorator stores decorated functions into own dict 'table'.
28 This decorator stores decorated functions into own dict 'table'.
29
29
30 The least derived class can be defined by overriding 'formatdoc',
30 The least derived class can be defined by overriding 'formatdoc',
31 for example::
31 for example::
32
32
33 class keyword(_funcregistrarbase):
33 class keyword(_funcregistrarbase):
34 _docformat = ":%s: %s"
34 _docformat = ":%s: %s"
35
35
36 This should be used as below:
36 This should be used as below:
37
37
38 keyword = registrar.keyword()
38 keyword = registrar.keyword()
39
39
40 @keyword('bar')
40 @keyword('bar')
41 def barfunc(*args, **kwargs):
41 def barfunc(*args, **kwargs):
42 '''Explanation of bar keyword ....
42 '''Explanation of bar keyword ....
43 '''
43 '''
44 pass
44 pass
45
45
46 In this case:
46 In this case:
47
47
48 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
48 - 'barfunc' is stored as 'bar' in '_table' of an instance 'keyword' above
49 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
49 - 'barfunc.__doc__' becomes ":bar: Explanation of bar keyword"
50 """
50 """
51
51
52 def __init__(self, table=None):
52 def __init__(self, table=None):
53 if table is None:
53 if table is None:
54 self._table = {}
54 self._table = {}
55 else:
55 else:
56 self._table = table
56 self._table = table
57
57
58 def __call__(self, decl, *args, **kwargs):
58 def __call__(self, decl, *args, **kwargs):
59 return lambda func: self._doregister(func, decl, *args, **kwargs)
59 return lambda func: self._doregister(func, decl, *args, **kwargs)
60
60
61 def _doregister(self, func, decl, *args, **kwargs):
61 def _doregister(self, func, decl, *args, **kwargs):
62 name = self._getname(decl)
62 name = self._getname(decl)
63
63
64 if name in self._table:
64 if name in self._table:
65 msg = b'duplicate registration for name: "%s"' % name
65 msg = b'duplicate registration for name: "%s"' % name
66 raise error.ProgrammingError(msg)
66 raise error.ProgrammingError(msg)
67
67
68 if func.__doc__ and not util.safehasattr(func, b'_origdoc'):
68 if func.__doc__ and not util.safehasattr(func, '_origdoc'):
69 func._origdoc = func.__doc__.strip()
69 func._origdoc = func.__doc__.strip()
70 doc = pycompat.sysbytes(func._origdoc)
70 doc = pycompat.sysbytes(func._origdoc)
71 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
71 func.__doc__ = pycompat.sysstr(self._formatdoc(decl, doc))
72
72
73 self._table[name] = func
73 self._table[name] = func
74 self._extrasetup(name, func, *args, **kwargs)
74 self._extrasetup(name, func, *args, **kwargs)
75
75
76 return func
76 return func
77
77
78 def _merge(self, registrarbase):
78 def _merge(self, registrarbase):
79 """Merge the entries of the given registrar object into this one.
79 """Merge the entries of the given registrar object into this one.
80
80
81 The other registrar object must not contain any entries already in the
81 The other registrar object must not contain any entries already in the
82 current one, or a ProgrammmingError is raised. Additionally, the types
82 current one, or a ProgrammmingError is raised. Additionally, the types
83 of the two registrars must match.
83 of the two registrars must match.
84 """
84 """
85 if not isinstance(registrarbase, type(self)):
85 if not isinstance(registrarbase, type(self)):
86 msg = b"cannot merge different types of registrar"
86 msg = b"cannot merge different types of registrar"
87 raise error.ProgrammingError(msg)
87 raise error.ProgrammingError(msg)
88
88
89 dups = set(registrarbase._table).intersection(self._table)
89 dups = set(registrarbase._table).intersection(self._table)
90
90
91 if dups:
91 if dups:
92 msg = b'duplicate registration for names: "%s"' % b'", "'.join(dups)
92 msg = b'duplicate registration for names: "%s"' % b'", "'.join(dups)
93 raise error.ProgrammingError(msg)
93 raise error.ProgrammingError(msg)
94
94
95 self._table.update(registrarbase._table)
95 self._table.update(registrarbase._table)
96
96
97 def _parsefuncdecl(self, decl):
97 def _parsefuncdecl(self, decl):
98 """Parse function declaration and return the name of function in it
98 """Parse function declaration and return the name of function in it
99 """
99 """
100 i = decl.find(b'(')
100 i = decl.find(b'(')
101 if i >= 0:
101 if i >= 0:
102 return decl[:i]
102 return decl[:i]
103 else:
103 else:
104 return decl
104 return decl
105
105
106 def _getname(self, decl):
106 def _getname(self, decl):
107 """Return the name of the registered function from decl
107 """Return the name of the registered function from decl
108
108
109 Derived class should override this, if it allows more
109 Derived class should override this, if it allows more
110 descriptive 'decl' string than just a name.
110 descriptive 'decl' string than just a name.
111 """
111 """
112 return decl
112 return decl
113
113
114 _docformat = None
114 _docformat = None
115
115
116 def _formatdoc(self, decl, doc):
116 def _formatdoc(self, decl, doc):
117 """Return formatted document of the registered function for help
117 """Return formatted document of the registered function for help
118
118
119 'doc' is '__doc__.strip()' of the registered function.
119 'doc' is '__doc__.strip()' of the registered function.
120 """
120 """
121 return self._docformat % (decl, doc)
121 return self._docformat % (decl, doc)
122
122
123 def _extrasetup(self, name, func):
123 def _extrasetup(self, name, func):
124 """Execute exra setup for registered function, if needed
124 """Execute exra setup for registered function, if needed
125 """
125 """
126
126
127
127
128 class command(_funcregistrarbase):
128 class command(_funcregistrarbase):
129 """Decorator to register a command function to table
129 """Decorator to register a command function to table
130
130
131 This class receives a command table as its argument. The table should
131 This class receives a command table as its argument. The table should
132 be a dict.
132 be a dict.
133
133
134 The created object can be used as a decorator for adding commands to
134 The created object can be used as a decorator for adding commands to
135 that command table. This accepts multiple arguments to define a command.
135 that command table. This accepts multiple arguments to define a command.
136
136
137 The first argument is the command name (as bytes).
137 The first argument is the command name (as bytes).
138
138
139 The `options` keyword argument is an iterable of tuples defining command
139 The `options` keyword argument is an iterable of tuples defining command
140 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
140 arguments. See ``mercurial.fancyopts.fancyopts()`` for the format of each
141 tuple.
141 tuple.
142
142
143 The `synopsis` argument defines a short, one line summary of how to use the
143 The `synopsis` argument defines a short, one line summary of how to use the
144 command. This shows up in the help output.
144 command. This shows up in the help output.
145
145
146 There are three arguments that control what repository (if any) is found
146 There are three arguments that control what repository (if any) is found
147 and passed to the decorated function: `norepo`, `optionalrepo`, and
147 and passed to the decorated function: `norepo`, `optionalrepo`, and
148 `inferrepo`.
148 `inferrepo`.
149
149
150 The `norepo` argument defines whether the command does not require a
150 The `norepo` argument defines whether the command does not require a
151 local repository. Most commands operate against a repository, thus the
151 local repository. Most commands operate against a repository, thus the
152 default is False. When True, no repository will be passed.
152 default is False. When True, no repository will be passed.
153
153
154 The `optionalrepo` argument defines whether the command optionally requires
154 The `optionalrepo` argument defines whether the command optionally requires
155 a local repository. If no repository can be found, None will be passed
155 a local repository. If no repository can be found, None will be passed
156 to the decorated function.
156 to the decorated function.
157
157
158 The `inferrepo` argument defines whether to try to find a repository from
158 The `inferrepo` argument defines whether to try to find a repository from
159 the command line arguments. If True, arguments will be examined for
159 the command line arguments. If True, arguments will be examined for
160 potential repository locations. See ``findrepo()``. If a repository is
160 potential repository locations. See ``findrepo()``. If a repository is
161 found, it will be used and passed to the decorated function.
161 found, it will be used and passed to the decorated function.
162
162
163 The `intents` argument defines a set of intended actions or capabilities
163 The `intents` argument defines a set of intended actions or capabilities
164 the command is taking. These intents can be used to affect the construction
164 the command is taking. These intents can be used to affect the construction
165 of the repository object passed to the command. For example, commands
165 of the repository object passed to the command. For example, commands
166 declaring that they are read-only could receive a repository that doesn't
166 declaring that they are read-only could receive a repository that doesn't
167 have any methods allowing repository mutation. Other intents could be used
167 have any methods allowing repository mutation. Other intents could be used
168 to prevent the command from running if the requested intent could not be
168 to prevent the command from running if the requested intent could not be
169 fulfilled.
169 fulfilled.
170
170
171 If `helpcategory` is set (usually to one of the constants in the help
171 If `helpcategory` is set (usually to one of the constants in the help
172 module), the command will be displayed under that category in the help's
172 module), the command will be displayed under that category in the help's
173 list of commands.
173 list of commands.
174
174
175 The following intents are defined:
175 The following intents are defined:
176
176
177 readonly
177 readonly
178 The command is read-only
178 The command is read-only
179
179
180 The signature of the decorated function looks like this:
180 The signature of the decorated function looks like this:
181 def cmd(ui[, repo] [, <args>] [, <options>])
181 def cmd(ui[, repo] [, <args>] [, <options>])
182
182
183 `repo` is required if `norepo` is False.
183 `repo` is required if `norepo` is False.
184 `<args>` are positional args (or `*args`) arguments, of non-option
184 `<args>` are positional args (or `*args`) arguments, of non-option
185 arguments from the command line.
185 arguments from the command line.
186 `<options>` are keyword arguments (or `**options`) of option arguments
186 `<options>` are keyword arguments (or `**options`) of option arguments
187 from the command line.
187 from the command line.
188
188
189 See the WritingExtensions and MercurialApi documentation for more exhaustive
189 See the WritingExtensions and MercurialApi documentation for more exhaustive
190 descriptions and examples.
190 descriptions and examples.
191 """
191 """
192
192
193 # Command categories for grouping them in help output.
193 # Command categories for grouping them in help output.
194 # These can also be specified for aliases, like:
194 # These can also be specified for aliases, like:
195 # [alias]
195 # [alias]
196 # myalias = something
196 # myalias = something
197 # myalias:category = repo
197 # myalias:category = repo
198 CATEGORY_REPO_CREATION = b'repo'
198 CATEGORY_REPO_CREATION = b'repo'
199 CATEGORY_REMOTE_REPO_MANAGEMENT = b'remote'
199 CATEGORY_REMOTE_REPO_MANAGEMENT = b'remote'
200 CATEGORY_COMMITTING = b'commit'
200 CATEGORY_COMMITTING = b'commit'
201 CATEGORY_CHANGE_MANAGEMENT = b'management'
201 CATEGORY_CHANGE_MANAGEMENT = b'management'
202 CATEGORY_CHANGE_ORGANIZATION = b'organization'
202 CATEGORY_CHANGE_ORGANIZATION = b'organization'
203 CATEGORY_FILE_CONTENTS = b'files'
203 CATEGORY_FILE_CONTENTS = b'files'
204 CATEGORY_CHANGE_NAVIGATION = b'navigation'
204 CATEGORY_CHANGE_NAVIGATION = b'navigation'
205 CATEGORY_WORKING_DIRECTORY = b'wdir'
205 CATEGORY_WORKING_DIRECTORY = b'wdir'
206 CATEGORY_IMPORT_EXPORT = b'import'
206 CATEGORY_IMPORT_EXPORT = b'import'
207 CATEGORY_MAINTENANCE = b'maintenance'
207 CATEGORY_MAINTENANCE = b'maintenance'
208 CATEGORY_HELP = b'help'
208 CATEGORY_HELP = b'help'
209 CATEGORY_MISC = b'misc'
209 CATEGORY_MISC = b'misc'
210 CATEGORY_NONE = b'none'
210 CATEGORY_NONE = b'none'
211
211
212 def _doregister(
212 def _doregister(
213 self,
213 self,
214 func,
214 func,
215 name,
215 name,
216 options=(),
216 options=(),
217 synopsis=None,
217 synopsis=None,
218 norepo=False,
218 norepo=False,
219 optionalrepo=False,
219 optionalrepo=False,
220 inferrepo=False,
220 inferrepo=False,
221 intents=None,
221 intents=None,
222 helpcategory=None,
222 helpcategory=None,
223 helpbasic=False,
223 helpbasic=False,
224 ):
224 ):
225 func.norepo = norepo
225 func.norepo = norepo
226 func.optionalrepo = optionalrepo
226 func.optionalrepo = optionalrepo
227 func.inferrepo = inferrepo
227 func.inferrepo = inferrepo
228 func.intents = intents or set()
228 func.intents = intents or set()
229 func.helpcategory = helpcategory
229 func.helpcategory = helpcategory
230 func.helpbasic = helpbasic
230 func.helpbasic = helpbasic
231 if synopsis:
231 if synopsis:
232 self._table[name] = func, list(options), synopsis
232 self._table[name] = func, list(options), synopsis
233 else:
233 else:
234 self._table[name] = func, list(options)
234 self._table[name] = func, list(options)
235 return func
235 return func
236
236
237
237
238 INTENT_READONLY = b'readonly'
238 INTENT_READONLY = b'readonly'
239
239
240
240
241 class revsetpredicate(_funcregistrarbase):
241 class revsetpredicate(_funcregistrarbase):
242 """Decorator to register revset predicate
242 """Decorator to register revset predicate
243
243
244 Usage::
244 Usage::
245
245
246 revsetpredicate = registrar.revsetpredicate()
246 revsetpredicate = registrar.revsetpredicate()
247
247
248 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
248 @revsetpredicate('mypredicate(arg1, arg2[, arg3])')
249 def mypredicatefunc(repo, subset, x):
249 def mypredicatefunc(repo, subset, x):
250 '''Explanation of this revset predicate ....
250 '''Explanation of this revset predicate ....
251 '''
251 '''
252 pass
252 pass
253
253
254 The first string argument is used also in online help.
254 The first string argument is used also in online help.
255
255
256 Optional argument 'safe' indicates whether a predicate is safe for
256 Optional argument 'safe' indicates whether a predicate is safe for
257 DoS attack (False by default).
257 DoS attack (False by default).
258
258
259 Optional argument 'takeorder' indicates whether a predicate function
259 Optional argument 'takeorder' indicates whether a predicate function
260 takes ordering policy as the last argument.
260 takes ordering policy as the last argument.
261
261
262 Optional argument 'weight' indicates the estimated run-time cost, useful
262 Optional argument 'weight' indicates the estimated run-time cost, useful
263 for static optimization, default is 1. Higher weight means more expensive.
263 for static optimization, default is 1. Higher weight means more expensive.
264 Usually, revsets that are fast and return only one revision has a weight of
264 Usually, revsets that are fast and return only one revision has a weight of
265 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
265 0.5 (ex. a symbol); revsets with O(changelog) complexity and read only the
266 changelog have weight 10 (ex. author); revsets reading manifest deltas have
266 changelog have weight 10 (ex. author); revsets reading manifest deltas have
267 weight 30 (ex. adds); revset reading manifest contents have weight 100
267 weight 30 (ex. adds); revset reading manifest contents have weight 100
268 (ex. contains). Note: those values are flexible. If the revset has a
268 (ex. contains). Note: those values are flexible. If the revset has a
269 same big-O time complexity as 'contains', but with a smaller constant, it
269 same big-O time complexity as 'contains', but with a smaller constant, it
270 might have a weight of 90.
270 might have a weight of 90.
271
271
272 'revsetpredicate' instance in example above can be used to
272 'revsetpredicate' instance in example above can be used to
273 decorate multiple functions.
273 decorate multiple functions.
274
274
275 Decorated functions are registered automatically at loading
275 Decorated functions are registered automatically at loading
276 extension, if an instance named as 'revsetpredicate' is used for
276 extension, if an instance named as 'revsetpredicate' is used for
277 decorating in extension.
277 decorating in extension.
278
278
279 Otherwise, explicit 'revset.loadpredicate()' is needed.
279 Otherwise, explicit 'revset.loadpredicate()' is needed.
280 """
280 """
281
281
282 _getname = _funcregistrarbase._parsefuncdecl
282 _getname = _funcregistrarbase._parsefuncdecl
283 _docformat = b"``%s``\n %s"
283 _docformat = b"``%s``\n %s"
284
284
285 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
285 def _extrasetup(self, name, func, safe=False, takeorder=False, weight=1):
286 func._safe = safe
286 func._safe = safe
287 func._takeorder = takeorder
287 func._takeorder = takeorder
288 func._weight = weight
288 func._weight = weight
289
289
290
290
291 class filesetpredicate(_funcregistrarbase):
291 class filesetpredicate(_funcregistrarbase):
292 """Decorator to register fileset predicate
292 """Decorator to register fileset predicate
293
293
294 Usage::
294 Usage::
295
295
296 filesetpredicate = registrar.filesetpredicate()
296 filesetpredicate = registrar.filesetpredicate()
297
297
298 @filesetpredicate('mypredicate()')
298 @filesetpredicate('mypredicate()')
299 def mypredicatefunc(mctx, x):
299 def mypredicatefunc(mctx, x):
300 '''Explanation of this fileset predicate ....
300 '''Explanation of this fileset predicate ....
301 '''
301 '''
302 pass
302 pass
303
303
304 The first string argument is used also in online help.
304 The first string argument is used also in online help.
305
305
306 Optional argument 'callstatus' indicates whether a predicate
306 Optional argument 'callstatus' indicates whether a predicate
307 implies 'matchctx.status()' at runtime or not (False, by
307 implies 'matchctx.status()' at runtime or not (False, by
308 default).
308 default).
309
309
310 Optional argument 'weight' indicates the estimated run-time cost, useful
310 Optional argument 'weight' indicates the estimated run-time cost, useful
311 for static optimization, default is 1. Higher weight means more expensive.
311 for static optimization, default is 1. Higher weight means more expensive.
312 There are predefined weights in the 'filesetlang' module.
312 There are predefined weights in the 'filesetlang' module.
313
313
314 ====== =============================================================
314 ====== =============================================================
315 Weight Description and examples
315 Weight Description and examples
316 ====== =============================================================
316 ====== =============================================================
317 0.5 basic match patterns (e.g. a symbol)
317 0.5 basic match patterns (e.g. a symbol)
318 10 computing status (e.g. added()) or accessing a few files
318 10 computing status (e.g. added()) or accessing a few files
319 30 reading file content for each (e.g. grep())
319 30 reading file content for each (e.g. grep())
320 50 scanning working directory (ignored())
320 50 scanning working directory (ignored())
321 ====== =============================================================
321 ====== =============================================================
322
322
323 'filesetpredicate' instance in example above can be used to
323 'filesetpredicate' instance in example above can be used to
324 decorate multiple functions.
324 decorate multiple functions.
325
325
326 Decorated functions are registered automatically at loading
326 Decorated functions are registered automatically at loading
327 extension, if an instance named as 'filesetpredicate' is used for
327 extension, if an instance named as 'filesetpredicate' is used for
328 decorating in extension.
328 decorating in extension.
329
329
330 Otherwise, explicit 'fileset.loadpredicate()' is needed.
330 Otherwise, explicit 'fileset.loadpredicate()' is needed.
331 """
331 """
332
332
333 _getname = _funcregistrarbase._parsefuncdecl
333 _getname = _funcregistrarbase._parsefuncdecl
334 _docformat = b"``%s``\n %s"
334 _docformat = b"``%s``\n %s"
335
335
336 def _extrasetup(self, name, func, callstatus=False, weight=1):
336 def _extrasetup(self, name, func, callstatus=False, weight=1):
337 func._callstatus = callstatus
337 func._callstatus = callstatus
338 func._weight = weight
338 func._weight = weight
339
339
340
340
341 class _templateregistrarbase(_funcregistrarbase):
341 class _templateregistrarbase(_funcregistrarbase):
342 """Base of decorator to register functions as template specific one
342 """Base of decorator to register functions as template specific one
343 """
343 """
344
344
345 _docformat = b":%s: %s"
345 _docformat = b":%s: %s"
346
346
347
347
348 class templatekeyword(_templateregistrarbase):
348 class templatekeyword(_templateregistrarbase):
349 """Decorator to register template keyword
349 """Decorator to register template keyword
350
350
351 Usage::
351 Usage::
352
352
353 templatekeyword = registrar.templatekeyword()
353 templatekeyword = registrar.templatekeyword()
354
354
355 # new API (since Mercurial 4.6)
355 # new API (since Mercurial 4.6)
356 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
356 @templatekeyword('mykeyword', requires={'repo', 'ctx'})
357 def mykeywordfunc(context, mapping):
357 def mykeywordfunc(context, mapping):
358 '''Explanation of this template keyword ....
358 '''Explanation of this template keyword ....
359 '''
359 '''
360 pass
360 pass
361
361
362 The first string argument is used also in online help.
362 The first string argument is used also in online help.
363
363
364 Optional argument 'requires' should be a collection of resource names
364 Optional argument 'requires' should be a collection of resource names
365 which the template keyword depends on.
365 which the template keyword depends on.
366
366
367 'templatekeyword' instance in example above can be used to
367 'templatekeyword' instance in example above can be used to
368 decorate multiple functions.
368 decorate multiple functions.
369
369
370 Decorated functions are registered automatically at loading
370 Decorated functions are registered automatically at loading
371 extension, if an instance named as 'templatekeyword' is used for
371 extension, if an instance named as 'templatekeyword' is used for
372 decorating in extension.
372 decorating in extension.
373
373
374 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
374 Otherwise, explicit 'templatekw.loadkeyword()' is needed.
375 """
375 """
376
376
377 def _extrasetup(self, name, func, requires=()):
377 def _extrasetup(self, name, func, requires=()):
378 func._requires = requires
378 func._requires = requires
379
379
380
380
381 class templatefilter(_templateregistrarbase):
381 class templatefilter(_templateregistrarbase):
382 """Decorator to register template filer
382 """Decorator to register template filer
383
383
384 Usage::
384 Usage::
385
385
386 templatefilter = registrar.templatefilter()
386 templatefilter = registrar.templatefilter()
387
387
388 @templatefilter('myfilter', intype=bytes)
388 @templatefilter('myfilter', intype=bytes)
389 def myfilterfunc(text):
389 def myfilterfunc(text):
390 '''Explanation of this template filter ....
390 '''Explanation of this template filter ....
391 '''
391 '''
392 pass
392 pass
393
393
394 The first string argument is used also in online help.
394 The first string argument is used also in online help.
395
395
396 Optional argument 'intype' defines the type of the input argument,
396 Optional argument 'intype' defines the type of the input argument,
397 which should be (bytes, int, templateutil.date, or None for any.)
397 which should be (bytes, int, templateutil.date, or None for any.)
398
398
399 'templatefilter' instance in example above can be used to
399 'templatefilter' instance in example above can be used to
400 decorate multiple functions.
400 decorate multiple functions.
401
401
402 Decorated functions are registered automatically at loading
402 Decorated functions are registered automatically at loading
403 extension, if an instance named as 'templatefilter' is used for
403 extension, if an instance named as 'templatefilter' is used for
404 decorating in extension.
404 decorating in extension.
405
405
406 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
406 Otherwise, explicit 'templatefilters.loadkeyword()' is needed.
407 """
407 """
408
408
409 def _extrasetup(self, name, func, intype=None):
409 def _extrasetup(self, name, func, intype=None):
410 func._intype = intype
410 func._intype = intype
411
411
412
412
413 class templatefunc(_templateregistrarbase):
413 class templatefunc(_templateregistrarbase):
414 """Decorator to register template function
414 """Decorator to register template function
415
415
416 Usage::
416 Usage::
417
417
418 templatefunc = registrar.templatefunc()
418 templatefunc = registrar.templatefunc()
419
419
420 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3',
420 @templatefunc('myfunc(arg1, arg2[, arg3])', argspec='arg1 arg2 arg3',
421 requires={'ctx'})
421 requires={'ctx'})
422 def myfuncfunc(context, mapping, args):
422 def myfuncfunc(context, mapping, args):
423 '''Explanation of this template function ....
423 '''Explanation of this template function ....
424 '''
424 '''
425 pass
425 pass
426
426
427 The first string argument is used also in online help.
427 The first string argument is used also in online help.
428
428
429 If optional 'argspec' is defined, the function will receive 'args' as
429 If optional 'argspec' is defined, the function will receive 'args' as
430 a dict of named arguments. Otherwise 'args' is a list of positional
430 a dict of named arguments. Otherwise 'args' is a list of positional
431 arguments.
431 arguments.
432
432
433 Optional argument 'requires' should be a collection of resource names
433 Optional argument 'requires' should be a collection of resource names
434 which the template function depends on.
434 which the template function depends on.
435
435
436 'templatefunc' instance in example above can be used to
436 'templatefunc' instance in example above can be used to
437 decorate multiple functions.
437 decorate multiple functions.
438
438
439 Decorated functions are registered automatically at loading
439 Decorated functions are registered automatically at loading
440 extension, if an instance named as 'templatefunc' is used for
440 extension, if an instance named as 'templatefunc' is used for
441 decorating in extension.
441 decorating in extension.
442
442
443 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
443 Otherwise, explicit 'templatefuncs.loadfunction()' is needed.
444 """
444 """
445
445
446 _getname = _funcregistrarbase._parsefuncdecl
446 _getname = _funcregistrarbase._parsefuncdecl
447
447
448 def _extrasetup(self, name, func, argspec=None, requires=()):
448 def _extrasetup(self, name, func, argspec=None, requires=()):
449 func._argspec = argspec
449 func._argspec = argspec
450 func._requires = requires
450 func._requires = requires
451
451
452
452
453 class internalmerge(_funcregistrarbase):
453 class internalmerge(_funcregistrarbase):
454 """Decorator to register in-process merge tool
454 """Decorator to register in-process merge tool
455
455
456 Usage::
456 Usage::
457
457
458 internalmerge = registrar.internalmerge()
458 internalmerge = registrar.internalmerge()
459
459
460 @internalmerge('mymerge', internalmerge.mergeonly,
460 @internalmerge('mymerge', internalmerge.mergeonly,
461 onfailure=None, precheck=None,
461 onfailure=None, precheck=None,
462 binary=False, symlink=False):
462 binary=False, symlink=False):
463 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
463 def mymergefunc(repo, mynode, orig, fcd, fco, fca,
464 toolconf, files, labels=None):
464 toolconf, files, labels=None):
465 '''Explanation of this internal merge tool ....
465 '''Explanation of this internal merge tool ....
466 '''
466 '''
467 return 1, False # means "conflicted", "no deletion needed"
467 return 1, False # means "conflicted", "no deletion needed"
468
468
469 The first string argument is used to compose actual merge tool name,
469 The first string argument is used to compose actual merge tool name,
470 ":name" and "internal:name" (the latter is historical one).
470 ":name" and "internal:name" (the latter is historical one).
471
471
472 The second argument is one of merge types below:
472 The second argument is one of merge types below:
473
473
474 ========== ======== ======== =========
474 ========== ======== ======== =========
475 merge type precheck premerge fullmerge
475 merge type precheck premerge fullmerge
476 ========== ======== ======== =========
476 ========== ======== ======== =========
477 nomerge x x x
477 nomerge x x x
478 mergeonly o x o
478 mergeonly o x o
479 fullmerge o o o
479 fullmerge o o o
480 ========== ======== ======== =========
480 ========== ======== ======== =========
481
481
482 Optional argument 'onfailure' is the format of warning message
482 Optional argument 'onfailure' is the format of warning message
483 to be used at failure of merging (target filename is specified
483 to be used at failure of merging (target filename is specified
484 at formatting). Or, None or so, if warning message should be
484 at formatting). Or, None or so, if warning message should be
485 suppressed.
485 suppressed.
486
486
487 Optional argument 'precheck' is the function to be used
487 Optional argument 'precheck' is the function to be used
488 before actual invocation of internal merge tool itself.
488 before actual invocation of internal merge tool itself.
489 It takes as same arguments as internal merge tool does, other than
489 It takes as same arguments as internal merge tool does, other than
490 'files' and 'labels'. If it returns false value, merging is aborted
490 'files' and 'labels'. If it returns false value, merging is aborted
491 immediately (and file is marked as "unresolved").
491 immediately (and file is marked as "unresolved").
492
492
493 Optional argument 'binary' is a binary files capability of internal
493 Optional argument 'binary' is a binary files capability of internal
494 merge tool. 'nomerge' merge type implies binary=True.
494 merge tool. 'nomerge' merge type implies binary=True.
495
495
496 Optional argument 'symlink' is a symlinks capability of inetrnal
496 Optional argument 'symlink' is a symlinks capability of inetrnal
497 merge function. 'nomerge' merge type implies symlink=True.
497 merge function. 'nomerge' merge type implies symlink=True.
498
498
499 'internalmerge' instance in example above can be used to
499 'internalmerge' instance in example above can be used to
500 decorate multiple functions.
500 decorate multiple functions.
501
501
502 Decorated functions are registered automatically at loading
502 Decorated functions are registered automatically at loading
503 extension, if an instance named as 'internalmerge' is used for
503 extension, if an instance named as 'internalmerge' is used for
504 decorating in extension.
504 decorating in extension.
505
505
506 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
506 Otherwise, explicit 'filemerge.loadinternalmerge()' is needed.
507 """
507 """
508
508
509 _docformat = b"``:%s``\n %s"
509 _docformat = b"``:%s``\n %s"
510
510
511 # merge type definitions:
511 # merge type definitions:
512 nomerge = None
512 nomerge = None
513 mergeonly = b'mergeonly' # just the full merge, no premerge
513 mergeonly = b'mergeonly' # just the full merge, no premerge
514 fullmerge = b'fullmerge' # both premerge and merge
514 fullmerge = b'fullmerge' # both premerge and merge
515
515
516 def _extrasetup(
516 def _extrasetup(
517 self,
517 self,
518 name,
518 name,
519 func,
519 func,
520 mergetype,
520 mergetype,
521 onfailure=None,
521 onfailure=None,
522 precheck=None,
522 precheck=None,
523 binary=False,
523 binary=False,
524 symlink=False,
524 symlink=False,
525 ):
525 ):
526 func.mergetype = mergetype
526 func.mergetype = mergetype
527 func.onfailure = onfailure
527 func.onfailure = onfailure
528 func.precheck = precheck
528 func.precheck = precheck
529
529
530 binarycap = binary or mergetype == self.nomerge
530 binarycap = binary or mergetype == self.nomerge
531 symlinkcap = symlink or mergetype == self.nomerge
531 symlinkcap = symlink or mergetype == self.nomerge
532
532
533 # actual capabilities, which this internal merge tool has
533 # actual capabilities, which this internal merge tool has
534 func.capabilities = {b"binary": binarycap, b"symlink": symlinkcap}
534 func.capabilities = {b"binary": binarycap, b"symlink": symlinkcap}
@@ -1,633 +1,633 b''
1 # procutil.py - utility for managing processes and executable environment
1 # procutil.py - utility for managing processes and executable environment
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import contextlib
12 import contextlib
13 import errno
13 import errno
14 import imp
14 import imp
15 import io
15 import io
16 import os
16 import os
17 import signal
17 import signal
18 import subprocess
18 import subprocess
19 import sys
19 import sys
20 import time
20 import time
21
21
22 from ..i18n import _
22 from ..i18n import _
23 from ..pycompat import (
23 from ..pycompat import (
24 getattr,
24 getattr,
25 open,
25 open,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 encoding,
29 encoding,
30 error,
30 error,
31 policy,
31 policy,
32 pycompat,
32 pycompat,
33 )
33 )
34
34
35 osutil = policy.importmod(r'osutil')
35 osutil = policy.importmod(r'osutil')
36
36
37 stderr = pycompat.stderr
37 stderr = pycompat.stderr
38 stdin = pycompat.stdin
38 stdin = pycompat.stdin
39 stdout = pycompat.stdout
39 stdout = pycompat.stdout
40
40
41
41
42 def isatty(fp):
42 def isatty(fp):
43 try:
43 try:
44 return fp.isatty()
44 return fp.isatty()
45 except AttributeError:
45 except AttributeError:
46 return False
46 return False
47
47
48
48
49 # glibc determines buffering on first write to stdout - if we replace a TTY
49 # glibc determines buffering on first write to stdout - if we replace a TTY
50 # destined stdout with a pipe destined stdout (e.g. pager), we want line
50 # destined stdout with a pipe destined stdout (e.g. pager), we want line
51 # buffering (or unbuffered, on Windows)
51 # buffering (or unbuffered, on Windows)
52 if isatty(stdout):
52 if isatty(stdout):
53 if pycompat.iswindows:
53 if pycompat.iswindows:
54 # Windows doesn't support line buffering
54 # Windows doesn't support line buffering
55 stdout = os.fdopen(stdout.fileno(), r'wb', 0)
55 stdout = os.fdopen(stdout.fileno(), r'wb', 0)
56 else:
56 else:
57 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
57 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
58
58
59 if pycompat.iswindows:
59 if pycompat.iswindows:
60 from .. import windows as platform
60 from .. import windows as platform
61
61
62 stdout = platform.winstdout(stdout)
62 stdout = platform.winstdout(stdout)
63 else:
63 else:
64 from .. import posix as platform
64 from .. import posix as platform
65
65
66 findexe = platform.findexe
66 findexe = platform.findexe
67 _gethgcmd = platform.gethgcmd
67 _gethgcmd = platform.gethgcmd
68 getuser = platform.getuser
68 getuser = platform.getuser
69 getpid = os.getpid
69 getpid = os.getpid
70 hidewindow = platform.hidewindow
70 hidewindow = platform.hidewindow
71 quotecommand = platform.quotecommand
71 quotecommand = platform.quotecommand
72 readpipe = platform.readpipe
72 readpipe = platform.readpipe
73 setbinary = platform.setbinary
73 setbinary = platform.setbinary
74 setsignalhandler = platform.setsignalhandler
74 setsignalhandler = platform.setsignalhandler
75 shellquote = platform.shellquote
75 shellquote = platform.shellquote
76 shellsplit = platform.shellsplit
76 shellsplit = platform.shellsplit
77 spawndetached = platform.spawndetached
77 spawndetached = platform.spawndetached
78 sshargs = platform.sshargs
78 sshargs = platform.sshargs
79 testpid = platform.testpid
79 testpid = platform.testpid
80
80
81 try:
81 try:
82 setprocname = osutil.setprocname
82 setprocname = osutil.setprocname
83 except AttributeError:
83 except AttributeError:
84 pass
84 pass
85 try:
85 try:
86 unblocksignal = osutil.unblocksignal
86 unblocksignal = osutil.unblocksignal
87 except AttributeError:
87 except AttributeError:
88 pass
88 pass
89
89
90 closefds = pycompat.isposix
90 closefds = pycompat.isposix
91
91
92
92
93 def explainexit(code):
93 def explainexit(code):
94 """return a message describing a subprocess status
94 """return a message describing a subprocess status
95 (codes from kill are negative - not os.system/wait encoding)"""
95 (codes from kill are negative - not os.system/wait encoding)"""
96 if code >= 0:
96 if code >= 0:
97 return _(b"exited with status %d") % code
97 return _(b"exited with status %d") % code
98 return _(b"killed by signal %d") % -code
98 return _(b"killed by signal %d") % -code
99
99
100
100
101 class _pfile(object):
101 class _pfile(object):
102 """File-like wrapper for a stream opened by subprocess.Popen()"""
102 """File-like wrapper for a stream opened by subprocess.Popen()"""
103
103
104 def __init__(self, proc, fp):
104 def __init__(self, proc, fp):
105 self._proc = proc
105 self._proc = proc
106 self._fp = fp
106 self._fp = fp
107
107
108 def close(self):
108 def close(self):
109 # unlike os.popen(), this returns an integer in subprocess coding
109 # unlike os.popen(), this returns an integer in subprocess coding
110 self._fp.close()
110 self._fp.close()
111 return self._proc.wait()
111 return self._proc.wait()
112
112
113 def __iter__(self):
113 def __iter__(self):
114 return iter(self._fp)
114 return iter(self._fp)
115
115
116 def __getattr__(self, attr):
116 def __getattr__(self, attr):
117 return getattr(self._fp, attr)
117 return getattr(self._fp, attr)
118
118
119 def __enter__(self):
119 def __enter__(self):
120 return self
120 return self
121
121
122 def __exit__(self, exc_type, exc_value, exc_tb):
122 def __exit__(self, exc_type, exc_value, exc_tb):
123 self.close()
123 self.close()
124
124
125
125
126 def popen(cmd, mode=b'rb', bufsize=-1):
126 def popen(cmd, mode=b'rb', bufsize=-1):
127 if mode == b'rb':
127 if mode == b'rb':
128 return _popenreader(cmd, bufsize)
128 return _popenreader(cmd, bufsize)
129 elif mode == b'wb':
129 elif mode == b'wb':
130 return _popenwriter(cmd, bufsize)
130 return _popenwriter(cmd, bufsize)
131 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
131 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
132
132
133
133
134 def _popenreader(cmd, bufsize):
134 def _popenreader(cmd, bufsize):
135 p = subprocess.Popen(
135 p = subprocess.Popen(
136 tonativestr(quotecommand(cmd)),
136 tonativestr(quotecommand(cmd)),
137 shell=True,
137 shell=True,
138 bufsize=bufsize,
138 bufsize=bufsize,
139 close_fds=closefds,
139 close_fds=closefds,
140 stdout=subprocess.PIPE,
140 stdout=subprocess.PIPE,
141 )
141 )
142 return _pfile(p, p.stdout)
142 return _pfile(p, p.stdout)
143
143
144
144
145 def _popenwriter(cmd, bufsize):
145 def _popenwriter(cmd, bufsize):
146 p = subprocess.Popen(
146 p = subprocess.Popen(
147 tonativestr(quotecommand(cmd)),
147 tonativestr(quotecommand(cmd)),
148 shell=True,
148 shell=True,
149 bufsize=bufsize,
149 bufsize=bufsize,
150 close_fds=closefds,
150 close_fds=closefds,
151 stdin=subprocess.PIPE,
151 stdin=subprocess.PIPE,
152 )
152 )
153 return _pfile(p, p.stdin)
153 return _pfile(p, p.stdin)
154
154
155
155
156 def popen2(cmd, env=None):
156 def popen2(cmd, env=None):
157 # Setting bufsize to -1 lets the system decide the buffer size.
157 # Setting bufsize to -1 lets the system decide the buffer size.
158 # The default for bufsize is 0, meaning unbuffered. This leads to
158 # The default for bufsize is 0, meaning unbuffered. This leads to
159 # poor performance on Mac OS X: http://bugs.python.org/issue4194
159 # poor performance on Mac OS X: http://bugs.python.org/issue4194
160 p = subprocess.Popen(
160 p = subprocess.Popen(
161 tonativestr(cmd),
161 tonativestr(cmd),
162 shell=True,
162 shell=True,
163 bufsize=-1,
163 bufsize=-1,
164 close_fds=closefds,
164 close_fds=closefds,
165 stdin=subprocess.PIPE,
165 stdin=subprocess.PIPE,
166 stdout=subprocess.PIPE,
166 stdout=subprocess.PIPE,
167 env=tonativeenv(env),
167 env=tonativeenv(env),
168 )
168 )
169 return p.stdin, p.stdout
169 return p.stdin, p.stdout
170
170
171
171
172 def popen3(cmd, env=None):
172 def popen3(cmd, env=None):
173 stdin, stdout, stderr, p = popen4(cmd, env)
173 stdin, stdout, stderr, p = popen4(cmd, env)
174 return stdin, stdout, stderr
174 return stdin, stdout, stderr
175
175
176
176
177 def popen4(cmd, env=None, bufsize=-1):
177 def popen4(cmd, env=None, bufsize=-1):
178 p = subprocess.Popen(
178 p = subprocess.Popen(
179 tonativestr(cmd),
179 tonativestr(cmd),
180 shell=True,
180 shell=True,
181 bufsize=bufsize,
181 bufsize=bufsize,
182 close_fds=closefds,
182 close_fds=closefds,
183 stdin=subprocess.PIPE,
183 stdin=subprocess.PIPE,
184 stdout=subprocess.PIPE,
184 stdout=subprocess.PIPE,
185 stderr=subprocess.PIPE,
185 stderr=subprocess.PIPE,
186 env=tonativeenv(env),
186 env=tonativeenv(env),
187 )
187 )
188 return p.stdin, p.stdout, p.stderr, p
188 return p.stdin, p.stdout, p.stderr, p
189
189
190
190
191 def pipefilter(s, cmd):
191 def pipefilter(s, cmd):
192 '''filter string S through command CMD, returning its output'''
192 '''filter string S through command CMD, returning its output'''
193 p = subprocess.Popen(
193 p = subprocess.Popen(
194 tonativestr(cmd),
194 tonativestr(cmd),
195 shell=True,
195 shell=True,
196 close_fds=closefds,
196 close_fds=closefds,
197 stdin=subprocess.PIPE,
197 stdin=subprocess.PIPE,
198 stdout=subprocess.PIPE,
198 stdout=subprocess.PIPE,
199 )
199 )
200 pout, perr = p.communicate(s)
200 pout, perr = p.communicate(s)
201 return pout
201 return pout
202
202
203
203
204 def tempfilter(s, cmd):
204 def tempfilter(s, cmd):
205 '''filter string S through a pair of temporary files with CMD.
205 '''filter string S through a pair of temporary files with CMD.
206 CMD is used as a template to create the real command to be run,
206 CMD is used as a template to create the real command to be run,
207 with the strings INFILE and OUTFILE replaced by the real names of
207 with the strings INFILE and OUTFILE replaced by the real names of
208 the temporary files generated.'''
208 the temporary files generated.'''
209 inname, outname = None, None
209 inname, outname = None, None
210 try:
210 try:
211 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
211 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
212 fp = os.fdopen(infd, r'wb')
212 fp = os.fdopen(infd, r'wb')
213 fp.write(s)
213 fp.write(s)
214 fp.close()
214 fp.close()
215 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
215 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
216 os.close(outfd)
216 os.close(outfd)
217 cmd = cmd.replace(b'INFILE', inname)
217 cmd = cmd.replace(b'INFILE', inname)
218 cmd = cmd.replace(b'OUTFILE', outname)
218 cmd = cmd.replace(b'OUTFILE', outname)
219 code = system(cmd)
219 code = system(cmd)
220 if pycompat.sysplatform == b'OpenVMS' and code & 1:
220 if pycompat.sysplatform == b'OpenVMS' and code & 1:
221 code = 0
221 code = 0
222 if code:
222 if code:
223 raise error.Abort(
223 raise error.Abort(
224 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
224 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
225 )
225 )
226 with open(outname, b'rb') as fp:
226 with open(outname, b'rb') as fp:
227 return fp.read()
227 return fp.read()
228 finally:
228 finally:
229 try:
229 try:
230 if inname:
230 if inname:
231 os.unlink(inname)
231 os.unlink(inname)
232 except OSError:
232 except OSError:
233 pass
233 pass
234 try:
234 try:
235 if outname:
235 if outname:
236 os.unlink(outname)
236 os.unlink(outname)
237 except OSError:
237 except OSError:
238 pass
238 pass
239
239
240
240
241 _filtertable = {
241 _filtertable = {
242 b'tempfile:': tempfilter,
242 b'tempfile:': tempfilter,
243 b'pipe:': pipefilter,
243 b'pipe:': pipefilter,
244 }
244 }
245
245
246
246
247 def filter(s, cmd):
247 def filter(s, cmd):
248 b"filter a string through a command that transforms its input to its output"
248 b"filter a string through a command that transforms its input to its output"
249 for name, fn in pycompat.iteritems(_filtertable):
249 for name, fn in pycompat.iteritems(_filtertable):
250 if cmd.startswith(name):
250 if cmd.startswith(name):
251 return fn(s, cmd[len(name) :].lstrip())
251 return fn(s, cmd[len(name) :].lstrip())
252 return pipefilter(s, cmd)
252 return pipefilter(s, cmd)
253
253
254
254
255 def mainfrozen():
255 def mainfrozen():
256 """return True if we are a frozen executable.
256 """return True if we are a frozen executable.
257
257
258 The code supports py2exe (most common, Windows only) and tools/freeze
258 The code supports py2exe (most common, Windows only) and tools/freeze
259 (portable, not much used).
259 (portable, not much used).
260 """
260 """
261 return (
261 return (
262 pycompat.safehasattr(sys, b"frozen")
262 pycompat.safehasattr(sys, "frozen")
263 or pycompat.safehasattr(sys, b"importers") # new py2exe
263 or pycompat.safehasattr(sys, "importers") # new py2exe
264 or imp.is_frozen(r"__main__") # old py2exe
264 or imp.is_frozen(r"__main__") # old py2exe
265 ) # tools/freeze
265 ) # tools/freeze
266
266
267
267
268 _hgexecutable = None
268 _hgexecutable = None
269
269
270
270
271 def hgexecutable():
271 def hgexecutable():
272 """return location of the 'hg' executable.
272 """return location of the 'hg' executable.
273
273
274 Defaults to $HG or 'hg' in the search path.
274 Defaults to $HG or 'hg' in the search path.
275 """
275 """
276 if _hgexecutable is None:
276 if _hgexecutable is None:
277 hg = encoding.environ.get(b'HG')
277 hg = encoding.environ.get(b'HG')
278 mainmod = sys.modules[r'__main__']
278 mainmod = sys.modules[r'__main__']
279 if hg:
279 if hg:
280 _sethgexecutable(hg)
280 _sethgexecutable(hg)
281 elif mainfrozen():
281 elif mainfrozen():
282 if getattr(sys, 'frozen', None) == b'macosx_app':
282 if getattr(sys, 'frozen', None) == b'macosx_app':
283 # Env variable set by py2app
283 # Env variable set by py2app
284 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
284 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
285 else:
285 else:
286 _sethgexecutable(pycompat.sysexecutable)
286 _sethgexecutable(pycompat.sysexecutable)
287 elif (
287 elif (
288 not pycompat.iswindows
288 not pycompat.iswindows
289 and os.path.basename(
289 and os.path.basename(
290 pycompat.fsencode(getattr(mainmod, '__file__', b''))
290 pycompat.fsencode(getattr(mainmod, '__file__', b''))
291 )
291 )
292 == b'hg'
292 == b'hg'
293 ):
293 ):
294 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
294 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
295 else:
295 else:
296 _sethgexecutable(
296 _sethgexecutable(
297 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
297 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
298 )
298 )
299 return _hgexecutable
299 return _hgexecutable
300
300
301
301
302 def _sethgexecutable(path):
302 def _sethgexecutable(path):
303 """set location of the 'hg' executable"""
303 """set location of the 'hg' executable"""
304 global _hgexecutable
304 global _hgexecutable
305 _hgexecutable = path
305 _hgexecutable = path
306
306
307
307
308 def _testfileno(f, stdf):
308 def _testfileno(f, stdf):
309 fileno = getattr(f, 'fileno', None)
309 fileno = getattr(f, 'fileno', None)
310 try:
310 try:
311 return fileno and fileno() == stdf.fileno()
311 return fileno and fileno() == stdf.fileno()
312 except io.UnsupportedOperation:
312 except io.UnsupportedOperation:
313 return False # fileno() raised UnsupportedOperation
313 return False # fileno() raised UnsupportedOperation
314
314
315
315
316 def isstdin(f):
316 def isstdin(f):
317 return _testfileno(f, sys.__stdin__)
317 return _testfileno(f, sys.__stdin__)
318
318
319
319
320 def isstdout(f):
320 def isstdout(f):
321 return _testfileno(f, sys.__stdout__)
321 return _testfileno(f, sys.__stdout__)
322
322
323
323
324 def protectstdio(uin, uout):
324 def protectstdio(uin, uout):
325 """Duplicate streams and redirect original if (uin, uout) are stdio
325 """Duplicate streams and redirect original if (uin, uout) are stdio
326
326
327 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
327 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
328 redirected to stderr so the output is still readable.
328 redirected to stderr so the output is still readable.
329
329
330 Returns (fin, fout) which point to the original (uin, uout) fds, but
330 Returns (fin, fout) which point to the original (uin, uout) fds, but
331 may be copy of (uin, uout). The returned streams can be considered
331 may be copy of (uin, uout). The returned streams can be considered
332 "owned" in that print(), exec(), etc. never reach to them.
332 "owned" in that print(), exec(), etc. never reach to them.
333 """
333 """
334 uout.flush()
334 uout.flush()
335 fin, fout = uin, uout
335 fin, fout = uin, uout
336 if _testfileno(uin, stdin):
336 if _testfileno(uin, stdin):
337 newfd = os.dup(uin.fileno())
337 newfd = os.dup(uin.fileno())
338 nullfd = os.open(os.devnull, os.O_RDONLY)
338 nullfd = os.open(os.devnull, os.O_RDONLY)
339 os.dup2(nullfd, uin.fileno())
339 os.dup2(nullfd, uin.fileno())
340 os.close(nullfd)
340 os.close(nullfd)
341 fin = os.fdopen(newfd, r'rb')
341 fin = os.fdopen(newfd, r'rb')
342 if _testfileno(uout, stdout):
342 if _testfileno(uout, stdout):
343 newfd = os.dup(uout.fileno())
343 newfd = os.dup(uout.fileno())
344 os.dup2(stderr.fileno(), uout.fileno())
344 os.dup2(stderr.fileno(), uout.fileno())
345 fout = os.fdopen(newfd, r'wb')
345 fout = os.fdopen(newfd, r'wb')
346 return fin, fout
346 return fin, fout
347
347
348
348
349 def restorestdio(uin, uout, fin, fout):
349 def restorestdio(uin, uout, fin, fout):
350 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
350 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
351 uout.flush()
351 uout.flush()
352 for f, uif in [(fin, uin), (fout, uout)]:
352 for f, uif in [(fin, uin), (fout, uout)]:
353 if f is not uif:
353 if f is not uif:
354 os.dup2(f.fileno(), uif.fileno())
354 os.dup2(f.fileno(), uif.fileno())
355 f.close()
355 f.close()
356
356
357
357
358 def shellenviron(environ=None):
358 def shellenviron(environ=None):
359 """return environ with optional override, useful for shelling out"""
359 """return environ with optional override, useful for shelling out"""
360
360
361 def py2shell(val):
361 def py2shell(val):
362 b'convert python object into string that is useful to shell'
362 b'convert python object into string that is useful to shell'
363 if val is None or val is False:
363 if val is None or val is False:
364 return b'0'
364 return b'0'
365 if val is True:
365 if val is True:
366 return b'1'
366 return b'1'
367 return pycompat.bytestr(val)
367 return pycompat.bytestr(val)
368
368
369 env = dict(encoding.environ)
369 env = dict(encoding.environ)
370 if environ:
370 if environ:
371 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
371 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
372 env[b'HG'] = hgexecutable()
372 env[b'HG'] = hgexecutable()
373 return env
373 return env
374
374
375
375
376 if pycompat.iswindows:
376 if pycompat.iswindows:
377
377
378 def shelltonative(cmd, env):
378 def shelltonative(cmd, env):
379 return platform.shelltocmdexe(cmd, shellenviron(env))
379 return platform.shelltocmdexe(cmd, shellenviron(env))
380
380
381 tonativestr = encoding.strfromlocal
381 tonativestr = encoding.strfromlocal
382 else:
382 else:
383
383
384 def shelltonative(cmd, env):
384 def shelltonative(cmd, env):
385 return cmd
385 return cmd
386
386
387 tonativestr = pycompat.identity
387 tonativestr = pycompat.identity
388
388
389
389
390 def tonativeenv(env):
390 def tonativeenv(env):
391 '''convert the environment from bytes to strings suitable for Popen(), etc.
391 '''convert the environment from bytes to strings suitable for Popen(), etc.
392 '''
392 '''
393 return pycompat.rapply(tonativestr, env)
393 return pycompat.rapply(tonativestr, env)
394
394
395
395
396 def system(cmd, environ=None, cwd=None, out=None):
396 def system(cmd, environ=None, cwd=None, out=None):
397 '''enhanced shell command execution.
397 '''enhanced shell command execution.
398 run with environment maybe modified, maybe in different dir.
398 run with environment maybe modified, maybe in different dir.
399
399
400 if out is specified, it is assumed to be a file-like object that has a
400 if out is specified, it is assumed to be a file-like object that has a
401 write() method. stdout and stderr will be redirected to out.'''
401 write() method. stdout and stderr will be redirected to out.'''
402 try:
402 try:
403 stdout.flush()
403 stdout.flush()
404 except Exception:
404 except Exception:
405 pass
405 pass
406 cmd = quotecommand(cmd)
406 cmd = quotecommand(cmd)
407 env = shellenviron(environ)
407 env = shellenviron(environ)
408 if out is None or isstdout(out):
408 if out is None or isstdout(out):
409 rc = subprocess.call(
409 rc = subprocess.call(
410 tonativestr(cmd),
410 tonativestr(cmd),
411 shell=True,
411 shell=True,
412 close_fds=closefds,
412 close_fds=closefds,
413 env=tonativeenv(env),
413 env=tonativeenv(env),
414 cwd=pycompat.rapply(tonativestr, cwd),
414 cwd=pycompat.rapply(tonativestr, cwd),
415 )
415 )
416 else:
416 else:
417 proc = subprocess.Popen(
417 proc = subprocess.Popen(
418 tonativestr(cmd),
418 tonativestr(cmd),
419 shell=True,
419 shell=True,
420 close_fds=closefds,
420 close_fds=closefds,
421 env=tonativeenv(env),
421 env=tonativeenv(env),
422 cwd=pycompat.rapply(tonativestr, cwd),
422 cwd=pycompat.rapply(tonativestr, cwd),
423 stdout=subprocess.PIPE,
423 stdout=subprocess.PIPE,
424 stderr=subprocess.STDOUT,
424 stderr=subprocess.STDOUT,
425 )
425 )
426 for line in iter(proc.stdout.readline, b''):
426 for line in iter(proc.stdout.readline, b''):
427 out.write(line)
427 out.write(line)
428 proc.wait()
428 proc.wait()
429 rc = proc.returncode
429 rc = proc.returncode
430 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
430 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
431 rc = 0
431 rc = 0
432 return rc
432 return rc
433
433
434
434
435 def gui():
435 def gui():
436 '''Are we running in a GUI?'''
436 '''Are we running in a GUI?'''
437 if pycompat.isdarwin:
437 if pycompat.isdarwin:
438 if b'SSH_CONNECTION' in encoding.environ:
438 if b'SSH_CONNECTION' in encoding.environ:
439 # handle SSH access to a box where the user is logged in
439 # handle SSH access to a box where the user is logged in
440 return False
440 return False
441 elif getattr(osutil, 'isgui', None):
441 elif getattr(osutil, 'isgui', None):
442 # check if a CoreGraphics session is available
442 # check if a CoreGraphics session is available
443 return osutil.isgui()
443 return osutil.isgui()
444 else:
444 else:
445 # pure build; use a safe default
445 # pure build; use a safe default
446 return True
446 return True
447 else:
447 else:
448 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
448 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
449
449
450
450
451 def hgcmd():
451 def hgcmd():
452 """Return the command used to execute current hg
452 """Return the command used to execute current hg
453
453
454 This is different from hgexecutable() because on Windows we want
454 This is different from hgexecutable() because on Windows we want
455 to avoid things opening new shell windows like batch files, so we
455 to avoid things opening new shell windows like batch files, so we
456 get either the python call or current executable.
456 get either the python call or current executable.
457 """
457 """
458 if mainfrozen():
458 if mainfrozen():
459 if getattr(sys, 'frozen', None) == b'macosx_app':
459 if getattr(sys, 'frozen', None) == b'macosx_app':
460 # Env variable set by py2app
460 # Env variable set by py2app
461 return [encoding.environ[b'EXECUTABLEPATH']]
461 return [encoding.environ[b'EXECUTABLEPATH']]
462 else:
462 else:
463 return [pycompat.sysexecutable]
463 return [pycompat.sysexecutable]
464 return _gethgcmd()
464 return _gethgcmd()
465
465
466
466
467 def rundetached(args, condfn):
467 def rundetached(args, condfn):
468 """Execute the argument list in a detached process.
468 """Execute the argument list in a detached process.
469
469
470 condfn is a callable which is called repeatedly and should return
470 condfn is a callable which is called repeatedly and should return
471 True once the child process is known to have started successfully.
471 True once the child process is known to have started successfully.
472 At this point, the child process PID is returned. If the child
472 At this point, the child process PID is returned. If the child
473 process fails to start or finishes before condfn() evaluates to
473 process fails to start or finishes before condfn() evaluates to
474 True, return -1.
474 True, return -1.
475 """
475 """
476 # Windows case is easier because the child process is either
476 # Windows case is easier because the child process is either
477 # successfully starting and validating the condition or exiting
477 # successfully starting and validating the condition or exiting
478 # on failure. We just poll on its PID. On Unix, if the child
478 # on failure. We just poll on its PID. On Unix, if the child
479 # process fails to start, it will be left in a zombie state until
479 # process fails to start, it will be left in a zombie state until
480 # the parent wait on it, which we cannot do since we expect a long
480 # the parent wait on it, which we cannot do since we expect a long
481 # running process on success. Instead we listen for SIGCHLD telling
481 # running process on success. Instead we listen for SIGCHLD telling
482 # us our child process terminated.
482 # us our child process terminated.
483 terminated = set()
483 terminated = set()
484
484
485 def handler(signum, frame):
485 def handler(signum, frame):
486 terminated.add(os.wait())
486 terminated.add(os.wait())
487
487
488 prevhandler = None
488 prevhandler = None
489 SIGCHLD = getattr(signal, 'SIGCHLD', None)
489 SIGCHLD = getattr(signal, 'SIGCHLD', None)
490 if SIGCHLD is not None:
490 if SIGCHLD is not None:
491 prevhandler = signal.signal(SIGCHLD, handler)
491 prevhandler = signal.signal(SIGCHLD, handler)
492 try:
492 try:
493 pid = spawndetached(args)
493 pid = spawndetached(args)
494 while not condfn():
494 while not condfn():
495 if (pid in terminated or not testpid(pid)) and not condfn():
495 if (pid in terminated or not testpid(pid)) and not condfn():
496 return -1
496 return -1
497 time.sleep(0.1)
497 time.sleep(0.1)
498 return pid
498 return pid
499 finally:
499 finally:
500 if prevhandler is not None:
500 if prevhandler is not None:
501 signal.signal(signal.SIGCHLD, prevhandler)
501 signal.signal(signal.SIGCHLD, prevhandler)
502
502
503
503
504 @contextlib.contextmanager
504 @contextlib.contextmanager
505 def uninterruptible(warn):
505 def uninterruptible(warn):
506 """Inhibit SIGINT handling on a region of code.
506 """Inhibit SIGINT handling on a region of code.
507
507
508 Note that if this is called in a non-main thread, it turns into a no-op.
508 Note that if this is called in a non-main thread, it turns into a no-op.
509
509
510 Args:
510 Args:
511 warn: A callable which takes no arguments, and returns True if the
511 warn: A callable which takes no arguments, and returns True if the
512 previous signal handling should be restored.
512 previous signal handling should be restored.
513 """
513 """
514
514
515 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
515 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
516 shouldbail = []
516 shouldbail = []
517
517
518 def disabledsiginthandler(*args):
518 def disabledsiginthandler(*args):
519 if warn():
519 if warn():
520 signal.signal(signal.SIGINT, oldsiginthandler[0])
520 signal.signal(signal.SIGINT, oldsiginthandler[0])
521 del oldsiginthandler[0]
521 del oldsiginthandler[0]
522 shouldbail.append(True)
522 shouldbail.append(True)
523
523
524 try:
524 try:
525 try:
525 try:
526 signal.signal(signal.SIGINT, disabledsiginthandler)
526 signal.signal(signal.SIGINT, disabledsiginthandler)
527 except ValueError:
527 except ValueError:
528 # wrong thread, oh well, we tried
528 # wrong thread, oh well, we tried
529 del oldsiginthandler[0]
529 del oldsiginthandler[0]
530 yield
530 yield
531 finally:
531 finally:
532 if oldsiginthandler:
532 if oldsiginthandler:
533 signal.signal(signal.SIGINT, oldsiginthandler[0])
533 signal.signal(signal.SIGINT, oldsiginthandler[0])
534 if shouldbail:
534 if shouldbail:
535 raise KeyboardInterrupt
535 raise KeyboardInterrupt
536
536
537
537
538 if pycompat.iswindows:
538 if pycompat.iswindows:
539 # no fork on Windows, but we can create a detached process
539 # no fork on Windows, but we can create a detached process
540 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
540 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
541 # No stdlib constant exists for this value
541 # No stdlib constant exists for this value
542 DETACHED_PROCESS = 0x00000008
542 DETACHED_PROCESS = 0x00000008
543 # Following creation flags might create a console GUI window.
543 # Following creation flags might create a console GUI window.
544 # Using subprocess.CREATE_NEW_CONSOLE might helps.
544 # Using subprocess.CREATE_NEW_CONSOLE might helps.
545 # See https://phab.mercurial-scm.org/D1701 for discussion
545 # See https://phab.mercurial-scm.org/D1701 for discussion
546 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
546 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
547
547
548 def runbgcommand(
548 def runbgcommand(
549 script, env, shell=False, stdout=None, stderr=None, ensurestart=True
549 script, env, shell=False, stdout=None, stderr=None, ensurestart=True
550 ):
550 ):
551 '''Spawn a command without waiting for it to finish.'''
551 '''Spawn a command without waiting for it to finish.'''
552 # we can't use close_fds *and* redirect stdin. I'm not sure that we
552 # we can't use close_fds *and* redirect stdin. I'm not sure that we
553 # need to because the detached process has no console connection.
553 # need to because the detached process has no console connection.
554 subprocess.Popen(
554 subprocess.Popen(
555 tonativestr(script),
555 tonativestr(script),
556 shell=shell,
556 shell=shell,
557 env=tonativeenv(env),
557 env=tonativeenv(env),
558 close_fds=True,
558 close_fds=True,
559 creationflags=_creationflags,
559 creationflags=_creationflags,
560 stdout=stdout,
560 stdout=stdout,
561 stderr=stderr,
561 stderr=stderr,
562 )
562 )
563
563
564
564
565 else:
565 else:
566
566
567 def runbgcommand(
567 def runbgcommand(
568 cmd, env, shell=False, stdout=None, stderr=None, ensurestart=True
568 cmd, env, shell=False, stdout=None, stderr=None, ensurestart=True
569 ):
569 ):
570 '''Spawn a command without waiting for it to finish.'''
570 '''Spawn a command without waiting for it to finish.'''
571 # double-fork to completely detach from the parent process
571 # double-fork to completely detach from the parent process
572 # based on http://code.activestate.com/recipes/278731
572 # based on http://code.activestate.com/recipes/278731
573 pid = os.fork()
573 pid = os.fork()
574 if pid:
574 if pid:
575 if not ensurestart:
575 if not ensurestart:
576 return
576 return
577 # Parent process
577 # Parent process
578 (_pid, status) = os.waitpid(pid, 0)
578 (_pid, status) = os.waitpid(pid, 0)
579 if os.WIFEXITED(status):
579 if os.WIFEXITED(status):
580 returncode = os.WEXITSTATUS(status)
580 returncode = os.WEXITSTATUS(status)
581 else:
581 else:
582 returncode = -(os.WTERMSIG(status))
582 returncode = -(os.WTERMSIG(status))
583 if returncode != 0:
583 if returncode != 0:
584 # The child process's return code is 0 on success, an errno
584 # The child process's return code is 0 on success, an errno
585 # value on failure, or 255 if we don't have a valid errno
585 # value on failure, or 255 if we don't have a valid errno
586 # value.
586 # value.
587 #
587 #
588 # (It would be slightly nicer to return the full exception info
588 # (It would be slightly nicer to return the full exception info
589 # over a pipe as the subprocess module does. For now it
589 # over a pipe as the subprocess module does. For now it
590 # doesn't seem worth adding that complexity here, though.)
590 # doesn't seem worth adding that complexity here, though.)
591 if returncode == 255:
591 if returncode == 255:
592 returncode = errno.EINVAL
592 returncode = errno.EINVAL
593 raise OSError(
593 raise OSError(
594 returncode,
594 returncode,
595 b'error running %r: %s' % (cmd, os.strerror(returncode)),
595 b'error running %r: %s' % (cmd, os.strerror(returncode)),
596 )
596 )
597 return
597 return
598
598
599 returncode = 255
599 returncode = 255
600 try:
600 try:
601 # Start a new session
601 # Start a new session
602 os.setsid()
602 os.setsid()
603
603
604 stdin = open(os.devnull, b'r')
604 stdin = open(os.devnull, b'r')
605 if stdout is None:
605 if stdout is None:
606 stdout = open(os.devnull, b'w')
606 stdout = open(os.devnull, b'w')
607 if stderr is None:
607 if stderr is None:
608 stderr = open(os.devnull, b'w')
608 stderr = open(os.devnull, b'w')
609
609
610 # connect stdin to devnull to make sure the subprocess can't
610 # connect stdin to devnull to make sure the subprocess can't
611 # muck up that stream for mercurial.
611 # muck up that stream for mercurial.
612 subprocess.Popen(
612 subprocess.Popen(
613 cmd,
613 cmd,
614 shell=shell,
614 shell=shell,
615 env=env,
615 env=env,
616 close_fds=True,
616 close_fds=True,
617 stdin=stdin,
617 stdin=stdin,
618 stdout=stdout,
618 stdout=stdout,
619 stderr=stderr,
619 stderr=stderr,
620 )
620 )
621 returncode = 0
621 returncode = 0
622 except EnvironmentError as ex:
622 except EnvironmentError as ex:
623 returncode = ex.errno & 0xFF
623 returncode = ex.errno & 0xFF
624 if returncode == 0:
624 if returncode == 0:
625 # This shouldn't happen, but just in case make sure the
625 # This shouldn't happen, but just in case make sure the
626 # return code is never 0 here.
626 # return code is never 0 here.
627 returncode = 255
627 returncode = 255
628 except Exception:
628 except Exception:
629 returncode = 255
629 returncode = 255
630 finally:
630 finally:
631 # mission accomplished, this child needs to exit and not
631 # mission accomplished, this child needs to exit and not
632 # continue the hg process here.
632 # continue the hg process here.
633 os._exit(returncode)
633 os._exit(returncode)
General Comments 0
You need to be logged in to leave comments. Login now