whistle 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/History.txt +3 -0
  2. data/README.txt +38 -0
  3. data/bin/whistle +90 -0
  4. data/lib/config.rb +19 -0
  5. data/lib/phash.rb +16 -0
  6. data/lib/relay.rb +24 -0
  7. data/lib/resource.rb +113 -0
  8. data/lib/ssl_patch.rb +15 -0
  9. data/lib/switchbox.rb +54 -0
  10. data/lib/time_ext.rb +30 -0
  11. data/lib/version.rb +3 -0
  12. data/sample/config.yml +12 -0
  13. data/vendor/rscm-0.5.1-patched-stripped/README +218 -0
  14. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm.rb +14 -0
  15. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/abstract_log_parser.rb +35 -0
  16. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/base.rb +289 -0
  17. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/command_line.rb +146 -0
  18. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/difftool.rb +44 -0
  19. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/line_editor.rb +46 -0
  20. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/mockit.rb +157 -0
  21. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/parser.rb +39 -0
  22. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/path_converter.rb +60 -0
  23. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/platform.rb +26 -0
  24. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision.rb +103 -0
  25. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_file.rb +85 -0
  26. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revision_poller.rb +93 -0
  27. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/revisions.rb +79 -0
  28. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/clearcase.rb +182 -0
  29. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs.rb +374 -0
  30. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/cvs_log_parser.rb +154 -0
  31. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs.rb +120 -0
  32. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/darcs_log_parser.rb +65 -0
  33. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone.rb +338 -0
  34. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/monotone_log_parser.rb +109 -0
  35. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/mooky.rb +6 -0
  36. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/perforce.rb +216 -0
  37. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/star_team.rb +104 -0
  38. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion.rb +397 -0
  39. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/scm/subversion_log_parser.rb +165 -0
  40. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/tempdir.rb +17 -0
  41. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/time_ext.rb +11 -0
  42. data/vendor/rscm-0.5.1-patched-stripped/lib/rscm/version.rb +13 -0
  43. data/vendor/ruby-feedparser-0.5-stripped/README +14 -0
  44. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser.rb +28 -0
  45. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/feedparser.rb +300 -0
  46. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/filesizes.rb +12 -0
  47. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html-output.rb +126 -0
  48. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/html2text-parser.rb +409 -0
  49. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/rexml_patch.rb +28 -0
  50. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/sgml-parser.rb +332 -0
  51. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/text-output.rb +83 -0
  52. data/vendor/ruby-feedparser-0.5-stripped/lib/feedparser/textconverters.rb +120 -0
  53. metadata +132 -0
