[webui] Fixed last commit that broke the testsuite.
[opensuse:build-service.git] / src / webui / app / controllers / project_controller.rb
1 require 'project_status'
2 require 'collection'
3 require 'buildresult'
4 require 'role'
5 require 'models/package'
6
7 include ActionView::Helpers::UrlHelper
8 include ApplicationHelper
9 include RequestHelper
10
11 class ProjectController < ApplicationController
12
13   class NoChangesError < Exception; end
14
15   before_filter :require_project, :except => [:arch_list, 
16     :autocomplete_projects, :clear_failed_comment, :edit_comment_form, :index, 
17     :list, :list_all, :list_public, :new, :package_buildresult, :save_new, :save_prjconf,
18     :rebuild_time_png]
19
20   before_filter :load_current_requests, :only => [:delete, :view,
21     :edit, :save, :add_repository_from_default_list, :add_repository, :save_targets, :status, :prjconf,
22     :remove_person, :save_person, :add_person, :remove_target,
23     :show, :monitor, :edit_prjconf, :list_requests,
24     :packages, :users, :subprojects, :repositories, :attributes, :meta, :edit_meta ]
25   before_filter :require_prjconf, :only => [:edit_prjconf, :prjconf ]
26   before_filter :require_meta, :only => [:edit_meta, :meta ]
27   before_filter :require_login, :only => [:save_new, :toggle_watch, :delete]
28
29   def index
30     redirect_to :action => 'list_public'
31   end
32
33   def list_all
34     list and return
35   end
36
37   def list_public
38     params['excludefilter'] = 'home:'
39     list and return
40   end
41
42   def list
43     @important_projects = get_important_projects
44     @filterstring = params[:searchtext] || ''
45     @excludefilter = params['excludefilter'] if params['excludefilter'] and params['excludefilter'] != 'undefined'
46     get_filtered_projectlist @filterstring, @excludefilter
47     if request.xhr?
48       render :partial => 'search_project', :locals => {:project_list => @projects} and return
49     end
50     render :list, :status => params[:nextstatus]
51   end
52
53   def autocomplete_projects
54     required_parameters :q
55     get_filtered_projectlist params[:q], ''
56     render :text => @projects.join("\n")
57   end
58
59   def autocomplete_repositories
60     @repos = @project.repositories
61     render :text => @repos.join("\n")
62   end
63
64   def project_key(a)
65     a = a.downcase
66
67     if a[0..4] == 'home:'
68       a = 'zz' + a
69     end
70     return a
71   end
72   private :project_key
73
74   def get_filtered_projectlist(filterstring, excludefilter='')
75     # remove illegal xpath characters
76     filterstring.gsub!(/[\[\]\n]/, '')
77     filterstring.gsub!(/[']/, '&apos;')
78     filterstring.gsub!(/["]/, '&quot;')
79     predicate = filterstring.empty? ? '' : "contains(@name, '#{filterstring}')"
80     predicate += " and " if !predicate.empty? and !excludefilter.blank?
81     predicate += "not(starts-with(@name,'#{excludefilter}'))" if !excludefilter.blank?
82     result = find_cached Collection, :id, :what => "project", :predicate => predicate, :expires_in => 2.minutes
83     @projects = Array.new
84     result.each { |p| @projects << p.name }
85     @projects =  @projects.sort_by { |a| project_key a }
86   end
87   private :get_filtered_projectlist
88
89   def users
90     @email_hash = Hash.new
91     @project.each_person do |person|
92       @email_hash[person.userid.to_s] = find_cached(Person, person.userid, :expires_in => 30.minutes ).email.to_s
93     end
94     @roles = Role.local_roles
95   end
96
97   def subprojects
98     @subprojects = Hash.new
99     sub_names = Collection.find :id, :what => "project", :predicate => "starts-with(@name,'#{@project}:')"
100     sub_names.each do |sub|
101       @subprojects[sub.name] = find_cached( Project, sub.name )
102     end
103     @parentprojects = Hash.new
104     parent_names = @project.name.split ':'
105     parent_names.each_with_index do |parent, idx|
106       parent_name = parent_names.slice(0, idx+1).join(':')
107       unless [@project.name, 'home'].include?( parent_name )
108         parent_project = find_cached(Project, parent_name )
109         @parentprojects[parent_name] = parent_project unless parent_project.blank?
110       end
111     end
112   end
113
114   def attributes
115     @attributes = find_cached(Attribute, {:project => @project.name}, :expires_in => 2.minutes)
116   end
117
118   def new
119     @namespace = params[:ns]
120     @project_name = params[:project]
121     if params[:ns] == "home:#{session[:login]}"
122       @project = find_cached Project, params[:ns]
123       unless @project
124         flash.now[:note] = "Your home project doesn't exist yet. You can create it now by entering some" +
125           " descriptive data and press the 'Create Project' button."
126         @project_name = params[:ns]
127       end
128     end
129     if @project_name =~ /home:(.+)/
130       @project_title = "#$1's Home Project"
131     else
132       @project_title = ""
133     end
134   end
135
136   def load_packages_mainpage
137     @packages = Rails.cache.fetch("%s_packages_mainpage" % @project, :expires_in => 30.minutes) do
138       find_cached(Package, :all, :project => @project.name, :expires_in => 30.seconds )
139     end
140   end
141   protected :load_packages_mainpage
142
143   def show
144     @bugowners_mail = []
145     if @project.bugowners
146       @project.bugowners.each do |bugowner|
147         mail = find_cached(Person, bugowner).email.to_s
148         @bugowners_mail.push mail if mail
149       end
150     end
151
152     load_packages_mainpage
153
154     Rails.cache.delete("%s_problem_packages" % @project.name) if discard_cache?
155     @nr_of_problem_packages = Rails.cache.fetch("%s_problem_packages" % @project.name, :expires_in => 30.minutes) do
156       buildresult = find_cached(Buildresult, :project => @project, :view => 'status', :code => ['failed', 'broken', 'unresolvable'], :expires_in => 2.minutes )
157       if buildresult
158         results = buildresult.data.find( 'result/status' )
159         results.map{|e| e.attributes['package'] }.uniq.size
160       else
161         0
162       end
163     end
164
165     linking_projects
166
167     load_buildresult
168
169     render :show, :status => params[:nextstatus] if params[:nextstatus]
170   end
171
172   def linking_projects
173     Rails.cache.delete("%s_linking_projects" % @project.name) if discard_cache?
174     @linking_projects = Rails.cache.fetch("%s_linking_projects" % @project.name, :expires_in => 30.minutes) do
175        @project.linking_projects
176     end
177   end
178
179   # TODO we need the architectures in api/distributions
180   def add_repository_from_default_list
181     Rails.cache.delete("distributions") if discard_cache?
182     dist_xml = Rails.cache.fetch("distributions", :expires_in => 30.minutes) do
183       frontend = ActiveXML::Config::transport_for( :package )
184       frontend.direct_http URI("/distributions"), :method => "GET"
185     end
186     @distributions = XML::Document.string dist_xml
187   end
188
189   def add_repository
190     @torepository = params[:torepository]
191   end
192
193
194   def add_person
195     @roles = Role.local_roles
196   end
197
198   def load_buildresult(cache = true)
199     unless cache
200       Buildresult.free_cache( :project => params[:project], :view => 'summary' )
201     end
202     @buildresult = find_cached(Buildresult, :project => params[:project], :view => 'summary', :expires_in => 3.minutes )
203
204     @repohash = Hash.new
205     @statushash = Hash.new
206     @repostatushash = Hash.new
207     @packagenames = Array.new
208
209     @buildresult.each_result do |result|
210       repo = result.repository
211       arch = result.arch
212
213       # repository status cache
214       @repostatushash[repo] ||= Hash.new
215       @repostatushash[repo][arch] = Hash.new
216
217       if result.has_attribute? :state
218         if result.has_attribute? :dirty
219           @repostatushash[repo][arch] = "outdated_" + result.state.to_s
220         else
221           @repostatushash[repo][arch] = result.state.to_s
222         end
223       end
224     end if @buildresult
225     if @buildresult
226       @buildresult = @buildresult.to_a
227     else
228       @buildresult = Array.new
229     end
230   end
231   protected :load_buildresult
232
233   def buildresult
234     unless request.xhr?
235       render :text => 'no ajax', :status => 400 and return
236     end
237     load_buildresult false
238     render :partial => 'buildstatus'
239   end
240
241   def delete
242     valid_http_methods :post
243     @confirmed = params[:confirmed]
244     if @confirmed == "1"
245       begin
246         if params[:force] == "1"
247           @project.delete :force => 1
248         else
249           @project.delete
250         end
251         Rails.cache.delete("%s_packages_mainpage" % @project)
252         Rails.cache.delete("%s_problem_packages" % @project)
253       rescue ActiveXML::Transport::Error => err
254         @error, @code, @api_exception = ActiveXML::Transport.extract_error_message err
255         logger.error "Could not delete project #{@project}: #{@error}"
256       end
257     end
258   end
259
260   def arch_list
261     if @arch_list.nil?
262       tmp = []
263       @project.each_repository do |repo|
264         tmp += repo.archs
265       end
266       @arch_list = tmp.sort.uniq
267     end
268     return @arch_list
269   end
270
271   def edit_repository
272     repo = @project.repository[params[:repository]]
273     @arch_list = arch_list
274     render :partial => 'edit_repository', :locals => { :repository => repo, :error => nil }
275   end
276
277   def update_target
278     valid_http_methods :post
279     repo = @project.repository[params[:repo]]
280     repo.archs = params[:arch].to_a
281     @arch_list = arch_list
282     begin
283       @project.save
284       render :partial => 'edit_repository', :locals => { :repository => repo, :has_data => true }
285     rescue => e
286       render :partial => 'edit_repository', :locals => { :repository => repo, :error => "#{ActiveXML::Transport.extract_error_message( e )[0]}" }
287     end
288   end
289
290   def repositories
291     # overwrite @project with different view
292     # TODO to get this cached we need to make sure it gets purged on repo updates
293     @project = Project.find( params[:project], :view => :flagdetails )
294   end
295
296   def repository_state
297     # Get cycles of the repository build dependency information
298     # 
299     @repocycles = Hash.new
300     @repositories = Array.new
301     if params[:repository]
302       @repositories << params[:repository]
303     elsif @project.has_element? :repository
304       @project.each_repository { |repository| @repositories << repository.name }
305     end
306    
307     @project.each_repository do |repository| 
308       next unless @repositories.include? repository.name
309       @repocycles[repository.name] = Hash.new
310          
311       repository.each_arch do |arch|
312
313         cycles = Array.new
314         # skip all packages via package=- to speed up the api call, we only parse the cycles anyway
315         deps = find_cached(BuilddepInfo, :project => @project.name, :package => "-", :repository => repository.name, :arch => arch)
316         nr_cycles = 0
317         if deps and deps.has_element? :cycle
318           packages = Hash.new
319           deps.each_cycle do |cycle|
320             current_cycles = Array.new
321             cycle.each_package do |p|
322               p = p.text
323               if packages.has_key? p
324                 current_cycles << packages[p]
325               end
326             end
327             current_cycles.uniq!
328             if current_cycles.empty?
329               nr_cycles += 1
330               nr_cycle = nr_cycles
331             elsif current_cycles.length == 1
332               nr_cycle = current_cycles[0]
333             else
334               logger.debug "HELP! #{current_cycles.inspect}"
335             end
336             cycle.each_package do |p|
337               packages[p.text] = nr_cycle
338             end
339           end
340         end
341         cycles = Array.new
342         1.upto(nr_cycles) do |i|
343           list = Array.new
344           packages.each do |package,cycle|
345             list.push(package) if cycle == i
346           end
347           cycles << list.sort
348         end
349         @repocycles[repository.name][arch.text] = cycles unless cycles.empty?
350       end
351     end
352   end
353
354   def rebuild_time
355     required_parameters :repository, :arch
356     load_packages_mainpage 
357     @repository = params[:repository]
358     @arch = params[:arch]
359     @hosts = begin Integer(params[:hosts] || '40') rescue 40 end
360     @scheduler = params[:scheduler] || 'needed'
361     bdep = find_cached(BuilddepInfo, :project => @project.name, :repository => @repository, :arch => @arch)
362     jobs = find_cached(Jobhislist , :project => @project.name, :repository => @repository, :arch => @arch, 
363             :limit => @packages.each.size * 3, :code => ['succeeded', 'unchanged'])
364     unless bdep and jobs
365       flash[:error] = "Could not collect infos about repository #{@repository}/#{@arch}"
366       redirect_to :action => :show, :project => @project
367       return
368     end
369     indir = Dir.mktmpdir 
370     f = File.open(indir + "/_builddepinfo.xml", 'w')
371     f.write(bdep.dump_xml) 
372     f.close
373     f = File.open(indir + "/_jobhistory.xml", 'w')
374     f.write(jobs.dump_xml)
375     f.close
376     outdir = Dir.mktmpdir
377     cmd="perl ./mkdiststats '--srcdir=#{indir}' '--destdir=#{outdir}' --outfmt=xml #{@project.name}/#{@repository}/#{@arch} --width=910 --buildhosts=#{@hosts} --scheduler=#{@scheduler}"
378     logger.debug "cd #{RAILS_ROOT}/vendor/diststats && #{cmd}"
379     system("cd #{RAILS_ROOT}/vendor/diststats && #{cmd}")
380     f=File.open(outdir + "/rebuild.png")
381     png=f.read
382     f.close 
383     @pngkey = MD5::md5( params.to_s )
384     Rails.cache.write("rebuild-%s.png" % @pngkey, png)
385     f=File.open(outdir + "/longest.xml")
386     longest = ActiveXML::LibXMLNode.new(f.read)
387     @timings = Hash.new
388     longest.timings.each_package do |p|
389       @timings[p.value :name] = [p.value(:buildtime), p.value(:finished)]
390     end
391     @rebuildtime = Integer(longest.value :rebuildtime)
392     f.close
393     @longestpaths = Array.new
394     longest.longestpath.each_path do |path|
395       currentpath = Array.new
396       path.each_package do |p|
397         currentpath << p.text
398       end
399       @longestpaths << currentpath
400     end
401     # we append 4 empty paths, so there are always at least 4 in the array
402     # to simplify the view code
403     4.times { @longestpaths << Array.new }
404     FileUtils.rm_rf indir
405     FileUtils.rm_rf outdir
406   end
407
408   def rebuild_time_png
409     key = params[:key]
410     data = Rails.cache.read("rebuild-%s.png" % key)
411     headers['Content-Type'] = 'image/png'
412     send_data(data, :type => 'image/png', :disposition => 'inline')
413   end
414
415   def load_packages
416     @packages = find_cached(Package, :all, :project => @project.name, :expires_in => 30.seconds )
417   end
418   protected :load_packages
419
420   def packages
421     load_packages
422     # push to long time cache for the project frontpage
423     Rails.cache.write("#{@project}_packages_mainpage", @packages, :expires_in => 30.minutes)
424     @patchinfo = []
425     unless @packages.blank?
426       @packages.each do |p|
427         @patchinfo << p.name if p.name =~ %r{^_patchinfo}
428       end
429     end
430   end
431
432   def autocomplete_packages
433     packages
434     if valid_package_name_read?( params[:q] ) or params[:q] == ""
435       render :text => @packages.each.select{|p| p.name.index(params[:q]) }.map{|p| p.name}.join("\n")
436     else
437       render :text => ""
438     end
439   end
440
441   def list_requests
442     @current_requests.each do |c|
443       logger.debug c.dump_xml
444     end
445   end
446
447   def save_new
448     if params[:name].blank? || !valid_project_name?( params[:name] )
449       flash[:error] = "Invalid project name '#{params[:name]}'."
450       redirect_to :action => "new", :ns => params[:ns] and return
451     end
452
453     project_name = params[:name].strip
454     project_name = params[:ns].strip + ":" + project_name.strip if params[:ns]
455
456     if Project.exists? project_name
457       flash[:error] = "Project '#{project_name}' already exists."
458       redirect_to :action => "new", :ns => params[:ns] and return
459     end
460
461     #store project
462     @project = Project.new(:name => project_name)
463     @project.title.text = params[:title]
464     @project.description.text = params[:description]
465     @project.set_remoteurl(params[:remoteurl]) if params[:remoteurl]
466     @project.add_person :userid => session[:login], :role => 'maintainer'
467     @project.add_person :userid => session[:login], :role => 'bugowner'
468     if params[:source_protection]
469       @project.add_element "sourceaccess"
470       @project.sourceaccess.add_element "disable"
471     end
472     if params[:disable_publishing]
473       @project.add_element "publish"
474       @project.publish.add_element "disable"
475     end
476     begin
477       if @project.save
478         flash[:note] = "Project '#{@project}' was created successfully"
479         redirect_to :action => 'show', :project => project_name and return
480       else
481         flash[:error] = "Failed to save project '#{@project}'"
482       end
483     rescue ActiveXML::Transport::ForbiddenError => err
484       flash[:error] = "You lack the permission to create the project '#{@project}'. " +
485         "Please create it in your home:%s namespace" % session[:login]
486       redirect_to :action => 'new', :ns => "home:" + session[:login] and return
487     end
488     redirect_to :action => 'new'
489   end
490
491   def save
492     if ( !params[:title] )
493       flash[:error] = "Title must not be empty"
494       redirect_to :action => 'edit', :project => params[:project]
495       return
496     end
497
498     @project.title.text = params[:title]
499     @project.description.text = params[:description]
500
501     if @project.save
502       flash[:note] = "Project '#{@project}' was saved successfully"
503     else
504       flash[:error] = "Failed to save project '#{@project}'"
505     end
506
507     redirect_to :action => :show, :project => @project
508   end
509
510   def save_targets
511     valid_http_methods :post
512
513     # extend an existing repository with a path
514     if (params['torepository'])
515       repo_path = "#{params['target_project']}/#{params['target_repo']}"
516       @project.add_path_to_repository :reponame => params['torepository'], :repo_path => repo_path
517       @project.save
518       redirect_to :action => :repositories, :project => @project
519       return
520     elsif params.has_key? :repo
521       # add new repositories
522       params['repo'].each do |repo|
523         if !valid_target_name? repo
524           flash[:error] = "Illegal target name #{repo}."
525           redirect_to :action => :add_repository_from_default_list, :project => @project and return
526         end
527         repo_path = params[repo + '_repo'] || "#{params['target_project']}/#{params['target_repo']}"
528         repo_archs = params[repo + '_arch'] || params['arch']
529         logger.debug "Adding repo: #{repo_path}, archs: #{repo_archs}"
530         @project.add_repository :reponame => repo, :repo_path => repo_path, :arch => repo_archs
531
532         # FIXME: will be cleaned up after implementing FATE #308899
533         if repo == "images"
534           prjconf = frontend.get_source(:project => params[:project], :filename => '_config')
535           unless prjconf =~ /^Type:/
536             prjconf = "%if \"%_repository\" == \"images\"\nType: kiwi\nRepotype: none\nPatterntype: none\n%endif\n" << prjconf
537             frontend.put_file(prjconf, :project => @project, :filename => '_config')
538           end
539         end
540       end
541     else
542       render :text => 'Missing argument, either torepository or repo', :status => 400
543       return
544     end
545
546     begin
547       if @project.save
548         flash[:note] = "Build targets were added successfully"
549       else
550         flash[:error] = "Failed to add build targets"
551       end
552     rescue ActiveXML::Transport::Error => e
553       message, code, api_exception = ActiveXML::Transport.extract_error_message e
554       flash[:error] = "Failed to add build targets: " + message
555     end
556     redirect_to :action => :repositories, :project => @project
557   end
558
559
560   def remove_target
561     valid_http_methods :post
562     if not params[:target]
563       flash[:error] = "Target removal failed, no target selected!"
564       redirect_to :action => :show, :project => params[:project]
565     end
566     @project.remove_repository params[:target]
567
568     begin
569       if @project.save
570         flash[:note] = "Target '#{params[:target]}' was removed"
571       else
572         flash[:error] = "Failed to remove target '#{params[:target]}'"
573       end
574     rescue ActiveXML::Transport::Error => e
575       message, code, api_exception = ActiveXML::Transport.extract_error_message e
576       flash[:error] = "Failed to remove target '#{params[:target]}' " + message
577     end
578
579     redirect_to :action => :repositories, :project => @project
580   end
581
582   def remove_path_from_target
583    @project.remove_path_from_target( params['repository'], params['path_project'], params['path_repository'] )
584    @project.save
585    redirect_to :action => :repositories, :project => @project
586    return
587   end
588
589   def save_person
590     valid_http_methods(:post)
591     user = find_cached( Person, params[:userid] )
592     unless user
593       flash[:error] = "Unknown user with id '#{params[:userid]}'"
594       redirect_to :action => :add_person, :project => @project, :role => params[:role]
595       return
596     end
597     @project.add_person( :userid => user.login.to_s, :role => params[:role] )
598     if @project.save
599       flash[:note] = "Added user #{user.login} with role #{params[:role]} to project #{@project}"
600     else
601       flash[:error] = "Failed to add user '#{params[:userid]}'"
602     end
603     redirect_to :action => :users, :project => @project
604   end
605
606
607   def remove_person
608     if params[:userid].blank?
609       flash[:note] = "User removal aborted, no user id given!"
610       redirect_to :action => :show, :project => params[:project] and return
611     end
612     @project.remove_persons( :userid => params[:userid], :role => params[:role] )
613     if @project.save
614       flash[:note] = "Removed user #{params[:userid]}"
615     else
616       flash[:error] = "Failed to remove user '#{params[:userid]}'"
617     end
618     redirect_to :action => :users, :project => params[:project]
619   end
620
621
622   def monitor
623     @name_filter = params[:pkgname]
624     @lastbuild_switch = params[:lastbuild]
625     if params[:defaults]
626       defaults = (Integer(params[:defaults]) rescue 1) > 0
627     else
628       defaults = true
629     end
630     params['expansionerror'] = 1 if params['unresolvable']
631     @avail_status_values = Buildresult.avail_status_values
632     @filter_out = ['disabled', 'excluded', 'unknown']
633     @status_filter = []
634     @avail_status_values.each { |s|
635       id=s.gsub(' ', '')
636       if params.has_key?(id)
637         next unless (Integer(params[id]) rescue 1) > 0
638       else
639         next unless defaults
640       end
641       next if defaults && @filter_out.include?(s)
642       @status_filter << s
643     }
644
645     @avail_arch_values = []
646     @avail_repo_values = []
647
648     @project.each_repository { |r|
649       @avail_repo_values << r.name
650       @avail_arch_values << r.archs if r.archs
651     }
652     @avail_arch_values = @avail_arch_values.flatten.uniq.sort
653     @avail_repo_values = @avail_repo_values.flatten.uniq.sort
654
655     @arch_filter = []
656     @avail_arch_values.each { |s|
657       archid = valid_xml_id('arch_' + s)
658       if defaults || (params.has_key?(archid) && params[archid])
659         @arch_filter << s
660       end
661     }
662
663     @repo_filter = []
664     @avail_repo_values.each { |s|
665       repoid = valid_xml_id('repo_' + s)
666       if defaults || (params.has_key?(repoid) && params[repoid])
667         @repo_filter << s
668       end
669     }
670
671     find_opt = { :project => @project, :view => 'status', :code => @status_filter,
672       :arch => @arch_filter, :repo => @repo_filter }
673     find_opt[:lastbuild] = 1 unless @lastbuild_switch.blank?
674
675     @buildresult = Buildresult.find( find_opt )
676     unless @buildresult
677       flash[:error] = "No build results for project '#{@project}'"
678       redirect_to :action => :show, :project => params[:project]
679       return
680     end
681
682     if not @buildresult.has_element? :result
683       @buildresult_unavailable = true
684       return
685     end
686
687     @repohash = Hash.new
688     @statushash = Hash.new
689     @repostatushash = Hash.new
690     @packagenames = Array.new
691
692     @buildresult.each_result do |result|
693       @resultvalue = result
694       repo = result.repository
695       arch = result.arch
696
697       next unless @repo_filter.include? repo
698       @repohash[repo] ||= Array.new
699       next unless @arch_filter.include? arch
700       @repohash[repo] << arch
701
702       # package status cache
703       @statushash[repo] ||= Hash.new
704
705       stathash = Hash.new
706       result.each_status do |status|
707         stathash[status.package.to_s] = status
708       end
709       stathash.keys.each do |p|
710         @packagenames << p.to_s
711       end
712
713       @statushash[repo][arch] = stathash
714
715       # repository status cache
716       @repostatushash[repo] ||= Hash.new
717       @repostatushash[repo][arch] = Hash.new
718
719       if result.has_attribute? :state
720         if result.has_attribute? :dirty
721           @repostatushash[repo][arch] = "outdated_" + result.state.to_s
722         else
723           @repostatushash[repo][arch] = result.state.to_s
724         end
725       end
726     end
727     @packagenames = @packagenames.flatten.uniq.sort
728
729     ## Filter for PackageNames ####
730     @packagenames.reject! {|name| not filter_matches?(name,@name_filter) } if not @name_filter.blank?
731     packagename_hash = Hash.new
732     @packagenames.each { |p| packagename_hash[p.to_s] = 1 }
733
734     # filter out repos without current packages
735     @statushash.each do |repo, hash|
736       hash.each do |arch, packages|
737
738         has_packages = false
739         packages.each do |p, status|
740           if packagename_hash.has_key? p
741             has_packages = true
742             break
743           end
744         end
745         unless has_packages
746           @repohash[repo].delete arch
747         end
748       end
749     end
750   end
751
752   def filter_matches?(input,filter_string)
753     result = false
754     filter_string.gsub!(/\s*/,'')
755     filter_string.split(',').each { |filter|
756       no_invert = filter.match(/(^!?)(.+)/)
757       if no_invert[1] == '!'
758         result = input.include?(no_invert[2]) ? result : true
759       else
760         result = input.include?(no_invert[2]) ? true : result
761       end
762     }
763     return result
764   end
765
766   # should be in the package controller, but all the helper functions to render the result of a build are in the project
767   def package_buildresult
768     unless request.xhr?
769       render :text => 'no ajax', :status => 400 and return
770     end
771
772     @project = params[:project]
773     @package = params[:package]
774     begin
775       @buildresult = find_cached(Buildresult, :project => params[:project], :package => params[:package], :view => 'status', :lastbuild => 1, :expires_in => 2.minutes )
776     rescue ActiveXML::Transport::Error # wild work around for backend bug (sends 400 for 'not found')
777     end
778     @repohash = Hash.new
779     @statushash = Hash.new
780
781     @buildresult.each_result do |result|
782       repo = result.repository
783       arch = result.arch
784
785       @repohash[repo] ||= Array.new
786       @repohash[repo] << arch
787
788       # package status cache
789       @statushash[repo] ||= Hash.new
790       @statushash[repo][arch] = Hash.new
791
792       stathash = @statushash[repo][arch]
793       result.each_status do |status|
794         stathash[status.package] = status
795       end
796     end if @buildresult
797     render :layout => false
798   end
799
800   def toggle_watch
801     if @user.watches? @project.name
802       logger.debug "Remove #{@project} from watchlist for #{@user}"
803       @user.remove_watched_project @project.name
804     else
805       logger.debug "Add #{@project} to watchlist for #{@user}"
806       @user.add_watched_project @project.name
807     end
808     @user.save
809     Person.free_cache( :login => session[:login] )
810
811     if request.env["HTTP_REFERER"]
812       redirect_to :back
813     else
814       redirect_to :action => :show, :project => @project
815     end
816   end
817
818   def edit_meta
819     render :template => "project/edit_meta"
820   end
821
822   def meta
823   end
824
825   def prjconf
826   end
827
828   def edit_prjconf
829   end
830
831   def change_flag
832     if request.post? and params[:cmd] and params[:flag]
833       frontend.source_cmd params[:cmd], :project => @project, :repository => params[:repository], :arch => params[:arch], :flag => params[:flag], :status => params[:status]
834     end
835     Project.free_cache( :name => params[:project], :view => :flagdetails )
836     if request.xhr?
837       @project = find_cached(Project, :name => params[:project], :view => :flagdetails )
838       render :partial => 'shared/repositories_flag_table', :locals => { :flags => @project.send(params[:flag]), :obj => @project }
839     else
840       redirect_to :action => :repositories, :project => @project
841     end
842   end
843
844   def save_prjconf
845     frontend.put_file(params[:config], :project => params[:project], :filename => '_config')
846     flash[:note] = "Project Config successfully saved"
847     redirect_to :action => :prjconf, :project => params[:project]
848   end
849
850   def save_meta
851     begin
852       frontend.put_file(params[:meta], :project => params[:project], :filename => '_meta')
853     rescue ActiveXML::Transport::Error => e
854       message, code, api_exception = ActiveXML::Transport.extract_error_message e
855       flash[:error] = message
856       @meta = params[:meta]
857       edit_meta
858       return
859     end
860
861     flash[:note] = "Config successfully saved"
862     Project.free_cache params[:project]
863     redirect_to :action => :meta, :project => params[:project]
864   end
865
866   def clear_failed_comment
867     # TODO(Jan): put this logic in the Attribute model
868     transport ||= ActiveXML::Config::transport_for(:package)
869     params["package"].to_a.each do |p|
870       begin
871         transport.direct_http URI("/source/#{params[:project]}/#{p}/_attribute/OBS:ProjectStatusPackageFailComment"), :method => "DELETE"
872       rescue ActiveXML::Transport::ForbiddenError => e
873         message, code, api_exception = ActiveXML::Transport.extract_error_message e
874         flash[:error] = message
875         redirect_to :action => :status, :project => params[:project]
876         return
877       end
878     end
879     if request.xhr?
880       render :text => '<em>Cleared comment</em>'
881       return
882     end
883     if params["package"].to_a.length > 1
884       flash[:note] = "Cleared comment for packages %s" % params[:package].to_a.join(',')
885     else
886       flash[:note] = "Cleared comment for package #{params[:package]}"
887     end
888     redirect_to :action => :status, :project => params[:project]
889   end
890
891   def edit
892   end
893
894   def edit_comment_form
895     @comment = params[:comment]
896     @project = params[:project]
897     @package = params[:package]
898     render :partial => "edit_comment_form"
899   end
900
901   def edit_comment
902     @package = params[:package]
903     attr = Attribute.new(:project => params[:project], :package => params[:package])
904     attr.set('OBS', 'ProjectStatusPackageFailComment', params[:text])
905     result = attr.save
906     @result = result
907     if result[:type] == :error
908       @comment = params[:last_comment]
909     else
910       @comment = params[:text]
911     end
912     render :partial => "edit_comment"
913   end
914
915   def get_changes_md5(project, package)
916     begin
917       dir = find_cached(Directory, :project => project, :package => package, :expand => "1")
918     rescue => e
919       dir = nil
920     end
921     return nil unless dir
922     changes = []
923     dir.each_entry do |e|
924       name = e.name.to_s
925       if name =~ /.changes$/
926         if name == package + ".changes"
927           return e.md5.to_s
928         end
929         changes << e.md5.to_s
930       end
931     end
932     if changes.size == 1
933       return changes[0]
934     end
935     logger.debug "can't find unique changes file: " + dir.dump_xml
936     raise NoChangesError, "no .changes file in #{project}/#{package}"
937   end
938   private :get_changes_md5
939
940   def changes_file_difference(project1, package1, project2, package2)
941     md5_1 = get_changes_md5(project1, package1)
942     md5_2 = get_changes_md5(project2, package2)
943     return md5_1 != md5_2
944   end
945   private :changes_file_difference
946
947   def status
948     status = Rails.cache.fetch("status_%s" % @project, :expires_in => 10.minutes) do
949       ProjectStatus.find(:project => @project)
950     end
951
952     all_packages = "All Packages"
953     no_project = "No Project"
954     @current_develproject = params[:filter_devel] || all_packages
955     @ignore_pending = params[:ignore_pending] || false
956     @limit_to_fails = !(!params[:limit_to_fails].nil? && params[:limit_to_fails] == 'false')
957     @include_versions = !(!params[:include_versions].nil? && params[:include_versions] == 'false')
958
959     attributes = find_cached(PackageAttribute, :namespace => 'OBS',
960       :name => 'ProjectStatusPackageFailComment', :project => @project, :expires_in => 2.minutes)
961     comments = Hash.new
962     attributes.data.find('/attribute/project/package/values').each do |p|
963       # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
964       p.each_element do |v|
965         comments[p.parent['name']] = v.content
966       end
967     end if attributes
968
969     upstream_versions = Hash.new
970     upstream_urls = Hash.new
971
972     if @include_versions
973       attributes = find_cached(PackageAttribute, :namespace => 'openSUSE',
974         :name => 'UpstreamVersion', :project => @project, :expires_in => 2.minutes)
975       attributes.data.find('//package//values').each do |p|
976         # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
977         p.each_element do |v|
978           upstream_versions[p.parent['name']] = v.content
979         end
980       end if attributes
981
982       attributes = find_cached(PackageAttribute, :namespace => 'openSUSE',
983         :name => 'UpstreamTarballURL', :project => @project, :expires_in => 2.minutes)
984       attributes.data.find('//package//values').each do |p|
985         # unfortunately libxml's find_first does not work on nodes, but on document (known bug)
986         p.each_element do |v|
987           upstream_urls[p.parent['name']] = v.content
988         end
989       end if attributes
990     end
991
992     raw_requests = Rails.cache.fetch("requests_new", :expires_in => 5.minutes) do
993       Collection.find(:what => 'request', :predicate => "(state/@name='new')")
994     end
995
996     @requests = Hash.new
997     submits = Hash.new
998     raw_requests.each_request do |r|
999       id = Integer(r.data['id'])
1000       @requests[id] = r
1001       #logger.debug r.dump_xml + " " + (r.has_element?('action') ? r.action.data['type'] : "false")
1002       if r.has_element?('action') && r.action.data['type'] == "submit"
1003         target = r.action.target.data
1004         key = target['project'] + "/" + target['package']
1005         submits[key] ||= Array.new
1006         submits[key] << id
1007       end
1008     end
1009
1010     @develprojects = Array.new
1011
1012     @packages = Array.new
1013     status.each_package do |p|
1014       currentpack = Hash.new
1015       currentpack['name'] = p.name
1016       currentpack['failedcomment'] = comments[p.name] if comments.has_key? p.name
1017       newest = 0
1018
1019       p.each_failure do |f|
1020         next if f.repo =~ /ppc/
1021         next if f.repo =~ /snapshot/
1022         next if newest > (Integer(f.time) rescue 0)
1023         next if f.srcmd5 != p.srcmd5
1024         currentpack['failedarch'] = f.repo.split('/')[1]
1025         currentpack['failedrepo'] = f.repo.split('/')[0]
1026         newest = Integer(f.time)
1027         currentpack['firstfail'] = newest
1028       end
1029
1030       currentpack['problems'] = Array.new
1031       currentpack['requests_from'] = Array.new
1032       currentpack['requests_to'] = Array.new
1033
1034       key = @project.name + "/" + p.name
1035       if submits.has_key? key
1036         currentpack['requests_from'].concat(submits[key])
1037       end
1038
1039       currentpack['version'] = p.version
1040       if upstream_versions.has_key? p.name
1041         upstream_version = upstream_versions[p.name]
1042         begin
1043           gup = Gem::Version.new(p.version)
1044           guv = Gem::Version.new(upstream_version)
1045         rescue ArgumentError
1046           # if one of the versions can't be parsed we simply can't say
1047         end
1048
1049         if gup && guv && gup < guv
1050           currentpack['upstream_version'] = upstream_version
1051           currentpack['upstream_url'] = upstream_urls[p.name] if upstream_urls.has_key? p.name
1052         end
1053       end
1054
1055       currentpack['md5'] = p.value 'verifymd5'
1056       currentpack['md5'] ||= p.srcmd5
1057
1058       if p.has_element? :develpack
1059         @develprojects << p.develpack.proj
1060         currentpack['develproject'] = p.develpack.proj
1061         if (@current_develproject != p.develpack.proj or @current_develproject == no_project) and @current_develproject != all_packages
1062           next
1063         end
1064         currentpack['develpackage'] = p.develpack.pack
1065         key = "%s/%s" % [p.develpack.proj, p.develpack.pack]
1066         if submits.has_key? key
1067           currentpack['requests_to'].concat(submits[key])
1068         end
1069         if p.develpack.has_element? 'package'
1070           currentpack['develmd5'] = p.develpack.package.value 'verifymd5'
1071           currentpack['develmd5'] ||= p.develpack.package.srcmd5
1072       
1073           if p.develpack.package.has_element? :error
1074              currentpack['problems'] << 'error-' + p.develpack.package.error.to_s
1075           end
1076         end
1077
1078         if currentpack['md5'] and currentpack['develmd5'] and currentpack['md5'] != currentpack['develmd5']
1079           currentpack['problems'] << Rails.cache.fetch("dd_%s_%s" % [currentpack['md5'], currentpack['develmd5']]) do
1080             begin
1081               if changes_file_difference(@project.name, p.name, currentpack['develproject'], currentpack['develpackage'])
1082                 'different_changes'
1083               else
1084                 'different_sources'
1085               end
1086             rescue NoChangesError => e
1087               e.message
1088             end
1089           end
1090         end
1091       elsif @current_develproject != no_project
1092         next if @current_develproject != all_packages
1093       end
1094
1095       if p.has_element? :link
1096         if currentpack['md5'] != p.link.targetmd5
1097           currentpack['problems'] << 'diff_against_link'
1098           currentpack['lproject'] = p.link.project
1099           currentpack['lpackage'] = p.link.package
1100         end
1101       end
1102
1103       next if !currentpack['requests_from'].empty? and @ignore_pending
1104       if @limit_to_fails
1105         next if !currentpack['firstfail']
1106       else
1107         next unless (currentpack['firstfail'] or currentpack['failedcomment'] or currentpack['upstream_version'] or
1108             !currentpack['problems'].empty? or !currentpack['requests_from'].empty? or !currentpack['requests_to'].empty?)
1109       end
1110       @packages << currentpack
1111     end
1112
1113     @develprojects.sort! { |x,y| x.downcase <=> y.downcase }.uniq!
1114     @develprojects.insert(0, all_packages)
1115     @develprojects.insert(1, no_project)
1116
1117     @packages.sort! { |x,y| x['name'] <=> y['name'] }
1118   end
1119
1120   private
1121
1122   def get_important_projects
1123     predicate = "[attribute/@name='OBS:VeryImportantProject']"
1124     return find_cached Collection, :id, :what => "project", :predicate => predicate
1125   end
1126
1127
1128   def filter_packages( project, filterstring )
1129     result = Collection.find :id, :what => "package",
1130       :predicate => "@project='#{project}' and contains(@name,'#{filterstring}')"
1131     return result.each.map {|x| x.name}
1132   end
1133
1134   def require_project
1135     if !valid_project_name? params[:project]
1136       unless request.xhr?
1137         flash[:error] = "#{params[:project]} is not a valid project name"
1138         redirect_to :controller => "project", :action => "list_public", :nextstatus => 404 and return
1139       else
1140         render :text => 'Not a valid project name', :status => 404 and return
1141       end
1142     end
1143     @project = find_cached(Project, params[:project], :expires_in => 5.minutes )
1144     check_user
1145     unless @project
1146       if @user and params[:project] == "home:#{@user}"
1147         # checks if the user is registered yet
1148         flash[:note] = "Your home project doesn't exist yet. You can create it now by entering some" +
1149           " descriptive data and press the 'Create Project' button."
1150         redirect_to :action => :new, :project => "home:" + session[:login] and return
1151       end
1152       # remove automatically if a user watches a removed project
1153       if @user and @user.watches? params[:project]
1154         @user.remove_watched_project params[:project] and @user.save
1155       end
1156       unless request.xhr?
1157         flash[:error] = "Project not found: #{params[:project]}"
1158         redirect_to :controller => "project", :action => "list_public", :nextstatus => 404 and return
1159       else
1160         render :text => "Project not found: #{params[:project]}", :status => 404 and return
1161       end
1162     end
1163   end
1164
1165   def require_prjconf
1166     begin
1167       @config = frontend.get_source(:project => params[:project], :filename => '_config')
1168     rescue ActiveXML::Transport::NotFoundError
1169       flash[:error] = "Project _config not found: #{params[:project]}"
1170       redirect_to :controller => "project", :action => "list_public", :nextstatus => 404
1171     end
1172   end
1173
1174   def require_meta
1175     begin
1176       @meta = frontend.get_source(:project => params[:project], :filename => '_meta')
1177     rescue ActiveXML::Transport::NotFoundError
1178       flash[:error] = "Project _meta not found: #{params[:project]}"
1179       redirect_to :controller => "project", :action => "list_public", :nextstatus => 404
1180     end
1181   end
1182
1183   def load_current_requests
1184     predicate = "state/@name='new' and action/target/@project='#{@project}'"
1185     @current_requests = Array.new
1186     coll = find_cached(Collection, :what => :request, :predicate => predicate, :expires_in => 1.minutes)
1187     coll.each_request do |req|
1188       @current_requests << req
1189     end
1190     @project_has_requests = !@current_requests.blank?
1191   end
1192
1193 end