Repository: conan-io/conan

Issue:
None value and default value in options
- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

conan v1.6.1, Windows

I know that the value `None` has a special meening in options. I think the exact meening is not fully documented (see also https://github.com/conan-io/conan/issues/2780#issuecomment-383234689). 

In the following example the behaviour seems inconsistent:

```python
class libaconan(ConanFile):
    name = "liba"
    version = "1.0"
    options = {"opt": [None, "a"]}
    default_options = "opt=a"
    
    def configure(self):
        print("opt =", self.options.opt)
```

- `conan install liba/1.0@user/testing` prints `opt = a`
- `conan install liba/1.0@user/testing -o opt=a` prints `opt = a`
- `conan install liba/1.0@user/testing -o opt=None` prints `opt = None` (!)

But with libb consuming liba:

```python
class libbconan(ConanFile):
    name = "libb"
    version = "1.0"
    requires  = "liba/1.0@user/testing"
```

- `conan install libb/1.0@user/testing` prints `opt = a`.
- `conan install libb/1.0@user/testing -o liba:opt=a` prints `opt = a`.
- `conan install libb/1.0@user/testing -o liba:opt=None` prints `opt = a`. (!)




Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index ee466be1d..aa6c567e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ htmlcov/
 nosetests.xml
 coverage.xml
 *,cover
+.pytest_cache/
 
 # Translations
 *.mo
diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py
index c694053db..88e501ee9 100644
--- a/conans/client/build/cmake.py
+++ b/conans/client/build/cmake.py
@@ -301,6 +301,7 @@ class CMake(object):
                 cmake.install()
                 cmake.patch_config_paths()
         """
+
         if not self._conanfile.should_install:
             return
         if not self._conanfile.name:
@@ -313,18 +314,16 @@ class CMake(object):
             for f in files:
                 if f.endswith(".cmake"):
                     path = os.path.join(root, f)
-                    tools.replace_in_file(path, pf, replstr, strict=False)
+                    tools.replace_path_in_file(path, pf, replstr, strict=False)
 
                     # patch paths of dependent packages that are found in any cmake files of the
                     # current package
-                    path_content = tools.load(path)
                     for dep in self._conanfile.deps_cpp_info.deps:
                         from_str = self._conanfile.deps_cpp_info[dep].rootpath
-                        # try to replace only if from str is found
-                        if path_content.find(from_str) != -1:
-                            dep_str = "${CONAN_%s_ROOT}" % dep.upper()
-                            self._conanfile.output.info("Patching paths for %s: %s to %s" % (dep, from_str, dep_str))
-                            tools.replace_in_file(path, from_str, dep_str, strict=False)
+                        dep_str = "${CONAN_%s_ROOT}" % dep.upper()
+                        ret = tools.replace_path_in_file(path, from_str, dep_str, strict=False)
+                        if ret:
+                            self._conanfile.output.info("Patched paths for %s: %s to %s" % (dep, from_str, dep_str))
 
     @staticmethod
     def get_version():
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
index 5f508dab7..65e0d709d 100644
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -76,15 +76,16 @@ def _capture_export_scm_data(conanfile, conanfile_dir, destination_folder, outpu
     scm = SCM(scm_data, conanfile_dir)
 
     if scm_data.url == "auto":
-        origin = scm.get_remote_url()
+        origin = scm.get_qualified_remote_url()
         if not origin:
             raise ConanException("Repo origin cannot be deduced by 'auto'")
-        if os.path.exists(origin):
+        if scm.is_local_repository():
             output.warn("Repo origin looks like a local path: %s" % origin)
-            origin = origin.replace("\\", "/")
         output.success("Repo origin deduced by 'auto': %s" % origin)
         scm_data.url = origin
     if scm_data.revision == "auto":
+        if not scm.is_pristine():
+            output.warn("Repo status is not pristine: there might be modified files")
         scm_data.revision = scm.get_revision()
         output.success("Revision deduced by 'auto': %s" % scm_data.revision)
 
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index 0ebf746f3..ce5f0363b 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -95,10 +95,12 @@ class CmdUpload(object):
                                      reference=conan_ref, remote=upload_remote)
 
         if policy != UPLOAD_POLICY_FORCE:
-            self._check_recipe_date(conan_ref, upload_remote)
+            remote_manifest = self._check_recipe_date(conan_ref, upload_remote)
+        else:
+            remote_manifest = None
 
         self._user_io.out.info("Uploading %s to remote '%s'" % (str(conan_ref), upload_remote.name))
-        self._upload_recipe(conan_ref, retry, retry_wait, policy, upload_remote)
+        self._upload_recipe(conan_ref, retry, retry_wait, policy, upload_remote, remote_manifest)
 
         recorder.add_recipe(str(conan_ref), upload_remote.name, upload_remote.url)
 
@@ -122,7 +124,7 @@ class CmdUpload(object):
         self._plugin_manager.execute("post_upload", conanfile_path=conanfile_path,
                                      reference=conan_ref, remote=upload_remote)
 
-    def _upload_recipe(self, conan_reference, retry, retry_wait, policy, remote):
+    def _upload_recipe(self, conan_reference, retry, retry_wait, policy, remote, remote_manifest):
         conan_file_path = self._client_cache.conanfile(conan_reference)
         current_remote = self._registry.get_recipe_remote(conan_reference)
         if remote != current_remote:
@@ -130,7 +132,7 @@ class CmdUpload(object):
             complete_recipe_sources(self._remote_manager, self._client_cache, self._registry,
                                     conanfile, conan_reference)
         result = self._remote_manager.upload_recipe(conan_reference, remote, retry, retry_wait,
-                                                    policy=policy)
+                                                    policy=policy, remote_manifest=remote_manifest)
         return result
 
     def _upload_package(self, package_ref, index=1, total=1, retry=None, retry_wait=None,
@@ -159,3 +161,5 @@ class CmdUpload(object):
             raise ConanException("Remote recipe is newer than local recipe: "
                                  "\n Remote date: %s\n Local date: %s" %
                                  (remote_recipe_manifest.time, local_manifest.time))
+
+        return remote_recipe_manifest
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index bee679558..0add63050 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -41,7 +41,8 @@ class RemoteManager(object):
         self._auth_manager = auth_manager
         self._plugin_manager = plugin_manager
 
-    def upload_recipe(self, conan_reference, remote, retry, retry_wait, policy=None):
+
+    def upload_recipe(self, conan_reference, remote, retry, retry_wait, policy, remote_manifest):
         conanfile_path = self._client_cache.conanfile(conan_reference)
         self._plugin_manager.execute("pre_upload_recipe", conanfile_path=conanfile_path,
                                      reference=conan_reference, remote=remote)
@@ -66,7 +67,7 @@ class RemoteManager(object):
             return None
 
         ret, new_ref = self._call_remote(remote, "upload_recipe", conan_reference, the_files, retry,
-                                         retry_wait, policy)
+                                         retry_wait, policy, remote_manifest)
         duration = time.time() - t1
         log_recipe_upload(new_ref, duration, the_files, remote.name)
         if ret:
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index 8093a74be..f01d0a251 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -125,9 +125,9 @@ class ConanApiAuthManager(object):
     # ######### CONAN API METHODS ##########
 
     @input_credentials_if_unauthorized
-    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, policy):
+    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, policy, remote_manifest):
         return self._rest_client.upload_recipe(conan_reference, the_files, retry, retry_wait,
-                                               policy)
+                                               policy, remote_manifest)
 
     @input_credentials_if_unauthorized
     def upload_package(self, package_reference, the_files, retry, retry_wait, policy):
diff --git a/conans/client/rest/rest_client.py b/conans/client/rest/rest_client.py
index ed7f7fe74..5ce6be0df 100644
--- a/conans/client/rest/rest_client.py
+++ b/conans/client/rest/rest_client.py
@@ -61,9 +61,9 @@ class RestApiClient(object):
     def get_path(self, conan_reference, package_id, path):
         return self._get_api().get_path(conan_reference, package_id, path)
 
-    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, no_overwrite):
+    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, policy, remote_manifest):
         return self._get_api().upload_recipe(conan_reference, the_files, retry, retry_wait,
-                                             no_overwrite)
+                                             policy, remote_manifest)
 
     def upload_package(self, package_reference, the_files, retry, retry_wait, no_overwrite):
         return self._get_api().upload_package(package_reference, the_files, retry, retry_wait,
diff --git a/conans/client/rest/rest_client_common.py b/conans/client/rest/rest_client_common.py
index 570bbf779..002ea336b 100644
--- a/conans/client/rest/rest_client_common.py
+++ b/conans/client/rest/rest_client_common.py
@@ -141,7 +141,8 @@ class RestCommonMethods(object):
             raise ConanException("Unexpected server response %s" % result)
         return result
 
-    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, policy):
+    def upload_recipe(self, conan_reference, the_files, retry, retry_wait, policy,
+                      remote_manifest):
         """
         the_files: dict with relative_path: content
         """
@@ -151,7 +152,7 @@ class RestCommonMethods(object):
         remote_snapshot, conan_reference = self._get_recipe_snapshot(conan_reference)
 
         if remote_snapshot and policy != UPLOAD_POLICY_FORCE:
-            remote_manifest = self.get_conan_manifest(conan_reference)
+            remote_manifest = remote_manifest or self.get_conan_manifest(conan_reference)
             local_manifest = FileTreeManifest.loads(load(the_files["conanmanifest.txt"]))
 
             if remote_manifest == local_manifest:
diff --git a/conans/client/rest/rest_client_v1.py b/conans/client/rest/rest_client_v1.py
index e67852b42..1d9f6ef83 100644
--- a/conans/client/rest/rest_client_v1.py
+++ b/conans/client/rest/rest_client_v1.py
@@ -12,10 +12,8 @@ from conans.model.manifest import FileTreeManifest
 from conans.model.ref import PackageReference
 from conans.paths import CONAN_MANIFEST, CONANINFO, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, \
     PACKAGE_TGZ_NAME
-from conans.util.files import decode_text, load
+from conans.util.files import decode_text
 from conans.util.log import logger
-from conans.client.cmd.uploader import UPLOAD_POLICY_NO_OVERWRITE,\
-    UPLOAD_POLICY_NO_OVERWRITE_RECIPE, UPLOAD_POLICY_FORCE
 
 
 def complete_url(base_url, url):
@@ -33,20 +31,23 @@ class RestV1Methods(RestCommonMethods):
     def remote_api_url(self):
         return "%s/v1" % self.remote_url.rstrip("/")
 
-    def _download_files(self, file_urls):
+    def _download_files(self, file_urls, quiet=False):
         """
         :param: file_urls is a dict with {filename: url}
 
         Its a generator, so it yields elements for memory performance
         """
-        downloader = Downloader(self.requester, self._output, self.verify_ssl)
+        output = self._output if not quiet else None
+        downloader = Downloader(self.requester, output, self.verify_ssl)
         # Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz
         # can be < conanfile, conaninfo, and sent always the last, so smaller files go first
         for filename, resource_url in sorted(file_urls.items(), reverse=True):
-            self._output.writeln("Downloading %s" % filename)
+            if output:
+                output.writeln("Downloading %s" % filename)
             auth, _ = self._file_server_capabilities(resource_url)
             contents = downloader.download(resource_url, auth=auth)
-            self._output.writeln("")
+            if output:
+                output.writeln("")
             yield os.path.normpath(filename), contents
 
     def _file_server_capabilities(self, resource_url):
@@ -69,7 +70,7 @@ class RestV1Methods(RestCommonMethods):
         urls = self._get_file_to_url_dict(url)
 
         # Get the digest
-        contents = self._download_files(urls)
+        contents = self._download_files(urls, quiet=True)
         # Unroll generator and decode shas (plain text)
         contents = {key: decode_text(value) for key, value in dict(contents).items()}
         return FileTreeManifest.loads(contents[CONAN_MANIFEST])
@@ -82,7 +83,7 @@ class RestV1Methods(RestCommonMethods):
         urls = self._get_file_to_url_dict(url)
 
         # Get the digest
-        contents = self._download_files(urls)
+        contents = self._download_files(urls, quiet=True)
         # Unroll generator and decode shas (plain text)
         contents = {key: decode_text(value) for key, value in dict(contents).items()}
         return FileTreeManifest.loads(contents[CONAN_MANIFEST])
diff --git a/conans/client/rest/uploader_downloader.py b/conans/client/rest/uploader_downloader.py
index 7f3a7a5a9..d1d6ffd87 100644
--- a/conans/client/rest/uploader_downloader.py
+++ b/conans/client/rest/uploader_downloader.py
@@ -169,9 +169,10 @@ class Downloader(object):
             if not file_path:
                 ret += response.content
             else:
-                total_length = len(response.content)
-                progress = human_readable_progress(total_length, total_length)
-                print_progress(self.output, 50, progress)
+                if self.output:
+                    total_length = len(response.content)
+                    progress = human_readable_progress(total_length, total_length)
+                    print_progress(self.output, 50, progress)
                 save_append(file_path, response.content)
         else:
             total_length = int(total_length)
@@ -191,13 +192,12 @@ class Downloader(object):
                         ret_buffer.extend(data)
                     if file_handler is not None:
                         file_handler.write(to_file_bytes(data))
-
-                    units = progress_units(download_size, total_length)
-                    progress = human_readable_progress(download_size, total_length)
-                    if last_progress != units:  # Avoid screen refresh if nothing has change
-                        if self.output:
+                    if self.output:
+                        units = progress_units(download_size, total_length)
+                        progress = human_readable_progress(download_size, total_length)
+                        if last_progress != units:  # Avoid screen refresh if nothing has change
                             print_progress(self.output, units, progress)
-                        last_progress = units
+                            last_progress = units
                 return download_size
 
             if file_path:
@@ -245,7 +245,8 @@ def call_with_retry(out, retry, retry_wait, method, *args, **kwargs):
             if counter == (retry - 1):
                 raise
             else:
-                msg = exception_message_safe(exc)
-                out.error(msg)
-                out.info("Waiting %d seconds to retry..." % retry_wait)
+                if out:
+                    msg = exception_message_safe(exc)
+                    out.error(msg)
+                    out.info("Waiting %d seconds to retry..." % retry_wait)
                 time.sleep(retry_wait)
diff --git a/conans/client/source.py b/conans/client/source.py
index 103141b04..8d28db9f4 100644
--- a/conans/client/source.py
+++ b/conans/client/source.py
@@ -198,6 +198,5 @@ def _fetch_scm(scm_data, dest_dir, local_sources_path, output):
     else:
         output.info("Getting sources from url: '%s'" % scm_data.url)
         scm = SCM(scm_data, dest_dir)
-        scm.clone()
         scm.checkout()
     _clean_source_folder(dest_dir)
diff --git a/conans/client/tools/files.py b/conans/client/tools/files.py
index e5f90c4d9..436c694c3 100644
--- a/conans/client/tools/files.py
+++ b/conans/client/tools/files.py
@@ -220,20 +220,51 @@ def patch(base_path=None, patch_file=None, patch_string=None, strip=0, output=No
         raise ConanException("Failed to apply patch: %s" % patch_file)
 
 
+def _manage_text_not_found(search, file_path, strict, function_name):
+    message = "%s didn't find pattern '%s' in '%s' file." % (function_name, search, file_path)
+    if strict:
+        raise ConanException(message)
+    else:
+        _global_output.warn(message)
+        return False
+
+
 def replace_in_file(file_path, search, replace, strict=True):
     content = load(file_path)
     if -1 == content.find(search):
-        message = "replace_in_file didn't find pattern '%s' in '%s' file." % (search, file_path)
-        if strict:
-            raise ConanException(message)
-        else:
-            _global_output.warn(message)
+        _manage_text_not_found(search, file_path, strict, "replace_in_file")
     content = content.replace(search, replace)
     content = content.encode("utf-8")
     with open(file_path, "wb") as handle:
         handle.write(content)
 
 
+def replace_path_in_file(file_path, search, replace, strict=True, windows_paths=None):
+    if windows_paths is False or (windows_paths is None and platform.system() != "Windows"):
+        return replace_in_file(file_path, search, replace, strict=strict)
+
+    def normalized_text(text):
+        return text.replace("\\", "/").lower()
+
+    content = load(file_path)
+    normalized_content = normalized_text(content)
+    normalized_search = normalized_text(search)
+    index = normalized_content.find(normalized_search)
+    if index == -1:
+        return _manage_text_not_found(search, file_path, strict, "replace_path_in_file")
+
+    while index != -1:
+        content = content[:index] + replace + content[index + len(search):]
+        normalized_content = normalized_text(content)
+        index = normalized_content.find(normalized_search)
+
+    content = content.encode("utf-8")
+    with open(file_path, "wb") as handle:
+        handle.write(content)
+
+    return True
+
+
 def replace_prefix_in_pc_file(pc_file, new_prefix):
     content = load(pc_file)
     lines = []
diff --git a/conans/client/tools/scm.py b/conans/client/tools/scm.py
index a59f5a29c..132c7de7d 100644
--- a/conans/client/tools/scm.py
+++ b/conans/client/tools/scm.py
@@ -1,19 +1,25 @@
 
 import os
+import sys
+import re
+
 import subprocess
-from six.moves.urllib.parse import urlparse, quote_plus
+from six.moves.urllib.parse import urlparse, quote_plus, unquote
 from subprocess import CalledProcessError, PIPE, STDOUT
+import platform
 
 from conans.client.tools.env import no_op, environment_append
 from conans.client.tools.files import chdir
 from conans.errors import ConanException
+from conans.model.version import Version
 from conans.util.files import decode_text, to_file_bytes, walk
 
 
-class Git(object):
+class SCMBase(object):
+    cmd_command = None
 
-    def __init__(self, folder=None, verify_ssl=True, username=None, password=None,
-                 force_english=True, runner=None):
+    def __init__(self, folder=None, verify_ssl=True, username=None, password=None, force_english=True,
+                 runner=None):
         self.folder = folder or os.getcwd()
         if not os.path.exists(self.folder):
             os.makedirs(self.folder)
@@ -24,17 +30,14 @@ class Git(object):
         self._runner = runner
 
     def run(self, command):
-        command = "git %s" % command
+        command = "%s %s" % (self.cmd_command, command)
         with chdir(self.folder) if self.folder else no_op():
             with environment_append({"LC_ALL": "en_US.UTF-8"}) if self._force_eng else no_op():
                 if not self._runner:
-                    return subprocess.check_output(command, shell=True).decode().strip()
+                    return decode_text(subprocess.check_output(command, shell=True).strip())
                 else:
                     return self._runner(command)
 
-    def get_repo_root(self):
-        return self.run("rev-parse --show-toplevel")
-
     def get_url_with_credentials(self, url):
         if not self._username or not self._password:
             return url
@@ -46,7 +49,12 @@ class Git(object):
         url = url.replace("://", "://" + user_enc + ":" + pwd_enc + "@", 1)
         return url
 
+
+class Git(SCMBase):
+    cmd_command = "git"
+
     def _configure_ssl_verify(self):
+        # TODO: This should be a context manager
         return self.run("config http.sslVerify %s" % ("true" if self._verify_ssl else "false"))
 
     def clone(self, url, branch=None):
@@ -106,20 +114,18 @@ class Git(object):
     def get_remote_url(self, remote_name=None):
         self._check_git_repo()
         remote_name = remote_name or "origin"
-        try:
-            remotes = self.run("remote -v")
-            for remote in remotes.splitlines():
-                try:
-                    name, url = remote.split(None, 1)
-                    url, _ = url.rsplit(None, 1)
-                    if name == remote_name:
-                        return url
-                except Exception:
-                    pass
-        except subprocess.CalledProcessError:
-            pass
+        remotes = self.run("remote -v")
+        for remote in remotes.splitlines():
+            name, url = remote.split(None, 1)
+            if name == remote_name:
+                url, _ = url.rsplit(None, 1)
+                return url
         return None
 
+    def is_local_repository(self):
+        url = self.get_remote_url()
+        return os.path.exists(url)   
+
     def get_commit(self):
         self._check_git_repo()
         try:
@@ -127,15 +133,21 @@ class Git(object):
             commit = commit.strip()
             return commit
         except Exception as e:
-            raise ConanException("Unable to get git commit from %s\n%s" % (self.folder, str(e)))
+            raise ConanException("Unable to get git commit from '%s': %s" % (self.folder, str(e)))
 
     get_revision = get_commit
 
-    def _check_git_repo(self):
-        try:
-            self.run("status")
-        except Exception:
-            raise ConanException("Not a valid git repository")
+    def is_pristine(self):
+        self._check_git_repo()
+        status = self.run("status --porcelain").strip()
+        if not status:
+            return True
+        else:
+            return False
+        
+    def get_repo_root(self):
+        self._check_git_repo()
+        return self.run("rev-parse --show-toplevel")
 
     def get_branch(self):
         self._check_git_repo()
@@ -145,4 +157,136 @@ class Git(object):
             branch = status.splitlines()[0].split("...")[0].strip("#").strip()
             return branch
         except Exception as e:
-            raise ConanException("Unable to get git branch from %s\n%s" % (self.folder, str(e)))
+            raise ConanException("Unable to get git branch from %s: %s" % (self.folder, str(e)))
+
+    def _check_git_repo(self):
+        try:
+            self.run("status")
+        except Exception:
+            raise ConanException("Not a valid git repository")
+
+
+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
+
+    def __init__(self, folder=None, runner=None, *args, **kwargs):
+        def runner_no_strip(command):
+            return decode_text(subprocess.check_output(command, shell=True))
+        runner = runner or runner_no_strip
+        super(SVN, self).__init__(folder=folder, runner=runner, *args, **kwargs)
+
+    @staticmethod
+    def get_version():
+        try:
+            out, err = subprocess.Popen(["svn", "--version"], stdout=subprocess.PIPE).communicate()
+            version_line = decode_text(out).split('\n', 1)[0]
+            version_str = version_line.split(' ', 3)[2]
+            return Version(version_str)
+        except Exception as e:
+            raise ConanException("Error retrieving SVN version: '{}'".format(e))
+
+    @property
+    def version(self):
+        if not hasattr(self, '_version'):
+            version = SVN.get_version()
+            setattr(self, '_version', version)
+        return getattr(self, '_version')
+
+    def run(self, command):
+        # Ensure we always pass some params
+        extra_options = " --no-auth-cache --non-interactive"
+        if not self._verify_ssl:
+            if self.version >= SVN.API_CHANGE_VERSION:
+                extra_options += " --trust-server-cert-failures=unknown-ca"
+            else:
+                extra_options += " --trust-server-cert"
+        return super(SVN, self).run(command="{} {}".format(command, extra_options))
+
+    def checkout(self, url, revision="HEAD"):
+        output = ""
+        try:
+            self._check_svn_repo()
+        except ConanException:
+            output += self.run('co "{url}" .'.format(url=url))
+        else:
+            assert url.lower() == self.get_remote_url().lower(), \
+                "%s != %s" % (url, self.get_remote_url())
+            output += self.run("revert . --recursive")
+        finally:
+            output += self.update(revision=revision)
+        return output
+
+    def update(self, revision='HEAD'):
+        self._check_svn_repo()
+        return self.run("update -r {rev}".format(rev=revision))
+
+    def excluded_files(self):
+        self._check_svn_repo()
+        excluded_list = []
+        output = self.run("status --no-ignore")
+        for it in output.splitlines():
+            if it[0] == 'I':  # Only ignored files
+                filepath = it[8:].strip()
+                excluded_list.append(os.path.normpath(filepath))
+        return excluded_list
+
+    def get_remote_url(self):
+        return self.run("info --show-item url").strip()
+
+    def get_qualified_remote_url(self):
+        # Return url with peg revision
+        url = self.get_remote_url()
+        revision = self.get_last_changed_revision()
+        return "{url}@{revision}".format(url=url, revision=revision)
+        
+    def is_local_repository(self):
+        url = self.get_remote_url()
+        return url.startswith(self.file_protocol) and \
+               os.path.exists(unquote(url[len(self.file_protocol):]))
+
+    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)
+
+        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
+
+    def get_revision(self):
+        return self.run("info --show-item revision").strip()
+
+    def get_repo_root(self):
+        return self.run("info --show-item wc-root").strip()
+
+    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()
+        else:
+            return self.run("info --show-item last-changed-revision").strip()
+
+    def get_branch(self):
+        url = self.run("info --show-item relative-url").strip()
+        try:
+            pattern = "(tags|branches)/[^/]+|trunk"
+            branch = re.search(pattern, url)
+            
+            if branch is None:
+                return None
+            else:
+                return branch.group(0)
+        except Exception as e:
+            raise ConanException("Unable to get svn branch from %s: %s" % (self.folder, str(e)))
+
+    def _check_svn_repo(self):
+        try:
+            self.run("info")
+        except Exception:
+            raise ConanException("Not a valid SVN repository")
\ No newline at end of file
diff --git a/conans/model/options.py b/conans/model/options.py
index 612e187c2..0915b907f 100644
--- a/conans/model/options.py
+++ b/conans/model/options.py
@@ -122,8 +122,7 @@ class PackageOptionValues(object):
 
         assert isinstance(down_package_values, PackageOptionValues)
         for (name, value) in down_package_values.items():
-            current_value = self._dict.get(name)
-            if value == current_value:
+            if name in self._dict and self._dict.get(name) == value:
                 continue
 
             modified = self._modified.get(name)
@@ -463,8 +462,7 @@ class PackageOptions(object):
             return
 
         for (name, value) in package_values.items():
-            current_value = self._data.get(name)
-            if value == current_value:
+            if name in self._data and self._data.get(name) == value:
                 continue
 
             modified = self._modified.get(name)
diff --git a/conans/model/scm.py b/conans/model/scm.py
index 3e1231026..07ae934b7 100644
--- a/conans/model/scm.py
+++ b/conans/model/scm.py
@@ -1,6 +1,7 @@
 import json
+import sys
 
-from conans.client.tools.scm import Git
+from conans.client.tools.scm import Git, SVN
 from conans.errors import ConanException
 
 
@@ -44,22 +45,25 @@ class SCM(object):
         self.repo = self._get_repo()
 
     def _get_repo(self):
-        repo = {"git": Git(self.repo_folder, verify_ssl=self._data.verify_ssl,
-                           username=self._data.username,
-                           password=self._data.password)}.get(self._data.type)
-        if not repo:
+        repo_class = {"git": Git, "svn": SVN}.get(self._data.type)
+        if not repo_class:
             raise ConanException("SCM not supported: %s" % self._data.type)
-        return repo
+
+        return repo_class(folder=self.repo_folder, verify_ssl=self._data.verify_ssl,
+                          username=self._data.username, password=self._data.password)
 
     @property
     def excluded_files(self):
         return self.repo.excluded_files()
 
-    def clone(self):
-        return self.repo.clone(self._data.url)
-
     def checkout(self):
-        return self.repo.checkout(self._data.revision, submodule=self._data.submodule)
+        output= ""
+        if self._data.type == "git":
+            output += self.repo.clone(url=self._data.url)
+            output += self.repo.checkout(element=self._data.revision, submodule=self._data.submodule)
+        else:
+            output += self.repo.checkout(url=self._data.url, revision=self._data.revision)
+        return output
 
     def get_remote_url(self):
         return self.repo.get_remote_url()
@@ -67,5 +71,18 @@ class SCM(object):
     def get_revision(self):
         return self.repo.get_revision()
 
+    def is_pristine(self):
+        return self.repo.is_pristine()
+
     def get_repo_root(self):
         return self.repo.get_repo_root()
+
+    def get_qualified_remote_url(self):
+        if self._data.type == "git":
+            return self.repo.get_remote_url()
+        else:
+            return self.repo.get_qualified_remote_url()
+
+    def is_local_repository(self):
+        return self.repo.is_local_repository()
+


Test patch:
diff --git a/conans/test/build_helpers/cmake_test.py b/conans/test/build_helpers/cmake_test.py
index 59986ea3b..ec78fea2e 100644
--- a/conans/test/build_helpers/cmake_test.py
+++ b/conans/test/build_helpers/cmake_test.py
@@ -24,6 +24,13 @@ from conans.model.options import Options, PackageOptions
 from conans.errors import ConanException
 
 
+def _format_path_as_cmake(pathstr):
+    if platform.system() == "Windows":
+        drive,path = os.path.splitdrive(pathstr)
+        return drive.upper() + path.replace(os.path.sep,"/")
+    return pathstr
+
+
 class CMakeTest(unittest.TestCase):
 
     def setUp(self):
@@ -35,6 +42,7 @@ class CMakeTest(unittest.TestCase):
         shutil.rmtree(self.tempdir2)
 
     def config_patch_test(self):
+
         conan_file = ConanFileMock()
         conan_file.name = "MyPkg"
         conan_file.settings = Settings()
@@ -43,7 +51,7 @@ class CMakeTest(unittest.TestCase):
         conan_file.package_folder = os.path.join(self.tempdir, "pkg")
         conan_file.deps_cpp_info = DepsCppInfo()
 
-        msg = "FOLDER: " + conan_file.package_folder
+        msg = "FOLDER: " + _format_path_as_cmake(conan_file.package_folder)
         for folder in (conan_file.build_folder, conan_file.package_folder):
             save(os.path.join(folder, "file1.cmake"), "Nothing")
             save(os.path.join(folder, "file2"), msg)
@@ -80,7 +88,7 @@ class CMakeTest(unittest.TestCase):
         self.assertEqual(conan_file.deps_cpp_info['MyPkg1'].rootpath,
                          self.tempdir2)
 
-        msg = "FOLDER: " + self.tempdir2
+        msg = "FOLDER: " + _format_path_as_cmake(self.tempdir2)
         for folder in (conan_file.build_folder, conan_file.package_folder):
             save(os.path.join(folder, "file1.cmake"), "Nothing")
             save(os.path.join(folder, "file2"), msg)
diff --git a/conans/test/functional/scm_test.py b/conans/test/functional/scm_test.py
index 559223f72..cc73eb620 100644
--- a/conans/test/functional/scm_test.py
+++ b/conans/test/functional/scm_test.py
@@ -3,11 +3,11 @@ import os
 import unittest
 from collections import namedtuple
 
-from conans.client.tools.scm import Git
+from conans.client.tools.scm import Git, SVN
 from conans.model.ref import ConanFileReference, PackageReference
 from conans.model.scm import SCMData
 from conans.test.utils.test_files import temp_folder
-from conans.test.utils.tools import TestClient, TestServer, create_local_git_repo
+from conans.test.utils.tools import TestClient, TestServer, create_local_git_repo, SVNLocalRepoTestCase
 from conans.util.files import load, rmdir, save, to_file_bytes
 from conans.client.tools.win import get_cased_path
 
@@ -21,7 +21,7 @@ class ConanLib(ConanFile):
     version = "0.1"
     short_paths = True
     scm = {{
-        "type": "git",
+        "type": "%s",
         "url": "{url}",
         "revision": "{revision}",
     }}
@@ -32,8 +32,11 @@ class ConanLib(ConanFile):
         self.output.warn(tools.load(path))
 '''
 
+base_git = base % "git"
+base_svn = base % "svn"
+
 
-class SCMTest(unittest.TestCase):
+class GitSCMTest(unittest.TestCase):
 
     def setUp(self):
         self.reference = ConanFileReference.loads("lib/0.1@user/channel")
@@ -88,7 +91,7 @@ class ConanLib(ConanFile):
 
     def test_auto_filesystem_remote_git(self):
         # https://github.com/conan-io/conan/issues/3109
-        conanfile = base.format(directory="None", url="auto", revision="auto")
+        conanfile = base_git.format(directory="None", url="auto", revision="auto")
         repo = temp_folder()
         self.client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"}, repo)
         self.client.runner("git init .", cwd=repo)
@@ -105,7 +108,7 @@ class ConanLib(ConanFile):
 
     def test_auto_git(self):
         curdir = get_cased_path(self.client.current_folder).replace("\\", "/")
-        conanfile = base.format(directory="None", url="auto", revision="auto")
+        conanfile = base_git.format(directory="None", url="auto", revision="auto")
         self.client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
         self._commit_contents()
         error = self.client.run("export . user/channel", ignore_error=True)
@@ -132,7 +135,7 @@ class ConanLib(ConanFile):
 
         # Export again but now with absolute reference, so no pointer file is created nor kept
         git = Git(curdir)
-        self.client.save({"conanfile.py": base.format(url=curdir, revision=git.get_revision())})
+        self.client.save({"conanfile.py": base_git.format(url=curdir, revision=git.get_revision())})
         self.client.run("create . user/channel")
         sources_dir = self.client.client_cache.scm_folder(self.reference)
         self.assertFalse(os.path.exists(sources_dir))
@@ -143,9 +146,9 @@ class ConanLib(ConanFile):
 
     def test_auto_subfolder(self):
         curdir = self.client.current_folder.replace("\\", "/")
-        conanfile = base.replace('"revision": "{revision}"',
-                                 '"revision": "{revision}",\n        '
-                                 '"subfolder": "mysub"')
+        conanfile = base_git.replace('"revision": "{revision}"',
+                                     '"revision": "{revision}",\n        '
+                                     '"subfolder": "mysub"')
         conanfile = conanfile.replace("short_paths = True", "short_paths = False")
         conanfile = conanfile.format(directory="None", url="auto", revision="auto")
         self.client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
@@ -162,7 +165,7 @@ class ConanLib(ConanFile):
         Conanfile is not in the root of the repo: https://github.com/conan-io/conan/issues/3465
         """
         curdir = get_cased_path(self.client.current_folder).replace("\\", "/")
-        conanfile = base.format(url="auto", revision="auto")
+        conanfile = base_git.format(url="auto", revision="auto")
         self.client.save({"conan/conanfile.py": conanfile, "myfile.txt": "content of my file"})
         self._commit_contents()
         self.client.runner('git remote add origin https://myrepo.com.git', cwd=curdir)
@@ -175,7 +178,7 @@ class ConanLib(ConanFile):
     def test_deleted_source_folder(self):
         path, commit = create_local_git_repo({"myfile": "contents"}, branch="my_release")
         curdir = self.client.current_folder.replace("\\", "/")
-        conanfile = base.format(url="auto", revision="auto")
+        conanfile = base_git.format(url="auto", revision="auto")
         self.client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
         self._commit_contents()
         self.client.runner('git remote add origin "%s"' % path.replace("\\", "/"), cwd=curdir)
@@ -189,8 +192,8 @@ class ConanLib(ConanFile):
         self.assertTrue(error)
         self.assertIn("Getting sources from url: '%s'" % path.replace("\\", "/"), self.client.out)
 
-    def test_excluded_repo_files(self):
-        conanfile = base.format(url="auto", revision="auto")
+    def test_excluded_repo_fies(self):
+        conanfile = base_git.format(url="auto", revision="auto")
         conanfile = conanfile.replace("short_paths = True", "short_paths = False")
         path, commit = create_local_git_repo({"myfile": "contents",
                                               "ignored.pyc": "bin",
@@ -222,7 +225,7 @@ other_folder/excluded_subfolder
 
     def test_local_source(self):
         curdir = self.client.current_folder
-        conanfile = base.format(url="auto", revision="auto")
+        conanfile = base_git.format(url="auto", revision="auto")
         conanfile += """
     def source(self):
         self.output.warn("SOURCE METHOD CALLED")
@@ -240,7 +243,7 @@ other_folder/excluded_subfolder
 
         # Export again but now with absolute reference, so no pointer file is created nor kept
         git = Git(curdir.replace("\\", "/"))
-        conanfile = base.format(url=curdir.replace("\\", "/"), revision=git.get_revision())
+        conanfile = base_git.format(url=curdir.replace("\\", "/"), revision=git.get_revision())
         conanfile += """
     def source(self):
         self.output.warn("SOURCE METHOD CALLED")
@@ -257,9 +260,9 @@ other_folder/excluded_subfolder
 
     def test_local_source_subfolder(self):
         curdir = self.client.current_folder
-        conanfile = base.replace('"revision": "{revision}"',
-                                 '"revision": "{revision}",\n        '
-                                 '"subfolder": "mysub"')
+        conanfile = base_git.replace('"revision": "{revision}"',
+                                     '"revision": "{revision}",\n        '
+                                     '"subfolder": "mysub"')
         conanfile = conanfile.format(url="auto", revision="auto")
         conanfile += """
     def source(self):
@@ -279,7 +282,7 @@ other_folder/excluded_subfolder
         self.client = TestClient(servers=self.servers, users={"myremote": [("lasote", "mypass")]})
 
         curdir = self.client.current_folder.replace("\\", "/")
-        conanfile = base.format(url="auto", revision="auto")
+        conanfile = base_git.format(url="auto", revision="auto")
         self.client.save({"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
         self._commit_contents()
         cmd = 'git remote add origin "%s"' % curdir
@@ -293,7 +296,6 @@ other_folder/excluded_subfolder
         self.assertIn("My file is copied", client2.out)
 
     def test_source_removed_in_local_cache(self):
-
         conanfile = '''
 from conans import ConanFile, tools
 
@@ -303,11 +305,11 @@ class ConanLib(ConanFile):
         "url": "auto",
         "revision": "auto",
     }
-    
+
     def build(self):
         contents = tools.load("myfile")
         self.output.warn("Contents: %s" % contents)
-        
+
 '''
         path, commit = create_local_git_repo({"myfile": "contents", "conanfile.py": conanfile},
                                              branch="my_release")
@@ -330,10 +332,10 @@ class ConanLib(ConanFile):
 
         def _relative_paths(folder):
             submodule_path = os.path.join(
-                folder, 
+                folder,
                 os.path.basename(os.path.normpath(submodule)))
             subsubmodule_path = os.path.join(
-                submodule_path, 
+                submodule_path,
                 os.path.basename(os.path.normpath(subsubmodule)))
             return submodule_path, subsubmodule_path
 
@@ -473,3 +475,296 @@ class ConanLib(ConanFile):
         the_json = str(scm_data)
         data2 = json.loads(the_json)
         self.assertEquals(data, data2)
+
+
+class SVNSCMTest(SVNLocalRepoTestCase):
+
+    def setUp(self):
+        self.reference = ConanFileReference.loads("lib/0.1@user/channel")
+        self.client = TestClient()
+
+    def _commit_contents(self):
+        # self.client.runner('svn co "{url}" "{path}"'.format(url=self.repo_url, path=self.client.current_folder))
+        self.client.runner("svn add *", cwd=self.client.current_folder)
+        self.client.runner('svn commit -m  "commiting"', cwd=self.client.current_folder)
+
+    def test_scm_other_type_ignored(self):
+        conanfile = '''
+from conans import ConanFile, tools
+
+class ConanLib(ConanFile):
+    name = "lib"
+    version = "0.1"
+    scm = ["Other stuff"]
+
+'''
+        self.client.save({"conanfile.py": conanfile})
+        # nothing breaks
+        self.client.run("export . user/channel")
+
+    def test_repeat_clone_changing_subfolder(self):
+        tmp = '''
+from conans import ConanFile, tools
+
+class ConanLib(ConanFile):
+    name = "lib"
+    version = "0.1"
+    scm = {{
+        "type": "svn",
+        "url": "{url}",
+        "revision": "{revision}",
+        "subfolder": "onesubfolder"
+    }}
+'''
+        project_url, rev = self.create_project(files={"myfile": "contents"})
+        conanfile = tmp.format(url=project_url, revision=rev)
+        self.client.save({"conanfile.py": conanfile,
+                          "myfile.txt": "My file is copied"})
+        self.client.run("create . user/channel")
+        conanfile = conanfile.replace('"onesubfolder"', '"othersubfolder"')
+        self.client.save({"conanfile.py": conanfile})
+        self.client.run("create . user/channel")
+        folder = self.client.client_cache.source(ConanFileReference.loads("lib/0.1@user/channel"))
+        self.assertIn("othersubfolder", os.listdir(folder))
+        self.assertTrue(os.path.exists(os.path.join(folder, "othersubfolder", "myfile")))
+
+    def test_auto_filesystem_remote_svn(self):
+        # SVN origin will never be a local path (local repo has at least protocol file:///)
+        pass
+
+    def test_auto_svn(self):
+        conanfile = base_svn.format(directory="None", url="auto", revision="auto")
+        project_url, rev = self.create_project(files={"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+
+        curdir = self.client.current_folder.replace("\\", "/")
+        # Create the package, will copy the sources from the local folder
+        self.client.run("create . user/channel")
+        sources_dir = self.client.client_cache.scm_folder(self.reference)
+        self.assertEquals(load(sources_dir), curdir)
+        self.assertIn("Repo origin deduced by 'auto': {}".format(project_url).lower(),
+            str(self.client.out).lower())
+        self.assertIn("Revision deduced by 'auto'", self.client.out)
+        self.assertIn("Getting sources from folder: %s" % curdir, self.client.out)
+        self.assertIn("My file is copied", self.client.out)
+
+        # Export again but now with absolute reference, so no pointer file is created nor kept
+        svn = SVN(curdir)
+        self.client.save({"conanfile.py": base_svn.format(url=svn.get_remote_url(), revision=svn.get_revision())})
+        self.client.run("create . user/channel", ignore_error=False)
+        sources_dir = self.client.client_cache.scm_folder(self.reference)
+        self.assertFalse(os.path.exists(sources_dir))
+        self.assertNotIn("Repo origin deduced by 'auto'", self.client.out)
+        self.assertNotIn("Revision deduced by 'auto'", self.client.out)
+        self.assertIn("Getting sources from url: '{}'".format(project_url).lower(),
+                      str(self.client.out).lower())
+        self.assertIn("My file is copied", self.client.out)
+
+    def test_auto_subfolder(self):
+        conanfile = base_svn.replace('"revision": "{revision}"',
+                                     '"revision": "{revision}",\n        '
+                                     '"subfolder": "mysub"')
+        conanfile = conanfile.replace("short_paths = True", "short_paths = False")
+        conanfile = conanfile.format(directory="None", url="auto", revision="auto")
+
+        project_url, rev = self.create_project(files={"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+        self.client.run("create . user/channel")
+
+        folder = self.client.client_cache.source(ConanFileReference.loads("lib/0.1@user/channel"))
+        self.assertTrue(os.path.exists(os.path.join(folder, "mysub", "myfile.txt")))
+        self.assertFalse(os.path.exists(os.path.join(folder, "mysub", "conanfile.py")))
+
+    def test_auto_conanfile_no_root(self):
+        """
+        Conanfile is not in the root of the repo: https://github.com/conan-io/conan/issues/3465
+        """
+        curdir = self.client.current_folder
+        conanfile = base_svn.format(url="auto", revision="auto")
+        project_url, rev = self.create_project(files={"conan/conanfile.py": conanfile,
+                                                      "myfile.txt": "My file is copied"})
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url,
+                                                            path=self.client.current_folder))
+        self.client.run("create conan/ user/channel")
+
+        sources_dir = self.client.client_cache.scm_folder(self.reference)
+        self.assertEquals(load(sources_dir), curdir.replace('\\', '/'))  # Root of git is 'curdir'
+
+    def test_deleted_source_folder(self):
+        # SVN will always retrieve from 'remote'
+        pass
+
+    def test_excluded_repo_fies(self):
+        conanfile = base_svn.format(url="auto", revision="auto")
+        conanfile = conanfile.replace("short_paths = True", "short_paths = False")
+        project_url, rev = self.create_project(files={"myfile": "contents",
+                                                      "ignored.pyc": "bin",  # SVN ignores pyc files by default: http://blogs.collab.net/subversion/repository-dictated-configuration-day-3-global-ignores
+                                                      # ".gitignore": "*.pyc\n",
+                                                      "myfile.txt": "My file!",
+                                                      "conanfile.py": conanfile})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+
+        self.client.run("create . user/channel")
+        self.assertIn("Copying sources to build folder", self.client.out)
+        pref = PackageReference(ConanFileReference.loads("lib/0.1/user/channel"),
+                                "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9")
+        bf = self.client.client_cache.build(pref)
+        self.assertTrue(os.path.exists(os.path.join(bf, "myfile.txt")))
+        self.assertTrue(os.path.exists(os.path.join(bf, "myfile")))
+        self.assertTrue(os.path.exists(os.path.join(bf, ".svn")))
+        self.assertFalse(os.path.exists(os.path.join(bf, "ignored.pyc")))
+
+    def test_local_source(self):
+        curdir = self.client.current_folder
+        conanfile = base_svn.format(url="auto", revision="auto")
+        conanfile += """
+    def source(self):
+        self.output.warn("SOURCE METHOD CALLED")
+"""
+        project_url, rev = self.create_project(files={"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+        self.client.save({"aditional_file.txt": "contents"})
+
+        self.client.run("source . --source-folder=./source")
+        self.assertTrue(os.path.exists(os.path.join(curdir, "source", "myfile.txt")))
+        self.assertIn("SOURCE METHOD CALLED", self.client.out)
+        # Even the not commited files are copied
+        self.assertTrue(os.path.exists(os.path.join(curdir, "source", "aditional_file.txt")))
+        self.assertIn("Getting sources from folder: %s" % curdir, self.client.out)
+
+        # Export again but now with absolute reference, so no pointer file is created nor kept
+        svn = SVN(curdir.replace("\\", "/"))
+        conanfile = base_svn.format(url=svn.get_remote_url(), revision=svn.get_revision())
+        conanfile += """
+    def source(self):
+        self.output.warn("SOURCE METHOD CALLED")
+"""
+        self.client.save({"conanfile.py": conanfile,
+                          "myfile2.txt": "My file is copied"})
+        self._commit_contents()
+        self.client.run("source . --source-folder=./source2")
+        # myfile2 is no in the specified commit
+        self.assertFalse(os.path.exists(os.path.join(curdir, "source2", "myfile2.txt")))
+        self.assertTrue(os.path.exists(os.path.join(curdir, "source2", "myfile.txt")))
+        self.assertIn("Getting sources from url: '{}'".format(project_url).lower(),
+                      str(self.client.out).lower())
+        self.assertIn("SOURCE METHOD CALLED", self.client.out)
+
+    def test_local_source_subfolder(self):
+        curdir = self.client.current_folder
+        conanfile = base_svn.replace('"revision": "{revision}"',
+                                     '"revision": "{revision}",\n        '
+                                     '"subfolder": "mysub"')
+        conanfile = conanfile.format(url="auto", revision="auto")
+        conanfile += """
+    def source(self):
+        self.output.warn("SOURCE METHOD CALLED")
+"""
+        project_url, rev = self.create_project(files={"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+
+        self.client.run("source . --source-folder=./source")
+        self.assertFalse(os.path.exists(os.path.join(curdir, "source", "myfile.txt")))
+        self.assertTrue(os.path.exists(os.path.join(curdir, "source", "mysub", "myfile.txt")))
+        self.assertIn("SOURCE METHOD CALLED", self.client.out)
+
+    def test_install_checked_out(self):
+        test_server = TestServer()
+        self.servers = {"myremote": test_server}
+        self.client = TestClient(servers=self.servers, users={"myremote": [("lasote", "mypass")]})
+
+        curdir = self.client.current_folder.replace("\\", "/")
+        conanfile = base_svn.format(url="auto", revision="auto")
+        project_url, rev = self.create_project(files={"conanfile.py": conanfile, "myfile.txt": "My file is copied"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+        self.client.run("export . lasote/channel")
+        self.client.run("upload lib* -c")
+
+        # Take other client, the old client folder will be used as a remote
+        client2 = TestClient(servers=self.servers, users={"myremote": [("lasote", "mypass")]})
+        client2.run("install lib/0.1@lasote/channel --build")
+        self.assertIn("My file is copied", client2.out)
+
+    def test_source_removed_in_local_cache(self):
+        conanfile = '''
+from conans import ConanFile, tools
+
+class ConanLib(ConanFile):
+    scm = {
+        "type": "svn",
+        "url": "auto",
+        "revision": "auto",
+    }
+
+    def build(self):
+        contents = tools.load("myfile")
+        self.output.warn("Contents: %s" % contents)
+
+'''
+        project_url, rev = self.create_project(files={"myfile": "contents", "conanfile.py": conanfile})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+
+        self.client.run("create . lib/1.0@user/channel")
+        self.assertIn("Contents: contents", self.client.out)
+        self.client.save({"myfile": "Contents 2"})
+        self.client.run("create . lib/1.0@user/channel")
+        self.assertIn("Contents: Contents 2", self.client.out)
+        self.assertIn("Detected 'scm' auto in conanfile, trying to remove source folder",
+                      self.client.out)
+
+    def test_submodule(self):
+        # SVN has no submodules, may add something related to svn:external?
+        pass
+
+    def test_source_method_export_sources_and_scm_mixed(self):
+        project_url, rev = self.create_project(files={"myfile": "contents"})
+        project_url = project_url.replace(" ", "%20")
+        self.client.runner('svn co "{url}" "{path}"'.format(url=project_url, path=self.client.current_folder))
+
+        conanfile = '''
+import os
+from conans import ConanFile, tools
+
+class ConanLib(ConanFile):
+    name = "lib"
+    version = "0.1"
+    exports_sources = "file.txt"
+    scm = {{
+        "type": "svn",
+        "url": "{url}",
+        "revision": "{rev}",
+        "subfolder": "src"
+    }}
+
+    def source(self):
+        self.output.warn("SOURCE METHOD CALLED")
+        assert(os.path.exists("file.txt"))
+        assert(os.path.exists(os.path.join("src", "myfile")))
+        tools.save("cosa.txt", "contents")
+
+    def build(self):
+        assert(os.path.exists("file.txt"))
+        assert(os.path.exists("cosa.txt"))
+        self.output.warn("BUILD METHOD CALLED")
+'''.format(url=project_url, rev=rev)
+        self.client.save({"conanfile.py": conanfile, "file.txt": "My file is copied"})
+        self.client.run("create . user/channel")
+        self.assertIn("SOURCE METHOD CALLED", self.client.out)
+        self.assertIn("BUILD METHOD CALLED", self.client.out)
+
+    def test_scm_serialization(self):
+        data = {"url": "myurl", "revision": "23", "username": "myusername",
+                "password": "mypassword", "type": "svn", "verify_ssl": True,
+                "subfolder": "mysubfolder"}
+        conanfile = namedtuple("ConanfileMock", "scm")(data)
+        scm_data = SCMData(conanfile)
+        the_json = str(scm_data)
+        data2 = json.loads(the_json)
+        self.assertEquals(data, data2)
diff --git a/conans/test/integration/cmake_install_package_test.py b/conans/test/integration/cmake_install_package_test.py
index f7c222c2b..cae30fc03 100644
--- a/conans/test/integration/cmake_install_package_test.py
+++ b/conans/test/integration/cmake_install_package_test.py
@@ -9,13 +9,22 @@ class CMakeInstallPackageTest(unittest.TestCase):
         conanfile = """from conans import ConanFile, CMake
 from conans.tools import save, load
 import os
+import platform
+
+def _mess_with_path(pathstr):
+   if platform.system() == "Windows":
+       drive, path = os.path.splitdrive(pathstr)
+       return drive.upper() + path.replace(os.path.sep,"/")
+   return pathstr
 
 class AConan(ConanFile):
     settings = "os", "compiler", "build_type", "arch"
     def build(self):
         cmake = CMake(self)
-        save("file1.cmake", "FOLDER " + self.package_folder)
-        save("sub/file1.cmake", "FOLDER " + self.package_folder)
+        pth = _mess_with_path(self.package_folder)
+        self.output.info("output path: %s" % pth)
+        save("file1.cmake", "FOLDER " + pth)
+        save("sub/file1.cmake", "FOLDER " + pth)
         cmake.patch_config_paths()
     def package(self):
         self.copy("*")
diff --git a/conans/test/model/options_test.py b/conans/test/model/options_test.py
index 6b4a24e2d..0e8cc7093 100644
--- a/conans/test/model/options_test.py
+++ b/conans/test/model/options_test.py
@@ -225,6 +225,34 @@ Poco:deps_bundled=True""")
                                                      ])
 
 
+class OptionsValuesPropagationUpstreamNone(unittest.TestCase):
+
+    def test_propagate_in_options(self):
+        package_options = PackageOptions.loads("""{opt: [None, "a", "b"],}""")
+        values = PackageOptionValues()
+        values.add_option("opt", "a")
+        package_options.values = values
+        sut = Options(package_options)
+
+        other_options = PackageOptionValues()
+        other_options.add_option("opt", None)
+        options = {"whatever.*": other_options}
+        down_ref = ConanFileReference.loads("Boost.Assert/0.1@diego/testing")
+        own_ref = ConanFileReference.loads("Boost.Assert/0.1@diego/testing")
+        sut.propagate_upstream(options, down_ref, own_ref)
+        self.assertEqual(sut.values.as_list(), [("opt", "a"),
+                                                ("whatever.*:opt", "None"),
+                                                ])
+
+    def test_propagate_in_pacakge_options(self):
+        package_options = PackageOptions.loads("""{opt: [None, "a", "b"],}""")
+        values = PackageOptionValues()
+        package_options.values = values
+
+        package_options.propagate_upstream({'opt': None}, None, None, [])
+        self.assertEqual(package_options.values.items(), [('opt', 'None'), ])
+
+
 class OptionsValuesTest(unittest.TestCase):
 
     def setUp(self):
diff --git a/conans/test/remote/rest_api_test.py b/conans/test/remote/rest_api_test.py
index 6a93da507..2e6cce410 100644
--- a/conans/test/remote/rest_api_test.py
+++ b/conans/test/remote/rest_api_test.py
@@ -281,4 +281,4 @@ class MyConan(ConanFile):
         abs_paths[CONAN_MANIFEST] = os.path.join(tmp_dir, CONAN_MANIFEST)
         conan_digest.save(tmp_dir)
 
-        self.api.upload_recipe(conan_reference, abs_paths, retry, retry_wait, None)
+        self.api.upload_recipe(conan_reference, abs_paths, retry, retry_wait, None, None)
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
index 825d8467f..d2b9e7f78 100644
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -4,12 +4,14 @@ import mock
 import os
 import platform
 import unittest
+import uuid
 
 from collections import namedtuple
 
 import six
 from mock.mock import patch, mock_open
 from six import StringIO
+from six.moves.urllib.parse import unquote
 
 from conans.client.client_cache import CONAN_CONF
 
@@ -18,7 +20,7 @@ from conans.client.conan_api import ConanAPIV1
 from conans.client.conf import default_settings_yml, default_client_conf
 from conans.client.output import ConanOutput
 from conans.client.tools.win import vcvars_dict, vswhere
-from conans.client.tools.scm import Git
+from conans.client.tools.scm import Git, SVN
 
 from conans.errors import ConanException, NotFoundException
 from conans.model.build_info import CppInfo
@@ -28,7 +30,7 @@ from conans.test.build_helpers.cmake_test import ConanFileMock
 from conans.test.utils.runner import TestRunner
 from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient, TestBufferConanOutput, create_local_git_repo, \
-    StoppableThreadBottle
+    SVNLocalRepoTestCase, StoppableThreadBottle
 
 from conans.tools import which
 from conans.tools import OSInfo, SystemPackageTool, replace_in_file, AptTool, ChocolateyTool,\
@@ -458,6 +460,42 @@ class ReplaceInFileTest(unittest.TestCase):
 
 class ToolsTest(unittest.TestCase):
 
+    def replace_paths_test(self):
+        folder = temp_folder()
+        path = os.path.join(folder, "file")
+        replace_with = "MYPATH"
+        expected = 'Some other contentsMYPATH"finally all text'
+
+        save(path, 'Some other contentsc:\\Path\\TO\\file.txt"finally all text')
+        ret = tools.replace_path_in_file(path, "C:/Path/to/file.txt", replace_with, windows_paths=True)
+        self.assertEquals(load(path), expected)
+        self.assertTrue(ret)
+
+        save(path, 'Some other contentsC:/Path\\TO\\file.txt"finally all text')
+        ret = tools.replace_path_in_file(path, "C:/PATH/to/FILE.txt", replace_with, windows_paths=True)
+        self.assertEquals(load(path), expected)
+        self.assertTrue(ret)
+
+        save(path, 'Some other contentsD:/Path\\TO\\file.txt"finally all text')
+        ret = tools.replace_path_in_file(path, "C:/PATH/to/FILE.txt", replace_with, strict=False, windows_paths=True)
+        self.assertEquals(load(path), 'Some other contentsD:/Path\\TO\\file.txt"finally all text')
+        self.assertFalse(ret)
+
+        # Multiple matches
+        save(path, 'Some other contentsD:/Path\\TO\\file.txt"finally all textd:\\PATH\\to\\file.TXTMoretext')
+        ret = tools.replace_path_in_file(path, "D:/PATH/to/FILE.txt", replace_with, strict=False, windows_paths=True)
+        self.assertEquals(load(path), 'Some other contentsMYPATH"finally all textMYPATHMoretext')
+        self.assertTrue(ret)
+
+        # Automatic windows_paths
+        save(path, 'Some other contentsD:/Path\\TO\\file.txt"finally all textd:\\PATH\\to\\file.TXTMoretext')
+        ret = tools.replace_path_in_file(path, "D:/PATH/to/FILE.txt", replace_with, strict=False)
+        if platform.system() == "Windows":
+            self.assertEquals(load(path), 'Some other contentsMYPATH"finally all textMYPATHMoretext')
+            self.assertTrue(ret)
+        else:
+            self.assertFalse(ret)
+
     def load_save_test(self):
         folder = temp_folder()
         path = os.path.join(folder, "file")
@@ -1197,6 +1235,75 @@ ProgramFiles(x86)=C:\Program Files (x86)
             self.assertTrue(os.path.exists("test_folder"))
         thread.stop()
 
+    def unix_to_dos_unit_test(self):
+
+        def save_file(contents):
+            tmp = temp_folder()
+            filepath = os.path.join(tmp, "a_file.txt")
+            save(filepath, contents)
+            return filepath
+
+        fp = save_file(b"a line\notherline\n")
+        if not tools.os_info.is_windows:
+            import subprocess
+            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
+            self.assertIn("ASCII text", str(output))
+            self.assertNotIn("CRLF", str(output))
+
+            tools.unix2dos(fp)
+            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
+            self.assertIn("ASCII text", str(output))
+            self.assertIn("CRLF", str(output))
+        else:
+            fc = tools.load(fp)
+            self.assertNotIn("\r\n", fc)
+            tools.unix2dos(fp)
+            fc = tools.load(fp)
+            self.assertIn("\r\n", fc)
+
+        self.assertEquals("a line\r\notherline\r\n", str(tools.load(fp)))
+
+        fp = save_file(b"a line\r\notherline\r\n")
+        if not tools.os_info.is_windows:
+            import subprocess
+            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
+            self.assertIn("ASCII text", str(output))
+            self.assertIn("CRLF", str(output))
+
+            tools.dos2unix(fp)
+            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
+            self.assertIn("ASCII text", str(output))
+            self.assertNotIn("CRLF", str(output))
+        else:
+            fc = tools.load(fp)
+            self.assertIn("\r\n", fc)
+            tools.dos2unix(fp)
+            fc = tools.load(fp)
+            self.assertNotIn("\r\n", fc)
+
+        self.assertEquals("a line\notherline\n", str(tools.load(fp)))
+
+    def unix_to_dos_conanfile_test(self):
+        client = TestClient()
+        conanfile = """
+import os
+from conans import ConanFile, tools
+
+class HelloConan(ConanFile):
+    name = "Hello"
+    version = "0.1"
+    exports_sources = "file.txt"
+
+    def build(self):
+        assert("\\r\\n" in tools.load("file.txt"))
+        tools.dos2unix("file.txt")
+        assert("\\r\\n" not in tools.load("file.txt"))
+        tools.unix2dos("file.txt")
+        assert("\\r\\n" in tools.load("file.txt"))
+"""
+        client.save({"conanfile.py": conanfile, "file.txt": "hello\r\n"})
+        client.run("create . user/channel")
+
 
 class GitToolTest(unittest.TestCase):
 
@@ -1213,6 +1320,29 @@ class GitToolTest(unittest.TestCase):
         git = Git(subfolder)
         self.assertEqual(root_path, git.get_repo_root())
 
+    def test_is_pristine(self):
+        root_path, _ = create_local_git_repo({"myfile": "anything"})
+
+        git = Git(root_path)
+        self.assertTrue(git.is_pristine())
+
+        save(os.path.join(root_path, "other_file"), "content")
+        self.assertFalse(git.is_pristine())
+
+        git.run("add .")
+        self.assertFalse(git.is_pristine())
+
+        git.run('commit -m "commit"')
+        self.assertTrue(git.is_pristine())
+
+    def test_is_local_repository(self):
+        root_path, _ = create_local_git_repo({"myfile": "anything"})
+
+        git = Git(temp_folder())
+        git.clone(root_path)
+        self.assertTrue(git.is_local_repository())
+        # TODO: Check that with remote one it is working too
+
     def test_clone_git(self):
         path, _ = create_local_git_repo({"myfile": "contents"})
         tmp = temp_folder()
@@ -1415,75 +1545,324 @@ class HelloConan(ConanFile):
         client.run("create . user/channel", ignore_error=True)
         self.assertIn("specify a branch to checkout", client.out)
 
-    def unix_to_dos_unit_test(self):
-
-        def save_file(contents):
-            tmp = temp_folder()
-            filepath = os.path.join(tmp, "a_file.txt")
-            save(filepath, contents)
-            return filepath
 
-        fp = save_file(b"a line\notherline\n")
-        if not tools.os_info.is_windows:
-            import subprocess
-            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
-            self.assertIn("ASCII text", str(output))
-            self.assertNotIn("CRLF", str(output))
+class SVNToolTestsBasic(SVNLocalRepoTestCase):
+
+    def test_clone(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+        self.assertTrue(os.path.exists(os.path.join(tmp_folder, 'myfile')))
+
+    def test_revision_number(self):
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        rev = int(svn.get_revision())
+        self.create_project(files={'another_file': "content"})
+        svn.run("update")
+        rev2 = int(svn.get_revision())
+        self.assertEqual(rev2, rev + 1)
+
+    def test_repo_url(self):
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        remote_url = svn.get_remote_url()
+        self.assertEqual(remote_url.lower(), self.repo_url.lower())
+
+        svn2 = SVN(folder=self.gimme_tmp(create=False))
+        svn2.checkout(url=remote_url)  # clone using quoted url
+        self.assertEqual(svn2.get_remote_url().lower(), self.repo_url.lower())
+
+    def test_repo_project_url(self):
+        project_url, _ = self.create_project(files={"myfile": "content"})
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=project_url)
+        self.assertEqual(svn.get_remote_url().lower(), project_url.lower())
+
+    def test_checkout(self):
+        # Ensure we have several revisions in the repository
+        self.create_project(files={'file': "content"})
+        self.create_project(files={'file': "content"})
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        rev = int(svn.get_revision())
+        svn.update(revision=rev - 1)  # Checkout previous revision
+        self.assertTrue(int(svn.get_revision()), rev-1)
+
+    def test_clone_over_dirty_directory(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+
+        new_file = os.path.join(tmp_folder, "new_file")
+        with open(new_file, "w") as f:
+            f.write("content")
+
+        mod_file = os.path.join(tmp_folder, "myfile")
+        with open(mod_file, "a") as f:
+            f.write("new content")
+
+        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))
+
+    def test_excluded_files(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+
+        # Add untracked file
+        new_file = os.path.join(tmp_folder, str(uuid.uuid4()))
+        with open(new_file, "w") as f:
+            f.write("content")
+
+        # Add ignore file
+        file_to_ignore = str(uuid.uuid4())
+        with open(os.path.join(tmp_folder, file_to_ignore), "w") as f:
+            f.write("content")
+        svn.run("propset svn:ignore {} .".format(file_to_ignore))
+        svn.run('commit -m "add ignored file"')
+
+        excluded_files = svn.excluded_files()
+        self.assertIn(file_to_ignore, excluded_files)
+        self.assertNotIn('.svn', excluded_files)
+        self.assertEqual(len(excluded_files), 1)
 
-            tools.unix2dos(fp)
-            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
-            self.assertIn("ASCII text", str(output))
-            self.assertIn("CRLF", str(output))
-        else:
-            fc = tools.load(fp)
-            self.assertNotIn("\r\n", fc)
-            tools.unix2dos(fp)
-            fc = tools.load(fp)
-            self.assertIn("\r\n", fc)
-
-        self.assertEquals("a line\r\notherline\r\n", str(tools.load(fp)))
-
-        fp = save_file(b"a line\r\notherline\r\n")
-        if not tools.os_info.is_windows:
-            import subprocess
-            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
-            self.assertIn("ASCII text", str(output))
-            self.assertIn("CRLF", str(output))
+    def test_credentials(self):
+        svn = SVN(folder=self.gimme_tmp(), username="ada", password="lovelace")
+        url_credentials = svn.get_url_with_credentials("https://some.url.com")
+        self.assertEquals(url_credentials, "https://ada:lovelace@some.url.com")
 
-            tools.dos2unix(fp)
-            output = subprocess.check_output(["file", fp], stderr=subprocess.STDOUT)
-            self.assertIn("ASCII text", str(output))
-            self.assertNotIn("CRLF", str(output))
+    def test_verify_ssl(self):
+        class MyRunner(object):
+            def __init__(self, svn):
+                self.calls = []
+                self._runner = svn._runner
+                svn._runner = self
+
+            def __call__(self, command, *args, **kwargs):
+                self.calls.append(command)
+                return self._runner(command, *args, **kwargs)
+
+        project_url, _ = self.create_project(files={'myfile': "contents",
+                                                    'subdir/otherfile': "content"})
+
+        svn = SVN(folder=self.gimme_tmp(), username="peter", password="otool", verify_ssl=True)
+        runner = MyRunner(svn)
+        svn.checkout(url=project_url)
+        self.assertNotIn("--trust-server-cert-failures=unknown-ca", runner.calls[1])
+
+        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:
+            self.assertIn("--trust-server-cert-failures=unknown-ca", runner.calls[1])
         else:
-            fc = tools.load(fp)
-            self.assertIn("\r\n", fc)
-            tools.dos2unix(fp)
-            fc = tools.load(fp)
-            self.assertNotIn("\r\n", fc)
+            self.assertIn("--trust-server-cert", runner.calls[1])
 
-        self.assertEquals("a line\notherline\n", str(tools.load(fp)))
-
-    def unix_to_dos_conanfile_test(self):
-        client = TestClient()
-        conanfile = """
+    def test_repo_root(self):
+        project_url, _ = self.create_project(files={'myfile': "contents",
+                                                    'subdir/otherfile': "content"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+
+        path = os.path.realpath(tmp_folder).replace('\\', '/').lower()
+        self.assertEqual(path, svn.get_repo_root().lower())
+
+        # SVN instantiated in a subfolder
+        svn2 = SVN(folder=os.path.join(tmp_folder, 'subdir'))
+        self.assertFalse(svn2.folder == tmp_folder)
+        path = os.path.realpath(tmp_folder).replace('\\', '/').lower()
+        self.assertEqual(path, svn2.get_repo_root().lower())
+
+    def test_is_local_repository(self):
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        self.assertTrue(svn.is_local_repository())
+
+        # TODO: Test not local repository
+
+    def test_last_changed_revision(self):
+        project_url, _ = self.create_project(files={'project1/myfile': "contents",
+                                                    'project2/myfile': "content",
+                                                    'project2/subdir1/myfile': "content",
+                                                    'project2/subdir2/myfile': "content",
+                                                    })
+        prj1 = SVN(folder=self.gimme_tmp())
+        prj1.checkout(url='/'.join([project_url, 'project1']))
+
+        prj2 = SVN(folder=self.gimme_tmp())
+        prj2.checkout(url='/'.join([project_url, 'project2']))
+
+        self.assertEqual(prj1.get_last_changed_revision(), prj2.get_last_changed_revision())
+
+        # Modify file in one subfolder of prj2
+        with open(os.path.join(prj2.folder, "subdir1", "myfile"), "a") as f:
+            f.write("new content")
+        prj2.run('commit -m "add to file"')
+        prj2.run('update')
+        prj1.run('update')
+
+        self.assertNotEqual(prj1.get_last_changed_revision(), prj2.get_last_changed_revision())
+        self.assertEqual(prj1.get_revision(), prj2.get_revision())
+
+        # Instantiate a SVN in the other subfolder
+        prj2_subdir2 = SVN(folder=os.path.join(prj2.folder, "subdir2"))
+        prj2_subdir2.run('update')
+        self.assertEqual(prj2.get_last_changed_revision(),
+                         prj2_subdir2.get_last_changed_revision())
+        self.assertNotEqual(prj2.get_last_changed_revision(use_wc_root=False),
+                            prj2_subdir2.get_last_changed_revision(use_wc_root=False))
+
+    def test_branch(self):
+        project_url, _ = self.create_project(files={'prj1/trunk/myfile': "contents",
+                                                    'prj1/branches/my_feature/myfile': "",
+                                                    'prj1/branches/issue3434/myfile': "",
+                                                    'prj1/tags/v12.3.4/myfile': "",
+                                                    })
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'trunk']))
+        self.assertEqual("trunk", svn.get_branch())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'branches', 'my_feature']))
+        self.assertEqual("branches/my_feature", svn.get_branch())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'branches', 'issue3434']))
+        self.assertEqual("branches/issue3434", svn.get_branch())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'tags', 'v12.3.4']))
+        self.assertEqual("tags/v12.3.4", svn.get_branch())
+
+
+class SVNToolTestsPristine(SVNLocalRepoTestCase):
+
+    def test_checkout(self):
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        self.assertTrue(svn.is_pristine())
+
+    def test_checkout_project(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+        self.assertTrue(svn.is_pristine())
+
+    def test_modified_file(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=project_url)
+        with open(os.path.join(tmp_folder, "myfile"), "a") as f:
+            f.write("new content")
+        self.assertFalse(svn.is_pristine())
+
+    def test_untracked_file(self):
+        self.create_project(files={'myfile': "contents"})
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=tmp_folder)
+        svn.checkout(url=self.repo_url)
+        with open(os.path.join(tmp_folder, "not_tracked.txt"), "w") as f:
+            f.write("content")
+        self.assertTrue(svn.is_pristine())
+
+    def test_ignored_file(self):
+        tmp_folder = self.gimme_tmp()
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url=self.repo_url)
+        file_to_ignore = "secret.txt"
+        with open(os.path.join(tmp_folder, file_to_ignore), "w") as f:
+            f.write("content")
+        svn.run("propset svn:ignore {} .".format(file_to_ignore))
+        self.assertFalse(svn.is_pristine())  # Folder properties have been modified
+        svn.run('commit -m "add ignored file"')
+        self.assertTrue(svn.is_pristine())
+
+    def test_conflicted_file(self):
+        project_url, _ = self.create_project(files={'myfile': "contents"})
+
+        def work_on_project(tmp_folder):
+            svn = SVN(folder=tmp_folder)
+            svn.checkout(url=project_url)
+            self.assertTrue(svn.is_pristine())
+            with open(os.path.join(tmp_folder, "myfile"), "a") as f:
+                f.write("random content: {}".format(uuid.uuid4()))
+            return svn
+
+        # Two users working on the same project
+        svn1 = work_on_project(self.gimme_tmp())
+        svn2 = work_on_project(self.gimme_tmp())
+
+        # User1 is faster
+        svn1.run('commit -m "user1 commit"')
+        self.assertFalse(svn1.is_pristine())
+        svn1.run('update')  # Yes, we need to update local copy in order to have the same revision everywhere.
+        self.assertTrue(svn1.is_pristine())
+
+        # User2 updates and get a conflicted file
+        svn2.run('update')
+        self.assertFalse(svn2.is_pristine())
+        svn2.run('revert . -R')
+        self.assertTrue(svn2.is_pristine())
+
+
+class SVNToolsTestsRecipe(SVNLocalRepoTestCase):
+
+    conanfile = """
 import os
 from conans import ConanFile, tools
 
 class HelloConan(ConanFile):
     name = "Hello"
     version = "0.1"
-    exports_sources = "file.txt"
+    exports_sources = "other"
+
+    def source(self):
+        svn = tools.SVN({svn_folder})
+        svn.checkout(url="{svn_url}")
 
     def build(self):
-        assert("\\r\\n" in tools.load("file.txt"))
-        tools.dos2unix("file.txt")
-        assert("\\r\\n" not in tools.load("file.txt"))
-        tools.unix2dos("file.txt")
-        assert("\\r\\n" in tools.load("file.txt"))
+        assert(os.path.exists("{file_path}"))
+        assert(os.path.exists("other"))
 """
