Skip to content

Commit dd50121

Browse files
authored
Merge pull request #2873 from ytausch/continue-git-backend-part-2
(Git-2) replace get_repo with new Git Backend Logic
2 parents 1e7c5d1 + a5a8207 commit dd50121

8 files changed

+341
-109
lines changed

conda_forge_tick/auto_tick.py

+88-30
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import github
1717
import github3
18+
import github3.repos
1819
import networkx as nx
1920
import tqdm
2021
from conda.models.version import VersionOrder
@@ -29,8 +30,11 @@
2930
from conda_forge_tick.deploy import deploy
3031
from conda_forge_tick.feedstock_parser import BOOTSTRAP_MAPPINGS
3132
from conda_forge_tick.git_utils import (
33+
DryRunBackend,
34+
GitPlatformBackend,
35+
RepositoryNotFoundError,
3236
comment_on_pr,
33-
get_repo,
37+
github3_client,
3438
github_backend,
3539
is_github_api_limit_reached,
3640
push_repo,
@@ -140,9 +144,55 @@ def _get_pre_pr_migrator_attempts(attrs, migrator_name, *, is_version):
140144
return pri.get("pre_pr_migrator_attempts", {}).get(migrator_name, 0)
141145

142146

147+
def _prepare_feedstock_repository(
148+
backend: GitPlatformBackend,
149+
context: ClonedFeedstockContext,
150+
branch: str,
151+
base_branch: str,
152+
) -> bool:
153+
"""
154+
Prepare a feedstock repository for migration by forking and cloning it. The local clone will be present in
155+
context.local_clone_dir.
156+
157+
Any errors are written to the pr_info attribute of the feedstock context and logged.
158+
159+
:param backend: The GitPlatformBackend instance to use.
160+
:param context: The current context
161+
:param branch: The branch to create in the forked repository.
162+
:param base_branch: The base branch to branch from.
163+
:return: True if the repository was successfully prepared, False otherwise.
164+
"""
165+
try:
166+
backend.fork(context.git_repo_owner, context.git_repo_name)
167+
except RepositoryNotFoundError:
168+
logger.warning(
169+
f"Could not fork {context.git_repo_owner}/{context.git_repo_name}: Not Found"
170+
)
171+
172+
error_message = f"{context.feedstock_name}: Git repository not found."
173+
logger.critical(
174+
f"Failed to migrate {context.feedstock_name}, {error_message}",
175+
)
176+
177+
with context.attrs["pr_info"] as pri:
178+
pri["bad"] = error_message
179+
180+
return False
181+
182+
backend.clone_fork_and_branch(
183+
upstream_owner=context.git_repo_owner,
184+
repo_name=context.git_repo_name,
185+
target_dir=context.local_clone_dir,
186+
new_branch=branch,
187+
base_branch=base_branch,
188+
)
189+
return True
190+
191+
143192
def run_with_tmpdir(
144193
context: FeedstockContext,
145194
migrator: Migrator,
195+
git_backend: GitPlatformBackend,
146196
rerender: bool = True,
147197
base_branch: str = "main",
148198
dry_run: bool = False,
@@ -161,6 +211,7 @@ def run_with_tmpdir(
161211
return run(
162212
context=cloned_context,
163213
migrator=migrator,
214+
git_backend=git_backend,
164215
rerender=rerender,
165216
base_branch=base_branch,
166217
dry_run=dry_run,
@@ -171,6 +222,7 @@ def run_with_tmpdir(
171222
def run(
172223
context: ClonedFeedstockContext,
173224
migrator: Migrator,
225+
git_backend: GitPlatformBackend,
174226
rerender: bool = True,
175227
base_branch: str = "main",
176228
dry_run: bool = False,
@@ -184,6 +236,8 @@ def run(
184236
The current feedstock context, already containing information about a temporary directory for the feedstock.
185237
migrator: Migrator instance
186238
The migrator to run on the feedstock
239+
git_backend: GitPlatformBackend
240+
The git backend to use. Use the DryRunBackend for testing.
187241
rerender : bool
188242
Whether to rerender
189243
base_branch : str, optional
@@ -202,9 +256,6 @@ def run(
202256
# sometimes we get weird directory issues so make sure we reset
203257
os.chdir(BOT_HOME_DIR)
204258

205-
# get the repo
206-
branch_name = migrator.remote_branch(context) + "_h" + uuid4().hex[0:6]
207-
208259
migrator_name = get_migrator_name(migrator)
209260
is_version_migration = isinstance(migrator, Version)
210261
_increment_pre_pr_migrator_attempt(
@@ -213,20 +264,25 @@ def run(
213264
is_version=is_version_migration,
214265
)
215266

216-
# TODO: run this in parallel
217-
repo = get_repo(context=context, branch=branch_name, base_branch=base_branch)
218-
219-
feedstock_dir = str(context.local_clone_dir)
220-
if not feedstock_dir or not repo:
221-
logger.critical(
222-
"Failed to migrate %s, %s",
223-
context.feedstock_name,
224-
context.attrs.get("pr_info", {}).get("bad"),
225-
)
267+
branch_name = migrator.remote_branch(context) + "_h" + uuid4().hex[0:6]
268+
if not _prepare_feedstock_repository(
269+
git_backend,
270+
context,
271+
branch_name,
272+
base_branch,
273+
):
274+
# something went wrong during forking or cloning
226275
return False, False
227276

228277
# need to use an absolute path here
229-
feedstock_dir = os.path.abspath(feedstock_dir)
278+
feedstock_dir = str(context.local_clone_dir.resolve())
279+
280+
# This is needed because we want to migrate to the new backend step-by-step
281+
repo: github3.repos.Repository | None = github3_client().repository(
282+
context.git_repo_owner, context.git_repo_name
283+
)
284+
285+
assert repo is not None
230286

231287
migration_run_data = run_migration(
232288
migrator=migrator,
@@ -566,7 +622,8 @@ def _run_migrator_on_feedstock_branch(
566622
attrs,
567623
base_branch,
568624
migrator,
569-
fctx,
625+
fctx: FeedstockContext,
626+
git_backend: GitPlatformBackend,
570627
dry_run,
571628
mctx,
572629
migrator_name,
@@ -581,6 +638,7 @@ def _run_migrator_on_feedstock_branch(
581638
migrator_uid, pr_json = run_with_tmpdir(
582639
context=fctx,
583640
migrator=migrator,
641+
git_backend=git_backend,
584642
rerender=migrator.rerender,
585643
hash_type=attrs.get("hash_type", "sha256"),
586644
base_branch=base_branch,
@@ -745,7 +803,9 @@ def _is_migrator_done(_mg_start, good_prs, time_per, pr_limit):
745803
return False
746804

747805

748-
def _run_migrator(migrator, mctx, temp, time_per, dry_run):
806+
def _run_migrator(
807+
migrator, mctx, temp, time_per, dry_run, git_backend: GitPlatformBackend
808+
):
749809
_mg_start = time.time()
750810

751811
migrator_name = get_migrator_name(migrator)
@@ -863,14 +923,15 @@ def _run_migrator(migrator, mctx, temp, time_per, dry_run):
863923
)
864924
):
865925
good_prs, break_loop = _run_migrator_on_feedstock_branch(
866-
attrs,
867-
base_branch,
868-
migrator,
869-
fctx,
870-
dry_run,
871-
mctx,
872-
migrator_name,
873-
good_prs,
926+
attrs=attrs,
927+
base_branch=base_branch,
928+
migrator=migrator,
929+
fctx=fctx,
930+
git_backend=git_backend,
931+
dry_run=dry_run,
932+
mctx=mctx,
933+
migrator_name=migrator_name,
934+
good_prs=good_prs,
874935
)
875936
if break_loop:
876937
break
@@ -1119,14 +1180,11 @@ def main(ctx: CliContext) -> None:
11191180
),
11201181
flush=True,
11211182
)
1183+
git_backend = github_backend() if not ctx.dry_run else DryRunBackend()
11221184

11231185
for mg_ind, migrator in enumerate(migrators):
11241186
good_prs = _run_migrator(
1125-
migrator,
1126-
mctx,
1127-
temp,
1128-
time_per_migrator[mg_ind],
1129-
ctx.dry_run,
1187+
migrator, mctx, temp, time_per_migrator[mg_ind], ctx.dry_run, git_backend
11301188
)
11311189
if good_prs > 0:
11321190
pass

conda_forge_tick/contexts.py

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def __post_init__(self):
4646
DEFAULT_BRANCHES.get(self.feedstock_name, "main"),
4747
)
4848

49+
@property
50+
def git_repo_owner(self) -> str:
51+
return "conda-forge"
52+
4953
@property
5054
def git_repo_name(self) -> str:
5155
return f"{self.feedstock_name}-feedstock"

conda_forge_tick/git_utils.py

+38-56
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from datetime import datetime
1212
from functools import cached_property
1313
from pathlib import Path
14-
from typing import Dict, Literal, Optional, Union
14+
from typing import Dict, Optional, Union
1515

1616
import backoff
1717
import github
@@ -28,7 +28,7 @@
2828
# and pull all the needed info from the various source classes)
2929
from conda_forge_tick.lazy_json_backends import LazyJson
3030

31-
from .contexts import ClonedFeedstockContext, FeedstockContext
31+
from .contexts import FeedstockContext
3232
from .executors import lock_git_operation
3333
from .os_utils import pushd
3434
from .utils import get_bot_run_url, run_command_hiding_token
@@ -404,8 +404,8 @@ def does_repository_exist(self, owner: str, repo_name: str) -> bool:
404404
"""
405405
pass
406406

407-
@staticmethod
408407
def get_remote_url(
408+
self,
409409
owner: str,
410410
repo_name: str,
411411
connection_mode: GitConnectionMode = GitConnectionMode.HTTPS,
@@ -416,6 +416,8 @@ def get_remote_url(
416416
:param repo_name: The name of the repository.
417417
:param connection_mode: The connection mode to use.
418418
:raises ValueError: If the connection mode is not supported.
419+
:raises RepositoryNotFoundError: If the repository does not exist. This is only raised if the backend relies on
420+
the repository existing to generate the URL.
419421
"""
420422
# Currently we don't need any abstraction for other platforms than GitHub, so we don't build such abstractions.
421423
match connection_mode:
@@ -641,6 +643,12 @@ class DryRunBackend(GitPlatformBackend):
641643
def __init__(self):
642644
super().__init__(GitCli())
643645
self._repos: set[str] = set()
646+
self._repos: dict[str, str] = {}
647+
"""
648+
_repos maps from repository name to the owner of the upstream repository.
649+
If a remote URL of a fork is requested with get_remote_url, _USER (the virtual current user) is
650+
replaced by the owner of the upstream repository. This allows cloning the forked repository.
651+
"""
644652

645653
def get_api_requests_left(self) -> Bound:
646654
return Bound.INFINITY
@@ -654,15 +662,40 @@ def does_repository_exist(self, owner: str, repo_name: str) -> bool:
654662
self.get_remote_url(owner, repo_name, GitConnectionMode.HTTPS)
655663
)
656664

665+
def get_remote_url(
666+
self,
667+
owner: str,
668+
repo_name: str,
669+
connection_mode: GitConnectionMode = GitConnectionMode.HTTPS,
670+
) -> str:
671+
if owner != self._USER:
672+
return super().get_remote_url(owner, repo_name, connection_mode)
673+
# redirect to the upstream repository
674+
try:
675+
upstream_owner = self._repos[repo_name]
676+
except KeyError:
677+
raise RepositoryNotFoundError(
678+
f"Repository {owner}/{repo_name} appears to be a virtual fork but does not exist. Note that dry-run "
679+
"forks are persistent only for the duration of the backend instance."
680+
)
681+
682+
return super().get_remote_url(upstream_owner, repo_name, connection_mode)
683+
657684
@lock_git_operation()
658685
def fork(self, owner: str, repo_name: str):
659686
if repo_name in self._repos:
660-
raise ValueError(f"Fork of {repo_name} already exists.")
687+
logger.debug(f"Fork of {repo_name} already exists. Doing nothing.")
688+
return
689+
690+
if not self.does_repository_exist(owner, repo_name):
691+
raise RepositoryNotFoundError(
692+
f"Cannot fork non-existing repository {owner}/{repo_name}."
693+
)
661694

662695
logger.debug(
663696
f"Dry Run: Creating fork of {owner}/{repo_name} for user {self._USER}."
664697
)
665-
self._repos.add(repo_name)
698+
self._repos[repo_name] = owner
666699

667700
def _sync_default_branch(self, upstream_owner: str, upstream_repo: str):
668701
logger.debug(
@@ -720,57 +753,6 @@ def feedstock_repo(fctx: FeedstockContext) -> str:
720753
return fctx.feedstock_name + "-feedstock"
721754

722755

723-
@lock_git_operation()
724-
def get_repo(
725-
context: ClonedFeedstockContext,
726-
branch: str,
727-
base_branch: str = "main",
728-
) -> github3.repos.Repository | Literal[False]:
729-
"""Get the feedstock repo
730-
731-
Parameters
732-
----------
733-
context : ClonedFeedstockContext
734-
Feedstock context used for constructing feedstock urls, etc.
735-
branch : str
736-
The branch to be made.
737-
base_branch : str, optional
738-
The base branch from which to make the new branch.
739-
740-
Returns
741-
-------
742-
repo : github3 repository
743-
The github3 repository object.
744-
"""
745-
backend = github_backend()
746-
feedstock_repo_name = feedstock_repo(context)
747-
748-
try:
749-
backend.fork("conda-forge", feedstock_repo_name)
750-
except RepositoryNotFoundError:
751-
logger.warning(f"Could not fork conda-forge/{feedstock_repo_name}")
752-
with context.attrs["pr_info"] as pri:
753-
pri["bad"] = f"{context.feedstock_name}: Git repository not found.\n"
754-
return False, False
755-
756-
backend.clone_fork_and_branch(
757-
upstream_owner="conda-forge",
758-
repo_name=feedstock_repo_name,
759-
target_dir=context.local_clone_dir,
760-
new_branch=branch,
761-
base_branch=base_branch,
762-
)
763-
764-
# This is needed because we want to migrate to the new backend step-by-step
765-
repo: github3.repos.Repository | None = github3_client().repository(
766-
"conda-forge", feedstock_repo_name
767-
)
768-
769-
assert repo is not None
770-
771-
return repo
772-
773-
774756
@lock_git_operation()
775757
def delete_branch(pr_json: LazyJson, dry_run: bool = False) -> None:
776758
ref = pr_json["head"]["ref"]

conda_forge_tick/migrators_types.py

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class AttrsTypedDict_(TypedDict, total=False):
128128
raw_meta_yaml: str
129129
req: Set[str]
130130
platforms: List[str]
131+
pr_info: typing.Any
131132
requirements: RequirementsTypedDict
132133
source: SourceTypedDict
133134
test: TestTypedDict

0 commit comments

Comments
 (0)