pmbootstrap status: rework (MR 2294)

Reimplement "pmbootstrap status" to be just a simple and useful status
overview. The previous version ran a bunch of checks every time, and
would fail on these even if pmaports was used for normal development:
  * "non-official" branch checked out in pmaports
  * pmaports.git is not clean

The information about aports.git was also considered not so useful upon
revisiting this command, since it is only used for "pmbootstrap
aportgen". Most users don't need this, and if the user runs this
command, it will tell if aports.git is outdated.

All of the above made the previous version unpleasant to use and I
suspect most people stopped using the command after trying it out a few
times and seeing the irrelevant but loud NOK complaints.

New version:

  $ pmbootstrap status
  Channel: edge (pmaports: master_staging_systemd)
  Device:  qemu-amd64 (x86_64, kernel: virt)
  UI:      console
  systemd: no (default for selected UI)

Old version (without --details it only shows NOK checks):

  $ pmbootstrap status --details
  [00:55:20] *** CONFIG ***
  [00:55:20] Device: qemu-amd64 (x86_64, "QEMU amd64")
  [00:55:20] Kernel: virt
  [00:55:20] User Interface: console
  [00:55:20]
  [00:55:20] *** GIT REPOS ***
  [00:55:20] Path: /home/user/.local/var/pmbootstrap/cache_git
  [00:55:20] - aports_upstream (master)
  [00:55:20] - pmaports (master)
  [00:55:20]
  [00:55:20] *** CHECKS ***
  [00:55:20] [OK ] Chroots zapped recently (or non-existing)
  [00:55:20] [OK ] aports_upstream: on official channel branch
  [00:55:20] [OK ] aports_upstream: workdir is clean
  [00:55:20] [OK ] aports_upstream: tracking proper remote branch 'origin/master'
  [00:55:20] [OK ] aports_upstream: up to date with remote branch
  [00:55:20] [OK ] aports_upstream: remote information updated recently (via git fetch/pull)
  [00:55:20] [OK ] pmaports: on official channel branch
  [00:55:20] [OK ] pmaports: workdir is clean
  [00:55:20] [OK ] pmaports: tracking proper remote branch 'origin/master'
  [00:55:20] [OK ] pmaports: up to date with remote branch
  [00:55:20] [OK ] pmaports: remote information updated recently (via git fetch/pull)
  [00:55:20]
  [00:55:20] NOTE: chroot is still active (use 'pmbootstrap shutdown' as necessary)
  [00:55:20] DONE!
This commit is contained in:
Oliver Smith 2024-04-12 00:09:55 +02:00
parent 56dfdd4ad3
commit 0d320d0613
No known key found for this signature in database
GPG Key ID: 5AE7F5513E0885CB
8 changed files with 54 additions and 290 deletions

View File

@ -1109,10 +1109,6 @@ git_repos = {
"pmaports": "https://gitlab.com/postmarketOS/pmaports.git",
}
# When a git repository is considered outdated (in seconds)
# (Measuring timestamp of FETCH_HEAD: https://stackoverflow.com/a/9229377)
git_repo_outdated = 3600 * 24 * 2
#
# APORTGEN
#

View File

@ -14,3 +14,17 @@ def is_systemd_selected(args):
if args.systemd == "never":
return False
return pmb.helpers.ui.check_option(args, args.ui, "pmb:systemd")
def systemd_selected_str(args):
if "systemd" not in pmb.config.pmaports.read_config_repos(args):
return "no", "not supported by pmaports branch"
if pmb.helpers.ui.check_option(args, args.ui, "pmb:systemd-never"):
return "no", "not supported by selected UI"
if args.systemd == "always":
return "yes", "'always' selected in 'pmbootstrap init'"
if args.systemd == "never":
return "no", "'never' selected in 'pmbootstrap init'"
if pmb.helpers.ui.check_option(args, args.ui, "pmb:systemd"):
return "yes", "default for selected UI"
return "no", "default for selected UI"

View File

@ -624,8 +624,10 @@ def lint(args):
def status(args: Namespace) -> None:
if not pmb.helpers.status.print_status(args, args.details):
sys.exit(1)
pmb.helpers.status.print_status(args)
# Do not print the DONE! line
sys.exit(0)
def ci(args):

