##// END OF EJS Templates
infinitepush: introduce server option to route every push to bundlestore...
Pulkit Goyal -
r37223:571f25da default
parent child Browse files
Show More
@@ -0,0 +1,344
1 Testing the case when there is no infinitepush extension present on the client
2 side and the server routes each push to bundlestore. This case is very much
3 similar to CI use case.
4
5 Setup
6 -----
7
8 $ . "$TESTDIR/library-infinitepush.sh"
9 $ cat >> $HGRCPATH <<EOF
10 > [alias]
11 > glog = log -GT "{rev}:{node|short} {desc}\n{phase}"
12 > EOF
13 $ cp $HGRCPATH $TESTTMP/defaulthgrc
14 $ hg init repo
15 $ cd repo
16 $ setupserver
17 $ echo "pushtobundlestore = True" >> .hg/hgrc
18 $ echo "[extensions]" >> .hg/hgrc
19 $ echo "infinitepush=" >> .hg/hgrc
20 $ echo initialcommit > initialcommit
21 $ hg ci -Aqm "initialcommit"
22 $ hg phase --public .
23
24 $ cd ..
25 $ hg clone repo client -q
26 $ cd client
27
28 Pushing a new commit from the client to the server
29 -----------------------------------------------------
30
31 $ echo foobar > a
32 $ hg ci -Aqm "added a"
33 $ hg glog
34 @ 1:6cb0989601f1 added a
35 | draft
36 o 0:67145f466344 initialcommit
37 public
38
39 $ hg push
40 pushing to $TESTTMP/repo
41 searching for changes
42 storing changesets on the bundlestore
43 pushing 1 commit:
44 6cb0989601f1 added a
45
46 $ scratchnodes
47 6cb0989601f1fb5805238edfb16f3606713d9a0b a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
48
49 Understanding how data is stored on the bundlestore in server
50 -------------------------------------------------------------
51
52 There are two things, filebundlestore and index
53 $ ls ../repo/.hg/scratchbranches
54 filebundlestore
55 index
56
57 filebundlestore stores the bundles
58 $ ls ../repo/.hg/scratchbranches/filebundlestore/a4/c2/
59 a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
60
61 index/nodemap stores a map of node id and file in which bundle is stored in filebundlestore
62 $ ls ../repo/.hg/scratchbranches/index/
63 nodemap
64 $ ls ../repo/.hg/scratchbranches/index/nodemap/
65 6cb0989601f1fb5805238edfb16f3606713d9a0b
66
67 $ cd ../repo
68
69 Checking that the commit was not applied to revlog on the server
70 ------------------------------------------------------------------
71
72 $ hg glog
73 @ 0:67145f466344 initialcommit
74 public
75
76 Applying the changeset from the bundlestore
77 --------------------------------------------
78
79 $ hg unbundle .hg/scratchbranches/filebundlestore/a4/c2/a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
80 adding changesets
81 adding manifests
82 adding file changes
83 added 1 changesets with 1 changes to 1 files
84 new changesets 6cb0989601f1
85 (run 'hg update' to get a working copy)
86
87 $ hg glog
88 o 1:6cb0989601f1 added a
89 | public
90 @ 0:67145f466344 initialcommit
91 public
92
93 Pushing more changesets from the local repo
94 --------------------------------------------
95
96 $ cd ../client
97 $ echo b > b
98 $ hg ci -Aqm "added b"
99 $ echo c > c
100 $ hg ci -Aqm "added c"
101 $ hg glog
102 @ 3:bf8a6e3011b3 added c
103 | draft
104 o 2:eaba929e866c added b
105 | draft
106 o 1:6cb0989601f1 added a
107 | public
108 o 0:67145f466344 initialcommit
109 public
110
111 $ hg push
112 pushing to $TESTTMP/repo
113 searching for changes
114 storing changesets on the bundlestore
115 pushing 2 commits:
116 eaba929e866c added b
117 bf8a6e3011b3 added c
118
119 Checking that changesets are not applied on the server
120 ------------------------------------------------------
121
122 $ hg glog -R ../repo
123 o 1:6cb0989601f1 added a
124 | public
125 @ 0:67145f466344 initialcommit
126 public
127
128 Both of the new changesets are stored in a single bundle-file
129 $ scratchnodes
130 6cb0989601f1fb5805238edfb16f3606713d9a0b a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
131 bf8a6e3011b345146bbbedbcb1ebd4837571492a ee41a41cefb7817cbfb235b4f6e9f27dbad6ca1f
132 eaba929e866c59bc9a6aada5a9dd2f6990db83c0 ee41a41cefb7817cbfb235b4f6e9f27dbad6ca1f
133
134 Pushing more changesets to the server
135 -------------------------------------
136
137 $ echo d > d
138 $ hg ci -Aqm "added d"
139 $ echo e > e
140 $ hg ci -Aqm "added e"
141
142 XXX: we should have pushed only the parts which are not in bundlestore
143 $ hg push
144 pushing to $TESTTMP/repo
145 searching for changes
146 storing changesets on the bundlestore
147 pushing 4 commits:
148 eaba929e866c added b
149 bf8a6e3011b3 added c
150 1bb96358eda2 added d
151 b4e4bce66051 added e
152
153 Sneak peek into the bundlestore at the server
154 $ scratchnodes
155 1bb96358eda285b536c6d1c66846a7cdb2336cea 57e00c0d4f26e2a2a72b751b63d9abc4f3eb28e7
156 6cb0989601f1fb5805238edfb16f3606713d9a0b a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
157 b4e4bce660512ad3e71189e14588a70ac8e31fef 57e00c0d4f26e2a2a72b751b63d9abc4f3eb28e7
158 bf8a6e3011b345146bbbedbcb1ebd4837571492a 57e00c0d4f26e2a2a72b751b63d9abc4f3eb28e7
159 eaba929e866c59bc9a6aada5a9dd2f6990db83c0 57e00c0d4f26e2a2a72b751b63d9abc4f3eb28e7
160
161 Checking if `hg pull` pulls something or `hg incoming` shows something
162 -----------------------------------------------------------------------
163
164 $ hg incoming
165 comparing with $TESTTMP/repo
166 searching for changes
167 no changes found
168 [1]
169
170 $ hg pull
171 pulling from $TESTTMP/repo
172 searching for changes
173 no changes found
174
175 Checking storage of phase information with the bundle on bundlestore
176 ---------------------------------------------------------------------
177
178 creating a draft commit
179 $ cat >> $HGRCPATH <<EOF
180 > [phases]
181 > publish = False
182 > EOF
183 $ echo f > f
184 $ hg ci -Aqm "added f"
185 $ hg glog -r '.^::'
186 @ 6:9b42578d4447 added f
187 | draft
188 o 5:b4e4bce66051 added e
189 | public
190 ~
191
192 $ hg push
193 pushing to $TESTTMP/repo
194 searching for changes
195 storing changesets on the bundlestore
196 pushing 5 commits:
197 eaba929e866c added b
198 bf8a6e3011b3 added c
199 1bb96358eda2 added d
200 b4e4bce66051 added e
201 9b42578d4447 added f
202
203 XXX: the phase of 9b42578d4447 should not be changed here
204 $ hg glog -r .
205 @ 6:9b42578d4447 added f
206 | public
207 ~
208
209 applying the bundle on the server to check preservation of phase-information
210
211 $ cd ../repo
212 $ scratchnodes
213 1bb96358eda285b536c6d1c66846a7cdb2336cea 0a6e70ecd5b98d22382f69b93909f557ac6a9927
214 6cb0989601f1fb5805238edfb16f3606713d9a0b a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
215 9b42578d44473575994109161430d65dd147d16d 0a6e70ecd5b98d22382f69b93909f557ac6a9927
216 b4e4bce660512ad3e71189e14588a70ac8e31fef 0a6e70ecd5b98d22382f69b93909f557ac6a9927
217 bf8a6e3011b345146bbbedbcb1ebd4837571492a 0a6e70ecd5b98d22382f69b93909f557ac6a9927
218 eaba929e866c59bc9a6aada5a9dd2f6990db83c0 0a6e70ecd5b98d22382f69b93909f557ac6a9927
219
220 $ hg unbundle .hg/scratchbranches/filebundlestore/0a/6e/0a6e70ecd5b98d22382f69b93909f557ac6a9927
221 adding changesets
222 adding manifests
223 adding file changes
224 added 5 changesets with 5 changes to 5 files
225 new changesets eaba929e866c:9b42578d4447
226 (run 'hg update' to get a working copy)
227
228 $ hg glog
229 o 6:9b42578d4447 added f
230 | draft
231 o 5:b4e4bce66051 added e
232 | public
233 o 4:1bb96358eda2 added d
234 | public
235 o 3:bf8a6e3011b3 added c
236 | public
237 o 2:eaba929e866c added b
238 | public
239 o 1:6cb0989601f1 added a
240 | public
241 @ 0:67145f466344 initialcommit
242 public
243
244 Checking storage of obsmarkers in the bundlestore
245 --------------------------------------------------
246
247 enabling obsmarkers and rebase extension
248
249 $ cat >> $HGRCPATH << EOF
250 > [experimental]
251 > evolution = all
252 > [extensions]
253 > rebase =
254 > EOF
255
256 $ cd ../client
257
258 $ hg phase -r . --draft --force
259 $ hg rebase -r 6 -d 3
260 rebasing 6:9b42578d4447 "added f" (tip)
261
262 $ hg glog
263 @ 7:99949238d9ac added f
264 | draft
265 | o 5:b4e4bce66051 added e
266 | | public
267 | o 4:1bb96358eda2 added d
268 |/ public
269 o 3:bf8a6e3011b3 added c
270 | public
271 o 2:eaba929e866c added b
272 | public
273 o 1:6cb0989601f1 added a
274 | public
275 o 0:67145f466344 initialcommit
276 public
277
278 $ hg push -f
279 pushing to $TESTTMP/repo
280 searching for changes
281 storing changesets on the bundlestore
282 pushing 1 commit:
283 99949238d9ac added f
284
285 XXX: the phase should not have changed here
286 $ hg glog -r .
287 @ 7:99949238d9ac added f
288 | public
289 ~
290
291 Unbundling on server to see obsmarkers being applied
292
293 $ cd ../repo
294
295 $ scratchnodes
296 1bb96358eda285b536c6d1c66846a7cdb2336cea 0a6e70ecd5b98d22382f69b93909f557ac6a9927
297 6cb0989601f1fb5805238edfb16f3606713d9a0b a4c202c147a9c4bb91bbadb56321fc5f3950f7f2
298 99949238d9ac7f2424a33a46dface6f866afd059 090a24fe63f31d3b4bee714447f835c8c362ff57
299 9b42578d44473575994109161430d65dd147d16d 0a6e70ecd5b98d22382f69b93909f557ac6a9927
300 b4e4bce660512ad3e71189e14588a70ac8e31fef 0a6e70ecd5b98d22382f69b93909f557ac6a9927
301 bf8a6e3011b345146bbbedbcb1ebd4837571492a 0a6e70ecd5b98d22382f69b93909f557ac6a9927
302 eaba929e866c59bc9a6aada5a9dd2f6990db83c0 0a6e70ecd5b98d22382f69b93909f557ac6a9927
303
304 $ hg glog
305 o 6:9b42578d4447 added f
306 | draft
307 o 5:b4e4bce66051 added e
308 | public
309 o 4:1bb96358eda2 added d
310 | public
311 o 3:bf8a6e3011b3 added c
312 | public
313 o 2:eaba929e866c added b
314 | public
315 o 1:6cb0989601f1 added a
316 | public
317 @ 0:67145f466344 initialcommit
318 public
319
320 $ hg unbundle .hg/scratchbranches/filebundlestore/09/0a/090a24fe63f31d3b4bee714447f835c8c362ff57
321 adding changesets
322 adding manifests
323 adding file changes
324 added 1 changesets with 0 changes to 1 files (+1 heads)
325 1 new obsolescence markers
326 obsoleted 1 changesets
327 new changesets 99949238d9ac
328 (run 'hg heads' to see heads, 'hg merge' to merge)
329
330 $ hg glog
331 o 7:99949238d9ac added f
332 | draft
333 | o 5:b4e4bce66051 added e
334 | | public
335 | o 4:1bb96358eda2 added d
336 |/ public
337 o 3:bf8a6e3011b3 added c
338 | public
339 o 2:eaba929e866c added b
340 | public
341 o 1:6cb0989601f1 added a
342 | public
343 @ 0:67145f466344 initialcommit
344 public
@@ -1,1126 +1,1186
1 1 # Infinite push
2 2 #
3 3 # Copyright 2016 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """ store some pushes in a remote blob store on the server (EXPERIMENTAL)
8 8
9 9 [infinitepush]
10 10 # Server-side and client-side option. Pattern of the infinitepush bookmark
11 11 branchpattern = PATTERN
12 12
13 13 # Server or client
14 14 server = False
15 15
16 16 # Server-side option. Possible values: 'disk' or 'sql'. Fails if not set
17 17 indextype = disk
18 18
19 19 # Server-side option. Used only if indextype=sql.
20 20 # Format: 'IP:PORT:DB_NAME:USER:PASSWORD'
21 21 sqlhost = IP:PORT:DB_NAME:USER:PASSWORD
22 22
23 23 # Server-side option. Used only if indextype=disk.
24 24 # Filesystem path to the index store
25 25 indexpath = PATH
26 26
27 27 # Server-side option. Possible values: 'disk' or 'external'
28 28 # Fails if not set
29 29 storetype = disk
30 30
31 31 # Server-side option.
32 32 # Path to the binary that will save bundle to the bundlestore
33 33 # Formatted cmd line will be passed to it (see `put_args`)
34 34 put_binary = put
35 35
36 36 # Serser-side option. Used only if storetype=external.
37 37 # Format cmd-line string for put binary. Placeholder: {filename}
38 38 put_args = {filename}
39 39
40 40 # Server-side option.
41 41 # Path to the binary that get bundle from the bundlestore.
42 42 # Formatted cmd line will be passed to it (see `get_args`)
43 43 get_binary = get
44 44
45 45 # Serser-side option. Used only if storetype=external.
46 46 # Format cmd-line string for get binary. Placeholders: {filename} {handle}
47 47 get_args = {filename} {handle}
48 48
49 49 # Server-side option
50 50 logfile = FIlE
51 51
52 52 # Server-side option
53 53 loglevel = DEBUG
54 54
55 55 # Server-side option. Used only if indextype=sql.
56 56 # Sets mysql wait_timeout option.
57 57 waittimeout = 300
58 58
59 59 # Server-side option. Used only if indextype=sql.
60 60 # Sets mysql innodb_lock_wait_timeout option.
61 61 locktimeout = 120
62 62
63 63 # Server-side option. Used only if indextype=sql.
64 64 # Name of the repository
65 65 reponame = ''
66 66
67 67 # Client-side option. Used by --list-remote option. List of remote scratch
68 68 # patterns to list if no patterns are specified.
69 69 defaultremotepatterns = ['*']
70 70
71 71 # Instructs infinitepush to forward all received bundle2 parts to the
72 72 # bundle for storage. Defaults to False.
73 73 storeallparts = True
74 74
75 # routes each incoming push to the bundlestore. defaults to False
76 pushtobundlestore = True
77
75 78 [remotenames]
76 79 # Client-side option
77 80 # This option should be set only if remotenames extension is enabled.
78 81 # Whether remote bookmarks are tracked by remotenames extension.
79 82 bookmarks = True
80 83 """
81 84
82 85 from __future__ import absolute_import
83 86
84 87 import collections
85 88 import contextlib
86 89 import errno
87 90 import functools
88 91 import logging
89 92 import os
90 93 import random
91 94 import re
92 95 import socket
93 96 import subprocess
94 97 import tempfile
95 98 import time
96 99
97 100 from mercurial.node import (
98 101 bin,
99 102 hex,
100 103 )
101 104
102 105 from mercurial.i18n import _
103 106
104 107 from mercurial.utils import (
105 108 procutil,
106 109 stringutil,
107 110 )
108 111
109 112 from mercurial import (
110 113 bundle2,
111 114 changegroup,
112 115 commands,
113 116 discovery,
114 117 encoding,
115 118 error,
116 119 exchange,
117 120 extensions,
118 121 hg,
119 122 localrepo,
120 123 peer,
121 124 phases,
122 125 pushkey,
123 126 registrar,
124 127 util,
125 128 wireproto,
126 129 )
127 130
128 131 from . import (
129 132 bundleparts,
130 133 common,
131 134 )
132 135
133 136 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
134 137 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
135 138 # be specifying the version(s) of Mercurial they are tested with, or
136 139 # leave the attribute unspecified.
137 140 testedwith = 'ships-with-hg-core'
138 141
139 142 configtable = {}
140 143 configitem = registrar.configitem(configtable)
141 144
142 145 configitem('infinitepush', 'server',
143 146 default=False,
144 147 )
145 148 configitem('infinitepush', 'storetype',
146 149 default='',
147 150 )
148 151 configitem('infinitepush', 'indextype',
149 152 default='',
150 153 )
151 154 configitem('infinitepush', 'indexpath',
152 155 default='',
153 156 )
154 157 configitem('infinitepush', 'storeallparts',
155 158 default=False,
156 159 )
157 160 configitem('infinitepush', 'reponame',
158 161 default='',
159 162 )
160 163 configitem('scratchbranch', 'storepath',
161 164 default='',
162 165 )
163 166 configitem('infinitepush', 'branchpattern',
164 167 default='',
165 168 )
169 configitem('infinitepush', 'pushtobundlestore',
170 default=False,
171 )
166 172 configitem('experimental', 'server-bundlestore-bookmark',
167 173 default='',
168 174 )
169 175 configitem('experimental', 'infinitepush-scratchpush',
170 176 default=False,
171 177 )
172 178
173 179 experimental = 'experimental'
174 180 configbookmark = 'server-bundlestore-bookmark'
175 181 configscratchpush = 'infinitepush-scratchpush'
176 182
177 183 scratchbranchparttype = bundleparts.scratchbranchparttype
178 184 revsetpredicate = registrar.revsetpredicate()
179 185 templatekeyword = registrar.templatekeyword()
180 186 _scratchbranchmatcher = lambda x: False
181 187 _maybehash = re.compile(r'^[a-f0-9]+$').search
182 188
183 189 def _buildexternalbundlestore(ui):
184 190 put_args = ui.configlist('infinitepush', 'put_args', [])
185 191 put_binary = ui.config('infinitepush', 'put_binary')
186 192 if not put_binary:
187 193 raise error.Abort('put binary is not specified')
188 194 get_args = ui.configlist('infinitepush', 'get_args', [])
189 195 get_binary = ui.config('infinitepush', 'get_binary')
190 196 if not get_binary:
191 197 raise error.Abort('get binary is not specified')
192 198 from . import store
193 199 return store.externalbundlestore(put_binary, put_args, get_binary, get_args)
194 200
195 201 def _buildsqlindex(ui):
196 202 sqlhost = ui.config('infinitepush', 'sqlhost')
197 203 if not sqlhost:
198 204 raise error.Abort(_('please set infinitepush.sqlhost'))
199 205 host, port, db, user, password = sqlhost.split(':')
200 206 reponame = ui.config('infinitepush', 'reponame')
201 207 if not reponame:
202 208 raise error.Abort(_('please set infinitepush.reponame'))
203 209
204 210 logfile = ui.config('infinitepush', 'logfile', '')
205 211 waittimeout = ui.configint('infinitepush', 'waittimeout', 300)
206 212 locktimeout = ui.configint('infinitepush', 'locktimeout', 120)
207 213 from . import sqlindexapi
208 214 return sqlindexapi.sqlindexapi(
209 215 reponame, host, port, db, user, password,
210 216 logfile, _getloglevel(ui), waittimeout=waittimeout,
211 217 locktimeout=locktimeout)
212 218
213 219 def _getloglevel(ui):
214 220 loglevel = ui.config('infinitepush', 'loglevel', 'DEBUG')
215 221 numeric_loglevel = getattr(logging, loglevel.upper(), None)
216 222 if not isinstance(numeric_loglevel, int):
217 223 raise error.Abort(_('invalid log level %s') % loglevel)
218 224 return numeric_loglevel
219 225
220 226 def _tryhoist(ui, remotebookmark):
221 227 '''returns a bookmarks with hoisted part removed
222 228
223 229 Remotenames extension has a 'hoist' config that allows to use remote
224 230 bookmarks without specifying remote path. For example, 'hg update master'
225 231 works as well as 'hg update remote/master'. We want to allow the same in
226 232 infinitepush.
227 233 '''
228 234
229 235 if common.isremotebooksenabled(ui):
230 236 hoist = ui.config('remotenames', 'hoist') + '/'
231 237 if remotebookmark.startswith(hoist):
232 238 return remotebookmark[len(hoist):]
233 239 return remotebookmark
234 240
235 241 class bundlestore(object):
236 242 def __init__(self, repo):
237 243 self._repo = repo
238 244 storetype = self._repo.ui.config('infinitepush', 'storetype', '')
239 245 if storetype == 'disk':
240 246 from . import store
241 247 self.store = store.filebundlestore(self._repo.ui, self._repo)
242 248 elif storetype == 'external':
243 249 self.store = _buildexternalbundlestore(self._repo.ui)
244 250 else:
245 251 raise error.Abort(
246 252 _('unknown infinitepush store type specified %s') % storetype)
247 253
248 254 indextype = self._repo.ui.config('infinitepush', 'indextype', '')
249 255 if indextype == 'disk':
250 256 from . import fileindexapi
251 257 self.index = fileindexapi.fileindexapi(self._repo)
252 258 elif indextype == 'sql':
253 259 self.index = _buildsqlindex(self._repo.ui)
254 260 else:
255 261 raise error.Abort(
256 262 _('unknown infinitepush index type specified %s') % indextype)
257 263
258 264 def _isserver(ui):
259 265 return ui.configbool('infinitepush', 'server')
260 266
261 267 def reposetup(ui, repo):
262 268 if _isserver(ui) and repo.local():
263 269 repo.bundlestore = bundlestore(repo)
264 270
265 271 def extsetup(ui):
266 272 commonsetup(ui)
267 273 if _isserver(ui):
268 274 serverextsetup(ui)
269 275 else:
270 276 clientextsetup(ui)
271 277
272 278 def commonsetup(ui):
273 279 wireproto.commands['listkeyspatterns'] = (
274 280 wireprotolistkeyspatterns, 'namespace patterns')
275 281 scratchbranchpat = ui.config('infinitepush', 'branchpattern')
276 282 if scratchbranchpat:
277 283 global _scratchbranchmatcher
278 284 kind, pat, _scratchbranchmatcher = \
279 285 stringutil.stringmatcher(scratchbranchpat)
280 286
281 287 def serverextsetup(ui):
282 288 origpushkeyhandler = bundle2.parthandlermapping['pushkey']
283 289
284 290 def newpushkeyhandler(*args, **kwargs):
285 291 bundle2pushkey(origpushkeyhandler, *args, **kwargs)
286 292 newpushkeyhandler.params = origpushkeyhandler.params
287 293 bundle2.parthandlermapping['pushkey'] = newpushkeyhandler
288 294
289 295 orighandlephasehandler = bundle2.parthandlermapping['phase-heads']
290 296 newphaseheadshandler = lambda *args, **kwargs: \
291 297 bundle2handlephases(orighandlephasehandler, *args, **kwargs)
292 298 newphaseheadshandler.params = orighandlephasehandler.params
293 299 bundle2.parthandlermapping['phase-heads'] = newphaseheadshandler
294 300
295 301 extensions.wrapfunction(localrepo.localrepository, 'listkeys',
296 302 localrepolistkeys)
297 303 wireproto.commands['lookup'] = (
298 304 _lookupwrap(wireproto.commands['lookup'][0]), 'key')
299 305 extensions.wrapfunction(exchange, 'getbundlechunks', getbundlechunks)
300 306
301 307 extensions.wrapfunction(bundle2, 'processparts', processparts)
302 308
303 309 def clientextsetup(ui):
304 310 entry = extensions.wrapcommand(commands.table, 'push', _push)
305 311
306 312 entry[1].append(
307 313 ('', 'bundle-store', None,
308 314 _('force push to go to bundle store (EXPERIMENTAL)')))
309 315
310 316 extensions.wrapcommand(commands.table, 'pull', _pull)
311 317
312 318 extensions.wrapfunction(discovery, 'checkheads', _checkheads)
313 319
314 320 wireproto.wirepeer.listkeyspatterns = listkeyspatterns
315 321
316 322 partorder = exchange.b2partsgenorder
317 323 index = partorder.index('changeset')
318 324 partorder.insert(
319 325 index, partorder.pop(partorder.index(scratchbranchparttype)))
320 326
321 327 def _checkheads(orig, pushop):
322 328 if pushop.ui.configbool(experimental, configscratchpush, False):
323 329 return
324 330 return orig(pushop)
325 331
326 332 def wireprotolistkeyspatterns(repo, proto, namespace, patterns):
327 333 patterns = wireproto.decodelist(patterns)
328 334 d = repo.listkeys(encoding.tolocal(namespace), patterns).iteritems()
329 335 return pushkey.encodekeys(d)
330 336
331 337 def localrepolistkeys(orig, self, namespace, patterns=None):
332 338 if namespace == 'bookmarks' and patterns:
333 339 index = self.bundlestore.index
334 340 results = {}
335 341 bookmarks = orig(self, namespace)
336 342 for pattern in patterns:
337 343 results.update(index.getbookmarks(pattern))
338 344 if pattern.endswith('*'):
339 345 pattern = 're:^' + pattern[:-1] + '.*'
340 346 kind, pat, matcher = stringutil.stringmatcher(pattern)
341 347 for bookmark, node in bookmarks.iteritems():
342 348 if matcher(bookmark):
343 349 results[bookmark] = node
344 350 return results
345 351 else:
346 352 return orig(self, namespace)
347 353
348 354 @peer.batchable
349 355 def listkeyspatterns(self, namespace, patterns):
350 356 if not self.capable('pushkey'):
351 357 yield {}, None
352 358 f = peer.future()
353 359 self.ui.debug('preparing listkeys for "%s" with pattern "%s"\n' %
354 360 (namespace, patterns))
355 361 yield {
356 362 'namespace': encoding.fromlocal(namespace),
357 363 'patterns': wireproto.encodelist(patterns)
358 364 }, f
359 365 d = f.value
360 366 self.ui.debug('received listkey for "%s": %i bytes\n'
361 367 % (namespace, len(d)))
362 368 yield pushkey.decodekeys(d)
363 369
364 370 def _readbundlerevs(bundlerepo):
365 371 return list(bundlerepo.revs('bundle()'))
366 372
367 373 def _includefilelogstobundle(bundlecaps, bundlerepo, bundlerevs, ui):
368 374 '''Tells remotefilelog to include all changed files to the changegroup
369 375
370 376 By default remotefilelog doesn't include file content to the changegroup.
371 377 But we need to include it if we are fetching from bundlestore.
372 378 '''
373 379 changedfiles = set()
374 380 cl = bundlerepo.changelog
375 381 for r in bundlerevs:
376 382 # [3] means changed files
377 383 changedfiles.update(cl.read(r)[3])
378 384 if not changedfiles:
379 385 return bundlecaps
380 386
381 387 changedfiles = '\0'.join(changedfiles)
382 388 newcaps = []
383 389 appended = False
384 390 for cap in (bundlecaps or []):
385 391 if cap.startswith('excludepattern='):
386 392 newcaps.append('\0'.join((cap, changedfiles)))
387 393 appended = True
388 394 else:
389 395 newcaps.append(cap)
390 396 if not appended:
391 397 # Not found excludepattern cap. Just append it
392 398 newcaps.append('excludepattern=' + changedfiles)
393 399
394 400 return newcaps
395 401
396 402 def _rebundle(bundlerepo, bundleroots, unknownhead):
397 403 '''
398 404 Bundle may include more revision then user requested. For example,
399 405 if user asks for revision but bundle also consists its descendants.
400 406 This function will filter out all revision that user is not requested.
401 407 '''
402 408 parts = []
403 409
404 410 version = '02'
405 411 outgoing = discovery.outgoing(bundlerepo, commonheads=bundleroots,
406 412 missingheads=[unknownhead])
407 413 cgstream = changegroup.makestream(bundlerepo, outgoing, version, 'pull')
408 414 cgstream = util.chunkbuffer(cgstream).read()
409 415 cgpart = bundle2.bundlepart('changegroup', data=cgstream)
410 416 cgpart.addparam('version', version)
411 417 parts.append(cgpart)
412 418
413 419 return parts
414 420
415 421 def _getbundleroots(oldrepo, bundlerepo, bundlerevs):
416 422 cl = bundlerepo.changelog
417 423 bundleroots = []
418 424 for rev in bundlerevs:
419 425 node = cl.node(rev)
420 426 parents = cl.parents(node)
421 427 for parent in parents:
422 428 # include all revs that exist in the main repo
423 429 # to make sure that bundle may apply client-side
424 430 if parent in oldrepo:
425 431 bundleroots.append(parent)
426 432 return bundleroots
427 433
428 434 def _needsrebundling(head, bundlerepo):
429 435 bundleheads = list(bundlerepo.revs('heads(bundle())'))
430 436 return not (len(bundleheads) == 1 and
431 437 bundlerepo[bundleheads[0]].node() == head)
432 438
433 439 def _generateoutputparts(head, bundlerepo, bundleroots, bundlefile):
434 440 '''generates bundle that will be send to the user
435 441
436 442 returns tuple with raw bundle string and bundle type
437 443 '''
438 444 parts = []
439 445 if not _needsrebundling(head, bundlerepo):
440 446 with util.posixfile(bundlefile, "rb") as f:
441 447 unbundler = exchange.readbundle(bundlerepo.ui, f, bundlefile)
442 448 if isinstance(unbundler, changegroup.cg1unpacker):
443 449 part = bundle2.bundlepart('changegroup',
444 450 data=unbundler._stream.read())
445 451 part.addparam('version', '01')
446 452 parts.append(part)
447 453 elif isinstance(unbundler, bundle2.unbundle20):
448 454 haschangegroup = False
449 455 for part in unbundler.iterparts():
450 456 if part.type == 'changegroup':
451 457 haschangegroup = True
452 458 newpart = bundle2.bundlepart(part.type, data=part.read())
453 459 for key, value in part.params.iteritems():
454 460 newpart.addparam(key, value)
455 461 parts.append(newpart)
456 462
457 463 if not haschangegroup:
458 464 raise error.Abort(
459 465 'unexpected bundle without changegroup part, ' +
460 466 'head: %s' % hex(head),
461 467 hint='report to administrator')
462 468 else:
463 469 raise error.Abort('unknown bundle type')
464 470 else:
465 471 parts = _rebundle(bundlerepo, bundleroots, head)
466 472
467 473 return parts
468 474
469 475 def getbundlechunks(orig, repo, source, heads=None, bundlecaps=None, **kwargs):
470 476 heads = heads or []
471 477 # newheads are parents of roots of scratch bundles that were requested
472 478 newphases = {}
473 479 scratchbundles = []
474 480 newheads = []
475 481 scratchheads = []
476 482 nodestobundle = {}
477 483 allbundlestocleanup = []
478 484 try:
479 485 for head in heads:
480 486 if head not in repo.changelog.nodemap:
481 487 if head not in nodestobundle:
482 488 newbundlefile = common.downloadbundle(repo, head)
483 489 bundlepath = "bundle:%s+%s" % (repo.root, newbundlefile)
484 490 bundlerepo = hg.repository(repo.ui, bundlepath)
485 491
486 492 allbundlestocleanup.append((bundlerepo, newbundlefile))
487 493 bundlerevs = set(_readbundlerevs(bundlerepo))
488 494 bundlecaps = _includefilelogstobundle(
489 495 bundlecaps, bundlerepo, bundlerevs, repo.ui)
490 496 cl = bundlerepo.changelog
491 497 bundleroots = _getbundleroots(repo, bundlerepo, bundlerevs)
492 498 for rev in bundlerevs:
493 499 node = cl.node(rev)
494 500 newphases[hex(node)] = str(phases.draft)
495 501 nodestobundle[node] = (bundlerepo, bundleroots,
496 502 newbundlefile)
497 503
498 504 scratchbundles.append(
499 505 _generateoutputparts(head, *nodestobundle[head]))
500 506 newheads.extend(bundleroots)
501 507 scratchheads.append(head)
502 508 finally:
503 509 for bundlerepo, bundlefile in allbundlestocleanup:
504 510 bundlerepo.close()
505 511 try:
506 512 os.unlink(bundlefile)
507 513 except (IOError, OSError):
508 514 # if we can't cleanup the file then just ignore the error,
509 515 # no need to fail
510 516 pass
511 517
512 518 pullfrombundlestore = bool(scratchbundles)
513 519 wrappedchangegrouppart = False
514 520 wrappedlistkeys = False
515 521 oldchangegrouppart = exchange.getbundle2partsmapping['changegroup']
516 522 try:
517 523 def _changegrouppart(bundler, *args, **kwargs):
518 524 # Order is important here. First add non-scratch part
519 525 # and only then add parts with scratch bundles because
520 526 # non-scratch part contains parents of roots of scratch bundles.
521 527 result = oldchangegrouppart(bundler, *args, **kwargs)
522 528 for bundle in scratchbundles:
523 529 for part in bundle:
524 530 bundler.addpart(part)
525 531 return result
526 532
527 533 exchange.getbundle2partsmapping['changegroup'] = _changegrouppart
528 534 wrappedchangegrouppart = True
529 535
530 536 def _listkeys(orig, self, namespace):
531 537 origvalues = orig(self, namespace)
532 538 if namespace == 'phases' and pullfrombundlestore:
533 539 if origvalues.get('publishing') == 'True':
534 540 # Make repo non-publishing to preserve draft phase
535 541 del origvalues['publishing']
536 542 origvalues.update(newphases)
537 543 return origvalues
538 544
539 545 extensions.wrapfunction(localrepo.localrepository, 'listkeys',
540 546 _listkeys)
541 547 wrappedlistkeys = True
542 548 heads = list((set(newheads) | set(heads)) - set(scratchheads))
543 549 result = orig(repo, source, heads=heads,
544 550 bundlecaps=bundlecaps, **kwargs)
545 551 finally:
546 552 if wrappedchangegrouppart:
547 553 exchange.getbundle2partsmapping['changegroup'] = oldchangegrouppart
548 554 if wrappedlistkeys:
549 555 extensions.unwrapfunction(localrepo.localrepository, 'listkeys',
550 556 _listkeys)
551 557 return result
552 558
553 559 def _lookupwrap(orig):
554 560 def _lookup(repo, proto, key):
555 561 localkey = encoding.tolocal(key)
556 562
557 563 if isinstance(localkey, str) and _scratchbranchmatcher(localkey):
558 564 scratchnode = repo.bundlestore.index.getnode(localkey)
559 565 if scratchnode:
560 566 return "%s %s\n" % (1, scratchnode)
561 567 else:
562 568 return "%s %s\n" % (0, 'scratch branch %s not found' % localkey)
563 569 else:
564 570 try:
565 571 r = hex(repo.lookup(localkey))
566 572 return "%s %s\n" % (1, r)
567 573 except Exception as inst:
568 574 if repo.bundlestore.index.getbundle(localkey):
569 575 return "%s %s\n" % (1, localkey)
570 576 else:
571 577 r = str(inst)
572 578 return "%s %s\n" % (0, r)
573 579 return _lookup
574 580
575 581 def _pull(orig, ui, repo, source="default", **opts):
576 582 # Copy paste from `pull` command
577 583 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
578 584
579 585 scratchbookmarks = {}
580 586 unfi = repo.unfiltered()
581 587 unknownnodes = []
582 588 for rev in opts.get('rev', []):
583 589 if rev not in unfi:
584 590 unknownnodes.append(rev)
585 591 if opts.get('bookmark'):
586 592 bookmarks = []
587 593 revs = opts.get('rev') or []
588 594 for bookmark in opts.get('bookmark'):
589 595 if _scratchbranchmatcher(bookmark):
590 596 # rev is not known yet
591 597 # it will be fetched with listkeyspatterns next
592 598 scratchbookmarks[bookmark] = 'REVTOFETCH'
593 599 else:
594 600 bookmarks.append(bookmark)
595 601
596 602 if scratchbookmarks:
597 603 other = hg.peer(repo, opts, source)
598 604 fetchedbookmarks = other.listkeyspatterns(
599 605 'bookmarks', patterns=scratchbookmarks)
600 606 for bookmark in scratchbookmarks:
601 607 if bookmark not in fetchedbookmarks:
602 608 raise error.Abort('remote bookmark %s not found!' %
603 609 bookmark)
604 610 scratchbookmarks[bookmark] = fetchedbookmarks[bookmark]
605 611 revs.append(fetchedbookmarks[bookmark])
606 612 opts['bookmark'] = bookmarks
607 613 opts['rev'] = revs
608 614
609 615 if scratchbookmarks or unknownnodes:
610 616 # Set anyincoming to True
611 617 extensions.wrapfunction(discovery, 'findcommonincoming',
612 618 _findcommonincoming)
613 619 try:
614 620 # Remote scratch bookmarks will be deleted because remotenames doesn't
615 621 # know about them. Let's save it before pull and restore after
616 622 remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, source)
617 623 result = orig(ui, repo, source, **opts)
618 624 # TODO(stash): race condition is possible
619 625 # if scratch bookmarks was updated right after orig.
620 626 # But that's unlikely and shouldn't be harmful.
621 627 if common.isremotebooksenabled(ui):
622 628 remotescratchbookmarks.update(scratchbookmarks)
623 629 _saveremotebookmarks(repo, remotescratchbookmarks, source)
624 630 else:
625 631 _savelocalbookmarks(repo, scratchbookmarks)
626 632 return result
627 633 finally:
628 634 if scratchbookmarks:
629 635 extensions.unwrapfunction(discovery, 'findcommonincoming')
630 636
631 637 def _readscratchremotebookmarks(ui, repo, other):
632 638 if common.isremotebooksenabled(ui):
633 639 remotenamesext = extensions.find('remotenames')
634 640 remotepath = remotenamesext.activepath(repo.ui, other)
635 641 result = {}
636 642 # Let's refresh remotenames to make sure we have it up to date
637 643 # Seems that `repo.names['remotebookmarks']` may return stale bookmarks
638 644 # and it results in deleting scratch bookmarks. Our best guess how to
639 645 # fix it is to use `clearnames()`
640 646 repo._remotenames.clearnames()
641 647 for remotebookmark in repo.names['remotebookmarks'].listnames(repo):
642 648 path, bookname = remotenamesext.splitremotename(remotebookmark)
643 649 if path == remotepath and _scratchbranchmatcher(bookname):
644 650 nodes = repo.names['remotebookmarks'].nodes(repo,
645 651 remotebookmark)
646 652 if nodes:
647 653 result[bookname] = hex(nodes[0])
648 654 return result
649 655 else:
650 656 return {}
651 657
652 658 def _saveremotebookmarks(repo, newbookmarks, remote):
653 659 remotenamesext = extensions.find('remotenames')
654 660 remotepath = remotenamesext.activepath(repo.ui, remote)
655 661 branches = collections.defaultdict(list)
656 662 bookmarks = {}
657 663 remotenames = remotenamesext.readremotenames(repo)
658 664 for hexnode, nametype, remote, rname in remotenames:
659 665 if remote != remotepath:
660 666 continue
661 667 if nametype == 'bookmarks':
662 668 if rname in newbookmarks:
663 669 # It's possible if we have a normal bookmark that matches
664 670 # scratch branch pattern. In this case just use the current
665 671 # bookmark node
666 672 del newbookmarks[rname]
667 673 bookmarks[rname] = hexnode
668 674 elif nametype == 'branches':
669 675 # saveremotenames expects 20 byte binary nodes for branches
670 676 branches[rname].append(bin(hexnode))
671 677
672 678 for bookmark, hexnode in newbookmarks.iteritems():
673 679 bookmarks[bookmark] = hexnode
674 680 remotenamesext.saveremotenames(repo, remotepath, branches, bookmarks)
675 681
676 682 def _savelocalbookmarks(repo, bookmarks):
677 683 if not bookmarks:
678 684 return
679 685 with repo.wlock(), repo.lock(), repo.transaction('bookmark') as tr:
680 686 changes = []
681 687 for scratchbook, node in bookmarks.iteritems():
682 688 changectx = repo[node]
683 689 changes.append((scratchbook, changectx.node()))
684 690 repo._bookmarks.applychanges(repo, tr, changes)
685 691
686 692 def _findcommonincoming(orig, *args, **kwargs):
687 693 common, inc, remoteheads = orig(*args, **kwargs)
688 694 return common, True, remoteheads
689 695
690 696 def _push(orig, ui, repo, dest=None, *args, **opts):
691 697
692 698 bookmark = opts.get('bookmark')
693 699 # we only support pushing one infinitepush bookmark at once
694 700 if len(bookmark) == 1:
695 701 bookmark = bookmark[0]
696 702 else:
697 703 bookmark = ''
698 704
699 705 oldphasemove = None
700 706 overrides = {(experimental, configbookmark): bookmark}
701 707
702 708 with ui.configoverride(overrides, 'infinitepush'):
703 709 scratchpush = opts.get('bundle_store')
704 710 if _scratchbranchmatcher(bookmark):
705 711 scratchpush = True
706 712 # bundle2 can be sent back after push (for example, bundle2
707 713 # containing `pushkey` part to update bookmarks)
708 714 ui.setconfig(experimental, 'bundle2.pushback', True)
709 715
710 716 if scratchpush:
711 717 # this is an infinitepush, we don't want the bookmark to be applied
712 718 # rather that should be stored in the bundlestore
713 719 opts['bookmark'] = []
714 720 ui.setconfig(experimental, configscratchpush, True)
715 721 oldphasemove = extensions.wrapfunction(exchange,
716 722 '_localphasemove',
717 723 _phasemove)
718 724 # Copy-paste from `push` command
719 725 path = ui.paths.getpath(dest, default=('default-push', 'default'))
720 726 if not path:
721 727 raise error.Abort(_('default repository not configured!'),
722 728 hint=_("see 'hg help config.paths'"))
723 729 destpath = path.pushloc or path.loc
724 730 # Remote scratch bookmarks will be deleted because remotenames doesn't
725 731 # know about them. Let's save it before push and restore after
726 732 remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, destpath)
727 733 result = orig(ui, repo, dest, *args, **opts)
728 734 if common.isremotebooksenabled(ui):
729 735 if bookmark and scratchpush:
730 736 other = hg.peer(repo, opts, destpath)
731 737 fetchedbookmarks = other.listkeyspatterns('bookmarks',
732 738 patterns=[bookmark])
733 739 remotescratchbookmarks.update(fetchedbookmarks)
734 740 _saveremotebookmarks(repo, remotescratchbookmarks, destpath)
735 741 if oldphasemove:
736 742 exchange._localphasemove = oldphasemove
737 743 return result
738 744
739 745 def _deleteinfinitepushbookmarks(ui, repo, path, names):
740 746 """Prune remote names by removing the bookmarks we don't want anymore,
741 747 then writing the result back to disk
742 748 """
743 749 remotenamesext = extensions.find('remotenames')
744 750
745 751 # remotename format is:
746 752 # (node, nametype ("branches" or "bookmarks"), remote, name)
747 753 nametype_idx = 1
748 754 remote_idx = 2
749 755 name_idx = 3
750 756 remotenames = [remotename for remotename in \
751 757 remotenamesext.readremotenames(repo) \
752 758 if remotename[remote_idx] == path]
753 759 remote_bm_names = [remotename[name_idx] for remotename in \
754 760 remotenames if remotename[nametype_idx] == "bookmarks"]
755 761
756 762 for name in names:
757 763 if name not in remote_bm_names:
758 764 raise error.Abort(_("infinitepush bookmark '{}' does not exist "
759 765 "in path '{}'").format(name, path))
760 766
761 767 bookmarks = {}
762 768 branches = collections.defaultdict(list)
763 769 for node, nametype, remote, name in remotenames:
764 770 if nametype == "bookmarks" and name not in names:
765 771 bookmarks[name] = node
766 772 elif nametype == "branches":
767 773 # saveremotenames wants binary nodes for branches
768 774 branches[name].append(bin(node))
769 775
770 776 remotenamesext.saveremotenames(repo, path, branches, bookmarks)
771 777
772 778 def _phasemove(orig, pushop, nodes, phase=phases.public):
773 779 """prevent commits from being marked public
774 780
775 781 Since these are going to a scratch branch, they aren't really being
776 782 published."""
777 783
778 784 if phase != phases.public:
779 785 orig(pushop, nodes, phase)
780 786
781 787 @exchange.b2partsgenerator(scratchbranchparttype)
782 788 def partgen(pushop, bundler):
783 789 bookmark = pushop.ui.config(experimental, configbookmark)
784 790 scratchpush = pushop.ui.configbool(experimental, configscratchpush)
785 791 if 'changesets' in pushop.stepsdone or not scratchpush:
786 792 return
787 793
788 794 if scratchbranchparttype not in bundle2.bundle2caps(pushop.remote):
789 795 return
790 796
791 797 pushop.stepsdone.add('changesets')
792 798 if not pushop.outgoing.missing:
793 799 pushop.ui.status(_('no changes found\n'))
794 800 pushop.cgresult = 0
795 801 return
796 802
797 803 # This parameter tells the server that the following bundle is an
798 804 # infinitepush. This let's it switch the part processing to our infinitepush
799 805 # code path.
800 806 bundler.addparam("infinitepush", "True")
801 807
802 808 scratchparts = bundleparts.getscratchbranchparts(pushop.repo,
803 809 pushop.remote,
804 810 pushop.outgoing,
805 811 pushop.ui,
806 812 bookmark)
807 813
808 814 for scratchpart in scratchparts:
809 815 bundler.addpart(scratchpart)
810 816
811 817 def handlereply(op):
812 818 # server either succeeds or aborts; no code to read
813 819 pushop.cgresult = 1
814 820
815 821 return handlereply
816 822
817 823 bundle2.capabilities[bundleparts.scratchbranchparttype] = ()
818 824
819 825 def _getrevs(bundle, oldnode, force, bookmark):
820 826 'extracts and validates the revs to be imported'
821 827 revs = [bundle[r] for r in bundle.revs('sort(bundle())')]
822 828
823 829 # new bookmark
824 830 if oldnode is None:
825 831 return revs
826 832
827 833 # Fast forward update
828 834 if oldnode in bundle and list(bundle.set('bundle() & %s::', oldnode)):
829 835 return revs
830 836
831 837 return revs
832 838
833 839 @contextlib.contextmanager
834 840 def logservicecall(logger, service, **kwargs):
835 841 start = time.time()
836 842 logger(service, eventtype='start', **kwargs)
837 843 try:
838 844 yield
839 845 logger(service, eventtype='success',
840 846 elapsedms=(time.time() - start) * 1000, **kwargs)
841 847 except Exception as e:
842 848 logger(service, eventtype='failure',
843 849 elapsedms=(time.time() - start) * 1000, errormsg=str(e),
844 850 **kwargs)
845 851 raise
846 852
847 853 def _getorcreateinfinitepushlogger(op):
848 854 logger = op.records['infinitepushlogger']
849 855 if not logger:
850 856 ui = op.repo.ui
851 857 try:
852 858 username = procutil.getuser()
853 859 except Exception:
854 860 username = 'unknown'
855 861 # Generate random request id to be able to find all logged entries
856 862 # for the same request. Since requestid is pseudo-generated it may
857 863 # not be unique, but we assume that (hostname, username, requestid)
858 864 # is unique.
859 865 random.seed()
860 866 requestid = random.randint(0, 2000000000)
861 867 hostname = socket.gethostname()
862 868 logger = functools.partial(ui.log, 'infinitepush', user=username,
863 869 requestid=requestid, hostname=hostname,
864 870 reponame=ui.config('infinitepush',
865 871 'reponame'))
866 872 op.records.add('infinitepushlogger', logger)
867 873 else:
868 874 logger = logger[0]
869 875 return logger
870 876
877 def storetobundlestore(orig, repo, op, unbundler):
878 """stores the incoming bundle coming from push command to the bundlestore
879 instead of applying on the revlogs"""
880
881 repo.ui.status(_("storing changesets on the bundlestore\n"))
882 bundler = bundle2.bundle20(repo.ui)
883
884 # processing each part and storing it in bundler
885 with bundle2.partiterator(repo, op, unbundler) as parts:
886 for part in parts:
887 bundlepart = None
888 if part.type == 'replycaps':
889 # This configures the current operation to allow reply parts.
890 bundle2._processpart(op, part)
891 else:
892 bundlepart = bundle2.bundlepart(part.type, data=part.read())
893 for key, value in part.params.iteritems():
894 bundlepart.addparam(key, value)
895
896 # Certain parts require a response
897 if part.type in ('pushkey', 'changegroup'):
898 if op.reply is not None:
899 rpart = op.reply.newpart('reply:%s' % part.type)
900 rpart.addparam('in-reply-to', str(part.id),
901 mandatory=False)
902 rpart.addparam('return', '1', mandatory=False)
903
904 op.records.add(part.type, {
905 'return': 1,
906 })
907 if bundlepart:
908 bundler.addpart(bundlepart)
909
910 # storing the bundle in the bundlestore
911 buf = util.chunkbuffer(bundler.getchunks())
912 fd, bundlefile = tempfile.mkstemp()
913 try:
914 try:
915 fp = os.fdopen(fd, 'wb')
916 fp.write(buf.read())
917 finally:
918 fp.close()
919 storebundle(op, {}, bundlefile)
920 finally:
921 try:
922 os.unlink(bundlefile)
923 except Exception:
924 # we would rather see the original exception
925 pass
926
871 927 def processparts(orig, repo, op, unbundler):
872 928
873 929 # make sure we don't wrap processparts in case of `hg unbundle`
874 930 tr = repo.currenttransaction()
875 931 if tr:
876 932 if tr.names[0].startswith('unbundle'):
877 933 return orig(repo, op, unbundler)
878 934
935 # this server routes each push to bundle store
936 if repo.ui.configbool('infinitepush', 'pushtobundlestore'):
937 return storetobundlestore(orig, repo, op, unbundler)
938
879 939 if unbundler.params.get('infinitepush') != 'True':
880 940 return orig(repo, op, unbundler)
881 941
882 942 handleallparts = repo.ui.configbool('infinitepush', 'storeallparts')
883 943
884 944 bundler = bundle2.bundle20(repo.ui)
885 945 cgparams = None
886 946 with bundle2.partiterator(repo, op, unbundler) as parts:
887 947 for part in parts:
888 948 bundlepart = None
889 949 if part.type == 'replycaps':
890 950 # This configures the current operation to allow reply parts.
891 951 bundle2._processpart(op, part)
892 952 elif part.type == bundleparts.scratchbranchparttype:
893 953 # Scratch branch parts need to be converted to normal
894 954 # changegroup parts, and the extra parameters stored for later
895 955 # when we upload to the store. Eventually those parameters will
896 956 # be put on the actual bundle instead of this part, then we can
897 957 # send a vanilla changegroup instead of the scratchbranch part.
898 958 cgversion = part.params.get('cgversion', '01')
899 959 bundlepart = bundle2.bundlepart('changegroup', data=part.read())
900 960 bundlepart.addparam('version', cgversion)
901 961 cgparams = part.params
902 962
903 963 # If we're not dumping all parts into the new bundle, we need to
904 964 # alert the future pushkey and phase-heads handler to skip
905 965 # the part.
906 966 if not handleallparts:
907 967 op.records.add(scratchbranchparttype + '_skippushkey', True)
908 968 op.records.add(scratchbranchparttype + '_skipphaseheads',
909 969 True)
910 970 else:
911 971 if handleallparts:
912 972 # Ideally we would not process any parts, and instead just
913 973 # forward them to the bundle for storage, but since this
914 974 # differs from previous behavior, we need to put it behind a
915 975 # config flag for incremental rollout.
916 976 bundlepart = bundle2.bundlepart(part.type, data=part.read())
917 977 for key, value in part.params.iteritems():
918 978 bundlepart.addparam(key, value)
919 979
920 980 # Certain parts require a response
921 981 if part.type == 'pushkey':
922 982 if op.reply is not None:
923 983 rpart = op.reply.newpart('reply:pushkey')
924 984 rpart.addparam('in-reply-to', str(part.id),
925 985 mandatory=False)
926 986 rpart.addparam('return', '1', mandatory=False)
927 987 else:
928 988 bundle2._processpart(op, part)
929 989
930 990 if handleallparts:
931 991 op.records.add(part.type, {
932 992 'return': 1,
933 993 })
934 994 if bundlepart:
935 995 bundler.addpart(bundlepart)
936 996
937 997 # If commits were sent, store them
938 998 if cgparams:
939 999 buf = util.chunkbuffer(bundler.getchunks())
940 1000 fd, bundlefile = tempfile.mkstemp()
941 1001 try:
942 1002 try:
943 1003 fp = os.fdopen(fd, 'wb')
944 1004 fp.write(buf.read())
945 1005 finally:
946 1006 fp.close()
947 1007 storebundle(op, cgparams, bundlefile)
948 1008 finally:
949 1009 try:
950 1010 os.unlink(bundlefile)
951 1011 except Exception:
952 1012 # we would rather see the original exception
953 1013 pass
954 1014
955 1015 def storebundle(op, params, bundlefile):
956 1016 log = _getorcreateinfinitepushlogger(op)
957 1017 parthandlerstart = time.time()
958 1018 log(scratchbranchparttype, eventtype='start')
959 1019 index = op.repo.bundlestore.index
960 1020 store = op.repo.bundlestore.store
961 1021 op.records.add(scratchbranchparttype + '_skippushkey', True)
962 1022
963 1023 bundle = None
964 1024 try: # guards bundle
965 1025 bundlepath = "bundle:%s+%s" % (op.repo.root, bundlefile)
966 1026 bundle = hg.repository(op.repo.ui, bundlepath)
967 1027
968 1028 bookmark = params.get('bookmark')
969 1029 bookprevnode = params.get('bookprevnode', '')
970 1030 force = params.get('force')
971 1031
972 1032 if bookmark:
973 1033 oldnode = index.getnode(bookmark)
974 1034 else:
975 1035 oldnode = None
976 1036 bundleheads = bundle.revs('heads(bundle())')
977 1037 if bookmark and len(bundleheads) > 1:
978 1038 raise error.Abort(
979 1039 _('cannot push more than one head to a scratch branch'))
980 1040
981 1041 revs = _getrevs(bundle, oldnode, force, bookmark)
982 1042
983 1043 # Notify the user of what is being pushed
984 1044 plural = 's' if len(revs) > 1 else ''
985 1045 op.repo.ui.warn(_("pushing %s commit%s:\n") % (len(revs), plural))
986 1046 maxoutput = 10
987 1047 for i in range(0, min(len(revs), maxoutput)):
988 1048 firstline = bundle[revs[i]].description().split('\n')[0][:50]
989 1049 op.repo.ui.warn((" %s %s\n") % (revs[i], firstline))
990 1050
991 1051 if len(revs) > maxoutput + 1:
992 1052 op.repo.ui.warn((" ...\n"))
993 1053 firstline = bundle[revs[-1]].description().split('\n')[0][:50]
994 1054 op.repo.ui.warn((" %s %s\n") % (revs[-1], firstline))
995 1055
996 1056 nodesctx = [bundle[rev] for rev in revs]
997 1057 inindex = lambda rev: bool(index.getbundle(bundle[rev].hex()))
998 1058 if bundleheads:
999 1059 newheadscount = sum(not inindex(rev) for rev in bundleheads)
1000 1060 else:
1001 1061 newheadscount = 0
1002 1062 # If there's a bookmark specified, there should be only one head,
1003 1063 # so we choose the last node, which will be that head.
1004 1064 # If a bug or malicious client allows there to be a bookmark
1005 1065 # with multiple heads, we will place the bookmark on the last head.
1006 1066 bookmarknode = nodesctx[-1].hex() if nodesctx else None
1007 1067 key = None
1008 1068 if newheadscount:
1009 1069 with open(bundlefile, 'r') as f:
1010 1070 bundledata = f.read()
1011 1071 with logservicecall(log, 'bundlestore',
1012 1072 bundlesize=len(bundledata)):
1013 1073 bundlesizelimit = 100 * 1024 * 1024 # 100 MB
1014 1074 if len(bundledata) > bundlesizelimit:
1015 1075 error_msg = ('bundle is too big: %d bytes. ' +
1016 1076 'max allowed size is 100 MB')
1017 1077 raise error.Abort(error_msg % (len(bundledata),))
1018 1078 key = store.write(bundledata)
1019 1079
1020 1080 with logservicecall(log, 'index', newheadscount=newheadscount), index:
1021 1081 if key:
1022 1082 index.addbundle(key, nodesctx)
1023 1083 if bookmark:
1024 1084 index.addbookmark(bookmark, bookmarknode)
1025 1085 _maybeaddpushbackpart(op, bookmark, bookmarknode,
1026 1086 bookprevnode, params)
1027 1087 log(scratchbranchparttype, eventtype='success',
1028 1088 elapsedms=(time.time() - parthandlerstart) * 1000)
1029 1089
1030 1090 except Exception as e:
1031 1091 log(scratchbranchparttype, eventtype='failure',
1032 1092 elapsedms=(time.time() - parthandlerstart) * 1000,
1033 1093 errormsg=str(e))
1034 1094 raise
1035 1095 finally:
1036 1096 if bundle:
1037 1097 bundle.close()
1038 1098
1039 1099 @bundle2.parthandler(scratchbranchparttype,
1040 1100 ('bookmark', 'bookprevnode', 'force',
1041 1101 'pushbackbookmarks', 'cgversion'))
1042 1102 def bundle2scratchbranch(op, part):
1043 1103 '''unbundle a bundle2 part containing a changegroup to store'''
1044 1104
1045 1105 bundler = bundle2.bundle20(op.repo.ui)
1046 1106 cgversion = part.params.get('cgversion', '01')
1047 1107 cgpart = bundle2.bundlepart('changegroup', data=part.read())
1048 1108 cgpart.addparam('version', cgversion)
1049 1109 bundler.addpart(cgpart)
1050 1110 buf = util.chunkbuffer(bundler.getchunks())
1051 1111
1052 1112 fd, bundlefile = tempfile.mkstemp()
1053 1113 try:
1054 1114 try:
1055 1115 fp = os.fdopen(fd, 'wb')
1056 1116 fp.write(buf.read())
1057 1117 finally:
1058 1118 fp.close()
1059 1119 storebundle(op, part.params, bundlefile)
1060 1120 finally:
1061 1121 try:
1062 1122 os.unlink(bundlefile)
1063 1123 except OSError as e:
1064 1124 if e.errno != errno.ENOENT:
1065 1125 raise
1066 1126
1067 1127 return 1
1068 1128
1069 1129 def _maybeaddpushbackpart(op, bookmark, newnode, oldnode, params):
1070 1130 if params.get('pushbackbookmarks'):
1071 1131 if op.reply and 'pushback' in op.reply.capabilities:
1072 1132 params = {
1073 1133 'namespace': 'bookmarks',
1074 1134 'key': bookmark,
1075 1135 'new': newnode,
1076 1136 'old': oldnode,
1077 1137 }
1078 1138 op.reply.newpart('pushkey', mandatoryparams=params.iteritems())
1079 1139
1080 1140 def bundle2pushkey(orig, op, part):
1081 1141 '''Wrapper of bundle2.handlepushkey()
1082 1142
1083 1143 The only goal is to skip calling the original function if flag is set.
1084 1144 It's set if infinitepush push is happening.
1085 1145 '''
1086 1146 if op.records[scratchbranchparttype + '_skippushkey']:
1087 1147 if op.reply is not None:
1088 1148 rpart = op.reply.newpart('reply:pushkey')
1089 1149 rpart.addparam('in-reply-to', str(part.id), mandatory=False)
1090 1150 rpart.addparam('return', '1', mandatory=False)
1091 1151 return 1
1092 1152
1093 1153 return orig(op, part)
1094 1154
1095 1155 def bundle2handlephases(orig, op, part):
1096 1156 '''Wrapper of bundle2.handlephases()
1097 1157
1098 1158 The only goal is to skip calling the original function if flag is set.
1099 1159 It's set if infinitepush push is happening.
1100 1160 '''
1101 1161
1102 1162 if op.records[scratchbranchparttype + '_skipphaseheads']:
1103 1163 return
1104 1164
1105 1165 return orig(op, part)
1106 1166
1107 1167 def _asyncsavemetadata(root, nodes):
1108 1168 '''starts a separate process that fills metadata for the nodes
1109 1169
1110 1170 This function creates a separate process and doesn't wait for it's
1111 1171 completion. This was done to avoid slowing down pushes
1112 1172 '''
1113 1173
1114 1174 maxnodes = 50
1115 1175 if len(nodes) > maxnodes:
1116 1176 return
1117 1177 nodesargs = []
1118 1178 for node in nodes:
1119 1179 nodesargs.append('--node')
1120 1180 nodesargs.append(node)
1121 1181 with open(os.devnull, 'w+b') as devnull:
1122 1182 cmdline = [util.hgexecutable(), 'debugfillinfinitepushmetadata',
1123 1183 '-R', root] + nodesargs
1124 1184 # Process will run in background. We don't care about the return code
1125 1185 subprocess.Popen(cmdline, close_fds=True, shell=False,
1126 1186 stdin=devnull, stdout=devnull, stderr=devnull)
General Comments 0
You need to be logged in to leave comments. Login now