Review Board

beta

Properly escape execute commands and handle environment variables cross-platform

Updated 4 months, 2 weeks ago

Christian Hammond Reviewers
trunk reviewboard
423, 543, 582, 594
None Review Board SVN
We were relying on some really bad assumptions in post-review. First, as of r1415, we were assuming all parameters to diff should be quoted, which broke post-review with Perforce when it expected a value that could be cast to an integer. We now represent all commands as lists so that we can use subprocess's support for auto-escaping/quoting of a command and parameters. This should remove the hacks we use and prevent future problems.

Second, we were assuming that we could call "env" to set an environment variable for Subversion. This of course doesn't work on Windows. We should have instead used subprocess.Popen's native support for setting the environment.
Couldn't test other SCMTools due to not having a setup here, but I used this code to post this review request.

Diff revision 1 (Latest)

  1. /trunk/reviewboard/contrib/tools/post-review: 40 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ]
/trunk/reviewboard/contrib/tools/post-review
Revision 1453 New Change
539
539
540
        CVS repositories do not support branches of branches in a way that
540
        CVS repositories do not support branches of branches in a way that
541
        makes parent diffs possible, so we never return a parent diff
541
        makes parent diffs possible, so we never return a parent diff
542
        (the second value in the tuple).
542
        (the second value in the tuple).
543
        """
543
        """
544
        return (self.do_diff(' '.join(files)), None)
544
        return (self.do_diff(files), None)
545
545
546
    def diff_between_revisions(self, revision_range):
546
    def diff_between_revisions(self, revision_range):
547
        """
547
        """
548
        Performs a diff between 2 revisions of a CVS repository.
548
        Performs a diff between 2 revisions of a CVS repository.
549
        """
549
        """
550
        rev_str = ''
550
        revs = []
551
551
552
        for rev in revision_range.split(":"):
552
        for rev in revision_range.split(":"):
553
            rev_str += "-r %s" % rev
553
            revs += ["-r", rev]
554
554
555
        return self.do_diff(rev_str)
555
        return self.do_diff(revs)
556
556
557
    def do_diff(self, params):
557
    def do_diff(self, params):
558
        """
558
        """
559
        Performs the actual diff operation through cvs diff, handling
559
        Performs the actual diff operation through cvs diff, handling
560
        fake errors generated by CVS.
560
        fake errors generated by CVS.
561
        """
561
        """
562
        # Diff returns "1" if differences were found.
562
        # Diff returns "1" if differences were found.
563
        return execute('cvs diff -uN %s' % params,
563
        return execute(["cvs", "diff", "-uN"] + params,
564
                        extra_ignore_errors=(1,))
564
                        extra_ignore_errors=(1,))
565
565
566
566
567
class SVNClient(SCMClient):
567
class SVNClient(SCMClient):
568
    """
568
    """
569
    A wrapper around the svn Subversion tool that fetches repository
569
    A wrapper around the svn Subversion tool that fetches repository
570
    information and generates compatible diffs.
570
    information and generates compatible diffs.
571
    """
571
    """
572
    def get_repository_info(self):
572
    def get_repository_info(self):
573
        if not check_install('svn help'):
573
        if not check_install('svn help'):
574
            return None
574
            return None
575
575
576
        data = execute('env LANG=en_US.UTF-8 svn info', ignore_errors=True)
576
        data = execute(["svn", "info"],
577
577
                       env={'LANG': 'en_US.UTF-8'},
578
                       ignore_errors=True)
578
        m = re.search(r'^Repository Root: (.+)$', data, re.M)
579
        m = re.search(r'^Repository Root: (.+)$', data, re.M)
579
        if not m:
580
        if not m:
580
            return None
581
            return None
581
582
582
        path = m.group(1)
583
        path = m.group(1)
604
605
605
        return self.scan_for_server_property(repository_info)
606
        return self.scan_for_server_property(repository_info)
606
607
607
    def scan_for_server_property(self, repository_info):
608
    def scan_for_server_property(self, repository_info):
608
        def get_url_prop(path):
609
        def get_url_prop(path):
609
            url = execute("env LANG=en_US.UTF-8 svn propget reviewboard:url %s" % path).strip()
610
            url = execute(["svn", "propget", "reviewboard:url", path],
611
                          env={'LANG': 'en_US.UTF-8'}).strip()
610
            return url or None
612
            return url or None
611
613
612
        for path in walk_parents(os.getcwd()):
614
        for path in walk_parents(os.getcwd()):
613
            if not os.path.exists(os.path.join(path, ".svn")):
