Skip to content

Commit 4705cc5

Browse files
authored
g.extension: get branch from version (#1700)
* branch from version * black * address review, flake8 * get branch from github API * handle branch=None * handle branch=None better * black/flake8 * cleanup after code review * update from code review * try orgs and users * use repo API * pass propper API URL * defaults for get_github_branches, fix l-flag, updates from code review * abandon github API due to rate limits * version over default * use URLError * use urlparse * use urlparse * use get_default_branch for gitlab * fix keyword issue with urlopen * global GIT_URL, get_default update
1 parent 7b09143 commit 4705cc5

File tree

1 file changed

+112
-56
lines changed

1 file changed

+112
-56
lines changed

scripts/g.extension/g.extension.py

Lines changed: 112 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
# % description: Specific branch to fetch addon from (only used when fetching from git)
7878
# % required: no
7979
# % multiple: no
80-
# % answer: main
8180
# %end
8281

8382
# %flag
@@ -169,6 +168,7 @@
169168

170169
from six.moves.urllib import request as urlrequest
171170
from six.moves.urllib.error import HTTPError, URLError
171+
from six.moves.urllib.parse import urlparse
172172

173173
# Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
174174
# and ElementTree 1.3.
@@ -191,6 +191,7 @@
191191
"User-Agent": "Mozilla/5.0",
192192
}
193193
HTTP_STATUS_CODES = list(http.HTTPStatus)
194+
GIT_URL = "https://github.com/OSGeo/grass-addons"
194195

195196

196197
def replace_shebang_win(python_file):
@@ -234,6 +235,78 @@ def urlopen(url, *args, **kwargs):
234235
return urlrequest.urlopen(request, *args, **kwargs)
235236

236237

