EXTRACTOR = 'Extractor'
DOWNLOADER = 'Downloader'
POSTPROCESSOR = 'Postprocessor'
+ NETWORKING = 'Networking'
MISC = 'Misc.'
- @classmethod
- @property
- def ignorable_prefixes(cls):
- return ('core', 'downloader', 'extractor', 'misc', 'postprocessor', 'upstream')
-
@classmethod
@lru_cache
- def commit_lookup(cls):
+ def subgroup_lookup(cls):
return {
name: group
for group, names in {
- cls.PRIORITY: {'priority'},
cls.CORE: {
'aes',
'cache',
'compat_utils',
'compat',
'cookies',
- 'core',
'dependencies',
+ 'formats',
'jsinterp',
'outtmpl',
'plugins',
'update',
- 'upstream',
'utils',
},
cls.MISC: {
'build',
+ 'ci',
'cleanup',
'devscripts',
'docs',
- 'misc',
'test',
},
- cls.EXTRACTOR: {'extractor'},
- cls.DOWNLOADER: {'downloader'},
- cls.POSTPROCESSOR: {'postprocessor'},
+ cls.NETWORKING: {
+ 'rh',
+ },
}.items()
for name in names
}
@classmethod
- def get(cls, value):
- result = cls.commit_lookup().get(value)
- if result:
- logger.debug(f'Mapped {value!r} => {result.name}')
+ @lru_cache
+ def group_lookup(cls):
+ result = {
+ 'fd': cls.DOWNLOADER,
+ 'ie': cls.EXTRACTOR,
+ 'pp': cls.POSTPROCESSOR,
+ 'upstream': cls.CORE,
+ }
+ result.update({item.name.lower(): item for item in iter(cls)})
return result
+ @classmethod
+ def get(cls, value: str) -> tuple[CommitGroup | None, str | None]:
+ group, _, subgroup = (group.strip().lower() for group in value.partition('/'))
+
+ result = cls.group_lookup().get(group)
+ if not result:
+ if subgroup:
+ return None, value
+ subgroup = group
+ result = cls.subgroup_lookup().get(subgroup)
+
+ return result, subgroup or None
+
@dataclass
class Commit:
return sorted_items
- def format_single_change(self, info):
- message = self._format_message_link(info.message, info.commit.hash)
+ def format_single_change(self, info: CommitInfo):
+ message, sep, rest = info.message.partition('\n')
+ if '[' not in message:
+ # If the message doesn't already contain markdown links, try to add a link to the commit
+ message = self._format_message_link(message, info.commit.hash)
+
if info.issues:
message = f'{message} ({self._format_issues(info.issues)})'
message = f'{message} (With fixes in {fix_message})'
- return message
+ return message if not sep else f'{message}{sep}{rest}'
def _format_message_link(self, message, hash):
assert message or hash, 'Improperly defined commit message or override'
AUTHOR_INDICATOR_RE = re.compile(r'Authored by:? ', re.IGNORECASE)
MESSAGE_RE = re.compile(r'''
(?:\[(?P<prefix>[^\]]+)\]\ )?
- (?:(?P<sub_details>`?[^:`]+`?): )?
+ (?:(?P<sub_details>`?[\w.-]+`?): )?
(?P<message>.+?)
(?:\ \((?P<issues>\#\d+(?:,\ \#\d+)*)\))?
''', re.VERBOSE | re.DOTALL)
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
+ REVERT_RE = re.compile(r'(?:\[[^\]]+\]\s+)?(?i:Revert)\s+([\da-f]{40})')
FIXES_RE = re.compile(r'(?i:Fix(?:es)?(?:\s+bugs?)?(?:\s+in|\s+for)?|Revert)\s+([\da-f]{40})')
UPSTREAM_MERGE_RE = re.compile(r'Update to ytdl-commit-([\da-f]+)')
self.COMMAND, 'log', f'--format=%H%n%s%n%b%n{self.COMMIT_SEPARATOR}',
f'{self._start}..{self._end}' if self._start else self._end).stdout
- commits = {}
+ commits, reverts = {}, {}
fixes = defaultdict(list)
lines = iter(result.splitlines(False))
for i, commit_hash in enumerate(lines):
logger.debug(f'Reached Release commit, breaking: {commit}')
break
+ revert_match = self.REVERT_RE.fullmatch(commit.short)
+ if revert_match:
+ reverts[revert_match.group(1)] = commit
+ continue
+
fix_match = self.FIXES_RE.search(commit.short)
if fix_match:
commitish = fix_match.group(1)
commits[commit.hash] = commit
+ for commitish, revert_commit in reverts.items():
+ reverted = commits.pop(commitish, None)
+ if reverted:
+ logger.debug(f'{commitish} fully reverted {reverted}')
+ else:
+ commits[revert_commit.hash] = revert_commit
+
for commitish, fix_commits in fixes.items():
if commitish in commits:
hashes = ', '.join(commit.hash[:HASH_LENGTH] for commit in fix_commits)
for override in overrides:
when = override.get('when')
if when and when not in self and when != self._start:
- logger.debug(f'Ignored {when!r}, not in commits {self._start!r}')
+ logger.debug(f'Ignored {when!r} override')
continue
override_hash = override.get('hash') or when
for commit in self:
upstream_re = self.UPSTREAM_MERGE_RE.search(commit.short)
if upstream_re:
- commit.short = f'[core/upstream] Merged with 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:
if not prefix:
return CommitGroup.CORE, None, ()
- prefix, _, details = prefix.partition('/')
- prefix = prefix.strip()
- details = details.strip()
+ prefix, *sub_details = prefix.split(':')
- group = CommitGroup.get(prefix.lower())
- if group is CommitGroup.PRIORITY:
- prefix, _, details = details.partition('/')
+ group, details = CommitGroup.get(prefix)
+ if group is CommitGroup.PRIORITY and details:
+ details = details.partition('/')[2].strip()
- 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 and '/' in details:
+ logger.error(f'Prefix is overnested, using first part: {prefix}')
+ details = details.partition('/')[0].strip()
if details == 'common':
details = None
-
- if details:
- details, *sub_details = details.split(':')
- else:
- sub_details = []
+ elif group is CommitGroup.NETWORKING and details == 'rh':
+ details = 'Request Handler'
return group, details, sub_details