Fix link to the Coding Standards wiki page.
[arvados-dev.git] / git / hooks / check-copyright-headers.rb
1 #!/usr/bin/env ruby
2
3 # Copyright (C) The Arvados Authors. All rights reserved.
4 #
5 # SPDX-License-Identifier: AGPL-3.0
6
7 # This script can be installed as a git update hook.
8
9 # It can also be installed as a gitolite 'hooklet' in the
10 # hooks/common/update.secondary.d/ directory.
11
12 # NOTE: this script runs under the same assumptions as the 'update' hook, so
13 # the starting directory must be maintained and arguments must be passed on.
14
15 $refname = ARGV[0]
16 $oldrev  = ARGV[1]
17 $newrev  = ARGV[2]
18 $user    = ENV['USER']
19
20 puts "Enforcing copyright headers..."
21 puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
22
23 def load_licenseignore
24   $licenseignore = `git show #{$newrev}:.licenseignore 2>/dev/null`.gsub(/\./,'\\.').gsub(/\*/,'.*').gsub(/\?/,'.').split("\n")
25 end
26
27 def check_file(filename, header, broken)
28   ignore = false
29   $licenseignore.each do |li|
30     if filename =~ /#{li}/
31       ignore = true
32     end
33   end
34   return broken if ignore
35
36   if header !~ /SPDX-License-Identifier:/
37     if not broken
38       puts "\nERROR\n"
39     end
40     puts "missing or invalid copyright header in file #{filename}"
41     broken = true
42   end
43   return broken
44 end
45
46 # enforce copyright headers
47 def check_copyright_headers
48   if ($newrev[0,6] ==  '000000')
49     # A branch is being deleted. Do not check old commits for DCO signoff!
50     all_objects = []
51     commits = []
52   elsif ($oldrev[0,6] ==  '000000')
53     if $refname != 'refs/heads/master'
54       # A new branch was pushed. Check all new commits in this branch.
55       puts "git rev-list --objects master..#{$newrev} | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)'| sed -n 's/^blob //p'"
56       blob_objects  = `git rev-list --objects master..#{$newrev} | git cat-file --follow-symlinks --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)'| sed -n 's/^blob //p'`.split("\n")
57       commit_objects  = `git rev-list --objects master..#{$newrev} | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)'| sed -n 's/^commit //p'`.split("\n")
58       all_objects = blob_objects + commit_objects
59       commits = `git rev-list master..#{$newrev}`.split("\n")
60     else
61       # When does this happen?
62       puts "UNEXPECTED ERROR"
63       exit 1
64     end
65   else
66     blob_objects = `git rev-list --objects #{$oldrev}..#{$newrev} --not --branches='*' | git cat-file --follow-symlinks --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)'| sed -n 's/^blob //p'`.split("\n")
67     commit_objects  = `git rev-list --objects #{$oldrev}..#{$newrev} | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)'| sed -n 's/^commit //p'`.split("\n")
68     all_objects = blob_objects + commit_objects
69     commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
70   end
71
72   broken = false
73
74   all_objects.each do |rev|
75     ignore = false
76     tmp = rev.split(' ')
77     if tmp[2].nil?
78       # git object of type 'commit'
79        # This could be a new file that was added in this commit
80       # If this wasn't a bare repo, we could run the following to get the list of new files in this commit:
81       #    new_files = `git show #{tmp[0]} --name-only --diff-filter=A --pretty=""`.split("\n")
82       # Instead, we just look at all the files touched in the commit and check the diff to see
83       # see if it is a new file. This could prove brittle...
84       files = `git show #{tmp[0]} --name-only --pretty=""`.split("\n")
85       files.each do |f|
86         filename = f
87         commit = `git show #{tmp[0]} -- #{f}`
88         # Only consider files, not symlinks (mode 120000)
89         if commit =~ /^new file mode (100644|10755)\nindex 000000/
90           headerCount = 0
91           lineCount = 0
92           header = ""
93           previousLine = ""
94           commit.each_line do |line|
95             if ((headerCount == 0) and (line =~ /Copyright.*All rights reserved./))
96               header = previousLine
97               header += line
98               headerCount = 1
99             elsif ((headerCount > 0) and (headerCount < 3))
100               header += line
101               headerCount += 1
102             elsif (headerCount == 3)
103               break
104             end
105             previousLine = line
106             lineCount += 1
107             if lineCount > 50
108               break
109             end
110           end
111           broken = check_file(filename, header, broken)
112         end
113       end
114     else
115       # git object of type 'blob'
116       filename = tmp[2]
117       # test if this is a symlink.
118       # Get the tree for each revision we are considering, find the blob hash in there, check the mode at start of line.
119       # Stop looking at revisions once we have a match.
120       symlink = false
121       commits.each do |r|
122         tree = `git cat-file -p #{r}^{tree}`
123         if tree =~ /#{tmp[0]}/
124            if tree =~ /^120000.blob.#{tmp[0]}/
125             symlink = true
126           end
127           break
128         end
129       end
130       if symlink == false
131         header = `git show #{tmp[0]} | head -n20 | egrep -A3 -B1 'Copyright.*All rights reserved.'`
132         broken = check_file(filename, header, broken)
133       else
134         #puts "#{filename} is a symbolic link, skipping"
135       end
136     end
137   end
138
139   if broken
140     puts
141     puts "[POLICY] all files must contain copyright headers, for more information see"
142     puts
143     puts "      https://dev.arvados.org/projects/arvados/wiki/Coding_Standards#Copyright-headers"
144     puts
145     puts "Enforcing copyright headers: FAIL"
146     exit 1
147
148   end
149   puts "Enforcing copyright headers: PASS"
150 end
151
152 load_licenseignore
153 check_copyright_headers