615
            if not os.path.exists(os.path.join(path, ".svn")):
614
                break
616
                break
625
627
626
        SVN repositories do not support branches of branches in a way that
628
        SVN repositories do not support branches of branches in a way that
627
        makes parent diffs possible, so we never return a parent diff
629
        makes parent diffs possible, so we never return a parent diff
628
        (the second value in the tuple).
630
        (the second value in the tuple).
629
        """
631
        """
630
        return (self.do_diff('env LANG=en_US.UTF-8 svn diff --diff-cmd=diff %s' % ' '.join(files)),
632
        return (self.do_diff(["svn", "diff", "--diff-cmd=diff"] + files),
631
                None)
633
                None)
632
634
633
    def diff_between_revisions(self, revision_range):
635
    def diff_between_revisions(self, revision_range):
634
        """
636
        """
635
        Performs a diff between 2 revisions of a Subversion repository.
637
        Performs a diff between 2 revisions of a Subversion repository.
636
        """
638
        """
637
        return self.do_diff('env LANG=en_US.UTF-8 svn diff --diff-cmd=diff -r %s' % revision_range)
639
        return self.do_diff(["svn", "diff", "--diff-cmd=diff", "-r",
640
                             revision_range])
638
641
639
    def do_diff(self, cmd):
642
    def do_diff(self, cmd):
640
        """
643
        """
641
        Performs the actual diff operation, handling renames and converting
644
        Performs the actual diff operation, handling renames and converting
642
        paths to absolute.
645
        paths to absolute.
643
        """
646
        """
644
        diff = execute(cmd, split_lines=True)
647
        diff = execute(cmd, env={'LANG': 'en_US.UTF-8'}, split_lines=True)
645
        diff = self.handle_renames(diff)
648
        diff = self.handle_renames(diff)
646
        diff = self.convert_to_absolute_paths(diff)
649
        diff = self.convert_to_absolute_paths(diff)
647
650
648
        return ''.join(diff)
651
        return ''.join(diff)
649
652
716
        return result
719
        return result
717
720
718
    def svn_info(self, path):
721
    def svn_info(self, path):
719
        """Return a dict which is the result of 'svn info' at a given path."""
722
        """Return a dict which is the result of 'svn info' at a given path."""
720
        svninfo = {}
723
        svninfo = {}
721
        for info in execute("env LANG=en_US.UTF-8 svn info '%s'" % path).splitlines():
724
        for info in execute(["svn", "info", path],
725
                            env={'LANG': 'en_US.UTF-8'},
726
                            split_lines=True):
722
            parts = info.split(": ", 1)
727
            parts = info.split(": ", 1)
723
            if len(parts) == 2:
728
            if len(parts) == 2:
724
                key, value = parts
729
                key, value = parts
725
                svninfo[key] = value
730
                svninfo[key] = value
726
731
757
    """
762
    """
758
    def get_repository_info(self):
763
    def get_repository_info(self):
759
        if not check_install('p4 help'):
764
        if not check_install('p4 help'):
760
            return None
765
            return None
761
766
762
        data = execute('p4 info', ignore_errors=True)
767
        data = execute(["p4", "info"], ignore_errors=True)
763
768
764
        m = re.search(r'^Server address: (.+)$', data, re.M)
769
        m = re.search(r'^Server address: (.+)$', data, re.M)
765
        if not m:
770
        if not m:
766
            return None
771
            return None
767
772
796
        except ValueError:
801
        except ValueError:
797
            die("You must enter a valid change number")
802
            die("You must enter a valid change number")
798
803
799
        debug("Generating diff for changenum %s" % changenum)
804
        debug("Generating diff for changenum %s" % changenum)
800
805
801
        description = execute('p4 describe -s %d' % changenum).splitlines()
806
        description = execute(["p4", "describe", "-s", str(changenum)],
807
                              split_lines=True)
802
        if '*pending*' in description[0]:
808
        if '*pending*' in description[0]:
803
            cl_is_pending = True
809
            cl_is_pending = True
804
810
805
        # Get the file list
811
        # Get the file list
806
        for line_num, line in enumerate(description):
812
        for line_num, line in enumerate(description):
885
                old_file = tmp_diff_from_filename
891
                old_file = tmp_diff_from_filename
886
                changetype_short = "D"
892
                changetype_short = "D"
887
            else:
893
            else:
888
                die("Unknown change type '%s' for %s" % (changetype, depot_path))
894
                die("Unknown change type '%s' for %s" % (changetype, depot_path))
