rscm 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +198 -0
- data/Rakefile +118 -0
- data/ext/rscm.jar +0 -0
- data/lib/rscm.rb +10 -0
- data/lib/rscm/abstract_log_parser.rb +49 -0
- data/lib/rscm/abstract_scm.rb +229 -0
- data/lib/rscm/changes.rb +271 -0
- data/lib/rscm/cvs/cvs.rb +363 -0
- data/lib/rscm/cvs/cvs_log_parser.rb +161 -0
- data/lib/rscm/darcs/darcs.rb +69 -0
- data/lib/rscm/line_editor.rb +46 -0
- data/lib/rscm/logging.rb +5 -0
- data/lib/rscm/monotone/monotone.rb +107 -0
- data/lib/rscm/mooky/mooky.rb +13 -0
- data/lib/rscm/parser.rb +39 -0
- data/lib/rscm/path_converter.rb +92 -0
- data/lib/rscm/perforce/perforce.rb +415 -0
- data/lib/rscm/starteam/starteam.rb +99 -0
- data/lib/rscm/svn/svn.rb +337 -0
- data/lib/rscm/svn/svn_log_parser.rb +134 -0
- data/lib/rscm/time_ext.rb +125 -0
- data/test/rscm/apply_label_scm_tests.rb +26 -0
- data/test/rscm/changes_fixture.rb +20 -0
- data/test/rscm/changes_test.rb +129 -0
- data/test/rscm/cvs/cvs_log_parser_test.rb +575 -0
- data/test/rscm/cvs/cvs_test.rb +22 -0
- data/test/rscm/darcs/darcs_test.rb +14 -0
- data/test/rscm/difftool_test.rb +40 -0
- data/test/rscm/file_ext.rb +12 -0
- data/test/rscm/generic_scm_tests.rb +282 -0
- data/test/rscm/line_editor_test.rb +76 -0
- data/test/rscm/mockit.rb +130 -0
- data/test/rscm/mockit_test.rb +117 -0
- data/test/rscm/monotone/monotone_test.rb +19 -0
- data/test/rscm/mooky/mooky_test.rb +14 -0
- data/test/rscm/parser_test.rb +47 -0
- data/test/rscm/path_converter_test.rb +52 -0
- data/test/rscm/perforce/perforce_test.rb +14 -0
- data/test/rscm/starteam/starteam_test.rb +36 -0
- data/test/rscm/svn/svn_log_parser_test.rb +111 -0
- data/test/rscm/svn/svn_test.rb +28 -0
- data/test/rscm/tempdir.rb +12 -0
- metadata +81 -0
data/lib/rscm/changes.rb
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'xmlrpc/utils'
|
2
|
+
require 'rscm/time_ext'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
|
6
|
+
# TODO: add a hook to get committers from a separate class - to support registered pairs
|
7
|
+
# We'll be able to do lots of cool analysis with visitors later -> graphs. mmmmm.
|
8
|
+
|
9
|
+
# A collection of changesets.
|
10
|
+
class ChangeSets
|
11
|
+
include Enumerable
|
12
|
+
include XMLRPC::Marshallable
|
13
|
+
|
14
|
+
attr_reader :changesets
|
15
|
+
|
16
|
+
def initialize(changesets=[])
|
17
|
+
@changesets = changesets
|
18
|
+
end
|
19
|
+
|
20
|
+
# Accepts a visitor that will receive callbacks while
|
21
|
+
# iterating over this instance's internal structure.
|
22
|
+
# The visitor should respond to the following methods:
|
23
|
+
#
|
24
|
+
# * visit_changesets(changesets)
|
25
|
+
# * visit_changeset(changeset)
|
26
|
+
# * visit_change(change)
|
27
|
+
#
|
28
|
+
def accept(visitor)
|
29
|
+
visitor.visit_changesets(self)
|
30
|
+
self.each{|changeset| changeset.accept(visitor)}
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](change)
|
34
|
+
@changesets[change]
|
35
|
+
end
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
@changesets.each(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def reverse
|
42
|
+
ChangeSets.new(@changesets.dup.reverse)
|
43
|
+
end
|
44
|
+
|
45
|
+
def length
|
46
|
+
@changesets.length
|
47
|
+
end
|
48
|
+
|
49
|
+
def ==(other)
|
50
|
+
return false if !other.is_a?(self.class)
|
51
|
+
@changesets == other.changesets
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty?
|
55
|
+
@changesets.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
# The set of developers that contributed to all of the contained ChangeSet s.
|
59
|
+
def developers
|
60
|
+
result = []
|
61
|
+
each do |changeset|
|
62
|
+
result << changeset.developer unless result.index(changeset.developer)
|
63
|
+
end
|
64
|
+
result
|
65
|
+
end
|
66
|
+
|
67
|
+
# The latest ChangeSet (with the latest time)
|
68
|
+
# or nil if there are none.
|
69
|
+
def latest
|
70
|
+
result = nil
|
71
|
+
each do |changeset|
|
72
|
+
result = changeset if result.nil? || result.time < changeset.time
|
73
|
+
end
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds a Change or a ChangeSet.
|
78
|
+
# If the argument is a Change and no corresponding ChangeSet exists,
|
79
|
+
# a new ChangeSet is created, added, and the Change is added to that ChangeSet -
|
80
|
+
# and then finally the newly created ChangeSet is returned.
|
81
|
+
# Otherwise nil is returned.
|
82
|
+
def add(change_or_changeset)
|
83
|
+
if(change_or_changeset.is_a?(ChangeSet))
|
84
|
+
@changesets << change_or_changeset
|
85
|
+
return change_or_changeset
|
86
|
+
else
|
87
|
+
changeset = @changesets.find { |a_changeset| a_changeset.can_contain?(change_or_changeset) }
|
88
|
+
if(changeset.nil?)
|
89
|
+
changeset = ChangeSet.new
|
90
|
+
@changesets << changeset
|
91
|
+
changeset << change_or_changeset
|
92
|
+
return changeset
|
93
|
+
end
|
94
|
+
changeset << change_or_changeset
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def push(*change_or_changesets)
|
100
|
+
change_or_changesets.each { |change_or_changeset| self << (change_or_changeset) }
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
# The most recent time of all the ChangeSet s.
|
105
|
+
def time
|
106
|
+
time = nil
|
107
|
+
changesets.each do |changeset|
|
108
|
+
time = changeset.time if @time.nil? || @time < changeset.time
|
109
|
+
end
|
110
|
+
time
|
111
|
+
end
|
112
|
+
|
113
|
+
# Sorts the changesets according to time
|
114
|
+
def sort!
|
115
|
+
@changesets.sort!
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
# Represents a collection of Change that were committed at the same time.
|
122
|
+
# Non-transactional SCMs (such as CVS and StarTeam) emulate ChangeSet
|
123
|
+
# by grouping Change s that were committed by the same developer, with the
|
124
|
+
# same commit message, and within a "reasonably" small timespan.
|
125
|
+
class ChangeSet
|
126
|
+
include Enumerable
|
127
|
+
include XMLRPC::Marshallable
|
128
|
+
|
129
|
+
attr_reader :changes
|
130
|
+
attr_accessor :revision
|
131
|
+
attr_accessor :developer
|
132
|
+
attr_accessor :message
|
133
|
+
attr_accessor :time
|
134
|
+
|
135
|
+
def initialize(changes=[])
|
136
|
+
@changes = changes
|
137
|
+
end
|
138
|
+
|
139
|
+
def accept(visitor)
|
140
|
+
visitor.visit_changeset(self)
|
141
|
+
@changes.each{|change| change.accept(visitor)}
|
142
|
+
end
|
143
|
+
|
144
|
+
def << (change)
|
145
|
+
@changes << change
|
146
|
+
self.time = change.time if self.time.nil? || self.time < change.time unless change.time.nil?
|
147
|
+
self.developer = change.developer if change.developer
|
148
|
+
self.message = change.message if change.message
|
149
|
+
end
|
150
|
+
|
151
|
+
def [] (change)
|
152
|
+
@changes[change]
|
153
|
+
end
|
154
|
+
|
155
|
+
def each(&block)
|
156
|
+
@changes.each(&block)
|
157
|
+
end
|
158
|
+
|
159
|
+
def length
|
160
|
+
@changes.length
|
161
|
+
end
|
162
|
+
|
163
|
+
def time=(t)
|
164
|
+
raise "time must be a Time object - it was a #{t.class.name} with the string value #{t}" unless t.is_a?(Time)
|
165
|
+
raise "can't set time to an inferiour value than the previous value" if @time && (t < @time)
|
166
|
+
@time = t
|
167
|
+
end
|
168
|
+
|
169
|
+
def ==(other)
|
170
|
+
return false if !other.is_a?(self.class)
|
171
|
+
@changes == other.changes
|
172
|
+
end
|
173
|
+
|
174
|
+
def <=>(other)
|
175
|
+
@time <=> other.time
|
176
|
+
end
|
177
|
+
|
178
|
+
# Whether this instance can contain a Change. Used
|
179
|
+
# by non-transactional SCMs.
|
180
|
+
def can_contain?(change)
|
181
|
+
self.developer == change.developer &&
|
182
|
+
self.message == change.message &&
|
183
|
+
(self.time - change.time).abs < 60
|
184
|
+
end
|
185
|
+
|
186
|
+
# String representation that can be used for debugging.
|
187
|
+
def to_s
|
188
|
+
result = "#{revision} | #{developer} | #{time} | #{message}\n"
|
189
|
+
self.each do |change|
|
190
|
+
result << " " << change.to_s << "\n"
|
191
|
+
end
|
192
|
+
result
|
193
|
+
end
|
194
|
+
|
195
|
+
# Returns the identifier of the changeset. This is the revision
|
196
|
+
# (if defined) or an UTC time if revision is undefined.
|
197
|
+
def identifier
|
198
|
+
@revision || @time
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
# Represents a change to an individual file.
|
204
|
+
class Change
|
205
|
+
include XMLRPC::Marshallable
|
206
|
+
|
207
|
+
MODIFIED = "MODIFIED"
|
208
|
+
DELETED = "DELETED"
|
209
|
+
ADDED = "ADDED"
|
210
|
+
MOVED = "MOVED"
|
211
|
+
|
212
|
+
attr_accessor :status
|
213
|
+
attr_accessor :path
|
214
|
+
attr_accessor :previous_revision
|
215
|
+
attr_accessor :revision
|
216
|
+
|
217
|
+
# TODO: Remove redundant attributes that are in ChangeSet
|
218
|
+
attr_accessor :developer
|
219
|
+
attr_accessor :message
|
220
|
+
# This is a UTC ruby time
|
221
|
+
attr_accessor :time
|
222
|
+
|
223
|
+
def initialize(path=nil, developer=nil, message=nil, revision=nil, time=nil, status=DELETED)
|
224
|
+
@path, @developer, @message, @revision, @time, @status = path, developer, message, revision, time, status
|
225
|
+
end
|
226
|
+
|
227
|
+
def accept(visitor)
|
228
|
+
visitor.visit_change(self)
|
229
|
+
end
|
230
|
+
|
231
|
+
def to_s
|
232
|
+
"#{path} | #{revision}"
|
233
|
+
end
|
234
|
+
|
235
|
+
def developer=(developer)
|
236
|
+
raise "can't be null" if developer.nil?
|
237
|
+
@developer = developer
|
238
|
+
end
|
239
|
+
|
240
|
+
def message=(message)
|
241
|
+
raise "can't be null" if message.nil?
|
242
|
+
@message = message
|
243
|
+
end
|
244
|
+
|
245
|
+
def path=(path)
|
246
|
+
raise "can't be null" if path.nil?
|
247
|
+
@path = path
|
248
|
+
end
|
249
|
+
|
250
|
+
def revision=(revision)
|
251
|
+
raise "can't be null" if revision.nil?
|
252
|
+
@revision = revision
|
253
|
+
end
|
254
|
+
|
255
|
+
def time=(time)
|
256
|
+
raise "time must be a Time object" unless time.is_a?(Time)
|
257
|
+
@time = time
|
258
|
+
end
|
259
|
+
|
260
|
+
def ==(other)
|
261
|
+
return false if !other.is_a?(self.class)
|
262
|
+
self.path == other.path &&
|
263
|
+
self.developer == other.developer &&
|
264
|
+
self.message == other.message &&
|
265
|
+
self.revision == other.revision &&
|
266
|
+
self.time == other.time
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
data/lib/rscm/cvs/cvs.rb
ADDED
@@ -0,0 +1,363 @@
|
|
1
|
+
require 'rscm/abstract_scm'
|
2
|
+
require 'rscm/path_converter'
|
3
|
+
require 'rscm/line_editor'
|
4
|
+
require 'rscm/cvs/cvs_log_parser'
|
5
|
+
|
6
|
+
module RSCM
|
7
|
+
|
8
|
+
# RSCM implementation for CVS.
|
9
|
+
#
|
10
|
+
# You need the cvs executable on the PATH in order for it to work.
|
11
|
+
#
|
12
|
+
# NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
|
13
|
+
class CVS < AbstractSCM
|
14
|
+
|
15
|
+
public
|
16
|
+
attr_accessor :root
|
17
|
+
attr_accessor :mod
|
18
|
+
attr_accessor :branch
|
19
|
+
attr_accessor :password
|
20
|
+
|
21
|
+
def initialize(root=nil, mod=nil, branch=nil, password=nil)
|
22
|
+
@root, @mod, @branch, @password = root, mod, branch, password
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
"CVS"
|
27
|
+
end
|
28
|
+
|
29
|
+
def import(dir, message)
|
30
|
+
modname = File.basename(dir)
|
31
|
+
cvs(dir, "import -m \"#{message}\" #{modname} VENDOR START")
|
32
|
+
end
|
33
|
+
|
34
|
+
def add(checkout_dir, relative_filename)
|
35
|
+
cvs(checkout_dir, "add #{relative_filename}")
|
36
|
+
end
|
37
|
+
|
38
|
+
# The extra simulate parameter is not in accordance with the AbstractSCM API,
|
39
|
+
# but it's optional and is only being used from within this class (uptodate? method).
|
40
|
+
def checkout(checkout_dir, to_identifier=nil, simulate=false)
|
41
|
+
checked_out_files = []
|
42
|
+
if(checked_out?(checkout_dir))
|
43
|
+
path_regex = /^[U|P] (.*)/
|
44
|
+
cvs(checkout_dir, update_command(to_identifier), simulate) do |line|
|
45
|
+
if(line =~ path_regex)
|
46
|
+
path = $1.chomp
|
47
|
+
yield path if block_given?
|
48
|
+
checked_out_files << path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
prefix = File.basename(checkout_dir)
|
53
|
+
path_regex = /^[U|P] #{prefix}\/(.*)/
|
54
|
+
# This is a workaround for the fact that -d . doesn't work - must be an existing sub folder.
|
55
|
+
mkdir_p(checkout_dir) unless File.exist?(checkout_dir)
|
56
|
+
target_dir = File.basename(checkout_dir)
|
57
|
+
run_checkout_command_dir = File.dirname(checkout_dir)
|
58
|
+
# -D is sticky, but subsequent updates will reset stickiness with -A
|
59
|
+
cvs(run_checkout_command_dir, checkout_command(target_dir, to_identifier), simulate) do |line|
|
60
|
+
if(line =~ path_regex)
|
61
|
+
path = $1.chomp
|
62
|
+
yield path if block_given?
|
63
|
+
checked_out_files << path
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
checked_out_files
|
68
|
+
end
|
69
|
+
|
70
|
+
def checkout_commandline(to_identifier=Time.infinity)
|
71
|
+
"cvs checkout #{branch_option} #{revision_option(to_identifier)} #{mod}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_commandline
|
75
|
+
"cvs update #{branch_option} -d -P -A"
|
76
|
+
end
|
77
|
+
|
78
|
+
def commit(checkout_dir, message, &proc)
|
79
|
+
cvs(checkout_dir, commit_command(message), &proc)
|
80
|
+
end
|
81
|
+
|
82
|
+
def uptodate?(checkout_dir, since)
|
83
|
+
if(!checked_out?(checkout_dir))
|
84
|
+
return false
|
85
|
+
end
|
86
|
+
|
87
|
+
# simulate a checkout
|
88
|
+
files = checkout(checkout_dir, nil, true)
|
89
|
+
files.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity, files=nil)
|
93
|
+
checkout(checkout_dir) unless uptodate?(checkout_dir, nil) # must checkout to get changesets
|
94
|
+
begin
|
95
|
+
parse_log(checkout_dir, new_changes_command(from_identifier, to_identifier, files))
|
96
|
+
rescue => e
|
97
|
+
parse_log(checkout_dir, old_changes_command(from_identifier, to_identifier, files))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def diff(checkout_dir, change)
|
102
|
+
with_working_dir(checkout_dir) do
|
103
|
+
opts = case change.status
|
104
|
+
when /#{Change::MODIFIED}/; "#{revision_option(change.previous_revision)} #{revision_option(change.revision)}"
|
105
|
+
when /#{Change::DELETED}/; "#{revision_option(change.previous_revision)}"
|
106
|
+
when /#{Change::ADDED}/; "#{revision_option(Time.epoch)} #{revision_option(change.revision)}"
|
107
|
+
end
|
108
|
+
# IPORTANT! CVS NT has a bug in the -N diff option
|
109
|
+
# http://www.cvsnt.org/pipermail/cvsnt-bugs/2004-November/000786.html
|
110
|
+
cmd = command_line("diff -Nu #{opts} #{change.path}")
|
111
|
+
safer_popen(cmd, "r", 1) do |io|
|
112
|
+
return(yield(io))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def apply_label(checkout_dir, label)
|
118
|
+
cvs(checkout_dir, "tag -c #{label}")
|
119
|
+
end
|
120
|
+
|
121
|
+
def install_trigger(trigger_command, trigger_files_checkout_dir)
|
122
|
+
raise "mod can't be null or empty" if (mod.nil? || mod == "")
|
123
|
+
|
124
|
+
root_cvs = create_root_cvs
|
125
|
+
root_cvs.checkout(trigger_files_checkout_dir)
|
126
|
+
with_working_dir(trigger_files_checkout_dir) do
|
127
|
+
trigger_line = "#{mod} #{trigger_command}\n"
|
128
|
+
File.open("loginfo", File::WRONLY | File::APPEND) do |file|
|
129
|
+
file.puts(trigger_line)
|
130
|
+
end
|
131
|
+
begin
|
132
|
+
commit(trigger_files_checkout_dir, "Installed trigger for CVS module '#{mod}'")
|
133
|
+
rescue
|
134
|
+
raise "Couldn't commit the trigger back to CVS. Try to manually check out CVSROOT/loginfo, " +
|
135
|
+
"add the following line and commit it back:\n\n#{trigger_line}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def trigger_installed?(trigger_command, trigger_files_checkout_dir)
|
141
|
+
loginfo_line = "#{mod} #{trigger_command}"
|
142
|
+
regex = Regexp.new(Regexp.escape(loginfo_line))
|
143
|
+
|
144
|
+
root_cvs = create_root_cvs
|
145
|
+
begin
|
146
|
+
root_cvs.checkout(trigger_files_checkout_dir)
|
147
|
+
loginfo = File.join(trigger_files_checkout_dir, "loginfo")
|
148
|
+
return false if !File.exist?(loginfo)
|
149
|
+
|
150
|
+
# returns true if commented out. doesn't modify the file.
|
151
|
+
in_local_copy = LineEditor.comment_out(File.new(loginfo), regex, "# ", "")
|
152
|
+
# Also verify that loginfo has been committed back to the repo
|
153
|
+
entries = File.join(trigger_files_checkout_dir, "CVS", "Entries")
|
154
|
+
committed = File.mtime(entries) >= File.mtime(loginfo)
|
155
|
+
|
156
|
+
in_local_copy && committed
|
157
|
+
rescue Exception => e
|
158
|
+
$stderr.puts(e.message)
|
159
|
+
$stderr.puts(e.backtrace.join("\n"))
|
160
|
+
false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def uninstall_trigger(trigger_command, trigger_files_checkout_dir)
|
165
|
+
loginfo_line = "#{mod} #{trigger_command}"
|
166
|
+
regex = Regexp.new(Regexp.escape(loginfo_line))
|
167
|
+
|
168
|
+
root_cvs = create_root_cvs
|
169
|
+
root_cvs.checkout(trigger_files_checkout_dir)
|
170
|
+
loginfo_path = File.join(trigger_files_checkout_dir, "loginfo")
|
171
|
+
File.comment_out(loginfo_path, regex, "# ")
|
172
|
+
with_working_dir(trigger_files_checkout_dir) do
|
173
|
+
commit(trigger_files_checkout_dir, "Uninstalled trigger for CVS mod '#{mod}'")
|
174
|
+
end
|
175
|
+
raise "Couldn't uninstall/commit trigger to loginfo" if trigger_installed?(trigger_command, trigger_files_checkout_dir)
|
176
|
+
end
|
177
|
+
|
178
|
+
def create
|
179
|
+
raise "Can't create CVS repository for #{root}" unless can_create?
|
180
|
+
File.mkpath(path)
|
181
|
+
cvs(path, "init")
|
182
|
+
end
|
183
|
+
|
184
|
+
def can_create?
|
185
|
+
begin
|
186
|
+
local?
|
187
|
+
rescue
|
188
|
+
false
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def supports_trigger?
|
193
|
+
true
|
194
|
+
end
|
195
|
+
|
196
|
+
def exists?
|
197
|
+
if(local?)
|
198
|
+
File.exists?("#{path}/CVSROOT/loginfo")
|
199
|
+
else
|
200
|
+
# don't know. assume yes.
|
201
|
+
true
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def checked_out?(checkout_dir)
|
206
|
+
rootcvs = File.expand_path("#{checkout_dir}/CVS/Root")
|
207
|
+
File.exists?(rootcvs)
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def cvs(dir, cmd, simulate=false)
|
213
|
+
dir = PathConverter.nativepath_to_filepath(dir)
|
214
|
+
dir = File.expand_path(dir)
|
215
|
+
execed_command_line = command_line(cmd, password, simulate)
|
216
|
+
with_working_dir(dir) do
|
217
|
+
safer_popen(execed_command_line) do |stdout|
|
218
|
+
stdout.each_line do |progress|
|
219
|
+
yield progress if block_given?
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def parse_log(checkout_dir, cmd, &proc)
|
226
|
+
logged_command_line = command_line(cmd, hidden_password)
|
227
|
+
yield logged_command_line if block_given?
|
228
|
+
|
229
|
+
execed_command_line = command_line(cmd, password)
|
230
|
+
changesets = nil
|
231
|
+
with_working_dir(checkout_dir) do
|
232
|
+
safer_popen(execed_command_line) do |stdout|
|
233
|
+
parser = CVSLogParser.new(stdout)
|
234
|
+
parser.cvspath = path
|
235
|
+
parser.cvsmodule = mod
|
236
|
+
changesets = parser.parse_changesets
|
237
|
+
end
|
238
|
+
end
|
239
|
+
changesets
|
240
|
+
end
|
241
|
+
|
242
|
+
def new_changes_command(from_identifier, to_identifier, files)
|
243
|
+
# https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_16.html#SEC144
|
244
|
+
# -N => Suppress the header if no revisions are selected.
|
245
|
+
# -S => Do not print the list of tags for this file.
|
246
|
+
"log #{branch_option} -N -S #{period_option(from_identifier, to_identifier)}"
|
247
|
+
end
|
248
|
+
|
249
|
+
def branch_specified?
|
250
|
+
branch && branch.strip != ""
|
251
|
+
end
|
252
|
+
|
253
|
+
def branch_option
|
254
|
+
branch_specified? ? "-r#{branch}" : ""
|
255
|
+
end
|
256
|
+
|
257
|
+
def update_command(to_identifier)
|
258
|
+
"update #{branch_option} -d -P -A #{revision_option(to_identifier)}"
|
259
|
+
end
|
260
|
+
|
261
|
+
def checkout_command(target_dir, to_identifier)
|
262
|
+
"checkout #{branch_option} -d #{target_dir} #{mod} #{revision_option(to_identifier)}"
|
263
|
+
end
|
264
|
+
|
265
|
+
def old_changes_command(from_identifier, to_identifier, files)
|
266
|
+
# Many servers don't support the new -S option
|
267
|
+
"log #{branch_option} -N #{period_option(from_identifier, to_identifier)}"
|
268
|
+
end
|
269
|
+
|
270
|
+
def hidden_password
|
271
|
+
if(password && password != "")
|
272
|
+
"********"
|
273
|
+
else
|
274
|
+
""
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def period_option(from_identifier, to_identifier)
|
279
|
+
if(from_identifier.nil? && to_identifier.nil?)
|
280
|
+
""
|
281
|
+
else
|
282
|
+
"-d\"#{cvsdate(from_identifier)}<=#{cvsdate(to_identifier)}\" "
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def cvsdate(time)
|
287
|
+
return "" unless time
|
288
|
+
# CVS wants all dates as UTC.
|
289
|
+
time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
290
|
+
end
|
291
|
+
|
292
|
+
def root_with_password(password)
|
293
|
+
result = nil
|
294
|
+
if local?
|
295
|
+
result = root
|
296
|
+
elsif password && password != ""
|
297
|
+
protocol, user, host, path = parse_cvs_root
|
298
|
+
result = ":#{protocol}:#{user}:#{password}@#{host}:#{path}"
|
299
|
+
else
|
300
|
+
result = root
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def command_line(cmd, password=nil, simulate=false)
|
305
|
+
cvs_options = simulate ? "-n" : ""
|
306
|
+
"cvs \"-d#{root_with_password(password)}\" #{cvs_options} -q #{cmd}"
|
307
|
+
end
|
308
|
+
|
309
|
+
def create_root_cvs
|
310
|
+
CVS.new(self.root, "CVSROOT", nil, self.password)
|
311
|
+
end
|
312
|
+
|
313
|
+
def revision_option(identifier)
|
314
|
+
option = nil
|
315
|
+
if(identifier.is_a?(Time))
|
316
|
+
option = "-D\"#{cvsdate(identifier)}\""
|
317
|
+
elsif(identifier.is_a?(String))
|
318
|
+
option = "-r#{identifier}"
|
319
|
+
else
|
320
|
+
""
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def commit_command(message)
|
325
|
+
"commit -m \"#{message}\""
|
326
|
+
end
|
327
|
+
|
328
|
+
def local?
|
329
|
+
protocol == "local"
|
330
|
+
end
|
331
|
+
|
332
|
+
def path
|
333
|
+
parse_cvs_root[3]
|
334
|
+
end
|
335
|
+
|
336
|
+
def protocol
|
337
|
+
parse_cvs_root[0]
|
338
|
+
end
|
339
|
+
|
340
|
+
# parses the root into tokens
|
341
|
+
# [protocol, user, host, path]
|
342
|
+
#
|
343
|
+
def parse_cvs_root
|
344
|
+
md = case
|
345
|
+
when root =~ /^:local:/ then /^:(local):(.*)/.match(root)
|
346
|
+
when root =~ /^:ext:/ then /^:(ext):(.*)@(.*):(.*)/.match(root)
|
347
|
+
when root =~ /^:pserver:/ then /^:(pserver):(.*)@(.*):(.*)/.match(root)
|
348
|
+
end
|
349
|
+
result = case
|
350
|
+
when root =~ /^:local:/ then [md[1], nil, nil, md[2]]
|
351
|
+
when root =~ /^:ext:/ then md[1..4]
|
352
|
+
when root =~ /^:pserver:/ then md[1..4]
|
353
|
+
else ["local", nil, nil, root]
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
# Convenience factory method used in testing
|
359
|
+
def CVS.local(cvsroot_dir, mod)
|
360
|
+
cvsroot_dir = PathConverter.filepath_to_nativepath(cvsroot_dir, true)
|
361
|
+
CVS.new(":local:#{cvsroot_dir}", mod)
|
362
|
+
end
|
363
|
+
end
|