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.
- data/README +75 -0
- data/README.license +5 -0
- data/Rakefile +111 -0
- data/app/controllers/admin_controller.rb +10 -0
- data/app/controllers/application.rb +163 -0
- data/app/controllers/files_controller.rb +19 -0
- data/app/controllers/project_controller.rb +284 -0
- data/app/controllers/scm_controller.rb +49 -0
- data/app/helpers/admin_helper.rb +2 -0
- data/app/helpers/application_helper.rb +3 -0
- data/app/helpers/project_helper.rb +2 -0
- data/app/views/dhtml_sites.txt +6 -0
- data/app/views/files/list.rhtml +4 -0
- data/app/views/layouts/rscm.rhtml +79 -0
- data/app/views/project/_bugzilla.rhtml +13 -0
- data/app/views/project/_changesets_list.rhtml +52 -0
- data/app/views/project/_cvs.rhtml +171 -0
- data/app/views/project/_jira.rhtml +19 -0
- data/app/views/project/_mooky.rhtml +23 -0
- data/app/views/project/_null.rhtml +0 -0
- data/app/views/project/_project.rhtml +36 -0
- data/app/views/project/_rubyforge.rhtml +19 -0
- data/app/views/project/_scarab.rhtml +19 -0
- data/app/views/project/_scms.rhtml +15 -0
- data/app/views/project/_sourceforge.rhtml +19 -0
- data/app/views/project/_starteam.rhtml +43 -0
- data/app/views/project/_svn.rhtml +22 -0
- data/app/views/project/_trac.rhtml +13 -0
- data/app/views/project/_trackers.rhtml +18 -0
- data/app/views/project/changesets.rhtml +31 -0
- data/app/views/project/index.rhtml +23 -0
- data/app/views/project/view.rhtml +70 -0
- data/app/views/scm/checkout_status.rhtml +44 -0
- data/app/views/scm/diff.rhtml +1 -0
- data/app/views/scm/scroll.html +27 -0
- data/bin/damagecontrol +7 -0
- data/bin/damagecontrol-webrick +2 -0
- data/config/database.yml +20 -0
- data/config/environment.rb +60 -0
- data/config/environments/development.rb +3 -0
- data/config/environments/production.rb +2 -0
- data/config/environments/test.rb +3 -0
- data/lib/damagecontrol/app.rb +74 -0
- data/lib/damagecontrol/build.rb +104 -0
- data/lib/damagecontrol/diff_htmlizer.rb +82 -0
- data/lib/damagecontrol/diff_parser.rb +153 -0
- data/lib/damagecontrol/directories.rb +126 -0
- data/lib/damagecontrol/poller.rb +72 -0
- data/lib/damagecontrol/project.rb +213 -0
- data/lib/damagecontrol/project_dependencies.rb +8 -0
- data/lib/damagecontrol/scm_web.rb +50 -0
- data/lib/damagecontrol/standard_persister.rb +49 -0
- data/lib/damagecontrol/tracker.rb +164 -0
- data/lib/damagecontrol/visitor/build_executor.rb +32 -0
- data/lib/damagecontrol/visitor/diff_persister.rb +41 -0
- data/lib/damagecontrol/visitor/rss_writer.rb +43 -0
- data/lib/damagecontrol/visitor/yaml_persister.rb +71 -0
- data/public/404.html +6 -0
- data/public/500.html +6 -0
- data/public/dispatch.cgi +10 -0
- data/public/dispatch.fcgi +7 -0
- data/public/dispatch.rb +10 -0
- data/public/images/16x16/about.png +0 -0
- data/public/images/16x16/bug_green.png +0 -0
- data/public/images/16x16/bug_red.png +0 -0
- data/public/images/16x16/bug_yellow.png +0 -0
- data/public/images/16x16/component.png +0 -0
- data/public/images/16x16/console.png +0 -0
- data/public/images/16x16/console_error.png +0 -0
- data/public/images/16x16/document_add.png +0 -0
- data/public/images/16x16/document_delete.png +0 -0
- data/public/images/16x16/document_edit.png +0 -0
- data/public/images/16x16/document_exchange.png +0 -0
- data/public/images/16x16/document_new.png +0 -0
- data/public/images/16x16/document_warning.png +0 -0
- data/public/images/16x16/safe.png +0 -0
- data/public/images/16x16/scroll_information.png +0 -0
- data/public/images/16x16/wrench.png +0 -0
- data/public/images/24x24/box_delete.png +0 -0
- data/public/images/24x24/box_into.png +0 -0
- data/public/images/24x24/box_new.png +0 -0
- data/public/images/24x24/console_network.png +0 -0
- data/public/images/24x24/document_edit.png +0 -0
- data/public/images/24x24/find.png +0 -0
- data/public/images/24x24/folders.png +0 -0
- data/public/images/24x24/garbage.png +0 -0
- data/public/images/24x24/gear_connection.png +0 -0
- data/public/images/24x24/gear_delete.png +0 -0
- data/public/images/24x24/gears_run.png +0 -0
- data/public/images/24x24/home.png +0 -0
- data/public/images/24x24/navigate_left.png +0 -0
- data/public/images/24x24/navigate_right.png +0 -0
- data/public/images/24x24/package_new.png +0 -0
- data/public/images/24x24/safe.png +0 -0
- data/public/images/24x24/safe_new.png +0 -0
- data/public/images/24x24/safe_out.png +0 -0
- data/public/images/24x24/scroll_information.png +0 -0
- data/public/images/24x24/stop.png +0 -0
- data/public/images/24x24/wrench.png +0 -0
- data/public/images/README.license +2 -0
- data/public/images/blue-16.gif +0 -0
- data/public/images/blue-32.gif +0 -0
- data/public/images/bugzilla.png +0 -0
- data/public/images/cvs.png +0 -0
- data/public/images/footer.gif +0 -0
- data/public/images/green-128.gif +0 -0
- data/public/images/green-16.gif +0 -0
- data/public/images/green-32.gif +0 -0
- data/public/images/grey-16.gif +0 -0
- data/public/images/grey-32.gif +0 -0
- data/public/images/jira.gif +0 -0
- data/public/images/red-16.gif +0 -0
- data/public/images/red-32.gif +0 -0
- data/public/images/red-pulse-32.gif +0 -0
- data/public/images/rss.gif +0 -0
- data/public/images/rubyforge.png +0 -0
- data/public/images/scarab.gif +0 -0
- data/public/images/sourceforge.gif +0 -0
- data/public/images/starteam.png +0 -0
- data/public/images/svnlogo64.png +0 -0
- data/public/images/trac.png +0 -0
- data/public/index.html +1 -0
- data/public/javascripts/dw_event.js +34 -0
- data/public/javascripts/dw_tooltip.js +86 -0
- data/public/javascripts/dw_viewport.js +55 -0
- data/public/javascripts/pngfix.js +29 -0
- data/public/javascripts/toggle_diff.js +25 -0
- data/public/licenses/DAMAGECONTROL.license +28 -0
- data/public/licenses/INCORS.license +32 -0
- data/public/stylesheets/diff.css +23 -0
- data/public/stylesheets/style.css +307 -0
- data/script/breakpointer +5 -0
- data/script/console +30 -0
- data/script/generate +70 -0
- data/script/server +61 -0
- data/test/damagecontrol/a_program.rb +3 -0
- data/test/damagecontrol/a_slow_program.rb +3 -0
- data/test/damagecontrol/build_test.rb +59 -0
- data/test/damagecontrol/diff_htmlizer_test.rb +31 -0
- data/test/damagecontrol/diff_parser_test.rb +61 -0
- data/test/damagecontrol/file_ext.rb +12 -0
- data/test/damagecontrol/poller_test.rb +56 -0
- data/test/damagecontrol/project_test.rb +144 -0
- data/test/damagecontrol/scm_web_test.rb +22 -0
- data/test/damagecontrol/test.diff +38 -0
- data/test/damagecontrol/test.html +40 -0
- data/test/damagecontrol/tracker_test.rb +48 -0
- data/test/damagecontrol/visitor/changesets.rss +34 -0
- data/test/damagecontrol/visitor/diff_persister_test.rb +49 -0
- data/test/damagecontrol/visitor/rss_writer_test.rb +40 -0
- data/test/damagecontrol/visitor/yaml_persister_test.rb +40 -0
- data/test/functional/admin_controller_test.rb +17 -0
- data/test/functional/project_controller_test.rb +17 -0
- data/test/test_helper.rb +14 -0
- 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,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
|