--- /dev/null
+#!/usr/bin/env python
+#
+# Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program. If not, see
+# <http://www.gnu.org/licenses/>.
+
+"""Publish a local Git repository on a remote host.
+
+This script wraps my usual workflow so I don't have to remember it ;).
+"""
+
+import logging as _logging
+import os as _os
+import os.path as _os_path
+import shutil as _shutil
+import subprocess as _subprocess
+import tempfile as _tempfile
+import urlparse as _urlparse
+
+
+__version__ = '0.2'
+
+_LOG = _logging.getLogger('git-publish')
+_LOG.addHandler(_logging.StreamHandler())
+_LOG.setLevel(_logging.WARNING)
+
+PUBLIC_BRANCH_NAME = 'public'
+
+
+def parse_remote(remote):
+ """
+
+ >>> parse_remote('ssh://example.com/~/')
+ (None, 'example.com', 22, '~')
+ >>> parse_remote('ssh://jdoe@example.com:2222/a/b/c')
+ ('jdoe', 'example.com', 2222, '/a/b/c')
+ """
+ _LOG.debug("parse {} using Git's URL syntax".format(remote))
+ p = _urlparse.urlparse(remote)
+ assert p.scheme == 'ssh', p.scheme
+ assert p.params == '', p.params
+ assert p.query == '', p.query
+ assert p.fragment == '', p.fragment
+
+ _LOG.debug('extract username, host, and port from {}'.format(p.netloc))
+ userhost_port = p.netloc.split(':', 1)
+ if len(userhost_port) > 2:
+ _LOG.error("more than one ':' in netloc: {}".format(p.netloc))
+ raise ValueError(p.netloc)
+ elif len(userhost_port) == 2:
+ port = int(userhost_port[1])
+ else:
+ port = 22
+ userhost = userhost_port[0]
+
+ _LOG.debug('extract username and host from {}'.format(userhost))
+ user_host = userhost.split('@', 1)
+ if len(user_host) > 2:
+ _LOG.error("more than one '@' in netloc: {}".format(p.netloc))
+ raise ValueError(userhost)
+ elif len(user_host) == 2:
+ user,host = user_host
+ else:
+ user = None
+ host = user_host[0]
+
+ _LOG.debug('extract path from {}'.format(p.path))
+ basedir = p.path.rstrip('/')
+ if basedir.startswith('/~'):
+ basedir = basedir[1:] # allow user expansion on the remote host
+
+ _LOG.debug('user: {}, host: {}, port: {}, basedir: {}'.format(
+ user, host, port, basedir))
+ return (user, host, port, basedir)
+
+def touch(path):
+ with open(path, 'a') as f:
+ pass
+
+def git(args, repo=None):
+ """
+
+ >>> print(git(['help'])) # doctest: +ELLIPSIS
+ usage: git ...
+ """
+ if repo is None:
+ repo='.'
+ _LOG.debug('{}: git {}'.format(repo, args))
+ output = _subprocess.check_output(['git'] + args, cwd=repo)
+ _LOG.debug(output)
+ return output
+
+def has_remote(repo, remote):
+ for line in git(repo=repo, args=['remote']).splitlines():
+ if line == remote:
+ return True
+ return False
+
+def make_bare_local_checkout(repo):
+ bare = _tempfile.mkdtemp(prefix='git-publish-')
+ _LOG.debug('make a bare local checkout of {} in {}'.format(repo, bare))
+ git(args=['clone', '--bare', repo, bare])
+ _LOG.debug('locally configure the bare checkout')
+ _shutil.copy(_os_path.join(repo, '.git', 'description'), bare)
+ touch(_os_path.join(bare, 'git-daemon-export-ok'))
+ _shutil.move(_os_path.join(bare, 'hooks', 'post-update.sample'),
+ _os_path.join(bare, 'hooks', 'post-update'))
+ git(repo=bare, args=['--bare', 'update-server-info'])
+ return bare
+
+def recursive_copy(source, user, host, port, path):
+ source = source.rstrip('/') + '/'
+ path = path.rstrip('/') + '/'
+ target = '{}@{}:{}'.format(user, host, path)
+ _LOG.debug('copy {} to {}'.format(source, target))
+ _subprocess.check_call(
+ ['rsync', '-az', '--delete', '--rsh', 'ssh -p{:d}'.format(port),
+ source, target])
+
+def add_remote(repo, remote, user, host, port=22, path=None):
+ url = 'ssh://{}@{}:{:d}/{}'.format(user, host, port, path)
+ _LOG.debug('add {} remote to {} pointing to {}'.format(remote, repo, url))
+ git(repo=repo, args=['remote', 'add', remote, url])
+ git(repo=repo, args=['fetch', remote])
+ git(repo=repo, args=[
+ 'branch', '--set-upstream', 'master', '{}/master'.format(remote)])
+
+def publish(repo, host, basedir='.', port=22, user=None, name=None):
+ if user is None:
+ user = _os.getlogin()
+ repo = _os_path.abspath(_os_path.expanduser(repo))
+ if name is None:
+ name = _os_path.basename(repo)
+ target_dir = _os_path.join(basedir, '{}.git'.format(name))
+ _LOG.info('publishing {} at {}@{}:{:d}/{}'.format(
+ repo, user, host, port, target_dir))
+ if has_remote(repo=repo, remote=PUBLIC_BRANCH_NAME):
+ _LOG.info('{} already published'.format(name))
+ return
+ bare = make_bare_local_checkout(repo)
+ recursive_copy(
+ source=bare, user=user, host=host, port=port, path=target_dir)
+ _LOG.debug('cleanup {}'.format(bare))
+ _shutil.rmtree(bare)
+ add_remote(
+ repo=repo, remote=PUBLIC_BRANCH_NAME, user=user, host=host, port=port,
+ path=target_dir)
+
+
+if __name__ == '__main__':
+ from argparse import ArgumentParser
+ import sys
+
+ parser = ArgumentParser(description=__doc__, version=__version__)
+ parser.add_argument(
+ '-V', '--verbose', default=0, action='count',
+ help='increment verbosity')
+ parser.add_argument(
+ '-r', '--remote',
+ help=("the remote target. Use Git's SSH URL, e.g. "
+ 'ssh://user@host:port/~/path/to/base'))
+ parser.add_argument(
+ '-n', '--name',
+ help=('override the name of the new remote repository (defaults to '
+ 'the local dirname + .git)'))
+ parser.add_argument(
+ 'repo', default='.', nargs='?',
+ help='local Git repository to publish ({default})')
+ args = parser.parse_args()
+
+ if args.verbose >= 2:
+ _LOG.setLevel(_logging.DEBUG)
+ elif args.verbose >= 1:
+ _LOG.setLevel(_logging.INFO)
+
+ if args.remote is None:
+ _LOG.error('--remote argument is required.')
+ sys.exit(1)
+
+ user,host,port,basedir = parse_remote(remote=args.remote)
+ publish(
+ repo=args.repo, user=user, host=host, port=port, basedir=basedir,
+ name=args.name)