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.
Files changed (55) hide show
  1. data/CHANGES +12 -0
  2. data/README +14 -0
  3. data/Rakefile +4 -24
  4. data/lib/rscm.rb +1 -2
  5. data/lib/rscm/base.rb +289 -281
  6. data/lib/rscm/command_line.rb +135 -112
  7. data/lib/rscm/revision.rb +63 -166
  8. data/lib/rscm/revision_file.rb +8 -2
  9. data/lib/rscm/revision_poller.rb +78 -67
  10. data/lib/rscm/revisions.rb +79 -0
  11. data/lib/rscm/scm/clearcase.rb +11 -9
  12. data/lib/rscm/scm/cvs.rb +374 -352
  13. data/lib/rscm/scm/cvs_log_parser.rb +1 -0
  14. data/lib/rscm/scm/darcs.rb +9 -0
  15. data/lib/rscm/scm/perforce.rb +216 -149
  16. data/lib/rscm/scm/subversion.rb +44 -24
  17. data/lib/rscm/scm/subversion_log_parser.rb +37 -51
  18. data/lib/rscm/time_ext.rb +0 -1
  19. data/lib/rscm/version.rb +2 -2
  20. data/test/rscm/command_line_test.rb +7 -5
  21. data/test/rscm/compatibility/config.yml +4 -4
  22. data/test/rscm/compatibility/cvs_metaproject/diff.txt +52 -0
  23. data/test/rscm/compatibility/cvs_metaproject/file.txt +48 -0
  24. data/test/rscm/compatibility/cvs_metaproject/old.yml +13 -0
  25. data/test/rscm/compatibility/full.rb +2 -223
  26. data/test/rscm/compatibility/p4_gfx/files_0.yml +10 -0
  27. data/test/rscm/compatibility/p4_gfx/old.yml +26 -0
  28. data/test/rscm/compatibility/p4_gfx/revisions.yml +24 -0
  29. data/test/rscm/compatibility/p4_gfx/scm.yml +4 -0
  30. data/test/rscm/compatibility/rscm_engine.rb +197 -0
  31. data/test/rscm/compatibility/subversion_rscm/diff.txt +12 -0
  32. data/test/rscm/compatibility/subversion_rscm/file.txt +567 -0
  33. data/test/rscm/compatibility/subversion_rscm/old.yml +14 -0
  34. data/test/rscm/compatibility/subversion_rscm/revisions.yml +17 -0
  35. data/test/rscm/compatibility/subversion_rscm/scm.yml +1 -0
  36. data/test/rscm/revision_file_test.rb +10 -0
  37. data/test/rscm/revision_poller_test.rb +91 -0
  38. data/test/rscm/revision_test.rb +22 -117
  39. data/test/rscm/revisions_test.rb +80 -0
  40. data/test/rscm/scm/cvs_log_parser_test.rb +569 -567
  41. data/test/rscm/scm/cvs_test.rb +6 -3
  42. data/test/rscm/scm/darcs_test.rb +4 -7
  43. data/test/rscm/scm/perforce_test.rb +6 -2
  44. data/test/rscm/scm/star_team_test.rb +10 -0
  45. data/test/rscm/scm/subversion_log_parser_test.rb +38 -5
  46. data/test/rscm/scm/subversion_test.rb +2 -3
  47. data/test/rscm/test_helper.rb +41 -2
  48. data/testproject/damagecontrolled/build.xml +154 -154
  49. data/testproject/damagecontrolled/src/java/com/thoughtworks/damagecontrolled/Thingy.java +6 -6
  50. metadata +19 -7
  51. data/lib/rscm/historic_file.rb +0 -30
  52. data/test/rscm/compatibility/damage_control_minimal.rb +0 -104
  53. data/test/rscm/revision_fixture.rb +0 -20
  54. data/test/rscm/revisions.yaml +0 -42
  55. data/test/rscm/scm/star_team.rb +0 -36
@@ -27,6 +27,7 @@ module RSCM
27
27
  end
28
28
  end
29
29
  revisions.sort!
30
+ revisions
30
31
  end
31
32
 
32
33
  def next_log_entry
