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']) |
General Comments 0
You need to be logged in to leave comments.
Login now