##// END OF EJS Templates
Add back links from file revisions to changeset revisions...
mpm@selenic.com -
r0:9117c656 default
parent child Browse files
Show More
@@ -0,0 +1,10
1 Metadata-Version: 1.0
2 Name: mercurial
3 Version: 0.4c
4 Summary: scalable distributed SCM
5 Home-page: http://selenic.com/mercurial
6 Author: Matt Mackall
7 Author-email: mpm@selenic.com
8 License: GNU GPL
9 Description: UNKNOWN
10 Platform: UNKNOWN
@@ -0,0 +1,80
1 Setting up Mercurial in your home directory:
2
3 Note: Debian fails to include bits of distutils, you'll need
4 python-dev to install. Alternately, shove everything somewhere in
5 your path.
6
7 $ tar xvzf mercurial-<ver>.tar.gz
8 $ cd mercurial-<ver>
9 $ python setup.py install --home ~
10 $ export PYTHONPATH=${HOME}/lib/python # add this to your .bashrc
11 $ export HGMERGE=tkmerge # customize this
12 $ hg # test installation, show help
13
14 If you get complaints about missing modules, you probably haven't set
15 PYTHONPATH correctly.
16
17 You may also want to install psyco, the python specializing compiler.
18 It makes commits more than twice as fast. The relevant Debian package
19 is python-psyco
20
21 Setting up a Mercurial project:
22
23 $ cd linux/
24 $ hg init # creates .hg
25 $ hg status # show changes between repo and working dir
26 $ hg diff # generate a unidiff
27 $ hg addremove # add all unknown files and remove all missing files
28 $ hg commit # commit all changes, edit changelog entry
29
30 Mercurial will look for a file named .hgignore in the root of your
31 repository contains a set of regular expressions to ignore in file
32 paths.
33
34 Mercurial commands:
35
36 $ hg history # show changesets
37 $ hg log Makefile # show commits per file
38 $ hg checkout # check out the tip revision
39 $ hg checkout <hash> # check out a specified changeset
40 $ hg add foo # add a new file for the next commit
41 $ hg remove bar # mark a file as removed
42 $ hg verify # check repo integrity
43
44 Branching and merging:
45
46 $ cd ..
47 $ mkdir linux-work
48 $ cd linux-work
49 $ hg branch ../linux # create a new branch
50 $ hg checkout # populate the working directory
51 $ <make changes>
52 $ hg commit
53 $ cd ../linux
54 $ hg merge ../linux-work # pull changesets from linux-work
55
56 Importing patches:
57
58 Fast:
59 $ patch < ../p/foo.patch
60 $ hg addremove
61 $ hg commit
62
63 Faster:
64 $ patch < ../p/foo.patch
65 $ hg commit `lsdiff -p1 ../p/foo.patch`
66
67 Fastest:
68 $ cat ../p/patchlist | xargs hg import -p1 -b ../p
69
70 Network support (highly experimental):
71
72 # export your .hg directory as a directory on your webserver
73 foo$ ln -s .hg ~/public_html/hg-linux
74
75 # merge changes from a remote machine
76 bar$ hg merge http://foo/~user/hg-linux
77
78 This is just a proof of concept of grabbing byte ranges, and is not
79 expected to perform well.
80
@@ -0,0 +1,255
1 #!/usr/bin/env python
2 #
3 # mercurial - a minimal scalable distributed SCM
4 # v0.4c "oedipa maas"
5 #
6 # Copyright 2005 Matt Mackall <mpm@selenic.com>
7 #
8 # This software may be used and distributed according to the terms
9 # of the GNU General Public License, incorporated herein by reference.
10
11 # the psyco compiler makes commits about twice as fast
12 try:
13 import psyco
14 psyco.full()
15 except:
16 pass
17
18 import sys, os
19 from mercurial import hg, mdiff, fancyopts
20
21 options = {}
22 opts = [('v', 'verbose', None, 'verbose'),
23 ('d', 'debug', None, 'debug')]
24
25 args = fancyopts.fancyopts(sys.argv[1:], opts, options,
26 'hg [options] <command> [command options] [files]')
27
28 try:
29 cmd = args[0]
30 args = args[1:]
31 except:
32 cmd = ""
33
34 ui = hg.ui(options["verbose"], options["debug"])
35
36 if cmd == "init":
37 repo = hg.repository(ui, ".", create=1)
38 sys.exit(0)
39 elif cmd == "branch" or cmd == "clone":
40 os.system("cp -al %s/.hg .hg" % args[0])
41 sys.exit(0)
42 else:
43 repo = hg.repository(ui=ui)
44
45 if cmd == "checkout" or cmd == "co":
46 node = repo.changelog.tip()
47 if len(args): rev = int(args[0])
48 repo.checkout(node)
49
50 elif cmd == "add":
51 repo.add(args)
52
53 elif cmd == "remove" or cmd == "rm" or cmd == "del" or cmd == "delete":
54 repo.remove(args)
55
56 elif cmd == "commit" or cmd == "checkin" or cmd == "ci":
57 if 1:
58 if len(args) > 0:
59 repo.commit(args)
60 else:
61 repo.commit()
62
63 elif cmd == "import" or cmd == "patch":
64 ioptions = {}
65 opts = [('p', 'strip', 1, 'path strip'),
66 ('b', 'base', "", 'base path')]
67
68 args = fancyopts.fancyopts(args, opts, ioptions,
69 'hg import [options] <patch names>')
70 d = ioptions["base"]
71 strip = ioptions["strip"]
72
73 for patch in args:
74 ui.status("applying %s\n" % patch)
75 pf = d + patch
76 os.system("patch -p%d < %s > /dev/null" % (strip, pf))
77 f = os.popen("lsdiff --strip %d %s" % (strip, pf))
78 files = f.read().splitlines()
79 f.close()
80 repo.commit(files)
81
82 elif cmd == "status":
83 (c, a, d) = repo.diffdir(repo.root)
84 for f in c: print "C", f
85 for f in a: print "?", f
86 for f in d: print "R", f
87
88 elif cmd == "diff":
89 mmap = {}
90 if repo.current:
91 change = repo.changelog.read(repo.current)
92 mmap = repo.manifest.read(change[0])
93
94 (c, a, d) = repo.diffdir(repo.root)
95 for f in c:
96 to = repo.file(f).read(mmap[f])
97 tn = file(f).read()
98 sys.stdout.write(mdiff.unidiff(to, tn, f))
99 for f in a:
100 to = ""
101 tn = file(f).read()
102 sys.stdout.write(mdiff.unidiff(to, tn, f))
103 for f in d:
104 to = repo.file(f).read(mmap[f])
105 tn = ""
106 sys.stdout.write(mdiff.unidiff(to, tn, f))
107
108 elif cmd == "addremove":
109 (c, a, d) = repo.diffdir(repo.root)
110 repo.add(a)
111 repo.remove(d)
112
113 elif cmd == "history":
114 for i in range(repo.changelog.count()):
115 n = repo.changelog.node(i)
116 changes = repo.changelog.read(n)
117 (p1, p2) = repo.changelog.parents(n)
118 (h, h1, h2) = map(hg.hex, (n, p1, p2))
119 (i1, i2) = map(repo.changelog.rev, (p1, p2))
120 print "rev: %4d:%s" % (i, h)
121 print "parents: %4d:%s" % (i1, h1)
122 if i2: print " %4d:%s" % (i2, h2)
123 print "manifest: %4d:%s" % (repo.manifest.rev(changes[0]),
124 hg.hex(changes[0]))
125 print "user:", changes[1]
126 print "files:", len(changes[3])
127 print "description:"
128 print changes[4]
129
130 elif cmd == "log":
131 if args:
132 r = repo.file(args[0])
133 for i in range(r.count()):
134 n = r.node(i)
135 (p1, p2) = r.parents(n)
136 (h, h1, h2) = map(hg.hex, (n, p1, p2))
137 (i1, i2) = map(r.rev, (p1, p2))
138 cr = r.linkrev(n)
139 cn = hg.hex(repo.changelog.node(cr))
140 print "rev: %4d:%s" % (i, h)
141 print "changeset: %4d:%s" % (cr, cn)
142 print "parents: %4d:%s" % (i1, h1)
143 if i2: print " %4d:%s" % (i2, h2)
144 else:
145 print "missing filename"
146
147 elif cmd == "dump":
148 if args:
149 r = repo.file(args[0])
150 n = r.tip()
151 if len(args) > 1: n = hg.bin(args[1])
152 sys.stdout.write(r.read(n))
153 else:
154 print "missing filename"
155
156 elif cmd == "dumpmanifest":
157 n = repo.manifest.tip()
158 if len(args) > 0:
159 n = hg.bin(args[0])
160 m = repo.manifest.read(n)
161 files = m.keys()
162 files.sort()
163
164 for f in files:
165 print hg.hex(m[f]), f
166
167 elif cmd == "merge":
168 if args:
169 other = hg.repository(ui, args[0])
170 repo.merge(other)
171 else:
172 print "missing source repository"
173
174 elif cmd == "verify":
175 filelinkrevs = {}
176 filenodes = {}
177 manifestchangeset = {}
178 changesets = revisions = files = 0
179
180 print "checking changesets"
181 for i in range(repo.changelog.count()):
182 changesets += 1
183 n = repo.changelog.node(i)
184 changes = repo.changelog.read(n)
185 manifestchangeset[changes[0]] = n
186 for f in changes[3]:
187 revisions += 1
188 filelinkrevs.setdefault(f, []).append(i)
189
190 print "checking manifests"
191 for i in range(repo.manifest.count()):
192 n = repo.manifest.node(i)
193 ca = repo.changelog.node(repo.manifest.linkrev(n))
194 cc = manifestchangeset[n]
195 if ca != cc:
196 print "manifest %s points to %s, not %s" % \
197 (hg.hex(n), hg.hex(ca), hg.hex(cc))
198 m = repo.manifest.read(n)
199 for f, fn in m.items():
200 filenodes.setdefault(f, {})[fn] = 1
201
202 print "crosschecking files in changesets and manifests"
203 for f in filenodes:
204 if f not in filelinkrevs:
205 print "file %s in manifest but not in changesets"
206
207 for f in filelinkrevs:
208 if f not in filenodes:
209 print "file %s in changeset but not in manifest"
210
211 print "checking files"
212 for f in filenodes:
213 files += 1
214 fl = repo.file(f)
215 nodes = {"\0"*20: 1}
216 for i in range(fl.count()):
217 n = fl.node(i)
218 if n not in filenodes[f]:
219 print "%s:%s not in manifests" % (f, hg.hex(n))
220 if fl.linkrev(n) not in filelinkrevs[f]:
221 print "%s:%s points to unknown changeset %s" \
222 % (f, hg.hex(n), hg.hex(fl.changeset(n)))
223 t = fl.read(n)
224 (p1, p2) = fl.parents(n)
225 if p1 not in nodes:
226 print "%s:%s unknown parent 1 %s" % (f, hg.hex(n), hg.hex(p1))
227 if p2 not in nodes:
228 print "file %s:%s unknown parent %s" % (f, hg.hex(n), hg.hex(p1))
229
230 nodes[n] = 1
231
232 print "%d files, %d changesets, %d total revisions" % (files, changesets,
233 revisions)
234
235 else:
236 print """\
237 unknown command
238
239 commands:
240
241 init create a new repository in this directory
242 branch <path> create a branch of <path> in this directory
243 merge <path> merge changes from <path> into local repository
244 checkout [changeset] checkout the latest or given changeset
245 status show new, missing, and changed files in working dir
246 add [files...] add the given files in the next commit
247 remove [files...] remove the given files in the next commit
248 addremove add all new files, delete all missing files
249 commit commit all changes to the repository
250 history show changeset history
251 log <file> show revision history of a single file
252 dump <file> [rev] dump the latest or given revision of a file
253 dumpmanifest [rev] dump the latest or given revision of the manifest
254 """
255 sys.exit(1)
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,452
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
5 #
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
10 #
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, write to the
13 # Free Software Foundation, Inc.,
14 # 59 Temple Place, Suite 330,
15 # Boston, MA 02111-1307 USA
16
17 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
18 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
19
20 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
21
22 import os
23 import stat
24 import urllib
25 import urllib2
26 import rfc822
27
28 try:
29 from cStringIO import StringIO
30 except ImportError, msg:
31 from StringIO import StringIO
32
33 class RangeError(IOError):
34 """Error raised when an unsatisfiable range is requested."""
35 pass
36
37 class HTTPRangeHandler(urllib2.BaseHandler):
38 """Handler that enables HTTP Range headers.
39
40 This was extremely simple. The Range header is a HTTP feature to
41 begin with so all this class does is tell urllib2 that the
42 "206 Partial Content" reponse from the HTTP server is what we
43 expected.
44
45 Example:
46 import urllib2
47 import byterange
48
49 range_handler = range.HTTPRangeHandler()
50 opener = urllib2.build_opener(range_handler)
51
52 # install it
53 urllib2.install_opener(opener)
54
55 # create Request and set Range header
56 req = urllib2.Request('http://www.python.org/')
57 req.header['Range'] = 'bytes=30-50'
58 f = urllib2.urlopen(req)
59 """
60
61 def http_error_206(self, req, fp, code, msg, hdrs):
62 # 206 Partial Content Response
63 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
64 r.code = code
65 r.msg = msg
66 return r
67
68 def http_error_416(self, req, fp, code, msg, hdrs):
69 # HTTP's Range Not Satisfiable error
70 raise RangeError('Requested Range Not Satisfiable')
71
72 class RangeableFileObject:
73 """File object wrapper to enable raw range handling.
74 This was implemented primarilary for handling range
75 specifications for file:// urls. This object effectively makes
76 a file object look like it consists only of a range of bytes in
77 the stream.
78
79 Examples:
80 # expose 10 bytes, starting at byte position 20, from
81 # /etc/aliases.
82 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
83 # seek seeks within the range (to position 23 in this case)
84 >>> fo.seek(3)
85 # tell tells where your at _within the range_ (position 3 in
86 # this case)
87 >>> fo.tell()
88 # read EOFs if an attempt is made to read past the last
89 # byte in the range. the following will return only 7 bytes.
90 >>> fo.read(30)
91 """
92
93 def __init__(self, fo, rangetup):
94 """Create a RangeableFileObject.
95 fo -- a file like object. only the read() method need be
96 supported but supporting an optimized seek() is
97 preferable.
98 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
99 to work over.
100 The file object provided is assumed to be at byte offset 0.
101 """
102 self.fo = fo
103 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
104 self.realpos = 0
105 self._do_seek(self.firstbyte)
106
107 def __getattr__(self, name):
108 """This effectively allows us to wrap at the instance level.
109 Any attribute not found in _this_ object will be searched for
110 in self.fo. This includes methods."""
111 if hasattr(self.fo, name):
112 return getattr(self.fo, name)
113 raise AttributeError, name
114
115 def tell(self):
116 """Return the position within the range.
117 This is different from fo.seek in that position 0 is the
118 first byte position of the range tuple. For example, if
119 this object was created with a range tuple of (500,899),
120 tell() will return 0 when at byte position 500 of the file.
121 """
122 return (self.realpos - self.firstbyte)
123
124 def seek(self,offset,whence=0):
125 """Seek within the byte range.
126 Positioning is identical to that described under tell().
127 """
128 assert whence in (0, 1, 2)
129 if whence == 0: # absolute seek
130 realoffset = self.firstbyte + offset
131 elif whence == 1: # relative seek
132 realoffset = self.realpos + offset
133 elif whence == 2: # absolute from end of file
134 # XXX: are we raising the right Error here?
135 raise IOError('seek from end of file not supported.')
136
137 # do not allow seek past lastbyte in range
138 if self.lastbyte and (realoffset >= self.lastbyte):
139 realoffset = self.lastbyte
140
141 self._do_seek(realoffset - self.realpos)
142
143 def read(self, size=-1):
144 """Read within the range.
145 This method will limit the size read based on the range.
146 """
147 size = self._calc_read_size(size)
148 rslt = self.fo.read(size)
149 self.realpos += len(rslt)
150 return rslt
151
152 def readline(self, size=-1):
153 """Read lines within the range.
154 This method will limit the size read based on the range.
155 """
156 size = self._calc_read_size(size)
157 rslt = self.fo.readline(size)
158 self.realpos += len(rslt)
159 return rslt
160
161 def _calc_read_size(self, size):
162 """Handles calculating the amount of data to read based on
163 the range.
164 """
165 if self.lastbyte:
166 if size > -1:
167 if ((self.realpos + size) >= self.lastbyte):
168 size = (self.lastbyte - self.realpos)
169 else:
170 size = (self.lastbyte - self.realpos)
171 return size
172
173 def _do_seek(self,offset):
174 """Seek based on whether wrapped object supports seek().
175 offset is relative to the current position (self.realpos).
176 """
177 assert offset >= 0
178 if not hasattr(self.fo, 'seek'):
179 self._poor_mans_seek(offset)
180 else:
181 self.fo.seek(self.realpos + offset)
182 self.realpos+= offset
183
184 def _poor_mans_seek(self,offset):
185 """Seek by calling the wrapped file objects read() method.
186 This is used for file like objects that do not have native
187 seek support. The wrapped objects read() method is called
188 to manually seek to the desired position.
189 offset -- read this number of bytes from the wrapped
190 file object.
191 raise RangeError if we encounter EOF before reaching the
192 specified offset.
193 """
194 pos = 0
195 bufsize = 1024
196 while pos < offset:
197 if (pos + bufsize) > offset:
198 bufsize = offset - pos
199 buf = self.fo.read(bufsize)
200 if len(buf) != bufsize:
201 raise RangeError('Requested Range Not Satisfiable')
202 pos+= bufsize
203
204 class FileRangeHandler(urllib2.FileHandler):
205 """FileHandler subclass that adds Range support.
206 This class handles Range headers exactly like an HTTP
207 server would.
208 """
209 def open_local_file(self, req):
210 import mimetypes
211 import mimetools
212 host = req.get_host()
213 file = req.get_selector()
214 localfile = urllib.url2pathname(file)
215 stats = os.stat(localfile)
216 size = stats[stat.ST_SIZE]
217 modified = rfc822.formatdate(stats[stat.ST_MTIME])
218 mtype = mimetypes.guess_type(file)[0]
219 if host:
220 host, port = urllib.splitport(host)
221 if port or socket.gethostbyname(host) not in self.get_names():
222 raise URLError('file not on local host')
223 fo = open(localfile,'rb')
224 brange = req.headers.get('Range',None)
225 brange = range_header_to_tuple(brange)
226 assert brange != ()
227 if brange:
228 (fb,lb) = brange
229 if lb == '': lb = size
230 if fb < 0 or fb > size or lb > size:
231 raise RangeError('Requested Range Not Satisfiable')
232 size = (lb - fb)
233 fo = RangeableFileObject(fo, (fb,lb))
234 headers = mimetools.Message(StringIO(
235 'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
236 (mtype or 'text/plain', size, modified)))
237 return urllib.addinfourl(fo, headers, 'file:'+file)
238
239
240 # FTP Range Support
241 # Unfortunately, a large amount of base FTP code had to be copied
242 # from urllib and urllib2 in order to insert the FTP REST command.
243 # Code modifications for range support have been commented as
244 # follows:
245 # -- range support modifications start/end here
246
247 from urllib import splitport, splituser, splitpasswd, splitattr, \
248 unquote, addclosehook, addinfourl
249 import ftplib
250 import socket
251 import sys
252 import ftplib
253 import mimetypes
254 import mimetools
255
256 class FTPRangeHandler(urllib2.FTPHandler):
257 def ftp_open(self, req):
258 host = req.get_host()
259 if not host:
260 raise IOError, ('ftp error', 'no host given')
261 host, port = splitport(host)
262 if port is None:
263 port = ftplib.FTP_PORT
264
265 # username/password handling
266 user, host = splituser(host)
267 if user:
268 user, passwd = splitpasswd(user)
269 else:
270 passwd = None
271 host = unquote(host)
272 user = unquote(user or '')
273 passwd = unquote(passwd or '')
274
275 try:
276 host = socket.gethostbyname(host)
277 except socket.error, msg:
278 raise URLError(msg)
279 path, attrs = splitattr(req.get_selector())
280 dirs = path.split('/')
281 dirs = map(unquote, dirs)
282 dirs, file = dirs[:-1], dirs[-1]
283 if dirs and not dirs[0]:
284 dirs = dirs[1:]
285 try:
286 fw = self.connect_ftp(user, passwd, host, port, dirs)
287 type = file and 'I' or 'D'
288 for attr in attrs:
289 attr, value = splitattr(attr)
290 if attr.lower() == 'type' and \
291 value in ('a', 'A', 'i', 'I', 'd', 'D'):
292 type = value.upper()
293
294 # -- range support modifications start here
295 rest = None
296 range_tup = range_header_to_tuple(req.headers.get('Range',None))
297 assert range_tup != ()
298 if range_tup:
299 (fb,lb) = range_tup
300 if fb > 0: rest = fb
301 # -- range support modifications end here
302
303 fp, retrlen = fw.retrfile(file, type, rest)
304
305 # -- range support modifications start here
306 if range_tup:
307 (fb,lb) = range_tup
308 if lb == '':
309 if retrlen is None or retrlen == 0:
310 raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
311 lb = retrlen
312 retrlen = lb - fb
313 if retrlen < 0:
314 # beginning of range is larger than file
315 raise RangeError('Requested Range Not Satisfiable')
316 else:
317 retrlen = lb - fb
318 fp = RangeableFileObject(fp, (0,retrlen))
319 # -- range support modifications end here
320
321 headers = ""
322 mtype = mimetypes.guess_type(req.get_full_url())[0]
323 if mtype:
324 headers += "Content-Type: %s\n" % mtype
325 if retrlen is not None and retrlen >= 0:
326 headers += "Content-Length: %d\n" % retrlen
327 sf = StringIO(headers)
328 headers = mimetools.Message(sf)
329 return addinfourl(fp, headers, req.get_full_url())
330 except ftplib.all_errors, msg:
331 raise IOError, ('ftp error', msg), sys.exc_info()[2]
332
333 def connect_ftp(self, user, passwd, host, port, dirs):
334 fw = ftpwrapper(user, passwd, host, port, dirs)
335 return fw
336
337 class ftpwrapper(urllib.ftpwrapper):
338 # range support note:
339 # this ftpwrapper code is copied directly from
340 # urllib. The only enhancement is to add the rest
341 # argument and pass it on to ftp.ntransfercmd
342 def retrfile(self, file, type, rest=None):
343 self.endtransfer()
344 if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
345 else: cmd = 'TYPE ' + type; isdir = 0
346 try:
347 self.ftp.voidcmd(cmd)
348 except ftplib.all_errors:
349 self.init()
350 self.ftp.voidcmd(cmd)
351 conn = None
352 if file and not isdir:
353 # Use nlst to see if the file exists at all
354 try:
355 self.ftp.nlst(file)
356 except ftplib.error_perm, reason:
357 raise IOError, ('ftp error', reason), sys.exc_info()[2]
358 # Restore the transfer mode!
359 self.ftp.voidcmd(cmd)
360 # Try to retrieve as a file
361 try:
362 cmd = 'RETR ' + file
363 conn = self.ftp.ntransfercmd(cmd, rest)
364 except ftplib.error_perm, reason:
365 if str(reason)[:3] == '501':
366 # workaround for REST not supported error
367 fp, retrlen = self.retrfile(file, type)
368 fp = RangeableFileObject(fp, (rest,''))
369 return (fp, retrlen)
370 elif str(reason)[:3] != '550':
371 raise IOError, ('ftp error', reason), sys.exc_info()[2]
372 if not conn:
373 # Set transfer mode to ASCII!
374 self.ftp.voidcmd('TYPE A')
375 # Try a directory listing
376 if file: cmd = 'LIST ' + file
377 else: cmd = 'LIST'
378 conn = self.ftp.ntransfercmd(cmd)
379 self.busy = 1
380 # Pass back both a suitably decorated object and a retrieval length
381 return (addclosehook(conn[0].makefile('rb'),
382 self.endtransfer), conn[1])
383
384
385 ####################################################################
386 # Range Tuple Functions
387 # XXX: These range tuple functions might go better in a class.
388
389 _rangere = None
390 def range_header_to_tuple(range_header):
391 """Get a (firstbyte,lastbyte) tuple from a Range header value.
392
393 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
394 function pulls the firstbyte and lastbyte values and returns
395 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
396 the header value, it is returned as an empty string in the
397 tuple.
398
399 Return None if range_header is None
400 Return () if range_header does not conform to the range spec
401 pattern.
402
403 """
404 global _rangere
405 if range_header is None: return None
406 if _rangere is None:
407 import re
408 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
409 match = _rangere.match(range_header)
410 if match:
411 tup = range_tuple_normalize(match.group(1,2))
412 if tup and tup[1]:
413 tup = (tup[0],tup[1]+1)
414 return tup
415 return ()
416
417 def range_tuple_to_header(range_tup):
418 """Convert a range tuple to a Range header value.
419 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
420 if no range is needed.
421 """
422 if range_tup is None: return None
423 range_tup = range_tuple_normalize(range_tup)
424 if range_tup:
425 if range_tup[1]:
426 range_tup = (range_tup[0],range_tup[1] - 1)
427 return 'bytes=%s-%s' % range_tup
428
429 def range_tuple_normalize(range_tup):
430 """Normalize a (first_byte,last_byte) range tuple.
431 Return a tuple whose first element is guaranteed to be an int
432 and whose second element will be '' (meaning: the last byte) or
433 an int. Finally, return None if the normalized tuple == (0,'')
434 as that is equivelant to retrieving the entire file.
435 """
436 if range_tup is None: return None
437 # handle first byte
438 fb = range_tup[0]
439 if fb in (None,''): fb = 0
440 else: fb = int(fb)
441 # handle last byte
442 try: lb = range_tup[1]
443 except IndexError: lb = ''
444 else:
445 if lb is None: lb = ''
446 elif lb != '': lb = int(lb)
447 # check if range is over the entire file
448 if (fb,lb) == (0,''): return None
449 # check that the range is valid
450 if lb < fb: raise RangeError('Invalid byte range: %s-%s' % (fb,lb))
451 return (fb,lb)
452
@@ -0,0 +1,51
1 import sys, os, getopt
2
3 def fancyopts(args, options, state, syntax=''):
4 long=[]
5 short=''
6 map={}
7 dt={}
8
9 def help(state, opt, arg, options=options, syntax=syntax):
10 print "Usage: ", syntax
11
12 for s, l, d, c in options:
13 opt=' '
14 if s: opt = opt + '-' + s + ' '
15 if l: opt = opt + '--' + l + ' '
16 if d: opt = opt + '(' + str(d) + ')'
17 print opt
18 if c: print ' %s' % c
19 sys.exit(0)
20
21 if len(args) == 0:
22 help(state, None, args)
23
24 options=[('h', 'help', help, 'Show usage info')] + options
25
26 for s, l, d, c in options:
27 map['-'+s] = map['--'+l]=l
28 state[l] = d
29 dt[l] = type(d)
30 if not d is None and not type(d) is type(help): s, l=s+':', l+'='
31 if s: short = short + s
32 if l: long.append(l)
33
34 if os.environ.has_key("HG_OPTS"):
35 args = os.environ["HG_OPTS"].split() + args
36
37 try:
38 opts, args = getopt.getopt(args, short, long)
39 except getopt.GetoptError:
40 help(state, None, args)
41 sys.exit(-1)
42
43 for opt, arg in opts:
44 if dt[map[opt]] is type(help): state[map[opt]](state,map[opt],arg)
45 elif dt[map[opt]] is type(1): state[map[opt]] = int(arg)
46 elif dt[map[opt]] is type(''): state[map[opt]] = arg
47 elif dt[map[opt]] is type([]): state[map[opt]].append(arg)
48 elif dt[map[opt]] is type(None): state[map[opt]] = 1
49
50 return args
51
This diff has been collapsed as it changes many lines, (573 lines changed) Show them Hide them
@@ -0,0 +1,573
1 # hg.py - repository classes for mercurial
2 #
3 # Copyright 2005 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
7
8 import sys, struct, sha, socket, os, time, base64, re, urllib2
9 from mercurial import byterange
10 from mercurial.transaction import *
11 from mercurial.revlog import *
12
13 def hex(node): return binascii.hexlify(node)
14 def bin(node): return binascii.unhexlify(node)
15
16 class filelog(revlog):
17 def __init__(self, opener, path):
18 s = self.encodepath(path)
19 revlog.__init__(self, opener, os.path.join("data", s + "i"),
20 os.path.join("data", s))
21
22 def encodepath(self, path):
23 s = sha.sha(path).digest()
24 s = base64.encodestring(s)[:-3]
25 s = re.sub("\+", "%", s)
26 s = re.sub("/", "_", s)
27 return s
28
29 def read(self, node):
30 return self.revision(node)
31 def add(self, text, transaction, link, p1=None, p2=None):
32 return self.addrevision(text, transaction, link, p1, p2)
33
34 def resolvedag(self, old, new, transaction, link):
35 """resolve unmerged heads in our DAG"""
36 if old == new: return None
37 a = self.ancestor(old, new)
38 if old == a: return new
39 return self.merge3(old, new, a, transaction, link)
40
41 def merge3(self, my, other, base, transaction, link):
42 """perform a 3-way merge and append the result"""
43 def temp(prefix, node):
44 (fd, name) = tempfile.mkstemp(prefix)
45 f = os.fdopen(fd, "w")
46 f.write(self.revision(node))
47 f.close()
48 return name
49
50 a = temp("local", my)
51 b = temp("remote", other)
52 c = temp("parent", base)
53
54 cmd = os.environ["HGMERGE"]
55 r = os.system("%s %s %s %s" % (cmd, a, b, c))
56 if r:
57 raise "Merge failed, implement rollback!"
58
59 t = open(a).read()
60 os.unlink(a)
61 os.unlink(b)
62 os.unlink(c)
63 return self.addrevision(t, transaction, link, my, other)
64
65 def merge(self, other, transaction, linkseq, link):
66 """perform a merge and resolve resulting heads"""
67 (o, n) = self.mergedag(other, transaction, linkseq)
68 return self.resolvedag(o, n, transaction, link)
69
70 class manifest(revlog):
71 def __init__(self, opener):
72 self.mapcache = None
73 self.listcache = None
74 self.addlist = None
75 revlog.__init__(self, opener, "00manifest.i", "00manifest.d")
76
77 def read(self, node):
78 if self.mapcache and self.mapcache[0] == node:
79 return self.mapcache[1]
80 text = self.revision(node)
81 map = {}
82 self.listcache = text.splitlines(1)
83 for l in self.listcache:
84 (f, n) = l.split('\0')
85 map[f] = bin(n[:40])
86 self.mapcache = (node, map)
87 return map
88
89 def diff(self, a, b):
90 # this is sneaky, as we're not actually using a and b
91 if self.listcache:
92 return mdiff.diff(self.listcache, self.addlist, 1)
93 else:
94 return mdiff.diff(a, b)
95
96 def add(self, map, transaction, link, p1=None, p2=None):
97 files = map.keys()
98 files.sort()
99
100 self.addlist = ["%s\000%s\n" % (f, hex(map[f])) for f in files]
101 text = "".join(self.addlist)
102
103 n = self.addrevision(text, transaction, link, p1, p2)
104 self.mapcache = (n, map)
105 self.listcache = self.addlist
106
107 return n
108
109 class changelog(revlog):
110 def __init__(self, opener):
111 revlog.__init__(self, opener, "00changelog.i", "00changelog.d")
112
113 def extract(self, text):
114 last = text.index("\n\n")
115 desc = text[last + 2:]
116 l = text[:last].splitlines()
117 manifest = bin(l[0])
118 user = l[1]
119 date = l[2]
120 files = l[3:]
121 return (manifest, user, date, files, desc)
122
123 def read(self, node):
124 return self.extract(self.revision(node))
125
126 def add(self, manifest, list, desc, transaction, p1=None, p2=None):
127 try: user = os.environ["HGUSER"]
128 except: user = os.environ["LOGNAME"] + '@' + socket.getfqdn()
129 date = "%d %d" % (time.time(), time.timezone)
130 list.sort()
131 l = [hex(manifest), user, date] + list + ["", desc]
132 text = "\n".join(l)
133 return self.addrevision(text, transaction, self.count(), p1, p2)
134
135 def merge3(self, my, other, base):
136 pass
137
138 class dircache:
139 def __init__(self, opener):
140 self.opener = opener
141 self.dirty = 0
142 self.map = None
143 def __del__(self):
144 if self.dirty: self.write()
145 def __getitem__(self, key):
146 try:
147 return self.map[key]
148 except TypeError:
149 self.read()
150 return self[key]
151
152 def read(self):
153 if self.map is not None: return self.map
154
155 self.map = {}
156 try:
157 st = self.opener("dircache").read()
158 except: return
159
160 pos = 0
161 while pos < len(st):
162 e = struct.unpack(">llll", st[pos:pos+16])
163 l = e[3]
164 pos += 16
165 f = st[pos:pos + l]
166 self.map[f] = e[:3]
167 pos += l
168
169 def update(self, files):
170 if not files: return
171 self.read()
172 self.dirty = 1
173 for f in files:
174 try:
175 s = os.stat(f)
176 self.map[f] = (s.st_mode, s.st_size, s.st_mtime)
177 except IOError:
178 self.remove(f)
179
180 def taint(self, files):
181 if not files: return
182 self.read()
183 self.dirty = 1
184 for f in files:
185 self.map[f] = (0, -1, 0)
186
187 def remove(self, files):
188 if not files: return
189 self.read()
190 self.dirty = 1
191 for f in files:
192 try: del self[f]
193 except: pass
194
195 def clear(self):
196 self.map = {}
197 self.dirty = 1
198
199 def write(self):
200 st = self.opener("dircache", "w")
201 for f, e in self.map.items():
202 e = struct.pack(">llll", e[0], e[1], e[2], len(f))
203 st.write(e + f)
204 self.dirty = 0
205
206 def copy(self):
207 self.read()
208 return self.map.copy()
209
210 # used to avoid circular references so destructors work
211 def opener(base):
212 p = base
213 def o(path, mode="r"):
214 f = os.path.join(p, path)
215 if p[:7] == "http://":
216 return httprangereader(f)
217
218 if mode != "r" and os.path.isfile(f):
219 s = os.stat(f)
220 if s.st_nlink > 1:
221 file(f + ".tmp", "w").write(file(f).read())
222 os.rename(f+".tmp", f)
223
224 return file(f, mode)
225
226 return o
227
228 class repository:
229 def __init__(self, ui, path=None, create=0):
230 self.remote = 0
231 if path and path[:7] == "http://":
232 self.remote = 1
233 self.path = path
234 else:
235 if not path:
236 p = os.getcwd()
237 while not os.path.isdir(os.path.join(p, ".hg")):
238 p = os.path.dirname(p)
239 if p == "/": raise "No repo found"
240 path = p
241 self.path = os.path.join(path, ".hg")
242
243 self.root = path
244 self.ui = ui
245
246 if create:
247 os.mkdir(self.path)
248 os.mkdir(self.join("data"))
249
250 self.opener = opener(self.path)
251 self.manifest = manifest(self.opener)
252 self.changelog = changelog(self.opener)
253 self.ignorelist = None
254
255 if not self.remote:
256 self.dircache = dircache(self.opener)
257 try:
258 self.current = bin(self.open("current").read())
259 except:
260 self.current = None
261
262 def setcurrent(self, node):
263 self.current = node
264 self.opener("current", "w").write(hex(node))
265
266 def ignore(self, f):
267 if self.ignorelist is None:
268 self.ignorelist = []
269 try:
270 l = open(os.path.join(self.root, ".hgignore")).readlines()
271 for pat in l:
272 self.ignorelist.append(re.compile(pat[:-1]))
273 except IOError: pass
274 for pat in self.ignorelist:
275 if pat.search(f): return True
276 return False
277
278 def join(self, f):
279 return os.path.join(self.path, f)
280
281 def file(self, f):
282 return filelog(self.opener, f)
283
284 def transaction(self):
285 return transaction(self.opener, self.join("journal"))
286
287 def merge(self, other):
288 tr = self.transaction()
289 changed = {}
290 new = {}
291 nextrev = seqrev = self.changelog.count()
292
293 # helpers for back-linking file revisions to local changeset
294 # revisions so we can immediately get to changeset from annotate
295 def accumulate(text):
296 n = nextrev
297 # track which files are added in which changeset and the
298 # corresponding _local_ changeset revision
299 files = self.changelog.extract(text)[3]
300 for f in files:
301 changed.setdefault(f, []).append(n)
302 n += 1
303
304 def seq(start):
305 while 1:
306 yield start
307 start += 1
308
309 def lseq(l):
310 for r in l:
311 yield r
312
313 # begin the import/merge of changesets
314 self.ui.status("merging new changesets\n")
315 (co, cn) = self.changelog.mergedag(other.changelog, tr,
316 seq(seqrev), accumulate)
317 resolverev = self.changelog.count()
318
319 # is there anything to do?
320 if co == cn:
321 tr.close()
322 return
323
324 # do we need to resolve?
325 simple = (co == self.changelog.ancestor(co, cn))
326
327 # merge all files changed by the changesets,
328 # keeping track of the new tips
329 changelist = changed.keys()
330 changelist.sort()
331 for f in changelist:
332 sys.stdout.write(".")
333 sys.stdout.flush()
334 r = self.file(f)
335 node = r.merge(other.file(f), tr, lseq(changed[f]), resolverev)
336 if node:
337 new[f] = node
338 sys.stdout.write("\n")
339
340 # begin the merge of the manifest
341 self.ui.status("merging manifests\n")
342 (mm, mo) = self.manifest.mergedag(other.manifest, tr, seq(seqrev))
343
344 # For simple merges, we don't need to resolve manifests or changesets
345 if simple:
346 tr.close()
347 return
348
349 ma = self.manifest.ancestor(mm, mo)
350
351 # resolve the manifest to point to all the merged files
352 self.ui.status("resolving manifests\n")
353 mmap = self.manifest.read(mm) # mine
354 omap = self.manifest.read(mo) # other
355 amap = self.manifest.read(ma) # ancestor
356 nmap = {}
357
358 for f, mid in mmap.iteritems():
359 if f in omap:
360 if mid != omap[f]:
361 nmap[f] = new.get(f, mid) # use merged version
362 else:
363 nmap[f] = new.get(f, mid) # they're the same
364 del omap[f]
365 elif f in amap:
366 if mid != amap[f]:
367 pass # we should prompt here
368 else:
369 pass # other deleted it
370 else:
371 nmap[f] = new.get(f, mid) # we created it
372
373 del mmap
374
375 for f, oid in omap.iteritems():
376 if f in amap:
377 if oid != amap[f]:
378 pass # this is the nasty case, we should prompt
379 else:
380 pass # probably safe
381 else:
382 nmap[f] = new.get(f, oid) # remote created it
383
384 del omap
385 del amap
386
387 node = self.manifest.add(nmap, tr, resolverev, mm, mo)
388
389 # Now all files and manifests are merged, we add the changed files
390 # and manifest id to the changelog
391 self.ui.status("committing merge changeset\n")
392 new = new.keys()
393 new.sort()
394 if co == cn: cn = -1
395
396 edittext = "\n"+"".join(["HG: changed %s\n" % f for f in new])
397 edittext = self.ui.edit(edittext)
398 n = self.changelog.add(node, new, edittext, tr, co, cn)
399
400 tr.close()
401
402 def commit(self, update = None, text = ""):
403 tr = self.transaction()
404
405 try:
406 remove = [ l[:-1] for l in self.opener("to-remove") ]
407 os.unlink(self.join("to-remove"))
408
409 except IOError:
410 remove = []
411
412 if update == None:
413 update = self.diffdir(self.root)[0]
414
415 # check in files
416 new = {}
417 linkrev = self.changelog.count()
418 for f in update:
419 try:
420 t = file(f).read()
421 except IOError:
422 remove.append(f)
423 continue
424 r = self.file(f)
425 new[f] = r.add(t, tr, linkrev)
426
427 # update manifest
428 mmap = self.manifest.read(self.manifest.tip())
429 mmap.update(new)
430 for f in remove:
431 del mmap[f]
432 mnode = self.manifest.add(mmap, tr, linkrev)
433
434 # add changeset
435 new = new.keys()
436 new.sort()
437
438 edittext = text + "\n"+"".join(["HG: changed %s\n" % f for f in new])
439 edittext = self.ui.edit(edittext)
440
441 n = self.changelog.add(mnode, new, edittext, tr)
442 tr.close()
443
444 self.setcurrent(n)
445 self.dircache.update(new)
446 self.dircache.remove(remove)
447
448 def checkdir(self, path):
449 d = os.path.dirname(path)
450 if not d: return
451 if not os.path.isdir(d):
452 self.checkdir(d)
453 os.mkdir(d)
454
455 def checkout(self, node):
456 # checkout is really dumb at the moment
457 # it ought to basically merge
458 change = self.changelog.read(node)
459 mmap = self.manifest.read(change[0])
460
461 l = mmap.keys()
462 l.sort()
463 stats = []
464 for f in l:
465 r = self.file(f)
466 t = r.revision(mmap[f])
467 try:
468 file(f, "w").write(t)
469 except:
470 self.checkdir(f)
471 file(f, "w").write(t)
472
473 self.setcurrent(node)
474 self.dircache.clear()
475 self.dircache.update(l)
476
477 def diffdir(self, path):
478 dc = self.dircache.copy()
479 changed = []
480 added = []
481
482 mmap = {}
483 if self.current:
484 change = self.changelog.read(self.current)
485 mmap = self.manifest.read(change[0])
486
487 for dir, subdirs, files in os.walk(self.root):
488 d = dir[len(self.root)+1:]
489 if ".hg" in subdirs: subdirs.remove(".hg")
490
491 for f in files:
492 fn = os.path.join(d, f)
493 try: s = os.stat(fn)
494 except: continue
495 if fn in dc:
496 c = dc[fn]
497 del dc[fn]
498 if c[1] != s.st_size:
499 changed.append(fn)
500 elif c[0] != s.st_mode or c[2] != s.st_mtime:
501 t1 = file(fn).read()
502 t2 = self.file(fn).revision(mmap[fn])
503 if t1 != t2:
504 changed.append(fn)
505 else:
506 if self.ignore(fn): continue
507 added.append(fn)
508
509 deleted = dc.keys()
510 deleted.sort()
511
512 return (changed, added, deleted)
513
514 def add(self, list):
515 self.dircache.taint(list)
516
517 def remove(self, list):
518 dl = self.opener("to-remove", "a")
519 for f in list:
520 dl.write(f + "\n")
521
522 class ui:
523 def __init__(self, verbose=False, debug=False):
524 self.verbose = verbose
525 def write(self, *args):
526 for a in args:
527 sys.stdout.write(str(a))
528 def prompt(self, msg, pat):
529 while 1:
530 sys.stdout.write(msg)
531 r = sys.stdin.readline()[:-1]
532 if re.match(pat, r):
533 return r
534 def status(self, *msg):
535 self.write(*msg)
536 def warn(self, msg):
537 self.write(*msg)
538 def note(self, msg):
539 if self.verbose: self.write(*msg)
540 def debug(self, msg):
541 if self.debug: self.write(*msg)
542 def edit(self, text):
543 (fd, name) = tempfile.mkstemp("hg")
544 f = os.fdopen(fd, "w")
545 f.write(text)
546 f.close()
547
548 editor = os.environ.get("EDITOR", "vi")
549 r = os.system("%s %s" % (editor, name))
550 if r:
551 raise "Edit failed!"
552
553 t = open(name).read()
554 t = re.sub("(?m)^HG:.*\n", "", t)
555
556 return t
557
558
559 class httprangereader:
560 def __init__(self, url):
561 self.url = url
562 self.pos = 0
563 def seek(self, pos):
564 self.pos = pos
565 def read(self, bytes=None):
566 opener = urllib2.build_opener(byterange.HTTPRangeHandler())
567 urllib2.install_opener(opener)
568 req = urllib2.Request(self.url)
569 end = ''
570 if bytes: end = self.pos + bytes
571 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
572 f = urllib2.urlopen(req)
573 return f.read()
@@ -0,0 +1,76
1 #!/usr/bin/python
2 import difflib, struct
3 from cStringIO import StringIO
4
5 def unidiff(a, b, fn):
6 a = a.splitlines(1)
7 b = b.splitlines(1)
8 l = difflib.unified_diff(a, b, fn, fn)
9 return "".join(l)
10
11 def textdiff(a, b):
12 return diff(a.splitlines(1), b.splitlines(1))
13
14 def sortdiff(a, b):
15 la = lb = 0
16
17 while 1:
18 if la >= len(a) or lb >= len(b): break
19 if b[lb] < a[la]:
20 si = lb
21 while lb < len(b) and b[lb] < a[la] : lb += 1
22 yield "insert", la, la, si, lb
23 elif a[la] < b[lb]:
24 si = la
25 while la < len(a) and a[la] < b[lb]: la += 1
26 yield "delete", si, la, lb, lb
27 else:
28 la += 1
29 lb += 1
30
31 si = lb
32 while lb < len(b):
33 lb += 1
34 yield "insert", la, la, si, lb
35
36 si = la
37 while la < len(a):
38 la += 1
39 yield "delete", si, la, lb, lb
40
41 def diff(a, b, sorted=0):
42 bin = []
43 p = [0]
44 for i in a: p.append(p[-1] + len(i))
45
46 if sorted:
47 d = sortdiff(a, b)
48 else:
49 d = difflib.SequenceMatcher(None, a, b).get_opcodes()
50
51 for o, m, n, s, t in d:
52 if o == 'equal': continue
53 s = "".join(b[s:t])
54 bin.append(struct.pack(">lll", p[m], p[n], len(s)) + s)
55
56 return "".join(bin)
57
58 def patch(a, bin):
59 last = pos = 0
60 r = []
61
62 while pos < len(bin):
63 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
64 pos += 12
65 r.append(a[last:p1])
66 r.append(bin[pos:pos + l])
67 pos += l
68 last = p2
69 r.append(a[last:])
70
71 return "".join(r)
72
73
74
75
76
@@ -0,0 +1,199
1 # revlog.py - storage back-end for mercurial
2 #
3 # This provides efficient delta storage with O(1) retrieve and append
4 # and O(changes) merge between branches
5 #
6 # Copyright 2005 Matt Mackall <mpm@selenic.com>
7 #
8 # This software may be used and distributed according to the terms
9 # of the GNU General Public License, incorporated herein by reference.
10
11 import zlib, struct, sha, binascii, os, tempfile
12 from mercurial import mdiff
13
14 def compress(text):
15 return zlib.compress(text)
16
17 def decompress(bin):
18 return zlib.decompress(bin)
19
20 def hash(text, p1, p2):
21 l = [p1, p2]
22 l.sort()
23 return sha.sha(l[0] + l[1] + text).digest()
24
25 nullid = "\0" * 20
26 indexformat = ">4l20s20s20s"
27
28 class revlog:
29 def __init__(self, opener, indexfile, datafile):
30 self.indexfile = indexfile
31 self.datafile = datafile
32 self.index = []
33 self.opener = opener
34 self.cache = None
35 self.nodemap = { -1: nullid, nullid: -1 }
36 # read the whole index for now, handle on-demand later
37 try:
38 n = 0
39 i = self.opener(self.indexfile).read()
40 s = struct.calcsize(indexformat)
41 for f in range(0, len(i), s):
42 # offset, size, base, linkrev, p1, p2, nodeid, changeset
43 e = struct.unpack(indexformat, i[f:f + s])
44 self.nodemap[e[6]] = n
45 self.index.append(e)
46 n += 1
47 except IOError: pass
48
49 def tip(self): return self.node(len(self.index) - 1)
50 def count(self): return len(self.index)
51 def node(self, rev): return rev < 0 and nullid or self.index[rev][6]
52 def rev(self, node): return self.nodemap[node]
53 def linkrev(self, node): return self.index[self.nodemap[node]][3]
54 def parents(self, node): return self.index[self.nodemap[node]][4:6]
55
56 def start(self, rev): return self.index[rev][0]
57 def length(self, rev): return self.index[rev][1]
58 def end(self, rev): return self.start(rev) + self.length(rev)
59 def base(self, rev): return self.index[rev][2]
60
61 def revisions(self, list):
62 # this can be optimized to do spans, etc
63 # be stupid for now
64 for r in list:
65 yield self.revision(r)
66
67 def diff(self, a, b):
68 return mdiff.textdiff(a, b)
69
70 def patch(self, text, patch):
71 return mdiff.patch(text, patch)
72
73 def revision(self, node):
74 if node is nullid: return ""
75 if self.cache and self.cache[0] == node: return self.cache[2]
76
77 text = None
78 rev = self.rev(node)
79 base = self.base(rev)
80 start = self.start(base)
81 end = self.end(rev)
82
83 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
84 base = self.cache[1]
85 start = self.start(base + 1)
86 text = self.cache[2]
87 last = 0
88
89 f = self.opener(self.datafile)
90 f.seek(start)
91 data = f.read(end - start)
92
93 if not text:
94 last = self.length(base)
95 text = decompress(data[:last])
96
97 for r in range(base + 1, rev + 1):
98 s = self.length(r)
99 b = decompress(data[last:last + s])
100 text = self.patch(text, b)
101 last = last + s
102
103 (p1, p2) = self.parents(node)
104 if self.node(rev) != hash(text, p1, p2):
105 raise "Consistency check failed on %s:%d" % (self.datafile, rev)
106
107 self.cache = (node, rev, text)
108 return text
109
110 def addrevision(self, text, transaction, link, p1=None, p2=None):
111 if text is None: text = ""
112 if p1 is None: p1 = self.tip()
113 if p2 is None: p2 = nullid
114
115 node = hash(text, p1, p2)
116
117 n = self.count()
118 t = n - 1
119
120 if n:
121 start = self.start(self.base(t))
122 end = self.end(t)
123 prev = self.revision(self.tip())
124 if 0:
125 dd = self.diff(prev, text)
126 tt = self.patch(prev, dd)
127 if tt != text:
128 print prev
129 print text
130 print tt
131 raise "diff+patch failed"
132 data = compress(self.diff(prev, text))
133
134 # full versions are inserted when the needed deltas
135 # become comparable to the uncompressed text
136 if not n or (end + len(data) - start) > len(text) * 2:
137 data = compress(text)
138 base = n
139 else:
140 base = self.base(t)
141
142 offset = 0
143 if t >= 0:
144 offset = self.end(t)
145
146 e = (offset, len(data), base, link, p1, p2, node)
147
148 self.index.append(e)
149 self.nodemap[node] = n
150 entry = struct.pack(indexformat, *e)
151
152 transaction.add(self.datafile, e[0])
153 self.opener(self.datafile, "a").write(data)
154 transaction.add(self.indexfile, n * len(entry))
155 self.opener(self.indexfile, "a").write(entry)
156
157 self.cache = (node, n, text)
158 return node
159
160 def ancestor(self, a, b):
161 def expand(e1, e2, a1, a2):
162 ne = []
163 for n in e1:
164 (p1, p2) = self.parents(n)
165 if p1 in a2: return p1
166 if p2 in a2: return p2
167 if p1 != nullid and p1 not in a1:
168 a1[p1] = 1
169 ne.append(p1)
170 if p2 != nullid and p2 not in a1:
171 a1[p2] = 1
172 ne.append(p2)
173 return expand(e2, ne, a2, a1)
174 return expand([a], [b], {a:1}, {b:1})
175
176 def mergedag(self, other, transaction, linkseq, accumulate = None):
177 """combine the nodes from other's DAG into ours"""
178 old = self.tip()
179 i = self.count()
180 l = []
181
182 # merge the other revision log into our DAG
183 for r in range(other.count()):
184 id = other.node(r)
185 if id not in self.nodemap:
186 (xn, yn) = other.parents(id)
187 l.append((id, xn, yn))
188 self.nodemap[id] = i
189 i += 1
190
191 # merge node date for new nodes
192 r = other.revisions([e[0] for e in l])
193 for e in l:
194 t = r.next()
195 if accumulate: accumulate(t)
196 self.addrevision(t, transaction, linkseq.next(), e[1], e[2])
197
198 # return the unmerged heads for later resolving
199 return (old, self.tip())
@@ -0,0 +1,62
1 # transaction.py - simple journalling scheme for mercurial
2 #
3 # This transaction scheme is intended to gracefully handle program
4 # errors and interruptions. More serious failures like system crashes
5 # can be recovered with an fsck-like tool. As the whole repository is
6 # effectively log-structured, this should amount to simply truncating
7 # anything that isn't referenced in the changelog.
8 #
9 # Copyright 2005 Matt Mackall <mpm@selenic.com>
10 #
11 # This software may be used and distributed according to the terms
12 # of the GNU General Public License, incorporated herein by reference.
13
14 import os
15
16 class transaction:
17 def __init__(self, opener, journal):
18 self.opener = opener
19 self.entries = []
20 self.journal = journal
21
22 # abort here if the journal already exists
23 if os.path.exists(self.journal):
24 raise "Journal already exists!"
25 self.file = open(self.journal, "w")
26
27 def __del__(self):
28 if self.entries: self.abort()
29
30 def add(self, file, offset):
31 self.entries.append((file, offset))
32 # add enough data to the journal to do the truncate
33 self.file.write("%s\0%d\n" % (file, offset))
34 self.file.flush()
35
36 def close(self):
37 self.file.close()
38 self.entries = []
39 os.unlink(self.journal)
40
41 def abort(self):
42 if not self.entries: return
43
44 print "transaction abort!"
45
46 for f, o in self.entries:
47 self.opener(f, "a").truncate(o)
48
49 self.entries = []
50
51 try:
52 os.unlink(self.journal)
53 self.file.close()
54 except: pass
55
56 print "rollback completed"
57
58 def recover(self):
59 for l in open(self.journal).readlines():
60 f, o = l.split('\0')
61 self.opener(f, "a").truncate(int(o))
62
@@ -0,0 +1,159
1 Some notes about Mercurial's design
2
3 Revlogs:
4
5 The fundamental storage type in Mercurial is a "revlog". A revlog is
6 the set of all revisions to a file. Each revision is either stored
7 compressed in its entirety or as a compressed binary delta against the
8 previous version. The decision of when to store a full version is made
9 based on how much data would be needed to reconstruct the file. This
10 lets us ensure that we never need to read huge amounts of data to
11 reconstruct a file, regardless of how many revisions of it we store.
12
13 In fact, we should always be able to do it with a single read,
14 provided we know when and where to read. This is where the index comes
15 in. Each revlog has an index containing a special hash (nodeid) of the
16 text, hashes for its parents, and where and how much of the revlog
17 data we need to read to reconstruct it. Thus, with one read of the
18 index and one read of the data, we can reconstruct any version in time
19 proportional to the file size.
20
21 Similarly, revlogs and their indices are append-only. This means that
22 adding a new version is also O(1) seeks.
23
24 Generally revlogs are used to represent revisions of files, but they
25 also are used to represent manifests and changesets.
26
27 Manifests:
28
29 A manifest is simply a list of all files in a given revision of a
30 project along with the nodeids of the corresponding file revisions. So
31 grabbing a given version of the project means simply looking up its
32 manifest and reconstruction all the file revisions pointed to by it.
33
34 Changesets:
35
36 A changeset is a list of all files changed in a check-in along with a
37 change description and some metadata like user and date. It also
38 contains a nodeid to the relevent revision of the manifest. Changesets
39 and manifests are one-to-one, but contain different data for
40 convenience.
41
42 Nodeids:
43
44 Nodeids are unique ids that are used to represent the contents of a
45 file AND its position in the project history. That is, if you change a
46 file and then change it back, the result will have a different nodeid
47 because it has different history. This is accomplished by including
48 the parents of a given revision's nodeids with the revision's text
49 when calculating the hash.
50
51 Graph merging:
52
53 Nodeids are implemented as they are to simplify merging. Merging a
54 pair of directed acyclic graphs (aka "the family tree" of the file
55 history) requires some method of determining if nodes in different
56 graphs correspond. Simply comparing the contents of the node (by
57 comparing text of given revisions or their hashes) can get confused by
58 identical revisions in the tree.
59
60 The nodeid approach makes it trivial - the hash uniquely describes a
61 revision's contents and its graph position relative to the root, so
62 merge is simply checking whether each nodeid in graph A is in the hash
63 table of graph B. If not, we pull them in, adding them sequentially to
64 the revlog.
65
66 Graph resolving:
67
68 Mercurial does branching by copying (or COWing) a repository and thus
69 keeps everything nice and linear within a repository. However, when a
70 merge of repositories (a "pull") is done, we may often have two head
71 revisions in a given graph. To keep things simple, Mercurial forces
72 the head revisions to be merged.
73
74 It first finds the closest common ancestor of the two heads. If one is
75 a child of the other, it becomes the new head. Otherwise, we call out
76 to a user-specified 3-way merge tool.
77
78 Merging files, manifests, and changesets:
79
80 We begin by comparing changeset DAGs, pulling all nodes we don't have
81 in our DAG from the other repository. As we do so, we collect a list
82 of changed files to merge.
83
84 Then for each file, we perform a graph merge and resolve as above.
85 It's important to merge files using per-file DAGs rather than just
86 changeset level DAGs as this diagram illustrates:
87
88 M M1 M2
89
90 AB
91 |`-------v M2 clones M
92 aB AB file A is change in mainline
93 |`---v AB' file B is changed in M2
94 | aB / | M1 clones M
95 | ab/ | M1 changes B
96 | ab' | M1 merges from M2, changes to B conflict
97 | | A'B' M2 changes A
98 `---+--.|
99 | a'B' M2 merges from mainline, changes to A conflict
100 `--.|
101 ??? depending on which ancestor we choose, we will have
102 to redo A hand-merge, B hand-merge, or both
103 but if we look at the files independently, everything
104 is fine
105
106 After we've merged files, we merge the manifest log DAG and resolve
107 additions and deletions. Then we are ready to resolve the changeset
108 DAG - if our merge required any changes (the new head is not a
109 decendent of our tip), we must create a new changeset describing all
110 of the changes needed to merge it into the tip.
111
112 Merge performance:
113
114 The I/O operations for performing a merge are O(changed files), not
115 O(total changes) and in many cases, we needn't even unpack the deltas
116 to add them to our repository (though this optimization isn't
117 necessary).
118
119 Rollback:
120
121 Rollback is not yet implemented, but will be easy to add. When
122 performing a commit or a merge, we order things so that the changeset
123 entry gets added last. We keep a transaction log of the name of each
124 file and its length prior to the transaction. On abort, we simply
125 truncate each file to its prior length. This is one of the nice
126 properties of the append-only structure of the revlogs.
127
128 Remote access:
129
130 Mercurial currently supports pulling from "serverless" repositories.
131 Simply making the repo directory accessibly via the web and pointing
132 hg at it can accomplish a pull. This is relatively bandwidth efficient
133 but no effort has been spent on pipelining, so it won't work
134 especially well over LAN yet.
135
136 It's also quite amenable to rsync, if you don't mind keeping an intact
137 copy of the master around locally.
138
139 Also note the append-only and ordering properties of the commit
140 guarantee that readers will always see a repository in a consistent
141 state and no special locking is necessary. As there is generally only
142 one writer to an hg repository, there is in fact no exclusion
143 implemented yet.
144
145
146 Some comparisons to git:
147
148 Most notably, Mercurial uses delta compression and repositories
149 created with it will grow much more slowly over time. This also allows
150 it to be much more bandwidth efficient. I expect repos sizes and sync
151 speeds to be similar to or better than BK, given the use of binary diffs.
152
153 Mercurial is roughly the same performance as git and is faster in
154 others as it keeps around more metadata. One example is listing and
155 retrieving past versions of a file, which it can do without reading
156 all the changesets. This metadata will also allow it to perform better
157 merges as described above.
158
159
@@ -0,0 +1,18
1 #!/usr/bin/env python
2
3 # This is the mercurial setup script.
4 #
5 # './setup.py install', or
6 # './setup.py --help' for more options
7
8 from distutils.core import setup
9
10 setup(name='mercurial',
11 version='0.4c',
12 author='Matt Mackall',
13 author_email='mpm@selenic.com',
14 url='http://selenic.com/mercurial',
15 description='scalable distributed SCM',
16 license='GNU GPL',
17 packages=['mercurial'],
18 scripts=['hg'])
@@ -0,0 +1,2
1 merge $1 $3 $2 || tkdiff -conflict $1 -o $1
2
General Comments 0
You need to be logged in to leave comments. Login now