@@ -7,6 +7,15 @@ module RSCM
7
7
  # See http://progetti.arstecnica.it/tailor/browser/vcpx/darcs.py
8
8
  attr_accessor :dir
9
9
 
10
+ def installed?
11
+ begin
12
+ darcs("--version", {})
13
+ true
14
+ rescue
15
+ false
16
+ end
17
+ end
18
+
10
19
  def initialize(dir=".")
11
20
  @dir = File.expand_path(dir)
12
21
  end
@@ -1,149 +1,216 @@
1
- # TODO
2
- # Support int revision numbers AND dates
3
- # Leverage default P4 client settings (optional)
4
-
5
- require 'rscm/base'
6
- require 'rscm/path_converter'
7
- require 'rscm/line_editor'
8
-
9
- require 'fileutils'
10
- require 'time'
11
- require 'socket'
12
-
13
- module RSCM
14
- class Perforce < Base
15
- DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
16
- # Doesn't work for empty messages, (Like 21358 in Aslak's P4 repo)
17
- CHANGELIST_PATTERN = /^Change \d+ by (.*)@.* on (.*)\n\n(.*)\n\nAffected files ...\n\n(.*)/m
18
- # But this one does
19
- CHANGELIST_PATTERN_NO_MSG = /^Change \d+ by (.*)@.* on (.*)\n\nAffected files ...\n\n(.*)/m
20
-
21
- def initialize(user=nil, password=nil, view=nil)
22
- @user, @password = user, password
23
- end
24
-
25
- def revisions(from_identifier, options={})
26
- from_identifier = Time.epoch unless from_identifier
27
- from_identifier = Time.epoch if (from_identifier.is_a? Time and from_identifier < Time.epoch)
28
- from = revision_spec(from_identifier+1) # We have to add 1 because of the constract of this method.
29
-
30
- to_identifier = options[:to_identifier] ? options[:to_identifier] : Time.infinity
31
- to = revision_spec(to_identifier)
32
-
33
- changes = execute("p4 #{p4_opts(false)} changes //...@#{from},#{to}", options) do |io|
34
- io.read
35
- end
36
-
37
- revs = changes.collect do |line|
38
- revision = Revision.new
39
- revision.identifier = line.match(/^Change (\d+)/)[1].to_i
40
-
41
- execute("p4 #{p4_opts(false)} describe -s #{revision.identifier}", options) do |io|
42
- log = io.read
43
-
44
- if log =~ CHANGELIST_PATTERN
45
- revision.developer, time, revision.message, files = $1, $2, $3, $4
46
- elsif log =~ CHANGELIST_PATTERN_NO_MSG
47
- revision.developer, time, files = $1, $2, $3
48
- else
49
- puts "PARSE ERROR:"
50
- puts log
51
- puts "\nDIDN'T MATCH:"
52
- puts CHANGELIST_PATTERN
53
- end
54
-
55
- revision.time = Time.parse(time)
56
-
57
- files.each_line do |line|
58
- if line =~ /^\.\.\. (\/\/.+)#(\d+) (.+)/
59
- if(STATES[$3.strip])
60
- file = RevisionFile.new
61
- file.path = $1
62
- file.native_revision_identifier = $2.to_i
63
- file.previous_native_revision_identifier = file.native_revision_identifier-1
64
- file.status = STATES[$3.strip]
65
- file.time = revision.time
66
- file.message = revision.message
67
- file.developer = revision.developer
68
- revision << file
69
- end
70
- end
71
- end
72
-
73
- end
74
-
75
- revision
76
- end
77
- Revisions.new(revs.reverse)
78
- end
79
-
80
- protected
81
-
82
- def checkout_silent(to_identifier, options)
83
- checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
84
- FileUtils.mkdir_p(@checkout_dir)
85
-
86
- ensure_client(options)
87
- execute("p4 #{p4_opts} sync", options)
88
- end
89
-
90
- def ignore_paths
91
- []
92
- end
93
-
94
- private
95
-
96
- STATES = {
97
- "add" => RevisionFile::ADDED,
98
- "edit" => RevisionFile::MODIFIED,
99
- "delete" => RevisionFile::DELETED
100
- }
101
-
102
- def p4_opts(with_client=true)
103
- user_opt = @user.to_s.empty? ? "" : "-u #{@user}"
104
- password_opt = @password.to_s.empty? ? "" : "-P #{@password}"
105
- client_opt = with_client ? "-c \"#{client_name}\"" : ""
106
- "#{user_opt} #{password_opt} #{client_opt}"
107
- end
108
-
109
- def client_name
110
- raise "checkout_dir not set" unless @checkout_dir
111
- Socket.gethostname + ":" + @checkout_dir
112
- end
113
-
114
- def ensure_client(options)
115
- create_client(options)
116
- end
117
-
118
- def create_client(options)
119
- options = {:mode => "w+"}.merge(options)
120
- execute("p4 #{p4_opts(false)} client -i", options) do |io|
121
- io.puts(client_spec)
122
- io.close_write
123
- end
124
- end
125
-
126
- def client_spec
127
- <<-EOF
128
- Client: #{client_name}
129
- Owner: #{@user}
130
- Host: #{Socket.gethostname}
131
- Description: RSCM client
132
- Root: #{@checkout_dir}
133
- Options: noallwrite noclobber nocompress unlocked nomodtime normdir
134
- LineEnd: local
135
- View: #{@view} //#{client_name}/...
136
- EOF
137
- end
138
-
139
- def revision_spec(identifier)
140
- if identifier.is_a?(Time)
141
- identifier.strftime(DATE_FORMAT)
142
- else
143
- identifier.to_i
144
- end
145
- end
146
-
147
- end
148
-
149
- end
1
+ # TODO
2
+ # Support int revision numbers AND dates
3
+ # Leverage default P4 client settings (optional)
4
+
5
+ require 'rscm/base'
6
+ require 'rscm/path_converter'
7
+ require 'rscm/line_editor'
8
+
9
+ require 'fileutils'
10
+ require 'time'
11
+ require 'socket'
12
+
13
+ module RSCM
14
+ class Perforce < Base
15
+ unless defined? DATE_FORMAT
16
+ DATE_FORMAT = "%Y/%m/%d:%H:%M:%S"
17
+ # Doesn't work for empty messages, (Like 21358 in Aslak's P4 repo)
18
+ CHANGELIST_PATTERN = /^Change \d+ by (.*)@.* on (.*)\n\n(.*)\n\nAffected files ...\n\n(.*)/m
19
+ # But this one does
20
+ CHANGELIST_PATTERN_NO_MSG = /^Change \d+ by (.*)@.* on (.*)\n\nAffected files ...\n\n(.*)/m
21
+
22
+ STATES = {
23
+ "add" => RevisionFile::ADDED,
24
+ "edit" => RevisionFile::MODIFIED,
25
+ "delete" => RevisionFile::DELETED
26
+ }
27
+ end
28
+
29
+ attr_accessor :view
30
+ attr_accessor :username
31
+ attr_accessor :password
32
+
33
+ def installed?
34
+ begin
35
+ execute("p4 info", {})
36
+ true
37
+ rescue
38
+ false
39
+ end
40
+ end
41
+
42
+ def revisions(from_identifier=Time.new.utc, options={})
43
+ raise "from_identifer cannot be nil" if from_identifier.nil?
44
+ set_utc_offset(options)
45
+ view_as_regexp = "^" + @view.gsub(/\.\.\./, "(.*)")
46
+ relative_path_pattern = Regexp.new(view_as_regexp)
47
+
48
+ from_identifier = Time.epoch unless from_identifier
49
+ from_identifier = Time.epoch if (from_identifier.is_a? Time and from_identifier < Time.epoch)
50
+ from = revision_spec(from_identifier + 1) # We have to add 1 because of the contract of this method.
51
+
52
+ to_identifier = options[:to_identifier] ? options[:to_identifier] : Time.infinity
53
+ to = revision_spec(to_identifier - 1) # We have to subtract 1 because of the contract of this method.
54
+
55
+ cmd = "p4 #{p4_opts(false)} changes #{@view}@#{from},#{to}"
56
+ revisions = Revisions.new
57
+ revisions.cmd = cmd if store_revisions_command?
58
+
59
+ changes = execute(cmd, options) do |io|
60
+ io.read
61
+ end
62
+
63
+ changes.each do |line|
64
+ revision = nil
65
+ identifier = line.match(/^Change (\d+)/)[1].to_i
66
+
67
+ execute("p4 #{p4_opts(false)} describe -s #{identifier}", options) do |io|
68
+ log = io.read
69
+
70
+ if log =~ CHANGELIST_PATTERN
71
+ developer, time, message, files = $1, $2, $3, $4
72
+ elsif log =~ CHANGELIST_PATTERN_NO_MSG
73
+ developer, time, files = $1, $2, $3
74
+ else
75
+ puts "PARSE ERROR:"
76
+ puts log
77
+ puts "\nDIDN'T MATCH:"
78
+ puts CHANGELIST_PATTERN
79
+ end
80
+
81
+ # The parsed time doesn't have timezone info. We'll tweak it.
82
+ time = Time.parse(time + " UTC") - @utc_offset
83
+
84
+ files.each_line do |line|
85
+ if line =~ /^\.\.\. (\/\/.+)#(\d+) (.+)/
86
+ depot_path = $1
87
+ file_identifier = $2.to_i
88
+ state = $3.strip
89
+ if(STATES[state])
90
+ if(depot_path =~ relative_path_pattern)
91
+ relative_path = $1
92
+
93
+ if revision.nil?
94
+ revision = Revision.new
95
+ revision.identifier = identifier
96
+ revision.developer = developer
97
+ revision.message = message
98
+ revision.time = time
99
+ revisions.add revision
100
+ end
101
+
102
+ file = RevisionFile.new
103
+ file.path = relative_path
104
+ file.native_revision_identifier = file_identifier
105
+ file.previous_native_revision_identifier = file.native_revision_identifier-1
106
+ file.status = STATES[state]
107
+ revision.add file
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ revisions
115
+ end
116
+
117
+ def destroy_working_copy(options={})
118
+ execute("p4 #{p4_opts(false)} client -d #{client_name}", options)
119
+ end
120
+
121
+ def open(revision_file, options={}, &block)
122
+ path = @view.gsub(/\.\.\./, revision_file.path) # + "@" + revision_file.native_revision_identifier
123
+ cmd = "p4 #{p4_opts(false)} print -q #{path}"
124
+ execute(cmd, options) do |io|
125
+ block.call io
126
+ end
127
+ end
128
+
129
+ def diff
130
+ #p4 diff2 //depot/trunk/build.xml@26405 //depot/trunk/build.xml@26409
131
+ end
132
+
133
+ protected
134
+
135
+ def checkout_silent(to_identifier, options)
136
+ checkout_dir = PathConverter.filepath_to_nativepath(@checkout_dir, false)
137
+ FileUtils.mkdir_p(@checkout_dir)
138
+
139
+ ensure_client(options)
140
+ execute("p4 #{p4_opts} sync #{@view}@#{to_identifier}", options)
141
+ end
142
+
143
+ def ignore_paths
144
+ []
145
+ end
146
+
147
+ private
148
+
149
+ def p4_opts(with_client=true)
150
+ user_opt = @username.to_s.empty? ? "" : "-u #{@username}"
151
+ password_opt = @password.to_s.empty? ? "" : "-P #{@password}"
152
+ client_opt = with_client ? "-c \"#{client_name}\"" : ""
153
+ "#{user_opt} #{password_opt} #{client_opt}"
154
+ end
155
+
156
+ def client_name
157
+ raise "checkout_dir not set" unless @checkout_dir
158
+ Socket.gethostname + ":" + @checkout_dir
159
+ end
160
+
161
+ def ensure_client(options)
162
+ create_client(options)
163
+ end
164
+
165
+ def create_client(options)
166
+ options = {:mode => "w+"}.merge(options)
167
+ FileUtils.mkdir_p(@checkout_dir)
168
+ execute("p4 #{p4_opts(false)} client -i", options) do |io|
169
+ io.puts(client_spec)
170
+ io.close_write
171
+ end
172
+ end
173
+
174
+ def client_spec
175
+ <<-EOF
176
+ Client: #{client_name}
177
+ Owner: #{@username}
178
+ Host: #{Socket.gethostname}
179
+ Description: RSCM client
180
+ Root: #{@checkout_dir}
181
+ Options: noallwrite noclobber nocompress unlocked nomodtime normdir
182
+ LineEnd: local
183
+ View: #{@view} //#{client_name}/...
184
+ EOF
185
+ end
186
+
187
+ def revision_spec(identifier)
188
+ if identifier.is_a?(Time)
189
+ # The p4 client uses local time, but rscm uses utc
190
+ # We have to convert to local time
191
+ identifier += @utc_offset
192
+ identifier.strftime(DATE_FORMAT)
193
+ else
194
+ identifier.to_i
195
+ end
196
+ end
197
+
198
+ # Queries the server for the time offset. Required in order to get proper
199
+ # timezone for revisions
200
+ def set_utc_offset(options)
201
+ unless @utc_offset
202
+ execute("p4 #{p4_opts(false)} info", options) do |io|
203
+ io.each do |line|
204
+ if line =~ /^Server date: (.*)/
205
+ server_time = Time.parse($1)
206
+ @utc_offset = server_time.utc_offset
207
+ end
208
+ end
209
+ end
210
+ raise "Couldn't get server's UTC offset" if @utc_offset.nil?
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ end
@@ -2,6 +2,7 @@ require 'rscm/base'
2
2
  require 'rscm/path_converter'
