Files
StarPilot/openpilot/starpilot/software/history.py
T
firestar5683 0db0d2f589 Software and AGNOS
Manage software updates and AGNOS retained state through explicit manifests and update preferences.

Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>

Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
2026-10-14 20:02:39 -05:00

91 lines
3.2 KiB
Python

"""Bounded local history and plain-text notes from the existing updater."""
from datetime import datetime
from html.parser import HTMLParser
from pathlib import Path
import re
import subprocess
from openpilot.starpilot.saved_source import read_saved
COMMIT = re.compile(r'[0-9a-f]{40}\Z')
MAX_COMMITS = 20
MAX_NOTES_BYTES = 32768
def commit_history(path: Path, commit: str, *, first_parent: bool = False) -> list[dict]:
if not COMMIT.fullmatch(commit):
return []
try:
output = subprocess.run(
['git', '-C', str(path), '--no-pager', 'log', '--no-decorate', '--no-color', '--no-show-signature',
*(['--first-parent'] if first_parent else []), f'-{MAX_COMMITS}', '--format=%H%x00%cI%x00%<(240,trunc)%s%x00', commit, '--'],
check=True, capture_output=True, text=True, timeout=2).stdout
if len(output.encode()) > 32768:
return []
fields = output.split('\0')
rows = []
for index in range(0, len(fields) - 2, 3):
sha, date, subject = fields[index].strip(), fields[index + 1], fields[index + 2].rstrip()
if not COMMIT.fullmatch(sha) or len(subject) > 500:
return []
parsed = datetime.fromisoformat(date)
if parsed.tzinfo is None:
return []
rows.append({'hash': sha, 'date': parsed.isoformat(), 'subject': ''.join(c for c in subject if c.isprintable())})
return rows[:MAX_COMMITS]
except (OSError, ValueError, subprocess.SubprocessError):
return []
class _NotesText(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.parts = []
self.hidden = 0
def handle_starttag(self, tag, attrs):
if tag in ('script', 'style'):
self.hidden += 1
elif not self.hidden and tag in ('br', 'p', 'li', 'h1', 'h2', 'h3'):
self.parts.append('\n')
def handle_endtag(self, tag):
if tag in ('script', 'style'):
self.hidden = max(0, self.hidden - 1)
elif not self.hidden and tag in ('p', 'li', 'h1', 'h2', 'h3'):
self.parts.append('\n')
def handle_data(self, data):
if not self.hidden:
self.parts.append(data)
def release_notes(params, key: str) -> str | None:
raw, readable = read_saved(params, key, MAX_NOTES_BYTES)
if not readable or raw is None:
return None
try:
parser = _NotesText()
parser.feed(raw.decode('utf-8'))
parser.close()
return re.sub(r'\n{3,}', '\n\n', ''.join(parser.parts)).strip()
except (UnicodeError, ValueError):
return None
def recent_versions(path: Path, branch: str | None) -> dict:
empty = {'branch': branch, 'head': None, 'entries': []}
if type(branch) is not str or not re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9._/+@-]{0,127}', branch):
return empty
ref = f'refs/starpilot/recent/{branch}'
try:
subprocess.run(['git', '-C', str(path), 'check-ref-format', ref], check=True, capture_output=True, timeout=2)
head = subprocess.run(['git', '-C', str(path), 'rev-parse', '--verify', ref + '^{commit}'],
check=True, capture_output=True, text=True, timeout=2).stdout.strip()
if not COMMIT.fullmatch(head):
return empty
return {'branch': branch, 'head': head, 'entries': commit_history(path, head, first_parent=True)}
except (OSError, subprocess.SubprocessError):
return empty