aboutsummaryrefslogtreecommitdiff
path: root/bdep-util
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-08-06 23:32:13 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-08-12 12:33:41 +0300
commit768727f37afa6fbb5082833a4c14c8134ec42122 (patch)
tree8c7b3d85f9735d8585781a45f9e61b15fdaa3530 /bdep-util
parent4c61819dedbc3547bf3f352809ecfb6a95ab8753 (diff)
Add implementation
Diffstat (limited to 'bdep-util')
-rw-r--r--bdep-util/.gitignore3
-rw-r--r--bdep-util/buildfile38
-rw-r--r--bdep-util/git-hooks/.gitignore1
-rw-r--r--bdep-util/git-hooks/pre-commit.in23
-rw-r--r--bdep-util/git-pre-commit-copyright-check.in73
-rw-r--r--bdep-util/git-pre-commit-version-check.in220
-rw-r--r--bdep-util/git-pre-commit.in17
7 files changed, 375 insertions, 0 deletions
diff --git a/bdep-util/.gitignore b/bdep-util/.gitignore
new file mode 100644
index 0000000..7ecc5ca
--- /dev/null
+++ b/bdep-util/.gitignore
@@ -0,0 +1,3 @@
+bdep-git-pre-commit
+bdep-git-pre-commit-version-check
+bdep-git-pre-commit-copyright-check
diff --git a/bdep-util/buildfile b/bdep-util/buildfile
new file mode 100644
index 0000000..5fc25c8
--- /dev/null
+++ b/bdep-util/buildfile
@@ -0,0 +1,38 @@
+# file : bdep-util/buildfile
+# license : MIT; see accompanying LICENSE file
+
+import mods = libbutl.bash%bash{manifest-parser}
+import mods += libbutl.bash%bash{standard-version}
+
+# @@ TMP Note that git-hooks/pre-commit, bdep-git-pre-commit, and
+# bdep-git-pre-commit-copyright-check have no dependencies on any bash
+# module. However, we add such dependencies for the bash module rule to
+# match. Eventually, there will be a better way to achieve that (hints).
+#
+
+# Note that git-hooks/pre-commit script just sources bdep-git-pre-commit. The
+# reason to have both scripts is to be able to configure git's pre-commit hook
+# via the core.hooksPath configuration value (should refer, for example, to
+# the /usr/local/bin/bdep-git-hooks directory) and to import
+# bdep-git-pre-commit, for example, for testing the installed hook.
+#
+git-hooks/
+{
+ exe{pre-commit}: in{pre-commit} ../exe{bdep-git-pre-commit} $mods
+}
+
+exe{bdep-git-pre-commit}: in{git-pre-commit} \
+ exe{bdep-git-pre-commit-version-check \
+ bdep-git-pre-commit-copyright-check} \
+ $mods
+
+exe{bdep-git-pre-commit-version-check}: in{git-pre-commit-version-check} \
+ $mods
+
+exe{bdep-git-pre-commit-copyright-check}: in{git-pre-commit-copyright-check} \
+ $mods
+
+# Install the git pre-commit hook into bdep-git-hooks/ subdirectory of, say,
+# /usr/bin/.
+#
+git-hooks/exe{pre-commit}: install = bin/bdep-git-hooks/
diff --git a/bdep-util/git-hooks/.gitignore b/bdep-util/git-hooks/.gitignore
new file mode 100644
index 0000000..416634f
--- /dev/null
+++ b/bdep-util/git-hooks/.gitignore
@@ -0,0 +1 @@
+pre-commit
diff --git a/bdep-util/git-hooks/pre-commit.in b/bdep-util/git-hooks/pre-commit.in
new file mode 100644
index 0000000..3339187
--- /dev/null
+++ b/bdep-util/git-hooks/pre-commit.in
@@ -0,0 +1,23 @@
+#! /usr/bin/env bash
+
+# file : bdep-util/git-hooks/pre-commit.in
+# license : MIT; see accompanying LICENSE file
+
+# Forward the execution to the pre-commit script implementation.
+#
+# To enable the hooks globally run, for example:
+#
+# $ git config --global core.hooksPath /usr/local/bin/bdep-git-hooks
+#
+# Notes:
+#
+# - git passes no parameters to this kind of hooks.
+#
+# - git changes CWD to the git repository root directory for the hook process.
+#
+# - git command running from inside the hook sees files that should be
+# auto-staged due to the -a git-commit option or similar as already staged.
+#
+trap 'exit 1' ERR
+
+source "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/../bdep-git-pre-commit"
diff --git a/bdep-util/git-pre-commit-copyright-check.in b/bdep-util/git-pre-commit-copyright-check.in
new file mode 100644
index 0000000..2e0f982
--- /dev/null
+++ b/bdep-util/git-pre-commit-copyright-check.in
@@ -0,0 +1,73 @@
+#! /usr/bin/env bash
+
+# file : bdep-util/git-pre-commit-copyright-check.in
+# license : MIT; see accompanying LICENSE file
+
+# Check copyright notices in the COPYRIGHT and LICENSE files and issue a
+# warning if they don't include the current year.
+#
+# Specifically, look first for COPYRIGHT and then LICENSE in the root
+# directory and all subdirectories in a project.
+#
+# Then in each file look for the first line matching the:
+#
+# ^Copyright (\([cC]\))? ...
+#
+# Regex, where "..." matches various year lists/ranges (e.g., "2010", "2010,
+# 2011", "2010-2011", and their combinations; see the pattern below for
+# details). Specifically, we don't consider Copyright notices that:
+#
+# - don't start at the very beginning of a line (indented, etc)
+# - contain something other than years prior to the last year (names, etc)
+# - wrap over multiple lines (long year list, etc)
+#
+# Note also that the current year is obtained in the UTC timezone.
+#
+trap 'exit 1' ERR
+set -o errtrace # Trap in functions.
+
+function info () { echo "$*" 1>&2; }
+function error () { info "$*"; exit 1; }
+
+# Recursively collect the COPYRIGHT and LICENSE files, skipping the LICENSE
+# files in directories that contain the COPYRIGHT file.
+#
+# @@ Note that for now we assume that there are no spaces in the project file
+# and directory names.
+#
+files=()
+
+fs=($(find . \( -type f -o -type l \) -name COPYRIGHT))
+
+for f in "${fs[@]}"; do
+ files+=("$f")
+done
+
+fs=($(find . \( -type f -o -type l \) -name LICENSE))
+
+for f in "${fs[@]}"; do
+ d="$(dirname "$f")"
+
+ if [ ! -f "$d/COPYRIGHT" ]; then
+ files+=("$f")
+ fi
+done
+
+# Grep for the Copyright notice in the collected files and issue the warning
+# if it is present and is outdated.
+#
+# @@ We should probably skip the COPYRIGHT/LICENSE files whose parent
+# directories don't (recursively) contain staged files (think of projects
+# with multiple packages, bundled third-party code, etc). Note that we can
+# obtain the staged file list with the `git diff --name-only --cached`
+# command.
+#
+current_year="$(date -u +'%Y')"
+
+for f in "${files[@]}"; do
+ year="$(sed -n -re 's%^Copyright( +\([cC]\))?[ 0-9,-]*[ ,-]([0-9]{4}).*$%\2%p' "$f" | head -n 1)"
+
+ if [ -n "$year" -a "$year" != "$current_year" ]; then
+ info "WARNING: last copyright year in '${f#./}' is $year"
+ fi
+done
diff --git a/bdep-util/git-pre-commit-version-check.in b/bdep-util/git-pre-commit-version-check.in
new file mode 100644
index 0000000..20cd4b0
--- /dev/null
+++ b/bdep-util/git-pre-commit-version-check.in
@@ -0,0 +1,220 @@
+#!/usr/bin/env bash
+
+# file : bdep-util/git-pre-commit-version-check.in
+# license : MIT; see accompanying LICENSE file
+
+# Check that the changes being commited are compatible with the version state
+# of the package(s).
+#
+# Specifically, fail if there are any changes staged for the released packages
+# (the version is final or a stub) unless an appropriate version change is
+# also staged.
+#
+# To achieve this, extract and compare versions corresponding to two states:
+# the latest revision in the current branch (committed) and the potential
+# result of the forthcoming commit (staged).
+#
+trap "{ exit 1; }" ERR
+set -o errtrace # Trap ERR in functions.
+
+function info () { echo "$*" 1>&2; }
+function error () { info "$*"; exit 1; }
+
+@import libbutl/manifest-parser@
+@import libbutl/standard-version@
+
+# Note that here and below a file in the existing repository revision is
+# referred to as '<rev>:<path>' (for example 'HEAD:manifest') and in the
+# staged revision as just ':<path>', where the path is relative to the project
+# root directory (see gitrevisions(7) for details).
+
+# Return 0 if the specified file revision exists and 1 otherwise.
+#
+function file_exists () # [<rev>]:<path>
+{
+ local f="$1"
+
+ if git cat-file -e "$f" 2>/dev/null; then # Repository object exists?
+ local t
+ t="$(git cat-file -t "$f")"
+
+ if [ "$t" == "blob" ]; then
+ return 0
+ fi
+ fi
+
+ return 1
+}
+
+# Wrap libbutl manifest parsing functions to parse manifest revisions and to
+# shorten names. Assumes that the specified manifest revision exists (for
+# example, this is checked with the above file_exists() function).
+#
+function manifest_parser_start () # [<rev>]:<path>
+{
+ butl_manifest_parser_start < <(git cat-file -p "$1")
+ manifest_parser_ofd="$butl_manifest_parser_ofd"
+}
+
+function manifest_parser_finish ()
+{
+ butl_manifest_parser_finish
+}
+
+# Find packages in the repository revision saving them into the specified by
+# name associative array (needs to be declared prior to the function call)
+# mapping the package names to the version/path pairs (for example "libfoo" ->
+# "1.2.3 lib/foo"). Optionally, return only released packages.
+#
+# Note that the repository revisions can be in arbitrary states and the
+# package manifests may not be necessarily present or valid. Thus, we consider
+# a package to be present in the repository revision if its manifest
+# (potentially referred to via the packages.manifest file) exists and contains
+# a non-empty package name and the valid package version. Otherwise, for the
+# staged revision, if it looks like it should be a package but something is
+# missing, we warn.
+#
+function find_packages () # <rev> <result> [<released>]
+{
+ local rev="$1"
+ local -n r="$2"
+ local rel="$3"
+
+ # Collect the potential package directories.
+ #
+ local ds=()
+ local n v
+ if file_exists "$rev:packages.manifest"; then
+ manifest_parser_start "$rev:packages.manifest"
+
+ while IFS=: read -ru "$manifest_parser_ofd" -d '' n v; do
+ if [ "$n" == "location" ]; then
+ ds+=("${v%/}")
+ fi
+ done
+
+ manifest_parser_finish
+ else
+ ds+=(.)
+ fi
+
+ # Fill the resulting package map.
+ #
+ local d
+ for d in "${ds[@]}"; do
+ local m="$d/manifest"
+ local mr="$rev:$m"
+
+ if ! file_exists "$mr"; then
+
+ # Don't warn about absence of the root package manifest file, since this
+ # git repository may well not be a build2 package.
+ #
+ if [ -z "$rev" -a "$d" != "." ]; then
+ info "warning: package manifest file $m does not exist"
+ fi
+ continue
+ fi
+
+ local name=
+ local version=
+
+ manifest_parser_start "$mr"
+
+ while IFS=: read -ru "$manifest_parser_ofd" -d '' n v; do
+ case "$n" in
+ name) name="$v" ;;
+ version) version="$v" ;;
+ esac
+ done
+
+ manifest_parser_finish
+
+ # Check if a non-empty package name is present.
+ #
+ if [ -z "$name" ]; then
+ if [ -z "$rev" ]; then
+ info "warning: package name is missing in $m"
+ fi
+ continue
+ fi
+
+ # Check if a non-empty package version is present.
+ #
+ if [ -z "$version" ]; then
+ if [ -z "$rev" ]; then
+ info "warning: package version is missing in $m"
+ fi
+ continue
+ fi
+
+ # Check if the package version is a valid standard version.
+ #
+ if ! butl_standard_version --is-version --is-not-earliest "$version"; then
+ if [ -z "$rev" ]; then
+ info "warning: package version '$version' in $m is not a valid standard version"
+ fi
+ continue
+ fi
+
+ # Optionally, skip the unreleased version.
+ #
+ if [ ! $rel ] || butl_standard_version --is-not-snapshot "$version"; then
+ r["$name"]="$version $d"
+ fi
+ done
+}
+
+# Collect the commited released packages.
+#
+declare -A committed_packages
+find_packages 'HEAD' committed_packages true
+
+# Collect all the staged packages.
+#
+# Note that while we could bail out if there are no committed released
+# packages, we will still collect the staged packages to potentially issue
+# warnings about some of the manifest errors (empty package name, etc).
+#
+declare -A staged_packages
+find_packages '' staged_packages
+
+# Iterate through the committed released packages and fail if there is a
+# change but no version change staged for this package.
+#
+for p in "${!committed_packages[@]}"; do
+ read cv cd <<<"${committed_packages[$p]}"
+
+ # Check if this is still a package in the staged revision.
+ #
+ if [[ -v staged_packages["$p"] ]]; then
+ read sv sd <<<"${staged_packages[$p]}"
+
+ # If the package version didn't change, then check for any package changes
+ # and fail if there are any.
+ #
+ if [ "$sv" == "$cv" ]; then
+
+ # Check that the package directory didn't change.
+ #
+ # If the package is moved, then detecting its changes becomes too
+ # complicated and we don't want to miss any. Let's keep it simple and
+ # deny moving the released packages (the user can always suppress the
+ # verification with --no-verify anyway).
+ #
+ if [ "$sd" != "$cd" ]; then
+ info "error: moving released package $p $cv"
+ info " info: use --no-verify git option to suppress"
+ exit 1
+ fi
+
+ # Check if the package has some staged changes in its directory.
+ #
+ if ! git diff-index --cached --quiet HEAD -- "$sd"; then
+ info "error: changing released package $p $cv without version increment"
+ info " info: use --no-verify git option to suppress"
+ exit 1
+ fi
+ fi
+ fi
+done
diff --git a/bdep-util/git-pre-commit.in b/bdep-util/git-pre-commit.in
new file mode 100644
index 0000000..27e76f3
--- /dev/null
+++ b/bdep-util/git-pre-commit.in
@@ -0,0 +1,17 @@
+#! /usr/bin/env bash
+
+# file : bdep-util/git-pre-commit.in
+# license : MIT; see accompanying LICENSE file
+
+# Execute various pre-commit scripts in a git repository.
+#
+trap 'exit 1' ERR
+
+scr_dir="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+
+# Run each hook checking the exit status and bailing out if unsuccessful. We
+# can just exec the last one.
+#
+"$scr_dir/bdep-git-pre-commit-version-check"
+
+exec "$scr_dir/bdep-git-pre-commit-copyright-check"