Show More
@@ -0,0 +1,10 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 |
@@ -0,0 +1,452 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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 b'' | |||
|
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