rscm 0.4.5 → 0.5.0

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