damagecontrol 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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