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