Dulwich.io dulwich / 65a7a61
porcelain clean (issue 398) (#690) Implement a basic dulwich.porcelain.clean (#398) Lane Barlow authored 2 months ago Jelmer Vernooń≥ committed 2 months ago
2 changed file(s) with 200 addition(s) and 8 deletion(s). Raw diff Collapse all Expand all
407407 return (relpaths, ignored)
408408
409409
410 def _is_subdir(subdir, parentdir):
411 """Check whether subdir is parentdir or a subdir of parentdir
412
413 If parentdir or subdir is a relative path, it will be disamgibuated
414 relative to the pwd.
415 """
416 parentdir_abs = os.path.realpath(parentdir) + os.path.sep
417 subdir_abs = os.path.realpath(subdir) + os.path.sep
418 return subdir_abs.startswith(parentdir_abs)
419
420
421 # TODO: option to remove ignored files also, in line with `git clean -fdx`
422 def clean(repo=".", target_dir=None):
423 """Remove any untracked files from the target directory recursively
424
425 Equivalent to running `git clean -fd` in target_dir.
426
427 :param repo: Repository where the files may be tracked
428 :param target_dir: Directory to clean - current directory if None
429 """
430 if target_dir is None:
431 target_dir = os.getcwd()
432
433 if not _is_subdir(target_dir, repo):
434 raise ValueError("target_dir must be in the repo's working dir")
435
436 with open_repo_closing(repo) as r:
437 index = r.open_index()
438 ignore_manager = IgnoreFilterManager.from_repo(r)
439
440 paths_in_wd = _walk_working_dir_paths(target_dir, r.path)
441 # Reverse file visit order, so that files and subdirectories are
442 # removed before containing directory
443 for ap, is_dir in reversed(list(paths_in_wd)):
444 if is_dir:
445 # All subdirectories and files have been removed if untracked,
446 # so dir contains no tracked files iff it is empty.
447 is_empty = len(os.listdir(ap)) == 0
448 if is_empty:
449 os.rmdir(ap)
450 else:
451 ip = path_to_tree_path(r.path, ap)
452 is_tracked = ip in index
453
454 rp = os.path.relpath(ap, r.path)
455 is_ignored = ignore_manager.is_ignored(rp)
456
457 if not is_tracked and not is_ignored:
458 os.remove(ap)
459
460
410461 def remove(repo=".", paths=None, cached=False):
411462 """Remove files from the staging area.
412463
899950 return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
900951
901952
902 def get_untracked_paths(frompath, basepath, index):
903 """Get untracked paths.
904
905 ;param frompath: Path to walk
953 def _walk_working_dir_paths(frompath, basepath):
954 """Get path, is_dir for files in working dir from frompath
955
956 :param frompath: Path to begin walk
906957 :param basepath: Path to compare to
907 :param index: Index to check against
908 """
909 # If nothing is specified, add all non-ignored files.
958 """
910959 for dirpath, dirnames, filenames in os.walk(frompath):
911960 # Skip .git and below.
912961 if '.git' in dirnames:
917966 filenames.remove('.git')
918967 if dirpath != basepath:
919968 continue
969
970 if dirpath != frompath:
971 yield dirpath, True
972
920973 for filename in filenames:
921 ap = os.path.join(dirpath, filename)
974 filepath = os.path.join(dirpath, filename)
975 yield filepath, False
976
977
978 def get_untracked_paths(frompath, basepath, index):
979 """Get untracked paths.
980
981 ;param frompath: Path to walk
982 :param basepath: Path to compare to
983 :param index: Index to check against
984 """
985 for ap, is_dir in _walk_working_dir_paths(frompath, basepath):
986 if not is_dir:
922987 ip = path_to_tree_path(basepath, ap)
923988 if ip not in index:
924989 yield os.path.relpath(ap, frompath)
2424 from StringIO import StringIO
2525 except ImportError:
2626 from io import StringIO
27 import errno
2728 import os
2829 import shutil
2930 import tarfile
5253 )
5354
5455
56 def flat_walk_dir(dir_to_walk):
57 for dirpath, _, filenames in os.walk(dir_to_walk):
58 rel_dirpath = os.path.relpath(dirpath, dir_to_walk)
59 if not dirpath == dir_to_walk:
60 yield rel_dirpath
61 for filename in filenames:
62 if dirpath == dir_to_walk:
63 yield filename
64 else:
65 yield os.path.join(rel_dirpath, filename)
66
67
5568 class PorcelainTestCase(TestCase):
5669
5770 def setUp(self):
113126 committer="Bob <bob@example.com>")
114127 self.assertTrue(isinstance(sha, bytes))
115128 self.assertEqual(len(sha), 40)
129
130
131 class CleanTests(PorcelainTestCase):
132 def path_in_wd(self, name):
133 """Get path of file in wd
134 """
135 return os.path.join(self.repo.path, name)
136
137 def put_files(self, tracked, ignored, untracked, empty_dirs):
138 """Put the described files in the wd
139 """
140 all_files = tracked | ignored | untracked
141 for file_path in all_files:
142 abs_path = self.path_in_wd(file_path)
143 # File may need to be written in a dir that doesn't exist yet, so
144 # create the parent dir(s) as necessary
145 parent_dir = os.path.dirname(abs_path)
146 try:
147 os.makedirs(parent_dir)
148 except OSError as err:
149 if not err.errno == errno.EEXIST:
150 raise err
151 with open(abs_path, 'w') as f:
152 f.write('')
153
154 with open(self.path_in_wd('.gitignore'), 'w') as f:
155 f.writelines(ignored)
156
157 for dir_path in empty_dirs:
158 os.mkdir(self.path_in_wd('empty_dir'))
159
160 files_to_add = [self.path_in_wd(t) for t in tracked]
161 porcelain.add(repo=self.repo.path, paths=files_to_add)
162 porcelain.commit(repo=self.repo.path, message="init commit")
163
164 def clean(self, target_dir):
165 """Clean the target_dir and assert control dir unchanged
166 """
167 controldir = self.repo._controldir
168
169 controldir_before = set(flat_walk_dir(controldir))
170 porcelain.clean(repo=self.repo.path, target_dir=target_dir)
171 controldir_after = set(flat_walk_dir(controldir))
172
173 self.assertEqual(controldir_after, controldir_before)
174
175 def assert_wd(self, expected_paths):
176 """Assert paths of files and dirs in wd are same as expected_paths
177 """
178 control_dir = self.repo._controldir
179 control_dir_rel = os.path.relpath(control_dir, self.repo.path)
180
181 # normalize paths to simplify comparison across platforms
182 from os.path import normpath
183 found_paths = {
184 normpath(p)
185 for p in flat_walk_dir(self.repo.path)
186 if not p.split(os.sep)[0] == control_dir_rel}
187 norm_expected_paths = {normpath(p) for p in expected_paths}
188 self.assertEqual(found_paths, norm_expected_paths)
189
190 def test_from_root(self):
191 self.put_files(
192 tracked={
193 'tracked_file',
194 'tracked_dir/tracked_file',
195 '.gitignore'},
196 ignored={
197 'ignored_file'},
198 untracked={
199 'untracked_file',
200 'tracked_dir/untracked_dir/untracked_file',
201 'untracked_dir/untracked_dir/untracked_file'},
202 empty_dirs={
203 'empty_dir'})
204
205 self.clean(self.repo.path)
206
207 self.assert_wd({
208 'tracked_file',
209 'tracked_dir/tracked_file',
210 '.gitignore',
211 'ignored_file',
212 'tracked_dir'})
213
214 def test_from_subdir(self):
215 self.put_files(
216 tracked={
217 'tracked_file',
218 'tracked_dir/tracked_file',
219 '.gitignore'},
220 ignored={
221 'ignored_file'},
222 untracked={
223 'untracked_file',
224 'tracked_dir/untracked_dir/untracked_file',
225 'untracked_dir/untracked_dir/untracked_file'},
226 empty_dirs={
227 'empty_dir'})
228
229 target_dir = self.path_in_wd('untracked_dir')
230 self.clean(target_dir)
231
232 self.assert_wd({
233 'tracked_file',
234 'tracked_dir/tracked_file',
235 '.gitignore',
236 'ignored_file',
237 'untracked_file',
238 'tracked_dir/untracked_dir/untracked_file',
239 'empty_dir',
240 'untracked_dir',
241 'tracked_dir',
242 'tracked_dir/untracked_dir'})
116243
117244
118245 class CloneTests(PorcelainTestCase):