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