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