diff --git a/gitosis/repository.py b/gitosis/repository.py index 17a8818..764c980 100644 --- a/gitosis/repository.py +++ b/gitosis/repository.py @@ -3,6 +3,15 @@ import subprocess from gitosis import util +class GitError(Exception): + """git failed""" + + def __str__(self): + return '%s: %s' % (self.__doc__, ': '.join(self.args)) + +class GitInitError(Exception): + """git init failed""" + def init( path, template=None, @@ -15,17 +24,98 @@ def init( args = [_git, 'init'] if template is not None: args.append('--template=%s' % template) - env = {} - env.update(os.environ) - env['GIT_DIR'] = '.' returncode = subprocess.call( args=args, cwd=path, close_fds=True, - env=env, + env=dict(GIT_DIR='.'), ) if returncode != 0: - raise RuntimeError( - ("Command '%r' returned non-zero exit status %d" - % (args, returncode)), - ) + raise GitInitError('exit status %d' % returncode) + + +class GitFastImportError(GitError): + """git fast-import failed""" + pass + +def fast_import( + git_dir, + commit_msg, + committer, + files, + ): + """ + Create an initial commit. + """ + init(path=git_dir) + child = subprocess.Popen( + args=['git', 'fast-import', '--quiet', '--date-format=now'], + cwd=git_dir, + stdin=subprocess.PIPE, + close_fds=True, + env=dict(GIT_DIR=git_dir), + ) + files = list(files) + for index, (path, content) in enumerate(files): + child.stdin.write("""\ +blob +mark :%(mark)d +data %(len)d +%(content)s +""" % dict( + mark=index+1, + len=len(content), + content=content, + )) + child.stdin.write("""\ +commit refs/heads/master +committer %(committer)s now +data %(commit_msg_len)d +%(commit_msg)s +""" % dict( + committer=committer, + commit_msg_len=len(commit_msg), + commit_msg=commit_msg, + )) + for index, (path, content) in enumerate(files): + child.stdin.write('M 100644 :%d %s\n' % (index+1, path)) + child.stdin.close() + returncode = child.wait() + if returncode != 0: + raise GitFastImportError( + 'git fast-import failed', 'exit status %d' % returncode) + +class GitExportError(GitError): + """Export failed""" + pass + +class GitReadTreeError(GitExportError): + """git read-tree failed""" + +class GitCheckoutIndexError(GitExportError): + """git checkout-index failed""" + +def export(git_dir, path): + # it's a literal prefix for git, a trailing slash is needed to + # extract to the subdirectory + path = os.path.join(path, '') + returncode = subprocess.call( + args=['git', 'read-tree', 'HEAD'], + close_fds=True, + env=dict(GIT_DIR=git_dir), + ) + if returncode != 0: + raise GitReadTreeError('exit status %d' % returncode) + returncode = subprocess.call( + args=[ + 'git', + 'checkout-index', + '-a', + '-f', + '--prefix=%s' % path, + ], + close_fds=True, + env=dict(GIT_DIR=git_dir), + ) + if returncode != 0: + raise GitCheckoutIndexError('exit status %d' % returncode) diff --git a/gitosis/test/test_repository.py b/gitosis/test/test_repository.py index c9fbc58..79d1561 100644 --- a/gitosis/test/test_repository.py +++ b/gitosis/test/test_repository.py @@ -1,6 +1,7 @@ from nose.tools import eq_ as eq import os +import subprocess from gitosis import repository @@ -55,3 +56,49 @@ def test_init_templates(): eq(got, '#!/bin/sh\n# i can override standard templates\n') # standard templates are there, too assert os.path.isfile(os.path.join(path, 'hooks', 'pre-rebase')) + +def test_export_simple(): + tmp = maketemp() + git_dir = os.path.join(tmp, 'repo.git') + repository.init(path=git_dir) + repository.fast_import( + git_dir=git_dir, + committer='John Doe ', + commit_msg="""\ +Reverse the polarity of the neutron flow. + +Frobitz the quux and eschew obfuscation. +""", + files=[ + ('foo', 'content'), + ('bar/quux', 'another'), + ], + ) + export = os.path.join(tmp, 'export') + repository.export(git_dir=git_dir, path=export) + eq(sorted(os.listdir(export)), + sorted(['foo', 'bar'])) + eq(readFile(os.path.join(export, 'foo')), 'content') + eq(os.listdir(os.path.join(export, 'bar')), ['quux']) + eq(readFile(os.path.join(export, 'bar', 'quux')), 'another') + child = subprocess.Popen( + args=['git', 'cat-file', 'commit', 'HEAD'], + cwd=git_dir, + stdout=subprocess.PIPE, + close_fds=True, + env=dict(GIT_DIR=git_dir), + ) + got = child.stdout.read().splitlines() + returncode = child.wait() + if returncode != 0: + raise RuntimeError('git exit status %d' % returncode) + eq(got[0].split(None, 1)[0], 'tree') + eq(got[1].rsplit(None, 2)[0], + 'author John Doe ') + eq(got[2].rsplit(None, 2)[0], + 'committer John Doe ') + eq(got[3], '') + eq(got[4], 'Reverse the polarity of the neutron flow.') + eq(got[5], '') + eq(got[6], 'Frobitz the quux and eschew obfuscation.') + eq(got[7:], [])