Merge lp:~parthm/bzr/300062-bad-pattern-error-part-2 into lp:bzr

Proposed by Parth Malwankar
Status: Rejected
Rejected by: Parth Malwankar
Proposed branch: lp:~parthm/bzr/300062-bad-pattern-error-part-2
Merge into: lp:bzr
Diff against target: 272 lines (+128/-38)
6 files modified
NEWS (+1/-0)
bzrlib/builtins.py (+8/-0)
bzrlib/globbing.py (+73/-23)
bzrlib/tests/blackbox/test_ignore.py (+16/-0)
bzrlib/tests/per_workingtree/test_is_ignored.py (+25/-13)
bzrlib/tests/test_globbing.py (+5/-2)
To merge this branch: bzr merge lp:~parthm/bzr/300062-bad-pattern-error-part-2
Reviewer Review Type Date Requested Status
bzr-core Pending
Review via email: mp+29467@code.launchpad.net

Description of the change

=== Fix towards bug #300062 ===
This is the second patch towards bug #300062. The first patch can be found at [1].
This does the following:
 1. `bzr ignore` now fails in case an invalid pattern is supplied. The bad pattern is displayed to the user.
 2. In case an invalid pattern is encountered in .bzrignore or user ignore file, the error message shown now displays the specific pattern(s) that caused the failure.
 3. Some code cleanup is done Globster.

One thing pending is that the error messages do not show what file the bad pattern came from. Ideally, I would like to show a message like "file:line_number:pattern". This requires updating the ignore file parsing logic to store line number and file name information. I plan to look at that in a separate patch. Suggestions welcome.

