[Updater] Support git tag

If the upstream is GIT and version is not a hash, updater will try to
list upstream tags to find a newer version. And suggest to merge from
that tag.

Change-Id: I34651815c761c78fd0485df0a84adc086a810e9e
Test: Change googletest/METADATA to follow upstream releases. And run updater.
diff --git a/git_updater.py b/git_updater.py
index bde48a7..4c878c8 100644
--- a/git_updater.py
+++ b/git_updater.py
@@ -19,6 +19,7 @@
 import fileutils
 import git_utils
 import metadata_pb2    # pylint: disable=import-error
+import updater_utils
 
 
 class GitUpdater():
@@ -32,7 +33,8 @@
         self.upstream_url = url
         self.upstream_remote_name = None
         self.android_remote_name = None
-        self.latest_commit = None
+        self.new_version = None
+        self.merge_from = None
 
     def _setup_remote(self):
         remotes = git_utils.list_remotes(self.proj_path)
@@ -56,6 +58,25 @@
         """Checks upstream and returns whether a new version is available."""
 
         self._setup_remote()
+        if git_utils.is_commit(self.metadata.third_party.version):
+            # Update to remote head.
+            return self._check_head()
+
+        # Update to latest version tag.
+        return self._check_tag()
+
+    def _check_tag(self):
+        tags = git_utils.list_remote_tags(self.proj_path,
+                                          self.upstream_remote_name)
+        current_ver = self.metadata.third_party.version
+        self.new_version = updater_utils.get_latest_version(
+            current_ver, tags)
+        self.merge_from = self.new_version
+        print('Current version: {}. Latest version: {}'.format(
+            current_ver, self.new_version), end='')
+        return self.new_version != current_ver
+
+    def _check_head(self):
         commits = git_utils.get_commits_ahead(
             self.proj_path, self.upstream_remote_name + '/master',
             self.android_remote_name + '/master')
@@ -63,7 +84,19 @@
         if not commits:
             return False
 
