diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0d400..486ada5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ Note, "non-notable" changes may be small patches with no noticeable effect to th ## Unreleased +## 0.2.0 2020-12-17 +### Added +- Configuration file + - `~/.jockrc` stores repositories and groups for commands to be run on (YAML formatted) + - `--group` can be used to refer to groups of repositories (same usage as repositories) + +### Changed +- `--respository` now refers to repository stored in `.jockrc`, previously looked at adjacent directories + + ## 0.1.0 2020-11-27 ### Added - Initial usage: `jock [OPTIONS] COMMAND [ARGS]` diff --git a/README.md b/README.md index f210c5a..a6d65e4 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,47 @@ execution._ ## Usage +### Configuration + +Repositories and groups must be configured in `~/.jockrc`, in YAML format like below + +```yaml +repositories: + auth-service: + address: git@github.com:some-startup/authentication-service.git + location: /home/jock/git/authentication-service + shared-entities: + address: git@github.com:some-startup/shared-entities.git + location: ~/shared-entities + ... + user-service: + address: git@github.com:some-startup/user-service.git + location: ../users + +groups: + - name: services + repositories: + - auth-service + - user-service +``` + +- `address` is the remote git address +- `location` is the local location, can be relative to home or absolute + +### CLI Usage + ``` Usage: jock [OPTIONS] COMMAND [ARGS]... Options: --version Show the version and exit. - -r, --repository TEXT Repository you wish to run commands on. Multiple - repositories can be specified using multiple flags. + -r, --repository TEXT Repository, specified in ~/.jockrc, you wish to run + commands on. Multiple repositories can be specified + using multiple flags. + + -g, --group TEXT Group of repositories, specified in ~/.jockrc, you + wish to run commands on.Multiple groups can be + specified using multiple flags. --help Show this message and exit. @@ -82,46 +116,11 @@ some-service` `pull`, `push`, `reset`, `restore`, `rm`, `switch`, or `tag` - ARGS are git arguments passed directly to the git command -Until grouping is supported (see the roadmap below), you can save your repo groups using environment variables in your -.bashrc file, e.g: -```shell script -export SERVICES="--repository auth-service --repository user-service" -``` -Then you can run your commands as `jock $(echo $SERVICES) checkout main` - ## Roadmap This is a loose roadmap to explain where the tool will end up, the versions & functionality against them are open to changes. - -### 0.2 - -Stored repository settings and groups. - -e.g. with a config of -```yaml -repositories: - - name: auth-service - address: git@github.com:some-startup/authentication-service.git - directory: ../authentication-service - - name: shared-entities - address: git@github.com:some-startup/shared-entities.git - directory: ../shared-entities - - ... - - name: user-service - address: git@github.com:some-startup/user-service.git - directory: ../users - -groups: - - name: services - repositories: - - auth-service - - user-service -``` -Commands could be grouped without stating individual repositories - -`jock -g=services checkout -b update-shared-entities-version` ### 0.3 + diff --git a/jock/cli.py b/jock/cli.py index 7af6b3a..502a6ff 100644 --- a/jock/cli.py +++ b/jock/cli.py @@ -1,7 +1,7 @@ import click from jock import __version__ -from jock.config import load_repositories +from jock.config import get_selected_repositories from jock.git import git_command CONFIG_REPOSITORIES = 'config_repositories' @@ -17,102 +17,110 @@ 'you wish to run commands on. Multiple ' 'repositories can be specified using ' 'multiple flags.') +@click.option('--group', '-g', type=str, multiple=True, + help='Group of repositories, specified in ' + '~/.jockrc, you wish to run commands on.' + 'Multiple groups can be specified using ' + 'multiple flags.') @click.pass_context -def main(ctx, repository): +def main(ctx, repository, group): ctx.ensure_object(dict) - ctx.obj[CONFIG_REPOSITORIES] = load_repositories() - ctx.obj[SELECTED_REPOSITORIES] = tuple(map(lambda x: x.lstrip(" ="), repository)) + + repositories = tuple(map(lambda x: x.lstrip(" ="), repository)) + groups = tuple(map(lambda x: x.lstrip(" ="), group)) + + ctx.obj[SELECTED_REPOSITORIES] = get_selected_repositories(repositories, groups) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def clone(ctx, git_args): - git_command('clone', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('clone', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def add(ctx, git_args): - git_command('add', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('add', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def restore(ctx, git_args): - git_command('restore', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('restore', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def rm(ctx, git_args): - git_command('rm', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('rm', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def branch(ctx, git_args): - git_command('branch', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('branch', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def commit(ctx, git_args): - git_command('commit', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('commit', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def reset(ctx, git_args): - git_command('reset', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('reset', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def switch(ctx, git_args): - git_command('switch', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('switch', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def tag(ctx, git_args): - git_command('tag', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('tag', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def fetch(ctx, git_args): - git_command('fetch', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('fetch', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def pull(ctx, git_args): - git_command('pull', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('pull', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def push(ctx, git_args): - git_command('push', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('push', ctx.obj[SELECTED_REPOSITORIES], git_args) @main.command(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.argument(GIT_ARGS, nargs=-1, required=False) def checkout(ctx, git_args): - git_command('checkout', ctx.obj[CONFIG_REPOSITORIES], ctx.obj[SELECTED_REPOSITORIES], git_args) + git_command('checkout', ctx.obj[SELECTED_REPOSITORIES], git_args) if __name__ == '__main__': diff --git a/jock/config.py b/jock/config.py index a7427a5..21fbb23 100644 --- a/jock/config.py +++ b/jock/config.py @@ -9,7 +9,23 @@ from yaml import Loader, Dumper -def load_repositories(): +def load_config(): with open(os.path.expanduser('~/.jockrc'), 'r') as file: - config = yaml.load(file, Loader=Loader) - return config['repositories'] + return yaml.load(file, Loader=Loader) + + +def get_selected_repositories(selected_repositories, selected_groups): + config = load_config() + + repositories = dict({}) + config_repositories = config['repositories'] + for repo_name in selected_repositories: + repositories[repo_name] = config_repositories[repo_name] + + if config.get('groups') is not None: + config_groups = config['groups'] + for group_name in selected_groups: + for repo_name in config_groups[group_name]['repositories']: + repositories[repo_name] = config_repositories[repo_name] + + return repositories diff --git a/jock/git.py b/jock/git.py index 1f7c733..e202115 100644 --- a/jock/git.py +++ b/jock/git.py @@ -6,25 +6,26 @@ from jock.utils import get_repository_path -def git_common(command, config_repositories, selected_repositories, git_args=()): +def git_common(command, selected_repositories, git_args=()): for repository_name in selected_repositories: - repository_path = get_repository_path(config_repositories[repository_name]['location']) + repository = selected_repositories[repository_name] + repository_path = get_repository_path(repository['location']) click.echo('Executing [{}] in [{}]'.format(command, repository_path)) subprocess.run(('git', '-C', repository_path, command) + git_args) -def git_clone(config_repositories, selected_repositories, git_args=()): +def git_clone(selected_repositories, git_args=()): for repository_name in selected_repositories: - config_repository = config_repositories[repository_name] - repository_path = get_repository_path(config_repository['location']) + repository = selected_repositories[repository_name] + repository_path = get_repository_path(repository['location']) click.echo( 'Cloning [{}] in [{}]'.format(repository_name, repository_path) ) - subprocess.run(('git', 'clone', config_repository['address'], repository_path) + git_args) + subprocess.run(('git', 'clone', repository['address'], repository_path) + git_args) GIT_COMMANDS = { - 'clone': lambda _, cr, sr, a: git_clone(cr, sr, a), + 'clone': lambda _, sr, a: git_clone(sr, a), 'add': git_common, 'restore': git_common, 'rm': git_common, @@ -40,11 +41,11 @@ def git_clone(config_repositories, selected_repositories, git_args=()): } -def git_command(command, config_repositories, selected_repositories, git_args): - release_func = GIT_COMMANDS.get(command) +def git_command(command, selected_repositories, git_args): + git_func = GIT_COMMANDS.get(command) - if release_func is None: + if git_func is None: print('Unsupported command ' + command) sys.exit(1) - release_func(command, config_repositories, selected_repositories, git_args) + git_func(command, selected_repositories, git_args) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index ec3d8ae..e89761d 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -12,11 +12,11 @@ class TestCLI(TestCase): def setUp(self): self.runner = CliRunner() - @patch('jock.cli.load_repositories') + @patch('jock.cli.get_selected_repositories') @patch('jock.cli.git_command') - def test_git_commands(self, git_command, load_repositories_mock): + def test_git_commands(self, git_command, get_selected_repositories_mock): # Given - load_repositories_mock.return_value = CONFIG_REPOSITORIES + get_selected_repositories_mock.return_value = CONFIG_REPOSITORIES commands = [ 'clone', 'add', @@ -35,19 +35,19 @@ def test_git_commands(self, git_command, load_repositories_mock): args = ('-a', '--woof') expected_calls = [ - call(commands[0], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[1], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[2], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[3], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[4], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[5], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[6], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[7], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[8], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[9], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[10], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[11], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), - call(commands[12], CONFIG_REPOSITORIES, REPOSITORY_NAMES, args), + call(commands[0], CONFIG_REPOSITORIES, args), + call(commands[1], CONFIG_REPOSITORIES, args), + call(commands[2], CONFIG_REPOSITORIES, args), + call(commands[3], CONFIG_REPOSITORIES, args), + call(commands[4], CONFIG_REPOSITORIES, args), + call(commands[5], CONFIG_REPOSITORIES, args), + call(commands[6], CONFIG_REPOSITORIES, args), + call(commands[7], CONFIG_REPOSITORIES, args), + call(commands[8], CONFIG_REPOSITORIES, args), + call(commands[9], CONFIG_REPOSITORIES, args), + call(commands[10], CONFIG_REPOSITORIES, args), + call(commands[11], CONFIG_REPOSITORIES, args), + call(commands[12], CONFIG_REPOSITORIES, args), ] flagged_repositories = \ map_list_with_repository_flag(REPOSITORY_NAMES) @@ -59,11 +59,11 @@ def test_git_commands(self, git_command, load_repositories_mock): git_command.assert_has_calls(expected_calls) self.assertEqual(git_command.call_count, len(expected_calls)) - @patch('jock.cli.load_repositories') + @patch('jock.cli.get_selected_repositories') @patch('jock.cli.git_command') - def test_all_repository_flags_work(self, git_mock, load_repositories_mock): + def test_all_repository_flags_work(self, git_mock, get_selected_repositories_mock): # Given - load_repositories_mock.return_value = CONFIG_REPOSITORIES + get_selected_repositories_mock.return_value = CONFIG_REPOSITORIES flagged_repository_names = ( '--repository=' + REPOSITORY_NAMES[0], '-r ' + REPOSITORY_NAMES[1], @@ -72,4 +72,4 @@ def test_all_repository_flags_work(self, git_mock, load_repositories_mock): # When self.runner.invoke(main, flagged_repository_names + ('clone',)) # Then - git_mock.assert_called_once_with('clone', CONFIG_REPOSITORIES, REPOSITORY_NAMES, ()) + git_mock.assert_called_once_with('clone', CONFIG_REPOSITORIES, ()) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 31bb1f3..8f6f3a7 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -4,24 +4,83 @@ import yaml -from jock.config import load_repositories -from tests.utils import CONFIG_REPOSITORIES - -CONFIG = dict({'repositories': CONFIG_REPOSITORIES}) +from jock.config import load_config, get_selected_repositories +from tests.utils import CONFIG_REPOSITORIES, REPOSITORY_NAMES, GROUP_NAMES, CONFIG_GROUPS open_name = '%s.open' % __name__ class TestConfig(TestCase): - @patch("builtins.open", read_data="data") + @patch('builtins.open', read_data='data') @patch.object(yaml, 'load') @patch.object(os.path, 'expanduser') - def test_load_repositories(self, mock_expanduser, mock_yaml, mock_open): + def test_load_config(self, mock_expanduser, mock_yaml, mock_open): # Given expected_rc_path = '~/.jockrc' - mock_expanduser.return_value = '/some/path' - mock_yaml.return_value = CONFIG + expected_expanded_path = '/some/path' + expected_config = dict({'repositories': CONFIG_REPOSITORIES}) + mock_expanduser.return_value = expected_expanded_path + mock_yaml.return_value = expected_config + # When + actual_config = load_config() + # Then + mock_expanduser.assert_called_once_with(expected_rc_path) + mock_open.assert_called_once_with(expected_expanded_path, 'r') + mock_yaml.assert_called_once() + self.assertEqual(expected_config, actual_config) + + @patch('jock.config.load_config') + def test_get_selected_repositories_returns_selected_repos(self, mock_load_config): + # Given + selected_repositories = (REPOSITORY_NAMES[0], REPOSITORY_NAMES[2]) + mock_load_config.return_value = dict({ + 'repositories': CONFIG_REPOSITORIES, + 'groups': CONFIG_REPOSITORIES + }) + expected_repositories = dict({ + REPOSITORY_NAMES[0]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[0]], + REPOSITORY_NAMES[2]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[2]] + }) + # When + actual_repositories = get_selected_repositories(selected_repositories, tuple()) + # Then + mock_load_config.assert_called_once() + self.assertEqual(expected_repositories, actual_repositories) + + @patch('jock.config.load_config') + def test_get_selected_repositories_returns_selected_groups(self, mock_load_config): + # Given + selected_groups = (GROUP_NAMES[1], GROUP_NAMES[2]) + mock_load_config.return_value = dict({ + 'repositories': CONFIG_REPOSITORIES, + 'groups': CONFIG_GROUPS + }) + expected_repositories = dict({ + REPOSITORY_NAMES[0]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[0]], + REPOSITORY_NAMES[2]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[2]] + }) + # When + actual_repositories = get_selected_repositories(tuple(), selected_groups) + # Then + mock_load_config.assert_called_once() + self.assertEqual(expected_repositories, actual_repositories) + + @patch('jock.config.load_config') + def test_get_selected_repositories_returns_both_selections(self, mock_load_config): + # Given + selected_repositories = (REPOSITORY_NAMES[1],) + selected_groups = (GROUP_NAMES[1], GROUP_NAMES[2]) + mock_load_config.return_value = dict({ + 'repositories': CONFIG_REPOSITORIES, + 'groups': CONFIG_GROUPS + }) + expected_repositories = dict({ + REPOSITORY_NAMES[0]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[0]], + REPOSITORY_NAMES[1]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[1]], + REPOSITORY_NAMES[2]: CONFIG_REPOSITORIES[REPOSITORY_NAMES[2]] + }) # When - actual_repositories = load_repositories() + actual_repositories = get_selected_repositories(selected_repositories, selected_groups) # Then - self.assertEqual(CONFIG_REPOSITORIES, actual_repositories) + mock_load_config.assert_called_once() + self.assertEqual(expected_repositories, actual_repositories) diff --git a/tests/unit/test_git.py b/tests/unit/test_git.py index 99c3339..10137c9 100644 --- a/tests/unit/test_git.py +++ b/tests/unit/test_git.py @@ -31,7 +31,7 @@ def test_clone_clones_all(self, mock_run, mock_get_repository_path): self._get_clone_call(REPOSITORY_NAMES[2], args) ] # When - git_command('clone', CONFIG_REPOSITORIES, REPOSITORY_NAMES, args) + git_command('clone', CONFIG_REPOSITORIES, args) # Then mock_run.assert_has_calls(expected_calls) self.assertEqual(mock_run.call_count, len(REPOSITORY_NAMES)) @@ -53,7 +53,7 @@ def test_common_call(self, mock_run, mock_get_repository_path): self._get_common_call(REPOSITORY_NAMES[2], command, args) ] # When - git_common(command, CONFIG_REPOSITORIES, REPOSITORY_NAMES, args) + git_common(command, CONFIG_REPOSITORIES, args) # Then mock_run.assert_has_calls(expected_calls) self.assertEqual(mock_run.call_count, len(REPOSITORY_NAMES)) @@ -79,6 +79,7 @@ def test_git_command(self, mock_run, mock_get_repository_path): ] args = ('-a', '--woof') repository_name = REPOSITORY_NAMES[0] + selected_repositories = dict({repository_name: CONFIG_REPOSITORIES[repository_name]}) expected_calls = [ self._get_common_call(repository_name, common_commands[0], args), self._get_common_call(repository_name, common_commands[1], args), @@ -96,8 +97,8 @@ def test_git_command(self, mock_run, mock_get_repository_path): ] # When for command in common_commands: - git_command(command, CONFIG_REPOSITORIES, (repository_name,), args) - git_command('clone', CONFIG_REPOSITORIES, (repository_name,), args) + git_command(command, selected_repositories, args) + git_command('clone', selected_repositories, args) # Then mock_run.assert_has_calls(expected_calls) self.assertEqual(mock_run.call_count, len(expected_calls)) @@ -108,6 +109,6 @@ def test_git_command_exits(self): expected_exit_code = 1 # When with pytest.raises(SystemExit) as wrapped_exit: - git_command(unknown_command, CONFIG_REPOSITORIES, ('repository',), ()) + git_command(unknown_command, CONFIG_REPOSITORIES, ()) # Then self.assertEqual(expected_exit_code, wrapped_exit.value.code) diff --git a/tests/utils.py b/tests/utils.py index db54448..8e10a23 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,6 +15,12 @@ def map_list_with_repository_flag(repositories): 'repo3', ) +GROUP_NAMES = ( + 'oneandtwo', + 'oneandthree', + 'three', +) + CONFIG_REPOSITORIES = dict({ REPOSITORY_NAMES[0]: dict({ 'address': 'git@github.com:some-owner/repo-1.git', @@ -29,3 +35,9 @@ def map_list_with_repository_flag(repositories): 'location': '/home/jock/git/repo3', }), }) + +CONFIG_GROUPS = dict({ + GROUP_NAMES[0]: {'repositories': [REPOSITORY_NAMES[0], REPOSITORY_NAMES[1]]}, + GROUP_NAMES[1]: {'repositories': [REPOSITORY_NAMES[0], REPOSITORY_NAMES[2]]}, + GROUP_NAMES[2]: {'repositories': [REPOSITORY_NAMES[2]]}, +})