Sample output:
[a1234]% ~/src/bzr.dev/300062-bad-pattern-error-part-2/bzr ignore "RE:[" foo "RE:*.cpp"
bzr: error: Invalid ignore pattern(s) found.
  RE:[
  RE:*.cpp
bzr: ERROR: Invalid pattern(s) found.
[a1234]% ~/src/bzr.dev/300062-bad-pattern-error-part-2/bzr st
bzr: ERROR: Invalid pattern(s) found. File ~/.bazaar/ignore or .bzrignore contains error(s).
  RE:[
[a1234]%

[1] https://code.launchpad.net/~parthm/bzr/300062-bad-pattern-error-part-1/+merge/28889

To post a comment you must log in.
Revision history for this message
Parth Malwankar (parthm) wrote :

Is 2.2b4 being considered a feature freeze or can this patch be considered for 2.2?
IMO it would be nice to have this in if possible as the user gets to see the failing pattern in the error message. The specific failing ignore file is not shown but that can be a 2.3 feature.

Revision history for this message
Martin Pool (mbp) wrote :

On 10 July 2010 19:37, Parth Malwankar <email address hidden> wrote:
> Is 2.2b4 being considered a feature freeze or can this patch be considered for 2.2?
> IMO it would be nice to have this in if possible as the user gets to see the failing pattern in the error message. The specific failing ignore file is not shown but that can be a 2.3 feature.

Based on the cover letter I think this would be fine for 2.2.

--
Martin

Revision history for this message
Parth Malwankar (parthm) wrote :

I am rejecting this since https://code.launchpad.net/~parthm/bzr/300062-2.2-bad-pattern-error-part-2/+merge/29949 proposes the same changes for 2.2

Unmerged revisions

5348. By Parth Malwankar

order of patterns added using _add_pattern is predictable

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-07-08 10:34:12 +0000
+++ NEWS 2010-07-08 13:41:54 +0000
@@ -94,6 +94,7 @@
9494
95* Invalid patterns supplied to ``Globster`` or ``lazy_regex`` now raise95* Invalid patterns supplied to ``Globster`` or ``lazy_regex`` now raise
96 ``InvalidPattern`` exception showing clear error message to the user.96 ``InvalidPattern`` exception showing clear error message to the user.
97 ``bzr ignore PATTERNS`` exits with error if a bad pattern is supplied.
97 (Parth Malwankar #300062)98 (Parth Malwankar #300062)
9899
99* Progress output is cleaned up when exiting. (Aaron Bentley)100* Progress output is cleaned up when exiting. (Aaron Bentley)
100101
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2010-06-30 12:13:14 +0000
+++ bzrlib/builtins.py 2010-07-08 13:41:54 +0000
@@ -2712,6 +2712,14 @@
2712 "NAME_PATTERN or --default-rules.")2712 "NAME_PATTERN or --default-rules.")
2713 name_pattern_list = [globbing.normalize_pattern(p)2713 name_pattern_list = [globbing.normalize_pattern(p)
2714 for p in name_pattern_list]2714 for p in name_pattern_list]
2715 bad_patterns = ''
2716 for p in name_pattern_list:
2717 if not globbing.Globster.is_pattern_valid(p):
2718 bad_patterns += ('\n %s' % p)
2719 if bad_patterns:
2720 msg = ('Invalid ignore pattern(s) found. %s' % bad_patterns)
2721 ui.ui_factory.show_error(msg)
2722 raise errors.InvalidPattern('')
2715 for name_pattern in name_pattern_list:2723 for name_pattern in name_pattern_list:
2716 if (name_pattern[0] == '/' or2724 if (name_pattern[0] == '/' or
2717 (len(name_pattern) > 1 and name_pattern[1] == ':')):2725 (len(name_pattern) > 1 and name_pattern[1] == ':')):
27182726
=== modified file 'bzrlib/globbing.py'
--- bzrlib/globbing.py 2010-07-08 05:11:16 +0000
+++ bzrlib/globbing.py 2010-07-08 13:41:54 +0000
@@ -179,24 +179,38 @@
179 so are matched first, then the basename patterns, then the fullpath179 so are matched first, then the basename patterns, then the fullpath
180 patterns.180 patterns.
181 """181 """
182 TYPE_FULLPATH = 1
183 TYPE_BASENAME = 2
184 TYPE_EXTENSION = 3
185
186 translators = {
187 TYPE_FULLPATH : _sub_fullpath,
188 TYPE_BASENAME : _sub_basename,
189 TYPE_EXTENSION : _sub_extension,
190 }
191
192 # Prefixes used to combine various patterns.
193 # See: Globster._add_patterns
194 prefixes = {
195 TYPE_FULLPATH : r'',
196 TYPE_BASENAME : r'(?:.*/)?(?!.*/)',
197 TYPE_EXTENSION : r'(?:.*/)?(?!.*/)(?:.*\.)',
198 }
199
182 def __init__(self, patterns):200 def __init__(self, patterns):
183 self._regex_patterns = []201 self._regex_patterns = []
184 path_patterns = []202 pattern_lists = {
185 base_patterns = []203 Globster.TYPE_FULLPATH : [],
186 ext_patterns = []204 Globster.TYPE_EXTENSION : [],
205 Globster.TYPE_BASENAME : [],
206 }
187 for pat in patterns:207 for pat in patterns:
188 pat = normalize_pattern(pat)208 pat = normalize_pattern(pat)
189 if pat.startswith(u'RE:') or u'/' in pat:209 pattern_lists[Globster.identify(pat)].append(pat)
190 path_patterns.append(pat)210 for pattern_type, patterns in pattern_lists.iteritems():
191 elif pat.startswith(u'*.'):211 self._add_patterns(patterns,
192 ext_patterns.append(pat)212 Globster.translators[pattern_type],
193 else:213 Globster.prefixes[pattern_type])
194 base_patterns.append(pat)
195 self._add_patterns(ext_patterns,_sub_extension,
196 prefix=r'(?:.*/)?(?!.*/)(?:.*\.)')
197 self._add_patterns(base_patterns,_sub_basename,
198 prefix=r'(?:.*/)?(?!.*/)')
199 self._add_patterns(path_patterns,_sub_fullpath)
200214
201 def _add_patterns(self, patterns, translator, prefix=''):215 def _add_patterns(self, patterns, translator, prefix=''):
202 while patterns:216 while patterns:
@@ -221,10 +235,51 @@
221 # the combined pattern we sent to regex. Instead we indicate to235 # the combined pattern we sent to regex. Instead we indicate to
222 # the user that an ignore file needs fixing.236 # the user that an ignore file needs fixing.
223 mutter('Invalid pattern found in regex: %s.', e.message)237 mutter('Invalid pattern found in regex: %s.', e.message)
224 e.message = "File ~/.bazaar/ignore or .bzrignore contains errors."238 e.message = "File ~/.bazaar/ignore or .bzrignore contains error(s)."
239 bad_patterns = ''
240 for _, patterns in self._regex_patterns:
241 for p in patterns:
242 if not Globster.is_pattern_valid(p):
243 bad_patterns += ('\n %s' % p)
244 e.message += bad_patterns
225 raise e245 raise e
226 return None246 return None
227247
248 @staticmethod
249 def identify(pattern):
250 """Returns pattern category.
251
252 :param pattern: normalized pattern.
253 Identify if a pattern is fullpath, basename or extension
254 and returns the appropriate type.
255 """
256 if pattern.startswith(u'RE:') or u'/' in pattern:
257 return Globster.TYPE_FULLPATH
258 elif pattern.startswith(u'*.'):
259 return Globster.TYPE_EXTENSION
260 else:
261 return Globster.TYPE_BASENAME
262
263 @staticmethod
264 def is_pattern_valid(pattern):
265 """Returns True if pattern is valid.
266
267 :param pattern: Normalized pattern.
268 is_pattern_valid() assumes pattern to be normalized.
269 see: globbing.normalize_pattern
270 """
271 result = True
272 translator = Globster.translators[Globster.identify(pattern)]
273 tpattern = '(%s)' % translator(pattern)
274 try:
275 re_obj = re.compile(tpattern, re.UNICODE)
276 re_obj.search("") # force compile
277 except errors.InvalidPattern, e:
278 result = False
279 return result
280
281
282
228class ExceptionGlobster(object):283class ExceptionGlobster(object):
229 """A Globster that supports exception patterns.284 """A Globster that supports exception patterns.
230 285
@@ -272,14 +327,9 @@
272 self._regex_patterns = []327 self._regex_patterns = []
273 for pat in patterns:328 for pat in patterns:
274 pat = normalize_pattern(pat)329 pat = normalize_pattern(pat)
275 if pat.startswith(u'RE:') or u'/' in pat:330 pat_type = Globster.identify(pat)
276 self._add_patterns([pat], _sub_fullpath)331 self._add_patterns([pat], Globster.translators[pat_type],
277 elif pat.startswith(u'*.'):332 Globster.prefixes[pat_type])
278 self._add_patterns([pat], _sub_extension,
279 prefix=r'(?:.*/)?(?!.*/)(?:.*\.)')
280 else:
281 self._add_patterns([pat], _sub_basename,
282 prefix=r'(?:.*/)?(?!.*/)')
283333
284334
285_slashes = re.compile(r'[\\/]+')335_slashes = re.compile(r'[\\/]+')
286336
=== modified file 'bzrlib/tests/blackbox/test_ignore.py'
--- bzrlib/tests/blackbox/test_ignore.py 2010-06-11 07:32:12 +0000
+++ bzrlib/tests/blackbox/test_ignore.py 2010-07-08 13:41:54 +0000
@@ -162,3 +162,19 @@
162 tree = self.make_branch_and_tree('a')162 tree = self.make_branch_and_tree('a')
163 self.run_bzr(['ignore', '--directory=a', 'README'])163 self.run_bzr(['ignore', '--directory=a', 'README'])
164 self.check_file_contents('a/.bzrignore', 'README\n')164 self.check_file_contents('a/.bzrignore', 'README\n')
165
166 def test_ignored_invalid_pattern(self):
167 """Ensure graceful handling for invalid ignore pattern.
168
169 Test case for #300062.
170 Invalid pattern should show clear error message.
171 Invalid pattern should not be added to .bzrignore file.
172 """
173 tree = self.make_branch_and_tree('.')
174 out, err = self.run_bzr(['ignore', 'RE:*.cpp', 'foo', 'RE:['], 3)
175 self.assertEqual(out, '')
176 self.assertContainsRe(err,
177 'Invalid ignore pattern.*RE:\*\.cpp.*RE:\[', re.DOTALL)
178 self.assertNotContainsRe(err, 'foo', re.DOTALL)
179 self.assertFalse(os.path.isfile('.bzrignore'))
180
165181
=== modified file 'bzrlib/tests/per_workingtree/test_is_ignored.py'
--- bzrlib/tests/per_workingtree/test_is_ignored.py 2010-02-17 17:11:16 +0000
+++ bzrlib/tests/per_workingtree/test_is_ignored.py 2010-07-08 13:41:54 +0000
@@ -22,6 +22,16 @@
2222
23class TestIsIgnored(TestCaseWithWorkingTree):23class TestIsIgnored(TestCaseWithWorkingTree):
2424
25 def _set_user_ignore_content(self, ignores):
26 """Create user ignore file and set its content to ignores."""
27 config.ensure_config_dir_exists()
28 user_ignore_file = config.user_ignore_config_filename()
29 f = open(user_ignore_file, 'wb')
30 try:
31 f.write(ignores)
32 finally:
33 f.close()
34
25 def test_is_ignored(self):35 def test_is_ignored(self):
26 tree = self.make_branch_and_tree('.')36 tree = self.make_branch_and_tree('.')
27 # this will break if a tree changes the ignored format. That is fine37 # this will break if a tree changes the ignored format. That is fine
@@ -46,6 +56,11 @@
46 '#comment\n'56 '#comment\n'
47 ' xx \n' # whitespace57 ' xx \n' # whitespace
48 )])58 )])
59 # We set user ignore file to contain '' to avoid patterns from
60 # user ignore being used instead of bzrignore. For .e.g. If we
61 # don't do this 'foo.~1~' will match '*~' default user ignore
62 # pattern instead of '*.~*' from bzr ignore as we expect below.
63 self._set_user_ignore_content('')
49 # is_ignored returns the matching ignore regex when a path is ignored.64 # is_ignored returns the matching ignore regex when a path is ignored.
50 # we check some expected matches for each rule, and one or more65 # we check some expected matches for each rule, and one or more
51 # relevant not-matches that look plausible as cases for bugs.66 # relevant not-matches that look plausible as cases for bugs.
@@ -119,19 +134,16 @@
119134
120 config.ensure_config_dir_exists()135 config.ensure_config_dir_exists()
121 user_ignore_file = config.user_ignore_config_filename()136 user_ignore_file = config.user_ignore_config_filename()
122 f = open(user_ignore_file, 'wb')137 self._set_user_ignore_content(
123 try:138 '*.py[co]\n'
124 f.write('*.py[co]\n'139 './.shelf\n'
125 './.shelf\n'140 '# comment line\n'
126 '# comment line\n'141 '\n' #Blank line
127 '\n' #Blank line142 '\r\n' #Blank dos line
128 '\r\n' #Blank dos line143 ' * \n' #Trailing and suffix spaces
129 ' * \n' #Trailing and suffix spaces144 'crlf\r\n' # dos style line
130 'crlf\r\n' # dos style line145 '*\xc3\xa5*\n' # u'\xe5'.encode('utf8')
131 '*\xc3\xa5*\n' # u'\xe5'.encode('utf8')146 )
132 )
133 finally:
134 f.close()
135147
136 # Rooted148 # Rooted
137 self.assertEqual('./.shelf', tree.is_ignored('.shelf'))149 self.assertEqual('./.shelf', tree.is_ignored('.shelf'))
138150
=== modified file 'bzrlib/tests/test_globbing.py'
--- bzrlib/tests/test_globbing.py 2010-06-30 13:49:02 +0000
+++ bzrlib/tests/test_globbing.py 2010-07-08 13:41:54 +0000
@@ -15,6 +15,8 @@
15# along with this program; if not, write to the Free Software15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1717
18import re
19
18from bzrlib import errors20from bzrlib import errors
19from bzrlib.globbing import (21from bzrlib.globbing import (
20 Globster,22 Globster,
@@ -311,10 +313,11 @@
311313
312 def test_bad_pattern(self):314 def test_bad_pattern(self):
313 """Ensure that globster handles bad patterns cleanly."""315 """Ensure that globster handles bad patterns cleanly."""
314 patterns = [u'RE:[']316 patterns = [u'RE:[', u'foo', u'RE:*.cpp']
315 g = Globster(patterns)317 g = Globster(patterns)
316 e = self.assertRaises(errors.InvalidPattern, g.match, 'foo')318 e = self.assertRaises(errors.InvalidPattern, g.match, 'foo')
317 self.assertContainsRe(e.message, "File.*ignore.*contains errors")319 self.assertContainsRe(e.message,
320 "File.*ignore.*contains error.*RE:\[.*RE:\*\.cpp", flags=re.DOTALL)
318321
319322
320class TestExceptionGlobster(TestCase):323class TestExceptionGlobster(TestCase):