##// END OF EJS Templates
largefiles: respect store.createmode in basestore.get...
Martin Geisler -
r16154:9b072a5f stable
parent child Browse files
Show More
@@ -1,205 +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 os
12 import tempfile
13 import binascii
11 import binascii
14 import re
12 import re
15
13
16 from mercurial import util, node, hg
14 from mercurial import util, node, hg
17 from mercurial.i18n import _
15 from mercurial.i18n import _
18
16
19 import lfutil
17 import lfutil
20
18
21 class StoreError(Exception):
19 class StoreError(Exception):
22 '''Raised when there is a problem getting files from or putting
20 '''Raised when there is a problem getting files from or putting
23 files to a central store.'''
21 files to a central store.'''
24 def __init__(self, filename, hash, url, detail):
22 def __init__(self, filename, hash, url, detail):
25 self.filename = filename
23 self.filename = filename
26 self.hash = hash
24 self.hash = hash
27 self.url = url
25 self.url = url
28 self.detail = detail
26 self.detail = detail
29
27
30 def longmessage(self):
28 def longmessage(self):
31 if self.url:
29 if self.url:
32 return ('%s: %s\n'
30 return ('%s: %s\n'
33 '(failed URL: %s)\n'
31 '(failed URL: %s)\n'
34 % (self.filename, self.detail, self.url))
32 % (self.filename, self.detail, self.url))
35 else:
33 else:
36 return ('%s: %s\n'
34 return ('%s: %s\n'
37 '(no default or default-push path set in hgrc)\n'
35 '(no default or default-push path set in hgrc)\n'
38 % (self.filename, self.detail))
36 % (self.filename, self.detail))
39
37
40 def __str__(self):
38 def __str__(self):
41 return "%s: %s" % (self.url, self.detail)
39 return "%s: %s" % (self.url, self.detail)
42
40
43 class basestore(object):
41 class basestore(object):
44 def __init__(self, ui, repo, url):
42 def __init__(self, ui, repo, url):
45 self.ui = ui
43 self.ui = ui
46 self.repo = repo
44 self.repo = repo
47 self.url = url
45 self.url = url
48
46
49 def put(self, source, hash):
47 def put(self, source, hash):
50 '''Put source file into the store under <filename>/<hash>.'''
48 '''Put source file into the store under <filename>/<hash>.'''
51 raise NotImplementedError('abstract method')
49 raise NotImplementedError('abstract method')
52
50
53 def exists(self, hash):
51 def exists(self, hash):
54 '''Check to see if the store contains the given hash.'''
52 '''Check to see if the store contains the given hash.'''
55 raise NotImplementedError('abstract method')
53 raise NotImplementedError('abstract method')
56
54
57 def get(self, files):
55 def get(self, files):
58 '''Get the specified largefiles from the store and write to local
56 '''Get the specified largefiles from the store and write to local
59 files under repo.root. files is a list of (filename, hash)
57 files under repo.root. files is a list of (filename, hash)
60 tuples. Return (success, missing), lists of files successfuly
58 tuples. Return (success, missing), lists of files successfuly
61 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
62 of (filename, hash) tuples; missing is a list of filenames that
60 of (filename, hash) tuples; missing is a list of filenames that
63 we could not get. (The detailed error message will already have
61 we could not get. (The detailed error message will already have
64 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
65 summary.)'''
63 summary.)'''
66 success = []
64 success = []
67 missing = []
65 missing = []
68 ui = self.ui
66 ui = self.ui
69
67
70 at = 0
68 at = 0
71 for filename, hash in files:
69 for filename, hash in files:
72 ui.progress(_('getting largefiles'), at, unit='lfile',
70 ui.progress(_('getting largefiles'), at, unit='lfile',
73 total=len(files))
71 total=len(files))
74 at += 1
72 at += 1
75 ui.note(_('getting %s:%s\n') % (filename, hash))
73 ui.note(_('getting %s:%s\n') % (filename, hash))
76
74
77 storefilename = lfutil.storepath(self.repo, hash)
75 storefilename = lfutil.storepath(self.repo, hash)
78 storedir = os.path.dirname(storefilename)
76 tmpfile = util.atomictempfile(storefilename,
79
77 createmode=self.repo.store.createmode)
80 # No need to pass mode='wb' to fdopen(), since mkstemp() already
81 # opened the file in binary mode.
82 (tmpfd, tmpfilename) = tempfile.mkstemp(
83 dir=storedir, prefix=os.path.basename(filename))
84 tmpfile = os.fdopen(tmpfd, 'w')
85
78
86 try:
79 try:
87 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
80 hhash = binascii.hexlify(self._getfile(tmpfile, filename, hash))
88 except StoreError, err:
81 except StoreError, err:
89 ui.warn(err.longmessage())
82 ui.warn(err.longmessage())
90 hhash = ""
83 hhash = ""
91
84
92 if hhash != hash:
85 if hhash != hash:
93 if hhash != "":
86 if hhash != "":
94 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
87 ui.warn(_('%s: data corruption (expected %s, got %s)\n')
95 % (filename, hash, hhash))
88 % (filename, hash, hhash))
96 tmpfile.close() # no-op if it's already closed
89 tmpfile.discard() # no-op if it's already closed
97 os.remove(tmpfilename)
98 missing.append(filename)
90 missing.append(filename)
99 continue
91 continue
100
92
101 if os.path.exists(storefilename): # Windows
93 tmpfile.close()
102 os.remove(storefilename)
103 os.rename(tmpfilename, storefilename)
104 lfutil.linktousercache(self.repo, hash)
94 lfutil.linktousercache(self.repo, hash)
105 success.append((filename, hhash))
95 success.append((filename, hhash))
106
96
107 ui.progress(_('getting largefiles'), None)
97 ui.progress(_('getting largefiles'), None)
108 return (success, missing)
98 return (success, missing)
109
99
110 def verify(self, revs, contents=False):
100 def verify(self, revs, contents=False):
111 '''Verify the existence (and, optionally, contents) of every big
101 '''Verify the existence (and, optionally, contents) of every big
112 file revision referenced by every changeset in revs.
102 file revision referenced by every changeset in revs.
113 Return 0 if all is well, non-zero on any errors.'''
103 Return 0 if all is well, non-zero on any errors.'''
114 write = self.ui.write
104 write = self.ui.write
115 failed = False
105 failed = False
116
106
117 write(_('searching %d changesets for largefiles\n') % len(revs))
107 write(_('searching %d changesets for largefiles\n') % len(revs))
118 verified = set() # set of (filename, filenode) tuples
108 verified = set() # set of (filename, filenode) tuples
119
109
120 for rev in revs:
110 for rev in revs:
121 cctx = self.repo[rev]
111 cctx = self.repo[rev]
122 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
112 cset = "%d:%s" % (cctx.rev(), node.short(cctx.node()))
123
113
124 failed = util.any(self._verifyfile(
114 failed = util.any(self._verifyfile(
125 cctx, cset, contents, standin, verified) for standin in cctx)
115 cctx, cset, contents, standin, verified) for standin in cctx)
126
116
127 num_revs = len(verified)
117 num_revs = len(verified)
128 num_lfiles = len(set([fname for (fname, fnode) in verified]))
118 num_lfiles = len(set([fname for (fname, fnode) in verified]))
129 if contents:
119 if contents:
130 write(_('verified contents of %d revisions of %d largefiles\n')
120 write(_('verified contents of %d revisions of %d largefiles\n')
131 % (num_revs, num_lfiles))
121 % (num_revs, num_lfiles))
132 else:
122 else:
133 write(_('verified existence of %d revisions of %d largefiles\n')
123 write(_('verified existence of %d revisions of %d largefiles\n')
134 % (num_revs, num_lfiles))
124 % (num_revs, num_lfiles))
135
125
136 return int(failed)
126 return int(failed)
137
127
138 def _getfile(self, tmpfile, filename, hash):
128 def _getfile(self, tmpfile, filename, hash):
139 '''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
140 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
141 downloads and return the binary hash. Close tmpfile. Raise
131 downloads and return the binary hash. Close tmpfile. Raise
142 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
143 exist in the store).'''
133 exist in the store).'''
144 raise NotImplementedError('abstract method')
134 raise NotImplementedError('abstract method')
145
135
146 def _verifyfile(self, cctx, cset, contents, standin, verified):
136 def _verifyfile(self, cctx, cset, contents, standin, verified):
147 '''Perform the actual verification of a file in the store.
137 '''Perform the actual verification of a file in the store.
148 '''
138 '''
149 raise NotImplementedError('abstract method')
139 raise NotImplementedError('abstract method')
150
140
151 import localstore, wirestore
141 import localstore, wirestore
152
142
153 _storeprovider = {
143 _storeprovider = {
154 'file': [localstore.localstore],
144 'file': [localstore.localstore],
155 'http': [wirestore.wirestore],
145 'http': [wirestore.wirestore],
156 'https': [wirestore.wirestore],
146 'https': [wirestore.wirestore],
157 'ssh': [wirestore.wirestore],
147 'ssh': [wirestore.wirestore],
158 }
148 }
159
149
160 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
150 _scheme_re = re.compile(r'^([a-zA-Z0-9+-.]+)://')
161
151
162 # During clone this function is passed the src's ui object
152 # During clone this function is passed the src's ui object
163 # but it needs the dest's ui object so it can read out of
153 # but it needs the dest's ui object so it can read out of
164 # the config file. Use repo.ui instead.
154 # the config file. Use repo.ui instead.
165 def _openstore(repo, remote=None, put=False):
155 def _openstore(repo, remote=None, put=False):
166 ui = repo.ui
156 ui = repo.ui
167
157
168 if not remote:
158 if not remote:
169 lfpullsource = getattr(repo, 'lfpullsource', None)
159 lfpullsource = getattr(repo, 'lfpullsource', None)
170 if lfpullsource:
160 if lfpullsource:
171 path = ui.expandpath(lfpullsource)
161 path = ui.expandpath(lfpullsource)
172 else:
162 else:
173 path = ui.expandpath('default-push', 'default')
163 path = ui.expandpath('default-push', 'default')
174
164
175 # ui.expandpath() leaves 'default-push' and 'default' alone if
165 # ui.expandpath() leaves 'default-push' and 'default' alone if
176 # they cannot be expanded: fallback to the empty string,
166 # they cannot be expanded: fallback to the empty string,
177 # meaning the current directory.
167 # meaning the current directory.
178 if path == 'default-push' or path == 'default':
168 if path == 'default-push' or path == 'default':
179 path = ''
169 path = ''
180 remote = repo
170 remote = repo
181 else:
171 else:
182 remote = hg.peer(repo, {}, path)
172 remote = hg.peer(repo, {}, path)
183
173
184 # 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
185 # to resolve the scheme to a repository and use its path
175 # to resolve the scheme to a repository and use its path
186 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
176 path = util.safehasattr(remote, 'url') and remote.url() or remote.path
187
177
188 match = _scheme_re.match(path)
178 match = _scheme_re.match(path)
189 if not match: # regular filesystem path
179 if not match: # regular filesystem path
190 scheme = 'file'
180 scheme = 'file'
191 else:
181 else:
192 scheme = match.group(1)
182 scheme = match.group(1)
193
183
194 try:
184 try:
195 storeproviders = _storeprovider[scheme]
185 storeproviders = _storeprovider[scheme]
196 except KeyError:
186 except KeyError:
197 raise util.Abort(_('unsupported URL scheme %r') % scheme)
187 raise util.Abort(_('unsupported URL scheme %r') % scheme)
198
188
199 for class_obj in storeproviders:
189 for class_obj in storeproviders:
200 try:
190 try:
201 return class_obj(ui, repo, remote)
191 return class_obj(ui, repo, remote)
202 except lfutil.storeprotonotcapable:
192 except lfutil.storeprotonotcapable:
203 pass
193 pass
204
194
205 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,96 +1,105 b''
1 $ "$TESTDIR/hghave" unix-permissions || exit 80
1 $ "$TESTDIR/hghave" unix-permissions || exit 80
2
2
3 Create user cache directory
3 Create user cache directory
4
4
5 $ USERCACHE=`pwd`/cache; export USERCACHE
5 $ USERCACHE=`pwd`/cache; export USERCACHE
6 $ cat <<EOF >> ${HGRCPATH}
6 $ cat <<EOF >> ${HGRCPATH}
7 > [extensions]
7 > [extensions]
8 > hgext.largefiles=
8 > hgext.largefiles=
9 > [largefiles]
9 > [largefiles]
10 > usercache=${USERCACHE}
10 > usercache=${USERCACHE}
11 > EOF
11 > EOF
12 $ mkdir -p ${USERCACHE}
12 $ mkdir -p ${USERCACHE}
13
13
14 Create source repo, and commit adding largefile.
14 Create source repo, and commit adding largefile.
15
15
16 $ hg init src
16 $ hg init src
17 $ cd src
17 $ cd src
18 $ echo large > large
18 $ echo large > large
19 $ hg add --large large
19 $ hg add --large large
20 $ hg commit -m 'add largefile'
20 $ hg commit -m 'add largefile'
21 $ cd ..
21 $ cd ..
22
22
23 Discard all cached largefiles in USERCACHE
23 Discard all cached largefiles in USERCACHE
24
24
25 $ rm -rf ${USERCACHE}
25 $ rm -rf ${USERCACHE}
26
26
27 Create mirror repo, and pull from source without largefile:
27 Create mirror repo, and pull from source without largefile:
28 "pull" is used instead of "clone" for suppression of (1) updating to
28 "pull" is used instead of "clone" for suppression of (1) updating to
29 tip (= cahcing largefile from source repo), and (2) recording source
29 tip (= cahcing largefile from source repo), and (2) recording source
30 repo as "default" path in .hg/hgrc.
30 repo as "default" path in .hg/hgrc.
31
31
32 $ hg init mirror
32 $ hg init mirror
33 $ cd mirror
33 $ cd mirror
34 $ hg pull ../src
34 $ hg pull ../src
35 pulling from ../src
35 pulling from ../src
36 requesting all changes
36 requesting all changes
37 adding changesets
37 adding changesets
38 adding manifests
38 adding manifests
39 adding file changes
39 adding file changes
40 added 1 changesets with 1 changes to 1 files
40 added 1 changesets with 1 changes to 1 files
41 (run 'hg update' to get a working copy)
41 (run 'hg update' to get a working copy)
42 caching new largefiles
42 caching new largefiles
43 0 largefiles cached
43 0 largefiles cached
44
44
45 Update working directory to "tip", which requires largefile("large"),
45 Update working directory to "tip", which requires largefile("large"),
46 but there is no cache file for it. So, hg must treat it as
46 but there is no cache file for it. So, hg must treat it as
47 "missing"(!) file.
47 "missing"(!) file.
48
48
49 $ hg update
49 $ hg update
50 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
50 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
51 getting changed largefiles
51 getting changed largefiles
52 large: Can't get file locally
52 large: Can't get file locally
53 (no default or default-push path set in hgrc)
53 (no default or default-push path set in hgrc)
54 0 largefiles updated, 0 removed
54 0 largefiles updated, 0 removed
55 $ hg status
55 $ hg status
56 ! large
56 ! large
57
57
58 Update working directory to null: this cleanup .hg/largefiles/dirstate
58 Update working directory to null: this cleanup .hg/largefiles/dirstate
59
59
60 $ hg update null
60 $ hg update null
61 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
61 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
62 getting changed largefiles
62 getting changed largefiles
63 0 largefiles updated, 0 removed
63 0 largefiles updated, 0 removed
64
64
65 Update working directory to tip, again.
65 Update working directory to tip, again.
66
66
67 $ hg update
67 $ hg update
68 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
69 getting changed largefiles
69 getting changed largefiles
70 large: Can't get file locally
70 large: Can't get file locally
71 (no default or default-push path set in hgrc)
71 (no default or default-push path set in hgrc)
72 0 largefiles updated, 0 removed
72 0 largefiles updated, 0 removed
73 $ hg status
73 $ hg status
74 ! large
74 ! large
75
75
76 Portable way to print file permissions:
76 Portable way to print file permissions:
77
77
78 $ cd ..
78 $ cd ..
79 $ cat > ls-l.py <<EOF
79 $ cat > ls-l.py <<EOF
80 > #!/usr/bin/env python
80 > #!/usr/bin/env python
81 > import sys, os
81 > import sys, os
82 > path = sys.argv[1]
82 > path = sys.argv[1]
83 > print '%03o' % (os.lstat(path).st_mode & 0777)
83 > print '%03o' % (os.lstat(path).st_mode & 0777)
84 > EOF
84 > EOF
85 $ chmod +x ls-l.py
85 $ chmod +x ls-l.py
86
86
87 Test that files in .hg/largefiles inherit mode from .hg/store, not
87 Test that files in .hg/largefiles inherit mode from .hg/store, not
88 from file in working copy:
88 from file in working copy:
89
89
90 $ cd src
90 $ cd src
91 $ chmod 750 .hg/store
91 $ chmod 750 .hg/store
92 $ chmod 660 large
92 $ chmod 660 large
93 $ echo change >> large
93 $ echo change >> large
94 $ hg commit -m change
94 $ hg commit -m change
95 $ ../ls-l.py .hg/largefiles/e151b474069de4ca6898f67ce2f2a7263adf8fea
95 $ ../ls-l.py .hg/largefiles/e151b474069de4ca6898f67ce2f2a7263adf8fea
96 640
96 640
97
98 Test permission of with files in .hg/largefiles created by update:
99
100 $ cd ../mirror
101 $ rm -r "$USERCACHE" .hg/largefiles # avoid links
102 $ chmod 750 .hg/store
103 $ hg pull ../src --update -q
104 $ ../ls-l.py .hg/largefiles/e151b474069de4ca6898f67ce2f2a7263adf8fea
105 640
General Comments 0
You need to be logged in to leave comments. Login now