Merge branch 'master' into 4592-input-selection
[arvados.git] / services / api / app / models / commit.rb
1 class Commit < ActiveRecord::Base
2   require 'shellwords'
3
4   def self.git_check_ref_format(e)
5     if !e or e.empty? or e[0] == '-' or e[0] == '$'
6       # definitely not valid
7       false
8     else
9       `git check-ref-format --allow-onelevel #{e.shellescape}`
10       $?.success?
11     end
12   end
13
14   def self.find_commit_range(current_user, repository, minimum, maximum, exclude)
15     if minimum and minimum.empty?
16       minimum = nil
17     end
18
19     if minimum and !git_check_ref_format(minimum)
20       logger.warn "find_commit_range called with invalid minimum revision: '#{minimum}'"
21       return nil
22     end
23
24     if maximum and !git_check_ref_format(maximum)
25       logger.warn "find_commit_range called with invalid maximum revision: '#{maximum}'"
26       return nil
27     end
28
29     if !maximum
30       maximum = "HEAD"
31     end
32
33     # Get list of actual repository directories under management
34     on_disk_repos = repositories
35
36     # Get list of repository objects readable by user
37     readable = Repository.readable_by(current_user)
38
39     # filter repository objects on requested repository name
40     if repository
41       readable = readable.where(name: repository)
42     end
43
44     commits = []
45     readable.each do |r|
46       if on_disk_repos[r.name]
47         ENV['GIT_DIR'] = on_disk_repos[r.name][:git_dir]
48
49         # We've filtered for invalid characters, so we can pass the contents of
50         # minimum and maximum safely on the command line
51
52         # Get the commit hash for the upper bound
53         max_hash = nil
54         IO.foreach("|git rev-list --max-count=1 #{maximum.shellescape} --") do |line|
55           max_hash = line.strip
56         end
57
58         # If not found or string is invalid, nothing else to do
59         next if !max_hash or !git_check_ref_format(max_hash)
60
61         resolved_exclude = nil
62         if exclude
63           resolved_exclude = []
64           exclude.each do |e|
65             if git_check_ref_format(e)
66               IO.foreach("|git rev-list --max-count=1 #{e.shellescape} --") do |line|
67                 resolved_exclude.push(line.strip)
68               end
69             else
70               logger.warn "find_commit_range called with invalid exclude invalid characters: '#{exclude}'"
71               return nil
72             end
73           end
74         end
75
76         if minimum
77           # Get the commit hash for the lower bound
78           min_hash = nil
79           IO.foreach("|git rev-list --max-count=1 #{minimum.shellescape} --") do |line|
80             min_hash = line.strip
81           end
82
83           # If not found or string is invalid, nothing else to do
84           next if !min_hash or !git_check_ref_format(min_hash)
85
86           # Now find all commits between them
87           IO.foreach("|git rev-list #{min_hash.shellescape}..#{max_hash.shellescape} --") do |line|
88             hash = line.strip
89             commits.push(hash) if !resolved_exclude or !resolved_exclude.include? hash
90           end
91
92           commits.push(min_hash) if !resolved_exclude or !resolved_exclude.include? min_hash
93         else
94           commits.push(max_hash) if !resolved_exclude or !resolved_exclude.include? max_hash
95         end
96       else
97         logger.warn "Repository #{r.name} exists in table but not found on disk"
98       end
99     end
100
101     if !commits or commits.empty?
102       nil
103     else
104       commits
105     end
106   end
107
108   # Import all commits from configured git directory into the commits
109   # database.
110
111   def self.import_all
112     repositories.each do |repo_name, repo|
113       stat = { true => 0, false => 0 }
114       ENV['GIT_DIR'] = repo[:git_dir]
115       IO.foreach("|git rev-list --format=oneline --all") do |line|
116         sha1, message = line.strip.split " ", 2
117         imported = false
118         Commit.find_or_create_by_repository_name_and_sha1_and_message(repo_name, sha1, message[0..254]) do
119           imported = true
120         end
121         stat[!!imported] += 1
122         if (stat[true] + stat[false]) % 100 == 0
123           if $stdout.tty? or ARGV[0] == '-v'
124             puts "#{$0} #{$$}: repo #{repo_name} add #{stat[true]} skip #{stat[false]}"
125           end
126         end
127       end
128       if $stdout.tty? or ARGV[0] == '-v'
129         puts "#{$0} #{$$}: repo #{repo_name} add #{stat[true]} skip #{stat[false]}"
130       end
131     end
132   end
133
134   def self.refresh_repositories
135     @repositories = nil
136   end
137
138   protected
139
140   def self.repositories
141     return @repositories if @repositories
142
143     @repositories = {}
144     @gitdirbase = Rails.configuration.git_repositories_dir
145     Dir.foreach @gitdirbase do |repo|
146       next if repo.match /^\./
147       git_dir = File.join(@gitdirbase,
148                           repo.match(/\.git$/) ? repo : File.join(repo, '.git'))
149       next if git_dir == Rails.configuration.git_internal_dir
150       repo_name = repo.sub(/\.git$/, '')
151       @repositories[repo_name] = {git_dir: git_dir}
152     end
153
154     @repositories
155   end
156 end