##// END OF EJS Templates
merge with stable
Thomas Arendsen Hein -
r16920:4dd03697 merge default
parent child Browse files
Show More
@@ -1,284 +1,289
1 1 # archival.py - revision archival for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 from node import hex
10 10 import cmdutil
11 11 import scmutil, util, encoding
12 12 import cStringIO, os, tarfile, time, zipfile
13 13 import zlib, gzip
14 14
15 15 def tidyprefix(dest, kind, prefix):
16 16 '''choose prefix to use for names in archive. make sure prefix is
17 17 safe for consumers.'''
18 18
19 19 if prefix:
20 20 prefix = util.normpath(prefix)
21 21 else:
22 22 if not isinstance(dest, str):
23 23 raise ValueError('dest must be string if no prefix')
24 24 prefix = os.path.basename(dest)
25 25 lower = prefix.lower()
26 26 for sfx in exts.get(kind, []):
27 27 if lower.endswith(sfx):
28 28 prefix = prefix[:-len(sfx)]
29 29 break
30 30 lpfx = os.path.normpath(util.localpath(prefix))
31 31 prefix = util.pconvert(lpfx)
32 32 if not prefix.endswith('/'):
33 33 prefix += '/'
34 34 if prefix.startswith('../') or os.path.isabs(lpfx) or '/../' in prefix:
35 35 raise util.Abort(_('archive prefix contains illegal components'))
36 36 return prefix
37 37
38 38 exts = {
39 39 'tar': ['.tar'],
40 40 'tbz2': ['.tbz2', '.tar.bz2'],
41 41 'tgz': ['.tgz', '.tar.gz'],
42 42 'zip': ['.zip'],
43 43 }
44 44
45 45 def guesskind(dest):
46 46 for kind, extensions in exts.iteritems():
47 47 if util.any(dest.endswith(ext) for ext in extensions):
48 48 return kind
49 49 return None
50 50
51 51
52 52 class tarit(object):
53 53 '''write archive to tar file or stream. can write uncompressed,
54 54 or compress with gzip or bzip2.'''
55 55
56 56 class GzipFileWithTime(gzip.GzipFile):
57 57
58 58 def __init__(self, *args, **kw):
59 59 timestamp = None
60 60 if 'timestamp' in kw:
61 61 timestamp = kw.pop('timestamp')
62 62 if timestamp is None:
63 63 self.timestamp = time.time()
64 64 else:
65 65 self.timestamp = timestamp
66 66 gzip.GzipFile.__init__(self, *args, **kw)
67 67
68 68 def _write_gzip_header(self):
69 69 self.fileobj.write('\037\213') # magic header
70 70 self.fileobj.write('\010') # compression method
71 71 # Python 2.6 deprecates self.filename
72 72 fname = getattr(self, 'name', None) or self.filename
73 73 if fname and fname.endswith('.gz'):
74 74 fname = fname[:-3]
75 75 flags = 0
76 76 if fname:
77 77 flags = gzip.FNAME
78 78 self.fileobj.write(chr(flags))
79 79 gzip.write32u(self.fileobj, long(self.timestamp))
80 80 self.fileobj.write('\002')
81 81 self.fileobj.write('\377')
82 82 if fname:
83 83 self.fileobj.write(fname + '\000')
84 84
85 85 def __init__(self, dest, mtime, kind=''):
86 86 self.mtime = mtime
87 87 self.fileobj = None
88 88
89 89 def taropen(name, mode, fileobj=None):
90 90 if kind == 'gz':
91 91 mode = mode[0]
92 92 if not fileobj:
93 93 fileobj = open(name, mode + 'b')
94 94 gzfileobj = self.GzipFileWithTime(name, mode + 'b',
95 95 zlib.Z_BEST_COMPRESSION,
96 96 fileobj, timestamp=mtime)
97 97 self.fileobj = gzfileobj
98 98 return tarfile.TarFile.taropen(name, mode, gzfileobj)
99 99 else:
100 100 self.fileobj = fileobj
101 101 return tarfile.open(name, mode + kind, fileobj)
102 102
103 103 if isinstance(dest, str):
104 104 self.z = taropen(dest, mode='w:')
105 105 else:
106 106 # Python 2.5-2.5.1 have a regression that requires a name arg
107 107 self.z = taropen(name='', mode='w|', fileobj=dest)
108 108
109 109 def addfile(self, name, mode, islink, data):
110 110 i = tarfile.TarInfo(name)
111 111 i.mtime = self.mtime
112 112 i.size = len(data)
113 113 if islink:
114 114 i.type = tarfile.SYMTYPE
115 115 i.mode = 0777
116 116 i.linkname = data
117 117 data = None
118 118 i.size = 0
119 119 else:
120 120 i.mode = mode
121 121 data = cStringIO.StringIO(data)
122 122 self.z.addfile(i, data)
123 123
124 124 def done(self):
125 125 self.z.close()
126 126 if self.fileobj:
127 127 self.fileobj.close()
128 128
129 129 class tellable(object):
130 130 '''provide tell method for zipfile.ZipFile when writing to http
131 131 response file object.'''
132 132
133 133 def __init__(self, fp):
134 134 self.fp = fp
135 135 self.offset = 0
136 136
137 137 def __getattr__(self, key):
138 138 return getattr(self.fp, key)
139 139
140 140 def write(self, s):
141 141 self.fp.write(s)
142 142 self.offset += len(s)
143 143
144 144 def tell(self):
145 145 return self.offset
146 146
147 147 class zipit(object):
148 148 '''write archive to zip file or stream. can write uncompressed,
149 149 or compressed with deflate.'''
150 150
151 151 def __init__(self, dest, mtime, compress=True):
152 152 if not isinstance(dest, str):
153 153 try:
154 154 dest.tell()
155 155 except (AttributeError, IOError):
156 156 dest = tellable(dest)
157 157 self.z = zipfile.ZipFile(dest, 'w',
158 158 compress and zipfile.ZIP_DEFLATED or
159 159 zipfile.ZIP_STORED)
160 160
161 161 # Python's zipfile module emits deprecation warnings if we try
162 162 # to store files with a date before 1980.
163 163 epoch = 315532800 # calendar.timegm((1980, 1, 1, 0, 0, 0, 1, 1, 0))
164 164 if mtime < epoch:
165 165 mtime = epoch
166 166
167 167 self.date_time = time.gmtime(mtime)[:6]
168 168
169 169 def addfile(self, name, mode, islink, data):
170 170 i = zipfile.ZipInfo(name, self.date_time)
171 171 i.compress_type = self.z.compression
172 172 # unzip will not honor unix file modes unless file creator is
173 173 # set to unix (id 3).
174 174 i.create_system = 3
175 175 ftype = 0x8000 # UNX_IFREG in unzip source code
176 176 if islink:
177 177 mode = 0777
178 178 ftype = 0xa000 # UNX_IFLNK in unzip source code
179 179 i.external_attr = (mode | ftype) << 16L
180 180 self.z.writestr(i, data)
181 181
182 182 def done(self):
183 183 self.z.close()
184 184
185 185 class fileit(object):
186 186 '''write archive as files in directory.'''
187 187
188 188 def __init__(self, name, mtime):
189 189 self.basedir = name
190 190 self.opener = scmutil.opener(self.basedir)
191 191
192 192 def addfile(self, name, mode, islink, data):
193 193 if islink:
194 194 self.opener.symlink(data, name)
195 195 return
196 196 f = self.opener(name, "w", atomictemp=True)
197 197 f.write(data)
198 198 f.close()
199 199 destfile = os.path.join(self.basedir, name)
200 200 os.chmod(destfile, mode)
201 201
202 202 def done(self):
203 203 pass
204 204
205 205 archivers = {
206 206 'files': fileit,
207 207 'tar': tarit,
208 208 'tbz2': lambda name, mtime: tarit(name, mtime, 'bz2'),
209 209 'tgz': lambda name, mtime: tarit(name, mtime, 'gz'),
210 210 'uzip': lambda name, mtime: zipit(name, mtime, False),
211 211 'zip': zipit,
212 212 }
213 213
214 214 def archive(repo, dest, node, kind, decode=True, matchfn=None,
215 215 prefix=None, mtime=None, subrepos=False):
216 216 '''create archive of repo as it was at node.
217 217
218 218 dest can be name of directory, name of archive file, or file
219 219 object to write archive to.
220 220
221 221 kind is type of archive to create.
222 222
223 223 decode tells whether to put files through decode filters from
224 224 hgrc.
225 225
226 226 matchfn is function to filter names of files to write to archive.
227 227
228 228 prefix is name of path to put before every archive member.'''
229 229
230 230 if kind == 'files':
231 231 if prefix:
232 232 raise util.Abort(_('cannot give prefix when archiving to files'))
233 233 else:
234 234 prefix = tidyprefix(dest, kind, prefix)
235 235
236 236 def write(name, mode, islink, getdata):
237 if matchfn and not matchfn(name):
238 return
239 237 data = getdata()
240 238 if decode:
241 239 data = repo.wwritedata(name, data)
242 240 archiver.addfile(prefix + name, mode, islink, data)
243 241
244 242 if kind not in archivers:
245 243 raise util.Abort(_("unknown archive type '%s'") % kind)
246 244
247 245 ctx = repo[node]
248 246 archiver = archivers[kind](dest, mtime or ctx.date()[0])
249 247
250 248 if repo.ui.configbool("ui", "archivemeta", True):
251 249 def metadata():
252 250 base = 'repo: %s\nnode: %s\nbranch: %s\n' % (
253 251 repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch()))
254 252
255 253 tags = ''.join('tag: %s\n' % t for t in ctx.tags()
256 254 if repo.tagtype(t) == 'global')
257 255 if not tags:
258 256 repo.ui.pushbuffer()
259 257 opts = {'template': '{latesttag}\n{latesttagdistance}',
260 258 'style': '', 'patch': None, 'git': None}
261 259 cmdutil.show_changeset(repo.ui, repo, opts).show(ctx)
262 260 ltags, dist = repo.ui.popbuffer().split('\n')
263 261 tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':'))
264 262 tags += 'latesttagdistance: %s\n' % dist
265 263
266 264 return base + tags
267 265
268 write('.hg_archival.txt', 0644, False, metadata)
266 name = '.hg_archival.txt'
267 if not matchfn or matchfn(name):
268 write(name, 0644, False, metadata)
269 269
270 total = len(ctx.manifest())
270 if matchfn:
271 files = [f for f in ctx.manifest().keys() if matchfn(f)]
272 else:
273 files = ctx.manifest().keys()
274 files.sort()
275 total = len(files)
271 276 repo.ui.progress(_('archiving'), 0, unit=_('files'), total=total)
272 for i, f in enumerate(ctx):
277 for i, f in enumerate(files):
273 278 ff = ctx.flags(f)
274 279 write(f, 'x' in ff and 0755 or 0644, 'l' in ff, ctx[f].data)
275 280 repo.ui.progress(_('archiving'), i + 1, item=f,
276 281 unit=_('files'), total=total)
277 282 repo.ui.progress(_('archiving'), None)
278 283
279 284 if subrepos:
280 285 for subpath in ctx.substate:
281 286 sub = ctx.sub(subpath)
282 287 sub.archive(repo.ui, archiver, prefix)
283 288
284 289 archiver.done()
@@ -1,269 +1,272
1 1 $ "$TESTDIR/hghave" serve || exit 80
2 2
3 3 $ hg init test
4 4 $ cd test
5 5 $ echo foo>foo
6 6 $ hg commit -Am 1 -d '1 0'
7 7 adding foo
8 8 $ echo bar>bar
9 9 $ hg commit -Am 2 -d '2 0'
10 10 adding bar
11 11 $ mkdir baz
12 12 $ echo bletch>baz/bletch
13 13 $ hg commit -Am 3 -d '1000000000 0'
14 14 adding baz/bletch
15 15 $ echo "[web]" >> .hg/hgrc
16 16 $ echo "name = test-archive" >> .hg/hgrc
17 17 $ cp .hg/hgrc .hg/hgrc-base
18 18 > test_archtype() {
19 19 > echo "allow_archive = $1" >> .hg/hgrc
20 20 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
21 21 > cat hg.pid >> $DAEMON_PIDS
22 22 > echo % $1 allowed should give 200
23 23 > "$TESTDIR/get-with-headers.py" localhost:$HGPORT "/archive/tip.$2" | head -n 1
24 24 > echo % $3 and $4 disallowed should both give 403
25 25 > "$TESTDIR/get-with-headers.py" localhost:$HGPORT "/archive/tip.$3" | head -n 1
26 26 > "$TESTDIR/get-with-headers.py" localhost:$HGPORT "/archive/tip.$4" | head -n 1
27 27 > "$TESTDIR/killdaemons.py"
28 28 > cat errors.log
29 29 > cp .hg/hgrc-base .hg/hgrc
30 30 > }
31 31
32 32 check http return codes
33 33
34 34 $ test_archtype gz tar.gz tar.bz2 zip
35 35 % gz allowed should give 200
36 36 200 Script output follows
37 37 % tar.bz2 and zip disallowed should both give 403
38 38 403 Archive type not allowed: bz2
39 39 403 Archive type not allowed: zip
40 40 $ test_archtype bz2 tar.bz2 zip tar.gz
41 41 % bz2 allowed should give 200
42 42 200 Script output follows
43 43 % zip and tar.gz disallowed should both give 403
44 44 403 Archive type not allowed: zip
45 45 403 Archive type not allowed: gz
46 46 $ test_archtype zip zip tar.gz tar.bz2
47 47 % zip allowed should give 200
48 48 200 Script output follows
49 49 % tar.gz and tar.bz2 disallowed should both give 403
50 50 403 Archive type not allowed: gz
51 51 403 Archive type not allowed: bz2
52 52
53 53 $ echo "allow_archive = gz bz2 zip" >> .hg/hgrc
54 54 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
55 55 $ cat hg.pid >> $DAEMON_PIDS
56 56
57 57 invalid arch type should give 404
58 58
59 59 $ "$TESTDIR/get-with-headers.py" localhost:$HGPORT "/archive/tip.invalid" | head -n 1
60 60 404 Unsupported archive type: None
61 61
62 62 $ TIP=`hg id -v | cut -f1 -d' '`
63 63 $ QTIP=`hg id -q`
64 64 $ cat > getarchive.py <<EOF
65 65 > import os, sys, urllib2
66 66 > try:
67 67 > # Set stdout to binary mode for win32 platforms
68 68 > import msvcrt
69 69 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
70 70 > except ImportError:
71 71 > pass
72 72 > node, archive = sys.argv[1:]
73 73 > f = urllib2.urlopen('http://127.0.0.1:%s/?cmd=archive;node=%s;type=%s'
74 74 > % (os.environ['HGPORT'], node, archive))
75 75 > sys.stdout.write(f.read())
76 76 > EOF
77 77 $ python getarchive.py "$TIP" gz | gunzip | tar tf - 2>/dev/null
78 78 test-archive-2c0277f05ed4/.hg_archival.txt
79 79 test-archive-2c0277f05ed4/bar
80 80 test-archive-2c0277f05ed4/baz/bletch
81 81 test-archive-2c0277f05ed4/foo
82 82 $ python getarchive.py "$TIP" bz2 | bunzip2 | tar tf - 2>/dev/null
83 83 test-archive-2c0277f05ed4/.hg_archival.txt
84 84 test-archive-2c0277f05ed4/bar
85 85 test-archive-2c0277f05ed4/baz/bletch
86 86 test-archive-2c0277f05ed4/foo
87 87 $ python getarchive.py "$TIP" zip > archive.zip
88 88 $ unzip -t archive.zip
89 89 Archive: archive.zip
90 90 testing: test-archive-2c0277f05ed4/.hg_archival.txt OK
91 91 testing: test-archive-2c0277f05ed4/bar OK
92 92 testing: test-archive-2c0277f05ed4/baz/bletch OK
93 93 testing: test-archive-2c0277f05ed4/foo OK
94 94 No errors detected in compressed data of archive.zip.
95 95
96 96 $ "$TESTDIR/killdaemons.py"
97 97
98 98 $ hg archive -t tar test.tar
99 99 $ tar tf test.tar
100 100 test/.hg_archival.txt
101 101 test/bar
102 102 test/baz/bletch
103 103 test/foo
104 104
105 $ hg archive -t tbz2 -X baz test.tar.bz2
105 $ hg archive --debug -t tbz2 -X baz test.tar.bz2
106 archiving: 0/2 files (0.00%)
107 archiving: bar 1/2 files (50.00%)
108 archiving: foo 2/2 files (100.00%)
106 109 $ bunzip2 -dc test.tar.bz2 | tar tf - 2>/dev/null
107 110 test/.hg_archival.txt
108 111 test/bar
109 112 test/foo
110 113
111 114 $ hg archive -t tgz -p %b-%h test-%h.tar.gz
112 115 $ gzip -dc test-$QTIP.tar.gz | tar tf - 2>/dev/null
113 116 test-2c0277f05ed4/.hg_archival.txt
114 117 test-2c0277f05ed4/bar
115 118 test-2c0277f05ed4/baz/bletch
116 119 test-2c0277f05ed4/foo
117 120
118 121 $ hg archive autodetected_test.tar
119 122 $ tar tf autodetected_test.tar
120 123 autodetected_test/.hg_archival.txt
121 124 autodetected_test/bar
122 125 autodetected_test/baz/bletch
123 126 autodetected_test/foo
124 127
125 128 The '-t' should override autodetection
126 129
127 130 $ hg archive -t tar autodetect_override_test.zip
128 131 $ tar tf autodetect_override_test.zip
129 132 autodetect_override_test.zip/.hg_archival.txt
130 133 autodetect_override_test.zip/bar
131 134 autodetect_override_test.zip/baz/bletch
132 135 autodetect_override_test.zip/foo
133 136
134 137 $ for ext in tar tar.gz tgz tar.bz2 tbz2 zip; do
135 138 > hg archive auto_test.$ext
136 139 > if [ -d auto_test.$ext ]; then
137 140 > echo "extension $ext was not autodetected."
138 141 > fi
139 142 > done
140 143
141 144 $ cat > md5comp.py <<EOF
142 145 > try:
143 146 > from hashlib import md5
144 147 > except ImportError:
145 148 > from md5 import md5
146 149 > import sys
147 150 > f1, f2 = sys.argv[1:3]
148 151 > h1 = md5(file(f1, 'rb').read()).hexdigest()
149 152 > h2 = md5(file(f2, 'rb').read()).hexdigest()
150 153 > print h1 == h2 or "md5 differ: " + repr((h1, h2))
151 154 > EOF
152 155
153 156 archive name is stored in the archive, so create similar archives and
154 157 rename them afterwards.
155 158
156 159 $ hg archive -t tgz tip.tar.gz
157 160 $ mv tip.tar.gz tip1.tar.gz
158 161 $ sleep 1
159 162 $ hg archive -t tgz tip.tar.gz
160 163 $ mv tip.tar.gz tip2.tar.gz
161 164 $ python md5comp.py tip1.tar.gz tip2.tar.gz
162 165 True
163 166
164 167 $ hg archive -t zip -p /illegal test.zip
165 168 abort: archive prefix contains illegal components
166 169 [255]
167 170 $ hg archive -t zip -p very/../bad test.zip
168 171
169 172 $ hg archive --config ui.archivemeta=false -t zip -r 2 test.zip
170 173 $ unzip -t test.zip
171 174 Archive: test.zip
172 175 testing: test/bar OK
173 176 testing: test/baz/bletch OK
174 177 testing: test/foo OK
175 178 No errors detected in compressed data of test.zip.
176 179
177 180 $ hg archive -t tar - | tar tf - 2>/dev/null
178 181 test-2c0277f05ed4/.hg_archival.txt
179 182 test-2c0277f05ed4/bar
180 183 test-2c0277f05ed4/baz/bletch
181 184 test-2c0277f05ed4/foo
182 185
183 186 $ hg archive -r 0 -t tar rev-%r.tar
184 187 $ if [ -f rev-0.tar ]; then
185 188 $ fi
186 189
187 190 test .hg_archival.txt
188 191
189 192 $ hg archive ../test-tags
190 193 $ cat ../test-tags/.hg_archival.txt
191 194 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
192 195 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
193 196 branch: default
194 197 latesttag: null
195 198 latesttagdistance: 3
196 199 $ hg tag -r 2 mytag
197 200 $ hg tag -r 2 anothertag
198 201 $ hg archive -r 2 ../test-lasttag
199 202 $ cat ../test-lasttag/.hg_archival.txt
200 203 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
201 204 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
202 205 branch: default
203 206 tag: anothertag
204 207 tag: mytag
205 208
206 209 $ hg archive -t bogus test.bogus
207 210 abort: unknown archive type 'bogus'
208 211 [255]
209 212
210 213 enable progress extension:
211 214
212 215 $ cp $HGRCPATH $HGRCPATH.no-progress
213 216 $ cat >> $HGRCPATH <<EOF
214 217 > [extensions]
215 218 > progress =
216 219 > [progress]
217 220 > assume-tty = 1
218 221 > format = topic bar number
219 222 > delay = 0
220 223 > refresh = 0
221 224 > width = 60
222 225 > EOF
223 226
224 227 $ hg archive ../with-progress 2>&1 | "$TESTDIR/filtercr.py"
225 228
226 229 archiving [ ] 0/4
227 230 archiving [ ] 0/4
228 231 archiving [=========> ] 1/4
229 232 archiving [=========> ] 1/4
230 233 archiving [====================> ] 2/4
231 234 archiving [====================> ] 2/4
232 235 archiving [===============================> ] 3/4
233 236 archiving [===============================> ] 3/4
234 237 archiving [==========================================>] 4/4
235 238 archiving [==========================================>] 4/4
236 239 \r (esc)
237 240
238 241 cleanup after progress extension test:
239 242
240 243 $ cp $HGRCPATH.no-progress $HGRCPATH
241 244
242 245 server errors
243 246
244 247 $ cat errors.log
245 248
246 249 empty repo
247 250
248 251 $ hg init ../empty
249 252 $ cd ../empty
250 253 $ hg archive ../test-empty
251 254 abort: no working directory: please specify a revision
252 255 [255]
253 256
254 257 old file -- date clamped to 1980
255 258
256 259 $ touch -t 197501010000 old
257 260 $ hg add old
258 261 $ hg commit -m old
259 262 $ hg archive ../old.zip
260 263 $ unzip -l ../old.zip
261 264 Archive: ../old.zip
262 265 \s*Length.* (re)
263 266 *-----* (glob)
264 267 *147*80*00:00*old/.hg_archival.txt (glob)
265 268 *0*80*00:00*old/old (glob)
266 269 *-----* (glob)
267 270 \s*147\s+2 files (re)
268 271
269 272 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now