rscm 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/CHANGES +12 -0
  2. data/README +14 -0
  3. data/Rakefile +4 -24
  4. data/lib/rscm.rb +1 -2
  5. data/lib/rscm/base.rb +289 -281
  6. data/lib/rscm/command_line.rb +135 -112
  7. data/lib/rscm/revision.rb +63 -166
  8. data/lib/rscm/revision_file.rb +8 -2
  9. data/lib/rscm/revision_poller.rb +78 -67
  10. data/lib/rscm/revisions.rb +79 -0
  11. data/lib/rscm/scm/clearcase.rb +11 -9
  12. data/lib/rscm/scm/cvs.rb +374 -352
  13. data/lib/rscm/scm/cvs_log_parser.rb +1 -0
  14. data/lib/rscm/scm/darcs.rb +9 -0
  15. data/lib/rscm/scm/perforce.rb +216 -149
  16. data/lib/rscm/scm/subversion.rb +44 -24
  17. data/lib/rscm/scm/subversion_log_parser.rb +37 -51
  18. data/lib/rscm/time_ext.rb +0 -1
  19. data/lib/rscm/version.rb +2 -2
  20. data/test/rscm/command_line_test.rb +7 -5
  21. data/test/rscm/compatibility/config.yml +4 -4
  22. data/test/rscm/compatibility/cvs_metaproject/diff.txt +52 -0
  23. data/test/rscm/compatibility/cvs_metaproject/file.txt +48 -0
  24. data/test/rscm/compatibility/cvs_metaproject/old.yml +13 -0
  25. data/test/rscm/compatibility/full.rb +2 -223
  26. data/test/rscm/compatibility/p4_gfx/files_0.yml +10 -0
  27. data/test/rscm/compatibility/p4_gfx/old.yml +26 -0
  28. data/test/rscm/compatibility/p4_gfx/revisions.yml +24 -0
  29. data/test/rscm/compatibility/p4_gfx/scm.yml +4 -0
  30. data/test/rscm/compatibility/rscm_engine.rb +197 -0
  31. data/test/rscm/compatibility/subversion_rscm/diff.txt +12 -0
  32. data/test/rscm/compatibility/subversion_rscm/file.txt +567 -0
  33. data/test/rscm/compatibility/subversion_rscm/old.yml +14 -0
  34. data/test/rscm/compatibility/subversion_rscm/revisions.yml +17 -0
  35. data/test/rscm/compatibility/subversion_rscm/scm.yml +1 -0
  36. data/test/rscm/revision_file_test.rb +10 -0
  37. data/test/rscm/revision_poller_test.rb +91 -0
  38. data/test/rscm/revision_test.rb +22 -117
  39. data/test/rscm/revisions_test.rb +80 -0
  40. data/test/rscm/scm/cvs_log_parser_test.rb +569 -567
  41. data/test/rscm/scm/cvs_test.rb +6 -3
  42. data/test/rscm/scm/darcs_test.rb +4 -7
  43. data/test/rscm/scm/perforce_test.rb +6 -2
  44. data/test/rscm/scm/star_team_test.rb +10 -0
  45. data/test/rscm/scm/subversion_log_parser_test.rb +38 -5
  46. data/test/rscm/scm/subversion_test.rb +2 -3
  47. data/test/rscm/test_helper.rb +41 -2
  48. data/testproject/damagecontrolled/build.xml +154 -154
  49. data/testproject/damagecontrolled/src/java/com/thoughtworks/damagecontrolled/Thingy.java +6 -6
  50. metadata +19 -7
  51. data/lib/rscm/historic_file.rb +0 -30
  52. data/test/rscm/compatibility/damage_control_minimal.rb +0 -104
  53. data/test/rscm/revision_fixture.rb +0 -20
  54. data/test/rscm/revisions.yaml +0 -42
  55. data/test/rscm/scm/star_team.rb +0 -36