889
895
890
            diff_cmd = 'diff -urNp "%s" "%s"' % (old_file, new_file)
896
            diff_cmd = ["diff", "-urNp", old_file, new_file]
891
            # Diff returns "1" if differences were found.
897
            # Diff returns "1" if differences were found.
892
            dl = execute(diff_cmd, extra_ignore_errors=(1,2)).splitlines(True)
898
            dl = execute(diff_cmd, extra_ignore_errors=(1,2)).splitlines(True)
893
899
894
            if local_name.startswith(cwd):
900
            if local_name.startswith(cwd):
895
                local_path = local_name[len(cwd) + 1:]
901
                local_path = local_name[len(cwd) + 1:]
958
        Grabs a file from Perforce and writes it to a temp file. We do this
964
        Grabs a file from Perforce and writes it to a temp file. We do this
959
        wrather than telling p4 print to write it out in order to work around
965
        wrather than telling p4 print to write it out in order to work around
960
        a permissions bug on Windows.
966
        a permissions bug on Windows.
961
        """
967
        """
962
        debug('Writing "%s" to "%s"' % (depot_path, tmpfile))
968
        debug('Writing "%s" to "%s"' % (depot_path, tmpfile))
963
        data = execute('p4 print -q "%s"' % depot_path)
969
        data = execute(["p4", "print", "-q", depot_path])
964
970
965
        f = open(tmpfile, "w")
971
        f = open(tmpfile, "w")
966
        f.write(data)
972
        f.write(data)
967
        f.close()
973
        f.close()
968
974
969
    def _depot_to_local(self, depot_path):
975
    def _depot_to_local(self, depot_path):
970
        """
976
        """
971
        Given a path in the depot return the path on the local filesystem to
977
        Given a path in the depot return the path on the local filesystem to
972
        the same file.
978
        the same file.
973
        """
979
        """
974
        # $ p4 where //user/bvanzant/main/testing
980
        # $ p4 where //user/bvanzant/main/testing
975
        # //user/bvanzant/main/testing //bvanzant:test05:home/testing /home/bvanzant/home-versioned/testing
981
        # //user/bvanzant/main/testing //bvanzant:test05:home/testing /home/bvanzant/home-versioned/testing
976
        cmd = 'p4 where "%s"' % (depot_path,)
982
        where_output = execute(["p4", "where", depot_path], split_lines=True)
977
        where_output = execute(cmd).splitlines()
978
        # Take only the last line from the where command.  If you have a
983
        # Take only the last line from the where command.  If you have a
979
        # multi-line view mapping with exclusions, Perforce will display
984
        # multi-line view mapping with exclusions, Perforce will display
980
        # the exclusions in order, with the last line showing the actual
985
        # the exclusions in order, with the last line showing the actual
981
        # location.
986
        # location.
982
        last_line = where_output[-1]
987
        last_line = where_output[-1]
992
    """
997
    """
993
    def get_repository_info(self):
998
    def get_repository_info(self):
994
        if not check_install('hg --help'):
999
        if not check_install('hg --help'):
995
            return None
1000
            return None
996
1001
997
        data = execute('hg root', ignore_errors=True)
1002
        data = execute(["hg", "root"], ignore_errors=True)
998
        if data.startswith('abort:'):
1003
        if data.startswith('abort:'):
999
            # hg aborted => no mercurial repository here.
1004
            # hg aborted => no mercurial repository here.
1000
            return None
1005
            return None
1001
1006
1002
        # Elsewhere, hg root output give us the repository path.
1007
        # Elsewhere, hg root output give us the repository path.
1029
        """
1034
        """
1030
        Performs a diff across all modified files in a Mercurial repository.
1035
        Performs a diff across all modified files in a Mercurial repository.
1031
        """
1036
        """
1032
        # We don't support parent diffs with Mercurial yet, so return None
1037
        # We don't support parent diffs with Mercurial yet, so return None
1033
        # for the parent diff.
1038
        # for the parent diff.
1034
        return (execute('hg diff %s' % ' '.join(files)), None)
1039
        return (execute(["hg", "diff"] + files), None)
1035
1040
1036
    def diff_between_revisions(self, revision_range):
1041
    def diff_between_revisions(self, revision_range):
1037
        """
1042
        """
1038
        Performs a diff between 2 revisions of a Mercurial repository.
1043
        Performs a diff between 2 revisions of a Mercurial repository.
1039
        """
1044
        """
1040
        r1, r2 = revision_range.split(':')
1045
        r1, r2 = revision_range.split(':')