-        self.latest_commit = commits[0]
+        self.new_version = commits[0]
+
+        # See whether we have a local upstream.
+        branches = git_utils.list_remote_branches(
+            self.proj_path, self.android_remote_name)
+        upstreams = [
+            branch for branch in branches if branch.startswith('upstream-')]
+        if upstreams:
+            self.merge_from = '{}/{}'.format(
+                self.android_remote_name, upstreams[0])
+        else:
+            self.merge_from = 'update_origin/master'
+
         commit_time = git_utils.get_commit_time(self.proj_path, commits[-1])
         time_behind = datetime.datetime.now() - commit_time
         print('{} commits ({} days) behind.'.format(
@@ -73,7 +106,7 @@
     def _write_metadata(self, path):
         updated_metadata = metadata_pb2.MetaData()
         updated_metadata.CopyFrom(self.metadata)
-        updated_metadata.third_party.version = self.latest_commit
+        updated_metadata.third_party.version = self.new_version
         fileutils.write_metadata(path, updated_metadata)
 
     def update(self):
@@ -81,32 +114,19 @@
 
         Has to call check() before this function.
         """
-        # See whether we have a local upstream.
-        branches = git_utils.list_remote_branches(
-            self.proj_path, self.android_remote_name)
-        upstreams = [
-            branch for branch in branches if branch.startswith('upstream-')]
-        if len(upstreams) == 1:
-            merge_branch = '{}/{}'.format(
-                self.android_remote_name, upstreams[0])
-        elif not upstreams:
-            merge_branch = 'update_origin/master'
-        else:
-            raise ValueError('Ambiguous upstream branch. ' + upstreams)
-
         upstream_branch = self.upstream_remote_name + '/master'
 
         commits = git_utils.get_commits_ahead(
-            self.proj_path, merge_branch, upstream_branch)
+            self.proj_path, self.merge_from, upstream_branch)
         if commits:
-            print('Warning! {} is {} commits ahead of {}. {}'.format(
-                merge_branch, len(commits), upstream_branch, commits))
+            print('{} is {} commits ahead of {}. {}'.format(
+                self.merge_from, len(commits), upstream_branch, commits))
 
         commits = git_utils.get_commits_ahead(
-            self.proj_path, upstream_branch, merge_branch)
+            self.proj_path, upstream_branch, self.merge_from)
         if commits:
-            print('Warning! {} is {} commits behind of {}.'.format(
-                merge_branch, len(commits), upstream_branch))
+            print('{} is {} commits behind of {}.'.format(
+                self.merge_from, len(commits), upstream_branch))
 
         self._write_metadata(self.proj_path)
         print("""
@@ -115,4 +135,4 @@
 
 To check all local changes:
     git diff {merge_branch} HEAD
-""".format(merge_branch=merge_branch))
+""".format(merge_branch=self.merge_from))
diff --git a/git_utils.py b/git_utils.py
index b2f6bbb..abfcea7 100644
--- a/git_utils.py
+++ b/git_utils.py
@@ -14,6 +14,7 @@
 '''Helper functions to communicate with Git.'''
 
 import datetime
+import re
 import subprocess
 
 
@@ -81,3 +82,33 @@
     remote_path_len = len(remote_path)
     return [line[remote_path_len:] for line in stripped
             if line.startswith(remote_path)]
+
+
+def _parse_remote_tag(line):
+    tag_prefix = 'refs/tags/'
+    tag_suffix = '^{}'
+    try:
+        line = line[line.index(tag_prefix):]
+    except ValueError:
+        return None
+    line = line[len(tag_prefix):]
+    if line.endswith(tag_suffix):
+        line = line[:-len(tag_suffix)]
+    return line
+
+
+def list_remote_tags(proj_path, remote_name):
+    """Lists all tags for a remote."""
+    out = _run(['git', "ls-remote", "--tags", remote_name],
+               cwd=proj_path)
+    lines = out.stdout.decode('utf-8').splitlines()
+    tags = [_parse_remote_tag(line) for line in lines]
+    return list(set(tags))
+
+
+COMMIT_PATTERN = r'^[a-f0-9]{40}$'
+COMMIT_RE = re.compile(COMMIT_PATTERN)
+
+
+def is_commit(commit):
+    return bool(COMMIT_RE.match(commit))
diff --git a/updater_utils.py b/updater_utils.py
index e9d9620..cb1de54 100644
--- a/updater_utils.py
+++ b/updater_utils.py
@@ -14,6 +14,7 @@
 """Helper functions for updaters."""
 
 import os
+import re
 import subprocess
 import sys
 
@@ -55,3 +56,43 @@
             sys.argv[0]),
         'update_package.sh')
     subprocess.check_call(['bash', script_path, source_dir, target_dir])
+
+
+VERSION_PATTERN = (r'^(?P<prefix>[^\d]*)' +
+                   r'(?P<version>\d+(\.\d+)*)' +
+                   r'(?P<suffix>.*)$')
+VERSION_RE = re.compile(VERSION_PATTERN)
+
+
+def parse_version(version):
+    match = VERSION_RE.match(version)
+    if match is None:
+        raise ValueError('Invalid version.')
+    try:
+        return match.group('prefix', 'version', 'suffix')
+    except IndexError:
+        raise ValueError('Invalid version.')
+
+
+def _match_and_get_version(prefix, suffix, version):
+    try:
+        version_prefix, version, version_suffix = parse_version(version)
+    except ValueError:
+        return []
+
+    if version_prefix != prefix or version_suffix != suffix:
+        return []
+
+    return [int(v) for v in version.split('.')]
+
+
+def get_latest_version(old_version, version_list):
+    old_prefix, _, old_suffix = parse_version(old_version)
+
+    latest = max(version_list + [old_version],
+                 key=lambda ver: _match_and_get_version(
+                     old_prefix, old_suffix, ver))
+    if not latest:
+        return None
+
+    return latest