-        client.save({"conanfile.py": conanfile, "file.txt": "hello\r\n"})
+
+    def test_clone_root_folder(self):
+        tmp_folder = self.gimme_tmp()
+        client = TestClient()
+        client.runner('svn co "{}" "{}"'.format(self.repo_url, tmp_folder))
+        save(os.path.join(tmp_folder, "file.h"), "contents")
+        client.runner("svn add file.h", cwd=tmp_folder)
+        client.runner('svn commit -m "message"', cwd=tmp_folder)
+
+        conanfile = self.conanfile.format(svn_folder="", svn_url=self.repo_url,
+                                          file_path="file.h")
+        client.save({"conanfile.py": conanfile, "other": "hello"})
         client.run("create . user/channel")
 
+    def test_clone_subfolder(self):
+        tmp_folder = self.gimme_tmp()
+        client = TestClient()
+        client.runner('svn co "{}" "{}"'.format(self.repo_url, tmp_folder))
+        save(os.path.join(tmp_folder, "file.h"), "contents")
+        client.runner("svn add file.h", cwd=tmp_folder)
+        client.runner('svn commit -m "message"', cwd=tmp_folder)
+
+        conanfile = self.conanfile.format(svn_folder="\"src\"", svn_url=self.repo_url,
+                                          file_path="src/file.h")
+        client.save({"conanfile.py": conanfile, "other": "hello"})
+        client.run("create . user/channel")
+
+
+class CollectLibTestCase(unittest.TestCase):
     def collect_libs_test(self):
         conanfile = ConanFileMock()
         # Without package_folder
