Create a separate tool for generating git changelogs
Change-Id: Ide0aed0b16ea06e8e0368ad1352a7808a6fef35f
This commit is contained in:
parent
27f9e46fcf
commit
06fc741026
@ -83,6 +83,12 @@ builds RPMs (current directory is used by default)::
|
|||||||
Wrote: /home/guest/rpmbuild/RPMS/noarch/python-multipip-0.1-1.noarch.rpm
|
Wrote: /home/guest/rpmbuild/RPMS/noarch/python-multipip-0.1-1.noarch.rpm
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
git-changelog
|
||||||
|
-------------
|
||||||
|
This tool generates a pretty software's changelog from git history.
|
||||||
|
|
||||||
|
|
||||||
build-install-node-from-source.sh
|
build-install-node-from-source.sh
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
|
237
tools/git-changelog
Executable file
237
tools/git-changelog
Executable file
@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
This tool generates a pretty software's changelog from git history.
|
||||||
|
|
||||||
|
http://fedoraproject.org/wiki/How_to_create_an_RPM_package says:
|
||||||
|
|
||||||
|
%changelog: Changes in the package. Use the format example above.
|
||||||
|
Do NOT put software's changelog at here. This changelog
|
||||||
|
is for RPM itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import collections
|
||||||
|
import iso8601
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
per_call_am = 50
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def translate_utf8(text):
|
||||||
|
return text.decode('utf8').encode('ascii', 'replace')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mailmap(wkdir):
|
||||||
|
mapping = {}
|
||||||
|
mailmap_fn = os.path.join(wkdir, '.mailmap')
|
||||||
|
if not os.path.isfile(mailmap_fn):
|
||||||
|
return mapping
|
||||||
|
for line in open(mailmap_fn, 'rb').read().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if len(line) and not line.startswith('#') and ' ' in line:
|
||||||
|
try:
|
||||||
|
(canonical_email, alias) = [x for x in line.split(' ')
|
||||||
|
if x.startswith('<')]
|
||||||
|
mapping[alias] = canonical_email
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
# Based off of http://www.brianlane.com/nice-changelog-entries.html
|
||||||
|
class GitChangeLog(object):
|
||||||
|
def __init__(self, wkdir):
|
||||||
|
self.wkdir = wkdir
|
||||||
|
self.date_buckets = None
|
||||||
|
|
||||||
|
def _get_commit_detail(self, commit, field, am=1):
|
||||||
|
detail_cmd = ['git', 'log', '--color=never', '-%s' % (am),
|
||||||
|
"--pretty=format:%s" % (field), commit]
|
||||||
|
(stdout, _stderr) = call_subprocess(detail_cmd, cwd=self.wkdir,
|
||||||
|
show_stdout=False)
|
||||||
|
ret = stdout.strip('\n').splitlines()
|
||||||
|
if len(ret) == 1:
|
||||||
|
ret = ret[0]
|
||||||
|
else:
|
||||||
|
ret = [x for x in ret if x.strip() != '']
|
||||||
|
ret = "\n".join(ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_log(self, commit):
|
||||||
|
if self.date_buckets is None:
|
||||||
|
self.date_buckets = self._get_log(commit)
|
||||||
|
return self.date_buckets
|
||||||
|
|
||||||
|
def _skip_entry(self, summary, date, email, name):
|
||||||
|
for f in [summary, name, email]:
|
||||||
|
try:
|
||||||
|
translate_utf8(f)
|
||||||
|
except UnicodeError:
|
||||||
|
logger.warn("Non-utf8 field %s found", f)
|
||||||
|
return True
|
||||||
|
email = email.lower().strip()
|
||||||
|
summary = summary.strip()
|
||||||
|
if not all([summary, date, email, name]):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_log(self, commit):
|
||||||
|
log_cmd = ['git', 'log',
|
||||||
|
'--no-merges', '--pretty=oneline',
|
||||||
|
'--color=never', commit]
|
||||||
|
(sysout, _stderr) = call_subprocess(log_cmd, cwd=self.wkdir,
|
||||||
|
show_stdout=False)
|
||||||
|
lines = sysout.strip('\n').splitlines()
|
||||||
|
|
||||||
|
# Extract the raw commit details
|
||||||
|
mailmap = parse_mailmap(self.wkdir)
|
||||||
|
log = []
|
||||||
|
|
||||||
|
for i in range(0, len(lines), per_call_am):
|
||||||
|
line = lines[i]
|
||||||
|
fields = line.split(' ')
|
||||||
|
if not len(fields):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# See: http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/pretty-formats.txt
|
||||||
|
commit_id = fields[0]
|
||||||
|
commit_details = self._get_commit_detail(commit_id,
|
||||||
|
"[%s][%ai][%aE][%an]",
|
||||||
|
per_call_am)
|
||||||
|
|
||||||
|
# Extracts the pieces that should be in brackets.
|
||||||
|
details_matcher = r"^\s*\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\s*$"
|
||||||
|
for a_commit in commit_details.splitlines():
|
||||||
|
matcher = re.match(details_matcher, a_commit)
|
||||||
|
if not matcher:
|
||||||
|
continue
|
||||||
|
(summary, date, author_email, author_name) = matcher.groups()
|
||||||
|
author_email = mailmap.get(author_email, author_email)
|
||||||
|
try:
|
||||||
|
date = iso8601.parse_date(date)
|
||||||
|
except iso8601.ParseError:
|
||||||
|
date = None
|
||||||
|
if self._skip_entry(summary, date, author_email, author_name):
|
||||||
|
continue
|
||||||
|
log.append({
|
||||||
|
'summary': translate_utf8(summary),
|
||||||
|
'when': date,
|
||||||
|
'author_email': translate_utf8(author_email),
|
||||||
|
'author_name': translate_utf8(author_name),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Bucketize the dates by day
|
||||||
|
date_buckets = collections.defaultdict(list)
|
||||||
|
for entry in log:
|
||||||
|
day = entry['when'].date()
|
||||||
|
date_buckets[day].append(entry)
|
||||||
|
return date_buckets
|
||||||
|
|
||||||
|
def format_log(self, commit):
|
||||||
|
date_buckets = self.get_log(commit)
|
||||||
|
lines = []
|
||||||
|
for d in reversed(sorted(date_buckets.keys())):
|
||||||
|
entries = date_buckets[d]
|
||||||
|
for entry in entries:
|
||||||
|
header = "* %s %s <%s>" % (d.strftime("%a %b %d %Y"),
|
||||||
|
entry['author_name'],
|
||||||
|
entry['author_email'])
|
||||||
|
lines.append(header)
|
||||||
|
summary = entry['summary']
|
||||||
|
sublines = textwrap.wrap(summary, 77)
|
||||||
|
if len(sublines):
|
||||||
|
lines.append("- %s" % sublines[0])
|
||||||
|
if len(sublines) > 1:
|
||||||
|
for subline in sublines[1:]:
|
||||||
|
lines.append(" %s" % subline)
|
||||||
|
lines.append("")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug", "-d",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Print debug information")
|
||||||
|
parser.add_argument(
|
||||||
|
"--filename", "-f",
|
||||||
|
default="ChangeLog",
|
||||||
|
help="Name of changelog file (default: ChangeLog)")
|
||||||
|
parser.add_argument(
|
||||||
|
"commit",
|
||||||
|
metavar="<commit>",
|
||||||
|
default="HEAD",
|
||||||
|
nargs="?",
|
||||||
|
help="The name of a commit for which to generate the log"
|
||||||
|
" (default: HEAD)")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def call_subprocess(cmd, cwd=None, show_stdout=True, raise_on_returncode=True):
|
||||||
|
if show_stdout:
|
||||||
|
stdout = None
|
||||||
|
else:
|
||||||
|
stdout = subprocess.PIPE
|
||||||
|
proc = subprocess.Popen(cmd, cwd=cwd, stderr=None, stdin=None, stdout=stdout)
|
||||||
|
ret = proc.communicate()
|
||||||
|
if proc.returncode:
|
||||||
|
cwd = cwd or os.getcwd()
|
||||||
|
command_desc = " ".join(cmd)
|
||||||
|
if raise_on_returncode:
|
||||||
|
raise ExecutionError(
|
||||||
|
"Command %s failed with error code %s in %s"
|
||||||
|
% (command_desc, proc.returncode, cwd))
|
||||||
|
else:
|
||||||
|
logger.warn(
|
||||||
|
"Command %s had error code %s in %s"
|
||||||
|
% (command_desc, proc.returncode, cwd))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(options):
|
||||||
|
level = logging.DEBUG if options.debug else logging.WARNING
|
||||||
|
handler = logging.StreamHandler(sys.stderr)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(level)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = create_parser()
|
||||||
|
options = parser.parse_args()
|
||||||
|
setup_logging(options)
|
||||||
|
source_dir = os.getcwd()
|
||||||
|
|
||||||
|
# .git can be a dir or a gitref regular file (for a git submodule)
|
||||||
|
if not os.path.exists(os.path.join(source_dir, ".git")):
|
||||||
|
print >> sys.stderr, "fatal: Not a git repository"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("%s/%s" % (source_dir, options.filename), "wb") as out:
|
||||||
|
out.write(GitChangeLog(source_dir).format_log(options.commit))
|
||||||
|
except Exception as ex:
|
||||||
|
print >> sys.stderr, ex
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exp:
|
||||||
|
print >> sys.stderr, exp
|
179
tools/py2rpm
179
tools/py2rpm
@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import collections
|
|
||||||
import distutils.spawn
|
import distutils.spawn
|
||||||
import iso8601
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
@ -12,7 +10,6 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import pip.util
|
import pip.util
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
@ -49,22 +46,8 @@ arch_dependent = [
|
|||||||
|
|
||||||
epoch_map = {}
|
epoch_map = {}
|
||||||
|
|
||||||
skip_emails = [
|
|
||||||
'jenkins@review.openstack.org',
|
|
||||||
]
|
|
||||||
|
|
||||||
requirements_section_re = re.compile(r'\[(.*?)\]')
|
requirements_section_re = re.compile(r'\[(.*?)\]')
|
||||||
version_re = re.compile(r"^(.*[^.0])(\.0+)*$")
|
version_re = re.compile(r"^(.*[^.0])(\.0+)*$")
|
||||||
|
|
||||||
skip_summaries = [
|
|
||||||
re.compile(r'^merge commit', re.I),
|
|
||||||
re.compile(r'^merge branch', re.I),
|
|
||||||
re.compile(r'^merge pull', re.I),
|
|
||||||
re.compile(r'^merge remote', re.I),
|
|
||||||
]
|
|
||||||
|
|
||||||
per_call_am = 50
|
|
||||||
|
|
||||||
setup_py = "setup.py"
|
setup_py = "setup.py"
|
||||||
|
|
||||||
|
|
||||||
@ -72,145 +55,6 @@ class InstallationError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def translate_utf8(text):
|
|
||||||
return text.decode('utf8').encode('ascii', 'replace')
|
|
||||||
|
|
||||||
|
|
||||||
def parse_mailmap(wkdir):
|
|
||||||
mapping = {}
|
|
||||||
mailmap_fn = os.path.join(wkdir, '.mailmap')
|
|
||||||
if not os.path.isfile(mailmap_fn):
|
|
||||||
return mapping
|
|
||||||
for line in open(mailmap_fn, 'rb').read().splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if len(line) and not line.startswith('#') and ' ' in line:
|
|
||||||
try:
|
|
||||||
(canonical_email, alias) = [x for x in line.split(' ')
|
|
||||||
if x.startswith('<')]
|
|
||||||
mapping[alias] = canonical_email
|
|
||||||
except (TypeError, ValueError, IndexError):
|
|
||||||
pass
|
|
||||||
return mapping
|
|
||||||
|
|
||||||
|
|
||||||
# Based off of http://www.brianlane.com/nice-changelog-entries.html
|
|
||||||
class GitChangeLog(object):
|
|
||||||
def __init__(self, wkdir):
|
|
||||||
self.wkdir = wkdir
|
|
||||||
self.date_buckets = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_build_for(wkdir):
|
|
||||||
if os.path.isdir(os.path.join(wkdir, ".git")):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_commit_detail(self, commit, field, am=1):
|
|
||||||
detail_cmd = ['git', 'log', '--color=never', '-%s' % (am),
|
|
||||||
"--pretty=format:%s" % (field), commit]
|
|
||||||
(stdout, _stderr) = call_subprocess(detail_cmd, cwd=self.wkdir,
|
|
||||||
show_stdout=False)
|
|
||||||
ret = stdout.strip('\n').splitlines()
|
|
||||||
if len(ret) == 1:
|
|
||||||
ret = ret[0]
|
|
||||||
else:
|
|
||||||
ret = [x for x in ret if x.strip() != '']
|
|
||||||
ret = "\n".join(ret)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_log(self):
|
|
||||||
if self.date_buckets is None:
|
|
||||||
self.date_buckets = self._get_log()
|
|
||||||
return self.date_buckets
|
|
||||||
|
|
||||||
def _skip_entry(self, summary, date, email, name):
|
|
||||||
for f in [summary, name, email]:
|
|
||||||
try:
|
|
||||||
translate_utf8(f)
|
|
||||||
except UnicodeError:
|
|
||||||
logger.warn("Non-utf8 field %s found", f)
|
|
||||||
return True
|
|
||||||
email = email.lower().strip()
|
|
||||||
if email in skip_emails:
|
|
||||||
return True
|
|
||||||
summary = summary.strip()
|
|
||||||
for r in skip_summaries:
|
|
||||||
if r.search(summary):
|
|
||||||
return True
|
|
||||||
if not all([summary, date, email, name]):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_log(self):
|
|
||||||
log_cmd = ['git', 'log', '--pretty=oneline', '--color=never']
|
|
||||||
(sysout, _stderr) = call_subprocess(log_cmd, cwd=self.wkdir,
|
|
||||||
show_stdout=False)
|
|
||||||
lines = sysout.strip('\n').splitlines()
|
|
||||||
|
|
||||||
# Extract the raw commit details
|
|
||||||
mailmap = parse_mailmap(self.wkdir)
|
|
||||||
log = []
|
|
||||||
|
|
||||||
for i in range(0, len(lines), per_call_am):
|
|
||||||
line = lines[i]
|
|
||||||
fields = line.split(' ')
|
|
||||||
if not len(fields):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# See: http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/pretty-formats.txt
|
|
||||||
commit_id = fields[0]
|
|
||||||
commit_details = self._get_commit_detail(commit_id,
|
|
||||||
"[%s][%ai][%aE][%an]",
|
|
||||||
per_call_am)
|
|
||||||
|
|
||||||
# Extracts the pieces that should be in brackets.
|
|
||||||
details_matcher = r"^\s*\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\s*$"
|
|
||||||
for a_commit in commit_details.splitlines():
|
|
||||||
matcher = re.match(details_matcher, a_commit)
|
|
||||||
if not matcher:
|
|
||||||
continue
|
|
||||||
(summary, date, author_email, author_name) = matcher.groups()
|
|
||||||
author_email = mailmap.get(author_email, author_email)
|
|
||||||
try:
|
|
||||||
date = iso8601.parse_date(date)
|
|
||||||
except iso8601.ParseError:
|
|
||||||
date = None
|
|
||||||
if self._skip_entry(summary, date, author_email, author_name):
|
|
||||||
continue
|
|
||||||
log.append({
|
|
||||||
'summary': translate_utf8(summary),
|
|
||||||
'when': date,
|
|
||||||
'author_email': translate_utf8(author_email),
|
|
||||||
'author_name': translate_utf8(author_name),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Bucketize the dates by day
|
|
||||||
date_buckets = collections.defaultdict(list)
|
|
||||||
for entry in log:
|
|
||||||
day = entry['when'].date()
|
|
||||||
date_buckets[day].append(entry)
|
|
||||||
return date_buckets
|
|
||||||
|
|
||||||
def format_log(self):
|
|
||||||
date_buckets = self.get_log()
|
|
||||||
lines = []
|
|
||||||
for d in reversed(sorted(date_buckets.keys())):
|
|
||||||
entries = date_buckets[d]
|
|
||||||
for entry in entries:
|
|
||||||
header = "* %s %s <%s>" % (d.strftime("%a %b %d %Y"),
|
|
||||||
entry['author_name'],
|
|
||||||
entry['author_email'])
|
|
||||||
lines.append(header)
|
|
||||||
summary = entry['summary']
|
|
||||||
sublines = textwrap.wrap(summary, 77)
|
|
||||||
if len(sublines):
|
|
||||||
lines.append("- %s" % sublines[0])
|
|
||||||
if len(sublines) > 1:
|
|
||||||
for subline in sublines[1:]:
|
|
||||||
lines.append(" %s" % subline)
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def package_name_python2rpm(python_name):
|
def package_name_python2rpm(python_name):
|
||||||
python_name = python_name.lower()
|
python_name = python_name.lower()
|
||||||
try:
|
try:
|
||||||
@ -303,12 +147,6 @@ def create_parser():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Only generate source RPM")
|
help="Only generate source RPM")
|
||||||
parser.add_argument(
|
|
||||||
"--no-changelog",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
dest="no_changelog",
|
|
||||||
help="Do not attempt to build a changelog automatically.")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--rpm-base",
|
"--rpm-base",
|
||||||
metavar="<dir>",
|
metavar="<dir>",
|
||||||
@ -421,19 +259,6 @@ def build_epoch_map(options):
|
|||||||
epoch_map[name] = epoch
|
epoch_map[name] = epoch
|
||||||
|
|
||||||
|
|
||||||
def build_changelog(source_dir, options):
|
|
||||||
if options.no_changelog:
|
|
||||||
return ""
|
|
||||||
cls = None
|
|
||||||
for c in [GitChangeLog,]:
|
|
||||||
if c.can_build_for(source_dir):
|
|
||||||
cls = c
|
|
||||||
break
|
|
||||||
if not cls:
|
|
||||||
return ""
|
|
||||||
return "\n".join(['', '%changelog', cls(source_dir).format_log(), ''])
|
|
||||||
|
|
||||||
|
|
||||||
def run_egg_info(source_dir, options):
|
def run_egg_info(source_dir, options):
|
||||||
script = """
|
script = """
|
||||||
__file__ = __SETUP_PY__
|
__file__ = __SETUP_PY__
|
||||||
@ -694,5 +519,5 @@ mv -f INSTALLED_FILES{.tmp,}
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except Exception as ex:
|
except Exception as exp:
|
||||||
print >> sys.stderr, ex
|
print >> sys.stderr, exp
|
||||||
|
Loading…
x
Reference in New Issue
Block a user