chiark / gitweb /
build/checkupdates/update: log current fdroiddata commit to wiki
[fdroidserver.git] / fdroidserver / checkupdates.py
index 7f07dc0aaf089245a2f00b9d56561a9b605cf29f..72c8b22b4fa3407542e9fc55404e3a0985ffe530 100644 (file)
@@ -1,5 +1,4 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
 #
 # checkupdates.py - part of the FDroid server tools
 # Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import sys
 import os
 import re
-import urllib2
+import urllib.request
+import urllib.error
 import time
 import subprocess
+import sys
 from argparse import ArgumentParser
 import traceback
-import HTMLParser
+import html
 from distutils.version import LooseVersion
 import logging
+import copy
+import urllib.parse
 
-import common
-import metadata
-from common import VCSException, FDroidException
-from metadata import MetaDataException
+from . import _
+from . import common
+from . import metadata
+from .exception import VCSException, NoSubmodulesException, FDroidException, MetaDataException
 
 
 # Check for a new version by looking at a document retrieved via HTTP.
@@ -43,30 +45,37 @@ def check_http(app):
 
     try:
 
-        if 'Update Check Data' not in app:
+        if not app.UpdateCheckData:
             raise FDroidException('Missing Update Check Data')
 
-        urlcode, codeex, urlver, verex = app['Update Check Data'].split('|')
+        urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|')
+        parsed = urllib.parse.urlparse(urlcode)
+        if not parsed.netloc or not parsed.scheme or parsed.scheme != 'https':
+            raise FDroidException(_('UpdateCheckData has invalid URL: {url}').format(url=urlcode))
+        if urlver != '.':
+            parsed = urllib.parse.urlparse(urlver)
+            if not parsed.netloc or not parsed.scheme or parsed.scheme != 'https':
+                raise FDroidException(_('UpdateCheckData has invalid URL: {url}').format(url=urlcode))
 
         vercode = "99999999"
         if len(urlcode) > 0:
             logging.debug("...requesting {0}".format(urlcode))
-            req = urllib2.Request(urlcode, None)
-            resp = urllib2.urlopen(req, None, 20)
-            page = resp.read()
+            req = urllib.request.Request(urlcode, None)
+            resp = urllib.request.urlopen(req, None, 20)
+            page = resp.read().decode('utf-8')
 
             m = re.search(codeex, page)
             if not m:
                 raise FDroidException("No RE match for version code")
-            vercode = m.group(1)
+            vercode = m.group(1).strip()
 
         version = "??"
         if len(urlver) > 0:
             if urlver != '.':
                 logging.debug("...requesting {0}".format(urlver))
-                req = urllib2.Request(urlver, None)
-                resp = urllib2.urlopen(req, None, 20)
-                page = resp.read()
+                req = urllib.request.Request(urlver, None)
+                resp = urllib.request.urlopen(req, None, 20)
+                page = resp.read().decode('utf-8')
 
             m = re.search(verex, page)
             if not m:
@@ -76,19 +85,10 @@ def check_http(app):
         return (version, vercode)
 
     except FDroidException:
-        msg = "Could not complete http check for app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
+        msg = "Could not complete http check for app {0} due to unknown error: {1}".format(app.id, traceback.format_exc())
         return (None, msg)
 
 
-def app_matches_packagename(app, package):
-    if not package:
-        return False
-    appid = app['Update Check Name'] or app['id']
-    if appid == "Ignore":
-        return True
-    return appid == package
-
-
 # Check for a new version by looking at the tags in the source repo.
 # Whether this can be used reliably or not depends on
 # the development procedures used by the project's developers. Use it with
@@ -99,43 +99,51 @@ def check_tags(app, pattern):
 
     try:
 
-        if app['Repo Type'] == 'srclib':
-            build_dir = os.path.join('build', 'srclib', app['Repo'])
-            repotype = common.getsrclibvcs(app['Repo'])
+        if app.RepoType == 'srclib':
+            build_dir = os.path.join('build', 'srclib', app.Repo)
+            repotype = common.getsrclibvcs(app.Repo)
         else:
