Review Board

beta

Fix keyword expansion in files from SVN repositories

Updated 10 months, 2 weeks ago

Christian Hammond Reviewers
trunk reviewboard
362
None Review Board SVN
Fixed issues with SVN keyword expansion. If a file had $Id$ or $Date$ or something and the keyword was defined in that file's svn:keywords property, svn cat would return the file with the keyword expanded. This broke patching for us and essentially meant that any patches modifying anything near a keyword expansion would break.

We now collapse the keyword by turning $Keyword:.*$ into $Keyword$. We only do this for keywords actually defined in that file's svn:keywords, in order to prevent collapsing things that aren't actually defined keywords but look like them.

This also supports known keyword aliases.
Unit tests succeed! Debug output shows we're collapsing correctly.
/trunk/reviewboard/scmtools/svn.py
Revision 1190 New Change
1
import re
1
import re
2
2
3
try:
3
try:
4
    from pysvn import ClientError, Revision, opt_revision_kind
4
    from pysvn import ClientError, Revision, opt_revision_kind
5
except ImportError:
5
except ImportError:
6
    pass
6
    pass
7
7
8
from reviewboard.diffviewer.parser import DiffParser
8
from reviewboard.diffviewer.parser import DiffParser
9
from reviewboard.scmtools.core import \
9
from reviewboard.scmtools.core import \
10
    SCMError, FileNotFoundError, SCMTool, HEAD, PRE_CREATION, UNKNOWN
10
    SCMError, FileNotFoundError, SCMTool, HEAD, PRE_CREATION, UNKNOWN
11
11
12
12
class SVNTool(SCMTool):
13
class SVNTool(SCMTool):
14
    # Mapping of keywords to known aliases
15
    keywords = {
16
        # Standard keywords
17
        'Date':                ['Date', 'LastChangedDate'],
18
        'Revision':            ['Revision', 'LastChangedRevision', 'Rev'],
19
        'Author':              ['Author', 'LastChangedBy'],
20
        'HeadURL':             ['HeadURL', 'URL'],
21
22
        # Aliases
23
        'LastChangedDate':     ['LastChangedDate', 'Date'],
24
        'LastChangedRevision': ['LastChangedRevision', 'Rev', 'Revision'],
25
        'LastChangedBy':       ['LastChangedBy', 'Author'],
26
        'URL':                 ['URL', 'HeadURL'],
27
    }
28
13
    def __init__(self, repository):
29
    def __init__(self, repository):
14
        self.repopath = repository.path
30
        self.repopath = repository.path
15
        if self.repopath[-1] == '/':
31
        if self.repopath[-1] == '/':
16
            self.repopath = self.repopath[:-1]
32
            self.repopath = self.repopath[:-1]
17
33
47
    def get_file(self, path, revision=HEAD):
63
    def get_file(self, path, revision=HEAD):
48
        if not path:
64
        if not path:
49
            raise FileNotFoundError(path, revision)
65
            raise FileNotFoundError(path, revision)
50
66
51
        try:
67
        try:
52
            return self.client.cat(self.__normalize_path(path),
68
            normpath = self.__normalize_path(path)
53
                                   self.__normalize_revision(revision))
69
            normrev  = self.__normalize_revision(revision)
70
71
            data = self.client.cat(normpath, normrev)
72
73
            # Find out if this file has any keyword expansion set.
74
            # If it does, collapse these keywords. This is because SVN
75
            # will return the file expanded to us, which would break patching.
76
            keywords = self.client.propget("svn:keywords", normpath, normrev,
77
                                           recurse=True)
78
79
            if normpath in keywords:
80
                data = self.collapse_keywords(data, keywords[normpath])
81
82
            return data
54
        except ClientError, e:
83
        except ClientError, e:
55
            stre = str(e)
84
            stre = str(e)
56
            if 'File not found' in stre:
85
            if 'File not found' in stre:
57
                raise FileNotFoundError(path, revision, str(e))
86
                raise FileNotFoundError(path, revision, str(e))
58
            elif 'callback_ssl_server_trust_prompt required' in stre:
87
            elif 'callback_ssl_server_trust_prompt required' in stre:
59
                raise SCMError(
88
                raise SCMError(
60
                    'HTTPS certificate not accepted.  Please ensure that ' +
89
                    'HTTPS certificate not accepted.  Please ensure that ' +
61
                    'the proper certificate exists in ~/.subversion/auth ' +
90
                    'the proper certificate exists in ~/.subversion/auth ' +
62
                    'for the user that reviewboard is running as.')
91
                    'for the user that reviewboard is running as.')
63
            else:
92
            else:
64
                raise SCMError(e)
93
                raise SCMError(e)
65
94
95
    def collapse_keywords(self, data, keyword_str):
