##// END OF EJS Templates
largefiles: move basestore._openstore into new module to remove cycle
liscju -
r29305:814076f4 default
parent child Browse files
Show More
@@ -1,226 +1,164
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 import re
12
13 from mercurial import util, node, hg, error
11 from mercurial import util, node
14 12 from mercurial.i18n import _
15 13
16 14 import lfutil
17 15
18 16 class StoreError(Exception):
19 17 '''Raised when there is a problem getting files from or putting
20 18 files to a central store.'''
21 19 def __init__(self, filename, hash, url, detail):
22 20 self.filename = filename
23 21 self.hash = hash
24 22 self.url = url
25 23 self.detail = detail
26 24
27 25 def longmessage(self):
28 26 return (_("error getting id %s from url %s for file %s: %s\n") %
29 27 (self.hash, util.hidepassword(self.url), self.filename,
30 28 self.detail))
31 29
32 30 def __str__(self):
33 31 return "%s: %s" % (util.hidepassword(self.url), self.detail)
34 32
35 33 class basestore(object):
36 34 def __init__(self, ui, repo, url):
37 35 self.ui = ui
38 36 self.repo = repo
39 37 self.url = url
40 38
41 39 def put(self, source, hash):
42 40 '''Put source file into the store so it can be retrieved by hash.'''
43 41 raise NotImplementedError('abstract method')
44 42
45 43 def exists(self, hashes):
46 44 '''Check to see if the store contains the given hashes. Given an
47 45 iterable of hashes it returns a mapping from hash to bool.'''
48 46 raise NotImplementedError('abstract method')
49 47
50 48 def get(self, files):
51 49 '''Get the specified largefiles from the store and write to local
52 50 files under repo.root. files is a list of (filename, hash)
53 51 tuples. Return (success, missing), lists of files successfully
54 52 downloaded and those not found in the store. success is a list
55 53 of (filename, hash) tuples; missing is a list of filenames that
56 54 we could not get. (The detailed error message will already have
57 55 been presented to the user, so missing is just supplied as a
58 56 summary.)'''
59 57 success = []
60 58 missing = []
61 59 ui = self.ui
62 60
63 61 at = 0
64 62 available = self.exists(set(hash for (_filename, hash) in files))
65 63 for filename, hash in files:
66 64 ui.progress(_('getting largefiles'), at, unit=_('files'),
67 65 total=len(files))
68 66 at += 1
69 67 ui.note(_('getting %s:%s\n') % (filename, hash))
70 68
71 69 if not available.get(hash):
72 70 ui.warn(_('%s: largefile %s not available from %s\n')
73 71 % (filename, hash, util.hidepassword(self.url)))
74 72 missing.append(filename)
75 73 continue
76 74
77 75 if self._gethash(filename, hash):
78 76 success.append((filename, hash))
79 77 else:
80 78 missing.append(filename)
81 79
82 80 ui.progress(_('getting largefiles'), None)
83 81 return (success, missing)
84 82
85 83 def _gethash(self, filename, hash):
86 84 """Get file with the provided hash and store it in the local repo's
87 85 store and in the usercache.
88 86 filename is for informational messages only.
89 87 """
90 88 util.makedirs(lfutil.storepath(self.repo, ''))
91 89 storefilename = lfutil.storepath(self.repo, hash)
92 90
93 91 tmpname = storefilename + '.tmp'
94 92 tmpfile = util.atomictempfile(tmpname,
95 93 createmode=self.repo.store.createmode)
96 94
97 95 try:
98 96 gothash = self._getfile(tmpfile, filename, hash)
99 97 except StoreError as err:
100 98 self.ui.warn(err.longmessage())
101 99 gothash = ""
102 100 tmpfile.close()
103 101
104 102 if gothash != hash:
105 103 if gothash != "":
106 104 self.ui.warn(_('%s: data corruption (expected %s, got %s)\n')
107 105 % (filename, hash, gothash))
108 106 util.unlink(tmpname)
109 107 return False
110 108
111 109 util.rename(tmpname, storefilename)
112 110 lfutil.linktousercache(self.repo, hash)
113 111 return True
114 112
115 113 def verify(self, revs, contents=False):
116 114 '''Verify the existence (and, optionally, contents) of every big
117 115 file revision referenced by every changeset in revs.
118 116 Return 0 if all is well, non-zero on any errors.'''
119 117
120 118 self.ui.status(_('searching %d changesets for largefiles\n') %
121 119 len(revs))
122 120 verified = set() # set of (filename, filenode) tuples
123 121 filestocheck = [] # list of (cset, filename, expectedhash)
124 122 for rev in revs:
125 123 cctx = self.repo[rev]
126 124 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
127 125
128 126 for standin in cctx:
129 127 filename = lfutil.splitstandin(standin)
130 128 if filename:
131 129 fctx = cctx[standin]
132 130 key = (filename, fctx.filenode())
133 131 if key not in verified:
134 132 verified.add(key)
135 133 expectedhash = fctx.data()[0:40]
136 134 filestocheck.append((cset, filename, expectedhash))
137 135
138 136 failed = self._verifyfiles(contents, filestocheck)
139 137
140 138 numrevs = len(verified)
141 139 numlfiles = len(set([fname for (fname, fnode) in verified]))
142 140 if contents:
143 141 self.ui.status(
144 142 _('verified contents of %d revisions of %d largefiles\n')
145 143 % (numrevs, numlfiles))
146 144 else:
147 145 self.ui.status(
148 146 _('verified existence of %d revisions of %d largefiles\n')
149 147 % (numrevs, numlfiles))
150 148 return int(failed)
151 149
152 150 def _getfile(self, tmpfile, filename, hash):
153 151 '''Fetch one revision of one file from the store and write it
154 152 to tmpfile. Compute the hash of the file on-the-fly as it
155 153 downloads and return the hash. Close tmpfile. Raise
156 154 StoreError if unable to download the file (e.g. it does not
157 155 exist in the store).'''
158 156 raise NotImplementedError('abstract method')
159 157
160 158 def _verifyfiles(self, contents, filestocheck):
161 159 '''Perform the actual verification of files in the store.
162 160 'contents' controls verification of content hash.
163 161 'filestocheck' is list of files to check.
164 162 Returns _true_ if any problems are found!
165 163 '''
166 164 raise NotImplementedError('abstract method')
167
168 import localstore, wirestore
169
170 _storeprovider = {
171 'file': [localstore.localstore],
172 'http': [wirestore.wirestore],
173 'https': [wirestore.wirestore],
174 'ssh': [wirestore.wirestore],
175 }
176
177 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
178
179 # During clone this function is passed the src's ui object
180 # but it needs the dest's ui object so it can read out of
181 # the config file. Use repo.ui instead.
182 def _openstore(repo, remote=None, put=False):
183 ui = repo.ui
184
185 if not remote:
186 lfpullsource = getattr(repo, 'lfpullsource', None)
187 if lfpullsource:
188 path = ui.expandpath(lfpullsource)
189 elif put:
190 path = ui.expandpath('default-push', 'default')
191 else:
192 path = ui.expandpath('default')
193
194 # ui.expandpath() leaves 'default-push' and 'default' alone if
195 # they cannot be expanded: fallback to the empty string,
196 # meaning the current directory.
197 if path == 'default-push' or path == 'default':
198 path = ''
199 remote = repo
200 else:
201 path, _branches = hg.parseurl(path)
202 remote = hg.peer(repo, {}, path)
203
204 # The path could be a scheme so use Mercurial's normal functionality
205 # to resolve the scheme to a repository and use its path
206 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
207
208 match = _scheme_re.match(path)
209 if not match: # regular filesystem path
210 scheme = 'file'
211 else:
212 scheme = match.group(1)
213
214 try:
215 storeproviders = _storeprovider[scheme]
216 except KeyError:
217 raise error.Abort(_('unsupported URL scheme %r') % scheme)
218
219 for classobj in storeproviders:
220 try:
221 return classobj(ui, repo, remote)
222 except lfutil.storeprotonotcapable:
223 pass
224
225 raise error.Abort(_('%s does not appear to be a largefile store') %
226 util.hidepassword(path))
@@ -1,550 +1,550
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, errno
12 12 import shutil
13 13
14 14 from mercurial import util, match as match_, hg, node, context, error, \
15 15 cmdutil, scmutil, commands
16 16 from mercurial.i18n import _
17 17 from mercurial.lock import release
18 18
19 19 from hgext.convert import convcmd
20 20 from hgext.convert import filemap
21 21
22 22 import lfutil
23 import basestore
23 import storefactory
24 24
25 25 # -- Commands ----------------------------------------------------------
26 26
27 27 cmdtable = {}
28 28 command = cmdutil.command(cmdtable)
29 29
30 30 @command('lfconvert',
31 31 [('s', 'size', '',
32 32 _('minimum size (MB) for files to be converted as largefiles'), 'SIZE'),
33 33 ('', 'to-normal', False,
34 34 _('convert from a largefiles repo to a normal repo')),
35 35 ],
36 36 _('hg lfconvert SOURCE DEST [FILE ...]'),
37 37 norepo=True,
38 38 inferrepo=True)
39 39 def lfconvert(ui, src, dest, *pats, **opts):
40 40 '''convert a normal repository to a largefiles repository
41 41
42 42 Convert repository SOURCE to a new repository DEST, identical to
43 43 SOURCE except that certain files will be converted as largefiles:
44 44 specifically, any file that matches any PATTERN *or* whose size is
45 45 above the minimum size threshold is converted as a largefile. The
46 46 size used to determine whether or not to track a file as a
47 47 largefile is the size of the first version of the file. The
48 48 minimum size can be specified either with --size or in
49 49 configuration as ``largefiles.size``.
50 50
51 51 After running this command you will need to make sure that
52 52 largefiles is enabled anywhere you intend to push the new
53 53 repository.
54 54
55 55 Use --to-normal to convert largefiles back to normal files; after
56 56 this, the DEST repository can be used without largefiles at all.'''
57 57
58 58 if opts['to_normal']:
59 59 tolfile = False
60 60 else:
61 61 tolfile = True
62 62 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
63 63
64 64 if not hg.islocal(src):
65 65 raise error.Abort(_('%s is not a local Mercurial repo') % src)
66 66 if not hg.islocal(dest):
67 67 raise error.Abort(_('%s is not a local Mercurial repo') % dest)
68 68
69 69 rsrc = hg.repository(ui, src)
70 70 ui.status(_('initializing destination %s\n') % dest)
71 71 rdst = hg.repository(ui, dest, create=True)
72 72
73 73 success = False
74 74 dstwlock = dstlock = None
75 75 try:
76 76 # Get a list of all changesets in the source. The easy way to do this
77 77 # is to simply walk the changelog, using changelog.nodesbetween().
78 78 # Take a look at mercurial/revlog.py:639 for more details.
79 79 # Use a generator instead of a list to decrease memory usage
80 80 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
81 81 rsrc.heads())[0])
82 82 revmap = {node.nullid: node.nullid}
83 83 if tolfile:
84 84 # Lock destination to prevent modification while it is converted to.
85 85 # Don't need to lock src because we are just reading from its
86 86 # history which can't change.
87 87 dstwlock = rdst.wlock()
88 88 dstlock = rdst.lock()
89 89
90 90 lfiles = set()
91 91 normalfiles = set()
92 92 if not pats:
93 93 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
94 94 if pats:
95 95 matcher = match_.match(rsrc.root, '', list(pats))
96 96 else:
97 97 matcher = None
98 98
99 99 lfiletohash = {}
100 100 for ctx in ctxs:
101 101 ui.progress(_('converting revisions'), ctx.rev(),
102 102 unit=_('revisions'), total=rsrc['tip'].rev())
103 103 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
104 104 lfiles, normalfiles, matcher, size, lfiletohash)
105 105 ui.progress(_('converting revisions'), None)
106 106
107 107 if rdst.wvfs.exists(lfutil.shortname):
108 108 rdst.wvfs.rmtree(lfutil.shortname)
109 109
110 110 for f in lfiletohash.keys():
111 111 if rdst.wvfs.isfile(f):
112 112 rdst.wvfs.unlink(f)
113 113 try:
114 114 rdst.wvfs.removedirs(rdst.wvfs.dirname(f))
115 115 except OSError:
116 116 pass
117 117
118 118 # If there were any files converted to largefiles, add largefiles
119 119 # to the destination repository's requirements.
120 120 if lfiles:
121 121 rdst.requirements.add('largefiles')
122 122 rdst._writerequirements()
123 123 else:
124 124 class lfsource(filemap.filemap_source):
125 125 def __init__(self, ui, source):
126 126 super(lfsource, self).__init__(ui, source, None)
127 127 self.filemapper.rename[lfutil.shortname] = '.'
128 128
129 129 def getfile(self, name, rev):
130 130 realname, realrev = rev
131 131 f = super(lfsource, self).getfile(name, rev)
132 132
133 133 if (not realname.startswith(lfutil.shortnameslash)
134 134 or f[0] is None):
135 135 return f
136 136
137 137 # Substitute in the largefile data for the hash
138 138 hash = f[0].strip()
139 139 path = lfutil.findfile(rsrc, hash)
140 140
141 141 if path is None:
142 142 raise error.Abort(_("missing largefile for '%s' in %s")
143 143 % (realname, realrev))
144 144 return util.readfile(path), f[1]
145 145
146 146 class converter(convcmd.converter):
147 147 def __init__(self, ui, source, dest, revmapfile, opts):
148 148 src = lfsource(ui, source)
149 149
150 150 super(converter, self).__init__(ui, src, dest, revmapfile,
151 151 opts)
152 152
153 153 found, missing = downloadlfiles(ui, rsrc)
154 154 if missing != 0:
155 155 raise error.Abort(_("all largefiles must be present locally"))
156 156
157 157 orig = convcmd.converter
158 158 convcmd.converter = converter
159 159
160 160 try:
161 161 convcmd.convert(ui, src, dest)
162 162 finally:
163 163 convcmd.converter = orig
164 164 success = True
165 165 finally:
166 166 if tolfile:
167 167 rdst.dirstate.clear()
168 168 release(dstlock, dstwlock)
169 169 if not success:
170 170 # we failed, remove the new directory
171 171 shutil.rmtree(rdst.root)
172 172
173 173 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
174 174 matcher, size, lfiletohash):
175 175 # Convert src parents to dst parents
176 176 parents = _convertparents(ctx, revmap)
177 177
178 178 # Generate list of changed files
179 179 files = _getchangedfiles(ctx, parents)
180 180
181 181 dstfiles = []
182 182 for f in files:
183 183 if f not in lfiles and f not in normalfiles:
184 184 islfile = _islfile(f, ctx, matcher, size)
185 185 # If this file was renamed or copied then copy
186 186 # the largefile-ness of its predecessor
187 187 if f in ctx.manifest():
188 188 fctx = ctx.filectx(f)
189 189 renamed = fctx.renamed()
190 190 renamedlfile = renamed and renamed[0] in lfiles
191 191 islfile |= renamedlfile
192 192 if 'l' in fctx.flags():
193 193 if renamedlfile:
194 194 raise error.Abort(
195 195 _('renamed/copied largefile %s becomes symlink')
196 196 % f)
197 197 islfile = False
198 198 if islfile:
199 199 lfiles.add(f)
200 200 else:
201 201 normalfiles.add(f)
202 202
203 203 if f in lfiles:
204 204 dstfiles.append(lfutil.standin(f))
205 205 # largefile in manifest if it has not been removed/renamed
206 206 if f in ctx.manifest():
207 207 fctx = ctx.filectx(f)
208 208 if 'l' in fctx.flags():
209 209 renamed = fctx.renamed()
210 210 if renamed and renamed[0] in lfiles:
211 211 raise error.Abort(_('largefile %s becomes symlink') % f)
212 212
213 213 # largefile was modified, update standins
214 214 m = util.sha1('')
215 215 m.update(ctx[f].data())
216 216 hash = m.hexdigest()
217 217 if f not in lfiletohash or lfiletohash[f] != hash:
218 218 rdst.wwrite(f, ctx[f].data(), ctx[f].flags())
219 219 executable = 'x' in ctx[f].flags()
220 220 lfutil.writestandin(rdst, lfutil.standin(f), hash,
221 221 executable)
222 222 lfiletohash[f] = hash
223 223 else:
224 224 # normal file
225 225 dstfiles.append(f)
226 226
227 227 def getfilectx(repo, memctx, f):
228 228 if lfutil.isstandin(f):
229 229 # if the file isn't in the manifest then it was removed
230 230 # or renamed, raise IOError to indicate this
231 231 srcfname = lfutil.splitstandin(f)
232 232 try:
233 233 fctx = ctx.filectx(srcfname)
234 234 except error.LookupError:
235 235 return None
236 236 renamed = fctx.renamed()
237 237 if renamed:
238 238 # standin is always a largefile because largefile-ness
239 239 # doesn't change after rename or copy
240 240 renamed = lfutil.standin(renamed[0])
241 241
242 242 return context.memfilectx(repo, f, lfiletohash[srcfname] + '\n',
243 243 'l' in fctx.flags(), 'x' in fctx.flags(),
244 244 renamed)
245 245 else:
246 246 return _getnormalcontext(repo, ctx, f, revmap)
247 247
248 248 # Commit
249 249 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
250 250
251 251 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
252 252 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
253 253 getfilectx, ctx.user(), ctx.date(), ctx.extra())
254 254 ret = rdst.commitctx(mctx)
255 255 lfutil.copyalltostore(rdst, ret)
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(repo, ctx, f, revmap):
283 283 try:
284 284 fctx = ctx.filectx(f)
285 285 except error.LookupError:
286 286 return None
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 (repo.ui, revmap, data)
294 294 return context.memfilectx(repo, 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 store = basestore._openstore(rsrc, rdst, put=True)
340 store = storefactory._openstore(rsrc, rdst, put=True)
341 341
342 342 at = 0
343 343 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
344 344 retval = store.exists(files)
345 345 files = filter(lambda h: not retval[h], files)
346 346 ui.debug("%d largefiles need to be uploaded\n" % len(files))
347 347
348 348 for hash in files:
349 349 ui.progress(_('uploading largefiles'), at, unit=_('files'),
350 350 total=len(files))
351 351 source = lfutil.findfile(rsrc, hash)
352 352 if not source:
353 353 raise error.Abort(_('largefile %s missing from store'
354 354 ' (needs to be uploaded)') % hash)
355 355 # XXX check for errors here
356 356 store.put(source, hash)
357 357 at += 1
358 358 ui.progress(_('uploading largefiles'), None)
359 359
360 360 def verifylfiles(ui, repo, all=False, contents=False):
361 361 '''Verify that every largefile revision in the current changeset
362 362 exists in the central store. With --contents, also verify that
363 363 the contents of each local largefile file revision are correct (SHA-1 hash
364 364 matches the revision ID). With --all, check every changeset in
365 365 this repository.'''
366 366 if all:
367 367 revs = repo.revs('all()')
368 368 else:
369 369 revs = ['.']
370 370
371 store = basestore._openstore(repo)
371 store = storefactory._openstore(repo)
372 372 return store.verify(revs, contents=contents)
373 373
374 374 def cachelfiles(ui, repo, node, filelist=None):
375 375 '''cachelfiles ensures that all largefiles needed by the specified revision
376 376 are present in the repository's largefile cache.
377 377
378 378 returns a tuple (cached, missing). cached is the list of files downloaded
379 379 by this operation; missing is the list of files that were needed but could
380 380 not be found.'''
381 381 lfiles = lfutil.listlfiles(repo, node)
382 382 if filelist:
383 383 lfiles = set(lfiles) & set(filelist)
384 384 toget = []
385 385
386 386 for lfile in lfiles:
387 387 try:
388 388 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
389 389 except IOError as err:
390 390 if err.errno == errno.ENOENT:
391 391 continue # node must be None and standin wasn't found in wctx
392 392 raise
393 393 if not lfutil.findfile(repo, expectedhash):
394 394 toget.append((lfile, expectedhash))
395 395
396 396 if toget:
397 store = basestore._openstore(repo)
397 store = storefactory._openstore(repo)
398 398 ret = store.get(toget)
399 399 return ret
400 400
401 401 return ([], [])
402 402
403 403 def downloadlfiles(ui, repo, rev=None):
404 404 matchfn = scmutil.match(repo[None],
405 405 [repo.wjoin(lfutil.shortname)], {})
406 406 def prepare(ctx, fns):
407 407 pass
408 408 totalsuccess = 0
409 409 totalmissing = 0
410 410 if rev != []: # walkchangerevs on empty list would return all revs
411 411 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
412 412 prepare):
413 413 success, missing = cachelfiles(ui, repo, ctx.node())
414 414 totalsuccess += len(success)
415 415 totalmissing += len(missing)
416 416 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
417 417 if totalmissing > 0:
418 418 ui.status(_("%d largefiles failed to download\n") % totalmissing)
419 419 return totalsuccess, totalmissing
420 420
421 421 def updatelfiles(ui, repo, filelist=None, printmessage=None,
422 422 normallookup=False):
423 423 '''Update largefiles according to standins in the working directory
424 424
425 425 If ``printmessage`` is other than ``None``, it means "print (or
426 426 ignore, for false) message forcibly".
427 427 '''
428 428 statuswriter = lfutil.getstatuswriter(ui, repo, printmessage)
429 429 with repo.wlock():
430 430 lfdirstate = lfutil.openlfdirstate(ui, repo)
431 431 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
432 432
433 433 if filelist is not None:
434 434 filelist = set(filelist)
435 435 lfiles = [f for f in lfiles if f in filelist]
436 436
437 437 update = {}
438 438 updated, removed = 0, 0
439 439 wvfs = repo.wvfs
440 440 for lfile in lfiles:
441 441 rellfile = lfile
442 442 rellfileorig = os.path.relpath(
443 443 scmutil.origpath(ui, repo, wvfs.join(rellfile)),
444 444 start=repo.root)
445 445 relstandin = lfutil.standin(lfile)
446 446 relstandinorig = os.path.relpath(
447 447 scmutil.origpath(ui, repo, wvfs.join(relstandin)),
448 448 start=repo.root)
449 449 if wvfs.exists(relstandin):
450 450 if (wvfs.exists(relstandinorig) and
451 451 wvfs.exists(rellfile)):
452 452 shutil.copyfile(wvfs.join(rellfile),
453 453 wvfs.join(rellfileorig))
454 454 wvfs.unlinkpath(relstandinorig)
455 455 expecthash = lfutil.readstandin(repo, lfile)
456 456 if expecthash != '':
457 457 if lfile not in repo[None]: # not switched to normal file
458 458 wvfs.unlinkpath(rellfile, ignoremissing=True)
459 459 # use normallookup() to allocate an entry in largefiles
460 460 # dirstate to prevent lfilesrepo.status() from reporting
461 461 # missing files as removed.
462 462 lfdirstate.normallookup(lfile)
463 463 update[lfile] = expecthash
464 464 else:
465 465 # Remove lfiles for which the standin is deleted, unless the
466 466 # lfile is added to the repository again. This happens when a
467 467 # largefile is converted back to a normal file: the standin
468 468 # disappears, but a new (normal) file appears as the lfile.
469 469 if (wvfs.exists(rellfile) and
470 470 repo.dirstate.normalize(lfile) not in repo[None]):
471 471 wvfs.unlinkpath(rellfile)
472 472 removed += 1
473 473
474 474 # largefile processing might be slow and be interrupted - be prepared
475 475 lfdirstate.write()
476 476
477 477 if lfiles:
478 478 statuswriter(_('getting changed largefiles\n'))
479 479 cachelfiles(ui, repo, None, lfiles)
480 480
481 481 for lfile in lfiles:
482 482 update1 = 0
483 483
484 484 expecthash = update.get(lfile)
485 485 if expecthash:
486 486 if not lfutil.copyfromcache(repo, expecthash, lfile):
487 487 # failed ... but already removed and set to normallookup
488 488 continue
489 489 # Synchronize largefile dirstate to the last modified
490 490 # time of the file
491 491 lfdirstate.normal(lfile)
492 492 update1 = 1
493 493
494 494 # copy the state of largefile standin from the repository's
495 495 # dirstate to its state in the lfdirstate.
496 496 rellfile = lfile
497 497 relstandin = lfutil.standin(lfile)
498 498 if wvfs.exists(relstandin):
499 499 mode = wvfs.stat(relstandin).st_mode
500 500 if mode != wvfs.stat(rellfile).st_mode:
501 501 wvfs.chmod(rellfile, mode)
502 502 update1 = 1
503 503
504 504 updated += update1
505 505
506 506 lfutil.synclfdirstate(repo, lfdirstate, lfile, normallookup)
507 507
508 508 lfdirstate.write()
509 509 if lfiles:
510 510 statuswriter(_('%d largefiles updated, %d removed\n') % (updated,
511 511 removed))
512 512
513 513 @command('lfpull',
514 514 [('r', 'rev', [], _('pull largefiles for these revisions'))
515 515 ] + commands.remoteopts,
516 516 _('-r REV... [-e CMD] [--remotecmd CMD] [SOURCE]'))
517 517 def lfpull(ui, repo, source="default", **opts):
518 518 """pull largefiles for the specified revisions from the specified source
519 519
520 520 Pull largefiles that are referenced from local changesets but missing
521 521 locally, pulling from a remote repository to the local cache.
522 522
523 523 If SOURCE is omitted, the 'default' path will be used.
524 524 See :hg:`help urls` for more information.
525 525
526 526 .. container:: verbose
527 527
528 528 Some examples:
529 529
530 530 - pull largefiles for all branch heads::
531 531
532 532 hg lfpull -r "head() and not closed()"
533 533
534 534 - pull largefiles on the default branch::
535 535
536 536 hg lfpull -r "branch(default)"
537 537 """
538 538 repo.lfpullsource = source
539 539
540 540 revs = opts.get('rev', [])
541 541 if not revs:
542 542 raise error.Abort(_('no revisions specified'))
543 543 revs = scmutil.revrange(repo, revs)
544 544
545 545 numcached = 0
546 546 for rev in revs:
547 547 ui.note(_('pulling largefiles for revision %s\n') % rev)
548 548 (cached, missing) = cachelfiles(ui, repo, rev)
549 549 numcached += len(cached)
550 550 ui.status(_("%d largefiles cached\n") % numcached)
@@ -1,1419 +1,1419
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 '''Overridden Mercurial commands and functions for the largefiles extension'''
10 10
11 11 import os
12 12 import copy
13 13
14 14 from mercurial import hg, util, cmdutil, scmutil, match as match_, \
15 15 archival, pathutil, registrar, revset, error
16 16 from mercurial.i18n import _
17 17
18 18 import lfutil
19 19 import lfcommands
20 import basestore
20 import storefactory
21 21
22 22 # -- Utility functions: commonly/repeatedly needed functionality ---------------
23 23
24 24 def composelargefilematcher(match, manifest):
25 25 '''create a matcher that matches only the largefiles in the original
26 26 matcher'''
27 27 m = copy.copy(match)
28 28 lfile = lambda f: lfutil.standin(f) in manifest
29 29 m._files = filter(lfile, m._files)
30 30 m._fileroots = set(m._files)
31 31 m._always = False
32 32 origmatchfn = m.matchfn
33 33 m.matchfn = lambda f: lfile(f) and origmatchfn(f)
34 34 return m
35 35
36 36 def composenormalfilematcher(match, manifest, exclude=None):
37 37 excluded = set()
38 38 if exclude is not None:
39 39 excluded.update(exclude)
40 40
41 41 m = copy.copy(match)
42 42 notlfile = lambda f: not (lfutil.isstandin(f) or lfutil.standin(f) in
43 43 manifest or f in excluded)
44 44 m._files = filter(notlfile, m._files)
45 45 m._fileroots = set(m._files)
46 46 m._always = False
47 47 origmatchfn = m.matchfn
48 48 m.matchfn = lambda f: notlfile(f) and origmatchfn(f)
49 49 return m
50 50
51 51 def installnormalfilesmatchfn(manifest):
52 52 '''installmatchfn with a matchfn that ignores all largefiles'''
53 53 def overridematch(ctx, pats=(), opts=None, globbed=False,
54 54 default='relpath', badfn=None):
55 55 if opts is None:
56 56 opts = {}
57 57 match = oldmatch(ctx, pats, opts, globbed, default, badfn=badfn)
58 58 return composenormalfilematcher(match, manifest)
59 59 oldmatch = installmatchfn(overridematch)
60 60
61 61 def installmatchfn(f):
62 62 '''monkey patch the scmutil module with a custom match function.
63 63 Warning: it is monkey patching the _module_ on runtime! Not thread safe!'''
64 64 oldmatch = scmutil.match
65 65 setattr(f, 'oldmatch', oldmatch)
66 66 scmutil.match = f
67 67 return oldmatch
68 68
69 69 def restorematchfn():
70 70 '''restores scmutil.match to what it was before installmatchfn
71 71 was called. no-op if scmutil.match is its original function.
72 72
73 73 Note that n calls to installmatchfn will require n calls to
74 74 restore the original matchfn.'''
75 75 scmutil.match = getattr(scmutil.match, 'oldmatch')
76 76
77 77 def installmatchandpatsfn(f):
78 78 oldmatchandpats = scmutil.matchandpats
79 79 setattr(f, 'oldmatchandpats', oldmatchandpats)
80 80 scmutil.matchandpats = f
81 81 return oldmatchandpats
82 82
83 83 def restorematchandpatsfn():
84 84 '''restores scmutil.matchandpats to what it was before
85 85 installmatchandpatsfn was called. No-op if scmutil.matchandpats
86 86 is its original function.
87 87
88 88 Note that n calls to installmatchandpatsfn will require n calls
89 89 to restore the original matchfn.'''
90 90 scmutil.matchandpats = getattr(scmutil.matchandpats, 'oldmatchandpats',
91 91 scmutil.matchandpats)
92 92
93 93 def addlargefiles(ui, repo, isaddremove, matcher, **opts):
94 94 large = opts.get('large')
95 95 lfsize = lfutil.getminsize(
96 96 ui, lfutil.islfilesrepo(repo), opts.get('lfsize'))
97 97
98 98 lfmatcher = None
99 99 if lfutil.islfilesrepo(repo):
100 100 lfpats = ui.configlist(lfutil.longname, 'patterns', default=[])
101 101 if lfpats:
102 102 lfmatcher = match_.match(repo.root, '', list(lfpats))
103 103
104 104 lfnames = []
105 105 m = matcher
106 106
107 107 wctx = repo[None]
108 108 for f in repo.walk(match_.badmatch(m, lambda x, y: None)):
109 109 exact = m.exact(f)
110 110 lfile = lfutil.standin(f) in wctx
111 111 nfile = f in wctx
112 112 exists = lfile or nfile
113 113
114 114 # addremove in core gets fancy with the name, add doesn't
115 115 if isaddremove:
116 116 name = m.uipath(f)
117 117 else:
118 118 name = m.rel(f)
119 119
120 120 # Don't warn the user when they attempt to add a normal tracked file.
121 121 # The normal add code will do that for us.
122 122 if exact and exists:
123 123 if lfile:
124 124 ui.warn(_('%s already a largefile\n') % name)
125 125 continue
126 126
127 127 if (exact or not exists) and not lfutil.isstandin(f):
128 128 # In case the file was removed previously, but not committed
129 129 # (issue3507)
130 130 if not repo.wvfs.exists(f):
131 131 continue
132 132
133 133 abovemin = (lfsize and
134 134 repo.wvfs.lstat(f).st_size >= lfsize * 1024 * 1024)
135 135 if large or abovemin or (lfmatcher and lfmatcher(f)):
136 136 lfnames.append(f)
137 137 if ui.verbose or not exact:
138 138 ui.status(_('adding %s as a largefile\n') % name)
139 139
140 140 bad = []
141 141
142 142 # Need to lock, otherwise there could be a race condition between
143 143 # when standins are created and added to the repo.
144 144 with repo.wlock():
145 145 if not opts.get('dry_run'):
146 146 standins = []
147 147 lfdirstate = lfutil.openlfdirstate(ui, repo)
148 148 for f in lfnames:
149 149 standinname = lfutil.standin(f)
150 150 lfutil.writestandin(repo, standinname, hash='',
151 151 executable=lfutil.getexecutable(repo.wjoin(f)))
152 152 standins.append(standinname)
153 153 if lfdirstate[f] == 'r':
154 154 lfdirstate.normallookup(f)
155 155 else:
156 156 lfdirstate.add(f)
157 157 lfdirstate.write()
158 158 bad += [lfutil.splitstandin(f)
159 159 for f in repo[None].add(standins)
160 160 if f in m.files()]
161 161
162 162 added = [f for f in lfnames if f not in bad]
163 163 return added, bad
164 164
165 165 def removelargefiles(ui, repo, isaddremove, matcher, **opts):
166 166 after = opts.get('after')
167 167 m = composelargefilematcher(matcher, repo[None].manifest())
168 168 try:
169 169 repo.lfstatus = True
170 170 s = repo.status(match=m, clean=not isaddremove)
171 171 finally:
172 172 repo.lfstatus = False
173 173 manifest = repo[None].manifest()
174 174 modified, added, deleted, clean = [[f for f in list
175 175 if lfutil.standin(f) in manifest]
176 176 for list in (s.modified, s.added,
177 177 s.deleted, s.clean)]
178 178
179 179 def warn(files, msg):
180 180 for f in files:
181 181 ui.warn(msg % m.rel(f))
182 182 return int(len(files) > 0)
183 183
184 184 result = 0
185 185
186 186 if after:
187 187 remove = deleted
188 188 result = warn(modified + added + clean,
189 189 _('not removing %s: file still exists\n'))
190 190 else:
191 191 remove = deleted + clean
192 192 result = warn(modified, _('not removing %s: file is modified (use -f'
193 193 ' to force removal)\n'))
194 194 result = warn(added, _('not removing %s: file has been marked for add'
195 195 ' (use forget to undo)\n')) or result
196 196
197 197 # Need to lock because standin files are deleted then removed from the
198 198 # repository and we could race in-between.
199 199 with repo.wlock():
200 200 lfdirstate = lfutil.openlfdirstate(ui, repo)
201 201 for f in sorted(remove):
202 202 if ui.verbose or not m.exact(f):
203 203 # addremove in core gets fancy with the name, remove doesn't
204 204 if isaddremove:
205 205 name = m.uipath(f)
206 206 else:
207 207 name = m.rel(f)
208 208 ui.status(_('removing %s\n') % name)
209 209
210 210 if not opts.get('dry_run'):
211 211 if not after:
212 212 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
213 213
214 214 if opts.get('dry_run'):
215 215 return result
216 216
217 217 remove = [lfutil.standin(f) for f in remove]
218 218 # If this is being called by addremove, let the original addremove
219 219 # function handle this.
220 220 if not isaddremove:
221 221 for f in remove:
222 222 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
223 223 repo[None].forget(remove)
224 224
225 225 for f in remove:
226 226 lfutil.synclfdirstate(repo, lfdirstate, lfutil.splitstandin(f),
227 227 False)
228 228
229 229 lfdirstate.write()
230 230
231 231 return result
232 232
233 233 # For overriding mercurial.hgweb.webcommands so that largefiles will
234 234 # appear at their right place in the manifests.
235 235 def decodepath(orig, path):
236 236 return lfutil.splitstandin(path) or path
237 237
238 238 # -- Wrappers: modify existing commands --------------------------------
239 239
240 240 def overrideadd(orig, ui, repo, *pats, **opts):
241 241 if opts.get('normal') and opts.get('large'):
242 242 raise error.Abort(_('--normal cannot be used with --large'))
243 243 return orig(ui, repo, *pats, **opts)
244 244
245 245 def cmdutiladd(orig, ui, repo, matcher, prefix, explicitonly, **opts):
246 246 # The --normal flag short circuits this override
247 247 if opts.get('normal'):
248 248 return orig(ui, repo, matcher, prefix, explicitonly, **opts)
249 249
250 250 ladded, lbad = addlargefiles(ui, repo, False, matcher, **opts)
251 251 normalmatcher = composenormalfilematcher(matcher, repo[None].manifest(),
252 252 ladded)
253 253 bad = orig(ui, repo, normalmatcher, prefix, explicitonly, **opts)
254 254
255 255 bad.extend(f for f in lbad)
256 256 return bad
257 257
258 258 def cmdutilremove(orig, ui, repo, matcher, prefix, after, force, subrepos):
259 259 normalmatcher = composenormalfilematcher(matcher, repo[None].manifest())
260 260 result = orig(ui, repo, normalmatcher, prefix, after, force, subrepos)
261 261 return removelargefiles(ui, repo, False, matcher, after=after,
262 262 force=force) or result
263 263
264 264 def overridestatusfn(orig, repo, rev2, **opts):
265 265 try:
266 266 repo._repo.lfstatus = True
267 267 return orig(repo, rev2, **opts)
268 268 finally:
269 269 repo._repo.lfstatus = False
270 270
271 271 def overridestatus(orig, ui, repo, *pats, **opts):
272 272 try:
273 273 repo.lfstatus = True
274 274 return orig(ui, repo, *pats, **opts)
275 275 finally:
276 276 repo.lfstatus = False
277 277
278 278 def overridedirty(orig, repo, ignoreupdate=False):
279 279 try:
280 280 repo._repo.lfstatus = True
281 281 return orig(repo, ignoreupdate)
282 282 finally:
283 283 repo._repo.lfstatus = False
284 284
285 285 def overridelog(orig, ui, repo, *pats, **opts):
286 286 def overridematchandpats(ctx, pats=(), opts=None, globbed=False,
287 287 default='relpath', badfn=None):
288 288 """Matcher that merges root directory with .hglf, suitable for log.
289 289 It is still possible to match .hglf directly.
290 290 For any listed files run log on the standin too.
291 291 matchfn tries both the given filename and with .hglf stripped.
292 292 """
293 293 if opts is None:
294 294 opts = {}
295 295 matchandpats = oldmatchandpats(ctx, pats, opts, globbed, default,
296 296 badfn=badfn)
297 297 m, p = copy.copy(matchandpats)
298 298
299 299 if m.always():
300 300 # We want to match everything anyway, so there's no benefit trying
301 301 # to add standins.
302 302 return matchandpats
303 303
304 304 pats = set(p)
305 305
306 306 def fixpats(pat, tostandin=lfutil.standin):
307 307 if pat.startswith('set:'):
308 308 return pat
309 309
310 310 kindpat = match_._patsplit(pat, None)
311 311
312 312 if kindpat[0] is not None:
313 313 return kindpat[0] + ':' + tostandin(kindpat[1])
314 314 return tostandin(kindpat[1])
315 315
316 316 if m._cwd:
317 317 hglf = lfutil.shortname
318 318 back = util.pconvert(m.rel(hglf)[:-len(hglf)])
319 319
320 320 def tostandin(f):
321 321 # The file may already be a standin, so truncate the back
322 322 # prefix and test before mangling it. This avoids turning
323 323 # 'glob:../.hglf/foo*' into 'glob:../.hglf/../.hglf/foo*'.
324 324 if f.startswith(back) and lfutil.splitstandin(f[len(back):]):
325 325 return f
326 326
327 327 # An absolute path is from outside the repo, so truncate the
328 328 # path to the root before building the standin. Otherwise cwd
329 329 # is somewhere in the repo, relative to root, and needs to be
330 330 # prepended before building the standin.
331 331 if os.path.isabs(m._cwd):
332 332 f = f[len(back):]
333 333 else:
334 334 f = m._cwd + '/' + f
335 335 return back + lfutil.standin(f)
336 336
337 337 pats.update(fixpats(f, tostandin) for f in p)
338 338 else:
339 339 def tostandin(f):
340 340 if lfutil.splitstandin(f):
341 341 return f
342 342 return lfutil.standin(f)
343 343 pats.update(fixpats(f, tostandin) for f in p)
344 344
345 345 for i in range(0, len(m._files)):
346 346 # Don't add '.hglf' to m.files, since that is already covered by '.'
347 347 if m._files[i] == '.':
348 348 continue
349 349 standin = lfutil.standin(m._files[i])
350 350 # If the "standin" is a directory, append instead of replace to
351 351 # support naming a directory on the command line with only
352 352 # largefiles. The original directory is kept to support normal
353 353 # files.
354 354 if standin in repo[ctx.node()]:
355 355 m._files[i] = standin
356 356 elif m._files[i] not in repo[ctx.node()] \
357 357 and repo.wvfs.isdir(standin):
358 358 m._files.append(standin)
359 359
360 360 m._fileroots = set(m._files)
361 361 m._always = False
362 362 origmatchfn = m.matchfn
363 363 def lfmatchfn(f):
364 364 lf = lfutil.splitstandin(f)
365 365 if lf is not None and origmatchfn(lf):
366 366 return True
367 367 r = origmatchfn(f)
368 368 return r
369 369 m.matchfn = lfmatchfn
370 370
371 371 ui.debug('updated patterns: %s\n' % sorted(pats))
372 372 return m, pats
373 373
374 374 # For hg log --patch, the match object is used in two different senses:
375 375 # (1) to determine what revisions should be printed out, and
376 376 # (2) to determine what files to print out diffs for.
377 377 # The magic matchandpats override should be used for case (1) but not for
378 378 # case (2).
379 379 def overridemakelogfilematcher(repo, pats, opts, badfn=None):
380 380 wctx = repo[None]
381 381 match, pats = oldmatchandpats(wctx, pats, opts, badfn=badfn)
382 382 return lambda rev: match
383 383
384 384 oldmatchandpats = installmatchandpatsfn(overridematchandpats)
385 385 oldmakelogfilematcher = cmdutil._makenofollowlogfilematcher
386 386 setattr(cmdutil, '_makenofollowlogfilematcher', overridemakelogfilematcher)
387 387
388 388 try:
389 389 return orig(ui, repo, *pats, **opts)
390 390 finally:
391 391 restorematchandpatsfn()
392 392 setattr(cmdutil, '_makenofollowlogfilematcher', oldmakelogfilematcher)
393 393
394 394 def overrideverify(orig, ui, repo, *pats, **opts):
395 395 large = opts.pop('large', False)
396 396 all = opts.pop('lfa', False)
397 397 contents = opts.pop('lfc', False)
398 398
399 399 result = orig(ui, repo, *pats, **opts)
400 400 if large or all or contents:
401 401 result = result or lfcommands.verifylfiles(ui, repo, all, contents)
402 402 return result
403 403
404 404 def overridedebugstate(orig, ui, repo, *pats, **opts):
405 405 large = opts.pop('large', False)
406 406 if large:
407 407 class fakerepo(object):
408 408 dirstate = lfutil.openlfdirstate(ui, repo)
409 409 orig(ui, fakerepo, *pats, **opts)
410 410 else:
411 411 orig(ui, repo, *pats, **opts)
412 412
413 413 # Before starting the manifest merge, merge.updates will call
414 414 # _checkunknownfile to check if there are any files in the merged-in
415 415 # changeset that collide with unknown files in the working copy.
416 416 #
417 417 # The largefiles are seen as unknown, so this prevents us from merging
418 418 # in a file 'foo' if we already have a largefile with the same name.
419 419 #
420 420 # The overridden function filters the unknown files by removing any
421 421 # largefiles. This makes the merge proceed and we can then handle this
422 422 # case further in the overridden calculateupdates function below.
423 423 def overridecheckunknownfile(origfn, repo, wctx, mctx, f, f2=None):
424 424 if lfutil.standin(repo.dirstate.normalize(f)) in wctx:
425 425 return False
426 426 return origfn(repo, wctx, mctx, f, f2)
427 427
428 428 # The manifest merge handles conflicts on the manifest level. We want
429 429 # to handle changes in largefile-ness of files at this level too.
430 430 #
431 431 # The strategy is to run the original calculateupdates and then process
432 432 # the action list it outputs. There are two cases we need to deal with:
433 433 #
434 434 # 1. Normal file in p1, largefile in p2. Here the largefile is
435 435 # detected via its standin file, which will enter the working copy
436 436 # with a "get" action. It is not "merge" since the standin is all
437 437 # Mercurial is concerned with at this level -- the link to the
438 438 # existing normal file is not relevant here.
439 439 #
440 440 # 2. Largefile in p1, normal file in p2. Here we get a "merge" action
441 441 # since the largefile will be present in the working copy and
442 442 # different from the normal file in p2. Mercurial therefore
443 443 # triggers a merge action.
444 444 #
445 445 # In both cases, we prompt the user and emit new actions to either
446 446 # remove the standin (if the normal file was kept) or to remove the
447 447 # normal file and get the standin (if the largefile was kept). The
448 448 # default prompt answer is to use the largefile version since it was
449 449 # presumably changed on purpose.
450 450 #
451 451 # Finally, the merge.applyupdates function will then take care of
452 452 # writing the files into the working copy and lfcommands.updatelfiles
453 453 # will update the largefiles.
454 454 def overridecalculateupdates(origfn, repo, p1, p2, pas, branchmerge, force,
455 455 acceptremote, *args, **kwargs):
456 456 overwrite = force and not branchmerge
457 457 actions, diverge, renamedelete = origfn(
458 458 repo, p1, p2, pas, branchmerge, force, acceptremote, *args, **kwargs)
459 459
460 460 if overwrite:
461 461 return actions, diverge, renamedelete
462 462
463 463 # Convert to dictionary with filename as key and action as value.
464 464 lfiles = set()
465 465 for f in actions:
466 466 splitstandin = lfutil.splitstandin(f)
467 467 if splitstandin in p1:
468 468 lfiles.add(splitstandin)
469 469 elif lfutil.standin(f) in p1:
470 470 lfiles.add(f)
471 471
472 472 for lfile in sorted(lfiles):
473 473 standin = lfutil.standin(lfile)
474 474 (lm, largs, lmsg) = actions.get(lfile, (None, None, None))
475 475 (sm, sargs, smsg) = actions.get(standin, (None, None, None))
476 476 if sm in ('g', 'dc') and lm != 'r':
477 477 if sm == 'dc':
478 478 f1, f2, fa, move, anc = sargs
479 479 sargs = (p2[f2].flags(), False)
480 480 # Case 1: normal file in the working copy, largefile in
481 481 # the second parent
482 482 usermsg = _('remote turned local normal file %s into a largefile\n'
483 483 'use (l)argefile or keep (n)ormal file?'
484 484 '$$ &Largefile $$ &Normal file') % lfile
485 485 if repo.ui.promptchoice(usermsg, 0) == 0: # pick remote largefile
486 486 actions[lfile] = ('r', None, 'replaced by standin')
487 487 actions[standin] = ('g', sargs, 'replaces standin')
488 488 else: # keep local normal file
489 489 actions[lfile] = ('k', None, 'replaces standin')
490 490 if branchmerge:
491 491 actions[standin] = ('k', None, 'replaced by non-standin')
492 492 else:
493 493 actions[standin] = ('r', None, 'replaced by non-standin')
494 494 elif lm in ('g', 'dc') and sm != 'r':
495 495 if lm == 'dc':
496 496 f1, f2, fa, move, anc = largs
497 497 largs = (p2[f2].flags(), False)
498 498 # Case 2: largefile in the working copy, normal file in
499 499 # the second parent
500 500 usermsg = _('remote turned local largefile %s into a normal file\n'
501 501 'keep (l)argefile or use (n)ormal file?'
502 502 '$$ &Largefile $$ &Normal file') % lfile
503 503 if repo.ui.promptchoice(usermsg, 0) == 0: # keep local largefile
504 504 if branchmerge:
505 505 # largefile can be restored from standin safely
506 506 actions[lfile] = ('k', None, 'replaced by standin')
507 507 actions[standin] = ('k', None, 'replaces standin')
508 508 else:
509 509 # "lfile" should be marked as "removed" without
510 510 # removal of itself
511 511 actions[lfile] = ('lfmr', None,
512 512 'forget non-standin largefile')
513 513
514 514 # linear-merge should treat this largefile as 're-added'
515 515 actions[standin] = ('a', None, 'keep standin')
516 516 else: # pick remote normal file
517 517 actions[lfile] = ('g', largs, 'replaces standin')
518 518 actions[standin] = ('r', None, 'replaced by non-standin')
519 519
520 520 return actions, diverge, renamedelete
521 521
522 522 def mergerecordupdates(orig, repo, actions, branchmerge):
523 523 if 'lfmr' in actions:
524 524 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
525 525 for lfile, args, msg in actions['lfmr']:
526 526 # this should be executed before 'orig', to execute 'remove'
527 527 # before all other actions
528 528 repo.dirstate.remove(lfile)
529 529 # make sure lfile doesn't get synclfdirstate'd as normal
530 530 lfdirstate.add(lfile)
531 531 lfdirstate.write()
532 532
533 533 return orig(repo, actions, branchmerge)
534 534
535 535
536 536 # Override filemerge to prompt the user about how they wish to merge
537 537 # largefiles. This will handle identical edits without prompting the user.
538 538 def overridefilemerge(origfn, premerge, repo, mynode, orig, fcd, fco, fca,
539 539 labels=None):
540 540 if not lfutil.isstandin(orig) or fcd.isabsent() or fco.isabsent():
541 541 return origfn(premerge, repo, mynode, orig, fcd, fco, fca,
542 542 labels=labels)
543 543
544 544 ahash = fca.data().strip().lower()
545 545 dhash = fcd.data().strip().lower()
546 546 ohash = fco.data().strip().lower()
547 547 if (ohash != ahash and
548 548 ohash != dhash and
549 549 (dhash == ahash or
550 550 repo.ui.promptchoice(
551 551 _('largefile %s has a merge conflict\nancestor was %s\n'
552 552 'keep (l)ocal %s or\ntake (o)ther %s?'
553 553 '$$ &Local $$ &Other') %
554 554 (lfutil.splitstandin(orig), ahash, dhash, ohash),
555 555 0) == 1)):
556 556 repo.wwrite(fcd.path(), fco.data(), fco.flags())
557 557 return True, 0, False
558 558
559 559 def copiespathcopies(orig, ctx1, ctx2, match=None):
560 560 copies = orig(ctx1, ctx2, match=match)
561 561 updated = {}
562 562
563 563 for k, v in copies.iteritems():
564 564 updated[lfutil.splitstandin(k) or k] = lfutil.splitstandin(v) or v
565 565
566 566 return updated
567 567
568 568 # Copy first changes the matchers to match standins instead of
569 569 # largefiles. Then it overrides util.copyfile in that function it
570 570 # checks if the destination largefile already exists. It also keeps a
571 571 # list of copied files so that the largefiles can be copied and the
572 572 # dirstate updated.
573 573 def overridecopy(orig, ui, repo, pats, opts, rename=False):
574 574 # doesn't remove largefile on rename
575 575 if len(pats) < 2:
576 576 # this isn't legal, let the original function deal with it
577 577 return orig(ui, repo, pats, opts, rename)
578 578
579 579 # This could copy both lfiles and normal files in one command,
580 580 # but we don't want to do that. First replace their matcher to
581 581 # only match normal files and run it, then replace it to just
582 582 # match largefiles and run it again.
583 583 nonormalfiles = False
584 584 nolfiles = False
585 585 installnormalfilesmatchfn(repo[None].manifest())
586 586 try:
587 587 result = orig(ui, repo, pats, opts, rename)
588 588 except error.Abort as e:
589 589 if str(e) != _('no files to copy'):
590 590 raise e
591 591 else:
592 592 nonormalfiles = True
593 593 result = 0
594 594 finally:
595 595 restorematchfn()
596 596
597 597 # The first rename can cause our current working directory to be removed.
598 598 # In that case there is nothing left to copy/rename so just quit.
599 599 try:
600 600 repo.getcwd()
601 601 except OSError:
602 602 return result
603 603
604 604 def makestandin(relpath):
605 605 path = pathutil.canonpath(repo.root, repo.getcwd(), relpath)
606 606 return repo.wvfs.join(lfutil.standin(path))
607 607
608 608 fullpats = scmutil.expandpats(pats)
609 609 dest = fullpats[-1]
610 610
611 611 if os.path.isdir(dest):
612 612 if not os.path.isdir(makestandin(dest)):
613 613 os.makedirs(makestandin(dest))
614 614
615 615 try:
616 616 # When we call orig below it creates the standins but we don't add
617 617 # them to the dir state until later so lock during that time.
618 618 wlock = repo.wlock()
619 619
620 620 manifest = repo[None].manifest()
621 621 def overridematch(ctx, pats=(), opts=None, globbed=False,
622 622 default='relpath', badfn=None):
623 623 if opts is None:
624 624 opts = {}
625 625 newpats = []
626 626 # The patterns were previously mangled to add the standin
627 627 # directory; we need to remove that now
628 628 for pat in pats:
629 629 if match_.patkind(pat) is None and lfutil.shortname in pat:
630 630 newpats.append(pat.replace(lfutil.shortname, ''))
631 631 else:
632 632 newpats.append(pat)
633 633 match = oldmatch(ctx, newpats, opts, globbed, default, badfn=badfn)
634 634 m = copy.copy(match)
635 635 lfile = lambda f: lfutil.standin(f) in manifest
636 636 m._files = [lfutil.standin(f) for f in m._files if lfile(f)]
637 637 m._fileroots = set(m._files)
638 638 origmatchfn = m.matchfn
639 639 m.matchfn = lambda f: (lfutil.isstandin(f) and
640 640 (f in manifest) and
641 641 origmatchfn(lfutil.splitstandin(f)) or
642 642 None)
643 643 return m
644 644 oldmatch = installmatchfn(overridematch)
645 645 listpats = []
646 646 for pat in pats:
647 647 if match_.patkind(pat) is not None:
648 648 listpats.append(pat)
649 649 else:
650 650 listpats.append(makestandin(pat))
651 651
652 652 try:
653 653 origcopyfile = util.copyfile
654 654 copiedfiles = []
655 655 def overridecopyfile(src, dest):
656 656 if (lfutil.shortname in src and
657 657 dest.startswith(repo.wjoin(lfutil.shortname))):
658 658 destlfile = dest.replace(lfutil.shortname, '')
659 659 if not opts['force'] and os.path.exists(destlfile):
660 660 raise IOError('',
661 661 _('destination largefile already exists'))
662 662 copiedfiles.append((src, dest))
663 663 origcopyfile(src, dest)
664 664
665 665 util.copyfile = overridecopyfile
666 666 result += orig(ui, repo, listpats, opts, rename)
667 667 finally:
668 668 util.copyfile = origcopyfile
669 669
670 670 lfdirstate = lfutil.openlfdirstate(ui, repo)
671 671 for (src, dest) in copiedfiles:
672 672 if (lfutil.shortname in src and
673 673 dest.startswith(repo.wjoin(lfutil.shortname))):
674 674 srclfile = src.replace(repo.wjoin(lfutil.standin('')), '')
675 675 destlfile = dest.replace(repo.wjoin(lfutil.standin('')), '')
676 676 destlfiledir = repo.wvfs.dirname(repo.wjoin(destlfile)) or '.'
677 677 if not os.path.isdir(destlfiledir):
678 678 os.makedirs(destlfiledir)
679 679 if rename:
680 680 os.rename(repo.wjoin(srclfile), repo.wjoin(destlfile))
681 681
682 682 # The file is gone, but this deletes any empty parent
683 683 # directories as a side-effect.
684 684 util.unlinkpath(repo.wjoin(srclfile), True)
685 685 lfdirstate.remove(srclfile)
686 686 else:
687 687 util.copyfile(repo.wjoin(srclfile),
688 688 repo.wjoin(destlfile))
689 689
690 690 lfdirstate.add(destlfile)
691 691 lfdirstate.write()
692 692 except error.Abort as e:
693 693 if str(e) != _('no files to copy'):
694 694 raise e
695 695 else:
696 696 nolfiles = True
697 697 finally:
698 698 restorematchfn()
699 699 wlock.release()
700 700
701 701 if nolfiles and nonormalfiles:
702 702 raise error.Abort(_('no files to copy'))
703 703
704 704 return result
705 705
706 706 # When the user calls revert, we have to be careful to not revert any
707 707 # changes to other largefiles accidentally. This means we have to keep
708 708 # track of the largefiles that are being reverted so we only pull down
709 709 # the necessary largefiles.
710 710 #
711 711 # Standins are only updated (to match the hash of largefiles) before
712 712 # commits. Update the standins then run the original revert, changing
713 713 # the matcher to hit standins instead of largefiles. Based on the
714 714 # resulting standins update the largefiles.
715 715 def overriderevert(orig, ui, repo, ctx, parents, *pats, **opts):
716 716 # Because we put the standins in a bad state (by updating them)
717 717 # and then return them to a correct state we need to lock to
718 718 # prevent others from changing them in their incorrect state.
719 719 with repo.wlock():
720 720 lfdirstate = lfutil.openlfdirstate(ui, repo)
721 721 s = lfutil.lfdirstatestatus(lfdirstate, repo)
722 722 lfdirstate.write()
723 723 for lfile in s.modified:
724 724 lfutil.updatestandin(repo, lfutil.standin(lfile))
725 725 for lfile in s.deleted:
726 726 if (repo.wvfs.exists(lfutil.standin(lfile))):
727 727 repo.wvfs.unlink(lfutil.standin(lfile))
728 728
729 729 oldstandins = lfutil.getstandinsstate(repo)
730 730
731 731 def overridematch(mctx, pats=(), opts=None, globbed=False,
732 732 default='relpath', badfn=None):
733 733 if opts is None:
734 734 opts = {}
735 735 match = oldmatch(mctx, pats, opts, globbed, default, badfn=badfn)
736 736 m = copy.copy(match)
737 737
738 738 # revert supports recursing into subrepos, and though largefiles
739 739 # currently doesn't work correctly in that case, this match is
740 740 # called, so the lfdirstate above may not be the correct one for
741 741 # this invocation of match.
742 742 lfdirstate = lfutil.openlfdirstate(mctx.repo().ui, mctx.repo(),
743 743 False)
744 744
745 745 def tostandin(f):
746 746 standin = lfutil.standin(f)
747 747 if standin in ctx or standin in mctx:
748 748 return standin
749 749 elif standin in repo[None] or lfdirstate[f] == 'r':
750 750 return None
751 751 return f
752 752 m._files = [tostandin(f) for f in m._files]
753 753 m._files = [f for f in m._files if f is not None]
754 754 m._fileroots = set(m._files)
755 755 origmatchfn = m.matchfn
756 756 def matchfn(f):
757 757 if lfutil.isstandin(f):
758 758 return (origmatchfn(lfutil.splitstandin(f)) and
759 759 (f in ctx or f in mctx))
760 760 return origmatchfn(f)
761 761 m.matchfn = matchfn
762 762 return m
763 763 oldmatch = installmatchfn(overridematch)
764 764 try:
765 765 orig(ui, repo, ctx, parents, *pats, **opts)
766 766 finally:
767 767 restorematchfn()
768 768
769 769 newstandins = lfutil.getstandinsstate(repo)
770 770 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
771 771 # lfdirstate should be 'normallookup'-ed for updated files,
772 772 # because reverting doesn't touch dirstate for 'normal' files
773 773 # when target revision is explicitly specified: in such case,
774 774 # 'n' and valid timestamp in dirstate doesn't ensure 'clean'
775 775 # of target (standin) file.
776 776 lfcommands.updatelfiles(ui, repo, filelist, printmessage=False,
777 777 normallookup=True)
778 778
779 779 # after pulling changesets, we need to take some extra care to get
780 780 # largefiles updated remotely
781 781 def overridepull(orig, ui, repo, source=None, **opts):
782 782 revsprepull = len(repo)
783 783 if not source:
784 784 source = 'default'
785 785 repo.lfpullsource = source
786 786 result = orig(ui, repo, source, **opts)
787 787 revspostpull = len(repo)
788 788 lfrevs = opts.get('lfrev', [])
789 789 if opts.get('all_largefiles'):
790 790 lfrevs.append('pulled()')
791 791 if lfrevs and revspostpull > revsprepull:
792 792 numcached = 0
793 793 repo.firstpulled = revsprepull # for pulled() revset expression
794 794 try:
795 795 for rev in scmutil.revrange(repo, lfrevs):
796 796 ui.note(_('pulling largefiles for revision %s\n') % rev)
797 797 (cached, missing) = lfcommands.cachelfiles(ui, repo, rev)
798 798 numcached += len(cached)
799 799 finally:
800 800 del repo.firstpulled
801 801 ui.status(_("%d largefiles cached\n") % numcached)
802 802 return result
803 803
804 804 def overridepush(orig, ui, repo, *args, **kwargs):
805 805 """Override push command and store --lfrev parameters in opargs"""
806 806 lfrevs = kwargs.pop('lfrev', None)
807 807 if lfrevs:
808 808 opargs = kwargs.setdefault('opargs', {})
809 809 opargs['lfrevs'] = scmutil.revrange(repo, lfrevs)
810 810 return orig(ui, repo, *args, **kwargs)
811 811
812 812 def exchangepushoperation(orig, *args, **kwargs):
813 813 """Override pushoperation constructor and store lfrevs parameter"""
814 814 lfrevs = kwargs.pop('lfrevs', None)
815 815 pushop = orig(*args, **kwargs)
816 816 pushop.lfrevs = lfrevs
817 817 return pushop
818 818
819 819 revsetpredicate = registrar.revsetpredicate()
820 820
821 821 @revsetpredicate('pulled()')
822 822 def pulledrevsetsymbol(repo, subset, x):
823 823 """Changesets that just has been pulled.
824 824
825 825 Only available with largefiles from pull --lfrev expressions.
826 826
827 827 .. container:: verbose
828 828
829 829 Some examples:
830 830
831 831 - pull largefiles for all new changesets::
832 832
833 833 hg pull -lfrev "pulled()"
834 834
835 835 - pull largefiles for all new branch heads::
836 836
837 837 hg pull -lfrev "head(pulled()) and not closed()"
838 838
839 839 """
840 840
841 841 try:
842 842 firstpulled = repo.firstpulled
843 843 except AttributeError:
844 844 raise error.Abort(_("pulled() only available in --lfrev"))
845 845 return revset.baseset([r for r in subset if r >= firstpulled])
846 846
847 847 def overrideclone(orig, ui, source, dest=None, **opts):
848 848 d = dest
849 849 if d is None:
850 850 d = hg.defaultdest(source)
851 851 if opts.get('all_largefiles') and not hg.islocal(d):
852 852 raise error.Abort(_(
853 853 '--all-largefiles is incompatible with non-local destination %s') %
854 854 d)
855 855
856 856 return orig(ui, source, dest, **opts)
857 857
858 858 def hgclone(orig, ui, opts, *args, **kwargs):
859 859 result = orig(ui, opts, *args, **kwargs)
860 860
861 861 if result is not None:
862 862 sourcerepo, destrepo = result
863 863 repo = destrepo.local()
864 864
865 865 # When cloning to a remote repo (like through SSH), no repo is available
866 866 # from the peer. Therefore the largefiles can't be downloaded and the
867 867 # hgrc can't be updated.
868 868 if not repo:
869 869 return result
870 870
871 871 # If largefiles is required for this repo, permanently enable it locally
872 872 if 'largefiles' in repo.requirements:
873 873 fp = repo.vfs('hgrc', 'a', text=True)
874 874 try:
875 875 fp.write('\n[extensions]\nlargefiles=\n')
876 876 finally:
877 877 fp.close()
878 878
879 879 # Caching is implicitly limited to 'rev' option, since the dest repo was
880 880 # truncated at that point. The user may expect a download count with
881 881 # this option, so attempt whether or not this is a largefile repo.
882 882 if opts.get('all_largefiles'):
883 883 success, missing = lfcommands.downloadlfiles(ui, repo, None)
884 884
885 885 if missing != 0:
886 886 return None
887 887
888 888 return result
889 889
890 890 def overriderebase(orig, ui, repo, **opts):
891 891 if not util.safehasattr(repo, '_largefilesenabled'):
892 892 return orig(ui, repo, **opts)
893 893
894 894 resuming = opts.get('continue')
895 895 repo._lfcommithooks.append(lfutil.automatedcommithook(resuming))
896 896 repo._lfstatuswriters.append(lambda *msg, **opts: None)
897 897 try:
898 898 return orig(ui, repo, **opts)
899 899 finally:
900 900 repo._lfstatuswriters.pop()
901 901 repo._lfcommithooks.pop()
902 902
903 903 def overridearchivecmd(orig, ui, repo, dest, **opts):
904 904 repo.unfiltered().lfstatus = True
905 905
906 906 try:
907 907 return orig(ui, repo.unfiltered(), dest, **opts)
908 908 finally:
909 909 repo.unfiltered().lfstatus = False
910 910
911 911 def hgwebarchive(orig, web, req, tmpl):
912 912 web.repo.lfstatus = True
913 913
914 914 try:
915 915 return orig(web, req, tmpl)
916 916 finally:
917 917 web.repo.lfstatus = False
918 918
919 919 def overridearchive(orig, repo, dest, node, kind, decode=True, matchfn=None,
920 920 prefix='', mtime=None, subrepos=None):
921 921 # For some reason setting repo.lfstatus in hgwebarchive only changes the
922 922 # unfiltered repo's attr, so check that as well.
923 923 if not repo.lfstatus and not repo.unfiltered().lfstatus:
924 924 return orig(repo, dest, node, kind, decode, matchfn, prefix, mtime,
925 925 subrepos)
926 926
927 927 # No need to lock because we are only reading history and
928 928 # largefile caches, neither of which are modified.
929 929 if node is not None:
930 930 lfcommands.cachelfiles(repo.ui, repo, node)
931 931
932 932 if kind not in archival.archivers:
933 933 raise error.Abort(_("unknown archive type '%s'") % kind)
934 934
935 935 ctx = repo[node]
936 936
937 937 if kind == 'files':
938 938 if prefix:
939 939 raise error.Abort(
940 940 _('cannot give prefix when archiving to files'))
941 941 else:
942 942 prefix = archival.tidyprefix(dest, kind, prefix)
943 943
944 944 def write(name, mode, islink, getdata):
945 945 if matchfn and not matchfn(name):
946 946 return
947 947 data = getdata()
948 948 if decode:
949 949 data = repo.wwritedata(name, data)
950 950 archiver.addfile(prefix + name, mode, islink, data)
951 951
952 952 archiver = archival.archivers[kind](dest, mtime or ctx.date()[0])
953 953
954 954 if repo.ui.configbool("ui", "archivemeta", True):
955 955 write('.hg_archival.txt', 0o644, False,
956 956 lambda: archival.buildmetadata(ctx))
957 957
958 958 for f in ctx:
959 959 ff = ctx.flags(f)
960 960 getdata = ctx[f].data
961 961 if lfutil.isstandin(f):
962 962 if node is not None:
963 963 path = lfutil.findfile(repo, getdata().strip())
964 964
965 965 if path is None:
966 966 raise error.Abort(
967 967 _('largefile %s not found in repo store or system cache')
968 968 % lfutil.splitstandin(f))
969 969 else:
970 970 path = lfutil.splitstandin(f)
971 971
972 972 f = lfutil.splitstandin(f)
973 973
974 974 getdata = lambda: util.readfile(path)
975 975 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, getdata)
976 976
977 977 if subrepos:
978 978 for subpath in sorted(ctx.substate):
979 979 sub = ctx.workingsub(subpath)
980 980 submatch = match_.subdirmatcher(subpath, matchfn)
981 981 sub._repo.lfstatus = True
982 982 sub.archive(archiver, prefix, submatch)
983 983
984 984 archiver.done()
985 985
986 986 def hgsubrepoarchive(orig, repo, archiver, prefix, match=None):
987 987 if not repo._repo.lfstatus:
988 988 return orig(repo, archiver, prefix, match)
989 989
990 990 repo._get(repo._state + ('hg',))
991 991 rev = repo._state[1]
992 992 ctx = repo._repo[rev]
993 993
994 994 if ctx.node() is not None:
995 995 lfcommands.cachelfiles(repo.ui, repo._repo, ctx.node())
996 996
997 997 def write(name, mode, islink, getdata):
998 998 # At this point, the standin has been replaced with the largefile name,
999 999 # so the normal matcher works here without the lfutil variants.
1000 1000 if match and not match(f):
1001 1001 return
1002 1002 data = getdata()
1003 1003
1004 1004 archiver.addfile(prefix + repo._path + '/' + name, mode, islink, data)
1005 1005
1006 1006 for f in ctx:
1007 1007 ff = ctx.flags(f)
1008 1008 getdata = ctx[f].data
1009 1009 if lfutil.isstandin(f):
1010 1010 if ctx.node() is not None:
1011 1011 path = lfutil.findfile(repo._repo, getdata().strip())
1012 1012
1013 1013 if path is None:
1014 1014 raise error.Abort(
1015 1015 _('largefile %s not found in repo store or system cache')
1016 1016 % lfutil.splitstandin(f))
1017 1017 else:
1018 1018 path = lfutil.splitstandin(f)
1019 1019
1020 1020 f = lfutil.splitstandin(f)
1021 1021
1022 1022 getdata = lambda: util.readfile(os.path.join(prefix, path))
1023 1023
1024 1024 write(f, 'x' in ff and 0o755 or 0o644, 'l' in ff, getdata)
1025 1025
1026 1026 for subpath in sorted(ctx.substate):
1027 1027 sub = ctx.workingsub(subpath)
1028 1028 submatch = match_.subdirmatcher(subpath, match)
1029 1029 sub._repo.lfstatus = True
1030 1030 sub.archive(archiver, prefix + repo._path + '/', submatch)
1031 1031
1032 1032 # If a largefile is modified, the change is not reflected in its
1033 1033 # standin until a commit. cmdutil.bailifchanged() raises an exception
1034 1034 # if the repo has uncommitted changes. Wrap it to also check if
1035 1035 # largefiles were changed. This is used by bisect, backout and fetch.
1036 1036 def overridebailifchanged(orig, repo, *args, **kwargs):
1037 1037 orig(repo, *args, **kwargs)
1038 1038 repo.lfstatus = True
1039 1039 s = repo.status()
1040 1040 repo.lfstatus = False
1041 1041 if s.modified or s.added or s.removed or s.deleted:
1042 1042 raise error.Abort(_('uncommitted changes'))
1043 1043
1044 1044 def postcommitstatus(orig, repo, *args, **kwargs):
1045 1045 repo.lfstatus = True
1046 1046 try:
1047 1047 return orig(repo, *args, **kwargs)
1048 1048 finally:
1049 1049 repo.lfstatus = False
1050 1050
1051 1051 def cmdutilforget(orig, ui, repo, match, prefix, explicitonly):
1052 1052 normalmatcher = composenormalfilematcher(match, repo[None].manifest())
1053 1053 bad, forgot = orig(ui, repo, normalmatcher, prefix, explicitonly)
1054 1054 m = composelargefilematcher(match, repo[None].manifest())
1055 1055
1056 1056 try:
1057 1057 repo.lfstatus = True
1058 1058 s = repo.status(match=m, clean=True)
1059 1059 finally:
1060 1060 repo.lfstatus = False
1061 1061 forget = sorted(s.modified + s.added + s.deleted + s.clean)
1062 1062 forget = [f for f in forget if lfutil.standin(f) in repo[None].manifest()]
1063 1063
1064 1064 for f in forget:
1065 1065 if lfutil.standin(f) not in repo.dirstate and not \
1066 1066 repo.wvfs.isdir(lfutil.standin(f)):
1067 1067 ui.warn(_('not removing %s: file is already untracked\n')
1068 1068 % m.rel(f))
1069 1069 bad.append(f)
1070 1070
1071 1071 for f in forget:
1072 1072 if ui.verbose or not m.exact(f):
1073 1073 ui.status(_('removing %s\n') % m.rel(f))
1074 1074
1075 1075 # Need to lock because standin files are deleted then removed from the
1076 1076 # repository and we could race in-between.
1077 1077 with repo.wlock():
1078 1078 lfdirstate = lfutil.openlfdirstate(ui, repo)
1079 1079 for f in forget:
1080 1080 if lfdirstate[f] == 'a':
1081 1081 lfdirstate.drop(f)
1082 1082 else:
1083 1083 lfdirstate.remove(f)
1084 1084 lfdirstate.write()
1085 1085 standins = [lfutil.standin(f) for f in forget]
1086 1086 for f in standins:
1087 1087 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1088 1088 rejected = repo[None].forget(standins)
1089 1089
1090 1090 bad.extend(f for f in rejected if f in m.files())
1091 1091 forgot.extend(f for f in forget if f not in rejected)
1092 1092 return bad, forgot
1093 1093
1094 1094 def _getoutgoings(repo, other, missing, addfunc):
1095 1095 """get pairs of filename and largefile hash in outgoing revisions
1096 1096 in 'missing'.
1097 1097
1098 1098 largefiles already existing on 'other' repository are ignored.
1099 1099
1100 1100 'addfunc' is invoked with each unique pairs of filename and
1101 1101 largefile hash value.
1102 1102 """
1103 1103 knowns = set()
1104 1104 lfhashes = set()
1105 1105 def dedup(fn, lfhash):
1106 1106 k = (fn, lfhash)
1107 1107 if k not in knowns:
1108 1108 knowns.add(k)
1109 1109 lfhashes.add(lfhash)
1110 1110 lfutil.getlfilestoupload(repo, missing, dedup)
1111 1111 if lfhashes:
1112 lfexists = basestore._openstore(repo, other).exists(lfhashes)
1112 lfexists = storefactory._openstore(repo, other).exists(lfhashes)
1113 1113 for fn, lfhash in knowns:
1114 1114 if not lfexists[lfhash]: # lfhash doesn't exist on "other"
1115 1115 addfunc(fn, lfhash)
1116 1116
1117 1117 def outgoinghook(ui, repo, other, opts, missing):
1118 1118 if opts.pop('large', None):
1119 1119 lfhashes = set()
1120 1120 if ui.debugflag:
1121 1121 toupload = {}
1122 1122 def addfunc(fn, lfhash):
1123 1123 if fn not in toupload:
1124 1124 toupload[fn] = []
1125 1125 toupload[fn].append(lfhash)
1126 1126 lfhashes.add(lfhash)
1127 1127 def showhashes(fn):
1128 1128 for lfhash in sorted(toupload[fn]):
1129 1129 ui.debug(' %s\n' % (lfhash))
1130 1130 else:
1131 1131 toupload = set()
1132 1132 def addfunc(fn, lfhash):
1133 1133 toupload.add(fn)
1134 1134 lfhashes.add(lfhash)
1135 1135 def showhashes(fn):
1136 1136 pass
1137 1137 _getoutgoings(repo, other, missing, addfunc)
1138 1138
1139 1139 if not toupload:
1140 1140 ui.status(_('largefiles: no files to upload\n'))
1141 1141 else:
1142 1142 ui.status(_('largefiles to upload (%d entities):\n')
1143 1143 % (len(lfhashes)))
1144 1144 for file in sorted(toupload):
1145 1145 ui.status(lfutil.splitstandin(file) + '\n')
1146 1146 showhashes(file)
1147 1147 ui.status('\n')
1148 1148
1149 1149 def summaryremotehook(ui, repo, opts, changes):
1150 1150 largeopt = opts.get('large', False)
1151 1151 if changes is None:
1152 1152 if largeopt:
1153 1153 return (False, True) # only outgoing check is needed
1154 1154 else:
1155 1155 return (False, False)
1156 1156 elif largeopt:
1157 1157 url, branch, peer, outgoing = changes[1]
1158 1158 if peer is None:
1159 1159 # i18n: column positioning for "hg summary"
1160 1160 ui.status(_('largefiles: (no remote repo)\n'))
1161 1161 return
1162 1162
1163 1163 toupload = set()
1164 1164 lfhashes = set()
1165 1165 def addfunc(fn, lfhash):
1166 1166 toupload.add(fn)
1167 1167 lfhashes.add(lfhash)
1168 1168 _getoutgoings(repo, peer, outgoing.missing, addfunc)
1169 1169
1170 1170 if not toupload:
1171 1171 # i18n: column positioning for "hg summary"
1172 1172 ui.status(_('largefiles: (no files to upload)\n'))
1173 1173 else:
1174 1174 # i18n: column positioning for "hg summary"
1175 1175 ui.status(_('largefiles: %d entities for %d files to upload\n')
1176 1176 % (len(lfhashes), len(toupload)))
1177 1177
1178 1178 def overridesummary(orig, ui, repo, *pats, **opts):
1179 1179 try:
1180 1180 repo.lfstatus = True
1181 1181 orig(ui, repo, *pats, **opts)
1182 1182 finally:
1183 1183 repo.lfstatus = False
1184 1184
1185 1185 def scmutiladdremove(orig, repo, matcher, prefix, opts=None, dry_run=None,
1186 1186 similarity=None):
1187 1187 if opts is None:
1188 1188 opts = {}
1189 1189 if not lfutil.islfilesrepo(repo):
1190 1190 return orig(repo, matcher, prefix, opts, dry_run, similarity)
1191 1191 # Get the list of missing largefiles so we can remove them
1192 1192 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1193 1193 unsure, s = lfdirstate.status(match_.always(repo.root, repo.getcwd()), [],
1194 1194 False, False, False)
1195 1195
1196 1196 # Call into the normal remove code, but the removing of the standin, we want
1197 1197 # to have handled by original addremove. Monkey patching here makes sure
1198 1198 # we don't remove the standin in the largefiles code, preventing a very
1199 1199 # confused state later.
1200 1200 if s.deleted:
1201 1201 m = copy.copy(matcher)
1202 1202
1203 1203 # The m._files and m._map attributes are not changed to the deleted list
1204 1204 # because that affects the m.exact() test, which in turn governs whether
1205 1205 # or not the file name is printed, and how. Simply limit the original
1206 1206 # matches to those in the deleted status list.
1207 1207 matchfn = m.matchfn
1208 1208 m.matchfn = lambda f: f in s.deleted and matchfn(f)
1209 1209
1210 1210 removelargefiles(repo.ui, repo, True, m, **opts)
1211 1211 # Call into the normal add code, and any files that *should* be added as
1212 1212 # largefiles will be
1213 1213 added, bad = addlargefiles(repo.ui, repo, True, matcher, **opts)
1214 1214 # Now that we've handled largefiles, hand off to the original addremove
1215 1215 # function to take care of the rest. Make sure it doesn't do anything with
1216 1216 # largefiles by passing a matcher that will ignore them.
1217 1217 matcher = composenormalfilematcher(matcher, repo[None].manifest(), added)
1218 1218 return orig(repo, matcher, prefix, opts, dry_run, similarity)
1219 1219
1220 1220 # Calling purge with --all will cause the largefiles to be deleted.
1221 1221 # Override repo.status to prevent this from happening.
1222 1222 def overridepurge(orig, ui, repo, *dirs, **opts):
1223 1223 # XXX Monkey patching a repoview will not work. The assigned attribute will
1224 1224 # be set on the unfiltered repo, but we will only lookup attributes in the
1225 1225 # unfiltered repo if the lookup in the repoview object itself fails. As the
1226 1226 # monkey patched method exists on the repoview class the lookup will not
1227 1227 # fail. As a result, the original version will shadow the monkey patched
1228 1228 # one, defeating the monkey patch.
1229 1229 #
1230 1230 # As a work around we use an unfiltered repo here. We should do something
1231 1231 # cleaner instead.
1232 1232 repo = repo.unfiltered()
1233 1233 oldstatus = repo.status
1234 1234 def overridestatus(node1='.', node2=None, match=None, ignored=False,
1235 1235 clean=False, unknown=False, listsubrepos=False):
1236 1236 r = oldstatus(node1, node2, match, ignored, clean, unknown,
1237 1237 listsubrepos)
1238 1238 lfdirstate = lfutil.openlfdirstate(ui, repo)
1239 1239 unknown = [f for f in r.unknown if lfdirstate[f] == '?']
1240 1240 ignored = [f for f in r.ignored if lfdirstate[f] == '?']
1241 1241 return scmutil.status(r.modified, r.added, r.removed, r.deleted,
1242 1242 unknown, ignored, r.clean)
1243 1243 repo.status = overridestatus
1244 1244 orig(ui, repo, *dirs, **opts)
1245 1245 repo.status = oldstatus
1246 1246 def overriderollback(orig, ui, repo, **opts):
1247 1247 with repo.wlock():
1248 1248 before = repo.dirstate.parents()
1249 1249 orphans = set(f for f in repo.dirstate
1250 1250 if lfutil.isstandin(f) and repo.dirstate[f] != 'r')
1251 1251 result = orig(ui, repo, **opts)
1252 1252 after = repo.dirstate.parents()
1253 1253 if before == after:
1254 1254 return result # no need to restore standins
1255 1255
1256 1256 pctx = repo['.']
1257 1257 for f in repo.dirstate:
1258 1258 if lfutil.isstandin(f):
1259 1259 orphans.discard(f)
1260 1260 if repo.dirstate[f] == 'r':
1261 1261 repo.wvfs.unlinkpath(f, ignoremissing=True)
1262 1262 elif f in pctx:
1263 1263 fctx = pctx[f]
1264 1264 repo.wwrite(f, fctx.data(), fctx.flags())
1265 1265 else:
1266 1266 # content of standin is not so important in 'a',
1267 1267 # 'm' or 'n' (coming from the 2nd parent) cases
1268 1268 lfutil.writestandin(repo, f, '', False)
1269 1269 for standin in orphans:
1270 1270 repo.wvfs.unlinkpath(standin, ignoremissing=True)
1271 1271
1272 1272 lfdirstate = lfutil.openlfdirstate(ui, repo)
1273 1273 orphans = set(lfdirstate)
1274 1274 lfiles = lfutil.listlfiles(repo)
1275 1275 for file in lfiles:
1276 1276 lfutil.synclfdirstate(repo, lfdirstate, file, True)
1277 1277 orphans.discard(file)
1278 1278 for lfile in orphans:
1279 1279 lfdirstate.drop(lfile)
1280 1280 lfdirstate.write()
1281 1281 return result
1282 1282
1283 1283 def overridetransplant(orig, ui, repo, *revs, **opts):
1284 1284 resuming = opts.get('continue')
1285 1285 repo._lfcommithooks.append(lfutil.automatedcommithook(resuming))
1286 1286 repo._lfstatuswriters.append(lambda *msg, **opts: None)
1287 1287 try:
1288 1288 result = orig(ui, repo, *revs, **opts)
1289 1289 finally:
1290 1290 repo._lfstatuswriters.pop()
1291 1291 repo._lfcommithooks.pop()
1292 1292 return result
1293 1293
1294 1294 def overridecat(orig, ui, repo, file1, *pats, **opts):
1295 1295 ctx = scmutil.revsingle(repo, opts.get('rev'))
1296 1296 err = 1
1297 1297 notbad = set()
1298 1298 m = scmutil.match(ctx, (file1,) + pats, opts)
1299 1299 origmatchfn = m.matchfn
1300 1300 def lfmatchfn(f):
1301 1301 if origmatchfn(f):
1302 1302 return True
1303 1303 lf = lfutil.splitstandin(f)
1304 1304 if lf is None:
1305 1305 return False
1306 1306 notbad.add(lf)
1307 1307 return origmatchfn(lf)
1308 1308 m.matchfn = lfmatchfn
1309 1309 origbadfn = m.bad
1310 1310 def lfbadfn(f, msg):
1311 1311 if not f in notbad:
1312 1312 origbadfn(f, msg)
1313 1313 m.bad = lfbadfn
1314 1314
1315 1315 origvisitdirfn = m.visitdir
1316 1316 def lfvisitdirfn(dir):
1317 1317 if dir == lfutil.shortname:
1318 1318 return True
1319 1319 ret = origvisitdirfn(dir)
1320 1320 if ret:
1321 1321 return ret
1322 1322 lf = lfutil.splitstandin(dir)
1323 1323 if lf is None:
1324 1324 return False
1325 1325 return origvisitdirfn(lf)
1326 1326 m.visitdir = lfvisitdirfn
1327 1327
1328 1328 for f in ctx.walk(m):
1329 1329 fp = cmdutil.makefileobj(repo, opts.get('output'), ctx.node(),
1330 1330 pathname=f)
1331 1331 lf = lfutil.splitstandin(f)
1332 1332 if lf is None or origmatchfn(f):
1333 1333 # duplicating unreachable code from commands.cat
1334 1334 data = ctx[f].data()
1335 1335 if opts.get('decode'):
1336 1336 data = repo.wwritedata(f, data)
1337 1337 fp.write(data)
1338 1338 else:
1339 1339 hash = lfutil.readstandin(repo, lf, ctx.rev())
1340 1340 if not lfutil.inusercache(repo.ui, hash):
1341 store = basestore._openstore(repo)
1341 store = storefactory._openstore(repo)
1342 1342 success, missing = store.get([(lf, hash)])
1343 1343 if len(success) != 1:
1344 1344 raise error.Abort(
1345 1345 _('largefile %s is not in cache and could not be '
1346 1346 'downloaded') % lf)
1347 1347 path = lfutil.usercachepath(repo.ui, hash)
1348 1348 fpin = open(path, "rb")
1349 1349 for chunk in util.filechunkiter(fpin, 128 * 1024):
1350 1350 fp.write(chunk)
1351 1351 fpin.close()
1352 1352 fp.close()
1353 1353 err = 0
1354 1354 return err
1355 1355
1356 1356 def mergeupdate(orig, repo, node, branchmerge, force,
1357 1357 *args, **kwargs):
1358 1358 matcher = kwargs.get('matcher', None)
1359 1359 # note if this is a partial update
1360 1360 partial = matcher and not matcher.always()
1361 1361 with repo.wlock():
1362 1362 # branch | | |
1363 1363 # merge | force | partial | action
1364 1364 # -------+-------+---------+--------------
1365 1365 # x | x | x | linear-merge
1366 1366 # o | x | x | branch-merge
1367 1367 # x | o | x | overwrite (as clean update)
1368 1368 # o | o | x | force-branch-merge (*1)
1369 1369 # x | x | o | (*)
1370 1370 # o | x | o | (*)
1371 1371 # x | o | o | overwrite (as revert)
1372 1372 # o | o | o | (*)
1373 1373 #
1374 1374 # (*) don't care
1375 1375 # (*1) deprecated, but used internally (e.g: "rebase --collapse")
1376 1376
1377 1377 lfdirstate = lfutil.openlfdirstate(repo.ui, repo)
1378 1378 unsure, s = lfdirstate.status(match_.always(repo.root,
1379 1379 repo.getcwd()),
1380 1380 [], False, False, False)
1381 1381 pctx = repo['.']
1382 1382 for lfile in unsure + s.modified:
1383 1383 lfileabs = repo.wvfs.join(lfile)
1384 1384 if not repo.wvfs.exists(lfileabs):
1385 1385 continue
1386 1386 lfhash = lfutil.hashrepofile(repo, lfile)
1387 1387 standin = lfutil.standin(lfile)
1388 1388 lfutil.writestandin(repo, standin, lfhash,
1389 1389 lfutil.getexecutable(lfileabs))
1390 1390 if (standin in pctx and
1391 1391 lfhash == lfutil.readstandin(repo, lfile, '.')):
1392 1392 lfdirstate.normal(lfile)
1393 1393 for lfile in s.added:
1394 1394 lfutil.updatestandin(repo, lfutil.standin(lfile))
1395 1395 lfdirstate.write()
1396 1396
1397 1397 oldstandins = lfutil.getstandinsstate(repo)
1398 1398
1399 1399 result = orig(repo, node, branchmerge, force, *args, **kwargs)
1400 1400
1401 1401 newstandins = lfutil.getstandinsstate(repo)
1402 1402 filelist = lfutil.getlfilestoupdate(oldstandins, newstandins)
1403 1403 if branchmerge or force or partial:
1404 1404 filelist.extend(s.deleted + s.removed)
1405 1405
1406 1406 lfcommands.updatelfiles(repo.ui, repo, filelist=filelist,
1407 1407 normallookup=partial)
1408 1408
1409 1409 return result
1410 1410
1411 1411 def scmutilmarktouched(orig, repo, files, *args, **kwargs):
1412 1412 result = orig(repo, files, *args, **kwargs)
1413 1413
1414 1414 filelist = [lfutil.splitstandin(f) for f in files if lfutil.isstandin(f)]
1415 1415 if filelist:
1416 1416 lfcommands.updatelfiles(repo.ui, repo, filelist=filelist,
1417 1417 printmessage=False, normallookup=True)
1418 1418
1419 1419 return result
@@ -1,226 +1,78
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
5 #
6 1 # This software may be used and distributed according to the terms of the
7 2 # GNU General Public License version 2 or any later version.
8 3
9 '''base class for store implementations and store-related utility code'''
4 from __future__ import absolute_import
10 5
11 6 import re
12 7
13 from mercurial import util, node, hg, error
14 8 from mercurial.i18n import _
15 9
16 import lfutil
17
18 class StoreError(Exception):
19 '''Raised when there is a problem getting files from or putting
20 files to a central store.'''
21 def __init__(self, filename, hash, url, detail):
22 self.filename = filename
23 self.hash = hash
24 self.url = url
25 self.detail = detail
26
27 def longmessage(self):
28 return (_("error getting id %s from url %s for file %s: %s\n") %
29 (self.hash, util.hidepassword(self.url), self.filename,
30 self.detail))
31
32 def __str__(self):
33 return "%s: %s" % (util.hidepassword(self.url), self.detail)
34
35 class basestore(object):
36 def __init__(self, ui, repo, url):
37 self.ui = ui
38 self.repo = repo
39 self.url = url
40
41 def put(self, source, hash):
42 '''Put source file into the store so it can be retrieved by hash.'''
43 raise NotImplementedError('abstract method')
44
45 def exists(self, hashes):
46 '''Check to see if the store contains the given hashes. Given an
47 iterable of hashes it returns a mapping from hash to bool.'''
48 raise NotImplementedError('abstract method')
49
50 def get(self, files):
51 '''Get the specified largefiles from the store and write to local
52 files under repo.root. files is a list of (filename, hash)
53 tuples. Return (success, missing), lists of files successfully
54 downloaded and those not found in the store. success is a list
55 of (filename, hash) tuples; missing is a list of filenames that
56 we could not get. (The detailed error message will already have
57 been presented to the user, so missing is just supplied as a
58 summary.)'''
59 success = []
60 missing = []
61 ui = self.ui
62
63 at = 0
64 available = self.exists(set(hash for (_filename, hash) in files))
65 for filename, hash in files:
66 ui.progress(_('getting largefiles'), at, unit=_('files'),
67 total=len(files))
68 at += 1
69 ui.note(_('getting %s:%s\n') % (filename, hash))
70
71 if not available.get(hash):
72 ui.warn(_('%s: largefile %s not available from %s\n')
73 % (filename, hash, util.hidepassword(self.url)))
74 missing.append(filename)
75 continue
76
77 if self._gethash(filename, hash):
78 success.append((filename, hash))
79 else:
80 missing.append(filename)
81
82 ui.progress(_('getting largefiles'), None)
83 return (success, missing)
84
85 def _gethash(self, filename, hash):
86 """Get file with the provided hash and store it in the local repo's
87 store and in the usercache.
88 filename is for informational messages only.
89 """
90 util.makedirs(lfutil.storepath(self.repo, ''))
91 storefilename = lfutil.storepath(self.repo, hash)
92
93 tmpname = storefilename + '.tmp'
94 tmpfile = util.atomictempfile(tmpname,
95 createmode=self.repo.store.createmode)
10 from mercurial import (
11 error,
12 hg,
13 util,
14 )
96 15
97 try:
98 gothash = self._getfile(tmpfile, filename, hash)
99 except StoreError as err:
100 self.ui.warn(err.longmessage())
101 gothash = ""
102 tmpfile.close()
103
104 if gothash != hash:
105 if gothash != "":
106 self.ui.warn(_('%s: data corruption (expected %s, got %s)\n')
107 % (filename, hash, gothash))
108 util.unlink(tmpname)
109 return False
110
111 util.rename(tmpname, storefilename)
112 lfutil.linktousercache(self.repo, hash)
113 return True
114
115 def verify(self, revs, contents=False):
116 '''Verify the existence (and, optionally, contents) of every big
117 file revision referenced by every changeset in revs.
118 Return 0 if all is well, non-zero on any errors.'''
119
120 self.ui.status(_('searching %d changesets for largefiles\n') %
121 len(revs))
122 verified = set() # set of (filename, filenode) tuples
123 filestocheck = [] # list of (cset, filename, expectedhash)
124 for rev in revs:
125 cctx = self.repo[rev]
126 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
127
128 for standin in cctx:
129 filename = lfutil.splitstandin(standin)
130 if filename:
131 fctx = cctx[standin]
132 key = (filename, fctx.filenode())
133 if key not in verified:
134 verified.add(key)
135 expectedhash = fctx.data()[0:40]
136 filestocheck.append((cset, filename, expectedhash))
137
138 failed = self._verifyfiles(contents, filestocheck)
139
140 numrevs = len(verified)
141 numlfiles = len(set([fname for (fname, fnode) in verified]))
142 if contents:
143 self.ui.status(
144 _('verified contents of %d revisions of %d largefiles\n')
145 % (numrevs, numlfiles))
146 else:
147 self.ui.status(
148 _('verified existence of %d revisions of %d largefiles\n')
149 % (numrevs, numlfiles))
150 return int(failed)
151
152 def _getfile(self, tmpfile, filename, hash):
153 '''Fetch one revision of one file from the store and write it
154 to tmpfile. Compute the hash of the file on-the-fly as it
155 downloads and return the hash. Close tmpfile. Raise
156 StoreError if unable to download the file (e.g. it does not
157 exist in the store).'''
158 raise NotImplementedError('abstract method')
159
160 def _verifyfiles(self, contents, filestocheck):
161 '''Perform the actual verification of files in the store.
162 'contents' controls verification of content hash.
163 'filestocheck' is list of files to check.
164 Returns _true_ if any problems are found!
165 '''
166 raise NotImplementedError('abstract method')
167
168 import localstore, wirestore
169
170 _storeprovider = {
171 'file': [localstore.localstore],
172 'http': [wirestore.wirestore],
173 'https': [wirestore.wirestore],
174 'ssh': [wirestore.wirestore],
175 }
176
177 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
16 from . import (
17 lfutil,
18 localstore,
19 wirestore,
20 )
178 21
179 22 # During clone this function is passed the src's ui object
180 23 # but it needs the dest's ui object so it can read out of
181 24 # the config file. Use repo.ui instead.
182 25 def _openstore(repo, remote=None, put=False):
183 26 ui = repo.ui
184 27
185 28 if not remote:
186 29 lfpullsource = getattr(repo, 'lfpullsource', None)
187 30 if lfpullsource:
188 31 path = ui.expandpath(lfpullsource)
189 32 elif put:
190 33 path = ui.expandpath('default-push', 'default')
191 34 else:
192 35 path = ui.expandpath('default')
193 36
194 37 # ui.expandpath() leaves 'default-push' and 'default' alone if
195 38 # they cannot be expanded: fallback to the empty string,
196 39 # meaning the current directory.
197 40 if path == 'default-push' or path == 'default':
198 41 path = ''
199 42 remote = repo
200 43 else:
201 44 path, _branches = hg.parseurl(path)
202 45 remote = hg.peer(repo, {}, path)
203 46
204 47 # The path could be a scheme so use Mercurial's normal functionality
205 48 # to resolve the scheme to a repository and use its path
206 49 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
207 50
208 51 match = _scheme_re.match(path)
209 52 if not match: # regular filesystem path
210 53 scheme = 'file'
211 54 else:
212 55 scheme = match.group(1)
213 56
214 57 try:
215 58 storeproviders = _storeprovider[scheme]
216 59 except KeyError:
217 60 raise error.Abort(_('unsupported URL scheme %r') % scheme)
218 61
219 62 for classobj in storeproviders:
220 63 try:
221 64 return classobj(ui, repo, remote)
222 65 except lfutil.storeprotonotcapable:
223 66 pass
224 67
225 68 raise error.Abort(_('%s does not appear to be a largefile store') %
226 69 util.hidepassword(path))
70
71 _storeprovider = {
72 'file': [localstore.localstore],
73 'http': [wirestore.wirestore],
74 'https': [wirestore.wirestore],
75 'ssh': [wirestore.wirestore],
76 }
77
78 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
@@ -1,183 +1,181
1 1 #require test-repo
2 2
3 3 $ . "$TESTDIR/helpers-testrepo.sh"
4 4 $ import_checker="$TESTDIR"/../contrib/import-checker.py
5 5
6 6 Run the doctests from the import checker, and make sure
7 7 it's working correctly.
8 8 $ TERM=dumb
9 9 $ export TERM
10 10 $ python -m doctest $import_checker
11 11
12 12 Run additional tests for the import checker
13 13
14 14 $ mkdir testpackage
15 15 $ touch testpackage/__init__.py
16 16
17 17 $ cat > testpackage/multiple.py << EOF
18 18 > from __future__ import absolute_import
19 19 > import os, sys
20 20 > EOF
21 21
22 22 $ cat > testpackage/unsorted.py << EOF
23 23 > from __future__ import absolute_import
24 24 > import sys
25 25 > import os
26 26 > EOF
27 27
28 28 $ cat > testpackage/stdafterlocal.py << EOF
29 29 > from __future__ import absolute_import
30 30 > from . import unsorted
31 31 > import os
32 32 > EOF
33 33
34 34 $ cat > testpackage/requirerelative.py << EOF
35 35 > from __future__ import absolute_import
36 36 > import testpackage.unsorted
37 37 > EOF
38 38
39 39 $ cat > testpackage/importalias.py << EOF
40 40 > from __future__ import absolute_import
41 41 > import ui
42 42 > EOF
43 43
44 44 $ cat > testpackage/relativestdlib.py << EOF
45 45 > from __future__ import absolute_import
46 46 > from .. import os
47 47 > EOF
48 48
49 49 $ cat > testpackage/symbolimport.py << EOF
50 50 > from __future__ import absolute_import
51 51 > from .unsorted import foo
52 52 > EOF
53 53
54 54 $ cat > testpackage/latesymbolimport.py << EOF
55 55 > from __future__ import absolute_import
56 56 > from . import unsorted
57 57 > from mercurial.node import hex
58 58 > EOF
59 59
60 60 $ cat > testpackage/multiplegroups.py << EOF
61 61 > from __future__ import absolute_import
62 62 > from . import unsorted
63 63 > from . import more
64 64 > EOF
65 65
66 66 $ mkdir testpackage/subpackage
67 67 $ cat > testpackage/subpackage/levelpriority.py << EOF
68 68 > from __future__ import absolute_import
69 69 > from . import foo
70 70 > from .. import parent
71 71 > EOF
72 72
73 73 $ touch testpackage/subpackage/foo.py
74 74 $ cat > testpackage/subpackage/__init__.py << EOF
75 75 > from __future__ import absolute_import
76 76 > from . import levelpriority # should not cause cycle
77 77 > EOF
78 78
79 79 $ cat > testpackage/subpackage/localimport.py << EOF
80 80 > from __future__ import absolute_import
81 81 > from . import foo
82 82 > def bar():
83 83 > # should not cause "higher-level import should come first"
84 84 > from .. import unsorted
85 85 > # but other errors should be detected
86 86 > from .. import more
87 87 > import testpackage.subpackage.levelpriority
88 88 > EOF
89 89
90 90 $ cat > testpackage/importmodulefromsub.py << EOF
91 91 > from __future__ import absolute_import
92 92 > from .subpackage import foo # not a "direct symbol import"
93 93 > EOF
94 94
95 95 $ cat > testpackage/importsymbolfromsub.py << EOF
96 96 > from __future__ import absolute_import
97 97 > from .subpackage import foo, nonmodule
98 98 > EOF
99 99
100 100 $ cat > testpackage/sortedentries.py << EOF
101 101 > from __future__ import absolute_import
102 102 > from . import (
103 103 > foo,
104 104 > bar,
105 105 > )
106 106 > EOF
107 107
108 108 $ cat > testpackage/importfromalias.py << EOF
109 109 > from __future__ import absolute_import
110 110 > from . import ui
111 111 > EOF
112 112
113 113 $ cat > testpackage/importfromrelative.py << EOF
114 114 > from __future__ import absolute_import
115 115 > from testpackage.unsorted import foo
116 116 > EOF
117 117
118 118 $ mkdir testpackage2
119 119 $ touch testpackage2/__init__.py
120 120
121 121 $ cat > testpackage2/latesymbolimport.py << EOF
122 122 > from __future__ import absolute_import
123 123 > from testpackage import unsorted
124 124 > from mercurial.node import hex
125 125 > EOF
126 126
127 127 $ python "$import_checker" testpackage*/*.py testpackage/subpackage/*.py
128 128 testpackage/importalias.py:2: ui module must be "as" aliased to uimod
129 129 testpackage/importfromalias.py:2: ui from testpackage must be "as" aliased to uimod
130 130 testpackage/importfromrelative.py:2: import should be relative: testpackage.unsorted
131 131 testpackage/importfromrelative.py:2: direct symbol import foo from testpackage.unsorted
132 132 testpackage/importsymbolfromsub.py:2: direct symbol import nonmodule from testpackage.subpackage
133 133 testpackage/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
134 134 testpackage/multiple.py:2: multiple imported names: os, sys
135 135 testpackage/multiplegroups.py:3: multiple "from . import" statements
136 136 testpackage/relativestdlib.py:2: relative import of stdlib module
137 137 testpackage/requirerelative.py:2: import should be relative: testpackage.unsorted
138 138 testpackage/sortedentries.py:2: imports from testpackage not lexically sorted: bar < foo
139 139 testpackage/stdafterlocal.py:3: stdlib import "os" follows local import: testpackage
140 140 testpackage/subpackage/levelpriority.py:3: higher-level import should come first: testpackage
141 141 testpackage/subpackage/localimport.py:7: multiple "from .. import" statements
142 142 testpackage/subpackage/localimport.py:8: import should be relative: testpackage.subpackage.levelpriority
143 143 testpackage/symbolimport.py:2: direct symbol import foo from testpackage.unsorted
144 144 testpackage/unsorted.py:3: imports not lexically sorted: os < sys
145 145 testpackage2/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
146 146 [1]
147 147
148 148 $ cd "$TESTDIR"/..
149 149
150 150 There are a handful of cases here that require renaming a module so it
151 151 doesn't overlap with a stdlib module name. There are also some cycles
152 152 here that we should still endeavor to fix, and some cycles will be
153 153 hidden by deduplication algorithm in the cycle detector, so fixing
154 154 these may expose other cycles.
155 155
156 156 Known-bad files are excluded by -X as some of them would produce unstable
157 157 outputs, which should be fixed later.
158 158
159 159 $ hg locate 'set:**.py or grep(r"^#!.*?python")' \
160 160 > 'tests/**.t' \
161 161 > -X contrib/debugshell.py \
162 162 > -X contrib/win32/hgwebdir_wsgi.py \
163 163 > -X doc/gendoc.py \
164 164 > -X doc/hgmanpage.py \
165 165 > -X i18n/posplit \
166 166 > -X tests/test-hgweb-auth.py \
167 167 > -X tests/hypothesishelpers.py \
168 168 > -X tests/test-ctxmanager.py \
169 169 > -X tests/test-lock.py \
170 170 > -X tests/test-verify-repo-operations.py \
171 171 > -X tests/test-hook.t \
172 172 > -X tests/test-import.t \
173 173 > -X tests/test-check-module-imports.t \
174 174 > -X tests/test-commit-interactive.t \
175 175 > -X tests/test-contrib-check-code.t \
176 176 > -X tests/test-extension.t \
177 177 > -X tests/test-hghave.t \
178 178 > -X tests/test-hgweb-no-path-info.t \
179 179 > -X tests/test-hgweb-no-request-uri.t \
180 180 > -X tests/test-hgweb-non-interactive.t \
181 181 > | sed 's-\\-/-g' | python "$import_checker" -
182 Import cycle: hgext.largefiles.basestore -> hgext.largefiles.localstore -> hgext.largefiles.basestore
183 [1]
General Comments 0
You need to be logged in to leave comments. Login now