-            build_dir = os.path.join('build', app['id'])
-            repotype = app['Repo Type']
+            build_dir = os.path.join('build', app.id)
+            repotype = app.RepoType
 
         if repotype not in ('git', 'git-svn', 'hg', 'bzr'):
             return (None, 'Tags update mode only works for git, hg, bzr and git-svn repositories currently', None)
 
-        if repotype == 'git-svn' and ';' not in app['Repo']:
+        if repotype == 'git-svn' and ';' not in app.Repo:
             return (None, 'Tags update mode used in git-svn, but the repo was not set up with tags', None)
 
         # Set up vcs interface and make sure we have the latest code...
-        vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
+        vcs = common.getvcs(app.RepoType, app.Repo, build_dir)
 
         vcs.gotorevision(None)
 
-        flavours = []
-        if len(app['builds']) > 0:
-            if app['builds'][-1]['gradle']:
-                flavours = app['builds'][-1]['gradle']
+        last_build = app.get_last_build()
+
+        try_init_submodules(app, last_build, vcs)
 
         hpak = None
         htag = None
         hver = None
         hcode = "0"
 
-        tags = vcs.gettags()
+        tags = []
+        if repotype == 'git':
+            tags = vcs.latesttags()
+        else:
+            tags = vcs.gettags()
+        if not tags:
+            return (None, "No tags found", None)
+
         logging.debug("All tags: " + ','.join(tags))
         if pattern:
             pat = re.compile(pattern)
             tags = [tag for tag in tags if pat.match(tag)]
+            if not tags:
+                return (None, "No matching tags found", None)
             logging.debug("Matching tags: " + ','.join(tags))
 
-        if repotype in ('git',):
-            tags = vcs.latesttags(tags, 5)
+        if len(tags) > 5 and repotype == 'git':
+            tags = tags[:5]
             logging.debug("Latest tags: " + ','.join(tags))
 
         for tag in tags:
@@ -143,11 +151,13 @@ def check_tags(app, pattern):
             vcs.gotorevision(tag)
 
             for subdir in possible_subdirs(app):
-                root_dir = os.path.join(build_dir, subdir)
-                paths = common.manifest_paths(root_dir, flavours)
-                version, vercode, package = \
-                    common.parse_androidmanifests(paths, app['Update Check Ignore'])
-                if app_matches_packagename(app, package) and version and vercode:
+                if subdir == '.':
+                    root_dir = build_dir
+                else:
+                    root_dir = os.path.join(build_dir, subdir)
+                paths = common.manifest_paths(root_dir, last_build.gradle)
+                version, vercode, package = common.parse_androidmanifests(paths, app)
+                if vercode:
                     logging.debug("Manifest exists in subdir '{0}'. Found version {1} ({2})"
                                   .format(subdir, version, vercode))
                     if int(vercode) > int(hcode):
@@ -163,10 +173,10 @@ def check_tags(app, pattern):
         return (None, "Couldn't find any version information", None)
 
     except VCSException as vcse:
-        msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
+        msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse)
         return (None, msg, None)
     except Exception:
-        msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
+        msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc())
         return (None, msg, None)
 
 
@@ -180,15 +190,15 @@ def check_repomanifest(app, branch=None):
 
     try:
 
-        if app['Repo Type'] == 'srclib':
-            build_dir = os.path.join('build', 'srclib', app['Repo'])
-            repotype = common.getsrclibvcs(app['Repo'])
+        if app.RepoType == 'srclib':
+            build_dir = os.path.join('build', 'srclib', app.Repo)
+            repotype = common.getsrclibvcs(app.Repo)
         else:
-            build_dir = os.path.join('build', app['id'])
-            repotype = app['Repo Type']
+            build_dir = os.path.join('build', app.id)
+            repotype = app.RepoType
 
         # Set up vcs interface and make sure we have the latest code...
-        vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
+        vcs = common.getvcs(app.RepoType, app.Repo, build_dir)
 
         if repotype == 'git':
             if branch:
@@ -201,71 +211,69 @@ def check_repomanifest(app, branch=None):
         elif repotype == 'bzr':
             vcs.gotorevision(None)
 