96
        """
97
        Collapse SVN keywords in string.
98
99
        SVN allows for several keywords (such as $Id$ and $Revision$) to
100
        be expanded, though these keywords are limited to a fixed set
101
        (and associated aliases) and must be enabled per-file.
102
103
        Keywords can take two forms: $Keyword$ and $Keyword::     $
104
        The latter allows the field to take a fixed size when expanded.
105
106
        When we cat a file on SVN, the keywords come back expanded, which
107
        isn't good for us as we need to diff against the collapsed version.
108
        This function makes that transformation.
109
        """
110
        def repl(m):
111
            if m.group(2):
112
                return "$%s::%s$" % (m.group(1), " " * len(m.group(3)))
113
114
            return "$%s$" % m.group(1)
115
116
        # Get any aliased keywords
117
        keywords = [keyword
118
                    for name in keyword_str.split(" ")
119
                    for keyword in self.keywords.get(name, [])]
120
121
        return re.sub(r"\$(%s):(:?)([^\$]+)\$" % '|'.join(keywords),
122
                      repl, data)
123
66
124
67
    def parse_diff_revision(self, file_str, revision_str):
125
    def parse_diff_revision(self, file_str, revision_str):
68
        if revision_str == "(working copy)":
126
        if revision_str == "(working copy)":
69
            return file_str, HEAD
127
            return file_str, HEAD
70
128
/trunk/reviewboard/scmtools/tests.py
Revision 1190 New Change
247
    def testKeywordDiff(self):
247
    def testKeywordDiff(self):
248
        """Testing parsing SVN diff with keywords"""
248
        """Testing parsing SVN diff with keywords"""
249
        # 'svn cat' will expand special variables in svn:keywords,
249
        # 'svn cat' will expand special variables in svn:keywords,
250
        # but 'svn diff' doesn't expand anything.  This causes the
250
        # but 'svn diff' doesn't expand anything.  This causes the
251
        # patch to fail if those variables appear in the patch context.
251
        # patch to fail if those variables appear in the patch context.
252
        diff = "Index: Makefile\n==========================================" + \
252
        diff = "Index: Makefile\n" \
253
               "=========================\n--- Makefile    (revision 3)\n++" + \
253
               "===========================================================" \
254
               "+ Makefile    (working copy)\n@@ -1,4 +1,5 @@\n # $Id$\n+# " + \
254
               "========\n" \
255
               "foo\n include ../tools/Makefile.base-vars\n NAME = misc-doc" + \
255
               "--- Makefile    (revision 4)\n" \
256
               "s\n OUTNAME = svn-misc-docs\n"
256
               "+++ Makefile    (working copy)\n" \
257
               "@@ -1,6 +1,7 @@\n" \
258
               " # $Id$\n" \
259
               " # $Rev$\n" \
260
               " # $Revision::     $\n" \
261
               "+# foo\n" \
262
               " include ../tools/Makefile.base-vars\n" \
263
               " NAME = misc-docs\n" \
264
               " OUTNAME = svn-misc-docs\n"
257
265
258
        filename = 'trunk/doc/misc-docs/Makefile'
266
        filename = 'trunk/doc/misc-docs/Makefile'
259
        rev = Revision('3')
267
        rev = Revision('4')
260
        file = self.tool.get_file(filename, rev)
268
        file = self.tool.get_file(filename, rev)
261
        newfile = patch(diff, file, filename) # Throws an exception!
269
        patch(diff, file, filename)
262
270
263
271
264
class PerforceTests(unittest.TestCase):
272
class PerforceTests(unittest.TestCase):
265
    """Unit tests for perforce.
273
    """Unit tests for perforce.
266
274
/trunk/reviewboard/scmtools/testdata/svn_repo/db/current
Revision 1190 New Change
1
3 5 1
1
4 5 1
/trunk/reviewboard/scmtools/testdata/svn_repo/db/revprops/4
New File
1
K 10
2
svn:author
3
V 7
4
chipx86
5
K 8
6
svn:date
7
V 27
8
2008-02-24T23:04:51.477951Z
9
K 7
10
svn:log
11
V 66
12
Add a couple keyword tests, and add Revision to the keyword list.
13
14
END
/trunk/reviewboard/scmtools/testdata/svn_repo/db/revs/4
Revision UNKNOWN New Change
This is a binary file. The content cannot be displayed.
  1. /trunk/reviewboard/scmtools/svn.py: 5 changes [ 1 2 3 4 5 ]
  2. /trunk/reviewboard/scmtools/tests.py: 4 changes [ 1 2 3 4 ]
  3. /trunk/reviewboard/scmtools/testdata/svn_repo/db/current: 1 change [ ]
  4. /trunk/reviewboard/scmtools/testdata/svn_repo/db/revprops/4: 1 change [ new content ]
  5. /trunk/reviewboard/scmtools/testdata/svn_repo/db/revs/4: binary file