whistle 0.1

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 (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