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.
Diff revision 4 (Latest)
- /trunk/reviewboard/scmtools/svn.py: 5 changes [ 1 2 3 4 5 ]
- /trunk/reviewboard/scmtools/tests.py: 4 changes [ 1 2 3 4 ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/current: 1 change [ ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/revprops/4: 1 change [ new content ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/revs/4: binary file
| /trunk/reviewboard/scmtools/svn.py | |||
|---|---|---|---|
| Revision 1190 | New Change | ||
| 1 |
|
1 |
|
| 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 | ||
| ... | 29 lines hidden [Expand] | ||
| 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 | ||
| ... | 85 lines hidden [Expand] | ||
| /trunk/reviewboard/scmtools/tests.py | |||
|---|---|---|---|
| Revision 1190 | New Change | ||
| ... | 246 lines hidden [Expand] | ||
| 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 | ||
| ... | 504 lines hidden [Expand] | ||
| /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/revs/4 | |||
|---|---|---|---|
| Revision UNKNOWN | New Change | ||
| This is a binary file. The content cannot be displayed. | |||
- /trunk/reviewboard/scmtools/svn.py: 5 changes [ 1 2 3 4 5 ]
- /trunk/reviewboard/scmtools/tests.py: 4 changes [ 1 2 3 4 ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/current: 1 change [ ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/revprops/4: 1 change [ new content ]
- /trunk/reviewboard/scmtools/testdata/svn_repo/db/revs/4: binary file
Other reviews