Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/hg.py @ 0:9117c6561b0b
Add back links from file revisions to changeset revisions
Add simple transaction support
Add hg verify
Improve caching in revlog
Fix a bunch of bugs
Self-hosting now that the metadata is close to finalized
author | mpm@selenic.com |
---|---|
date | Tue, 03 May 2005 13:16:10 -0800 |
parents | |
children | ce3bd728b858 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:9117c6561b0b |
---|---|
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() |