diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bee8a64b79a99590d5303307144172cfe824fbf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/external_updater.py b/external_updater.py index 4200676ff84edf240b9f89084253f568005d2671..4ceaa21d3bcd7b54e49eebdc48851b595a2e4d61 100644 --- a/external_updater.py +++ b/external_updater.py @@ -20,15 +20,17 @@ updater.sh update kotlinc import argparse import os +import subprocess from google.protobuf import text_format # pylint: disable=import-error import fileutils +from git_updater import GitUpdater from github_archive_updater import GithubArchiveUpdater import updater_utils -UPDATERS = [GithubArchiveUpdater] +UPDATERS = [GithubArchiveUpdater, GitUpdater] def color_string(string, color): @@ -82,21 +84,28 @@ def check_update(proj_path): end='') updater = build_updater(proj_path) if updater is None: - return + return (None, None) try: - latest = updater.get_latest_version() - current = updater.get_current_version() - except IOError as e: - print('{} {}.'.format(color_string('Failed to check latest version.', - 'FAILED'), - e)) - return - - if current != latest: - print('{} Current version: {}. Latest version: {}.'. format( - color_string('New version found.', 'SUCCESS'), current, latest)) - else: - print('No new version. Current version: {}.'.format(latest)) + new_version = updater.check() + if new_version: + print(color_string(' New version found.', 'SUCCESS')) + else: + print(' No new version.') + return (updater, new_version) + except IOError as err: + print('{} {}.'.format(color_string('Failed.', 'FAILED'), + err)) + return (None, None) + except subprocess.CalledProcessError as err: + print( + '{} {}\nstdout: {}\nstderr: {}.'.format( + color_string( + 'Failed.', + 'FAILED'), + err, + err.stdout, + err.stderr)) + return (None, None) def check(args): @@ -108,33 +117,12 @@ def check(args): def update(args): """Handler for update command.""" - updater = build_updater(args.path) + updater, new_version = check_update(args.path) if updater is None: return - try: - latest = updater.get_latest_version() - current = updater.get_current_version() - except IOError as e: - print('{} {}.'.format( - color_string('Failed to check latest version.', - 'FAILED'), - e)) + if not new_version and not args.force: return - if current == latest and not args.force: - print( - '{} for {}. Current version {} is latest. ' - 'Use --force to update anyway.'.format( - color_string( - 'Nothing to update', - 'FAILED'), - args.path, - current)) - return - - print('{} from version {} to version {}.{}'.format( - color_string('Updating', 'SUCCESS'), args.path, current, latest)) - updater.update() diff --git a/git_updater.py b/git_updater.py new file mode 100644 index 0000000000000000000000000000000000000000..bde48a70d498c0b18cfb4ff80f7548891e0e3a82 --- /dev/null +++ b/git_updater.py @@ -0,0 +1,118 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module to check updates from Git upstream.""" + + +import datetime + +import fileutils +import git_utils +import metadata_pb2 # pylint: disable=import-error + + +class GitUpdater(): + """Updater for Git upstream.""" + + def __init__(self, url, proj_path, metadata): + if url.type != metadata_pb2.URL.GIT: + raise ValueError('Only support GIT upstream.') + self.proj_path = proj_path + self.metadata = metadata + self.upstream_url = url + self.upstream_remote_name = None + self.android_remote_name = None + self.latest_commit = None + + def _setup_remote(self): + remotes = git_utils.list_remotes(self.proj_path) + for name, url in remotes.items(): + if url == self.upstream_url.value: + self.upstream_remote_name = name + + # Guess android remote name. + if '/platform/external/' in url: + self.android_remote_name = name + + if self.upstream_remote_name is None: + self.upstream_remote_name = "update_origin" + git_utils.add_remote(self.proj_path, self.upstream_remote_name, + self.upstream_url.value) + + git_utils.fetch(self.proj_path, + [self.upstream_remote_name, self.android_remote_name]) + + def check(self): + """Checks upstream and returns whether a new version is available.""" + + self._setup_remote() + commits = git_utils.get_commits_ahead( + self.proj_path, self.upstream_remote_name + '/master', + self.android_remote_name + '/master') + + if not commits: + return False + + self.latest_commit = commits[0] + commit_time = git_utils.get_commit_time(self.proj_path, commits[-1]) + time_behind = datetime.datetime.now() - commit_time + print('{} commits ({} days) behind.'.format( + len(commits), time_behind.days), end='') + return True + + def _write_metadata(self, path): + updated_metadata = metadata_pb2.MetaData() + updated_metadata.CopyFrom(self.metadata) + updated_metadata.third_party.version = self.latest_commit + fileutils.write_metadata(path, updated_metadata) + + def update(self): + """Updates the package. + + 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) + if commits: + print('Warning! {} is {} commits ahead of {}. {}'.format( + merge_branch, len(commits), upstream_branch, commits)) + + commits = git_utils.get_commits_ahead( + self.proj_path, upstream_branch, merge_branch) + if commits: + print('Warning! {} is {} commits behind of {}.'.format( + merge_branch, len(commits), upstream_branch)) + + self._write_metadata(self.proj_path) + print(""" +This tool only updates METADATA. Run the following command to update: + git merge {merge_branch} + +To check all local changes: + git diff {merge_branch} HEAD +""".format(merge_branch=merge_branch)) diff --git a/git_utils.py b/git_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8e8f96d827f7ef2cda33a1589cd196787a586ad9 --- /dev/null +++ b/git_utils.py @@ -0,0 +1,77 @@ +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Helper functions to communicate with Git.''' + +import datetime +import subprocess + + +def _run(cmd, cwd): + """Runs a command with stdout and stderr redirected.""" + return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True, cwd=cwd) + +def fetch(proj_path, remote_names): + """Runs git fetch. + + Args: + proj_path: Path to Git repository. + remote_names: Array of string to specify remote names. + """ + _run(['git', 'fetch', '--multiple'] + remote_names, cwd=proj_path) + +def add_remote(proj_path, name, url): + """Adds a git remote. + + Args: + proj_path: Path to Git repository. + name: Name of the new remote. + url: Url of the new remote. + """ + _run(['git', 'remote', 'add', name, url], cwd=proj_path) + +def list_remotes(proj_path): + """Lists all Git remotes. + + Args: + proj_path: Path to Git repository. + + Returns: + A dict from remote name to remote url. + """ + out = _run(['git', 'remote', '-v'], proj_path) + lines = out.stdout.decode('utf-8').splitlines() + return dict([line.split()[0:2] for line in lines]) + +def get_commits_ahead(proj_path, branch, base_branch): + """Lists commits in `branch` but not `base_branch`.""" + out = _run(['git', 'rev-list', '--left-only', + '{}...{}'.format(branch, base_branch)], + proj_path) + return out.stdout.decode('utf-8').splitlines() + +def get_commit_time(proj_path, commit): + """Gets commit time of one commit.""" + out = _run(['git', 'show', '-s', '--format=%ct', commit], cwd=proj_path) + return datetime.datetime.fromtimestamp(int(out.stdout)) + +def list_remote_branches(proj_path, remote_name): + """Lists all branches for a remote.""" + out = _run(['git', 'branch', '-r'], cwd=proj_path) + lines = out.stdout.decode('utf-8').splitlines() + stripped = [line.strip() for line in lines] + remote_path = remote_name + '/' + remote_path_len = len(remote_path) + return [line[remote_path_len:] for line in stripped + if line.startswith(remote_path)] diff --git a/github_archive_updater.py b/github_archive_updater.py index 7f0bb770994c6f3a25df65cb1d97df0e9e5a1514..3abe5be6bb349a13f7cbf7c5f87557722fe1da58 100644 --- a/github_archive_updater.py +++ b/github_archive_updater.py @@ -58,7 +58,7 @@ class GithubArchiveUpdater(): except IndexError: raise ValueError('Url format is not supported.') - def get_latest_version(self): + def _get_latest_version(self): """Checks upstream and returns the latest version name we found.""" url = 'https://api.github.com/repos/{}/{}/releases/latest'.format( @@ -67,7 +67,7 @@ class GithubArchiveUpdater(): self.data = json.loads(request.read().decode()) return self.data[self.VERSION_FIELD] - def get_current_version(self): + def _get_current_version(self): """Returns the latest version name recorded in METADATA.""" return self.metadata.third_party.version @@ -80,10 +80,21 @@ class GithubArchiveUpdater(): metadata_url.value = url fileutils.write_metadata(path, updated_metadata) + def check(self): + """Checks update for package. + + Returns True if a new version is available. + """ + latest = self._get_latest_version() + current = self._get_current_version() + print('Current version: {}. Latest version: {}'.format( + current, latest), end='') + return current != latest + def update(self): """Updates the package. - Has to call get_latest_version() before this function. + Has to call check() before this function. """ supported_assets = [ diff --git a/updater.sh b/updater.sh index 2a3da75381596e88e3e81e8d90d8d5913f4e4832..4f03c89710f3ceb112118da17f68ed57ea8ee68b 100755 --- a/updater.sh +++ b/updater.sh @@ -16,5 +16,6 @@ cd $(dirname "$0")/../.. source build/envsetup.sh +lunch aosp_arm-eng mmma tools/external_updater out/soong/host/linux-x86/bin/external_updater $@