##// END OF EJS Templates
largefiles: batch statlfile requests when pushing a largefiles repo (issue3386)...
Na'Tosha Bard -
r17127:9e161630 default
parent child Browse files
Show More
@@ -1,195 +1,195 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''base class for store implementations and store-related utility code'''
10 10
11 11 import binascii
12 12 import re
13 13
14 14 from mercurial import util, node, hg
15 15 from mercurial.i18n import _
16 16
17 17 import lfutil
18 18
19 19 class StoreError(Exception):
20 20 '''Raised when there is a problem getting files from or putting
21 21 files to a central store.'''
22 22 def __init__(self, filename, hash, url, detail):
23 23 self.filename = filename
24 24 self.hash = hash
25 25 self.url = url
26 26 self.detail = detail
27 27
28 28 def longmessage(self):
29 29 if self.url:
30 30 return ('%s: %s\n'
31 31 '(failed URL: %s)\n'
32 32 % (self.filename, self.detail, self.url))
33 33 else:
34 34 return ('%s: %s\n'
35 35 '(no default or default-push path set in hgrc)\n'
36 36 % (self.filename, self.detail))
37 37
38 38 def __str__(self):
39 39 return "%s: %s" % (self.url, self.detail)
40 40
41 41 class basestore(object):
42 42 def __init__(self, ui, repo, url):
43 43 self.ui = ui
44 44 self.repo = repo
45 45 self.url = url
46 46
47 47 def put(self, source, hash):
48 48 '''Put source file into the store under <filename>/<hash>.'''
49 49 raise NotImplementedError('abstract method')
50 50
51 def exists(self, hash):
52 '''Check to see if the store contains the given hash.'''
51 def exists(self, hashes):
52 '''Check to see if the store contains the given hashes.'''
53 53 raise NotImplementedError('abstract method')
54 54
55 55 def get(self, files):
56 56 '''Get the specified largefiles from the store and write to local
57 57 files under repo.root. files is a list of (filename, hash)
58 58 tuples. Return (success, missing), lists of files successfuly
59 59 downloaded and those not found in the store. success is a list
60 60 of (filename, hash) tuples; missing is a list of filenames that
61 61 we could not get. (The detailed error message will already have
62 62 been presented to the user, so missing is just supplied as a
63 63 summary.)'''
64 64 success = []
65 65 missing = []
66 66 ui = self.ui
67 67
68 68 at = 0
69 69 for filename, hash in files:
70 70 ui.progress(_('getting largefiles'), at, unit='lfile',
71 71 total=len(files))
72 72 at += 1
73 73 ui.note(_('getting %s:%s\n') % (filename, hash))
74 74
75 75 storefilename = lfutil.storepath(self.repo, hash)
76 76 tmpfile = util.atomictempfile(storefilename,
77 77 createmode=self.repo.store.createmode)
78 78
79 79 try:
80 80 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
81 81 except StoreError, err:
82 82 ui.warn(err.longmessage())
83 83 hhash = ""
84 84
85 85 if hhash != hash:
86 86 if hhash != "":
87 87 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
88 88 % (filename, hash, hhash))
89 89 tmpfile.discard() # no-op if it's already closed
90 90 missing.append(filename)
91 91 continue
92 92
93 93 tmpfile.close()
94 94 lfutil.linktousercache(self.repo, hash)
95 95 success.append((filename, hhash))
96 96
97 97 ui.progress(_('getting largefiles'), None)
98 98 return (success, missing)
99 99
100 100 def verify(self, revs, contents=False):
101 101 '''Verify the existence (and, optionally, contents) of every big
102 102 file revision referenced by every changeset in revs.
103 103 Return 0 if all is well, non-zero on any errors.'''
104 104 write = self.ui.write
105 105 failed = False
106 106
107 107 write(_('searching %d changesets for largefiles\n') % len(revs))
108 108 verified = set() # set of (filename, filenode) tuples
109 109
110 110 for rev in revs:
111 111 cctx = self.repo[rev]
112 112 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
113 113
114 114 failed = util.any(self._verifyfile(
115 115 cctx, cset, contents, standin, verified) for standin in cctx)
116 116
117 117 numrevs = len(verified)
118 118 numlfiles = len(set([fname for (fname, fnode) in verified]))
119 119 if contents:
120 120 write(_('verified contents of %d revisions of %d largefiles\n')
121 121 % (numrevs, numlfiles))
122 122 else:
123 123 write(_('verified existence of %d revisions of %d largefiles\n')
124 124 % (numrevs, numlfiles))
125 125
126 126 return int(failed)
127 127
128 128 def _getfile(self, tmpfile, filename, hash):
129 129 '''Fetch one revision of one file from the store and write it
130 130 to tmpfile. Compute the hash of the file on-the-fly as it
131 131 downloads and return the binary hash. Close tmpfile. Raise
132 132 StoreError if unable to download the file (e.g. it does not
133 133 exist in the store).'''
134 134 raise NotImplementedError('abstract method')
135 135
136 136 def _verifyfile(self, cctx, cset, contents, standin, verified):
137 137 '''Perform the actual verification of a file in the store.
138 138 '''
139 139 raise NotImplementedError('abstract method')
140 140
141 141 import localstore, wirestore
142 142
143 143 _storeprovider = {
144 144 'file': [localstore.localstore],
145 145 'http': [wirestore.wirestore],
146 146 'https': [wirestore.wirestore],
147 147 'ssh': [wirestore.wirestore],
148 148 }
149 149
150 150 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
151 151
152 152 # During clone this function is passed the src's ui object
153 153 # but it needs the dest's ui object so it can read out of
154 154 # the config file. Use repo.ui instead.
155 155 def _openstore(repo, remote=None, put=False):
156 156 ui = repo.ui
157 157
158 158 if not remote:
159 159 lfpullsource = getattr(repo, 'lfpullsource', None)
160 160 if lfpullsource:
161 161 path = ui.expandpath(lfpullsource)
162 162 else:
163 163 path = ui.expandpath('default-push', 'default')
164 164
165 165 # ui.expandpath() leaves 'default-push' and 'default' alone if
166 166 # they cannot be expanded: fallback to the empty string,
167 167 # meaning the current directory.
168 168 if path == 'default-push' or path == 'default':
169 169 path = ''
170 170 remote = repo
171 171 else:
172 172 remote = hg.peer(repo, {}, path)
173 173
174 174 # The path could be a scheme so use Mercurial's normal functionality
175 175 # to resolve the scheme to a repository and use its path
176 176 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
177 177
178 178 match = _scheme_re.match(path)
179 179 if not match: # regular filesystem path
180 180 scheme = 'file'
181 181 else:
182 182 scheme = match.group(1)
183 183
184 184 try:
185 185 storeproviders = _storeprovider[scheme]
186 186 except KeyError:
187 187 raise util.Abort(_('unsupported URL scheme %r') % scheme)
188 188
189 189 for classobj in storeproviders:
190 190 try:
191 191 return classobj(ui, repo, remote)
192 192 except lfutil.storeprotonotcapable:
193 193 pass
194 194
195 195 raise util.Abort(_('%s does not appear to be a largefile store') % path)
@@ -1,541 +1,545 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''High-level command function for lfconvert, plus the cmdtable.'''
10 10
11 11 import os
12 12 import shutil
13 13
14 14 from mercurial import util, match as match_, hg, node, context, error, \
15 15 cmdutil, scmutil
16 16 from mercurial.i18n import _
17 17 from mercurial.lock import release
18 18
19 19 import lfutil
20 20 import basestore
21 21
22 22 # -- Commands ----------------------------------------------------------
23 23
24 24 def lfconvert(ui, src, dest, *pats, **opts):
25 25 '''convert a normal repository to a largefiles repository
26 26
27 27 Convert repository SOURCE to a new repository DEST, identical to
28 28 SOURCE except that certain files will be converted as largefiles:
29 29 specifically, any file that matches any PATTERN *or* whose size is
30 30 above the minimum size threshold is converted as a largefile. The
31 31 size used to determine whether or not to track a file as a
32 32 largefile is the size of the first version of the file. The
33 33 minimum size can be specified either with --size or in
34 34 configuration as ``largefiles.size``.
35 35
36 36 After running this command you will need to make sure that
37 37 largefiles is enabled anywhere you intend to push the new
38 38 repository.
39 39
40 40 Use --to-normal to convert largefiles back to normal files; after
41 41 this, the DEST repository can be used without largefiles at all.'''
42 42
43 43 if opts['to_normal']:
44 44 tolfile = False
45 45 else:
46 46 tolfile = True
47 47 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
48 48
49 49 if not hg.islocal(src):
50 50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
51 51 if not hg.islocal(dest):
52 52 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
53 53
54 54 rsrc = hg.repository(ui, src)
55 55 ui.status(_('initializing destination %s\n') % dest)
56 56 rdst = hg.repository(ui, dest, create=True)
57 57
58 58 success = False
59 59 dstwlock = dstlock = None
60 60 try:
61 61 # Lock destination to prevent modification while it is converted to.
62 62 # Don't need to lock src because we are just reading from its history
63 63 # which can't change.
64 64 dstwlock = rdst.wlock()
65 65 dstlock = rdst.lock()
66 66
67 67 # Get a list of all changesets in the source. The easy way to do this
68 68 # is to simply walk the changelog, using changelog.nodesbewteen().
69 69 # Take a look at mercurial/revlog.py:639 for more details.
70 70 # Use a generator instead of a list to decrease memory usage
71 71 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
72 72 rsrc.heads())[0])
73 73 revmap = {node.nullid: node.nullid}
74 74 if tolfile:
75 75 lfiles = set()
76 76 normalfiles = set()
77 77 if not pats:
78 78 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
79 79 if pats:
80 80 matcher = match_.match(rsrc.root, '', list(pats))
81 81 else:
82 82 matcher = None
83 83
84 84 lfiletohash = {}
85 85 for ctx in ctxs:
86 86 ui.progress(_('converting revisions'), ctx.rev(),
87 87 unit=_('revision'), total=rsrc['tip'].rev())
88 88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
89 89 lfiles, normalfiles, matcher, size, lfiletohash)
90 90 ui.progress(_('converting revisions'), None)
91 91
92 92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
93 93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
94 94
95 95 for f in lfiletohash.keys():
96 96 if os.path.isfile(rdst.wjoin(f)):
97 97 os.unlink(rdst.wjoin(f))
98 98 try:
99 99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
100 100 except OSError:
101 101 pass
102 102
103 103 # If there were any files converted to largefiles, add largefiles
104 104 # to the destination repository's requirements.
105 105 if lfiles:
106 106 rdst.requirements.add('largefiles')
107 107 rdst._writerequirements()
108 108 else:
109 109 for ctx in ctxs:
110 110 ui.progress(_('converting revisions'), ctx.rev(),
111 111 unit=_('revision'), total=rsrc['tip'].rev())
112 112 _addchangeset(ui, rsrc, rdst, ctx, revmap)
113 113
114 114 ui.progress(_('converting revisions'), None)
115 115 success = True
116 116 finally:
117 117 rdst.dirstate.clear()
118 118 release(dstlock, dstwlock)
119 119 if not success:
120 120 # we failed, remove the new directory
121 121 shutil.rmtree(rdst.root)
122 122
123 123 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
124 124 # Convert src parents to dst parents
125 125 parents = _convertparents(ctx, revmap)
126 126
127 127 # Generate list of changed files
128 128 files = _getchangedfiles(ctx, parents)
129 129
130 130 def getfilectx(repo, memctx, f):
131 131 if lfutil.standin(f) in files:
132 132 # if the file isn't in the manifest then it was removed
133 133 # or renamed, raise IOError to indicate this
134 134 try:
135 135 fctx = ctx.filectx(lfutil.standin(f))
136 136 except error.LookupError:
137 137 raise IOError
138 138 renamed = fctx.renamed()
139 139 if renamed:
140 140 renamed = lfutil.splitstandin(renamed[0])
141 141
142 142 hash = fctx.data().strip()
143 143 path = lfutil.findfile(rsrc, hash)
144 144 ### TODO: What if the file is not cached?
145 145 data = ''
146 146 fd = None
147 147 try:
148 148 fd = open(path, 'rb')
149 149 data = fd.read()
150 150 finally:
151 151 if fd:
152 152 fd.close()
153 153 return context.memfilectx(f, data, 'l' in fctx.flags(),
154 154 'x' in fctx.flags(), renamed)
155 155 else:
156 156 return _getnormalcontext(repo.ui, ctx, f, revmap)
157 157
158 158 dstfiles = []
159 159 for file in files:
160 160 if lfutil.isstandin(file):
161 161 dstfiles.append(lfutil.splitstandin(file))
162 162 else:
163 163 dstfiles.append(file)
164 164 # Commit
165 165 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
166 166
167 167 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
168 168 matcher, size, lfiletohash):
169 169 # Convert src parents to dst parents
170 170 parents = _convertparents(ctx, revmap)
171 171
172 172 # Generate list of changed files
173 173 files = _getchangedfiles(ctx, parents)
174 174
175 175 dstfiles = []
176 176 for f in files:
177 177 if f not in lfiles and f not in normalfiles:
178 178 islfile = _islfile(f, ctx, matcher, size)
179 179 # If this file was renamed or copied then copy
180 180 # the lfileness of its predecessor
181 181 if f in ctx.manifest():
182 182 fctx = ctx.filectx(f)
183 183 renamed = fctx.renamed()
184 184 renamedlfile = renamed and renamed[0] in lfiles
185 185 islfile |= renamedlfile
186 186 if 'l' in fctx.flags():
187 187 if renamedlfile:
188 188 raise util.Abort(
189 189 _('renamed/copied largefile %s becomes symlink')
190 190 % f)
191 191 islfile = False
192 192 if islfile:
193 193 lfiles.add(f)
194 194 else:
195 195 normalfiles.add(f)
196 196
197 197 if f in lfiles:
198 198 dstfiles.append(lfutil.standin(f))
199 199 # largefile in manifest if it has not been removed/renamed
200 200 if f in ctx.manifest():
201 201 fctx = ctx.filectx(f)
202 202 if 'l' in fctx.flags():
203 203 renamed = fctx.renamed()
204 204 if renamed and renamed[0] in lfiles:
205 205 raise util.Abort(_('largefile %s becomes symlink') % f)
206 206
207 207 # largefile was modified, update standins
208 208 fullpath = rdst.wjoin(f)
209 209 util.makedirs(os.path.dirname(fullpath))
210 210 m = util.sha1('')
211 211 m.update(ctx[f].data())
212 212 hash = m.hexdigest()
213 213 if f not in lfiletohash or lfiletohash[f] != hash:
214 214 try:
215 215 fd = open(fullpath, 'wb')
216 216 fd.write(ctx[f].data())
217 217 finally:
218 218 if fd:
219 219 fd.close()
220 220 executable = 'x' in ctx[f].flags()
221 221 os.chmod(fullpath, lfutil.getmode(executable))
222 222 lfutil.writestandin(rdst, lfutil.standin(f), hash,
223 223 executable)
224 224 lfiletohash[f] = hash
225 225 else:
226 226 # normal file
227 227 dstfiles.append(f)
228 228
229 229 def getfilectx(repo, memctx, f):
230 230 if lfutil.isstandin(f):
231 231 # if the file isn't in the manifest then it was removed
232 232 # or renamed, raise IOError to indicate this
233 233 srcfname = lfutil.splitstandin(f)
234 234 try:
235 235 fctx = ctx.filectx(srcfname)
236 236 except error.LookupError:
237 237 raise IOError
238 238 renamed = fctx.renamed()
239 239 if renamed:
240 240 # standin is always a largefile because largefile-ness
241 241 # doesn't change after rename or copy
242 242 renamed = lfutil.standin(renamed[0])
243 243
244 244 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
245 245 fctx.flags(), 'x' in fctx.flags(), renamed)
246 246 else:
247 247 return _getnormalcontext(repo.ui, ctx, f, revmap)
248 248
249 249 # Commit
250 250 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
251 251
252 252 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
253 253 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
254 254 getfilectx, ctx.user(), ctx.date(), ctx.extra())
255 255 ret = rdst.commitctx(mctx)
256 256 rdst.setparents(ret)
257 257 revmap[ctx.node()] = rdst.changelog.tip()
258 258
259 259 # Generate list of changed files
260 260 def _getchangedfiles(ctx, parents):
261 261 files = set(ctx.files())
262 262 if node.nullid not in parents:
263 263 mc = ctx.manifest()
264 264 mp1 = ctx.parents()[0].manifest()
265 265 mp2 = ctx.parents()[1].manifest()
266 266 files |= (set(mp1) | set(mp2)) - set(mc)
267 267 for f in mc:
268 268 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
269 269 files.add(f)
270 270 return files
271 271
272 272 # Convert src parents to dst parents
273 273 def _convertparents(ctx, revmap):
274 274 parents = []
275 275 for p in ctx.parents():
276 276 parents.append(revmap[p.node()])
277 277 while len(parents) < 2:
278 278 parents.append(node.nullid)
279 279 return parents
280 280
281 281 # Get memfilectx for a normal file
282 282 def _getnormalcontext(ui, ctx, f, revmap):
283 283 try:
284 284 fctx = ctx.filectx(f)
285 285 except error.LookupError:
286 286 raise IOError
287 287 renamed = fctx.renamed()
288 288 if renamed:
289 289 renamed = renamed[0]
290 290
291 291 data = fctx.data()
292 292 if f == '.hgtags':
293 293 data = _converttags (ui, revmap, data)
294 294 return context.memfilectx(f, data, 'l' in fctx.flags(),
295 295 'x' in fctx.flags(), renamed)
296 296
297 297 # Remap tag data using a revision map
298 298 def _converttags(ui, revmap, data):
299 299 newdata = []
300 300 for line in data.splitlines():
301 301 try:
302 302 id, name = line.split(' ', 1)
303 303 except ValueError:
304 304 ui.warn(_('skipping incorrectly formatted tag %s\n'
305 305 % line))
306 306 continue
307 307 try:
308 308 newid = node.bin(id)
309 309 except TypeError:
310 310 ui.warn(_('skipping incorrectly formatted id %s\n'
311 311 % id))
312 312 continue
313 313 try:
314 314 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
315 315 name))
316 316 except KeyError:
317 317 ui.warn(_('no mapping for id %s\n') % id)
318 318 continue
319 319 return ''.join(newdata)
320 320
321 321 def _islfile(file, ctx, matcher, size):
322 322 '''Return true if file should be considered a largefile, i.e.
323 323 matcher matches it or it is larger than size.'''
324 324 # never store special .hg* files as largefiles
325 325 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
326 326 return False
327 327 if matcher and matcher(file):
328 328 return True
329 329 try:
330 330 return ctx.filectx(file).size() >= size * 1024 * 1024
331 331 except error.LookupError:
332 332 return False
333 333
334 334 def uploadlfiles(ui, rsrc, rdst, files):
335 335 '''upload largefiles to the central store'''
336 336
337 337 if not files:
338 338 return
339 339
340 340 store = basestore._openstore(rsrc, rdst, put=True)
341 341
342 342 at = 0
343 files = filter(lambda h: not store.exists(h), files)
343 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
344 retval = store.exists(files)
345 files = filter(lambda h: not retval[h], files)
346 ui.debug("%d largefiles need to be uploaded\n" % len(files))
347
344 348 for hash in files:
345 349 ui.progress(_('uploading largefiles'), at, unit='largefile',
346 350 total=len(files))
347 351 source = lfutil.findfile(rsrc, hash)
348 352 if not source:
349 353 raise util.Abort(_('largefile %s missing from store'
350 354 ' (needs to be uploaded)') % hash)
351 355 # XXX check for errors here
352 356 store.put(source, hash)
353 357 at += 1
354 358 ui.progress(_('uploading largefiles'), None)
355 359
356 360 def verifylfiles(ui, repo, all=False, contents=False):
357 361 '''Verify that every big file revision in the current changeset
358 362 exists in the central store. With --contents, also verify that
359 363 the contents of each big file revision are correct (SHA-1 hash
360 364 matches the revision ID). With --all, check every changeset in
361 365 this repository.'''
362 366 if all:
363 367 # Pass a list to the function rather than an iterator because we know a
364 368 # list will work.
365 369 revs = range(len(repo))
366 370 else:
367 371 revs = ['.']
368 372
369 373 store = basestore._openstore(repo)
370 374 return store.verify(revs, contents=contents)
371 375
372 376 def cachelfiles(ui, repo, node, filelist=None):
373 377 '''cachelfiles ensures that all largefiles needed by the specified revision
374 378 are present in the repository's largefile cache.
375 379
376 380 returns a tuple (cached, missing). cached is the list of files downloaded
377 381 by this operation; missing is the list of files that were needed but could
378 382 not be found.'''
379 383 lfiles = lfutil.listlfiles(repo, node)
380 384 if filelist:
381 385 lfiles = set(lfiles) & set(filelist)
382 386 toget = []
383 387
384 388 for lfile in lfiles:
385 389 # If we are mid-merge, then we have to trust the standin that is in the
386 390 # working copy to have the correct hashvalue. This is because the
387 391 # original hg.merge() already updated the standin as part of the normal
388 392 # merge process -- we just have to udpate the largefile to match.
389 393 if (getattr(repo, "_ismerging", False) and
390 394 os.path.exists(repo.wjoin(lfutil.standin(lfile)))):
391 395 expectedhash = lfutil.readstandin(repo, lfile)
392 396 else:
393 397 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
394 398
395 399 # if it exists and its hash matches, it might have been locally
396 400 # modified before updating and the user chose 'local'. in this case,
397 401 # it will not be in any store, so don't look for it.
398 402 if ((not os.path.exists(repo.wjoin(lfile)) or
399 403 expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and
400 404 not lfutil.findfile(repo, expectedhash)):
401 405 toget.append((lfile, expectedhash))
402 406
403 407 if toget:
404 408 store = basestore._openstore(repo)
405 409 ret = store.get(toget)
406 410 return ret
407 411
408 412 return ([], [])
409 413
410 414 def downloadlfiles(ui, repo, rev=None):
411 415 matchfn = scmutil.match(repo[None],
412 416 [repo.wjoin(lfutil.shortname)], {})
413 417 def prepare(ctx, fns):
414 418 pass
415 419 totalsuccess = 0
416 420 totalmissing = 0
417 421 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
418 422 prepare):
419 423 success, missing = cachelfiles(ui, repo, ctx.node())
420 424 totalsuccess += len(success)
421 425 totalmissing += len(missing)
422 426 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
423 427 if totalmissing > 0:
424 428 ui.status(_("%d largefiles failed to download\n") % totalmissing)
425 429 return totalsuccess, totalmissing
426 430
427 431 def updatelfiles(ui, repo, filelist=None, printmessage=True):
428 432 wlock = repo.wlock()
429 433 try:
430 434 lfdirstate = lfutil.openlfdirstate(ui, repo)
431 435 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
432 436
433 437 if filelist is not None:
434 438 lfiles = [f for f in lfiles if f in filelist]
435 439
436 440 printed = False
437 441 if printmessage and lfiles:
438 442 ui.status(_('getting changed largefiles\n'))
439 443 printed = True
440 444 cachelfiles(ui, repo, '.', lfiles)
441 445
442 446 updated, removed = 0, 0
443 447 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
444 448 # increment the appropriate counter according to _updatelfile's
445 449 # return value
446 450 updated += i > 0 and i or 0
447 451 removed -= i < 0 and i or 0
448 452 if printmessage and (removed or updated) and not printed:
449 453 ui.status(_('getting changed largefiles\n'))
450 454 printed = True
451 455
452 456 lfdirstate.write()
453 457 if printed and printmessage:
454 458 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
455 459 removed))
456 460 finally:
457 461 wlock.release()
458 462
459 463 def _updatelfile(repo, lfdirstate, lfile):
460 464 '''updates a single largefile and copies the state of its standin from
461 465 the repository's dirstate to its state in the lfdirstate.
462 466
463 467 returns 1 if the file was modified, -1 if the file was removed, 0 if the
464 468 file was unchanged, and None if the needed largefile was missing from the
465 469 cache.'''
466 470 ret = 0
467 471 abslfile = repo.wjoin(lfile)
468 472 absstandin = repo.wjoin(lfutil.standin(lfile))
469 473 if os.path.exists(absstandin):
470 474 if os.path.exists(absstandin+'.orig'):
471 475 shutil.copyfile(abslfile, abslfile+'.orig')
472 476 expecthash = lfutil.readstandin(repo, lfile)
473 477 if (expecthash != '' and
474 478 (not os.path.exists(abslfile) or
475 479 expecthash != lfutil.hashfile(abslfile))):
476 480 if not lfutil.copyfromcache(repo, expecthash, lfile):
477 481 # use normallookup() to allocate entry in largefiles dirstate,
478 482 # because lack of it misleads lfilesrepo.status() into
479 483 # recognition that such cache missing files are REMOVED.
480 484 lfdirstate.normallookup(lfile)
481 485 return None # don't try to set the mode
482 486 ret = 1
483 487 mode = os.stat(absstandin).st_mode
484 488 if mode != os.stat(abslfile).st_mode:
485 489 os.chmod(abslfile, mode)
486 490 ret = 1
487 491 else:
488 492 # Remove lfiles for which the standin is deleted, unless the
489 493 # lfile is added to the repository again. This happens when a
490 494 # largefile is converted back to a normal file: the standin
491 495 # disappears, but a new (normal) file appears as the lfile.
492 496 if os.path.exists(abslfile) and lfile not in repo[None]:
493 497 util.unlinkpath(abslfile)
494 498 ret = -1
495 499 state = repo.dirstate[lfutil.standin(lfile)]
496 500 if state == 'n':
497 501 # When rebasing, we need to synchronize the standin and the largefile,
498 502 # because otherwise the largefile will get reverted. But for commit's
499 503 # sake, we have to mark the file as unclean.
500 504 if getattr(repo, "_isrebasing", False):
501 505 lfdirstate.normallookup(lfile)
502 506 else:
503 507 lfdirstate.normal(lfile)
504 508 elif state == 'r':
505 509 lfdirstate.remove(lfile)
506 510 elif state == 'a':
507 511 lfdirstate.add(lfile)
508 512 elif state == '?':
509 513 lfdirstate.drop(lfile)
510 514 return ret
511 515
512 516 def catlfile(repo, lfile, rev, filename):
513 517 hash = lfutil.readstandin(repo, lfile, rev)
514 518 if not lfutil.inusercache(repo.ui, hash):
515 519 store = basestore._openstore(repo)
516 520 success, missing = store.get([(lfile, hash)])
517 521 if len(success) != 1:
518 522 raise util.Abort(
519 523 _('largefile %s is not in cache and could not be downloaded')
520 524 % lfile)
521 525 path = lfutil.usercachepath(repo.ui, hash)
522 526 fpout = cmdutil.makefileobj(repo, filename)
523 527 fpin = open(path, "rb")
524 528 fpout.write(fpin.read())
525 529 fpout.close()
526 530 fpin.close()
527 531 return 0
528 532
529 533 # -- hg commands declarations ------------------------------------------------
530 534
531 535 cmdtable = {
532 536 'lfconvert': (lfconvert,
533 537 [('s', 'size', '',
534 538 _('minimum size (MB) for files to be converted '
535 539 'as largefiles'),
536 540 'SIZE'),
537 541 ('', 'to-normal', False,
538 542 _('convert from a largefiles repo to a normal repo')),
539 543 ],
540 544 _('hg lfconvert SOURCE DEST [FILE ...]')),
541 545 }
@@ -1,168 +1,173 b''
1 1 # Copyright 2011 Fog Creek Software
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 import os
7 7 import urllib2
8 8
9 9 from mercurial import error, httprepo, util, wireproto
10 from mercurial.wireproto import batchable, future
10 11 from mercurial.i18n import _
11 12
12 13 import lfutil
13 14
14 15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
15 16 '\n\nPlease enable it in your Mercurial config '
16 17 'file.\n')
17 18
18 19 def putlfile(repo, proto, sha):
19 20 '''Put a largefile into a repository's local store and into the
20 21 user cache.'''
21 22 proto.redirect()
22 23
23 24 path = lfutil.storepath(repo, sha)
24 25 util.makedirs(os.path.dirname(path))
25 26 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
26 27
27 28 try:
28 29 try:
29 30 proto.getfile(tmpfp)
30 31 tmpfp._fp.seek(0)
31 32 if sha != lfutil.hexsha1(tmpfp._fp):
32 33 raise IOError(0, _('largefile contents do not match hash'))
33 34 tmpfp.close()
34 35 lfutil.linktousercache(repo, sha)
35 36 except IOError, e:
36 37 repo.ui.warn(_('largefiles: failed to put %s into store: %s') %
37 38 (sha, e.strerror))
38 39 return wireproto.pushres(1)
39 40 finally:
40 41 tmpfp.discard()
41 42
42 43 return wireproto.pushres(0)
43 44
44 45 def getlfile(repo, proto, sha):
45 46 '''Retrieve a largefile from the repository-local cache or system
46 47 cache.'''
47 48 filename = lfutil.findfile(repo, sha)
48 49 if not filename:
49 50 raise util.Abort(_('requested largefile %s not present in cache') % sha)
50 51 f = open(filename, 'rb')
51 52 length = os.fstat(f.fileno())[6]
52 53
53 54 # Since we can't set an HTTP content-length header here, and
54 55 # Mercurial core provides no way to give the length of a streamres
55 56 # (and reading the entire file into RAM would be ill-advised), we
56 57 # just send the length on the first line of the response, like the
57 58 # ssh proto does for string responses.
58 59 def generator():
59 60 yield '%d\n' % length
60 61 for chunk in f:
61 62 yield chunk
62 63 return wireproto.streamres(generator())
63 64
64 65 def statlfile(repo, proto, sha):
65 66 '''Return '2\n' if the largefile is missing, '1\n' if it has a
66 67 mismatched checksum, or '0\n' if it is in good condition'''
67 68 filename = lfutil.findfile(repo, sha)
68 69 if not filename:
69 70 return '2\n'
70 71 fd = None
71 72 try:
72 73 fd = open(filename, 'rb')
73 74 return lfutil.hexsha1(fd) == sha and '0\n' or '1\n'
74 75 finally:
75 76 if fd:
76 77 fd.close()
77 78
78 79 def wirereposetup(ui, repo):
79 80 class lfileswirerepository(repo.__class__):
80 81 def putlfile(self, sha, fd):
81 82 # unfortunately, httprepository._callpush tries to convert its
82 83 # input file-like into a bundle before sending it, so we can't use
83 84 # it ...
84 85 if issubclass(self.__class__, httprepo.httprepository):
85 86 res = None
86 87 try:
87 88 res = self._call('putlfile', data=fd, sha=sha,
88 89 headers={'content-type':'application/mercurial-0.1'})
89 90 d, output = res.split('\n', 1)
90 91 for l in output.splitlines(True):
91 92 self.ui.warn(_('remote: '), l, '\n')
92 93 return int(d)
93 94 except (ValueError, urllib2.HTTPError):
94 95 self.ui.warn(_('unexpected putlfile response: %s') % res)
95 96 return 1
96 97 # ... but we can't use sshrepository._call because the data=
97 98 # argument won't get sent, and _callpush does exactly what we want
98 99 # in this case: send the data straight through
99 100 else:
100 101 try:
101 102 ret, output = self._callpush("putlfile", fd, sha=sha)
102 103 if ret == "":
103 104 raise error.ResponseError(_('putlfile failed:'),
104 105 output)
105 106 return int(ret)
106 107 except IOError:
107 108 return 1
108 109 except ValueError:
109 110 raise error.ResponseError(
110 111 _('putlfile failed (unexpected response):'), ret)
111 112
112 113 def getlfile(self, sha):
113 114 stream = self._callstream("getlfile", sha=sha)
114 115 length = stream.readline()
115 116 try:
116 117 length = int(length)
117 118 except ValueError:
118 119 self._abort(error.ResponseError(_("unexpected response:"),
119 120 length))
120 121 return (length, stream)
121 122
123 @batchable
122 124 def statlfile(self, sha):
125 f = future()
126 result = {'sha': sha}
127 yield result, f
123 128 try:
124 return int(self._call("statlfile", sha=sha))
129 yield int(f.value)
125 130 except (ValueError, urllib2.HTTPError):
126 131 # If the server returns anything but an integer followed by a
127 132 # newline, newline, it's not speaking our language; if we get
128 133 # an HTTP error, we can't be sure the largefile is present;
129 134 # either way, consider it missing.
130 return 2
135 yield 2
131 136
132 137 repo.__class__ = lfileswirerepository
133 138
134 139 # advertise the largefiles=serve capability
135 140 def capabilities(repo, proto):
136 141 return capabilitiesorig(repo, proto) + ' largefiles=serve'
137 142
138 143 # duplicate what Mercurial's new out-of-band errors mechanism does, because
139 144 # clients old and new alike both handle it well
140 145 def webprotorefuseclient(self, message):
141 146 self.req.header([('Content-Type', 'application/hg-error')])
142 147 return message
143 148
144 149 def sshprotorefuseclient(self, message):
145 150 self.ui.write_err('%s\n-\n' % message)
146 151 self.fout.write('\n')
147 152 self.fout.flush()
148 153
149 154 return ''
150 155
151 156 def heads(repo, proto):
152 157 if lfutil.islfilesrepo(repo):
153 158 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
154 159 return wireproto.heads(repo, proto)
155 160
156 161 def sshrepocallstream(self, cmd, **args):
157 162 if cmd == 'heads' and self.capable('largefiles'):
158 163 cmd = 'lheads'
159 164 if cmd == 'batch' and self.capable('largefiles'):
160 165 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
161 166 return ssholdcallstream(self, cmd, **args)
162 167
163 168 def httprepocallstream(self, cmd, **args):
164 169 if cmd == 'heads' and self.capable('largefiles'):
165 170 cmd = 'lheads'
166 171 if cmd == 'batch' and self.capable('largefiles'):
167 172 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
168 173 return httpoldcallstream(self, cmd, **args)
@@ -1,106 +1,110 b''
1 1 # Copyright 2010-2011 Fog Creek Software
2 2 # Copyright 2010-2011 Unity Technologies
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 '''remote largefile store; the base class for servestore'''
8 8
9 9 import urllib2
10 10
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 from mercurial.wireproto import remotebatch
13 14
14 15 import lfutil
15 16 import basestore
16 17
17 18 class remotestore(basestore.basestore):
18 19 '''a largefile store accessed over a network'''
19 20 def __init__(self, ui, repo, url):
20 21 super(remotestore, self).__init__(ui, repo, url)
21 22
22 23 def put(self, source, hash):
23 if self._verify(hash):
24 return
25 24 if self.sendfile(source, hash):
26 25 raise util.Abort(
27 26 _('remotestore: could not put %s to remote store %s')
28 27 % (source, self.url))
29 28 self.ui.debug(
30 29 _('remotestore: put %s to remote store %s') % (source, self.url))
31 30
32 def exists(self, hash):
33 return self._verify(hash)
31 def exists(self, hashes):
32 return self._verify(hashes)
34 33
35 34 def sendfile(self, filename, hash):
36 35 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 36 fd = None
38 37 try:
39 38 try:
40 39 fd = lfutil.httpsendfile(self.ui, filename)
41 40 except IOError, e:
42 41 raise util.Abort(
43 42 _('remotestore: could not open file %s: %s')
44 43 % (filename, str(e)))
45 44 return self._put(hash, fd)
46 45 finally:
47 46 if fd:
48 47 fd.close()
49 48
50 49 def _getfile(self, tmpfile, filename, hash):
51 50 # quit if the largefile isn't there
52 51 stat = self._stat(hash)
53 52 if stat == 1:
54 53 raise util.Abort(_('remotestore: largefile %s is invalid') % hash)
55 54 elif stat == 2:
56 55 raise util.Abort(_('remotestore: largefile %s is missing') % hash)
57 56
58 57 try:
59 58 length, infile = self._get(hash)
60 59 except urllib2.HTTPError, e:
61 60 # 401s get converted to util.Aborts; everything else is fine being
62 61 # turned into a StoreError
63 62 raise basestore.StoreError(filename, hash, self.url, str(e))
64 63 except urllib2.URLError, e:
65 64 # This usually indicates a connection problem, so don't
66 65 # keep trying with the other files... they will probably
67 66 # all fail too.
68 67 raise util.Abort('%s: %s' % (self.url, e.reason))
69 68 except IOError, e:
70 69 raise basestore.StoreError(filename, hash, self.url, str(e))
71 70
72 71 # Mercurial does not close its SSH connections after writing a stream
73 72 if length is not None:
74 73 infile = lfutil.limitreader(infile, length)
75 74 return lfutil.copyandhash(lfutil.blockstream(infile), tmpfile)
76 75
77 def _verify(self, hash):
78 return not self._stat(hash)
76 def _verify(self, hashes):
77 return self._stat(hashes)
79 78
80 79 def _verifyfile(self, cctx, cset, contents, standin, verified):
81 80 filename = lfutil.splitstandin(standin)
82 81 if not filename:
83 82 return False
84 83 fctx = cctx[standin]
85 84 key = (filename, fctx.filenode())
86 85 if key in verified:
87 86 return False
88 87
89 88 verified.add(key)
90 89
91 90 stat = self._stat(hash)
92 91 if not stat:
93 92 return False
94 93 elif stat == 1:
95 94 self.ui.warn(
96 95 _('changeset %s: %s: contents differ\n')
97 96 % (cset, filename))
98 97 return True # failed
99 98 elif stat == 2:
100 99 self.ui.warn(
101 100 _('changeset %s: %s missing\n')
102 101 % (cset, filename))
103 102 return True # failed
104 103 else:
105 104 raise RuntimeError('verify failed: unexpected response from '
106 105 'statlfile (%r)' % stat)
106
107 def batch(self):
108 '''Support for remote batching.'''
109 return remotebatch(self)
110
@@ -1,29 +1,37 b''
1 1 # Copyright 2010-2011 Fog Creek Software
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''largefile store working over Mercurial's wire protocol'''
7 7
8 8 import lfutil
9 9 import remotestore
10 10
11 11 class wirestore(remotestore.remotestore):
12 12 def __init__(self, ui, repo, remote):
13 13 cap = remote.capable('largefiles')
14 14 if not cap:
15 15 raise lfutil.storeprotonotcapable([])
16 16 storetypes = cap.split(',')
17 17 if 'serve' not in storetypes:
18 18 raise lfutil.storeprotonotcapable(storetypes)
19 19 self.remote = remote
20 20 super(wirestore, self).__init__(ui, repo, remote.url())
21 21
22 22 def _put(self, hash, fd):
23 23 return self.remote.putlfile(hash, fd)
24 24
25 25 def _get(self, hash):
26 26 return self.remote.getlfile(hash)
27 27
28 def _stat(self, hash):
29 return self.remote.statlfile(hash)
28 def _stat(self, hashes):
29 batch = self.remote.batch()
30 futures = {}
31 for hash in hashes:
32 futures[hash] = batch.statlfile(hash)
33 batch.submit()
34 retval = {}
35 for hash in hashes:
36 retval[hash] = not futures[hash].value
37 return retval
General Comments 0
You need to be logged in to leave comments. Login now