mercurial/hg.py
changeset 0 9117c6561b0b
child 4 ce3bd728b858
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()