]> arthur.barton.de Git - bup.git/blobdiff - cmd-save.py
executable files: don't assume python2.5.
[bup.git] / cmd-save.py
index 1dba9b0479fa124681e7953202c665dd41a8d8e5..8c7ec9ef7ca60c8c6f239d41c8ca4ad98e9a6648 100755 (executable)
@@ -1,94 +1,19 @@
 #!/usr/bin/env python
-import sys, re, errno
-import hashsplit, git, options
+import sys, re, errno, stat, client
+import hashsplit, git, options, index
 from helpers import *
 
-saved_errors = []
 
+saved_errors = []
 def add_error(e):
     saved_errors.append(e)
     log('\n%s\n' % e)
 
 
-def direxpand(names):
-    for n in names:
-        try:
-            for sub in os.listdir(n):
-                subfull = os.path.join(n, sub)
-                for sub2 in direxpand([subfull]):
-                    yield sub2
-        except OSError, e:
-            if e.errno == errno.ENOTDIR:
-                yield n
-            elif e.errno in [errno.ENOENT, errno.EPERM, errno.EACCES]:
-                add_error(e)
-            else:
-                raise
-            
-
-def _normpath(dir):
-    p = os.path.normpath(dir)
-    return (p != '.') and p or ''
-
-
-class Tree:
-    def __init__(self, parent, name):
-        assert(name != '.')
-        assert(not (parent and not name))
-        self.parent = parent
-        self.name = name
-        self.sha = None
-        self.children = {}
-        if self.parent:
-            self.parent.children[self.name] = self
-    
-    def fullpath(self):
-        if self.parent:
-            return os.path.join(self.parent.fullpath(), self.name)
-        else:
-            return self.name
-        
-    def gettop(self):
-        p = self
-        while p.parent:
-            p = p.parent
-        return p
-        
-    def getdir(self, dir):
-        # FIXME: deal with '..' somehow
-        if dir.startswith('/'):
-            dir = dir[1:]
-        top = self.gettop()
-        if not dir:
-            return top
-        for part in _normpath(dir).split('/'):
-            sub = top.children.get(part)
-            if not sub:
-                sub = top.children[part] = Tree(top, part)
-            top = sub
-        return top
-    
-    def addfile(self, mode, fullname, id):
-        (dir, name) = os.path.split(fullname)
-        self.getdir(dir).children[name] = (mode, name, id)
-        
-    def shalist(self, w):
-        for c in self.children.values():
-            if isinstance(c, tuple):  # sha1 entry for a file
-                yield c
-            else:  # tree
-                t = ('40000', c.name, c.gen_tree(w))
-                yield t
-        
-    def gen_tree(self, w):
-        if not self.sha:
-            self.sha = w.new_tree(self.shalist(w))
-        return self.sha
-
-
 optspec = """
 bup save [-tc] [-n name] <filenames...>
 --
+r,remote=  remote repository path
 t,tree     output a tree id
 c,commit   output a commit id
 n,name=    name of backup set to update (if any)
@@ -97,31 +22,119 @@ v,verbose  increase log output (can be used more than once)
 o = options.Options('bup save', optspec)
 (opt, flags, extra) = o.parse(sys.argv[1:])
 
+git.check_repo_or_die()
 if not (opt.tree or opt.commit or opt.name):
     log("bup save: use one or more of -t, -c, -n\n")
     o.usage()
+if not extra:
+    log("bup save: no filenames given.\n")
+    o.usage()
 
 if opt.verbose >= 2:
     git.verbose = opt.verbose - 1
     hashsplit.split_verbosely = opt.verbose - 1
 
-w = git.PackWriter()
-root = Tree(None, '')
-for fn in direxpand(extra):
+refname = opt.name and 'refs/heads/%s' % opt.name or None
+if opt.remote:
+    cli = client.Client(opt.remote)
+    oldref = refname and cli.read_ref(refname) or None
+    w = cli.new_packwriter()
+else:
+    cli = None
+    oldref = refname and git.read_ref(refname) or None
+    w = git.PackWriter()
+
+
+def eatslash(dir):
+    if dir.endswith('/'):
+        return dir[:-1]
+    else:
+        return dir
+
+
+parts = ['']
+shalists = [[]]
+
+def _push(part):
+    assert(part)
+    parts.append(part)
+    shalists.append([])
+
+def _pop():
+    assert(len(parts) > 1)
+    part = parts.pop()
+    shalist = shalists.pop()
+    tree = w.new_tree(shalist)
+    shalists[-1].append(('40000', part, tree))
+
+
+for (transname,ent) in index.Reader(git.repo('bupindex')).filter(extra):
+    (dir, file) = os.path.split(ent.name)
+    exists = (ent.flags & index.IX_EXISTS)
+    hashvalid = (ent.flags & index.IX_HASHVALID) and w.exists(ent.sha)
     if opt.verbose:
-        log('\n%s ' % fn)
-    try:
-        # FIXME: symlinks etc.
-        f = open(fn)
-    except IOError, e:
-        add_error(e)
-        continue
-    except OSError, e:
-        add_error(e)
+        if not exists:
+            status = 'D'
+        elif not hashvalid:
+            if ent.sha == index.EMPTY_SHA:
+                status = 'A'
+            else:
+                status = 'M'
+        else:
+            status = ' '
+        if opt.verbose >= 2 or (status in ['A','M'] 
+                                and not stat.S_ISDIR(ent.mode)):
+            log('\n%s %s ' % (status, ent.name))
+
+    if not exists:
         continue
-    (mode, id) = hashsplit.split_to_blob_or_tree(w, [f])
-    root.addfile(mode, fn, id)
-tree = root.gen_tree(w)
+
+    assert(dir.startswith('/'))
+    dirp = dir.split('/')
+    while parts > dirp:
+        _pop()
+    if dir != '/':
+        for part in dirp[len(parts):]:
+            _push(part)
+
+    if not file:
+        # directory already handled.
+        # FIXME: not using the indexed tree sha1's for anything, which is
+        # a waste.  That's a potential optimization...
+        continue  
+
+    id = None
+    if hashvalid:
+        mode = '%o' % ent.mode
+        id = ent.sha
+        shalists[-1].append((mode, file, id))
+    else:
+        try:
+            if stat.S_ISREG(ent.mode):
+                f = open(ent.name)
+                (mode, id) = hashsplit.split_to_blob_or_tree(w, [f])
+            elif stat.S_ISDIR(ent.mode):
+                assert(0)  # handled above
+            elif stat.S_ISLNK(ent.mode):
+                (mode, id) = ('120000', w.new_blob(os.readlink(ent.name)))
+            else:
+                add_error(Exception('skipping special file "%s"' % ent.name))
+        except IOError, e:
+            add_error(e)
+        except OSError, e:
+            add_error(e)
+        if id:
+            ent.validate(id)
+            ent.repack()
+            shalists[-1].append((mode, file, id))
+#log('parts out: %r\n' % parts)
+#log('stk out: %r\n' % shalists)
+while len(parts) > 1:
+    _pop()
+#log('parts out: %r\n' % parts)
+#log('stk out: %r\n' % shalists)
+assert(len(shalists) == 1)
+tree = w.new_tree(shalists[-1])
 if opt.verbose:
     log('\n')
 if opt.tree:
@@ -129,11 +142,22 @@ if opt.tree:
 if opt.commit or opt.name:
     msg = 'bup save\n\nGenerated by command:\n%r' % sys.argv
     ref = opt.name and ('refs/heads/%s' % opt.name) or None
-    commit = w.new_commit(ref, tree, msg)
+    commit = w.new_commit(oldref, tree, msg)
     if opt.commit:
+        if opt.verbose:
+            log('\n')
         print commit.encode('hex')
 
-w.close()
+w.close()  # must close before we can update the ref
+        
+if opt.name:
+    if cli:
+        cli.update_ref(refname, commit, oldref)
+    else:
+        git.update_ref(refname, commit, oldref)
+
+if cli:
+    cli.close()
 
 if saved_errors:
     log('WARNING: %d errors encountered while saving.\n' % len(saved_errors))