xref: /openssl/util/check-format-commit.sh (revision 7d91d5ba)
1#!/bin/bash
2# Copyright 2020-2024 The OpenSSL Project Authors. All Rights Reserved.
3#
4# Licensed under the Apache License 2.0 (the "License").
5# You may not use this file except in compliance with the License.
6# You can obtain a copy in the file LICENSE in the source distribution
7# or at https://www.openssl.org/source/license.html
8#
9# This script is a wrapper around check-format.pl.  It accepts the same commit
10# revision range as 'git diff' as arguments, and uses it to identify the files
11# and ranges that were changed in that range, filtering check-format.pl output
12# only to lines that fall into the change ranges of the changed files.
13#
14
15# Allowlist of files to scan
16# Currently this is any .c or .h file (with an optional .in suffix
17FILE_ALLOWLIST=("\.[ch]\(.in\)\?")
18
19# Exit code for the script
20EXIT_CODE=0
21
22# Global vars
23
24# TEMPDIR is used to hold any files this script creates
25# And is cleaned on EXIT with a trap function
26TEMPDIR=$(mktemp -d /tmp/checkformat.XXXXXX)
27
28# TOPDIR always points to the root of the git tree we are working in
29# used to locate the check-format.pl script
30TOPDIR=$(git rev-parse --show-toplevel)
31
32
33# cleanup handler function, returns us to the root of the git tree
34# and erases our temp directory
35cleanup() {
36    rm -rf $TEMPDIR
37    cd $TOPDIR
38}
39
40trap cleanup EXIT
41
42# Get the canonical sha256 sum for the commits we are checking
43# This lets us pass in symbolic ref names like master/etc and
44# resolve them to sha256 sums easily
45COMMIT_RANGE="$@"
46COMMIT_LAST=$(git rev-parse $COMMIT_RANGE)
47
48# Fail gracefully if git rev-parse doesn't produce a valid
49# commit
50if [ $? -ne 0 ]
51then
52    echo "$1 is not a valid revision"
53    exit 1
54fi
55
56# If the commit range was just one single revision, git rev-parse
57# will output jut commit id of that one alone.  In that case, we
58# must manipulate a little to get a desirable result, 'cause git
59# diff has a slightly different interpretation of a single commit
60# id, and takes that to mean all commits up to HEAD.
61if [ $(echo "$COMMIT_LAST" | wc -l) -gt 1 ]; then
62    COMMIT_LAST=$(echo "$COMMIT_LAST" | head -1)
63else
64    # $COMMIT_RANGE is just one commit, make it an actual range
65    COMMIT_RANGE=$COMMIT_RANGE^..$COMMIT_RANGE
66fi
67
68# Create an iterable list of files to check formatting on,
69# including the line ranges that are changed by the commits
70# It produces output of this format:
71# <file name> <change start line>, <change line count>
72touch $TEMPDIR/ranges.txt
73git diff -U0 $COMMIT_RANGE | awk '
74    BEGIN {myfile=""}
75    /+{3}/ {
76        gsub(/b\//,"",$2);
77        myfile=$2
78    }
79    /@@/ {
80        gsub(/+/,"",$3);
81        printf myfile " " $3 "\n"
82    }' >> $TEMPDIR/ranges.txt || true
83
84# filter in anything that matches on a filter regex
85for i in ${FILE_ALLOWLIST[@]}
86do
87    touch $TEMPDIR/ranges.filter
88    # Note the space after the $i below.  This is done because we want
89    # to match on file suffixes, but the input file is of the form
90    # <commit> <file> <range start>, <range length>
91    # So we can't just match on end of line.  The additional space
92    # here lets us match on suffixes followed by the expected space
93    # in the input file
94    grep "$i " $TEMPDIR/ranges.txt >> $TEMPDIR/ranges.filter || true
95done
96cp $TEMPDIR/ranges.filter $TEMPDIR/ranges.txt
97REMAINING_FILES=$(wc -l $TEMPDIR/ranges.filter | awk '{print $1}')
98if [ $REMAINING_FILES -eq 0 ]
99then
100    echo "This commit has no files that require checking"
101    exit 0
102fi
103
104# check out the files from the commit level.
105# For each file name in ranges, we show that file at the commit
106# level we are checking, and redirect it to the same path, relative
107# to $TEMPDIR/check-format.  This give us the full file to run
108# check-format.pl on with line numbers matching the ranges in the
109# $TEMPDIR/ranges.txt file
110for j in $(cat $TEMPDIR/ranges.txt | awk '{print $1}' | sort | uniq)
111do
112    FDIR=$(dirname $j)
113    mkdir -p $TEMPDIR/check-format/$FDIR
114    git show $COMMIT_LAST:$j > $TEMPDIR/check-format/$j
115done
116
117# Now for each file in $TEMPDIR/ranges.txt, run check-format.pl
118for j in $(cat $TEMPDIR/ranges.txt | awk '{print $1}' | sort | uniq)
119do
120    range_start=()
121    range_end=()
122
123    # Get the ranges for this file. Create 2 arrays.  range_start contains
124    # the start lines for valid ranges from the commit.  the range_end array
125    # contains the corresponding end line (note, since diff output gives us
126    # a line count for a change, the range_end[k] entry is actually
127    # range_start[k]+line count
128    for k in $(grep ^$j $TEMPDIR/ranges.txt | awk '{print $2}')
129    do
130        RANGE=$k
131        RSTART=$(echo $RANGE | awk -F',' '{print $1}')
132        RLEN=$(echo $RANGE | awk -F',' '{print $2}')
133        # when the hunk is just one line, its length is implied
134        if [ -z "$RLEN" ]; then RLEN=1; fi
135        let REND=$RSTART+$RLEN
136        range_start+=($RSTART)
137        range_end+=($REND)
138    done
139
140    # Go to our checked out tree
141    cd $TEMPDIR/check-format
142
143    # Actually run check-format.pl on the file, capturing the output
144    # in a temporary file.  Note the format of check-patch.pl output is
145    # <file name>:<line number>:<error text>:<offending line contents>
146    $TOPDIR/util/check-format.pl $j > $TEMPDIR/format-results.txt
147
148    # Now we filter the check-format.pl output based on the changed lines
149    # captured in the range_start/end arrays
150    let maxidx=${#range_start[@]}-1
151    for k in $(seq 0 1 $maxidx)
152    do
153        RSTART=${range_start[$k]}
154        REND=${range_end[$k]}
155
156        # field 2 of check-format.pl output is the offending line number
157        # Check here if any line in that output falls between any of the
158        # start/end ranges defined in the range_start/range_end array.
159        # If it does fall in that range, print the entire line to stdout
160        # If anything is printed, have awk exit with a non-zero exit code
161        awk -v rstart=$RSTART -v rend=$REND -F':' '
162                BEGIN {rc=0}
163                /:/ {
164                    if (($2 >= rstart) && ($2 <= rend)) {
165                        print $0;
166                        rc=1
167                    }
168                }
169                END {exit rc;}
170            ' $TEMPDIR/format-results.txt
171
172        # If awk exited with a non-zero code, this script will also exit
173        # with a non-zero code
174        if [ $? -ne 0 ]
175        then
176            EXIT_CODE=1
177        fi
178    done
179done
180
181# Exit with the recorded exit code above
182exit $EXIT_CODE
183