1041
        return execute('hg diff -r %s -r %s' % (r1, r2))
1046
        return execute(["hg", "diff", "-r", r1, "-r", r2])
1042
1047
1043
1048
1044
class GitClient(SCMClient):
1049
class GitClient(SCMClient):
1045
    """
1050
    """
1046
    A wrapper around git that fetches repository information and generates
1051
    A wrapper around git that fetches repository information and generates
1047
    compatible diffs. This will attempt to generate a diff suitable for the
1052
    compatible diffs. This will attempt to generate a diff suitable for the
1048
    remote repository, whether git, SVN or Perforce.
1053
    remote repository, whether git, SVN or Perforce.
1049
    """
1054
    """
1050
    def get_repository_info(self):
1055
    def get_repository_info(self):
1051
        if not check_install('git --help'):
1056
        if not check_install('git --help'):
1052
            return None
1057
            return None
1053
1058
1054
        git_dir = execute('git rev-parse --git-dir', ignore_errors=True).strip()
1059
        git_dir = execute(["git", "rev-parse", "--git-dir"],
1060
                          ignore_errors=True).strip()
1055
1061
1056
        if git_dir.startswith("fatal:") or not os.path.isdir(git_dir):
1062
        if git_dir.startswith("fatal:") or not os.path.isdir(git_dir):
1057
            return None
1063
            return None
1058
1064
1059
        # We know we have something we can work with. Let's find out
1065
        # We know we have something we can work with. Let's find out
1060
        # what it is. We'll try SVN first.
1066
        # what it is. We'll try SVN first.
1061
        data = execute("git svn info", ignore_errors=True)
1067
        data = execute(["git", "svn", "info"], ignore_errors=True)
1062
1068
1063
        m = re.search(r'^Repository Root: (.+)$', data, re.M)
1069
        m = re.search(r'^Repository Root: (.+)$', data, re.M)
1064
        if m:
1070
        if m:
1065
            path = m.group(1)
1071
            path = m.group(1)
1066
            m = re.search(r'^URL: (.+)$', data, re.M)
1072
            m = re.search(r'^URL: (.+)$', data, re.M)
1073
        else:
1079
        else:
1074
            # Versions of git-svn before 1.5.4 don't (appear to) support
1080
            # Versions of git-svn before 1.5.4 don't (appear to) support
1075
            # 'git svn info'.  If we fail because of an older git install,
1081
            # 'git svn info'.  If we fail because of an older git install,
1076
            # here, figure out what version of git is installed and give
1082
            # here, figure out what version of git is installed and give
1077
            # the user a hint about what to do next.
1083
            # the user a hint about what to do next.
1078
            version = execute("git svn --version", ignore_errors=True)
1084
            version = execute(["git", "svn", "--version"], ignore_errors=True)
1079
            version_parts = re.search('version (\d+)\.(\d+)\.(\d+)',
1085
            version_parts = re.search('version (\d+)\.(\d+)\.(\d+)',
1080
                                      version)
1086
                                      version)