238+
def get_version_branch(major_version):
239+
"""Check if version branch for the current GRASS version exists,
240+
if not, take branch for the previous version
241+
For the official repo we assume that at least one version branch is present"""
242+
version_branch = f"grass{major_version}"
243+
try:
244+
urlrequest.urlopen(f"{GIT_URL}/tree/{version_branch}/src")
245+
except URLError:
246+
version_branch = "grass{}".format(int(major_version) - 1)
247+
return version_branch
248+
249+
250+
def get_github_branches(
251+
github_api_url="https://api.github.com/repos/OSGeo/grass-addons/branches",
252+
version_only=True,
253+
):
254+
"""Get ordered list of branch names in repo using github API
255+
For the official repo we assume that at least one version branch is present
256+
Due to strict rate limits in the github API (60 calls per hour) this function
257+
is currently not used."""
258+
req = urlrequest.urlopen(github_api_url)
259+
content = json.loads(req.read())
260+
branches = [repo_branch["name"] for repo_branch in content]
261+
if version_only:
262+
branches = [
263+
version_branch
264+
for version_branch in branches
265+
if version_branch.startswith("grass")
266+
]
267+
branches.sort()
268+
return branches
269+
270+
271+
def get_default_branch(full_url):
272+
"""Get default branch for repository in known hosting services
273+
(currently only implemented for github, gitlab and bitbucket API)
274+
In all other cases "main" is used as default"""
275+
# Parse URL
276+
url_parts = urlparse(full_url)
277+
# Get organization and repository component
278+
try:
279+
organization, repository = url_parts.path.split("/")[1:3]
280+
except URLError:
281+
gscript.fatal(
282+
_(
283+
"Cannot retrieve organization and repository from URL: <{}>.".format(
284+
full_url
285+
)
286+
)
287+
)
288+
# Construct API call and retrieve default branch
289+
api_calls = {
290+
"github.com": f"https://api.github.com/repos/{organization}/{repository}",
291+
"gitlab.com": f"https://gitlab.com/api/v4/projects/{organization}%2F{repository}",
292+
"bitbucket.org": f"https://api.bitbucket.org/2.0/repositories/{organization}/{repository}/branching-model?",
293+
}
294+
# Try to get default branch via API. The API call is known to fail a) if the full_url
295+
# does not belong to an implemented hosting service or b) if the rate limit of the
296+
# API is exceeded
297+
try:
298+
req = urlrequest.urlopen(api_calls.get(url_parts.netloc))
299+
content = json.loads(req.read())
300+
# For github and gitlab
301+
default_branch = content.get("default_branch")
302+
# For bitbucket
303+
if not default_branch:
304+
default_branch = content.get("development").get("name")
305+
except URLError:
306+
default_branch = "main"
307+
return default_branch
308+
309+
237310
def download_addons_paths_file(url, response_format, *args, **kwargs):
238311
"""Generates JSON file containing the download URLs of the official
239312
Addons
@@ -1403,8 +1476,6 @@ def download_source_code_official_github(url, name, outdev, directory=None):
14031476
"""
14041477
if not directory:
14051478
directory = os.path.join(os.getcwd, name)
1406-
classchar = name.split(".", 1)[0]
1407-
moduleclass = expand_module_class_name(classchar)
14081479
if grass.call(["svn", "export", url, directory], stdout=outdev) != 0:
14091480
grass.fatal(_("GRASS Addons <%s> not found") % name)
14101481
return directory
@@ -1555,7 +1626,7 @@ def download_source_code(
15551626
response = urlopen(url)
15561627
except URLError:
15571628
# Try download add-on from 'master' branch if default "main" fails
1558-
if branch == "main":
1629+
if not branch:
15591630
try:
15601631
url = url.replace("main", "master")
15611632
gscript.message(
@@ -1611,8 +1682,6 @@ def download_source_code(
16111682
def install_extension_std_platforms(name, source, url, branch):
16121683
"""Install extension on standard platforms"""
16131684
gisbase = os.getenv("GISBASE")
1614-
# TODO: workaround, https://github.com/OSGeo/grass-addons/issues/528
1615-
source_url = "https://github.com/OSGeo/grass-addons/tree/master/grass7/"
16161685

16171686
# to hide non-error messages from subprocesses
16181687
if grass.verbosity() <= 2:
@@ -1694,7 +1763,7 @@ def install_extension_std_platforms(name, source, url, branch):
16941763
"SCRIPTDIR=%s" % dirs["script"],
16951764
"STRINGDIR=%s" % dirs["string"],
16961765
"ETC=%s" % os.path.join(dirs["etc"]),
1697-
"SOURCE_URL=%s" % source_url,
1766+
"SOURCE_URL=%s" % url,
16981767
]
16991768

17001769
install_cmd = [
@@ -2140,7 +2209,10 @@ def resolve_xmlurl_prefix(url, source=None):
21402209
gscript.debug("resolve_xmlurl_prefix(url={0}, source={1})".format(url, source))
21412210
if source == "official":
21422211
# use pregenerated modules XML file
2143-
url = "https://grass.osgeo.org/addons/grass%s/" % version[0]
2212+
# Define branch to fetch from (latest or current version)
2213+
version_branch = get_version_branch(version[0])
2214+
2215+
url = "https://grass.osgeo.org/addons/{}/".format(version_branch)
21442216
# else try to get extensions XMl from SVN repository (provided URL)
21452217
# the exact action depends on subsequent code (somewhere)
21462218

@@ -2218,7 +2290,10 @@ def resolve_known_host_service(url, name, branch):
22182290
else:
22192291
actual_start = ""
22202292
if "branch" in match["url_end"]:
2221-
suffix = match["url_end"].format(name=name, branch=branch)
2293+
suffix = match["url_end"].format(
2294+
name=name,
2295+
branch=branch if branch else get_default_branch(url),
2296+
)
22222297
else:
22232298
suffix = match["url_end"].format(name=name)
22242299
url = "{prefix}{base}{suffix}".format(
@@ -2299,47 +2374,32 @@ def resolve_source_code(url=None, name=None, branch=None, fork=False):
22992374
>>> resolve_source_code('https://bitbucket.org/joe-user/grass-module') # doctest: +SKIP
23002375
('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
23012376
"""
2302-
if not url and name:
2303-
module_class = get_module_class_name(name)
2304-
# note: 'trunk' is required to make URL usable for 'svn export' call
2305-
git_url = (
2306-
"https://github.com/OSGeo/grass-addons/trunk/"
2307-
"grass{version}/{module_class}/{module_name}".format(
2308-
version=version[0], module_class=module_class, module_name=name
2309-
)
2310-
)
2311-
# trac_url = 'https://trac.osgeo.org/grass/browser/grass-addons/' \
2312-
# 'grass{version}/{module_class}/{module_name}?format=zip' \
2313-
# .format(version=version[0],
2314-
# module_class=module_class, module_name=name)
2315-
# return 'official', trac_url
2316-
return "official", git_url
2317-
2318-
if url and fork:
2377+
# Handle URL for the offical repo
2378+
if name and (not url or fork):
23192379
module_class = get_module_class_name(name)
2320-
23212380
# note: 'trunk' is required to make URL usable for 'svn export' call
2322-
if branch in ["master", "main"]:
2323-
svn_reference = "trunk"
2381+
# and fetches the default branch
2382+
if not branch:
2383+
# Fetch from default branch
2384+
version_branch = get_version_branch(version[0])
2385+
try:
2386+
url = url.rstrip("/") if url else GIT_URL
2387+
urlrequest.urlopen(f"{url}/tree/{version_branch}/src")
2388+
svn_reference = "branches/{}".format(version_branch)
2389+
except URLError:
2390+
svn_reference = "trunk"
23242391
else:
23252392
svn_reference = "branches/{}".format(branch)
23262393

2327-
git_url = (
2328-
"{url}/{branch}/"
2329-
"grass{version}/{module_class}/{module_name}".format(
2330-
url=url,
2331-
version=version[0],
2332-
module_class=module_class,
2333-
module_name=name,
2334-
branch=svn_reference,
2335-
)
2336-
)
2337-
# trac_url = 'https://trac.osgeo.org/grass/browser/grass-addons/' \
2338-
# 'grass{version}/{module_class}/{module_name}?format=zip' \
2339-
# .format(version=version[0],
2340-
# module_class=module_class, module_name=name)
2341-
# return 'official', trac_url
2342-
return "official_fork", git_url
2394+
if not url:
2395+
# Set URL for the given GRASS version
2396+
git_url = f"{GIT_URL}/{svn_reference}/src/{module_class}/{name}"
2397+
return "official", git_url
2398+
else:
2399+
# Forks from the official repo should reflect the current structure
2400+
url = url.rstrip("/")
2401+
git_url = f"{url}/{svn_reference}/src/{module_class}/{name}"
2402+
return "official_fork", git_url
23432403

23442404
# Check if URL can be found
23452405
# Catch corner case if local URL is given starting with file://
@@ -2351,20 +2411,20 @@ def resolve_source_code(url=None, name=None, branch=None, fork=False):
23512411
open_url = urlopen(url)
23522412
open_url.close()
23532413
url_validated = True
2354-
except:
2414+
except URLError:
23552415
pass
23562416
else:
23572417
try:
23582418
open_url = urlopen("http://" + url)
23592419
open_url.close()
23602420
url_validated = True
2361-
except:
2421+
except URLError:
23622422
pass
23632423
try:
23642424
open_url = urlopen("https://" + url)
23652425
open_url.close()
23662426
url_validated = True
2367-
except:
2427+
except URLError:
23682428
pass
23692429

23702430
if not url_validated:
@@ -2402,10 +2462,9 @@ def get_addons_paths(gg_addons_base_dir):
24022462
and their paths (mkhmtl.py tool)
24032463
"""
24042464
get_addons_paths.json_file = "addons_paths.json"
2405-
2406-
url = (
2407-
"https://api.github.com/repos/OSGeo/grass-addons/git/trees/" "main?recursive=1"
2408-
)
2465+
# Define branch to fetch from (latest or current version)
2466+
addons_branch = get_version_branch(version[0])
2467+
url = f"https://api.github.com/repos/OSGeo/grass-addons/git/trees/{addons_branch}?recursive=1"
24092468

24102469
response = download_addons_paths_file(
24112470
url=url,
@@ -2505,10 +2564,7 @@ def main():
25052564

25062565
grass_version = grass.version()
25072566
version = grass_version["version"].split(".")
2508-
# TODO: update temporary workaround of using grass7 subdir of addon-repo, see
2509-
# https://github.com/OSGeo/grass-addons/issues/528
2510-
version[0] = 7
2511-
version[1] = 9
2567+
25122568
build_platform = grass_version["build_platform"].split("-", 1)[0]
25132569

25142570
sys.exit(main())

0 commit comments

Comments
 (0)