summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--project.py97
-rw-r--r--tests/test_project.py24
2 files changed, 89 insertions, 32 deletions
diff --git a/project.py b/project.py
index 9e8d8605a..58366e90c 100644
--- a/project.py
+++ b/project.py
@@ -1999,28 +1999,51 @@ class Project:
1999 1999
2000 Args: 2000 Args:
2001 verbose: Whether to show verbose messages. 2001 verbose: Whether to show verbose messages.
2002 force: Always delete tree even if dirty. 2002 force: Always delete tree even if dirty or corrupted.
2003 2003
2004 Returns: 2004 Returns:
2005 True if the worktree was completely cleaned out. 2005 True if the worktree was completely cleaned out.
2006 """ 2006 """
2007 if self.IsDirty(): 2007 is_dirty = False
2008 is_corrupted = False
2009 try:
2010 is_dirty = self.IsDirty()
2011 except GitError:
2012 is_corrupted = True
2013
2014 rel_path = self.RelPath(local=False)
2015
2016 if is_dirty or is_corrupted:
2008 if force: 2017 if force:
2009 logger.warning( 2018 if is_corrupted:
2010 "warning: %s: Removing dirty project: uncommitted changes " 2019 logger.warning(
2011 "lost.", 2020 "warning: %s: Removing corrupted project.",
2012 self.RelPath(local=False), 2021 rel_path,
2013 ) 2022 )
2023 else:
2024 logger.warning(
2025 "warning: %s: Removing dirty project: "
2026 "uncommitted changes lost.",
2027 rel_path,
2028 )
2014 else: 2029 else:
2015 msg = ( 2030 if is_corrupted:
2016 "error: %s: Cannot remove project: uncommitted " 2031 msg = (
2017 "changes are present.\n" % self.RelPath(local=False) 2032 f"error: {rel_path}: Cannot remove project: "
2018 ) 2033 "project is corrupted.\n"
2019 logger.error(msg) 2034 )
2020 raise DeleteDirtyWorktreeError(msg, project=self.name) 2035 logger.error(msg)
2036 raise DeleteWorktreeError(msg, project=self.name)
2037 else:
2038 msg = (
2039 f"error: {rel_path}: Cannot remove project: "
2040 "uncommitted changes are present.\n"
2041 )
2042 logger.error(msg)
2043 raise DeleteDirtyWorktreeError(msg, project=self.name)
2021 2044
2022 if verbose: 2045 if verbose:
2023 print(f"{self.RelPath(local=False)}: Deleting obsolete checkout.") 2046 print(f"{rel_path}: Deleting obsolete checkout.")
2024 2047
2025 # Unlock and delink from the main worktree. We don't use git's worktree 2048 # Unlock and delink from the main worktree. We don't use git's worktree
2026 # remove because it will recursively delete projects -- we handle that 2049 # remove because it will recursively delete projects -- we handle that
@@ -2028,23 +2051,33 @@ class Project:
2028 if self.use_git_worktrees: 2051 if self.use_git_worktrees:
2029 needle = os.path.realpath(self.gitdir) 2052 needle = os.path.realpath(self.gitdir)
2030 # Find the git worktree commondir under .repo/worktrees/. 2053 # Find the git worktree commondir under .repo/worktrees/.
2031 output = self.bare_git.worktree("list", "--porcelain").splitlines()[ 2054 try:
2032 0 2055 output = self.bare_git.worktree(
2033 ] 2056 "list", "--porcelain"
2034 assert output.startswith("worktree "), output 2057 ).splitlines()[0]
2035 commondir = output[9:] 2058 assert output.startswith("worktree "), output
2036 # Walk each of the git worktrees to see where they point. 2059 commondir = output[9:]
2037 configs = os.path.join(commondir, "worktrees") 2060 # Walk each of the git worktrees to see where they point.
2038 for name in os.listdir(configs): 2061 configs = os.path.join(commondir, "worktrees")
2039 gitdir = os.path.join(configs, name, "gitdir") 2062 if os.path.exists(configs):
2040 with open(gitdir) as fp: 2063 for name in os.listdir(configs):
2041 relpath = fp.read().strip() 2064 gitdir = os.path.join(configs, name, "gitdir")
2042 # Resolve the checkout path and see if it matches this project. 2065 with open(gitdir) as fp:
2043 fullpath = os.path.realpath( 2066 relpath = fp.read().strip()
2044 os.path.join(configs, name, relpath) 2067 # Resolve the checkout path and see if it
2068 # matches this project.
2069 fullpath = os.path.realpath(
2070 os.path.join(configs, name, relpath)
2071 )
2072 if fullpath == needle:
2073 platform_utils.rmtree(os.path.join(configs, name))
2074 except GitError as e:
2075 logger.warning(
2076 "warning: %s: Failed to list worktrees, skipping worktree "
2077 "cleanup: %s",
2078 rel_path,
2079 e,
2045 ) 2080 )
2046 if fullpath == needle:
2047 platform_utils.rmtree(os.path.join(configs, name))
2048 2081
2049 # Delete the .git directory first, so we're less likely to have a 2082 # Delete the .git directory first, so we're less likely to have a
2050 # partially working git repository around. There shouldn't be any git 2083 # partially working git repository around. There shouldn't be any git
@@ -2065,7 +2098,7 @@ class Project:
2065 logger.error( 2098 logger.error(
2066 "error: %s: Failed to delete obsolete checkout; remove " 2099 "error: %s: Failed to delete obsolete checkout; remove "
2067 "manually, then run `repo sync -l`.", 2100 "manually, then run `repo sync -l`.",
2068 self.RelPath(local=False), 2101 rel_path,
2069 ) 2102 )
2070 raise DeleteWorktreeError(aggregate_errors=[e]) 2103 raise DeleteWorktreeError(aggregate_errors=[e])
2071 2104
@@ -2129,7 +2162,7 @@ class Project:
2129 logger.error( 2162 logger.error(
2130 "%s: Failed to delete obsolete checkout.\n", 2163 "%s: Failed to delete obsolete checkout.\n",
2131 " Remove manually, then run `repo sync -l`.", 2164 " Remove manually, then run `repo sync -l`.",
2132 self.RelPath(local=False), 2165 rel_path,
2133 ) 2166 )
2134 raise DeleteWorktreeError(aggregate_errors=errors) 2167 raise DeleteWorktreeError(aggregate_errors=errors)
2135 2168
diff --git a/tests/test_project.py b/tests/test_project.py
index af8bea4f0..dd24afa04 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -742,6 +742,30 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
742 result = fakeproj.Sync(use_local_gitdirs=True, mirror=True) 742 result = fakeproj.Sync(use_local_gitdirs=True, mirror=True)
743 self.assertFalse(result) 743 self.assertFalse(result)
744 744
745 def test_delete_worktree_corrupted(self):
746 """Test DeleteWorktree gracefully handles corrupted projects."""
747 for use_git_worktrees in (False, True):
748 with self.subTest(use_git_worktrees=use_git_worktrees):
749 with utils_for_test.TempGitTree() as tempdir:
750 proj = _create_mock_project(tempdir)
751 os.makedirs(os.path.join(tempdir, "worktree"))
752 os.makedirs(os.path.join(tempdir, "gitdir"))
753 proj.worktree = os.path.join(tempdir, "worktree")
754 proj.gitdir = os.path.join(tempdir, "gitdir")
755 proj.use_git_worktrees = use_git_worktrees
756
757 with mock.patch.object(
758 proj,
759 "IsDirty",
760 side_effect=error.GitError("mock error"),
761 ):
762 with self.assertRaises(project.DeleteWorktreeError):
763 proj.DeleteWorktree(force=False)
764
765 self.assertTrue(proj.DeleteWorktree(force=True))
766 self.assertFalse(os.path.exists(proj.worktree))
767 self.assertFalse(os.path.exists(proj.gitdir))
768
745 769
746def _create_mock_project( 770def _create_mock_project(
747 tempdir, 771 tempdir,