##// END OF EJS Templates
largefiles: batch statlfile requests when pushing a largefiles repo (issue3386)...
Na'Tosha Bard -
r17127:9e161630 default
parent child Browse files
Show More
@@ -1,195 +1,195 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 '''base class for store implementations and store-related utility code'''
9 '''base class for store implementations and store-related utility code'''
10
10
11 import binascii
11 import binascii
12 import re
12 import re
13
13
14 from mercurial import util, node, hg
14 from mercurial import util, node, hg
15 from mercurial.i18n import _
15 from mercurial.i18n import _
16
16
17 import lfutil
17 import lfutil
18
18
19 class StoreError(Exception):
19 class StoreError(Exception):
20 '''Raised when there is a problem getting files from or putting
20 '''Raised when there is a problem getting files from or putting
21 files to a central store.'''
21 files to a central store.'''
22 def __init__(self, filename, hash, url, detail):
22 def __init__(self, filename, hash, url, detail):
23 self.filename = filename
23 self.filename = filename
24 self.hash = hash
24 self.hash = hash
25 self.url = url
25 self.url = url
26 self.detail = detail
26 self.detail = detail
27
27
28 def longmessage(self):
28 def longmessage(self):
29 if self.url:
29 if self.url:
30 return ('%s: %s\n'
30 return ('%s: %s\n'
31 '(failed URL: %s)\n'
31 '(failed URL: %s)\n'
32 % (self.filename, self.detail, self.url))
32 % (self.filename, self.detail, self.url))
33 else:
33 else:
34 return ('%s: %s\n'
34 return ('%s: %s\n'
35 '(no default or default-push path set in hgrc)\n'
35 '(no default or default-push path set in hgrc)\n'
36 % (self.filename, self.detail))
36 % (self.filename, self.detail))
37
37
38 def __str__(self):
38 def __str__(self):
39 return "%s: %s" % (self.url, self.detail)
39 return "%s: %s" % (self.url, self.detail)
40
40
41 class basestore(object):
41 class basestore(object):
42 def __init__(self, ui, repo, url):
42 def __init__(self, ui, repo, url):
43 self.ui = ui
43 self.ui = ui
44 self.repo = repo
44 self.repo = repo
45 self.url = url
45 self.url = url
46
46
47 def put(self, source, hash):
47 def put(self, source, hash):
48 '''Put source file into the store under <filename>/<hash>.'''
48 '''Put source file into the store under <filename>/<hash>.'''
49 raise NotImplementedError('abstract method')
49 raise NotImplementedError('abstract method')
50
50
51 def exists(self, hash):
51 def exists(self, hashes):
52 '''Check to see if the store contains the given hash.'''
52 '''Check to see if the store contains the given hashes.'''
53 raise NotImplementedError('abstract method')
53 raise NotImplementedError('abstract method')
54
54
55 def get(self, files):
55 def get(self, files):
56 '''Get the specified largefiles from the store and write to local
56 '''Get the specified largefiles from the store and write to local
57 files under repo.root. files is a list of (filename, hash)
57 files under repo.root. files is a list of (filename, hash)
58 tuples. Return (success, missing), lists of files successfuly
58 tuples. Return (success, missing), lists of files successfuly
59 downloaded and those not found in the store. success is a list
59 downloaded and those not found in the store. success is a list
60 of (filename, hash) tuples; missing is a list of filenames that
60 of (filename, hash) tuples; missing is a list of filenames that
61 we could not get. (The detailed error message will already have
61 we could not get. (The detailed error message will already have
62 been presented to the user, so missing is just supplied as a
62 been presented to the user, so missing is just supplied as a
63 summary.)'''
63 summary.)'''
64 success = []
64 success = []
65 missing = []
65 missing = []
66 ui = self.ui
66 ui = self.ui
67
67
68 at = 0
68 at = 0
69 for filename, hash in files:
69 for filename, hash in files:
70 ui.progress(_('getting largefiles'), at, unit='lfile',
70 ui.progress(_('getting largefiles'), at, unit='lfile',
71 total=len(files))
71 total=len(files))
72 at += 1
72 at += 1
73 ui.note(_('getting %s:%s\n') % (filename, hash))
73 ui.note(_('getting %s:%s\n') % (filename, hash))
74
74
75 storefilename = lfutil.storepath(self.repo, hash)
75 storefilename = lfutil.storepath(self.repo, hash)
76 tmpfile = util.atomictempfile(storefilename,
76 tmpfile = util.atomictempfile(storefilename,
77 createmode=self.repo.store.createmode)
77 createmode=self.repo.store.createmode)
78
78
79 try:
79 try:
80 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
80 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
81 except StoreError, err:
81 except StoreError, err:
82 ui.warn(err.longmessage())
82 ui.warn(err.longmessage())
83 hhash = ""
83 hhash = ""
84
84
85 if hhash != hash:
85 if hhash != hash:
86 if hhash != "":
86 if hhash != "":
87 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
87 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
88 % (filename, hash, hhash))
88 % (filename, hash, hhash))
89 tmpfile.discard() # no-op if it's already closed
89 tmpfile.discard() # no-op if it's already closed
90 missing.append(filename)
90 missing.append(filename)
91 continue
91 continue
92
92
93 tmpfile.close()
93 tmpfile.close()
94 lfutil.linktousercache(self.repo, hash)
94 lfutil.linktousercache(self.repo, hash)
95 success.append((filename, hhash))
95 success.append((filename, hhash))
96
96
97 ui.progress(_('getting largefiles'), None)
97 ui.progress(_('getting largefiles'), None)
98 return (success, missing)
98 return (success, missing)
99
99
100 def verify(self, revs, contents=False):
100 def verify(self, revs, contents=False):
101 '''Verify the existence (and, optionally, contents) of every big
101 '''Verify the existence (and, optionally, contents) of every big
102 file revision referenced by every changeset in revs.
102 file revision referenced by every changeset in revs.
103 Return 0 if all is well, non-zero on any errors.'''
103 Return 0 if all is well, non-zero on any errors.'''
104 write = self.ui.write
104 write = self.ui.write
105 failed = False
105 failed = False
106
106
107 write(_('searching %d changesets for largefiles\n') % len(revs))
107 write(_('searching %d changesets for largefiles\n') % len(revs))
108 verified = set() # set of (filename, filenode) tuples
108 verified = set() # set of (filename, filenode) tuples
109
109
110 for rev in revs:
110 for rev in revs:
111 cctx = self.repo[rev]
111 cctx = self.repo[rev]
112 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
112 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
113
113
114 failed = util.any(self._verifyfile(
114 failed = util.any(self._verifyfile(
115 cctx, cset, contents, standin, verified) for standin in cctx)
115 cctx, cset, contents, standin, verified) for standin in cctx)
116
116
117 numrevs = len(verified)
117 numrevs = len(verified)
118 numlfiles = len(set([fname for (fname, fnode) in verified]))
118 numlfiles = len(set([fname for (fname, fnode) in verified]))
119 if contents:
119 if contents:
120 write(_('verified contents of %d revisions of %d largefiles\n')
120 write(_('verified contents of %d revisions of %d largefiles\n')
121 % (numrevs, numlfiles))
121 % (numrevs, numlfiles))
122 else:
122 else:
123 write(_('verified existence of %d revisions of %d largefiles\n')
123 write(_('verified existence of %d revisions of %d largefiles\n')
124 % (numrevs, numlfiles))
124 % (numrevs, numlfiles))
125
125
126 return int(failed)
126 return int(failed)
127
127
128 def _getfile(self, tmpfile, filename, hash):
128 def _getfile(self, tmpfile, filename, hash):
129 '''Fetch one revision of one file from the store and write it
129 '''Fetch one revision of one file from the store and write it
130 to tmpfile. Compute the hash of the file on-the-fly as it
130 to tmpfile. Compute the hash of the file on-the-fly as it
131 downloads and return the binary hash. Close tmpfile. Raise
131 downloads and return the binary hash. Close tmpfile. Raise
132 StoreError if unable to download the file (e.g. it does not
132 StoreError if unable to download the file (e.g. it does not
133 exist in the store).'''
133 exist in the store).'''
134 raise NotImplementedError('abstract method')
134 raise NotImplementedError('abstract method')
135
135
136 def _verifyfile(self, cctx, cset, contents, standin, verified):
136 def _verifyfile(self, cctx, cset, contents, standin, verified):
137 '''Perform the actual verification of a file in the store.
137 '''Perform the actual verification of a file in the store.
138 '''
138 '''
139 raise NotImplementedError('abstract method')
139 raise NotImplementedError('abstract method')
140
140
141 import localstore, wirestore
141 import localstore, wirestore
142
142
143 _storeprovider = {
143 _storeprovider = {
144 'file': [localstore.localstore],
144 'file': [localstore.localstore],
145 'http': [wirestore.wirestore],
145 'http': [wirestore.wirestore],
146 'https': [wirestore.wirestore],
146 'https': [wirestore.wirestore],
147 'ssh': [wirestore.wirestore],
147 'ssh': [wirestore.wirestore],
148 }
148 }
149
149
150 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
150 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
151
151
152 # During clone this function is passed the src's ui object
152 # During clone this function is passed the src's ui object
153 # but it needs the dest's ui object so it can read out of
153 # but it needs the dest's ui object so it can read out of
154 # the config file. Use repo.ui instead.
154 # the config file. Use repo.ui instead.
155 def _openstore(repo, remote=None, put=False):
155 def _openstore(repo, remote=None, put=False):
156 ui = repo.ui
156 ui = repo.ui
157
157
158 if not remote:
158 if not remote:
159 lfpullsource = getattr(repo, 'lfpullsource', None)
159 lfpullsource = getattr(repo, 'lfpullsource', None)
160 if lfpullsource:
160 if lfpullsource:
161 path = ui.expandpath(lfpullsource)
161 path = ui.expandpath(lfpullsource)
162 else:
162 else:
163 path = ui.expandpath('default-push', 'default')
163 path = ui.expandpath('default-push', 'default')
164
164
165 # ui.expandpath() leaves 'default-push' and 'default' alone if
165 # ui.expandpath() leaves 'default-push' and 'default' alone if
166 # they cannot be expanded: fallback to the empty string,
166 # they cannot be expanded: fallback to the empty string,
167 # meaning the current directory.
167 # meaning the current directory.
168 if path == 'default-push' or path == 'default':
168 if path == 'default-push' or path == 'default':
169 path = ''
169 path = ''
170 remote = repo
170 remote = repo
171 else:
171 else:
172 remote = hg.peer(repo, {}, path)
172 remote = hg.peer(repo, {}, path)
173
173
174 # The path could be a scheme so use Mercurial's normal functionality
174 # The path could be a scheme so use Mercurial's normal functionality
175 # to resolve the scheme to a repository and use its path
175 # to resolve the scheme to a repository and use its path
176 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
176 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
177
177
178 match = _scheme_re.match(path)
178 match = _scheme_re.match(path)
179 if not match: # regular filesystem path
179 if not match: # regular filesystem path
180 scheme = 'file'
180 scheme = 'file'
181 else:
181 else:
182 scheme = match.group(1)
182 scheme = match.group(1)
183
183
184 try:
184 try:
185 storeproviders = _storeprovider[scheme]
185 storeproviders = _storeprovider[scheme]
186 except KeyError:
186 except KeyError:
187 raise util.Abort(_('unsupported URL scheme %r') % scheme)
187 raise util.Abort(_('unsupported URL scheme %r') % scheme)
188
188
189 for classobj in storeproviders:
189 for classobj in storeproviders:
190 try:
190 try:
191 return classobj(ui, repo, remote)
191 return classobj(ui, repo, remote)
192 except lfutil.storeprotonotcapable:
192 except lfutil.storeprotonotcapable:
193 pass
193 pass
194
194
195 raise util.Abort(_('%s does not appear to be a largefile store') % path)
195 raise util.Abort(_('%s does not appear to be a largefile store') % path)
@@ -1,541 +1,545 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 '''High-level command function for lfconvert, plus the cmdtable.'''
9 '''High-level command function for lfconvert, plus the cmdtable.'''
10
10
11 import os
11 import os
12 import shutil
12 import shutil
13
13
14 from mercurial import util, match as match_, hg, node, context, error, \
14 from mercurial import util, match as match_, hg, node, context, error, \
15 cmdutil, scmutil
15 cmdutil, scmutil
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.lock import release
17 from mercurial.lock import release
18
18
19 import lfutil
19 import lfutil
20 import basestore
20 import basestore
21
21
22 # -- Commands ----------------------------------------------------------
22 # -- Commands ----------------------------------------------------------
23
23
24 def lfconvert(ui, src, dest, *pats, **opts):
24 def lfconvert(ui, src, dest, *pats, **opts):
25 '''convert a normal repository to a largefiles repository
25 '''convert a normal repository to a largefiles repository
26
26
27 Convert repository SOURCE to a new repository DEST, identical to
27 Convert repository SOURCE to a new repository DEST, identical to
28 SOURCE except that certain files will be converted as largefiles:
28 SOURCE except that certain files will be converted as largefiles:
29 specifically, any file that matches any PATTERN *or* whose size is
29 specifically, any file that matches any PATTERN *or* whose size is
30 above the minimum size threshold is converted as a largefile. The
30 above the minimum size threshold is converted as a largefile. The
31 size used to determine whether or not to track a file as a
31 size used to determine whether or not to track a file as a
32 largefile is the size of the first version of the file. The
32 largefile is the size of the first version of the file. The
33 minimum size can be specified either with --size or in
33 minimum size can be specified either with --size or in
34 configuration as ``largefiles.size``.
34 configuration as ``largefiles.size``.
35
35
36 After running this command you will need to make sure that
36 After running this command you will need to make sure that
37 largefiles is enabled anywhere you intend to push the new
37 largefiles is enabled anywhere you intend to push the new
38 repository.
38 repository.
39
39
40 Use --to-normal to convert largefiles back to normal files; after
40 Use --to-normal to convert largefiles back to normal files; after
41 this, the DEST repository can be used without largefiles at all.'''
41 this, the DEST repository can be used without largefiles at all.'''
42
42
43 if opts['to_normal']:
43 if opts['to_normal']:
44 tolfile = False
44 tolfile = False
45 else:
45 else:
46 tolfile = True
46 tolfile = True
47 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
47 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
48
48
49 if not hg.islocal(src):
49 if not hg.islocal(src):
50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
50 raise util.Abort(_('%s is not a local Mercurial repo') % src)
51 if not hg.islocal(dest):
51 if not hg.islocal(dest):
52 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
52 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
53
53
54 rsrc = hg.repository(ui, src)
54 rsrc = hg.repository(ui, src)
55 ui.status(_('initializing destination %s\n') % dest)
55 ui.status(_('initializing destination %s\n') % dest)
56 rdst = hg.repository(ui, dest, create=True)
56 rdst = hg.repository(ui, dest, create=True)
57
57
58 success = False
58 success = False
59 dstwlock = dstlock = None
59 dstwlock = dstlock = None
60 try:
60 try:
61 # Lock destination to prevent modification while it is converted to.
61 # Lock destination to prevent modification while it is converted to.
62 # Don't need to lock src because we are just reading from its history
62 # Don't need to lock src because we are just reading from its history
63 # which can't change.
63 # which can't change.
64 dstwlock = rdst.wlock()
64 dstwlock = rdst.wlock()
65 dstlock = rdst.lock()
65 dstlock = rdst.lock()
66
66
67 # Get a list of all changesets in the source. The easy way to do this
67 # Get a list of all changesets in the source. The easy way to do this
68 # is to simply walk the changelog, using changelog.nodesbewteen().
68 # is to simply walk the changelog, using changelog.nodesbewteen().
69 # Take a look at mercurial/revlog.py:639 for more details.
69 # Take a look at mercurial/revlog.py:639 for more details.
70 # Use a generator instead of a list to decrease memory usage
70 # Use a generator instead of a list to decrease memory usage
71 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
71 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
72 rsrc.heads())[0])
72 rsrc.heads())[0])
73 revmap = {node.nullid: node.nullid}
73 revmap = {node.nullid: node.nullid}
74 if tolfile:
74 if tolfile:
75 lfiles = set()
75 lfiles = set()
76 normalfiles = set()
76 normalfiles = set()
77 if not pats:
77 if not pats:
78 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
78 pats = ui.configlist(lfutil.longname, 'patterns', default=[])
79 if pats:
79 if pats:
80 matcher = match_.match(rsrc.root, '', list(pats))
80 matcher = match_.match(rsrc.root, '', list(pats))
81 else:
81 else:
82 matcher = None
82 matcher = None
83
83
84 lfiletohash = {}
84 lfiletohash = {}
85 for ctx in ctxs:
85 for ctx in ctxs:
86 ui.progress(_('converting revisions'), ctx.rev(),
86 ui.progress(_('converting revisions'), ctx.rev(),
87 unit=_('revision'), total=rsrc['tip'].rev())
87 unit=_('revision'), total=rsrc['tip'].rev())
88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
89 lfiles, normalfiles, matcher, size, lfiletohash)
89 lfiles, normalfiles, matcher, size, lfiletohash)
90 ui.progress(_('converting revisions'), None)
90 ui.progress(_('converting revisions'), None)
91
91
92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
94
94
95 for f in lfiletohash.keys():
95 for f in lfiletohash.keys():
96 if os.path.isfile(rdst.wjoin(f)):
96 if os.path.isfile(rdst.wjoin(f)):
97 os.unlink(rdst.wjoin(f))
97 os.unlink(rdst.wjoin(f))
98 try:
98 try:
99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
100 except OSError:
100 except OSError:
101 pass
101 pass
102
102
103 # If there were any files converted to largefiles, add largefiles
103 # If there were any files converted to largefiles, add largefiles
104 # to the destination repository's requirements.
104 # to the destination repository's requirements.
105 if lfiles:
105 if lfiles:
106 rdst.requirements.add('largefiles')
106 rdst.requirements.add('largefiles')
107 rdst._writerequirements()
107 rdst._writerequirements()
108 else:
108 else:
109 for ctx in ctxs:
109 for ctx in ctxs:
110 ui.progress(_('converting revisions'), ctx.rev(),
110 ui.progress(_('converting revisions'), ctx.rev(),
111 unit=_('revision'), total=rsrc['tip'].rev())
111 unit=_('revision'), total=rsrc['tip'].rev())
112 _addchangeset(ui, rsrc, rdst, ctx, revmap)
112 _addchangeset(ui, rsrc, rdst, ctx, revmap)
113
113
114 ui.progress(_('converting revisions'), None)
114 ui.progress(_('converting revisions'), None)
115 success = True
115 success = True
116 finally:
116 finally:
117 rdst.dirstate.clear()
117 rdst.dirstate.clear()
118 release(dstlock, dstwlock)
118 release(dstlock, dstwlock)
119 if not success:
119 if not success:
120 # we failed, remove the new directory
120 # we failed, remove the new directory
121 shutil.rmtree(rdst.root)
121 shutil.rmtree(rdst.root)
122
122
123 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
123 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
124 # Convert src parents to dst parents
124 # Convert src parents to dst parents
125 parents = _convertparents(ctx, revmap)
125 parents = _convertparents(ctx, revmap)
126
126
127 # Generate list of changed files
127 # Generate list of changed files
128 files = _getchangedfiles(ctx, parents)
128 files = _getchangedfiles(ctx, parents)
129
129
130 def getfilectx(repo, memctx, f):
130 def getfilectx(repo, memctx, f):
131 if lfutil.standin(f) in files:
131 if lfutil.standin(f) in files:
132 # if the file isn't in the manifest then it was removed
132 # if the file isn't in the manifest then it was removed
133 # or renamed, raise IOError to indicate this
133 # or renamed, raise IOError to indicate this
134 try:
134 try:
135 fctx = ctx.filectx(lfutil.standin(f))
135 fctx = ctx.filectx(lfutil.standin(f))
136 except error.LookupError:
136 except error.LookupError:
137 raise IOError
137 raise IOError
138 renamed = fctx.renamed()
138 renamed = fctx.renamed()
139 if renamed:
139 if renamed:
140 renamed = lfutil.splitstandin(renamed[0])
140 renamed = lfutil.splitstandin(renamed[0])
141
141
142 hash = fctx.data().strip()
142 hash = fctx.data().strip()
143 path = lfutil.findfile(rsrc, hash)
143 path = lfutil.findfile(rsrc, hash)
144 ### TODO: What if the file is not cached?
144 ### TODO: What if the file is not cached?
145 data = ''
145 data = ''
146 fd = None
146 fd = None
147 try:
147 try:
148 fd = open(path, 'rb')
148 fd = open(path, 'rb')
149 data = fd.read()
149 data = fd.read()
150 finally:
150 finally:
151 if fd:
151 if fd:
152 fd.close()
152 fd.close()
153 return context.memfilectx(f, data, 'l' in fctx.flags(),
153 return context.memfilectx(f, data, 'l' in fctx.flags(),
154 'x' in fctx.flags(), renamed)
154 'x' in fctx.flags(), renamed)
155 else:
155 else:
156 return _getnormalcontext(repo.ui, ctx, f, revmap)
156 return _getnormalcontext(repo.ui, ctx, f, revmap)
157
157
158 dstfiles = []
158 dstfiles = []
159 for file in files:
159 for file in files:
160 if lfutil.isstandin(file):
160 if lfutil.isstandin(file):
161 dstfiles.append(lfutil.splitstandin(file))
161 dstfiles.append(lfutil.splitstandin(file))
162 else:
162 else:
163 dstfiles.append(file)
163 dstfiles.append(file)
164 # Commit
164 # Commit
165 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
165 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
166
166
167 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
167 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
168 matcher, size, lfiletohash):
168 matcher, size, lfiletohash):
169 # Convert src parents to dst parents
169 # Convert src parents to dst parents
170 parents = _convertparents(ctx, revmap)
170 parents = _convertparents(ctx, revmap)
171
171
172 # Generate list of changed files
172 # Generate list of changed files
173 files = _getchangedfiles(ctx, parents)
173 files = _getchangedfiles(ctx, parents)
174
174
175 dstfiles = []
175 dstfiles = []
176 for f in files:
176 for f in files:
177 if f not in lfiles and f not in normalfiles:
177 if f not in lfiles and f not in normalfiles:
178 islfile = _islfile(f, ctx, matcher, size)
178 islfile = _islfile(f, ctx, matcher, size)
179 # If this file was renamed or copied then copy
179 # If this file was renamed or copied then copy
180 # the lfileness of its predecessor
180 # the lfileness of its predecessor
181 if f in ctx.manifest():
181 if f in ctx.manifest():
182 fctx = ctx.filectx(f)
182 fctx = ctx.filectx(f)
183 renamed = fctx.renamed()
183 renamed = fctx.renamed()
184 renamedlfile = renamed and renamed[0] in lfiles
184 renamedlfile = renamed and renamed[0] in lfiles
185 islfile |= renamedlfile
185 islfile |= renamedlfile
186 if 'l' in fctx.flags():
186 if 'l' in fctx.flags():
187 if renamedlfile:
187 if renamedlfile:
188 raise util.Abort(
188 raise util.Abort(
189 _('renamed/copied largefile %s becomes symlink')
189 _('renamed/copied largefile %s becomes symlink')
190 % f)
190 % f)
191 islfile = False
191 islfile = False
192 if islfile:
192 if islfile:
193 lfiles.add(f)
193 lfiles.add(f)
194 else:
194 else:
195 normalfiles.add(f)
195 normalfiles.add(f)
196
196
197 if f in lfiles:
197 if f in lfiles:
198 dstfiles.append(lfutil.standin(f))
198 dstfiles.append(lfutil.standin(f))
199 # largefile in manifest if it has not been removed/renamed
199 # largefile in manifest if it has not been removed/renamed
200 if f in ctx.manifest():
200 if f in ctx.manifest():
201 fctx = ctx.filectx(f)
201 fctx = ctx.filectx(f)
202 if 'l' in fctx.flags():
202 if 'l' in fctx.flags():
203 renamed = fctx.renamed()
203 renamed = fctx.renamed()
204 if renamed and renamed[0] in lfiles:
204 if renamed and renamed[0] in lfiles:
205 raise util.Abort(_('largefile %s becomes symlink') % f)
205 raise util.Abort(_('largefile %s becomes symlink') % f)
206
206
207 # largefile was modified, update standins
207 # largefile was modified, update standins
208 fullpath = rdst.wjoin(f)
208 fullpath = rdst.wjoin(f)
209 util.makedirs(os.path.dirname(fullpath))
209 util.makedirs(os.path.dirname(fullpath))
210 m = util.sha1('')
210 m = util.sha1('')
211 m.update(ctx[f].data())
211 m.update(ctx[f].data())
212 hash = m.hexdigest()
212 hash = m.hexdigest()
213 if f not in lfiletohash or lfiletohash[f] != hash:
213 if f not in lfiletohash or lfiletohash[f] != hash:
214 try:
214 try:
215 fd = open(fullpath, 'wb')
215 fd = open(fullpath, 'wb')
216 fd.write(ctx[f].data())
216 fd.write(ctx[f].data())
217 finally:
217 finally:
218 if fd:
218 if fd:
219 fd.close()
219 fd.close()
220 executable = 'x' in ctx[f].flags()
220 executable = 'x' in ctx[f].flags()
221 os.chmod(fullpath, lfutil.getmode(executable))
221 os.chmod(fullpath, lfutil.getmode(executable))
222 lfutil.writestandin(rdst, lfutil.standin(f), hash,
222 lfutil.writestandin(rdst, lfutil.standin(f), hash,
223 executable)
223 executable)
224 lfiletohash[f] = hash
224 lfiletohash[f] = hash
225 else:
225 else:
226 # normal file
226 # normal file
227 dstfiles.append(f)
227 dstfiles.append(f)
228
228
229 def getfilectx(repo, memctx, f):
229 def getfilectx(repo, memctx, f):
230 if lfutil.isstandin(f):
230 if lfutil.isstandin(f):
231 # if the file isn't in the manifest then it was removed
231 # if the file isn't in the manifest then it was removed
232 # or renamed, raise IOError to indicate this
232 # or renamed, raise IOError to indicate this
233 srcfname = lfutil.splitstandin(f)
233 srcfname = lfutil.splitstandin(f)
234 try:
234 try:
235 fctx = ctx.filectx(srcfname)
235 fctx = ctx.filectx(srcfname)
236 except error.LookupError:
236 except error.LookupError:
237 raise IOError
237 raise IOError
238 renamed = fctx.renamed()
238 renamed = fctx.renamed()
239 if renamed:
239 if renamed:
240 # standin is always a largefile because largefile-ness
240 # standin is always a largefile because largefile-ness
241 # doesn't change after rename or copy
241 # doesn't change after rename or copy
242 renamed = lfutil.standin(renamed[0])
242 renamed = lfutil.standin(renamed[0])
243
243
244 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
244 return context.memfilectx(f, lfiletohash[srcfname] + '\n', 'l' in
245 fctx.flags(), 'x' in fctx.flags(), renamed)
245 fctx.flags(), 'x' in fctx.flags(), renamed)
246 else:
246 else:
247 return _getnormalcontext(repo.ui, ctx, f, revmap)
247 return _getnormalcontext(repo.ui, ctx, f, revmap)
248
248
249 # Commit
249 # Commit
250 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
250 _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap)
251
251
252 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
252 def _commitcontext(rdst, parents, ctx, dstfiles, getfilectx, revmap):
253 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
253 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
254 getfilectx, ctx.user(), ctx.date(), ctx.extra())
254 getfilectx, ctx.user(), ctx.date(), ctx.extra())
255 ret = rdst.commitctx(mctx)
255 ret = rdst.commitctx(mctx)
256 rdst.setparents(ret)
256 rdst.setparents(ret)
257 revmap[ctx.node()] = rdst.changelog.tip()
257 revmap[ctx.node()] = rdst.changelog.tip()
258
258
259 # Generate list of changed files
259 # Generate list of changed files
260 def _getchangedfiles(ctx, parents):
260 def _getchangedfiles(ctx, parents):
261 files = set(ctx.files())
261 files = set(ctx.files())
262 if node.nullid not in parents:
262 if node.nullid not in parents:
263 mc = ctx.manifest()
263 mc = ctx.manifest()
264 mp1 = ctx.parents()[0].manifest()
264 mp1 = ctx.parents()[0].manifest()
265 mp2 = ctx.parents()[1].manifest()
265 mp2 = ctx.parents()[1].manifest()
266 files |= (set(mp1) | set(mp2)) - set(mc)
266 files |= (set(mp1) | set(mp2)) - set(mc)
267 for f in mc:
267 for f in mc:
268 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
268 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
269 files.add(f)
269 files.add(f)
270 return files
270 return files
271
271
272 # Convert src parents to dst parents
272 # Convert src parents to dst parents
273 def _convertparents(ctx, revmap):
273 def _convertparents(ctx, revmap):
274 parents = []
274 parents = []
275 for p in ctx.parents():
275 for p in ctx.parents():
276 parents.append(revmap[p.node()])
276 parents.append(revmap[p.node()])
277 while len(parents) < 2:
277 while len(parents) < 2:
278 parents.append(node.nullid)
278 parents.append(node.nullid)
279 return parents
279 return parents
280
280
281 # Get memfilectx for a normal file
281 # Get memfilectx for a normal file
282 def _getnormalcontext(ui, ctx, f, revmap):
282 def _getnormalcontext(ui, ctx, f, revmap):
283 try:
283 try:
284 fctx = ctx.filectx(f)
284 fctx = ctx.filectx(f)
285 except error.LookupError:
285 except error.LookupError:
286 raise IOError
286 raise IOError
287 renamed = fctx.renamed()
287 renamed = fctx.renamed()
288 if renamed:
288 if renamed:
289 renamed = renamed[0]
289 renamed = renamed[0]
290
290
291 data = fctx.data()
291 data = fctx.data()
292 if f == '.hgtags':
292 if f == '.hgtags':
293 data = _converttags (ui, revmap, data)
293 data = _converttags (ui, revmap, data)
294 return context.memfilectx(f, data, 'l' in fctx.flags(),
294 return context.memfilectx(f, data, 'l' in fctx.flags(),
295 'x' in fctx.flags(), renamed)
295 'x' in fctx.flags(), renamed)
296
296
297 # Remap tag data using a revision map
297 # Remap tag data using a revision map
298 def _converttags(ui, revmap, data):
298 def _converttags(ui, revmap, data):
299 newdata = []
299 newdata = []
300 for line in data.splitlines():
300 for line in data.splitlines():
301 try:
301 try:
302 id, name = line.split(' ', 1)
302 id, name = line.split(' ', 1)
303 except ValueError:
303 except ValueError:
304 ui.warn(_('skipping incorrectly formatted tag %s\n'
304 ui.warn(_('skipping incorrectly formatted tag %s\n'
305 % line))
305 % line))
306 continue
306 continue
307 try:
307 try:
308 newid = node.bin(id)
308 newid = node.bin(id)
309 except TypeError:
309 except TypeError:
310 ui.warn(_('skipping incorrectly formatted id %s\n'
310 ui.warn(_('skipping incorrectly formatted id %s\n'
311 % id))
311 % id))
312 continue
312 continue
313 try:
313 try:
314 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
314 newdata.append('%s %s\n' % (node.hex(revmap[newid]),
315 name))
315 name))
316 except KeyError:
316 except KeyError:
317 ui.warn(_('no mapping for id %s\n') % id)
317 ui.warn(_('no mapping for id %s\n') % id)
318 continue
318 continue
319 return ''.join(newdata)
319 return ''.join(newdata)
320
320
321 def _islfile(file, ctx, matcher, size):
321 def _islfile(file, ctx, matcher, size):
322 '''Return true if file should be considered a largefile, i.e.
322 '''Return true if file should be considered a largefile, i.e.
323 matcher matches it or it is larger than size.'''
323 matcher matches it or it is larger than size.'''
324 # never store special .hg* files as largefiles
324 # never store special .hg* files as largefiles
325 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
325 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
326 return False
326 return False
327 if matcher and matcher(file):
327 if matcher and matcher(file):
328 return True
328 return True
329 try:
329 try:
330 return ctx.filectx(file).size() >= size * 1024 * 1024
330 return ctx.filectx(file).size() >= size * 1024 * 1024
331 except error.LookupError:
331 except error.LookupError:
332 return False
332 return False
333
333
334 def uploadlfiles(ui, rsrc, rdst, files):
334 def uploadlfiles(ui, rsrc, rdst, files):
335 '''upload largefiles to the central store'''
335 '''upload largefiles to the central store'''
336
336
337 if not files:
337 if not files:
338 return
338 return
339
339
340 store = basestore._openstore(rsrc, rdst, put=True)
340 store = basestore._openstore(rsrc, rdst, put=True)
341
341
342 at = 0
342 at = 0
343 files = filter(lambda h: not store.exists(h), files)
343 ui.debug("sending statlfile command for %d largefiles\n" % len(files))
344 retval = store.exists(files)
345 files = filter(lambda h: not retval[h], files)
346 ui.debug("%d largefiles need to be uploaded\n" % len(files))
347
344 for hash in files:
348 for hash in files:
345 ui.progress(_('uploading largefiles'), at, unit='largefile',
349 ui.progress(_('uploading largefiles'), at, unit='largefile',
346 total=len(files))
350 total=len(files))
347 source = lfutil.findfile(rsrc, hash)
351 source = lfutil.findfile(rsrc, hash)
348 if not source:
352 if not source:
349 raise util.Abort(_('largefile %s missing from store'
353 raise util.Abort(_('largefile %s missing from store'
350 ' (needs to be uploaded)') % hash)
354 ' (needs to be uploaded)') % hash)
351 # XXX check for errors here
355 # XXX check for errors here
352 store.put(source, hash)
356 store.put(source, hash)
353 at += 1
357 at += 1
354 ui.progress(_('uploading largefiles'), None)
358 ui.progress(_('uploading largefiles'), None)
355
359
356 def verifylfiles(ui, repo, all=False, contents=False):
360 def verifylfiles(ui, repo, all=False, contents=False):
357 '''Verify that every big file revision in the current changeset
361 '''Verify that every big file revision in the current changeset
358 exists in the central store. With --contents, also verify that
362 exists in the central store. With --contents, also verify that
359 the contents of each big file revision are correct (SHA-1 hash
363 the contents of each big file revision are correct (SHA-1 hash
360 matches the revision ID). With --all, check every changeset in
364 matches the revision ID). With --all, check every changeset in
361 this repository.'''
365 this repository.'''
362 if all:
366 if all:
363 # Pass a list to the function rather than an iterator because we know a
367 # Pass a list to the function rather than an iterator because we know a
364 # list will work.
368 # list will work.
365 revs = range(len(repo))
369 revs = range(len(repo))
366 else:
370 else:
367 revs = ['.']
371 revs = ['.']
368
372
369 store = basestore._openstore(repo)
373 store = basestore._openstore(repo)
370 return store.verify(revs, contents=contents)
374 return store.verify(revs, contents=contents)
371
375
372 def cachelfiles(ui, repo, node, filelist=None):
376 def cachelfiles(ui, repo, node, filelist=None):
373 '''cachelfiles ensures that all largefiles needed by the specified revision
377 '''cachelfiles ensures that all largefiles needed by the specified revision
374 are present in the repository's largefile cache.
378 are present in the repository's largefile cache.
375
379
376 returns a tuple (cached, missing). cached is the list of files downloaded
380 returns a tuple (cached, missing). cached is the list of files downloaded
377 by this operation; missing is the list of files that were needed but could
381 by this operation; missing is the list of files that were needed but could
378 not be found.'''
382 not be found.'''
379 lfiles = lfutil.listlfiles(repo, node)
383 lfiles = lfutil.listlfiles(repo, node)
380 if filelist:
384 if filelist:
381 lfiles = set(lfiles) & set(filelist)
385 lfiles = set(lfiles) & set(filelist)
382 toget = []
386 toget = []
383
387
384 for lfile in lfiles:
388 for lfile in lfiles:
385 # If we are mid-merge, then we have to trust the standin that is in the
389 # If we are mid-merge, then we have to trust the standin that is in the
386 # working copy to have the correct hashvalue. This is because the
390 # working copy to have the correct hashvalue. This is because the
387 # original hg.merge() already updated the standin as part of the normal
391 # original hg.merge() already updated the standin as part of the normal
388 # merge process -- we just have to udpate the largefile to match.
392 # merge process -- we just have to udpate the largefile to match.
389 if (getattr(repo, "_ismerging", False) and
393 if (getattr(repo, "_ismerging", False) and
390 os.path.exists(repo.wjoin(lfutil.standin(lfile)))):
394 os.path.exists(repo.wjoin(lfutil.standin(lfile)))):
391 expectedhash = lfutil.readstandin(repo, lfile)
395 expectedhash = lfutil.readstandin(repo, lfile)
392 else:
396 else:
393 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
397 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
394
398
395 # if it exists and its hash matches, it might have been locally
399 # if it exists and its hash matches, it might have been locally
396 # modified before updating and the user chose 'local'. in this case,
400 # modified before updating and the user chose 'local'. in this case,
397 # it will not be in any store, so don't look for it.
401 # it will not be in any store, so don't look for it.
398 if ((not os.path.exists(repo.wjoin(lfile)) or
402 if ((not os.path.exists(repo.wjoin(lfile)) or
399 expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and
403 expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and
400 not lfutil.findfile(repo, expectedhash)):
404 not lfutil.findfile(repo, expectedhash)):
401 toget.append((lfile, expectedhash))
405 toget.append((lfile, expectedhash))
402
406
403 if toget:
407 if toget:
404 store = basestore._openstore(repo)
408 store = basestore._openstore(repo)
405 ret = store.get(toget)
409 ret = store.get(toget)
406 return ret
410 return ret
407
411
408 return ([], [])
412 return ([], [])
409
413
410 def downloadlfiles(ui, repo, rev=None):
414 def downloadlfiles(ui, repo, rev=None):
411 matchfn = scmutil.match(repo[None],
415 matchfn = scmutil.match(repo[None],
412 [repo.wjoin(lfutil.shortname)], {})
416 [repo.wjoin(lfutil.shortname)], {})
413 def prepare(ctx, fns):
417 def prepare(ctx, fns):
414 pass
418 pass
415 totalsuccess = 0
419 totalsuccess = 0
416 totalmissing = 0
420 totalmissing = 0
417 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
421 for ctx in cmdutil.walkchangerevs(repo, matchfn, {'rev' : rev},
418 prepare):
422 prepare):
419 success, missing = cachelfiles(ui, repo, ctx.node())
423 success, missing = cachelfiles(ui, repo, ctx.node())
420 totalsuccess += len(success)
424 totalsuccess += len(success)
421 totalmissing += len(missing)
425 totalmissing += len(missing)
422 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
426 ui.status(_("%d additional largefiles cached\n") % totalsuccess)
423 if totalmissing > 0:
427 if totalmissing > 0:
424 ui.status(_("%d largefiles failed to download\n") % totalmissing)
428 ui.status(_("%d largefiles failed to download\n") % totalmissing)
425 return totalsuccess, totalmissing
429 return totalsuccess, totalmissing
426
430
427 def updatelfiles(ui, repo, filelist=None, printmessage=True):
431 def updatelfiles(ui, repo, filelist=None, printmessage=True):
428 wlock = repo.wlock()
432 wlock = repo.wlock()
429 try:
433 try:
430 lfdirstate = lfutil.openlfdirstate(ui, repo)
434 lfdirstate = lfutil.openlfdirstate(ui, repo)
431 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
435 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
432
436
433 if filelist is not None:
437 if filelist is not None:
434 lfiles = [f for f in lfiles if f in filelist]
438 lfiles = [f for f in lfiles if f in filelist]
435
439
436 printed = False
440 printed = False
437 if printmessage and lfiles:
441 if printmessage and lfiles:
438 ui.status(_('getting changed largefiles\n'))
442 ui.status(_('getting changed largefiles\n'))
439 printed = True
443 printed = True
440 cachelfiles(ui, repo, '.', lfiles)
444 cachelfiles(ui, repo, '.', lfiles)
441
445
442 updated, removed = 0, 0
446 updated, removed = 0, 0
443 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
447 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
444 # increment the appropriate counter according to _updatelfile's
448 # increment the appropriate counter according to _updatelfile's
445 # return value
449 # return value
446 updated += i > 0 and i or 0
450 updated += i > 0 and i or 0
447 removed -= i < 0 and i or 0
451 removed -= i < 0 and i or 0
448 if printmessage and (removed or updated) and not printed:
452 if printmessage and (removed or updated) and not printed:
449 ui.status(_('getting changed largefiles\n'))
453 ui.status(_('getting changed largefiles\n'))
450 printed = True
454 printed = True
451
455
452 lfdirstate.write()
456 lfdirstate.write()
453 if printed and printmessage:
457 if printed and printmessage:
454 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
458 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
455 removed))
459 removed))
456 finally:
460 finally:
457 wlock.release()
461 wlock.release()
458
462
459 def _updatelfile(repo, lfdirstate, lfile):
463 def _updatelfile(repo, lfdirstate, lfile):
460 '''updates a single largefile and copies the state of its standin from
464 '''updates a single largefile and copies the state of its standin from
461 the repository's dirstate to its state in the lfdirstate.
465 the repository's dirstate to its state in the lfdirstate.
462
466
463 returns 1 if the file was modified, -1 if the file was removed, 0 if the
467 returns 1 if the file was modified, -1 if the file was removed, 0 if the
464 file was unchanged, and None if the needed largefile was missing from the
468 file was unchanged, and None if the needed largefile was missing from the
465 cache.'''
469 cache.'''
466 ret = 0
470 ret = 0
467 abslfile = repo.wjoin(lfile)
471 abslfile = repo.wjoin(lfile)
468 absstandin = repo.wjoin(lfutil.standin(lfile))
472 absstandin = repo.wjoin(lfutil.standin(lfile))
469 if os.path.exists(absstandin):
473 if os.path.exists(absstandin):
470 if os.path.exists(absstandin+'.orig'):
474 if os.path.exists(absstandin+'.orig'):
471 shutil.copyfile(abslfile, abslfile+'.orig')
475 shutil.copyfile(abslfile, abslfile+'.orig')
472 expecthash = lfutil.readstandin(repo, lfile)
476 expecthash = lfutil.readstandin(repo, lfile)
473 if (expecthash != '' and
477 if (expecthash != '' and
474 (not os.path.exists(abslfile) or
478 (not os.path.exists(abslfile) or
475 expecthash != lfutil.hashfile(abslfile))):
479 expecthash != lfutil.hashfile(abslfile))):
476 if not lfutil.copyfromcache(repo, expecthash, lfile):
480 if not lfutil.copyfromcache(repo, expecthash, lfile):
477 # use normallookup() to allocate entry in largefiles dirstate,
481 # use normallookup() to allocate entry in largefiles dirstate,
478 # because lack of it misleads lfilesrepo.status() into
482 # because lack of it misleads lfilesrepo.status() into
479 # recognition that such cache missing files are REMOVED.
483 # recognition that such cache missing files are REMOVED.
480 lfdirstate.normallookup(lfile)
484 lfdirstate.normallookup(lfile)
481 return None # don't try to set the mode
485 return None # don't try to set the mode
482 ret = 1
486 ret = 1
483 mode = os.stat(absstandin).st_mode
487 mode = os.stat(absstandin).st_mode
484 if mode != os.stat(abslfile).st_mode:
488 if mode != os.stat(abslfile).st_mode:
485 os.chmod(abslfile, mode)
489 os.chmod(abslfile, mode)
486 ret = 1
490 ret = 1
487 else:
491 else:
488 # Remove lfiles for which the standin is deleted, unless the
492 # Remove lfiles for which the standin is deleted, unless the
489 # lfile is added to the repository again. This happens when a
493 # lfile is added to the repository again. This happens when a
490 # largefile is converted back to a normal file: the standin
494 # largefile is converted back to a normal file: the standin
491 # disappears, but a new (normal) file appears as the lfile.
495 # disappears, but a new (normal) file appears as the lfile.
492 if os.path.exists(abslfile) and lfile not in repo[None]:
496 if os.path.exists(abslfile) and lfile not in repo[None]:
493 util.unlinkpath(abslfile)
497 util.unlinkpath(abslfile)
494 ret = -1
498 ret = -1
495 state = repo.dirstate[lfutil.standin(lfile)]
499 state = repo.dirstate[lfutil.standin(lfile)]
496 if state == 'n':
500 if state == 'n':
497 # When rebasing, we need to synchronize the standin and the largefile,
501 # When rebasing, we need to synchronize the standin and the largefile,
498 # because otherwise the largefile will get reverted. But for commit's
502 # because otherwise the largefile will get reverted. But for commit's
499 # sake, we have to mark the file as unclean.
503 # sake, we have to mark the file as unclean.
500 if getattr(repo, "_isrebasing", False):
504 if getattr(repo, "_isrebasing", False):
501 lfdirstate.normallookup(lfile)
505 lfdirstate.normallookup(lfile)
502 else:
506 else:
503 lfdirstate.normal(lfile)
507 lfdirstate.normal(lfile)
504 elif state == 'r':
508 elif state == 'r':
505 lfdirstate.remove(lfile)
509 lfdirstate.remove(lfile)
506 elif state == 'a':
510 elif state == 'a':
507 lfdirstate.add(lfile)
511 lfdirstate.add(lfile)
508 elif state == '?':
512 elif state == '?':
509 lfdirstate.drop(lfile)
513 lfdirstate.drop(lfile)
510 return ret
514 return ret
511
515
512 def catlfile(repo, lfile, rev, filename):
516 def catlfile(repo, lfile, rev, filename):
513 hash = lfutil.readstandin(repo, lfile, rev)
517 hash = lfutil.readstandin(repo, lfile, rev)
514 if not lfutil.inusercache(repo.ui, hash):
518 if not lfutil.inusercache(repo.ui, hash):
515 store = basestore._openstore(repo)
519 store = basestore._openstore(repo)
516 success, missing = store.get([(lfile, hash)])
520 success, missing = store.get([(lfile, hash)])
517 if len(success) != 1:
521 if len(success) != 1:
518 raise util.Abort(
522 raise util.Abort(
519 _('largefile %s is not in cache and could not be downloaded')
523 _('largefile %s is not in cache and could not be downloaded')
520 % lfile)
524 % lfile)
521 path = lfutil.usercachepath(repo.ui, hash)
525 path = lfutil.usercachepath(repo.ui, hash)
522 fpout = cmdutil.makefileobj(repo, filename)
526 fpout = cmdutil.makefileobj(repo, filename)
523 fpin = open(path, "rb")
527 fpin = open(path, "rb")
524 fpout.write(fpin.read())
528 fpout.write(fpin.read())
525 fpout.close()
529 fpout.close()
526 fpin.close()
530 fpin.close()
527 return 0
531 return 0
528
532
529 # -- hg commands declarations ------------------------------------------------
533 # -- hg commands declarations ------------------------------------------------
530
534
531 cmdtable = {
535 cmdtable = {
532 'lfconvert': (lfconvert,
536 'lfconvert': (lfconvert,
533 [('s', 'size', '',
537 [('s', 'size', '',
534 _('minimum size (MB) for files to be converted '
538 _('minimum size (MB) for files to be converted '
535 'as largefiles'),
539 'as largefiles'),
536 'SIZE'),
540 'SIZE'),
537 ('', 'to-normal', False,
541 ('', 'to-normal', False,
538 _('convert from a largefiles repo to a normal repo')),
542 _('convert from a largefiles repo to a normal repo')),
539 ],
543 ],
540 _('hg lfconvert SOURCE DEST [FILE ...]')),
544 _('hg lfconvert SOURCE DEST [FILE ...]')),
541 }
545 }
@@ -1,168 +1,173 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
8
9 from mercurial import error, httprepo, util, wireproto
9 from mercurial import error, httprepo, util, wireproto
10 from mercurial.wireproto import batchable, future
10 from mercurial.i18n import _
11 from mercurial.i18n import _
11
12
12 import lfutil
13 import lfutil
13
14
14 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
15 '\n\nPlease enable it in your Mercurial config '
16 '\n\nPlease enable it in your Mercurial config '
16 'file.\n')
17 'file.\n')
17
18
18 def putlfile(repo, proto, sha):
19 def putlfile(repo, proto, sha):
19 '''Put a largefile into a repository's local store and into the
20 '''Put a largefile into a repository's local store and into the
20 user cache.'''
21 user cache.'''
21 proto.redirect()
22 proto.redirect()
22
23
23 path = lfutil.storepath(repo, sha)
24 path = lfutil.storepath(repo, sha)
24 util.makedirs(os.path.dirname(path))
25 util.makedirs(os.path.dirname(path))
25 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
26 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
26
27
27 try:
28 try:
28 try:
29 try:
29 proto.getfile(tmpfp)
30 proto.getfile(tmpfp)
30 tmpfp._fp.seek(0)
31 tmpfp._fp.seek(0)
31 if sha != lfutil.hexsha1(tmpfp._fp):
32 if sha != lfutil.hexsha1(tmpfp._fp):
32 raise IOError(0, _('largefile contents do not match hash'))
33 raise IOError(0, _('largefile contents do not match hash'))
33 tmpfp.close()
34 tmpfp.close()
34 lfutil.linktousercache(repo, sha)
35 lfutil.linktousercache(repo, sha)
35 except IOError, e:
36 except IOError, e:
36 repo.ui.warn(_('largefiles: failed to put %s into store: %s') %
37 repo.ui.warn(_('largefiles: failed to put %s into store: %s') %
37 (sha, e.strerror))
38 (sha, e.strerror))
38 return wireproto.pushres(1)
39 return wireproto.pushres(1)
39 finally:
40 finally:
40 tmpfp.discard()
41 tmpfp.discard()
41
42
42 return wireproto.pushres(0)
43 return wireproto.pushres(0)
43
44
44 def getlfile(repo, proto, sha):
45 def getlfile(repo, proto, sha):
45 '''Retrieve a largefile from the repository-local cache or system
46 '''Retrieve a largefile from the repository-local cache or system
46 cache.'''
47 cache.'''
47 filename = lfutil.findfile(repo, sha)
48 filename = lfutil.findfile(repo, sha)
48 if not filename:
49 if not filename:
49 raise util.Abort(_('requested largefile %s not present in cache') % sha)
50 raise util.Abort(_('requested largefile %s not present in cache') % sha)
50 f = open(filename, 'rb')
51 f = open(filename, 'rb')
51 length = os.fstat(f.fileno())[6]
52 length = os.fstat(f.fileno())[6]
52
53
53 # Since we can't set an HTTP content-length header here, and
54 # Since we can't set an HTTP content-length header here, and
54 # Mercurial core provides no way to give the length of a streamres
55 # Mercurial core provides no way to give the length of a streamres
55 # (and reading the entire file into RAM would be ill-advised), we
56 # (and reading the entire file into RAM would be ill-advised), we
56 # just send the length on the first line of the response, like the
57 # just send the length on the first line of the response, like the
57 # ssh proto does for string responses.
58 # ssh proto does for string responses.
58 def generator():
59 def generator():
59 yield '%d\n' % length
60 yield '%d\n' % length
60 for chunk in f:
61 for chunk in f:
61 yield chunk
62 yield chunk
62 return wireproto.streamres(generator())
63 return wireproto.streamres(generator())
63
64
64 def statlfile(repo, proto, sha):
65 def statlfile(repo, proto, sha):
65 '''Return '2\n' if the largefile is missing, '1\n' if it has a
66 '''Return '2\n' if the largefile is missing, '1\n' if it has a
66 mismatched checksum, or '0\n' if it is in good condition'''
67 mismatched checksum, or '0\n' if it is in good condition'''
67 filename = lfutil.findfile(repo, sha)
68 filename = lfutil.findfile(repo, sha)
68 if not filename:
69 if not filename:
69 return '2\n'
70 return '2\n'
70 fd = None
71 fd = None
71 try:
72 try:
72 fd = open(filename, 'rb')
73 fd = open(filename, 'rb')
73 return lfutil.hexsha1(fd) == sha and '0\n' or '1\n'
74 return lfutil.hexsha1(fd) == sha and '0\n' or '1\n'
74 finally:
75 finally:
75 if fd:
76 if fd:
76 fd.close()
77 fd.close()
77
78
78 def wirereposetup(ui, repo):
79 def wirereposetup(ui, repo):
79 class lfileswirerepository(repo.__class__):
80 class lfileswirerepository(repo.__class__):
80 def putlfile(self, sha, fd):
81 def putlfile(self, sha, fd):
81 # unfortunately, httprepository._callpush tries to convert its
82 # unfortunately, httprepository._callpush tries to convert its
82 # input file-like into a bundle before sending it, so we can't use
83 # input file-like into a bundle before sending it, so we can't use
83 # it ...
84 # it ...
84 if issubclass(self.__class__, httprepo.httprepository):
85 if issubclass(self.__class__, httprepo.httprepository):
85 res = None
86 res = None
86 try:
87 try:
87 res = self._call('putlfile', data=fd, sha=sha,
88 res = self._call('putlfile', data=fd, sha=sha,
88 headers={'content-type':'application/mercurial-0.1'})
89 headers={'content-type':'application/mercurial-0.1'})
89 d, output = res.split('\n', 1)
90 d, output = res.split('\n', 1)
90 for l in output.splitlines(True):
91 for l in output.splitlines(True):
91 self.ui.warn(_('remote: '), l, '\n')
92 self.ui.warn(_('remote: '), l, '\n')
92 return int(d)
93 return int(d)
93 except (ValueError, urllib2.HTTPError):
94 except (ValueError, urllib2.HTTPError):
94 self.ui.warn(_('unexpected putlfile response: %s') % res)
95 self.ui.warn(_('unexpected putlfile response: %s') % res)
95 return 1
96 return 1
96 # ... but we can't use sshrepository._call because the data=
97 # ... but we can't use sshrepository._call because the data=
97 # argument won't get sent, and _callpush does exactly what we want
98 # argument won't get sent, and _callpush does exactly what we want
98 # in this case: send the data straight through
99 # in this case: send the data straight through
99 else:
100 else:
100 try:
101 try:
101 ret, output = self._callpush("putlfile", fd, sha=sha)
102 ret, output = self._callpush("putlfile", fd, sha=sha)
102 if ret == "":
103 if ret == "":
103 raise error.ResponseError(_('putlfile failed:'),
104 raise error.ResponseError(_('putlfile failed:'),
104 output)
105 output)
105 return int(ret)
106 return int(ret)
106 except IOError:
107 except IOError:
107 return 1
108 return 1
108 except ValueError:
109 except ValueError:
109 raise error.ResponseError(
110 raise error.ResponseError(
110 _('putlfile failed (unexpected response):'), ret)
111 _('putlfile failed (unexpected response):'), ret)
111
112
112 def getlfile(self, sha):
113 def getlfile(self, sha):
113 stream = self._callstream("getlfile", sha=sha)
114 stream = self._callstream("getlfile", sha=sha)
114 length = stream.readline()
115 length = stream.readline()
115 try:
116 try:
116 length = int(length)
117 length = int(length)
117 except ValueError:
118 except ValueError:
118 self._abort(error.ResponseError(_("unexpected response:"),
119 self._abort(error.ResponseError(_("unexpected response:"),
119 length))
120 length))
120 return (length, stream)
121 return (length, stream)
121
122
123 @batchable
122 def statlfile(self, sha):
124 def statlfile(self, sha):
125 f = future()
126 result = {'sha': sha}
127 yield result, f
123 try:
128 try:
124 return int(self._call("statlfile", sha=sha))
129 yield int(f.value)
125 except (ValueError, urllib2.HTTPError):
130 except (ValueError, urllib2.HTTPError):
126 # If the server returns anything but an integer followed by a
131 # If the server returns anything but an integer followed by a
127 # newline, newline, it's not speaking our language; if we get
132 # newline, newline, it's not speaking our language; if we get
128 # an HTTP error, we can't be sure the largefile is present;
133 # an HTTP error, we can't be sure the largefile is present;
129 # either way, consider it missing.
134 # either way, consider it missing.
130 return 2
135 yield 2
131
136
132 repo.__class__ = lfileswirerepository
137 repo.__class__ = lfileswirerepository
133
138
134 # advertise the largefiles=serve capability
139 # advertise the largefiles=serve capability
135 def capabilities(repo, proto):
140 def capabilities(repo, proto):
136 return capabilitiesorig(repo, proto) + ' largefiles=serve'
141 return capabilitiesorig(repo, proto) + ' largefiles=serve'
137
142
138 # duplicate what Mercurial's new out-of-band errors mechanism does, because
143 # duplicate what Mercurial's new out-of-band errors mechanism does, because
139 # clients old and new alike both handle it well
144 # clients old and new alike both handle it well
140 def webprotorefuseclient(self, message):
145 def webprotorefuseclient(self, message):
141 self.req.header([('Content-Type', 'application/hg-error')])
146 self.req.header([('Content-Type', 'application/hg-error')])
142 return message
147 return message
143
148
144 def sshprotorefuseclient(self, message):
149 def sshprotorefuseclient(self, message):
145 self.ui.write_err('%s\n-\n' % message)
150 self.ui.write_err('%s\n-\n' % message)
146 self.fout.write('\n')
151 self.fout.write('\n')
147 self.fout.flush()
152 self.fout.flush()
148
153
149 return ''
154 return ''
150
155
151 def heads(repo, proto):
156 def heads(repo, proto):
152 if lfutil.islfilesrepo(repo):
157 if lfutil.islfilesrepo(repo):
153 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
158 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
154 return wireproto.heads(repo, proto)
159 return wireproto.heads(repo, proto)
155
160
156 def sshrepocallstream(self, cmd, **args):
161 def sshrepocallstream(self, cmd, **args):
157 if cmd == 'heads' and self.capable('largefiles'):
162 if cmd == 'heads' and self.capable('largefiles'):
158 cmd = 'lheads'
163 cmd = 'lheads'
159 if cmd == 'batch' and self.capable('largefiles'):
164 if cmd == 'batch' and self.capable('largefiles'):
160 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
165 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
161 return ssholdcallstream(self, cmd, **args)
166 return ssholdcallstream(self, cmd, **args)
162
167
163 def httprepocallstream(self, cmd, **args):
168 def httprepocallstream(self, cmd, **args):
164 if cmd == 'heads' and self.capable('largefiles'):
169 if cmd == 'heads' and self.capable('largefiles'):
165 cmd = 'lheads'
170 cmd = 'lheads'
166 if cmd == 'batch' and self.capable('largefiles'):
171 if cmd == 'batch' and self.capable('largefiles'):
167 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
172 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
168 return httpoldcallstream(self, cmd, **args)
173 return httpoldcallstream(self, cmd, **args)
@@ -1,106 +1,110 b''
1 # Copyright 2010-2011 Fog Creek Software
1 # Copyright 2010-2011 Fog Creek Software
2 # Copyright 2010-2011 Unity Technologies
2 # Copyright 2010-2011 Unity Technologies
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 '''remote largefile store; the base class for servestore'''
7 '''remote largefile store; the base class for servestore'''
8
8
9 import urllib2
9 import urllib2
10
10
11 from mercurial import util
11 from mercurial import util
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.wireproto import remotebatch
13
14
14 import lfutil
15 import lfutil
15 import basestore
16 import basestore
16
17
17 class remotestore(basestore.basestore):
18 class remotestore(basestore.basestore):
18 '''a largefile store accessed over a network'''
19 '''a largefile store accessed over a network'''
19 def __init__(self, ui, repo, url):
20 def __init__(self, ui, repo, url):
20 super(remotestore, self).__init__(ui, repo, url)
21 super(remotestore, self).__init__(ui, repo, url)
21
22
22 def put(self, source, hash):
23 def put(self, source, hash):
23 if self._verify(hash):
24 return
25 if self.sendfile(source, hash):
24 if self.sendfile(source, hash):
26 raise util.Abort(
25 raise util.Abort(
27 _('remotestore: could not put %s to remote store %s')
26 _('remotestore: could not put %s to remote store %s')
28 % (source, self.url))
27 % (source, self.url))
29 self.ui.debug(
28 self.ui.debug(
30 _('remotestore: put %s to remote store %s') % (source, self.url))
29 _('remotestore: put %s to remote store %s') % (source, self.url))
31
30
32 def exists(self, hash):
31 def exists(self, hashes):
33 return self._verify(hash)
32 return self._verify(hashes)
34
33
35 def sendfile(self, filename, hash):
34 def sendfile(self, filename, hash):
36 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
35 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 fd = None
36 fd = None
38 try:
37 try:
39 try:
38 try:
40 fd = lfutil.httpsendfile(self.ui, filename)
39 fd = lfutil.httpsendfile(self.ui, filename)
41 except IOError, e:
40 except IOError, e:
42 raise util.Abort(
41 raise util.Abort(
43 _('remotestore: could not open file %s: %s')
42 _('remotestore: could not open file %s: %s')
44 % (filename, str(e)))
43 % (filename, str(e)))
45 return self._put(hash, fd)
44 return self._put(hash, fd)
46 finally:
45 finally:
47 if fd:
46 if fd:
48 fd.close()
47 fd.close()
49
48
50 def _getfile(self, tmpfile, filename, hash):
49 def _getfile(self, tmpfile, filename, hash):
51 # quit if the largefile isn't there
50 # quit if the largefile isn't there
52 stat = self._stat(hash)
51 stat = self._stat(hash)
53 if stat == 1:
52 if stat == 1:
54 raise util.Abort(_('remotestore: largefile %s is invalid') % hash)
53 raise util.Abort(_('remotestore: largefile %s is invalid') % hash)
55 elif stat == 2:
54 elif stat == 2:
56 raise util.Abort(_('remotestore: largefile %s is missing') % hash)
55 raise util.Abort(_('remotestore: largefile %s is missing') % hash)
57
56
58 try:
57 try:
59 length, infile = self._get(hash)
58 length, infile = self._get(hash)
60 except urllib2.HTTPError, e:
59 except urllib2.HTTPError, e:
61 # 401s get converted to util.Aborts; everything else is fine being
60 # 401s get converted to util.Aborts; everything else is fine being
62 # turned into a StoreError
61 # turned into a StoreError
63 raise basestore.StoreError(filename, hash, self.url, str(e))
62 raise basestore.StoreError(filename, hash, self.url, str(e))
64 except urllib2.URLError, e:
63 except urllib2.URLError, e:
65 # This usually indicates a connection problem, so don't
64 # This usually indicates a connection problem, so don't
66 # keep trying with the other files... they will probably
65 # keep trying with the other files... they will probably
67 # all fail too.
66 # all fail too.
68 raise util.Abort('%s: %s' % (self.url, e.reason))
67 raise util.Abort('%s: %s' % (self.url, e.reason))
69 except IOError, e:
68 except IOError, e:
70 raise basestore.StoreError(filename, hash, self.url, str(e))
69 raise basestore.StoreError(filename, hash, self.url, str(e))
71
70
72 # Mercurial does not close its SSH connections after writing a stream
71 # Mercurial does not close its SSH connections after writing a stream
73 if length is not None:
72 if length is not None:
74 infile = lfutil.limitreader(infile, length)
73 infile = lfutil.limitreader(infile, length)
75 return lfutil.copyandhash(lfutil.blockstream(infile), tmpfile)
74 return lfutil.copyandhash(lfutil.blockstream(infile), tmpfile)
76
75
77 def _verify(self, hash):
76 def _verify(self, hashes):
78 return not self._stat(hash)
77 return self._stat(hashes)
79
78
80 def _verifyfile(self, cctx, cset, contents, standin, verified):
79 def _verifyfile(self, cctx, cset, contents, standin, verified):
81 filename = lfutil.splitstandin(standin)
80 filename = lfutil.splitstandin(standin)
82 if not filename:
81 if not filename:
83 return False
82 return False
84 fctx = cctx[standin]
83 fctx = cctx[standin]
85 key = (filename, fctx.filenode())
84 key = (filename, fctx.filenode())
86 if key in verified:
85 if key in verified:
87 return False
86 return False
88
87
89 verified.add(key)
88 verified.add(key)
90
89
91 stat = self._stat(hash)
90 stat = self._stat(hash)
92 if not stat:
91 if not stat:
93 return False
92 return False
94 elif stat == 1:
93 elif stat == 1:
95 self.ui.warn(
94 self.ui.warn(
96 _('changeset %s: %s: contents differ\n')
95 _('changeset %s: %s: contents differ\n')
97 % (cset, filename))
96 % (cset, filename))
98 return True # failed
97 return True # failed
99 elif stat == 2:
98 elif stat == 2:
100 self.ui.warn(
99 self.ui.warn(
101 _('changeset %s: %s missing\n')
100 _('changeset %s: %s missing\n')
102 % (cset, filename))
101 % (cset, filename))
103 return True # failed
102 return True # failed
104 else:
103 else:
105 raise RuntimeError('verify failed: unexpected response from '
104 raise RuntimeError('verify failed: unexpected response from '
106 'statlfile (%r)' % stat)
105 'statlfile (%r)' % stat)
106
107 def batch(self):
108 '''Support for remote batching.'''
109 return remotebatch(self)
110
@@ -1,29 +1,37 b''
1 # Copyright 2010-2011 Fog Creek Software
1 # Copyright 2010-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 '''largefile store working over Mercurial's wire protocol'''
6 '''largefile store working over Mercurial's wire protocol'''
7
7
8 import lfutil
8 import lfutil
9 import remotestore
9 import remotestore
10
10
11 class wirestore(remotestore.remotestore):
11 class wirestore(remotestore.remotestore):
12 def __init__(self, ui, repo, remote):
12 def __init__(self, ui, repo, remote):
13 cap = remote.capable('largefiles')
13 cap = remote.capable('largefiles')
14 if not cap:
14 if not cap:
15 raise lfutil.storeprotonotcapable([])
15 raise lfutil.storeprotonotcapable([])
16 storetypes = cap.split(',')
16 storetypes = cap.split(',')
17 if 'serve' not in storetypes:
17 if 'serve' not in storetypes:
18 raise lfutil.storeprotonotcapable(storetypes)
18 raise lfutil.storeprotonotcapable(storetypes)
19 self.remote = remote
19 self.remote = remote
20 super(wirestore, self).__init__(ui, repo, remote.url())
20 super(wirestore, self).__init__(ui, repo, remote.url())
21
21
22 def _put(self, hash, fd):
22 def _put(self, hash, fd):
23 return self.remote.putlfile(hash, fd)
23 return self.remote.putlfile(hash, fd)
24
24
25 def _get(self, hash):
25 def _get(self, hash):
26 return self.remote.getlfile(hash)
26 return self.remote.getlfile(hash)
27
27
28 def _stat(self, hash):
28 def _stat(self, hashes):
29 return self.remote.statlfile(hash)
29 batch = self.remote.batch()
30 futures = {}
31 for hash in hashes:
32 futures[hash] = batch.statlfile(hash)
33 batch.submit()
34 retval = {}
35 for hash in hashes:
36 retval[hash] = not futures[hash].value
37 return retval
General Comments 0
You need to be logged in to leave comments. Login now