Skip to content

Commit

Permalink
[devscripts] make_changelog: Various improvements
Browse files Browse the repository at this point in the history
- Make single items collapse into one line
- Don't hide "Important changes" in `<details>`
- Move upstream merge into priority
- Properly support comma separated prefixes

Authored by: Grub4K
  • Loading branch information
Grub4K committed May 20, 2023
1 parent b73193c commit 23c39a4
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 81 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/publish.yml
Expand Up @@ -54,9 +54,7 @@ jobs:
cat >> ./RELEASE_NOTES << EOF
#### A description of the various files are in the [README](https://github.com/yt-dlp/yt-dlp#release-files)
---
<details><summary><h3>Changelog</h3></summary>
$(python ./devscripts/make_changelog.py -vv)
</details>
$(python ./devscripts/make_changelog.py -vv --collapsible)
EOF
printf '%s\n\n' '**This is an automated nightly pre-release build**' >> ./NIGHTLY_NOTES
cat ./RELEASE_NOTES >> ./NIGHTLY_NOTES
Expand Down
179 changes: 101 additions & 78 deletions devscripts/make_changelog.py
Expand Up @@ -26,22 +26,25 @@


class CommitGroup(enum.Enum):
UPSTREAM = None
PRIORITY = 'Important'
CORE = 'Core'
EXTRACTOR = 'Extractor'
DOWNLOADER = 'Downloader'
POSTPROCESSOR = 'Postprocessor'
MISC = 'Misc.'

@classmethod
@property
def ignorable_prefixes(cls):
return ('core', 'downloader', 'extractor', 'misc', 'postprocessor', 'upstream')