-        root_dir = build_dir
-        flavours = []
-        if len(app['builds']) > 0:
-            if app['builds'][-1]['subdir']:
-                root_dir = os.path.join(build_dir, app['builds'][-1]['subdir'])
-            if app['builds'][-1]['gradle']:
-                flavours = app['builds'][-1]['gradle']
+        last_build = metadata.Build()
+        if len(app.builds) > 0:
+            last_build = app.builds[-1]
 
-        if not os.path.isdir(root_dir):
-            return (None, "Subdir '" + app['builds'][-1]['subdir'] + "'is not a valid directory")
+        try_init_submodules(app, last_build, vcs)
 
-        paths = common.manifest_paths(root_dir, flavours)
+        hpak = None
+        hver = None
+        hcode = "0"
+        for subdir in possible_subdirs(app):
+            if subdir == '.':
+                root_dir = build_dir
+            else:
+                root_dir = os.path.join(build_dir, subdir)
+            paths = common.manifest_paths(root_dir, last_build.gradle)
+            version, vercode, package = common.parse_androidmanifests(paths, app)
+            if vercode:
+                logging.debug("Manifest exists in subdir '{0}'. Found version {1} ({2})"
+                              .format(subdir, version, vercode))
+                if int(vercode) > int(hcode):
+                    hpak = package
+                    hcode = str(int(vercode))
+                    hver = version
 
-        version, vercode, package = \
-            common.parse_androidmanifests(paths, app['Update Check Ignore'])
-        if not package:
+        if not hpak:
             return (None, "Couldn't find package ID")
-        if not app_matches_packagename(app, package):
-            return (None, "Package ID mismatch - got {0}".format(package))
-        if not version:
-            return (None, "Couldn't find latest version name")
-        if not vercode:
-            if "Ignore" == version:
-                return (None, "Latest version is ignored")
-            return (None, "Couldn't find latest version code")
-
-        vercode = str(int(vercode))
-
-        logging.debug("Manifest exists. Found version {0} ({1})".format(version, vercode))
-
-        return (version, vercode)
+        if hver:
+            return (hver, hcode)
+        return (None, "Couldn't find any version information")
 
     except VCSException as vcse:
-        msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
+        msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse)
         return (None, msg)
     except Exception:
-        msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
+        msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc())
         return (None, msg)
 
 
-def check_repotrunk(app, branch=None):
+def check_repotrunk(app):
 
     try:
-        if app['Repo Type'] == 'srclib':
-            build_dir = os.path.join('build', 'srclib', app['Repo'])
-            repotype = common.getsrclibvcs(app['Repo'])
+        if app.RepoType == 'srclib':
+            build_dir = os.path.join('build', 'srclib', app.Repo)
+            repotype = common.getsrclibvcs(app.Repo)
         else:
-            build_dir = os.path.join('build', app['id'])
-            repotype = app['Repo Type']
+            build_dir = os.path.join('build', app.id)
+            repotype = app.RepoType
 
         if repotype not in ('git-svn', ):
             return (None, 'RepoTrunk update mode only makes sense in git-svn repositories')
 
         # Set up vcs interface and make sure we have the latest code...
-        vcs = common.getvcs(app['Repo Type'], app['Repo'], build_dir)
+        vcs = common.getvcs(app.RepoType, app.Repo, build_dir)
 
         vcs.gotorevision(None)
 
         ref = vcs.getref()
         return (ref, ref)
     except VCSException as vcse:
-        msg = "VCS error while scanning app {0}: {1}".format(app['id'], vcse)
+        msg = "VCS error while scanning app {0}: {1}".format(app.id, vcse)
         return (None, msg)
     except Exception:
-        msg = "Could not scan app {0} due to unknown error: {1}".format(app['id'], traceback.format_exc())
+        msg = "Could not scan app {0} due to unknown error: {1}".format(app.id, traceback.format_exc())
         return (None, msg)
 
 
@@ -274,23 +282,22 @@ def check_repotrunk(app, branch=None):
 # the details of the current version.
 def check_gplay(app):
     time.sleep(15)