@@ -1,113 +1,136 @@
1
- require 'rscm/platform'
2
-
3
- module RSCM
4
- module CommandLine
5
- QUOTE_REPLACEMENT = (Platform.family == "mswin32") ? "\"" : "\\\""
6
- LESS_THAN_REPLACEMENT = (Platform.family == "mswin32") ? "<" : "\\<"
7
- class OptionError < StandardError; end
8
- class ExecutionError < StandardError
9
- attr_reader :cmd, :dir, :exitstatus, :stderr
10
- def initialize(cmd, dir, exitstatus, stderr); @cmd, @dir, @exitstatus, @stderr = cmd, dir, exitstatus, stderr; end
11
- def to_s
12
- "\ndir : #{@dir}\n" +
13
- "command : #{@cmd}\n" +
14
- "exitstatus: #{@exitstatus}\n" +
15
- "STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n"
16
- end
17
- end
18
-
19
- # Executes +cmd+.
20
- # If the +:stdout+ and +:stderr+ options are specified, a line consisting
21
- # of a prompt (including +cmd+) will be appended to the respective output streams will be appended
22
- # to those files, followed by the output itself. Example:
23
- #
24
- # CommandLine.execute("echo hello world", {:stdout => "stdout.log", :stderr => "stderr.log"})
25
- #
26
- # will result in the following being written to stdout.log:
27
- #
28
- # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
29
- # hello world
30
- #
31
- # -and to stderr.log:
32
- # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
33
- #
34
- # If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
35
- # will not be written to the stdout file (even if it's specified):
36
- #
37
- # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
38
- # [output captured and therefore not logged]
39
- #
40
- # If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
41
- # (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
42
- # (provided +:stderr+ was specified)
43
- #
44
- # You can also specify the +:dir+ option, which will cause the command to be executed in that directory
45
- # (default is current directory).
46
- #
47
- # You can also specify a hash of environment variables in +:env+, which will add additional environment variables
48
- # to the default environment.
49
- #
50
- # Finally, you can specify several commands within one by separating them with '&&' (as you would in a shell).
51
- # This will result in several lines to be appended to the log (as if you had executed the commands separately).
52
- #
53
- # See the unit test for more examples.
54
- def execute(cmd, options={}, &proc)
55
- raise "Can't have newline in cmd" if cmd =~ /\n/
56
- options = {
57
- :dir => Dir.pwd,
58
- :env => {},
59
- :exitstatus => 0
60
- }.merge(options)
61
-
62
- options[:stdout] = File.expand_path(options[:stdout]) if options[:stdout]
63
- options[:stderr] = File.expand_path(options[:stderr]) if options[:stderr]
64
-
65
- commands = cmd.split("&&").collect{|c| c.strip}
66
- Dir.chdir(options[:dir]) do
67
- stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""
68
- stderr_opt = options[:stderr] ? "2>> #{options[:stderr]}" : ""
69
- capture_info_command = (block_given? && options[:stdout])? "echo [output captured and therefore not logged] >> #{options[:stdout]} && " : ""
70
-
71
- full_cmd = commands.collect do |c|
72
- escaped_command = c.gsub(/"/, QUOTE_REPLACEMENT).gsub(/</, LESS_THAN_REPLACEMENT)
73
- stdout_prompt_command = options[:stdout] ? "echo #{RSCM::Platform.prompt} #{escaped_command} >> #{options[:stdout]} && " : ""
74
- stderr_prompt_command = options[:stderr] ? "echo #{RSCM::Platform.prompt} #{escaped_command} >> #{options[:stderr]} && " : ""
75
- redirected_command = block_given? ? "#{c} #{stderr_opt}" : "#{c} #{stdout_opt} #{stderr_opt}"
76
-
77
- stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
78
- end.join(" && ")
79
-
80
- options[:env].each{|k,v| ENV[k]=v}
81
- begin
82
- IO.popen(full_cmd) do |io|
83
- if(block_given?)
84
- return(proc.call(io))
85
- else
86
- io.read
87
- end
88
- end
89
- rescue Errno::ENOENT => e
90
- File.open(options[:stderr], "a") {|io| io.write(e.message)}
91
- raise ExecutionError.new(cmd, options[:dir], nil, e.message)
92
- ensure
93
- if($?.exitstatus != options[:exitstatus])
94
- error_message = "#{options[:stderr]} doesn't exist"
95
- if options[:stderr] && File.exist?(options[:stderr])
96
- File.open(options[:stderr]) do |errio|
97
- begin
98
- errio.seek(-1200, IO::SEEK_END)
99
- rescue Errno::EINVAL
100
- # ignore - it just means we didn't have 400 bytes.
101
- end
102
- error_message = errio.read
103
- end
104
- end
105
- raise ExecutionError.new(cmd, options[:dir], $?.exitstatus, error_message)
106
- end
107
- end
108
- end
109
- $?.exitstatus
110
- end
111
- module_function :execute
112
- end
1
+ require 'rscm/platform'
2
+
3
+ module RSCM
4
+ module CommandLine
5
+ QUOTE_REPLACEMENT = (Platform.family == "mswin32") ? "\"" : "\\\""
6
+ LESS_THAN_REPLACEMENT = (Platform.family == "mswin32") ? "<" : "\\<"
7
+ class OptionError < StandardError; end
8
+ class ExecutionError < StandardError
9
+ attr_reader :cmd, :dir, :exitstatus, :stderr
10
+ def initialize(cmd, full_cmd, dir, exitstatus, stderr)
11
+ @cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
12
+ end
13
+ def to_s
14
+ "\ndir : #{@dir}\n" +
15
+ "command : #{@cmd}\n" +
16
+ "executed command : #{@full_cmd}\n" +
17
+ "exitstatus: #{@exitstatus}\n" +
18
+ "STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n"
19
+ end
20
+ end
21
+
22
+ # Executes +cmd+.
23
+ # If the +:stdout+ and +:stderr+ options are specified, a line consisting
24
+ # of a prompt (including +cmd+) will be appended to the respective output streams will be appended
25
+ # to those files, followed by the output itself. Example:
26
+ #
27
+ # CommandLine.execute("echo hello world", {:stdout => "stdout.log", :stderr => "stderr.log"})
28
+ #
29
+ # will result in the following being written to stdout.log:
30
+ #
31
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
32
+ # hello world
33
+ #
34
+ # -and to stderr.log:
35
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
36
+ #
37
+ # If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
38
+ # will not be written to the stdout file (even if it's specified):
39
+ #
40
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
41
+ # [output captured and therefore not logged]
42
+ #
43
+ # If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
44
+ # (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
45
+ # (provided +:stderr+ was specified)
46
+ #
47
+ # You can also specify the +:dir+ option, which will cause the command to be executed in that directory
48
+ # (default is current directory).
49
+ #
50
+ # You can also specify a hash of environment variables in +:env+, which will add additional environment variables
51
+ # to the default environment.
52
+ #
53
+ # Finally, you can specify several commands within one by separating them with '&&' (as you would in a shell).
54
+ # This will result in several lines to be appended to the log (as if you had executed the commands separately).
55
+ #
56
+ # See the unit test for more examples.
57
+ def execute(cmd, options={}, &proc)
58
+ raise "Can't have newline in cmd" if cmd =~ /\n/
59
+ options = {
60
+ :dir => Dir.pwd,
61
+ :env => {},
62
+ :mode => 'r',
63
+ :exitstatus => 0
64
+ }.merge(options)
65
+
66
+ options[:stdout] = File.expand_path(options[:stdout]) if options[:stdout]
67
+ options[:stderr] = File.expand_path(options[:stderr]) if options[:stderr]
68
+
69
+ commands = cmd.split("&&").collect{|c| c.strip}
70
+ if options[:dir].nil?
71
+ e(cmd, commands, options, &proc)
72
+ else
73
+ Dir.chdir(options[:dir]) do
74
+ e(cmd, commands, options, &proc)
75
+ end
76
+ end
77
+ end
78
+ module_function :execute
79
+
80
+ private
81
+
82
+ def e(cmd, commands, options, &proc)
83
+ stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""
84
+ stderr_opt = options[:stderr] ? "2>> #{options[:stderr]}" : ""
85
+ capture_info_command = (block_given? && options[:stdout])? "echo [output captured and therefore not logged] >> #{options[:stdout]} && " : ""
86
+
87
+ full_cmd = commands.collect do |c|
88
+ escaped_command = c.gsub(/"/, QUOTE_REPLACEMENT).gsub(/</, LESS_THAN_REPLACEMENT)
89
+ stdout_prompt_command = options[:stdout] ? "echo #{RSCM::Platform.prompt} #{escaped_command} >> #{options[:stdout]} && " : ""
90
+ stderr_prompt_command = options[:stderr] ? "echo #{RSCM::Platform.prompt} #{escaped_command} >> #{options[:stderr]} && " : ""
91
+ redirected_command = block_given? ? "#{c} #{stderr_opt}" : "#{c} #{stdout_opt} #{stderr_opt}"
92
+
93
+ stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
94
+ end.join(" && ")
95
+
96
+ options[:env].each{|k,v| ENV[k]=v}
97
+ begin
98
+ STDOUT.puts "#{RSCM::Platform.prompt} #{cmd}" if options[:stdout].nil?
99
+ IO.popen(full_cmd, options[:mode]) do |io|
100
+ if(block_given?)
101
+ return(proc.call(io))
102
+ else
103
+ io.each_line do |line|
104
+ STDOUT.puts line if options[:stdout].nil?
105
+ end
106
+ end
107
+ end
108
+
109
+ if($?.exitstatus != options[:exitstatus])
110
+ error_message = "#{options[:stderr]} doesn't exist"
111
+ if options[:stderr] && File.exist?(options[:stderr])
112
+ File.open(options[:stderr]) do |errio|
113
+ begin
114
+ errio.seek(-1200, IO::SEEK_END)
115
+ rescue Errno::EINVAL
116
+ # ignore - it just means we didn't have 400 bytes.
117
+ end
118
+ error_message = errio.read
119
+ end
120
+ end
121
+ raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, $?.exitstatus, error_message)
122
+ end
123
+ rescue Errno::ENOENT => e
124
+ unless options[:stderr].nil?
125
+ File.open(options[:stderr], "a") {|io| io.write(e.message)}
126
+ else
127
+ STDERR.puts e.message
128
+ STDERR.puts e.backtrace.join("\n")
129
+ end
130
+ raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, nil, e.message)
131
+ end
132
+ end
133
+ module_function :e
134
+
135
+ end
113
136
  end
