##// END OF EJS Templates
Add tag support
mpm@selenic.com -
r67:a182f256 default
parent child Browse files
Show More
@@ -1,100 +1,100 b''
1 1 Setting up Mercurial in your home directory:
2 2
3 3 Note: Debian fails to include bits of distutils, you'll need
4 4 python-dev to install. Alternately, shove everything somewhere in
5 5 your path.
6 6
7 7 $ tar xvzf mercurial-<ver>.tar.gz
8 8 $ cd mercurial-<ver>
9 9 $ python2.3 setup.py install --home ~
10 10 $ export PYTHONPATH=${HOME}/lib/python # add this to your .bashrc
11 11 $ export HGMERGE=tkmerge # customize this
12 12 $ hg # test installation, show help
13 13
14 14 If you get complaints about missing modules, you probably haven't set
15 15 PYTHONPATH correctly.
16 16
17 17 Setting up a Mercurial project:
18 18
19 19 $ cd linux/
20 20 $ hg init # creates .hg
21 21 $ hg status # show changes between repo and working dir
22 22 $ hg diff # generate a unidiff
23 23 $ hg export # export a changeset as a diff
24 24 $ hg addremove # add all unknown files and remove all missing files
25 25 $ hg commit # commit all changes, edit changelog entry
26 26
27 27 Mercurial will look for a file named .hgignore in the root of your
28 28 repository contains a set of regular expressions to ignore in file
29 29 paths.
30 30
31 31 Mercurial commands:
32 32
33 33 $ hg history # show changesets
34 34 $ hg log Makefile # show commits per file
35 35 $ hg checkout # check out the tip revision
36 36 $ hg checkout <hash> # check out a specified changeset
37 37 $ hg add foo # add a new file for the next commit
38 38 $ hg remove bar # mark a file as removed
39 39 $ hg verify # check repo integrity
40 $ hg tags # show current tags
40 41
41 42 Branching and merging:
42 43
43 44 $ cd ..
44 45 $ mkdir linux-work
45 46 $ cd linux-work
46 47 $ hg branch ../linux # create a new branch
47 48 $ hg checkout # populate the working directory
48 49 $ <make changes>
49 50 $ hg commit
50 51 $ cd ../linux
51 52 $ hg merge ../linux-work # pull changesets from linux-work
52 53
53 54 Importing patches:
54 55
55 56 Fast:
56 57 $ patch < ../p/foo.patch
57 58 $ hg addremove
58 59 $ hg commit
59 60
60 61 Faster:
61 62 $ patch < ../p/foo.patch
62 63 $ hg commit `lsdiff -p1 ../p/foo.patch`
63 64
64 65 Fastest:
65 66 $ cat ../p/patchlist | xargs hg import -p1 -b ../p
66 67
67 68 Network support:
68 69
69 70 The simple way:
70 71
71 72 # pull the self-hosting hg repo
72 73 foo$ hg init
73 74 foo$ hg merge http://selenic.com/hg/
74 75 foo$ hg checkout # hg co works too
75 76
76 77 # export your .hg directory as a directory on your webserver
77 78 foo$ ln -s .hg ~/public_html/hg-linux
78 79
79 80 # merge changes from a remote machine
80 81 bar$ hg merge http://foo/~user/hg-linux
81 82
82 83 The new, fast, experimental way:
83 84
84 85 # pull the self-hosting hg repo
85 86 foo$ hg init
86 87 foo$ hg merge hg://selenic.com/hg/
87 88 foo$ hg checkout # hg co works too
88 89
89 90 # Set up the CGI server on your webserver
90 91 foo$ ln -s .hg ~/public_html/hg-linux/.hg
91 92 foo$ cp hgweb.py ~/public_html/hg-linux/index.cgi
92 93
93 94 # merge changes from a remote machine
94 95 bar$ hg merge hg://foo/~user/hg-linux
95 96
96
97 97 Another approach which does perform well right now is to use rsync.
98 98 Simply rsync the remote repo to a read-only local copy and then do a
99 99 local pull.
100 100
@@ -1,380 +1,392 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # mercurial - a minimal scalable distributed SCM
4 # v0.4e "sabina"
4 # v0.4f "jane dark"
5 5 #
6 6 # Copyright 2005 Matt Mackall <mpm@selenic.com>
7 7 #
8 8 # This software may be used and distributed according to the terms
9 9 # of the GNU General Public License, incorporated herein by reference.
10 10
11 11 # the psyco compiler makes commits a bit faster
12 12 # and makes changegroup merge about 20 times slower!
13 13 # try:
14 14 # import psyco
15 15 # psyco.full()
16 16 # except:
17 17 # pass
18 18
19 19 import sys, os, time
20 20 from mercurial import hg, mdiff, fancyopts
21 21
22 22 def help():
23 23 print """\
24 24 commands:
25 25
26 26 init create a new repository in this directory
27 27 branch <path> create a branch of <path> in this directory
28 28 merge <path> merge changes from <path> into local repository
29 29 checkout [changeset] checkout the latest or given changeset
30 30 status show new, missing, and changed files in working dir
31 31 add [files...] add the given files in the next commit
32 32 remove [files...] remove the given files in the next commit
33 33 addremove add all new files, delete all missing files
34 34 commit commit all changes to the repository
35 35 history show changeset history
36 36 log <file> show revision history of a single file
37 37 dump <file> [rev] dump the latest or given revision of a file
38 38 dumpmanifest [rev] dump the latest or given revision of the manifest
39 39 diff [files...] diff working directory (or selected files)
40 tags show current changeset tags
40 41 """
41 42
42 43 def filterfiles(list, files):
43 44 l = [ x for x in list if x in files ]
44 45
45 46 for f in files:
46 47 if f[-1] != os.sep: f += os.sep
47 48 l += [ x for x in list if x.startswith(f) ]
48 49 return l
49 50
50 51 def diff(files = None, node1 = None, node2 = None):
51 52 def date(c):
52 53 return time.asctime(time.gmtime(float(c[2].split(' ')[0])))
53 54
54 55 if node2:
55 56 change = repo.changelog.read(node2)
56 57 mmap2 = repo.manifest.read(change[0])
57 58 (c, a, d) = repo.diffrevs(node1, node2)
58 59 def read(f): return repo.file(f).read(mmap2[f])
59 60 date2 = date(change)
60 61 else:
61 62 date2 = time.asctime()
62 63 if not node1:
63 64 node1 = repo.current
64 65 (c, a, d) = repo.diffdir(repo.root, node1)
65 66 def read(f): return file(os.path.join(repo.root, f)).read()
66 67
67 68 change = repo.changelog.read(node1)
68 69 mmap = repo.manifest.read(change[0])
69 70 date1 = date(change)
70 71
71 72 if files:
72 73 (c, a, d) = map(lambda x: filterfiles(x, files), (c, a, d))
73 74
74 75 for f in c:
75 76 to = repo.file(f).read(mmap[f])
76 77 tn = read(f)
77 78 sys.stdout.write(mdiff.unidiff(to, date1, tn, date2, f))
78 79 for f in a:
79 80 to = ""
80 81 tn = read(f)
81 82 sys.stdout.write(mdiff.unidiff(to, date1, tn, date2, f))
82 83 for f in d:
83 84 to = repo.file(f).read(mmap[f])
84 85 tn = ""
85 86 sys.stdout.write(mdiff.unidiff(to, date1, tn, date2, f))
86 87
87 88 options = {}
88 89 opts = [('v', 'verbose', None, 'verbose'),
89 90 ('d', 'debug', None, 'debug')]
90 91
91 92 args = fancyopts.fancyopts(sys.argv[1:], opts, options,
92 93 'hg [options] <command> [command options] [files]')
93 94
94 95 try:
95 96 cmd = args[0]
96 97 args = args[1:]
97 98 except:
98 99 cmd = ""
99 100
100 101 ui = hg.ui(options["verbose"], options["debug"])
101 102
102 103 if cmd == "init":
103 104 repo = hg.repository(ui, ".", create=1)
104 105 sys.exit(0)
105 106 elif cmd == "branch" or cmd == "clone":
106 107 os.system("cp -al %s/.hg .hg" % args[0])
107 108 sys.exit(0)
108 109 elif cmd == "help":
109 110 help()
110 111 sys.exit(0)
111 112 else:
112 113 try:
113 114 repo = hg.repository(ui=ui)
114 115 except:
115 116 print "Unable to open repository"
116 117 sys.exit(0)
117 118
118 119 if cmd == "checkout" or cmd == "co":
119 120 node = repo.changelog.tip()
120 121 if args:
121 node = repo.changelog.lookup(args[0])
122 node = repo.lookup(args[0])
122 123 repo.checkout(node)
123 124
124 125 elif cmd == "add":
125 126 repo.add(args)
126 127
127 128 elif cmd == "remove" or cmd == "rm" or cmd == "del" or cmd == "delete":
128 129 repo.remove(args)
129 130
130 131 elif cmd == "commit" or cmd == "checkin" or cmd == "ci":
131 132 if 1:
132 133 if len(args) > 0:
133 134 repo.commit(repo.current, args)
134 135 else:
135 136 repo.commit(repo.current)
136 137
137 138 elif cmd == "import" or cmd == "patch":
138 139 ioptions = {}
139 140 opts = [('p', 'strip', 1, 'path strip'),
140 141 ('b', 'base', "", 'base path'),
141 142 ('q', 'quiet', "", 'silence diff')
142 143 ]
143 144
144 145 args = fancyopts.fancyopts(args, opts, ioptions,
145 146 'hg import [options] <patch names>')
146 147 d = ioptions["base"]
147 148 strip = ioptions["strip"]
148 149 quiet = ioptions["quiet"] and "> /dev/null" or ""
149 150
150 151 for patch in args:
151 152 ui.status("applying %s\n" % patch)
152 153 pf = os.path.join(d, patch)
153 154
154 155 text = ""
155 156 for l in file(pf):
156 157 if l[:3] == "---": break
157 158 text += l
158 159
159 160 if os.system("patch -p%d < %s %s" % (strip, pf, quiet)):
160 161 raise "patch failed!"
161 162 f = os.popen("lsdiff --strip %d %s" % (strip, pf))
162 163 files = filter(None, map(lambda x: x.rstrip(), f.read().splitlines()))
163 164 f.close()
164 165 repo.commit(repo.current, files, text)
165 166
166 167 elif cmd == "status":
167 168 (c, a, d) = repo.diffdir(repo.root, repo.current)
168 169 for f in c: print "C", f
169 170 for f in a: print "?", f
170 171 for f in d: print "R", f
171 172
172 173 elif cmd == "diff":
173 174 revs = []
174 175
175 176 if args:
176 177 doptions = {}
177 178 opts = [('r', 'revision', [], 'revision')]
178 179 args = fancyopts.fancyopts(args, opts, doptions,
179 180 'hg diff [options] [files]')
180 revs = map(lambda x: repo.changelog.lookup(x), doptions['revision'])
181 revs = map(lambda x: repo.lookup(x), doptions['revision'])
181 182
182 183 if len(revs) > 2:
183 184 print "too many revisions to diff"
184 185 sys.exit(1)
185 186
186 187 if os.getcwd() != repo.root:
187 188 relpath = os.getcwd()[len(repo.root) + 1: ]
188 189 if not args: args = [ relpath ]
189 190 else: args = [ os.path.join(relpath, x) for x in args ]
190 191
191 192 diff(args, *revs)
192 193
193 194 elif cmd == "export":
194 node = repo.changelog.lookup(args[0])
195 node = repo.lookup(args[0])
195 196 prev = repo.changelog.parents(node)[0]
196 197 diff(None, prev, node)
197 198
198 199 elif cmd == "debugchangegroup":
199 newer = repo.newer(map(repo.changelog.lookup, args))
200 newer = repo.newer(map(repo.lookup, args))
200 201 for chunk in repo.changegroup(newer):
201 202 sys.stdout.write(chunk)
202 203
203 204 elif cmd == "debugaddchangegroup":
204 205 data = sys.stdin.read()
205 206 repo.addchangegroup(data)
206 207
207 208 elif cmd == "addremove":
208 209 (c, a, d) = repo.diffdir(repo.root, repo.current)
209 210 repo.add(a)
210 211 repo.remove(d)
211 212
212 213 elif cmd == "history":
213 214 for i in range(repo.changelog.count()):
214 215 n = repo.changelog.node(i)
215 216 changes = repo.changelog.read(n)
216 217 (p1, p2) = repo.changelog.parents(n)
217 218 (h, h1, h2) = map(hg.hex, (n, p1, p2))
218 219 (i1, i2) = map(repo.changelog.rev, (p1, p2))
219 220 print "rev: %4d:%s" % (i, h)
220 221 print "parents: %4d:%s" % (i1, h1)
221 222 if i2: print " %4d:%s" % (i2, h2)
222 223 print "manifest: %4d:%s" % (repo.manifest.rev(changes[0]),
223 224 hg.hex(changes[0]))
224 225 print "user:", changes[1]
225 226 print "date:", time.asctime(
226 227 time.localtime(float(changes[2].split(' ')[0])))
227 228 print "files:", " ".join(changes[3])
228 229 print "description:"
229 230 print changes[4]
230 231
231 232 elif cmd == "log":
232 233 if args:
233 234 r = repo.file(args[0])
234 235 for i in range(r.count()):
235 236 n = r.node(i)
236 237 (p1, p2) = r.parents(n)
237 238 (h, h1, h2) = map(hg.hex, (n, p1, p2))
238 239 (i1, i2) = map(r.rev, (p1, p2))
239 240 cr = r.linkrev(n)
240 241 cn = hg.hex(repo.changelog.node(cr))
241 242 print "rev: %4d:%s" % (i, h)
242 243 print "changeset: %4d:%s" % (cr, cn)
243 244 print "parents: %4d:%s" % (i1, h1)
244 245 if i2: print " %4d:%s" % (i2, h2)
245 246 else:
246 247 print "missing filename"
247 248
248 249 elif cmd == "dump":
249 250 if args:
250 251 r = repo.file(args[0])
251 252 n = r.tip()
252 253 if len(args) > 1: n = r.lookup(args[1])
253 254 sys.stdout.write(r.read(n))
254 255 else:
255 256 print "missing filename"
256 257
257 258 elif cmd == "dumpmanifest":
258 259 n = repo.manifest.tip()
259 260 if len(args) > 0:
260 261 n = repo.manifest.lookup(args[0])
261 262 m = repo.manifest.read(n)
262 263 files = m.keys()
263 264 files.sort()
264 265
265 266 for f in files:
266 267 print hg.hex(m[f]), f
267 268
268 269 elif cmd == "debughash":
269 270 f = repo.file(args[0])
270 271 print f.encodepath(args[0])
271 272
272 273 elif cmd == "debugindex":
273 274 r = hg.revlog(open, args[0], "")
274 275 print " rev offset length base linkrev"+\
275 276 " p1 p2 nodeid"
276 277 for i in range(r.count()):
277 278 e = r.index[i]
278 279 print "% 6d % 9d % 7d % 5d % 7d %s.. %s.. %s.." % (
279 280 i, e[0], e[1], e[2], e[3],
280 281 hg.hex(e[4][:5]), hg.hex(e[5][:5]), hg.hex(e[6][:5]))
281 282
282 283 elif cmd == "merge":
283 284 if args:
284 285 other = hg.repository(ui, args[0])
285 286 print "requesting changegroup"
286 287 cg = repo.getchangegroup(other)
287 288 repo.addchangegroup(cg)
288 289 else:
289 290 print "missing source repository"
290 291
292 elif cmd == "tags":
293 repo.lookup(0) # prime the cache
294 i = repo.tags.items()
295 i.sort()
296 for k, n in i:
297 try:
298 r = repo.changelog.rev(n)
299 except KeyError:
300 r = "?"
301 print "%-30s %5d:%s" % (k, repo.changelog.rev(n), hg.hex(n))
302
291 303 elif cmd == "debugoldmerge":
292 304 if args:
293 305 other = hg.repository(ui, args[0])
294 306 repo.merge(other)
295 307 else:
296 308 print "missing source repository"
297 309
298 310 elif cmd == "verify":
299 311 filelinkrevs = {}
300 312 filenodes = {}
301 313 manifestchangeset = {}
302 314 changesets = revisions = files = 0
303 315
304 316 print "checking changesets"
305 317 for i in range(repo.changelog.count()):
306 318 changesets += 1
307 319 n = repo.changelog.node(i)
308 320 changes = repo.changelog.read(n)
309 321 manifestchangeset[changes[0]] = n
310 322 for f in changes[3]:
311 323 revisions += 1
312 324 filelinkrevs.setdefault(f, []).append(i)
313 325
314 326 print "checking manifests"
315 327 for i in range(repo.manifest.count()):
316 328 n = repo.manifest.node(i)
317 329 ca = repo.changelog.node(repo.manifest.linkrev(n))
318 330 cc = manifestchangeset[n]
319 331 if ca != cc:
320 332 print "manifest %s points to %s, not %s" % \
321 333 (hg.hex(n), hg.hex(ca), hg.hex(cc))
322 334 m = repo.manifest.read(n)
323 335 for f, fn in m.items():
324 336 filenodes.setdefault(f, {})[fn] = 1
325 337
326 338 print "crosschecking files in changesets and manifests"
327 339 for f in filenodes:
328 340 if f not in filelinkrevs:
329 341 print "file %s in manifest but not in changesets"
330 342
331 343 for f in filelinkrevs:
332 344 if f not in filenodes:
333 345 print "file %s in changeset but not in manifest"
334 346
335 347 print "checking files"
336 348 for f in filenodes:
337 349 files += 1
338 350 fl = repo.file(f)
339 351 nodes = {"\0"*20: 1}
340 352 for i in range(fl.count()):
341 353 n = fl.node(i)
342 354
343 355 if n not in filenodes[f]:
344 356 print "%s:%s not in manifests" % (f, hg.hex(n))
345 357 else:
346 358 del filenodes[f][n]
347 359
348 360 flr = fl.linkrev(n)
349 361 if flr not in filelinkrevs[f]:
350 362 print "%s:%s points to unexpected changeset rev %d" \
351 363 % (f, hg.hex(n), fl.linkrev(n))
352 364 else:
353 365 filelinkrevs[f].remove(flr)
354 366
355 367 # verify contents
356 368 t = fl.read(n)
357 369
358 370 # verify parents
359 371 (p1, p2) = fl.parents(n)
360 372 if p1 not in nodes:
361 373 print "%s:%s unknown parent 1 %s" % (f, hg.hex(n), hg.hex(p1))
362 374 if p2 not in nodes:
363 375 print "file %s:%s unknown parent %s" % (f, hg.hex(n), hg.hex(p1))
364 376 nodes[n] = 1
365 377
366 378 # cross-check
367 379 for flr in filelinkrevs[f]:
368 380 print "changeset rev %d not in %s" % (flr, f)
369 381
370 382 for node in filenodes[f]:
371 383 print "node %s in manifests not in %s" % (hg.hex(n), f)
372 384
373 385
374 386 print "%d files, %d changesets, %d total revisions" % (files, changesets,
375 387 revisions)
376 388
377 389 else:
378 390 print "unknown command\n"
379 391 help()
380 392 sys.exit(1)
@@ -1,893 +1,909 b''
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 import sys, struct, sha, socket, os, time, base64, re, urllib2
9 9 import urllib
10 10 from mercurial import byterange
11 11 from mercurial.transaction import *
12 12 from mercurial.revlog import *
13 13
14 14 class filelog(revlog):
15 15 def __init__(self, opener, path):
16 16 s = self.encodepath(path)
17 17 revlog.__init__(self, opener, os.path.join("data", s + "i"),
18 18 os.path.join("data", s))
19 19
20 20 def encodepath(self, path):
21 21 s = sha.sha(path).digest()
22 22 s = base64.encodestring(s)[:-3]
23 23 s = re.sub("\+", "%", s)
24 24 s = re.sub("/", "_", s)
25 25 return s
26 26
27 27 def read(self, node):
28 28 return self.revision(node)
29 29 def add(self, text, transaction, link, p1=None, p2=None):
30 30 return self.addrevision(text, transaction, link, p1, p2)
31 31
32 32 def resolvedag(self, old, new, transaction, link):
33 33 """resolve unmerged heads in our DAG"""
34 34 if old == new: return None
35 35 a = self.ancestor(old, new)
36 36 if old == a: return None
37 37 return self.merge3(old, new, a, transaction, link)
38 38
39 39 def merge3(self, my, other, base, transaction, link):
40 40 """perform a 3-way merge and append the result"""
41 41 def temp(prefix, node):
42 42 (fd, name) = tempfile.mkstemp(prefix)
43 43 f = os.fdopen(fd, "w")
44 44 f.write(self.revision(node))
45 45 f.close()
46 46 return name
47 47
48 48 a = temp("local", my)
49 49 b = temp("remote", other)
50 50 c = temp("parent", base)
51 51
52 52 cmd = os.environ["HGMERGE"]
53 53 r = os.system("%s %s %s %s" % (cmd, a, b, c))
54 54 if r:
55 55 raise "Merge failed, implement rollback!"
56 56
57 57 t = open(a).read()
58 58 os.unlink(a)
59 59 os.unlink(b)
60 60 os.unlink(c)
61 61 return self.addrevision(t, transaction, link, my, other)
62 62
63 63 def merge(self, other, transaction, linkseq, link):
64 64 """perform a merge and resolve resulting heads"""
65 65 (o, n) = self.mergedag(other, transaction, linkseq)
66 66 return self.resolvedag(o, n, transaction, link)
67 67
68 68 class manifest(revlog):
69 69 def __init__(self, opener):
70 70 self.mapcache = None
71 71 self.listcache = None
72 72 self.addlist = None
73 73 revlog.__init__(self, opener, "00manifest.i", "00manifest.d")
74 74
75 75 def read(self, node):
76 76 if self.mapcache and self.mapcache[0] == node:
77 77 return self.mapcache[1]
78 78 text = self.revision(node)
79 79 map = {}
80 80 self.listcache = (text, text.splitlines(1))
81 81 for l in self.listcache[1]:
82 82 (f, n) = l.split('\0')
83 83 map[f] = bin(n[:40])
84 84 self.mapcache = (node, map)
85 85 return map
86 86
87 87 def diff(self, a, b):
88 88 # this is sneaky, as we're not actually using a and b
89 89 if self.listcache and len(self.listcache[0]) == len(a):
90 90 return mdiff.diff(self.listcache[1], self.addlist, 1)
91 91 else:
92 92 return mdiff.textdiff(a, b)
93 93
94 94 def add(self, map, transaction, link, p1=None, p2=None):
95 95 files = map.keys()
96 96 files.sort()
97 97
98 98 self.addlist = ["%s\000%s\n" % (f, hex(map[f])) for f in files]
99 99 text = "".join(self.addlist)
100 100
101 101 n = self.addrevision(text, transaction, link, p1, p2)
102 102 self.mapcache = (n, map)
103 103 self.listcache = (text, self.addlist)
104 104
105 105 return n
106 106
107 107 class changelog(revlog):
108 108 def __init__(self, opener):
109 109 revlog.__init__(self, opener, "00changelog.i", "00changelog.d")
110 110
111 111 def extract(self, text):
112 112 if not text:
113 113 return (nullid, "", "0", [], "")
114 114 last = text.index("\n\n")
115 115 desc = text[last + 2:]
116 116 l = text[:last].splitlines()
117 117 manifest = bin(l[0])
118 118 user = l[1]
119 119 date = l[2]
120 120 files = l[3:]
121 121 return (manifest, user, date, files, desc)
122 122
123 123 def read(self, node):
124 124 return self.extract(self.revision(node))
125 125
126 126 def add(self, manifest, list, desc, transaction, p1=None, p2=None):
127 127 user = (os.environ.get("HGUSER") or
128 128 os.environ.get("EMAIL") or
129 129 os.environ.get("LOGNAME", "unknown") + '@' + socket.getfqdn())
130 130 date = "%d %d" % (time.time(), time.timezone)
131 131 list.sort()
132 132 l = [hex(manifest), user, date] + list + ["", desc]
133 133 text = "\n".join(l)
134 134 return self.addrevision(text, transaction, self.count(), p1, p2)
135 135
136 136 def merge3(self, my, other, base):
137 137 pass
138 138
139 139 class dircache:
140 140 def __init__(self, opener, ui):
141 141 self.opener = opener
142 142 self.dirty = 0
143 143 self.ui = ui
144 144 self.map = None
145 145 def __del__(self):
146 146 if self.dirty: self.write()
147 147 def __getitem__(self, key):
148 148 try:
149 149 return self.map[key]
150 150 except TypeError:
151 151 self.read()
152 152 return self[key]
153 153
154 154 def read(self):
155 155 if self.map is not None: return self.map
156 156
157 157 self.map = {}
158 158 try:
159 159 st = self.opener("dircache").read()
160 160 except: return
161 161
162 162 pos = 0
163 163 while pos < len(st):
164 164 e = struct.unpack(">llll", st[pos:pos+16])
165 165 l = e[3]
166 166 pos += 16
167 167 f = st[pos:pos + l]
168 168 self.map[f] = e[:3]
169 169 pos += l
170 170
171 171 def update(self, files):
172 172 if not files: return
173 173 self.read()
174 174 self.dirty = 1
175 175 for f in files:
176 176 try:
177 177 s = os.stat(f)
178 178 self.map[f] = (s.st_mode, s.st_size, s.st_mtime)
179 179 except IOError:
180 180 self.remove(f)
181 181
182 182 def taint(self, files):
183 183 if not files: return
184 184 self.read()
185 185 self.dirty = 1
186 186 for f in files:
187 187 self.map[f] = (0, -1, 0)
188 188
189 189 def remove(self, files):
190 190 if not files: return
191 191 self.read()
192 192 self.dirty = 1
193 193 for f in files:
194 194 try:
195 195 del self.map[f]
196 196 except KeyError:
197 197 self.ui.warn("Not in dircache: %s\n" % f)
198 198 pass
199 199
200 200 def clear(self):
201 201 self.map = {}
202 202 self.dirty = 1
203 203
204 204 def write(self):
205 205 st = self.opener("dircache", "w")
206 206 for f, e in self.map.items():
207 207 e = struct.pack(">llll", e[0], e[1], e[2], len(f))
208 208 st.write(e + f)
209 209 self.dirty = 0
210 210
211 211 def copy(self):
212 212 self.read()
213 213 return self.map.copy()
214 214
215 215 # used to avoid circular references so destructors work
216 216 def opener(base):
217 217 p = base
218 218 def o(path, mode="r"):
219 219 if p[:7] == "http://":
220 220 f = os.path.join(p, urllib.quote(path))
221 221 return httprangereader(f)
222 222
223 223 f = os.path.join(p, path)
224 224
225 225 if mode != "r" and os.path.isfile(f):
226 226 s = os.stat(f)
227 227 if s.st_nlink > 1:
228 228 file(f + ".tmp", "w").write(file(f).read())
229 229 os.rename(f+".tmp", f)
230 230
231 231 return file(f, mode)
232 232
233 233 return o
234 234
235 235 class localrepository:
236 236 def __init__(self, ui, path=None, create=0):
237 237 self.remote = 0
238 238 if path and path[:7] == "http://":
239 239 self.remote = 1
240 240 self.path = path
241 241 else:
242 242 if not path:
243 243 p = os.getcwd()
244 244 while not os.path.isdir(os.path.join(p, ".hg")):
245 245 p = os.path.dirname(p)
246 246 if p == "/": raise "No repo found"
247 247 path = p
248 248 self.path = os.path.join(path, ".hg")
249 249
250 250 self.root = path
251 251 self.ui = ui
252 252
253 253 if create:
254 254 os.mkdir(self.path)
255 255 os.mkdir(self.join("data"))
256 256
257 257 self.opener = opener(self.path)
258 258 self.manifest = manifest(self.opener)
259 259 self.changelog = changelog(self.opener)
260 260 self.ignorelist = None
261 self.tags = None
261 262
262 263 if not self.remote:
263 264 self.dircache = dircache(self.opener, ui)
264 265 try:
265 266 self.current = bin(self.opener("current").read())
266 267 except IOError:
267 268 self.current = None
268 269
269 270 def setcurrent(self, node):
270 271 self.current = node
271 272 self.opener("current", "w").write(hex(node))
272 273
273 274 def ignore(self, f):
274 275 if self.ignorelist is None:
275 276 self.ignorelist = []
276 277 try:
277 l = open(os.path.join(self.root, ".hgignore")).readlines()
278 l = open(os.path.join(self.root, ".hgignore"))
278 279 for pat in l:
279 280 if pat != "\n":
280 281 self.ignorelist.append(re.compile(pat[:-1]))
281 282 except IOError: pass
282 283 for pat in self.ignorelist:
283 284 if pat.search(f): return True
284 285 return False
285 286
287 def lookup(self, key):
288 if self.tags is None:
289 self.tags = {}
290 try:
291 fl = self.file(".hgtags")
292 for l in fl.revision(fl.tip()).splitlines():
293 if l:
294 n, k = l.split(" ")
295 self.tags[k] = bin(n)
296 except KeyError: pass
297 try:
298 return self.tags[key]
299 except KeyError:
300 return self.changelog.lookup(key)
301
286 302 def join(self, f):
287 303 return os.path.join(self.path, f)
288 304
289 305 def file(self, f):
290 306 return filelog(self.opener, f)
291 307
292 308 def transaction(self):
293 309 return transaction(self.opener, self.join("journal"))
294 310
295 311 def merge(self, other):
296 312 tr = self.transaction()
297 313 changed = {}
298 314 new = {}
299 315 seqrev = self.changelog.count()
300 316 # some magic to allow fiddling in nested scope
301 317 nextrev = [seqrev]
302 318
303 319 # helpers for back-linking file revisions to local changeset
304 320 # revisions so we can immediately get to changeset from annotate
305 321 def accumulate(text):
306 322 # track which files are added in which changeset and the
307 323 # corresponding _local_ changeset revision
308 324 files = self.changelog.extract(text)[3]
309 325 for f in files:
310 326 changed.setdefault(f, []).append(nextrev[0])
311 327 nextrev[0] += 1
312 328
313 329 def seq(start):
314 330 while 1:
315 331 yield start
316 332 start += 1
317 333
318 334 def lseq(l):
319 335 for r in l:
320 336 yield r
321 337
322 338 # begin the import/merge of changesets
323 339 self.ui.status("merging new changesets\n")
324 340 (co, cn) = self.changelog.mergedag(other.changelog, tr,
325 341 seq(seqrev), accumulate)
326 342 resolverev = self.changelog.count()
327 343
328 344 # is there anything to do?
329 345 if co == cn:
330 346 tr.close()
331 347 return
332 348
333 349 # do we need to resolve?
334 350 simple = (co == self.changelog.ancestor(co, cn))
335 351
336 352 # merge all files changed by the changesets,
337 353 # keeping track of the new tips
338 354 changelist = changed.keys()
339 355 changelist.sort()
340 356 for f in changelist:
341 357 sys.stdout.write(".")
342 358 sys.stdout.flush()
343 359 r = self.file(f)
344 360 node = r.merge(other.file(f), tr, lseq(changed[f]), resolverev)
345 361 if node:
346 362 new[f] = node
347 363 sys.stdout.write("\n")
348 364
349 365 # begin the merge of the manifest
350 366 self.ui.status("merging manifests\n")
351 367 (mm, mo) = self.manifest.mergedag(other.manifest, tr, seq(seqrev))
352 368
353 369 # For simple merges, we don't need to resolve manifests or changesets
354 370 if simple:
355 371 tr.close()
356 372 return
357 373
358 374 ma = self.manifest.ancestor(mm, mo)
359 375
360 376 # resolve the manifest to point to all the merged files
361 377 self.ui.status("resolving manifests\n")
362 378 mmap = self.manifest.read(mm) # mine
363 379 omap = self.manifest.read(mo) # other
364 380 amap = self.manifest.read(ma) # ancestor
365 381 nmap = {}
366 382
367 383 for f, mid in mmap.iteritems():
368 384 if f in omap:
369 385 if mid != omap[f]:
370 386 nmap[f] = new.get(f, mid) # use merged version
371 387 else:
372 388 nmap[f] = new.get(f, mid) # they're the same
373 389 del omap[f]
374 390 elif f in amap:
375 391 if mid != amap[f]:
376 392 pass # we should prompt here
377 393 else:
378 394 pass # other deleted it
379 395 else:
380 396 nmap[f] = new.get(f, mid) # we created it
381 397
382 398 del mmap
383 399
384 400 for f, oid in omap.iteritems():
385 401 if f in amap:
386 402 if oid != amap[f]:
387 403 pass # this is the nasty case, we should prompt
388 404 else:
389 405 pass # probably safe
390 406 else:
391 407 nmap[f] = new.get(f, oid) # remote created it
392 408
393 409 del omap
394 410 del amap
395 411
396 412 node = self.manifest.add(nmap, tr, resolverev, mm, mo)
397 413
398 414 # Now all files and manifests are merged, we add the changed files
399 415 # and manifest id to the changelog
400 416 self.ui.status("committing merge changeset\n")
401 417 new = new.keys()
402 418 new.sort()
403 419 if co == cn: cn = -1
404 420
405 421 edittext = "\n"+"".join(["HG: changed %s\n" % f for f in new])
406 422 edittext = self.ui.edit(edittext)
407 423 n = self.changelog.add(node, new, edittext, tr, co, cn)
408 424
409 425 tr.close()
410 426
411 427 def commit(self, parent, update = None, text = ""):
412 428 tr = self.transaction()
413 429
414 430 try:
415 431 remove = [ l[:-1] for l in self.opener("to-remove") ]
416 432 os.unlink(self.join("to-remove"))
417 433
418 434 except IOError:
419 435 remove = []
420 436
421 437 if update == None:
422 438 update = self.diffdir(self.root, parent)[0]
423 439
424 440 # check in files
425 441 new = {}
426 442 linkrev = self.changelog.count()
427 443 for f in update:
428 444 try:
429 445 t = file(f).read()
430 446 except IOError:
431 447 remove.append(f)
432 448 continue
433 449 r = self.file(f)
434 450 new[f] = r.add(t, tr, linkrev)
435 451
436 452 # update manifest
437 453 mmap = self.manifest.read(self.manifest.tip())
438 454 mmap.update(new)
439 455 for f in remove:
440 456 del mmap[f]
441 457 mnode = self.manifest.add(mmap, tr, linkrev)
442 458
443 459 # add changeset
444 460 new = new.keys()
445 461 new.sort()
446 462
447 463 edittext = text + "\n"+"".join(["HG: changed %s\n" % f for f in new])
448 464 edittext += "".join(["HG: removed %s\n" % f for f in remove])
449 465 edittext = self.ui.edit(edittext)
450 466
451 467 n = self.changelog.add(mnode, new, edittext, tr)
452 468 tr.close()
453 469
454 470 self.setcurrent(n)
455 471 self.dircache.update(new)
456 472 self.dircache.remove(remove)
457 473
458 474 def checkdir(self, path):
459 475 d = os.path.dirname(path)
460 476 if not d: return
461 477 if not os.path.isdir(d):
462 478 self.checkdir(d)
463 479 os.mkdir(d)
464 480
465 481 def checkout(self, node):
466 482 # checkout is really dumb at the moment
467 483 # it ought to basically merge
468 484 change = self.changelog.read(node)
469 485 mmap = self.manifest.read(change[0])
470 486
471 487 l = mmap.keys()
472 488 l.sort()
473 489 stats = []
474 490 for f in l:
475 491 r = self.file(f)
476 492 t = r.revision(mmap[f])
477 493 try:
478 494 file(f, "w").write(t)
479 495 except:
480 496 self.checkdir(f)
481 497 file(f, "w").write(t)
482 498
483 499 self.setcurrent(node)
484 500 self.dircache.clear()
485 501 self.dircache.update(l)
486 502
487 503 def diffdir(self, path, changeset):
488 504 changed = []
489 505 mf = {}
490 506 added = []
491 507
492 508 if changeset:
493 509 change = self.changelog.read(changeset)
494 510 mf = self.manifest.read(change[0])
495 511
496 512 if changeset == self.current:
497 513 dc = self.dircache.copy()
498 514 else:
499 515 dc = dict.fromkeys(mf)
500 516
501 517 def fcmp(fn):
502 518 t1 = file(os.path.join(self.root, fn)).read()
503 519 t2 = self.file(fn).revision(mf[fn])
504 520 return cmp(t1, t2)
505 521
506 522 for dir, subdirs, files in os.walk(self.root):
507 523 d = dir[len(self.root)+1:]
508 524 if ".hg" in subdirs: subdirs.remove(".hg")
509 525
510 526 for f in files:
511 527 fn = os.path.join(d, f)
512 528 try: s = os.stat(os.path.join(self.root, fn))
513 529 except: continue
514 530 if fn in dc:
515 531 c = dc[fn]
516 532 del dc[fn]
517 533 if not c:
518 534 if fcmp(fn):
519 535 changed.append(fn)
520 536 elif c[1] != s.st_size:
521 537 changed.append(fn)
522 538 elif c[0] != s.st_mode or c[2] != s.st_mtime:
523 539 if fcmp(fn):
524 540 changed.append(fn)
525 541 else:
526 542 if self.ignore(fn): continue
527 543 added.append(fn)
528 544
529 545 deleted = dc.keys()
530 546 deleted.sort()
531 547
532 548 return (changed, added, deleted)
533 549
534 550 def diffrevs(self, node1, node2):
535 551 changed, added = [], []
536 552
537 553 change = self.changelog.read(node1)
538 554 mf1 = self.manifest.read(change[0])
539 555 change = self.changelog.read(node2)
540 556 mf2 = self.manifest.read(change[0])
541 557
542 558 for fn in mf2:
543 559 if mf1.has_key(fn):
544 560 if mf1[fn] != mf2[fn]:
545 561 changed.append(fn)
546 562 del mf1[fn]
547 563 else:
548 564 added.append(fn)
549 565
550 566 deleted = mf1.keys()
551 567 deleted.sort()
552 568
553 569 return (changed, added, deleted)
554 570
555 571 def add(self, list):
556 572 self.dircache.taint(list)
557 573
558 574 def remove(self, list):
559 575 dl = self.opener("to-remove", "a")
560 576 for f in list:
561 577 dl.write(f + "\n")
562 578
563 579 def branches(self, nodes):
564 580 if not nodes: nodes = [self.changelog.tip()]
565 581 b = []
566 582 for n in nodes:
567 583 t = n
568 584 while n:
569 585 p = self.changelog.parents(n)
570 586 if p[1] != nullid or p[0] == nullid:
571 587 b.append((t, n, p[0], p[1]))
572 588 break
573 589 n = p[0]
574 590 return b
575 591
576 592 def between(self, pairs):
577 593 r = []
578 594
579 595 for top, bottom in pairs:
580 596 n, l, i = top, [], 0
581 597 f = 1
582 598
583 599 while n != bottom:
584 600 p = self.changelog.parents(n)[0]
585 601 if i == f:
586 602 l.append(n)
587 603 f = f * 2
588 604 n = p
589 605 i += 1
590 606
591 607 r.append(l)
592 608
593 609 return r
594 610
595 611 def newer(self, nodes):
596 612 m = {}
597 613 nl = []
598 614 cl = self.changelog
599 615 t = l = cl.count()
600 616 for n in nodes:
601 617 l = min(l, cl.rev(n))
602 618 for p in cl.parents(n):
603 619 m[p] = 1
604 620
605 621 for i in xrange(l, t):
606 622 n = cl.node(i)
607 623 for p in cl.parents(n):
608 624 if p in m and n not in m:
609 625 m[n] = 1
610 626 nl.append(n)
611 627
612 628 return nl
613 629
614 630 def getchangegroup(self, remote):
615 631 tip = remote.branches([])[0]
616 632 m = self.changelog.nodemap
617 633 unknown = [tip]
618 634 search = []
619 635 fetch = []
620 636
621 637 if tip[0] in m:
622 638 return None
623 639
624 640 while unknown:
625 641 n = unknown.pop(0)
626 642 if n == nullid: break
627 643 if n[1] and n[1] in m: # do we know the base?
628 644 search.append(n) # schedule branch range for scanning
629 645 else:
630 646 for b in remote.branches([n[2], n[3]]):
631 647 if b[0] in m:
632 648 if n[1] not in fetch:
633 649 fetch.append(n[1]) # earliest unknown
634 650 else:
635 651 unknown.append(b)
636 652
637 653 while search:
638 654 n = search.pop(0)
639 655 l = remote.between([(n[0], n[1])])[0]
640 656 p = n[0]
641 657 f = 1
642 658 for i in l + [n[1]]:
643 659 if i in m:
644 660 if f <= 4:
645 661 fetch.append(p)
646 662 else:
647 663 search.append((p, i))
648 664 break
649 665 p, f = i, f * 2
650 666
651 667 for f in fetch:
652 668 if f in m:
653 669 raise "already have", hex(f[:4])
654 670
655 671 return remote.changegroup(fetch)
656 672
657 673 def changegroup(self, basenodes):
658 674 nodes = self.newer(basenodes)
659 675
660 676 # construct the link map
661 677 linkmap = {}
662 678 for n in nodes:
663 679 linkmap[self.changelog.rev(n)] = n
664 680
665 681 # construct a list of all changed files
666 682 changed = {}
667 683 for n in nodes:
668 684 c = self.changelog.read(n)
669 685 for f in c[3]:
670 686 changed[f] = 1
671 687 changed = changed.keys()
672 688 changed.sort()
673 689
674 690 # the changegroup is changesets + manifests + all file revs
675 691 revs = [ self.changelog.rev(n) for n in nodes ]
676 692
677 693 yield self.changelog.group(linkmap)
678 694 yield self.manifest.group(linkmap)
679 695
680 696 for f in changed:
681 697 g = self.file(f).group(linkmap)
682 698 if not g: raise "couldn't find change to %s" % f
683 699 l = struct.pack(">l", len(f))
684 700 yield "".join([l, f, g])
685 701
686 702 def addchangegroup(self, generator):
687 703 class genread:
688 704 def __init__(self, generator):
689 705 self.g = generator
690 706 self.buf = ""
691 707 def read(self, l):
692 708 while l > len(self.buf):
693 709 try:
694 710 self.buf += self.g.next()
695 711 except StopIteration:
696 712 break
697 713 d, self.buf = self.buf[:l], self.buf[l:]
698 714 return d
699 715
700 716 if not generator: return
701 717 source = genread(generator)
702 718
703 719 def getchunk(add = 0):
704 720 d = source.read(4)
705 721 if not d: return ""
706 722 l = struct.unpack(">l", d)[0]
707 723 return source.read(l - 4 + add)
708 724
709 725 tr = self.transaction()
710 726 simple = True
711 727
712 728 print "merging changesets"
713 729 # pull off the changeset group
714 730 csg = getchunk()
715 731 co = self.changelog.tip()
716 732 cn = self.changelog.addgroup(csg, lambda x: self.changelog.count(), tr)
717 733
718 734 print "merging manifests"
719 735 # pull off the manifest group
720 736 mfg = getchunk()
721 737 mo = self.manifest.tip()
722 738 mm = self.manifest.addgroup(mfg, lambda x: self.changelog.rev(x), tr)
723 739
724 740 # do we need a resolve?
725 741 if self.changelog.ancestor(co, cn) != co:
726 742 simple = False
727 743 resolverev = self.changelog.count()
728 744
729 745 # process the files
730 746 print "merging files"
731 747 new = {}
732 748 while 1:
733 749 f = getchunk(4)
734 750 if not f: break
735 751 fg = getchunk()
736 752
737 753 fl = self.file(f)
738 754 o = fl.tip()
739 755 n = fl.addgroup(fg, lambda x: self.changelog.rev(x), tr)
740 756 if not simple:
741 757 nn = fl.resolvedag(o, n, tr, resolverev)
742 758 if nn: new[f] = nn
743 759
744 760 # For simple merges, we don't need to resolve manifests or changesets
745 761 if simple:
746 762 tr.close()
747 763 return
748 764
749 765 # resolve the manifest to point to all the merged files
750 766 self.ui.status("resolving manifests\n")
751 767 ma = self.manifest.ancestor(mm, mo)
752 768 mmap = self.manifest.read(mm) # mine
753 769 omap = self.manifest.read(mo) # other
754 770 amap = self.manifest.read(ma) # ancestor
755 771 nmap = {}
756 772
757 773 for f, mid in mmap.iteritems():
758 774 if f in omap:
759 775 if mid != omap[f]:
760 776 nmap[f] = new.get(f, mid) # use merged version
761 777 else:
762 778 nmap[f] = new.get(f, mid) # they're the same
763 779 del omap[f]
764 780 elif f in amap:
765 781 if mid != amap[f]:
766 782 pass # we should prompt here
767 783 else:
768 784 pass # other deleted it
769 785 else:
770 786 nmap[f] = new.get(f, mid) # we created it
771 787
772 788 del mmap
773 789
774 790 for f, oid in omap.iteritems():
775 791 if f in amap:
776 792 if oid != amap[f]:
777 793 pass # this is the nasty case, we should prompt
778 794 else:
779 795 pass # probably safe
780 796 else:
781 797 nmap[f] = new.get(f, oid) # remote created it
782 798
783 799 del omap
784 800 del amap
785 801
786 802 node = self.manifest.add(nmap, tr, resolverev, mm, mo)
787 803
788 804 # Now all files and manifests are merged, we add the changed files
789 805 # and manifest id to the changelog
790 806 self.ui.status("committing merge changeset\n")
791 807 new = new.keys()
792 808 new.sort()
793 809 if co == cn: cn = -1
794 810
795 811 edittext = "\n"+"".join(["HG: changed %s\n" % f for f in new])
796 812 edittext = self.ui.edit(edittext)
797 813 n = self.changelog.add(node, new, edittext, tr, co, cn)
798 814
799 815 tr.close()
800 816
801 817 class remoterepository:
802 818 def __init__(self, ui, path):
803 819 self.url = path.replace("hg://", "http://", 1)
804 820 self.ui = ui
805 821
806 822 def do_cmd(self, cmd, **args):
807 823 q = {"cmd": cmd}
808 824 q.update(args)
809 825 qs = urllib.urlencode(q)
810 826 cu = "%s?%s" % (self.url, qs)
811 827 return urllib.urlopen(cu)
812 828
813 829 def branches(self, nodes):
814 830 n = " ".join(map(hex, nodes))
815 831 d = self.do_cmd("branches", nodes=n).read()
816 832 br = [ map(bin, b.split(" ")) for b in d.splitlines() ]
817 833 return br
818 834
819 835 def between(self, pairs):
820 836 n = "\n".join(["-".join(map(hex, p)) for p in pairs])
821 837 d = self.do_cmd("between", pairs=n).read()
822 838 p = [ map(bin, l.split(" ")) for l in d.splitlines() ]
823 839 return p
824 840
825 841 def changegroup(self, nodes):
826 842 n = " ".join(map(hex, nodes))
827 843 zd = zlib.decompressobj()
828 844 f = self.do_cmd("changegroup", roots=n)
829 845 while 1:
830 846 d = f.read(4096)
831 847 if not d:
832 848 yield zd.flush()
833 849 break
834 850 yield zd.decompress(d)
835 851
836 852 def repository(ui, path=None, create=0):
837 853 if path and path[:5] == "hg://":
838 854 return remoterepository(ui, path)
839 855 else:
840 856 return localrepository(ui, path, create)
841 857
842 858 class ui:
843 859 def __init__(self, verbose=False, debug=False):
844 860 self.verbose = verbose
845 861 def write(self, *args):
846 862 for a in args:
847 863 sys.stdout.write(str(a))
848 864 def prompt(self, msg, pat):
849 865 while 1:
850 866 sys.stdout.write(msg)
851 867 r = sys.stdin.readline()[:-1]
852 868 if re.match(pat, r):
853 869 return r
854 870 def status(self, *msg):
855 871 self.write(*msg)
856 872 def warn(self, msg):
857 873 self.write(*msg)
858 874 def note(self, msg):
859 875 if self.verbose: self.write(*msg)
860 876 def debug(self, msg):
861 877 if self.debug: self.write(*msg)
862 878 def edit(self, text):
863 879 (fd, name) = tempfile.mkstemp("hg")
864 880 f = os.fdopen(fd, "w")
865 881 f.write(text)
866 882 f.close()
867 883
868 884 editor = os.environ.get("EDITOR", "vi")
869 885 r = os.system("%s %s" % (editor, name))
870 886 if r:
871 887 raise "Edit failed!"
872 888
873 889 t = open(name).read()
874 890 t = re.sub("(?m)^HG:.*\n", "", t)
875 891
876 892 return t
877 893
878 894
879 895 class httprangereader:
880 896 def __init__(self, url):
881 897 self.url = url
882 898 self.pos = 0
883 899 def seek(self, pos):
884 900 self.pos = pos
885 901 def read(self, bytes=None):
886 902 opener = urllib2.build_opener(byterange.HTTPRangeHandler())
887 903 urllib2.install_opener(opener)
888 904 req = urllib2.Request(self.url)
889 905 end = ''
890 906 if bytes: end = self.pos + bytes
891 907 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
892 908 f = urllib2.urlopen(req)
893 909 return f.read()
@@ -1,412 +1,412 b''
1 1 # revlog.py - storage back-end for mercurial
2 2 #
3 3 # This provides efficient delta storage with O(1) retrieve and append
4 4 # and O(changes) merge between branches
5 5 #
6 6 # Copyright 2005 Matt Mackall <mpm@selenic.com>
7 7 #
8 8 # This software may be used and distributed according to the terms
9 9 # of the GNU General Public License, incorporated herein by reference.
10 10
11 11 import zlib, struct, sha, os, tempfile, binascii
12 12 from mercurial import mdiff
13 13
14 14 def hex(node): return binascii.hexlify(node)
15 15 def bin(node): return binascii.unhexlify(node)
16 16
17 17 def compress(text):
18 18 return zlib.compress(text)
19 19
20 20 def decompress(bin):
21 21 return zlib.decompress(bin)
22 22
23 23 def hash(text, p1, p2):
24 24 l = [p1, p2]
25 25 l.sort()
26 26 return sha.sha(l[0] + l[1] + text).digest()
27 27
28 28 nullid = "\0" * 20
29 29 indexformat = ">4l20s20s20s"
30 30
31 31 class revlog:
32 32 def __init__(self, opener, indexfile, datafile):
33 33 self.indexfile = indexfile
34 34 self.datafile = datafile
35 35 self.index = []
36 36 self.opener = opener
37 37 self.cache = None
38 38 self.nodemap = {nullid: -1}
39 39 # read the whole index for now, handle on-demand later
40 40 try:
41 41 n = 0
42 42 i = self.opener(self.indexfile).read()
43 43 s = struct.calcsize(indexformat)
44 44 for f in range(0, len(i), s):
45 45 # offset, size, base, linkrev, p1, p2, nodeid
46 46 e = struct.unpack(indexformat, i[f:f + s])
47 47 self.nodemap[e[6]] = n
48 48 self.index.append(e)
49 49 n += 1
50 50 except IOError: pass
51 51
52 52 def tip(self): return self.node(len(self.index) - 1)
53 53 def count(self): return len(self.index)
54 54 def node(self, rev): return (rev < 0) and nullid or self.index[rev][6]
55 55 def rev(self, node): return self.nodemap[node]
56 56 def linkrev(self, node): return self.index[self.nodemap[node]][3]
57 57 def parents(self, node):
58 58 if node == nullid: return (nullid, nullid)
59 59 return self.index[self.nodemap[node]][4:6]
60 60
61 61 def start(self, rev): return self.index[rev][0]
62 62 def length(self, rev): return self.index[rev][1]
63 63 def end(self, rev): return self.start(rev) + self.length(rev)
64 64 def base(self, rev): return self.index[rev][2]
65 65
66 66 def lookup(self, id):
67 67 try:
68 68 rev = int(id)
69 69 return self.node(rev)
70 70 except ValueError:
71 71 c = []
72 72 for n in self.nodemap:
73 73 if id in hex(n):
74 74 c.append(n)
75 75 if len(c) > 1: raise KeyError("Ambiguous identifier")
76 if len(c) < 1: raise KeyError
76 if len(c) < 1: raise KeyError("No match found")
77 77 return c[0]
78 78
79 79 return None
80 80
81 81 def revisions(self, list):
82 82 # this can be optimized to do spans, etc
83 83 # be stupid for now
84 84 for node in list:
85 85 yield self.revision(node)
86 86
87 87 def diff(self, a, b):
88 88 return mdiff.textdiff(a, b)
89 89
90 90 def patch(self, text, patch):
91 91 return mdiff.patch(text, patch)
92 92
93 93 def revision(self, node):
94 94 if node == nullid: return ""
95 95 if self.cache and self.cache[0] == node: return self.cache[2]
96 96
97 97 text = None
98 98 rev = self.rev(node)
99 99 base = self.base(rev)
100 100 start = self.start(base)
101 101 end = self.end(rev)
102 102
103 103 if self.cache and self.cache[1] >= base and self.cache[1] < rev:
104 104 base = self.cache[1]
105 105 start = self.start(base + 1)
106 106 text = self.cache[2]
107 107 last = 0
108 108
109 109 f = self.opener(self.datafile)
110 110 f.seek(start)
111 111 data = f.read(end - start)
112 112
113 113 if not text:
114 114 last = self.length(base)
115 115 text = decompress(data[:last])
116 116
117 117 for r in xrange(base + 1, rev + 1):
118 118 s = self.length(r)
119 119 b = decompress(data[last:last + s])
120 120 text = self.patch(text, b)
121 121 last = last + s
122 122
123 123 (p1, p2) = self.parents(node)
124 124 if node != hash(text, p1, p2):
125 125 raise "integrity check failed on %s:%d" % (self.datafile, rev)
126 126
127 127 self.cache = (node, rev, text)
128 128 return text
129 129
130 130 def addrevision(self, text, transaction, link, p1=None, p2=None):
131 131 if text is None: text = ""
132 132 if p1 is None: p1 = self.tip()
133 133 if p2 is None: p2 = nullid
134 134
135 135 node = hash(text, p1, p2)
136 136
137 137 n = self.count()
138 138 t = n - 1
139 139
140 140 if n:
141 141 base = self.base(t)
142 142 start = self.start(base)
143 143 end = self.end(t)
144 144 prev = self.revision(self.tip())
145 145 data = compress(self.diff(prev, text))
146 146 dist = end - start + len(data)
147 147
148 148 # full versions are inserted when the needed deltas
149 149 # become comparable to the uncompressed text
150 150 if not n or dist > len(text) * 2:
151 151 data = compress(text)
152 152 base = n
153 153 else:
154 154 base = self.base(t)
155 155
156 156 offset = 0
157 157 if t >= 0:
158 158 offset = self.end(t)
159 159
160 160 e = (offset, len(data), base, link, p1, p2, node)
161 161
162 162 self.index.append(e)
163 163 self.nodemap[node] = n
164 164 entry = struct.pack(indexformat, *e)
165 165
166 166 transaction.add(self.datafile, e[0])
167 167 self.opener(self.datafile, "a").write(data)
168 168 transaction.add(self.indexfile, n * len(entry))
169 169 self.opener(self.indexfile, "a").write(entry)
170 170
171 171 self.cache = (node, n, text)
172 172 return node
173 173
174 174 def ancestor(self, a, b):
175 175 def expand(list, map):
176 176 a = []
177 177 while list:
178 178 n = list.pop(0)
179 179 map[n] = 1
180 180 yield n
181 181 for p in self.parents(n):
182 182 if p != nullid and p not in map:
183 183 list.append(p)
184 184 yield nullid
185 185
186 186 amap = {}
187 187 bmap = {}
188 188 ag = expand([a], amap)
189 189 bg = expand([b], bmap)
190 190 adone = bdone = 0
191 191
192 192 while not adone or not bdone:
193 193 if not adone:
194 194 an = ag.next()
195 195 if an == nullid:
196 196 adone = 1
197 197 elif an in bmap:
198 198 return an
199 199 if not bdone:
200 200 bn = bg.next()
201 201 if bn == nullid:
202 202 bdone = 1
203 203 elif bn in amap:
204 204 return bn
205 205
206 206 return nullid
207 207
208 208 def mergedag(self, other, transaction, linkseq, accumulate = None):
209 209 """combine the nodes from other's DAG into ours"""
210 210 old = self.tip()
211 211 i = self.count()
212 212 l = []
213 213
214 214 # merge the other revision log into our DAG
215 215 for r in range(other.count()):
216 216 id = other.node(r)
217 217 if id not in self.nodemap:
218 218 (xn, yn) = other.parents(id)
219 219 l.append((id, xn, yn))
220 220 self.nodemap[id] = i
221 221 i += 1
222 222
223 223 # merge node date for new nodes
224 224 r = other.revisions([e[0] for e in l])
225 225 for e in l:
226 226 t = r.next()
227 227 if accumulate: accumulate(t)
228 228 self.addrevision(t, transaction, linkseq.next(), e[1], e[2])
229 229
230 230 # return the unmerged heads for later resolving
231 231 return (old, self.tip())
232 232
233 233 def group(self, linkmap):
234 234 # given a list of changeset revs, return a set of deltas and
235 235 # metadata corresponding to nodes the first delta is
236 236 # parent(nodes[0]) -> nodes[0] the receiver is guaranteed to
237 237 # have this parent as it has all history before these
238 238 # changesets. parent is parent[0]
239 239
240 240 revs = []
241 241 needed = {}
242 242
243 243 # find file nodes/revs that match changeset revs
244 244 for i in xrange(0, self.count()):
245 245 if self.index[i][3] in linkmap:
246 246 revs.append(i)
247 247 needed[i] = 1
248 248
249 249 # if we don't have any revisions touched by these changesets, bail
250 250 if not revs: return struct.pack(">l", 0)
251 251
252 252 # add the parent of the first rev
253 253 p = self.parents(self.node(revs[0]))[0]
254 254 revs.insert(0, self.rev(p))
255 255
256 256 # for each delta that isn't contiguous in the log, we need to
257 257 # reconstruct the base, reconstruct the result, and then
258 258 # calculate the delta. We also need to do this where we've
259 259 # stored a full version and not a delta
260 260 for i in xrange(0, len(revs) - 1):
261 261 a, b = revs[i], revs[i + 1]
262 262 if a + 1 != b or self.base(b) == b:
263 263 for j in xrange(self.base(a), a + 1):
264 264 needed[j] = 1
265 265 for j in xrange(self.base(b), b + 1):
266 266 needed[j] = 1
267 267
268 268 # calculate spans to retrieve from datafile
269 269 needed = needed.keys()
270 270 needed.sort()
271 271 spans = []
272 272 for n in needed:
273 273 if n < 0: continue
274 274 o = self.start(n)
275 275 l = self.length(n)
276 276 spans.append((o, l, [(n, l)]))
277 277
278 278 # merge spans
279 279 merge = [spans.pop(0)]
280 280 while spans:
281 281 e = spans.pop(0)
282 282 f = merge[-1]
283 283 if e[0] == f[0] + f[1]:
284 284 merge[-1] = (f[0], f[1] + e[1], f[2] + e[2])
285 285 else:
286 286 merge.append(e)
287 287
288 288 # read spans in, divide up chunks
289 289 chunks = {}
290 290 for span in merge:
291 291 # we reopen the file for each span to make http happy for now
292 292 f = self.opener(self.datafile)
293 293 f.seek(span[0])
294 294 data = f.read(span[1])
295 295
296 296 # divide up the span
297 297 pos = 0
298 298 for r, l in span[2]:
299 299 chunks[r] = data[pos: pos + l]
300 300 pos += l
301 301
302 302 # helper to reconstruct intermediate versions
303 303 def construct(text, base, rev):
304 304 for r in range(base + 1, rev + 1):
305 305 b = decompress(chunks[r])
306 306 text = self.patch(text, b)
307 307 return text
308 308
309 309 # build deltas
310 310 deltas = []
311 311 for d in range(0, len(revs) - 1):
312 312 a, b = revs[d], revs[d + 1]
313 313 n = self.node(b)
314 314
315 315 if a + 1 != b or self.base(b) == b:
316 316 if a >= 0:
317 317 base = self.base(a)
318 318 ta = decompress(chunks[self.base(a)])
319 319 ta = construct(ta, base, a)
320 320 else:
321 321 ta = ""
322 322
323 323 base = self.base(b)
324 324 if a > base:
325 325 base = a
326 326 tb = ta
327 327 else:
328 328 tb = decompress(chunks[self.base(b)])
329 329 tb = construct(tb, base, b)
330 330 d = self.diff(ta, tb)
331 331 else:
332 332 d = decompress(chunks[b])
333 333
334 334 p = self.parents(n)
335 335 meta = n + p[0] + p[1] + linkmap[self.linkrev(n)]
336 336 l = struct.pack(">l", len(meta) + len(d) + 4)
337 337 deltas.append(l + meta + d)
338 338
339 339 l = struct.pack(">l", sum(map(len, deltas)) + 4)
340 340 deltas.insert(0, l)
341 341 return "".join(deltas)
342 342
343 343 def addgroup(self, data, linkmapper, transaction):
344 344 # given a set of deltas, add them to the revision log. the
345 345 # first delta is against its parent, which should be in our
346 346 # log, the rest are against the previous delta.
347 347
348 348 if not data: return self.tip()
349 349
350 350 # retrieve the parent revision of the delta chain
351 351 chain = data[24:44]
352 352
353 353 # track the base of the current delta log
354 354 r = self.count()
355 355 t = r - 1
356 356
357 357 base = prev = -1
358 358 start = end = 0
359 359 if r:
360 360 start = self.start(self.base(t))
361 361 end = self.end(t)
362 362 measure = self.length(self.base(t))
363 363 base = self.base(t)
364 364 prev = self.tip()
365 365
366 366 transaction.add(self.datafile, end)
367 367 transaction.add(self.indexfile, r * struct.calcsize(indexformat))
368 368 dfh = self.opener(self.datafile, "a")
369 369 ifh = self.opener(self.indexfile, "a")
370 370
371 371 # loop through our set of deltas
372 372 pos = 0
373 373 while pos < len(data):
374 374 l, node, p1, p2, cs = struct.unpack(">l20s20s20s20s",
375 375 data[pos:pos+84])
376 376 link = linkmapper(cs)
377 377 delta = data[pos + 84:pos + l]
378 378 pos += l
379 379
380 380 # full versions are inserted when the needed deltas become
381 381 # comparable to the uncompressed text or when the previous
382 382 # version is not the one we have a delta against. We use
383 383 # the size of the previous full rev as a proxy for the
384 384 # current size.
385 385
386 386 if chain == prev:
387 387 cdelta = compress(delta)
388 388
389 389 if chain != prev or (end - start + len(cdelta)) > measure * 2:
390 390 # flush our writes here so we can read it in revision
391 391 dfh.flush()
392 392 ifh.flush()
393 393 text = self.revision(chain)
394 394 text = self.patch(text, delta)
395 395 chk = self.addrevision(text, transaction, link, p1, p2)
396 396 if chk != node:
397 397 raise "consistency error adding group"
398 398 measure = len(text)
399 399 else:
400 400 e = (end, len(cdelta), self.base(t), link, p1, p2, node)
401 401 self.index.append(e)
402 402 self.nodemap[node] = r
403 403 dfh.write(cdelta)
404 404 ifh.write(struct.pack(indexformat, *e))
405 405
406 406 t, r, chain, prev = r, r + 1, node, node
407 407 start = self.start(self.base(t))
408 408 end = self.end(t)
409 409
410 410 dfh.close()
411 411 ifh.close()
412 412 return node
@@ -1,18 +1,18 b''
1 1 #!/usr/bin/env python
2 2
3 3 # This is the mercurial setup script.
4 4 #
5 5 # './setup.py install', or
6 6 # './setup.py --help' for more options
7 7
8 8 from distutils.core import setup
9 9
10 10 setup(name='mercurial',
11 version='0.4e',
11 version='0.4f',
12 12 author='Matt Mackall',
13 13 author_email='mpm@selenic.com',
14 14 url='http://selenic.com/mercurial',
15 15 description='scalable distributed SCM',
16 16 license='GNU GPL',
17 17 packages=['mercurial'],
18 18 scripts=['hg', 'hgweb.py'])
General Comments 0
You need to be logged in to leave comments. Login now