View File

@ -3,7 +3,6 @@
import configparser
import logging
import os
import time
import pmb.build
import pmb.chroot.apk
@ -229,25 +228,6 @@ def pull(args, name_repo):
return 0
def is_outdated(path):
# FETCH_HEAD always exists in repositories cloned by pmbootstrap.
# Usually it does not (before first git fetch/pull), but there is no good
# fallback. For exampe, getting the _creation_ date of .git/HEAD is non-
# trivial with python on linux (https://stackoverflow.com/a/39501288).
# Note that we have to assume here that the user had fetched the "origin"
# repository. If the user fetched another repository, FETCH_HEAD would also
# get updated, even though "origin" may be outdated. For pmbootstrap status
# it is good enough, because it should help the users that are not doing
# much with pmaports.git to know when it is outdated. People who manually
# fetch other repos should usually know that and how to handle that
# situation.
path_head = path + "/.git/FETCH_HEAD"
date_head = os.path.getmtime(path_head)
date_outdated = time.time() - pmb.config.git_repo_outdated
return date_head <= date_outdated
def get_topdir(args, path):
""" :returns: a string with the top dir of the git repository, or an
empty string if it's not a git repository. """

View File

@ -1,165 +1,58 @@
# Copyright 2023 Oliver Smith
# Copyright 2024 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import logging
import pmb.config
import pmb.config.workdir
import pmb.helpers.git
from argparse import Namespace
from typing import List, Tuple
def print_config(args: Namespace) -> None:
""" Print an overview of what was set in "pmbootstrap init". """
logging.info("*** CONFIG ***")
info = args.deviceinfo
logging.info("Device: {} ({}, \"{}\")"
.format(args.device, info["arch"], info["name"]))
def print_status_line(key: str, value: str):
styles = pmb.config.styles
key = f"{styles['GREEN']}{key}{styles['END']}:"
padding = 17
if pmb.parse._apkbuild.kernels(args, args.device):
logging.info("Kernel: " + args.kernel)
if args.extra_packages != "none":
logging.info("Extra packages: {}".format(args.extra_packages))
logging.info("User Interface: {}".format(args.ui))
print(f"{key.ljust(padding)} {value}")
def print_git_repos(args: Namespace) -> None:
logging.info("*** GIT REPOS ***")
logging.info("Path: {}/cache_git".format(args.work))
for repo in pmb.config.git_repos.keys():
path = pmb.helpers.git.get_path(args, repo)
if not os.path.exists(path):
continue
def print_channel(args: Namespace) -> None:
pmaports_cfg = pmb.config.pmaports.read_config(args)
channel = pmaports_cfg["channel"]
# Get branch name (if on branch) or current commit
ref = pmb.helpers.git.rev_parse(args, path,
extra_args=["--abbrev-ref"])
if ref == "HEAD":
ref = pmb.helpers.git.rev_parse(args, path)[0:8]
logging.info("- {} ({})".format(repo, ref))
def print_checks_git_repo(args: Namespace, repo: str, details: bool=True) -> Tuple[int, str]:
""" Perform various checks on one checked out git repo.
:param details: if True, print each passing check (this is True by
default for the testsuite)
:returns: status, todo_msg
- status: integer, 0 if all passed, < 0 on failure
- msg_todo: message to help the user resolve the failure """
def log_ok(msg_ok):
if details:
logging.info("[OK ] {}: {}".format(repo, msg_ok))
def log_nok_ret(status, msg_nok, msg_todo):
logging.warning("[NOK] {}: {}".format(repo, msg_nok))
return (status, msg_todo)
# On official branch
path = pmb.helpers.git.get_path(args, repo)
branches = pmb.helpers.git.get_branches_official(args, repo)
# Get branch name (if on branch) or current commit
path = pmb.helpers.git.get_path(args, "pmaports")
ref = pmb.helpers.git.rev_parse(args, path, extra_args=["--abbrev-ref"])
if ref not in branches:
return log_nok_ret(-1, "not on official channel branch",
"consider checking out: " + ", ".join(branches))
log_ok("on official channel branch")
if ref == "HEAD":
ref = pmb.helpers.git.rev_parse(args, path)[0:8]
# Workdir clean
if not pmb.helpers.git.clean_worktree(args, path):
return log_nok_ret(-2, "workdir is not clean",
"consider cleaning your workdir")
log_ok("workdir is clean")
ref += ", dirty"
# Tracking proper remote
remote_upstream = pmb.helpers.git.get_upstream_remote(args, repo)
branch_upstream = remote_upstream + "/" + ref
remote_ref = pmb.helpers.git.rev_parse(args, path, ref + "@{u}",
["--abbrev-ref"])
if remote_ref != branch_upstream:
return log_nok_ret(-3, "tracking unexpected remote branch",
"consider tracking remote branch '{}' instead of"
" '{}'".format(branch_upstream, remote_ref))
log_ok("tracking proper remote branch '{}'".format(branch_upstream))
# Up to date
ref_branch = pmb.helpers.git.rev_parse(args, path, ref)
ref_branch_upstream = pmb.helpers.git.rev_parse(args, path,
branch_upstream)
if ref_branch != ref_branch_upstream:
return log_nok_ret(-4, "not up to date with remote branch",
"update with 'pmbootstrap pull'")
log_ok("up to date with remote branch")
# Outdated remote information
if pmb.helpers.git.is_outdated(path):
return log_nok_ret(-5, "outdated remote information",
"update with 'pmbootstrap pull'")
log_ok("remote information updated recently (via git fetch/pull)")
return (0, "")
value = f"{channel} (pmaports: {ref})"
print_status_line("Channel", value)
def print_checks_git_repos(args: Namespace, details: bool) -> List[str]:
""" Perform various checks on the checked out git repos.
:param details: if True, print each passing check
:returns: list of unresolved checklist items """
ret = []
for repo in pmb.config.git_repos.keys():
path = pmb.helpers.git.get_path(args, repo)
if not os.path.exists(path):
continue
status, todo_msg = print_checks_git_repo(args, repo, details)
if status:
ret += ["{}: {}".format(repo, todo_msg)]
return ret
def print_device(args: Namespace) -> None:
kernel = ""
if pmb.parse._apkbuild.kernels(args, args.device):
kernel = f", kernel: {args.kernel}"
value = f"{args.device} ({args.deviceinfo['arch']}{kernel})"
print_status_line("Device", value)
def print_checks_chroots_outdated(args: Namespace, details: bool) -> List[str]:
""" Check if chroots were zapped recently.
:param details: if True, print each passing check instead of a summary
:returns: list of unresolved checklist items """
if pmb.config.workdir.chroots_outdated(args):
logging.info("[NOK] Chroots not zapped recently")
return ["Run 'pmbootstrap zap' to delete possibly outdated chroots"]
elif details:
logging.info("[OK ] Chroots zapped recently (or non-existing)")
return []
def print_ui(args: Namespace) -> None:
print_status_line("UI", args.ui)
def print_checks(args: Namespace, details: bool) -> bool:
def print_systemd(args: Namespace) -> None:
yesno, reason = pmb.config.other.systemd_selected_str(args)
print_status_line("systemd", f"{yesno} ({reason})")
def print_status(args: Namespace) -> None:
""" :param details: if True, print each passing check instead of a summary
:returns: True if all checks passed, False otherwise """
logging.info("*** CHECKS ***")
checklist = []
checklist += print_checks_chroots_outdated(args, details)
checklist += print_checks_git_repos(args, details)
# All OK
if not checklist:
if not details:
logging.info("All checks passed! \\o/")
logging.info("")
return True
# Some NOK: print checklist
logging.info("")
logging.info("*** CHECKLIST ***")
for item in checklist:
logging.info("- " + item)
logging.info("- Run 'pmbootstrap status' to verify that all is resolved")
return False
def print_status(args: Namespace, details: bool=False) -> bool:
""" :param details: if True, print each passing check instead of a summary
:returns: True if all checks passed, False otherwise """
print_config(args)
logging.info("")
print_git_repos(args)
logging.info("")
ret = print_checks(args, details)
return ret
print_channel(args)
print_device(args)
print_ui(args)
print_systemd(args)

