damagecontrol 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. data/README +75 -0
  2. data/README.license +5 -0
  3. data/Rakefile +111 -0
  4. data/app/controllers/admin_controller.rb +10 -0
  5. data/app/controllers/application.rb +163 -0
  6. data/app/controllers/files_controller.rb +19 -0
  7. data/app/controllers/project_controller.rb +284 -0
  8. data/app/controllers/scm_controller.rb +49 -0
  9. data/app/helpers/admin_helper.rb +2 -0
  10. data/app/helpers/application_helper.rb +3 -0
  11. data/app/helpers/project_helper.rb +2 -0
  12. data/app/views/dhtml_sites.txt +6 -0
  13. data/app/views/files/list.rhtml +4 -0
  14. data/app/views/layouts/rscm.rhtml +79 -0
  15. data/app/views/project/_bugzilla.rhtml +13 -0
  16. data/app/views/project/_changesets_list.rhtml +52 -0
  17. data/app/views/project/_cvs.rhtml +171 -0
  18. data/app/views/project/_jira.rhtml +19 -0
  19. data/app/views/project/_mooky.rhtml +23 -0
  20. data/app/views/project/_null.rhtml +0 -0
  21. data/app/views/project/_project.rhtml +36 -0
  22. data/app/views/project/_rubyforge.rhtml +19 -0
  23. data/app/views/project/_scarab.rhtml +19 -0
  24. data/app/views/project/_scms.rhtml +15 -0
  25. data/app/views/project/_sourceforge.rhtml +19 -0
  26. data/app/views/project/_starteam.rhtml +43 -0
  27. data/app/views/project/_svn.rhtml +22 -0
  28. data/app/views/project/_trac.rhtml +13 -0
  29. data/app/views/project/_trackers.rhtml +18 -0
  30. data/app/views/project/changesets.rhtml +31 -0
  31. data/app/views/project/index.rhtml +23 -0
  32. data/app/views/project/view.rhtml +70 -0
  33. data/app/views/scm/checkout_status.rhtml +44 -0
  34. data/app/views/scm/diff.rhtml +1 -0
  35. data/app/views/scm/scroll.html +27 -0
  36. data/bin/damagecontrol +7 -0
  37. data/bin/damagecontrol-webrick +2 -0
  38. data/config/database.yml +20 -0
  39. data/config/environment.rb +60 -0
  40. data/config/environments/development.rb +3 -0
  41. data/config/environments/production.rb +2 -0
  42. data/config/environments/test.rb +3 -0
  43. data/lib/damagecontrol/app.rb +74 -0
  44. data/lib/damagecontrol/build.rb +104 -0
  45. data/lib/damagecontrol/diff_htmlizer.rb +82 -0
  46. data/lib/damagecontrol/diff_parser.rb +153 -0
  47. data/lib/damagecontrol/directories.rb +126 -0
  48. data/lib/damagecontrol/poller.rb +72 -0
  49. data/lib/damagecontrol/project.rb +213 -0
  50. data/lib/damagecontrol/project_dependencies.rb +8 -0
  51. data/lib/damagecontrol/scm_web.rb +50 -0
  52. data/lib/damagecontrol/standard_persister.rb +49 -0
  53. data/lib/damagecontrol/tracker.rb +164 -0
  54. data/lib/damagecontrol/visitor/build_executor.rb +32 -0
  55. data/lib/damagecontrol/visitor/diff_persister.rb +41 -0
  56. data/lib/damagecontrol/visitor/rss_writer.rb +43 -0
  57. data/lib/damagecontrol/visitor/yaml_persister.rb +71 -0
  58. data/public/404.html +6 -0
  59. data/public/500.html +6 -0
  60. data/public/dispatch.cgi +10 -0
  61. data/public/dispatch.fcgi +7 -0
  62. data/public/dispatch.rb +10 -0
  63. data/public/images/16x16/about.png +0 -0
  64. data/public/images/16x16/bug_green.png +0 -0
  65. data/public/images/16x16/bug_red.png +0 -0
  66. data/public/images/16x16/bug_yellow.png +0 -0
  67. data/public/images/16x16/component.png +0 -0
  68. data/public/images/16x16/console.png +0 -0
  69. data/public/images/16x16/console_error.png +0 -0
  70. data/public/images/16x16/document_add.png +0 -0
  71. data/public/images/16x16/document_delete.png +0 -0
  72. data/public/images/16x16/document_edit.png +0 -0
  73. data/public/images/16x16/document_exchange.png +0 -0
  74. data/public/images/16x16/document_new.png +0 -0
  75. data/public/images/16x16/document_warning.png +0 -0
  76. data/public/images/16x16/safe.png +0 -0
  77. data/public/images/16x16/scroll_information.png +0 -0
  78. data/public/images/16x16/wrench.png +0 -0
  79. data/public/images/24x24/box_delete.png +0 -0
  80. data/public/images/24x24/box_into.png +0 -0
  81. data/public/images/24x24/box_new.png +0 -0
  82. data/public/images/24x24/console_network.png +0 -0
  83. data/public/images/24x24/document_edit.png +0 -0
  84. data/public/images/24x24/find.png +0 -0
  85. data/public/images/24x24/folders.png +0 -0
  86. data/public/images/24x24/garbage.png +0 -0
  87. data/public/images/24x24/gear_connection.png +0 -0
  88. data/public/images/24x24/gear_delete.png +0 -0
  89. data/public/images/24x24/gears_run.png +0 -0
  90. data/public/images/24x24/home.png +0 -0
  91. data/public/images/24x24/navigate_left.png +0 -0
  92. data/public/images/24x24/navigate_right.png +0 -0
  93. data/public/images/24x24/package_new.png +0 -0
  94. data/public/images/24x24/safe.png +0 -0
  95. data/public/images/24x24/safe_new.png +0 -0
  96. data/public/images/24x24/safe_out.png +0 -0
  97. data/public/images/24x24/scroll_information.png +0 -0
  98. data/public/images/24x24/stop.png +0 -0
  99. data/public/images/24x24/wrench.png +0 -0
  100. data/public/images/README.license +2 -0
  101. data/public/images/blue-16.gif +0 -0
  102. data/public/images/blue-32.gif +0 -0
  103. data/public/images/bugzilla.png +0 -0
  104. data/public/images/cvs.png +0 -0
  105. data/public/images/footer.gif +0 -0
  106. data/public/images/green-128.gif +0 -0
  107. data/public/images/green-16.gif +0 -0
  108. data/public/images/green-32.gif +0 -0
  109. data/public/images/grey-16.gif +0 -0
  110. data/public/images/grey-32.gif +0 -0
  111. data/public/images/jira.gif +0 -0
  112. data/public/images/red-16.gif +0 -0
  113. data/public/images/red-32.gif +0 -0
  114. data/public/images/red-pulse-32.gif +0 -0
  115. data/public/images/rss.gif +0 -0
  116. data/public/images/rubyforge.png +0 -0
  117. data/public/images/scarab.gif +0 -0
  118. data/public/images/sourceforge.gif +0 -0
  119. data/public/images/starteam.png +0 -0
  120. data/public/images/svnlogo64.png +0 -0
  121. data/public/images/trac.png +0 -0
  122. data/public/index.html +1 -0
  123. data/public/javascripts/dw_event.js +34 -0
  124. data/public/javascripts/dw_tooltip.js +86 -0
  125. data/public/javascripts/dw_viewport.js +55 -0
  126. data/public/javascripts/pngfix.js +29 -0
  127. data/public/javascripts/toggle_diff.js +25 -0
  128. data/public/licenses/DAMAGECONTROL.license +28 -0
  129. data/public/licenses/INCORS.license +32 -0
  130. data/public/stylesheets/diff.css +23 -0
  131. data/public/stylesheets/style.css +307 -0
  132. data/script/breakpointer +5 -0
  133. data/script/console +30 -0
  134. data/script/generate +70 -0
  135. data/script/server +61 -0
  136. data/test/damagecontrol/a_program.rb +3 -0
  137. data/test/damagecontrol/a_slow_program.rb +3 -0
  138. data/test/damagecontrol/build_test.rb +59 -0
  139. data/test/damagecontrol/diff_htmlizer_test.rb +31 -0
  140. data/test/damagecontrol/diff_parser_test.rb +61 -0
  141. data/test/damagecontrol/file_ext.rb +12 -0
  142. data/test/damagecontrol/poller_test.rb +56 -0
  143. data/test/damagecontrol/project_test.rb +144 -0
  144. data/test/damagecontrol/scm_web_test.rb +22 -0
  145. data/test/damagecontrol/test.diff +38 -0
  146. data/test/damagecontrol/test.html +40 -0
  147. data/test/damagecontrol/tracker_test.rb +48 -0
  148. data/test/damagecontrol/visitor/changesets.rss +34 -0
  149. data/test/damagecontrol/visitor/diff_persister_test.rb +49 -0
  150. data/test/damagecontrol/visitor/rss_writer_test.rb +40 -0
  151. data/test/damagecontrol/visitor/yaml_persister_test.rb +40 -0
  152. data/test/functional/admin_controller_test.rb +17 -0
  153. data/test/functional/project_controller_test.rb +17 -0
  154. data/test/test_helper.rb +14 -0
  155. metadata +245 -0