3
3
  require 'rscm/line_editor'
4
4
  require 'rscm/scm/subversion_log_parser'
5
+ require 'stringio'
5
6
 
6
7
  module RSCM
7
8
 
@@ -27,17 +28,30 @@ module RSCM
27
28
  @username = ""
28
29
  @password = ""
29
30
  end
31
+
32
+ def path
33
+ @path ||= ""
34
+ end
35
+
36
+ def installed?
37
+ begin
38
+ svn("--version", {})
39
+ true
40
+ rescue
41
+ false
42
+ end
43
+ end
30
44
 
31
45
  def to_identifier(raw_identifier)
32
46
  raw_identifier.to_i
33
47
  end
34
48
 
35
49
  def add(relative_filename, options={})
36
- svn("add #{relative_filename}", options)
50
+ svn("add #{checkout_dir}/#{relative_filename}", options)
37
51
  end
38
52
 
39
53
  def move(relative_src, relative_dest, options={})
40
- svn("mv #{relative_src} #{relative_dest}", options)
54
+ svn("mv #{checkout_dir}/#{relative_src} #{checkout_dir}/#{relative_dest}", options)
41
55
  end
42
56
 
43
57
  def transactional?
@@ -56,24 +70,25 @@ module RSCM
56
70
  def commit(message, options={})
