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.
- data/CHANGES +12 -0
- data/README +14 -0
- data/Rakefile +4 -24
- data/lib/rscm.rb +1 -2
- data/lib/rscm/base.rb +289 -281
- data/lib/rscm/command_line.rb +135 -112
- data/lib/rscm/revision.rb +63 -166
- data/lib/rscm/revision_file.rb +8 -2
- data/lib/rscm/revision_poller.rb +78 -67
- data/lib/rscm/revisions.rb +79 -0
- data/lib/rscm/scm/clearcase.rb +11 -9
- data/lib/rscm/scm/cvs.rb +374 -352
- data/lib/rscm/scm/cvs_log_parser.rb +1 -0
- data/lib/rscm/scm/darcs.rb +9 -0
- data/lib/rscm/scm/perforce.rb +216 -149
- data/lib/rscm/scm/subversion.rb +44 -24
- data/lib/rscm/scm/subversion_log_parser.rb +37 -51
- data/lib/rscm/time_ext.rb +0 -1
- data/lib/rscm/version.rb +2 -2
- data/test/rscm/command_line_test.rb +7 -5
- data/test/rscm/compatibility/config.yml +4 -4
- data/test/rscm/compatibility/cvs_metaproject/diff.txt +52 -0
- data/test/rscm/compatibility/cvs_metaproject/file.txt +48 -0
- data/test/rscm/compatibility/cvs_metaproject/old.yml +13 -0
- data/test/rscm/compatibility/full.rb +2 -223
- data/test/rscm/compatibility/p4_gfx/files_0.yml +10 -0
- data/test/rscm/compatibility/p4_gfx/old.yml +26 -0
- data/test/rscm/compatibility/p4_gfx/revisions.yml +24 -0
- data/test/rscm/compatibility/p4_gfx/scm.yml +4 -0
- data/test/rscm/compatibility/rscm_engine.rb +197 -0
- data/test/rscm/compatibility/subversion_rscm/diff.txt +12 -0
- data/test/rscm/compatibility/subversion_rscm/file.txt +567 -0
- data/test/rscm/compatibility/subversion_rscm/old.yml +14 -0
- data/test/rscm/compatibility/subversion_rscm/revisions.yml +17 -0
- data/test/rscm/compatibility/subversion_rscm/scm.yml +1 -0
- data/test/rscm/revision_file_test.rb +10 -0
- data/test/rscm/revision_poller_test.rb +91 -0
- data/test/rscm/revision_test.rb +22 -117
- data/test/rscm/revisions_test.rb +80 -0
- data/test/rscm/scm/cvs_log_parser_test.rb +569 -567
- data/test/rscm/scm/cvs_test.rb +6 -3
- data/test/rscm/scm/darcs_test.rb +4 -7
- data/test/rscm/scm/perforce_test.rb +6 -2
- data/test/rscm/scm/star_team_test.rb +10 -0
- data/test/rscm/scm/subversion_log_parser_test.rb +38 -5
- data/test/rscm/scm/subversion_test.rb +2 -3
- data/test/rscm/test_helper.rb +41 -2
- data/testproject/damagecontrolled/build.xml +154 -154
- data/testproject/damagecontrolled/src/java/com/thoughtworks/damagecontrolled/Thingy.java +6 -6
- metadata +19 -7
- data/lib/rscm/historic_file.rb +0 -30
- data/test/rscm/compatibility/damage_control_minimal.rb +0 -104
- data/test/rscm/revision_fixture.rb +0 -20
- data/test/rscm/revisions.yaml +0 -42
- data/test/rscm/scm/star_team.rb +0 -36
data/lib/rscm/command_line.rb
CHANGED
@@ -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,
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
"
|
15
|
-
"
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# (
|
46
|
-
#
|
47
|
-
# You can also specify
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
data/lib/rscm/revision.rb
CHANGED
@@ -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
|
-
#
|
7
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@revisions = revisions
|
14
|
-
end
|
13
|
+
attr_writer :identifier
|
14
|
+
attr_accessor :developer
|
15
|
+
attr_accessor :message
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
34
|
-
|
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
|
38
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
@
|
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
|
-
#
|
74
|
-
|
75
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
self
|
58
|
+
close_enough and
|
59
|
+
self.developer == file.developer and
|
60
|
+
self.message == file.message
|
114
61
|
end
|
115
62
|
|
116
|
-
|
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
|
-
|
141
|
-
|
142
|
-
if(
|
143
|
-
|
144
|
-
|
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
|
-
|
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
|
160
|
-
@files
|
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
|
184
|
-
@
|
94
|
+
def pop
|
95
|
+
@files.pop
|
185
96
|
end
|
186
97
|
|
187
|
-
|
188
|
-
|
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
|