def test__with_suffix__startswith_path_reg(self): # e.g. '.*/py_venv.xml'. should return has suffix (True) with_suffix = ExPath('py_venv.xml') assert with_suffix.has_file_suffix() is True for reg in path_regexes(): val = ExPath(f'{reg}{with_suffix}') actual = val.has_file_suffix() assert actual is True
def test__no_suffix__endswith_path_reg(self): # e.g. 'py_venv.*/'. should return no suffix (False) no_suffix = ExPath('py_venv') assert no_suffix.has_file_suffix() is False for reg in path_regexes(): val = ExPath(f'{no_suffix}{reg}') actual = val.has_file_suffix() assert actual is False
def test__no_suffix__surrounded_by_path_reg(self): # e.g. '.*/py_venv.*/'. should return no suffix (False) no_suffix = ExPath('py_venv') assert no_suffix.has_file_suffix() is False for reg in path_regexes(): for morereg in path_regexes(): val = ExPath(f'{morereg}{no_suffix}{reg}') actual = val.has_file_suffix() assert actual is False
def test__doesnt_exist__0(self): bad = ExPath('/uzer/*') assert not bad.exists() assert not bad.is_dir() assert not bad.is_file() bad2 = ExPath('/*DOESNTEXIST') assert not bad2.exists() assert not bad2.is_dir() assert not bad2.is_file()
def values(self) -> List[ExPath]: with self.file.open(mode='r') as file: data = file.read() lines = data.splitlines() paths = [] for x in lines: if not bool(x) or '#' in x or not WORD_RE.search(x): continue paths.append(ExPath(x)) return [*paths, ExPath('.git')]
def test____eq__(): gilad = ExPath(giladdirstr) for path in path_ctor_permutations(giladdirstr): assert gilad == path for path in path_ctor_permutations('~/'): assert gilad == path gilad = ExPath('~/') for path in path_ctor_permutations('~/'): assert gilad == path for path in path_ctor_permutations(giladdirstr): assert gilad == path
def test__everything_mixed_with_regex(self): # e.g. '.*/py_v[en]*v.xm?l'. should return has suffix (True) assert ExPath('.*/py_v[en]*v.xm?l').has_file_suffix() is True mixed_stems = get_permutations_in_size_range(f'{REGEX_CHAR}.py_venv-1257', slice(5), has_letters_and_punc) for stem in mixed_stems: for suffix in mixed_suffixes(): name = ExPath(f'{stem}.{suffix}') actual = name.has_file_suffix() assert actual is True for reg in path_regexes(): val = ExPath(f'{reg}{name}') actual = val.has_file_suffix() assert actual is True
def search(self, keyword: Union[str, ExPath], *, noprompt=True) -> ExPath: """Tries to return an ExPath in status. First assumes `keyword` is an exact file (str or ExPath), and if fails, uses `search` module. @param noprompt: specify False to allow using search_and_prompt(keyword, file) in case nothing matched earlier. """ darkprint(f'Status.search({repr(keyword)}) |') path = ExPath(keyword) for file in self.files: if file == path: return file has_suffix = path.has_file_suffix() has_slash = '/' in keyword has_regex = regex.has_regex(keyword) darkprint(f'\t{has_suffix = }, {has_slash = }, {has_regex = }') if has_suffix: files = self.files else: files = [f.with_suffix('') for f in self.files] if has_regex: for expath in files: if re.search(keyword, str(expath)): return expath if has_slash: darkprint( f"looking for the nearest match among status paths for: '{keyword}'" ) return ExPath(search.nearest(keyword, files)) darkprint( f"looking for a matching part among status paths ({'with' if has_suffix else 'without'} suffixes...) for: '{keyword}'" ) for f in files: # TODO: integrate into git.search somehow for i, part in enumerate(f.parts): if part == keyword: ret = ExPath(os.path.join(*f.parts[0:i + 1])) return ret if noprompt: return None darkprint( f"didn't find a matching part, calling search_and_prompt()...") choice = search_and_prompt(keyword, [str(f) for f in files], criterion='substring') if choice: return ExPath(choice) prompt.generic(colors.red(f"'{keyword}' didn't match anything"), flowopts=True)
def test__usr__1(self): # because '/usr/*' is parent of '/usr/local', # '/usr/' isn't parent of '/usr/*' # (because they function the same) usr_wc = ExPath('/usr/') for path in path_ctor_permutations('/usr/*'): assert not usr_wc.parent_of(path)
def is_subpath_of_ignored(self, p) -> bool: """Returns True if `p` is strictly a subpath of a path in .gitignore""" path = ExPath(p) for ignored in self: if ignored.parent_of(path): return True return False
def handle_exclude_paths( exclude_paths_str: str) -> Tuple[ExPath, List[ExPath]]: """Does the '.config/dconf copyq' trick""" if not exclude_paths_str: return None, None if '/' in exclude_paths_str: exclude_parent, _, exclude_paths = exclude_paths_str.rpartition('/') exclude_parent = ExPath(exclude_parent) exclude_paths = [ exclude_parent / ExPath(ex) for ex in exclude_paths.split(' ') ] else: raise NotImplementedError( f'only currently handled format is `.ipython/profile_default/startup ipython_config.py`' ) return exclude_parent, exclude_paths
def test__single_wildcard__part_of_part(self): assert ExPath('/home/gilad/.bashr*').is_file() assert ExPath('/home/gil*d').is_dir() assert ExPath('/home/gil*d').is_file() is False assert ExPath('/*ome/gilad').is_dir() assert ExPath('/*ome/gilad').is_file() is False assert ExPath('/home/gil*d/.bashrc').is_dir() is False assert ExPath('/home/gil*d/.bashrc').is_file() assert ExPath('/*ome/gilad/.bashrc').is_file()
def test__parent_is_actually_file(self): # both really exist but a file isn't a parent of anything file = ExPath('/home/gilad/.local/bin/pip*') for otherfile in path_ctor_permutations('/home/gilad/.local/bin/pip3'): try: assert not file.parent_of(otherfile) except AssertionError as e: misc.brightredprint(f'file: {repr(file)} ({type(file)}) is not parent of otherfile: {repr(otherfile)} ({type(otherfile)})') raise
def build_paths(exclude_parent, exclude_paths, ignore_paths) -> List[ExPath]: logger.debug(f'exclude_parent:', exclude_parent, 'exclude_paths:', exclude_paths, 'ignore_paths:', ignore_paths) statusfiles = None paths: List[ExPath] = [] skip_non_existent = False ignore_non_existent = False for f in ignore_paths: # * wildcard if "*" in f: paths.append(f) # TODO: now that ExPath supports glob, ... continue # * index try: if not statusfiles: statusfiles = Status().files f = statusfiles[int(str(f))] except IndexError: i = input( f'Index error with {f} (len of files: {len(statusfiles)}), try again:\t' ) f = statusfiles[int(i)] except ValueError: pass # not a number path = ExPath(f) if not path.exists(): if skip_non_existent: continue if not ignore_non_existent: key, choice = prompt.action(f'{path} does not exist', 'skip', 'ignore anyway', sA='skip all', iA='ignore all') if key == 'skip': continue if key == 'sA': skip_non_existent = True elif key == 'iA': ignore_non_existent = True continue if path == exclude_parent: # path: '.config', exclude_paths: ['.config/dconf'] for sub in filter(lambda p: p not in exclude_paths, path.iterdir()): paths.append(sub) else: paths.append(path) if not paths: sys.exit(colors.brightred(f'no paths in {ExPath(".").absolute()}')) return paths
def is_ignored(self, p) -> bool: """Returns True if `p` in .gitignore, or if `p` is a subpath of a path in .gitignore, or if `p` fullmatches any part of a path in .gitignore (i.e. if 'env' is ignored, then 'src/env' returns True)""" path = ExPath(p) for ignored in self: if ignored == path: return True if ignored.parent_of(path): return True if any(re.fullmatch('env', part) for part in path.parts): return True return False
def unignore(self, path, *, confirm=False, dry_run=False, backup=True): path = ExPath(path) newvals = [] found = False for ignored in self: if ignored == path: breakpoint() found = True continue newvals.append(ignored) if not found: logging.warning(f'Gitignore.unignore(path={path}): not in self. returning') return if confirm and not prompt.confirm(f'Remove {path} from .gitignore?'): print('aborting') return self.write(newvals, verify_paths=False, dry_run=dry_run, backup=backup)
def backup(self, confirm: bool): if confirm and not prompt.confirm(f'Backup .gitignore to .gitignore.backup?'): print('aborting') return False absolute = self.file.absolute() try: shell.run(f'cp {absolute} {absolute}.backup', raiseexc='summary') except Exception as e: if not prompt.confirm('Backup failed, overwrite .gitignore anyway?', flowopts='debug'): print('aborting') return False return True else: backup = ExPath(f'{absolute}.backup') if not backup.exists() and not prompt.confirm(f'Backup command completed without error, but {backup} doesnt exist. overwrite .gitignore anyway?', flowopts='debug'): print('aborting') return False return True
def _clean_shortstatus( _statusline: str) -> Optional[Tuple[ExPath, str]]: if _statusline.startswith("#"): return None _status, _file = map(misc.unquote, map(str.strip, _statusline.split(maxsplit=1))) if 'R' in _status: if '->' not in _file: raise ValueError( f"'R' in status but '->' not in file. file: {_file}, status: {_status}", locals()) _, _, _file = _file.partition(' -> ') # return only existing else: if '->' in _file: raise ValueError( f"'R' not in status but '->' in file. file: {_file}, status: {_status}", locals()) return ExPath(_file), _status
def main(commitmsg: str, dry_run: bool = False): status = Status() if not status.files: if prompt.confirm('No files in status, just push?'): if dry_run: misc.whiteprint('dry run, not pushing. returning') return return git.push() cwd = ExPath(os.getcwd()) largefiles: Dict[ExPath, float] = get_large_files_from_status(cwd, status) if largefiles: handle_large_files(cwd, largefiles, dry_run) if '.zsh_history' in status: pwd = misc.getsecret('general') with open('/home/gilad/.zsh_history', errors='backslashreplace') as f: history = f.readlines() for i, line in enumerate(history): if pwd in line: misc.redprint( f"password found in .zsh_history in line {i}. exiting") return if not commitmsg: if len(status.files) == 1: commitmsg = status.files[0] elif len(status.files) < 3: commitmsg = ', '.join([f.name for f in status.files]) else: os.system('git status -s') commitmsg = input('commit msg:\t') commitmsg = misc.unquote(commitmsg) if dry_run: misc.whiteprint('dry run. exiting') return shell.run('git add .', f'git commit -am "{commitmsg}"') git.push()
def test__subpath_is_actually_file(self): file = ExPath('/home/gilad/.local/*') for otherfile in path_ctor_permutations('/home/gilad/.local/bin/pip3'): assert file.parent_of(otherfile)
def test__multiple_wildcards__whole_part(self): assert ExPath('/*/gilad/*').is_dir() assert ExPath('/*/gilad/*').is_file() is False
def test__real_world(self): for nosuffix in ['.*/py_venv.*/', 'file', 'home/gilad/.local/*', ]: assert not ExPath(nosuffix).has_file_suffix()
def test__with_mixed_regex_suffix(self): # e.g. 'py_venv.xm?l'. should return has suffix (True) for suffix in mixed_suffixes(): with_suffix = ExPath(f'py_venv.{suffix}') actual = with_suffix.has_file_suffix() assert actual is True
def __contains__(self, item): path = ExPath(item) for ignored in self: if ignored == path: return True return False
def test__single_wildcard__whole_part__0(self): assert ExPath('/usr/*').is_dir() assert ExPath('/usr/*/share').is_dir() assert ExPath('/*/local/share').is_dir() assert ExPath('/usr/*/BAD').is_dir() is False assert ExPath('/usr/*/shar').is_dir() is False assert ExPath('/usr/*/shar').is_file() is False assert ExPath('/usr/*/shar').exists() is False assert ExPath('/*/local/shar').is_dir() is False assert ExPath('/*/local/shar').is_file() is False assert ExPath('/*/local/shar').exists() is False assert ExPath('/home/gilad/*').is_dir() assert ExPath('/home/gilad/*').is_file() is False assert ExPath('/home/*/.bashrc').is_dir() is False assert ExPath('/home/*/.bashrc').is_file() assert ExPath('/*/gilad/.bashrc').is_file()
def __init__(self): self.file = ExPath('.gitignore') if not self.file.exists(): raise FileNotFoundError(f'Gitignore.__init__(): {self.file.absolute()} does not exist')
def test__single_wildcard__whole_part__1(self): assert ExPath('/*/gilad').is_dir() assert ExPath('/*/gilad').is_file() is False
def test__subpath_not_exists__0(self): usr_wc_share = ExPath('/usr/*/share') for path in path_ctor_permutations('/usr/local/share/bad'): assert not usr_wc_share.parent_of(path)
def test__multiple_wildcards__part_of_part(self): assert ExPath('/home/*/.b*shrc').is_dir() is False assert ExPath('/home/*/.b*shrc').is_file() assert ExPath('/*ome/*/.b*shrc').is_dir() is False assert ExPath('/*ome/*/.b*shrc').is_file() assert ExPath('/*/*/.b*shrc').is_file() assert ExPath('/*/*/.b*shrc').is_dir() is False assert ExPath('/home/gil*d/.b*shrc').is_dir() is False assert ExPath('/home/gil*d/.b*shrc').is_file() assert ExPath('/home/gil*d/*').is_file() is False assert ExPath('/home/gil*d/*').is_dir() assert ExPath('/*/gil*d/*').is_dir() assert ExPath('/*/gil*d/*').is_file() is False
def test__mixed_dirs_and_files__multiple_wildcards(self): # assumes .yarn/ and .yarnrc exist yarn = ExPath('/home/*/.yarn*') assert yarn.exists() assert not yarn.is_dir() assert not yarn.is_file()