1081
            if (version_parts and
1087
            if (version_parts and
1082
                not self.is_valid_version((int(version_parts.group(1)),
1088
                not self.is_valid_version((int(version_parts.group(1)),
1083
                                           int(version_parts.group(2)),
1089
                                           int(version_parts.group(2)),
1108
    def scan_for_server(self, repository_info):
1114
    def scan_for_server(self, repository_info):
1109
        # Scan first for dot files, since it's faster and will cover the
1115
        # Scan first for dot files, since it's faster and will cover the
1110
        # user's $HOME/.reviewboardrc
1116
        # user's $HOME/.reviewboardrc
1111
1117
1112
        # TODO: Maybe support a server per remote later? Is that useful?
1118
        # TODO: Maybe support a server per remote later? Is that useful?
1113
        url = execute("git config --get reviewboard.url").strip()
1119
        url = execute(["git", "config", "--get", "reviewboard.url"]).strip()
1114
        if url:
1120
        if url:
1115
            return url
1121
            return url
1116
1122
1117
        if self.type == "svn":
1123
        if self.type == "svn":
1118
            # Try using the reviewboard:url property on the SVN repo, if it
1124
            # Try using the reviewboard:url property on the SVN repo, if it
1142
1148
1143
    def make_diff(self, parent_branch, source_branch=""):
1149
    def make_diff(self, parent_branch, source_branch=""):
1144
        """
1150
        """
1145
        Performs a diff on a particular branch range.
1151
        Performs a diff on a particular branch range.
1146
        """
1152
        """
1147
        diff_lines = execute("git diff --no-color --no-prefix -r -u %s..%s" %
1153
        diff_lines = execute(["git", "diff", "--no-color", "--no-prefix",
1148
                             (parent_branch, source_branch),
1154
                              "-r", "-u", "%s..%s" % (parent_branch,
1155
                                                      source_branch)],
1149
                             split_lines=True)
1156
                             split_lines=True)
1150
1157
1151
        if self.type == "svn":
1158
        if self.type == "svn":
1152
            return self.make_svn_diff(parent_branch, diff_lines)
1159
            return self.make_svn_diff(parent_branch, diff_lines)
1153
1160
1158
        """
1165
        """
1159
        Formats the output of git diff such that it's in a form that
1166
        Formats the output of git diff such that it's in a form that
1160
        svn diff would generate. This is needed so the SVNTool in Review
1167
        svn diff would generate. This is needed so the SVNTool in Review
1161
        Board can properly parse this diff.
1168
        Board can properly parse this diff.
1162
        """
1169
        """
1163
        rev = execute("git-svn find-rev master").strip()
1170
        rev = execute(["git-svn", "find-rev", "master"]).strip()
1164
1171
1165
        if not rev:
1172
        if not rev:
1166
            return None
1173
            return None
1167
1174
1168
        diff_data = ""
1175
        diff_data = ""
1241
        return True
1248
        return True
1242
    except OSError:
1249
    except OSError:
1243
        return False
1250
        return False
1244
1251
1245
1252
1246
def execute(command, split_lines=False, ignore_errors=False,
1253
def execute(command, env=None, split_lines=False, ignore_errors=False,
1247
            extra_ignore_errors=()):
1254
            extra_ignore_errors=()):
1248
    """
1255
    """
1249
    Utility function to execute a command and return the output.
1256
    Utility function to execute a command and return the output.
1250
    """
1257
    """
1258
    # Internally, subprocess.Popen calls this anyway, but since we're
1259
    # showing it on the command line, we may as well do it ourselves.
1260
    if isinstance(command, list):
1261
        command = subprocess.list2cmdline(command)
1262
1251
    debug(command)
1263
    debug(command)
1252
1264
1253
    if sys.platform.startswith('win'):
1265
    if sys.platform.startswith('win'):
1254
        p = subprocess.Popen(command,
1266
        p = subprocess.Popen(command,
1255
                             stdin=subprocess.PIPE,
1267
                             stdin=subprocess.PIPE,
1256
                             stdout=subprocess.PIPE,
1268
                             stdout=subprocess.PIPE,
1257
                             stderr=subprocess.STDOUT,
1269
                             stderr=subprocess.STDOUT,
1258
                             shell=True,
1270
                             shell=True,
1259
                             universal_newlines=True)
1271
                             universal_newlines=True,
1272
                             env=env)
1260
    else:
1273
    else:
1261
        p = subprocess.Popen(command,
1274
        p = subprocess.Popen(command,
1262
                             stdin=subprocess.PIPE,
1275
                             stdin=subprocess.PIPE,
1263
                             stdout=subprocess.PIPE,
1276
                             stdout=subprocess.PIPE,
1264
                             stderr=subprocess.STDOUT,
1277
                             stderr=subprocess.STDOUT,
1265
                             shell=True,
1278
                             shell=True,
1266
                             close_fds=True,
1279
                             close_fds=True,
1267
                             universal_newlines=True)
1280
                             universal_newlines=True,
1281
                             env=env)
1268
    if split_lines:
1282
    if split_lines:
1269
        data = p.stdout.readlines()
1283
        data = p.stdout.readlines()
1270
    else:
1284
    else:
1271
        data = p.stdout.read()
1285
        data = p.stdout.read()
1272
    rc = p.wait()
1286
    rc = p.wait()
1536
1550
1537
    if options.revision_range:
1551
    if options.revision_range:
1538
        diff = tool.diff_between_revisions(options.revision_range)
1552
        diff = tool.diff_between_revisions(options.revision_range)
1539
        parent_diff = None
1553
        parent_diff = None
1540
    else:
1554
    else:
1541
        args = ["'%s'" % arg for arg in args] # quote filenames
1542
        diff, parent_diff = tool.diff(args)
1555
        diff, parent_diff = tool.diff(args)
1543
1556
1544
    if options.output_diff_only:
1557
    if options.output_diff_only:
1545
        print diff
1558
        print diff
1546
        sys.exit(0)
1559
        sys.exit(0)
  1. /trunk/reviewboard/contrib/tools/post-review: 40 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ]