diff --git a/.gitignore b/.gitignore index 5ed4e3d..5498b46 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /dist/ /.DS_Store /build/ +/cover/ *.pyc venv* *.svn @@ -15,4 +16,5 @@ venv* .tox /pip-selfcheck.json /man +.mypy_cache .python-version diff --git a/svn/common.py b/svn/common.py index a53e2f2..9c4f334 100644 --- a/svn/common.py +++ b/svn/common.py @@ -19,6 +19,20 @@ _HUNK_HEADER_LINE_NUMBERS_PREFIX = '@@ ' +def get_depth_options(depth, is_set_depth=False): + """Get options for depth and check (is_set_depth=True for --set-depth)""" + depth_values = {"empty", "files", "immediates", "infinity"} + if is_set_depth: + depth_values = depth_values.union({"exclude"}) + + if depth not in depth_values: + raise svn.exception.SvnException( + "Invalid depth '{d}' (values allowed: {v})!".format(d=depth, v=depth_values) + ) + + return ["--set-depth" if is_set_depth else "--depth", depth] + + class CommonClient(svn.common_base.CommonBase): def __init__(self, url_or_path, type_, username=None, password=None, svn_filepath='svn', trust_cert=None, env={}, *args, **kwargs): @@ -61,7 +75,7 @@ def __element_text(self, element): return None - def info(self, rel_path=None, revision=None): + def info(self, rel_path=None, revision=None, include_ext=False): cmd = [] if revision is not None: cmd += ['-r', str(revision)] @@ -70,6 +84,8 @@ def info(self, rel_path=None, revision=None): if rel_path is not None: full_url_or_path += '/' + rel_path cmd += ['--xml', full_url_or_path] + if include_ext: + cmd += ["--include-externals"] result = self.run_command( 'info', @@ -79,7 +95,8 @@ def info(self, rel_path=None, revision=None): root = xml.etree.ElementTree.fromstring(result) entry_attr = root.find('entry').attrib - commit_attr = root.find('entry/commit').attrib + commit_tag = root.find('entry/commit') + commit_attr = commit_tag.attrib if commit_tag else None relative_url = root.find('entry/relative-url') author = root.find('entry/commit/author') @@ -97,20 +114,22 @@ def info(self, rel_path=None, revision=None): 'entry#kind': entry_attr['kind'], 'entry#path': entry_attr['path'], - 'entry#revision': int(entry_attr['revision']), 'repository/root': root.find('entry/repository/root').text, 'repository/uuid': root.find('entry/repository/uuid').text, 'wc-info/wcroot-abspath': self.__element_text(wcroot_abspath), 'wc-info/schedule': self.__element_text(wcinfo_schedule), - 'wc-info/depth': self.__element_text(wcinfo_depth), - 'commit/author': self.__element_text(author), - - 'commit/date': dateutil.parser.parse( - root.find('entry/commit/date').text), - 'commit#revision': int(commit_attr['revision']), + 'wc-info/depth': self.__element_text(wcinfo_depth) } + if commit_attr: + info.update({ + 'entry#revision': int(entry_attr['revision']), + 'commit/author': self.__element_text(author), + 'commit/date': dateutil.parser.parse( + root.find('entry/commit/date').text), + 'commit#revision': int(commit_attr['revision']), + }) # Set some more intuitive keys, because no one likes dealing with # symbols. However, we retain the old ones to maintain backwards- @@ -121,15 +140,17 @@ def info(self, rel_path=None, revision=None): info['entry_kind'] = info['entry#kind'] info['entry_path'] = info['entry#path'] - info['entry_revision'] = info['entry#revision'] info['repository_root'] = info['repository/root'] info['repository_uuid'] = info['repository/uuid'] info['wcinfo_wcroot_abspath'] = info['wc-info/wcroot-abspath'] info['wcinfo_schedule'] = info['wc-info/schedule'] info['wcinfo_depth'] = info['wc-info/depth'] - info['commit_author'] = info['commit/author'] - info['commit_date'] = info['commit/date'] - info['commit_revision'] = info['commit#revision'] + + if commit_attr: + info['entry_revision'] = info['entry#revision'] + info['commit_author'] = info['commit/author'] + info['commit_date'] = info['commit/date'] + info['commit_revision'] = info['commit#revision'] return info @@ -285,15 +306,19 @@ def export(self, to_path, revision=None, force=False): self.run_command('export', cmd) - def list(self, extended=False, rel_path=None): + def list(self, extended=False, rel_path=None, depth=None, include_ext=False): full_url_or_path = self.__url_or_path if rel_path is not None: full_url_or_path += '/' + rel_path + cmd = [full_url_or_path] + if depth: + cmd += get_depth_options(depth) + if include_ext: + cmd += ["--include-externals"] + if extended is False: - for line in self.run_command( - 'ls', - [full_url_or_path]): + for line in self.run_command('ls', cmd): line = line.strip() if line: yield line @@ -301,7 +326,7 @@ def list(self, extended=False, rel_path=None): else: raw = self.run_command( 'ls', - ['--xml', full_url_or_path], + ['--xml'] + cmd, do_combine=True) root = xml.etree.ElementTree.fromstring(raw) diff --git a/svn/local.py b/svn/local.py index 686063b..c3bbfbe 100644 --- a/svn/local.py +++ b/svn/local.py @@ -29,29 +29,54 @@ def __init__(self, path_, *args, **kwargs): def __repr__(self): return '' % self.path - def add(self, rel_path, do_include_parents=False): + def add(self, rel_path, do_include_parents=False, depth=None): args = [rel_path] - if do_include_parents is True: + if do_include_parents: args.append('--parents') + if depth: + args += svn.common.get_depth_options(depth) self.run_command( 'add', args, wd=self.path) - def commit(self, message, rel_filepaths=[]): - args = ['-m', message] + rel_filepaths + def commit(self, message, rel_filepaths=None, depth=None, include_ext=False): + args = ['-m', message] + if depth: + args += svn.common.get_depth_options(depth) + if include_ext: + args += ["--include-externals"] + if rel_filepaths: + args += rel_filepaths output = self.run_command( 'commit', args, wd=self.path) - def update(self, rel_filepaths=[], revision=None): + def update( + self, + rel_filepaths=[], + revision=None, + force=False, + depth=None, + set_depth=None, + ignore_ext=False, + ): cmd = [] if revision is not None: - cmd += ['-r', str(revision)] + cmd += ["-r", str(revision)] + if force: + cmd += ["--force"] + if depth: + cmd += svn.common.get_depth_options(depth) + if set_depth: + cmd += svn.common.get_depth_options(set_depth, is_set_depth=True) + if ignore_ext: + cmd += ["--ignore-externals"] + cmd += rel_filepaths self.run_command( 'update', diff --git a/svn/remote.py b/svn/remote.py index c55bf96..f45e6df 100644 --- a/svn/remote.py +++ b/svn/remote.py @@ -10,10 +10,16 @@ def __init__(self, url, *args, **kwargs): svn.constants.LT_URL, *args, **kwargs) - def checkout(self, path, revision=None): + def checkout(self, path, revision=None, force=False, depth=None, ignore_ext=False): cmd = [] if revision is not None: cmd += ['-r', str(revision)] + if force: + cmd += ["--force"] + if depth: + cmd += svn.common.get_depth_options(depth) + if ignore_ext: + cmd += ["--ignore-externals"] cmd += [self.url, path] diff --git a/tests/test_common.py b/tests/test_common.py index ba74bec..e8442e1 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -23,7 +23,14 @@ def test_update(self): lc.update() self.assertEqual(3, lc.info()['commit_revision']) - lc.update(revision=1) + lc.update(set_depth="files") + self.assertEqual("files", lc.info()['wcinfo_depth']) + + lc.update(depth="empty") # depth is not changed + self.assertEqual("files", lc.info()['wcinfo_depth']) + + lc.update(revision=1, ignore_ext=True) + # TODO: ignore_ext not really tested self.assertEqual(1, lc.info()['commit_revision']) def test_diff_summary(self): @@ -114,9 +121,6 @@ def test_list(self): with svn.test_support.temp_common() as (_, _, cc): svn.test_support.populate_bigger_file_changes1() - entries = cc.list() - entries = sorted(entries) - expected = [ 'committed_changed', 'committed_deleted', @@ -124,7 +128,15 @@ def test_list(self): 'new_file', ] - self.assertEqual(entries, expected) + entries = cc.list() + self.assertListEqual(sorted(entries), expected) + + empty_entries = cc.list(depth="empty") + self.assertListEqual(list(empty_entries), []) + + entries = cc.list(include_ext=True) + self.assertListEqual(sorted(entries), expected) + # TODO: include_ext/--include-externals not really tested def test_info(self): with svn.test_support.temp_common() as (repo_path, _, cc): @@ -132,19 +144,17 @@ def test_info(self): info = cc.info() - self.assertEqual( - info['entry_path'], - '.') - + self.assertEqual(info['entry_path'], '.') uri = 'file://{}'.format(repo_path) + self.assertEqual(info['repository_root'], uri) + self.assertEqual(info['entry_kind'], 'dir') - self.assertEqual( - info['repository_root'], - uri) + info = cc.info(revision=1) + self.assertEqual(info['commit_revision'], 1) - self.assertEqual( - info['entry#kind'], - 'dir') + info = cc.info(include_ext=True) + self.assertIsNotNone(info) + # TODO: include_ext/--include-externals not really tested def test_info_revision(self): with svn.test_support.temp_common() as (_, working_path, cc): @@ -161,9 +171,23 @@ def test_info_revision(self): info1 = cc.info(revision=1) self.assertEquals(info1['commit_revision'], 1) - info2 = cc.info(revision=2) + info2 = cc.info(".", revision=2) self.assertEquals(info2['commit_revision'], 2) + rel_filepath_not_committed = "to_be_added" + with open(rel_filepath_not_committed, 'w') as _: + pass + lc.add(rel_filepath_not_committed) + + # Get information on file not yet committed to SVN + info3 = cc.info(rel_filepath_not_committed) + self.assertEquals(info3['wcinfo_schedule'], "add") + self.assertEqual(info3['entry_kind'], 'file') + self.assertNotIn('entry_revision', info3) + for attr in {"date", "revision", "author"}: + self.assertNotIn('commit_' + attr, info3) + + def test_log(self): with svn.test_support.temp_common() as (_, _, cc): svn.test_support.populate_bigger_file_changes1() diff --git a/tests/test_local.py b/tests/test_local.py index 445f4b8..9df78cf 100644 --- a/tests/test_local.py +++ b/tests/test_local.py @@ -1,4 +1,6 @@ import os +from shutil import rmtree +from svn.exception import SvnException import unittest import svn.constants @@ -87,3 +89,84 @@ def test_cleanup(self): with svn.test_support.temp_repo(): with svn.test_support.temp_checkout() as (_, lc): lc.cleanup() + + def test_add(self): + with svn.test_support.temp_repo(): + with svn.test_support.temp_checkout() as (_, lc): + dir1 = "d1" + dir12 = os.path.join(dir1, "d2") + dir123 = os.path.join(dir12, "d3") + dir124 = os.path.join(dir12, "d4") + for dir_ in [dir1, dir12, dir123, dir124]: + if os.path.isdir(dir_): + rmtree(dir_) + os.mkdir(dir_) + + added = "added" + f12 = os.path.join(dir12, "f12") + f123_1 = os.path.join(dir123, "f123.1") + f123_2 = os.path.join(dir123, "f123.2") + f124 = os.path.join(dir124, "f124") + for file in [added, f12, f123_1, f123_2, f124]: + with open(file, 'w') as f: + pass + + lc.add(dir1, depth="empty") + self.assertRaises(SvnException, lc.info, dir12) + lc.run_command("revert", [dir1]) + + lc.add(added) + info = lc.info(added) + self.assertEqual(info["wcinfo_schedule"], "add") + + lc.add(f124, do_include_parents=True) + for path in [f124, dir124]: + info = lc.info(path) + self.assertEqual(info["wcinfo_schedule"], "add") + + lc.add(dir123, depth="infinity") + for path in [f123_1, f123_2]: + info = lc.info(path) + self.assertEqual(info["wcinfo_schedule"], "add") + + + + def test_commit(self): + with svn.test_support.temp_repo(): + with svn.test_support.temp_checkout() as (_, lc): + svn.test_support.populate_bigger_file_changes1() + + rel_dirpath = "dir_to_be_added" + if os.path.isdir(rel_dirpath): + rmtree(rel_dirpath) + os.mkdir(rel_dirpath) + lc.add(rel_dirpath) + + rel_filepath_added = os.path.join(rel_dirpath, "added") + with open(rel_filepath_added, 'w') as f: + pass + lc.add(rel_filepath_added) + + rel_filepath_committed = "committed" + with open(rel_filepath_committed, 'w') as f: + pass + lc.add(rel_filepath_committed) + + lc.commit("empty commit", ["."], depth="empty") + info = lc.info(rel_filepath_committed) + info_in_dir = lc.info(rel_filepath_added) + self.assertEqual(info["wcinfo_schedule"], "add") + self.assertEqual(info_in_dir["wcinfo_schedule"], "add") + + lc.commit("commit files", depth="files") + info = lc.info(rel_filepath_committed) + info_in_dir = lc.info(rel_filepath_added) + self.assertEqual(info["wcinfo_schedule"], "normal") + self.assertEqual(info_in_dir["wcinfo_schedule"], "add") + + lc.commit("commit all", depth="infinity") + info_in_dir = lc.info(rel_filepath_added) + self.assertEqual(info_in_dir["wcinfo_schedule"], "normal") + + lc.commit("commit external", include_ext=True) + # TODO: include_ext/--include-externals not really tested diff --git a/tests/test_remote.py b/tests/test_remote.py index 5b83c85..9d3eec9 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,3 +1,4 @@ +import glob import os import shutil import unittest @@ -27,6 +28,10 @@ def tearDown(self): shutil.rmtree('trial') def test_error_client_formation(self): + """ + Testing checkout of incorrect URL + :return: + """ try: svn.remote.RemoteClient(self.test_fake_url).checkout('.') except svn.exception.SvnException: @@ -42,7 +47,47 @@ def test_checkout(self): svn.remote.RemoteClient(self.test_svn_url).checkout('trial') self.assertTrue(os.path.exists('trial')) + def test_error_checkout_depth(self): + """ + Testing checkout with incorrect argument for option depth + :return: + """ + repo = "trial_checkout_opt_err" + try: + svn.remote.RemoteClient(self.test_svn_url).checkout(repo, depth="toto") + except svn.exception.SvnException: + self.assertFalse(os.path.exists(repo)) + else: + if os.path.exists(repo): + shutil.rmtree(repo) + raise Exception("Expected exception for bad URL.") + + def test_checkout_options(self): + """ + Testing options of checkout + :return: + """ + repo = "trial_checkout_opt" + rc = svn.remote.RemoteClient(self.test_svn_url) + + rc.checkout(repo, force=True) + self.assertTrue(os.path.exists(repo)) + shutil.rmtree(repo) + + rc.checkout(repo, depth="empty") + self.assertTrue(os.path.exists(repo)) + self.assertListEqual(glob.glob(repo + "/*.*"), []) + shutil.rmtree(repo) + + rc.checkout(repo, ignore_ext=True) + self.assertTrue(os.path.exists(repo)) + shutil.rmtree(repo) + def test_remove(self): + """ + Testing remove + :return: + """ with svn.test_support.temp_repo() as (_, rc): with svn.test_support.temp_checkout(): svn.test_support.populate_bigger_file_changes1()