Example #1
0
def _split_file(abspath: ExPath, cwd, dry_run: bool):
    try:
        splits = abspath.split(verify_integrity=True)
    except FileExistsError as e:
        # splits already existed
        misc.whiteprint('found pre-existing splits')
        splits = abspath.getsplits()
        splits_ok = abspath.splits_integrity_ok()
        if not splits_ok:
            prompt.generic(
                f"splits of '{abspath}' aren't good, joined file at /tmp/joined",
                flowopts=('quit', 'debug'))
            return
        misc.greenprint('splits are good')

        biggest_split = max([split.size('mb') for split in splits])
        promptstr = f'found {len(splits)} pre-existing splits, biggest is {biggest_split}MB.'
        _handle_already_split_file(promptstr, abspath, cwd, dry_run=dry_run)
        return
    except OSError:
        prompt.generic(
            f"Just split {abspath} but splits aren't good, joined file at /tmp/joined",
            flowopts=('quit', 'debug'))
    else:
        biggest_split = max([split.size('mb') for split in splits])
        promptstr = f'no pre-existing splits found. split file to {len(splits)} good splits, biggest is {biggest_split}MB.'
        _handle_already_split_file(promptstr, abspath, cwd, dry_run=dry_run)
Example #2
0
 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)
Example #3
0
 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
Example #4
0
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
Example #5
0
 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')]
Example #6
0
 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()
Example #7
0
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
Example #8
0
def _handle_already_split_file(promptstr, main_file: ExPath, cwd,
                               dry_run: bool):
    key, answer = prompt.action(promptstr,
                                'ignore main file',
                                'move main file to trash://',
                                flowopts='quit')
    if key == 'i':
        ignore(main_file.relative_to(cwd), dry_run=dry_run)
        return
    # key == 'm' for move to trash
    if dry_run:
        misc.whiteprint('dry run, not trashing file')
        return
    main_file.trash()
Example #9
0
 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
Example #10
0
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
Example #11
0
 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()
Example #12
0
 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
Example #13
0
 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
Example #14
0
 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
Example #15
0
 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
Example #16
0
def _get_large_files_in_dir(path: ExPath) -> Dict[ExPath, float]:
    large_subfiles = {}
    for p in path.iterdir():
        if not p.exists():
            misc.yellowprint(
                f'_get_large_files_in_dir() | does not exist: {repr(p)}')
            continue
        if p.is_file():
            mb = p.size('mb')
            if mb >= 50:
                large_subfiles[p] = mb
        else:
            large_subfiles.update(_get_large_files_in_dir(p))
    return large_subfiles
Example #17
0
 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
Example #18
0
 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
Example #19
0
 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)
Example #20
0
        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
Example #21
0
    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)
Example #22
0
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()
Example #23
0
 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()
Example #24
0
 def test__single_wildcard__whole_part__1(self):
     assert ExPath('/*/gilad').is_dir()
     assert ExPath('/*/gilad').is_file() is False
Example #25
0
 def test__multiple_wildcards__whole_part(self):
     assert ExPath('/*/gilad/*').is_dir()
     assert ExPath('/*/gilad/*').is_file() is False
Example #26
0
 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
Example #27
0
 def __contains__(self, item):
     path = ExPath(item)
     for ignored in self:
         if ignored == path:
             return True
     return False
Example #28
0
 def __init__(self):
     self.file = ExPath('.gitignore')
     if not self.file.exists():
         raise FileNotFoundError(f'Gitignore.__init__(): {self.file.absolute()} does not exist')
Example #29
0
class Gitignore:
    # https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files
    def __init__(self):
        self.file = ExPath('.gitignore')
        if not self.file.exists():
            raise FileNotFoundError(f'Gitignore.__init__(): {self.file.absolute()} does not exist')
    
    def __getitem__(self, item):
        return self.values[item]
    
    def __contains__(self, item):
        path = ExPath(item)
        for ignored in self:
            if ignored == path:
                return True
        return False
    
    def __getattribute__(self, name: str) -> Any:
        try:
            return super().__getattribute__(name)
        except AttributeError as e:
            # support all ExPath methods
            return getattr(self.file, name)
    
    def __iter__(self):
        yield from self.values
    
    # @cachedprop # TODO: uncomment when possible to clear cache
    @property
    @memoize
    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 should_add_to_gitignore(self, p: ExPath, quiet=False) -> bool:
        for ignored in self.values:
            if ignored == p:
                if not quiet:
                    logging.warning(f'{p} already in gitignore, continuing')
                return False
            if ignored.parent_of(p):
                if quiet:
                    return False
                msg = colors.yellow(f"parent '{ignored}' of '{p}' already in gitignore")
                key, action = prompt.action(msg, 'skip', 'ignore anyway', flowopts=('debug', 'quit'))
                if action.value == 'skip':
                    print('skipping')
                    return False
        return True
    
    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 write(self, paths, *, verify_paths=True, confirm=False, dry_run=False, backup=True):
        logging.debug(f'paths:', paths, 'verify_paths:', verify_paths, 'confirm:', confirm, 'dry_run:', dry_run, 'backup:', backup)
        writelines = []
        if verify_paths:
            should_add_to_gitignore = self.should_add_to_gitignore
        else:
            should_add_to_gitignore = lambda _p: True
        for p in filter(should_add_to_gitignore, paths):
            to_write = f'\n{p}'
            if confirm and not prompt.confirm(f'Add {p} to .gitignore?'):
                continue
            logging.info(f'Adding {p} to .gitignore. dry_run={dry_run}, backup={backup}, confirm={confirm}')
            writelines.append(to_write)
        if dry_run:
            print('dry_run, returning')
            return
        if backup:
            backup_ok = self.backup(confirm)
            if not backup_ok:
                return
        
        with self.file.open(mode='a') as file:
            file.write(''.join(sorted(writelines)))
        Gitignore.values.fget.clear_cache()
    
    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 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 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 paths_where(self, predicate: Callable[[Any], bool]) -> Generator[ExPath, None, None]:
        for ignored in self:
            if predicate(ignored):
                yield ignored
Example #30
0
 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()