diff --git a/pubspec.yaml b/pubspec.yaml index a96e3d6..06253f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.0.1+1 environment: sdk: ^3.12.2 diff --git a/scripts/bump_patch_version.sh b/scripts/bump_patch_version.sh new file mode 100755 index 0000000..c1b1994 --- /dev/null +++ b/scripts/bump_patch_version.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# ============================================================================ +# Bump the patch version in pubspec.yaml on every commit. +# +# `version: X.Y.Z+B` -> `version: X.Y.(Z+1)+B`. The +build suffix is left +# alone: CI replaces it with the commit count, so it is not ours to manage. +# +# Wired as a pre-commit hook so each commit ships its own bump. That keeps the +# release-apk workflow's clean `vX.Y.Z` tag moving with HEAD instead of +# sitting on whatever version was last hand-edited, which is what left the AUR +# package advertising a tag 8 commits behind. +# +# Deliberately NOT bumping minor: minor/major are the "how much broke" signal. +# Auto-incrementing them on every commit throws that signal away and reaches +# absurd numbers within a year. +# ============================================================================ + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_NAME +PUBSPEC="" + +usage() { + echo "Usage: $SCRIPT_NAME [--pubspec PATH]" + echo "Options:" + echo " --pubspec PATH pubspec.yaml to bump (default: repo root's)" + echo " -h, --help Show this help" + exit 0 +} + +# Bumping while a rebase/merge/amend is replaying commits would inflate the +# version once per replayed commit, so skip those states entirely. +in_replay_state() { + local git_dir + git_dir="$(git rev-parse --git-dir)" + [[ -d "$git_dir/rebase-merge" || -d "$git_dir/rebase-apply" \ + || -f "$git_dir/MERGE_HEAD" || -f "$git_dir/CHERRY_PICK_HEAD" ]] +} + +# Only bump when something other than pubspec.yaml is staged. A commit that +# only touches the version (or only docs the hook itself rewrote) must not +# trigger another bump, or `git commit --amend` would climb a version per +# amend. +has_non_pubspec_changes() { + local staged + staged="$(git diff --cached --name-only)" + [[ -n "$(grep -v -e '^$' -e 'pubspec\.yaml$' <<<"$staged")" ]] +} + +main() { + if [[ -z "$PUBSPEC" ]]; then + PUBSPEC="$(git rev-parse --show-toplevel)/pubspec.yaml" + fi + + if [[ ! -f "$PUBSPEC" ]]; then + echo "Error: no pubspec.yaml at $PUBSPEC" >&2 + exit 1 + fi + + if in_replay_state; then + echo "bump-version: rebase/merge in progress; leaving version alone" + exit 0 + fi + + if ! has_non_pubspec_changes; then + echo "bump-version: nothing staged but the version itself; not bumping" + exit 0 + fi + + local raw semver build major minor patch next committed rel_path + raw="$(sed -nE 's/^version:[[:space:]]*(.*)$/\1/p' "$PUBSPEC" | head -1)" + if [[ -z "$raw" ]]; then + echo "Error: no 'version:' line in $PUBSPEC" >&2 + exit 1 + fi + + # Bump relative to the LAST COMMITTED version, not the working-tree one. + # Otherwise re-running the hook over the same staged change (a hook that + # failed, got fixed, and was retried) climbs a patch per attempt instead + # of per commit. + rel_path="${PUBSPEC#"$(git rev-parse --show-toplevel)/"}" + committed="$(git show "HEAD:$rel_path" 2>/dev/null \ + | sed -nE 's/^version:[[:space:]]*(.*)$/\1/p' | head -1 || true)" + [[ -n "$committed" ]] && raw="$committed" + + semver="${raw%%+*}" + build="" + [[ "$raw" == *+* ]] && build="+${raw#*+}" + + if [[ ! "$semver" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "Error: version '$raw' is not X.Y.Z[+B]" >&2 + exit 1 + fi + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + + next="$major.$minor.$((patch + 1))" + sed -i -E "s/^version:[[:space:]]*.*$/version: $next$build/" "$PUBSPEC" + git add "$PUBSPEC" + echo "bump-version: $semver -> $next" +} + +while [[ $# -gt 0 ]]; do + case $1 in + --pubspec) + PUBSPEC="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +main "$@" diff --git a/scripts/install_hooks.sh b/scripts/install_hooks.sh new file mode 100755 index 0000000..45c6c6b --- /dev/null +++ b/scripts/install_hooks.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# ============================================================================ +# Install this repo's git hooks. +# +# .git/hooks/ is not tracked, so a fresh clone has no hooks. Run this once +# after cloning. Unlike habit_stack and diet-guard, this repo does not use the +# pre-commit framework (its quality gate is CI), so the hook is a plain script. +# ============================================================================ + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +readonly REPO_ROOT +readonly HOOK="$REPO_ROOT/.git/hooks/pre-commit" + +main() { + if [[ -e "$HOOK" ]] && ! grep -q 'bump_patch_version.sh' "$HOOK"; then + echo "Error: $HOOK exists and is not ours; refusing to clobber it." >&2 + exit 1 + fi + + cat > "$HOOK" <<'HOOK_BODY' +#!/bin/bash +# Installed by scripts/install_hooks.sh — see that file. +set -euo pipefail +exec bash "$(git rev-parse --show-toplevel)/scripts/bump_patch_version.sh" +HOOK_BODY + + chmod 755 "$HOOK" + echo "Installed pre-commit hook: $HOOK" +} + +main "$@"