Repository: conan-io/conan

Issue:
[SCM][revisions] Supporting the `scm revsion` as package `version`
Disclaimer: I discussed this issue on Slack and post it now here to keep track of it. Thank you @lasote for the great discussion!

Problem
-------
I currently have several recipes which "manually" capture the git revision based on [James' idea](https://github.com/memsharded/sourcegit/blob/master/conanfile.py) when exporting/creating the package. Given that conan now has `scm` support I want to get rid of my custom code and use the upstream feature instead.

However, the way I use my `scm revision` is apparently not supported by the current `scm` implementation. Namely, when building packages from the development branch, I directly use the `scm revision` as package `version`. See https://gist.github.com/niosHD/dc669dcb6c8d92b51e3d9c6bab774e90 for a sketch of one of my conanfiles.

Background
----------
My reasoning on why I want to use the `scm revision` as `version` in non-release packages is as follows. For me a version should uniquely identify a package. However, version numbers are only assigned when a release is made. Subsequently, non-release builds (e.g., CI, builds on the developer machine of a certain feature branch, ...) typically have no real `version`.

Sill, as conan really needs a `version`, some kind of version has to be generated. This can either be done by generating a new unique version (e.g., something like a build number) or by using some arbitrary fixed string (e.g., branch name, the next expected version as working title, ...) which results in continuously replacing the package. (Note that `package revisions` will dramatically improve the usefulness/reproducibility of the second option.)

We opted for the first approach since it enables us to also package/test/distribute non-release versions in a reproducible way via conan. However, instead of generating an arbitrary build number, which again does not directly convey what has been packaged, we use the `scm revision` which states exactly what is in the respective package.

Given this approach, we basically then use two types of dependency management. The first one is similar to a git submodule approach where a very specific `(scm) version` is required by a package. An example for such a dependency is `clang`'s dependency on `llvm` where both repositories are typically always incremented in lockstep. Using the `scm version` as package `version` enables us to easily manage these kind of dependencies. Of course CI builds are used to verify that we get working results.

The second dependency type is more dynamic. For some libraries, where the API is more or less stable, we simple want to depend on the latest sucessfully CI tested version of this library (in a certain git branch). There we use conan's package alias, which gets updated by our CI server after each successful build, with the branch name as version. The using such an alias target is then done requiring `libfoo/develop@...` or `libfoo/feature-x@...` (we use gitflow on some projects) and means that the actually used version depends on the build time of the consuming package. However, given that the alias packages are transparently resolved, the resulting package is still fully reproducible and records the actual versions. Furthermore, CI is used again to ensure that we get working results anyway.


Discussion:
I have been thinking about this and maybe we can use the scm feature normally and for capturing the commit, use directly the internal SCM() or Git() classes. I'll assign it to the 1.6 to give it a try, maybe it doesn't require any feature to make it work, but maybe to make it easy.
Sorry, I did not really understand what you are proposing, but thank you for looking into it.

As it turns out, I currently can only use the scm module when the version field is removed from the recipe. However, this requires that I have to provide the scm version, as part of the full package reference, on the command line which is error prone. Furthermore, it completely obsoletes the scm module in the first place. Therefore, any improvement is highly appreciated!


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
@@ -1,8 +1,9 @@
 import os
-import subprocess
-from subprocess import CalledProcessError, PIPE, STDOUT
 
+import re
+import subprocess
 from six.moves.urllib.parse import urlparse, quote_plus
+from subprocess import CalledProcessError, PIPE, STDOUT
 
 from conans.client.tools.env import no_op, environment_append
 from conans.client.tools.files import chdir
@@ -80,6 +81,7 @@ def clone(self, url, branch=None, submodule=None):
         return output
 
     def checkout(self, element):
+        self._check_git_repo()
         # Element can be a tag, branch or commit
         return self.run('checkout "%s"' % element)
 
@@ -98,6 +100,7 @@ def excluded_files(self):
         return tmp
 
     def get_remote_url(self, remote_name=None):
+        self._check_git_repo()
         remote_name = remote_name or "origin"
         try:
             remotes = self.run("remote -v")
@@ -113,10 +116,29 @@ def get_remote_url(self, remote_name=None):
             pass
         return None
 
-    def get_revision(self):
+    def get_commit(self):
+        self._check_git_repo()
         try:
             commit = self.run("rev-parse HEAD")
             commit = commit.strip()
             return commit
         except Exception as e:
             raise ConanException("Unable to get git commit from %s\n%s" % (self.folder, str(e)))
+
+    get_revision = get_commit
+
+    def _check_git_repo(self):
+        if not os.path.exists(os.path.join(self.folder, ".git")):
+            raise ConanException("Not a git repository")
+
+    def get_branch(self):
+        self._check_git_repo()
+        try:
+            status = self.run("status -bs --porcelain")
+            # ## feature/scm_branch...myorigin/feature/scm_branch
+            branch = status.splitlines()[0].split("...")[0].strip("#").strip()
+            # Replace non alphanumeric
+            branch = re.sub('[^0-9a-zA-Z]+', '_', branch)
+            return branch
+        except Exception as e:
+            raise ConanException("Unable to get git branch from %s\n%s" % (self.folder, str(e)))


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
@@ -1163,6 +1163,30 @@ def _create_paths():
         self.assertTrue(os.path.exists(os.path.join(submodule_path, "submodule")))
         self.assertTrue(os.path.exists(os.path.join(subsubmodule_path, "subsubmodule")))
 
+    def git_to_capture_branch_test(self):
+        conanfile = """
+from conans import ConanFile, tools
+
+def get_version():
+    git = tools.Git()
+    try:
+        return "%s_%s" % (git.get_branch(), git.get_revision())
+    except:
+        return None
+
+class HelloConan(ConanFile):
+    name = "Hello"
+    version = get_version()
+
+    def build(self):
+        assert("r3le_ase__" in self.version)
+        assert(len(self.version) == 50)
+"""
+        path, _ = create_local_git_repo({"conanfile.py": conanfile}, branch="r3le-ase-")
+        client = TestClient()
+        client.current_folder = path
+        client.run("create . user/channel")
+
     def git_helper_in_recipe_test(self):
         client = TestClient()
         git_repo = temp_folder()