From 708429a38a6cf2c0d94ee4da168fc518c0f974e6 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 5 Dec 2011 20:50:16 -0500 Subject: [PATCH] Rewrite git-publish as a Python script, and add it to the blog's version control. --- posts/Git.mdwn | 2 +- posts/Git/git-publish.py | 195 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100755 posts/Git/git-publish.py diff --git a/posts/Git.mdwn b/posts/Git.mdwn index 9be0e13..c14e734 100644 --- a/posts/Git.mdwn +++ b/posts/Git.mdwn @@ -1,7 +1,7 @@ [Git][] is a [distributed][DVCS] [verision control system][VCS]. Highly recommended. There is an excellent [tutorial][], or you can take a look at my [[notes]] (best viewed in [[Emacs]] org-mode). See -[[git-publish]] for a useful Git public-repository setup script. +[[git-publish.py]] for a useful Git public-repository setup script. I've published Git repositories for most of my more involved [projects][]. diff --git a/posts/Git/git-publish.py b/posts/Git/git-publish.py new file mode 100755 index 0000000..10ec076 --- /dev/null +++ b/posts/Git/git-publish.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +# +# Copyright (C) 2010-2011 W. Trevor King +# +# 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 +# . + +"""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) -- 2.26.2