@@ -0,0 +1,153 @@
1
+ module DamageControl
2
+
3
+ class DiffParser
4
+
5
+ DIFF_START = /@@ \-([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@/
6
+
7
+ def parse_diffs(io)
8
+ diffs = []
9
+ diff = nil
10
+ io.each_line do |line|
11
+ if(line =~ DIFF_START)
12
+ diffs << diff if diff
13
+ diff = Diff.new($1.to_i, $2.to_i, $3.to_i, $4.to_i)
14
+ elsif(diff)
15
+ diff << line
16
+ end
17
+ end
18
+ diffs << diff if diff
19
+ diffs.each do |diff|
20
+ diff.parse
21
+ end
22
+ diffs
23
+ end
24
+ end
25
+
26
+ # Represents a unified diff section in a diff file.
27
+ class Diff
28
+ attr_reader :minus, :nminus, :plus, :nplus, :removed_range
29
+
30
+ # Create a new Diff. +minus+ and +plus+ represent the number of
31
+ # removed and added lines.
32
+ def initialize(minus, nminus, plus, nplus)
33
+ @minus, @nminus, @plus, @nplus = minus, nminus, plus, nplus
34
+ @lines = []
35
+ end
36
+
37
+ def <<(line)
38
+ @lines << line
39
+ end
40
+
41
+ def line_count
42
+ @lines.length
43
+ end
44
+
45
+ def[](n)
46
+ @lines[n]
47
+ end
48
+
49
+ def parse
50
+ prev = nil
51
+ @lines.each do |line|
52
+ prefix_length = 1
53
+ suffix_len = 0
54
+ if(prev && prev.removed? && line.added?)
55
+ a = line[1..-1]
56
+ b = prev[1..-1]
57
+ prefix_length = a.common_prefix_length(b)+1
58
+ suffix_len = a.reverse.common_prefix_length(b.reverse)
59
+ # prevent prefix/suffix having overlap,
60
+ suffix_len = min(suffix_len, min(line.length,prev.length)-prefix_length)
61
+ remove_infix_length = prev.length - (prefix_length+suffix_len)
62
+ add_infix_length = line.length - (prefix_length+suffix_len)
63
+ oversize_change = remove_infix_length*100/prev.length>33 || add_infix_length*100/line.length>33
64
+
65
+ if prefix_length==1 && suffix_len==0 || remove_infix_length<=0 || oversize_change
66
+ else
67
+ prev.removed_range = (prefix_length..prefix_length+remove_infix_length-1)
68
+ end
69
+
70
+ if prefix_length==1 && suffix_len==0 || add_infix_length<=0 || oversize_change
71
+ else
72
+ line.added_range = (prefix_length..prefix_length+add_infix_length-1)
73
+ end
74
+ end
75
+ prev = line
76
+ end
77
+ end
78
+
79
+ def accept(visitor)
80
+ @lines.each do |line|
81
+ visitor.visitLine(line)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def min(a, b)
88
+ a<b ? a : b
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ # Extra methods added to String to ease diff manipulation
95
+ class String
96
+ attr_accessor :removed_range
97
+ attr_accessor :added_range
98
+
99
+ def removed?
100
+ (self =~ /\-/) == 0
101
+ end
102
+
103
+ def removed
104
+ removed_range ? self[removed_range] : nil
105
+ end
106
+
107
+ def added?
108
+ (self =~ /\+/) == 0
109
+ end
110
+
111
+ def added
112
+ added_range ? self[added_range] : nil
113
+ end
114
+
115
+ def prefix
116
+ self[0..range.first-1]
117
+ end
118
+
119
+ def suffix
120
+ self[range.last+1..-1]
121
+ end
122
+
123
+ def content
124
+ "\n" == self ? "\n" : self[1..-1]
125
+ end
126
+
127
+ def common_prefix_length(o)
128
+ length = 0
129
+ each_byte do |char|
130
+ break unless o[length] == char
131
+ length = length + 1
132
+ end
133
+ length
134
+ end
135
+
136
+ private
137
+
138
+ def range
139
+ removed? ? removed_range : added_range
140
+ end
141
+
142
+ end
143
+
144
+ # Add visiting capabilities to Array
145
+ class Array
146
+ def accept(visitor)
147
+ each do |d|
148
+ visitor.visitDiff(d)
149
+ d.accept(visitor)
150
+ visitor.visitDiffEnd(d)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,126 @@
1
+ require 'fileutils'
2
+ require 'rscm/path_converter'
3
+ require 'rscm/abstract_scm' # for the modified Time.to_s
4
+
5
+ module DamageControl
6
+
7
+ # This class knows about locations of various files and directories.
8
+ #
9
+ module Directories
10
+ include FileUtils
11
+
12
+ def project_names
13
+ result = Dir["#{basedir}/*/project.yaml"].collect do |f|
14
+ File.basename(File.dirname(f))
15
+ end
16
+ result.sort
17
+ end
18
+ module_function :project_names
19
+
20
+ def checkout_dir(project_name)
21
+ "#{project_dir(project_name)}/checkout"
22
+ end
23
+ module_function :checkout_dir
24
+
25
+ # File containing list of files *currently* being
26
+ # checked out.
27
+ def checkout_list_file(project_name)
28
+ "#{project_dir(project_name)}/checkout_list.txt"
29
+ end
30
+ module_function :checkout_list_file
31
+
32
+ def changeset_dir(project_name, changeset_identifier)
33
+ "#{changesets_dir(project_name)}/#{changeset_identifier.to_s}"
34
+ end
35
+ module_function :changeset_dir
36
+
37
+ def builds_dir(project_name, changeset_identifier)
38
+ "#{changeset_dir(project_name, changeset_identifier)}/builds"
39
+ end
40
+ module_function :builds_dir
41
+
42
+ def build_dirs(project_name, changeset_identifier)
43
+ Dir["#{builds_dir(project_name, changeset_identifier)}/*"]
44
+ end
45
+ module_function :build_dirs
46
+
47
+ # Dir for a build created at time +time+
48
+ def build_dir(project_name, changeset_identifier, time)
49
+ "#{builds_dir(project_name, changeset_identifier)}/#{time.to_s}"
50
+ end
51
+ module_function :build_dir
52
+
53
+ # File where stdout for the build command is written
54
+ def stdout(project_name, changeset_identifier, time)
55
+ "#{build_dir(project_name, changeset_identifier, time)}/stdout.log"
56
+ end
57
+ module_function :stdout
58
+
59
+ # File where stderr for the build command is written
60
+ def stderr(project_name, changeset_identifier, time)
61
+ "#{build_dir(project_name, changeset_identifier, time)}/stderr.log"
62
+ end
63
+ module_function :stderr
64
+
65
+ # File where the exit code for the build command execution is stored
66
+ def build_exit_code_file(project_name, changeset_identifier, time)
67
+ "#{build_dir(project_name, changeset_identifier, time)}/exit_code"
68
+ end
69
+ module_function :build_exit_code_file
70
+
71
+ # File where the pid for the build command execution is stored
72
+ def build_pid_file(project_name, changeset_identifier, time)
73
+ "#{build_dir(project_name, changeset_identifier, time)}/pid"
74
+ end
75
+ module_function :build_pid_file
76
+
77
+ # File containing the build command for a build created at time +time+
78
+ def build_command_file(project_name, changeset_identifier, time)
79
+ "#{build_dir(project_name, changeset_identifier, time)}/command"
80
+ end
81
+ module_function :build_command_file
82
+
83
+ def changesets_dir(project_name)
84
+ "#{project_dir(project_name)}/changesets"
85
+ end
86
+ module_function :changesets_dir
87
+
88
+ def changesets_rss_file(project_name)
89
+ "#{changesets_dir(project_name)}/changesets.rss"
90
+ end
91
+ module_function :changesets_rss_file
92
+
93
+ def diff_file(project_name, changeset, change)
94
+ "#{changesets_dir(project_name)}/#{changeset.identifier.to_s}/diffs/#{change.path}.diff"
95
+ end
96
+ module_function :diff_file
97
+
98
+ def trigger_checkout_dir(project_name)
99
+ "#{project_dir(project_name)}/trigger_checkout"
100
+ end
101
+ module_function :trigger_checkout_dir
102
+
103
+ def project_config_file(project_name)
104
+ "#{project_dir(project_name)}/project.yaml"
105
+ end
106
+ module_function :project_config_file
107
+
108
+ def project_dir(project_name)
109
+ "#{basedir}/#{project_name}"
110
+ end
111
+ module_function :project_dir
112
+
113
+ def basedir
114
+ if(ENV['DAMAGECONTROL_HOME'])
115
+ ENV['DAMAGECONTROL_HOME']
116
+ elsif(WINDOWS)
117
+ RSCM::PathConverter.nativepath_to_filepath("#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}/.damagecontrol").gsub(/\\/, "/")
118
+ else
119
+ "#{ENV['HOME']}/.damagecontrol"
120
+ end
121
+ end
122
+ module_function :basedir
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,72 @@
1
+ require 'rscm/logging'
2
+ require 'rscm/time_ext'
3
+ require 'damagecontrol/project'
4
+
5
+ module DamageControl
6
+ # Polls all projects in intervals.
7
+ class Poller
8
+ attr_reader :projects
9
+
10
+ # Creates a new poller. Takes a block that will
11
+ # receive |project, changesets| each time new
12
+ # +changesets+ are found in a polled +project+
13
+ def initialize(sleeptime=60, &proc)
14
+ @projects = []
15
+ @sleeptime = sleeptime
16
+ @proc = proc
17
+ end
18
+
19
+ # Adds a project to poll. If the project is already added it is replaced
20
+ # with then new one, otherwise appended to the end.
21
+ def add_project(project)
22
+ index = @projects.index(project) || @projects.length
23
+ @projects[index] = project
24
+ end
25
+
26
+ # Polls all registered projects and persists RSS, changesets and diffs to disk.
27
+ # If a block is passed, the project and the changesets will be yielded to the block
28
+ # for each new changesets object.
29
+ def poll
30
+ @projects.each do |project|
31
+ begin
32
+ if(project.scm_exists?)
33
+ project.poll do |changesets|
34
+ if(changesets.empty?)
35
+ Log.info "No changesets for #{project.name}"
36
+ else
37
+ @proc.call(project, changesets)
38
+ end
39
+ end
40
+ end
41
+ rescue => e
42
+ $stderr.puts "Error polling #{project.name}"
43
+ $stderr.puts e.message
44
+ $stderr.puts " " + e.backtrace.join(" \n")
45
+ end
46
+ end
47
+ end
48
+
49
+ # Runs +poll+ in a separate thread.
50
+ def start
51
+ add_all_projects
52
+ @t = Thread.new do
53
+ while(true)
54
+ poll
55
+ sleep(@sleeptime)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Stops thread after a +start+.
61
+ def stop
62
+ @t.kill if @t && @t.alive?
63
+ end
64
+
65
+ # Adds all projects
66
+ def add_all_projects
67
+ Project.find_all.each do |project|
68
+ add_project(project)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,213 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'rscm'
4
+ require 'damagecontrol/build'
5
+ require 'damagecontrol/directories'
6
+ require 'damagecontrol/diff_parser'
7
+ require 'damagecontrol/diff_htmlizer'
8
+ require 'damagecontrol/scm_web'
9
+ require 'damagecontrol/tracker'
10
+ require 'damagecontrol/visitor/yaml_persister'
11
+ require 'damagecontrol/visitor/diff_persister'
12
+ require 'damagecontrol/visitor/rss_writer'
13
+
14
+ module DamageControl
15
+ # Represents a project with associated SCM, Tracker and SCMWeb
16
+ class Project
17
+
18
+ attr_accessor :name
19
+ attr_accessor :description
20
+ attr_accessor :home_page
21
+
22
+ attr_accessor :scm
23
+ attr_accessor :tracker
24
+ attr_accessor :scm_web
25
+
26
+ # How long to sleep between each changesets invocation for non-transactional SCMs
27
+ attr_accessor :quiet_period
28
+
29
+ attr_accessor :build_command
30
+
31
+ # Loads the project with the given +name+.
32
+ def Project.load(name)
33
+ config_file = Directories.project_config_file(name)
34
+ Log.info "Loading project from #{config_file}"
35
+ File.open(config_file) do |io|
36
+ YAML::load(io)
37
+ end
38
+ end
39
+
40
+ # Loads all projects
41
+ def Project.find_all
42
+ Directories.project_names.collect do |name|
43
+ Project.load(name)
44
+ end
45
+ end
46
+
47
+ def initialize(name=nil)
48
+ @name = name
49
+ @scm = nil
50
+ @tracker = Tracker::Null.new
51
+ @scm_web = SCMWeb::Null.new
52
+ end
53
+
54
+ # Saves the state of this project to persistent store (YAML)
55
+ def save
56
+ f = project_config_file
57
+ FileUtils.mkdir_p(File.dirname(f))
58
+ File.open(f, "w") do |io|
59
+ YAML::dump(self, io)
60
+ end
61
+
62
+ REGISTRY.poller.add_project(self) if REGISTRY
63
+ end
64
+
65
+ # Path to file containing pathnames of latest checked out files.
66
+ def checkout_list_file
67
+ Directories.checkout_list_file(name)
68
+ end
69
+
70
+ # Checks out files to project's checkout directory.
71
+ # Writes the checked out files to +checkout_list_file+.
72
+ # The +changeset_identifier+ parameter is a String or a Time
73
+ # representing a changeset.
74
+ def checkout(changeset_identifier)
75
+ File.open(checkout_list_file, "w") do |f|
76
+ scm.checkout(checkout_dir, changeset_identifier) do |file_name|
77
+ f << file_name << "\n"
78
+ f.flush
79
+ end
80
+ end
81
+ end
82
+
83
+ # Polls SCM for new changesets and yields them to the given block.
84
+ def poll(from_if_first_poll=Time.epoch)
85
+ start = Time.now
86
+ from = next_changeset_identifier || from_if_first_poll
87
+
88
+ Log.info "Getting changesets for #{name} from #{from} (retrieved from #{checkout_dir})"
89
+ changesets = @scm.changesets(checkout_dir, from)
90
+ if(!changesets.empty? && !@scm.transactional?)
91
+ # We're dealing with a non-transactional SCM (like CVS/StarTeam/ClearCase,
92
+ # unlike Subversion/Monotone). Sleep a little, get the changesets again.
93
+ # When the changesets are not changing, we can consider the last commit done
94
+ # and the quiet period elapsed. This is not 100% failsafe, but will work
95
+ # under most circumstances. In the worst case, we'll miss some files in
96
+ # the changesets, but they will be part of the next changeset (on next poll).
97
+ commit_in_progress = true
98
+ while(commit_in_progress)
99
+ @quiet_period ||= 5
100
+ Log.info "Sleeping for #{@quiet_period} seconds since #{name}'s SCM (#{@scm.name}) is not transactional."
101
+ sleep @quiet_period
102
+ next_changesets = @scm.changesets(checkout_dir, from)
103
+ commit_in_progress = changesets != next_changesets
104
+ changesets = next_changesets
105
+ end
106
+ Log.info "Quiet period elapsed for #{name}"
107
+ end
108
+ Log.info "Got changesets for #{@name} in #{Time.now.difference_as_text(start)}"
109
+ yield changesets
110
+ end
111
+
112
+ # Returns the identifier (int label or time) that should be used to get the next (unrecorded)
113
+ # changeset. This is the identifier *following* the latest recorded changeset.
114
+ # This identifier is determined by looking at the directory names under
115
+ # +changesets_dir+. If there are none, this method returns nil.
116
+ def next_changeset_identifier(d=changesets_dir)
117
+ # See String extension at top of this file.
118
+ latest_identifier = DamageControl::Visitor::YamlPersister.new(d).latest_identifier
119
+ latest_identifier ? latest_identifier + 1 : nil
120
+ end
121
+
122
+ # Where RSS is written.
123
+ def changesets_rss_file
124
+ Directories.changesets_rss_file(name)
125
+ end
126
+
127
+ def checked_out?
128
+ @scm.checked_out?(checkout_dir)
129
+ end
130
+
131
+ def exists?
132
+ File.exists?(project_config_file)
133
+ end
134
+
135
+ def scm_exists?
136
+ scm.exists?
137
+ end
138
+
139
+ def checkout_dir
140
+ Directories.checkout_dir(name)
141
+ end
142
+
143
+ def delete_working_copy
144
+ File.delete(checkout_dir)
145
+ end
146
+
147
+ def changesets_rss_exists?
148
+ File.exist?(changesets_rss_file)
149
+ end
150
+
151
+ def changesets_dir
152
+ Directories.changesets_dir(name)
153
+ end
154
+
155
+ def changesets(changeset_identifier, prior)
156
+ changesets_persister.load_upto(changeset_identifier, prior)
157
+ end
158
+
159
+ def changeset_identifiers
160
+ changesets_persister.identifiers
161
+ end
162
+
163
+ def latest_changeset_identifier
164
+ changesets_persister.latest_identifier
165
+ end
166
+
167
+ def delete
168
+ File.delete(Directories.project_dir(name))
169
+ end
170
+
171
+ def == (o)
172
+ return false unless o.is_a?(Project)
173
+ name == o.name
174
+ end
175
+
176
+ def changesets_persister
177
+ DamageControl::Visitor::YamlPersister.new(changesets_dir)
178
+ end
179
+
180
+ # Creates, persists and executes a build for the changeset with the given
181
+ # +changeset_identifier+.
182
+ # Should be called with a block of arity 1 that will receive the build.
183
+ def build(changeset_identifier)
184
+ scm.checkout(checkout_dir, changeset_identifier)
185
+ build = Build.new(name, changeset_identifier, Time.now.utc)
186
+ yield build
187
+ end
188
+
189
+ # Returns an array of existing builds for the given +changeset+.
190
+ def builds(changeset_identifier)
191
+ Directories.build_dirs(name, changeset_identifier).collect do |dir|
192
+ # The dir's basename will always be a Time
193
+ Build.new(name, changeset_identifier, File.basename(dir).to_identifier)
194
+ end
195
+ end
196
+
197
+ # Returns the latest build.
198
+ def latest_build
199
+ changeset_identifiers.reverse.each do |changeset_identifier|
200
+ builds = builds(changeset_identifier)
201
+ return builds[-1] unless builds.empty?
202
+ end
203
+ nil
204
+ end
205
+
206
+ private
207
+
208
+ def project_config_file
209
+ Directories.project_config_file(name)
210
+ end
211
+
212
+ end
213
+ end
@@ -0,0 +1,8 @@
1
+ require 'damagecontrol/project'
2
+
3
+ module DamageControl
4
+ class Project
5
+ def add_dependency(project)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ require 'rscm/path_converter'
2
+
3
+ module DamageControl
4
+ module SCMWeb
5
+
6
+ class Null
7
+ def change_url(change, anchor=false)
8
+ change.path
9
+ end
10
+
11
+ def changeset_url(changeset, anchor=false)
12
+ "http://foo.bar/"
13
+ end
14
+ end
15
+
16
+ class ViewCVS
17
+ attr_accessor :baseurl
18
+
19
+ def initialize(baseurl)
20
+ @baseurl = baseurl
21
+ end
22
+
23
+ def url
24
+ RSCM::PathConverter.ensure_trailing_slash(baseurl)
25
+ end
26
+
27
+ def change_url(change, anchor=false)
28
+ result = nil
29
+ if(change.previous_revision)
30
+ result = "#{url}#{change.path}?r1=#{change.previous_revision}&r2=#{change.revision}"
31
+ else
32
+ # point to the viewcvs (rev) and fisheye (r) revisions (no diff view)
33
+ result = "#{url}#{change.path}?rev=#{change.revision}&r=#{change.revision}"
34
+ end
35
+ anchor ? "<a href=\"#{result}\">#{change.path}</a>" : result
36
+ end
37
+
38
+ def changeset_url(changeset, anchor=false)
39
+ url
40
+ end
41
+ end
42
+
43
+ class Fisheye < ViewCVS
44
+ def changeset_url(changeset, anchor=false)
45
+ # TODO: link to their faked CVS changesets (or proper SVN ones when that happens).
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,49 @@
1
+ require 'rss/maker'
2
+ require 'rscm/logging'
3
+ require 'rscm/time_ext'
4
+ require 'damagecontrol/project'
5
+
6
+ module DamageControl
7
+ # Persists standard stuff like changesets, diffs and RSS.
8
+ # Typically used within a block passed to a Poller. See App.
9
+ class StandardPersister
10
+
11
+ # Save the changesets to disk as YAML
12
+ def save_changesets(project, changesets)
13
+ Log.info "Saving changesets for #{project.name}"
14
+ changesets.accept(project.changesets_persister)
15
+ end
16
+
17
+ # Get the diffs for each change and save them.
18
+ def save_diffs(project, changesets)
19
+ Log.info "Getting diffs for #{project.name}"
20
+ dp = DamageControl::Visitor::DiffPersister.new(project.scm, project.name)
21
+ changesets.accept(dp)
22
+ Log.info "Saved diffs for #{project.name}"
23
+ end
24
+
25
+ # Save RSS to disk. The RSS spec says max 15 items in a channel,
26
+ # (http://www.chadfowler.com/ruby/rss/)
27
+ # We'll get upto the latest 15 changesets and turn them into RSS.
28
+ def save_rss(project)
29
+ Log.info "Generating RSS for #{project.name}"
30
+ last_15_changesets = project.changesets_persister.load_upto(project.changesets_persister.latest_identifier, 15)
31
+ RSS::Maker.make("2.0") do |rss|
32
+ FileUtils.mkdir_p(File.dirname(project.changesets_rss_file))
33
+ File.open(project.changesets_rss_file, "w") do |io|
34
+ rss_writer = DamageControl::Visitor::RssWriter.new(
35
+ rss,
36
+ "Changesets for #{@name}",
37
+ "http://localhost:4712/", # TODO point to web version of changeset
38
+ project.description,
39
+ project.tracker || Tracker::Null.new,
40
+ project.scm_web || SCMWeb::Null.new
41
+ )
42
+ last_15_changesets.accept(rss_writer)
43
+ io.write(rss.to_rss)
44
+ end
45
+ end
46
+ Log.info "Saved RSS for #{project.name} to #{project.changesets_rss_file}"
47
+ end
48
+ end
49
+ end