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