@@ -589,6 +589,64 @@ def _resolve_rst_includes(content, srctree):
return ''.join(result)
+_GIT_LOG_COMMIT_RE = re.compile(r'^commit ([0-9a-f]{7,40})\b.*$', re.MULTILINE)
+_GIT_LOG_HEADER_RE = re.compile(r'^(Merge|Author|AuthorDate|Commit|CommitDate):')
+
+def _diff_git_log_changelog(old_content, new_content):
+ """Structurally diff a ChangeLog file that is itself the output of
+ `git log` (e.g. nghttp2). Such files are regenerated wholesale on every
+ release: new commits are prepended, so every existing commit shifts
+ position even though its content is unchanged. A textual diff would
+ see that shift as removed/added lines instead of showing just the
+ commits that were actually added. Comparing commit hashes instead of
+ line content tells the two cases apart: hashes already present in
+ old_content are commits that merely moved, so they're skipped; any
+ other hash is a genuinely new commit. Returns the subject line of
+ each new commit as a single string, or None if either file doesn't
+ look like git log output."""
+ old_hashes = set(_GIT_LOG_COMMIT_RE.findall(old_content))
+ new_commits = _GIT_LOG_COMMIT_RE.split(new_content)[1:] # [hash, block, hash, block, ...]
+ if not old_hashes or not new_commits:
+ return None
+
+ subjects = []
+ for commit_hash, block in zip(new_commits[0::2], new_commits[1::2]):
+ if commit_hash in old_hashes:
+ continue
+ message_lines = [line.strip() for line in block.splitlines()
+ if line.strip() and not _GIT_LOG_HEADER_RE.match(line)]
+ # Skip the 'Merge pull request ...' line GitHub adds as the first
+ # message line of a merge commit; the actual change subject is the
+ # next line, and keeping the merge line would duplicate it.
+ subject = next((l for l in message_lines if not l.startswith('Merge pull request ')), None)
+ # Collapse adjacent duplicates: squash-merged PRs can produce
+ # consecutive commits with the same subject line.
+ if subject and (not subjects or subjects[-1] != subject):
+ subjects.append(subject)
+ return '\n'.join(subjects) if subjects else None
+
+
+def _changelog_file_candidate(srctree, old_tag, new_tag, fname):
+ """Return the changelog text to use for a single changelog-like file that
+ changed between old_tag and new_tag, or None if it has no usable
+ content. Tries a structural git-log diff first (for files like
+ nghttp2's ChangeLog that are `git log` output regenerated on every
+ release), falling back to the added lines of a plain-text diff."""
+ try:
+ old_file, _ = _run('git show %s' % shlex.quote('%s:%s' % (old_tag, fname)), srctree)
+ new_file, _ = _run('git show %s' % shlex.quote('%s:%s' % (new_tag, fname)), srctree)
+ candidate = _diff_git_log_changelog(old_file, new_file)
+ if candidate is not None:
+ return candidate
+ except bb.process.ExecutionError:
+ pass
+
+ diff_out, _ = _run('git diff %s %s -- %s' % (old_tag, new_tag, shlex.quote(fname)), srctree)
+ added_lines = [line[1:] for line in diff_out.splitlines()
+ if line.startswith('+') and not line.startswith('+++')]
+ return '\n'.join(added_lines) if added_lines else None
+
+
def _extract_changelog(srctree, pn, old_ver, new_ver, old_tag, new_tag, workspace_path, is_git_source):
"""Extract changelog between old and new version using devtool git tags."""
changelog_content = None
@@ -630,15 +688,10 @@ def _extract_changelog(srctree, pn, old_ver, new_ver, old_tag, new_tag, workspac
for fname in changed_files:
basename = os.path.basename(fname).lower()
if basename in _CHANGELOG_BASENAMES:
- diff_out, _ = _run('git diff %s %s -- %s' % (old_tag, new_tag, shlex.quote(fname)), srctree)
- if diff_out.strip():
- lines = [line[1:] for line in diff_out.splitlines()
- if line.startswith('+') and not line.startswith('+++')]
- if lines:
- candidate = '\n'.join(lines)
- if not changelog_content or len(candidate) > len(changelog_content):
- changelog_content = candidate
- changelog_fname = fname
+ candidate = _changelog_file_candidate(srctree, old_tag, new_tag, fname)
+ if candidate and (not changelog_content or len(candidate) > len(changelog_content)):
+ changelog_content = candidate
+ changelog_fname = fname
except bb.process.ExecutionError as e:
logger.warning('Changelog file extraction failed: %s' % str(e))