@@ -1,206 +1,103 @@
1
1
  require 'rscm/time_ext'
2
2
  require 'rscm/revision_file'
3
+ require 'yaml'
3
4
 
4
5
  module RSCM
5
-
6
- # A collection of Revision.
7
- class Revisions
6
+ # Represents a collection of RevisionFile that were committed at the
7
+ # same time, or "more or less at the same time" for non-atomic
8
+ # SCMs (such as CVS and StarTeam). See Revisions for how to emulate
9
+ # atomicity for non-atomic SCMs.
10
+ class Revision
8
11
  include Enumerable
9
12
 
10
- attr_accessor :revisions
11
-
12
- def initialize(revisions=[])
13
- @revisions = revisions
14
- end
13
+ attr_writer :identifier
14
+ attr_accessor :developer
15
+ attr_accessor :message
15
16
 
16
- # Accepts a visitor that will receive callbacks while
17
- # iterating over this instance's internal structure.
18
- # The visitor should respond to the following methods:
19
- #
20
- # * visit_revisions(revisions)
21
- # * visit_revision(revision)
22
- # * visit_file(file)
23
- #
24
- def accept(visitor)
25
- visitor.visit_revisions(self)
26
- self.each{|revision| revision.accept(visitor)}
27
- end
28
-
29
- def [](file)
30
- @revisions[file]
17
+ def initialize(identifier=nil, time=nil)
18
+ @identifier = identifier
19
+ @time = time
20
+ @files = []
31
21
  end