@@ -0,0 +1,109 @@
1
+ require 'rscm'
2
+ require 'time'
3
+ require 'stringio'
4
+
5
+ module RSCM
6
+
7
+ class MonotoneLogParser
8
+
9
+ def parse_revisions(io, from_identifier=Time.epoch, to_identifier=Time.infinity)
10
+ # skip first separator
11
+ io.readline
12
+
13
+ all_revisions = []
14
+ revision_string = ""
15
+
16
+ # hash of path => [array of revisions]
17
+ path_revisions = {}
18
+ io.each_line do |line|
19
+ if(line =~ /-----------------------------------------------------------------/)
20
+ revision = parse_revision(StringIO.new(revision_string), path_revisions)
21
+ all_revisions << revision
22
+ revision_string = ""
23
+ else
24
+ revision_string << line
25
+ end
26
+ end
27
+ revision = parse_revision(StringIO.new(revision_string), path_revisions)
28
+ all_revisions << revision
29
+
30
+ # Filter out the revisions and set the previous revisions, knowing that most recent is at index 0.
31
+
32
+ from_time = time(all_revisions, from_identifier, Time.epoch)
33
+ to_time = time(all_revisions, to_identifier, Time.infinity)
34
+
35
+ revisions = Revisions.new
36
+
37
+ all_revisions.each do |revision|
38
+ if((from_time < revision.time) && (revision.time <= to_time))
39
+ revisions.add(revision)
40
+ revision.each do |change|
41
+ current_index = path_revisions[change.path].index(change.native_revision_identifier)
42
+ change.previous_native_revision_identifier = path_revisions[change.path][current_index + 1]
43
+ end
44
+ end
45
+ end
46
+ revisions
47
+ end
48
+
49
+ def parse_revision(revision_io, path_revisions)
50
+ revision = Revision.new
51
+ state = nil
52
+ revision_io.each_line do |line|
53
+ if(line =~ /^Revision: (.*)$/ && revision.identifier.nil?)
54
+ revision.identifier = $1
55
+ elsif(line =~ /^Author: (.*)$/ && revision.developer.nil?)
56
+ revision.developer = $1
57
+ elsif(line =~ /^Date: (.*)$/ && revision.time.nil?)
58
+ revision.time = Time.utc(
59
+ $1[0..3].to_i,
60
+ $1[5..6].to_i,
61
+ $1[8..9].to_i,
62
+ $1[11..12].to_i,
63
+ $1[14..15].to_i,
64
+ $1[17..18].to_i
65
+ )
66
+ elsif(line =~ /^ChangeLog:\s*$/ && revision.message.nil?)
67
+ state = :message
68
+ elsif(state == :message && revision.message.nil?)
69
+ revision.message = ""
70
+ elsif(state == :message && revision.message)
71
+ revision.message << line
72
+ elsif(line =~ /^Added files:\s*$/)
73
+ state = :added
74
+ elsif(state == :added)
75
+ add_changes(revision, line, RevisionFile::ADDED, path_revisions)
76
+ elsif(line =~ /^Modified files:\s*$/)
77
+ state = :modified
78
+ elsif(state == :modified)
79
+ add_changes(revision, line, RevisionFile::MODIFIED, path_revisions)
80
+ end
81
+ end
82
+ revision.message.chomp! rescue revision.message = ''
83
+ revision
84
+ end
85
+
86
+ private
87
+
88
+ def time(revisions, identifier, default)
89
+ cs = revisions.find do |revision|
90
+ revision.identifier == identifier
91
+ end
92
+ cs ? cs.time : (identifier.is_a?(Time) ? identifier : default)
93
+ end
94
+
95
+ def add_changes(revision, line, state, path_revisions)
96
+ paths = line.split(" ")
97
+ paths.each do |path|
98
+ revision << RevisionFile.new(path, state, revision.developer, nil, revision.identifier, revision.time)
99
+
100
+ # now record path revisions so we can keep track of previous rev for each path
101
+ # doesn't work for moved files, and have no idea how to make it work either.
102
+ path_revisions[path] ||= []
103
+ path_revisions[path] << revision.identifier
104
+ end
105
+
106
+ end
107
+ end
108
+
109
+ end
@@ -0,0 +1,6 @@
1
+ require 'rscm/base'
2
+
3
+ module RSCM
4
+ class Mooky < Base
5
+ end
6
+ end
@@ -0,0 +1,216 @@
1
+ # TODO
2
+ # Support int revision numbers AND dates
3
+ # Leverage default P4 client settings (optional)
4
+
5
+ require 'rscm/base'
6
+ require 'rscm/path_converter'
7
+ require 'rscm/line_editor'
8
+
9
+ require 'fileutils'
10
+ require 'time'
11
+ require 'socket'
12
+
13
+ module RSCM
14
+ class Perforce < Base
15
+ unless defined? DATE_FORMAT
16
+ DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
17
+ # Doesn't work for empty messages, (Like 21358 in Aslak's P4 repo)
18
+ CHANGELIST_PATTERN = /^Change \d+ by (.*)@.* on (.*)\n\n(.*)\n\nAffected files ...\n\n(.*)/m
19
+ # But this one does
20
+ CHANGELIST_PATTERN_NO_MSG = /^Change \d+ by (.*)@.* on (.*)\n\nAffected files ...\n\n(.*)/m
21
+
22
+ STATES = {
23
+ "add" => RevisionFile::ADDED,
24
+ "edit" => RevisionFile::MODIFIED,
25
+ "delete" => RevisionFile::DELETED
26
+ }
27
+ end
28
+
29
+ attr_accessor :view
30
+ attr_accessor :username
31
+ attr_accessor :password
32
+
33
+ def installed?
34
+ begin
35
+ execute("p4 info", {})
36
+ true
37
+ rescue
38
+ false
39
+ end
40
+ end
41
+
42
+ def revisions(from_identifier=Time.new.utc, options={})
43
+ raise "from_identifer cannot be nil" if from_identifier.nil?
44
+ set_utc_offset(options)
45
+ view_as_regexp = "^" + @view.gsub(/\.\.\./, "(.*)")
46
+ relative_path_pattern = Regexp.new(view_as_regexp)
47
+
48
+ from_identifier = Time.epoch unless from_identifier
49
+ from_identifier = Time.epoch if (from_identifier.is_a? Time and from_identifier < Time.epoch)
50
+ from = revision_spec(from_identifier + 1) # We have to add 1 because of the contract of this method.
51
+
52
+ to_identifier = options[:to_identifier] ? options[:to_identifier] : Time.infinity
53
+ to = revision_spec(to_identifier - 1) # We have to subtract 1 because of the contract of this method.
54
+
55
+ cmd = "p4 #{p4_opts(false)} changes #{@view}@#{from},#{to}"
56
+ revisions = Revisions.new
57
+ revisions.cmd = cmd if store_revisions_command?
58
+
59
+ changes = execute(cmd, options) do |io|
60
+ io.read
61
+ end
62
+
63
+ changes.each do |line|
64
+ revision = nil
65
+ identifier = line.match(/^Change (\d+)/)[1].to_i
66
+
67
+ execute("p4 #{p4_opts(false)} describe -s #{identifier}", options) do |io|
68
+ log = io.read
69
+
70
+ if log =~ CHANGELIST_PATTERN
71
+ developer, time, message, files = $1, $2, $3, $4
72
+ elsif log =~ CHANGELIST_PATTERN_NO_MSG
73
+ developer, time, files = $1, $2, $3
74
+ else
75
+ puts "PARSE ERROR:"
76
+ puts log
77
+ puts "\nDIDN'T MATCH:"
78
+ puts CHANGELIST_PATTERN
79
+ end
80
+
81
+ # The parsed time doesn't have timezone info. We'll tweak it.
82
+ time = Time.parse(time + " UTC") - @utc_offset
83
+
84
+ files.each_line do |line|
85
+ if line =~ /^\.\.\. (\/\/.+)#(\d+) (.+)/
86
+ depot_path = $1
87
+ file_identifier = $2.to_i
88
+ state = $3.strip
89
+ if(STATES[state])
90
+ if(depot_path =~ relative_path_pattern)
91
+ relative_path = $1
92
+
93
+ if revision.nil?
94
+ revision = Revision.new
95
+ revision.identifier = identifier
96
+ revision.developer = developer
97
+ revision.message = message
98
+ revision.time = time
99
+ revisions.add revision
100
+ end
101
+
102
+ file = RevisionFile.new
103
+ file.path = relative_path
104
+ file.native_revision_identifier = file_identifier
105
+ file.previous_native_revision_identifier = file.native_revision_identifier-1
106
+ file.status = STATES[state]
107
+ revision.add file
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ revisions
115
+ end
116
+
117
+ def destroy_working_copy(options={})
118
+ execute("p4 #{p4_opts(false)} client -d #{client_name}", options)
119
+ end
120
+
121
+ def open(revision_file, options={}, &block)
122
+ path = @view.gsub(/\.\.\./, revision_file.path) # + "@" + revision_file.native_revision_identifier
123
+ cmd = "p4 #{p4_opts(false)} print -q #{path}"
124
+ execute(cmd, options) do |io|
125
+ block.call io
126
+ end
127
+ end
128
+
129
+ def diff
130
+ #p4 diff2 //depot/trunk/build.xml@26405 //depot/trunk/build.xml@26409
131
+ end
132
+
133
+ protected
134
+
135
+ def checkout_silent(to_identifier, options)
136
+ checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
137
+ FileUtils.mkdir_p(@checkout_dir)
138
+
139
+ ensure_client(options)
140
+ execute("p4 #{p4_opts} sync #{@view}@#{to_identifier}", options)
141
+ end
142
+
143
+ def ignore_paths
144
+ []
145
+ end
146
+
147
+ private
148
+
149
+ def p4_opts(with_client=true)
150
+ user_opt = @username.to_s.empty? ? "" : "-u #{@username}"
151
+ password_opt = @password.to_s.empty? ? "" : "-P #{@password}"
152
+ client_opt = with_client ? "-c \"#{client_name}\"" : ""
153
+ "#{user_opt} #{password_opt} #{client_opt}"
154
+ end
155
+
156
+ def client_name
157
+ raise "checkout_dir not set" unless @checkout_dir
158
+ Socket.gethostname + ":" + @checkout_dir
159
+ end
160
+
161
+ def ensure_client(options)
162
+ create_client(options)
163
+ end
164
+
165
+ def create_client(options)
166
+ options = {:mode => "w+"}.merge(options)
167
+ FileUtils.mkdir_p(@checkout_dir)
168
+ execute("p4 #{p4_opts(false)} client -i", options) do |io|
169
+ io.puts(client_spec)
170
+ io.close_write
171
+ end
172
+ end
173
+
174
+ def client_spec
175
+ <<-EOF
176
+ Client: #{client_name}
177
+ Owner: #{@username}
178
+ Host: #{Socket.gethostname}
179
+ Description: RSCM client
180
+ Root: #{@checkout_dir}
181
+ Options: noallwrite noclobber nocompress unlocked nomodtime normdir
182
+ LineEnd: local
183
+ View: #{@view} //#{client_name}/...
184
+ EOF
185
+ end
186
+
187
+ def revision_spec(identifier)
188
+ if identifier.is_a?(Time)
189
+ # The p4 client uses local time, but rscm uses utc
190
+ # We have to convert to local time
191
+ identifier += @utc_offset
192
+ identifier.strftime(DATE_FORMAT)
193
+ else
194
+ identifier.to_i
195
+ end
196
+ end
197
+
198
+ # Queries the server for the time offset. Required in order to get proper
199
+ # timezone for revisions
200
+ def set_utc_offset(options)
201
+ unless @utc_offset
202
+ execute("p4 #{p4_opts(false)} info", options) do |io|
203
+ io.each do |line|
204
+ if line =~ /^Server date: (.*)/
205
+ server_time = Time.parse($1)
206
+ @utc_offset = server_time.utc_offset
207
+ end
208
+ end
209
+ end
210
+ raise "Couldn't get server's UTC offset" if @utc_offset.nil?
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ end
@@ -0,0 +1,104 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ require 'rscm/revision'
4
+ require 'rscm/base'
5
+ require 'yaml'
6
+
7
+ class Time
8
+ def to_rfc2822
9
+ utc.strftime("%a, %d %b %Y %H:%M:%S +0000")
10
+ end
11
+ end
12
+
13
+ module RSCM
14
+ # The RSCM StarTeam class requires that the following software be installed:
15
+ #
16
+ # * Java Runtime (1.4.2)
17
+ # * StarTeam SDK
18
+ # * Apache Ant (http://ant.apache.org/)
19
+ #
20
+ class StarTeam < Base
21
+ attr_accessor :user_name
22
+ attr_accessor :password
23
+ attr_accessor :server_name
24
+ attr_accessor :server_port
25
+ attr_accessor :project_name
26
+ attr_accessor :view_name
27
+ attr_accessor :folder_name
28
+
29
+ def initialize(user_name="", password="", server_name="", server_port="", project_name="", view_name="", folder_name="")
30
+ @user_name, @password, @server_name, @server_port, @project_name, @view_name, @folder_name = user_name, password, server_name, server_port, project_name, view_name, folder_name
31
+ end
32
+
33
+ def revisions(checkout_dir, from_identifier=Time.epoch, to_identifier=Time.infinity, &proc)
34
+ # just assuming it is a Time for now, may support labels later.
35
+ # the java class really wants rfc822 and not rfc2822, but this works ok anyway.
36
+ from = from_identifier.to_rfc2822
37
+ to = to_identifier.to_rfc2822
38
+
39
+ revisions = java("getRevisions(\"#{from}\";\"#{to}\")", &proc)
40
+ raise "revisions must be of type #{Revisions.name} - was #{revisions.class.name}" unless revisions.is_a?(::RSCM::Revisions)
41
+
42
+ # Just a little sanity check
43
+ if(revisions.latest)
44
+ latetime = revisions.latest.time
45
+ if(latetime < from_identifier || to_identifier < latetime)
46
+ raise "Latest time (#{latetime}) is not within #{from_identifier}-#{to_identifier}"
47
+ end
48
+ end
49
+ revisions
50
+ end
51
+
52
+ def checkout(checkout_dir, to_identifier, &proc)
53
+ # TODO: Take the to_identifier arg into consideration
54
+ files = java("checkout(\"#{checkout_dir}\")", &proc)
55
+ files
56
+ end
57
+
58
+ def supports_trigger?
59
+ true
60
+ end
61
+
62
+ private
63
+
64
+ def cmd
65
+ rscm_jar = File.expand_path(File.dirname(__FILE__) + "../../../../ext/rscm.jar")
66
+ starteam_jars = Dir["#{ENV['RSCM_STARTEAM']}/Lib/*jar"].join(File::PATH_SEPARATOR)
67
+ ant_jars = Dir["#{ENV['ANT_HOME']}/lib/*jar"].join(File::PATH_SEPARATOR)
68
+ classpath = "#{rscm_jar}#{File::PATH_SEPARATOR}#{ant_jars}#{File::PATH_SEPARATOR}#{starteam_jars}"
69
+
70
+ "java -Djava.library.path=\"#{ENV['RSCM_STARTEAM']}#{File::SEPARATOR}Lib\" -classpath \"#{classpath}\" org.rubyforge.rscm.Main"
71
+ end
72
+
73
+ def java(m, &proc)
74
+ raise "The RSCM_STARTEAM environment variable must be defined and point to the StarTeam SDK directory" unless ENV['RSCM_STARTEAM']
75
+ raise "The ANT_HOME environment variable must be defined and point to the Ant installation directory" unless ENV['ANT_HOME']
76
+
77
+ clazz = "org.rubyforge.rscm.starteam.StarTeam"
78
+ ctor_args = "#{@user_name};#{@password};#{@server_name};#{@server_port};#{@project_name};#{@view_name};#{@folder_name}"
79
+
80
+ # Uncomment if you're not Aslak - to run against a bogus java class.
81
+ # clazz = "org.rubyforge.rscm.TestScm"
82
+ # ctor_args = "hubba;bubba"
83
+
84
+ command = "new #{clazz}(#{ctor_args}).#{m}"
85
+ tf = Tempfile.new("rscm_starteam")
86
+ tf.puts(command)
87
+ tf.close
88
+ cmdline = "#{cmd} #{tf.path}"
89
+ IO.popen(cmdline) do |io|
90
+ yaml_source = io
91
+ if(block_given?)
92
+ yaml_source = ""
93
+ io.each_line do |line|
94
+ yield line
95
+ yaml_source << line << "\n"
96
+ end
97
+ else
98
+ end
99
+ YAML::load(yaml_source)
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,397 @@
1
+ require 'rscm/base'
2
+ require 'rscm/path_converter'
3
+ require 'rscm/line_editor'
4
+ require 'rscm/scm/subversion_log_parser'
5
+ require 'stringio'
6
+
7
+ module RSCM
8
+
9
+ # RSCM implementation for Subversion.
10
+ #
11
+ # You need the svn/svnadmin executable on the PATH in order for it to work.
12
+ #
13
+ # NOTE: On Cygwin these have to be the win32 builds of svn/svnadmin and not the Cygwin ones.
14
+ class Subversion < Base
15
+
16
+ include FileUtils
17
+ include PathConverter
18
+
19
+ attr_accessor :url
20
+ # Must be specified in order to create repo and install triggers.
21
+ # Must also be specified as "" if the url represents the root of the repository, or files in revisions will not be detected.
22
+ attr_accessor :path
23
+ attr_accessor :username
24
+ attr_accessor :password
25
+
26
+ def initialize(url="", path="")
27
+ @url, @path = url, path
28
+ @username = ""
29
+ @password = ""
30
+ end
31
+
32
+ def path
33
+ @path ||= ""
34
+ end
35
+
36
+ def installed?
37
+ begin
38
+ svn("--version", {})
39
+ true
40
+ rescue
41
+ false
42
+ end
43
+ end
44
+
45
+ def to_identifier(raw_identifier)
46
+ raw_identifier.to_i
47
+ end
48
+
49
+ def add(relative_filename, options={})
50
+ svn("add #{checkout_dir}/#{relative_filename}", options)
51
+ end
52
+
53
+ def move(relative_src, relative_dest, options={})
54
+ svn("mv #{checkout_dir}/#{relative_src} #{checkout_dir}/#{relative_dest}", options)
55
+ end
56
+
57
+ def transactional?
58
+ true
59
+ end
60
+
61
+ def uptodate?(identifier, options={})
62
+ if(!checked_out?)
63
+ false
64
+ else
65
+ rev = identifier.nil? ? head_revision_identifier(options) : identifier
66
+ local_revision_identifier(options) == rev
67
+ end
68
+ end
69
+
70
+ def commit(message, options={})
71
+ svn(commit_command(message), options)
72
+ # We have to do an update to get the local revision right
73
+ checkout_silent(nil, options)
74
+ end
75
+
76
+ def label
77
+ local_revision_identifier.to_s
78
+ end
79
+
80
+ def diff(path, from, to, options={}, &block)
81
+ cmd = "svn diff --revision #{from}:#{to} \"#{url}/#{path}\""
82
+ execute(cmd, options) do |io|
83
+ return(block.call(io))
84
+ end
85
+ end
86
+
87
+ def open(path, native_revision_identifier, options={}, &block)
88
+ raise "native_revision_identifier cannot be nil" if native_revision_identifier.nil?
89
+ cmd = "svn cat #{url}/#{path}@#{native_revision_identifier}"
90
+ execute(cmd, options) do |io|
91
+ return(block.call(io))
92
+ end
93
+ end
94
+
95
+ def can_create_central?
96
+ local? && !path.nil? && !(path == "")
97
+ end
98
+
99
+ def destroy_central
100
+ if(File.exist?(svnrootdir) && local?)
101
+ FileUtils.rm_rf(svnrootdir)
102
+ else
103
+ raise "Cannot destroy central repository. '#{svnrootdir}' doesn't exist or central repo isn't local to this machine"
104
+ end
105
+ end
106
+
107
+ def central_exists?
108
+ if(local?)
109
+ File.exists?("#{svnrootdir}/db")
110
+ else
111
+ # Do a simple command over the network
112
+ # If the repo/path doesn't exist, we'll get zero output
113
+ # on stdout (and an error msg on std err).
114
+ exists = false
115
+ cmd = "svn log #{url} -r HEAD"
116
+ execute(cmd) do |stdout|
117
+ stdout.each_line do |line|
118
+ exists = true
119
+ end
120
+ end
121
+ exists
122
+ end
123
+ end
124
+
125
+ def supports_trigger?
126
+ true
127
+ # we'll assume it supports trigger even if not local. this is to ensure user interfaces
128
+ # can display appropriate options, even if the object is not 'fully initialised'
129
+ # local?
130
+ end
131
+
132
+ def trigger_mechanism
133
+ "hooks/post-commit"
134
+ end
135
+
136
+ def create_central(options={})
137
+ options = options.dup.merge({:dir => svnrootdir})
138
+ native_path = PathConverter.filepath_to_nativepath(svnrootdir, false)
139
+ mkdir_p(PathConverter.nativepath_to_filepath(native_path))
140
+ svnadmin("create #{native_path}", options)
141
+ if(path != "")
142
+ options = options.dup.merge({:dir => "."})
143
+ # create the directories
144
+ paths = path.split("/")
145
+ paths.each_with_index do |p,i|
146
+ p = paths[0..i]
147
+ u = "#{repourl}/#{p.join('/')}"
148
+ svn("mkdir #{u} -m \"Adding directories\"", options)
149
+ end
150
+ end
151
+ end
152
+
153
+ def install_trigger(trigger_command, trigger_files_checkout_dir, options={})
154
+ if (WINDOWS)
155
+ install_win_trigger(trigger_command, trigger_files_checkout_dir, options)
156
+ else
157
+ install_unix_trigger(trigger_command, trigger_files_checkout_dir, options)
158
+ end
159
+ end
160
+
161
+ def uninstall_trigger(trigger_command, trigger_files_checkout_dir, options={})
162
+ File.comment_out(post_commit_file, /#{Regexp.escape(trigger_command)}/, nil)
163
+ end
164
+
165
+ def trigger_installed?(trigger_command, trigger_files_checkout_dir, options={})
166
+ return false unless File.exist?(post_commit_file)
167
+ not_already_commented = LineEditor.comment_out(File.new(post_commit_file), /#{Regexp.escape(trigger_command)}/, "# ", "")
168
+ not_already_commented
169
+ end
170
+
171
+ def import_central(dir, options={})
172
+ import_cmd = "import #{dir} #{url} -m \"#{options[:message]}\""
173
+ svn(import_cmd, options)
174
+ end
175
+
176
+ def revisions(from_identifier=Time.new.utc, options={})
177
+ raise "from_identifer cannot be nil" if from_identifier.nil?
178
+ options = {
179
+ :from_identifier => from_identifier,
180
+ :to_identifier => Time.infinity,
181
+ :relative_path => "",
182
+ :dir => Dir.pwd
183
+ }.merge(options)
184
+
185
+ checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
186
+ revisions = nil
187
+ command = "svn #{changes_command(options[:from_identifier], options[:to_identifier], options[:relative_path])}"
188
+ execute(command, options) do |stdout|
189
+ stdout = StringIO.new(stdout.read)
190
+ parser = SubversionLogParser.new(stdout, @url, options[:from_identifier], options[:to_identifier], path)
191
+ revisions = parser.parse_revisions
192
+ end
193
+ revisions.cmd = command if store_revisions_command?
194
+ revisions
195
+ end
196
+
197
+ # url pointing to the root of the repo
198
+ def repourl
199
+ last = (path.nil? || path == "") ? -1 : -(path.length)-2
200
+ url[0..last]
201
+ end
202
+
203
+ def checked_out?
204
+ rootentries = File.expand_path("#{checkout_dir}/.svn/entries")
205
+ result = File.exists?(rootentries)
206
+ result
207
+ end
208
+
209
+ protected
210
+
211
+ def checkout_silent(to_identifier, options)
212
+ checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
213
+ mkdir_p(@checkout_dir)
214
+ if(checked_out?)
215
+ svn(update_command(to_identifier), options)
216
+ else
217
+ svn(checkout_command(to_identifier), options)
218
+ end
219
+ end
220
+
221
+ def ignore_paths
222
+ [/\.svn\/.*/]
223
+ end
224
+
225
+ private
226
+
227
+ def local_revision_identifier(options)
228
+ local_revision_identifier = nil
229
+ svn("info #{quoted_checkout_dir}", options) do |line|
230
+ if(line =~ /Revision: ([0-9]*)/)
231
+ return $1.to_i
232
+ end
233
+ end
234
+ end
235
+
236
+ def head_revision_identifier(options)
237
+ # This command only seems to yield any changesets if the url is the root of
238
+ # the repo, which we don't know in the case where path is not specified (likely)
239
+ # We therefore don't specify it and get the latest revision from the full url instead.
240
+ # cmd = "svn log #{login_options} #{repourl} -r HEAD"
241
+ cmd = "svn log #{login_options} #{url}"
242
+ execute(cmd, options) do |stdout|
243
+ parser = SubversionLogParser.new(stdout, @url)
244
+ revisions = parser.parse_revisions
245
+ revisions[0].identifier
246
+ end
247
+ end
248
+
249
+ def install_unix_trigger(trigger_command, damagecontrol_install_dir, options)
250
+ post_commit_exists = File.exists?(post_commit_file)
251
+ mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
252
+ begin
253
+ File.open(post_commit_file, mode) do |file|
254
+ file.puts("#!/bin/sh") unless post_commit_exists
255
+ file.puts("#{trigger_command}\n" )
256
+ end
257
+ File.chmod(0744, post_commit_file)
258
+ rescue
259
+ raise ["Didn't have permission to write to #{post_commit_file}.",
260
+ "Try to manually add the following line:",
261
+ trigger_command,
262
+ "Finally make it executable with chmod g+x #{post_commit_file}"]
263
+ end
264
+ end
265
+
266
+ def install_win_trigger(trigger_command, damagecontrol_install_dir, options)
267
+ post_commit_exists = File.exists?(post_commit_file)
268
+ mode = post_commit_exists ? File::APPEND|File::WRONLY : File::CREAT|File::WRONLY
269
+ begin
270
+ File.open(post_commit_file, mode) do |file|
271
+ file.puts("#{trigger_command}\n" )
272
+ end
273
+ rescue
274
+ raise ["Didn't have permission to write to #{post_commit_file}.",
275
+ "Try to manually add the following line:",
276
+ trigger_command]
277
+ end
278
+ end
279
+
280
+ def svnrootdir
281
+ last = (path.nil? || path == "") ? -1 : -(path.length)-2
282
+ result = url["file://".length..last]
283
+ # for windows, turn /c:/blabla into c:/blabla"
284
+ if(result =~ /^\/[a-zA-Z]:/)
285
+ result = result[1..-1]
286
+ end
287
+ result
288
+ end
289
+
290
+ def svnadmin(cmd, options={}, &proc)
291
+ svncommand("svnadmin", cmd, options, &proc)
292
+ end
293
+
294
+ def svn(cmd, options={}, &proc)
295
+ svncommand("svn", cmd, options, &proc)
296
+ end
297
+
298
+ def svncommand(executable, cmd, options, &proc)
299
+ command_line = "#{executable} #{cmd}"
300
+ execute(command_line, options) do |stdout|
301
+ stdout.each_line do |line|
302
+ yield line if block_given?
303
+ end
304
+ end
305
+ end
306
+
307
+ def checkout_command(to_identifier)
308
+ "checkout #{login_options} #{url} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
309
+ end
310
+
311
+ def update_command(to_identifier)
312
+ "update #{login_options} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
313
+ end
314
+
315
+ def changes_command(from_identifier, to_identifier, relative_path)
316
+ # http://svnbook.red-bean.com/svnbook-1.1/svn-book.html#svn-ch-3-sect-3.3
317
+ # file_list = files.join('\n')
318
+ "log --verbose #{login_options} #{revision_option(from_identifier, to_identifier)} #{@url}"
319
+ end
320
+
321
+ def login_options
322
+ result = ""
323
+ u = @username ? @username.strip : ""
324
+ p = @password ? @password.strip : ""
325
+ result << "--username #{u} " unless u == ""
326
+ result << "--password #{p} " unless p == ""
327
+ result
328
+ end
329
+
330
+ def revision_option(from_identifier, to_identifier)
331
+ # The inclusive start
332
+ from = nil
333
+ if(from_identifier.is_a?(Time))
334
+ from = svndate(from_identifier)
335
+ elsif(from_identifier.is_a?(Numeric))
336
+ from = from_identifier
337
+ elsif(!from_identifier.nil?)
338
+ raise "from_identifier must be Numeric, Time or nil. Was: #{from_identifier} (#{from_identifier.class.name})"
339
+ end
340
+
341
+ to = nil
342
+ if(to_identifier.is_a?(Time))
343
+ to = svndate(to_identifier)
344
+ elsif(to_identifier.is_a?(Numeric))
345
+ to = to_identifier
346
+ elsif(!from_identifier.nil?)
347
+ raise "to_identifier must be Numeric, Time or nil. Was: #{to_identifier} (#{to_identifier.class.name})"
348
+ end
349
+
350
+ revision_option = nil
351
+ if(from && to.nil?)
352
+ revision_option = "--revision #{from}:HEAD"
353
+ elsif(from.nil? && to)
354
+ revision_option = "--revision #{to}"
355
+ elsif(from.nil? && to.nil?)
356
+ revision_option = ""
357
+ elsif(from && to)
358
+ revision_option = "--revision #{from}:#{to}"
359
+ end
360
+ revision_option
361
+ end
362
+
363
+ def svndate(time)
364
+ return nil unless time
365
+ time.utc.strftime("{\"%Y-%m-%d %H:%M:%S\"}")
366
+ end
367
+
368
+ def commit_command(message)
369
+ "commit #{login_options} --force-log -m \"#{message}\" #{quoted_checkout_dir}"
370
+ end
371
+
372
+ def quoted_checkout_dir
373
+ cd = '"' + PathConverter.filepath_to_nativepath(checkout_dir, false) + '"'
374
+ raise "checkout_dir not set" if cd == ""
375
+ cd
376
+ end
377
+
378
+ def local?
379
+ if(url =~ /^file:/)
380
+ return true
381
+ else
382
+ return false
383
+ end
384
+ end
385
+
386
+ def post_commit_file
387
+ # We actualy need to use the .cmd when on cygwin. The cygwin svn post-commit
388
+ # hook is hosed. We'll be relying on native windows
389
+ if(local?)
390
+ WINDOWS ? "#{svnrootdir}/hooks/post-commit.cmd" : "#{svnrootdir}/hooks/post-commit"
391
+ else
392
+ raise "The repository is not local. Cannot install or uninstall trigger."
393
+ end
394
+ end
395
+
396
+ end
397
+ end