Skip to content

Commit 14df71e

Browse files
committed
Capturing changes to pre-commit and testing commit hook correction. Related to ietf-tools#3297. Commit ready to merge.
- Legacy-Id: 19056
1 parent ffd82f1 commit 14df71e

1 file changed

Lines changed: 69 additions & 162 deletions

File tree

hooks/pre-commit

Lines changed: 69 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,69 @@
1-
#!/usr/bin/env python
2-
3-
"""
4-
An SVN pre-commit hook which requires that commits either are marked as
5-
whitespace cleanup commits, and contain no non-whitespace changes, or
6-
leave whitespace alone on lines without code changes.
7-
"""
8-
9-
import os
10-
import sys
11-
import difflib
12-
#import debug
13-
from pysvn import Client, Transaction
14-
15-
prog = os.path.basename(sys.argv[0])
16-
17-
def die(msg):
18-
sys.stderr.write("\n%s: Error: %s\n" % (prog, msg))
19-
sys.exit(1)
20-
21-
if len(sys.argv) <= 1:
22-
die("Expected arguments: REPOSITORY TRANSACTION, found none")
23-
24-
if len(sys.argv) <= 2:
25-
die( "Expected arguments: REPOSITORY TRANSACTION, found only '%s'" % sys.argv[1])
26-
27-
repo = sys.argv[1]
28-
txname = sys.argv[2]
29-
tx = Transaction(repo, txname)
30-
client = Client()
31-
32-
is_whitespace_cleanup = "whitespace cleanup" in tx.revpropget("svn:log").lower()
33-
34-
def normalize(s):
35-
return s.rstrip().expandtabs()
36-
37-
def normalize_sequence(seq):
38-
o = []
39-
for l in seq:
40-
o.append(normalize(l))
41-
return o
42-
43-
def normalize_file_end(seq):
44-
while True and seq:
45-
if seq[-1].strip() == "":
46-
del seq[-1]
47-
else:
48-
break
49-
return seq
50-
51-
def count(gen):
52-
return sum(1 for _ in gen)
53-
54-
# Function with side effects. Acts on global varaibles
55-
def inc_ab(flag):
56-
global a, b
57-
if flag == ' ':
58-
a += 1; b += 1
59-
elif flag == '-':
60-
a += 1
61-
elif flag == '+':
62-
b += 1
63-
elif flag == '?':
64-
pass
65-
else:
66-
raise ValueError("Unexpected ndiff mark: '%s' in: %s" % (flag, plain_diff[i]))
67-
68-
def get_chunks(unidiff):
69-
if not unidiff:
70-
return [], []
71-
chunks = []
72-
chunk = []
73-
intro = unidiff[0:2]
74-
for line in unidiff[2:]:
75-
if line.startswith("@@"):
76-
if chunk:
77-
chunks.append(chunk)
78-
chunk = [line]
79-
else:
80-
chunk.append(line)
81-
chunks.append(chunk)
82-
return intro, chunks
83-
84-
changes = tx.changed()
85-
issues = {}
86-
context = 3
87-
for path in changes:
88-
action, kind, mod, propmod = changes[path]
89-
90-
# Don't try to diff added or deleted files, on ly changed text files
91-
if not (mod and action == "R"):
92-
continue
93-
94-
# Don't try do diff binary files
95-
mimetype = tx.propget("svn:mime-type", path)
96-
if mimetype and not mimetype.startswith("text/"):
97-
continue
98-
99-
new = tx.cat(path).splitlines()
100-
old = client.cat("file://"+os.path.join(repo,path)).splitlines()
101-
102-
# Added trailing space can mess up the comparison -- eliminate it
103-
new = normalize_file_end(new)
104-
old = normalize_file_end(old)
105-
106-
plain_diff = list(difflib.unified_diff(old, new, "%s (repository)"%path, "%s (commit)"%path, lineterm="" ))
107-
old = normalize_sequence(old)
108-
new = normalize_sequence(new)
109-
white_diff = list(difflib.unified_diff(old, new, "%s (repository)"%path, "%s (commit)"%path, lineterm=""))
110-
111-
plain_count = len(plain_diff)
112-
white_count = len(white_diff)
113-
114-
# for i in range(len(white_diff)):
115-
# sys.stderr.write("%-80s | %-80s\n" % (normalize(plain_diff[i][:80]), normalize(white_diff[i][:80])))
116-
if white_count != plain_count and not is_whitespace_cleanup:
117-
intro, plain_chunks = get_chunks(plain_diff)
118-
intro, white_chunks = get_chunks(white_diff)
119-
deletes = []
120-
for chunk in white_chunks:
121-
for i in range(len(plain_chunks)):
122-
if chunk == plain_chunks[i]:
123-
deletes += [i]
124-
deletes.reverse()
125-
for i in deletes:
126-
del plain_chunks[i]
127-
issue = intro
128-
for chunk in plain_chunks:
129-
issue += chunk
130-
if len(plain_chunks) > 1:
131-
are = "are"; s = "s"; an = ""
132-
else:
133-
are = "is"; s = ""; an = "an "
134-
issues[path] = issue
135-
if white_count != 0 and is_whitespace_cleanup:
136-
intro, white_chunks = get_chunks(white_diff)
137-
if len(white_chunks) > 1:
138-
are = "are"; s = "s"; an = ""
139-
else:
140-
are = "is"; s = ""; an = "an "
141-
issues[path] = white_diff
142-
143-
if issues:
144-
if is_whitespace_cleanup:
145-
die("It looks as if there are non-whitespace changes in\n"
146-
"this commit, but it was marked as a whitespace cleanup commit.\n\n"
147-
"Here %s the diff chunk%s with unexpected change%s:\n\n%s\n\n"
148-
"Declining the commit due to a mix of code and spaces-only changes. Please\n"
149-
"avoid mixing whitespace-only changes with code changes. See details above." %
150-
(are, s, s, '\n\n'.join([ '\n'.join(issues[path]) for path in issues ]))
151-
)
152-
153-
else:
154-
die("It looks as if there are spaces-only changes in this\n"
155-
"commit, but it was not marked as a whitespace cleanup commit.\n\n"
156-
"Here %s the diff chunk%s with unexpected change%s:\n\n%s\n\n"
157-
"Declining the commit due to a mix of code and spaces-only changes. Please\n"
158-
"avoid mixing whitespace-only changes with code changes. See details above." %
159-
(are, s, s, '\n\n'.join([ '\n'.join(issues[path]) for path in issues ]))
160-
)
161-
162-
sys.exit(0)
1+
#!/bin/bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing,
14+
# software distributed under the License is distributed on an
15+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
# KIND, either express or implied. See the License for the
17+
# specific language governing permissions and limitations
18+
# under the License.
19+
#
20+
#
21+
# $Id$
22+
#
23+
# Prevents some SHA-1 collisions to be commited
24+
# Test fo the 320 byte prefix found on https://shattered.io/
25+
# If the files are committed in the same transaction, svnlook
26+
# will error out itself due to the apparent corruption in the
27+
# candidate revision
28+
29+
REPOS="$1"
30+
TXN="$2"
31+
SVNLOOK=/usr/bin/svnlook
32+
YEAR=$(date +%Y)
33+
34+
$SVNLOOK changed -t "$TXN" "$REPOS"
35+
if [ $? -ne 0 ]; then
36+
echo "svnlook failed, possible SHA-1 collision" >&2
37+
exit 2
38+
fi
39+
40+
FILES=$($SVNLOOK changed -t "$TXN" "$REPOS" | grep -Ev '^D ' | /usr/bin/awk '{print $2}')
41+
for FILE in $FILES; do
42+
if [ -f $FILE ]; then
43+
# Check against known sha-1 collision attack. Someone committing 2 different files with this
44+
# known hash collision could otherwise break the repository.
45+
PREFIX=$($SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | head -c320 | /usr/bin/sha1sum | cut -c-40)
46+
if [ "$PREFIX" = 'f92d74e3874587aaf443d1db961d4e26dde13e9c' ]; then
47+
echo "known SHA-1 collision rejected" >&2
48+
exit 3
49+
fi
50+
51+
# Verify copyright year
52+
if [[ $FILE == */ietf/*.py || -s $FILE ]]; then
53+
$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | head -n 3 | grep -q "Copyright .*IETF Trust .*$YEAR.*" || {
54+
echo "
55+
Bad or missing copyright note in $FILE.
56+
Expected 'Copyright The IETF Trust ... $YEAR, All Rights Reserved',
57+
(or similar) at the start of the file.
58+
59+
For bulk correction of copyright statements, try bin/check-copyright with
60+
patching:
61+
62+
\$ bin/check-copyright -p \$(svn st | cut -c 9- | grep '\.py\$' ) | patch -p0
63+
64+
" >&2
65+
exit 3
66+
}
67+
fi
68+
fi
69+
done

0 commit comments

Comments
 (0)