-    url = 'https://play.google.com/store/apps/details?id=' + app['id']
+    url = 'https://play.google.com/store/apps/details?id=' + app.id
     headers = {'User-Agent': 'Mozilla/5.0 (X11; Linux i686; rv:18.0) Gecko/20100101 Firefox/18.0'}
-    req = urllib2.Request(url, None, headers)
+    req = urllib.request.Request(url, None, headers)
     try:
-        resp = urllib2.urlopen(req, None, 20)
-        page = resp.read()
-    except urllib2.HTTPError, e:
+        resp = urllib.request.urlopen(req, None, 20)
+        page = resp.read().decode()
+    except urllib.error.HTTPError as e:
         return (None, str(e.code))
-    except Exception, e:
+    except Exception as e:
         return (None, 'Failed:' + str(e))
 
     version = None
 
     m = re.search('itemprop="softwareVersion">[ ]*([^<]+)[ ]*</div>', page)
     if m:
-        html_parser = HTMLParser.HTMLParser()
-        version = html_parser.unescape(m.group(1))
+        version = html.unescape(m.group(1))
 
     if version == 'Varies with device':
         return (None, 'Device-variable version, cannot use this method')
@@ -300,75 +307,81 @@ def check_gplay(app):
     return (version.strip(), None)
 
 
+def try_init_submodules(app, last_build, vcs):
+    """Try to init submodules if the last build entry used them.
+    They might have been removed from the app's repo in the meantime,
+    so if we can't find any submodules we continue with the updates check.
+    If there is any other error in initializing them then we stop the check.
+    """
+    if last_build.submodules:
+        try:
+            vcs.initsubmodules()
+        except NoSubmodulesException:
+            logging.info("No submodules present for {}".format(app.Name))
+
+
 # Return all directories under startdir that contain any of the manifest
 # files, and thus are probably an Android project.
 def dirs_with_manifest(startdir):
