summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Frysinger <vapier@google.com>2020-02-19 19:19:18 -0500
committerMike Frysinger <vapier@google.com>2020-02-20 00:51:42 +0000
commitc0d1866b35d3e8d0bee3760a9f52574fa2ab8491 (patch)
treec6be761e1dbf5e93dfe499e8f53d90386a2d6769
parentf81c72ed7727ca408d1859a86c56b532a8de8d59 (diff)
downloadgit-repo-c0d1866b35d3e8d0bee3760a9f52574fa2ab8491.tar.gz
project/sync: move DeleteProject helper to Project
Since deleting a source checkout involves a good bit of internal knowledge of .repo/, move the DeleteProject helper out of the sync code and into the Project class itself. This allows us to add git worktree support to it so we can unlock/unlink project checkouts. Change-Id: If9af8bd4a9c7e29743827d8166bc3db81547ca50 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/256072 Reviewed-by: Jonathan Nieder <jrn@google.com> Tested-by: Mike Frysinger <vapier@google.com>
-rw-r--r--project.py116
-rw-r--r--subcmds/sync.py85
2 files changed, 120 insertions, 81 deletions
diff --git a/project.py b/project.py
index 5bead641..a5d35bf3 100644
--- a/project.py
+++ b/project.py
@@ -1832,6 +1832,122 @@ class Project(object):
1832 patch_id, 1832 patch_id,
1833 self.bare_git.rev_parse('FETCH_HEAD')) 1833 self.bare_git.rev_parse('FETCH_HEAD'))
1834 1834
1835 def DeleteWorktree(self, quiet=False, force=False):
1836 """Delete the source checkout and any other housekeeping tasks.
1837
1838 This currently leaves behind the internal .repo/ cache state. This helps
1839 when switching branches or manifest changes get reverted as we don't have
1840 to redownload all the git objects. But we should do some GC at some point.
1841
1842 Args:
1843 quiet: Whether to hide normal messages.
1844 force: Always delete tree even if dirty.
1845
1846 Returns:
1847 True if the worktree was completely cleaned out.
1848 """
1849 if self.IsDirty():
1850 if force:
1851 print('warning: %s: Removing dirty project: uncommitted changes lost.' %
1852 (self.relpath,), file=sys.stderr)
1853 else:
1854 print('error: %s: Cannot remove project: uncommitted changes are '
1855 'present.\n' % (self.relpath,), file=sys.stderr)
1856 return False
1857
1858 if not quiet:
1859 print('%s: Deleting obsolete checkout.' % (self.relpath,))
1860
1861 # Unlock and delink from the main worktree. We don't use git's worktree
1862 # remove because it will recursively delete projects -- we handle that
1863 # ourselves below. https://crbug.com/git/48
1864 if self.use_git_worktrees:
1865 needle = platform_utils.realpath(self.gitdir)
1866 # Find the git worktree commondir under .repo/worktrees/.
1867 output = self.bare_git.worktree('list', '--porcelain').splitlines()[0]
1868 assert output.startswith('worktree '), output
1869 commondir = output[9:]
1870 # Walk each of the git worktrees to see where they point.
1871 configs = os.path.join(commondir, 'worktrees')
1872 for name in os.listdir(configs):
1873 gitdir = os.path.join(configs, name, 'gitdir')
1874 with open(gitdir) as fp:
1875 relpath = fp.read().strip()
1876 # Resolve the checkout path and see if it matches this project.
1877 fullpath = platform_utils.realpath(os.path.join(configs, name, relpath))
1878 if fullpath == needle:
1879 platform_utils.rmtree(os.path.join(configs, name))
1880
1881 # Delete the .git directory first, so we're less likely to have a partially
1882 # working git repository around. There shouldn't be any git projects here,
1883 # so rmtree works.
1884
1885 # Try to remove plain files first in case of git worktrees. If this fails
1886 # for any reason, we'll fall back to rmtree, and that'll display errors if
1887 # it can't remove things either.
1888 try:
1889 platform_utils.remove(self.gitdir)
1890 except OSError:
1891 pass
1892 try:
1893 platform_utils.rmtree(self.gitdir)
1894 except OSError as e:
1895 if e.errno != errno.ENOENT:
1896 print('error: %s: %s' % (self.gitdir, e), file=sys.stderr)
1897 print('error: %s: Failed to delete obsolete checkout; remove manually, '
1898 'then run `repo sync -l`.' % (self.relpath,), file=sys.stderr)
1899 return False
1900
1901 # Delete everything under the worktree, except for directories that contain
1902 # another git project.
1903 dirs_to_remove = []
1904 failed = False
1905 for root, dirs, files in platform_utils.walk(self.worktree):
1906 for f in files:
1907 path = os.path.join(root, f)
1908 try:
1909 platform_utils.remove(path)
1910 except OSError as e:
1911 if e.errno != errno.ENOENT:
1912 print('error: %s: Failed to remove: %s' % (path, e), file=sys.stderr)
1913 failed = True
1914 dirs[:] = [d for d in dirs
1915 if not os.path.lexists(os.path.join(root, d, '.git'))]
1916 dirs_to_remove += [os.path.join(root, d) for d in dirs
1917 if os.path.join(root, d) not in dirs_to_remove]
1918 for d in reversed(dirs_to_remove):
1919 if platform_utils.islink(d):
1920 try:
1921 platform_utils.remove(d)
1922 except OSError as e:
1923 if e.errno != errno.ENOENT:
1924 print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
1925 failed = True
1926 elif not platform_utils.listdir(d):
1927 try:
1928 platform_utils.rmdir(d)
1929 except OSError as e:
1930 if e.errno != errno.ENOENT:
1931 print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr)
1932 failed = True
1933 if failed:
1934 print('error: %s: Failed to delete obsolete checkout.' % (self.relpath,),
1935 file=sys.stderr)
1936 print(' Remove manually, then run `repo sync -l`.', file=sys.stderr)
1937 return False
1938
1939 # Try deleting parent dirs if they are empty.
1940 path = self.worktree
1941 while path != self.manifest.topdir:
1942 try:
1943 platform_utils.rmdir(path)
1944 except OSError as e:
1945 if e.errno != errno.ENOENT:
1946 break
1947 path = os.path.dirname(path)
1948
1949 return True
1950
1835# Branch Management ## 1951# Branch Management ##
1836 def GetHeadPath(self): 1952 def GetHeadPath(self):
1837 """Return the full path to the HEAD ref.""" 1953 """Return the full path to the HEAD ref."""
diff --git a/subcmds/sync.py b/subcmds/sync.py
index eada76a7..f2af0ec3 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -16,7 +16,6 @@
16 16
17from __future__ import print_function 17from __future__ import print_function
18 18
19import errno
20import json 19import json
21import netrc 20import netrc
22from optparse import SUPPRESS_HELP 21from optparse import SUPPRESS_HELP
@@ -633,74 +632,6 @@ later is required to fix a server side protocol bug.
633 else: 632 else:
634 self.manifest._Unload() 633 self.manifest._Unload()
635 634
636 def _DeleteProject(self, path):
637 print('Deleting obsolete path %s' % path, file=sys.stderr)
638
639 # Delete the .git directory first, so we're less likely to have a partially
640 # working git repository around. There shouldn't be any git projects here,
641 # so rmtree works.
642 dotgit = os.path.join(path, '.git')
643 # Try to remove plain files first in case of git worktrees. If this fails
644 # for any reason, we'll fall back to rmtree, and that'll display errors if
645 # it can't remove things either.
646 try:
647 platform_utils.remove(dotgit)
648 except OSError:
649 pass
650 try:
651 platform_utils.rmtree(dotgit)
652 except OSError as e:
653 if e.errno != errno.ENOENT:
654 print('error: %s: %s' % (dotgit, str(e)), file=sys.stderr)
655 print('error: %s: Failed to delete obsolete path; remove manually, then '
656 'run sync again' % (path,), file=sys.stderr)
657 return 1
658
659 # Delete everything under the worktree, except for directories that contain
660 # another git project
661 dirs_to_remove = []
662 failed = False
663 for root, dirs, files in platform_utils.walk(path):
664 for f in files:
665 try:
666 platform_utils.remove(os.path.join(root, f))
667 except OSError as e:
668 print('Failed to remove %s (%s)' % (os.path.join(root, f), str(e)), file=sys.stderr)
669 failed = True
670 dirs[:] = [d for d in dirs
671 if not os.path.lexists(os.path.join(root, d, '.git'))]
672 dirs_to_remove += [os.path.join(root, d) for d in dirs
673 if os.path.join(root, d) not in dirs_to_remove]
674 for d in reversed(dirs_to_remove):
675 if platform_utils.islink(d):
676 try:
677 platform_utils.remove(d)
678 except OSError as e:
679 print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
680 failed = True
681 elif len(platform_utils.listdir(d)) == 0:
682 try:
683 platform_utils.rmdir(d)
684 except OSError as e:
685 print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr)
686 failed = True
687 continue
688 if failed:
689 print('error: Failed to delete obsolete path %s' % path, file=sys.stderr)
690 print(' remove manually, then run sync again', file=sys.stderr)
691 return 1
692
693 # Try deleting parent dirs if they are empty
694 project_dir = path
695 while project_dir != self.manifest.topdir:
696 if len(platform_utils.listdir(project_dir)) == 0:
697 platform_utils.rmdir(project_dir)
698 else:
699 break
700 project_dir = os.path.dirname(project_dir)
701
702 return 0
703
704 def UpdateProjectList(self, opt): 635 def UpdateProjectList(self, opt):
705 new_project_paths = [] 636 new_project_paths = []
706 for project in self.GetProjects(None, missing_ok=True): 637 for project in self.GetProjects(None, missing_ok=True):
@@ -727,23 +658,15 @@ later is required to fix a server side protocol bug.
727 remote=RemoteSpec('origin'), 658 remote=RemoteSpec('origin'),
728 gitdir=gitdir, 659 gitdir=gitdir,
729 objdir=gitdir, 660 objdir=gitdir,
661 use_git_worktrees=os.path.isfile(gitdir),
730 worktree=os.path.join(self.manifest.topdir, path), 662 worktree=os.path.join(self.manifest.topdir, path),
731 relpath=path, 663 relpath=path,
732 revisionExpr='HEAD', 664 revisionExpr='HEAD',
733 revisionId=None, 665 revisionId=None,
734 groups=None) 666 groups=None)
735 667 if not project.DeleteWorktree(
736 if project.IsDirty() and opt.force_remove_dirty: 668 quiet=opt.quiet,
737 print('WARNING: Removing dirty project "%s": uncommitted changes ' 669 force=opt.force_remove_dirty):
738 'erased' % project.relpath, file=sys.stderr)
739 self._DeleteProject(project.worktree)
740 elif project.IsDirty():
741 print('error: Cannot remove project "%s": uncommitted changes '
742 'are present' % project.relpath, file=sys.stderr)
743 print(' commit changes, then run sync again',
744 file=sys.stderr)
745 return 1
746 elif self._DeleteProject(project.worktree):
747 return 1 670 return 1
748 671
749 new_project_paths.sort() 672 new_project_paths.sort()