Repository: conan-io/conan

Issue:
[scm] --show-item not supported on older versions of subversion
Thank you to @jgsogo for implementing SVN for the scm attribute.

I've finally got the chance to try this new feature and discovered that my version of subversion (1.7.14 as supplied in RHEL 7) does not support --show-item for the svn info command. Unfortunately, this means I can't use this feature :sob:

Can this be changed to something like:

`svn info <other args> --xml`

And the XML parsed? Bit like is suggested in #3653


Discussion:
Ohhh... anyway, I will work on the XML parsing as it will be available for every SVN version 🤞. I'm tagging it for v1.9.

Resolving patch:
diff --git a/conans/client/tools/scm.py b/conans/client/tools/scm.py
--- a/conans/client/tools/scm.py
+++ b/conans/client/tools/scm.py
@@ -7,6 +7,7 @@
 from six.moves.urllib.parse import urlparse, quote_plus, unquote
 from subprocess import CalledProcessError, PIPE, STDOUT
 import platform
+import xml.etree.ElementTree as ET
 
 from conans.client.tools.env import no_op, environment_append
 from conans.client.tools.files import chdir
@@ -169,7 +170,7 @@ def _check_git_repo(self):
 class SVN(SCMBase):
     cmd_command = "svn"
     file_protocol = 'file:///' if platform.system() == "Windows" else 'file://'
-    API_CHANGE_VERSION = Version("1.10")  # CLI changes in 1.9.x
+    API_CHANGE_VERSION = Version("1.8")  # CLI changes in 1.8
 
     def __init__(self, folder=None, runner=None, *args, **kwargs):
         def runner_no_strip(command):
@@ -204,6 +205,29 @@ def run(self, command):
                 extra_options += " --trust-server-cert"
         return super(SVN, self).run(command="{} {}".format(command, extra_options))
 
+    def _show_item(self, item, target='.'):
+        if self.version >= SVN.API_CHANGE_VERSION:
+            value = self.run("info --show-item {item} \"{target}\"".format(item=item, target=target))
+            return value.strip()
+        else:
+            output = self.run("info --xml \"{target}\"".format(target=target))
+            root = ET.fromstring(output)
+            if item == 'revision':
+                return root.findall("./entry")[0].get("revision")
+            elif item == 'url':
+                return root.findall("./entry/url")[0].text
+            elif item == 'wc-root':
+                return root.findall("./entry/wc-info/wcroot-abspath")[0].text
+            elif item == 'last-changed-revision':
+                return root.findall("./entry/commit")[0].get("revision")
+            elif item == 'relative-url':
+                root_url = root.findall("./entry/repository/root")[0].text
+                url = self._show_item(item='url', target=target)
+                if url.startswith(root_url):
+                    return url[len(root_url):]
+            raise ConanException("Retrieval of item '{}' not implemented for SVN<{}".format(
+                item, SVN.API_CHANGE_VERSION))
+
     def checkout(self, url, revision="HEAD"):
         output = ""
         try:
@@ -233,7 +257,7 @@ def excluded_files(self):
         return excluded_list
 
     def get_remote_url(self):
-        return self.run("info --show-item url").strip()
+        return self._show_item('url')
 
     def get_qualified_remote_url(self):
         # Return url with peg revision
@@ -248,32 +272,38 @@ def is_local_repository(self):
 
     def is_pristine(self):
         # Check if working copy is pristine/consistent
-        output = self.run("status -u -r {}".format(self.get_revision()))
-        offending_columns = [0, 1, 2, 3, 4, 6, 7, 8]  # 5th column informs if the file is locked (7th is always blank)
+        if self.version >= SVN.API_CHANGE_VERSION:
+            output = self.run("status -u -r {}".format(self.get_revision()))
+            offending_columns = [0, 1, 2, 3, 4, 6, 7, 8]  # 5th column informs if the file is locked (7th is always blank)
 
-        for item in output.splitlines()[:-1]:
-            if item[0] == '?':  # Untracked file
-                continue
-            if any(item[i] != ' ' for i in offending_columns):
-                return False
+            for item in output.splitlines()[:-1]:
+                if item[0] == '?':  # Untracked file
+                    continue
+                if any(item[i] != ' ' for i in offending_columns):
+                    return False
 
-        return True
+            return True
+        else:
+            import warnings
+            warnings.warn("SVN::is_pristine for SVN v{} (less than {}) is not implemented, it is"
+                          " returning not-pristine always because it cannot compare with"
+                          " checked out version.".format(self.version, SVN.API_CHANGE_VERSION))
+            return False
 
     def get_revision(self):
