git_deploy: improve recovery implementation

After performing a recovery attempt to reset to the commit the
repo was at prior to the repo failure.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2023-07-13 06:26:00 -04:00
parent 4443bfbd41
commit 0437d1623e
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
1 changed files with 68 additions and 23 deletions

View File

@ -98,26 +98,20 @@ class GitDeploy(AppDeploy):
self.notify_status("Attempting Repo Recovery...") self.notify_status("Attempting Repo Recovery...")
dep_info = await self._collect_dependency_info() dep_info = await self._collect_dependency_info()
if hard: if hard:
if self.repo.is_submodule_or_worktree():
raise self.server.error(
f"Cannot re-clone git repo {self.name}, it is either "
f"a submodule or worktree."
)
await self.repo.clone() await self.repo.clone()
if self.channel != Channel.DEV:
if self.repo.upstream_commit != "?":
# If on beta or stable reset to the latest tagged
# upstream commit
await self.repo.reset()
else:
self.notify_status(
f"No upstream commit for repo on {self.channel} channel, "
"skipping reset."
)
await self._update_repo_state() await self._update_repo_state()
else: else:
self.notify_status("Resetting Git Repo...") self.notify_status("Resetting Git Repo...")
await self.repo.reset() reset_ref = await self.repo.get_recovery_ref()
if self.repo.is_dirty():
# Try to restore modified files. If the attempt fails we
# can still try the reset
try:
await self.repo.checkout("-- .")
except self.server.error:
pass
await self.repo.checkout(self.primary_branch)
await self.repo.reset(reset_ref)
await self._update_repo_state() await self._update_repo_state()
self.repo.set_rollback_state(None) self.repo.set_rollback_state(None)
@ -647,24 +641,29 @@ class GitRepo:
raise self.server.error( raise self.server.error(
f"Git Repo {self.alias}: Initialization failure") f"Git Repo {self.alias}: Initialization failure")
async def check_diverged(self) -> bool: async def is_ancestor(self, ancestor_ref: str, descendent_ref: str) -> bool:
self._verify_repo(check_remote=True) self._verify_repo()
if self.head_detached: cmd = f"merge-base --is-ancestor {ancestor_ref} {descendent_ref}"
return False
async with self.git_operation_lock: async with self.git_operation_lock:
cmd = f"merge-base --is-ancestor HEAD {self.git_remote}/{self.git_branch}"
for _ in range(3): for _ in range(3):
try: try:
await self._run_git_cmd(cmd, retries=1, corrupt_msg="error: ") await self._run_git_cmd(cmd, retries=1, corrupt_msg="error: ")
except self.cmd_helper.scmd_error as err: except self.cmd_helper.scmd_error as err:
if err.return_code == 1: if err.return_code == 1:
return True return False
if self.repo_corrupt: if self.repo_corrupt:
raise raise
else: else:
break break
await asyncio.sleep(.5) await asyncio.sleep(.2)
return True
async def check_diverged(self) -> bool:
self._verify_repo(check_remote=True)
if self.head_detached:
return False return False
descendent = f"{self.git_remote}/{self.git_branch}"
return not (await self.is_ancestor("HEAD", descendent))
def log_repo_info(self) -> None: def log_repo_info(self) -> None:
warnings = "" warnings = ""
@ -868,6 +867,11 @@ class GitRepo:
await self._run_git_cmd("fsck --full", timeout=300., retries=1) await self._run_git_cmd("fsck --full", timeout=300., retries=1)
async def clone(self) -> None: async def clone(self) -> None:
if self.is_submodule_or_worktree():
raise self.server.error(
f"Cannot clone git repo {self.alias}, it is a {self.get_repo_type()} "
"of another git repo."
)
async with self.git_operation_lock: async with self.git_operation_lock:
if self.recovery_url == "?": if self.recovery_url == "?":
raise self.server.error( raise self.server.error(
@ -891,8 +895,20 @@ class GitRepo:
await event_loop.run_in_thread( await event_loop.run_in_thread(
shutil.move, str(self.backup_path), str(self.src_path)) shutil.move, str(self.backup_path), str(self.src_path))
self.repo_corrupt = False self.repo_corrupt = False
self.valid_git_repo = True
self.cmd_helper.notify_update_response( self.cmd_helper.notify_update_response(
f"Git Repo {self.alias}: Git Clone Complete") f"Git Repo {self.alias}: Git Clone Complete")
if self.current_commit != "?":
try:
can_reset = await self.is_ancestor(self.current_commit, "HEAD")
except self.server.error:
can_reset = False
if can_reset:
self.cmd_helper.notify_update_response(
f"Git Repo {self.alias}: Moving HEAD to previous "
f"commit {self.current_commit}"
)
await self.reset(self.current_commit)
async def rollback(self) -> bool: async def rollback(self) -> bool:
if self.rollback_commit == "?" or self.rollback_branch == "?": if self.rollback_commit == "?" or self.rollback_branch == "?":
@ -1069,6 +1085,35 @@ class GitRepo:
detached_err detached_err
) )
def get_repo_type(self) -> str:
type_name = self.git_folder_path.parent.name
if type_name == "modules":
return "submodule"
elif type_name == "worktrees":
return "worktree"
return "repo"
async def get_recovery_ref(self) -> str:
""" Fetch the best reference for a 'reset' recovery attempt
Returns the ref to reset to for "soft" recovery requests. The
preference is to reset to the current commit, however that is
only possible if the commit is known and if it is an ancestor of
the primary branch.
"""
remote = await self.config_get(f"branch.{self.primary_branch}.remote")
if remote is None:
raise self.server.error(
f"Failed to find remote for primary branch '{self.primary_branch}'"
)
upstream_ref = f"{remote}/{self.primary_branch}"
if (
self.current_commit != "?" and
await self.is_ancestor(self.current_commit, upstream_ref)
):
return self.current_commit
return upstream_ref
async def _check_lock_file_exists(self, remove: bool = False) -> bool: async def _check_lock_file_exists(self, remove: bool = False) -> bool:
lock_path = self.git_folder_path.joinpath("index.lock") lock_path = self.git_folder_path.joinpath("index.lock")
if lock_path.is_file(): if lock_path.is_file():