3 # Copyright (C) 2010-2011 W. Trevor King <wking@drexel.edu>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # Lesser General Public License for more details.
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program. If not, see
17 # <http://www.gnu.org/licenses/>.
19 """Publish a local Git repository on a remote host.
21 This script wraps my usual workflow so I don't have to remember it ;).
24 import logging as _logging
26 import os.path as _os_path
27 import shutil as _shutil
28 import subprocess as _subprocess
29 import tempfile as _tempfile
31 import urllib.parse as _urllib_parse
32 except ImportError: # Python 2
33 import urlparse as _urllib_parse
38 _LOG = _logging.getLogger('git-publish')
39 _LOG.addHandler(_logging.StreamHandler())
40 _LOG.setLevel(_logging.WARNING)
42 PUBLIC_BRANCH_NAME = 'public'
45 def parse_remote(remote):
48 >>> parse_remote('ssh://example.com/~/')
49 (None, 'example.com', 22, '~')
50 >>> parse_remote('ssh://jdoe@example.com:2222/a/b/c')
51 ('jdoe', 'example.com', 2222, '/a/b/c')
53 _LOG.debug("parse {} using Git's URL syntax".format(remote))
54 p = _urllib_parse.urlparse(remote)
55 assert p.scheme == 'ssh', p.scheme
56 assert p.params == '', p.params
57 assert p.query == '', p.query
58 assert p.fragment == '', p.fragment
60 _LOG.debug('extract username, host, and port from {}'.format(p.netloc))
61 userhost_port = p.netloc.split(':', 1)
62 if len(userhost_port) > 2:
63 _LOG.error("more than one ':' in netloc: {}".format(p.netloc))
64 raise ValueError(p.netloc)
65 elif len(userhost_port) == 2:
66 port = int(userhost_port[1])
69 userhost = userhost_port[0]
71 _LOG.debug('extract username and host from {}'.format(userhost))
72 user_host = userhost.split('@', 1)
73 if len(user_host) > 2:
74 _LOG.error("more than one '@' in netloc: {}".format(p.netloc))
75 raise ValueError(userhost)
76 elif len(user_host) == 2:
82 _LOG.debug('extract path from {}'.format(p.path))
83 basedir = p.path.rstrip('/')
84 if basedir.startswith('/~'):
85 basedir = basedir[1:] # allow user expansion on the remote host
87 _LOG.debug('user: {}, host: {}, port: {}, basedir: {}'.format(
88 user, host, port, basedir))
89 return (user, host, port, basedir)
92 with open(path, 'a') as f:
95 def git(args, repo=None):
98 >>> print(git(['help'])) # doctest: +ELLIPSIS
103 _LOG.debug('{}: git {}'.format(repo, args))
104 output = _subprocess.check_output(['git'] + args, cwd=repo)
108 def has_remote(repo, remote):
109 for line in git(repo=repo, args=['remote']).splitlines():
114 def make_bare_local_checkout(repo):
115 bare = _tempfile.mkdtemp(prefix='git-publish-')
116 _LOG.debug('make a bare local checkout of {} in {}'.format(repo, bare))
117 git(args=['clone', '--bare', repo, bare])
118 _LOG.debug('locally configure the bare checkout')
119 _shutil.copy(_os_path.join(repo, '.git', 'description'), bare)
120 touch(_os_path.join(bare, 'git-daemon-export-ok'))
121 _shutil.move(_os_path.join(bare, 'hooks', 'post-update.sample'),
122 _os_path.join(bare, 'hooks', 'post-update'))
123 git(repo=bare, args=['--bare', 'update-server-info'])
124 _os.chmod(bare, 0o0755)
127 def recursive_copy(source, user, host, port, path):
128 source = source.rstrip('/') + '/'
129 path = path.rstrip('/') + '/'
130 target = '{}@{}:{}'.format(user, host, path)
131 _LOG.debug('copy {} to {}'.format(source, target))
132 _subprocess.check_call(
133 ['rsync', '-az', '--delete', '--rsh', 'ssh -p{:d}'.format(port),
136 def add_remote(repo, remote, user, host, port=22, path=None):
137 url = 'ssh://{}@{}:{:d}/{}'.format(user, host, port, path)
138 _LOG.debug('add {} remote to {} pointing to {}'.format(remote, repo, url))
139 git(repo=repo, args=['remote', 'add', remote, url])
140 git(repo=repo, args=['fetch', remote])
141 git(repo=repo, args=[
142 'branch', '--set-upstream', 'master', '{}/master'.format(remote)])
144 def publish(repo, host, basedir='.', port=22, user=None, name=None):
146 user = _os.getlogin()
147 repo = _os_path.abspath(_os_path.expanduser(repo))
149 name = _os_path.basename(repo)
150 target_dir = _os_path.join(basedir, '{}.git'.format(name))
151 _LOG.info('publishing {} at {}@{}:{:d}/{}'.format(
152 repo, user, host, port, target_dir))
153 if has_remote(repo=repo, remote=PUBLIC_BRANCH_NAME):
154 _LOG.info('{} already published'.format(name))
156 bare = make_bare_local_checkout(repo)
158 source=bare, user=user, host=host, port=port, path=target_dir)
159 _LOG.debug('cleanup {}'.format(bare))
162 repo=repo, remote=PUBLIC_BRANCH_NAME, user=user, host=host, port=port,
166 if __name__ == '__main__':
167 from argparse import ArgumentParser
170 parser = ArgumentParser(description=__doc__, version=__version__)
172 '-V', '--verbose', default=0, action='count',
173 help='increment verbosity')
176 help=("the remote target. Use Git's SSH URL, e.g. "
177 'ssh://user@host:port/~/path/to/base'))
180 help=('override the name of the new remote repository (defaults to '
181 'the local dirname + .git)'))
183 'repo', default='.', nargs='?',
184 help='local Git repository to publish (default: %(default)s)')
185 args = parser.parse_args()
187 if args.verbose >= 2:
188 _LOG.setLevel(_logging.DEBUG)
189 elif args.verbose >= 1:
190 _LOG.setLevel(_logging.INFO)
192 if args.remote is None:
193 _LOG.error('--remote argument is required.')
196 user,host,port,basedir = parse_remote(remote=args.remote)
198 repo=args.repo, user=user, host=host, port=port, basedir=basedir,