def _ParseChangeLogFromLogData(self, data):
        change_info = commit_util.ExtractChangeInfo(data['message'], self._ref)

        touched_files = []
        for file_diff in data['tree_diff']:
            change_type = file_diff['type'].lower()
            if not diff.IsKnownChangeType(change_type):
                raise Exception('Unknown change type "%s"' % change_type)
            touched_files.append(
                FileChangeInfo(change_type, file_diff['old_path'],
                               file_diff['new_path']))

        reverted_revision = commit_util.GetRevertedRevision(data['message'])
        url = '%s/+/%s' % (self.repo_url, data['commit'])

        return ChangeLog(self._ContributorFromDict(data['author']),
                         self._ContributorFromDict(data['committer']),
                         data['commit'], change_info.get('commit_position'),
                         data['message'], touched_files, url,
                         change_info.get('code_review_url'), reverted_revision,
                         change_info.get('host'), change_info.get('change_id'))
    def testExtractChangeInfo(self):
        testcases = [{
            'message':
            'balabala...\n'
            '\n'
            'BUG=604502\n'
            '\n'
            'Review-Url: https://codereview.chromium.org/1927593004\n'
            'Cr-Commit-Position: refs/heads/master@{#390254}\n',
            'commit_position':
            390254,
            'code_review_url':
            'https://codereview.chromium.org/1927593004',
            'change_id':
            '1927593004',
            'host':
            'codereview.chromium.org',
        }, {
            'message':
            'balabala...\n'
            '\n'
            'BUG=409934\n'
            '\n'
            'Review URL: https://codereview.chromium.org/547753003\n'
            '\n'
            'Cr-Commit-Position: refs/heads/master@{#293661}',
            'commit_position':
            293661,
            'code_review_url':
            'https://codereview.chromium.org/547753003',
            'change_id':
            '547753003',
            'host':
            'codereview.chromium.org',
        }, {
            'message':
            'Review URL: https://codereview.chromium.org/469523002\n'
            '\n'
            'Cr-Commit-Position: refs/heads/master@{#289120}',
            'commit_position':
            289120,
            'code_review_url':
            'https://codereview.chromium.org/469523002',
            'change_id':
            '469523002',
            'host':
            'codereview.chromium.org',
        }, {
            'message':
            'balabala...\n'
            '\n'
            'balabala...\n'
            '\n'
            '[email protected]\n'
            '\n'
            'Review URL: https://codereview.chromium.org/469523002\n',
            'commit_position':
            None,
            'code_review_url':
            'https://codereview.chromium.org/469523002',
            'change_id':
            '469523002',
            'host':
            'codereview.chromium.org',
        }, {
            'message': None,
            'commit_position': None,
            'code_review_url': None,
            'change_id': None,
            'host': None,
        }, {
            'message': 'abc',
            'commit_position': None,
            'code_review_url': None,
            'change_id': None,
            'host': None,
        }, {
            'message':
            'balabala...\n'
            '\n'
            'balabala...\n'
            '\n'
            '[email protected]\n'
            '\n'
            'Change-Id: Iaa54f242b5b2fa10870503ef88291b9422cb47ca\n'
            'Reviewed-on: https://chromium-review.googlesource.com/45425\n'
            'Cr-Commit-Position: refs/heads/master@{#456563}',
            'commit_position':
            456563,
            'code_review_url':
            'https://chromium-review.googlesource.com/q/'
            'Iaa54f242b5b2fa10870503ef88291b9422cb47ca',
            'change_id':
            'Iaa54f242b5b2fa10870503ef88291b9422cb47ca',
            'host':
            'chromium-review.googlesource.com',
        }]

        for testcase in testcases:
            change_info = commit_util.ExtractChangeInfo(testcase['message'])
            self.assertEqual(change_info.get('commit_position'),
                             testcase['commit_position'])
            self.assertEqual(change_info.get('code_review_url'),
                             testcase['code_review_url'])
            self.assertEqual(change_info.get('host'), testcase['host'])
            self.assertEqual(change_info.get('change_id'),
                             testcase['change_id'])
    def __call__(self, output, repo_url):  # pylint:disable=W
        """Parses output of 'git log --pretty=format:<format>.

    For example:
    Git changelog output is:
    commit 21a8979218c096f4a96b07b67c9531f5f09e28a3
    tree 7d9a79c9b060c9a030abe20a8429d2b81ca1d4db
    parents 9640406d426a2d153b16e1d9ae7f9105268b36c9

    author Test
    author-email [email protected]
    author-time 2016-10-24 22:21:45

    committer Test
    committer-email [email protected]
    committer-time 2016-10-24 22:25:45

    --Message start--
    Commit messages...
    --Message end--

    :100644 100644 25f95f c766f1 M      src/a/delta/git_parsers.py

    Returns:
    Parsed ChangeLog object.
    """
        if not output:
            return None

        is_message_line = False
        info = {
            'author': {},
            'committer': {},
            'message': '',
            'touched_files': []
        }
        for line in output.splitlines():
            if MESSAGE_START_PATTERN.match(line):
                is_message_line = True
                continue

            if MESSAGE_END_PATTERN.match(line):
                is_message_line = False
                # Remove the added '\n' at the end.
                info['message'] = info['message'][:-1]
                continue

            if is_message_line:
                info['message'] += line + '\n'
            elif COMMIT_HASH_PATTERN.match(line):
                info['revision'] = COMMIT_HASH_PATTERN.match(line).group(1)
            elif AUTHOR_NAME_PATTERN.match(line):
                info['author']['name'] = AUTHOR_NAME_PATTERN.match(line).group(
                    1)
            elif AUTHOR_MAIL_PATTERN.match(line):
                info['author']['email'] = commit_util.NormalizeEmail(
                    AUTHOR_MAIL_PATTERN.match(line).group(1))
            elif AUTHOR_TIME_PATTERN.match(line):
                info['author']['time'] = datetime.strptime(
                    AUTHOR_TIME_PATTERN.match(line).group(1), DATETIME_FORMAT)
            elif COMMITTER_NAME_PATTERN.match(line):
                info['committer']['name'] = (
                    COMMITTER_NAME_PATTERN.match(line).group(1))
            elif COMMITTER_MAIL_PATTERN.match(line):
                info['committer']['email'] = commit_util.NormalizeEmail(
                    COMMITTER_MAIL_PATTERN.match(line).group(1))
            elif COMMITTER_TIME_PATTERN.match(line):
                info['committer']['time'] = datetime.strptime(
                    COMMITTER_TIME_PATTERN.match(line).group(1),
                    DATETIME_FORMAT)
            elif (CHANGED_FILE_PATTERN1.match(line)
                  or CHANGED_FILE_PATTERN2.match(line)):
                match = (CHANGED_FILE_PATTERN1.match(line)
                         or CHANGED_FILE_PATTERN2.match(line))
                # For modify, add, delete, the pattern is like:
                # :100644 100644 df565d 6593e M modules/audio_coding/BUILD.gn
                # For rename, copy, the pattern is like:
                # :100644 100644 3f2e 20a5 R078 path1 path2
                info['touched_files'].append(
                    GetFileChangeInfo(
                        GetChangeType(match.group(5)), match.group(6),
                        None if len(match.groups()) < 7 else match.group(7)))

        # If commit is not parsed, the changelog will be {'author': {}, 'committer':
        # {}, 'message': ''}, return None instead.
        if not 'revision' in info:
            return None

        change_info = commit_util.ExtractChangeInfo(info['message'])
        info['commit_position'] = change_info.get('commit_position')
        info['code_review_url'] = change_info.get('code_review_url')
        info['reverted_revision'] = commit_util.GetRevertedRevision(
            info['message'])
        info['commit_url'] = '%s/+/%s' % (repo_url, info['revision'])

        return ChangeLog.FromDict(info)