summaryrefslogtreecommitdiff
path: root/gbp/dch.py
blob: f983234fa9c6c386910436154c96a336363df585 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# vim: set fileencoding=utf-8 :
#
# (C) 2010 Rob Browning <rlb@defaultvalue.org>
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""provides git-dch helpers"""

import re

MAX_CHANGELOG_LINE_LENGTH = 76

def extract_git_dch_cmds(lines, options):
    """Return a dictionary of all Git-Dch: commands found in lines.
    The command keys will be lowercased, i.e. {'ignore' : True,
    'short': True}.  For now, all the options are binary.  Also return
    all of the lines that do not contain Git-Dch: commands."""
    commands = {}
    other_lines = []
    for line in lines:
        if line.startswith('Git-Dch: '):
            cmd = line.split(' ', 1)[1].strip().lower()
            commands[cmd] = True
        else:
            other_lines.append(line)
    return (commands, other_lines)


def filter_ignore_rx_matches(lines, options):
    """Filter any lines that match options.ignore_regex
    (i.e. --ignore-regex)."""
    if options.ignore_regex:
        ignore_re = re.compile(options.ignore_regex)
        return [line for line in lines if not ignore_re.match(line)]
    else:
        return lines


_bug_r = r'(?:bug|issue)?\#?\s?\d+'
_bug_re = re.compile(_bug_r, re.I)

def extract_bts_cmds(lines, opts):
    """Return a dictionary of the bug tracking system commands
    contained in the the given lines.  i.e. {'closed' : [1], 'fixed':
    [3, 4]}.  Right now, this will only notice a single directive
    clause on a line.  Also return all of the lines that do not
    contain bug tracking system commands."""
    bts_rx = re.compile(r'(?P<bts>%s):\s+%s' % (opts.meta_closes, _bug_r), re.I)
    commands = {}
    other_lines = []
    for line in lines:
        m = bts_rx.match(line)
        if m:
            bug_nums = [ bug.strip() for bug in _bug_re.findall(line, re.I) ]
            try:
                commands[m.group('bts')] += bug_nums
            except KeyError:
                commands[m.group('bts')] = bug_nums
        else:
            other_lines.append(line)
    return (commands, other_lines)


def extract_thanks_info(lines, options):
    """Return a list of all of the Thanks: entries, and a list of all
    of the lines that do not contain Thanks: entries."""
    thanks = []
    other_lines = []
    for line in lines:
        if line.startswith('Thanks: '):
            thanks.append(line.split(' ', 1)[1].strip())
        else:
            other_lines.append(line)
    return (thanks, other_lines)


def _ispunct(ch):
    return not ch.isalnum() and not ch.isspace()


def terminate_first_line_if_needed(lines):
    """Terminate the first line of lines with a '.' if multi-line."""
    # Don't add a period to empty or one line commit messages.
    if len(lines) < 2:
        return lines
    if lines[0] and _ispunct(lines[0][-1]):
        return lines
    if lines[1] and (_ispunct(lines[1][0]) or lines[1][0].islower()):
        return lines
    return [lines[0] + "."] + lines[1:]


def format_changelog_entry(commit_info, options, last_commit=False):
    """Return a list of lines (without newlines) as the changelog
    entry for commit_info (generated by
    GitRepository.get_commit_info()).  If last_commit is not False,
    then this entry is the last one in the series."""
    entry = [commit_info['subject']]
    body = commit_info['body'].splitlines()
    commitid = commit_info['id']
    (git_dch_cmds, body) = extract_git_dch_cmds(body, options)

    if 'ignore' in git_dch_cmds:
        return None
    if options.idlen:
        entry[0] = '[%s] ' % commitid[0:options.idlen] + entry[0]

    bts_cmds = {}
    thanks  = []
    if options.meta:
        (bts_cmds, body) = extract_bts_cmds(body, options)
        (thanks, body) = extract_thanks_info(body, options)
    body = filter_ignore_rx_matches(body, options)

    if 'full' in git_dch_cmds or (options.full and not 'short' in git_dch_cmds):
        # Add all non-blank body lines.
        entry.extend([line for line in body if line.strip()])
    if thanks:
        # Last wins for now (match old behavior).
        thanks_msg = 'Thanks to %s' % thanks[-1]
        entry.extend([thanks_msg])
    for bts in bts_cmds:
        bts_msg = '(%s: %s)' % (bts, ', '.join(bts_cmds[bts]))
        if len(entry[-1]) + len(bts_msg) >= MAX_CHANGELOG_LINE_LENGTH:
            entry.extend([''])
        else:
            entry[-1] += " "
        entry[-1] += bts_msg

    entry = terminate_first_line_if_needed(entry)
    return entry