Merge remote-tracking branch 'origin/master' into 2051-nondeterministic-jobs
[arvados.git] / services / api / app / models / commit.rb
1 class Commit < ActiveRecord::Base
2   require 'shellwords'
3
4   # Make sure the specified commit really exists, and return the full
5   # sha1 commit hash.
6   #
7   # Accepts anything "git rev-list" accepts, optionally (and
8   # preferably) preceded by "repo_name:".
9   #
10   # Examples: "1234567", "master", "apps:1234567", "apps:master",
11   # "apps:HEAD"
12
13   # def self.find_by_commit_ish(commit_ish)
14   #   if only_valid_chars.match(commit_ish)       
15   #     logger.warn "find_by_commit_ish called with string containing invalid characters: '#{commit_ish}'"
16   #     return nil
17   #   end
18
19   #   want_repo = nil
20   #   if commit_ish.index(':')
21   #     want_repo, commit_ish = commit_ish.split(':',2)
22   #   end
23   #   repositories.each do |repo_name, repo|
24   #     next if want_repo and want_repo != repo_name
25   #     ENV['GIT_DIR'] = repo[:git_dir]
26   #     # we're passing user input to a command line, this is a potential a security hole but I am reasonably confident that shellescape sanitizes the input adequately
27   #     IO.foreach("|git rev-list --max-count=1 --format=oneline 'origin/'#{commit_ish.shellescape} 2>/dev/null || git rev-list --max-count=1 --format=oneline ''#{commit_ish.shellescape}") do |line|
28   #       sha1, message = line.strip.split " ", 2
29   #       next if sha1.length != 40
30   #       begin
31   #         Commit.find_or_create_by_repository_name_and_sha1_and_message(repo_name, sha1, message[0..254])
32   #       rescue
33   #         logger.warn "find_or_create failed: repo_name #{repo_name} sha1 #{sha1} message #{message[0..254]}"
34   #         # Ignore cache failure. Commit is real. We should proceed.
35   #       end
36   #       return sha1
37   #     end
38   #   end
39   #   nil
40   # end
41
42   def self.find_commit_range(current_user, repository, minimum, maximum, exclude)
43     only_valid_chars = /[^A-Za-z0-9_-]/
44     if only_valid_chars.match(minimum) || only_valid_chars.match(maximum) 
45       logger.warn "find_commit_range called with string containing invalid characters: '#{minimum}', '#{maximum}'"
46       return nil
47     end
48
49     if minimum and minimum.empty?
50         minimum = nil
51     end
52     
53     if !maximum
54       maximum = "HEAD"
55     end
56
57     # Get list of actual repository directories under management
58     on_disk_repos = repositories
59
60     # Get list of repository objects readable by user
61     readable = Repository.readable_by(current_user)
62
63     # filter repository objects on requested repository name
64     if repository
65       readable = readable.where(name: repository)
66     end
67
68     #puts "min #{minimum}"
69     #puts "max #{maximum}"
70     #puts "rep #{repository}"
71
72     commits = []
73     readable.each do |r|
74       if on_disk_repos[r.name]
75         ENV['GIT_DIR'] = on_disk_repos[r.name][:git_dir]
76
77         #puts "dir #{on_disk_repos[r.name][:git_dir]}"
78
79         # We've filtered for invalid characters, so we can pass the contents of
80         # minimum and maximum safely on the command line
81
82         #puts "git rev-list --max-count=1 #{maximum}"
83
84         # Get the commit hash for the upper bound
85         max_hash = nil
86         IO.foreach("|git rev-list --max-count=1 #{maximum}") do |line|
87           max_hash = line.strip
88         end
89
90         # If not found, nothing else to do
91         next if !max_hash
92
93         resolved_exclude = nil
94         if exclude
95           resolved_exclude = []
96           exclude.each do |e|
97             IO.foreach("|git rev-list --max-count=1 #{e}") do |line|
98               resolved_exclude.push(line.strip)
99             end  
100           end
101         end
102
103         if minimum          
104           # Get the commit hash for the lower bound
105           min_hash = nil
106           IO.foreach("|git rev-list --max-count=1 #{minimum}") do |line|
107             min_hash = line.strip
108           end
109
110           # If not found, nothing else to do
111           next if !min_hash
112           
113           # Now find all commits between them
114           #puts "git rev-list #{min_hash}..#{max_hash}"
115           IO.foreach("|git rev-list #{min_hash}..#{max_hash}") do |line|
116             hash = line.strip
117             commits.push(hash) if !resolved_exclude or !resolved_exclude.include? hash              
118           end
119
120           commits.push(min_hash) if !resolved_exclude or !resolved_exclude.include? min_hash
121         else
122           commits.push(max_hash) if !resolved_exclude or !resolved_exclude.include? max_hash
123         end
124       end
125     end
126
127     if !commits or commits.empty?
128       nil
129     else
130       commits
131     end
132   end
133
134   # Import all commits from configured git directory into the commits
135   # database.
136
137   def self.import_all
138     repositories.each do |repo_name, repo|
139       stat = { true => 0, false => 0 }
140       ENV['GIT_DIR'] = repo[:git_dir]
141       IO.foreach("|git rev-list --format=oneline --all") do |line|
142         sha1, message = line.strip.split " ", 2
143         imported = false
144         Commit.find_or_create_by_repository_name_and_sha1_and_message(repo_name, sha1, message[0..254]) do
145           imported = true
146         end
147         stat[!!imported] += 1
148         if (stat[true] + stat[false]) % 100 == 0
149           if $stdout.tty? or ARGV[0] == '-v'
150             puts "#{$0} #{$$}: repo #{repo_name} add #{stat[true]} skip #{stat[false]}"
151           end
152         end
153       end
154       if $stdout.tty? or ARGV[0] == '-v'
155         puts "#{$0} #{$$}: repo #{repo_name} add #{stat[true]} skip #{stat[false]}"
156       end
157     end
158   end
159
160   def self.refresh_repositories
161     @repositories = nil
162   end
163
164   protected
165
166   def self.repositories
167     return @repositories if @repositories
168
169     @repositories = {}
170     @gitdirbase = Rails.configuration.git_repositories_dir
171     Dir.foreach @gitdirbase do |repo|
172       next if repo.match /^\./
173       git_dir = File.join(@gitdirbase,
174                           repo.match(/\.git$/) ? repo : File.join(repo, '.git'))
175       repo_name = repo.sub(/\.git$/, '')
176       @repositories[repo_name] = {git_dir: git_dir}
177     end
178
179     @repositories
180   end
181 end