diff options
| -rw-r--r-- | project.py | 97 | ||||
| -rw-r--r-- | tests/test_project.py | 24 |
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 | ||
| 746 | def _create_mock_project( | 770 | def _create_mock_project( |
| 747 | tempdir, | 771 | tempdir, |