-        return self.run("info --show-item revision").strip()
+        return self._show_item('revision')
 
     def get_repo_root(self):
-        return self.run("info --show-item wc-root").strip()
+        return self._show_item('wc-root')
 
     def get_last_changed_revision(self, use_wc_root=True):
         if use_wc_root:
-            return self.run('info "{root}" --show-item last-changed-revision'.format(
-                root=self.get_repo_root())).strip()
+            return self._show_item(item='last-changed-revision', target=self.get_repo_root())
         else:
-            return self.run("info --show-item last-changed-revision").strip()
+            return self._show_item(item='last-changed-revision')
 
     def get_branch(self):
-        url = self.run("info --show-item relative-url").strip()
+        url = self._show_item('relative-url')
         try:
             pattern = "(tags|branches)/[^/]+|trunk"
             branch = re.search(pattern, url)


Test patch:
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -14,6 +14,7 @@
 from six.moves.urllib.parse import unquote
 
 from conans.client.client_cache import CONAN_CONF
+from conans.model.version import Version
 
 from conans import tools
 from conans.client.conan_api import ConanAPIV1
@@ -1565,7 +1566,6 @@ def build(self):
 
 
 class SVNToolTestsBasic(SVNLocalRepoTestCase):
-
     def test_clone(self):
         project_url, _ = self.create_project(files={'myfile': "contents"})
         tmp_folder = self.gimme_tmp()
@@ -1624,8 +1624,9 @@ def test_clone_over_dirty_directory(self):
 
         self.assertFalse(svn.is_pristine())
         svn.checkout(url=project_url)  # SVN::clone over a dirty repo reverts all changes (but it doesn't delete non versioned files)
-        self.assertTrue(svn.is_pristine())
-        # self.assertFalse(os.path.exists(new_file))
+        if svn.version >= SVN.API_CHANGE_VERSION:  # SVN::is_pristine returns always False.
+            self.assertTrue(svn.is_pristine())
+            # self.assertFalse(os.path.exists(new_file))
 
     def test_excluded_files(self):
         project_url, _ = self.create_project(files={'myfile': "contents"})
@@ -1677,7 +1678,7 @@ def __call__(self, command, *args, **kwargs):
         svn = SVN(folder=self.gimme_tmp(), username="peter", password="otool", verify_ssl=False)
         runner = MyRunner(svn)
         svn.checkout(url=project_url)
-        if SVN.get_version() >= SVN.API_CHANGE_VERSION:
+        if svn.version >= SVN.API_CHANGE_VERSION:
             self.assertIn("--trust-server-cert-failures=unknown-ca", runner.calls[1])
         else:
             self.assertIn("--trust-server-cert", runner.calls[1])
@@ -1760,6 +1761,21 @@ def test_branch(self):
         self.assertEqual("tags/v12.3.4", svn.get_branch())
 
 
+class SVNToolTestsBasicOldVersion(SVNToolTestsBasic):
+    def run(self, *args, **kwargs):
+        try:
+            setattr(SVN, '_version', Version("1.5"))
+            self.assertTrue(SVN().version < SVN.API_CHANGE_VERSION)
+            super(SVNToolTestsBasicOldVersion, self).run(*args, **kwargs)
+        finally:
+            delattr(SVN, '_version')
+            assert SVN().version == SVN.get_version(), \
+                "{} != {}".format(SVN().version, SVN.get_version())
+
+    # Do not add tests to this class, all should be compatible with new version of SVN
+
+
+@unittest.skipUnless(SVN.get_version() >= SVN.API_CHANGE_VERSION, "SVN::is_pristine not implemented")
 class SVNToolTestsPristine(SVNLocalRepoTestCase):
 
     def test_checkout(self):
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -350,7 +350,13 @@ def create_project(self, files, rel_project_path=None, commit_msg='default commi
             with chdir(tmp_project_dir):
                 subprocess.check_output("svn add .", shell=True)
                 subprocess.check_output('svn commit -m "{}"'.format(commit_msg), shell=True)
-                rev = subprocess.check_output("svn info --show-item revision", shell=True).decode().strip()
+                if SVN.get_version() >= SVN.API_CHANGE_VERSION:
+                    rev = subprocess.check_output("svn info --show-item revision", shell=True).decode().strip()
+                else:
+                    import xml.etree.ElementTree as ET
+                    output = subprocess.check_output("svn info --xml", shell=True).decode().strip()
+                    root = ET.fromstring(output)
+                    rev = root.findall("./entry")[0].get("revision")
             project_url = self.repo_url + "/" + quote(rel_project_path.replace("\\", "/"))
             return project_url, rev
         finally: