From 3b7a6968d9be240c4ad4ec3d92bb99bc7fd888ef Mon Sep 17 00:00:00 2001 From: Jon Bernard Date: Fri, 1 Jan 2016 08:54:14 -0500 Subject: Implement dotfile state management --- dotfiles/cli.py | 6 ++++-- dotfiles/dotfile.py | 43 +++++++++++++++++++++++++++++-------- dotfiles/repository.py | 4 +++- tests/test_dotfile.py | 56 +++++++++++++++++++++++++++++++++++++++++++----- tests/test_repository.py | 28 +++++++++++++++--------- 5 files changed, 110 insertions(+), 27 deletions(-) diff --git a/dotfiles/cli.py b/dotfiles/cli.py index d5d49ee..5ca6771 100644 --- a/dotfiles/cli.py +++ b/dotfiles/cli.py @@ -29,8 +29,9 @@ def main(ctx, repository): @main.command() +@click.option('--color', is_flag=True, help='Enables colored output.') @pass_repo -def check(repo): +def check(repo, color): """Shows any broken or unsyned dotfiles.""" list = repo.check() if list: @@ -39,8 +40,9 @@ def check(repo): @main.command() +@click.option('--color', is_flag=True, help='Enables colored output.') @pass_repo -def status(repo): +def status(repo, color): """Shows the status of each dotfile.""" click.echo_via_pager(repo.status()) diff --git a/dotfiles/dotfile.py b/dotfiles/dotfile.py index 0b15dbd..1a86f63 100644 --- a/dotfiles/dotfile.py +++ b/dotfiles/dotfile.py @@ -23,22 +23,25 @@ class Dotfile(object): called, and not in the layers above. """ + states = { + 'error': {'text': '(error)', 'color': 'red'}, + 'missing': {'text': '(missing)', 'color': 'yellow'}, + 'conflict': {'text': '(conflict)', 'color': 'yellow'}, + 'ok': {'text': '(ok)', 'color': 'green'}, + } + def __init__(self, name, target): self.name = name self.target = target - self.state = '(unknown)' + self._set_state() def __str__(self): - short_name, short_target = self._truncate_paths() - return '%s -> %s %s' % (short_name, short_target, self.state) + short_name, _ = self._truncate_paths() + return '%-18s %-s' % (short_name, self.state['text']) def __repr__(self): return '' % self.name - def _truncate_paths(self): - discard = len(str(self.name.common(self.target))) + 1 - return (str(self.name)[discard:], str(self.target)[discard:]) - def add(self): if self.target.check(exists=1): raise OSError(errno.EEXIST, self.target) @@ -54,5 +57,27 @@ class Dotfile(object): def sync(self): self.name.mksymlinkto(self.target) - def invalid(self): - return self.state != '(ok)' + def is_ok(self): + return self.state == self.states['ok'] + + def _set_state(self): + + # only for testing, cli should never reach this state + if self.target.check(exists=0): + self.state = self.states['error'] + + # no $HOME symlink + elif self.name.check(exists=0): + self.state = self.states['missing'] + + # if name exists but isn't a link to the target + elif self.name.check(link=0) or not self.name.samefile(self.target): + self.state = self.states['conflict'] + + # all good + else: + self.state = self.states['ok'] + + def _truncate_paths(self): + discard = len(str(self.name.common(self.target))) + 1 + return (str(self.name)[discard:], str(self.target)[discard:]) diff --git a/dotfiles/repository.py b/dotfiles/repository.py index 0c3a4f3..f394ed1 100644 --- a/dotfiles/repository.py +++ b/dotfiles/repository.py @@ -68,12 +68,14 @@ class Repository(object): dotfiles.append(Dotfile(name, target)) return sorted(dotfiles, key=attrgetter('name')) + # TODO: pass dotfile objects to CLI instead of string + def _contents(self, all=True): """Convert loaded contents to human readable form.""" contents = '' dotfiles = self._load() for dotfile in dotfiles: - if all or dotfile.invalid(): + if all or not dotfile.is_ok(): contents += '\n%s' % dotfile return contents.lstrip() diff --git a/tests/test_dotfile.py b/tests/test_dotfile.py index 7b99db1..29908ba 100644 --- a/tests/test_dotfile.py +++ b/tests/test_dotfile.py @@ -73,7 +73,53 @@ def test_sync(tmpdir, times): assert name.samefile(target) -def test_valid(tmpdir): +def test_state_error(tmpdir): + + repo = tmpdir.ensure("Dotfiles", dir=1) + name = tmpdir.join(".vimrc") + target = repo.join("vimrc") + + dotfile = Dotfile(name, target) + + assert Dotfile.states['error'] == dotfile.state + + +def test_state_missing(tmpdir): + + repo = tmpdir.ensure("Dotfiles", dir=1) + name = tmpdir.join(".vimrc") + target = repo.ensure("vimrc") + + dotfile = Dotfile(name, target) + + assert Dotfile.states['missing'] == dotfile.state + + +def test_state_conflict(tmpdir): + + repo = tmpdir.ensure("Dotfiles", dir=1) + name = tmpdir.ensure(".vimrc") + target = repo.ensure("vimrc") + + dotfile = Dotfile(name, target) + + assert Dotfile.states['conflict'] == dotfile.state + + +def test_state_ok(tmpdir): + + repo = tmpdir.join("Dotfiles", dir=1) + name = tmpdir.join(".vimrc") + target = repo.ensure("vimrc") + + name.mksymlinkto(target) + + dotfile = Dotfile(name, target) + + assert Dotfile.states['ok'] == dotfile.state + + +def test_is_ok(tmpdir): repo = tmpdir.join("Dotfiles", dir=1) name = tmpdir.join(".vimrc") @@ -82,8 +128,8 @@ def test_valid(tmpdir): dotfile = Dotfile(name, target) - assert '(unknown)' == dotfile.state - assert True == dotfile.invalid() + assert Dotfile.states['ok'] == dotfile.state + assert True == dotfile.is_ok() - dotfile.state = '(ok)' - assert False == dotfile.invalid() + dotfile.state = Dotfile.states['conflict'] + assert False == dotfile.is_ok() diff --git a/tests/test_repository.py b/tests/test_repository.py index 8e6b48b..c8212e6 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -65,8 +65,8 @@ def test_empty_status(tmpdir): def test_status_manual(tmpdir, monkeypatch): repodir = tmpdir.join("Dotfiles", dir=1) - name = tmpdir.join(".vimrc") target = repodir.ensure("vimrc") + name = tmpdir.ensure(".vimrc") dotfile = Dotfile(name, target) @@ -74,9 +74,10 @@ def test_status_manual(tmpdir, monkeypatch): monkeypatch.setattr(Repository, "_load", lambda self: [dotfile, dotfile, dotfile]) - expected_status = (".vimrc -> Dotfiles/vimrc (unknown)\n" - ".vimrc -> Dotfiles/vimrc (unknown)\n" - ".vimrc -> Dotfiles/vimrc (unknown)") + dotfile_state = Dotfile.states['conflict']['text'] + expected_status = ("{0:<18} {1}\n" + "{0:<18} {1}\n" + "{0:<18} {1}".format(name.basename, dotfile_state)) assert expected_status == repo.status() @@ -91,9 +92,12 @@ def test_status_discover(tmpdir): repo = Repository(repodir, tmpdir) - expected_status = (".bashrc -> Dotfiles/bashrc (unknown)\n" - ".inputrc -> Dotfiles/inputrc (unknown)\n" - ".vimrc -> Dotfiles/vimrc (unknown)") + expected_status = ("{1:<18} {0}\n" + "{2:<18} {0}\n" + "{3:<18} {0}".format(Dotfile.states['ok']['text'], + '.bashrc', + '.inputrc', + '.vimrc')) assert expected_status == repo.status() @@ -106,14 +110,18 @@ def test_check(tmpdir, monkeypatch): dotfile_b = Dotfile(tmpdir.join('.bbb'), repodir.join('bbb')) dotfile_c = Dotfile(tmpdir.join('.ccc'), repodir.join('ccc')) - dotfile_b.state = '(ok)' + dotfile_b.state = Dotfile.states['ok'] repo = Repository(tmpdir) monkeypatch.setattr(Repository, "_load", lambda self: [dotfile_a, dotfile_b, dotfile_c]) - assert ('.aaa -> repo/aaa (unknown)\n' - '.ccc -> repo/ccc (unknown)') == repo.check() + expected_status = ("{1:<18} {0}\n" + "{2:<18} {0}".format(Dotfile.states['error']['text'], + dotfile_a.name.basename, + dotfile_c.name.basename)) + + assert expected_status == repo.check() def test_sync(): -- cgit v1.2.3