Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
225 lines (185 sloc)
7.66 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 = None | |
p = subprocess.Popen(cmd, stdout=stdout) | |
if pager: | |
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() |