1#!/usr/bin/env bash
2
3# TODO:
4# - inline replace
5# - clang-format-diff replacement
6# - uncrustify for patches (not git refs)
7# - maybe integrate into travis-ci?
8
9function usage()
10{
11    cat <<EOL
12$0 [ OPTS ] [ file-or-gitref [ ... ] ]
13
14Example:
15  # Chech HEAD git ref
16  $ $0 -r
17  $ $0 -r HEAD
18
19  # Check patch
20  $ git format-patch --stdout -1 | $0 -p
21  $ git show -1 | $0 -p
22
23  # Or via regular files
24  $ git format-patch --stdout -2
25  $ $0 *.patch
26
27  # Over a file
28  $ $0 -d event.c
29  $ $0 -d < event.c
30
31  # And print the whole file not only summary
32  $ $0 -f event.c
33  $ $0 -f < event.c
34
35OPTS:
36  -p   - treat as patch
37  -f   - treat as regular file
38  -f   - treat as regular file and print diff
39  -r   - treat as git revision (default)
40  -C   - check using clang-format (default)
41  -U   - check with uncrustify
42  -c   - config for clang-format/uncrustify
43  -h   - print this message
44EOL
45}
46function cfg()
47{
48    [ -z "${options[cfg]}" ] || {
49        echo "${options[cfg]}"
50        return
51    }
52
53    local dir="$(dirname "${BASH_SOURCE[0]}")"
54    [ "${options[clang]}" -eq 0 ] || {
55        echo "$dir/.clang-format"
56        return
57    }
58    [ "${options[uncrustify]}" -eq 0 ] || {
59        echo "$dir/.uncrustify"
60        return
61    }
62}
63function abort()
64{
65    local msg="$1"
66    shift
67
68    printf "$msg\n" "$@" >&2
69    exit 1
70}
71function default_arg()
72{
73    if [ "${options[ref]}" -eq 1 ]; then
74        echo "HEAD"
75    else
76        [ ! -t 0 ] || abort "<stdin> is a tty"
77        echo "/dev/stdin"
78    fi
79}
80function parse_options()
81{
82    options[patch]=0
83    options[file]=0
84    options[file_diff]=0
85    options[ref]=1
86    options[clang]=1
87    options[uncrustify]=0
88    options[cfg]=
89
90    local OPTARG OPTIND c
91    while getopts "pfrdCUc:h?" c; do
92        case "$c" in
93            p)
94                options[patch]=1
95                options[ref]=0
96                options[file]=0
97                options[file_diff]=0
98                ;;
99            f)
100                options[file]=1
101                options[ref]=0
102                options[patch]=0
103                options[file_diff]=0
104                ;;
105            r)
106                options[ref]=1
107                options[file]=0
108                options[patch]=0
109                options[file_diff]=0
110                ;;
111            d)
112                options[file_diff]=1
113                options[file]=0
114                options[patch]=0
115                options[ref]=0
116                ;;
117            C)
118                options[clang]=1
119                options[uncrustify]=0
120                ;;
121            U)
122                options[uncrustify]=1
123                options[clang]=0
124                ;;
125            c) options[cfg]="$OPTIND" ;;
126            ?|h)
127                usage
128                exit 0
129                ;;
130            *)
131                usage
132                exit 1
133                ;;
134        esac
135    done
136
137    options[cfg]="$(cfg)"
138
139    [ -f "${options[cfg]}" ] || \
140        abort "Config '%s' does not exist" "${options[cfg]}"
141
142    shift $((OPTIND - 1))
143    args=( "$@" )
144
145    if [ ${#args[@]} -eq 0 ]; then
146        # exit on error globally, not only in subshell
147        default_arg > /dev/null
148        args=( "$(default_arg)" )
149    fi
150
151    if [ "${args[0]}" = "/dev/stdin" ]; then
152        TMP_FILE="/tmp/libevent.checkpatch.$RANDOM"
153        cat > "$TMP_FILE"
154        trap "rm '$TMP_FILE'" EXIT
155
156        args[0]="$TMP_FILE"
157    fi
158}
159
160function diff() { command diff --color=always "$@"; }
161
162function clang_style()
163{
164    local c="${options[cfg]}"
165    echo "{ $(sed -e 's/#.*//' -e '/---/d' -e '/\.\.\./d' "$c" | tr $'\n' ,) }"
166}
167function clang_format() { clang-format --style="$(clang_style)" "$@"; }
168function clang_format_diff() { clang-format-diff --style="$(clang_style)" "$@"; }
169# for non-bare repo will work
170function clang_format_git()
171{ git format-patch --stdout "$@" -1 | clang_format_diff; }
172
173function uncrustify() { command uncrustify -c "${options[cfg]}" "$@"; }
174function uncrustify_frag() { uncrustify -l C --frag "$@"; }
175function uncrustify_indent_off() { echo '/* *INDENT-OFF* */'; }
176function uncrustify_indent_on() { echo '/* *INDENT-ON* */'; }
177function git_hunk()
178{
179    local ref=$1 f=$2
180    shift 2
181    git cat-file -p $ref:$f
182}
183function uncrustify_git_indent_hunk()
184{
185    local start=$1 end=$2
186    shift 2
187
188    # Will be beatier with tee(1), but doh bash async substitution
189    { uncrustify_indent_off; git_hunk "$@" | head -n$((start - 1)); }
190    { uncrustify_indent_on;  git_hunk "$@" | head -n$((end - 1)) | tail -n+$start; }
191    { uncrustify_indent_off; git_hunk "$@" | tail -n+$((end + 1)); }
192}
193function strip()
194{
195    local start=$1 end=$2
196    shift 2
197
198    # seek indent_{on,off}()
199    let start+=2
200    head -n$end | tail -n+$start
201}
202function patch_ranges()
203{
204    egrep -o '^@@ -[0-9]+(,[0-9]+|) \+[0-9]+(,[0-9]+|) @@' | \
205        cut -d' ' -f3
206}
207function git_ranges()
208{
209    local ref=$1 f=$2
210    shift 2
211
212    git diff -W $ref^..$ref -- $f | patch_ranges
213}
214function diff_substitute()
215{
216    local f="$1"
217    shift
218
219    sed \
220        -e "s#^--- /dev/fd.*\$#--- a/$f#" \
221        -e "s#^+++ /dev/fd.*\$#+++ b/$f#"
222}
223function uncrustify_git()
224{
225    local ref=$1 r f start end length
226    shift
227
228    local files=( $(git diff --name-only $ref^..$ref | egrep "\.(c|h)$") )
229    for f in "${files[@]}"; do
230        local ranges=( $(git_ranges $ref "$f") )
231        for r in "${ranges[@]}"; do
232            [[ ! "$r" =~ ^\+([0-9]+)(,([0-9]+)|)$ ]] && continue
233            start=${BASH_REMATCH[1]}
234            [ -n "${BASH_REMATCH[3]}" ] && \
235                length=${BASH_REMATCH[3]} || \
236                length=1
237            end=$((start + length))
238            echo "Range: $start:$end ($length)" >&2
239
240            diff -u \
241                <(uncrustify_git_indent_hunk $start $end $ref "$f" | strip $start $end) \
242                <(uncrustify_git_indent_hunk $start $end $ref "$f" | uncrustify_frag | strip $start $end) \
243            | diff_substitute "$f"
244        done
245    done
246}
247function uncrustify_diff() { abort "Not implemented"; }
248function uncrustify_file() { uncrustify -f "$@"; }
249
250function checker()
251{
252    local c=$1 u=$2
253    shift 2
254
255    [ "${options[clang]}" -eq 0 ] || {
256        $c "$@"
257        return
258    }
259    [ "${options[uncrustify]}" -eq 0 ] || {
260        $u "$@"
261        return
262    }
263}
264function check_patch() { checker clang_format_diff uncrustify_diff "$@"; }
265function check_file() { checker clang_format uncrustify_file "$@"; }
266function check_ref() { checker clang_format_git uncrustify_git "$@"; }
267
268function check_arg()
269{
270    [ "${options[patch]}" -eq 0 ] || {
271        check_patch "$@"
272        return
273    }
274    [ "${options[file]}" -eq 0 ] || {
275        check_file "$@"
276        return
277    }
278    [ "${options[file_diff]}" -eq 0 ] || {
279        diff -u "$@" <(check_file "$@") | diff_substitute "$@"
280        return
281    }
282    [ "${options[ref]}" -eq 0 ] || {
283        check_ref "$@"
284        return
285    }
286}
287
288function main()
289{
290    local a
291    for a in "${args}"; do
292        check_arg "$a"
293    done
294}
295
296declare -A options
297parse_options "$@"
298
299main "$@" | less -FRSX
300