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
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'rscm/changes'
|
2
|
+
require 'rscm/abstract_log_parser'
|
3
|
+
|
4
|
+
require 'ftools'
|
5
|
+
|
6
|
+
module RSCM
|
7
|
+
|
8
|
+
class CVSLogParser < AbstractLogParser
|
9
|
+
REVISION_SEPARATOR = /^----------------------------$/
|
10
|
+
ENTRY_SEPARATOR = /^=============================================================================$/
|
11
|
+
|
12
|
+
attr_accessor :cvspath
|
13
|
+
attr_accessor :cvsmodule
|
14
|
+
|
15
|
+
def initialize(io)
|
16
|
+
super(io)
|
17
|
+
@log = ""
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_changesets
|
21
|
+
changesets = ChangeSets.new
|
22
|
+
while(log_entry = next_log_entry)
|
23
|
+
@log<<log_entry
|
24
|
+
@log<<""
|
25
|
+
begin
|
26
|
+
parse_changes(log_entry, changesets)
|
27
|
+
rescue Exception => e
|
28
|
+
$stderr.puts("could not parse log entry: #{log_entry}\ndue to: #{e.message}\n\t")
|
29
|
+
$stderr.puts(e.backtrace.join("\n\t"))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
changesets.sort!
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_log_entry
|
36
|
+
read_until_matching_line(ENTRY_SEPARATOR)
|
37
|
+
end
|
38
|
+
|
39
|
+
def split_entries(log_entry)
|
40
|
+
entries = [""]
|
41
|
+
log_entry.each_line do |line|
|
42
|
+
if line=~REVISION_SEPARATOR
|
43
|
+
entries << ""
|
44
|
+
else
|
45
|
+
entries[entries.length-1] << line
|
46
|
+
end
|
47
|
+
end
|
48
|
+
entries
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_changes(log_entry, changesets)
|
52
|
+
entries = split_entries(log_entry)
|
53
|
+
|
54
|
+
entries[1..entries.length].each do |entry|
|
55
|
+
change = parse_change(entry)
|
56
|
+
next if change.nil?
|
57
|
+
change.path = parse_path(entries[0])
|
58
|
+
|
59
|
+
change.status = Change::ADDED if change.revision =~ /1\.1$/
|
60
|
+
|
61
|
+
changeset = changesets.add(change)
|
62
|
+
# CVS doesn't have revision for changesets, use
|
63
|
+
# Fisheye-style revision
|
64
|
+
# changeset.revision = "MAIN:#{change.developer}:#{change.time.utc.strftime('%Y%m%d%H%M%S')}" if changeset
|
65
|
+
end
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_head_revision(first_entry)
|
70
|
+
head_revision = extract_match(first_entry, /^head: (.*?)$/m)
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_path(first_entry)
|
74
|
+
working_file = extract_match(first_entry, /^Working file: (.*?)$/m)
|
75
|
+
return convert_all_slashes_to_forward_slashes(working_file) unless working_file.nil? || working_file == ""
|
76
|
+
make_relative_to_module(extract_required_match(first_entry, /^RCS file: (.*?)(,v|$)/m))
|
77
|
+
end
|
78
|
+
|
79
|
+
def make_relative_to_module(file)
|
80
|
+
return file if cvspath.nil? || cvsmodule.nil? || file.nil?
|
81
|
+
cvspath = convert_all_slashes_to_forward_slashes(self.cvspath)
|
82
|
+
convert_all_slashes_to_forward_slashes(file).gsub(/^#{cvspath}\/#{cvsmodule}\//, "")
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_change(change_entry)
|
86
|
+
raise "can't parse: #{change_entry}" if change_entry =~ REVISION_SEPARATOR
|
87
|
+
|
88
|
+
change_entry_lines = change_entry.split(/\r?\n/)
|
89
|
+
change = Change.new
|
90
|
+
|
91
|
+
change.revision = extract_match(change_entry_lines[0], /revision (.*)$/)
|
92
|
+
|
93
|
+
change.previous_revision = determine_previous_revision(change.revision)
|
94
|
+
change.time = parse_cvs_time(extract_required_match(change_entry_lines[1], /date: (.*?)(;|$)/))
|
95
|
+
change.developer = extract_match(change_entry_lines[1], /author: (.*?);/)
|
96
|
+
|
97
|
+
state = extract_match(change_entry_lines[1], /state: (.*?);/)
|
98
|
+
change.status = STATES[state]
|
99
|
+
|
100
|
+
message_start = 2
|
101
|
+
branches = nil
|
102
|
+
if(change_entry_lines[2] =~ /^branches:\s+(.*);/)
|
103
|
+
message_start = 3
|
104
|
+
branches = $1
|
105
|
+
end
|
106
|
+
|
107
|
+
change.message = change_entry_lines[message_start..-1].join("\n")
|
108
|
+
|
109
|
+
# Ignore the initial revision from import - we will have two of them
|
110
|
+
if(change.message == "Initial revision" && branches == "1.1.1")
|
111
|
+
return nil
|
112
|
+
end
|
113
|
+
|
114
|
+
change
|
115
|
+
end
|
116
|
+
|
117
|
+
def determine_previous_revision(revision)
|
118
|
+
if revision =~ /(.*)\.(.*)/
|
119
|
+
big_version_number = $1
|
120
|
+
small_version_number = $2.to_i
|
121
|
+
if small_version_number == 1
|
122
|
+
nil
|
123
|
+
else
|
124
|
+
"#{big_version_number}.#{small_version_number - 1}"
|
125
|
+
end
|
126
|
+
else
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_cvs_time(time)
|
132
|
+
# 2003/11/09 15:39:25
|
133
|
+
Time.utc(time[0..3], time[5..6], time[8..9], time[11..12], time[14..15], time[17..18])
|
134
|
+
end
|
135
|
+
|
136
|
+
def extract_required_match(string, regexp)
|
137
|
+
if string=~regexp
|
138
|
+
return($1)
|
139
|
+
else
|
140
|
+
$stderr.puts("can't parse: '#{string}'\nexpected to match regexp: #{regexp.to_s}")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def extract_match(string, regexp)
|
145
|
+
if string=~regexp
|
146
|
+
return($1)
|
147
|
+
else
|
148
|
+
""
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# The state field is "Exp" both for added and modified files. retards!
|
155
|
+
# We need some additional logic to figure out whether it is added or not.
|
156
|
+
# Maybe look at the revision. (1.1 means new I think. - deal with it later)
|
157
|
+
STATES = {"dead" => Change::DELETED, "Exp" => Change::MODIFIED}
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'rscm/abstract_scm'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'rscm/path_converter'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module RSCM
|
7
|
+
class Darcs < AbstractSCM
|
8
|
+
|
9
|
+
def initialize(dir=nil)
|
10
|
+
@dir = File.expand_path(dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
"Darcs"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
with_working_dir(@dir) do
|
19
|
+
IO.popen("darcs initialize") do |stdout|
|
20
|
+
stdout.each_line do |line|
|
21
|
+
yield line if block_given?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def import(dir, message)
|
28
|
+
ENV["EMAIL"] = "dcontrol@codehaus.org"
|
29
|
+
FileUtils.cp_r(Dir.glob("#{dir}/*"), @dir)
|
30
|
+
with_working_dir(@dir) do
|
31
|
+
puts "IN::::: #{@dir}"
|
32
|
+
cmd = "darcs add --recursive ."
|
33
|
+
puts cmd
|
34
|
+
IO.popen(cmd) do |stdout|
|
35
|
+
stdout.each_line do |line|
|
36
|
+
yield line if block_given?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
puts $?
|
40
|
+
logfile = Tempfile.new("darcs_logfile")
|
41
|
+
logfile.print(message)
|
42
|
+
logfile.close
|
43
|
+
|
44
|
+
cmd = "darcs record --all --patch-name \"something nice\" --logfile #{PathConverter.filepath_to_nativepath(logfile.path, false)}"
|
45
|
+
puts cmd
|
46
|
+
IO.popen(cmd) do |stdout|
|
47
|
+
stdout.each_line do |line|
|
48
|
+
yield line if block_given?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
puts $?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def checkout(checkout_dir) # :yield: file
|
56
|
+
with_working_dir(File.dirname(checkout_dir)) do
|
57
|
+
cmd = "darcs get --verbose --repo-name #{File.basename(checkout_dir)} #{@dir}"
|
58
|
+
puts cmd
|
59
|
+
IO.popen(cmd) do |stdout|
|
60
|
+
stdout.each_line do |line|
|
61
|
+
puts line
|
62
|
+
yield line if block_given?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
puts $?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'ftools'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
module LineEditor
|
6
|
+
# Comments out line by line if they match the line_regex.
|
7
|
+
# Does not comment out already commented out lines.
|
8
|
+
# If comment_template is nil, the matching lines will be deleted
|
9
|
+
# Returns true if at least one line is commented out or changed
|
10
|
+
def comment_out(original, line_regex, comment_template, output)
|
11
|
+
did_comment_out = false
|
12
|
+
already_commented_exp = /^[#{comment_template}]/ unless comment_template.nil?
|
13
|
+
original.each_line do |line|
|
14
|
+
out_line = nil
|
15
|
+
if(line_regex =~ line)
|
16
|
+
if(already_commented_exp && already_commented_exp =~ line)
|
17
|
+
out_line = line
|
18
|
+
else
|
19
|
+
did_comment_out = true
|
20
|
+
out_line = "#{comment_template}#{line}" unless comment_template.nil?
|
21
|
+
end
|
22
|
+
else
|
23
|
+
out_line = line
|
24
|
+
end
|
25
|
+
output << out_line unless out_line.nil?
|
26
|
+
end
|
27
|
+
did_comment_out
|
28
|
+
end
|
29
|
+
module_function :comment_out
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class File
|
34
|
+
|
35
|
+
def File.comment_out(path, line_regex, comment_template)
|
36
|
+
temp_file = Tempfile.new(File.basename(path))
|
37
|
+
temp_file_path = temp_file.path
|
38
|
+
original = File.new(path)
|
39
|
+
RSCM::LineEditor.comment_out(original, line_regex, comment_template, temp_file)
|
40
|
+
|
41
|
+
temp_file.close
|
42
|
+
original.close
|
43
|
+
|
44
|
+
File.copy(temp_file_path, path)
|
45
|
+
end
|
46
|
+
end
|
data/lib/rscm/logging.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'rscm/abstract_scm'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
class Monotone < AbstractSCM
|
6
|
+
def initialize(db_file=nil, branch=nil, key=nil, passphrase=nil, keys_file=nil)
|
7
|
+
@db_file = File.expand_path(db_file) if db_file
|
8
|
+
@branch = branch
|
9
|
+
@key = key
|
10
|
+
@passphrase = passphrase
|
11
|
+
@keys_file = keys_file
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
"Monotone"
|
16
|
+
end
|
17
|
+
|
18
|
+
def create
|
19
|
+
FileUtils.mkdir_p(File.dirname(@db_file))
|
20
|
+
monotone("db init")
|
21
|
+
monotone("read") do |io|
|
22
|
+
io.write(File.open(@keys_file).read)
|
23
|
+
io.close_write
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def transactional?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def import(dir, message)
|
32
|
+
dir = File.expand_path(dir)
|
33
|
+
|
34
|
+
# post 0.17, this can be "cd dir && cmd add ."
|
35
|
+
|
36
|
+
files = Dir["#{dir}/*"]
|
37
|
+
relative_paths_to_add = to_relative(dir, files)
|
38
|
+
|
39
|
+
with_working_dir(dir) do
|
40
|
+
monotone("add #{relative_paths_to_add.join(' ')}")
|
41
|
+
monotone("commit '#{message}'", @branch, @key) do |io|
|
42
|
+
io.puts(@passphrase)
|
43
|
+
io.close_write
|
44
|
+
io.read
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def checked_out?(checkout_dir)
|
50
|
+
File.exists?("#{checkout_dir}/MT")
|
51
|
+
end
|
52
|
+
|
53
|
+
def uptodate?(checkout_dir, from_identifier)
|
54
|
+
if (!checked_out?(checkout_dir))
|
55
|
+
false
|
56
|
+
else
|
57
|
+
with_working_dir(checkout_dir) do
|
58
|
+
monotone("heads") do |stdout|
|
59
|
+
stdout.each_line do |line|
|
60
|
+
next if (line =~ /^monotone:/)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def commit(checkout_dir, message)
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# Checks out silently. Called by superclass' checkout.
|
74
|
+
def checkout_silent(checkout_dir, to_identifier)
|
75
|
+
monotone("checkout #{checkout_dir}", @branch, @key) do |stdout|
|
76
|
+
stdout.each_line do |line|
|
77
|
+
# TODO: checkout prints nothing to stdout - may be fixed in a future monotone.
|
78
|
+
# When/if it happens we may want to do a kosher implementation of checkout
|
79
|
+
# to get yields as checkouts happen.
|
80
|
+
yield line if block_given?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Administrative files that should be ignored when counting files.
|
86
|
+
def ignore_paths
|
87
|
+
return [/MT/, /\.mt-attrs/]
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def monotone(monotone_cmd, branch=nil, key=nil)
|
93
|
+
branch_opt = branch ? "--branch=\"#{branch}\"" : ""
|
94
|
+
key_opt = key ? "--key=\"#{key}\"" : ""
|
95
|
+
cmd = "monotone --db=\"#{@db_file}\" #{branch_opt} #{key_opt} #{monotone_cmd}"
|
96
|
+
safer_popen(cmd, "r+") do |io|
|
97
|
+
if(block_given?)
|
98
|
+
return(yield(io))
|
99
|
+
else
|
100
|
+
# just read stdout so we can exit
|
101
|
+
io.read
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
data/lib/rscm/parser.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module RSCM
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
def initialize(break_regexp)
|
5
|
+
@break_regexp = break_regexp
|
6
|
+
end
|
7
|
+
|
8
|
+
def parse(io, skip_line_parsing=false, &line_proc)
|
9
|
+
parse_until_regexp_matches(io, skip_line_parsing, &line_proc)
|
10
|
+
if(skip_line_parsing)
|
11
|
+
nil
|
12
|
+
else
|
13
|
+
next_result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def parse_line(line)
|
20
|
+
raise "Must override parse_line(line)"
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_result
|
24
|
+
raise "Must override next_result(line)"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def parse_until_regexp_matches(io, skip_line_parsing, &line_proc)
|
30
|
+
io.each_line { |line|
|
31
|
+
yield line if block_given?
|
32
|
+
if line =~ @break_regexp
|
33
|
+
return
|
34
|
+
end
|
35
|
+
parse_line(line) unless skip_line_parsing
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|