-    for r, d, f in os.walk(startdir):
-        if any(m in f for m in [
+    for root, dirs, files in os.walk(startdir):
+        if any(m in files for m in [
                 'AndroidManifest.xml', 'pom.xml', 'build.gradle']):
-            yield r
+            yield root
 
 
 # Tries to find a new subdir starting from the root build_dir. Returns said
 # subdir relative to the build dir if found, None otherwise.
 def possible_subdirs(app):
 
-    if app['Repo Type'] == 'srclib':
-        build_dir = os.path.join('build', 'srclib', app['Repo'])
+    if app.RepoType == 'srclib':
+        build_dir = os.path.join('build', 'srclib', app.Repo)
     else:
-        build_dir = os.path.join('build', app['id'])
-
-    flavours = []
-    if len(app['builds']) > 0:
-        build = app['builds'][-1]
-        if build['gradle']:
-            flavours = build['gradle']
-        subdir = build['subdir']
-        if subdir and os.path.isdir(os.path.join(build_dir, subdir)):
-            logging.debug("Adding possible subdir %s" % subdir)
-            yield subdir
+        build_dir = os.path.join('build', app.id)
+
+    last_build = app.get_last_build()
 
     for d in dirs_with_manifest(build_dir):
-        m_paths = common.manifest_paths(d, flavours)
-        package = common.parse_androidmanifests(m_paths, app['Update Check Ignore'])[2]
-        if app_matches_packagename(app, package):
+        m_paths = common.manifest_paths(d, last_build.gradle)
+        package = common.parse_androidmanifests(m_paths, app)[2]
+        if package is not None:
             subdir = os.path.relpath(d, build_dir)
-            if subdir == '.':
-                continue
             logging.debug("Adding possible subdir %s" % subdir)
             yield subdir
 
 
 def fetch_autoname(app, tag):
 
-    if not app["Repo Type"] or app['Update Check Mode'] in ('None', 'Static'):
+    if not app.RepoType or app.UpdateCheckMode in ('None', 'Static'):
         return None
 
-    if app['Repo Type'] == 'srclib':
-        app_dir = os.path.join('build', 'srclib', app['Repo'])
+    if app.RepoType == 'srclib':
+        build_dir = os.path.join('build', 'srclib', app.Repo)
     else:
-        app_dir = os.path.join('build', app['id'])
+        build_dir = os.path.join('build', app.id)
 
     try:
-        vcs = common.getvcs(app["Repo Type"], app["Repo"], app_dir)
+        vcs = common.getvcs(app.RepoType, app.Repo, build_dir)
         vcs.gotorevision(tag)
     except VCSException:
         return None
 
-    flavours = []
-    if len(app['builds']) > 0:
-        if app['builds'][-1]['subdir']:
-            app_dir = os.path.join(app_dir, app['builds'][-1]['subdir'])
-        if app['builds'][-1]['gradle']:
-            flavours = app['builds'][-1]['gradle']
+    last_build = app.get_last_build()
 
-    logging.debug("...fetch auto name from " + app_dir)
-    new_name = common.fetch_real_name(app_dir, flavours)
+    logging.debug("...fetch auto name from " + build_dir)
+    new_name = None
+    for subdir in possible_subdirs(app):
+        if subdir == '.':
+            root_dir = build_dir
+        else:
+            root_dir = os.path.join(build_dir, subdir)
+        new_name = common.fetch_real_name(root_dir, last_build.gradle)
+        if new_name is not None:
+            break
     commitmsg = None
     if new_name:
         logging.debug("...got autoname '" + new_name + "'")
-        if new_name != app['Auto Name']:
-            app['Auto Name'] = new_name
+        if new_name != app.AutoName:
+            app.AutoName = new_name
             if not commitmsg:
                 commitmsg = "Set autoname of {0}".format(common.getappname(app))
     else:
@@ -377,7 +390,7 @@ def fetch_autoname(app, tag):
     return commitmsg
 
 
-def checkupdates_app(app, first=True):
+def checkupdates_app(app):
 
     # If a change is made, commitmsg should be set to a description of it.
     # Only if this is set will changes be written back to the metadata.
@@ -387,10 +400,12 @@ def checkupdates_app(app, first=True):
     msg = None
     vercode = None
     noverok = False
-    mode = app['Update Check Mode']
+    mode = app.UpdateCheckMode
     if mode.startswith('Tags'):
         pattern = mode[5:] if len(mode) > 4 else None
         (version, vercode, tag) = check_tags(app, pattern)
+        if version == 'Unknown':
+            version = tag
         msg = vercode
     elif mode == 'RepoManifest':
         (version, vercode) = check_repomanifest(app)
@@ -413,9 +428,12 @@ def checkupdates_app(app, first=True):
         version = None
         msg = 'Invalid update check method'
 
-    if version and vercode and app['Vercode Operation']:
+    if version and vercode and app.VercodeOperation:
+        if not common.VERCODE_OPERATION_RE.match(app.VercodeOperation):
+            raise MetaDataException(_('Invalid VercodeOperation: {field}')
+                                    .format(field=app.VercodeOperation))
         oldvercode = str(int(vercode))
-        op = app['Vercode Operation'].replace("%c", oldvercode)
+        op = app.VercodeOperation.replace("%c", oldvercode)
         vercode = str(eval(op))
         logging.debug("Applied vercode operation: %s -> %s" % (oldvercode, vercode))
 
@@ -427,16 +445,18 @@ def checkupdates_app(app, first=True):
 
     updating = False
     if version is None:
-        logmsg = "...{0} : {1}".format(app['id'], msg)
+        logmsg = "...{0} : {1}".format(app.id, msg)
         if noverok:
             logging.info(logmsg)
         else:
             logging.warn(logmsg)
-    elif vercode == app['Current Version Code']:
+    elif vercode == app.CurrentVersionCode:
         logging.info("...up to date")
     else:
-        app['Current Version'] = version
-        app['Current Version Code'] = str(int(vercode))
+        logging.debug("...updating - old vercode={0}, new vercode={1}".format(
+            app.CurrentVersionCode, vercode))
+        app.CurrentVersion = version
+        app.CurrentVersionCode = str(int(vercode))
         updating = True
 
     commitmsg = fetch_autoname(app, tag)
@@ -448,8 +468,10 @@ def checkupdates_app(app, first=True):
         commitmsg = 'Update CV of %s to %s' % (name, ver)
 
     if options.auto:
-        mode = app['Auto Update Mode']
-        if mode in ('None', 'Static'):
+        mode = app.AutoUpdateMode
+        if not app.CurrentVersionCode:
+            logging.warn("Can't auto-update app with no current version code: " + app.id)
+        elif mode in ('None', 'Static'):
             pass
         elif mode.startswith('Version '):
             pattern = mode[8:]
@@ -462,37 +484,34 @@ def checkupdates_app(app, first=True):
                 suffix = ''
             gotcur = False
             latest = None
-            for build in app['builds']:
-                if build['vercode'] == app['Current Version Code']:
+            for build in app.builds:
+                if int(build.versionCode) >= int(app.CurrentVersionCode):
                     gotcur = True
-                if not latest or int(build['vercode']) > int(latest['vercode']):
+                if not latest or int(build.versionCode) > int(latest.versionCode):
                     latest = build
 
-            if int(latest['vercode']) > int(app['Current Version Code']):
+            if int(latest.versionCode) > int(app.CurrentVersionCode):
                 logging.info("Refusing to auto update, since the latest build is newer")
 
             if not gotcur:
-                newbuild = latest.copy()
-                if 'origlines' in newbuild:
-                    del newbuild['origlines']
-                newbuild['disable'] = False
-                newbuild['vercode'] = app['Current Version Code']
-                newbuild['version'] = app['Current Version'] + suffix
-                logging.info("...auto-generating build for " + newbuild['version'])
-                commit = pattern.replace('%v', newbuild['version'])
-                commit = commit.replace('%c', newbuild['vercode'])
-                newbuild['commit'] = commit
-                app['builds'].append(newbuild)
+                newbuild = copy.deepcopy(latest)
+                newbuild.disable = False
+                newbuild.versionCode = app.CurrentVersionCode
+                newbuild.versionName = app.CurrentVersion + suffix
+                logging.info("...auto-generating build for " + newbuild.versionName)
+                commit = pattern.replace('%v', newbuild.versionName)
+                commit = commit.replace('%c', newbuild.versionCode)
+                newbuild.commit = commit
+                app.builds.append(newbuild)
                 name = common.getappname(app)
                 ver = common.getcvname(app)
                 commitmsg = "Update %s to %s" % (name, ver)
         else:
-            logging.warn('Invalid auto update mode "' + mode + '" on ' + app['id'])
+            logging.warn('Invalid auto update mode "' + mode + '" on ' + app.id)
 
     if commitmsg:
-        metadatapath = os.path.join('metadata', app['id'] + '.txt')
-        with open(metadatapath, 'w') as f:
-            metadata.write_metadata('txt', f, app)
+        metadatapath = os.path.join('metadata', app.id + '.txt')
+        metadata.write_metadata(metadatapath, app)
         if options.commit:
             logging.info("Commiting update for " + metadatapath)
             gitcmd = ["git", "commit", "-m", commitmsg]
@@ -500,12 +519,44 @@ def checkupdates_app(app, first=True):
                 gitcmd.extend(['--author', config['auto_author']])
             gitcmd.extend(["--", metadatapath])
             if subprocess.call(gitcmd) != 0:
-                logging.error("Git commit failed")
-                sys.exit(1)
+                raise FDroidException("Git commit failed")
+
+
+def update_wiki(gplaylog, locallog):
+    if config.get('wiki_server') and config.get('wiki_path'):
+        try:
+            import mwclient
+            site = mwclient.Site((config['wiki_protocol'], config['wiki_server']),
+                                 path=config['wiki_path'])
+            site.login(config['wiki_user'], config['wiki_password'])
+
+            # Write a page with the last build log for this version code
+            wiki_page_path = 'checkupdates_' + time.strftime('%s', start_timestamp)
+            newpage = site.Pages[wiki_page_path]
+            txt = ''
+            txt += "* command line: <code>" + ' '.join(sys.argv) + "</code>\n"
+            txt += common.get_git_describe_link()
+            txt += "* started at " + common.get_wiki_timestamp(start_timestamp) + '\n'
+            txt += "* completed at " + common.get_wiki_timestamp() + '\n'
+            txt += "\n\n"
+            txt += common.get_android_tools_version_log()
+            txt += "\n\n"
+            if gplaylog:
+                txt += '== --gplay check ==\n\n'
+                txt += gplaylog
+            if locallog:
+                txt += '== local source check ==\n\n'
+                txt += locallog
+            newpage.save(txt, summary='Run log')
+            newpage = site.Pages['checkupdates']
+            newpage.save('#REDIRECT [[' + wiki_page_path + ']]', summary='Update redirect')
+        except Exception as e:
+            logging.error(_('Error while attempting to publish log: %s') % e)
 
 
 config = None
 options = None
+start_timestamp = time.gmtime()
 
 
 def main():
@@ -515,26 +566,38 @@ def main():
     # Parse command line...
     parser = ArgumentParser(usage="%(prog)s [options] [APPID [APPID ...]]")
     common.setup_global_opts(parser)
-    parser.add_argument("appid", nargs='*', help="app-id to check for updates")
+    parser.add_argument("appid", nargs='*', help=_("applicationId to check for updates"))
     parser.add_argument("--auto", action="store_true", default=False,
-                        help="Process auto-updates")
+                        help=_("Process auto-updates"))
     parser.add_argument("--autoonly", action="store_true", default=False,
-                        help="Only process apps with auto-updates")
+                        help=_("Only process apps with auto-updates"))
     parser.add_argument("--commit", action="store_true", default=False,
-                        help="Commit changes")
+                        help=_("Commit changes"))
+    parser.add_argument("--allow-dirty", action="store_true", default=False,
+                        help=_("Run on git repo that has uncommitted changes"))
     parser.add_argument("--gplay", action="store_true", default=False,
-                        help="Only print differences with the Play Store")
+                        help=_("Only print differences with the Play Store"))
+    metadata.add_metadata_arguments(parser)
     options = parser.parse_args()