32
22
 
33
- def each(&block)
34
- @revisions.each(&block)
23
+ def add(file)
24
+ raise "Can't add #{file} to this revision" unless accept? file
25
+ @files << file
26
+ self.developer = file.developer if file.developer
27
+ self.message = file.message if file.message
35
28
  end
36
29
 
37
- def reverse
38
- r = clone
39
- r.revisions = @revisions.dup.reverse
40
- r
30
+ def identifier(min_or_max = :max)
31
+ @identifier || time(min_or_max)
41
32
  end
42
33
 
43
- def length
44
- @revisions.length
34
+ # The time of this revision. Depending on the value of +min_or_max+,
35
+ # (should be :min or :max), returns the min or max time of this
36
+ # revision. (min or max only matters for non-transactional scms)
37
+ def time(min_or_max = :max)
38
+ @time || self.collect{|file| file.time}.__send__(min_or_max)
45
39
  end
46
40
 
47
- def ==(other)
48
- return false if !other.is_a?(self.class)
49
- @revisions == other.revisions
50
- end
51
-
52
- def empty?
53
- @revisions.empty?
54
- end
55
-
56
- # The set of developers that contributed to all of the contained Revision s.
57
- def developers
58
- result = []
59
- each do |revision|
60
- result << revision.developer unless result.index(revision.developer)
61
- end
62
- result
63
- end
64
-
65
- def first
66
- @revisions.first
67
- end
68
-
69
- def last
70
- @revisions.last
41
+ # Sets the time for this revision. Should only be used by atomic SCMs.
42
+ # Non-atomic SCMs should <b>not</b> invoke this method, but instead create
43
+ # revisions by adding RscmFile objects to a Revisions object.
44
+ def time=(t)
45
+ raise "time must be a Time object - it was a #{t.class.name} with the string value #{t}" unless t.is_a?(Time)
46
+ raise "can't set time to an inferiour value than the previous value" if @time && (t < @time)
47
+ @time = t
71
48
  end