57
71
  svn(commit_command(message), options)
58
72
  # We have to do an update to get the local revision right
59
- svn("update", options)
73
+ checkout_silent(nil, options)
60
74
  end
61
75
 
62
76
  def label
63
77
  local_revision_identifier.to_s
64
78
  end
65
79
 
66
- def diff(file, options={}, &block)
67
- cmd = "svn diff --revision #{file.previous_native_revision_identifier}:#{file.native_revision_identifier} \"#{url}/#{file.path}\""
80
+ def diff(path, from, to, options={}, &block)
81
+ cmd = "svn diff --revision #{from}:#{to} \"#{url}/#{path}\""
68
82
  execute(cmd, options) do |io|
69
- return(yield(io))
83
+ return(block.call(io))
70
84
  end
71
85
  end
72
86
 
73
- def open(revision_file, options, &block)
74
- cmd = "svn cat #{url}/#{revision_file.path}@#{revision_file.native_revision_identifier}"
87
+ def open(path, native_revision_identifier, options={}, &block)
88
+ raise "native_revision_identifier cannot be nil" if native_revision_identifier.nil?
89
+ cmd = "svn cat #{url}/#{path}@#{native_revision_identifier}"
75
90
  execute(cmd, options) do |io|
76
- return(yield(io))
91
+ return(block.call(io))
77
92
  end
