summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2014-08-21 12:58:46 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2014-08-21 16:04:41 +0000
commit23e86c643a82731aba6561321b8bb5b168e80598 (patch)
tree4e50e39d30b775d3e911cfd375ec4bbcfa793eb4
parent35f3a8779a316ed6a44ce5bb62bea9e2d7c67a16 (diff)
downloadmorph-23e86c643a82731aba6561321b8bb5b168e80598.tar.gz
Prevent git-replace refs affecting git operations
We assumed that the sha1 of the tree of the commit of the ref we care about was sufficient to cache, but `git replace` means that you need to know the state of other branches too, since anything in refs/replace can completely change what the tree you check-out is. This behaviour can be disabled globally by setting GIT_NO_REPLACE_OBJECTS, so we're going to do that. If we need to integrate a project that uses git-replace to change the contents of their git trees then we could support that by: if any(refs/replace/*): potentially_replacable_objects = [ `git rev-parse HEAD`, `git rev-parse HEAD^{commit}`, `git rev-parse HEAD^{tree}`] potentially_replacable_objects.extend( `git ls-tree -r HEAD | awk '{print $3}'`) # NOTE: doesn't handle submodules, and you'd need to expand this # set whenever you process a replacement for object in refs/replace/*: if basename(object) not in potentially_replacable_objects: continue cache_key['replacements'][basename(object)] = `git rev-parse $object` If we were to support this would need to traverse the tree anyway, doing replacements, so we may as well use libgit to do the checkout anyway, and list which replacements were used. However, since the expected use-case of `git replace` is as a better way to do history grafting, we're unlikely to need it, as it would only have any effect if it replaced the commit we were using with a different one. Rubber-stamped-by: Daniel Silverstone
-rw-r--r--morphlib/branchmanager_tests.py38
-rw-r--r--morphlib/cachedrepo.py22
-rw-r--r--morphlib/git.py71
-rw-r--r--morphlib/gitdir.py93
-rw-r--r--morphlib/gitdir_tests.py42
-rw-r--r--morphlib/gitindex.py5
-rw-r--r--morphlib/gitindex_tests.py9
-rw-r--r--morphlib/localrepocache.py4
-rw-r--r--morphlib/morphologyfinder_tests.py7
-rw-r--r--morphlib/sysbranchdir.py6
-rw-r--r--morphlib/sysbranchdir_tests.py13
11 files changed, 171 insertions, 139 deletions
diff --git a/morphlib/branchmanager_tests.py b/morphlib/branchmanager_tests.py
index a7988c96..cf3be73c 100644
--- a/morphlib/branchmanager_tests.py
+++ b/morphlib/branchmanager_tests.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2013 Codethink Limited
+# Copyright (C) 2013-2014 Codethink Limited
#
# 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
@@ -35,13 +35,13 @@ class LocalRefManagerTests(unittest.TestCase):
gd = morphlib.gitdir.init(dirname)
with open(os.path.join(dirname, 'foo'), 'w') as f:
f.write('dummy text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
- gd._runcmd(['git', 'checkout', '-b', 'dev-branch'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
+ morphlib.git.gitcmd(gd._runcmd, 'checkout', '-b', 'dev-branch')
with open(os.path.join(dirname, 'foo'), 'w') as f:
f.write('updated text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Second commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Second commit')
self.repos.append(gd)
def tearDown(self):
@@ -245,18 +245,19 @@ class RemoteRefManagerTests(unittest.TestCase):
self.sgd = morphlib.gitdir.init(self.source)
with open(os.path.join(self.source, 'foo'), 'w') as f:
f.write('dummy text\n')
- self.sgd._runcmd(['git', 'add', '.'])
- self.sgd._runcmd(['git', 'commit', '-m', 'Initial commit'])
- self.sgd._runcmd(['git', 'checkout', '-b', 'dev-branch'])
+ morphlib.git.gitcmd(self.sgd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(self.sgd._runcmd, 'commit', '-m', 'Initial commit')
+ morphlib.git.gitcmd(self.sgd._runcmd, 'checkout', '-b', 'dev-branch')
with open(os.path.join(self.source, 'foo'), 'w') as f:
f.write('updated text\n')
- self.sgd._runcmd(['git', 'add', '.'])
- self.sgd._runcmd(['git', 'commit', '-m', 'Second commit'])
- self.sgd._runcmd(['git', 'checkout', '--orphan', 'no-ff'])
+ morphlib.git.gitcmd(self.sgd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(self.sgd._runcmd, 'commit', '-m', 'Second commit')
+ morphlib.git.gitcmd(self.sgd._runcmd, 'checkout', '--orphan', 'no-ff')
with open(os.path.join(self.source, 'foo'), 'w') as f:
f.write('parallel dimension text\n')
- self.sgd._runcmd(['git', 'add', '.'])
- self.sgd._runcmd(['git', 'commit', '-m', 'Non-fast-forward commit'])
+ morphlib.git.gitcmd(self.sgd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(self.sgd._runcmd, 'commit', '-m',
+ 'Non-fast-forward commit')
self.remotes = []
for i in xrange(self.TARGET_COUNT):
@@ -264,11 +265,12 @@ class RemoteRefManagerTests(unittest.TestCase):
dirname = os.path.join(self.tempdir, name)
# Allow deleting HEAD
- cliapp.runcmd(['git', 'init', '--bare', dirname])
+ morphlib.git.gitcmd(cliapp.runcmd, 'init', '--bare', dirname)
gd = morphlib.gitdir.GitDirectory(dirname)
gd.set_config('receive.denyDeleteCurrent', 'warn')
- self.sgd._runcmd(['git', 'remote', 'add', name, dirname])
+ morphlib.git.gitcmd(self.sgd._runcmd, 'remote', 'add',
+ name, dirname)
self.remotes.append((name, dirname, gd))
def tearDown(self):
@@ -276,8 +278,8 @@ class RemoteRefManagerTests(unittest.TestCase):
@staticmethod
def list_refs(gd):
- out = gd._runcmd(['git', 'for-each-ref',
- '--format=%(refname)%00%(objectname)%00'])
+ out = morphlib.git.gitcmd(gd._runcmd, 'for-each-ref',
+ '--format=%(refname)%00%(objectname)%00')
return dict(line.split('\0') for line in
out.strip('\0\n').split('\0\n') if line)
diff --git a/morphlib/cachedrepo.py b/morphlib/cachedrepo.py
index 88ceed48..2fc7cfa5 100644
--- a/morphlib/cachedrepo.py
+++ b/morphlib/cachedrepo.py
@@ -260,20 +260,21 @@ class CachedRepo(object):
return self.app.runcmd(*args, **kwargs)
def _rev_parse(self, ref): # pragma: no cover
- return self._runcmd(
- ['git', 'rev-parse', '--verify', '%s^{commit}' % ref])[0:40]
+ return morphlib.git.gitcmd(self._runcmd, 'rev-parse', '--verify',
+ '%s^{commit}' % ref)[0:40]
def _show_tree_hash(self, absref): # pragma: no cover
- return self._runcmd(
- ['git', 'rev-parse', '--verify', '%s^{tree}' % absref]).strip()
+ return morphlib.git.gitcmd(self._runcmd, 'rev-parse', '--verify',
+ '%s^{tree}' % absref).strip()
def _ls_tree(self, ref): # pragma: no cover
- result = self._runcmd(['git', 'ls-tree', '--name-only', ref])
+ result = morphlib.git.gitcmd(self._runcmd, 'ls-tree',
+ '--name-only', ref)
return result.split('\n')
def _cat_file(self, ref, filename): # pragma: no cover
- return self._runcmd(['git', 'cat-file', 'blob',
- '%s:%s' % (ref, filename)])
+ return morphlib.git.gitcmd(self._runcmd, 'cat-file', 'blob',
+ '%s:%s' % (ref, filename))
def _clone_into(self, target_dir, ref): #pragma: no cover
'''Actually perform the clone'''
@@ -297,10 +298,11 @@ class CachedRepo(object):
def _update(self): # pragma: no cover
try:
- self._runcmd(['git', 'remote', 'update', 'origin', '--prune'])
+ morphlib.git.gitcmd(self._runcmd, 'remote', 'update',
+ 'origin', '--prune')
except cliapp.AppException, ae:
- self._runcmd(['git', 'remote', 'prune', 'origin'])
- self._runcmd(['git', 'remote', 'update', 'origin'])
+ morphlib.git.gitcmd(self._runcmd, 'remote', 'prune', 'origin')
+ morphlib.git.gitcmd(self._runcmd, 'remote', 'update', 'origin')
def __str__(self): # pragma: no cover
return self.url
diff --git a/morphlib/git.py b/morphlib/git.py
index ccd06323..deb72eb6 100644
--- a/morphlib/git.py
+++ b/morphlib/git.py
@@ -81,9 +81,9 @@ class Submodules(object):
def _read_gitmodules_file(self):
try:
# try to read the .gitmodules file from the repo/ref
- content = self.app.runcmd(
- ['git', 'cat-file', 'blob', '%s:.gitmodules' % self.ref],
- cwd=self.repo, ignore_fail=True)
+ content = gitcmd(self.app.runcmd, 'cat-file', 'blob',
+ '%s:.gitmodules' % self.ref, cwd=self.repo,
+ ignore_fail=True)
# drop indentation in sections, as RawConfigParser cannot handle it
return '\n'.join([line.strip() for line in content.splitlines()])
@@ -105,8 +105,8 @@ class Submodules(object):
try:
# list objects in the parent repo tree to find the commit
# object that corresponds to the submodule
- commit = self.app.runcmd(['git', 'ls-tree', self.ref,
- submodule.name], cwd=self.repo)
+ commit = gitcmd(self.app.runcmd, 'ls-tree', self.ref,
+ submodule.name, cwd=self.repo)
# read the commit hash from the output
fields = commit.split()
@@ -149,15 +149,13 @@ def update_submodules(app, repo_dir): # pragma: no cover
if os.path.exists(os.path.join(repo_dir, '.gitmodules')):
resolver = morphlib.repoaliasresolver.RepoAliasResolver(
app.settings['repo-alias'])
- app.runcmd(['git', 'submodule', 'init'], cwd=repo_dir)
+ gitcmd(app.runcmd, 'submodule', 'init', cwd=repo_dir)
submodules = Submodules(app, repo_dir, 'HEAD')
submodules.load()
for submodule in submodules:
- app.runcmd(['git', 'config',
- 'submodule.%s.url' % submodule.name,
- resolver.pull_url(submodule.url)],
- cwd=repo_dir)
- app.runcmd(['git', 'submodule', 'update'], cwd=repo_dir)
+ gitcmd(app.runcmd, 'config', 'submodule.%s.url' % submodule.name,
+ resolver.pull_url(submodule.url), cwd=repo_dir)
+ gitcmd(app.runcmd, 'submodule', 'update', cwd=repo_dir)
class ConfigNotSetException(cliapp.AppException):
@@ -217,7 +215,7 @@ def check_config_set(runcmd, keys, cwd='.'):
found = {}
for key in keys:
try:
- value = runcmd(['git', 'config', key], cwd=cwd,
+ value = gitcmd(runcmd, 'config', key, cwd=cwd,
print_command=False).strip()
found[key] = value
except cliapp.AppException:
@@ -229,7 +227,7 @@ def check_config_set(runcmd, keys, cwd='.'):
def set_remote(runcmd, gitdir, name, url):
'''Set remote with name 'name' use a given url at gitdir'''
- return runcmd(['git', 'remote', 'set-url', name, url], cwd=gitdir)
+ return gitcmd(runcmd, 'remote', 'set-url', name, url, cwd=gitdir)
def copy_repository(runcmd, repo, destdir, is_mirror=True):
@@ -246,16 +244,16 @@ def copy_repository(runcmd, repo, destdir, is_mirror=True):
runcmd(['cp', '-a', repo, os.path.join(destdir, '.git')])
# core.bare should be false so that git believes work trees are possible
- runcmd(['git', 'config', 'core.bare', 'false'], cwd=destdir)
+ gitcmd(runcmd, 'config', 'core.bare', 'false', cwd=destdir)
# we do not want the origin remote to behave as a mirror for pulls
- runcmd(['git', 'config', '--unset', 'remote.origin.mirror'], cwd=destdir)
+ gitcmd(runcmd, 'config', '--unset', 'remote.origin.mirror', cwd=destdir)
# we want a traditional refs/heads -> refs/remotes/origin ref mapping
- runcmd(['git', 'config', 'remote.origin.fetch',
- '+refs/heads/*:refs/remotes/origin/*'], cwd=destdir)
+ gitcmd(runcmd, 'config', 'remote.origin.fetch',
+ '+refs/heads/*:refs/remotes/origin/*', cwd=destdir)
# set the origin url to the cached repo so that we can quickly clean up
- runcmd(['git', 'config', 'remote.origin.url', repo], cwd=destdir)
+ gitcmd(runcmd, 'config', 'remote.origin.url', repo, cwd=destdir)
# by packing the refs, we can then edit then en-masse easily
- runcmd(['git', 'pack-refs', '--all', '--prune'], cwd=destdir)
+ gitcmd(runcmd, 'pack-refs', '--all', '--prune', cwd=destdir)
# turn refs/heads/* into refs/remotes/origin/* in the packed refs
# so that the new copy behaves more like a traditional clone.
logging.debug("Adjusting packed refs for %s" % destdir)
@@ -273,12 +271,12 @@ def copy_repository(runcmd, repo, destdir, is_mirror=True):
refline = "%s %s" % (sha, ref)
ref_fh.write("%s\n" % (refline))
# Finally run a remote update to clear up the refs ready for use.
- runcmd(['git', 'remote', 'update', 'origin', '--prune'], cwd=destdir)
+ gitcmd(runcmd, 'remote', 'update', 'origin', '--prune', cwd=destdir)
def checkout_ref(runcmd, gitdir, ref):
'''Checks out a specific ref/SHA1 in a git working tree.'''
- runcmd(['git', 'checkout', ref], cwd=gitdir)
+ gitcmd(runcmd, 'checkout', ref, cwd=gitdir)
gd = morphlib.gitdir.GitDirectory(gitdir)
if gd.has_fat():
gd.fat_init()
@@ -288,8 +286,8 @@ def checkout_ref(runcmd, gitdir, ref):
def index_has_changes(runcmd, gitdir):
'''Returns True if there are no staged changes to commit'''
try:
- runcmd(['git', 'diff-index', '--cached', '--quiet',
- '--ignore-submodules', 'HEAD'], cwd=gitdir)
+ gitcmd(runcmd, 'diff-index', '--cached', '--quiet',
+ '--ignore-submodules', 'HEAD', cwd=gitdir)
except cliapp.AppException:
return True
return False
@@ -298,20 +296,20 @@ def index_has_changes(runcmd, gitdir):
def reset_workdir(runcmd, gitdir):
'''Removes any differences between the current commit '''
'''and the status of the working directory'''
- runcmd(['git', 'clean', '-fxd'], cwd=gitdir)
- runcmd(['git', 'reset', '--hard', 'HEAD'], cwd=gitdir)
+ gitcmd(runcmd, 'clean', '-fxd', cwd=gitdir)
+ gitcmd(runcmd, 'reset', '--hard', 'HEAD', cwd=gitdir)
def clone_into(runcmd, srcpath, targetpath, ref=None):
'''Clones a repo in srcpath into targetpath, optionally directly at ref.'''
if ref is None:
- runcmd(['git', 'clone', srcpath, targetpath])
+ gitcmd(runcmd, 'clone', srcpath, targetpath)
elif is_valid_sha1(ref):
- runcmd(['git', 'clone', srcpath, targetpath])
- runcmd(['git', 'checkout', ref], cwd=targetpath)
+ gitcmd(runcmd, 'clone', srcpath, targetpath)
+ gitcmd(runcmd, 'checkout', ref)
else:
- runcmd(['git', 'clone', '-b', ref, srcpath, targetpath])
+ gitcmd(runcmd, 'clone', '-b', ref, srcpath, targetpath)
gd = morphlib.gitdir.GitDirectory(targetpath)
if gd.has_fat():
gd.fat_init()
@@ -324,4 +322,17 @@ def is_valid_sha1(ref):
def rev_parse(runcmd, gitdir, ref):
'''Find the sha1 for the given ref'''
- return runcmd(['git', 'rev-parse', '--verify', ref], cwd=gitdir)[0:40]
+ return gitcmd(runcmd, 'rev-parse', '--verify', ref, cwd=gitdir)[0:40]
+
+
+def gitcmd(runcmd, *args, **kwargs):
+ '''Run git commands safely'''
+ if 'env' not in kwargs:
+ kwargs['env'] = dict(os.environ)
+ # git replace means we can't trust that just the sha1 of the branch
+ # is enough to say what it contains, so we turn it off by setting
+ # the right flag in an environment variable.
+ kwargs['env']['GIT_NO_REPLACE_OBJECTS'] = '1'
+ cmdline = ['git']
+ cmdline.extend(args)
+ return runcmd(cmdline, **kwargs)
diff --git a/morphlib/gitdir.py b/morphlib/gitdir.py
index 6b04b773..40ac643f 100644
--- a/morphlib/gitdir.py
+++ b/morphlib/gitdir.py
@@ -240,13 +240,14 @@ class Remote(object):
def set_fetch_url(self, url):
self.fetch_url = url
if self.name is not None:
- self.gd._runcmd(['git', 'remote', 'set-url', self.name, url])
+ morphlib.git.gitcmd(self.gd._runcmd, 'remote', 'set-url',
+ self.name, url)
def set_push_url(self, url):
self.push_url = url
if self.name is not None:
- self.gd._runcmd(['git', 'remote', 'set-url', '--push',
- self.name, url])
+ morphlib.git.gitcmd(self.gd._runcmd, 'remote', 'set-url',
+ '--push', self.name, url)
def _get_remote_url(self, remote_name, kind):
# As distasteful as it is to parse the output of porcelain
@@ -261,7 +262,7 @@ class Remote(object):
# It is only possible to use git to get the push url by parsing
# `git remote -v` or `git remote show -n <remote>`, and `git
# remote -v` is easier to parse.
- output = self.gd._runcmd(['git', 'remote', '-v'])
+ output = morphlib.git.gitcmd(self.gd._runcmd, 'remote', '-v')
for line in output.splitlines():
words = line.split()
if (len(words) == 3 and
@@ -288,7 +289,8 @@ class Remote(object):
yield sha1, refname
def ls(self): # pragma: no cover
- out = self.gd._runcmd(['git', 'ls-remote', self.get_fetch_url()])
+ out = morphlib.git.gitcmd(self.gd._runcmd, 'ls-remote',
+ self.get_fetch_url())
return self._parse_ls_remote_output(out)
@staticmethod
@@ -320,10 +322,11 @@ class Remote(object):
if not refspecs:
raise NoRefspecsError(self)
push_name = self.name or self.get_push_url()
- cmdline = ['git', 'push', '--porcelain', push_name]
+ cmdline = ['push', '--porcelain', push_name]
cmdline.extend(itertools.chain.from_iterable(
rs.push_args for rs in refspecs))
- exit, out, err = self.gd._runcmd_unchecked(cmdline)
+ exit, out, err = morphlib.git.gitcmd(self.gd._runcmd_unchecked,
+ *cmdline)
if exit != 0:
raise PushFailureError(self, refspecs, exit,
self._parse_push_output(out), err)
@@ -332,9 +335,9 @@ class Remote(object):
def pull(self, branch=None): # pragma: no cover
if branch:
repo = self.get_fetch_url()
- ret = self.gd._runcmd(['git', 'pull', repo, branch])
+ ret = morphlib.git.gitcmd(self.gd._runcmd, 'pull', repo, branch)
else:
- ret = self.gd._runcmd(['git', 'pull'])
+ ret = morphlib.git.gitcmd(self.gd._runcmd, 'pull')
return ret
@@ -374,7 +377,7 @@ class GitDirectory(object):
def checkout(self, branch_name): # pragma: no cover
'''Check out a git branch.'''
- self._runcmd(['git', 'checkout', branch_name])
+ morphlib.git.gitcmd(self._runcmd, 'checkout', branch_name)
if self.has_fat():
self.fat_init()
self.fat_pull()
@@ -388,10 +391,10 @@ class GitDirectory(object):
'''
- argv = ['git', 'branch', new_branch_name]
+ argv = ['branch', new_branch_name]
if base_ref is not None:
argv.append(base_ref)
- self._runcmd(argv)
+ morphlib.git.gitcmd(self._runcmd, *argv)
def is_currently_checked_out(self, ref): # pragma: no cover
'''Is ref currently checked out?'''
@@ -399,12 +402,12 @@ class GitDirectory(object):
# Try the ref name directly first. If that fails, prepend origin/
# to it. (FIXME: That's a kludge, and should be fixed.)
try:
- parsed_ref = self._runcmd(['git', 'rev-parse', ref]).strip()
+ parsed_ref = morphlib.git.gitcmd(self._runcmd, 'rev-parse', ref)
except cliapp.AppException:
- parsed_ref = self._runcmd(
- ['git', 'rev-parse', 'origin/%s' % ref]).strip()
- parsed_head = self._runcmd(['git', 'rev-parse', 'HEAD']).strip()
- return parsed_ref == parsed_head
+ parsed_ref = morphlib.git.gitcmd(self._runcmd, 'rev-parse',
+ 'origin/%s' % ref)
+ parsed_head = morphlib.git.gitcmd(self._runcmd, 'rev-parse', 'HEAD')
+ return parsed_ref.strip() == parsed_head.strip()
def get_file_from_ref(self, ref, filename): # pragma: no cover
'''Get file contents from git by ref and filename.
@@ -426,13 +429,13 @@ class GitDirectory(object):
def get_blob_contents(self, blob_id): # pragma: no cover
'''Get file contents from git by ID'''
- return self._runcmd(
- ['git', 'cat-file', 'blob', blob_id])
+ return morphlib.git.gitcmd(self._runcmd, 'cat-file', 'blob',
+ blob_id)
def get_commit_contents(self, commit_id): # pragma: no cover
'''Get commit contents from git by ID'''
- return self._runcmd(
- ['git', 'cat-file', 'commit', commit_id])
+ return morphlib.git.gitcmd(self._runcmd, 'cat-file', 'commit',
+ commit_id)
def update_submodules(self, app): # pragma: no cover
'''Change .gitmodules URLs, and checkout submodules.'''
@@ -447,14 +450,14 @@ class GitDirectory(object):
'''
- self._runcmd(['git', 'config', key, value])
+ morphlib.git.gitcmd(self._runcmd, 'config', key, value)
self._config[key] = value
def get_config(self, key):
'''Return value for a git repository configuration variable.'''
if key not in self._config:
- value = self._runcmd(['git', 'config', '-z', key])
+ value = morphlib.git.gitcmd(self._runcmd, 'config', '-z', key)
self._config[key] = value.rstrip('\0')
return self._config[key]
@@ -469,7 +472,7 @@ class GitDirectory(object):
def update_remotes(self): # pragma: no cover
'''Run "git remote update --prune".'''
- self._runcmd(['git', 'remote', 'update', '--prune'])
+ morphlib.git.gitcmd(self._runcmd, 'remote', 'update', '--prune')
def is_bare(self):
'''Determine whether the repository has no work tree (is bare)'''
@@ -494,14 +497,15 @@ class GitDirectory(object):
def _rev_parse(self, ref):
try:
- return self._runcmd(['git', 'rev-parse', '--verify', ref]).strip()
+ return morphlib.git.gitcmd(self._runcmd, 'rev-parse',
+ '--verify', ref).strip()
except cliapp.AppException as e:
raise InvalidRefError(self, ref)
def disambiguate_ref(self, ref): # pragma: no cover
try:
- out = self._runcmd(['git', 'rev-parse', '--symbolic-full-name',
- ref])
+ out = morphlib.git.gitcmd(self._runcmd, 'rev-parse',
+ '--symbolic-full-name', ref)
return out.strip()
except cliapp.AppException: # ref not found
if ref.startswith('refs/heads/'):
@@ -527,7 +531,8 @@ class GitDirectory(object):
def _list_files_in_ref(self, ref):
tree = self.resolve_ref_to_tree(ref)
- output = self._runcmd(['git', 'ls-tree', '--name-only', '-rz', tree])
+ output = morphlib.git.gitcmd(self._runcmd, 'ls-tree',
+ '--name-only', '-rz', tree)
# ls-tree appends \0 instead of interspersing, so we need to
# strip the trailing \0 before splitting
paths = output.strip('\0').split('\0')
@@ -548,13 +553,15 @@ class GitDirectory(object):
if ref is None:
filepath = os.path.join(self.dirname, filename.lstrip('/'))
return os.path.islink(filepath)
- tree_entry = self._runcmd(['git', 'ls-tree', ref, filename])
+ tree_entry = morphlib.git.gitcmd(self._runcmd, 'ls-tree', ref,
+ filename)
file_mode = tree_entry.split(' ', 1)[0]
return file_mode == '120000'
@property
def HEAD(self):
- output = self._runcmd(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])
+ output = morphlib.git.gitcmd(self._runcmd, 'rev-parse',
+ '--abbrev-ref', 'HEAD')
return output.strip()
def get_index(self, index_file=None):
@@ -572,8 +579,8 @@ class GitDirectory(object):
kwargs = {'feed_stdin': blob_contents}
else:
kwargs = {'stdin': blob_contents}
- return self._runcmd(['git', 'hash-object', '-t', 'blob',
- '-w', '--stdin'], **kwargs).strip()
+ return morphlib.git.gitcmd(self._runcmd, 'hash-object', '-t', 'blob',
+ '-w', '--stdin', **kwargs).strip()
def commit_tree(self, tree, parent, message, **kwargs):
'''Create a commit'''
@@ -590,9 +597,9 @@ class GitDirectory(object):
envname = 'GIT_%s_DATE' % who.upper()
if argname in kwargs:
env[envname] = kwargs[argname].isoformat()
- return self._runcmd(['git', 'commit-tree', tree,
- '-p', parent, '-m', message],
- env=env).strip()
+ return morphlib.git.gitcmd(self._runcmd, 'commit-tree', tree,
+ '-p', parent, '-m', message,
+ env=env).strip()
@staticmethod
def _check_is_sha1(string):
@@ -600,14 +607,14 @@ class GitDirectory(object):
raise ExpectedSha1Error(string)
def _update_ref(self, ref_args, message):
- args = ['git', 'update-ref']
+ args = ['update-ref']
# No test coverage, since while this functionality is useful,
# morph does not need an API for inspecting the reflog, so
# it existing purely to test ref updates is a tad overkill.
if message is not None: # pragma: no cover
args.extend(('-m', message))
args.extend(ref_args)
- self._runcmd(args)
+ morphlib.git.gitcmd(self._runcmd, *args)
def add_ref(self, ref, sha1, message=None):
'''Create a ref called `ref` in the repository pointing to `sha1`.
@@ -666,18 +673,18 @@ class GitDirectory(object):
raise RefDeleteError(self, ref, old_sha1, e)
def describe(self):
- version = self._runcmd(
- ['git', 'describe', '--always', '--dirty=-unreproducible'])
+ version = morphlib.git.gitcmd(self._runcmd, 'describe',
+ '--always', '--dirty=-unreproducible')
return version.strip()
def fat_init(self): # pragma: no cover
- return self._runcmd(['git', 'fat', 'init'])
+ return morphlib.git.gitcmd(self._runcmd, 'fat', 'init')
def fat_push(self): # pragma: no cover
- return self._runcmd(['git', 'fat', 'push'])
+ return morphlib.git.gitcmd(self._runcmd, 'fat', 'push')
def fat_pull(self): # pragma: no cover
- return self._runcmd(['git', 'fat', 'pull'])
+ return morphlib.git.gitcmd(self._runcmd, 'fat', 'pull')
def has_fat(self): # pragma: no cover
return os.path.isfile(self.join_path('.gitfat'))
@@ -692,7 +699,7 @@ class GitDirectory(object):
def init(dirname):
'''Initialise a new git repository.'''
- cliapp.runcmd(['git', 'init'], cwd=dirname)
+ morphlib.git.gitcmd(cliapp.runcmd, 'init', cwd=dirname)
gd = GitDirectory(dirname)
return gd
diff --git a/morphlib/gitdir_tests.py b/morphlib/gitdir_tests.py
index b3b4a8ab..456e3716 100644
--- a/morphlib/gitdir_tests.py
+++ b/morphlib/gitdir_tests.py
@@ -71,12 +71,13 @@ class GitDirectoryContentsTests(unittest.TestCase):
for fn in ('foo', 'bar.morph', 'baz.morph', 'quux'):
with open(os.path.join(self.dirname, fn), "w") as f:
f.write('dummy morphology text')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
os.rename(os.path.join(self.dirname, 'foo'),
os.path.join(self.dirname, 'foo.morph'))
self.mirror = os.path.join(self.tempdir, 'mirror')
- gd._runcmd(['git', 'clone', '--mirror', self.dirname, self.mirror])
+ morphlib.git.gitcmd(gd._runcmd, 'clone', '--mirror', self.dirname,
+ self.mirror)
def tearDown(self):
shutil.rmtree(self.tempdir)
@@ -209,10 +210,11 @@ class GitDirectoryContentsTests(unittest.TestCase):
def test_describe(self):
gd = morphlib.gitdir.GitDirectory(self.dirname)
- gd._runcmd(['git', 'tag', '-a', '-m', 'Example', 'example', 'HEAD'])
+ morphlib.git.gitcmd(gd._runcmd, 'tag', '-a', '-m', 'Example',
+ 'example', 'HEAD')
self.assertEqual(gd.describe(), 'example-unreproducible')
- gd._runcmd(['git', 'reset', '--hard'])
+ morphlib.git.gitcmd(gd._runcmd, 'reset', '--hard')
self.assertEqual(gd.describe(), 'example')
@@ -227,10 +229,11 @@ class GitDirectoryFileTypeTests(unittest.TestCase):
f.write('dummy morphology text')
os.symlink('file', os.path.join(self.dirname, 'link'))
os.symlink('no file', os.path.join(self.dirname, 'broken'))
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
self.mirror = os.path.join(self.tempdir, 'mirror')
- gd._runcmd(['git', 'clone', '--mirror', self.dirname, self.mirror])
+ morphlib.git.gitcmd(gd._runcmd, 'clone', '--mirror', self.dirname,
+ self.mirror)
def tearDown(self):
shutil.rmtree(self.tempdir)
@@ -262,14 +265,14 @@ class GitDirectoryRefTwiddlingTests(unittest.TestCase):
gd = morphlib.gitdir.init(self.dirname)
with open(os.path.join(self.dirname, 'foo'), 'w') as f:
f.write('dummy text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
# Add a second commit for update_ref test, so it has another
# commit to roll back from
with open(os.path.join(self.dirname, 'bar'), 'w') as f:
f.write('dummy text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Second commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Second commit')
def tearDown(self):
shutil.rmtree(self.tempdir)
@@ -347,7 +350,8 @@ class GitDirectoryRemoteConfigTests(unittest.TestCase):
self.assertEqual(remote.get_fetch_url(), None)
self.assertEqual(remote.get_push_url(), None)
- gitdir._runcmd(['git', 'remote', 'add', 'origin', 'foobar'])
+ morphlib.git.gitcmd(gitdir._runcmd, 'remote', 'add', 'origin',
+ 'foobar')
fetch_url = 'git://git.example.com/foo.git'
push_url = 'ssh://git@git.example.com/foo.git'
remote.set_fetch_url(fetch_url)
@@ -424,15 +428,15 @@ class GitDirectoryRemotePushTests(unittest.TestCase):
gd = morphlib.gitdir.init(self.dirname)
with open(os.path.join(self.dirname, 'foo'), 'w') as f:
f.write('dummy text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
- gd._runcmd(['git', 'checkout', '-b', 'foo'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
+ morphlib.git.gitcmd(gd._runcmd, 'checkout', '-b', 'foo')
with open(os.path.join(self.dirname, 'foo'), 'w') as f:
f.write('updated text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Second commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Second commit')
self.mirror = os.path.join(self.tempdir, 'mirror')
- gd._runcmd(['git', 'init', '--bare', self.mirror])
+ morphlib.git.gitcmd(gd._runcmd, 'init', '--bare', self.mirror)
def tearDown(self):
shutil.rmtree(self.tempdir)
diff --git a/morphlib/gitindex.py b/morphlib/gitindex.py
index 6be4aacb..e22f6225 100644
--- a/morphlib/gitindex.py
+++ b/morphlib/gitindex.py
@@ -17,6 +17,7 @@
import collections
+import os
import morphlib
@@ -47,9 +48,9 @@ class GitIndex(object):
def _run_git(self, *args, **kwargs):
if self._index_file is not None:
- kwargs['env'] = kwargs.get('env', {})
+ kwargs['env'] = kwargs.get('env', dict(os.environ))
kwargs['env']['GIT_INDEX_FILE'] = self._index_file
- return self._gd._runcmd(['git'] + list(args), **kwargs)
+ return morphlib.git.gitcmd(self._gd._runcmd, *args, **kwargs)
def _get_status(self):
'''Return git status output in a Python useful format
diff --git a/morphlib/gitindex_tests.py b/morphlib/gitindex_tests.py
index 7a8953f2..32d40a8c 100644
--- a/morphlib/gitindex_tests.py
+++ b/morphlib/gitindex_tests.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2013 Codethink Limited
+# Copyright (C) 2013-2014 Codethink Limited
#
# 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
@@ -33,10 +33,11 @@ class GitIndexTests(unittest.TestCase):
gd = morphlib.gitdir.init(self.dirname)
with open(os.path.join(self.dirname, 'foo'), 'w') as f:
f.write('dummy text\n')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
self.mirror = os.path.join(self.tempdir, 'mirror')
- gd._runcmd(['git', 'clone', '--mirror', self.dirname, self.mirror])
+ morphlib.git.gitcmd(gd._runcmd, 'clone', '--mirror', self.dirname,
+ self.mirror)
def tearDown(self):
shutil.rmtree(self.tempdir)
diff --git a/morphlib/localrepocache.py b/morphlib/localrepocache.py
index 9c20e4bc..8d2030c4 100644
--- a/morphlib/localrepocache.py
+++ b/morphlib/localrepocache.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012-2013 Codethink Limited
+# Copyright (C) 2012-2014 Codethink Limited
#
# 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
@@ -110,7 +110,7 @@ class LocalRepoCache(object):
'''
- self._app.runcmd(['git'] + args, cwd=cwd)
+ morphlib.git.gitcmd(self._app.runcmd, *args, cwd=cwd)
def _fetch(self, url, path): # pragma: no cover
'''Fetch contents of url into a file.
diff --git a/morphlib/morphologyfinder_tests.py b/morphlib/morphologyfinder_tests.py
index b07b2613..67161f9b 100644
--- a/morphlib/morphologyfinder_tests.py
+++ b/morphlib/morphologyfinder_tests.py
@@ -34,8 +34,8 @@ class MorphologyFinderTests(unittest.TestCase):
for fn in ('foo', 'bar.morph', 'baz.morph', 'quux'):
with open(os.path.join(self.dirname, fn), "w") as f:
f.write('dummy morphology text')
- gd._runcmd(['git', 'add', '.'])
- gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ morphlib.git.gitcmd(gd._runcmd, 'add', '.')
+ morphlib.git.gitcmd(gd._runcmd, 'commit', '-m', 'Initial commit')
# Changes for difference between commited and work tree
newmorphpath = os.path.join(self.dirname, 'foo.morph')
@@ -45,7 +45,8 @@ class MorphologyFinderTests(unittest.TestCase):
# Changes for bare repository
self.mirror = os.path.join(self.tempdir, 'mirror')
- gd._runcmd(['git', 'clone', '--mirror', self.dirname, self.mirror])
+ morphlib.git.gitcmd(gd._runcmd, 'clone', '--mirror', self.dirname,
+ self.mirror)
def tearDown(self):
shutil.rmtree(self.tempdir)
diff --git a/morphlib/sysbranchdir.py b/morphlib/sysbranchdir.py
index 19fba695..4351c6b3 100644
--- a/morphlib/sysbranchdir.py
+++ b/morphlib/sysbranchdir.py
@@ -61,11 +61,13 @@ class SystemBranchDirectory(object):
def set_config(self, key, value):
'''Set a configuration key/value pair.'''
- cliapp.runcmd(['git', 'config', '-f', self._config_path, key, value])
+ morphlib.git.gitcmd(cliapp.runcmd, 'config', '-f',
+ self._config_path, key, value)
def get_config(self, key):
'''Get a configuration value for a given key.'''
- value = cliapp.runcmd(['git', 'config', '-f', self._config_path, key])
+ value = morphlib.git.gitcmd(cliapp.runcmd, 'config', '-f',
+ self._config_path, key)
return value.strip()
def _find_git_directory(self, repo_url):
diff --git a/morphlib/sysbranchdir_tests.py b/morphlib/sysbranchdir_tests.py
index 8b40f69c..1aca54e6 100644
--- a/morphlib/sysbranchdir_tests.py
+++ b/morphlib/sysbranchdir_tests.py
@@ -50,16 +50,17 @@ class SystemBranchDirectoryTests(unittest.TestCase):
self.path = path
os.mkdir(self.path)
- cliapp.runcmd(['git', 'init', self.path])
+ morphlib.git.gitcmd(cliapp.runcmd, 'init', self.path)
with open(os.path.join(self.path, 'filename'), 'w') as f:
f.write('this is a file\n')
- cliapp.runcmd(['git', 'add', 'filename'], cwd=self.path)
- cliapp.runcmd(
- ['git', 'commit', '-m', 'initial'], cwd=self.path)
+ morphlib.git.gitcmd(cliapp.runcmd, 'add', 'filename',
+ cwd=self.path)
+ morphlib.git.gitcmd(cliapp.runcmd, 'commit', '-m', 'initial',
+ cwd=self.path)
def clone_checkout(self, ref, target_dir):
- cliapp.runcmd(
- ['git', 'clone', '-b', ref, self.path, target_dir])
+ morphlib.git.gitcmd(cliapp.runcmd, 'clone', '-b', ref,
+ self.path, target_dir)
subdir = tempfile.mkdtemp(dir=self.tempdir)
path = os.path.join(subdir, 'foo')