Skip to content

Commit 32a0647

Browse files
committed
Added a utility to check copyright statements in specified files.
- Legacy-Id: 16302
1 parent f9cd721 commit 32a0647

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

bin/check-copyright

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#!/usr/bin/env python
2+
# -*- python -*-
3+
# Copyright The IETF Trust 2019, All Rights Reserved
4+
"""
5+
NAME
6+
$program - Check for current copyright notice in given files
7+
8+
SYNOPSIS
9+
$program [OPTIONS] ARGS
10+
11+
DESCRIPTION
12+
Given a list of files or filename wildcard patterns, check all for
13+
an IETF Trust copyright notice with the current year.
14+
15+
%(options)s
16+
17+
FILES
18+
19+
AUTHOR
20+
Written by Henrik Levkowetz, <henrik@tools.ietf.org>
21+
22+
COPYRIGHT
23+
Copyright 2019 the IETF Trust
24+
25+
This program is free software; you can redistribute it and/or modify
26+
it under the terms of the Simplified BSD license as published by the
27+
Open Source Initiative at http://opensource.org/licenses/BSD-2-Clause.
28+
29+
"""
30+
from __future__ import print_function
31+
32+
import os
33+
import sys
34+
import time
35+
36+
path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
37+
if not path in sys.path:
38+
sys.path.insert(0, path)
39+
40+
import getopt
41+
import re
42+
import pytz
43+
import tzparse
44+
import debug
45+
46+
version = "0.10"
47+
program = os.path.basename(sys.argv[0])
48+
progdir = os.path.dirname(sys.argv[0])
49+
50+
# ----------------------------------------------------------------------
51+
# Parse options
52+
53+
options = ""
54+
for line in re.findall("\n +(if|elif) +opt in \[(.+)\]:\s+#(.+)\n", open(sys.argv[0]).read()):
55+
if not options:
56+
options += "OPTIONS\n"
57+
options += " %-16s %s\n" % (line[1].replace('"', ''), line[2])
58+
options = options.strip()
59+
60+
# with ' < 1:' on the next line, this is a no-op:
61+
if len(sys.argv) < 1:
62+
print(__doc__ % locals())
63+
sys.exit(1)
64+
65+
try:
66+
opts, files = getopt.gnu_getopt(sys.argv[1:], "hvV", ["help", "version", "verbose",])
67+
except Exception, e:
68+
print( "%s: %s" % (program, e))
69+
sys.exit(1)
70+
71+
# ----------------------------------------------------------------------
72+
# Handle options
73+
74+
# set default values, if any
75+
opt_verbose = 0
76+
77+
# handle individual options
78+
for opt, value in opts:
79+
if opt in ["-h", "--help"]: # Output this help, then exit
80+
print( __doc__ % locals() )
81+
sys.exit(1)
82+
elif opt in ["-V", "--version"]: # Output version information, then exit
83+
print( program, version )
84+
sys.exit(0)
85+
elif opt in ["-v", "--verbose"]: # Output version information, then exit
86+
opt_verbose += 1
87+
88+
# ----------------------------------------------------------------------
89+
def say(s):
90+
sys.stderr.write("%s\n" % (s))
91+
92+
# ----------------------------------------------------------------------
93+
def note(s):
94+
if opt_verbose:
95+
sys.stderr.write("%s\n" % (s))
96+
97+
# ----------------------------------------------------------------------
98+
def die(s, error=1):
99+
sys.stderr.write("\n%s: Error: %s\n\n" % (program, s))
100+
sys.exit(error)
101+
102+
# ----------------------------------------------------------------------
103+
104+
def pipe(cmd, inp=None):
105+
import shlex
106+
from subprocess import Popen, PIPE
107+
args = shlex.split(cmd)
108+
bufsize = 4096
109+
stdin = PIPE if inp else None
110+
pipe = Popen(args, stdin=stdin, stdout=PIPE, stderr=PIPE, bufsize=bufsize)
111+
out, err = pipe.communicate(inp)
112+
code = pipe.returncode
113+
if code != 0:
114+
raise OSError(err)
115+
return out
116+
117+
# ----------------------------------------------------------------------
118+
def split_loginfo(line):
119+
try:
120+
parts = line.split()
121+
rev = parts[0][1:]
122+
who = parts[2]
123+
date = parts[4]
124+
time = parts[5]
125+
tz = parts[6]
126+
when = tzparse.tzparse(" ".join(parts[4:7]), "%Y-%m-%d %H:%M:%S %Z")
127+
when = when.astimezone(pytz.utc)
128+
except ValueError as e:
129+
sys.stderr.write("Bad log line format: %s\n %s\n" % (line, e))
130+
131+
return rev, who, when
132+
133+
# ----------------------------------------------------------------------
134+
def get_first_commit(path):
135+
note("Getting first commit for '%s'" % path)
136+
cmd = 'svn log %s' % path
137+
if opt_verbose > 1:
138+
note("Running '%s' ..." % cmd)
139+
commit_log = pipe(cmd)
140+
commit_log = commit_log.splitlines()
141+
commit_log.reverse()
142+
for line in commit_log:
143+
if re.search(loginfo_format, line):
144+
rev, who, when = split_loginfo(line)
145+
break
146+
else:
147+
pass
148+
return { path: { 'rev': rev, 'who': who, 'date': when.strftime('%Y-%m-%d %H:%M:%S'), }, }
149+
150+
151+
# ----------------------------------------------------------------------
152+
# The program itself
153+
154+
import os
155+
import json
156+
157+
cwd = os.getcwd()
158+
159+
if cwd.split(os.path.sep)[-1] != 'trunk':
160+
die("Expected to run this operation in trunk, but the current\ndirectory is '%s'" % cwd)
161+
162+
# Get current initinfo from cache and svn
163+
cachefn = os.path.join(os.environ.get('HOME', '.'), '.initinfo')
164+
165+
if os.path.exists(cachefn):
166+
note("Reading initinfo cache file %s" % cachefn)
167+
with open(cachefn, "r") as file:
168+
cache = json.load(file)
169+
else:
170+
sys.stderr.write("No initinfo cache file found -- will have to extract all information from SVN.\n"+
171+
"This may take some time.\n\n")
172+
cache = {}
173+
initinfo = cache
174+
175+
merged_revs = {}
176+
write_cache = False
177+
loginfo_format = r'^r[0-9]+ \| [^@]+@[^@]+ \| \d\d\d\d-\d\d-\d\d '
178+
179+
year = time.strftime('%Y')
180+
for path in files:
181+
note("Checking path %s" % path)
182+
if not path in initinfo:
183+
initinfo.update(get_first_commit(path))
184+
write_cache = True
185+
date = initinfo[path]['date']
186+
init = date[:4]
187+
copyright = "(?i)Copyright The IETF Trust (%s-)?%s, All Rights Reserved" % (init, year)
188+
with open(path) as file:
189+
chunk = file.read(4000)
190+
if os.path.basename(path) == '__init__.py' and len(chunk)==0:
191+
continue
192+
if not re.search(copyright, chunk):
193+
sys.stdout.write("%s(1): Error: Missing or bad copyright. " % path)
194+
if year == init:
195+
print(" Expected: Copyright The IETF Trust %s, All Rights Reserved" % year)
196+
else:
197+
print(" Expected: Copyright The IETF Trust %s-%s, All Rights Reserved" % (init, year))
198+
199+
if write_cache:
200+
cache = initinfo
201+
with open(cachefn, "w") as file:
202+
json.dump(cache, file, indent=2, sort_keys=True)
203+

0 commit comments

Comments
 (0)