@classmethod
@lru_cache
def commit_lookup(cls):
return {
name: group
for group, names in {
cls.PRIORITY: {''},
cls.UPSTREAM: {'upstream'},
cls.CORE: {
'aes',
'cache',
Expand All @@ -54,6 +57,7 @@ def commit_lookup(cls):
'outtmpl',
'plugins',
'update',
'upstream',
'utils',
},
cls.MISC: {
Expand Down Expand Up @@ -111,22 +115,36 @@ def key(self):
return ((self.details or '').lower(), self.sub_details, self.message)


def unique(items):
return sorted({item.strip().lower(): item for item in items if item}.values())


class Changelog:
MISC_RE = re.compile(r'(?:^|\b)(?:lint(?:ing)?|misc|format(?:ting)?|fixes)(?:\b|$)', re.IGNORECASE)
ALWAYS_SHOWN = (CommitGroup.PRIORITY,)

def __init__(self, groups, repo):
def __init__(self, groups, repo, collapsible=False):
self._groups = groups
self._repo = repo
self._collapsible = collapsible

def __str__(self):
return '\n'.join(self._format_groups(self._groups)).replace('\t', ' ')

def _format_groups(self, groups):
first = True
for item in CommitGroup:
if self._collapsible and item not in self.ALWAYS_SHOWN and first:
first = False
yield '\n<details><summary><h3>Changelog</h3></summary>\n'

group = groups[item]
if group:
yield self.format_module(item.value, group)

if self._collapsible:
yield '\n</details>'

def format_module(self, name, group):
result = f'\n#### {name} changes\n' if name else '\n'
return result + '\n'.join(self._format_group(group))
Expand All @@ -137,62 +155,52 @@ def _format_group(self, group):
for _, items in detail_groups:
items = list(items)
details = items[0].details
if not details:
indent = ''
else:
yield f'- {details}'
indent = '\t'

if details == 'cleanup':
items, cleanup_misc_items = self._filter_cleanup_misc_items(items)
items = self._prepare_cleanup_misc_items(items)

prefix = '-'
if details:
if len(items) == 1:
prefix = f'- **{details}**:'
else:
yield f'- **{details}**'
prefix = '\t-'

sub_detail_groups = itertools.groupby(items, lambda item: tuple(map(str.lower, item.sub_details)))
for sub_details, entries in sub_detail_groups:
if not sub_details:
for entry in entries:
yield f'{indent}- {self.format_single_change(entry)}'
yield f'{prefix} {self.format_single_change(entry)}'
continue

entries = list(entries)
prefix = f'{indent}- {", ".join(entries[0].sub_details)}'
sub_prefix = f'{prefix} {", ".join(entries[0].sub_details)}'
if len(entries) == 1:
yield f'{prefix}: {self.format_single_change(entries[0])}'
yield f'{sub_prefix}: {self.format_single_change(entries[0])}'
continue

yield prefix
yield sub_prefix
for entry in entries:
yield f'{indent}\t- {self.format_single_change(entry)}'

if details == 'cleanup' and cleanup_misc_items:
yield from self._format_cleanup_misc_sub_group(cleanup_misc_items)
yield f'\t{prefix} {self.format_single_change(entry)}'

def _filter_cleanup_misc_items(self, items):
def _prepare_cleanup_misc_items(self, items):
cleanup_misc_items = defaultdict(list)
non_misc_items = []
sorted_items = []
for item in items:
if self.MISC_RE.search(item.message):
cleanup_misc_items[tuple(item.commit.authors)].append(item)
else:
non_misc_items.append(item)

return non_misc_items, cleanup_misc_items

def _format_cleanup_misc_sub_group(self, group):
prefix = '\t- Miscellaneous'
if len(group) == 1:
yield f'{prefix}: {next(self._format_cleanup_misc_items(group))}'
return
sorted_items.append(item)

yield prefix
for message in self._format_cleanup_misc_items(group):
yield f'\t\t- {message}'
for commit_infos in cleanup_misc_items.values():
sorted_items.append(CommitInfo(
'cleanup', ('Miscellaneous',), ', '.join(
self._format_message_link(None, info.commit.hash)
for info in sorted(commit_infos, key=lambda item: item.commit.hash or '')),
[], Commit(None, '', commit_infos[0].commit.authors), []))

def _format_cleanup_misc_items(self, group):
for authors, infos in group.items():
message = ', '.join(
self._format_message_link(None, info.commit.hash)
for info in sorted(infos, key=lambda item: item.commit.hash or ''))
yield f'{message} by {self._format_authors(authors)}'
return sorted_items

def format_single_change(self, info):
message = self._format_message_link(info.message, info.commit.hash)
Expand Down Expand Up @@ -236,12 +244,8 @@ class CommitRange:

AUTHOR_INDICATOR_RE = re.compile(r'Authored by:? ', re.IGNORECASE)
MESSAGE_RE = re.compile(r'''
(?:\[
(?P<prefix>[^\]\/:,]+)
(?:/(?P<details>[^\]:,]+))?
(?:[:,](?P<sub_details>[^\]]+))?
\]\ )?
(?:(?P<sub_details_alt>`?[^:`]+`?): )?
(?:\[(?P<prefix>[^\]]+)\]\ )?
(?:(?P<sub_details>`?[^:`]+`?): )?
(?P<message>.+?)
(?:\ \((?P<issues>\#\d+(?:,\ \#\d+)*)\))?
''', re.VERBOSE | re.DOTALL)
Expand Down Expand Up @@ -340,60 +344,76 @@ def apply_overrides(self, overrides):
self._commits = {key: value for key, value in reversed(self._commits.items())}

def groups(self):
groups = defaultdict(list)
group_dict = defaultdict(list)
for commit in self:
upstream_re = self.UPSTREAM_MERGE_RE.match(commit.short)
upstream_re = self.UPSTREAM_MERGE_RE.search(commit.short)
if upstream_re:
commit.short = f'[upstream] Merge up to youtube-dl {upstream_re.group(1)}'
commit.short = f'[upstream] Merged with youtube-dl {upstream_re.group(1)}'

match = self.MESSAGE_RE.fullmatch(commit.short)
if not match:
logger.error(f'Error parsing short commit message: {commit.short!r}')
continue

prefix, details, sub_details, sub_details_alt, message, issues = match.groups()
group = None
if prefix:
if prefix == 'priority':
prefix, _, details = (details or '').partition('/')
logger.debug(f'Priority: {message!r}')
group = CommitGroup.PRIORITY

if not details and prefix:
if prefix not in ('core', 'downloader', 'extractor', 'misc', 'postprocessor', 'upstream'):
logger.debug(f'Replaced details with {prefix!r}')
details = prefix or None

if details == 'common':
details = None

if details:
details = details.strip()
prefix, sub_details_alt, message, issues = match.groups()
issues = [issue.strip()[1:] for issue in issues.split(',')] if issues else []

if prefix:
groups, details, sub_details = zip(*map(self.details_from_prefix, prefix.split(',')))
group = next(iter(filter(None, groups)), None)
details = ', '.join(unique(details))
sub_details = list(itertools.chain.from_iterable(sub_details))
else:
group = CommitGroup.CORE
details = None
sub_details = []

sub_details = f'{sub_details or ""},{sub_details_alt or ""}'.replace(':', ',')
sub_details = tuple(filter(None, map(str.strip, sub_details.split(','))))

issues = [issue.strip()[1:] for issue in issues.split(',')] if issues else []
if sub_details_alt:
sub_details.append(sub_details_alt)
sub_details = tuple(unique(sub_details))

if not group:
group = CommitGroup.get(prefix.lower())
if not group:
if self.EXTRACTOR_INDICATOR_RE.search(commit.short):
group = CommitGroup.EXTRACTOR
else:
group = CommitGroup.POSTPROCESSOR
logger.warning(f'Failed to map {commit.short!r}, selected {group.name}')
if self.EXTRACTOR_INDICATOR_RE.search(commit.short):
group = CommitGroup.EXTRACTOR
else:
group = CommitGroup.POSTPROCESSOR
logger.warning(f'Failed to map {commit.short!r}, selected {group.name.lower()}')

commit_info = CommitInfo(
details, sub_details, message.strip(),
issues, commit, self._fixes[commit.hash])

logger.debug(f'Resolved {commit.short!r} to {commit_info!r}')
groups[group].append(commit_info)
group_dict[group].append(commit_info)

return group_dict

@staticmethod
def details_from_prefix(prefix):
if not prefix:
return CommitGroup.CORE, None, ()

return groups
prefix, _, details = prefix.partition('/')
prefix = prefix.strip().lower()
details = details.strip()

group = CommitGroup.get(prefix)
if group is CommitGroup.PRIORITY:
prefix, _, details = details.partition('/')

if not details and prefix and prefix not in CommitGroup.ignorable_prefixes:
logger.debug(f'Replaced details with {prefix!r}')
details = prefix or None

if details == 'common':
details = None

if details:
details, *sub_details = details.split(':')
else:
sub_details = []

return group, details, sub_details


def get_new_contributors(contributors_path, commits):
Expand Down Expand Up @@ -444,6 +464,9 @@ def get_new_contributors(contributors_path, commits):
parser.add_argument(
'--repo', default='yt-dlp/yt-dlp',
help='the github repository to use for the operations (default: %(default)s)')
parser.add_argument(
'--collapsible', action='store_true',
help='make changelog collapsible (default: %(default)s)')
args = parser.parse_args()

logging.basicConfig(
Expand All @@ -467,4 +490,4 @@ def get_new_contributors(contributors_path, commits):
write_file(args.contributors_path, '\n'.join(new_contributors) + '\n', mode='a')
logger.info(f'New contributors: {", ".join(new_contributors)}')

print(Changelog(commits.groups(), args.repo))
print(Changelog(commits.groups(), args.repo, args.collapsible))

0 comments on commit 23c39a4

Please sign in to comment.