Commit f2aa52b5 authored by tandrii@chromium.org's avatar tandrii@chromium.org

Refactor git_footers for later use in git cl.

Motivation: git_cl should start understanding Gerrit's git footers,
to avoid appending BUG= after Change-Id: Ixxx line.

R=sergiyb@chromium.org
BUG=614587

Review-Url: https://codereview.chromium.org/2028303006

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@300691 0039d316-1c4b-4281-b951-d872f2087c98
parent d605a51c
...@@ -23,6 +23,7 @@ def normalize_name(header): ...@@ -23,6 +23,7 @@ def normalize_name(header):
def parse_footer(line): def parse_footer(line):
"""Returns footer's (key, value) if footer is valid, else None."""
match = FOOTER_PATTERN.match(line) match = FOOTER_PATTERN.match(line)
if match: if match:
return (match.group(1), match.group(2)) return (match.group(1), match.group(2))
...@@ -32,21 +33,39 @@ def parse_footer(line): ...@@ -32,21 +33,39 @@ def parse_footer(line):
def parse_footers(message): def parse_footers(message):
"""Parses a git commit message into a multimap of footers.""" """Parses a git commit message into a multimap of footers."""
_, _, parsed_footers = split_footers(message)
footer_map = defaultdict(list)
if parsed_footers:
# Read footers from bottom to top, because latter takes precedense,
# and we want it to be first in the multimap value.
for (k, v) in reversed(parsed_footers):
footer_map[normalize_name(k)].append(v.strip())
return footer_map
def split_footers(message):
"""Returns (non_footer_lines, footer_lines, parsed footers).
Guarantees that:
(non_footer_lines + footer_lines) == message.splitlines().
parsed_footers is parse_footer applied on each line of footer_lines.
"""
message_lines = list(message.splitlines())
footer_lines = [] footer_lines = []
for line in reversed(message.splitlines()): for line in reversed(message_lines):
if line == '' or line.isspace(): if line == '' or line.isspace():
break break
footer_lines.append(line) footer_lines.append(line)
else:
# The whole description was consisting of footers,
# which means those aren't footers.
footer_lines = []
footer_lines.reverse()
footers = map(parse_footer, footer_lines) footers = map(parse_footer, footer_lines)
if not all(footers): if not footer_lines or not all(footers):
return defaultdict(list) return message_lines, [], []
return message_lines[:-len(footer_lines)], footer_lines, footers
footer_map = defaultdict(list)
for (k, v) in footers:
footer_map[normalize_name(k)].append(v.strip())
return footer_map
def get_footer_svn_id(branch=None): def get_footer_svn_id(branch=None):
...@@ -71,38 +90,46 @@ def get_footer_change_id(message): ...@@ -71,38 +90,46 @@ def get_footer_change_id(message):
def add_footer_change_id(message, change_id): def add_footer_change_id(message, change_id):
"""Returns message with Change-ID footer in it. """Returns message with Change-ID footer in it.
Assumes that Change-Id is not yet in footers, which is then Assumes that Change-Id is not yet in footers, which is then inserted at
inserted after any of these footers: Bug|Issue|Test|Feature. earliest footer line which is after all of these footers:
Bug|Issue|Test|Feature.
""" """
assert 0 == len(get_footer_change_id(message)) assert 'Change-Id' not in parse_footers(message)
change_id_line = 'Change-Id: %s' % change_id return add_footer(message, 'Change-Id', change_id,
# This code does the same as parse_footers, but keeps track of line after_keys=['Bug', 'Issue', 'Test', 'Feature'])
# numbers so that ChangeId is inserted in the right place.
lines = message.splitlines() def add_footer(message, key, value, after_keys=None):
footer_lines = [] """Returns a message with given footer appended.
for line in reversed(lines):
if line == '' or line.isspace(): If after_keys is None (default), appends footer last.
break Otherwise, after_keys must be iterable of footer keys, then the new footer
footer_lines.append(line) would be inserted at the topmost position such there would be no footer lines
else: after it with key matching one of after_keys.
# The whole description was consisting of footers, For example, given
# which means those aren't footers. message='Header.\n\nAdded: 2016\nBug: 123\nVerified-By: CQ'
footer_lines = [] after_keys=['Bug', 'Issue']
# footers order is from end to start of the message. the new footer will be inserted between Bug and Verified-By existing footers.
footers = map(parse_footer, footer_lines) """
if not footers or not all(footers): assert key == normalize_name(key), 'Use normalized key'
lines.append('') new_footer = '%s: %s' % (key, value)
lines.append(change_id_line)
top_lines, footer_lines, parsed_footers = split_footers(message)
if not footer_lines:
if not top_lines or top_lines[-1] != '':
top_lines.append('')
footer_lines = [new_footer]
elif not after_keys:
footer_lines.append(new_footer)
else: else:
after = set(map(normalize_name, ['Bug', 'Issue', 'Test', 'Feature'])) after_keys = set(map(normalize_name, after_keys))
for i, (key, _) in enumerate(footers): # Iterate from last to first footer till we find the footer keys above.
if normalize_name(key) in after: for i, (key, _) in reversed(list(enumerate(parsed_footers))):
insert_at = len(lines) - i if normalize_name(key) in after_keys:
footer_lines.insert(i + 1, new_footer)
break break
else: else:
insert_at = len(lines) - len(footers) footer_lines.insert(0, new_footer)
lines.insert(insert_at, change_id_line) return '\n'.join(top_lines + footer_lines)
return '\n'.join(lines)
def get_unique(footers, key): def get_unique(footers, key):
......
...@@ -35,8 +35,17 @@ My commit message is my best friend. It is my life. I must master it. ...@@ -35,8 +35,17 @@ My commit message is my best friend. It is my life. I must master it.
_git_svn_id_footer_branch = 'git-svn-id: %s\n' % _git_svn_id_branch _git_svn_id_footer_branch = 'git-svn-id: %s\n' % _git_svn_id_branch
def testFootersBasic(self): def testFootersBasic(self):
self.assertEqual(
git_footers.split_footers('Not-A: footer'),
(['Not-A: footer'], [], []))
self.assertEqual(
git_footers.split_footers('Header\n\nActual: footer'),
(['Header', ''], ['Actual: footer'], [('Actual', 'footer')]))
self.assertEqual(
git_footers.split_footers('\nActual: footer'),
([''], ['Actual: footer'], [('Actual', 'footer')]))
self.assertEqual( self.assertEqual(
git_footers.parse_footers(self._message), {}) git_footers.parse_footers(self._message), {})
self.assertEqual( self.assertEqual(
...@@ -82,6 +91,9 @@ My commit message is my best friend. It is my life. I must master it. ...@@ -82,6 +91,9 @@ My commit message is my best friend. It is my life. I must master it.
'desc\nBUG=not-a-valid-footer\n\nChange-Id: Ixxx')) 'desc\nBUG=not-a-valid-footer\n\nChange-Id: Ixxx'))
def testAddFooterChangeId(self): def testAddFooterChangeId(self):
with self.assertRaises(AssertionError):
git_footers.add_footer_change_id('Already has\n\nChange-Id: Ixxx', 'Izzz')
self.assertEqual( self.assertEqual(
git_footers.add_footer_change_id('header-only', 'Ixxx'), git_footers.add_footer_change_id('header-only', 'Ixxx'),
'header-only\n\nChange-Id: Ixxx') 'header-only\n\nChange-Id: Ixxx')
...@@ -107,6 +119,33 @@ My commit message is my best friend. It is my life. I must master it. ...@@ -107,6 +119,33 @@ My commit message is my best friend. It is my life. I must master it.
git_footers.add_footer_change_id('header: like footer', 'Ixxx'), git_footers.add_footer_change_id('header: like footer', 'Ixxx'),
'header: like footer\n\nChange-Id: Ixxx') 'header: like footer\n\nChange-Id: Ixxx')
def testAddFooter(self):
self.assertEqual(
git_footers.add_footer('', 'Key', 'Value'),
'\nKey: Value')
self.assertEqual(
git_footers.add_footer('Header with empty line.\n\n', 'Key', 'Value'),
'Header with empty line.\n\nKey: Value')
self.assertEqual(
git_footers.add_footer('Top\n\nSome: footer', 'Key', 'value'),
'Top\n\nSome: footer\nKey: value')
self.assertEqual(
git_footers.add_footer('Top\n\nSome: footer', 'Key', 'value',
after_keys=['Any']),
'Top\n\nKey: value\nSome: footer')
self.assertEqual(
git_footers.add_footer('Top\n\nSome: footer', 'Key', 'value',
after_keys=['Some']),
'Top\n\nSome: footer\nKey: value')
self.assertEqual(
git_footers.add_footer('Top\n\nSome: footer\nOther: footer',
'Key', 'value', after_keys=['Some']),
'Top\n\nSome: footer\nKey: value\nOther: footer')
def testReadStdin(self): def testReadStdin(self):
self.mock(git_footers.sys, 'stdin', StringIO.StringIO( self.mock(git_footers.sys, 'stdin', StringIO.StringIO(
'line\r\notherline\r\n\r\n\r\nFoo: baz')) 'line\r\notherline\r\n\r\n\r\nFoo: baz'))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment