#!/usr/bin/python # gitarch.py # # Copyright (C) 2012 Jan Stancek <jan@stancek.eu> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ git-archive in pure python """ __author__ = """Jan Stancek""" import socket import errno import os import sys import getopt import traceback import logging log = logging HDR_LEN_SIZE = 4 BUF_SIZE = 4096 def recv_safe(sock, bufSize, flags=0): n = 0 while True: try: n = sock.recv(bufSize, flags) break except socket.error, (value, message): if value != errno.EINTR and value != errno.EAGAIN: raise else: log.debug('recv_safe: got an EINTR/AGAIN, trying again') return n def send_safe(sock, data): n = 0 while True: try: n = sock.send(data) break except socket.error, (value, message): if value != errno.EINTR and value != errno.EAGAIN: raise else: log.debug('send_safe: got an EINTR/AGAIN, trying again') return n def send_fully(s, data): buflen = len(data) n = 0 while buflen > 0: n = send_safe(s, data) if (n < 0): raise socket.error(0, 'send_safe() < 0') data = data[n:] buflen = buflen - n return n def recv_fully(s, recvlen): data = '' n = recvlen while len(data) < recvlen: part = recv_safe(s, n) if part == '': data = '' break data += part n -= len(part) return data def recv_git_msg(sock): msg_len = 0 msg = '' msg_len_hex = recv_fully(sock, HDR_LEN_SIZE) try: if len(msg_len_hex) == HDR_LEN_SIZE: msg_len = int(msg_len_hex, 16) msg = recv_fully(sock, msg_len - HDR_LEN_SIZE) except ValueError: pass return (msg_len, msg) def prep_git_req(host, basepath, treeish, format): op = 'git-upload-archive %s\0host=%s side-band side-band-64k\0' % (basepath, host) arg1 = 'argument %s\0' % treeish arg2 = 'argument --format=%s\0' % format return make_git_msg(op) + make_git_msg(arg1) + \ make_git_msg(arg2) + make_git_msg('') def make_git_msg(msg, sideBand=0): msglen = len(msg) + HDR_LEN_SIZE msglenhex = hex(msglen)[2:] msglenhex = msglenhex.zfill(4) return '%s%s' % (msglenhex, msg) def get_archive(outname, host, port, basedir, treeish, format): ret = 0 data_available = True log.debug('outname: %s, host: %s, port: %s, basedir: %s, treeish: %s, format: %s', outname, host, port, basedir, treeish, format) request = prep_git_req(host, basedir, treeish, format) log.info('Request for remote side: <%s>', request) try: outfile = open(outname, 'wb') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) # send our request sent = send_fully(sock, request) if sent == 0: raise socket.error(0, 'Connection reset by peer') # server must reply ACK msg_len, msg = recv_git_msg(sock) if msg: msg = msg[:-1] log.debug('ACK/NAK: len: %d, <%s>', msg_len, msg) if msg_len < 8 or not msg.startswith('ACK'): raise socket.error(0, 'Server did not reply with ACK') # wait for empty msg while msg != '': msg_len, msg = recv_git_msg(sock) log.debug('got header: len: %d, <%s>', msg_len, msg) while True: msg_len, msg = recv_git_msg(sock) log.debug('got: len: %d', msg_len) if msg_len == 0 or msg == '': break sideband_type = msg[0] msg = msg[1:] if sideband_type == '\1': outfile.write(msg) elif sideband_type == '\2': log.info('sideband(2): %s', msg) elif sideband_type == '\3': log.info('sideband(3): %s', msg) outfile.flush() outfile.close() sock.close() except socket.error, (value, message): log.error('socket.error %s %s', value, message) ret = 2 except IOError, e: log.error('ioerror %s', e) ret = 3 log.info('_get_archive ret_code: %d', ret) return ret def get_archive_check(outname, host, port, basedir, treeish): ret = 1 format = outname[-3:] if format != 'zip' and format != 'tar': return ret try: log.info('Getting archive %s %s %s %s %s %s', outname, host, port, basedir, treeish, format) ret = get_archive(outname, host, port, basedir, treeish, format) if ret == 0: if format == 'zip': ret = os.system('zip -T %s > /dev/null' % outname) if format == 'tar': ret = os.system('tar -tf %s > /dev/null' % outname) except: traceback.print_exc() log.info('archive test ret_code: %d', ret) return ret def install_from_git(host, port, basedir, treeish, outname, targetdir, verbose = False, force = False): log.info('install_from_git %s %s %s %s %s %s', host, port, basedir, treeish, outname, targetdir) #check to see if the directory already exists and abort if os.path.exists(targetdir) and not force: log.info('Already Installed: %s' % os.path.basename(targetdir)) return 0 log.info('Installing: %s' % os.path.basename(targetdir)) ret = get_archive_check(outname, host, port, basedir, treeish) if ret == 0: ret = install_archive(outname, targetdir, verbose) if ret != 0: ret = 2 return ret def install_archive(outname, targetdir, verbose): ret = 1 log.info('Installing archive %s to %s', outname, targetdir) if not os.path.exists(targetdir): log.debug('Creating dir %s', targetdir) os.makedirs(targetdir) if not os.path.exists(targetdir): return ret archtype = outname[-3:] if archtype == 'zip': ret = install_zip(outname, targetdir, verbose) if archtype == 'tar': ret = install_tar(outname, targetdir, verbose) log.debug('install_archive ret code: %s', ret) return ret def install_zip(outname, targetdir, verbose): if verbose: quiet_flag = '' else: quiet_flag = '-q' cmd = 'unzip %s -o %s -d %s' % (quiet_flag, outname, targetdir) log.debug(cmd) ret = os.system(cmd) return ret def install_tar(outname, targetdir, verbose): cmd = 'tar -C %s --overwrite xf %s' % (targetdir, outname) log.debug(cmd) ret = os.system(cmd) return ret user_options = [('help', 'prints help'), ('outname', 'output file name, must end with tar or zip'), ('hostname', 'target host where git daemon is running'), ('port', 'port where git daemon is running'), ('basedir', 'location of git repo on host'), ('treeish', 'git tree-ish, which identifies some tree'), ('debug', 'show debug messages'), ('quiet', 'less messages'), ] def usage(): print 'gitarch.py: git-archive in python' print '' print 'Common switches:' for line in user_options: print ' --%s %s' % (line[0], line[1]) print print 'Example: ' print ' You have your git repo on host (127.0.0.1) at /usr/src/linux-stable' print ' You start git daemon:' print ' git-daemon --verbose --base-path=/usr/src --enable=upload-archive --export-all /usr/src/linux-stable' print ' Using git you would do:' print ' git archive --remote=git://127.0.0.1/linux-stable master:scripts > /tmp/a.tar' print ' Using gitarch.py:' print ' gitarch.py --outname=/tmp/a.tar --hostname=localhost --basedir=/linux-stable --treeish=master:scripts' def main(): outname = None hostname = None port = 9418 basedir = None treeish = None debug = False verbose = False quiet = False try: opts, args = getopt.getopt(sys.argv[1:], 'hdqv', ['help', 'outname=', 'hostname=', 'port=', 'basedir=', 'treeish=', 'debug', 'verbose', 'quiet']) except getopt.GetoptError, err: print str(err) usage() sys.exit(2) for o, a in opts: if o in ('-h', '--help'): usage() sys.exit() elif o in ('-d', '--debug'): debug = True elif o in ('-q', '--quiet'): quiet = True elif o in ('-v', '--verbose'): verbose = True elif o in ('--hostname='): hostname = a elif o in ('--outname='): outname = a elif o in ('--port='): port = a elif o in ('--basedir='): basedir = a elif o in ('--treeish='): treeish = a else: print o, a assert False, 'unhandled option' if not outname or not hostname or not port or not basedir or not treeish: usage() sys.exit() LOG_FORMAT = '%(asctime)s - %(levelname)s - %(filename)s - ' \ '%(funcName)s:%(lineno)s - %(message)s' if quiet: LOG_LEVEL = log.ERROR elif verbose: LOG_LEVEL = log.INFO elif debug: LOG_LEVEL = log.DEBUG else: LOG_LEVEL = log.WARN LOG_FORMAT = '%(message)s' formatter = logging.Formatter(LOG_FORMAT) stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) logger = logging.getLogger('') logger.addHandler(stdout_handler) logger.setLevel(LOG_LEVEL) ret = get_archive_check(outname, hostname, port, basedir, treeish) if ret > 255 or ret < 0: ret = 1 sys.exit(ret) if __name__ == '__main__': main()