diff --git a/hgext/convert/common.py b/hgext/convert/common.py index e27346b..842ae71 100644 --- a/hgext/convert/common.py +++ b/hgext/convert/common.py @@ -295,6 +295,9 @@ class commandline(object): def _run2(self, cmd, *args, **kwargs): return self._dorun(util.popen2, cmd, *args, **kwargs) + def _run3(self, cmd, *args, **kwargs): + return self._dorun(util.popen3, cmd, *args, **kwargs) + def _dorun(self, openfunc, cmd, *args, **kwargs): cmdline = self._cmdline(cmd, *args, **kwargs) self.ui.debug('running: %s\n' % (cmdline,)) diff --git a/hgext/convert/git.py b/hgext/convert/git.py index 3fd0884..8123b55 100644 --- a/hgext/convert/git.py +++ b/hgext/convert/git.py @@ -7,11 +7,11 @@ import os import subprocess -from mercurial import util, config +from mercurial import util, config, error from mercurial.node import hex, nullid from mercurial.i18n import _ -from common import NoRepo, commit, converter_source, checktool +from common import NoRepo, commit, converter_source, checktool, commandline class submodule(object): def __init__(self, path, node, url): @@ -25,54 +25,32 @@ class submodule(object): def hgsubstate(self): return "%s %s" % (self.node, self.path) -class convert_git(converter_source): +class convert_git(converter_source, commandline): # Windows does not support GIT_DIR= construct while other systems # cannot remove environment variable. Just assume none have # both issues. - if util.safehasattr(os, 'unsetenv'): - def gitopen(self, s, err=None): - prevgitdir = os.environ.get('GIT_DIR') - os.environ['GIT_DIR'] = self.path - try: - if err == subprocess.PIPE: - (stdin, stdout, stderr) = util.popen3(s) - return stdout - elif err == subprocess.STDOUT: - return self.popen_with_stderr(s) - else: - return util.popen(s, 'rb') - finally: - if prevgitdir is None: - del os.environ['GIT_DIR'] - else: - os.environ['GIT_DIR'] = prevgitdir - else: - def gitopen(self, s, err=None): - if err == subprocess.PIPE: - (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s)) - return so - elif err == subprocess.STDOUT: - return self.popen_with_stderr(s) - else: - return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') - - def popen_with_stderr(self, s): - p = subprocess.Popen(s, shell=True, bufsize=-1, - close_fds=util.closefds, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=False, - env=None) - return p.stdout - - def gitread(self, s): - fh = self.gitopen(s) - data = fh.read() - return data, fh.close() + + def _gitcmd(self, cmd, *args, **kwargs): + return cmd('--git-dir=%s' % self.path, *args, **kwargs) + + def gitrun0(self, *args, **kwargs): + return self._gitcmd(self.run0, *args, **kwargs) + + def gitrun(self, *args, **kwargs): + return self._gitcmd(self.run, *args, **kwargs) + + def gitrunlines0(self, *args, **kwargs): + return self._gitcmd(self.runlines0, *args, **kwargs) + + def gitrunlines(self, *args, **kwargs): + return self._gitcmd(self.runlines, *args, **kwargs) + + def gitpipe(self, *args, **kwargs): + return self._gitcmd(self._run3, *args, **kwargs) def __init__(self, ui, path, rev=None): super(convert_git, self).__init__(ui, path, rev=rev) + commandline.__init__(self, ui, 'git') if os.path.isdir(path + "/.git"): path += "/.git" @@ -86,11 +64,11 @@ class convert_git(converter_source): def getheads(self): if not self.rev: - heads, ret = self.gitread('git rev-parse --branches --remotes') - heads = heads.splitlines() + output, ret = self.gitrun('rev-parse', '--branches', '--remotes') + heads = output.splitlines() else: - heads, ret = self.gitread("git rev-parse --verify %s" % self.rev) - heads = [heads[:-1]] + rawhead, ret = self.gitrun('rev-parse', '--verify', self.rev) + heads = [rawhead[:-1]] if ret: raise util.Abort(_('cannot retrieve git heads')) return heads @@ -98,7 +76,7 @@ class convert_git(converter_source): def catfile(self, rev, type): if rev == hex(nullid): raise IOError - data, ret = self.gitread("git cat-file %s %s" % (type, rev)) + data, ret = self.gitrun('cat-file', type, rev) if ret: raise util.Abort(_('cannot read %r object at %s') % (type, rev)) return data @@ -137,25 +115,28 @@ class convert_git(converter_source): self.submodules.append(submodule(s['path'], '', s['url'])) def retrievegitmodules(self, version): - modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules')) + modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules')) if ret: raise util.Abort(_('cannot read submodules config file in %s') % version) self.parsegitmodules(modules) for m in self.submodules: - node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path)) + node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path)) if ret: continue m.node = node.strip() def getchanges(self, version): self.modecache = {} - fh = self.gitopen("git diff-tree -z --root -m -r %s" % version) + cmd = ['diff-tree','-z', '--root', '-m', '-r'] + [version] + output, status = self.gitrun(*cmd) + if status: + raise error.Abort(_('cannot read changes in %s') % version) changes = [] seen = set() entry = None subexists = False - for l in fh.read().split('\x00'): + for l in output.split('\x00'): if not entry: if not l.startswith(':'): continue @@ -178,8 +159,6 @@ class convert_git(converter_source): self.modecache[(f, h)] = (p and "x") or (s and "l") or "" changes.append((f, h)) entry = None - if fh.close(): - raise util.Abort(_('cannot read changes in %s') % version) if subexists: self.retrievegitmodules(version) @@ -224,12 +203,14 @@ class convert_git(converter_source): def gettags(self): tags = {} alltags = {} - fh = self.gitopen('git ls-remote --tags "%s"' % self.path, - err=subprocess.STDOUT) + output, status = self.gitrunlines('ls-remote', '--tags', self.path) + + if status: + raise error.Abort(_('cannot read tags from %s') % self.path) prefix = 'refs/tags/' # Build complete list of tags, both annotated and bare ones - for line in fh: + for line in output: line = line.strip() if line.startswith("error:") or line.startswith("fatal:"): raise util.Abort(_('cannot read tags from %s') % self.path) @@ -237,8 +218,6 @@ class convert_git(converter_source): if not tag.startswith(prefix): continue alltags[tag[len(prefix):]] = node - if fh.close(): - raise util.Abort(_('cannot read tags from %s') % self.path) # Filter out tag objects for annotated tag refs for tag in alltags: @@ -255,18 +234,20 @@ class convert_git(converter_source): def getchangedfiles(self, version, i): changes = [] if i is None: - fh = self.gitopen("git diff-tree --root -m -r %s" % version) - for l in fh: + output, status = self.gitrunlines('diff-tree', '--root', '-m', + '-r', version) + if status: + raise error.Abort(_('cannot read changes in %s') % version) + for l in output: if "\t" not in l: continue m, f = l[:-1].split("\t") changes.append(f) else: - fh = self.gitopen('git diff-tree --name-only --root -r %s ' - '"%s^%s" --' % (version, version, i + 1)) - changes = [f.rstrip('\n') for f in fh] - if fh.close(): - raise util.Abort(_('cannot read changes in %s') % version) + output, status = self.gitrunlines('diff-tree', '--name-only', + '--root', '-r', version, + '%s^%s' % (version, i + 1), '--') + changes = [f.rstrip('\n') for f in output] return changes @@ -278,14 +259,14 @@ class convert_git(converter_source): prefixlen = len(prefix) # factor two commands - gitcmd = { 'remote/': 'git ls-remote --heads origin', - '': 'git show-ref'} + gitcmd = { 'remote/': ['ls-remote', '--heads origin'], + '': ['show-ref']} # Origin heads for reftype in gitcmd: try: - fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE) - for line in fh: + output, status = self.gitrunlines(*gitcmd[reftype]) + for line in output: line = line.strip() rev, name = line.split(None, 1) if not name.startswith(prefix): diff --git a/tests/test-convert-git.t b/tests/test-convert-git.t index 21f18d2..7eb068b 100644 --- a/tests/test-convert-git.t +++ b/tests/test-convert-git.t @@ -341,7 +341,7 @@ damage git repository by renaming a commit object $ COMMIT_OBJ=1c/0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd $ mv git-repo4/.git/objects/$COMMIT_OBJ git-repo4/.git/objects/$COMMIT_OBJ.tmp $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' - abort: cannot read tags from git-repo4/.git + abort: cannot retrieve number of commits in git-repo4/.git $ mv git-repo4/.git/objects/$COMMIT_OBJ.tmp git-repo4/.git/objects/$COMMIT_OBJ damage git repository by renaming a blob object @@ -356,3 +356,20 @@ damage git repository by renaming a tree object $ mv git-repo4/.git/objects/$TREE_OBJ git-repo4/.git/objects/$TREE_OBJ.tmp $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' abort: cannot read changes in 1c0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd + +test for escaping the repo name (CVE-2016-3069) + + $ git init '`echo pwned >COMMAND-INJECTION`' + Initialized empty Git repository in $TESTTMP/`echo pwned >COMMAND-INJECTION`/.git/ + $ cd '`echo pwned >COMMAND-INJECTION`' + $ git commit -q --allow-empty -m 'empty' + $ cd .. + $ hg convert '`echo pwned >COMMAND-INJECTION`' 'converted' + initializing destination converted repository + scanning source... + sorting... + converting... + 0 empty + updating bookmarks + $ test -f COMMAND-INJECTION + [1]