#!/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)
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:
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))