From 768727f37afa6fbb5082833a4c14c8134ec42122 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 6 Aug 2020 23:32:13 +0300 Subject: Add implementation --- bdep-util/.gitignore | 3 + bdep-util/buildfile | 38 +++++ bdep-util/git-hooks/.gitignore | 1 + bdep-util/git-hooks/pre-commit.in | 23 +++ bdep-util/git-pre-commit-copyright-check.in | 73 +++++++++ bdep-util/git-pre-commit-version-check.in | 220 ++++++++++++++++++++++++++++ bdep-util/git-pre-commit.in | 17 +++ 7 files changed, 375 insertions(+) create mode 100644 bdep-util/.gitignore create mode 100644 bdep-util/buildfile create mode 100644 bdep-util/git-hooks/.gitignore create mode 100644 bdep-util/git-hooks/pre-commit.in create mode 100644 bdep-util/git-pre-commit-copyright-check.in create mode 100644 bdep-util/git-pre-commit-version-check.in create mode 100644 bdep-util/git-pre-commit.in (limited to 'bdep-util') 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 ':' (for example 'HEAD:manifest') and in the +# staged revision as just ':', 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 () # []: +{ + 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 () # []: +{ + 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 () # [] +{ + 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" -- cgit v1.1