damagecontrol 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|