##// END OF EJS Templates
largefiles: improve help...
Greg Ward -
r15230:697289c5 default
parent child Browse files
Show More
@@ -1,40 +1,94 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 '''track large binary files
9 '''track large binary files
10
10
11 Large binary files tend to be not very compressible, not very "diffable", and
11 Large binary files tend to be not very compressible, not very
12 not at all mergeable. Such files are not handled well by Mercurial\'s storage
12 diffable, and not at all mergeable. Such files are not handled
13 format (revlog), which is based on compressed binary deltas. largefiles solves
13 efficiently by Mercurial's storage format (revlog), which is based on
14 this problem by adding a centralized client-server layer on top of Mercurial:
14 compressed binary deltas; storing large binary files as regular
15 largefiles live in a *central store* out on the network somewhere, and you only
15 Mercurial files wastes bandwidth and disk space and increases
16 fetch the ones that you need when you need them.
16 Mercurial's memory usage. The largefiles extension addresses these
17 problems by adding a centralized client-server layer on top of
18 Mercurial: largefiles live in a *central store* out on the network
19 somewhere, and you only fetch the revisions that you need when you
20 need them.
21
22 largefiles works by maintaining a "standin file" in .hglf/ for each
23 largefile. The standins are small (41 bytes: an SHA-1 hash plus
24 newline) and are tracked by Mercurial. Largefile revisions are
25 identified by the SHA-1 hash of their contents, which is written to
26 the standin. largefiles uses that revision ID to get/put largefile
27 revisions from/to the central store. This saves both disk space and
28 bandwidth, since you don't need to retrieve all historical revisions
29 of large files when you clone or pull.
30
31 To start a new repository or add new large binary files, just add
32 --large to your ``hg add`` command. For example::
33
34 $ dd if=/dev/urandom of=randomdata count=2000
35 $ hg add --large randomdata
36 $ hg commit -m 'add randomdata as a largefile'
37
38 When you push a changeset that adds/modifies largefiles to a remote
39 repository, its largefile revisions will be uploaded along with it.
40 Note that the remote Mercurial must also have the largefiles extension
41 enabled for this to work.
17
42
18 largefiles works by maintaining a *standin* in .hglf/ for each largefile. The
43 When you pull a changeset that affects largefiles from a remote
19 standins are small (41 bytes: an SHA-1 hash plus newline) and are tracked by
44 repository, Mercurial behaves as normal. However, when you update to
20 Mercurial. Largefile revisions are identified by the SHA-1 hash of their
45 such a revision, any largefiles needed by that revision are downloaded
21 contents, which is written to the standin. largefiles uses that revision ID to
46 and cached (if they have never been downloaded before). This means
22 get/put largefile revisions from/to the central store.
47 that network access may be required to update to changesets you have
48 not previously updated to.
49
50 If you already have large files tracked by Mercurial without the
51 largefiles extension, you will need to convert your repository in
52 order to benefit from largefiles. This is done with the 'hg lfconvert'
53 command::
54
55 $ hg lfconvert --size 10 oldrepo newrepo
23
56
24 A complete tutorial for using lfiles is included in ``usage.txt`` in the lfiles
57 In repositories that already have largefiles in them, any new file
25 source distribution. See
58 over 10MB will automatically be added as a largefile. To change this
26 https://developers.kilnhg.com/Repo/Kiln/largefiles/largefiles/File/usage.txt
59 threshhold, set ``largefiles.size`` in your Mercurial config file to
60 the minimum size in megabytes to track as a largefile, or use the
61 --lfsize option to the add command (also in megabytes)::
62
63 [largefiles]
64 size = 2 XXX wouldn't minsize be a better name?
65
66 $ hg add --lfsize 2
67
68 The ``largefiles.patterns`` config option allows you to specify a list
69 of filename patterns (see ``hg help patterns``) that should always be
70 tracked as largefiles::
71
72 [largefiles]
73 patterns =
74 *.jpg
75 re:.*\.(png|bmp)$
76 library.zip
77 content/audio/*
78
79 Files that match one of these patterns will be added as largefiles
80 regardless of their size.
27 '''
81 '''
28
82
29 from mercurial import commands
83 from mercurial import commands
30
84
31 import lfcommands
85 import lfcommands
32 import reposetup
86 import reposetup
33 import uisetup
87 import uisetup
34
88
35 reposetup = reposetup.reposetup
89 reposetup = reposetup.reposetup
36 uisetup = uisetup.uisetup
90 uisetup = uisetup.uisetup
37
91
38 commands.norepo += " lfconvert"
92 commands.norepo += " lfconvert"
39
93
40 cmdtable = lfcommands.cmdtable
94 cmdtable = lfcommands.cmdtable
@@ -1,473 +1,484 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 functions: lfadd() et. al, plus the cmdtable.'''
9 '''High-level command functions: lfadd() et. al, 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 from mercurial.i18n import _
15 from mercurial.i18n import _
16
16
17 import lfutil
17 import lfutil
18 import basestore
18 import basestore
19
19
20 # -- Commands ----------------------------------------------------------
20 # -- Commands ----------------------------------------------------------
21
21
22 def lfconvert(ui, src, dest, *pats, **opts):
22 def lfconvert(ui, src, dest, *pats, **opts):
23 '''Convert a normal repository to a largefiles repository
23 '''convert a normal repository to a largefiles repository
24
24
25 Convert source repository creating an identical repository, except that all
25 Convert repository SOURCE to a new repository DEST, identical to
26 files that match the patterns given, or are over the given size will be
26 SOURCE except that certain files will be converted as largefiles:
27 added as largefiles. The size used to determine whether or not to track a
27 specifically, any file that matches any PATTERN *or* whose size is
28 file as a largefile is the size of the first version of the file. After
28 above the minimum size threshold is converted as a largefile. The
29 running this command you will need to make sure that largefiles is enabled
29 size used to determine whether or not to track a file as a
30 anywhere you intend to push the new repository.'''
30 largefile is the size of the first version of the file. The
31 minimum size can be specified either with --size or in
32 configuration as ``largefiles.size``.
33
34 After running this command you will need to make sure that
35 largefiles is enabled anywhere you intend to push the new
36 repository.
37
38 Use --tonormal to convert largefiles back to normal files; after
39 this, the DEST repository can be used without largefiles at all.'''
31
40
32 if opts['tonormal']:
41 if opts['tonormal']:
33 tolfile = False
42 tolfile = False
34 else:
43 else:
35 tolfile = True
44 tolfile = True
36 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
45 size = lfutil.getminsize(ui, True, opts.get('size'), default=None)
37 try:
46 try:
38 rsrc = hg.repository(ui, src)
47 rsrc = hg.repository(ui, src)
39 if not rsrc.local():
48 if not rsrc.local():
40 raise util.Abort(_('%s is not a local Mercurial repo') % src)
49 raise util.Abort(_('%s is not a local Mercurial repo') % src)
41 except error.RepoError, err:
50 except error.RepoError, err:
42 ui.traceback()
51 ui.traceback()
43 raise util.Abort(err.args[0])
52 raise util.Abort(err.args[0])
44 if os.path.exists(dest):
53 if os.path.exists(dest):
45 if not os.path.isdir(dest):
54 if not os.path.isdir(dest):
46 raise util.Abort(_('destination %s already exists') % dest)
55 raise util.Abort(_('destination %s already exists') % dest)
47 elif os.listdir(dest):
56 elif os.listdir(dest):
48 raise util.Abort(_('destination %s is not empty') % dest)
57 raise util.Abort(_('destination %s is not empty') % dest)
49 try:
58 try:
50 ui.status(_('initializing destination %s\n') % dest)
59 ui.status(_('initializing destination %s\n') % dest)
51 rdst = hg.repository(ui, dest, create=True)
60 rdst = hg.repository(ui, dest, create=True)
52 if not rdst.local():
61 if not rdst.local():
53 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
62 raise util.Abort(_('%s is not a local Mercurial repo') % dest)
54 except error.RepoError:
63 except error.RepoError:
55 ui.traceback()
64 ui.traceback()
56 raise util.Abort(_('%s is not a repo') % dest)
65 raise util.Abort(_('%s is not a repo') % dest)
57
66
58 success = False
67 success = False
59 try:
68 try:
60 # Lock destination to prevent modification while it is converted to.
69 # Lock destination to prevent modification while it is converted to.
61 # Don't need to lock src because we are just reading from its history
70 # Don't need to lock src because we are just reading from its history
62 # which can't change.
71 # which can't change.
63 dst_lock = rdst.lock()
72 dst_lock = rdst.lock()
64
73
65 # Get a list of all changesets in the source. The easy way to do this
74 # Get a list of all changesets in the source. The easy way to do this
66 # is to simply walk the changelog, using changelog.nodesbewteen().
75 # is to simply walk the changelog, using changelog.nodesbewteen().
67 # Take a look at mercurial/revlog.py:639 for more details.
76 # Take a look at mercurial/revlog.py:639 for more details.
68 # Use a generator instead of a list to decrease memory usage
77 # Use a generator instead of a list to decrease memory usage
69 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
78 ctxs = (rsrc[ctx] for ctx in rsrc.changelog.nodesbetween(None,
70 rsrc.heads())[0])
79 rsrc.heads())[0])
71 revmap = {node.nullid: node.nullid}
80 revmap = {node.nullid: node.nullid}
72 if tolfile:
81 if tolfile:
73 lfiles = set()
82 lfiles = set()
74 normalfiles = set()
83 normalfiles = set()
75 if not pats:
84 if not pats:
76 pats = ui.config(lfutil.longname, 'patterns', default=())
85 pats = ui.config(lfutil.longname, 'patterns', default=())
77 if pats:
86 if pats:
78 pats = pats.split(' ')
87 pats = pats.split(' ')
79 if pats:
88 if pats:
80 matcher = match_.match(rsrc.root, '', list(pats))
89 matcher = match_.match(rsrc.root, '', list(pats))
81 else:
90 else:
82 matcher = None
91 matcher = None
83
92
84 lfiletohash = {}
93 lfiletohash = {}
85 for ctx in ctxs:
94 for ctx in ctxs:
86 ui.progress(_('converting revisions'), ctx.rev(),
95 ui.progress(_('converting revisions'), ctx.rev(),
87 unit=_('revision'), total=rsrc['tip'].rev())
96 unit=_('revision'), total=rsrc['tip'].rev())
88 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
97 _lfconvert_addchangeset(rsrc, rdst, ctx, revmap,
89 lfiles, normalfiles, matcher, size, lfiletohash)
98 lfiles, normalfiles, matcher, size, lfiletohash)
90 ui.progress(_('converting revisions'), None)
99 ui.progress(_('converting revisions'), None)
91
100
92 if os.path.exists(rdst.wjoin(lfutil.shortname)):
101 if os.path.exists(rdst.wjoin(lfutil.shortname)):
93 shutil.rmtree(rdst.wjoin(lfutil.shortname))
102 shutil.rmtree(rdst.wjoin(lfutil.shortname))
94
103
95 for f in lfiletohash.keys():
104 for f in lfiletohash.keys():
96 if os.path.isfile(rdst.wjoin(f)):
105 if os.path.isfile(rdst.wjoin(f)):
97 os.unlink(rdst.wjoin(f))
106 os.unlink(rdst.wjoin(f))
98 try:
107 try:
99 os.removedirs(os.path.dirname(rdst.wjoin(f)))
108 os.removedirs(os.path.dirname(rdst.wjoin(f)))
100 except OSError:
109 except OSError:
101 pass
110 pass
102
111
103 else:
112 else:
104 for ctx in ctxs:
113 for ctx in ctxs:
105 ui.progress(_('converting revisions'), ctx.rev(),
114 ui.progress(_('converting revisions'), ctx.rev(),
106 unit=_('revision'), total=rsrc['tip'].rev())
115 unit=_('revision'), total=rsrc['tip'].rev())
107 _addchangeset(ui, rsrc, rdst, ctx, revmap)
116 _addchangeset(ui, rsrc, rdst, ctx, revmap)
108
117
109 ui.progress(_('converting revisions'), None)
118 ui.progress(_('converting revisions'), None)
110 success = True
119 success = True
111 finally:
120 finally:
112 if not success:
121 if not success:
113 # we failed, remove the new directory
122 # we failed, remove the new directory
114 shutil.rmtree(rdst.root)
123 shutil.rmtree(rdst.root)
115 dst_lock.release()
124 dst_lock.release()
116
125
117 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
126 def _addchangeset(ui, rsrc, rdst, ctx, revmap):
118 # Convert src parents to dst parents
127 # Convert src parents to dst parents
119 parents = []
128 parents = []
120 for p in ctx.parents():
129 for p in ctx.parents():
121 parents.append(revmap[p.node()])
130 parents.append(revmap[p.node()])
122 while len(parents) < 2:
131 while len(parents) < 2:
123 parents.append(node.nullid)
132 parents.append(node.nullid)
124
133
125 # Generate list of changed files
134 # Generate list of changed files
126 files = set(ctx.files())
135 files = set(ctx.files())
127 if node.nullid not in parents:
136 if node.nullid not in parents:
128 mc = ctx.manifest()
137 mc = ctx.manifest()
129 mp1 = ctx.parents()[0].manifest()
138 mp1 = ctx.parents()[0].manifest()
130 mp2 = ctx.parents()[1].manifest()
139 mp2 = ctx.parents()[1].manifest()
131 files |= (set(mp1) | set(mp2)) - set(mc)
140 files |= (set(mp1) | set(mp2)) - set(mc)
132 for f in mc:
141 for f in mc:
133 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
142 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
134 files.add(f)
143 files.add(f)
135
144
136 def getfilectx(repo, memctx, f):
145 def getfilectx(repo, memctx, f):
137 if lfutil.standin(f) in files:
146 if lfutil.standin(f) in files:
138 # if the file isn't in the manifest then it was removed
147 # if the file isn't in the manifest then it was removed
139 # or renamed, raise IOError to indicate this
148 # or renamed, raise IOError to indicate this
140 try:
149 try:
141 fctx = ctx.filectx(lfutil.standin(f))
150 fctx = ctx.filectx(lfutil.standin(f))
142 except error.LookupError:
151 except error.LookupError:
143 raise IOError()
152 raise IOError()
144 renamed = fctx.renamed()
153 renamed = fctx.renamed()
145 if renamed:
154 if renamed:
146 renamed = lfutil.splitstandin(renamed[0])
155 renamed = lfutil.splitstandin(renamed[0])
147
156
148 hash = fctx.data().strip()
157 hash = fctx.data().strip()
149 path = lfutil.findfile(rsrc, hash)
158 path = lfutil.findfile(rsrc, hash)
150 ### TODO: What if the file is not cached?
159 ### TODO: What if the file is not cached?
151 data = ''
160 data = ''
152 fd = None
161 fd = None
153 try:
162 try:
154 fd = open(path, 'rb')
163 fd = open(path, 'rb')
155 data = fd.read()
164 data = fd.read()
156 finally:
165 finally:
157 if fd:
166 if fd:
158 fd.close()
167 fd.close()
159 return context.memfilectx(f, data, 'l' in fctx.flags(),
168 return context.memfilectx(f, data, 'l' in fctx.flags(),
160 'x' in fctx.flags(), renamed)
169 'x' in fctx.flags(), renamed)
161 else:
170 else:
162 try:
171 try:
163 fctx = ctx.filectx(f)
172 fctx = ctx.filectx(f)
164 except error.LookupError:
173 except error.LookupError:
165 raise IOError()
174 raise IOError()
166 renamed = fctx.renamed()
175 renamed = fctx.renamed()
167 if renamed:
176 if renamed:
168 renamed = renamed[0]
177 renamed = renamed[0]
169 data = fctx.data()
178 data = fctx.data()
170 if f == '.hgtags':
179 if f == '.hgtags':
171 newdata = []
180 newdata = []
172 for line in data.splitlines():
181 for line in data.splitlines():
173 id, name = line.split(' ', 1)
182 id, name = line.split(' ', 1)
174 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
183 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
175 name))
184 name))
176 data = ''.join(newdata)
185 data = ''.join(newdata)
177 return context.memfilectx(f, data, 'l' in fctx.flags(),
186 return context.memfilectx(f, data, 'l' in fctx.flags(),
178 'x' in fctx.flags(), renamed)
187 'x' in fctx.flags(), renamed)
179
188
180 dstfiles = []
189 dstfiles = []
181 for file in files:
190 for file in files:
182 if lfutil.isstandin(file):
191 if lfutil.isstandin(file):
183 dstfiles.append(lfutil.splitstandin(file))
192 dstfiles.append(lfutil.splitstandin(file))
184 else:
193 else:
185 dstfiles.append(file)
194 dstfiles.append(file)
186 # Commit
195 # Commit
187 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
196 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
188 getfilectx, ctx.user(), ctx.date(), ctx.extra())
197 getfilectx, ctx.user(), ctx.date(), ctx.extra())
189 ret = rdst.commitctx(mctx)
198 ret = rdst.commitctx(mctx)
190 rdst.dirstate.setparents(ret)
199 rdst.dirstate.setparents(ret)
191 revmap[ctx.node()] = rdst.changelog.tip()
200 revmap[ctx.node()] = rdst.changelog.tip()
192
201
193 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
202 def _lfconvert_addchangeset(rsrc, rdst, ctx, revmap, lfiles, normalfiles,
194 matcher, size, lfiletohash):
203 matcher, size, lfiletohash):
195 # Convert src parents to dst parents
204 # Convert src parents to dst parents
196 parents = []
205 parents = []
197 for p in ctx.parents():
206 for p in ctx.parents():
198 parents.append(revmap[p.node()])
207 parents.append(revmap[p.node()])
199 while len(parents) < 2:
208 while len(parents) < 2:
200 parents.append(node.nullid)
209 parents.append(node.nullid)
201
210
202 # Generate list of changed files
211 # Generate list of changed files
203 files = set(ctx.files())
212 files = set(ctx.files())
204 if node.nullid not in parents:
213 if node.nullid not in parents:
205 mc = ctx.manifest()
214 mc = ctx.manifest()
206 mp1 = ctx.parents()[0].manifest()
215 mp1 = ctx.parents()[0].manifest()
207 mp2 = ctx.parents()[1].manifest()
216 mp2 = ctx.parents()[1].manifest()
208 files |= (set(mp1) | set(mp2)) - set(mc)
217 files |= (set(mp1) | set(mp2)) - set(mc)
209 for f in mc:
218 for f in mc:
210 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
219 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
211 files.add(f)
220 files.add(f)
212
221
213 dstfiles = []
222 dstfiles = []
214 for f in files:
223 for f in files:
215 if f not in lfiles and f not in normalfiles:
224 if f not in lfiles and f not in normalfiles:
216 islfile = _islfile(f, ctx, matcher, size)
225 islfile = _islfile(f, ctx, matcher, size)
217 # If this file was renamed or copied then copy
226 # If this file was renamed or copied then copy
218 # the lfileness of its predecessor
227 # the lfileness of its predecessor
219 if f in ctx.manifest():
228 if f in ctx.manifest():
220 fctx = ctx.filectx(f)
229 fctx = ctx.filectx(f)
221 renamed = fctx.renamed()
230 renamed = fctx.renamed()
222 renamedlfile = renamed and renamed[0] in lfiles
231 renamedlfile = renamed and renamed[0] in lfiles
223 islfile |= renamedlfile
232 islfile |= renamedlfile
224 if 'l' in fctx.flags():
233 if 'l' in fctx.flags():
225 if renamedlfile:
234 if renamedlfile:
226 raise util.Abort(
235 raise util.Abort(
227 _('Renamed/copied largefile %s becomes symlink')
236 _('Renamed/copied largefile %s becomes symlink')
228 % f)
237 % f)
229 islfile = False
238 islfile = False
230 if islfile:
239 if islfile:
231 lfiles.add(f)
240 lfiles.add(f)
232 else:
241 else:
233 normalfiles.add(f)
242 normalfiles.add(f)
234
243
235 if f in lfiles:
244 if f in lfiles:
236 dstfiles.append(lfutil.standin(f))
245 dstfiles.append(lfutil.standin(f))
237 # lfile in manifest if it has not been removed/renamed
246 # lfile in manifest if it has not been removed/renamed
238 if f in ctx.manifest():
247 if f in ctx.manifest():
239 if 'l' in ctx.filectx(f).flags():
248 if 'l' in ctx.filectx(f).flags():
240 if renamed and renamed[0] in lfiles:
249 if renamed and renamed[0] in lfiles:
241 raise util.Abort(_('largefile %s becomes symlink') % f)
250 raise util.Abort(_('largefile %s becomes symlink') % f)
242
251
243 # lfile was modified, update standins
252 # lfile was modified, update standins
244 fullpath = rdst.wjoin(f)
253 fullpath = rdst.wjoin(f)
245 lfutil.createdir(os.path.dirname(fullpath))
254 lfutil.createdir(os.path.dirname(fullpath))
246 m = util.sha1('')
255 m = util.sha1('')
247 m.update(ctx[f].data())
256 m.update(ctx[f].data())
248 hash = m.hexdigest()
257 hash = m.hexdigest()
249 if f not in lfiletohash or lfiletohash[f] != hash:
258 if f not in lfiletohash or lfiletohash[f] != hash:
250 try:
259 try:
251 fd = open(fullpath, 'wb')
260 fd = open(fullpath, 'wb')
252 fd.write(ctx[f].data())
261 fd.write(ctx[f].data())
253 finally:
262 finally:
254 if fd:
263 if fd:
255 fd.close()
264 fd.close()
256 executable = 'x' in ctx[f].flags()
265 executable = 'x' in ctx[f].flags()
257 os.chmod(fullpath, lfutil.getmode(executable))
266 os.chmod(fullpath, lfutil.getmode(executable))
258 lfutil.writestandin(rdst, lfutil.standin(f), hash,
267 lfutil.writestandin(rdst, lfutil.standin(f), hash,
259 executable)
268 executable)
260 lfiletohash[f] = hash
269 lfiletohash[f] = hash
261 else:
270 else:
262 # normal file
271 # normal file
263 dstfiles.append(f)
272 dstfiles.append(f)
264
273
265 def getfilectx(repo, memctx, f):
274 def getfilectx(repo, memctx, f):
266 if lfutil.isstandin(f):
275 if lfutil.isstandin(f):
267 # if the file isn't in the manifest then it was removed
276 # if the file isn't in the manifest then it was removed
268 # or renamed, raise IOError to indicate this
277 # or renamed, raise IOError to indicate this
269 srcfname = lfutil.splitstandin(f)
278 srcfname = lfutil.splitstandin(f)
270 try:
279 try:
271 fctx = ctx.filectx(srcfname)
280 fctx = ctx.filectx(srcfname)
272 except error.LookupError:
281 except error.LookupError:
273 raise IOError()
282 raise IOError()
274 renamed = fctx.renamed()
283 renamed = fctx.renamed()
275 if renamed:
284 if renamed:
276 # standin is always a lfile because lfileness
285 # standin is always a lfile because lfileness
277 # doesn't change after rename or copy
286 # doesn't change after rename or copy
278 renamed = lfutil.standin(renamed[0])
287 renamed = lfutil.standin(renamed[0])
279
288
280 return context.memfilectx(f, lfiletohash[srcfname], 'l' in
289 return context.memfilectx(f, lfiletohash[srcfname], 'l' in
281 fctx.flags(), 'x' in fctx.flags(), renamed)
290 fctx.flags(), 'x' in fctx.flags(), renamed)
282 else:
291 else:
283 try:
292 try:
284 fctx = ctx.filectx(f)
293 fctx = ctx.filectx(f)
285 except error.LookupError:
294 except error.LookupError:
286 raise IOError()
295 raise IOError()
287 renamed = fctx.renamed()
296 renamed = fctx.renamed()
288 if renamed:
297 if renamed:
289 renamed = renamed[0]
298 renamed = renamed[0]
290
299
291 data = fctx.data()
300 data = fctx.data()
292 if f == '.hgtags':
301 if f == '.hgtags':
293 newdata = []
302 newdata = []
294 for line in data.splitlines():
303 for line in data.splitlines():
295 id, name = line.split(' ', 1)
304 id, name = line.split(' ', 1)
296 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
305 newdata.append('%s %s\n' % (node.hex(revmap[node.bin(id)]),
297 name))
306 name))
298 data = ''.join(newdata)
307 data = ''.join(newdata)
299 return context.memfilectx(f, data, 'l' in fctx.flags(),
308 return context.memfilectx(f, data, 'l' in fctx.flags(),
300 'x' in fctx.flags(), renamed)
309 'x' in fctx.flags(), renamed)
301
310
302 # Commit
311 # Commit
303 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
312 mctx = context.memctx(rdst, parents, ctx.description(), dstfiles,
304 getfilectx, ctx.user(), ctx.date(), ctx.extra())
313 getfilectx, ctx.user(), ctx.date(), ctx.extra())
305 ret = rdst.commitctx(mctx)
314 ret = rdst.commitctx(mctx)
306 rdst.dirstate.setparents(ret)
315 rdst.dirstate.setparents(ret)
307 revmap[ctx.node()] = rdst.changelog.tip()
316 revmap[ctx.node()] = rdst.changelog.tip()
308
317
309 def _islfile(file, ctx, matcher, size):
318 def _islfile(file, ctx, matcher, size):
310 '''
319 '''
311 A file is a lfile if it matches a pattern or is over
320 A file is a lfile if it matches a pattern or is over
312 the given size.
321 the given size.
313 '''
322 '''
314 # Never store hgtags or hgignore as lfiles
323 # Never store hgtags or hgignore as lfiles
315 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
324 if file == '.hgtags' or file == '.hgignore' or file == '.hgsigs':
316 return False
325 return False
317 if matcher and matcher(file):
326 if matcher and matcher(file):
318 return True
327 return True
319 try:
328 try:
320 return ctx.filectx(file).size() >= size * 1024 * 1024
329 return ctx.filectx(file).size() >= size * 1024 * 1024
321 except error.LookupError:
330 except error.LookupError:
322 return False
331 return False
323
332
324 def uploadlfiles(ui, rsrc, rdst, files):
333 def uploadlfiles(ui, rsrc, rdst, files):
325 '''upload largefiles to the central store'''
334 '''upload largefiles to the central store'''
326
335
327 # Don't upload locally. All largefiles are in the system wide cache
336 # Don't upload locally. All largefiles are in the system wide cache
328 # so the other repo can just get them from there.
337 # so the other repo can just get them from there.
329 if not files or rdst.local():
338 if not files or rdst.local():
330 return
339 return
331
340
332 store = basestore._openstore(rsrc, rdst, put=True)
341 store = basestore._openstore(rsrc, rdst, put=True)
333
342
334 at = 0
343 at = 0
335 files = filter(lambda h: not store.exists(h), files)
344 files = filter(lambda h: not store.exists(h), files)
336 for hash in files:
345 for hash in files:
337 ui.progress(_('uploading largefiles'), at, unit='largefile',
346 ui.progress(_('uploading largefiles'), at, unit='largefile',
338 total=len(files))
347 total=len(files))
339 source = lfutil.findfile(rsrc, hash)
348 source = lfutil.findfile(rsrc, hash)
340 if not source:
349 if not source:
341 raise util.Abort(_('Missing largefile %s needs to be uploaded')
350 raise util.Abort(_('Missing largefile %s needs to be uploaded')
342 % hash)
351 % hash)
343 # XXX check for errors here
352 # XXX check for errors here
344 store.put(source, hash)
353 store.put(source, hash)
345 at += 1
354 at += 1
346 ui.progress(_('uploading largefiles'), None)
355 ui.progress(_('uploading largefiles'), None)
347
356
348 def verifylfiles(ui, repo, all=False, contents=False):
357 def verifylfiles(ui, repo, all=False, contents=False):
349 '''Verify that every big file revision in the current changeset
358 '''Verify that every big file revision in the current changeset
350 exists in the central store. With --contents, also verify that
359 exists in the central store. With --contents, also verify that
351 the contents of each big file revision are correct (SHA-1 hash
360 the contents of each big file revision are correct (SHA-1 hash
352 matches the revision ID). With --all, check every changeset in
361 matches the revision ID). With --all, check every changeset in
353 this repository.'''
362 this repository.'''
354 if all:
363 if all:
355 # Pass a list to the function rather than an iterator because we know a
364 # Pass a list to the function rather than an iterator because we know a
356 # list will work.
365 # list will work.
357 revs = range(len(repo))
366 revs = range(len(repo))
358 else:
367 else:
359 revs = ['.']
368 revs = ['.']
360
369
361 store = basestore._openstore(repo)
370 store = basestore._openstore(repo)
362 return store.verify(revs, contents=contents)
371 return store.verify(revs, contents=contents)
363
372
364 def cachelfiles(ui, repo, node):
373 def cachelfiles(ui, repo, node):
365 '''cachelfiles ensures that all largefiles needed by the specified revision
374 '''cachelfiles ensures that all largefiles needed by the specified revision
366 are present in the repository's largefile cache.
375 are present in the repository's largefile cache.
367
376
368 returns a tuple (cached, missing). cached is the list of files downloaded
377 returns a tuple (cached, missing). cached is the list of files downloaded
369 by this operation; missing is the list of files that were needed but could
378 by this operation; missing is the list of files that were needed but could
370 not be found.'''
379 not be found.'''
371 lfiles = lfutil.listlfiles(repo, node)
380 lfiles = lfutil.listlfiles(repo, node)
372 toget = []
381 toget = []
373
382
374 for lfile in lfiles:
383 for lfile in lfiles:
375 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
384 expectedhash = repo[node][lfutil.standin(lfile)].data().strip()
376 # if it exists and its hash matches, it might have been locally
385 # if it exists and its hash matches, it might have been locally
377 # modified before updating and the user chose 'local'. in this case,
386 # modified before updating and the user chose 'local'. in this case,
378 # it will not be in any store, so don't look for it.
387 # it will not be in any store, so don't look for it.
379 if (not os.path.exists(repo.wjoin(lfile)) \
388 if (not os.path.exists(repo.wjoin(lfile)) \
380 or expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and \
389 or expectedhash != lfutil.hashfile(repo.wjoin(lfile))) and \
381 not lfutil.findfile(repo, expectedhash):
390 not lfutil.findfile(repo, expectedhash):
382 toget.append((lfile, expectedhash))
391 toget.append((lfile, expectedhash))
383
392
384 if toget:
393 if toget:
385 store = basestore._openstore(repo)
394 store = basestore._openstore(repo)
386 ret = store.get(toget)
395 ret = store.get(toget)
387 return ret
396 return ret
388
397
389 return ([], [])
398 return ([], [])
390
399
391 def updatelfiles(ui, repo, filelist=None, printmessage=True):
400 def updatelfiles(ui, repo, filelist=None, printmessage=True):
392 wlock = repo.wlock()
401 wlock = repo.wlock()
393 try:
402 try:
394 lfdirstate = lfutil.openlfdirstate(ui, repo)
403 lfdirstate = lfutil.openlfdirstate(ui, repo)
395 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
404 lfiles = set(lfutil.listlfiles(repo)) | set(lfdirstate)
396
405
397 if filelist is not None:
406 if filelist is not None:
398 lfiles = [f for f in lfiles if f in filelist]
407 lfiles = [f for f in lfiles if f in filelist]
399
408
400 printed = False
409 printed = False
401 if printmessage and lfiles:
410 if printmessage and lfiles:
402 ui.status(_('getting changed largefiles\n'))
411 ui.status(_('getting changed largefiles\n'))
403 printed = True
412 printed = True
404 cachelfiles(ui, repo, '.')
413 cachelfiles(ui, repo, '.')
405
414
406 updated, removed = 0, 0
415 updated, removed = 0, 0
407 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
416 for i in map(lambda f: _updatelfile(repo, lfdirstate, f), lfiles):
408 # increment the appropriate counter according to _updatelfile's
417 # increment the appropriate counter according to _updatelfile's
409 # return value
418 # return value
410 updated += i > 0 and i or 0
419 updated += i > 0 and i or 0
411 removed -= i < 0 and i or 0
420 removed -= i < 0 and i or 0
412 if printmessage and (removed or updated) and not printed:
421 if printmessage and (removed or updated) and not printed:
413 ui.status(_('getting changed largefiles\n'))
422 ui.status(_('getting changed largefiles\n'))
414 printed = True
423 printed = True
415
424
416 lfdirstate.write()
425 lfdirstate.write()
417 if printed and printmessage:
426 if printed and printmessage:
418 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
427 ui.status(_('%d largefiles updated, %d removed\n') % (updated,
419 removed))
428 removed))
420 finally:
429 finally:
421 wlock.release()
430 wlock.release()
422
431
423 def _updatelfile(repo, lfdirstate, lfile):
432 def _updatelfile(repo, lfdirstate, lfile):
424 '''updates a single largefile and copies the state of its standin from
433 '''updates a single largefile and copies the state of its standin from
425 the repository's dirstate to its state in the lfdirstate.
434 the repository's dirstate to its state in the lfdirstate.
426
435
427 returns 1 if the file was modified, -1 if the file was removed, 0 if the
436 returns 1 if the file was modified, -1 if the file was removed, 0 if the
428 file was unchanged, and None if the needed largefile was missing from the
437 file was unchanged, and None if the needed largefile was missing from the
429 cache.'''
438 cache.'''
430 ret = 0
439 ret = 0
431 abslfile = repo.wjoin(lfile)
440 abslfile = repo.wjoin(lfile)
432 absstandin = repo.wjoin(lfutil.standin(lfile))
441 absstandin = repo.wjoin(lfutil.standin(lfile))
433 if os.path.exists(absstandin):
442 if os.path.exists(absstandin):
434 if os.path.exists(absstandin+'.orig'):
443 if os.path.exists(absstandin+'.orig'):
435 shutil.copyfile(abslfile, abslfile+'.orig')
444 shutil.copyfile(abslfile, abslfile+'.orig')
436 expecthash = lfutil.readstandin(repo, lfile)
445 expecthash = lfutil.readstandin(repo, lfile)
437 if expecthash != '' and \
446 if expecthash != '' and \
438 (not os.path.exists(abslfile) or \
447 (not os.path.exists(abslfile) or \
439 expecthash != lfutil.hashfile(abslfile)):
448 expecthash != lfutil.hashfile(abslfile)):
440 if not lfutil.copyfromcache(repo, expecthash, lfile):
449 if not lfutil.copyfromcache(repo, expecthash, lfile):
441 return None # don't try to set the mode or update the dirstate
450 return None # don't try to set the mode or update the dirstate
442 ret = 1
451 ret = 1
443 mode = os.stat(absstandin).st_mode
452 mode = os.stat(absstandin).st_mode
444 if mode != os.stat(abslfile).st_mode:
453 if mode != os.stat(abslfile).st_mode:
445 os.chmod(abslfile, mode)
454 os.chmod(abslfile, mode)
446 ret = 1
455 ret = 1
447 else:
456 else:
448 if os.path.exists(abslfile):
457 if os.path.exists(abslfile):
449 os.unlink(abslfile)
458 os.unlink(abslfile)
450 ret = -1
459 ret = -1
451 state = repo.dirstate[lfutil.standin(lfile)]
460 state = repo.dirstate[lfutil.standin(lfile)]
452 if state == 'n':
461 if state == 'n':
453 lfdirstate.normal(lfile)
462 lfdirstate.normal(lfile)
454 elif state == 'r':
463 elif state == 'r':
455 lfdirstate.remove(lfile)
464 lfdirstate.remove(lfile)
456 elif state == 'a':
465 elif state == 'a':
457 lfdirstate.add(lfile)
466 lfdirstate.add(lfile)
458 elif state == '?':
467 elif state == '?':
459 lfdirstate.drop(lfile)
468 lfdirstate.drop(lfile)
460 return ret
469 return ret
461
470
462 # -- hg commands declarations ------------------------------------------------
471 # -- hg commands declarations ------------------------------------------------
463
472
464
473
465 cmdtable = {
474 cmdtable = {
466 'lfconvert': (lfconvert,
475 'lfconvert': (lfconvert,
467 [('s', 'size', '', 'All files over this size (in megabytes) '
476 [('s', 'size', '',
468 'will be considered largefiles. This can also be specified '
477 _('minimum size (MB) for files to be converted '
469 'in your hgrc as [largefiles].size.'),
478 'as largefiles'),
470 ('','tonormal',False,
479 'SIZE'),
471 'Convert from a largefiles repo to a normal repo')],
480 ('', 'tonormal', False,
481 _('convert from a largefiles repo to a normal repo')),
482 ],
472 _('hg lfconvert SOURCE DEST [FILE ...]')),
483 _('hg lfconvert SOURCE DEST [FILE ...]')),
473 }
484 }
General Comments 0
You need to be logged in to leave comments. Login now