View File

@ -551,9 +551,7 @@ def arguments_lint(subparser):
def arguments_status(subparser):
ret = subparser.add_parser("status",
help="quick health check for the work dir")
ret.add_argument("--details", action="store_true",
help="list passing checks in detail, not as summary")
help="show a config and pmaports overview")
return ret

View File

@ -4,7 +4,6 @@ import os
import sys
import pytest
import shutil
import time
import pmb_test # noqa
import pmb_test.const
@ -166,29 +165,3 @@ def test_pull(args, monkeypatch, tmpdir):
run_git(["reset", "--hard", "origin/master"])
run_git(["commit", "--allow-empty", "-m", "new"], "remote")
assert func(args, name_repo) == 0
def test_is_outdated(tmpdir, monkeypatch):
func = pmb.helpers.git.is_outdated
# Override time.time(): now is "100"
def fake_time():
return 100.0
monkeypatch.setattr(time, "time", fake_time)
# Create .git/FETCH_HEAD
path = str(tmpdir)
os.mkdir(path + "/.git")
fetch_head = path + "/.git/FETCH_HEAD"
open(fetch_head, "w").close()
# Set mtime to 90
os.utime(fetch_head, times=(0, 90))
# Outdated (date_outdated: 90)
monkeypatch.setattr(pmb.config, "git_repo_outdated", 10)
assert func(path) is True
# Not outdated (date_outdated: 89)
monkeypatch.setattr(pmb.config, "git_repo_outdated", 11)
assert func(path) is False

