diff --git a/.gitattributes b/.gitattributes index 9ce661c79d..8f50204cdd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -43,7 +43,21 @@ *.yaml text eol=lf *.plist text eol=lf -# Vendored C headers (database driver bridges, tree-sitter grammars) +# Vendored C headers (database driver bridges, SQLite, tree-sitter grammars) Plugins/*/C*/include/** linguist-vendored +Packages/TableProCore/Sources/CSQLite/include/sqlite3*.h linguist-vendored TablePro/Core/SSH/CLibSSH2/** linguist-vendored Packages/TableProGrammars/Sources/TreeSitterGrammars/** linguist-vendored + +# Third-party license texts, copied verbatim +Native/DamengBridge/LICENSES/** linguist-vendored +Packages/TableProEditor/LICENSE-*.md linguist-vendored +TablePro/Resources/ThirdPartyLicenses/texts/** linguist-vendored +# Joins the SQLite copyright page and the source blessing with a sentence of its own. +TablePro/Resources/ThirdPartyLicenses/texts/sqlite.txt -linguist-vendored + +# Generated Linux files (Cargo, xgettext, cargo-about, license_metadata.py) +linux/Cargo.lock linguist-generated +linux/po/tablepro.pot linguist-generated +linux/data/licenses/** linguist-generated +linux/build-aux/debian/copyright linguist-generated diff --git a/.gitignore b/.gitignore index 096672c61b..a78a15eff8 100644 --- a/.gitignore +++ b/.gitignore @@ -173,9 +173,6 @@ Native/HanaBridge/bin .docs/ /plans/reports -# Linux meson build directory -linux/_build/ - # Translation exports from `xcodebuild -exportLocalizations`; the catalog is the source of truth. # Anchored: unanchored, it also matched TableProTests/Localization/, so the string-catalog # guard suite existed on disk and was invisible to git and to CI. diff --git a/linux/.editorconfig b/linux/.editorconfig new file mode 100644 index 0000000000..aa659063e0 --- /dev/null +++ b/linux/.editorconfig @@ -0,0 +1,25 @@ +# No root = true: the repository's .editorconfig sets the defaults, and this file overrides them. + +[{meson.build,meson.options,meson_options.txt}] +indent_size = 2 + +[*.{ui,xml,xml.in,xml.in.in,svg}] +indent_size = 2 + +[*.css] +indent_size = 2 + +[*.hbs] +indent_size = 2 + +[build-aux/flatpak/*.json] +indent_size = 4 + +[*.rs] +max_line_length = 120 + +[build-aux/debian/rules] +indent_style = tab + +[build-aux/debian/{control,copyright}] +indent_size = 1 diff --git a/linux/.gitignore b/linux/.gitignore index 215e33a75c..fb3d5826dd 100644 --- a/linux/.gitignore +++ b/linux/.gitignore @@ -1,6 +1,5 @@ target/ +_build/ _packages/ -*.swp -.DS_Store - -docs/audit-*.md +_flatpak/ +.flatpak-builder/ diff --git a/linux/build-aux/packaging/add-rust-toolchain-ppa.sh b/linux/build-aux/packaging/add-rust-toolchain-ppa.sh index e60743f613..9c54149b85 100755 --- a/linux/build-aux/packaging/add-rust-toolchain-ppa.sh +++ b/linux/build-aux/packaging/add-rust-toolchain-ppa.sh @@ -20,6 +20,7 @@ fi # The key is committed, so no run trusts a key it fetched. apt checks # InRelease against it, which makes plain HTTP as safe as the archive's # own sources, and the base image has no CA bundle for HTTPS. +# shellcheck source=/dev/null . /etc/os-release mkdir -p /etc/apt/keyrings cp "$here/rust-toolchain-staging.asc" /etc/apt/keyrings/rust-toolchain-staging.asc diff --git a/linux/build-aux/packaging/build-deb.sh b/linux/build-aux/packaging/build-deb.sh index 2072b09176..ac457c0c6d 100755 --- a/linux/build-aux/packaging/build-deb.sh +++ b/linux/build-aux/packaging/build-deb.sh @@ -34,6 +34,7 @@ build() { # login that is not root, 1024 open files among them. Launchpad passes # the builder's CPU count the same way as here. status=0 + # shellcheck disable=SC2016 # the inner sh expands $1 runuser -u builder -- env HOME=/home/builder DEB_BUILD_OPTIONS="parallel=$(nproc)" \ sh -euc 'cd /build/work && dpkg-source -x "$1" src && cd src && dpkg-buildpackage -b -us -uc' sh "$1" || status=$? diff --git a/scripts/check-banned-words.sh b/scripts/check-banned-words.sh index 55863cb4ae..980af631e2 100755 --- a/scripts/check-banned-words.sh +++ b/scripts/check-banned-words.sh @@ -10,15 +10,14 @@ # A term matches case-insensitively at the start of a word, so a capitalized or suffixed form is a # hit and a word that only ends in a term is not. A term that starts with punctuation, the em dash, # matches anywhere. Each hit prints as file:line: term. Exits 1 on a hit, 0 when clean and 2 on a -# usage error. A diff skips linux/data/licenses/ and linux/build-aux/debian/copyright, which reproduce -# third-party license text verbatim. +# usage error. A diff skips a file .gitattributes marks linguist-generated or linguist-vendored, +# since a generator or an upstream project wrote its text. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" LIST="$SCRIPT_DIR/banned-words.txt" LIST_PATH_IN_REPO="scripts/banned-words.txt" -VERBATIM_IN_REPO="linux/data/licenses/ linux/build-aux/debian/copyright" usage() { sed -n "5,8s/^# //p" "${BASH_SOURCE[0]}" @@ -72,15 +71,11 @@ function check(text, location, lowered, i) { ' DIFF_READER=' -BEGIN { verbatim_count = split(verbatim, verbatim_paths, " ") } - -function is_verbatim(path, i) { - for (i = 1; i <= verbatim_count; i++) { - if (index(path, verbatim_paths[i]) == 1) { - return 1 - } +BEGIN { + while ((getline path < exempt_list) > 0) { + exempt[path] = 1 } - return 0 + close(exempt_list) } /^diff --git / { in_header = 1; file = ""; next } @@ -88,7 +83,7 @@ in_header && /^\+\+\+ / { file = substr($0, 5) sub(/^b\//, "", file) sub(/\t$/, "", file) - if (file == "/dev/null" || file == skip || is_verbatim(file)) { + if (file == "/dev/null" || file == skip || file in exempt) { file = "" } next @@ -117,13 +112,22 @@ END { exit(hits > 0 ? 1 : 0) } ' check_diff() { - git rev-parse --show-toplevel > /dev/null 2>&1 || { + local top + top="$(git rev-parse --show-toplevel 2> /dev/null)" || { echo "check-banned-words.sh: not inside a git repository" >&2 exit 2 } + EXEMPT_LIST="$(mktemp)" || exit 2 + trap 'rm -f "$EXEMPT_LIST"' EXIT + # A renamed file still lists under its new path, and the diff below already prints any rename + # limit warning. The index holds the staged .gitattributes, and HEAD's when nothing is staged. + git -c core.quotePath=false diff --name-only --no-renames --diff-filter=ACMR "$@" | + git -C "$top" -c core.quotePath=false check-attr --cached --stdin \ + linguist-generated linguist-vendored | + sed -n -E 's/^(.*): linguist-(generated|vendored): (set|true)$/\1/p' > "$EXEMPT_LIST" || exit 2 git -c core.quotePath=false diff -U0 --no-color --no-ext-diff --diff-filter=ACMR \ --src-prefix=a/ --dst-prefix=b/ "$@" | - LC_ALL=C awk -v skip="$LIST_PATH_IN_REPO" -v verbatim="$VERBATIM_IN_REPO" "$MATCHER$DIFF_READER" + LC_ALL=C awk -v skip="$LIST_PATH_IN_REPO" -v exempt_list="$EXEMPT_LIST" "$MATCHER$DIFF_READER" } check_files() { diff --git a/scripts/ci/test_check_banned_words.py b/scripts/ci/test_check_banned_words.py new file mode 100644 index 0000000000..9916b69f6f --- /dev/null +++ b/scripts/ci/test_check_banned_words.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Tests check-banned-words.sh against throwaway git repositories.""" + +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parent.parent / "check-banned-words.sh" +TERM = "frobnicate" +SEED_TEXT = "".join(f"seed line {number}\n" for number in range(20)) + + +class CheckBannedWordsTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.repo = Path(self.temp.name) + self.env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} + self.git("-c", "init.defaultBranch=main", "init", "-q") + (self.repo / "scripts").mkdir() + shutil.copy(SCRIPT, self.repo / "scripts/check-banned-words.sh") + self.write("scripts/banned-words.txt", f"{TERM}\n") + self.write(".gitattributes", "gen/** linguist-generated\nvendor/** linguist-vendored\n") + self.write("README.md", SEED_TEXT) + self.write("gen/data.txt", SEED_TEXT) + self.git("add", "-A") + self.git("commit", "-q", "-m", "seed") + self.seed = self.git("rev-parse", "HEAD") + + def tearDown(self): + self.temp.cleanup() + + def git(self, *args): + result = subprocess.run( + ["git", "-c", "user.name=test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=self.repo, env=self.env, check=True, capture_output=True, text=True, + ) + return result.stdout.strip() + + def write(self, path, text): + file = self.repo / path + file.parent.mkdir(parents=True, exist_ok=True) + with open(file, "a", encoding="utf-8") as handle: + handle.write(text) + + def stage_term(self, *paths): + for path in paths: + self.write(path, f"a {TERM} line\n") + self.git("add", "-A") + + def check(self, *args, cwd=None): + result = subprocess.run( + ["bash", str(self.repo / "scripts/check-banned-words.sh"), *args], + cwd=cwd or self.repo, env=self.env, capture_output=True, text=True, + ) + return result.returncode, sorted(line.split(":")[0] for line in result.stdout.splitlines()) + + def commit_term(self, *paths): + self.stage_term(*paths) + self.git("commit", "-q", "-m", "change") + return self.check("--base", self.seed) + + def test_a_generated_or_vendored_file_is_skipped(self): + self.assertEqual(self.commit_term("gen/notice.md", "vendor/lib.h", "src/app.txt"), (1, ["src/app.txt"])) + + def test_a_diff_of_only_generated_and_vendored_files_passes(self): + self.assertEqual(self.commit_term("gen/notice.md", "vendor/lib.h"), (0, [])) + + def test_an_unset_or_false_attribute_is_checked(self): + self.write(".gitattributes", "vendor/own.txt -linguist-vendored\ngen/kept.txt linguist-generated=false\n") + self.assertEqual(self.commit_term("vendor/own.txt", "gen/kept.txt"), (1, ["gen/kept.txt", "vendor/own.txt"])) + + def test_staged_reads_the_staged_gitattributes(self): + self.write(".gitattributes", "notes/** linguist-generated\n") + self.stage_term("notes/new.txt", "src/app.txt") + self.assertEqual(self.check("--staged"), (1, ["src/app.txt"])) + + def test_a_renamed_file_follows_its_new_path(self): + (self.repo / "src").mkdir() + self.git("mv", "README.md", "gen/README.md") + self.git("mv", "gen/data.txt", "src/data.txt") + self.assertEqual(self.commit_term("gen/README.md", "src/data.txt"), (1, ["src/data.txt"])) + + def test_a_subdirectory_still_resolves_attributes_from_the_top_level(self): + self.commit_term("gen/notice.md", "src/app.txt") + self.assertEqual(self.check("--base", self.seed, cwd=self.repo / "src"), (1, ["src/app.txt"])) + + +if __name__ == "__main__": + unittest.main()