Repository: conan-io/conan

Issue:
Support for git describe --tags in tools.Git
In our CI release flow, we'd like to number Conan packages based on a git tag applied in our repo.

Right now if I apply a tag ```0.0.8```, then fetch that and and use ```git branch``` I see:

```
$ git branch
* (HEAD detached at 0.0.8)
```

whereas if I use ```git describe --tags``` I see:

```
0.0.8
```

I'd like to be able to use ```0.0.8``` as the Conan package version.

If I try using the example from https://docs.conan.io/en/latest/howtos/capture_version.html, namely:

```
return "%s_%s" % (git.get_branch(), git.get_revision())
```

on that checked out tag I see:

```
E:\DevelopBp\mmaguir1\src\nx\replicant>conan create . PORT/stable
version[HEAD (no branch)_0dfb63c9c341536e528ef63f2e66746cf83c8539]
ERROR: Value provided for package version, 'HEAD (no branch)_0dfb63c9c341536e528ef63f2e66746cf83c8539' (type Version), is too long. Valid names must contain at most 51 characters.
```


Alternatively, if there's some magic combination of the existing [Git tools](https://docs.conan.io/en/latest/reference/tools.html#tools-git) that will let me retrieve that tag name, that would be much appreciated!

Related:
https://github.com/conan-io/conan/issues/548 "generating version from git"
https://github.com/conan-io/conan/issues/1764 "Add Git helpers"


To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [v1.8.4 Windows Server 2012 git 2.12.1] 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.




Discussion:
A tag is just a pointer to a commit. To develop a `get_tag()`, probably we should use the `git describe` command:

``` 
The command finds the most recent tag that is reachable from a commit. 
If the tag points to the commit, then only the tag is shown. 
Otherwise, it suffixes the tag name with the number of additional commits on top of 
the tagged object and the abbreviated object name of the most recent commit.
``` 
e.g, on the Conan master branch:

```
$ git describe
1.9.2
```

e.g on the Conan develop branch:

```
$ git describe
1.9.1-31-g6bf1fc73
``` 

So we can detect if a simple "pure" `tag` is returned or return `None`  otherwise.

Actually, if the purpose is to create a version from the Git repo automatically, the `git describe` command is a flexible way to get a human-readable version string for a given commit. So a method like `Git.describe` with some keyword arguments for customization (derived from command line arguments) should be simple enough and solve most of the problem for Git repos.
For example, we usually use `git describe --always --tags --match 'v*.*'` in CI to get a version tag, so if the build is made from a tag matching 'v*.*', the version will be exactly the tag name.
Thanks for the info @himikof 

I'm not sure if Conan should provide a complete wrapper over `git` command line or just provide access to the features that we are consuming from SCM feature (or Conan core features). I'm afraid about introducing too many arguments, custom function calls that address single use cases,... it will be hard to maintain.

There is an function member that can be used to call _ad hoc_ commands, for example, given the proposal by @himikof we can just use `Git` class in the following way:

```python
def get_version():
    git = Git(os.path.dirname(__file__))  # Initialize git object in the same folder where this file is.
    v = git.run("describe --always --tags --match 'v*.*'")
    # May need some more logic in case it is not a matching tag
    return v
```


> I'm not sure if Conan should provide a complete wrapper over git command line or just provide access to the features that we are consuming from SCM feature (or Conan core features).

I would say both, but as you said, only for general purpose stuff. I think that getting the tag (if the repo is "in a commit tag") is not a single use case but a general purpose one. If you want more specific behavior like controlling the name of the tags (beginning with 'v') and so on, you would need to implement it yourself. 

