rscm 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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?