@@ -1546,3 +1925,4 @@ class HelloConan(ConanFile):
         self.assertEqual(["mylib"], result)
         self.assertIn("WARN: Lib folder doesn't exist, can't collect libraries: %s"
                       % no_folder_path, conanfile.output)
+
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index df7b77c08..629a6bee5 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -5,16 +5,22 @@ import shutil
 import sys
 import threading
 import uuid
+import errno
+import stat
 from collections import Counter
 from contextlib import contextmanager
 from io import StringIO
+import subprocess
+import unittest
+import tempfile
+import platform
 
 import bottle
 import requests
 import six
 import time
 from mock import Mock
-from six.moves.urllib.parse import urlsplit, urlunsplit
+from six.moves.urllib.parse import urlsplit, urlunsplit, quote
 from webtest.app import TestApp
 
 from conans import __version__ as CLIENT_VERSION, tools
@@ -28,8 +34,9 @@ from conans.client.plugin_manager import PluginManager
 from conans.client.remote_registry import RemoteRegistry
 from conans.client.rest.conan_requester import ConanRequester
 from conans.client.rest.uploader_downloader import IterableToFileAdapter
-from conans.client.tools.scm import Git
+from conans.client.tools.scm import Git, SVN
 from conans.client.userio import UserIO
+from conans.client.tools.files import chdir
 from conans.model.version import Version
 from conans.test.server.utils.server_launcher import (TESTING_REMOTE_PRIVATE_USER,
                                                       TESTING_REMOTE_PRIVATE_PASS,
@@ -45,6 +52,7 @@ from conans.model.manifest import FileTreeManifest
 from conans.client.tools.win import get_cased_path
 
 
+
 def inc_recipe_manifest_timestamp(client_cache, conan_ref, inc_time):
     conan_ref = ConanFileReference.loads(str(conan_ref))
     path = client_cache.export(conan_ref)
@@ -306,6 +314,60 @@ def create_local_git_repo(files=None, branch=None, submodules=None, folder=None)
     return tmp.replace("\\", "/"), git.get_revision()
 
 
+def handleRemoveReadonly(func, path, exc):  # TODO: May promote to conan tools?
+    # Credit: https://stackoverflow.com/questions/1213706/what-user-do-python-scripts-run-as-in-windows
+    excvalue = exc[1]
+    if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES:
+        os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
+        func(path)
+    else:
+        raise
+
+
+class SVNLocalRepoTestCase(unittest.TestCase):
+    path_with_spaces = True
+
+    def _create_local_svn_repo(self):
+        repo_url = os.path.join(self._tmp_folder, 'repo_server')
+        subprocess.check_output('svnadmin create "{}"'.format(repo_url), shell=True)
+        return SVN.file_protocol + quote(repo_url.replace("\\", "/"), safe='/:')
+
+    def gimme_tmp(self, create=True):
+        tmp = os.path.join(self._tmp_folder, str(uuid.uuid4()))
+        if create:
+            os.makedirs(tmp)
+        return tmp
+
+    def create_project(self, files, rel_project_path=None, commit_msg='default commit message', delete_checkout=True):
+        tmp_dir = self.gimme_tmp()
+        try:
+            rel_project_path = rel_project_path or str(uuid.uuid4())
+            # Do not use SVN class as it is what we will be testing
+            subprocess.check_output('svn co "{url}" "{path}"'.format(url=self.repo_url, path=tmp_dir), shell=True)
+            tmp_project_dir = os.path.join(tmp_dir, rel_project_path)
+            os.makedirs(tmp_project_dir)
+            save_files(tmp_project_dir, files)
+            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()
+            project_url = self.repo_url + "/" + quote(rel_project_path.replace("\\", "/"))
+            return project_url, rev
+        finally:
+            if delete_checkout:
+                shutil.rmtree(tmp_dir, ignore_errors=False, onerror=handleRemoveReadonly)
+
+    def run(self, *args, **kwargs):
+        tmp_folder = tempfile.mkdtemp(suffix='_conans')
+        try:
+            self._tmp_folder = os.path.join(tmp_folder, 'path with spaces' if self.path_with_spaces else 'pathwithoutspaces')
+            os.makedirs(self._tmp_folder)
+            self.repo_url = self._create_local_svn_repo()
+            super(SVNLocalRepoTestCase, self).run(*args, **kwargs)
+        finally:
+            shutil.rmtree(tmp_folder, ignore_errors=False, onerror=handleRemoveReadonly)
+
+
 class MockedUserIO(UserIO):
 
     """