78
93
  end
79
94
 
@@ -123,10 +138,10 @@ module RSCM
123
138
  native_path = PathConverter.filepath_to_nativepath(svnrootdir, false)
124
139
  mkdir_p(PathConverter.nativepath_to_filepath(native_path))
125
140
  svnadmin("create #{native_path}", options)
126
- if(@path && @path != "")
141
+ if(path != "")
127
142
  options = options.dup.merge({:dir => "."})
128
143
  # create the directories
129
- paths = @path.split("/")
144
+ paths = path.split("/")
130
145
  paths.each_with_index do |p,i|
131
146
  p = paths[0..i]
132
147
  u = "#{repourl}/#{p.join('/')}"
@@ -153,12 +168,13 @@ module RSCM
153
168
  not_already_commented
154
169
  end
155
170
 
156
- def import_central(options)
157
- import_cmd = "import #{url} -m \"#{options[:message]}\""
171
+ def import_central(dir, options={})
172
+ import_cmd = "import #{dir} #{url} -m \"#{options[:message]}\""
158
173
  svn(import_cmd, options)
159
174
  end
160
175
 
161
- def revisions(from_identifier, options={})
176
+ def revisions(from_identifier=Time.new.utc, options={})
177
+ raise "from_identifer cannot be nil" if from_identifier.nil?
162
178
  options = {
163
179
  :from_identifier => from_identifier,
164
180
  :to_identifier => Time.infinity,
@@ -170,9 +186,11 @@ module RSCM
170
186
  revisions = nil
171
187
  command = "svn #{changes_command(options[:from_identifier], options[:to_identifier], options[:relative_path])}"
172
188
  execute(command, options) do |stdout|
173
- parser = SubversionLogParser.new(stdout, @url, options[:from_identifier], @path)
189
+ stdout = StringIO.new(stdout.read)
190
+ parser = SubversionLogParser.new(stdout, @url, options[:from_identifier], options[:to_identifier], path)
174
191
  revisions = parser.parse_revisions
175
192
  end
193
+ revisions.cmd = command if store_revisions_command?
176
194
  revisions
177
195
  end
178
196
 
@@ -208,7 +226,7 @@ module RSCM
208
226
 
209
227
  def local_revision_identifier(options)
210
228
  local_revision_identifier = nil
211
- svn("info", options) do |line|
229
+ svn("info #{quoted_checkout_dir}", options) do |line|
212
230
  if(line =~ /Revision: ([0-9]*)/)
213
231
  return $1.to_i
214
232
  end
@@ -287,21 +305,17 @@ module RSCM
287
305
  end
288
306
 
289
307
  def checkout_command(to_identifier)
290
- cd = "\"#{checkout_dir}\""
291
- raise "checkout_dir not set" if cd == ""
292
- "checkout #{login_options} #{url} #{cd} #{revision_option(nil,to_identifier)}"
308
+ "checkout #{login_options} #{url} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
293
309
  end
294
310
 
295
311
  def update_command(to_identifier)
296
- "update #{login_options} #{revision_option(nil,to_identifier)}"
312
+ "update #{login_options} #{revision_option(nil,to_identifier)} #{quoted_checkout_dir}"
297
313
  end
298
314
 
299
315
  def changes_command(from_identifier, to_identifier, relative_path)
300
316
  # http://svnbook.red-bean.com/svnbook-1.1/svn-book.html#svn-ch-3-sect-3.3
301
317
  # file_list = files.join('\n')
302
- full_url = relative_path ? "#{url}/#{relative_path}" : ""
303
- cmd = "log --verbose #{login_options} #{revision_option(from_identifier, to_identifier)} #{full_url}"
304
- cmd
318
+ "log --verbose #{login_options} #{revision_option(from_identifier, to_identifier)} #{@url}"
305
319
  end
306
320
 
307
321
  def login_options
@@ -352,7 +366,13 @@ module RSCM
352
366
  end
353
367
 
354
368
  def commit_command(message)
355
- "commit #{login_options} --force-log -m \"#{message}\""
369
+ "commit #{login_options} --force-log -m \"#{message}\" #{quoted_checkout_dir}"
370
+ end
371
+
372
+ def quoted_checkout_dir
373
+ cd = '"' + PathConverter.filepath_to_nativepath(checkout_dir, false) + '"'
374
+ raise "checkout_dir not set" if cd == ""
375
+ cd
356
376
  end
357
377
 
358
378
  def local?