72
49
 
73
- # The latest Revision (with the latest time)
74
- # or nil if there are none.
75
- def latest
76
- result = nil
77
- each do |revision|
78
- result = revision if result.nil? || result.time < revision.time
79
- end
80
- result
81
- end
50
+ # Whether +file+ can be added to this instance.
51
+ def accept?(file) #:nodoc:
52
+ return true if empty? || @time
82
53
 
83
- # Adds a File or a Revision.
84
- # If the argument is a File and no corresponding Revision exists,
85
- # a new Revision is created, added, and the File is added to that Revision -
86
- # and then finally the newly created Revision is returned.
87
- # Otherwise nil is returned.
88
- def add(file_or_revision)
89
- if(file_or_revision.is_a?(Revision))
90
- @revisions << file_or_revision
91
- return file_or_revision
92
- else
93
- revision = @revisions.find { |a_revision| a_revision.can_contain?(file_or_revision) }
94
- if(revision.nil?)
95
- revision = Revision.new
96
- @revisions << revision
97
- revision << file_or_revision
98
- return revision
99
- end
100
- revision << file_or_revision
101
- return nil
102
- end
103
- end
104
-
105
- def push(*file_or_revisions)
106
- file_or_revisions.each { |file_or_revision| self << (file_or_revision) }
107
- self
108
- end
54
+ close_enough_to_min = (time(:min) - file.time).abs <= 60
55
+ close_enough_to_max = (time(:max) - file.time).abs <= 60
56
+ close_enough = close_enough_to_min or close_enough_to_max
109
57
 
