##// END OF EJS Templates
hgext: add largefiles extension...
various -
r15168:cfccd3be default
parent child Browse files
Show More
@@ -0,0 +1,4 b''
1 Greg Ward, author of the original bfiles extension
2 Na'Tosha Bard of Unity Technologies
3 Fog Creek Software
4 Special thanks to the University of Toronto and the UCOSP program
@@ -0,0 +1,40 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''track large binary files
10
11 Large binary files tend to be not very compressible, not very "diffable", and
12 not at all mergeable. Such files are not handled well by Mercurial\'s storage
13 format (revlog), which is based on compressed binary deltas. largefiles solves
14 this problem by adding a centralized client-server layer on top of Mercurial:
15 largefiles live in a *central store* out on the network somewhere, and you only
16 fetch the ones that you need when you need them.
17
18 largefiles works by maintaining a *standin* in .hglf/ for each largefile. The
19 standins are small (41 bytes: an SHA-1 hash plus newline) and are tracked by
20 Mercurial. Largefile revisions are identified by the SHA-1 hash of their
21 contents, which is written to the standin. largefiles uses that revision ID to
22 get/put largefile revisions from/to the central store.
23
24 A complete tutorial for using lfiles is included in ``usage.txt`` in the lfiles
25 source distribution. See
26 https://developers.kilnhg.com/Repo/Kiln/largefiles/largefiles/File/usage.txt
27 '''
28
29 from mercurial import commands
30
31 import lfcommands
32 import reposetup
33 import uisetup
34
35 reposetup = reposetup.reposetup
36 uisetup = uisetup.uisetup
37
38 commands.norepo += " lfconvert"
39
40 cmdtable = lfcommands.cmdtable
@@ -0,0 +1,201 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''Base class for store implementations and store-related utility code.'''
10
11 import os
12 import tempfile
13 import binascii
14 import re
15
16 from mercurial import util, node, hg
17 from mercurial.i18n import _
18
19 import lfutil
20
21 class StoreError(Exception):
22 '''Raised when there is a problem getting files from or putting
23 files to a central store.'''
24 def __init__(self, filename, hash, url, detail):
25 self.filename = filename
26 self.hash = hash
27 self.url = url
28 self.detail = detail
29
30 def longmessage(self):
31 if self.url:
32 return ('%s: %s\n'
33 '(failed URL: %s)\n'
34 % (self.filename, self.detail, self.url))
35 else:
36 return ('%s: %s\n'
37 '(no default or default-push path set in hgrc)\n'
38 % (self.filename, self.detail))
39
40 def __str__(self):
41 return "%s: %s" % (self.url, self.detail)
42
43 class basestore(object):
44 def __init__(self, ui, repo, url):
45 self.ui = ui
46 self.repo = repo
47 self.url = url
48
49 def put(self, source, hash):
50 '''Put source file into the store under <filename>/<hash>.'''
51 raise NotImplementedError('abstract method')
52
53 def exists(self, hash):
54 '''Check to see if the store contains the given hash.'''
55 raise NotImplementedError('abstract method')
56
57 def get(self, files):
58 '''Get the specified largefiles from the store and write to local
59 files under repo.root. files is a list of (filename, hash)
60 tuples. Return (success, missing), lists of files successfuly
61 downloaded and those not found in the store. success is a list
62 of (filename, hash) tuples; missing is a list of filenames that
63 we could not get. (The detailed error message will already have
64 been presented to the user, so missing is just supplied as a
65 summary.)'''
66 success = []
67 missing = []
68 ui = self.ui
69
70 at = 0
71 for filename, hash in files:
72 ui.progress(_('getting largefiles'), at, unit='lfile',
73 total=len(files))
74 at += 1
75 ui.note(_('getting %s:%s\n') % (filename, hash))
76
77 cachefilename = lfutil.cachepath(self.repo, hash)
78 cachedir = os.path.dirname(cachefilename)
79
80 # No need to pass mode='wb' to fdopen(), since mkstemp() already
81 # opened the file in binary mode.
82 (tmpfd, tmpfilename) = tempfile.mkstemp(
83 dir=cachedir, prefix=os.path.basename(filename))
84 tmpfile = os.fdopen(tmpfd, 'w')
85
86 try:
87 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
88 except StoreError, err:
89 ui.warn(err.longmessage())
90 hhash = ""
91
92 if hhash != hash:
93 if hhash != "":
94 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
95 % (filename, hash, hhash))
96 tmpfile.close() # no-op if it's already closed
97 os.remove(tmpfilename)
98 missing.append(filename)
99 continue
100
101 if os.path.exists(cachefilename): # Windows
102 os.remove(cachefilename)
103 os.rename(tmpfilename, cachefilename)
104 lfutil.linktosystemcache(self.repo, hash)
105 success.append((filename, hhash))
106
107 ui.progress(_('getting largefiles'), None)
108 return (success, missing)
109
110 def verify(self, revs, contents=False):
111 '''Verify the existence (and, optionally, contents) of every big
112 file revision referenced by every changeset in revs.
113 Return 0 if all is well, non-zero on any errors.'''
114 write = self.ui.write
115 failed = False
116
117 write(_('searching %d changesets for largefiles\n') % len(revs))
118 verified = set() # set of (filename, filenode) tuples
119
120 for rev in revs:
121 cctx = self.repo[rev]
122 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
123
124 failed = lfutil.any_(self._verifyfile(
125 cctx, cset, contents, standin, verified) for standin in cctx)
126
127 num_revs = len(verified)
128 num_lfiles = len(set([fname for (fname, fnode) in verified]))
129 if contents:
130 write(_('verified contents of %d revisions of %d largefiles\n')
131 % (num_revs, num_lfiles))
132 else:
133 write(_('verified existence of %d revisions of %d largefiles\n')
134 % (num_revs, num_lfiles))
135
136 return int(failed)
137
138 def _getfile(self, tmpfile, filename, hash):
139 '''Fetch one revision of one file from the store and write it
140 to tmpfile. Compute the hash of the file on-the-fly as it
141 downloads and return the binary hash. Close tmpfile. Raise
142 StoreError if unable to download the file (e.g. it does not
143 exist in the store).'''
144 raise NotImplementedError('abstract method')
145
146 def _verifyfile(self, cctx, cset, contents, standin, verified):
147 '''Perform the actual verification of a file in the store.
148 '''
149 raise NotImplementedError('abstract method')
150
151 import localstore, wirestore
152
153 _storeprovider = {
154 'file': [localstore.localstore],
155 'http': [wirestore.wirestore],
156 'https': [wirestore.wirestore],
157 'ssh': [wirestore.wirestore],
158 }
159
160 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
161
162 # During clone this function is passed the src's ui object
163 # but it needs the dest's ui object so it can read out of
164 # the config file. Use repo.ui instead.
165 def _openstore(repo, remote=None, put=False):
166 ui = repo.ui
167
168 if not remote:
169 path = getattr(repo, 'lfpullsource', None) or \
170 ui.expandpath('default-push', 'default')
171 # If 'default-push' and 'default' can't be expanded
172 # they are just returned. In that case use the empty string which
173 # use the filescheme.
174 if path == 'default-push' or path == 'default':
175 path = ''
176 remote = repo
177 else:
178 remote = hg.peer(repo, {}, path)
179
180 # The path could be a scheme so use Mercurial's normal functionality
181 # to resolve the scheme to a repository and use its path
182 path = hasattr(remote, 'url') and remote.url() or remote.path
183
184 match = _scheme_re.match(path)
185 if not match: # regular filesystem path
186 scheme = 'file'
187 else:
188 scheme = match.group(1)
189
190 try:
191 storeproviders = _storeprovider[scheme]
192 except KeyError:
193 raise util.Abort(_('unsupported URL scheme %r') % scheme)
194
195 for class_obj in storeproviders:
196 try:
197 return class_obj(ui, repo, remote)
198 except lfutil.storeprotonotcapable:
199 pass
200
201 raise util.Abort(_('%s does not appear to be a lfile store'), path)
@@ -0,0 +1,49 b''
1 = largefiles - manage large binary files =
2 This extension is based off of Greg Ward's bfiles extension which can be found
3 at http://mercurial.selenic.com/wiki/BfilesExtension.
4
5 == The largefile store ==
6
7 largefile stores are, in the typical use case, centralized servers that have
8 every past revision of a given binary file. Each largefile is identified by
9 its sha1 hash, and all interactions with the store take one of the following
10 forms.
11
12 -Download a bfile with this hash
13 -Upload a bfile with this hash
14 -Check if the store has a bfile with this hash
15
16 largefiles stores can take one of two forms:
17
18 -Directories on a network file share
19 -Mercurial wireproto servers, either via ssh or http (hgweb)
20
21 == The Local Repository ==
22
23 The local repository has a largefile cache in .hg/largefiles which holds a
24 subset of the largefiles needed. On a clone only the largefiles at tip are
25 downloaded. When largefiles are downloaded from the central store, a copy is
26 saved in this store.
27
28 == The Global Cache ==
29
30 largefiles in a local repository cache are hardlinked to files in the global
31 cache. Before a file is downloaded we check if it is in the global cache.
32
33 == Implementation Details ==
34
35 Each largefile has a standin which is in .hglf. The standin is tracked by
36 Mercurial. The standin contains the SHA1 hash of the largefile. When a
37 largefile is added/removed/copied/renamed/etc the same operation is applied to
38 the standin. Thus the history of the standin is the history of the largefile.
39
40 For performance reasons, the contents of a standin are only updated before a
41 commit. Standins are added/removed/copied/renamed from add/remove/copy/rename
42 Mercurial commands but their contents will not be updated. The contents of a
43 standin will always be the hash of the largefile as of the last commit. To
44 support some commands (revert) some standins are temporarily updated but will
45 be changed back after the command is finished.
46
47 A Mercurial dirstate object tracks the state of the largefiles. The dirstate
48 uses the last modified time and current size to detect if a file has changed
49 (without reading the entire contents of the file).
@@ -0,0 +1,483 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''High-level command functions: lfadd() et. al, plus the cmdtable.'''
10
11 import os
12 import shutil
13
14 from mercurial import util, match as match_, hg, node, context, error
15 from mercurial.i18n import _
16
17 import lfutil
18 import basestore
19
20 # -- Commands ----------------------------------------------------------
21
22 def lfconvert(ui, src, dest, *pats, **opts):
23 '''Convert a normal repository to a largefiles repository
24
25 Convert source repository creating an identical repository, except that all
26 files that match the patterns given, or are over the given size will be
27 added as largefiles. The size used to determine whether or not to track a
28 file as a largefile is the size of the first version of the file. After
29 running this command you will need to make sure that largefiles is enabled
30 anywhere you intend to push the new repository.'''
31
32 if opts['tonormal']:
33 tolfile = False
34 else:
35 tolfile = True
36 size = opts['size']
37 if not size:
38 size = ui.config(lfutil.longname, 'size', default=None)
39 try:
40 size = int(size)
41 except ValueError:
42 raise util.Abort(_('largefiles.size must be integer, was %s\n') % \
43 size)
44 except TypeError:
45 raise util.Abort(_('size must be specified'))
46
47 try:
48 rsrc = hg.repository(ui, src)
49 if not rsrc.local():
50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
51 except error.RepoError, err:
52 ui.traceback()
53 raise util.Abort(err.args[0])
54 if os.path.exists(dest):
55 if not os.path.isdir(dest):
56 raise util.Abort(_('destination %s already exists') % dest)
57 elif os.listdir(dest):
58 raise util.Abort(_('destination %s is not empty') % dest)
59 try:
60 ui.status(_('initializing destination %s\n') % dest)
61 rdst = hg.repository(ui, dest, create=True)
62 if not rdst.local():
63 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
64 except error.RepoError:
65 ui.traceback()
66 raise util.Abort(_('%s is not a repo') % dest)
67
68 try:
69 # Lock destination to prevent modification while it is converted to.
70 # Don't need to lock src because we are just reading from its history
71 # which can't change.
72 dst_lock = rdst.lock()
73
74 # Get a list of all changesets in the source. The easy way to do this
75 # is to simply walk the changelog, using changelog.nodesbewteen().
76 # Take a look at mercurial/revlog.py:639 for more details.
77 # Use a generator instead of a list to decrease memory usage
78 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
79 rsrc.heads())[0])
80 revmap = {node.nullid: node.nullid}
81 if tolfile:
82 lfiles = set()
83 normalfiles = set()
84 if not pats:
85 pats = ui.config(lfutil.longname, 'patterns', default=())
86 if pats:
87 pats = pats.split(' ')
88 if pats:
89 matcher = match_.match(rsrc.root, '', list(pats))
90 else:
91 matcher = None
92
93 lfiletohash = {}
94 for ctx in ctxs:
95 ui.progress(_('converting revisions'), ctx.rev(),
96 unit=_('revision'), total=rsrc['tip'].rev())
97 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
98 lfiles, normalfiles, matcher, size, lfiletohash)
99 ui.progress(_('converting revisions'), None)
100
101 if os.path.exists(rdst.wjoin(lfutil.shortname)):
102 shutil.rmtree(rdst.wjoin(lfutil.shortname))
103
104 for f in lfiletohash.keys():
105 if os.path.isfile(rdst.wjoin(f)):
106 os.unlink(rdst.wjoin(f))
107 try:
108 os.removedirs(os.path.dirname(rdst.wjoin(f)))
109 except:
110 pass
111
112 else:
113 for ctx in ctxs:
114 ui.progress(_('converting revisions'), ctx.rev(),
115 unit=_('revision'), total=rsrc['tip'].rev())
116 _addchangeset(ui, rsrc, rdst, ctx, revmap)
117
118 ui.progress(_('converting revisions'), None)
119 except:
120 # we failed, remove the new directory
121 shutil.rmtree(rdst.root)
122 raise
123 finally:
124 dst_lock.release()
125
126 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
127 # Convert src parents to dst parents
128 parents = []
129 for p in ctx.parents():
130 parents.append(revmap[p.node()])
131 while len(parents) < 2:
132 parents.append(node.nullid)
133
134 # Generate list of changed files
135 files = set(ctx.files())
136 if node.nullid not in parents:
137 mc = ctx.manifest()
138 mp1 = ctx.parents()[0].manifest()
139 mp2 = ctx.parents()[1].manifest()
140 files |= (set(mp1) | set(mp2)) - set(mc)
141 for f in mc:
142 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
143 files.add(f)
144
145 def getfilectx(repo, memctx, f):
146 if lfutil.standin(f) in files:
147 # if the file isn't in the manifest then it was removed
148 # or renamed, raise IOError to indicate this
149 try:
150 fctx = ctx.filectx(lfutil.standin(f))
151 except error.LookupError:
152 raise IOError()
153 renamed = fctx.renamed()
154 if renamed:
155 renamed = lfutil.splitstandin(renamed[0])
156
157 hash = fctx.data().strip()
158 path = lfutil.findfile(rsrc, hash)
159 ### TODO: What if the file is not cached?
160 data = ''
161 fd = None
162 try:
163 fd = open(path, 'rb')
164 data = fd.read()
165 finally:
166 if fd: fd.close()
167 return context.memfilectx(f, data, 'l' in fctx.flags(),
168 'x' in fctx.flags(), renamed)
169 else:
170 try:
171 fctx = ctx.filectx(f)
172 except error.LookupError:
173 raise IOError()
174 renamed = fctx.renamed()
175 if renamed:
176 renamed = renamed[0]
177 data = fctx.data()
178 if f == '.hgtags':
179 newdata = []
180 for line in data.splitlines():
181 id, name = line.split(' ', 1)
182 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
183 name))
184 data = ''.join(newdata)
185 return context.memfilectx(f, data, 'l' in fctx.flags(),
186 'x' in fctx.flags(), renamed)
187
188 dstfiles = []
189 for file in files:
190 if lfutil.isstandin(file):
191 dstfiles.append(lfutil.splitstandin(file))
192 else:
193 dstfiles.append(file)
194 # Commit
195 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
196 getfilectx, ctx.user(), ctx.date(), ctx.extra())
197 ret = rdst.commitctx(mctx)
198 rdst.dirstate.setparents(ret)
199 revmap[ctx.node()] = rdst.changelog.tip()
200
201 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
202 matcher, size, lfiletohash):
203 # Convert src parents to dst parents
204 parents = []
205 for p in ctx.parents():
206 parents.append(revmap[p.node()])
207 while len(parents) < 2:
208 parents.append(node.nullid)
209
210 # Generate list of changed files
211 files = set(ctx.files())
212 if node.nullid not in parents:
213 mc = ctx.manifest()
214 mp1 = ctx.parents()[0].manifest()
215 mp2 = ctx.parents()[1].manifest()
216 files |= (set(mp1) | set(mp2)) - set(mc)
217 for f in mc:
218 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
219 files.add(f)
220
221 dstfiles = []
222 for f in files:
223 if f not in lfiles and f not in normalfiles:
224 islfile = _islfile(f, ctx, matcher, size)
225 # If this file was renamed or copied then copy
226 # the lfileness of its predecessor
227 if f in ctx.manifest():
228 fctx = ctx.filectx(f)
229 renamed = fctx.renamed()
230 renamedlfile = renamed and renamed[0] in lfiles
231 islfile |= renamedlfile
232 if 'l' in fctx.flags():
233 if renamedlfile:
234 raise util.Abort(
235 _('Renamed/copied largefile %s becomes symlink') % f)
236 islfile = False
237 if islfile:
238 lfiles.add(f)
239 else:
240 normalfiles.add(f)
241
242 if f in lfiles:
243 dstfiles.append(lfutil.standin(f))
244 # lfile in manifest if it has not been removed/renamed
245 if f in ctx.manifest():
246 if 'l' in ctx.filectx(f).flags():
247 if renamed and renamed[0] in lfiles:
248 raise util.Abort(_('largefile %s becomes symlink') % f)
249
250 # lfile was modified, update standins
251 fullpath = rdst.wjoin(f)
252 lfutil.createdir(os.path.dirname(fullpath))
253 m = util.sha1('')
254 m.update(ctx[f].data())
255 hash = m.hexdigest()
256 if f not in lfiletohash or lfiletohash[f] != hash:
257 try:
258 fd = open(fullpath, 'wb')
259 fd.write(ctx[f].data())
260 finally:
261 if fd:
262 fd.close()
263 executable = 'x' in ctx[f].flags()
264 os.chmod(fullpath, lfutil.getmode(executable))
265 lfutil.writestandin(rdst, lfutil.standin(f), hash,
266 executable)
267 lfiletohash[f] = hash
268 else:
269 # normal file
270 dstfiles.append(f)
271
272 def getfilectx(repo, memctx, f):
273 if lfutil.isstandin(f):
274 # if the file isn't in the manifest then it was removed
275 # or renamed, raise IOError to indicate this
276 srcfname = lfutil.splitstandin(f)
277 try:
278 fctx = ctx.filectx(srcfname)
279 except error.LookupError:
280 raise IOError()
281 renamed = fctx.renamed()
282 if renamed:
283 # standin is always a lfile because lfileness
284 # doesn't change after rename or copy
285 renamed = lfutil.standin(renamed[0])
286
287 return context.memfilectx(f, lfiletohash[srcfname], 'l' in
288 fctx.flags(), 'x' in fctx.flags(), renamed)
289 else:
290 try:
291 fctx = ctx.filectx(f)
292 except error.LookupError:
293 raise IOError()
294 renamed = fctx.renamed()
295 if renamed:
296 renamed = renamed[0]
297
298 data = fctx.data()
299 if f == '.hgtags':
300 newdata = []
301 for line in data.splitlines():
302 id, name = line.split(' ', 1)
303 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
304 name))
305 data = ''.join(newdata)
306 return context.memfilectx(f, data, 'l' in fctx.flags(),
307 'x' in fctx.flags(), renamed)
308
309 # Commit
310 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
311 getfilectx, ctx.user(), ctx.date(), ctx.extra())
312 ret = rdst.commitctx(mctx)
313 rdst.dirstate.setparents(ret)
314 revmap[ctx.node()] = rdst.changelog.tip()
315
316 def _islfile(file, ctx, matcher, size):
317 '''
318 A file is a lfile if it matches a pattern or is over
319 the given size.
320 '''
321 # Never store hgtags or hgignore as lfiles
322 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
323 return False
324 if matcher and matcher(file):
325 return True
326 try:
327 return ctx.filectx(file).size() >= size * 1024 * 1024
328 except error.LookupError:
329 return False
330
331 def uploadlfiles(ui, rsrc, rdst, files):
332 '''upload largefiles to the central store'''
333
334 # Don't upload locally. All largefiles are in the system wide cache
335 # so the other repo can just get them from there.
336 if not files or rdst.local():
337 return
338
339 store = basestore._openstore(rsrc, rdst, put=True)
340
341 at = 0
342 files = filter(lambda h: not store.exists(h), files)
343 for hash in files:
344 ui.progress(_('uploading largefiles'), at, unit='largefile', total=len(files))
345 source = lfutil.findfile(rsrc, hash)
346 if not source:
347 raise util.Abort(_('Missing largefile %s needs to be uploaded') % hash)
348 # XXX check for errors here
349 store.put(source, hash)
350 at += 1
351 ui.progress('uploading largefiles', None)
352
353 def verifylfiles(ui, repo, all=False, contents=False):
354 '''Verify that every big file revision in the current changeset
355 exists in the central store. With --contents, also verify that
356 the contents of each big file revision are correct (SHA-1 hash
357 matches the revision ID). With --all, check every changeset in
358 this repository.'''
359 if all:
360 # Pass a list to the function rather than an iterator because we know a
361 # list will work.
362 revs = range(len(repo))
363 else:
364 revs = ['.']
365
366 store = basestore._openstore(repo)
367 return store.verify(revs, contents=contents)
368
369 def cachelfiles(ui, repo, node):
370 '''cachelfiles ensures that all largefiles needed by the specified revision
371 are present in the repository's largefile cache.
372
373 returns a tuple (cached, missing). cached is the list of files downloaded
374 by this operation; missing is the list of files that were needed but could
375 not be found.'''
376 lfiles = lfutil.listlfiles(repo, node)
377 toget = []
378
379 for lfile in lfiles:
380 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
381 # if it exists and its hash matches, it might have been locally
382 # modified before updating and the user chose 'local'. in this case,
383 # it will not be in any store, so don't look for it.
384 if (not os.path.exists(repo.wjoin(lfile)) \
385 or expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and \
386 not lfutil.findfile(repo, expectedhash):
387 toget.append((lfile, expectedhash))
388
389 if toget:
390 store = basestore._openstore(repo)
391 ret = store.get(toget)
392 return ret
393
394 return ([], [])
395
396 def updatelfiles(ui, repo, filelist=None, printmessage=True):
397 wlock = repo.wlock()
398 try:
399 lfdirstate = lfutil.openlfdirstate(ui, repo)
400 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
401
402 if filelist is not None:
403 lfiles = [f for f in lfiles if f in filelist]
404
405 printed = False
406 if printmessage and lfiles:
407 ui.status(_('getting changed largefiles\n'))
408 printed = True
409 cachelfiles(ui, repo, '.')
410
411 updated, removed = 0, 0
412 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
413 # increment the appropriate counter according to _updatelfile's
414 # return value
415 updated += i > 0 and i or 0
416 removed -= i < 0 and i or 0
417 if printmessage and (removed or updated) and not printed:
418 ui.status(_('getting changed largefiles\n'))
419 printed = True
420
421 lfdirstate.write()
422 if printed and printmessage:
423 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
424 removed))
425 finally:
426 wlock.release()
427
428 def _updatelfile(repo, lfdirstate, lfile):
429 '''updates a single largefile and copies the state of its standin from
430 the repository's dirstate to its state in the lfdirstate.
431
432 returns 1 if the file was modified, -1 if the file was removed, 0 if the
433 file was unchanged, and None if the needed largefile was missing from the
434 cache.'''
435 ret = 0
436 abslfile = repo.wjoin(lfile)
437 absstandin = repo.wjoin(lfutil.standin(lfile))
438 if os.path.exists(absstandin):
439 if os.path.exists(absstandin+'.orig'):
440 shutil.copyfile(abslfile, abslfile+'.orig')
441 expecthash = lfutil.readstandin(repo, lfile)
442 if expecthash != '' and \
443 (not os.path.exists(abslfile) or \
444 expecthash != lfutil.hashfile(abslfile)):
445 if not lfutil.copyfromcache(repo, expecthash, lfile):
446 return None # don't try to set the mode or update the dirstate
447 ret = 1
448 mode = os.stat(absstandin).st_mode
449 if mode != os.stat(abslfile).st_mode:
450 os.chmod(abslfile, mode)
451 ret = 1
452 else:
453 if os.path.exists(abslfile):
454 os.unlink(abslfile)
455 ret = -1
456 state = repo.dirstate[lfutil.standin(lfile)]
457 if state == 'n':
458 lfdirstate.normal(lfile)
459 elif state == 'r':
460 lfdirstate.remove(lfile)
461 elif state == 'a':
462 lfdirstate.add(lfile)
463 elif state == '?':
464 try:
465 # Mercurial >= 1.9
466 lfdirstate.drop(lfile)
467 except AttributeError:
468 # Mercurial <= 1.8
469 lfdirstate.forget(lfile)
470 return ret
471
472 # -- hg commands declarations ------------------------------------------------
473
474
475 cmdtable = {
476 'lfconvert': (lfconvert,
477 [('s', 'size', 0, 'All files over this size (in megabytes) '
478 'will be considered largefiles. This can also be specified in '
479 'your hgrc as [largefiles].size.'),
480 ('','tonormal',False,
481 'Convert from a largefiles repo to a normal repo')],
482 _('hg lfconvert SOURCE DEST [FILE ...]')),
483 }
This diff has been collapsed as it changes many lines, (502 lines changed) Show them Hide them
@@ -0,0 +1,502 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''largefiles utility code: must not import other modules in this package.'''
10
11 import os
12 import errno
13 import inspect
14 import shutil
15 import stat
16 import hashlib
17
18 from mercurial import cmdutil, dirstate, httpconnection, match as match_, \
19 url as url_, util
20 from mercurial.i18n import _
21
22 try:
23 from mercurial import scmutil
24 except ImportError:
25 pass
26
27 shortname = '.hglf'
28 longname = 'largefiles'
29
30
31 # -- Portability wrappers ----------------------------------------------
32
33 if 'subrepos' in inspect.getargspec(dirstate.dirstate.status)[0]:
34 # for Mercurial >= 1.5
35 def dirstate_walk(dirstate, matcher, unknown=False, ignored=False):
36 return dirstate.walk(matcher, [], unknown, ignored)
37 else:
38 # for Mercurial <= 1.4
39 def dirstate_walk(dirstate, matcher, unknown=False, ignored=False):
40 return dirstate.walk(matcher, unknown, ignored)
41
42 def repo_add(repo, list):
43 try:
44 # Mercurial <= 1.5
45 add = repo.add
46 except AttributeError:
47 # Mercurial >= 1.6
48 add = repo[None].add
49 return add(list)
50
51 def repo_remove(repo, list, unlink=False):
52 try:
53 # Mercurial <= 1.5
54 remove = repo.remove
55 except AttributeError:
56 # Mercurial >= 1.6
57 try:
58 # Mercurial <= 1.8
59 remove = repo[None].remove
60 except AttributeError:
61 # Mercurial >= 1.9
62 def remove(list, unlink):
63 wlock = repo.wlock()
64 try:
65 if unlink:
66 for f in list:
67 try:
68 util.unlinkpath(repo.wjoin(f))
69 except OSError, inst:
70 if inst.errno != errno.ENOENT:
71 raise
72 repo[None].forget(list)
73 finally:
74 wlock.release()
75
76 return remove(list, unlink=unlink)
77
78 def repo_forget(repo, list):
79 try:
80 # Mercurial <= 1.5
81 forget = repo.forget
82 except AttributeError:
83 # Mercurial >= 1.6
84 forget = repo[None].forget
85 return forget(list)
86
87 def findoutgoing(repo, remote, force):
88 # First attempt is for Mercurial <= 1.5 second is for >= 1.6
89 try:
90 return repo.findoutgoing(remote)
91 except AttributeError:
92 from mercurial import discovery
93 try:
94 # Mercurial <= 1.8
95 return discovery.findoutgoing(repo, remote, force=force)
96 except AttributeError:
97 # Mercurial >= 1.9
98 common, _anyinc, _heads = discovery.findcommonincoming(repo,
99 remote, force=force)
100 return repo.changelog.findmissing(common)
101
102 # -- Private worker functions ------------------------------------------
103
104 if os.name == 'nt':
105 from mercurial import win32
106 try:
107 linkfn = win32.oslink
108 except:
109 linkfn = win32.os_link
110 else:
111 linkfn = os.link
112
113 def link(src, dest):
114 try:
115 linkfn(src, dest)
116 except OSError:
117 # If hardlinks fail fall back on copy
118 shutil.copyfile(src, dest)
119 os.chmod(dest, os.stat(src).st_mode)
120
121 def systemcachepath(ui, hash):
122 path = ui.config(longname, 'systemcache', None)
123 if path:
124 path = os.path.join(path, hash)
125 else:
126 if os.name == 'nt':
127 path = os.path.join(os.getenv('LOCALAPPDATA') or \
128 os.getenv('APPDATA'), longname, hash)
129 elif os.name == 'posix':
130 path = os.path.join(os.getenv('HOME'), '.' + longname, hash)
131 else:
132 raise util.Abort(_('Unknown operating system: %s\n') % os.name)
133 return path
134
135 def insystemcache(ui, hash):
136 return os.path.exists(systemcachepath(ui, hash))
137
138 def findfile(repo, hash):
139 if incache(repo, hash):
140 repo.ui.note(_('Found %s in cache\n') % hash)
141 return cachepath(repo, hash)
142 if insystemcache(repo.ui, hash):
143 repo.ui.note(_('Found %s in system cache\n') % hash)
144 return systemcachepath(repo.ui, hash)
145 return None
146
147 class largefiles_dirstate(dirstate.dirstate):
148 def __getitem__(self, key):
149 return super(largefiles_dirstate, self).__getitem__(unixpath(key))
150 def normal(self, f):
151 return super(largefiles_dirstate, self).normal(unixpath(f))
152 def remove(self, f):
153 return super(largefiles_dirstate, self).remove(unixpath(f))
154 def add(self, f):
155 return super(largefiles_dirstate, self).add(unixpath(f))
156 def drop(self, f):
157 return super(largefiles_dirstate, self).drop(unixpath(f))
158 def forget(self, f):
159 return super(largefiles_dirstate, self).forget(unixpath(f))
160
161 def openlfdirstate(ui, repo):
162 '''
163 Return a dirstate object that tracks big files: i.e. its root is the
164 repo root, but it is saved in .hg/largefiles/dirstate.
165 '''
166 admin = repo.join(longname)
167 try:
168 # Mercurial >= 1.9
169 opener = scmutil.opener(admin)
170 except ImportError:
171 # Mercurial <= 1.8
172 opener = util.opener(admin)
173 if hasattr(repo.dirstate, '_validate'):
174 lfdirstate = largefiles_dirstate(opener, ui, repo.root,
175 repo.dirstate._validate)
176 else:
177 lfdirstate = largefiles_dirstate(opener, ui, repo.root)
178
179 # If the largefiles dirstate does not exist, populate and create it. This
180 # ensures that we create it on the first meaningful largefiles operation in
181 # a new clone. It also gives us an easy way to forcibly rebuild largefiles
182 # state:
183 # rm .hg/largefiles/dirstate && hg status
184 # Or even, if things are really messed up:
185 # rm -rf .hg/largefiles && hg status
186 if not os.path.exists(os.path.join(admin, 'dirstate')):
187 util.makedirs(admin)
188 matcher = getstandinmatcher(repo)
189 for standin in dirstate_walk(repo.dirstate, matcher):
190 lfile = splitstandin(standin)
191 hash = readstandin(repo, lfile)
192 lfdirstate.normallookup(lfile)
193 try:
194 if hash == hashfile(lfile):
195 lfdirstate.normal(lfile)
196 except IOError, err:
197 if err.errno != errno.ENOENT:
198 raise
199
200 lfdirstate.write()
201
202 return lfdirstate
203
204 def lfdirstate_status(lfdirstate, repo, rev):
205 wlock = repo.wlock()
206 try:
207 match = match_.always(repo.root, repo.getcwd())
208 s = lfdirstate.status(match, [], False, False, False)
209 unsure, modified, added, removed, missing, unknown, ignored, clean = s
210 for lfile in unsure:
211 if repo[rev][standin(lfile)].data().strip() != \
212 hashfile(repo.wjoin(lfile)):
213 modified.append(lfile)
214 else:
215 clean.append(lfile)
216 lfdirstate.normal(lfile)
217 lfdirstate.write()
218 finally:
219 wlock.release()
220 return (modified, added, removed, missing, unknown, ignored, clean)
221
222 def listlfiles(repo, rev=None, matcher=None):
223 '''list largefiles in the working copy or specified changeset'''
224
225 if matcher is None:
226 matcher = getstandinmatcher(repo)
227
228 # ignore unknown files in working directory
229 return [splitstandin(f) for f in repo[rev].walk(matcher) \
230 if rev is not None or repo.dirstate[f] != '?']
231
232 def incache(repo, hash):
233 return os.path.exists(cachepath(repo, hash))
234
235 def createdir(dir):
236 if not os.path.exists(dir):
237 os.makedirs(dir)
238
239 def cachepath(repo, hash):
240 return repo.join(os.path.join(longname, hash))
241
242 def copyfromcache(repo, hash, filename):
243 '''copyfromcache copies the specified largefile from the repo or system
244 cache to the specified location in the repository. It will not throw an
245 exception on failure, as it is meant to be called only after ensuring that
246 the needed largefile exists in the cache.'''
247 path = findfile(repo, hash)
248 if path is None:
249 return False
250 util.makedirs(os.path.dirname(repo.wjoin(filename)))
251 shutil.copy(path, repo.wjoin(filename))
252 return True
253
254 def copytocache(repo, rev, file, uploaded=False):
255 hash = readstandin(repo, file)
256 if incache(repo, hash):
257 return
258 copytocacheabsolute(repo, repo.wjoin(file), hash)
259
260 def copytocacheabsolute(repo, file, hash):
261 createdir(os.path.dirname(cachepath(repo, hash)))
262 if insystemcache(repo.ui, hash):
263 link(systemcachepath(repo.ui, hash), cachepath(repo, hash))
264 else:
265 shutil.copyfile(file, cachepath(repo, hash))
266 os.chmod(cachepath(repo, hash), os.stat(file).st_mode)
267 linktosystemcache(repo, hash)
268
269 def linktosystemcache(repo, hash):
270 createdir(os.path.dirname(systemcachepath(repo.ui, hash)))
271 link(cachepath(repo, hash), systemcachepath(repo.ui, hash))
272
273 def getstandinmatcher(repo, pats=[], opts={}):
274 '''Return a match object that applies pats to the standin directory'''
275 standindir = repo.pathto(shortname)
276 if pats:
277 # patterns supplied: search standin directory relative to current dir
278 cwd = repo.getcwd()
279 if os.path.isabs(cwd):
280 # cwd is an absolute path for hg -R <reponame>
281 # work relative to the repository root in this case
282 cwd = ''
283 pats = [os.path.join(standindir, cwd, pat) for pat in pats]
284 elif os.path.isdir(standindir):
285 # no patterns: relative to repo root
286 pats = [standindir]
287 else:
288 # no patterns and no standin dir: return matcher that matches nothing
289 match = match_.match(repo.root, None, [], exact=True)
290 match.matchfn = lambda f: False
291 return match
292 return getmatcher(repo, pats, opts, showbad=False)
293
294 def getmatcher(repo, pats=[], opts={}, showbad=True):
295 '''Wrapper around scmutil.match() that adds showbad: if false, neuter
296 the match object\'s bad() method so it does not print any warnings
297 about missing files or directories.'''
298 try:
299 # Mercurial >= 1.9
300 match = scmutil.match(repo[None], pats, opts)
301 except ImportError:
302 # Mercurial <= 1.8
303 match = cmdutil.match(repo, pats, opts)
304
305 if not showbad:
306 match.bad = lambda f, msg: None
307 return match
308
309 def composestandinmatcher(repo, rmatcher):
310 '''Return a matcher that accepts standins corresponding to the files
311 accepted by rmatcher. Pass the list of files in the matcher as the
312 paths specified by the user.'''
313 smatcher = getstandinmatcher(repo, rmatcher.files())
314 isstandin = smatcher.matchfn
315 def composed_matchfn(f):
316 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
317 smatcher.matchfn = composed_matchfn
318
319 return smatcher
320
321 def standin(filename):
322 '''Return the repo-relative path to the standin for the specified big
323 file.'''
324 # Notes:
325 # 1) Most callers want an absolute path, but _create_standin() needs
326 # it repo-relative so lfadd() can pass it to repo_add(). So leave
327 # it up to the caller to use repo.wjoin() to get an absolute path.
328 # 2) Join with '/' because that's what dirstate always uses, even on
329 # Windows. Change existing separator to '/' first in case we are
330 # passed filenames from an external source (like the command line).
331 return shortname + '/' + filename.replace(os.sep, '/')
332
333 def isstandin(filename):
334 '''Return true if filename is a big file standin. filename must
335 be in Mercurial\'s internal form (slash-separated).'''
336 return filename.startswith(shortname + '/')
337
338 def splitstandin(filename):
339 # Split on / because that's what dirstate always uses, even on Windows.
340 # Change local separator to / first just in case we are passed filenames
341 # from an external source (like the command line).
342 bits = filename.replace(os.sep, '/').split('/', 1)
343 if len(bits) == 2 and bits[0] == shortname:
344 return bits[1]
345 else:
346 return None
347
348 def updatestandin(repo, standin):
349 file = repo.wjoin(splitstandin(standin))
350 if os.path.exists(file):
351 hash = hashfile(file)
352 executable = getexecutable(file)
353 writestandin(repo, standin, hash, executable)
354
355 def readstandin(repo, filename, node=None):
356 '''read hex hash from standin for filename at given node, or working
357 directory if no node is given'''
358 return repo[node][standin(filename)].data().strip()
359
360 def writestandin(repo, standin, hash, executable):
361 '''write hhash to <repo.root>/<standin>'''
362 writehash(hash, repo.wjoin(standin), executable)
363
364 def copyandhash(instream, outfile):
365 '''Read bytes from instream (iterable) and write them to outfile,
366 computing the SHA-1 hash of the data along the way. Close outfile
367 when done and return the binary hash.'''
368 hasher = util.sha1('')
369 for data in instream:
370 hasher.update(data)
371 outfile.write(data)
372
373 # Blecch: closing a file that somebody else opened is rude and
374 # wrong. But it's so darn convenient and practical! After all,
375 # outfile was opened just to copy and hash.
376 outfile.close()
377
378 return hasher.digest()
379
380 def hashrepofile(repo, file):
381 return hashfile(repo.wjoin(file))
382
383 def hashfile(file):
384 if not os.path.exists(file):
385 return ''
386 hasher = util.sha1('')
387 fd = open(file, 'rb')
388 for data in blockstream(fd):
389 hasher.update(data)
390 fd.close()
391 return hasher.hexdigest()
392
393 class limitreader(object):
394 def __init__(self, f, limit):
395 self.f = f
396 self.limit = limit
397
398 def read(self, length):
399 if self.limit == 0:
400 return ''
401 length = length > self.limit and self.limit or length
402 self.limit -= length
403 return self.f.read(length)
404
405 def close(self):
406 pass
407
408 def blockstream(infile, blocksize=128 * 1024):
409 """Generator that yields blocks of data from infile and closes infile."""
410 while True:
411 data = infile.read(blocksize)
412 if not data:
413 break
414 yield data
415 # Same blecch as above.
416 infile.close()
417
418 def readhash(filename):
419 rfile = open(filename, 'rb')
420 hash = rfile.read(40)
421 rfile.close()
422 if len(hash) < 40:
423 raise util.Abort(_('bad hash in \'%s\' (only %d bytes long)')
424 % (filename, len(hash)))
425 return hash
426
427 def writehash(hash, filename, executable):
428 util.makedirs(os.path.dirname(filename))
429 if os.path.exists(filename):
430 os.unlink(filename)
431 wfile = open(filename, 'wb')
432
433 try:
434 wfile.write(hash)
435 wfile.write('\n')
436 finally:
437 wfile.close()
438 if os.path.exists(filename):
439 os.chmod(filename, getmode(executable))
440
441 def getexecutable(filename):
442 mode = os.stat(filename).st_mode
443 return (mode & stat.S_IXUSR) and (mode & stat.S_IXGRP) and (mode & \
444 stat.S_IXOTH)
445
446 def getmode(executable):
447 if executable:
448 return 0755
449 else:
450 return 0644
451
452 def urljoin(first, second, *arg):
453 def join(left, right):
454 if not left.endswith('/'):
455 left += '/'
456 if right.startswith('/'):
457 right = right[1:]
458 return left + right
459
460 url = join(first, second)
461 for a in arg:
462 url = join(url, a)
463 return url
464
465 def hexsha1(data):
466 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
467 object data"""
468 h = hashlib.sha1()
469 for chunk in util.filechunkiter(data):
470 h.update(chunk)
471 return h.hexdigest()
472
473 def httpsendfile(ui, filename):
474 try:
475 # Mercurial >= 1.9
476 return httpconnection.httpsendfile(ui, filename, 'rb')
477 except ImportError:
478 if 'ui' in inspect.getargspec(url_.httpsendfile.__init__)[0]:
479 # Mercurial == 1.8
480 return url_.httpsendfile(ui, filename, 'rb')
481 else:
482 # Mercurial <= 1.7
483 return url_.httpsendfile(filename, 'rb')
484
485 # Convert a path to a unix style path. This is used to give a
486 # canonical path to the lfdirstate.
487 def unixpath(path):
488 return os.path.normpath(path).replace(os.sep, '/')
489
490 def islfilesrepo(repo):
491 return 'largefiles' in repo.requirements and any_(shortname+'/' in f[0] for f in
492 repo.store.datafiles())
493
494 def any_(gen):
495 for x in gen:
496 if x:
497 return True
498 return False
499
500 class storeprotonotcapable(BaseException):
501 def __init__(self, storetypes):
502 self.storetypes = storetypes
@@ -0,0 +1,71 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''Store class for local filesystem.'''
10
11 import os
12
13 from mercurial import util
14 from mercurial.i18n import _
15
16 import lfutil
17 import basestore
18
19 class localstore(basestore.basestore):
20 '''Because there is a system wide cache, the local store always uses that
21 cache. Since the cache is updated elsewhere, we can just read from it here
22 as if it were the store.'''
23
24 def __init__(self, ui, repo, remote):
25 url = os.path.join(remote.path, '.hg', lfutil.longname)
26 super(localstore, self).__init__(ui, repo, util.expandpath(url))
27
28 def put(self, source, filename, hash):
29 '''Any file that is put must already be in the system wide cache so do
30 nothing.'''
31 return
32
33 def exists(self, hash):
34 return lfutil.insystemcache(self.repo.ui, hash)
35
36 def _getfile(self, tmpfile, filename, hash):
37 if lfutil.insystemcache(self.ui, hash):
38 return lfutil.systemcachepath(self.ui, hash)
39 raise basestore.StoreError(filename, hash, '',
40 _("Can't get file locally"))
41
42 def _verifyfile(self, cctx, cset, contents, standin, verified):
43 filename = lfutil.splitstandin(standin)
44 if not filename:
45 return False
46 fctx = cctx[standin]
47 key = (filename, fctx.filenode())
48 if key in verified:
49 return False
50
51 expecthash = fctx.data()[0:40]
52 verified.add(key)
53 if not lfutil.insystemcache(self.ui, expecthash):
54 self.ui.warn(
55 _('changeset %s: %s missing\n'
56 ' (looked for hash %s)\n')
57 % (cset, filename, expecthash))
58 return True # failed
59
60 if contents:
61 storepath = lfutil.systemcachepath(self.ui, expecthash)
62 actualhash = lfutil.hashfile(storepath)
63 if actualhash != expecthash:
64 self.ui.warn(
65 _('changeset %s: %s: contents differ\n'
66 ' (%s:\n'
67 ' expected hash %s,\n'
68 ' but got %s)\n')
69 % (cset, filename, storepath, expecthash, actualhash))
70 return True # failed
71 return False
This diff has been collapsed as it changes many lines, (902 lines changed) Show them Hide them
@@ -0,0 +1,902 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''Overridden Mercurial commands and functions for the largefiles extension'''
10
11 import os
12 import copy
13
14 from mercurial import hg, commands, util, cmdutil, match as match_, node, \
15 archival, error, merge
16 from mercurial.i18n import _
17 from mercurial.node import hex
18 from hgext import rebase
19
20 try:
21 from mercurial import scmutil
22 except ImportError:
23 pass
24
25 import lfutil
26 import lfcommands
27
28 def installnormalfilesmatchfn(manifest):
29 '''overrides scmutil.match so that the matcher it returns will ignore all
30 largefiles'''
31 oldmatch = None # for the closure
32 def override_match(repo, pats=[], opts={}, globbed=False,
33 default='relpath'):
34 match = oldmatch(repo, pats, opts, globbed, default)
35 m = copy.copy(match)
36 notlfile = lambda f: not (lfutil.isstandin(f) or lfutil.standin(f) in
37 manifest)
38 m._files = filter(notlfile, m._files)
39 m._fmap = set(m._files)
40 orig_matchfn = m.matchfn
41 m.matchfn = lambda f: notlfile(f) and orig_matchfn(f) or None
42 return m
43 oldmatch = installmatchfn(override_match)
44
45 def installmatchfn(f):
46 try:
47 # Mercurial >= 1.9
48 oldmatch = scmutil.match
49 except ImportError:
50 # Mercurial <= 1.8
51 oldmatch = cmdutil.match
52 setattr(f, 'oldmatch', oldmatch)
53 try:
54 # Mercurial >= 1.9
55 scmutil.match = f
56 except ImportError:
57 # Mercurial <= 1.8
58 cmdutil.match = f
59 return oldmatch
60
61 def restorematchfn():
62 '''restores scmutil.match to what it was before installnormalfilesmatchfn
63 was called. no-op if scmutil.match is its original function.
64
65 Note that n calls to installnormalfilesmatchfn will require n calls to
66 restore matchfn to reverse'''
67 try:
68 # Mercurial >= 1.9
69 scmutil.match = getattr(scmutil.match, 'oldmatch', scmutil.match)
70 except ImportError:
71 # Mercurial <= 1.8
72 cmdutil.match = getattr(cmdutil.match, 'oldmatch', cmdutil.match)
73
74 # -- Wrappers: modify existing commands --------------------------------
75
76 # Add works by going through the files that the user wanted to add
77 # and checking if they should be added as lfiles. Then making a new
78 # matcher which matches only the normal files and running the original
79 # version of add.
80 def override_add(orig, ui, repo, *pats, **opts):
81 large = opts.pop('large', None)
82
83 lfsize = opts.pop('lfsize', None)
84 if not lfsize and lfutil.islfilesrepo(repo):
85 lfsize = ui.config(lfutil.longname, 'size', default='10')
86 if lfsize:
87 try:
88 lfsize = int(lfsize)
89 except ValueError:
90 raise util.Abort(_('largefiles: size must be an integer, was %s\n') % lfsize)
91
92 lfmatcher = None
93 if os.path.exists(repo.wjoin(lfutil.shortname)):
94 lfpats = ui.config(lfutil.longname, 'patterns', default=())
95 if lfpats:
96 lfpats = lfpats.split(' ')
97 lfmatcher = match_.match(repo.root, '', list(lfpats))
98
99 lfnames = []
100 try:
101 # Mercurial >= 1.9
102 m = scmutil.match(repo[None], pats, opts)
103 except ImportError:
104 # Mercurial <= 1.8
105 m = cmdutil.match(repo, pats, opts)
106 m.bad = lambda x, y: None
107 wctx = repo[None]
108 for f in repo.walk(m):
109 exact = m.exact(f)
110 lfile = lfutil.standin(f) in wctx
111 nfile = f in wctx
112 exists = lfile or nfile
113
114 # Don't warn the user when they attempt to add a normal tracked file.
115 # The normal add code will do that for us.
116 if exact and exists:
117 if lfile:
118 ui.warn(_('%s already a largefile\n') % f)
119 continue
120
121 if exact or not exists:
122 if large or (lfsize and os.path.getsize(repo.wjoin(f)) >= \
123 lfsize * 1024 * 1024) or (lfmatcher and lfmatcher(f)):
124 lfnames.append(f)
125 if ui.verbose or not exact:
126 ui.status(_('adding %s as a largefile\n') % m.rel(f))
127
128 bad = []
129 standins = []
130
131 # Need to lock otherwise there could be a race condition inbetween when
132 # standins are created and added to the repo
133 wlock = repo.wlock()
134 try:
135 if not opts.get('dry_run'):
136 lfdirstate = lfutil.openlfdirstate(ui, repo)
137 for f in lfnames:
138 standinname = lfutil.standin(f)
139 lfutil.writestandin(repo, standinname, hash='',
140 executable=lfutil.getexecutable(repo.wjoin(f)))
141 standins.append(standinname)
142 if lfdirstate[f] == 'r':
143 lfdirstate.normallookup(f)
144 else:
145 lfdirstate.add(f)
146 lfdirstate.write()
147 bad += [lfutil.splitstandin(f) for f in lfutil.repo_add(repo,
148 standins) if f in m.files()]
149 finally:
150 wlock.release()
151
152 installnormalfilesmatchfn(repo[None].manifest())
153 result = orig(ui, repo, *pats, **opts)
154 restorematchfn()
155
156 return (result == 1 or bad) and 1 or 0
157
158 def override_remove(orig, ui, repo, *pats, **opts):
159 manifest = repo[None].manifest()
160 installnormalfilesmatchfn(manifest)
161 orig(ui, repo, *pats, **opts)
162 restorematchfn()
163
164 after, force = opts.get('after'), opts.get('force')
165 if not pats and not after:
166 raise util.Abort(_('no files specified'))
167 try:
168 # Mercurial >= 1.9
169 m = scmutil.match(repo[None], pats, opts)
170 except ImportError:
171 # Mercurial <= 1.8
172 m = cmdutil.match(repo, pats, opts)
173 try:
174 repo.lfstatus = True
175 s = repo.status(match=m, clean=True)
176 finally:
177 repo.lfstatus = False
178 modified, added, deleted, clean = [[f for f in list if lfutil.standin(f) \
179 in manifest] for list in [s[0], s[1], s[3], s[6]]]
180
181 def warn(files, reason):
182 for f in files:
183 ui.warn(_('not removing %s: file %s (use -f to force removal)\n')
184 % (m.rel(f), reason))
185
186 if force:
187 remove, forget = modified + deleted + clean, added
188 elif after:
189 remove, forget = deleted, []
190 warn(modified + added + clean, _('still exists'))
191 else:
192 remove, forget = deleted + clean, []
193 warn(modified, _('is modified'))
194 warn(added, _('has been marked for add'))
195
196 for f in sorted(remove + forget):
197 if ui.verbose or not m.exact(f):
198 ui.status(_('removing %s\n') % m.rel(f))
199
200 # Need to lock because standin files are deleted then removed from the
201 # repository and we could race inbetween.
202 wlock = repo.wlock()
203 try:
204 lfdirstate = lfutil.openlfdirstate(ui, repo)
205 for f in remove:
206 if not after:
207 os.unlink(repo.wjoin(f))
208 currentdir = os.path.split(f)[0]
209 while currentdir and not os.listdir(repo.wjoin(currentdir)):
210 os.rmdir(repo.wjoin(currentdir))
211 currentdir = os.path.split(currentdir)[0]
212 lfdirstate.remove(f)
213 lfdirstate.write()
214
215 forget = [lfutil.standin(f) for f in forget]
216 remove = [lfutil.standin(f) for f in remove]
217 lfutil.repo_forget(repo, forget)
218 lfutil.repo_remove(repo, remove, unlink=True)
219 finally:
220 wlock.release()
221
222 def override_status(orig, ui, repo, *pats, **opts):
223 try:
224 repo.lfstatus = True
225 return orig(ui, repo, *pats, **opts)
226 finally:
227 repo.lfstatus = False
228
229 def override_log(orig, ui, repo, *pats, **opts):
230 try:
231 repo.lfstatus = True
232 orig(ui, repo, *pats, **opts)
233 finally:
234 repo.lfstatus = False
235
236 def override_verify(orig, ui, repo, *pats, **opts):
237 large = opts.pop('large', False)
238 all = opts.pop('lfa', False)
239 contents = opts.pop('lfc', False)
240
241 result = orig(ui, repo, *pats, **opts)
242 if large:
243 result = result or lfcommands.verifylfiles(ui, repo, all, contents)
244 return result
245
246 # Override needs to refresh standins so that update's normal merge
247 # will go through properly. Then the other update hook (overriding repo.update)
248 # will get the new files. Filemerge is also overriden so that the merge
249 # will merge standins correctly.
250 def override_update(orig, ui, repo, *pats, **opts):
251 lfdirstate = lfutil.openlfdirstate(ui, repo)
252 s = lfdirstate.status(match_.always(repo.root, repo.getcwd()), [], False,
253 False, False)
254 (unsure, modified, added, removed, missing, unknown, ignored, clean) = s
255
256 # Need to lock between the standins getting updated and their lfiles
257 # getting updated
258 wlock = repo.wlock()
259 try:
260 if opts['check']:
261 mod = len(modified) > 0
262 for lfile in unsure:
263 standin = lfutil.standin(lfile)
264 if repo['.'][standin].data().strip() != \
265 lfutil.hashfile(repo.wjoin(lfile)):
266 mod = True
267 else:
268 lfdirstate.normal(lfile)
269 lfdirstate.write()
270 if mod:
271 raise util.Abort(_('uncommitted local changes'))
272 # XXX handle removed differently
273 if not opts['clean']:
274 for lfile in unsure + modified + added:
275 lfutil.updatestandin(repo, lfutil.standin(lfile))
276 finally:
277 wlock.release()
278 return orig(ui, repo, *pats, **opts)
279
280 # Override filemerge to prompt the user about how they wish to merge lfiles.
281 # This will handle identical edits, and copy/rename + edit without prompting
282 # the user.
283 def override_filemerge(origfn, repo, mynode, orig, fcd, fco, fca):
284 # Use better variable names here. Because this is a wrapper we cannot
285 # change the variable names in the function declaration.
286 fcdest, fcother, fcancestor = fcd, fco, fca
287 if not lfutil.isstandin(orig):
288 return origfn(repo, mynode, orig, fcdest, fcother, fcancestor)
289 else:
290 if not fcother.cmp(fcdest): # files identical?
291 return None
292
293 # backwards, use working dir parent as ancestor
294 if fcancestor == fcother:
295 fcancestor = fcdest.parents()[0]
296
297 if orig != fcother.path():
298 repo.ui.status(_('merging %s and %s to %s\n')
299 % (lfutil.splitstandin(orig),
300 lfutil.splitstandin(fcother.path()),
301 lfutil.splitstandin(fcdest.path())))
302 else:
303 repo.ui.status(_('merging %s\n')
304 % lfutil.splitstandin(fcdest.path()))
305
306 if fcancestor.path() != fcother.path() and fcother.data() == \
307 fcancestor.data():
308 return 0
309 if fcancestor.path() != fcdest.path() and fcdest.data() == \
310 fcancestor.data():
311 repo.wwrite(fcdest.path(), fcother.data(), fcother.flags())
312 return 0
313
314 if repo.ui.promptchoice(_('largefile %s has a merge conflict\n'
315 'keep (l)ocal or take (o)ther?') %
316 lfutil.splitstandin(orig),
317 (_('&Local'), _('&Other')), 0) == 0:
318 return 0
319 else:
320 repo.wwrite(fcdest.path(), fcother.data(), fcother.flags())
321 return 0
322
323 # Copy first changes the matchers to match standins instead of lfiles.
324 # Then it overrides util.copyfile in that function it checks if the destination
325 # lfile already exists. It also keeps a list of copied files so that the lfiles
326 # can be copied and the dirstate updated.
327 def override_copy(orig, ui, repo, pats, opts, rename=False):
328 # doesn't remove lfile on rename
329 if len(pats) < 2:
330 # this isn't legal, let the original function deal with it
331 return orig(ui, repo, pats, opts, rename)
332
333 def makestandin(relpath):
334 try:
335 # Mercurial >= 1.9
336 path = scmutil.canonpath(repo.root, repo.getcwd(), relpath)
337 except ImportError:
338 # Mercurial <= 1.8
339 path = util.canonpath(repo.root, repo.getcwd(), relpath)
340 return os.path.join(os.path.relpath('.', repo.getcwd()),
341 lfutil.standin(path))
342
343 try:
344 # Mercurial >= 1.9
345 fullpats = scmutil.expandpats(pats)
346 except ImportError:
347 # Mercurial <= 1.8
348 fullpats = cmdutil.expandpats(pats)
349 dest = fullpats[-1]
350
351 if os.path.isdir(dest):
352 if not os.path.isdir(makestandin(dest)):
353 os.makedirs(makestandin(dest))
354 # This could copy both lfiles and normal files in one command, but we don't
355 # want to do that first replace their matcher to only match normal files
356 # and run it then replace it to just match lfiles and run it again
357 nonormalfiles = False
358 nolfiles = False
359 try:
360 installnormalfilesmatchfn(repo[None].manifest())
361 result = orig(ui, repo, pats, opts, rename)
362 except util.Abort, e:
363 if str(e) != 'no files to copy':
364 raise e
365 else:
366 nonormalfiles = True
367 result = 0
368 finally:
369 restorematchfn()
370
371 # The first rename can cause our current working directory to be removed.
372 # In that case there is nothing left to copy/rename so just quit.
373 try:
374 repo.getcwd()
375 except OSError:
376 return result
377
378 try:
379 # When we call orig below it creates the standins but we don't add them
380 # to the dir state until later so lock during that time.
381 wlock = repo.wlock()
382
383 manifest = repo[None].manifest()
384 oldmatch = None # for the closure
385 def override_match(repo, pats=[], opts={}, globbed=False,
386 default='relpath'):
387 newpats = []
388 # The patterns were previously mangled to add the standin
389 # directory; we need to remove that now
390 for pat in pats:
391 if match_.patkind(pat) is None and lfutil.shortname in pat:
392 newpats.append(pat.replace(lfutil.shortname, ''))
393 else:
394 newpats.append(pat)
395 match = oldmatch(repo, newpats, opts, globbed, default)
396 m = copy.copy(match)
397 lfile = lambda f: lfutil.standin(f) in manifest
398 m._files = [lfutil.standin(f) for f in m._files if lfile(f)]
399 m._fmap = set(m._files)
400 orig_matchfn = m.matchfn
401 m.matchfn = lambda f: lfutil.isstandin(f) and \
402 lfile(lfutil.splitstandin(f)) and \
403 orig_matchfn(lfutil.splitstandin(f)) or None
404 return m
405 oldmatch = installmatchfn(override_match)
406 listpats = []
407 for pat in pats:
408 if match_.patkind(pat) is not None:
409 listpats.append(pat)
410 else:
411 listpats.append(makestandin(pat))
412
413 try:
414 origcopyfile = util.copyfile
415 copiedfiles = []
416 def override_copyfile(src, dest):
417 if lfutil.shortname in src and lfutil.shortname in dest:
418 destlfile = dest.replace(lfutil.shortname, '')
419 if not opts['force'] and os.path.exists(destlfile):
420 raise IOError('',
421 _('destination largefile already exists'))
422 copiedfiles.append((src, dest))
423 origcopyfile(src, dest)
424
425 util.copyfile = override_copyfile
426 result += orig(ui, repo, listpats, opts, rename)
427 finally:
428 util.copyfile = origcopyfile
429
430 lfdirstate = lfutil.openlfdirstate(ui, repo)
431 for (src, dest) in copiedfiles:
432 if lfutil.shortname in src and lfutil.shortname in dest:
433 srclfile = src.replace(lfutil.shortname, '')
434 destlfile = dest.replace(lfutil.shortname, '')
435 destlfiledir = os.path.dirname(destlfile) or '.'
436 if not os.path.isdir(destlfiledir):
437 os.makedirs(destlfiledir)
438 if rename:
439 os.rename(srclfile, destlfile)
440 lfdirstate.remove(os.path.relpath(srclfile,
441 repo.root))
442 else:
443 util.copyfile(srclfile, destlfile)
444 lfdirstate.add(os.path.relpath(destlfile,
445 repo.root))
446 lfdirstate.write()
447 except util.Abort, e:
448 if str(e) != 'no files to copy':
449 raise e
450 else:
451 nolfiles = True
452 finally:
453 restorematchfn()
454 wlock.release()
455
456 if nolfiles and nonormalfiles:
457 raise util.Abort(_('no files to copy'))
458
459 return result
460
461 # When the user calls revert, we have to be careful to not revert any changes
462 # to other lfiles accidentally. This means we have to keep track of the lfiles
463 # that are being reverted so we only pull down the necessary lfiles.
464 #
465 # Standins are only updated (to match the hash of lfiles) before commits.
466 # Update the standins then run the original revert (changing the matcher to hit
467 # standins instead of lfiles). Based on the resulting standins update the
468 # lfiles. Then return the standins to their proper state
469 def override_revert(orig, ui, repo, *pats, **opts):
470 # Because we put the standins in a bad state (by updating them) and then
471 # return them to a correct state we need to lock to prevent others from
472 # changing them in their incorrect state.
473 wlock = repo.wlock()
474 try:
475 lfdirstate = lfutil.openlfdirstate(ui, repo)
476 (modified, added, removed, missing, unknown, ignored, clean) = \
477 lfutil.lfdirstate_status(lfdirstate, repo, repo['.'].rev())
478 for lfile in modified:
479 lfutil.updatestandin(repo, lfutil.standin(lfile))
480
481 try:
482 ctx = repo[opts.get('rev')]
483 oldmatch = None # for the closure
484 def override_match(ctxorrepo, pats=[], opts={}, globbed=False,
485 default='relpath'):
486 if hasattr(ctxorrepo, 'match'):
487 ctx0 = ctxorrepo
488 else:
489 ctx0 = ctxorrepo[None]
490 match = oldmatch(ctxorrepo, pats, opts, globbed, default)
491 m = copy.copy(match)
492 def tostandin(f):
493 if lfutil.standin(f) in ctx0 or lfutil.standin(f) in ctx:
494 return lfutil.standin(f)
495 elif lfutil.standin(f) in repo[None]:
496 return None
497 return f
498 m._files = [tostandin(f) for f in m._files]
499 m._files = [f for f in m._files if f is not None]
500 m._fmap = set(m._files)
501 orig_matchfn = m.matchfn
502 def matchfn(f):
503 if lfutil.isstandin(f):
504 # We need to keep track of what lfiles are being
505 # matched so we know which ones to update later
506 # (otherwise we revert changes to other lfiles
507 # accidentally). This is repo specific, so duckpunch
508 # the repo object to keep the list of lfiles for us
509 # later.
510 if orig_matchfn(lfutil.splitstandin(f)) and \
511 (f in repo[None] or f in ctx):
512 lfileslist = getattr(repo, '_lfilestoupdate', [])
513 lfileslist.append(lfutil.splitstandin(f))
514 repo._lfilestoupdate = lfileslist
515 return True
516 else:
517 return False
518 return orig_matchfn(f)
519 m.matchfn = matchfn
520 return m
521 oldmatch = installmatchfn(override_match)
522 try:
523 # Mercurial >= 1.9
524 scmutil.match
525 matches = override_match(repo[None], pats, opts)
526 except ImportError:
527 # Mercurial <= 1.8
528 matches = override_match(repo, pats, opts)
529 orig(ui, repo, *pats, **opts)
530 finally:
531 restorematchfn()
532 lfileslist = getattr(repo, '_lfilestoupdate', [])
533 lfcommands.updatelfiles(ui, repo, filelist=lfileslist, printmessage=False)
534 # Empty out the lfiles list so we start fresh next time
535 repo._lfilestoupdate = []
536 for lfile in modified:
537 if lfile in lfileslist:
538 if os.path.exists(repo.wjoin(lfutil.standin(lfile))) and lfile\
539 in repo['.']:
540 lfutil.writestandin(repo, lfutil.standin(lfile),
541 repo['.'][lfile].data().strip(),
542 'x' in repo['.'][lfile].flags())
543 lfdirstate = lfutil.openlfdirstate(ui, repo)
544 for lfile in added:
545 standin = lfutil.standin(lfile)
546 if standin not in ctx and (standin in matches or opts.get('all')):
547 if lfile in lfdirstate:
548 try:
549 # Mercurial >= 1.9
550 lfdirstate.drop(lfile)
551 except AttributeError:
552 # Mercurial <= 1.8
553 lfdirstate.forget(lfile)
554 util.unlinkpath(repo.wjoin(standin))
555 lfdirstate.write()
556 finally:
557 wlock.release()
558
559 def hg_update(orig, repo, node):
560 result = orig(repo, node)
561 # XXX check if it worked first
562 lfcommands.updatelfiles(repo.ui, repo)
563 return result
564
565 def hg_clean(orig, repo, node, show_stats=True):
566 result = orig(repo, node, show_stats)
567 lfcommands.updatelfiles(repo.ui, repo)
568 return result
569
570 def hg_merge(orig, repo, node, force=None, remind=True):
571 result = orig(repo, node, force, remind)
572 lfcommands.updatelfiles(repo.ui, repo)
573 return result
574
575 # When we rebase a repository with remotely changed lfiles, we need
576 # to take some extra care so that the lfiles are correctly updated
577 # in the working copy
578 def override_pull(orig, ui, repo, source=None, **opts):
579 if opts.get('rebase', False):
580 repo._isrebasing = True
581 try:
582 if opts.get('update'):
583 del opts['update']
584 ui.debug('--update and --rebase are not compatible, ignoring '
585 'the update flag\n')
586 del opts['rebase']
587 try:
588 # Mercurial >= 1.9
589 cmdutil.bailifchanged(repo)
590 except AttributeError:
591 # Mercurial <= 1.8
592 cmdutil.bail_if_changed(repo)
593 revsprepull = len(repo)
594 origpostincoming = commands.postincoming
595 def _dummy(*args, **kwargs):
596 pass
597 commands.postincoming = _dummy
598 repo.lfpullsource = source
599 if not source:
600 source = 'default'
601 try:
602 result = commands.pull(ui, repo, source, **opts)
603 finally:
604 commands.postincoming = origpostincoming
605 revspostpull = len(repo)
606 if revspostpull > revsprepull:
607 result = result or rebase.rebase(ui, repo)
608 finally:
609 repo._isrebasing = False
610 else:
611 repo.lfpullsource = source
612 if not source:
613 source = 'default'
614 result = orig(ui, repo, source, **opts)
615 return result
616
617 def override_rebase(orig, ui, repo, **opts):
618 repo._isrebasing = True
619 try:
620 orig(ui, repo, **opts)
621 finally:
622 repo._isrebasing = False
623
624 def override_archive(orig, repo, dest, node, kind, decode=True, matchfn=None,
625 prefix=None, mtime=None, subrepos=None):
626 # No need to lock because we are only reading history and lfile caches
627 # neither of which are modified
628
629 lfcommands.cachelfiles(repo.ui, repo, node)
630
631 if kind not in archival.archivers:
632 raise util.Abort(_("unknown archive type '%s'") % kind)
633
634 ctx = repo[node]
635
636 # In Mercurial <= 1.5 the prefix is passed to the archiver so try that
637 # if that doesn't work we are probably in Mercurial >= 1.6 where the
638 # prefix is not handled by the archiver
639 try:
640 archiver = archival.archivers[kind](dest, prefix, mtime or \
641 ctx.date()[0])
642
643 def write(name, mode, islink, getdata):
644 if matchfn and not matchfn(name):
645 return
646 data = getdata()
647 if decode:
648 data = repo.wwritedata(name, data)
649 archiver.addfile(name, mode, islink, data)
650 except TypeError:
651 if kind == 'files':
652 if prefix:
653 raise util.Abort(
654 _('cannot give prefix when archiving to files'))
655 else:
656 prefix = archival.tidyprefix(dest, kind, prefix)
657
658 def write(name, mode, islink, getdata):
659 if matchfn and not matchfn(name):
660 return
661 data = getdata()
662 if decode:
663 data = repo.wwritedata(name, data)
664 archiver.addfile(prefix + name, mode, islink, data)
665
666 archiver = archival.archivers[kind](dest, mtime or ctx.date()[0])
667
668 if repo.ui.configbool("ui", "archivemeta", True):
669 def metadata():
670 base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
671 hex(repo.changelog.node(0)), hex(node), ctx.branch())
672
673 tags = ''.join('tag: %s\n' % t for t in ctx.tags()
674 if repo.tagtype(t) == 'global')
675 if not tags:
676 repo.ui.pushbuffer()
677 opts = {'template': '{latesttag}\n{latesttagdistance}',
678 'style': '', 'patch': None, 'git': None}
679 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
680 ltags, dist = repo.ui.popbuffer().split('\n')
681 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
682 tags += 'latesttagdistance: %s\n' % dist
683
684 return base + tags
685
686 write('.hg_archival.txt', 0644, False, metadata)
687
688 for f in ctx:
689 ff = ctx.flags(f)
690 getdata = ctx[f].data
691 if lfutil.isstandin(f):
692 path = lfutil.findfile(repo, getdata().strip())
693 f = lfutil.splitstandin(f)
694
695 def getdatafn():
696 try:
697 fd = open(path, 'rb')
698 return fd.read()
699 finally:
700 fd.close()
701
702 getdata = getdatafn
703 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, getdata)
704
705 if subrepos:
706 for subpath in ctx.substate:
707 sub = ctx.sub(subpath)
708 try:
709 sub.archive(repo.ui, archiver, prefix)
710 except TypeError:
711 sub.archive(archiver, prefix)
712
713 archiver.done()
714
715 # If a lfile is modified the change is not reflected in its standin until a
716 # commit. cmdutil.bailifchanged raises an exception if the repo has
717 # uncommitted changes. Wrap it to also check if lfiles were changed. This is
718 # used by bisect and backout.
719 def override_bailifchanged(orig, repo):
720 orig(repo)
721 repo.lfstatus = True
722 modified, added, removed, deleted = repo.status()[:4]
723 repo.lfstatus = False
724 if modified or added or removed or deleted:
725 raise util.Abort(_('outstanding uncommitted changes'))
726
727 # Fetch doesn't use cmdutil.bail_if_changed so override it to add the check
728 def override_fetch(orig, ui, repo, *pats, **opts):
729 repo.lfstatus = True
730 modified, added, removed, deleted = repo.status()[:4]
731 repo.lfstatus = False
732 if modified or added or removed or deleted:
733 raise util.Abort(_('outstanding uncommitted changes'))
734 return orig(ui, repo, *pats, **opts)
735
736 def override_forget(orig, ui, repo, *pats, **opts):
737 installnormalfilesmatchfn(repo[None].manifest())
738 orig(ui, repo, *pats, **opts)
739 restorematchfn()
740 try:
741 # Mercurial >= 1.9
742 m = scmutil.match(repo[None], pats, opts)
743 except ImportError:
744 # Mercurial <= 1.8
745 m = cmdutil.match(repo, pats, opts)
746
747 try:
748 repo.lfstatus = True
749 s = repo.status(match=m, clean=True)
750 finally:
751 repo.lfstatus = False
752 forget = sorted(s[0] + s[1] + s[3] + s[6])
753 forget = [f for f in forget if lfutil.standin(f) in repo[None].manifest()]
754
755 for f in forget:
756 if lfutil.standin(f) not in repo.dirstate and not \
757 os.path.isdir(m.rel(lfutil.standin(f))):
758 ui.warn(_('not removing %s: file is already untracked\n')
759 % m.rel(f))
760
761 for f in forget:
762 if ui.verbose or not m.exact(f):
763 ui.status(_('removing %s\n') % m.rel(f))
764
765 # Need to lock because standin files are deleted then removed from the
766 # repository and we could race inbetween.
767 wlock = repo.wlock()
768 try:
769 lfdirstate = lfutil.openlfdirstate(ui, repo)
770 for f in forget:
771 if lfdirstate[f] == 'a':
772 lfdirstate.drop(f)
773 else:
774 lfdirstate.remove(f)
775 lfdirstate.write()
776 lfutil.repo_remove(repo, [lfutil.standin(f) for f in forget],
777 unlink=True)
778 finally:
779 wlock.release()
780
781 def getoutgoinglfiles(ui, repo, dest=None, **opts):
782 dest = ui.expandpath(dest or 'default-push', dest or 'default')
783 dest, branches = hg.parseurl(dest, opts.get('branch'))
784 revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev'))
785 if revs:
786 revs = [repo.lookup(rev) for rev in revs]
787
788 # Mercurial <= 1.5 had remoteui in cmdutil, then it moved to hg
789 try:
790 remoteui = cmdutil.remoteui
791 except AttributeError:
792 remoteui = hg.remoteui
793
794 try:
795 remote = hg.repository(remoteui(repo, opts), dest)
796 except error.RepoError:
797 return None
798 o = lfutil.findoutgoing(repo, remote, False)
799 if not o:
800 return None
801 o = repo.changelog.nodesbetween(o, revs)[0]
802 if opts.get('newest_first'):
803 o.reverse()
804
805 toupload = set()
806 for n in o:
807 parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
808 ctx = repo[n]
809 files = set(ctx.files())
810 if len(parents) == 2:
811 mc = ctx.manifest()
812 mp1 = ctx.parents()[0].manifest()
813 mp2 = ctx.parents()[1].manifest()
814 for f in mp1:
815 if f not in mc:
816 files.add(f)
817 for f in mp2:
818 if f not in mc:
819 files.add(f)
820 for f in mc:
821 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
822 files.add(f)
823 toupload = toupload.union(set([f for f in files if lfutil.isstandin(f)\
824 and f in ctx]))
825 return toupload
826
827 def override_outgoing(orig, ui, repo, dest=None, **opts):
828 orig(ui, repo, dest, **opts)
829
830 if opts.pop('large', None):
831 toupload = getoutgoinglfiles(ui, repo, dest, **opts)
832 if toupload is None:
833 ui.status(_('largefiles: No remote repo\n'))
834 else:
835 ui.status(_('largefiles to upload:\n'))
836 for file in toupload:
837 ui.status(lfutil.splitstandin(file) + '\n')
838 ui.status('\n')
839
840 def override_summary(orig, ui, repo, *pats, **opts):
841 orig(ui, repo, *pats, **opts)
842
843 if opts.pop('large', None):
844 toupload = getoutgoinglfiles(ui, repo, None, **opts)
845 if toupload is None:
846 ui.status(_('largefiles: No remote repo\n'))
847 else:
848 ui.status(_('largefiles: %d to upload\n') % len(toupload))
849
850 def override_addremove(orig, ui, repo, *pats, **opts):
851 # Check if the parent or child has lfiles if they do don't allow it. If
852 # there is a symlink in the manifest then getting the manifest throws an
853 # exception catch it and let addremove deal with it. This happens in
854 # Mercurial's test test-addremove-symlink
855 try:
856 manifesttip = set(repo['tip'].manifest())
857 except util.Abort:
858 manifesttip = set()
859 try:
860 manifestworking = set(repo[None].manifest())
861 except util.Abort:
862 manifestworking = set()
863
864 # Manifests are only iterable so turn them into sets then union
865 for file in manifesttip.union(manifestworking):
866 if file.startswith(lfutil.shortname):
867 raise util.Abort(
868 _('addremove cannot be run on a repo with largefiles'))
869
870 return orig(ui, repo, *pats, **opts)
871
872 # Calling purge with --all will cause the lfiles to be deleted.
873 # Override repo.status to prevent this from happening.
874 def override_purge(orig, ui, repo, *dirs, **opts):
875 oldstatus = repo.status
876 def override_status(node1='.', node2=None, match=None, ignored=False,
877 clean=False, unknown=False, listsubrepos=False):
878 r = oldstatus(node1, node2, match, ignored, clean, unknown,
879 listsubrepos)
880 lfdirstate = lfutil.openlfdirstate(ui, repo)
881 modified, added, removed, deleted, unknown, ignored, clean = r
882 unknown = [f for f in unknown if lfdirstate[f] == '?']
883 ignored = [f for f in ignored if lfdirstate[f] == '?']
884 return modified, added, removed, deleted, unknown, ignored, clean
885 repo.status = override_status
886 orig(ui, repo, *dirs, **opts)
887 repo.status = oldstatus
888
889 def override_rollback(orig, ui, repo, **opts):
890 result = orig(ui, repo, **opts)
891 merge.update(repo, node=None, branchmerge=False, force=True,
892 partial=lfutil.isstandin)
893 lfdirstate = lfutil.openlfdirstate(ui, repo)
894 lfiles = lfutil.listlfiles(repo)
895 oldlfiles = lfutil.listlfiles(repo, repo[None].parents()[0].rev())
896 for file in lfiles:
897 if file in oldlfiles:
898 lfdirstate.normallookup(file)
899 else:
900 lfdirstate.add(file)
901 lfdirstate.write()
902 return result
@@ -0,0 +1,161 b''
1 # Copyright 2011 Fog Creek Software
2 #
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
5
6 import os
7 import tempfile
8 import urllib2
9
10 from mercurial import error, httprepo, util, wireproto
11 from mercurial.i18n import _
12
13 import lfutil
14
15 LARGEFILES_REQUIRED_MSG = '\nThis repository uses the largefiles extension.' \
16 '\n\nPlease enable it in your Mercurial config ' \
17 'file.\n'
18
19 def putlfile(repo, proto, sha):
20 """putlfile puts a largefile into a repository's local cache and into the
21 system cache."""
22 f = None
23 proto.redirect()
24 try:
25 try:
26 f = tempfile.NamedTemporaryFile(mode='wb+', prefix='hg-putlfile-')
27 proto.getfile(f)
28 f.seek(0)
29 if sha != lfutil.hexsha1(f):
30 return wireproto.pushres(1)
31 lfutil.copytocacheabsolute(repo, f.name, sha)
32 except IOError:
33 repo.ui.warn(
34 _('error: could not put received data into largefile store'))
35 return wireproto.pushres(1)
36 finally:
37 if f:
38 f.close()
39
40 return wireproto.pushres(0)
41
42 def getlfile(repo, proto, sha):
43 """getlfile retrieves a largefile from the repository-local cache or system
44 cache."""
45 filename = lfutil.findfile(repo, sha)
46 if not filename:
47 raise util.Abort(_('requested largefile %s not present in cache') % sha)
48 f = open(filename, 'rb')
49 length = os.fstat(f.fileno())[6]
50 # since we can't set an HTTP content-length header here, and mercurial core
51 # provides no way to give the length of a streamres (and reading the entire
52 # file into RAM would be ill-advised), we just send the length on the first
53 # line of the response, like the ssh proto does for string responses.
54 def generator():
55 yield '%d\n' % length
56 for chunk in f:
57 yield chunk
58 return wireproto.streamres(generator())
59
60 def statlfile(repo, proto, sha):
61 """statlfile sends '2\n' if the largefile is missing, '1\n' if it has a
62 mismatched checksum, or '0\n' if it is in good condition"""
63 filename = lfutil.findfile(repo, sha)
64 if not filename:
65 return '2\n'
66 fd = None
67 try:
68 fd = open(filename, 'rb')
69 return lfutil.hexsha1(fd) == sha and '0\n' or '1\n'
70 finally:
71 if fd:
72 fd.close()
73
74 def wirereposetup(ui, repo):
75 class lfileswirerepository(repo.__class__):
76 def putlfile(self, sha, fd):
77 # unfortunately, httprepository._callpush tries to convert its
78 # input file-like into a bundle before sending it, so we can't use
79 # it ...
80 if issubclass(self.__class__, httprepo.httprepository):
81 try:
82 return int(self._call('putlfile', data=fd, sha=sha,
83 headers={'content-type':'application/mercurial-0.1'}))
84 except (ValueError, urllib2.HTTPError):
85 return 1
86 # ... but we can't use sshrepository._call because the data=
87 # argument won't get sent, and _callpush does exactly what we want
88 # in this case: send the data straight through
89 else:
90 try:
91 ret, output = self._callpush("putlfile", fd, sha=sha)
92 if ret == "":
93 raise error.ResponseError(_('putlfile failed:'),
94 output)
95 return int(ret)
96 except IOError:
97 return 1
98 except ValueError:
99 raise error.ResponseError(
100 _('putlfile failed (unexpected response):'), ret)
101
102 def getlfile(self, sha):
103 stream = self._callstream("getlfile", sha=sha)
104 length = stream.readline()
105 try:
106 length = int(length)
107 except ValueError:
108 self._abort(error.ResponseError(_("unexpected response:"), length))
109 return (length, stream)
110
111 def statlfile(self, sha):
112 try:
113 return int(self._call("statlfile", sha=sha))
114 except (ValueError, urllib2.HTTPError):
115 # if the server returns anything but an integer followed by a
116 # newline, newline, it's not speaking our language; if we get
117 # an HTTP error, we can't be sure the largefile is present;
118 # either way, consider it missing
119 return 2
120
121 repo.__class__ = lfileswirerepository
122
123 # advertise the largefiles=serve capability
124 def capabilities(repo, proto):
125 return capabilities_orig(repo, proto) + ' largefiles=serve'
126
127 # duplicate what Mercurial's new out-of-band errors mechanism does, because
128 # clients old and new alike both handle it well
129 def webproto_refuseclient(self, message):
130 self.req.header([('Content-Type', 'application/hg-error')])
131 return message
132
133 def sshproto_refuseclient(self, message):
134 self.ui.write_err('%s\n-\n' % message)
135 self.fout.write('\n')
136 self.fout.flush()
137
138 return ''
139
140 def heads(repo, proto):
141 if lfutil.islfilesrepo(repo):
142 try:
143 # Mercurial >= f4522df38c65
144 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
145 except AttributeError:
146 return proto.refuseclient(LARGEFILES_REQUIRED_MSG)
147 return wireproto.heads(repo, proto)
148
149 def sshrepo_callstream(self, cmd, **args):
150 if cmd == 'heads' and self.capable('largefiles'):
151 cmd = 'lheads'
152 if cmd == 'batch' and self.capable('largefiles'):
153 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
154 return ssh_oldcallstream(self, cmd, **args)
155
156 def httprepo_callstream(self, cmd, **args):
157 if cmd == 'heads' and self.capable('largefiles'):
158 cmd = 'lheads'
159 if cmd == 'batch' and self.capable('largefiles'):
160 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
161 return http_oldcallstream(self, cmd, **args)
@@ -0,0 +1,106 b''
1 # Copyright 2010-2011 Fog Creek Software
2 # Copyright 2010-2011 Unity Technologies
3 #
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
6
7 '''Remote largefile store; the base class for servestore'''
8
9 import urllib2
10 import HTTPError
11
12 from mercurial import util
13 from mercurial.i18n import _
14
15 import lfutil
16 import basestore
17
18 class remotestore(basestore.basestore):
19 """A largefile store accessed over a network"""
20 def __init__(self, ui, repo, url):
21 super(remotestore, self).__init__(ui, repo, url)
22
23 def put(self, source, hash):
24 if self._verify(hash):
25 return
26 if self.sendfile(source, hash):
27 raise util.Abort(
28 _('remotestore: could not put %s to remote store %s')
29 % (source, self.url))
30 self.ui.debug(
31 _('remotestore: put %s to remote store %s') % (source, self.url))
32
33 def exists(self, hash):
34 return self._verify(hash)
35
36 def sendfile(self, filename, hash):
37 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
38 fd = None
39 try:
40 try:
41 fd = lfutil.httpsendfile(self.ui, filename)
42 except IOError, e:
43 raise util.Abort(
44 _('remotestore: could not open file %s: %s')
45 % (filename, str(e)))
46 return self._put(hash, fd)
47 finally:
48 if fd:
49 fd.close()
50
51 def _getfile(self, tmpfile, filename, hash):
52 # quit if the largefile isn't there
53 stat = self._stat(hash)
54 if stat:
55 raise util.Abort(_('remotestore: largefile %s is %s') %
56 (hash, stat == 1 and 'invalid' or 'missing'))
57
58 try:
59 length, infile = self._get(hash)
60 except HTTPError, e:
61 # 401s get converted to util.Aborts; everything else is fine being
62 # turned into a StoreError
63 raise basestore.StoreError(filename, hash, self.url, str(e))
64 except urllib2.URLError, e:
65 # This usually indicates a connection problem, so don't
66 # keep trying with the other files... they will probably
67 # all fail too.
68 raise util.Abort('%s: %s' % (self.url, str(e.reason)))
69 except IOError, e:
70 raise basestore.StoreError(filename, hash, self.url, str(e))
71
72 # Mercurial does not close its SSH connections after writing a stream
73 if length is not None:
74 infile = lfutil.limitreader(infile, length)
75 return lfutil.copyandhash(lfutil.blockstream(infile), tmpfile)
76
77 def _verify(self, hash):
78 return not self._stat(hash)
79
80 def _verifyfile(self, cctx, cset, contents, standin, verified):
81 filename = lfutil.splitstandin(standin)
82 if not filename:
83 return False
84 fctx = cctx[standin]
85 key = (filename, fctx.filenode())
86 if key in verified:
87 return False
88
89 verified.add(key)
90
91 stat = self._stat(hash)
92 if not stat:
93 return False
94 elif stat == 1:
95 self.ui.warn(
96 _('changeset %s: %s: contents differ\n')
97 % (cset, filename))
98 return True # failed
99 elif stat == 2:
100 self.ui.warn(
101 _('changeset %s: %s missing\n')
102 % (cset, filename))
103 return True # failed
104 else:
105 raise util.Abort(_('check failed, unexpected response'
106 'statlfile: %d') % stat)
@@ -0,0 +1,411 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''setup for largefiles repositories: reposetup'''
10 import copy
11 import types
12 import os
13 import re
14
15 from mercurial import context, error, manifest, match as match_, \
16 node, util
17 from mercurial.i18n import _
18
19 import lfcommands
20 import proto
21 import lfutil
22
23 def reposetup(ui, repo):
24 # wire repositories should be given new wireproto functions but not the
25 # other largefiles modifications
26 if not repo.local():
27 return proto.wirereposetup(ui, repo)
28
29 for name in ('status', 'commitctx', 'commit', 'push'):
30 method = getattr(repo, name)
31 #if not (isinstance(method, types.MethodType) and
32 # method.im_func is repo.__class__.commitctx.im_func):
33 if isinstance(method, types.FunctionType) and method.func_name == \
34 'wrap':
35 ui.warn(_('largefiles: repo method %r appears to have already been'
36 ' wrapped by another extension: '
37 'largefiles may behave incorrectly\n')
38 % name)
39
40 class lfiles_repo(repo.__class__):
41 lfstatus = False
42 def status_nolfiles(self, *args, **kwargs):
43 return super(lfiles_repo, self).status(*args, **kwargs)
44
45 # When lfstatus is set, return a context that gives the names of lfiles
46 # instead of their corresponding standins and identifies the lfiles as
47 # always binary, regardless of their actual contents.
48 def __getitem__(self, changeid):
49 ctx = super(lfiles_repo, self).__getitem__(changeid)
50 if self.lfstatus:
51 class lfiles_manifestdict(manifest.manifestdict):
52 def __contains__(self, filename):
53 if super(lfiles_manifestdict,
54 self).__contains__(filename):
55 return True
56 return super(lfiles_manifestdict,
57 self).__contains__(lfutil.shortname+'/' + filename)
58 class lfiles_ctx(ctx.__class__):
59 def files(self):
60 filenames = super(lfiles_ctx, self).files()
61 return [re.sub('^\\'+lfutil.shortname+'/', '', filename) for filename
62 in filenames]
63 def manifest(self):
64 man1 = super(lfiles_ctx, self).manifest()
65 man1.__class__ = lfiles_manifestdict
66 return man1
67 def filectx(self, path, fileid=None, filelog=None):
68 try:
69 result = super(lfiles_ctx, self).filectx(path,
70 fileid, filelog)
71 except error.LookupError:
72 # Adding a null character will cause Mercurial to
73 # identify this as a binary file.
74 result = super(lfiles_ctx, self).filectx(
75 lfutil.shortname + '/' + path, fileid,
76 filelog)
77 olddata = result.data
78 result.data = lambda: olddata() + '\0'
79 return result
80 ctx.__class__ = lfiles_ctx
81 return ctx
82
83 # Figure out the status of big files and insert them into the
84 # appropriate list in the result. Also removes standin files from
85 # the listing. This function reverts to the original status if
86 # self.lfstatus is False
87 def status(self, node1='.', node2=None, match=None, ignored=False,
88 clean=False, unknown=False, listsubrepos=False):
89 listignored, listclean, listunknown = ignored, clean, unknown
90 if not self.lfstatus:
91 try:
92 return super(lfiles_repo, self).status(node1, node2, match,
93 listignored, listclean, listunknown, listsubrepos)
94 except TypeError:
95 return super(lfiles_repo, self).status(node1, node2, match,
96 listignored, listclean, listunknown)
97 else:
98 # some calls in this function rely on the old version of status
99 self.lfstatus = False
100 if isinstance(node1, context.changectx):
101 ctx1 = node1
102 else:
103 ctx1 = repo[node1]
104 if isinstance(node2, context.changectx):
105 ctx2 = node2
106 else:
107 ctx2 = repo[node2]
108 working = ctx2.rev() is None
109 parentworking = working and ctx1 == self['.']
110
111 def inctx(file, ctx):
112 try:
113 if ctx.rev() is None:
114 return file in ctx.manifest()
115 ctx[file]
116 return True
117 except:
118 return False
119
120 # create a copy of match that matches standins instead of
121 # lfiles if matcher not set then it is the always matcher so
122 # overwrite that
123 if match is None:
124 match = match_.always(self.root, self.getcwd())
125
126 def tostandin(file):
127 if inctx(lfutil.standin(file), ctx2):
128 return lfutil.standin(file)
129 return file
130
131 m = copy.copy(match)
132 m._files = [tostandin(f) for f in m._files]
133
134 # get ignored clean and unknown but remove them later if they
135 # were not asked for
136 try:
137 result = super(lfiles_repo, self).status(node1, node2, m,
138 True, True, True, listsubrepos)
139 except TypeError:
140 result = super(lfiles_repo, self).status(node1, node2, m,
141 True, True, True)
142 if working:
143 # Hold the wlock while we read lfiles and update the
144 # lfdirstate
145 wlock = repo.wlock()
146 try:
147 # Any non lfiles that were explicitly listed must be
148 # taken out or lfdirstate.status will report an error.
149 # The status of these files was already computed using
150 # super's status.
151 lfdirstate = lfutil.openlfdirstate(ui, self)
152 match._files = [f for f in match._files if f in
153 lfdirstate]
154 s = lfdirstate.status(match, [], listignored,
155 listclean, listunknown)
156 (unsure, modified, added, removed, missing, unknown,
157 ignored, clean) = s
158 if parentworking:
159 for lfile in unsure:
160 if ctx1[lfutil.standin(lfile)].data().strip() \
161 != lfutil.hashfile(self.wjoin(lfile)):
162 modified.append(lfile)
163 else:
164 clean.append(lfile)
165 lfdirstate.normal(lfile)
166 lfdirstate.write()
167 else:
168 tocheck = unsure + modified + added + clean
169 modified, added, clean = [], [], []
170
171 for lfile in tocheck:
172 standin = lfutil.standin(lfile)
173 if inctx(standin, ctx1):
174 if ctx1[standin].data().strip() != \
175 lfutil.hashfile(self.wjoin(lfile)):
176 modified.append(lfile)
177 else:
178 clean.append(lfile)
179 else:
180 added.append(lfile)
181 finally:
182 wlock.release()
183
184 for standin in ctx1.manifest():
185 if not lfutil.isstandin(standin):
186 continue
187 lfile = lfutil.splitstandin(standin)
188 if not match(lfile):
189 continue
190 if lfile not in lfdirstate:
191 removed.append(lfile)
192 # Handle unknown and ignored differently
193 lfiles = (modified, added, removed, missing, [], [], clean)
194 result = list(result)
195 # Unknown files
196 result[4] = [f for f in unknown if repo.dirstate[f] == '?'\
197 and not lfutil.isstandin(f)]
198 # Ignored files must be ignored by both the dirstate and
199 # lfdirstate
200 result[5] = set(ignored).intersection(set(result[5]))
201 # combine normal files and lfiles
202 normals = [[fn for fn in filelist if not \
203 lfutil.isstandin(fn)] for filelist in result]
204 result = [sorted(list1 + list2) for (list1, list2) in \
205 zip(normals, lfiles)]
206 else:
207 def toname(f):
208 if lfutil.isstandin(f):
209 return lfutil.splitstandin(f)
210 return f
211 result = [[toname(f) for f in items] for items in result]
212
213 if not listunknown:
214 result[4] = []
215 if not listignored:
216 result[5] = []
217 if not listclean:
218 result[6] = []
219 self.lfstatus = True
220 return result
221
222 # This call happens after a commit has occurred. Copy all of the lfiles
223 # into the cache
224 def commitctx(self, *args, **kwargs):
225 node = super(lfiles_repo, self).commitctx(*args, **kwargs)
226 ctx = self[node]
227 for filename in ctx.files():
228 if lfutil.isstandin(filename) and filename in ctx.manifest():
229 realfile = lfutil.splitstandin(filename)
230 lfutil.copytocache(self, ctx.node(), realfile)
231
232 return node
233
234 # This call happens before a commit has occurred. The lfile standins
235 # have not had their contents updated (to reflect the hash of their
236 # lfile). Do that here.
237 def commit(self, text="", user=None, date=None, match=None,
238 force=False, editor=False, extra={}):
239 orig = super(lfiles_repo, self).commit
240
241 wlock = repo.wlock()
242 try:
243 if getattr(repo, "_isrebasing", False):
244 # We have to take the time to pull down the new lfiles now.
245 # Otherwise if we are rebasing, any lfiles that were
246 # modified in the changesets we are rebasing on top of get
247 # overwritten either by the rebase or in the first commit
248 # after the rebase.
249 lfcommands.updatelfiles(repo.ui, repo)
250 # Case 1: user calls commit with no specific files or
251 # include/exclude patterns: refresh and commit everything.
252 if (match is None) or (not match.anypats() and not \
253 match.files()):
254 lfiles = lfutil.listlfiles(self)
255 lfdirstate = lfutil.openlfdirstate(ui, self)
256 # this only loops through lfiles that exist (not
257 # removed/renamed)
258 for lfile in lfiles:
259 if os.path.exists(self.wjoin(lfutil.standin(lfile))):
260 # this handles the case where a rebase is being
261 # performed and the working copy is not updated
262 # yet.
263 if os.path.exists(self.wjoin(lfile)):
264 lfutil.updatestandin(self,
265 lfutil.standin(lfile))
266 lfdirstate.normal(lfile)
267 for lfile in lfdirstate:
268 if not os.path.exists(
269 repo.wjoin(lfutil.standin(lfile))):
270 try:
271 # Mercurial >= 1.9
272 lfdirstate.drop(lfile)
273 except AttributeError:
274 # Mercurial <= 1.8
275 lfdirstate.forget(lfile)
276 lfdirstate.write()
277
278 return orig(text=text, user=user, date=date, match=match,
279 force=force, editor=editor, extra=extra)
280
281 for file in match.files():
282 if lfutil.isstandin(file):
283 raise util.Abort(
284 "Don't commit largefile standin. Commit largefile.")
285
286 # Case 2: user calls commit with specified patterns: refresh
287 # any matching big files.
288 smatcher = lfutil.composestandinmatcher(self, match)
289 standins = lfutil.dirstate_walk(self.dirstate, smatcher)
290
291 # No matching big files: get out of the way and pass control to
292 # the usual commit() method.
293 if not standins:
294 return orig(text=text, user=user, date=date, match=match,
295 force=force, editor=editor, extra=extra)
296
297 # Refresh all matching big files. It's possible that the
298 # commit will end up failing, in which case the big files will
299 # stay refreshed. No harm done: the user modified them and
300 # asked to commit them, so sooner or later we're going to
301 # refresh the standins. Might as well leave them refreshed.
302 lfdirstate = lfutil.openlfdirstate(ui, self)
303 for standin in standins:
304 lfile = lfutil.splitstandin(standin)
305 if lfdirstate[lfile] <> 'r':
306 lfutil.updatestandin(self, standin)
307 lfdirstate.normal(lfile)
308 else:
309 try:
310 # Mercurial >= 1.9
311 lfdirstate.drop(lfile)
312 except AttributeError:
313 # Mercurial <= 1.8
314 lfdirstate.forget(lfile)
315 lfdirstate.write()
316
317 # Cook up a new matcher that only matches regular files or
318 # standins corresponding to the big files requested by the
319 # user. Have to modify _files to prevent commit() from
320 # complaining "not tracked" for big files.
321 lfiles = lfutil.listlfiles(repo)
322 match = copy.copy(match)
323 orig_matchfn = match.matchfn
324
325 # Check both the list of lfiles and the list of standins
326 # because if a lfile was removed, it won't be in the list of
327 # lfiles at this point
328 match._files += sorted(standins)
329
330 actualfiles = []
331 for f in match._files:
332 fstandin = lfutil.standin(f)
333
334 # Ignore known lfiles and standins
335 if f in lfiles or fstandin in standins:
336 continue
337
338 # Append directory separator to avoid collisions
339 if not fstandin.endswith(os.sep):
340 fstandin += os.sep
341
342 # Prevalidate matching standin directories
343 if lfutil.any_(st for st in match._files if \
344 st.startswith(fstandin)):
345 continue
346 actualfiles.append(f)
347 match._files = actualfiles
348
349 def matchfn(f):
350 if orig_matchfn(f):
351 return f not in lfiles
352 else:
353 return f in standins
354
355 match.matchfn = matchfn
356 return orig(text=text, user=user, date=date, match=match,
357 force=force, editor=editor, extra=extra)
358 finally:
359 wlock.release()
360
361 def push(self, remote, force=False, revs=None, newbranch=False):
362 o = lfutil.findoutgoing(repo, remote, force)
363 if o:
364 toupload = set()
365 o = repo.changelog.nodesbetween(o, revs)[0]
366 for n in o:
367 parents = [p for p in repo.changelog.parents(n) if p != \
368 node.nullid]
369 ctx = repo[n]
370 files = set(ctx.files())
371 if len(parents) == 2:
372 mc = ctx.manifest()
373 mp1 = ctx.parents()[0].manifest()
374 mp2 = ctx.parents()[1].manifest()
375 for f in mp1:
376 if f not in mc:
377 files.add(f)
378 for f in mp2:
379 if f not in mc:
380 files.add(f)
381 for f in mc:
382 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f,
383 None):
384 files.add(f)
385
386 toupload = toupload.union(set([ctx[f].data().strip() for f\
387 in files if lfutil.isstandin(f) and f in ctx]))
388 lfcommands.uploadlfiles(ui, self, remote, toupload)
389 # Mercurial >= 1.6 takes the newbranch argument, try that first.
390 try:
391 return super(lfiles_repo, self).push(remote, force, revs,
392 newbranch)
393 except TypeError:
394 return super(lfiles_repo, self).push(remote, force, revs)
395
396 repo.__class__ = lfiles_repo
397
398 def checkrequireslfiles(ui, repo, **kwargs):
399 if 'largefiles' not in repo.requirements and lfutil.any_(
400 lfutil.shortname+'/' in f[0] for f in repo.store.datafiles()):
401 # work around bug in mercurial 1.9 whereby requirements is a list
402 # on newly-cloned repos
403 repo.requirements = set(repo.requirements)
404
405 repo.requirements |= set(['largefiles'])
406 repo._writerequirements()
407
408 checkrequireslfiles(ui, repo)
409
410 ui.setconfig('hooks', 'changegroup.lfiles', checkrequireslfiles)
411 ui.setconfig('hooks', 'commit.lfiles', checkrequireslfiles)
@@ -0,0 +1,125 b''
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 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
8
9 '''setup for largefiles extension: uisetup'''
10
11 from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \
12 httprepo, localrepo, sshrepo, sshserver, wireproto
13 from mercurial.i18n import _
14 from mercurial.hgweb import hgweb_mod, protocol
15
16 import overrides
17 import proto
18
19 def uisetup(ui):
20 # Disable auto-status for some commands which assume that all
21 # files in the result are under Mercurial's control
22
23 entry = extensions.wrapcommand(commands.table, 'add', overrides.override_add)
24 addopt = [('', 'large', None, _('add as largefile')),
25 ('', 'lfsize', '', _('add all files above this size (in megabytes)'
26 'as largefiles (default: 10)'))]
27 entry[1].extend(addopt)
28
29 entry = extensions.wrapcommand(commands.table, 'addremove',
30 overrides.override_addremove)
31 entry = extensions.wrapcommand(commands.table, 'remove', overrides.override_remove)
32 entry = extensions.wrapcommand(commands.table, 'forget', overrides.override_forget)
33 entry = extensions.wrapcommand(commands.table, 'status', overrides.override_status)
34 entry = extensions.wrapcommand(commands.table, 'log', overrides.override_log)
35 entry = extensions.wrapcommand(commands.table, 'rollback',
36 overrides.override_rollback)
37
38 entry = extensions.wrapcommand(commands.table, 'verify', overrides.override_verify)
39 verifyopt = [('', 'large', None, _('verify largefiles')),
40 ('', 'lfa', None,
41 _('verify all revisions of largefiles not just current')),
42 ('', 'lfc', None,
43 _('verify largefile contents not just existence'))]
44 entry[1].extend(verifyopt)
45
46 entry = extensions.wrapcommand(commands.table, 'outgoing',
47 overrides.override_outgoing)
48 outgoingopt = [('', 'large', None, _('display outgoing largefiles'))]
49 entry[1].extend(outgoingopt)
50 entry = extensions.wrapcommand(commands.table, 'summary', overrides.override_summary)
51 summaryopt = [('', 'large', None, _('display outgoing largefiles'))]
52 entry[1].extend(summaryopt)
53
54 entry = extensions.wrapcommand(commands.table, 'update', overrides.override_update)
55 entry = extensions.wrapcommand(commands.table, 'pull', overrides.override_pull)
56 entry = extensions.wrapfunction(filemerge, 'filemerge', overrides.override_filemerge)
57 entry = extensions.wrapfunction(cmdutil, 'copy', overrides.override_copy)
58
59 # Backout calls revert so we need to override both the command and the
60 # function
61 entry = extensions.wrapcommand(commands.table, 'revert', overrides.override_revert)
62 entry = extensions.wrapfunction(commands, 'revert', overrides.override_revert)
63
64 # clone uses hg._update instead of hg.update even though they are the
65 # same function... so wrap both of them)
66 extensions.wrapfunction(hg, 'update', overrides.hg_update)
67 extensions.wrapfunction(hg, '_update', overrides.hg_update)
68 extensions.wrapfunction(hg, 'clean', overrides.hg_clean)
69 extensions.wrapfunction(hg, 'merge', overrides.hg_merge)
70
71 extensions.wrapfunction(archival, 'archive', overrides.override_archive)
72 if hasattr(cmdutil, 'bailifchanged'):
73 extensions.wrapfunction(cmdutil, 'bailifchanged',
74 overrides.override_bailifchanged)
75 else:
76 extensions.wrapfunction(cmdutil, 'bail_if_changed',
77 overrides.override_bailifchanged)
78
79 # create the new wireproto commands ...
80 wireproto.commands['putlfile'] = (proto.putlfile, 'sha')
81 wireproto.commands['getlfile'] = (proto.getlfile, 'sha')
82 wireproto.commands['statlfile'] = (proto.statlfile, 'sha')
83
84 # ... and wrap some existing ones
85 wireproto.commands['capabilities'] = (proto.capabilities, '')
86 wireproto.commands['heads'] = (proto.heads, '')
87 wireproto.commands['lheads'] = (wireproto.heads, '')
88
89 # make putlfile behave the same as push and {get,stat}lfile behave the same
90 # as pull w.r.t. permissions checks
91 hgweb_mod.perms['putlfile'] = 'push'
92 hgweb_mod.perms['getlfile'] = 'pull'
93 hgweb_mod.perms['statlfile'] = 'pull'
94
95 # the hello wireproto command uses wireproto.capabilities, so it won't see
96 # our largefiles capability unless we replace the actual function as well.
97 proto.capabilities_orig = wireproto.capabilities
98 wireproto.capabilities = proto.capabilities
99
100 # these let us reject non-lfiles clients and make them display our error
101 # messages
102 protocol.webproto.refuseclient = proto.webproto_refuseclient
103 sshserver.sshserver.refuseclient = proto.sshproto_refuseclient
104
105 # can't do this in reposetup because it needs to have happened before
106 # wirerepo.__init__ is called
107 proto.ssh_oldcallstream = sshrepo.sshrepository._callstream
108 proto.http_oldcallstream = httprepo.httprepository._callstream
109 sshrepo.sshrepository._callstream = proto.sshrepo_callstream
110 httprepo.httprepository._callstream = proto.httprepo_callstream
111
112 # don't die on seeing a repo with the largefiles requirement
113 localrepo.localrepository.supported |= set(['largefiles'])
114
115 # override some extensions' stuff as well
116 for name, module in extensions.extensions():
117 if name == 'fetch':
118 extensions.wrapcommand(getattr(module, 'cmdtable'), 'fetch',
119 overrides.override_fetch)
120 if name == 'purge':
121 extensions.wrapcommand(getattr(module, 'cmdtable'), 'purge',
122 overrides.override_purge)
123 if name == 'rebase':
124 extensions.wrapcommand(getattr(module, 'cmdtable'), 'rebase',
125 overrides.override_rebase)
@@ -0,0 +1,51 b''
1 Largefiles allows for tracking large, incompressible binary files in Mercurial
2 without requiring excessive bandwidth for clones and pulls. Files added as
3 largefiles are not tracked directly by Mercurial; rather, their revisions are
4 identified by a checksum, and Mercurial tracks these checksums. This way, when
5 you clone a repository or pull in changesets, the large files in older
6 revisions of the repository are not needed, and only the ones needed to update
7 to the current version are downloaded. This saves both disk space and
8 bandwidth.
9
10 If you are starting a new repository or adding new large binary files, using
11 largefiles for them is as easy as adding '--large' to your hg add command. For
12 example:
13
14 $ dd if=/dev/urandom of=thisfileislarge count=2000
15 $ hg add --large thisfileislarge
16 $ hg commit -m 'add thisfileislarge, which is large, as a largefile'
17
18 When you push a changeset that affects largefiles to a remote repository, its
19 largefile revisions will be uploaded along with it. Note that the remote
20 Mercurial must also have the largefiles extension enabled for this to work.
21
22 When you pull a changeset that affects largefiles from a remote repository,
23 nothing different from Mercurial's normal behavior happens. However, when you
24 update to such a revision, any largefiles needed by that revision are
25 downloaded and cached if they have never been downloaded before. This means
26 that network access is required to update to revision you have not yet updated
27 to.
28
29 If you already have large files tracked by Mercurial without the largefiles
30 extension, you will need to convert your repository in order to benefit from
31 largefiles. This is done with the 'hg lfconvert' command:
32
33 $ hg lfconvert --size 10 oldrepo newrepo
34
35 By default, in repositories that already have largefiles in them, any new file
36 over 10MB will automatically be added as largefiles. To change this
37 threshhold, set [largefiles].size in your Mercurial config file to the minimum
38 size in megabytes to track as a largefile, or use the --lfsize option to the
39 add command (also in megabytes):
40
41 [largefiles]
42 size = 2
43
44 $ hg add --lfsize 2
45
46 The [largefiles].patterns config option allows you to specify specific
47 space-separated filename patterns (in shell glob syntax) that should always be
48 tracked as largefiles:
49
50 [largefiles]
51 pattens = *.jpg *.{png,bmp} library.zip content/audio/*
@@ -0,0 +1,29 b''
1 # Copyright 2010-2011 Fog Creek Software
2 #
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
5
6 '''largefile store working over mercurial's wire protocol'''
7
8 import lfutil
9 import remotestore
10
11 class wirestore(remotestore.remotestore):
12 def __init__(self, ui, repo, remote):
13 cap = remote.capable('largefiles')
14 if not cap:
15 raise lfutil.storeprotonotcapable([])
16 storetypes = cap.split(',')
17 if not 'serve' in storetypes:
18 raise lfutil.storeprotonotcapable(storetypes)
19 self.remote = remote
20 super(wirestore, self).__init__(ui, repo, remote.url())
21
22 def _put(self, hash, fd):
23 return self.remote.putlfile(hash, fd)
24
25 def _get(self, hash):
26 return self.remote.getlfile(hash)
27
28 def _stat(self, hash):
29 return self.remote.statlfile(hash)
General Comments 0
You need to be logged in to leave comments. Login now