mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-26 07:11:34 -08:00
175 lines
5.2 KiB
Python
Executable file
175 lines
5.2 KiB
Python
Executable file
#!/usr/bin/env python
|
|
#
|
|
#
|
|
# Ravenbrook
|
|
# <http://www.ravenbrook.com/>
|
|
#
|
|
# P4-BISECT -- FIND CHANGE THAT INTRODUCED A BUG
|
|
#
|
|
# Gareth Rees, Ravenbrook Limited, 2014-04-14
|
|
#
|
|
#
|
|
# 1. INTRODUCTION
|
|
#
|
|
# This script automates (or partly automates) the process of finding,
|
|
# by binary search, the change that introduced a bug.
|
|
#
|
|
# The interface is modelled closely on git-bisect(1).
|
|
|
|
import argparse
|
|
from functools import partial
|
|
import json
|
|
from os import unlink
|
|
import p4
|
|
import subprocess
|
|
import sys
|
|
|
|
BISECT_FILE = '.p4-bisect'
|
|
|
|
def error(msg):
|
|
sys.stderr.write(msg)
|
|
sys.stderr.write('\n')
|
|
exit(1)
|
|
|
|
def sync(*filespecs):
|
|
try:
|
|
p4.do('sync', *filespecs)
|
|
except p4.Error as e:
|
|
if 'file(s) up-to-date' not in e.args[0]:
|
|
raise
|
|
|
|
class State(object):
|
|
def __init__(self, **d):
|
|
self.filespec = d['filespec']
|
|
self.changes = d['changes']
|
|
if 'current' in d:
|
|
self.current = d['current']
|
|
|
|
@classmethod
|
|
def load(cls):
|
|
try:
|
|
with open(BISECT_FILE, 'r') as f:
|
|
return cls(**json.load(f))
|
|
except FileNotFoundError:
|
|
error("p4-bisect not in progress here.")
|
|
|
|
def save(self):
|
|
with open(BISECT_FILE, 'w') as f:
|
|
json.dump(vars(self), f)
|
|
|
|
def update(self):
|
|
n = len(self.changes)
|
|
if n == 0:
|
|
print("no changes remaining.".format(**vars(self)))
|
|
elif n == 1:
|
|
print("{} change remaining: {}.".format(n, self.changes[0]))
|
|
elif n == 2:
|
|
print("{} changes remaining: [{}, {}]."
|
|
.format(n, self.changes[0], self.changes[-1]))
|
|
else:
|
|
print("{} changes remaining: [{}, ..., {}]."
|
|
.format(n, self.changes[0], self.changes[-1]))
|
|
if n > 0:
|
|
self.current = self.changes[n // 2]
|
|
print("Syncing to changelevel {current}.".format(**vars(self)))
|
|
sync(*['{}@{}'.format(f, self.current) for f in self.filespec])
|
|
self.save()
|
|
|
|
def help(parser, args):
|
|
parser.print_help()
|
|
|
|
def start(args):
|
|
args.filespec = args.filespec or ['...']
|
|
changes = sorted(int(c['change']) for c in p4.run('changes', *args.filespec))
|
|
if not changes:
|
|
error("No changes for {}".format(' '.join(args.filespec)))
|
|
if args.good is None:
|
|
args.good = changes[0]
|
|
if args.bad is None:
|
|
args.bad = changes[-1]
|
|
state = State(filespec=args.filespec,
|
|
changes=[c for c in changes if args.good <= c <= args.bad])
|
|
state.update()
|
|
|
|
def good(args):
|
|
state = State.load()
|
|
print("Change {current} good.".format(**vars(state)))
|
|
state.changes = [c for c in state.changes if c > state.current]
|
|
state.update()
|
|
|
|
def bad(args):
|
|
state = State.load()
|
|
print("Change {current} bad.".format(**vars(state)))
|
|
state.changes = [c for c in state.changes if c < state.current]
|
|
state.update()
|
|
|
|
def skip(args):
|
|
state = State.load()
|
|
print("Skipping change {current}.".format(**vars(state)))
|
|
state.changes.remove(state.current)
|
|
state.update()
|
|
|
|
def reset(args):
|
|
state = State.load()
|
|
sync(*state.filespec)
|
|
unlink(BISECT_FILE)
|
|
|
|
def run(args):
|
|
while True:
|
|
state = State.load()
|
|
if not state.changes:
|
|
break
|
|
result = subprocess.call([args.cmd] + args.args)
|
|
if result == 0:
|
|
good(None)
|
|
elif result == 125:
|
|
skip(None)
|
|
elif 0 < result < 128:
|
|
bad(None)
|
|
else:
|
|
exit(result)
|
|
|
|
def main(argv):
|
|
parser = argparse.ArgumentParser(
|
|
prog='p4-bisect', epilog='For help on CMD, use p4-bisect CMD -h')
|
|
subparsers = parser.add_subparsers()
|
|
a = subparsers.add_parser
|
|
|
|
help_parser = a('help', help='show this help message')
|
|
help_parser.set_defaults(func=partial(help, parser))
|
|
|
|
start_parser = a('start', help='start a p4-bisect session')
|
|
aa = start_parser.add_argument
|
|
start_parser.add_argument('-f', '--filespec', action='append',
|
|
help='filespec(s) to search')
|
|
start_parser.add_argument('good', nargs='?', type=int,
|
|
help='known good changelevel')
|
|
start_parser.add_argument('bad', nargs='?', type=int,
|
|
help='known bad changelevel')
|
|
start_parser.set_defaults(func=start)
|
|
|
|
good_parser = a('good', help='declare current revision good')
|
|
good_parser.set_defaults(func=good)
|
|
|
|
bad_parser = a('bad', help='declare current revision bad')
|
|
bad_parser.set_defaults(func=bad)
|
|
|
|
skip_parser = a('skip', help='skip current revision')
|
|
skip_parser.set_defaults(func=skip)
|
|
|
|
reset_parser = a('reset', help='finish p4-bisect session')
|
|
reset_parser.set_defaults(func=reset)
|
|
|
|
run_parser = a('run', help='run p4-bisect session automatically')
|
|
run_parser.add_argument('cmd',
|
|
help='command that determines if current '
|
|
'changelevel is good or bad')
|
|
run_parser.add_argument('args', nargs=argparse.REMAINDER,
|
|
help='arguments to pass to cmd')
|
|
run_parser.set_defaults(func=run)
|
|
|
|
args = parser.parse_args(argv[1:])
|
|
args.func(args)
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|