diff --git a/gita/__main__.py b/gita/__main__.py index 3428c41..fec01b3 100644 --- a/gita/__main__.py +++ b/gita/__main__.py @@ -23,7 +23,13 @@ def f_add(args: argparse.Namespace): - utils.add_repos(args.paths) + repos = utils.get_repos() + utils.add_repos(repos, args.paths) + + +def f_rename(args: argparse.Namespace): + repos = utils.get_repos() + utils.rename_repo(repos, args.repo[0], args.new_name) def f_ll(args: argparse.Namespace): @@ -123,6 +129,17 @@ def main(argv=None): help="remove the chosen repo(s)") p_rm.set_defaults(func=f_rm) + p_rename = subparsers.add_parser('rename', help='rename a repo') + p_rename.add_argument( + 'repo', + nargs=1, + choices=utils.get_repos(), + help="rename the chosen repo") + p_rename.add_argument( + 'new_name', + help="new name") + p_rename.set_defaults(func=f_rename) + ll_doc = f''' status symbols: +: staged changes *: unstaged changes @@ -153,8 +170,8 @@ def main(argv=None): # superman mode p_super = subparsers.add_parser( 'super', - help= - 'superman mode: delegate any git command/alias in specified or all repo(s).\n' + help='superman mode: delegate any git command/alias in specified or ' + 'all repo(s).\n' 'Examples:\n \t gita super myrepo1 commit -am "fix a bug"\n' '\t gita super repo1 repo2 repo3 checkout new-feature') p_super.add_argument( diff --git a/gita/utils.py b/gita/utils.py index a4fb774..5978f51 100644 --- a/gita/utils.py +++ b/gita/utils.py @@ -34,17 +34,27 @@ def get_repos() -> Dict[str, str]: Return a `dict` of repo name to repo absolute path """ path_file = get_path_fname() - paths = [] + data = [] if os.path.isfile(path_file) and os.stat(path_file).st_size > 0: with open(path_file) as f: - paths = f.read().splitlines()[0].split(os.pathsep) - data = ((os.path.basename(os.path.normpath(p)), p) for p in paths - if is_git(p)) + # TODO: read lines one by one + # for line in f: + data = f.read().splitlines() + # The file content is repos separated by : + # For each repo, there are path and repo name separated by , + # If repo name is not provided, the basename of the path is used as name. + # For example, "/a/b/c,xx:/a/b/d:/c/e/f" corresponds to + # {xx: /a/b/c, d: /a/b/d, f: /c/e/f} repos = {} - for name, path in data: + for d in data: + if not d: # blank line + continue + path, name = d.split(',') + if not is_git(path): + continue if name not in repos: repos[name] = path - else: + else: # repo name collision for different paths: include parent path name par_name = os.path.basename(os.path.dirname(path)) repos[os.path.join(par_name, name)] = path return repos @@ -79,22 +89,39 @@ def is_git(path: str) -> bool: return os.path.exists(loc) -def add_repos(new_paths: List[str]): +def rename_repo(repos: Dict[str, str], repo: str, new_name: str): + """ + Write new repo name to file + """ + path = repos[repo] + del repos[repo] + repos[new_name] = path + _write_to_repo_file(repos, 'w') + + +def _write_to_repo_file(repos: Dict[str, str], mode: str): + """ + """ + data = ''.join(f'{path},{name}\n' for name, path in repos.items()) + fname = get_path_fname() + os.makedirs(os.path.dirname(fname), exist_ok=True) + with open(fname, mode) as f: + f.write(data) + + +def add_repos(repos: Dict[str, str], new_paths: List[str]): """ Write new repo paths to file """ - existing_paths = set(get_repos().values()) + existing_paths = set(repos.values()) new_paths = set(os.path.abspath(p) for p in new_paths if is_git(p)) new_paths = new_paths - existing_paths if new_paths: - print(f"Found {len(new_paths)} new repo(s):") - for path in new_paths: - print(path) - existing_paths.update(new_paths) - fname = get_path_fname() - os.makedirs(os.path.dirname(fname), exist_ok=True) - with open(fname, 'w') as f: - f.write(os.pathsep.join(sorted(existing_paths))) + print(f"Found {len(new_paths)} new repo(s).") + new_repos = { + os.path.basename(os.path.normpath(path)): path + for path in new_paths} + _write_to_repo_file(new_repos, 'a+') else: print('No new repos found!') diff --git a/requirements.txt b/requirements.txt index c76a568..3e9e127 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pytest>=4.1.1 +pytest>=4.4.0 pytest-cov>=2.6.1 pytest-xdist>=1.26.0 setuptools>=40.6.3 diff --git a/setup.py b/setup.py index e496ba0..e1ba09d 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='gita', packages=['gita'], - version='0.9.7', + version='0.9.8', license='MIT', description='Manage multiple git repos', long_description=long_description, diff --git a/tests/clash_path_file b/tests/clash_path_file index 1ae1d50..4abbfca 100644 --- a/tests/clash_path_file +++ b/tests/clash_path_file @@ -1 +1,3 @@ -/a/bcd/repo1:/e/fgh/repo2:/root/x/repo1 +/a/bcd/repo1,repo1 +/e/fgh/repo2,repo2 +/root/x/repo1,repo1 diff --git a/tests/mock_path_file b/tests/mock_path_file index 507368f..2a5f9f9 100644 --- a/tests/mock_path_file +++ b/tests/mock_path_file @@ -1 +1,4 @@ -/a/bcd/repo1:/e/fgh/repo2 +/a/bcd/repo1,repo1 +/a/b/c/repo3,xxx +/e/fgh/repo2,repo2 + diff --git a/tests/test_main.py b/tests/test_main.py index a696f17..b64ac99 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -17,7 +17,7 @@ def testLl(self, mock_path_fname, capfd, tmp_path): __main__.main(['add', '.']) out, err = capfd.readouterr() assert err == '' - assert 'Found 1 new repo(s):' in out + assert 'Found 1 new repo(s).' in out # in production this is not needed utils.get_repos.cache_clear() @@ -52,7 +52,7 @@ def testLs(self, monkeypatch, capfd): @pytest.mark.parametrize('path_fname, expected', [ (PATH_FNAME, - "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\n"), + "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nxxx cmaster dsu\x1b[0m msg\n"), (PATH_FNAME_EMPTY, ""), (PATH_FNAME_CLASH, "repo1 cmaster dsu\x1b[0m msg\nrepo2 cmaster dsu\x1b[0m msg\nx/repo1 cmaster dsu\x1b[0m msg\n" @@ -69,6 +69,7 @@ def testWithPathFiles(self, mock_path_fname, _0, _1, _2, _3, path_fname, utils.get_repos.cache_clear() __main__.main(['ll']) out, err = capfd.readouterr() + print(out) assert err == '' assert out == expected diff --git a/tests/test_utils.py b/tests/test_utils.py index 2e759aa..4c4c099 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -29,7 +29,8 @@ def test_describe(test_input, diff_return, expected, monkeypatch): @pytest.mark.parametrize('path_fname, expected', [ (PATH_FNAME, { 'repo1': '/a/bcd/repo1', - 'repo2': '/e/fgh/repo2' + 'repo2': '/e/fgh/repo2', + 'xxx': '/a/b/c/repo3', }), (PATH_FNAME_EMPTY, {}), (PATH_FNAME_CLASH, { @@ -70,22 +71,34 @@ def test_custom_push_cmd(_): @pytest.mark.parametrize( 'path_input, expected', [ - (['/home/some/repo/'], '/home/some/repo:/nos/repo'), # add one new + (['/home/some/repo/'], '/home/some/repo,repo\n'), # add one new (['/home/some/repo1', '/repo2'], - '/home/some/repo1:/nos/repo:/repo2'), # add two new + {'/repo2,repo2\n/home/some/repo1,repo1\n', # add two new + '/home/some/repo1,repo1\n/repo2,repo2\n'}), # add two new (['/home/some/repo1', '/nos/repo'], - '/home/some/repo1:/nos/repo'), # add one old one new + '/home/some/repo1,repo1\n'), # add one old one new ]) @patch('os.makedirs') -@patch('gita.utils.get_repos', return_value={'repo': '/nos/repo'}) @patch('gita.utils.is_git', return_value=True) -def test_add_repos(_0, _1, _2, path_input, expected, monkeypatch): +def test_add_repos(_0, _1, path_input, expected, monkeypatch): monkeypatch.setenv('XDG_CONFIG_HOME', '/config') with patch('builtins.open', mock_open()) as mock_file: - utils.add_repos(path_input) - mock_file.assert_called_with('/config/gita/repo_path', 'w') + utils.add_repos({'repo': '/nos/repo'}, path_input) + mock_file.assert_called_with('/config/gita/repo_path', 'a+') handle = mock_file() - handle.write.assert_called_once_with(expected) + if type(expected) == str: + handle.write.assert_called_once_with(expected) + else: + handle.write.assert_called_once() + args, kwargs = handle.write.call_args + assert args[0] in expected + assert not kwargs + + +@patch('gita.utils._write_to_repo_file') +def test_rename_repo(mock_write): + utils.rename_repo({'r1': '/a/b', 'r2': '/c/c'}, 'r2', 'xxx') + mock_write.assert_called_once_with({'r1': '/a/b', 'xxx': '/c/c'}, 'w') def test_async_output(capfd):