##// END OF EJS Templates
largefiles: add some docstrings
Mads Kiilerich -
r28576:33bd9544 default
parent child Browse files
Show More
@@ -1,644 +1,655 b''
1 # Copyright 2009-2010 Gregory P. Ward
1 # Copyright 2009-2010 Gregory P. Ward
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 # Copyright 2010-2011 Fog Creek Software
3 # Copyright 2010-2011 Fog Creek Software
4 # Copyright 2010-2011 Unity Technologies
4 # Copyright 2010-2011 Unity Technologies
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 '''largefiles utility code: must not import other modules in this package.'''
9 '''largefiles utility code: must not import other modules in this package.'''
10
10
11 import os
11 import os
12 import platform
12 import platform
13 import stat
13 import stat
14 import copy
14 import copy
15
15
16 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
16 from mercurial import dirstate, httpconnection, match as match_, util, scmutil
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial import node, error
18 from mercurial import node, error
19
19
20 shortname = '.hglf'
20 shortname = '.hglf'
21 shortnameslash = shortname + '/'
21 shortnameslash = shortname + '/'
22 longname = 'largefiles'
22 longname = 'largefiles'
23
23
24
24
25 # -- Private worker functions ------------------------------------------
25 # -- Private worker functions ------------------------------------------
26
26
27 def getminsize(ui, assumelfiles, opt, default=10):
27 def getminsize(ui, assumelfiles, opt, default=10):
28 lfsize = opt
28 lfsize = opt
29 if not lfsize and assumelfiles:
29 if not lfsize and assumelfiles:
30 lfsize = ui.config(longname, 'minsize', default=default)
30 lfsize = ui.config(longname, 'minsize', default=default)
31 if lfsize:
31 if lfsize:
32 try:
32 try:
33 lfsize = float(lfsize)
33 lfsize = float(lfsize)
34 except ValueError:
34 except ValueError:
35 raise error.Abort(_('largefiles: size must be number (not %s)\n')
35 raise error.Abort(_('largefiles: size must be number (not %s)\n')
36 % lfsize)
36 % lfsize)
37 if lfsize is None:
37 if lfsize is None:
38 raise error.Abort(_('minimum size for largefiles must be specified'))
38 raise error.Abort(_('minimum size for largefiles must be specified'))
39 return lfsize
39 return lfsize
40
40
41 def link(src, dest):
41 def link(src, dest):
42 """Try to create hardlink - if that fails, efficiently make a copy."""
42 util.makedirs(os.path.dirname(dest))
43 util.makedirs(os.path.dirname(dest))
43 try:
44 try:
44 util.oslink(src, dest)
45 util.oslink(src, dest)
45 except OSError:
46 except OSError:
46 # if hardlinks fail, fallback on atomic copy
47 # if hardlinks fail, fallback on atomic copy
47 dst = util.atomictempfile(dest)
48 dst = util.atomictempfile(dest)
48 for chunk in util.filechunkiter(open(src, 'rb')):
49 for chunk in util.filechunkiter(open(src, 'rb')):
49 dst.write(chunk)
50 dst.write(chunk)
50 dst.close()
51 dst.close()
51 os.chmod(dest, os.stat(src).st_mode)
52 os.chmod(dest, os.stat(src).st_mode)
52
53
53 def usercachepath(ui, hash):
54 def usercachepath(ui, hash):
54 '''Return the correct location in the "global" largefiles cache for a file
55 '''Return the correct location in the "global" largefiles cache for a file
55 with the given hash.
56 with the given hash.
56 This cache is used for sharing of largefiles across repositories - both
57 This cache is used for sharing of largefiles across repositories - both
57 to preserve download bandwidth and storage space.'''
58 to preserve download bandwidth and storage space.'''
58 return os.path.join(_usercachedir(ui), hash)
59 return os.path.join(_usercachedir(ui), hash)
59
60
60 def _usercachedir(ui):
61 def _usercachedir(ui):
61 '''Return the location of the "global" largefiles cache.'''
62 '''Return the location of the "global" largefiles cache.'''
62 path = ui.configpath(longname, 'usercache', None)
63 path = ui.configpath(longname, 'usercache', None)
63 if path:
64 if path:
64 return path
65 return path
65 if os.name == 'nt':
66 if os.name == 'nt':
66 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
67 appdata = os.getenv('LOCALAPPDATA', os.getenv('APPDATA'))
67 if appdata:
68 if appdata:
68 return os.path.join(appdata, longname)
69 return os.path.join(appdata, longname)
69 elif platform.system() == 'Darwin':
70 elif platform.system() == 'Darwin':
70 home = os.getenv('HOME')
71 home = os.getenv('HOME')
71 if home:
72 if home:
72 return os.path.join(home, 'Library', 'Caches', longname)
73 return os.path.join(home, 'Library', 'Caches', longname)
73 elif os.name == 'posix':
74 elif os.name == 'posix':
74 path = os.getenv('XDG_CACHE_HOME')
75 path = os.getenv('XDG_CACHE_HOME')
75 if path:
76 if path:
76 return os.path.join(path, longname)
77 return os.path.join(path, longname)
77 home = os.getenv('HOME')
78 home = os.getenv('HOME')
78 if home:
79 if home:
79 return os.path.join(home, '.cache', longname)
80 return os.path.join(home, '.cache', longname)
80 else:
81 else:
81 raise error.Abort(_('unknown operating system: %s\n') % os.name)
82 raise error.Abort(_('unknown operating system: %s\n') % os.name)
82 raise error.Abort(_('unknown %s usercache location\n') % longname)
83 raise error.Abort(_('unknown %s usercache location\n') % longname)
83
84
84 def inusercache(ui, hash):
85 def inusercache(ui, hash):
85 path = usercachepath(ui, hash)
86 path = usercachepath(ui, hash)
86 return os.path.exists(path)
87 return os.path.exists(path)
87
88
88 def findfile(repo, hash):
89 def findfile(repo, hash):
90 '''Return store path of the largefile with the specified hash.
91 As a side effect, the file might be linked from user cache.
92 Return None if the file can't be found locally.'''
89 path, exists = findstorepath(repo, hash)
93 path, exists = findstorepath(repo, hash)
90 if exists:
94 if exists:
91 repo.ui.note(_('found %s in store\n') % hash)
95 repo.ui.note(_('found %s in store\n') % hash)
92 return path
96 return path
93 elif inusercache(repo.ui, hash):
97 elif inusercache(repo.ui, hash):
94 repo.ui.note(_('found %s in system cache\n') % hash)
98 repo.ui.note(_('found %s in system cache\n') % hash)
95 path = storepath(repo, hash)
99 path = storepath(repo, hash)
96 link(usercachepath(repo.ui, hash), path)
100 link(usercachepath(repo.ui, hash), path)
97 return path
101 return path
98 return None
102 return None
99
103
100 class largefilesdirstate(dirstate.dirstate):
104 class largefilesdirstate(dirstate.dirstate):
101 def __getitem__(self, key):
105 def __getitem__(self, key):
102 return super(largefilesdirstate, self).__getitem__(unixpath(key))
106 return super(largefilesdirstate, self).__getitem__(unixpath(key))
103 def normal(self, f):
107 def normal(self, f):
104 return super(largefilesdirstate, self).normal(unixpath(f))
108 return super(largefilesdirstate, self).normal(unixpath(f))
105 def remove(self, f):
109 def remove(self, f):
106 return super(largefilesdirstate, self).remove(unixpath(f))
110 return super(largefilesdirstate, self).remove(unixpath(f))
107 def add(self, f):
111 def add(self, f):
108 return super(largefilesdirstate, self).add(unixpath(f))
112 return super(largefilesdirstate, self).add(unixpath(f))
109 def drop(self, f):
113 def drop(self, f):
110 return super(largefilesdirstate, self).drop(unixpath(f))
114 return super(largefilesdirstate, self).drop(unixpath(f))
111 def forget(self, f):
115 def forget(self, f):
112 return super(largefilesdirstate, self).forget(unixpath(f))
116 return super(largefilesdirstate, self).forget(unixpath(f))
113 def normallookup(self, f):
117 def normallookup(self, f):
114 return super(largefilesdirstate, self).normallookup(unixpath(f))
118 return super(largefilesdirstate, self).normallookup(unixpath(f))
115 def _ignore(self, f):
119 def _ignore(self, f):
116 return False
120 return False
117 def write(self, tr=False):
121 def write(self, tr=False):
118 # (1) disable PENDING mode always
122 # (1) disable PENDING mode always
119 # (lfdirstate isn't yet managed as a part of the transaction)
123 # (lfdirstate isn't yet managed as a part of the transaction)
120 # (2) avoid develwarn 'use dirstate.write with ....'
124 # (2) avoid develwarn 'use dirstate.write with ....'
121 super(largefilesdirstate, self).write(None)
125 super(largefilesdirstate, self).write(None)
122
126
123 def openlfdirstate(ui, repo, create=True):
127 def openlfdirstate(ui, repo, create=True):
124 '''
128 '''
125 Return a dirstate object that tracks largefiles: i.e. its root is
129 Return a dirstate object that tracks largefiles: i.e. its root is
126 the repo root, but it is saved in .hg/largefiles/dirstate.
130 the repo root, but it is saved in .hg/largefiles/dirstate.
127 '''
131 '''
128 vfs = repo.vfs
132 vfs = repo.vfs
129 lfstoredir = longname
133 lfstoredir = longname
130 opener = scmutil.opener(vfs.join(lfstoredir))
134 opener = scmutil.opener(vfs.join(lfstoredir))
131 lfdirstate = largefilesdirstate(opener, ui, repo.root,
135 lfdirstate = largefilesdirstate(opener, ui, repo.root,
132 repo.dirstate._validate)
136 repo.dirstate._validate)
133
137
134 # If the largefiles dirstate does not exist, populate and create
138 # If the largefiles dirstate does not exist, populate and create
135 # it. This ensures that we create it on the first meaningful
139 # it. This ensures that we create it on the first meaningful
136 # largefiles operation in a new clone.
140 # largefiles operation in a new clone.
137 if create and not vfs.exists(vfs.join(lfstoredir, 'dirstate')):
141 if create and not vfs.exists(vfs.join(lfstoredir, 'dirstate')):
138 matcher = getstandinmatcher(repo)
142 matcher = getstandinmatcher(repo)
139 standins = repo.dirstate.walk(matcher, [], False, False)
143 standins = repo.dirstate.walk(matcher, [], False, False)
140
144
141 if len(standins) > 0:
145 if len(standins) > 0:
142 vfs.makedirs(lfstoredir)
146 vfs.makedirs(lfstoredir)
143
147
144 for standin in standins:
148 for standin in standins:
145 lfile = splitstandin(standin)
149 lfile = splitstandin(standin)
146 lfdirstate.normallookup(lfile)
150 lfdirstate.normallookup(lfile)
147 return lfdirstate
151 return lfdirstate
148
152
149 def lfdirstatestatus(lfdirstate, repo):
153 def lfdirstatestatus(lfdirstate, repo):
150 wctx = repo['.']
154 wctx = repo['.']
151 match = match_.always(repo.root, repo.getcwd())
155 match = match_.always(repo.root, repo.getcwd())
152 unsure, s = lfdirstate.status(match, [], False, False, False)
156 unsure, s = lfdirstate.status(match, [], False, False, False)
153 modified, clean = s.modified, s.clean
157 modified, clean = s.modified, s.clean
154 for lfile in unsure:
158 for lfile in unsure:
155 try:
159 try:
156 fctx = wctx[standin(lfile)]
160 fctx = wctx[standin(lfile)]
157 except LookupError:
161 except LookupError:
158 fctx = None
162 fctx = None
159 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
163 if not fctx or fctx.data().strip() != hashfile(repo.wjoin(lfile)):
160 modified.append(lfile)
164 modified.append(lfile)
161 else:
165 else:
162 clean.append(lfile)
166 clean.append(lfile)
163 lfdirstate.normal(lfile)
167 lfdirstate.normal(lfile)
164 return s
168 return s
165
169
166 def listlfiles(repo, rev=None, matcher=None):
170 def listlfiles(repo, rev=None, matcher=None):
167 '''return a list of largefiles in the working copy or the
171 '''return a list of largefiles in the working copy or the
168 specified changeset'''
172 specified changeset'''
169
173
170 if matcher is None:
174 if matcher is None:
171 matcher = getstandinmatcher(repo)
175 matcher = getstandinmatcher(repo)
172
176
173 # ignore unknown files in working directory
177 # ignore unknown files in working directory
174 return [splitstandin(f)
178 return [splitstandin(f)
175 for f in repo[rev].walk(matcher)
179 for f in repo[rev].walk(matcher)
176 if rev is not None or repo.dirstate[f] != '?']
180 if rev is not None or repo.dirstate[f] != '?']
177
181
178 def instore(repo, hash, forcelocal=False):
182 def instore(repo, hash, forcelocal=False):
183 '''Return true if a largefile with the given hash exists in the user
184 cache.'''
179 return os.path.exists(storepath(repo, hash, forcelocal))
185 return os.path.exists(storepath(repo, hash, forcelocal))
180
186
181 def storepath(repo, hash, forcelocal=False):
187 def storepath(repo, hash, forcelocal=False):
188 '''Return the correct location in the repository largefiles cache for a
189 file with the given hash.'''
182 if not forcelocal and repo.shared():
190 if not forcelocal and repo.shared():
183 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
191 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
184 return repo.join(longname, hash)
192 return repo.join(longname, hash)
185
193
186 def findstorepath(repo, hash):
194 def findstorepath(repo, hash):
187 '''Search through the local store path(s) to find the file for the given
195 '''Search through the local store path(s) to find the file for the given
188 hash. If the file is not found, its path in the primary store is returned.
196 hash. If the file is not found, its path in the primary store is returned.
189 The return value is a tuple of (path, exists(path)).
197 The return value is a tuple of (path, exists(path)).
190 '''
198 '''
191 # For shared repos, the primary store is in the share source. But for
199 # For shared repos, the primary store is in the share source. But for
192 # backward compatibility, force a lookup in the local store if it wasn't
200 # backward compatibility, force a lookup in the local store if it wasn't
193 # found in the share source.
201 # found in the share source.
194 path = storepath(repo, hash, False)
202 path = storepath(repo, hash, False)
195
203
196 if instore(repo, hash):
204 if instore(repo, hash):
197 return (path, True)
205 return (path, True)
198 elif repo.shared() and instore(repo, hash, True):
206 elif repo.shared() and instore(repo, hash, True):
199 return storepath(repo, hash, True)
207 return storepath(repo, hash, True)
200
208
201 return (path, False)
209 return (path, False)
202
210
203 def copyfromcache(repo, hash, filename):
211 def copyfromcache(repo, hash, filename):
204 '''Copy the specified largefile from the repo or system cache to
212 '''Copy the specified largefile from the repo or system cache to
205 filename in the repository. Return true on success or false if the
213 filename in the repository. Return true on success or false if the
206 file was not found in either cache (which should not happened:
214 file was not found in either cache (which should not happened:
207 this is meant to be called only after ensuring that the needed
215 this is meant to be called only after ensuring that the needed
208 largefile exists in the cache).'''
216 largefile exists in the cache).'''
209 wvfs = repo.wvfs
217 wvfs = repo.wvfs
210 path = findfile(repo, hash)
218 path = findfile(repo, hash)
211 if path is None:
219 if path is None:
212 return False
220 return False
213 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
221 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
214 # The write may fail before the file is fully written, but we
222 # The write may fail before the file is fully written, but we
215 # don't use atomic writes in the working copy.
223 # don't use atomic writes in the working copy.
216 with open(path, 'rb') as srcfd:
224 with open(path, 'rb') as srcfd:
217 with wvfs(filename, 'wb') as destfd:
225 with wvfs(filename, 'wb') as destfd:
218 gothash = copyandhash(srcfd, destfd)
226 gothash = copyandhash(srcfd, destfd)
219 if gothash != hash:
227 if gothash != hash:
220 repo.ui.warn(_('%s: data corruption in %s with hash %s\n')
228 repo.ui.warn(_('%s: data corruption in %s with hash %s\n')
221 % (filename, path, gothash))
229 % (filename, path, gothash))
222 wvfs.unlink(filename)
230 wvfs.unlink(filename)
223 return False
231 return False
224 return True
232 return True
225
233
226 def copytostore(repo, rev, file, uploaded=False):
234 def copytostore(repo, rev, file, uploaded=False):
227 wvfs = repo.wvfs
235 wvfs = repo.wvfs
228 hash = readstandin(repo, file, rev)
236 hash = readstandin(repo, file, rev)
229 if instore(repo, hash):
237 if instore(repo, hash):
230 return
238 return
231 if wvfs.exists(file):
239 if wvfs.exists(file):
232 copytostoreabsolute(repo, wvfs.join(file), hash)
240 copytostoreabsolute(repo, wvfs.join(file), hash)
233 else:
241 else:
234 repo.ui.warn(_("%s: largefile %s not available from local store\n") %
242 repo.ui.warn(_("%s: largefile %s not available from local store\n") %
235 (file, hash))
243 (file, hash))
236
244
237 def copyalltostore(repo, node):
245 def copyalltostore(repo, node):
238 '''Copy all largefiles in a given revision to the store'''
246 '''Copy all largefiles in a given revision to the store'''
239
247
240 ctx = repo[node]
248 ctx = repo[node]
241 for filename in ctx.files():
249 for filename in ctx.files():
242 if isstandin(filename) and filename in ctx.manifest():
250 if isstandin(filename) and filename in ctx.manifest():
243 realfile = splitstandin(filename)
251 realfile = splitstandin(filename)
244 copytostore(repo, ctx.node(), realfile)
252 copytostore(repo, ctx.node(), realfile)
245
253
246
254
247 def copytostoreabsolute(repo, file, hash):
255 def copytostoreabsolute(repo, file, hash):
248 if inusercache(repo.ui, hash):
256 if inusercache(repo.ui, hash):
249 link(usercachepath(repo.ui, hash), storepath(repo, hash))
257 link(usercachepath(repo.ui, hash), storepath(repo, hash))
250 else:
258 else:
251 util.makedirs(os.path.dirname(storepath(repo, hash)))
259 util.makedirs(os.path.dirname(storepath(repo, hash)))
252 dst = util.atomictempfile(storepath(repo, hash),
260 dst = util.atomictempfile(storepath(repo, hash),
253 createmode=repo.store.createmode)
261 createmode=repo.store.createmode)
254 for chunk in util.filechunkiter(open(file, 'rb')):
262 for chunk in util.filechunkiter(open(file, 'rb')):
255 dst.write(chunk)
263 dst.write(chunk)
256 dst.close()
264 dst.close()
257 linktousercache(repo, hash)
265 linktousercache(repo, hash)
258
266
259 def linktousercache(repo, hash):
267 def linktousercache(repo, hash):
268 '''Link / copy the largefile with the specified hash from the store
269 to the cache.'''
260 path = usercachepath(repo.ui, hash)
270 path = usercachepath(repo.ui, hash)
261 link(storepath(repo, hash), path)
271 link(storepath(repo, hash), path)
262
272
263 def getstandinmatcher(repo, rmatcher=None):
273 def getstandinmatcher(repo, rmatcher=None):
264 '''Return a match object that applies rmatcher to the standin directory'''
274 '''Return a match object that applies rmatcher to the standin directory'''
265 wvfs = repo.wvfs
275 wvfs = repo.wvfs
266 standindir = shortname
276 standindir = shortname
267
277
268 # no warnings about missing files or directories
278 # no warnings about missing files or directories
269 badfn = lambda f, msg: None
279 badfn = lambda f, msg: None
270
280
271 if rmatcher and not rmatcher.always():
281 if rmatcher and not rmatcher.always():
272 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
282 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
273 if not pats:
283 if not pats:
274 pats = [wvfs.join(standindir)]
284 pats = [wvfs.join(standindir)]
275 match = scmutil.match(repo[None], pats, badfn=badfn)
285 match = scmutil.match(repo[None], pats, badfn=badfn)
276 # if pats is empty, it would incorrectly always match, so clear _always
286 # if pats is empty, it would incorrectly always match, so clear _always
277 match._always = False
287 match._always = False
278 else:
288 else:
279 # no patterns: relative to repo root
289 # no patterns: relative to repo root
280 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
290 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
281 return match
291 return match
282
292
283 def composestandinmatcher(repo, rmatcher):
293 def composestandinmatcher(repo, rmatcher):
284 '''Return a matcher that accepts standins corresponding to the
294 '''Return a matcher that accepts standins corresponding to the
285 files accepted by rmatcher. Pass the list of files in the matcher
295 files accepted by rmatcher. Pass the list of files in the matcher
286 as the paths specified by the user.'''
296 as the paths specified by the user.'''
287 smatcher = getstandinmatcher(repo, rmatcher)
297 smatcher = getstandinmatcher(repo, rmatcher)
288 isstandin = smatcher.matchfn
298 isstandin = smatcher.matchfn
289 def composedmatchfn(f):
299 def composedmatchfn(f):
290 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
300 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
291 smatcher.matchfn = composedmatchfn
301 smatcher.matchfn = composedmatchfn
292
302
293 return smatcher
303 return smatcher
294
304
295 def standin(filename):
305 def standin(filename):
296 '''Return the repo-relative path to the standin for the specified big
306 '''Return the repo-relative path to the standin for the specified big
297 file.'''
307 file.'''
298 # Notes:
308 # Notes:
299 # 1) Some callers want an absolute path, but for instance addlargefiles
309 # 1) Some callers want an absolute path, but for instance addlargefiles
300 # needs it repo-relative so it can be passed to repo[None].add(). So
310 # needs it repo-relative so it can be passed to repo[None].add(). So
301 # leave it up to the caller to use repo.wjoin() to get an absolute path.
311 # leave it up to the caller to use repo.wjoin() to get an absolute path.
302 # 2) Join with '/' because that's what dirstate always uses, even on
312 # 2) Join with '/' because that's what dirstate always uses, even on
303 # Windows. Change existing separator to '/' first in case we are
313 # Windows. Change existing separator to '/' first in case we are
304 # passed filenames from an external source (like the command line).
314 # passed filenames from an external source (like the command line).
305 return shortnameslash + util.pconvert(filename)
315 return shortnameslash + util.pconvert(filename)
306
316
307 def isstandin(filename):
317 def isstandin(filename):
308 '''Return true if filename is a big file standin. filename must be
318 '''Return true if filename is a big file standin. filename must be
309 in Mercurial's internal form (slash-separated).'''
319 in Mercurial's internal form (slash-separated).'''
310 return filename.startswith(shortnameslash)
320 return filename.startswith(shortnameslash)
311
321
312 def splitstandin(filename):
322 def splitstandin(filename):
313 # Split on / because that's what dirstate always uses, even on Windows.
323 # Split on / because that's what dirstate always uses, even on Windows.
314 # Change local separator to / first just in case we are passed filenames
324 # Change local separator to / first just in case we are passed filenames
315 # from an external source (like the command line).
325 # from an external source (like the command line).
316 bits = util.pconvert(filename).split('/', 1)
326 bits = util.pconvert(filename).split('/', 1)
317 if len(bits) == 2 and bits[0] == shortname:
327 if len(bits) == 2 and bits[0] == shortname:
318 return bits[1]
328 return bits[1]
319 else:
329 else:
320 return None
330 return None
321
331
322 def updatestandin(repo, standin):
332 def updatestandin(repo, standin):
323 file = repo.wjoin(splitstandin(standin))
333 file = repo.wjoin(splitstandin(standin))
324 if repo.wvfs.exists(splitstandin(standin)):
334 if repo.wvfs.exists(splitstandin(standin)):
325 hash = hashfile(file)
335 hash = hashfile(file)
326 executable = getexecutable(file)
336 executable = getexecutable(file)
327 writestandin(repo, standin, hash, executable)
337 writestandin(repo, standin, hash, executable)
328 else:
338 else:
329 raise error.Abort(_('%s: file not found!') % splitstandin(standin))
339 raise error.Abort(_('%s: file not found!') % splitstandin(standin))
330
340
331 def readstandin(repo, filename, node=None):
341 def readstandin(repo, filename, node=None):
332 '''read hex hash from standin for filename at given node, or working
342 '''read hex hash from standin for filename at given node, or working
333 directory if no node is given'''
343 directory if no node is given'''
334 return repo[node][standin(filename)].data().strip()
344 return repo[node][standin(filename)].data().strip()
335
345
336 def writestandin(repo, standin, hash, executable):
346 def writestandin(repo, standin, hash, executable):
337 '''write hash to <repo.root>/<standin>'''
347 '''write hash to <repo.root>/<standin>'''
338 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
348 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
339
349
340 def copyandhash(instream, outfile):
350 def copyandhash(instream, outfile):
341 '''Read bytes from instream (iterable) and write them to outfile,
351 '''Read bytes from instream (iterable) and write them to outfile,
342 computing the SHA-1 hash of the data along the way. Return the hash.'''
352 computing the SHA-1 hash of the data along the way. Return the hash.'''
343 hasher = util.sha1('')
353 hasher = util.sha1('')
344 for data in instream:
354 for data in instream:
345 hasher.update(data)
355 hasher.update(data)
346 outfile.write(data)
356 outfile.write(data)
347 return hasher.hexdigest()
357 return hasher.hexdigest()
348
358
349 def hashrepofile(repo, file):
359 def hashrepofile(repo, file):
350 return hashfile(repo.wjoin(file))
360 return hashfile(repo.wjoin(file))
351
361
352 def hashfile(file):
362 def hashfile(file):
353 if not os.path.exists(file):
363 if not os.path.exists(file):
354 return ''
364 return ''
355 hasher = util.sha1('')
365 hasher = util.sha1('')
356 fd = open(file, 'rb')
366 fd = open(file, 'rb')
357 for data in util.filechunkiter(fd, 128 * 1024):
367 for data in util.filechunkiter(fd, 128 * 1024):
358 hasher.update(data)
368 hasher.update(data)
359 fd.close()
369 fd.close()
360 return hasher.hexdigest()
370 return hasher.hexdigest()
361
371
362 def getexecutable(filename):
372 def getexecutable(filename):
363 mode = os.stat(filename).st_mode
373 mode = os.stat(filename).st_mode
364 return ((mode & stat.S_IXUSR) and
374 return ((mode & stat.S_IXUSR) and
365 (mode & stat.S_IXGRP) and
375 (mode & stat.S_IXGRP) and
366 (mode & stat.S_IXOTH))
376 (mode & stat.S_IXOTH))
367
377
368 def urljoin(first, second, *arg):
378 def urljoin(first, second, *arg):
369 def join(left, right):
379 def join(left, right):
370 if not left.endswith('/'):
380 if not left.endswith('/'):
371 left += '/'
381 left += '/'
372 if right.startswith('/'):
382 if right.startswith('/'):
373 right = right[1:]
383 right = right[1:]
374 return left + right
384 return left + right
375
385
376 url = join(first, second)
386 url = join(first, second)
377 for a in arg:
387 for a in arg:
378 url = join(url, a)
388 url = join(url, a)
379 return url
389 return url
380
390
381 def hexsha1(data):
391 def hexsha1(data):
382 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
392 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
383 object data"""
393 object data"""
384 h = util.sha1()
394 h = util.sha1()
385 for chunk in util.filechunkiter(data):
395 for chunk in util.filechunkiter(data):
386 h.update(chunk)
396 h.update(chunk)
387 return h.hexdigest()
397 return h.hexdigest()
388
398
389 def httpsendfile(ui, filename):
399 def httpsendfile(ui, filename):
390 return httpconnection.httpsendfile(ui, filename, 'rb')
400 return httpconnection.httpsendfile(ui, filename, 'rb')
391
401
392 def unixpath(path):
402 def unixpath(path):
393 '''Return a version of path normalized for use with the lfdirstate.'''
403 '''Return a version of path normalized for use with the lfdirstate.'''
394 return util.pconvert(os.path.normpath(path))
404 return util.pconvert(os.path.normpath(path))
395
405
396 def islfilesrepo(repo):
406 def islfilesrepo(repo):
407 '''Return true if the repo is a largefile repo.'''
397 if ('largefiles' in repo.requirements and
408 if ('largefiles' in repo.requirements and
398 any(shortnameslash in f[0] for f in repo.store.datafiles())):
409 any(shortnameslash in f[0] for f in repo.store.datafiles())):
399 return True
410 return True
400
411
401 return any(openlfdirstate(repo.ui, repo, False))
412 return any(openlfdirstate(repo.ui, repo, False))
402
413
403 class storeprotonotcapable(Exception):
414 class storeprotonotcapable(Exception):
404 def __init__(self, storetypes):
415 def __init__(self, storetypes):
405 self.storetypes = storetypes
416 self.storetypes = storetypes
406
417
407 def getstandinsstate(repo):
418 def getstandinsstate(repo):
408 standins = []
419 standins = []
409 matcher = getstandinmatcher(repo)
420 matcher = getstandinmatcher(repo)
410 for standin in repo.dirstate.walk(matcher, [], False, False):
421 for standin in repo.dirstate.walk(matcher, [], False, False):
411 lfile = splitstandin(standin)
422 lfile = splitstandin(standin)
412 try:
423 try:
413 hash = readstandin(repo, lfile)
424 hash = readstandin(repo, lfile)
414 except IOError:
425 except IOError:
415 hash = None
426 hash = None
416 standins.append((lfile, hash))
427 standins.append((lfile, hash))
417 return standins
428 return standins
418
429
419 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
430 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
420 lfstandin = standin(lfile)
431 lfstandin = standin(lfile)
421 if lfstandin in repo.dirstate:
432 if lfstandin in repo.dirstate:
422 stat = repo.dirstate._map[lfstandin]
433 stat = repo.dirstate._map[lfstandin]
423 state, mtime = stat[0], stat[3]
434 state, mtime = stat[0], stat[3]
424 else:
435 else:
425 state, mtime = '?', -1
436 state, mtime = '?', -1
426 if state == 'n':
437 if state == 'n':
427 if (normallookup or mtime < 0 or
438 if (normallookup or mtime < 0 or
428 not repo.wvfs.exists(lfile)):
439 not repo.wvfs.exists(lfile)):
429 # state 'n' doesn't ensure 'clean' in this case
440 # state 'n' doesn't ensure 'clean' in this case
430 lfdirstate.normallookup(lfile)
441 lfdirstate.normallookup(lfile)
431 else:
442 else:
432 lfdirstate.normal(lfile)
443 lfdirstate.normal(lfile)
433 elif state == 'm':
444 elif state == 'm':
434 lfdirstate.normallookup(lfile)
445 lfdirstate.normallookup(lfile)
435 elif state == 'r':
446 elif state == 'r':
436 lfdirstate.remove(lfile)
447 lfdirstate.remove(lfile)
437 elif state == 'a':
448 elif state == 'a':
438 lfdirstate.add(lfile)
449 lfdirstate.add(lfile)
439 elif state == '?':
450 elif state == '?':
440 lfdirstate.drop(lfile)
451 lfdirstate.drop(lfile)
441
452
442 def markcommitted(orig, ctx, node):
453 def markcommitted(orig, ctx, node):
443 repo = ctx.repo()
454 repo = ctx.repo()
444
455
445 orig(node)
456 orig(node)
446
457
447 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
458 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
448 # because files coming from the 2nd parent are omitted in the latter.
459 # because files coming from the 2nd parent are omitted in the latter.
449 #
460 #
450 # The former should be used to get targets of "synclfdirstate",
461 # The former should be used to get targets of "synclfdirstate",
451 # because such files:
462 # because such files:
452 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
463 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
453 # - have to be marked as "n" after commit, but
464 # - have to be marked as "n" after commit, but
454 # - aren't listed in "repo[node].files()"
465 # - aren't listed in "repo[node].files()"
455
466
456 lfdirstate = openlfdirstate(repo.ui, repo)
467 lfdirstate = openlfdirstate(repo.ui, repo)
457 for f in ctx.files():
468 for f in ctx.files():
458 if isstandin(f):
469 if isstandin(f):
459 lfile = splitstandin(f)
470 lfile = splitstandin(f)
460 synclfdirstate(repo, lfdirstate, lfile, False)
471 synclfdirstate(repo, lfdirstate, lfile, False)
461 lfdirstate.write()
472 lfdirstate.write()
462
473
463 # As part of committing, copy all of the largefiles into the cache.
474 # As part of committing, copy all of the largefiles into the cache.
464 copyalltostore(repo, node)
475 copyalltostore(repo, node)
465
476
466 def getlfilestoupdate(oldstandins, newstandins):
477 def getlfilestoupdate(oldstandins, newstandins):
467 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
478 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
468 filelist = []
479 filelist = []
469 for f in changedstandins:
480 for f in changedstandins:
470 if f[0] not in filelist:
481 if f[0] not in filelist:
471 filelist.append(f[0])
482 filelist.append(f[0])
472 return filelist
483 return filelist
473
484
474 def getlfilestoupload(repo, missing, addfunc):
485 def getlfilestoupload(repo, missing, addfunc):
475 for i, n in enumerate(missing):
486 for i, n in enumerate(missing):
476 repo.ui.progress(_('finding outgoing largefiles'), i,
487 repo.ui.progress(_('finding outgoing largefiles'), i,
477 unit=_('revisions'), total=len(missing))
488 unit=_('revisions'), total=len(missing))
478 parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
489 parents = [p for p in repo.changelog.parents(n) if p != node.nullid]
479
490
480 oldlfstatus = repo.lfstatus
491 oldlfstatus = repo.lfstatus
481 repo.lfstatus = False
492 repo.lfstatus = False
482 try:
493 try:
483 ctx = repo[n]
494 ctx = repo[n]
484 finally:
495 finally:
485 repo.lfstatus = oldlfstatus
496 repo.lfstatus = oldlfstatus
486
497
487 files = set(ctx.files())
498 files = set(ctx.files())
488 if len(parents) == 2:
499 if len(parents) == 2:
489 mc = ctx.manifest()
500 mc = ctx.manifest()
490 mp1 = ctx.parents()[0].manifest()
501 mp1 = ctx.parents()[0].manifest()
491 mp2 = ctx.parents()[1].manifest()
502 mp2 = ctx.parents()[1].manifest()
492 for f in mp1:
503 for f in mp1:
493 if f not in mc:
504 if f not in mc:
494 files.add(f)
505 files.add(f)
495 for f in mp2:
506 for f in mp2:
496 if f not in mc:
507 if f not in mc:
497 files.add(f)
508 files.add(f)
498 for f in mc:
509 for f in mc:
499 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
510 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
500 files.add(f)
511 files.add(f)
501 for fn in files:
512 for fn in files:
502 if isstandin(fn) and fn in ctx:
513 if isstandin(fn) and fn in ctx:
503 addfunc(fn, ctx[fn].data().strip())
514 addfunc(fn, ctx[fn].data().strip())
504 repo.ui.progress(_('finding outgoing largefiles'), None)
515 repo.ui.progress(_('finding outgoing largefiles'), None)
505
516
506 def updatestandinsbymatch(repo, match):
517 def updatestandinsbymatch(repo, match):
507 '''Update standins in the working directory according to specified match
518 '''Update standins in the working directory according to specified match
508
519
509 This returns (possibly modified) ``match`` object to be used for
520 This returns (possibly modified) ``match`` object to be used for
510 subsequent commit process.
521 subsequent commit process.
511 '''
522 '''
512
523
513 ui = repo.ui
524 ui = repo.ui
514
525
515 # Case 1: user calls commit with no specific files or
526 # Case 1: user calls commit with no specific files or
516 # include/exclude patterns: refresh and commit all files that
527 # include/exclude patterns: refresh and commit all files that
517 # are "dirty".
528 # are "dirty".
518 if match is None or match.always():
529 if match is None or match.always():
519 # Spend a bit of time here to get a list of files we know
530 # Spend a bit of time here to get a list of files we know
520 # are modified so we can compare only against those.
531 # are modified so we can compare only against those.
521 # It can cost a lot of time (several seconds)
532 # It can cost a lot of time (several seconds)
522 # otherwise to update all standins if the largefiles are
533 # otherwise to update all standins if the largefiles are
523 # large.
534 # large.
524 lfdirstate = openlfdirstate(ui, repo)
535 lfdirstate = openlfdirstate(ui, repo)
525 dirtymatch = match_.always(repo.root, repo.getcwd())
536 dirtymatch = match_.always(repo.root, repo.getcwd())
526 unsure, s = lfdirstate.status(dirtymatch, [], False, False,
537 unsure, s = lfdirstate.status(dirtymatch, [], False, False,
527 False)
538 False)
528 modifiedfiles = unsure + s.modified + s.added + s.removed
539 modifiedfiles = unsure + s.modified + s.added + s.removed
529 lfiles = listlfiles(repo)
540 lfiles = listlfiles(repo)
530 # this only loops through largefiles that exist (not
541 # this only loops through largefiles that exist (not
531 # removed/renamed)
542 # removed/renamed)
532 for lfile in lfiles:
543 for lfile in lfiles:
533 if lfile in modifiedfiles:
544 if lfile in modifiedfiles:
534 if repo.wvfs.exists(standin(lfile)):
545 if repo.wvfs.exists(standin(lfile)):
535 # this handles the case where a rebase is being
546 # this handles the case where a rebase is being
536 # performed and the working copy is not updated
547 # performed and the working copy is not updated
537 # yet.
548 # yet.
538 if repo.wvfs.exists(lfile):
549 if repo.wvfs.exists(lfile):
539 updatestandin(repo,
550 updatestandin(repo,
540 standin(lfile))
551 standin(lfile))
541
552
542 return match
553 return match
543
554
544 lfiles = listlfiles(repo)
555 lfiles = listlfiles(repo)
545 match._files = repo._subdirlfs(match.files(), lfiles)
556 match._files = repo._subdirlfs(match.files(), lfiles)
546
557
547 # Case 2: user calls commit with specified patterns: refresh
558 # Case 2: user calls commit with specified patterns: refresh
548 # any matching big files.
559 # any matching big files.
549 smatcher = composestandinmatcher(repo, match)
560 smatcher = composestandinmatcher(repo, match)
550 standins = repo.dirstate.walk(smatcher, [], False, False)
561 standins = repo.dirstate.walk(smatcher, [], False, False)
551
562
552 # No matching big files: get out of the way and pass control to
563 # No matching big files: get out of the way and pass control to
553 # the usual commit() method.
564 # the usual commit() method.
554 if not standins:
565 if not standins:
555 return match
566 return match
556
567
557 # Refresh all matching big files. It's possible that the
568 # Refresh all matching big files. It's possible that the
558 # commit will end up failing, in which case the big files will
569 # commit will end up failing, in which case the big files will
559 # stay refreshed. No harm done: the user modified them and
570 # stay refreshed. No harm done: the user modified them and
560 # asked to commit them, so sooner or later we're going to
571 # asked to commit them, so sooner or later we're going to
561 # refresh the standins. Might as well leave them refreshed.
572 # refresh the standins. Might as well leave them refreshed.
562 lfdirstate = openlfdirstate(ui, repo)
573 lfdirstate = openlfdirstate(ui, repo)
563 for fstandin in standins:
574 for fstandin in standins:
564 lfile = splitstandin(fstandin)
575 lfile = splitstandin(fstandin)
565 if lfdirstate[lfile] != 'r':
576 if lfdirstate[lfile] != 'r':
566 updatestandin(repo, fstandin)
577 updatestandin(repo, fstandin)
567
578
568 # Cook up a new matcher that only matches regular files or
579 # Cook up a new matcher that only matches regular files or
569 # standins corresponding to the big files requested by the
580 # standins corresponding to the big files requested by the
570 # user. Have to modify _files to prevent commit() from
581 # user. Have to modify _files to prevent commit() from
571 # complaining "not tracked" for big files.
582 # complaining "not tracked" for big files.
572 match = copy.copy(match)
583 match = copy.copy(match)
573 origmatchfn = match.matchfn
584 origmatchfn = match.matchfn
574
585
575 # Check both the list of largefiles and the list of
586 # Check both the list of largefiles and the list of
576 # standins because if a largefile was removed, it
587 # standins because if a largefile was removed, it
577 # won't be in the list of largefiles at this point
588 # won't be in the list of largefiles at this point
578 match._files += sorted(standins)
589 match._files += sorted(standins)
579
590
580 actualfiles = []
591 actualfiles = []
581 for f in match._files:
592 for f in match._files:
582 fstandin = standin(f)
593 fstandin = standin(f)
583
594
584 # For largefiles, only one of the normal and standin should be
595 # For largefiles, only one of the normal and standin should be
585 # committed (except if one of them is a remove). In the case of a
596 # committed (except if one of them is a remove). In the case of a
586 # standin removal, drop the normal file if it is unknown to dirstate.
597 # standin removal, drop the normal file if it is unknown to dirstate.
587 # Thus, skip plain largefile names but keep the standin.
598 # Thus, skip plain largefile names but keep the standin.
588 if f in lfiles or fstandin in standins:
599 if f in lfiles or fstandin in standins:
589 if repo.dirstate[fstandin] != 'r':
600 if repo.dirstate[fstandin] != 'r':
590 if repo.dirstate[f] != 'r':
601 if repo.dirstate[f] != 'r':
591 continue
602 continue
592 elif repo.dirstate[f] == '?':
603 elif repo.dirstate[f] == '?':
593 continue
604 continue
594
605
595 actualfiles.append(f)
606 actualfiles.append(f)
596 match._files = actualfiles
607 match._files = actualfiles
597
608
598 def matchfn(f):
609 def matchfn(f):
599 if origmatchfn(f):
610 if origmatchfn(f):
600 return f not in lfiles
611 return f not in lfiles
601 else:
612 else:
602 return f in standins
613 return f in standins
603
614
604 match.matchfn = matchfn
615 match.matchfn = matchfn
605
616
606 return match
617 return match
607
618
608 class automatedcommithook(object):
619 class automatedcommithook(object):
609 '''Stateful hook to update standins at the 1st commit of resuming
620 '''Stateful hook to update standins at the 1st commit of resuming
610
621
611 For efficiency, updating standins in the working directory should
622 For efficiency, updating standins in the working directory should
612 be avoided while automated committing (like rebase, transplant and
623 be avoided while automated committing (like rebase, transplant and
613 so on), because they should be updated before committing.
624 so on), because they should be updated before committing.
614
625
615 But the 1st commit of resuming automated committing (e.g. ``rebase
626 But the 1st commit of resuming automated committing (e.g. ``rebase
616 --continue``) should update them, because largefiles may be
627 --continue``) should update them, because largefiles may be
617 modified manually.
628 modified manually.
618 '''
629 '''
619 def __init__(self, resuming):
630 def __init__(self, resuming):
620 self.resuming = resuming
631 self.resuming = resuming
621
632
622 def __call__(self, repo, match):
633 def __call__(self, repo, match):
623 if self.resuming:
634 if self.resuming:
624 self.resuming = False # avoids updating at subsequent commits
635 self.resuming = False # avoids updating at subsequent commits
625 return updatestandinsbymatch(repo, match)
636 return updatestandinsbymatch(repo, match)
626 else:
637 else:
627 return match
638 return match
628
639
629 def getstatuswriter(ui, repo, forcibly=None):
640 def getstatuswriter(ui, repo, forcibly=None):
630 '''Return the function to write largefiles specific status out
641 '''Return the function to write largefiles specific status out
631
642
632 If ``forcibly`` is ``None``, this returns the last element of
643 If ``forcibly`` is ``None``, this returns the last element of
633 ``repo._lfstatuswriters`` as "default" writer function.
644 ``repo._lfstatuswriters`` as "default" writer function.
634
645
635 Otherwise, this returns the function to always write out (or
646 Otherwise, this returns the function to always write out (or
636 ignore if ``not forcibly``) status.
647 ignore if ``not forcibly``) status.
637 '''
648 '''
638 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
649 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
639 return repo._lfstatuswriters[-1]
650 return repo._lfstatuswriters[-1]
640 else:
651 else:
641 if forcibly:
652 if forcibly:
642 return ui.status # forcibly WRITE OUT
653 return ui.status # forcibly WRITE OUT
643 else:
654 else:
644 return lambda *msg, **opts: None # forcibly IGNORE
655 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,175 +1,178 b''
1 # Copyright 2011 Fog Creek Software
1 # Copyright 2011 Fog Creek Software
2 #
2 #
3 # This software may be used and distributed according to the terms of the
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.
4 # GNU General Public License version 2 or any later version.
5
5
6 import os
6 import os
7 import urllib2
7 import urllib2
8 import re
8 import re
9
9
10 from mercurial import error, httppeer, util, wireproto
10 from mercurial import error, httppeer, util, wireproto
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 import lfutil
13 import lfutil
14
14
15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
16 '\n\nPlease enable it in your Mercurial config '
16 '\n\nPlease enable it in your Mercurial config '
17 'file.\n')
17 'file.\n')
18
18
19 # these will all be replaced by largefiles.uisetup
19 # these will all be replaced by largefiles.uisetup
20 capabilitiesorig = None
20 capabilitiesorig = None
21 ssholdcallstream = None
21 ssholdcallstream = None
22 httpoldcallstream = None
22 httpoldcallstream = None
23
23
24 def putlfile(repo, proto, sha):
24 def putlfile(repo, proto, sha):
25 '''Put a largefile into a repository's local store and into the
25 '''Server command for putting a largefile into a repository's local store
26 user cache.'''
26 and into the user cache.'''
27 proto.redirect()
27 proto.redirect()
28
28
29 path = lfutil.storepath(repo, sha)
29 path = lfutil.storepath(repo, sha)
30 util.makedirs(os.path.dirname(path))
30 util.makedirs(os.path.dirname(path))
31 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
31 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
32
32
33 try:
33 try:
34 proto.getfile(tmpfp)
34 proto.getfile(tmpfp)
35 tmpfp._fp.seek(0)
35 tmpfp._fp.seek(0)
36 if sha != lfutil.hexsha1(tmpfp._fp):
36 if sha != lfutil.hexsha1(tmpfp._fp):
37 raise IOError(0, _('largefile contents do not match hash'))
37 raise IOError(0, _('largefile contents do not match hash'))
38 tmpfp.close()
38 tmpfp.close()
39 lfutil.linktousercache(repo, sha)
39 lfutil.linktousercache(repo, sha)
40 except IOError as e:
40 except IOError as e:
41 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
41 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
42 (sha, e.strerror))
42 (sha, e.strerror))
43 return wireproto.pushres(1)
43 return wireproto.pushres(1)
44 finally:
44 finally:
45 tmpfp.discard()
45 tmpfp.discard()
46
46
47 return wireproto.pushres(0)
47 return wireproto.pushres(0)
48
48
49 def getlfile(repo, proto, sha):
49 def getlfile(repo, proto, sha):
50 '''Retrieve a largefile from the repository-local cache or system
50 '''Server command for retrieving a largefile from the repository-local
51 cache.'''
51 cache or user cache.'''
52 filename = lfutil.findfile(repo, sha)
52 filename = lfutil.findfile(repo, sha)
53 if not filename:
53 if not filename:
54 raise error.Abort(_('requested largefile %s not present in cache')
54 raise error.Abort(_('requested largefile %s not present in cache')
55 % sha)
55 % sha)
56 f = open(filename, 'rb')
56 f = open(filename, 'rb')
57 length = os.fstat(f.fileno())[6]
57 length = os.fstat(f.fileno())[6]
58
58
59 # Since we can't set an HTTP content-length header here, and
59 # Since we can't set an HTTP content-length header here, and
60 # Mercurial core provides no way to give the length of a streamres
60 # Mercurial core provides no way to give the length of a streamres
61 # (and reading the entire file into RAM would be ill-advised), we
61 # (and reading the entire file into RAM would be ill-advised), we
62 # just send the length on the first line of the response, like the
62 # just send the length on the first line of the response, like the
63 # ssh proto does for string responses.
63 # ssh proto does for string responses.
64 def generator():
64 def generator():
65 yield '%d\n' % length
65 yield '%d\n' % length
66 for chunk in util.filechunkiter(f):
66 for chunk in util.filechunkiter(f):
67 yield chunk
67 yield chunk
68 return wireproto.streamres(generator())
68 return wireproto.streamres(generator())
69
69
70 def statlfile(repo, proto, sha):
70 def statlfile(repo, proto, sha):
71 '''Return '2\n' if the largefile is missing, '0\n' if it seems to be in
71 '''Server command for checking if a largefile is present - returns '2\n' if
72 good condition.
72 the largefile is missing, '0\n' if it seems to be in good condition.
73
73
74 The value 1 is reserved for mismatched checksum, but that is too expensive
74 The value 1 is reserved for mismatched checksum, but that is too expensive
75 to be verified on every stat and must be caught be running 'hg verify'
75 to be verified on every stat and must be caught be running 'hg verify'
76 server side.'''
76 server side.'''
77 filename = lfutil.findfile(repo, sha)
77 filename = lfutil.findfile(repo, sha)
78 if not filename:
78 if not filename:
79 return '2\n'
79 return '2\n'
80 return '0\n'
80 return '0\n'
81
81
82 def wirereposetup(ui, repo):
82 def wirereposetup(ui, repo):
83 class lfileswirerepository(repo.__class__):
83 class lfileswirerepository(repo.__class__):
84 def putlfile(self, sha, fd):
84 def putlfile(self, sha, fd):
85 # unfortunately, httprepository._callpush tries to convert its
85 # unfortunately, httprepository._callpush tries to convert its
86 # input file-like into a bundle before sending it, so we can't use
86 # input file-like into a bundle before sending it, so we can't use
87 # it ...
87 # it ...
88 if issubclass(self.__class__, httppeer.httppeer):
88 if issubclass(self.__class__, httppeer.httppeer):
89 res = self._call('putlfile', data=fd, sha=sha,
89 res = self._call('putlfile', data=fd, sha=sha,
90 headers={'content-type':'application/mercurial-0.1'})
90 headers={'content-type':'application/mercurial-0.1'})
91 try:
91 try:
92 d, output = res.split('\n', 1)
92 d, output = res.split('\n', 1)
93 for l in output.splitlines(True):
93 for l in output.splitlines(True):
94 self.ui.warn(_('remote: '), l) # assume l ends with \n
94 self.ui.warn(_('remote: '), l) # assume l ends with \n
95 return int(d)
95 return int(d)
96 except ValueError:
96 except ValueError:
97 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
97 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
98 return 1
98 return 1
99 # ... but we can't use sshrepository._call because the data=
99 # ... but we can't use sshrepository._call because the data=
100 # argument won't get sent, and _callpush does exactly what we want
100 # argument won't get sent, and _callpush does exactly what we want
101 # in this case: send the data straight through
101 # in this case: send the data straight through
102 else:
102 else:
103 try:
103 try:
104 ret, output = self._callpush("putlfile", fd, sha=sha)
104 ret, output = self._callpush("putlfile", fd, sha=sha)
105 if ret == "":
105 if ret == "":
106 raise error.ResponseError(_('putlfile failed:'),
106 raise error.ResponseError(_('putlfile failed:'),
107 output)
107 output)
108 return int(ret)
108 return int(ret)
109 except IOError:
109 except IOError:
110 return 1
110 return 1
111 except ValueError:
111 except ValueError:
112 raise error.ResponseError(
112 raise error.ResponseError(
113 _('putlfile failed (unexpected response):'), ret)
113 _('putlfile failed (unexpected response):'), ret)
114
114
115 def getlfile(self, sha):
115 def getlfile(self, sha):
116 """returns an iterable with the chunks of the file with sha sha"""
116 """returns an iterable with the chunks of the file with sha sha"""
117 stream = self._callstream("getlfile", sha=sha)
117 stream = self._callstream("getlfile", sha=sha)
118 length = stream.readline()
118 length = stream.readline()
119 try:
119 try:
120 length = int(length)
120 length = int(length)
121 except ValueError:
121 except ValueError:
122 self._abort(error.ResponseError(_("unexpected response:"),
122 self._abort(error.ResponseError(_("unexpected response:"),
123 length))
123 length))
124
124
125 # SSH streams will block if reading more than length
125 # SSH streams will block if reading more than length
126 for chunk in util.filechunkiter(stream, 128 * 1024, length):
126 for chunk in util.filechunkiter(stream, 128 * 1024, length):
127 yield chunk
127 yield chunk
128 # HTTP streams must hit the end to process the last empty
128 # HTTP streams must hit the end to process the last empty
129 # chunk of Chunked-Encoding so the connection can be reused.
129 # chunk of Chunked-Encoding so the connection can be reused.
130 if issubclass(self.__class__, httppeer.httppeer):
130 if issubclass(self.__class__, httppeer.httppeer):
131 chunk = stream.read(1)
131 chunk = stream.read(1)
132 if chunk:
132 if chunk:
133 self._abort(error.ResponseError(_("unexpected response:"),
133 self._abort(error.ResponseError(_("unexpected response:"),
134 chunk))
134 chunk))
135
135
136 @wireproto.batchable
136 @wireproto.batchable
137 def statlfile(self, sha):
137 def statlfile(self, sha):
138 f = wireproto.future()
138 f = wireproto.future()
139 result = {'sha': sha}
139 result = {'sha': sha}
140 yield result, f
140 yield result, f
141 try:
141 try:
142 yield int(f.value)
142 yield int(f.value)
143 except (ValueError, urllib2.HTTPError):
143 except (ValueError, urllib2.HTTPError):
144 # If the server returns anything but an integer followed by a
144 # If the server returns anything but an integer followed by a
145 # newline, newline, it's not speaking our language; if we get
145 # newline, newline, it's not speaking our language; if we get
146 # an HTTP error, we can't be sure the largefile is present;
146 # an HTTP error, we can't be sure the largefile is present;
147 # either way, consider it missing.
147 # either way, consider it missing.
148 yield 2
148 yield 2
149
149
150 repo.__class__ = lfileswirerepository
150 repo.__class__ = lfileswirerepository
151
151
152 # advertise the largefiles=serve capability
152 # advertise the largefiles=serve capability
153 def capabilities(repo, proto):
153 def capabilities(repo, proto):
154 '''Wrap server command to announce largefile server capability'''
154 return capabilitiesorig(repo, proto) + ' largefiles=serve'
155 return capabilitiesorig(repo, proto) + ' largefiles=serve'
155
156
156 def heads(repo, proto):
157 def heads(repo, proto):
158 '''Wrap server command - largefile capable clients will know to call
159 lheads instead'''
157 if lfutil.islfilesrepo(repo):
160 if lfutil.islfilesrepo(repo):
158 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
161 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
159 return wireproto.heads(repo, proto)
162 return wireproto.heads(repo, proto)
160
163
161 def sshrepocallstream(self, cmd, **args):
164 def sshrepocallstream(self, cmd, **args):
162 if cmd == 'heads' and self.capable('largefiles'):
165 if cmd == 'heads' and self.capable('largefiles'):
163 cmd = 'lheads'
166 cmd = 'lheads'
164 if cmd == 'batch' and self.capable('largefiles'):
167 if cmd == 'batch' and self.capable('largefiles'):
165 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
168 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
166 return ssholdcallstream(self, cmd, **args)
169 return ssholdcallstream(self, cmd, **args)
167
170
168 headsre = re.compile(r'(^|;)heads\b')
171 headsre = re.compile(r'(^|;)heads\b')
169
172
170 def httprepocallstream(self, cmd, **args):
173 def httprepocallstream(self, cmd, **args):
171 if cmd == 'heads' and self.capable('largefiles'):
174 if cmd == 'heads' and self.capable('largefiles'):
172 cmd = 'lheads'
175 cmd = 'lheads'
173 if cmd == 'batch' and self.capable('largefiles'):
176 if cmd == 'batch' and self.capable('largefiles'):
174 args['cmds'] = headsre.sub('lheads', args['cmds'])
177 args['cmds'] = headsre.sub('lheads', args['cmds'])
175 return httpoldcallstream(self, cmd, **args)
178 return httpoldcallstream(self, cmd, **args)
General Comments 0
You need to be logged in to leave comments. Login now