For me, as I said, I would like a function to return a "pure" tag name (when the last commit is tagged) or None otherwise (If the tag doesn't belong to the latest commit).
Ok, then we should provide the same functionality for `SVN` too (it is almost implemented). Also, as the function `def get_branch(self):` is raising if no branch can be retrieved, I would suggest to raise here too if no tag can be found.
> Ok, then we should provide the same functionality for SVN too 

We could, but they are not strictly attached as this is not part of the SCM common feature. I think we should start with the git one and implement later the SVN when someone requests it. But if it is trivial and almost done we could include it also. If you have already some implementation, please refer it or paste it here to have better information when we assign this feature to a release.

> is raising if no branch can be retrieved, I would suggest to raise here too if no tag can be found.

Ok, if someone asks for a tag I assume he expects to get one, I'm ok raising.
Pasting here current implementation for `SVN::get_branch()`:

```python
    def get_branch(self):
        url = self._show_item('relative-url')
        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)))

```

If I can remember it well, it was introduced by @climblinne. We should split this functionality into `get_tag` and `get_branch` as the pattern at this moment is matching both (it can break something, but from the name if the function it is clear that the aim of it was retrieving branch name).
`get_branch()` isn't either directly used by the `scm` feature, right? In that case, yes, we should split it.

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
@@ -176,6 +176,15 @@ def get_branch(self):
         except Exception as e:
             raise ConanException("Unable to get git branch from %s: %s" % (self.folder, str(e)))
 
+    def get_tag(self):
+        self._check_git_repo()
+        try:
+            status = self.run("describe --exact-match --tags")
+            tag = status.strip()
+            return tag
+        except Exception:
+            return None
+
     def _check_git_repo(self):
         try:
             self.run("status")
@@ -333,20 +342,24 @@ def get_last_changed_revision(self, use_wc_root=True):
             return self._show_item(item='last-changed-revision')
 
     def get_branch(self):
-        url = self._show_item('relative-url')
+        item = self._get_item("branches/[^/]+|trunk", "branch")
+        return item.replace("branches/", "") if item else None
+
+    def get_tag(self):
+        item = self._get_item("tags/[^/]+", "tag")
+        return item.replace("tags/", "") if item else None
+
+    def _get_item(self, pattern, item_name):
         try:
-            pattern = "(tags|branches)/[^/]+|trunk"
-            branch = re.search(pattern, url)
-            
-            if branch is None:
-                return None
-            else:
-                return branch.group(0)
+            url = self._show_item('relative-url')
         except Exception as e:
-            raise ConanException("Unable to get svn branch from %s: %s" % (self.folder, str(e)))
+            raise ConanException("Unable to get svn %s from %s: %s"
+                                 % (item_name, self.folder, str(e)))
+        item = re.search(pattern, url)
+        return item.group(0) if item else None
 
     def _check_svn_repo(self):
         try:
             self.run("info")
         except Exception:
-            raise ConanException("Not a valid SVN repository")
\ No newline at end of file
+            raise ConanException("Not a valid SVN repository")


Test patch:
diff --git a/conans/test/unittests/util/tools_test.py b/conans/test/unittests/util/tools_test.py
--- a/conans/test/unittests/util/tools_test.py
+++ b/conans/test/unittests/util/tools_test.py
@@ -1646,6 +1646,49 @@ def build(self):
         self.assertIn("specify a branch to checkout", client.out)
 
 
+class GitToolsTests(unittest.TestCase):
+
+    def setUp(self):
+        self.folder, self.rev = create_local_git_repo({'myfile.txt': "contents"})
+
+    def test_no_tag(self):
+        """
+        No tags has been created in repo
+        """
+        git = Git(folder=self.folder)
+        tag = git.get_tag()
+        self.assertIsNone(tag)
+
+    def test_in_tag(self):
+        """
+        Current checkout is on a tag
+        """
+        git = Git(folder=self.folder)
+        git.run("tag 0.0.0")
+        tag = git.get_tag()
+        self.assertEqual("0.0.0", tag)
+
+    def test_in_branch_with_tag(self):
+        """
+        Tag is defined but current commit is ahead of it
+        """
+        git = Git(folder=self.folder)
+        git.run("tag 0.0.0")
+        save(os.path.join(self.folder, "file.txt"), "")
+        git.run("add .")
+        git.run("commit -m \"new file\"")
+        tag = git.get_tag()
+        self.assertIsNone(tag)
+
+    def test_get_tag_no_git_repo(self):
+        """
+        Try to get tag out of a git repo
+        """
+        git = Git(folder=temp_folder())
+        with self.assertRaisesRegexp(ConanException, "Not a valid git repository"):
+            git.get_tag()
+
+
 @attr("slow")
 @attr('svn')
 class SVNToolTestsBasic(SVNLocalRepoTestCase):
@@ -1832,15 +1875,46 @@ def test_branch(self):
 
         svn = SVN(folder=self.gimme_tmp())
         svn.checkout(url='/'.join([project_url, 'prj1', 'branches', 'my_feature']))
-        self.assertEqual("branches/my_feature", svn.get_branch())
+        self.assertEqual("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())
+        self.assertEqual("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())
+        self.assertIsNone(svn.get_branch())
+
+        svn = SVN(folder=self.gimme_tmp())
+        with self.assertRaisesRegexp(ConanException, "Unable to get svn branch"):
+            svn.get_branch()
+
+    def test_tag(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.assertIsNone(svn.get_tag())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'branches', 'my_feature']))
+        self.assertIsNone(svn.get_tag())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'branches', 'issue3434']))
+        self.assertIsNone(svn.get_tag())
+
+        svn = SVN(folder=self.gimme_tmp())
+        svn.checkout(url='/'.join([project_url, 'prj1', 'tags', 'v12.3.4']))
+        self.assertEqual("v12.3.4", svn.get_tag())
+
+        svn = SVN(folder=self.gimme_tmp())
+        with self.assertRaisesRegexp(ConanException, "Unable to get svn tag"):
+            svn.get_tag()
+
 
 @attr("slow")
 @attr('svn')