+    metadata.warnings_action = options.W
 
     config = common.read_config(options)
 
+    if not options.allow_dirty:
+        status = subprocess.check_output(['git', 'status', '--porcelain'])
+        if status:
+            logging.error(_('Build metadata git repo has uncommited changes!'))
+            sys.exit(1)
+
     # Get all apps...
     allapps = metadata.read_metadata()
 
     apps = common.read_app_args(options.appid, allapps, False)
 
+    gplaylog = ''
     if options.gplay:
-        for app in apps:
+        for appid, app in apps.items():
+            gplaylog += '* ' + appid + '\n'
             version, reason = check_gplay(app)
             if version is None:
                 if reason == '404':
@@ -542,7 +605,7 @@ def main():
                 else:
                     logging.info("{0} encountered a problem: {1}".format(common.getappname(app), reason))
             if version is not None:
-                stored = app['Current Version']
+                stored = app.CurrentVersion
                 if not stored:
                     logging.info("{0} has no Current Version but has version {1} on the Play Store"
                                  .format(common.getappname(app), version))
@@ -556,19 +619,31 @@ def main():
                     else:
                         logging.info("{0} has the same version {1} on the Play Store"
                                      .format(common.getappname(app), version))
+        update_wiki(gplaylog, None)
         return
 
-    for appid, app in apps.iteritems():
+    locallog = ''
+    for appid, app in apps.items():
 
-        if options.autoonly and app['Auto Update Mode'] in ('None', 'Static'):
-            logging.debug("Nothing to do for {0}...".format(appid))
+        if options.autoonly and app.AutoUpdateMode in ('None', 'Static'):
+            logging.debug(_("Nothing to do for {appid}.").format(appid=appid))
             continue
 
-        logging.info("Processing " + appid + '...')
+        msg = _("Processing {appid}").format(appid=appid)
+        logging.info(msg)
+        locallog += '* ' + msg + '\n'
+
+        try:
+            checkupdates_app(app)
+        except Exception as e:
+            msg = _("...checkupdate failed for {appid} : {error}").format(appid=appid, error=e)
+            logging.error(msg)
+            locallog += msg + '\n'
+
+    update_wiki(None, locallog)
 
-        checkupdates_app(app)
+    logging.info(_("Finished"))
 
-    logging.info("Finished.")
 
 if __name__ == "__main__":
     main()