rscm 0.1.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/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
|