@ -26,7 +26,6 @@ logger = logging.getLogger(__name__)
class CommitGroup ( enum . Enum ) :
class CommitGroup ( enum . Enum ) :
UPSTREAM = None
PRIORITY = ' Important '
PRIORITY = ' Important '
CORE = ' Core '
CORE = ' Core '
EXTRACTOR = ' Extractor '
EXTRACTOR = ' Extractor '
@ -34,6 +33,11 @@ class CommitGroup(enum.Enum):
POSTPROCESSOR = ' Postprocessor '
POSTPROCESSOR = ' Postprocessor '
MISC = ' Misc. '
MISC = ' Misc. '
@classmethod
@property
def ignorable_prefixes ( cls ) :
return ( ' core ' , ' downloader ' , ' extractor ' , ' misc ' , ' postprocessor ' , ' upstream ' )
@classmethod
@classmethod
@lru_cache
@lru_cache
def commit_lookup ( cls ) :
def commit_lookup ( cls ) :
@ -41,7 +45,6 @@ class CommitGroup(enum.Enum):
name : group
name : group
for group , names in {
for group , names in {
cls . PRIORITY : { ' ' } ,
cls . PRIORITY : { ' ' } ,
cls . UPSTREAM : { ' upstream ' } ,
cls . CORE : {
cls . CORE : {
' aes ' ,
' aes ' ,
' cache ' ,
' cache ' ,
@ -54,6 +57,7 @@ class CommitGroup(enum.Enum):
' outtmpl ' ,
' outtmpl ' ,
' plugins ' ,
' plugins ' ,
' update ' ,
' update ' ,
' upstream ' ,
' utils ' ,
' utils ' ,
} ,
} ,
cls . MISC : {
cls . MISC : {
@ -111,22 +115,36 @@ class CommitInfo:
return ( ( self . details or ' ' ) . lower ( ) , self . sub_details , self . message )
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 :
class Changelog :
MISC_RE = re . compile ( r ' (?:^| \ b)(?:lint(?:ing)?|misc|format(?:ting)?|fixes)(?: \ b|$) ' , re . IGNORECASE )
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 . _groups = groups
self . _repo = repo
self . _repo = repo
self . _collapsible = collapsible
def __str__ ( self ) :
def __str__ ( self ) :
return ' \n ' . join ( self . _format_groups ( self . _groups ) ) . replace ( ' \t ' , ' ' )
return ' \n ' . join ( self . _format_groups ( self . _groups ) ) . replace ( ' \t ' , ' ' )
def _format_groups ( self , groups ) :
def _format_groups ( self , groups ) :
first = True
for item in CommitGroup :
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 ]
group = groups [ item ]
if group :
if group :
yield self . format_module ( item . value , group )
yield self . format_module ( item . value , group )
if self . _collapsible :
yield ' \n </details> '
def format_module ( self , name , group ) :
def format_module ( self , name , group ) :
result = f ' \n #### { name } changes \n ' if name else ' \n '
result = f ' \n #### { name } changes \n ' if name else ' \n '
return result + ' \n ' . join ( self . _format_group ( group ) )
return result + ' \n ' . join ( self . _format_group ( group ) )
@ -137,62 +155,52 @@ class Changelog:
for _ , items in detail_groups :
for _ , items in detail_groups :
items = list ( items )
items = list ( items )
details = items [ 0 ] . details
details = items [ 0 ] . details
if not details :
indent = ' '
else :
yield f ' - { details } '
indent = ' \t '
if details == ' cleanup ' :
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 ) ) )
sub_detail_groups = itertools . groupby ( items , lambda item : tuple ( map ( str . lower , item . sub_details ) ) )
for sub_details , entries in sub_detail_groups :
for sub_details , entries in sub_detail_groups :
if not sub_details :
if not sub_details :
for entry in entries :
for entry in entries :
yield f ' { indent } - { self . format_single_change ( entry ) } '
yield f ' { prefix} { self . format_single_change ( entry ) } '
continue
continue
entries = list ( entries )
entries = list ( entries )
prefix = f ' { indent} - { " , " . join ( entries [ 0 ] . sub_details ) } '
sub_ prefix = f ' { prefix} { " , " . join ( entries [ 0 ] . sub_details ) } '
if len ( entries ) == 1 :
if len ( entries ) == 1 :
yield f ' { prefix} : { self . format_single_change ( entries [ 0 ] ) } '
yield f ' { sub_ prefix} : { self . format_single_change ( entries [ 0 ] ) } '
continue
continue
yield prefix
yield sub_ prefix
for entry in entries :
for entry in entries :
yield f ' { indent } \t - { self . format_single_change ( entry ) } '
yield f ' \t { prefix } { self . format_single_change ( entry ) } '
if details == ' cleanup ' and cleanup_misc_items :
yield from self . _format_cleanup_misc_sub_group ( cleanup_misc_items )
def _ filter _cleanup_misc_items( self , items ) :
def _prepare_cleanup_misc_items ( self , items ) :
cleanup_misc_items = defaultdict ( list )
cleanup_misc_items = defaultdict ( list )
non_misc _items = [ ]
sorted _items = [ ]
for item in items :
for item in items :
if self . MISC_RE . search ( item . message ) :
if self . MISC_RE . search ( item . message ) :
cleanup_misc_items [ tuple ( item . commit . authors ) ] . append ( item )
cleanup_misc_items [ tuple ( item . commit . authors ) ] . append ( item )
else :
else :
non_misc_items . append ( item )
sorted_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
yield prefix
for commit_infos in cleanup_misc_items . values ( ) :
for message in self . _format_cleanup_misc_items ( group ) :
sorted_items . append ( CommitInfo (
yield f ' \t \t - { message } '
' 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 ) :
return sorted_items
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 ) } '
def format_single_change ( self , info ) :
def format_single_change ( self , info ) :
message = self . _format_message_link ( info . message , info . commit . hash )
message = self . _format_message_link ( info . message , info . commit . hash )
@ -236,12 +244,8 @@ class CommitRange:
AUTHOR_INDICATOR_RE = re . compile ( r ' Authored by:? ' , re . IGNORECASE )
AUTHOR_INDICATOR_RE = re . compile ( r ' Authored by:? ' , re . IGNORECASE )
MESSAGE_RE = re . compile ( r '''
MESSAGE_RE = re . compile ( r '''
( ? : \[
( ? : \[ ( ? P < prefix > [ ^ \] ] + ) \] \ ) ?
( ? P < prefix > [ ^ \] \/ : , ] + )
( ? : ( ? P < sub_details > ` ? [ ^ : ` ] + ` ? ) : ) ?
( ? : / ( ? P < details > [ ^ \] : , ] + ) ) ?
( ? : [ : , ] ( ? P < sub_details > [ ^ \] ] + ) ) ?
\] \ ) ?
( ? : ( ? P < sub_details_alt > ` ? [ ^ : ` ] + ` ? ) : ) ?
( ? P < message > . + ? )
( ? P < message > . + ? )
( ? : \ \( ( ? P < issues > \#\d+(?:,\ \#\d+)*)\))?
( ? : \ \( ( ? P < issues > \#\d+(?:,\ \#\d+)*)\))?
''' , re.VERBOSE | re.DOTALL)
''' , re.VERBOSE | re.DOTALL)
@ -340,60 +344,76 @@ class CommitRange:
self . _commits = { key : value for key , value in reversed ( self . _commits . items ( ) ) }
self . _commits = { key : value for key , value in reversed ( self . _commits . items ( ) ) }
def groups ( self ) :
def groups ( self ) :
group s = defaultdict ( list )
group _dict = defaultdict ( list )
for commit in self :
for commit in self :
upstream_re = self . UPSTREAM_MERGE_RE . mat ch( commit . short )
upstream_re = self . UPSTREAM_MERGE_RE . sear ch( commit . short )
if upstream_re :
if upstream_re :
commit . short = f ' [upstream] Merge up to youtube-dl { upstream_re . group ( 1 ) } '
commit . short = f ' [upstream] Merge d with youtube-dl { upstream_re . group ( 1 ) } '
match = self . MESSAGE_RE . fullmatch ( commit . short )
match = self . MESSAGE_RE . fullmatch ( commit . short )
if not match :
if not match :
logger . error ( f ' Error parsing short commit message: { commit . short !r} ' )
logger . error ( f ' Error parsing short commit message: { commit . short !r} ' )
continue
continue
prefix , details , sub_details , sub_details_alt , message , issues = match . groups ( )
prefix , sub_details_alt , message , issues = match . groups ( )
group = None
issues = [ issue . strip ( ) [ 1 : ] for issue in issues . split ( ' , ' ) ] if issues else [ ]
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 ( )
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 :
else :
group = CommitGroup . CORE
group = CommitGroup . CORE
details = None
sub_details = [ ]
sub_details = f ' { sub_details or " " } , { sub_details_alt or " " } ' . replace ( ' : ' , ' , ' )
if sub_details_alt :
sub_details = tuple ( filter ( None , map ( str . strip , sub_details . split ( ' , ' ) ) ) )
sub_details . append ( sub_details_alt )
sub_details = tuple ( unique ( sub_details ) )
issues = [ issue . strip ( ) [ 1 : ] for issue in issues . split ( ' , ' ) ] if issues else [ ]
if not group :
if not group :
group = CommitGroup . get ( prefix . lower ( ) )
if self . EXTRACTOR_INDICATOR_RE . search ( commit . short ) :
if not group :
group = CommitGroup . EXTRACTOR
if self . EXTRACTOR_INDICATOR_RE . search ( commit . short ) :
else :
group = CommitGroup . EXTRACTOR
group = CommitGroup . POSTPROCESSOR
else :
logger . warning ( f ' Failed to map { commit . short !r} , selected { group . name . lower ( ) } ' )
group = CommitGroup . POSTPROCESSOR
logger . warning ( f ' Failed to map { commit . short !r} , selected { group . name } ' )
commit_info = CommitInfo (
commit_info = CommitInfo (
details , sub_details , message . strip ( ) ,
details , sub_details , message . strip ( ) ,
issues , commit , self . _fixes [ commit . hash ] )
issues , commit , self . _fixes [ commit . hash ] )
logger . debug ( f ' Resolved { commit . short !r} to { commit_info !r} ' )
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 ) :
def get_new_contributors ( contributors_path , commits ) :
@ -444,6 +464,9 @@ if __name__ == '__main__':
parser . add_argument (
parser . add_argument (
' --repo ' , default = ' yt-dlp/yt-dlp ' ,
' --repo ' , default = ' yt-dlp/yt-dlp ' ,
help = ' the github repository to use for the operations (default: %(default)s ) ' )
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 ( )
args = parser . parse_args ( )
logging . basicConfig (
logging . basicConfig (
@ -467,4 +490,4 @@ if __name__ == '__main__':
write_file ( args . contributors_path , ' \n ' . join ( new_contributors ) + ' \n ' , mode = ' a ' )
write_file ( args . contributors_path , ' \n ' . join ( new_contributors ) + ' \n ' , mode = ' a ' )
logger . info ( f ' New contributors: { " , " . join ( new_contributors ) } ' )
logger . info ( f ' New contributors: { " , " . join ( new_contributors ) } ' )
print ( Changelog ( commits . groups ( ) , args . repo ))
print ( Changelog ( commits . groups ( ) , args . repo , args . collapsible ))