aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarkus Lehtonen <markus.lehtonen@linux.intel.com>2014-02-04 17:54:36 +0200
committerGuido Günther <agx@sigxcpu.org>2016-06-30 16:44:25 +0200
commitc5cfb5ecc4c0fab706a5fb984ebf60dfa34390f2 (patch)
tree2be44bb6dce79846b4445e37d1a7536ed28bb573
parentaf2be3b977428958963f35c5507ac71e7018f65f (diff)
Introduce gbp-rpm-ch
Initial version of gbp rpm-ch command, a tool for maintaining RPM changelogs. The new command supports %changelog section inside spec files as well as separate changelog files ("OBS style"). Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com> Closes: #808027
-rw-r--r--docs/Makefile1
-rw-r--r--docs/common.ent1
-rw-r--r--docs/man.gbp-rpm-ch.sgml11
-rw-r--r--docs/manpages/gbp-buildpackage-rpm.sgml1
-rw-r--r--docs/manpages/gbp-import-srpm.sgml1
-rw-r--r--docs/manpages/gbp-rpm-ch.sgml357
-rw-r--r--docs/manpages/manpages.ent1
-rw-r--r--gbp/config.py15
-rw-r--r--gbp/rpm/policy.py113
-rw-r--r--gbp/scripts/rpm_ch.py449
-rw-r--r--packaging/git-buildpackage.spec1
-rw-r--r--tests/component/rpm/test_rpm_ch.py338
12 files changed, 1289 insertions, 0 deletions
diff --git a/docs/Makefile b/docs/Makefile
index 581e0a1..44786f2 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -15,6 +15,7 @@ MAN1S = \
gbp-buildpackage-rpm \
gbp-import-srpm \
gbp-pq-rpm \
+ gbp-rpm-ch \
$(NULL)
MAN5S = gbp.conf
diff --git a/docs/common.ent b/docs/common.ent
index 6a25178..303ea80 100644
--- a/docs/common.ent
+++ b/docs/common.ent
@@ -33,6 +33,7 @@
<!ENTITY gbp-buildpackage-rpm "<command>gbp&nbsp;buildpackage-rpm</command>">
<!ENTITY gbp-import-srpm "<command>gbp&nbsp;import-srpm</command>">
<!ENTITY gbp-pq-rpm "<command>gbp&nbsp;pq-rpm</command>">
+ <!ENTITY gbp-rpm-ch "<command>gbp rpm-ch</command>">
<!ENTITY rpmbuild "<command>rpmbuild</command>">
<!ENTITY gbp-builder-mock "<command>gbp-builder-mock</command>">
<!ENTITY wget "<command>wget</command>">
diff --git a/docs/man.gbp-rpm-ch.sgml b/docs/man.gbp-rpm-ch.sgml
new file mode 100644
index 0000000..63fed8d
--- /dev/null
+++ b/docs/man.gbp-rpm-ch.sgml
@@ -0,0 +1,11 @@
+<!DOCTYPE reference PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
+ <!ENTITY % COMMON SYSTEM "common.ent">
+ %COMMON;
+ <!ENTITY % MANPAGES SYSTEM "manpages/manpages-rpm.ent">
+ %MANPAGES;
+]>
+
+<reference>
+<title>git-buildpackage-rpm Manual</title>
+&man.gbp.rpm.ch;
+</reference>
diff --git a/docs/manpages/gbp-buildpackage-rpm.sgml b/docs/manpages/gbp-buildpackage-rpm.sgml
index b7138c8..ac020f8 100644
--- a/docs/manpages/gbp-buildpackage-rpm.sgml
+++ b/docs/manpages/gbp-buildpackage-rpm.sgml
@@ -639,6 +639,7 @@
<para>
<xref linkend="man.gbp.import.srpm"/>,
<xref linkend="man.gbp.pq.rpm"/>,
+ <xref linkend="man.gbp.rpm.ch">,
<citerefentry>
<refentrytitle>rpmbuild</refentrytitle>
<manvolnum>8</manvolnum>
diff --git a/docs/manpages/gbp-import-srpm.sgml b/docs/manpages/gbp-import-srpm.sgml
index daf2d75..d51ae7d 100644
--- a/docs/manpages/gbp-import-srpm.sgml
+++ b/docs/manpages/gbp-import-srpm.sgml
@@ -232,6 +232,7 @@
<para>
<xref linkend="man.gbp.buildpackage.rpm"/>,
<xref linkend="man.gbp.pq.rpm"/>,
+ <xref linkend="man.gbp.rpm.ch">,
<xref linkend="man.gbp.conf"/>,
&man.seealso.common;
</para>
diff --git a/docs/manpages/gbp-rpm-ch.sgml b/docs/manpages/gbp-rpm-ch.sgml
new file mode 100644
index 0000000..5a8b673
--- /dev/null
+++ b/docs/manpages/gbp-rpm-ch.sgml
@@ -0,0 +1,357 @@
+<refentry id="man.gbp.rpm.ch">
+ <refentryinfo>
+ <address>
+ &rpm-email;
+ </address>
+ <author>
+ &rpm-firstname;
+ &rpm-surname;
+ </author>
+ </refentryinfo>
+ <refmeta>
+ <refentrytitle>gbp-rpm-ch</refentrytitle>
+ &rpm-mansection;
+ </refmeta>
+ <refnamediv>
+ <refname>git-rpm-ch;</refname>
+ <refname>gbp-rpm-ch;</refname>
+ <refpurpose>Generate the RPM changelog from git commit messages</refpurpose>
+ </refnamediv>
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ &gbp-rpm-ch;
+ &man.common.options.synopsis;
+ <arg><option>--git-tmp-dir</option>=<replaceable>DIRECTORY</replaceable></arg>
+ <arg><option>--vendor</option>=<replaceable>VENDOR</replaceable></arg>
+ <arg><option>--packaging-branch=</option><replaceable>BRANCH-NAME</replaceable></arg>
+ <arg><option>--packaging-tag=</option><replaceable>TAG-FORMAT</replaceable></arg>
+ <arg><option>--ignore-branch</option></arg>
+ <arg><option>--packaging-dir=</option><replaceable>DIRECTORY</replaceable></arg>
+ <arg><option>--changelog-file=</option><replaceable>FILEPATH</replaceable></arg>
+ <arg><option>--spec-file=</option><replaceable>FILEPATH</replaceable></arg>
+ <arg><option>--since=</option><replaceable>COMMITISH</replaceable></arg>
+ <arg><option>--no-release</option></arg>
+ <arg><option>--[no-]git-author</option></arg>
+ <arg><option>--[no-]full</option></arg>
+ <arg><option>--id-length=</option><replaceable>NUMBER</replaceable></arg>
+ <arg><option>--ignore-regex=</option><replaceable>REGEX</replaceable></arg>
+ <arg><option>--changelog-revision=</option><replaceable>REV-FORMAT</replaceable></arg>
+ <arg><option>--git-log=</option><replaceable>GIT-LOG-OPTIONS</replaceable></arg>
+ <arg><option>--spawn-editor=<replaceable>[always|release|no]</replaceable></option></arg>
+ <arg><option>--editor-cmd=</option><replaceable>EDITOR</replaceable></arg>
+ <arg><option>--customizations=</option><replaceable>CUSTOMIZATION-FILE</replaceable></arg>
+ <arg choice="plain"><replaceable>[PATH1 PATH2]</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+ <refsect1>
+ <title>DESCRIPTION</title>
+ <para>
+ &gbp-rpm-ch; reads git commit messages up to the current tip of the current
+ branch and updates the RPM changelog from them.
+ </para>
+ <para>
+ By default, &gbp-rpm-ch; tries to guess the last &git; commit documented in
+ the changelog. Alternatively, <option>--since</option> can be used to
+ tell &gbp-rpm-ch; at which point it should start in the &git; history, or,
+ <option>--all</option> to use all commits from the &git; history.
+ </para>
+ <para>
+ The additional path arguments can be used to restrict the repository paths
+ &gbp-rpm-ch; looks at. For even more detailed control, you can use
+ <option>--git-log</option> to restrict the generated changelog entries
+ further. E.g. by using
+ <option>--git-log=</option><replaceable>"--author=Foo Bar"</replaceable>.
+ </para>
+ </refsect1>
+ <refsect1>
+ <title>OPTIONS</title>
+ <variablelist>
+ &man.common.options.description;
+
+ <varlistentry>
+ <term><option>--git-tmp-dir</option>=<replaceable>DIRECTORY</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Base directory under which temporary directories are created.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--vendor</option>=<replaceable>VENDOR</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Distribution vendor name.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--packaging-branch</option>=<replaceable>BRANCH-NAME</replaceable>
+ </term>
+ <listitem>
+ <para>
+ The branch in the Git repository the package is being developed on,
+ default is <replaceable>master</replaceable>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--ignore-branch</option>
+ </term>
+ <listitem>
+ <para>
+ Don't check if the current branch matches
+ <replaceable>PACKAGING-BRANCH</replaceable>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--packaging-tag=</option><replaceable>TAG-FORMAT</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Tag format used, when tagging releases,
+ default is <replaceable>%(vendor)s/%(version)s</replaceable>
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--packaging-dir=</option><replaceable>DIRECTORY</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Subdirectory that contains the RPM packaging files.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--changelog-file=</option><replaceable>FILEPATH</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Relative path to the changelog file to use. Special value
+ <replaceable>auto</replaceable> causes &gbp; to guess,
+ <replaceable>SPEC</replaceable> uses the spec file,
+ <replaceable>CHANGES</replaceable> uses a separate changelog file
+ (name derived spec file name with .spec suffix replaced by .changes).
+ Guessing logic is simple: use separate changelog file if it is found,
+ otherwise use the spec file.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--spec-file=</option><replaceable>FILEPATH</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Relative path to the spec file to use. Special value
+ <replaceable>auto</replaceable> causes &gbp; to search and guess.
+ Other values cause the <option>--packaging-dir</option> option to be
+ ignored: the directory of the spec file is used, instead.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--since=</option><replaceable>COMMITTISH</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Start reading commit messages at
+ <replaceable>COMMITTISH</replaceable>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--no-release</option>
+ </term>
+ <listitem>
+ <para>
+ Do not create a new changelog section, just update the last
+ changelog section.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--[no-]full</option>
+ </term>
+ <listitem>
+ <para>
+ Include the full commit message in the changelog output.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--git-log=</option><replaceable>GIT-LOG-OPTIONS</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Options passed on verbatim to git-log(1).
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--id-length=</option><replaceable>N</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Include <replaceable>N</replaceable> digits of the commit id in the
+ changelog entry. Default is to not include any commit ids at all.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--ignore-regex=</option><replaceable>REGEX</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Ignore lines in commit message matching
+ <replaceable>REGEX</replaceable> when generating the changelog.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--changelog-revision=</option><replaceable>REV-FORMAT</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Format string to use for revision field in the changelog header. The
+ following string fields are accepted:
+ <replaceable>%(upstreamversion)s</replaceable> the upstream version;
+ <replaceable>%(release)s</replaceable> the rpm patchlevel, i.e.
+ Release; <replaceable>%(version)s</replaceable> full rpm package
+ version; <replaceable>%(tagname)s</replaceable> tag/commit, i.e.
+ basically what <command>git-describe</command> would give.
+ If empty or not defined the default from packaging policy is used.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--ignore-regex=</option><replaceable>REGEX</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Ignore commit lines matching <replaceable>REGEX</replaceable>
+ when generating the changelog.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--[no-]git-author</option>
+ </term>
+ <listitem>
+ <para>
+ Use user.name and user.email from
+ <application>git-config</application>(1) for the changelog header.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--spawn-editor=<replaceable>[always|release|no]</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Whether to spawn an editor: always, when doing a release or never.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--editor-cmd=<replaceable>EDITOR</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ The editor to use for editing the changelog.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--customizations=</option><replaceable>CUSTOMIZATION-FILE</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Load Python code from <replaceable>CUSTOMIZATION-FILE</replaceable>.
+ At the moment, the only useful thing the code can do is define a
+ custom ChangelogEntryFormatter class.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+ <refsect1>
+ <title>META TAGS</title>
+ <para>
+ Additional to the above options the formatting of the new changelog entries
+ (one-per-commit) in the changelog can be modified by special tags (called
+ Meta Tags) given in the git commit message. The tags must start at the
+ first column of a commit message but can appear on any line. They are of
+ the form <option>Tagname</option>: <replaceable>VALUE</replaceable>. Valid
+ Meta Tags are:
+ </para>
+ <variablelist>
+ <varlistentry>
+ <term><option>Git-Rpm-Ch</option>: <replaceable>ACTION</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Supported actions are: <replaceable>Ignore</replaceable> which will
+ ignore this commit when generating new changelog entries.
+ <replaceable>Short</replaceable> which will only use the description
+ (the first line) of the commit message when generating the changelog
+ entry (useful when <option>--full</option> is given) and
+ <replaceable>Full</replaceable> which will use the full commit
+ message when generating the changelog entry (useful when
+ <option>--full</option> is not given).
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>[Close|Closes|...]</option>: <replaceable>BUGNUMBER</replaceable>
+ </term>
+ <listitem>
+ <para>
+ Indicate in the changelog entry that bug
+ <replaceable>BUGNUMBER</replaceable> was addressed in this commit.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ <para>
+ The following git commit message:
+ </para>
+ <screen>
+ Document meta tags
+
+ so one doesn't have to consult the manual
+
+ Git-Rpm-Ch: Short
+ Closes: #636088
+ </screen>
+ <para>
+ Results in this changelog entry:
+ </para>
+ <screen>
+ - Document meta tags (Closes: #636088)
+ </screen>
+ </refsect1>
+ <refsect1>
+ &man.gbp.config-files;
+ </refsect1>
+ <refsect1>
+ <title>SEE ALSO</title>
+ <para>
+ <xref linkend="man.gbp.buildpackage.rpm">,
+ <xref linkend="man.gbp.import.srpm">,
+ <xref linkend="man.gbp.import.orig.rpm">,
+ <xref linkend="man.gbp.conf">,
+ &man.seealso.common;
+ <ulink url="https://honk.sigxcpu.org/cl2vcs">
+ <citetitle>Cl2vcs</citetitle></ulink>,
+ </para>
+ </refsect1>
+ <refsect1>
+ <title>AUTHOR</title>
+ <para>
+ &rpm-username; &rpm-email;
+ </para>
+ </refsect1>
+</refentry>
diff --git a/docs/manpages/manpages.ent b/docs/manpages/manpages.ent
index c212150..01700bb 100644
--- a/docs/manpages/manpages.ent
+++ b/docs/manpages/manpages.ent
@@ -15,5 +15,6 @@
<!ENTITY man.gbp.buildpackage.rpm SYSTEM "gbp-buildpackage-rpm.sgml">
<!ENTITY man.gbp.import.srpm SYSTEM "gbp-import-srpm.sgml">
<!ENTITY man.gbp.pq.rpm SYSTEM "gbp-pq-rpm.sgml">
+<!ENTITY man.gbp.rpm.ch SYSTEM "gbp-rpm-ch.sgml">
<!ENTITY % COMMON.OPTIONS SYSTEM "man.common-options.ent">
%COMMON.OPTIONS;
diff --git a/gbp/config.py b/gbp/config.py
index 8b1d95c..fde6678 100644
--- a/gbp/config.py
+++ b/gbp/config.py
@@ -668,6 +668,10 @@ class GbpOptionParserRpm(GbpOptionParser):
'mock-root' : '',
'mock-options' : '',
'native' : 'auto',
+ 'changelog-file' : 'auto',
+ 'changelog-revision' : '',
+ 'spawn-editor' : 'always',
+ 'editor-cmd' : 'vim',
})
help = dict(GbpOptionParser.help)
@@ -717,6 +721,17 @@ class GbpOptionParserRpm(GbpOptionParser):
"default is '%(mock-options)s'"),
'native':
"Treat this package as native, default is '%(native)s'",
+ 'changelog-file':
+ "Changelog file to be used, default is '%(changelog-file)s'",
+ 'changelog-revision':
+ "Format string for the revision field in the changelog header. "
+ "If empty or not defined the default from packaging policy is "
+ "used.",
+ 'editor-cmd':
+ "Editor command to use",
+ 'git-author':
+ "Use name and email from git-config for the changelog header, "
+ "default is '%(git-author)s'",
})
# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·:
diff --git a/gbp/rpm/policy.py b/gbp/rpm/policy.py
index f52b870..0b389f4 100644
--- a/gbp/rpm/policy.py
+++ b/gbp/rpm/policy.py
@@ -17,7 +17,9 @@
"""Default packaging policy for RPM"""
import re
+
from gbp.pkg import PkgPolicy, parse_archive_filename
+from gbp.scripts.common.pq import parse_gbp_commands
class RpmPkgPolicy(PkgPolicy):
"""Packaging policy for RPM"""
@@ -86,3 +88,114 @@ class RpmPkgPolicy(PkgPolicy):
header_time_format = "%a %b %d %Y"
header_rev_format = "%(version)s"
+
+ class ChangelogEntryFormatter(object):
+ """Helper class for generating changelog entries from git commits"""
+
+ # Maximum length for a changelog entry line
+ max_entry_line_length = 76
+ # Bug tracking system related meta tags recognized from git commit msg
+ bts_meta_tags = ("Close", "Closes", "Fixes", "Fix")
+ # Regexp for matching bug tracking system ids (e.g. "bgo#123")
+ bug_id_re = r'[A-Za-z0-9#_\-]+'
+
+ @classmethod
+ def _parse_bts_tags(cls, lines, meta_tags):
+ """
+ Parse and filter out bug tracking system related meta tags from
+ commit message.
+
+ @param lines: commit message
+ @type lines: C{list} of C{str}
+ @param meta_tags: meta tags to look for
+ @type meta_tags: C{tuple} of C{str}
+ @return: bts-ids per meta tag and the non-mathced lines
+ @rtype: (C{dict}, C{list} of C{str})
+ """
+ tags = {}
+ other_lines = []
+ bts_re = re.compile(r'^(?P<tag>%s):\s*(?P<ids>.*)' %
+ ('|'.join(meta_tags)), re.I)
+ bug_id_re = re.compile(cls.bug_id_re)
+ for line in lines:
+ match = bts_re.match(line)
+ if match:
+ tag = match.group('tag')
+ ids_str = match.group('ids')
+ bug_ids = [bug_id.strip() for bug_id in
+ bug_id_re.findall(ids_str)]
+ if tag in tags:
+ tags[tag] += bug_ids
+ else:
+ tags[tag] = bug_ids
+ else:
+ other_lines.append(line)
+ return (tags, other_lines)
+
+ @classmethod
+ def _extra_filter(cls, lines, ignore_re):
+ """
+ Filter out specific lines from the commit message.
+
+ @param lines: commit message
+ @type lines: C{list} of C{str}
+ @param ignore_re: regexp for matching ignored lines
+ @type ignore_re: C{str}
+ @return: filtered commit message
+ @rtype: C{list} of C{str}
+ """
+ if ignore_re:
+ match = re.compile(ignore_re)
+ return [line for line in lines if not match.match(line)]
+ else:
+ return lines
+
+ @classmethod
+ def compose(cls, commit_info, **kwargs):
+ """
+ Generate a changelog entry from a git commit.
+
+ @param commit_info: info about the commit
+ @type commit_info: C{commit_info} object from
+ L{gbp.git.repository.GitRepository.get_commit_info()}.
+ @param kwargs: additional arguments to the compose() method,
+ currently we recognize 'full', 'id_len' and 'ignore_re'
+ @type kwargs: C{dict}
+ @return: formatted changelog entry
+ @rtype: C{list} of C{str}
+ """
+ # Parse and filter out gbp command meta-tags
+ cmds, body = parse_gbp_commands(commit_info, 'gbp-rpm-ch',
+ ('ignore', 'short', 'full'), ())
+ body = body.splitlines()
+ if 'ignore' in cmds:
+ return None
+
+ # Parse and filter out bts-related meta-tags
+ bts_tags, body = cls._parse_bts_tags(body, cls.bts_meta_tags)
+
+ # Additional filtering
+ body = cls._extra_filter(body, kwargs['ignore_re'])
+
+ # Generate changelog entry
+ subject = commit_info['subject']
+ commitid = commit_info['id']
+ if kwargs['id_len']:
+ text = ["- [%s] %s" % (commitid[0:kwargs['id_len']], subject)]
+ else:
+ text = ["- %s" % subject]
+
+ # Add all non-filtered-out lines from commit message, unless 'short'
+ if (kwargs['full'] or 'full' in cmds) and not 'short' in cmds:
+ # Add all non-blank body lines.
+ text.extend([" " + line for line in body if line.strip()])
+
+ # Add bts tags and ids in the end
+ for tag, ids in bts_tags.iteritems():
+ bts_msg = " (%s: %s)" % (tag, ', '.join(ids))
+ if len(text[-1]) + len(bts_msg) >= cls.max_entry_line_length:
+ text.append(" ")
+ text[-1] += bts_msg
+
+ return text
+
diff --git a/gbp/scripts/rpm_ch.py b/gbp/scripts/rpm_ch.py
new file mode 100644
index 0000000..3c5ea8d
--- /dev/null
+++ b/gbp/scripts/rpm_ch.py
@@ -0,0 +1,449 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2007, 2008, 2009, 2010, 2013 Guido Guenther <agx@sigxcpu.org>
+# (C) 2014-2015 Intel Corporation <markus.lehtonen@linux.intel.com>
+# 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, please see
+# <http://www.gnu.org/licenses/>
+#
+"""Generate RPM changelog entries from git commit messages"""
+
+import ConfigParser
+from datetime import datetime
+import os.path
+import pwd
+import re
+import sys
+import socket
+
+import gbp.command_wrappers as gbpc
+import gbp.log
+from gbp.config import GbpOptionParserRpm, GbpOptionGroup
+from gbp.errors import GbpError
+from gbp.rpm import (guess_spec, NoSpecError, SpecFile, split_version_str,
+ compose_version_str)
+from gbp.rpm.changelog import Changelog, ChangelogParser, ChangelogError
+from gbp.rpm.git import GitRepositoryError, RpmGitRepository
+from gbp.rpm.policy import RpmPkgPolicy
+from gbp.tmpfile import init_tmpdir, del_tmpdir
+
+
+ChangelogEntryFormatter = RpmPkgPolicy.ChangelogEntryFormatter
+
+
+class ChangelogFile(object):
+ """Container for changelog file, whether it be a standalone changelog
+ or a spec file"""
+
+ def __init__(self, file_path):
+ parser = ChangelogParser(RpmPkgPolicy)
+
+ if os.path.splitext(file_path)[1] == '.spec':
+ gbp.log.debug("Using spec file '%s' as changelog" % file_path)
+ self._file = SpecFile(file_path)
+ self.changelog = parser.raw_parse_string(self._file.get_changelog())
+ else:
+ self._file = os.path.abspath(file_path)
+ if not os.path.exists(file_path):
+ gbp.log.info("Changelog '%s' not found, creating new "
+ "changelog file" % file_path)
+ self.changelog = Changelog(RpmPkgPolicy)
+ else:
+ gbp.log.debug("Using changelog file '%s'" % file_path)
+ self.changelog = parser.raw_parse_file(self._file)
+
+ # Parse topmost section and try to determine the start commit
+ if self.changelog.sections:
+ self.changelog.sections[0] = parser.parse_section(
+ self.changelog.sections[0])
+
+ def write(self):
+ """Write changelog file to disk"""
+ if isinstance(self._file, SpecFile):
+ self._file.set_changelog(str(self.changelog))
+ self._file.write_spec_file()
+ else:
+ with open(self._file, 'w') as fobj:
+ fobj.write(str(self.changelog))
+
+ @property
+ def path(self):
+ """File path"""
+ if isinstance(self._file, SpecFile):
+ return self._file.specpath
+ else:
+ return self._file
+
+def load_customizations(customization_file):
+ """Load user defined customizations file"""
+ # Load customization file
+ if not customization_file:
+ return
+ customizations = {}
+ try:
+ execfile(customization_file, customizations, customizations)
+ except Exception as err:
+ raise GbpError("Failed to load customization file: %s" % err)
+
+ # Set customization classes / functions
+ global ChangelogEntryFormatter
+ if 'ChangelogEntryFormatter' in customizations:
+ ChangelogEntryFormatter = customizations.get('ChangelogEntryFormatter')
+
+
+def determine_editor(options):
+ """Determine text editor"""
+
+ # Check if we need to spawn an editor
+ states = ['always']
+ if options.release:
+ states.append('release')
+ if options.spawn_editor not in states:
+ return None
+
+ # Determine the correct editor
+ if options.editor_cmd:
+ return options.editor_cmd
+ elif 'EDITOR' in os.environ:
+ return os.environ['EDITOR']
+ else:
+ return 'vi'
+
+
+def check_branch(repo, options):
+ """Check the current git branch"""
+ try:
+ branch = repo.get_branch()
+ except GitRepositoryError:
+ branch = None
+ if options.packaging_branch != branch and not options.ignore_branch:
+ gbp.log.err("You are not on branch '%s' but on '%s'" %
+ (options.packaging_branch, branch))
+ raise GbpError("Use --ignore-branch to ignore or "
+ "--packaging-branch to set the branch name.")
+
+
+def parse_spec_file(repo, options):
+ """Find and parse spec file"""
+ if options.spec_file:
+ spec_path = os.path.join(repo.path, options.spec_file)
+ spec = SpecFile(spec_path)
+ else:
+ spec = guess_spec(os.path.join(repo.path, options.packaging_dir),
+ True, os.path.basename(repo.path) + '.spec')
+ options.packaging_dir = spec.specdir
+ return spec
+
+
+def parse_changelog_file(repo, spec, options):
+ """Find and parse changelog file"""
+ changes_file_name = os.path.splitext(spec.specfile)[0] + '.changes'
+ changes_file_path = os.path.join(options.packaging_dir, changes_file_name)
+
+ # Determine changelog file path
+ if options.changelog_file == "SPEC":
+ changelog_path = spec.specpath
+ elif options.changelog_file == "CHANGES":
+ changelog_path = changes_file_path
+ elif options.changelog_file == 'auto':
+ if os.path.exists(changes_file_path):
+ changelog_path = changes_file_path
+ else:
+ changelog_path = spec.specpath
+ else:
+ changelog_path = os.path.join(repo.path, options.changelog_file)
+
+ return ChangelogFile(changelog_path)
+
+
+def guess_commit(section, repo, options):
+ """Guess the last commit documented in a changelog header"""
+
+ if not section:
+ return None
+ header = section.header
+
+ # Try to parse the fields from the header revision
+ rev_re = '^%s$' % re.sub(r'%\((\S+?)\)s', r'(?P<\1>\S+)',
+ options.changelog_revision)
+ match = re.match(rev_re, header['revision'], re.I)
+ fields = match.groupdict() if match else {}
+
+ # First, try to find tag-name, if present
+ if 'tagname' in fields:
+ gbp.log.debug("Trying to find tagname %s" % fields['tagname'])
+ try:
+ return repo.rev_parse("%s^0" % fields['tagname'])
+ except GitRepositoryError:
+ gbp.log.warn("Changelog points to tagname '%s' which is not found "
+ "in the git repository" % fields['tagname'])
+
+ # Next, try to find packaging tag matching the version
+ tag_str_fields = {'vendor': options.vendor}
+ if 'version' in fields:
+ gbp.log.debug("Trying to find packaging tag for version '%s'" %
+ fields['version'])
+ full_version = fields['version']
+ tag_str_fields.update(split_version_str(full_version))
+ elif 'upstreamversion' in fields:
+ gbp.log.debug("Trying to find packaging tag for version '%s'" %
+ fields['upstreamversion'])
+ tag_str_fields['upstreamversion'] = fields['upstreamversion']
+ if 'release' in fields:
+ tag_str_fields['release'] = fields['release']
+ commit = repo.find_version(options.packaging_tag,
+ tag_str_fields)
+ if commit:
+ return commit
+ else:
+ gbp.log.info("Couldn't find packaging tag for version %s" %
+ header['revision'])
+
+ # As a last resort we look at the timestamp
+ timestamp = header['time'].isoformat()
+ last = repo.get_commits(num=1, options="--until='%s'" % timestamp)
+ if last:
+ gbp.log.info("Using commit (%s) before the last changelog timestamp "
+ "(%s)" % (last, timestamp))
+ return last[0]
+ return None
+
+
+def get_start_commit(changelog, repo, options):
+ """Get the start commit from which to generate new entries"""
+ if options.since:
+ since = options.since
+ else:
+ if changelog.sections:
+ since = guess_commit(changelog.sections[0], repo, options)
+ else:
+ since = None
+ if not since:
+ raise GbpError("Couldn't determine starting point from "
+ "changelog, please use the '--since' option")
+ gbp.log.info("Continuing from commit '%s'" % since)
+ return since
+
+
+def get_author(repo, use_git_config):
+ """Get author and email from git configuration"""
+ author = email = None
+
+ if use_git_config:
+ modifier = repo.get_author_info()
+ author = modifier.name
+ email = modifier.email
+
+ passwd_data = pwd.getpwuid(os.getuid())
+ if not author:
+ # On some distros (Ubuntu, at least) the gecos field has it's own
+ # internal structure of comma-separated fields
+ author = passwd_data.pw_gecos.split(',')[0].strip()
+ if not author:
+ author = passwd_data.pw_name
+ if not email:
+ if 'EMAIL' in os.environ:
+ email = os.environ['EMAIL']
+ else:
+ email = "%s@%s" % (passwd_data.pw_name, socket.getfqdn())
+
+ return author, email
+
+
+def entries_from_commits(changelog, repo, commits, options):
+ """Generate a list of formatted changelog entries from a list of commits"""
+ entries = []
+ for commit in commits:
+ info = repo.get_commit_info(commit)
+ entry_text = ChangelogEntryFormatter.compose(info, full=options.full,
+ ignore_re=options.ignore_regex, id_len=options.idlen)
+ if entry_text:
+ entries.append(changelog.create_entry(author=info['author'].name,
+ text=entry_text))
+ return entries
+
+
+def update_changelog(changelog, entries, repo, spec, options):
+ """Update the changelog with a range of commits"""
+ # Get info for section header
+ now = datetime.now()
+ name, email = get_author(repo, options.git_author)
+ rev_str_fields = dict(spec.version,
+ version=compose_version_str(spec.version),
+ vendor=options.vendor,
+ tagname=repo.describe('HEAD', longfmt=True,
+ always=True))
+ try:
+ revision = options.changelog_revision % rev_str_fields
+ except KeyError as err:
+ raise GbpError("Unable to construct revision field: unknown key "
+ "%s, only %s are accepted" % (err, rev_str_fields.keys()))
+
+ # Add a new changelog section if new release or an empty changelog
+ if options.release or not changelog.sections:
+ top_section = changelog.add_section(time=now, name=name,
+ email=email, revision=revision)
+ else:
+ # Re-use already parsed top section
+ top_section = changelog.sections[0]
+ top_section.set_header(time=now, name=name,
+ email=email, revision=revision)
+
+ # Add new entries to the topmost section
+ for entry in entries:
+ top_section.append_entry(entry)
+
+
+def build_parser(name):
+ """Construct command line parser"""
+ try:
+ parser = GbpOptionParserRpm(command=os.path.basename(name),
+ prefix='', usage='%prog [options] paths')
+ except ConfigParser.ParsingError as err:
+ gbp.log.error('invalid config file: %s' % err)
+ return None
+
+ range_grp = GbpOptionGroup(parser, "commit range options",
+ "which commits to add to the changelog")
+ format_grp = GbpOptionGroup(parser, "changelog entry formatting",
+ "how to format the changelog entries")
+ naming_grp = GbpOptionGroup(parser, "naming",
+ "branch names, tag formats, directory and file naming")
+ parser.add_option_group(range_grp)
+ parser.add_option_group(format_grp)
+ parser.add_option_group(naming_grp)
+
+ # Non-grouped options
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="verbose command execution")
+ parser.add_config_file_option(option_name="color", dest="color",
+ type='tristate')
+ parser.add_config_file_option(option_name="color-scheme",
+ dest="color_scheme")
+ parser.add_config_file_option(option_name="tmp-dir", dest="tmp_dir")
+ parser.add_config_file_option(option_name="vendor", action="store",
+ dest="vendor")
+ parser.add_config_file_option(option_name="git-log", dest="git_log",
+ help="options to pass to git-log, default is '%(git-log)s'")
+ parser.add_boolean_config_file_option(option_name="ignore-branch",
+ dest="ignore_branch")
+ parser.add_config_file_option(option_name="customizations",
+ dest="customization_file",
+ help="Load Python code from CUSTOMIZATION_FILE. At the "
+ "moment, the only useful thing the code can do is define a "
+ "custom ChangelogEntryFormatter class.")
+
+ # Naming group options
+ naming_grp.add_config_file_option(option_name="packaging-branch",
+ dest="packaging_branch")
+ naming_grp.add_config_file_option(option_name="packaging-tag",
+ dest="packaging_tag")
+ naming_grp.add_config_file_option(option_name="packaging-dir",
+ dest="packaging_dir")
+ naming_grp.add_config_file_option(option_name="changelog-file",
+ dest="changelog_file")
+ naming_grp.add_config_file_option(option_name="spec-file", dest="spec_file")
+ # Range group options
+ range_grp.add_option("-s", "--since", dest="since",
+ help="commit to start from (e.g. HEAD^^^, release/0.1.2)")
+ # Formatting group options
+ format_grp.add_option("--no-release", action="store_false", default=True,
+ dest="release",
+ help="no release, just update the last changelog section")
+ format_grp.add_boolean_config_file_option(option_name="git-author",
+ dest="git_author")
+ format_grp.add_boolean_config_file_option(option_name="full", dest="full")
+ format_grp.add_config_file_option(option_name="id-length", dest="idlen",
+ help="include N digits of the commit id in the changelog "
+ "entry, default is '%(id-length)s'",
+ type="int", metavar="N")
+ format_grp.add_config_file_option(option_name="ignore-regex",
+ dest="ignore_regex",
+ help="Ignore lines in commit message matching regex, "
+ "default is '%(ignore-regex)s'")
+ format_grp.add_config_file_option(option_name="changelog-revision",
+ dest="changelog_revision")
+ format_grp.add_config_file_option(option_name="spawn-editor",
+ dest="spawn_editor")
+ format_grp.add_config_file_option(option_name="editor-cmd",
+ dest="editor_cmd")
+ return parser
+
+def parse_args(argv):
+ """Parse command line and config file options"""
+ parser = build_parser(argv[0])
+ if not parser:
+ return None, None
+
+ options, args = parser.parse_args(argv[1:])
+
+ if not options.changelog_revision:
+ options.changelog_revision = RpmPkgPolicy.Changelog.header_rev_format
+
+ gbp.log.setup(options.color, options.verbose, options.color_scheme)
+
+ return options, args
+
+def main(argv):
+ """Script main function"""
+ options, args = parse_args(argv)
+ if not options:
+ return 1
+
+ try:
+ init_tmpdir(options.tmp_dir, prefix='rpm-ch_')
+
+ load_customizations(options.customization_file)
+ editor_cmd = determine_editor(options)
+
+ repo = RpmGitRepository('.')
+ check_branch(repo, options)
+
+ # Find and parse spec file
+ spec = parse_spec_file(repo, options)
+
+ # Find and parse changelog file
+ ch_file = parse_changelog_file(repo, spec, options)
+ since = get_start_commit(ch_file.changelog, repo, options)
+
+ # Get range of commits from where to generate changes
+ if args:
+ gbp.log.info("Only looking for changes in '%s'" % ", ".join(args))
+ commits = repo.get_commits(since=since, until='HEAD', paths=args,
+ options=options.git_log.split(" "))
+ commits.reverse()
+ if not commits:
+ gbp.log.info("No changes detected from %s to %s." % (since, 'HEAD'))
+
+ # Do the actual update
+ entries = entries_from_commits(ch_file.changelog, repo, commits,
+ options)
+ update_changelog(ch_file.changelog, entries, repo, spec, options)
+
+ # Write to file
+ ch_file.write()
+
+ if editor_cmd:
+ gbpc.Command(editor_cmd, [ch_file.path])()
+
+ except (GbpError, GitRepositoryError, ChangelogError, NoSpecError) as err:
+ if len(err.__str__()):
+ gbp.log.err(err)
+ return 1
+ finally:
+ del_tmpdir()
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/packaging/git-buildpackage.spec b/packaging/git-buildpackage.spec
index fcf03f1..d184b32 100644
--- a/packaging/git-buildpackage.spec
+++ b/packaging/git-buildpackage.spec
@@ -249,6 +249,7 @@ done
%{_mandir}/man1/gbp-buildpackage-rpm.1*
%{_mandir}/man1/gbp-pq-rpm.1*
%{_mandir}/man1/gbp-import-srpm.1*
+%{_mandir}/man1/gbp-rpm-ch.1*
%endif
diff --git a/tests/component/rpm/test_rpm_ch.py b/tests/component/rpm/test_rpm_ch.py
new file mode 100644
index 0000000..44507c4
--- /dev/null
+++ b/tests/component/rpm/test_rpm_ch.py
@@ -0,0 +1,338 @@
+# vim: set fileencoding=utf-8 :
+#
+# (C) 2013-2015 Intel Corporation <markus.lehtonen@linux.intel.com>
+# 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, please see
+# <http://www.gnu.org/licenses/>
+"""Tests for the git-rpm-ch tool"""
+
+import os
+import re
+from nose.tools import assert_raises, eq_, ok_ # pylint: disable=E0611
+
+from gbp.scripts.rpm_ch import main as rpm_ch
+from gbp.git import GitRepository
+
+from tests.component.rpm import RpmRepoTestBase
+
+# Disable "Method could be a function warning"
+# pylint: disable=R0201
+
+
+def mock_ch(args):
+ """Wrapper for git-rpm-ch"""
+
+ return rpm_ch(['arg0', '--packaging-branch=master',
+ '--spawn-editor=never'] + args)
+
+class TestRpmCh(RpmRepoTestBase):
+ """Basic tests for git-rpm-ch"""
+
+ def setUp(self):
+ """Test case setup"""
+ super(TestRpmCh, self).setUp()
+ # Set environment so that commits succeed without git config
+ os.environ['GIT_AUTHOR_NAME'] = 'My Name'
+ os.environ['GIT_COMMITTER_NAME'] = 'My Name'
+ os.environ['EMAIL'] = 'me@example.com'
+
+ @staticmethod
+ def read_file(filename):
+ """Read file to a list"""
+ with open(filename) as fobj:
+ return fobj.readlines()
+
+ def test_invalid_args(self):
+ """See that git-rpm-ch fails gracefully when called with invalid args"""
+ GitRepository.create('.')
+
+ with assert_raises(SystemExit):
+ mock_ch(['--invalid-opt'])
+
+ def test_import_outside_repo(self):
+ """Run git-rpm-ch when not in a git repository"""
+ eq_(mock_ch([]), 1)
+ self._check_log(0, 'gbp:error: No Git repository at ')
+
+ def test_invalid_config_file(self):
+ """Test invalid config file"""
+ # Create dummy invalid config file and run git-rpm-ch
+ GitRepository.create('.')
+ with open('.gbp.conf', 'w') as conffd:
+ conffd.write('foobar\n')
+ eq_(mock_ch([]), 1)
+ self._check_log(0, 'gbp:error: invalid config file: File contains no '
+ 'section headers.')
+
+ def test_update_spec_changelog(self):
+ """Test updating changelog in spec"""
+ repo = self.init_test_repo('gbp-test')
+ eq_(mock_ch([]), 0)
+ eq_(repo.status(), {' M': ['gbp-test.spec']})
+
+ def test_update_changes_file(self):
+ """Test updating a separate changes file"""
+ repo = self.init_test_repo('gbp-test-native')
+ eq_(mock_ch([]), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test-native.changes']})
+
+ def test_create_spec_changelog(self):
+ """Test creating changelog in spec file"""
+ repo = self.init_test_repo('gbp-test2')
+ orig_content = self.read_file('packaging/gbp-test2.spec')
+
+ # Fails if no starting point is given
+ eq_(mock_ch([]), 1)
+ self._check_log(-1, "gbp:error: Couldn't determine starting point")
+
+ # Give starting point
+ eq_(mock_ch(['--since=HEAD^']), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test2.spec']})
+ content = self.read_file('packaging/gbp-test2.spec')
+ # Should contain 4 lines (%changelog, header, 1 entry and an empty line)
+ eq_(len(content), len(orig_content) + 4)
+
+ def test_create_changes_file(self):
+ """Test creating a separate changes file"""
+ repo = self.init_test_repo('gbp-test2')
+
+ # Fails if no starting point is given
+ eq_(mock_ch(['--changelog-file=CHANGES']), 1)
+ self._check_log(-1, "gbp:error: Couldn't determine starting point")
+
+ # Give starting point
+ eq_(mock_ch(['--since=HEAD^', '--changelog-file=CHANGES']), 0)
+ eq_(repo.status(), {'??': ['packaging/gbp-test2.changes']})
+ content = self.read_file('packaging/gbp-test2.changes')
+ # Should contain 3 lines (header, 1 entry and an empty line)
+ eq_(len(content), 3)
+
+ def test_option_changelog_file(self):
+ """Test the --changelog-file cmdline option"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ # Guess changelog file
+ eq_(mock_ch(['--changelog-file=CHANGES']), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test-native.changes']})
+
+ # Use spec file as changelog
+ eq_(mock_ch(['--changelog-file=SPEC', '--since=HEAD^']), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test-native.changes',
+ 'packaging/gbp-test-native.spec']})
+
+ # Arbitrary name
+ eq_(mock_ch(['--changelog-file=foo.changes', '--since=HEAD^']), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test-native.changes',
+ 'packaging/gbp-test-native.spec'],
+ '??': ['foo.changes']})
+
+ def test_option_spec_file(self):
+ """Test the --spec-file cmdline option"""
+ repo = self.init_test_repo('gbp-test2')
+
+ eq_(mock_ch(['--spec-file=foo.spec']), 1)
+ self._check_log(-1, "gbp:error: Unable to read spec file")
+
+ eq_(mock_ch(['--spec-file=']), 1)
+ self._check_log(-1, "gbp:error: Multiple spec files found")
+
+ eq_(mock_ch(['--spec-file=packaging/gbp-test2.spec', '--since=HEAD^']),
+ 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test2.spec']})
+
+ def test_option_packaging_dir(self):
+ """Test the --packaging-dir cmdline option"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ eq_(mock_ch(['--packaging-dir=foo']), 1)
+ self._check_log(-1, "gbp:error: No spec file found")
+
+ # Packaging dir should be taken from spec file if it is defined
+ eq_(mock_ch(['--packaging-dir', 'foo', '--spec-file',
+ 'packaging/gbp-test-native.spec']), 0)
+ eq_(repo.status(), {' M': ['packaging/gbp-test-native.changes']})
+
+ def test_branch_options(self):
+ """Test the --packaging-branch and --ignore-branch cmdline options"""
+ self.init_test_repo('gbp-test-native')
+
+ eq_(mock_ch(['--packaging-branch=foo']), 1)
+ self._check_log(-2, "gbp:error: You are not on branch 'foo'")
+
+ eq_(mock_ch(['--packaging-branch=foo', '--ignore-branch']), 0)
+
+ def test_option_no_release(self):
+ """Test the --no-release cmdline option"""
+ self.init_test_repo('gbp-test-native')
+ orig_content = self.read_file('packaging/gbp-test-native.changes')
+
+ eq_(mock_ch(['--no-release']), 0)
+ content = self.read_file('packaging/gbp-test-native.changes')
+ # Only one line (entry) added
+ eq_(len(content), len(orig_content) + 1)
+
+ def test_author(self):
+ """Test determining the author name/email"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ # Test taking email address from env
+ os.environ['EMAIL'] = 'user@host.com'
+ eq_(mock_ch([]), 0)
+ header = self.read_file('packaging/gbp-test-native.changes')[0]
+ ok_(re.match(r'.+ <user@host\.com> .+', header))
+
+ # Missing git config setting should not cause a failure
+ del os.environ['EMAIL']
+ del os.environ['GIT_AUTHOR_NAME']
+ os.environ['GIT_CONFIG_NOSYSTEM'] = '1'
+ os.environ['HOME'] = os.path.abspath('.')
+ eq_(mock_ch(['--git-author', '--since=HEAD^1']), 0)
+
+ # Test the --git-author option
+ with open(os.path.join(repo.git_dir, 'config'), 'a') as fobj:
+ fobj.write('[user]\n name=John Doe\n email=jd@host.com\n')
+ eq_(mock_ch(['--git-author', '--since=HEAD^']), 0)
+ header = self.read_file('packaging/gbp-test-native.changes')[0]
+ ok_(re.match(r'.+ John Doe <jd@host\.com> .+', header), header)
+
+ def test_option_full(self):
+ """Test the --full cmdline option"""
+ repo = self.init_test_repo('gbp-test-native')
+ orig_content = self.read_file('packaging/gbp-test-native.changes')
+
+ eq_(mock_ch(['--full', '--since=HEAD^']), 0)
+ commit_msg_body = repo.get_commit_info('HEAD')['body']
+ full_msg = [line for line in commit_msg_body.splitlines() if line]
+ content = self.read_file('packaging/gbp-test-native.changes')
+ # New lines: header, 1 entry "header", entry "body" from commit message
+ # and one empty line
+ eq_(len(content), len(orig_content) + 3 + len(full_msg))
+
+ def test_option_ignore_regex(self):
+ """Test the --ignore-regex cmdline option"""
+ repo = self.init_test_repo('gbp-test-native')
+ orig_content = self.read_file('packaging/gbp-test-native.changes')
+
+ eq_(mock_ch(['--full', '--since', 'HEAD^', '--ignore-regex',
+ 'Signed-off-by:.*']), 0)
+ commit_msg_body = repo.get_commit_info('HEAD')['body']
+ full_msg = [line for line in commit_msg_body.splitlines() if
+ (line and not line.startswith('Signed-off-by:'))]
+ content = self.read_file('packaging/gbp-test-native.changes')
+ # New lines: header, 1 entry "header", filtered entry "body" from
+ # commit message and one empty line
+ eq_(len(content), len(orig_content) + 3 + len(full_msg))
+
+ def test_option_id_len(self):
+ """Test the --id-len cmdline option"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ eq_(mock_ch(['--id-len=10']), 0)
+ commit_id = repo.rev_parse('HEAD', 10)
+ content = self.read_file('packaging/gbp-test-native.changes')
+ ok_(content[1].startswith('- [%s] ' % commit_id))
+
+ def test_option_changelog_revision(self):
+ """Test the --id-len cmdline option"""
+ self.init_test_repo('gbp-test-native')
+
+ # Test invalid format (unknown field)
+ eq_(mock_ch(['--changelog-revision=%(unknown_field)s']), 1)
+ self._check_log(-1, 'gbp:error: Unable to construct revision field')
+
+ # Test acceptable format
+ eq_(mock_ch(['--changelog-revision=foobar']), 0)
+ header = self.read_file('packaging/gbp-test-native.changes')[0]
+ ok_(re.match(r'.+ foobar$', header))
+
+ def test_option_editor_cmd(self):
+ """Test the --editor-cmd and --spawn-editor cmdline options"""
+ repo = self.init_test_repo('gbp-test-native')
+ eq_(mock_ch(['--spawn-editor=release', '--editor-cmd=rm']), 0)
+ eq_(repo.status(), {' D': ['packaging/gbp-test-native.changes']})
+
+ repo.force_head('HEAD', hard=True)
+ ok_(repo.is_clean())
+
+ os.environ['EDITOR'] = 'rm'
+ eq_(mock_ch(['--spawn-editor=always', '--editor-cmd=']),
+ 0)
+
+ def test_user_customizations(self):
+ """Test the user customizations"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ # Non-existent customization file
+ eq_(mock_ch(['--customizations=customizations.py']), 1)
+
+ # Create user customizations file
+ with open('customizations.py', 'w') as fobj:
+ fobj.write("class ChangelogEntryFormatter(object):\n")
+ fobj.write(" @classmethod\n")
+ fobj.write(" def compose(cls, commit_info, **kwargs):\n")
+ fobj.write(" return ['- %s' % commit_info['id']]\n")
+
+ eq_(mock_ch(['--customizations=customizations.py']), 0)
+ entry = self.read_file('packaging/gbp-test-native.changes')[1]
+ sha = repo.rev_parse('HEAD')
+ eq_(entry, '- %s\n' % sha)
+
+ def test_paths(self):
+ """Test tracking of certain paths only"""
+ repo = self.init_test_repo('gbp-test-native')
+ orig_content = self.read_file('packaging/gbp-test-native.changes')
+
+ # Add new commit with known content
+ with open('new-file.txt', 'w') as fobj:
+ fobj.write('this is new content\n')
+ repo.add_files('new-file.txt')
+ repo.commit_staged('Add new file')
+
+ # Only track a non-existent file
+ eq_(mock_ch(['--since=HEAD^', 'non-existent-path']), 0)
+ content = self.read_file('packaging/gbp-test-native.changes')
+ # New lines: header and one empty line, no entries
+ eq_(len(content), len(orig_content) + 2)
+
+ # Track existing file
+ repo.force_head('HEAD', hard=True)
+ eq_(mock_ch(['--since=HEAD^', 'new-file.txt']), 0)
+ content = self.read_file('packaging/gbp-test-native.changes')
+ # New lines: header, one entry line and one empty line
+ eq_(len(content), len(orig_content) + 3)
+
+ def test_commit_guessing(self):
+ """Basic tests for guessing the starting point"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ # Check 'tagname' that is not found
+ eq_(mock_ch(['--changelog-revision=%(tagname)s']), 0)
+ self._check_log(0, 'gbp:warning: Changelog points to tagname')
+
+ # Check 'upstreamversion' and 'release' fields
+ repo.force_head('HEAD', hard=True)
+ eq_(mock_ch(['--changelog-revision=%(upstreamversion)s-%(release)s']),
+ 0)
+
+ def test_commit_guessing_fail(self):
+ """Test for failure of start commit guessing"""
+ repo = self.init_test_repo('gbp-test-native')
+
+ # Add "very old" header to changelog
+ with open('packaging/gbp-test-native.changes', 'w') as ch_fp:
+ ch_fp.write('* Sat Jan 01 2000 User <user@host.com> 123\n- foo\n')
+ # rpm-ch should fail by not being able to find any commits before the
+ # last changelog section
+ eq_(mock_ch([]), 1)
+ self._check_log(-1, "gbp:error: Couldn't determine starting point")
+