From 75388609b31f6c7a2d107721bdbbe6a71dc7d64e Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Fri, 17 Feb 2017 14:46:59 -0300 Subject: [PATCH] scripts: Add a script to allow bisecting failures The CLI is pretty similare to the `git bisect` one and works as follow: $ flatpak-bisect org.app.App start # Update application and get the history $ flatpak-bisect org.app.App bad # Sets current commit as first bad commit $ flatpak-bisect org.app.App checkout GoodHash # Checkout the first known good commit $ flatpak-bisect org.app.App good # Sets GoodHash as first good commit ... Here it starts bisection and checkouts a commit on the way, the user should launch the app to check if the commit is good or bad and run: $ flatpak-bisect org.app.App good # if commit is good $ flatpak-bisect org.app.App bad # if commit is bad flatpak-bisect will tell when the first bad commit is found. Fixes https://github.com/flatpak/flatpak/issues/530 --- Makefile.am | 3 + scripts/flatpak-bisect | 225 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100755 scripts/flatpak-bisect diff --git a/Makefile.am b/Makefile.am index 0aae5508..16f2be04 100644 --- a/Makefile.am +++ b/Makefile.am @@ -147,6 +147,9 @@ pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = flatpak.pc EXTRA_DIST += flatpak.pc.in +scriptsdir = $(bindir) +scripts_SCRIPTS = scripts/flatpak-bisect + EXTRA_DIST += README.md AM_DISTCHECK_CONFIGURE_FLAGS = \ diff --git a/scripts/flatpak-bisect b/scripts/flatpak-bisect new file mode 100755 index 00000000..eec49ae6 --- /dev/null +++ b/scripts/flatpak-bisect @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 + +import re +import argparse +import os +import gi +import json +import subprocess + +gi.require_version('Flatpak', '1.0') +from gi.repository import Flatpak +from gi.repository import GLib + +def get_bisection_data(): + return {'ref': None, 'good': None, 'bad': None, + 'refs': None, 'log': None, 'messages': None} + +class Bisector(): + def load_cache(self): + try: + os.makedirs(os.path.join(GLib.get_user_cache_dir(), 'flatpak')) + except FileExistsError: + pass + + self.cache_path = os.path.join(GLib.get_user_cache_dir(), + 'flatpak', '%s-%s-bisect.status' % ( + self.name, self.branch)) + try: + with open(self.cache_path, 'rb') as f: + self.data = json.load(f) + except FileNotFoundError: + self.data = None + + def dump_data(self): + with open(self.cache_path, 'w') as f: + json.dump(self.data, f) + + def setup_flatpak_app(self): + self.installation = Flatpak.Installation.new_user() + kind = Flatpak.RefKind.APP + if self.runtime: + kind = Flatpak.RefKind.RUNTIME + try: + self.cref = self.installation.get_installed_ref(kind, self.name, None, self.branch, None) + except GLib.Error as e: + print("%s\n\nMake sure %s is installed as a " + "user (flatpak install --user) and specify `--runtime`" + " if it is a runtime." % (e, self.name)) + return -1 + return 0 + + def run(self): + self.name = self.name[0] + self.load_cache() + res = self.setup_flatpak_app() + if res: + return res + + try: + func = getattr(self, self.subparser_name) + except AttributeError: + print('No action called %s' % self.subparser_name) + + return -1 + + res = func() + + if self.data: + self.dump_data() + + return res + + def set_reference_commits(self, set_name, check_name): + if not self.data: + print("You need to first start the bisection") + return -1 + ref = self.cref.get_latest_commit() + + if self.data[check_name] == ref: + print('Commit %s is already set as %s...' % ( + ref, check_name)) + return 1 + + if ref not in self.data['refs']: + print("%s is not a known commit." % ref) + return -1 + + print("Setting %s as %s commit" % (ref, set_name)) + self.data[set_name] = ref + + if self.data[set_name] and self.data[check_name]: + x1 = self.data['refs'].index(self.data['good']) + x2 = self.data['refs'].index(self.data['bad']) + + refs = self.data['refs'][x1:x2] + if not refs: + print("==========================" + "First bad commit is:\n%s" + "==========================" % self.data['message'][self.data['bad']]) + exit(0) + ref = refs[int(len(refs) / 2)] + if self.data['good'] == ref: + print("\n==========================\n" + "First bad commit is:\n\n%s" + "==========================" % self.data['messages'][self.data['bad']]) + exit(0) + + return self.checkout(ref) + + return -1 + + def load_refs(self): + repodir, refname = self.download_history() + history = subprocess.check_output(['ostree', 'log', '--repo', repodir, refname]).decode() + + refs = [] + messages = {} + message = "" + _hash = '' + for l in history.split('\n'): + rehash = re.search('(?<=^commit )\w+', l) + if rehash: + if message: + messages[_hash] = message + _hash = rehash.group(0) + refs.insert(0, _hash) + message = "" + message += l + '\n' + + if message: + messages[_hash] = message + + self.data['refs'] = refs + self.data['log'] = history + self.data['messages'] = messages + + def good(self): + if not self.data['bad']: + print("Set the bad commit first") + exit(-1) + return self.set_reference_commits('good', 'bad') + + def bad(self): + return self.set_reference_commits('bad', 'good') + + def start(self): + if self.data: + print('Bisection already started') + return -1 + + print("Updating to %s latest commit" % self.name) + self.reset(False) + self.data = get_bisection_data() + self.load_refs() + + def download_history(self): + print("Getting history") + appidir = os.path.abspath(os.path.join(self.cref.get_deploy_dir(), '..')) + dirname = "app" + if self.runtime: + dirname = "runtime" + appidir = appidir.split('/%s/' % dirname) + repodir = os.path.join(appidir[0], 'repo') + refname = self.cref.get_origin() + ':' + dirname + '/' + self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch() + # FIXME Getting `error: Exceeded maximum recursion` in ostree if using --depth=-1 (or > 250) + subprocess.call(['ostree', 'pull', '--depth=250', '--commit-metadata-only', '--repo', repodir, refname]) + + return repodir, refname + + def log(self): + if self.data: + cmd = ['echo', self.data['log']] + else: + repodir, refname = self.download_history() + cmd = ['ostree', 'log', '--repo', repodir, refname] + pager = os.environ.get('PAGER') + if pager: + stdout = subprocess.PIPE + else: + stdout = subprocess.STDOUT + p = subprocess.Popen(cmd, stdout=stdout) + if pager: + ps = subprocess.check_call((pager), stdin=p.stdout) + p.wait() + + def checkout(self, commit=None): + if not commit: + commit = self.commit[0] + refname = self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch() + print("Checking out %s" % commit) + return subprocess.call(['flatpak', 'update', '--user', refname, '--commit', commit]) + + def reset(self, v=True): + if not self.data: + if v: + print("Not bisecting, nothing to reset") + return -1 + + refname = self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch() + print("Removing %s" % self.cache_path) + os.remove(self.cache_path) + self.data = None + return subprocess.call(['flatpak', 'update', '--user', refname]) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('name', nargs=1, help='Application/Runtime to bisect') + parser.add_argument('-b', '--branch', default='master', help='The branch to bisect') + parser.add_argument('-r', '--runtime', action="store_true", help='Bisecting a runtime not an app') + + subparsers = parser.add_subparsers(dest='subparser_name') + subparsers.required = True + start_parser = subparsers.add_parser('start', help="Start bisection") + bad_parser = subparsers.add_parser('bad', help="Set current version as bad") + good_parser = subparsers.add_parser('good', help="Set current version as good") + log_parser = subparsers.add_parser('log', help="Download and print application commit history") + + checkout_parser = subparsers.add_parser('checkout', help="Checkout defined commit") + checkout_parser.add_argument('commit', nargs=1, help='The commit hash to checkout') + + reset_parser = subparsers.add_parser('reset', help="Reset all bisecting data and go back to latest commit") + + bisector = Bisector() + options = parser.parse_args(namespace=bisector) + bisector.run()