110
- # Sorts the revisions according to time
111
- def sort!
112
- @revisions.sort!
113
- self
58
+ close_enough and
59
+ self.developer == file.developer and
60
+ self.message == file.message
114
61
  end
115
62
 
116
- end
117
-
118
- # Represents a collection of File that were committed at the same time.
119
- # Non-transactional SCMs (such as CVS and StarTeam) emulate Revision
120
- # by grouping File s that were committed by the same developer, with the
121
- # same commit message, and within a "reasonably" small timespan.
122
- class Revision
123
- include Enumerable
124
-
125
- attr_reader :files
126
- attr_accessor :identifier
127
- attr_accessor :developer
128
- attr_accessor :message
129
- attr_accessor :time
130
-
131
- def initialize(files=[])
132
- @files = files
133
- end
134
-
135
- def accept(visitor)
136
- visitor.visit_revision(self)
137
- @files.each{|file| file.accept(visitor)}
63
+ def ==(other)
64
+ self.to_s == other.to_s
138
65
  end
139
66
 
140
- def << (file)
141
- @files << file
142
- if(self.time.nil? || self.time < file.time unless file.time.nil?)
143
- self.time = file.time
144
- self.identifier = self.time if(self.identifier.nil? || self.identifier.is_a?(Time))
67
+ # String representation that can be used for debugging.
68
+ def to_s
69
+ if(@to_s.nil?)
70
+ min = time(:min)
71
+ max = time(:max)
72
+ t = (min==max) ? min : "#{min}-#{max}"
73
+ @to_s = "#{identifier} | #{developer} | #{t} | #{message}\n"
74
+ self.each do |file|
75
+ @to_s << " " << file.to_s << "\n"
76
+ end
77
+ @to_s
145
78
  end
146
- self.developer = file.developer if file.developer
147
- self.message = file.message if file.message
148
- end
149
-
150
- def [] (index)
151
- @files[index]
79
+ @to_s
152
80
  end
153
81
 
154
- # Iterates over all the RevisionFile objects
155
82
  def each(&block)
156
83
  @files.each(&block)
157
84
  end
158
-
159
- def pop
160
- @files.pop
85
+
86
+ def [](n)
87
+ @files[n]
161
88
  end
162
-
89
+
163
90
  def length
164
91
  @files.length
165
92
  end
166
- alias :size :length
167
-
168
- def time=(t)
169
- raise "time must be a Time object - it was a #{t.class.name} with the string value #{t}" unless t.is_a?(Time)
170
- raise "can't set time to an inferiour value than the previous value" if @time && (t < @time)
171
- @time = t
172
- end
173
-
174
- def ==(other)
175
- other.is_a?(self.class) &&
176
- @developer == other.developer &&
177
- @identifier == other.identifier &&
178
- @message == other.message &&
179
- @time == other.time &&
180
- @files == other.files
181
- end
182
93
 
183
- def <=>(other)
184
- @time <=> other.time
94
+ def pop
95
+ @files.pop
185
96
  end
186
97
 
187
- # Whether this instance can contain a File. Used
188
- # by non-transactional SCMs.
189
- def can_contain?(file) #:nodoc:
190
- self.developer == file.developer &&
191
- self.message == file.message &&
192
- (self.time - file.time).abs < 60
98
+ def empty?
99
+ @files.empty?
193
100
  end
194
101
 
195
- # String representation that can be used for debugging.
196
- def to_s
197
- result = "#{identifier} | #{developer} | #{time} | #{message}\n"
198
- self.each do |file|
199
- result << " " << file.to_s << "\n"
200
- end
201
- result
202
- end
203
-
204
102
  end
205
-
206
103
  end