import re
import os
from os.path import dirname, normpath
+from urlparse import urlparse
try:
defpass = os.environ["PASSWORD"]
defpass = ""
opt_parser = OptionParser(usage="usage: %prog [options] repository revision")
-opt_parser.add_option("-H", "--host", dest="host",
- help="FTP server address. If not specified update files localy without FTP.")
-opt_parser.add_option("-u", "--user", dest="user",
- help="FTP login name")
+opt_parser.add_option("-U", "--url", dest="url", default="",
+ help="Destination URL (available protocols: sftp, ftp, file). Depricates -H, -u and -d.")
opt_parser.add_option("-p", "--pass", dest="password", default=defpass,
help="FTP password (defaults to environment variable PASSWORD)")
opt_parser.add_option("-r", "--repodir", dest="repodir", default="",
help="Synchronize only this directory (and its subdirectories) from within a repository")
+opt_parser.add_option("-H", "--host", dest="host",
+ help="FTP server address. If not specified update files localy without FTP.")
+opt_parser.add_option("-u", "--user", dest="user",
+ help="FTP login name")
opt_parser.add_option("-d", "--dir", dest="dir", default="",
help="An existing directory (on FTP site or localhost), where to store synchronized files.")
class RepoChange:
"""Represents one line in git diff --name-status"""
- def __init__(self, type, path, oldrev, newrev, options):
+ def __init__(self, type, path, oldrev, newrev, options, destroot=None):
self.type = type.strip() # one of ADMRCUX
self.repo_path = path
if path.startswith(options.repodir): # www/something -> something
path = path[len(options.repodir):]
- if options.dir: # something -> prefix/something
- path = options.dir+"/"+path
+ if destroot: # something -> prefix/something
+ path = destroot+"/"+path
self.dest_path = normpath(path)
self.oldrev = oldrev
self.newrev = newrev
self.mkd(change.dest_path)
elif change.type[0] in ["A", "C", "M"]:
# Check whether the target directory exists
- retcode = call("git ls-tree %s %s|grep -q ." % (change.oldrev, dirname(change.repo_path)), shell=True)
- if (retcode != 0 and normpath(dirname(change.dest_path)) != normpath(dest_root)):
- self.mkd(dirname(change.dest_path))
+ if (normpath(dirname(change.dest_path)) != normpath(dest_root)):
+ retcode = 1
+ if (change.oldrev): # If there is previous revision, check for it
+ retcode = call("git ls-tree %s %s|grep -q ." % (change.oldrev, dirname(change.repo_path)), shell=True)
+ if (retcode != 0):
+ dirs = normpath(dirname(change.dest_path)).split("/")
+ #print "YYYYYYYYYYYYY",dirname(change.dest_path)
+ destdir = ""
+ for i in dirs:
+ if (i==""): i="/"
+ destdir = os.path.join(destdir, i);
+ #print "XXXXXXXXXXXXX",destdir
+ #self.mkd(dirname(change.dest_path))
+ self.mkd(destdir)
+
# Upload the file
print "%s: UPLOAD"%self.__class__.__name__, change.dest_path
pipe = Popen("git cat-file blob %s:%s" % (change.newrev, change.repo_path),
except ftplib.error_perm, detail:
perror("FTP warning: %s %s" % (detail, path))
+class SFTPSync(Syncer):
+ def __init__(self, url):
+ import paramiko
+ # get host key, if we know one
+ hostkeytype = None
+ hostkey = None
+ try:
+ host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
+ except IOError:
+ try:
+ # try ~/ssh/ too, because windows can't have a folder named ~/.ssh/
+ host_keys = paramiko.util.load_host_keys(os.path.expanduser('~/ssh/known_hosts'))
+ except IOError:
+ print '*** Unable to open host keys file'
+ host_keys = {}
+
+ if host_keys.has_key(url.hostname):
+ hostkeytype = host_keys[url.hostname].keys()[0]
+ hostkey = host_keys[url.hostname][hostkeytype]
+ print 'Using host key of type %s' % hostkeytype
+
+
+ # now, connect and use paramiko Transport to negotiate SSH2 across the connection
+ port = 22
+ if url.port: port = url.port
+ self.t = paramiko.Transport((url.hostname, port))
+ password = url.password
+ if not password: password = options.password
+ self.t.connect(username=url.username, password=password, hostkey=hostkey)
+ self.sftp = paramiko.SFTPClient.from_transport(self.t)
+
+ def close(self):
+ self.sftp.close()
+ self.t.close()
+
+ def _mkd(self, path):
+ try:
+ self.sftp.mkdir(path)
+ except IOError, detail:
+ print "sftp warning:", detail
+
+ def _storbinary(self, file, path):
+ remote = self.sftp.open(path, 'w')
+ s = file.read(10000)
+ while s:
+ remote.write(s)
+ s = file.read(10000)
+ remote.close()
+
+
+ def _rmd(self, path):
+ """ Delete everything reachable from the directory named in 'self.dest_path',
+ assuming there are no symbolic links."""
+ self.sftp.rmdir(path)
+ # FIXME: this should be recursive deletion
+
+ def _delete(self, path):
+ self.sftp.remove(path)
+
class LocalSync(Syncer):
def _mkd(self, path):
mkdir www
# Commit something to www directory in repo without the activated hook
- echo 'Bye' > www/first.html
- git add www/first.html
+ mkdir www/dir
+ mkdir www/dir/dir2
+ echo 'Bye' > www/dir/dir2/first.html
+ git add www/dir/dir2/first.html
commit_push 'Added first.html'
# Activate the hook and commit a non-mirrored file
# Check that the first commit appears in www even if the hook was
# activated later (only for local sync)
- grep -q PASSWORD $REPO/hooks/post-receive || test -f ../www/first.html
+ grep -q PASSWORD $REPO/hooks/post-receive || test -f ../www/dir/dir2/first.html
cd www
echo 'Hello' > index.html
cd ..
git rm www/index2.html
commit_push 'Removed index2'
- git rm www/first.html
+ git rm www/dir/dir2/first.html
commit_push 'Removed first.html'
cd $REPO/..
rm -rf working repo.git www
ret = os.system(commands % hook)
if ret: return
+ print "\n\nChecking SFTP sync (server: localhost, user: test, password: env. variable PASSWORD)"
+ password = defpass
+ hook = """#!/bin/sh\nexport PASSWORD=%(password)s\n%(selfpath)s -r www --url sftp://test@localhost""" % locals()
+ ret = os.system(commands % hook)
+ if ret: return
+
print
print "Selftest succeeded!"
def update_ref(hash):
- remote = "ftp"
- branch = "master"
- if (not os.path.exists("refs/remotes/"+remote)):
- os.makedirs("refs/remotes/"+remote)
- file("refs/remotes/"+remote+"/"+branch, "w").write(newrev+"\n")
+ cmd = "git update-ref refs/remotes/ftp/master %s"%(hash)
+ #print "Runnging", cmd
+ os.system(cmd)
-def add_to_change_list(changes, git_command):
+def add_to_change_list(changes, git_command, oldrev, newrev, destroot):
# Read changes
+ ##print "Running: ", git_command
gitdiff = Popen(git_command,
stdout=PIPE, stderr=PIPE, close_fds=True, shell=True)
change_re = re.compile("(\S+)\s+(.*)$");
for line in gitdiff.stdout:
- #print line,
+ ##print line,
m = change_re.match(line)
if (m):
- change = RepoChange(m.group(1), m.group(2), oldrev, newrev, options)
+ change = RepoChange(m.group(1), m.group(2), oldrev, newrev, options, destroot)
if change.inDir(options.repodir):
changes.append(change)
options.repodir = normpath(options.repodir).lstrip(".")
if options.repodir: options.repodir+="/"
+if options.host:
+ url = urlparse("ftp://"+options.user+"@"+options.host+"/"+options.dir)
+elif options.dir and not options.url:
+ url = urlparse("file:///"+options.dir)
+else:
+ url = urlparse(options.url)
+
changes = list()
# Read the changes
-for line in sys.stdin:
- (oldrev, newrev, refname) = line.split()
- if refname == "refs/heads/master":
- try:
- oldrev=file("refs/remotes/ftp/master").readline().strip();
- git_command = "/usr/bin/git diff --name-status %s %s"%(oldrev, newrev)
- except IOError:
- # We are run for the first time, so (A)dd all files in the repo.
- git_command = r"git ls-tree -r --name-only %s | sed -e 's/\(.*\)/A \1/'"%(newrev);
-
- add_to_change_list(changes, git_command)
-
+if 'GIT_DIR' in os.environ:
+ # Invocation from hook
+ for line in sys.stdin:
+ (oldrev, newrev, refname) = line.split()
+ if refname == "refs/heads/master":
+ try:
+ oldrev=os.popen('git show-ref --hash refs/remotes/ftp/master').read().strip()
+ if not oldrev: raise IOError, "No ref" # Simulate failure if the branch doesn't exist
+ git_command = "git diff --name-status %s %s"%(oldrev, newrev)
+ except IOError:
+ # We are run for the first time, so (A)dd all files in the repo.
+ oldrev = None
+ git_command = r"git ls-tree -r --name-only %s | sed -e 's/\(.*\)/A \1/'"%(newrev);
+
+ add_to_change_list(changes, git_command, oldrev, newrev, url.path)
+else:
+ # Manual invocation
+ newrev = "HEAD"
+ oldrev = None;
+ git_command = r"git ls-tree -r --name-only HEAD | sed -e 's/\(.*\)/A \1/'";
+ add_to_change_list(changes, git_command, oldrev, newrev)
+
if not changes:
perror("No changes to sync")
sys.exit(0)
# Apply changes
try:
- if options.host:
+ if url.scheme == "ftp":
syncer = FTPSync(FTP(options.host, options.user, options.password))
- else:
+ elif url.scheme == "file":
syncer = LocalSync()
+ elif url.scheme == "sftp":
+ syncer = SFTPSync(url)
for change in changes:
syncer.sync(change, options.dir)
syncer.close()
-except ftplib.all_errors, detail:
- perror("FTP synchronization error: %s" % detail);
- perror("I will try it next time again");
- sys.exit(1)
+# except ftplib.all_errors, detail:
+# perror("FTP synchronization error: %s" % detail);
+# perror("I will try it next time again");
+# sys.exit(1)
+except:
+ raise
# If succeessfull, update remote ref
update_ref(newrev)