66import tomllib
77import yaml
88from pathlib import Path
9- from tempfile import TemporaryDirectory
109from packaging .version import Version
1110from git import Repo
1211
@@ -25,6 +24,12 @@ def options():
2524 "'supported'. Defaults to 'supported', see `supported_release_branches` in "
2625 "`plugin_template.yml`." ,
2726 )
27+ parser .add_argument (
28+ "--no-fetch" ,
29+ default = False ,
30+ action = "store_true" ,
31+ help = "Don't fetch remote. Run faster at the expense of maybe being outdated." ,
32+ )
2833 return parser .parse_args ()
2934
3035
@@ -50,10 +55,15 @@ def current_version(repo, commitish):
5055
5156def check_pyproject_dependencies (repo , from_commit , to_commit ):
5257 try :
53- old_pyproject = tomllib .load (repo .show ("{from_commit}:pyproject.toml" ))
58+ new_pyproject = tomllib .loads (repo .git .show (f"{ to_commit } :pyproject.toml" ))
59+ try :
60+ new_dependencies = set (new_pyproject ["project" ]["dependencies" ])
61+ except KeyError :
62+ # New branch does not declare dependencies in pyproject.toml.
63+ # Assume no release needed for this reason.
64+ return []
65+ old_pyproject = tomllib .loads (repo .git .show (f"{ from_commit } :pyproject.toml" ))
5466 old_dependencies = set (old_pyproject ["project" ]["dependencies" ])
55- new_pyproject = tomllib .load (repo .show ("{to_commit}:pyproject.toml" ))
56- new_dependencies = set (new_pyproject ["project" ]["dependencies" ])
5767 if old_dependencies != new_dependencies :
5868 return ["dependencies" ]
5969 else :
@@ -65,97 +75,100 @@ def check_pyproject_dependencies(repo, from_commit, to_commit):
6575
6676
6777def main (options , template_config ):
68- with TemporaryDirectory () as d :
69- # Clone from upstream to ensure we have updated branches & main
70- GITHUB_ORG = template_config ["github_org" ]
71- PLUGIN_NAME = template_config ["plugin_name" ]
72- UPSTREAM_REMOTE = f"https://github.com/{ GITHUB_ORG } /{ PLUGIN_NAME } .git"
73- DEFAULT_BRANCH = template_config ["plugin_default_branch" ]
74-
75- repo = Repo .clone_from (UPSTREAM_REMOTE , d , filter = "blob:none" )
76- heads = [h .split ("/" )[- 1 ] for h in repo .git .ls_remote ("--heads" ).split ("\n " )]
77- available_branches = [h for h in heads if re .search (RELEASE_BRANCH_REGEX , h )]
78- available_branches .sort (key = lambda ver : Version (ver ))
79- available_branches .append (DEFAULT_BRANCH )
80-
81- branches = options .branches
82- if branches == "supported" :
83- with open (f"{ d } /template_config.yml" , mode = "r" ) as f :
84- tc = yaml .safe_load (f )
85- branches = set (tc ["supported_release_branches" ])
86- latest_release_branch = tc ["latest_release_branch" ]
87- if latest_release_branch is not None :
88- branches .add (latest_release_branch )
89- branches .add (DEFAULT_BRANCH )
90- else :
91- branches = set (branches .split ("," ))
92-
93- if diff := branches - set (available_branches ):
94- print (f"Supplied branches contains non-existent branches! { diff } " )
95- exit (1 )
96-
97- print (f"Checking for releases on branches: { branches } " )
98-
99- releases = []
100- for branch in branches :
101- if branch != DEFAULT_BRANCH :
102- # Check if a Z release is needed
103- reasons = []
104- changes = repo .git .ls_tree ("-r" , "--name-only" , f"origin/{ branch } " , "CHANGES/" )
105- z_changelog = False
106- for change in changes .split ("\n " ):
107- # Check each changelog file to make sure everything checks out
108- _ , ext = os .path .splitext (change )
109- if ext in Y_CHANGELOG_EXTS :
110- print (
111- f"Warning: A non-backported changelog ({ change } ) is present in the "
112- f"{ branch } release branch!"
113- )
114- elif ext in Z_CHANGELOG_EXTS :
115- z_changelog = True
116- if z_changelog :
117- reasons .append ("Backports" )
118-
119- last_tag = repo .git .describe ("--tags" , "--abbrev=0" , f"origin/{ branch } " )
120- req_txt_diff = repo .git .diff (
121- f"{ last_tag } " , f"origin/{ branch } " , "--name-only" , "--" , "requirements.txt"
122- )
123- if req_txt_diff :
124- reasons .append ("requirements.txt" )
125- pyproject_diff = repo .git .diff (
126- f"{ last_tag } " , f"origin/{ branch } " , "--name-only" , "--" , "pyproject.toml"
127- )
128- if pyproject_diff :
129- reasons .extend (check_pyproject_dependencies (repo , last_tag , f"origin/{ branch } " ))
130-
131- if reasons :
132- curr_version = Version (last_tag )
133- assert curr_version .base_version .startswith (
134- branch
135- ), "Current-version has to belong to the current branch!"
136- next_version = Version (f"{ branch } .{ curr_version .micro + 1 } " )
78+ DEFAULT_BRANCH = template_config ["plugin_default_branch" ]
79+
80+ repo = Repo ()
81+
82+ upstream_default_branch = next (
83+ (branch for branch in repo .branches if branch .name == DEFAULT_BRANCH )
84+ ).tracking_branch ()
85+ remote = upstream_default_branch .remote_name
86+ if not options .no_fetch :
87+ repo .remote (remote ).fetch ()
88+
89+ # Warning: This will not work if branch names contain "/" but we don't really care here.
90+ heads = [h .split ("/" )[- 1 ] for h in repo .git .branch ("--remote" ).split ("\n " )]
91+ available_branches = [h for h in heads if re .search (RELEASE_BRANCH_REGEX , h )]
92+ available_branches .sort (key = lambda ver : Version (ver ))
93+ available_branches .append (DEFAULT_BRANCH )
94+
95+ branches = options .branches
96+ if branches == "supported" :
97+ tc = yaml .safe_load (repo .git .show (f"{ upstream_default_branch } :template_config.yml" ))
98+ branches = set (tc ["supported_release_branches" ])
99+ latest_release_branch = tc ["latest_release_branch" ]
100+ if latest_release_branch is not None :
101+ branches .add (latest_release_branch )
102+ branches .add (DEFAULT_BRANCH )
103+ else :
104+ branches = set (branches .split ("," ))
105+
106+ if diff := branches - set (available_branches ):
107+ print (f"Supplied branches contains non-existent branches! { diff } " )
108+ exit (1 )
109+
110+ print (f"Checking for releases on branches: { branches } " )
111+
112+ releases = []
113+ for branch in branches :
114+ if branch != DEFAULT_BRANCH :
115+ # Check if a Z release is needed
116+ reasons = []
117+ changes = repo .git .ls_tree ("-r" , "--name-only" , f"{ remote } /{ branch } " , "CHANGES/" )
118+ z_changelog = False
119+ for change in changes .split ("\n " ):
120+ # Check each changelog file to make sure everything checks out
121+ _ , ext = os .path .splitext (change )
122+ if ext in Y_CHANGELOG_EXTS :
137123 print (
138- f"A Z-release is needed for { branch } , "
139- f"Prev: { last_tag } , "
140- f"Next: { next_version .base_version } , "
141- f"Reason: { ',' .join (reasons )} "
124+ f"Warning: A non-backported changelog ({ change } ) is present in the "
125+ f"{ branch } release branch!"
142126 )
127+ elif ext in Z_CHANGELOG_EXTS :
128+ z_changelog = True
129+ if z_changelog :
130+ reasons .append ("Backports" )
131+
132+ last_tag = repo .git .describe ("--tags" , "--abbrev=0" , f"{ remote } /{ branch } " )
133+ req_txt_diff = repo .git .diff (
134+ f"{ last_tag } " , f"{ remote } /{ branch } " , "--name-only" , "--" , "requirements.txt"
135+ )
136+ if req_txt_diff :
137+ reasons .append ("requirements.txt" )
138+ pyproject_diff = repo .git .diff (
139+ f"{ last_tag } " , f"{ remote } /{ branch } " , "--name-only" , "--" , "pyproject.toml"
140+ )
141+ if pyproject_diff :
142+ reasons .extend (check_pyproject_dependencies (repo , last_tag , f"{ remote } /{ branch } " ))
143+
144+ if reasons :
145+ curr_version = Version (last_tag )
146+ assert curr_version .base_version .startswith (
147+ branch
148+ ), "Current-version has to belong to the current branch!"
149+ next_version = Version (f"{ branch } .{ curr_version .micro + 1 } " )
150+ print (
151+ f"A Z-release is needed for { branch } , "
152+ f"Prev: { last_tag } , "
153+ f"Next: { next_version .base_version } , "
154+ f"Reason: { ',' .join (reasons )} "
155+ )
156+ releases .append (next_version )
157+ else :
158+ # Check if a Y release is needed
159+ changes = repo .git .ls_tree ("-r" , "--name-only" , DEFAULT_BRANCH , "CHANGES/" )
160+ for change in changes .split ("\n " ):
161+ _ , ext = os .path .splitext (change )
162+ if ext in Y_CHANGELOG_EXTS :
163+ # We don't put Y release bumps in the commit message, check file instead.
164+ # The 'current_version' is always the dev of the next version to release.
165+ next_version = current_version (repo , DEFAULT_BRANCH ).base_version
166+ print (f"A new Y-release is needed! New Version: { next_version } " )
143167 releases .append (next_version )
144- else :
145- # Check if a Y release is needed
146- changes = repo .git .ls_tree ("-r" , "--name-only" , DEFAULT_BRANCH , "CHANGES/" )
147- for change in changes .split ("\n " ):
148- _ , ext = os .path .splitext (change )
149- if ext in Y_CHANGELOG_EXTS :
150- # We don't put Y release bumps in the commit message, check file instead.
151- # The 'current_version' is always the dev of the next version to release.
152- next_version = current_version (repo , DEFAULT_BRANCH ).base_version
153- print (f"A new Y-release is needed! New Version: { next_version } " )
154- releases .append (next_version )
155- break
156-
157- if len (releases ) == 0 :
158- print ("No new releases to perform." )
168+ break
169+
170+ if len (releases ) == 0 :
171+ print ("No new releases to perform." )
159172
160173
161174if __name__ == "__main__" :
0 commit comments