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/revision_file.rb
CHANGED
@@ -47,12 +47,18 @@ module RSCM
|
|
47
47
|
# Returns/yields an IO containing the contents of this file, using the +scm+ this
|
48
48
|
# file lives in.
|
49
49
|
def open(scm, options={}, &block) #:yield: io
|
50
|
-
scm.open(
|
50
|
+
scm.open(path, native_revision_identifier, options, &block)
|
51
51
|
end
|
52
52
|
|
53
53
|
# Yields the diff as an IO for this file
|
54
54
|
def diff(scm, options={}, &block)
|
55
|
-
|
55
|
+
from_to = case status
|
56
|
+
when /#{RevisionFile::MODIFIED}/; [previous_native_revision_identifier, native_revision_identifier]
|
57
|
+
when /#{RevisionFile::DELETED}/; [previous_native_revision_identifier, nil]
|
58
|
+
when /#{RevisionFile::ADDED}/; [nil, native_revision_identifier]
|
59
|
+
end
|
60
|
+
|
61
|
+
scm.diff(path, from_to[0], from_to[1], options, &block)
|
56
62
|
end
|
57
63
|
|
58
64
|
# Accepts a visitor that must respond to +visit_file(revision_file)+
|
data/lib/rscm/revision_poller.rb
CHANGED
@@ -1,82 +1,93 @@
|
|
1
1
|
module RSCM
|
2
|
-
|
2
|
+
module RevisionPoller
|
3
3
|
attr_accessor :logger
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
# This is the number of revisions we'll try to stick to for each
|
6
|
+
# call to revisions.
|
7
|
+
CRITICAL_REVISION_SIZE = 100
|
8
|
+
BASE_INCREMENT = 60*60 # 1 hour
|
9
|
+
TWENTY_FOUR_HOURS = 24*60*60
|
9
10
|
|
10
|
-
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
11
|
+
# Polls revisions from +point+ and either backwards in time until the beginning
|
12
|
+
# of time (Time.epoch) or forward in time until we're past +now+.
|
13
|
+
#
|
14
|
+
# Whether to poll forwards or backwards in time depends on the value of +direction+.
|
15
|
+
#
|
16
|
+
# The +point+ argument can be either a Revision, String, Time or Fixnum representing
|
17
|
+
# where to start from (upper boundary for backwards polling, lower boundary for
|
18
|
+
# forwards polling).
|
19
|
+
#
|
20
|
+
# The polling starts with a small interval from +point+ (1 hour) and increments (or decrements)
|
21
|
+
# gradually in order to try and keep the length of the yielded Revisions to about 100.
|
22
|
+
#
|
23
|
+
# The passed block will be called several times, each time with a Revisions object.
|
24
|
+
# In order to reduce the memory footprint and keep the performance decent, the length
|
25
|
+
# of each yielded Revisions object will usually be within the order of magnitude of 100.
|
26
|
+
#
|
27
|
+
# TODO: handle non-transactional SCMs. There was some handling of this in older revisions
|
28
|
+
# of this file. We should dig it out and reenable it.
|
29
|
+
def poll(point=nil, direction=:backwards, multiplier=1, now=Time.now.utc, options={}, &proc)
|
30
|
+
raise "A block of arity 1 must be called" if proc.nil?
|
31
|
+
backwards = direction == :backwards
|
32
|
+
point ||= now
|
33
|
+
|
34
|
+
if point.respond_to?(:time)
|
35
|
+
point_time = backwards ? point.time(:min) : point.time(:max)
|
36
|
+
point_identifier = backwards ? point.identifier(:min) : point.identifier(:max)
|
37
|
+
elsif point.is_a?(Time)
|
38
|
+
point_time = point
|
39
|
+
point_identifier = point
|
32
40
|
else
|
33
|
-
|
34
|
-
|
35
|
-
return []
|
36
|
-
else
|
37
|
-
logger.info "Latest revision is not known. Checking for revisions since: #{from}" if logger
|
38
|
-
end
|
41
|
+
point_time = now
|
42
|
+
point_identifier = point
|
39
43
|
end
|
40
44
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
logger.info "Last revision still not found, checking since #{double_seconds_before_now.ago}" if logger
|
49
|
-
new_opts = options.dup
|
50
|
-
new_opts[:seconds_before_now] = double_seconds_before_now
|
51
|
-
return poll_new_revisions(new_opts)
|
45
|
+
increment = multiplier * BASE_INCREMENT
|
46
|
+
if backwards
|
47
|
+
to = point_identifier
|
48
|
+
begin
|
49
|
+
from = point_time - increment
|
50
|
+
rescue ArgumentError
|
51
|
+
from = Time.epoch
|
52
52
|
end
|
53
|
+
from = Time.epoch if from < Time.epoch
|
53
54
|
else
|
54
|
-
|
55
|
+
from = point_identifier
|
56
|
+
begin
|
57
|
+
to = point_time + increment
|
58
|
+
rescue RangeError
|
59
|
+
raise "RSCM will not work this far in the future (#{from} plus #{increment})"
|
60
|
+
end
|
55
61
|
end
|
56
62
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
logger.info "Commit still in progress." if logger
|
75
|
-
end
|
76
|
-
end
|
77
|
-
logger.info "Quiet period elapsed" if logger
|
63
|
+
options = options.merge({:to_identifier => to})
|
64
|
+
|
65
|
+
revs = revisions(from, options)
|
66
|
+
raise "Got nil revision for from=#{from.inspect}" if revs.nil?
|
67
|
+
revs.sort!
|
68
|
+
proc.call(revs)
|
69
|
+
|
70
|
+
if from == Time.epoch
|
71
|
+
return
|
72
|
+
end
|
73
|
+
if !backwards and to.is_a?(Time) and (to) > now + TWENTY_FOUR_HOURS
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
if(revs.length < CRITICAL_REVISION_SIZE)
|
78
|
+
# We can do more
|
79
|
+
multiplier *= 2
|
78
80
|
end
|
79
|
-
|
81
|
+
if(revs.length > 2*CRITICAL_REVISION_SIZE)
|
82
|
+
# We must do less
|
83
|
+
multiplier /= 2
|
84
|
+
end
|
85
|
+
|
86
|
+
unless(revs.empty?)
|
87
|
+
point = backwards ? revs[0] : revs[-1]
|
88
|
+
end
|
89
|
+
poll(point, direction, multiplier, now, options, &proc)
|
80
90
|
end
|
91
|
+
|
81
92
|
end
|
82
93
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rscm/time_ext'
|
2
|
+
require 'rscm/revision_file'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
|
6
|
+
# A Revisions object is a collection of Revision objects with some
|
7
|
+
# additional behaviour.
|
8
|
+
#
|
9
|
+
# Most importantly, it provides logic to group individual RevisionFile
|
10
|
+
# objects into Revision objects internally. This means that implementors
|
11
|
+
# of RSCM adapters that don't support atomic changesets can still emulate
|
12
|
+
# them, simply by adding RevisionFile objects to a Revisions object. Example:
|
13
|
+
#
|
14
|
+
# revisions = Revisions.new
|
15
|
+
# revisions.add revision_file_1
|
16
|
+
# revisions.add revision_file_2
|
17
|
+
# revisions.add revision_file_3
|
18
|
+
#
|
19
|
+
# The added RevisionFile objects will end up in Revision objects grouped by
|
20
|
+
# their comment, developer and timestamp. A set of RevisionFile object with
|
21
|
+
# identical developer and message will end up in the same Revision provided
|
22
|
+
# their <tt>time</tt> attributes are a minute apart or less.
|
23
|
+
#
|
24
|
+
# Each Revisions object also has an attribute <tt>cmd</tt> which should contain
|
25
|
+
# the command used to retrieve the revision data and populate it. This is useful
|
26
|
+
# for debugging an RSCM adapter that might behaving incorrectly. Keep in mind that
|
27
|
+
# it is the responsibility of each RSCM adapter implementation to set this attribute,
|
28
|
+
# and that it should omit setting it if the <tt>store_revisions_command</tt> is
|
29
|
+
# <tt>true</tt>
|
30
|
+
class Revisions
|
31
|
+
include Enumerable
|
32
|
+
attr_accessor :cmd
|
33
|
+
|
34
|
+
def initialize(revisions=[])
|
35
|
+
@revisions = revisions
|
36
|
+
end
|
37
|
+
|
38
|
+
def add(file_or_revision)
|
39
|
+
if(file_or_revision.is_a?(Revision))
|
40
|
+
@revisions << file_or_revision
|
41
|
+
else
|
42
|
+
revision = find { |a_revision| a_revision.accept?(file_or_revision) }
|
43
|
+
if(revision.nil?)
|
44
|
+
revision = Revision.new
|
45
|
+
@revisions << revision
|
46
|
+
end
|
47
|
+
revision.add file_or_revision
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def sort!
|
52
|
+
@revisions.sort!{|r1,r2| r1.time<=>r2.time}
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
@revisions.collect{|revision| revision.to_s}.join("\n-----------")
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
self.to_s == other.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
def each(&block)
|
64
|
+
@revisions.each(&block)
|
65
|
+
end
|
66
|
+
|
67
|
+
def [](n)
|
68
|
+
@revisions[n]
|
69
|
+
end
|
70
|
+
|
71
|
+
def length
|
72
|
+
@revisions.length
|
73
|
+
end
|
74
|
+
|
75
|
+
def empty?
|
76
|
+
@revisions.empty?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/rscm/scm/clearcase.rb
CHANGED
@@ -6,15 +6,17 @@ require 'tempfile'
|
|
6
6
|
module RSCM
|
7
7
|
class ClearCase < Base
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
unless defined? LOG_FORMAT
|
10
|
+
LOG_FORMAT = "- !ruby/object:RSCM::RevisionFile\\n developer: %u\\n time: \\\"%Nd\\\"\\n native_revision_identifier: %Vn\\n previous_native_revision_identifier: %PVn\\n path: %En\\n status: %o\\n message: \\\"%Nc\\\"\\n\\n"
|
11
|
+
TIME_FORMAT = "%d-%b-%Y.%H:%M:%S"
|
12
|
+
MAGIC_TOKEN = "9q8w7e6r5t4y"
|
13
|
+
STATUSES = {
|
14
|
+
"checkin" => RevisionFile::MODIFIED,
|
15
|
+
"mkelem" => RevisionFile::ADDED,
|
16
|
+
"rmelem" => RevisionFile::DELETED,
|
17
|
+
}
|
18
|
+
DEFAULT_CONFIG_SPEC = "element * CHECKEDOUT\nelement * /main/LATEST"
|
19
|
+
end
|
18
20
|
|
19
21
|
attr_accessor :stream, :stgloc, :tag, :config_spec
|
20
22
|
|
data/lib/rscm/scm/cvs.rb
CHANGED
@@ -1,352 +1,374 @@
|
|
1
|
-
require 'stringio'
|
2
|
-
require 'rscm/base'
|
3
|
-
require 'rscm/path_converter'
|
4
|
-
require 'rscm/line_editor'
|
5
|
-
require 'rscm/scm/cvs_log_parser'
|
6
|
-
|
7
|
-
module RSCM
|
8
|
-
|
9
|
-
# RSCM implementation for CVS.
|
10
|
-
#
|
11
|
-
# You need a cvs executable on the PATH in order for it to work.
|
12
|
-
#
|
13
|
-
# NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
|
14
|
-
class Cvs < Base
|
15
|
-
attr_accessor :root
|
16
|
-
attr_accessor :mod
|
17
|
-
attr_accessor :branch
|
18
|
-
attr_accessor :password
|
19
|
-
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
cvs("
|
38
|
-
end
|
39
|
-
|
40
|
-
def
|
41
|
-
cvs(
|
42
|
-
end
|
43
|
-
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
cmd = "
|
85
|
-
execute(cmd, options) do |io|
|
86
|
-
block.call io
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
if
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
end
|
269
|
-
|
270
|
-
def
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
end
|
283
|
-
|
284
|
-
def
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
"
|
321
|
-
end
|
322
|
-
|
323
|
-
def
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
1
|
+
require 'stringio'
|
2
|
+
require 'rscm/base'
|
3
|
+
require 'rscm/path_converter'
|
4
|
+
require 'rscm/line_editor'
|
5
|
+
require 'rscm/scm/cvs_log_parser'
|
6
|
+
|
7
|
+
module RSCM
|
8
|
+
|
9
|
+
# RSCM implementation for CVS.
|
10
|
+
#
|
11
|
+
# You need a cvs executable on the PATH in order for it to work.
|
12
|
+
#
|
13
|
+
# NOTE: On Cygwin this has to be the win32 build of cvs and not the Cygwin one.
|
14
|
+
class Cvs < Base
|
15
|
+
attr_accessor :root
|
16
|
+
attr_accessor :mod
|
17
|
+
attr_accessor :branch
|
18
|
+
attr_accessor :password
|
19
|
+
|
20
|
+
def installed?
|
21
|
+
begin
|
22
|
+
cvs("--version", {})
|
23
|
+
true
|
24
|
+
rescue
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(root=nil, mod=nil, branch=nil, password=nil)
|
30
|
+
@root, @mod, @branch, @password = root, mod, branch, password
|
31
|
+
end
|
32
|
+
|
33
|
+
def import_central(dir, options={})
|
34
|
+
modname = File.basename(dir)
|
35
|
+
FileUtils.mkdir_p(@checkout_dir) unless File.exist?(@checkout_dir)
|
36
|
+
options = options.dup.merge :dir => dir
|
37
|
+
cvs("import -m \"#{options[:message]}\" #{modname} VENDOR START", options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add(relative_filename, options={})
|
41
|
+
cvs("add #{relative_filename}", options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def move(relative_src, relative_dest, options={})
|
45
|
+
FileUtils.mv(@checkout_dir + '/' + relative_src, @checkout_dir + '/' + relative_dest, :force=>true)
|
46
|
+
cvs("rm #{relative_src}", options)
|
47
|
+
# This will fail if the directories are new. More advanced support for adding can be added if needed.
|
48
|
+
cvs("add #{relative_dest}", options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def commit(message, options={})
|
52
|
+
cvs(commit_command(message), options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def uptodate?(identifier, options={})
|
56
|
+
if(!checked_out?)
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
|
60
|
+
checkout_silent(identifier, options.dup.merge({:simulate => true})) do |io|
|
61
|
+
path_regex = /^[U|P|C] (.*)/
|
62
|
+
io.each_line do |line|
|
63
|
+
return false if(line =~ path_regex)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
return true
|
67
|
+
end
|
68
|
+
|
69
|
+
def revisions(from_identifier=Time.new.utc, options={})
|
70
|
+
raise "from_identifer cannot be nil" if from_identifier.nil?
|
71
|
+
options = {
|
72
|
+
:from_identifier => from_identifier,
|
73
|
+
:to_identifier => Time.infinity,
|
74
|
+
:relative_path => nil
|
75
|
+
}.merge(options)
|
76
|
+
checkout(options[:to_identifier], options) unless checked_out? # must checkout to get revisions
|
77
|
+
parse_log(changes_command(options), options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def diff(path, from, to, options={}, &block)
|
81
|
+
# IMPORTANT! CVS NT has a bug in the -N diff option
|
82
|
+
# http://www.cvsnt.org/pipermail/cvsnt-bugs/2004-November/000786.html
|
83
|
+
from ||= Time.epoch
|
84
|
+
cmd = command_line("diff -Nu #{revision_option(from)} #{revision_option(to)} #{path}")
|
85
|
+
execute(cmd, options.dup.merge({:exitstatus => 1})) do |io|
|
86
|
+
block.call io
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def open(path, native_revision_identifier, options={}, &block)
|
91
|
+
raise "native_revision_identifier cannot be nil" if native_revision_identifier.nil?
|
92
|
+
cmd = "cvs -Q update -p -r #{native_revision_identifier} #{path}"
|
93
|
+
execute(cmd, options) do |io|
|
94
|
+
block.call io
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply_label(label)
|
99
|
+
cvs("tag -c #{label}")
|
100
|
+
end
|
101
|
+
|
102
|
+
def trigger_mechanism
|
103
|
+
"CVSROOT/loginfo"
|
104
|
+
end
|
105
|
+
|
106
|
+
def trigger_installed?(trigger_command, trigger_files_checkout_dir, options={})
|
107
|
+
trigger_command = fix_trigger_command(trigger_command)
|
108
|
+
loginfo_line = "#{mod} #{trigger_command}"
|
109
|
+
regex = Regexp.new(Regexp.escape(loginfo_line))
|
110
|
+
|
111
|
+
root_cvs = create_root_cvs(trigger_files_checkout_dir)
|
112
|
+
begin
|
113
|
+
root_cvs.checkout(nil, options)
|
114
|
+
loginfo = File.join(trigger_files_checkout_dir, "loginfo")
|
115
|
+
return false if !File.exist?(loginfo)
|
116
|
+
|
117
|
+
# returns true if commented out. doesn't modify the file.
|
118
|
+
in_local_copy = LineEditor.comment_out(File.new(loginfo), regex, "# ", "")
|
119
|
+
# Also verify that loginfo has been committed back to the repo
|
120
|
+
entries = File.join(trigger_files_checkout_dir, "CVS", "Entries")
|
121
|
+
committed = File.mtime(entries) >= File.mtime(loginfo)
|
122
|
+
|
123
|
+
in_local_copy && committed
|
124
|
+
rescue Exception => e
|
125
|
+
$stderr.puts(e.message)
|
126
|
+
$stderr.puts(e.backtrace.join("\n"))
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def install_trigger(trigger_command, trigger_files_checkout_dir, options={})
|
132
|
+
raise "mod can't be null or empty" if (mod.nil? || mod == "")
|
133
|
+
trigger_command = fix_trigger_command(trigger_command)
|
134
|
+
|
135
|
+
root_cvs = create_root_cvs(trigger_files_checkout_dir)
|
136
|
+
root_cvs.checkout(nil, options)
|
137
|
+
Dir.chdir(trigger_files_checkout_dir) do
|
138
|
+
trigger_line = "#{mod} #{trigger_command}\n"
|
139
|
+
File.open("loginfo", File::WRONLY | File::APPEND) do |file|
|
140
|
+
file.puts(trigger_line)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
begin
|
145
|
+
root_cvs.commit("Installed trigger for CVS module '#{mod}'", options)
|
146
|
+
rescue Errno::EACCES
|
147
|
+
raise ["Didn't have permission to commit CVSROOT/loginfo.",
|
148
|
+
"Try to manually add the following line:",
|
149
|
+
trigger_command,
|
150
|
+
"Finally make commit the file to the repository"].join("\n")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def uninstall_trigger(trigger_command, trigger_files_checkout_dir, options={})
|
155
|
+
trigger_command = fix_trigger_command(trigger_command)
|
156
|
+
loginfo_line = "#{mod} #{trigger_command}"
|
157
|
+
regex = Regexp.new(Regexp.escape(loginfo_line))
|
158
|
+
|
159
|
+
root_cvs = create_root_cvs(trigger_files_checkout_dir)
|
160
|
+
root_cvs.checkout nil, options
|
161
|
+
loginfo_path = File.join(trigger_files_checkout_dir, "loginfo")
|
162
|
+
File.comment_out(loginfo_path, regex, "# ")
|
163
|
+
root_cvs.commit("Uninstalled trigger for CVS mod '#{mod}'", options)
|
164
|
+
raise "Couldn't uninstall/commit trigger to loginfo" if trigger_installed?(trigger_command, trigger_files_checkout_dir, options)
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_central(options={})
|
168
|
+
options = options.dup.merge({:dir => path})
|
169
|
+
raise "Can't create central CVS repository for #{root}" unless can_create_central?
|
170
|
+
File.mkpath(path)
|
171
|
+
cvs("init", options)
|
172
|
+
end
|
173
|
+
|
174
|
+
def destroy_central
|
175
|
+
if(File.exist?(path) && local?)
|
176
|
+
FileUtils.rm_rf(path)
|
177
|
+
else
|
178
|
+
raise "Cannot destroy central repository. '#{path}' doesn't exist or central repo isn't local to this machine"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def central_exists?
|
183
|
+
if(local?)
|
184
|
+
File.exists?("#{path}/CVSROOT/loginfo")
|
185
|
+
else
|
186
|
+
# don't know. assume yes.
|
187
|
+
true
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def can_create_central?
|
192
|
+
begin
|
193
|
+
local?
|
194
|
+
rescue
|
195
|
+
false
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def supports_trigger?
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
def checked_out?
|
204
|
+
rootcvs = File.expand_path("#{checkout_dir}/CVS/Root")
|
205
|
+
File.exists?(rootcvs)
|
206
|
+
end
|
207
|
+
|
208
|
+
protected
|
209
|
+
|
210
|
+
def cmd_dir
|
211
|
+
@checkout_dir
|
212
|
+
end
|
213
|
+
|
214
|
+
def checkout_silent(to_identifier, options={}, &proc)
|
215
|
+
to_identifier = nil if to_identifier == Time.infinity
|
216
|
+
if(checked_out?)
|
217
|
+
options = options.dup.merge({
|
218
|
+
:dir => @checkout_dir
|
219
|
+
})
|
220
|
+
cvs(update_command(to_identifier), options, &proc)
|
221
|
+
else
|
222
|
+
# This is a workaround for the fact that -d . doesn't work - must be an existing sub folder.
|
223
|
+
FileUtils.mkdir_p(@checkout_dir) unless File.exist?(@checkout_dir)
|
224
|
+
target_dir = File.basename(@checkout_dir)
|
225
|
+
# -D is sticky, but subsequent updates will reset stickiness with -A
|
226
|
+
options = options.dup.merge({
|
227
|
+
:dir => File.dirname(@checkout_dir)
|
228
|
+
})
|
229
|
+
cvs(checkout_command(target_dir, to_identifier), options, &proc)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def ignore_paths
|
234
|
+
[/CVS\/.*/]
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
# Prepends a cat that will slurp stdin. Needed for triggers that don't read all of stdin passed
|
240
|
+
# from CVS to avoid broken pipe.
|
241
|
+
def fix_trigger_command(cmd)
|
242
|
+
"cat && #{cmd}"
|
243
|
+
end
|
244
|
+
|
245
|
+
def cvs(cmd, options={}, &proc)
|
246
|
+
options = {
|
247
|
+
:simulate => false,
|
248
|
+
:dir => @checkout_dir
|
249
|
+
}.merge(options)
|
250
|
+
|
251
|
+
options[:dir] = PathConverter.nativepath_to_filepath(options[:dir])
|
252
|
+
execed_command_line = command_line(cmd, password, options[:simulate])
|
253
|
+
execute(execed_command_line, options, &proc)
|
254
|
+
end
|
255
|
+
|
256
|
+
def parse_log(cmd, options, &proc)
|
257
|
+
execed_command_line = command_line(cmd, password)
|
258
|
+
revisions = nil
|
259
|
+
|
260
|
+
execute(execed_command_line, options) do |io|
|
261
|
+
parser = CvsLogParser.new(io)
|
262
|
+
parser.cvspath = path
|
263
|
+
parser.cvsmodule = mod
|
264
|
+
revisions = parser.parse_revisions
|
265
|
+
end
|
266
|
+
revisions.cmd = execed_command_line if store_revisions_command?
|
267
|
+
revisions
|
268
|
+
end
|
269
|
+
|
270
|
+
def changes_command(options)
|
271
|
+
# https://www.cvshome.org/docs/manual/cvs-1.11.17/cvs_16.html#SEC144
|
272
|
+
# -N => Suppress the header if no RevisionFiles are selected.
|
273
|
+
"log #{branch_option} -N #{period_option(options[:from_identifier], options[:to_identifier])} #{options[:relative_path]}"
|
274
|
+
end
|
275
|
+
|
276
|
+
def branch_specified?
|
277
|
+
branch && branch.strip != ""
|
278
|
+
end
|
279
|
+
|
280
|
+
def branch_option
|
281
|
+
branch_specified? ? "-r#{branch}" : ""
|
282
|
+
end
|
283
|
+
|
284
|
+
def update_command(to_identifier)
|
285
|
+
"update #{branch_option} -d -P -A #{revision_option(to_identifier)}"
|
286
|
+
end
|
287
|
+
|
288
|
+
def checkout_command(target_dir, to_identifier)
|
289
|
+
"checkout #{branch_option} -d #{target_dir} #{revision_option(to_identifier)} #{mod}"
|
290
|
+
end
|
291
|
+
|
292
|
+
def period_option(from_identifier, to_identifier)
|
293
|
+
if(from_identifier.nil? && to_identifier.nil?)
|
294
|
+
""
|
295
|
+
else
|
296
|
+
"-d\"#{cvsdate(from_identifier)}<#{cvsdate(to_identifier)}\" "
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def cvsdate(time)
|
301
|
+
return "" unless time
|
302
|
+
# CVS wants all dates as UTC.
|
303
|
+
time.utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
304
|
+
end
|
305
|
+
|
306
|
+
def root_with_password(password)
|
307
|
+
result = nil
|
308
|
+
if local?
|
309
|
+
result = root
|
310
|
+
elsif password && password != ""
|
311
|
+
protocol, user, host, path = parse_cvs_root
|
312
|
+
result = ":#{protocol}:#{user}:#{password}@#{host}:#{path}"
|
313
|
+
else
|
314
|
+
result = root
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def command_line(cmd, password=nil, simulate=false)
|
319
|
+
cvs_options = simulate ? "-n" : ""
|
320
|
+
"cvs -f \"-d#{root_with_password(password)}\" #{cvs_options} -q #{cmd}"
|
321
|
+
end
|
322
|
+
|
323
|
+
def create_root_cvs(checkout_dir)
|
324
|
+
cvs = Cvs.new(self.root, "CVSROOT", nil, self.password)
|
325
|
+
cvs.checkout_dir = checkout_dir
|
326
|
+
cvs.default_options = default_options
|
327
|
+
cvs
|
328
|
+
end
|
329
|
+
|
330
|
+
def revision_option(identifier)
|
331
|
+
option = nil
|
332
|
+
if(identifier.is_a?(Time))
|
333
|
+
option = "-D\"#{cvsdate(identifier)}\""
|
334
|
+
elsif(identifier.is_a?(String))
|
335
|
+
option = "-r#{identifier}"
|
336
|
+
else
|
337
|
+
""
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def commit_command(message)
|
342
|
+
"commit -m \"#{message}\""
|
343
|
+
end
|
344
|
+
|
345
|
+
def local?
|
346
|
+
protocol == "local"
|
347
|
+
end
|
348
|
+
|
349
|
+
def path
|
350
|
+
parse_cvs_root[3]
|
351
|
+
end
|
352
|
+
|
353
|
+
def protocol
|
354
|
+
parse_cvs_root[0]
|
355
|
+
end
|
356
|
+
|
357
|
+
# parses the root into tokens
|
358
|
+
# [protocol, user, host, path]
|
359
|
+
#
|
360
|
+
def parse_cvs_root
|
361
|
+
md = case
|
362
|
+
when root =~ /^:local:/ then /^:(local):(.*)/.match(root)
|
363
|
+
when root =~ /^:ext:/ then /^:(ext):(.*)@(.*):(.*)/.match(root)
|
364
|
+
when root =~ /^:pserver:/ then /^:(pserver):(.*)@(.*):(.*)/.match(root)
|
365
|
+
end
|
366
|
+
result = case
|
367
|
+
when root =~ /^:local:/ then [md[1], nil, nil, md[2]]
|
368
|
+
when root =~ /^:ext:/ then md[1..4]
|
369
|
+
when root =~ /^:pserver:/ then md[1..4]
|
370
|
+
else ["local", nil, nil, root]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|