View File

@ -1,92 +0,0 @@
# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
""" Test pmb/helpers/status.py """
import os
import pytest
import shutil
import sys
import pmb_test
import pmb_test.git
import pmb.config
import pmb.config.workdir
@pytest.fixture
def args(request):
import pmb.parse
sys.argv = ["pmbootstrap", "init"]
args = pmb.parse.arguments()
args.log = args.work + "/log_testsuite.txt"
pmb.helpers.logging.init(args)
request.addfinalizer(pmb.helpers.logging.logfd.close)
return args
def test_pmbootstrap_status(args, tmpdir):
""" High level testing of 'pmbootstrap status': run it twice, once with
a fine workdir, and once where one check is failing. """
# Prepare empty workdir
work = str(tmpdir)
with open(work + "/version", "w") as handle:
handle.write(str(pmb.config.work_version))
# "pmbootstrap status" succeeds (pmb.helpers.run.user verifies exit 0)
pmbootstrap = pmb.config.pmb_src + "/pmbootstrap.py"
pmb.helpers.run.user(args, [pmbootstrap, "-w", work, "status",
"--details"])
# Mark chroot_native as outdated
with open(work + "/workdir.cfg", "w") as handle:
handle.write("[chroot-init-dates]\nnative = 1234\n")
# "pmbootstrap status" fails
ret = pmb.helpers.run.user(args, [pmbootstrap, "-w", work, "status"],
check=False)
assert ret == 1
def test_print_checks_git_repo(args, monkeypatch, tmpdir):
""" Test pmb.helpers.status.print_checks_git_repo """
path, run_git = pmb_test.git.prepare_tmpdir(args, monkeypatch, tmpdir)
# Not on official branch
func = pmb.helpers.status.print_checks_git_repo
name_repo = "test"
run_git(["checkout", "-b", "inofficial-branch"])
status, _ = func(args, name_repo)
assert status == -1
# Workdir is not clean
run_git(["checkout", "master"])
shutil.copy(__file__, path + "/test.py")
status, _ = func(args, name_repo)
assert status == -2
os.unlink(path + "/test.py")
# Tracking different remote
status, _ = func(args, name_repo)
assert status == -3
# Let master track origin/master
run_git(["checkout", "-b", "temp"])
run_git(["branch", "-D", "master"])
run_git(["checkout", "-b", "master", "--track", "origin/master"])
# Not up to date
run_git(["commit", "--allow-empty", "-m", "new"], "remote")
run_git(["fetch"])
status, _ = func(args, name_repo)
assert status == -4
# Up to date
run_git(["pull"])
status, _ = func(args, name_repo)
assert status == 0
# Outdated remote information
def is_outdated(path):
return True
monkeypatch.setattr(pmb.helpers.git, "is_outdated", is_outdated)
status, _ = func(args, name_repo)
assert status == -5