perforce2svn 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +23 -0
- data/README.markdown +66 -0
- data/Rakefile +24 -0
- data/bin/perforce2svn +11 -0
- data/lib/VERSION.yml +6 -0
- data/lib/perforce2svn/cli.rb +117 -0
- data/lib/perforce2svn/environment.rb +66 -0
- data/lib/perforce2svn/errors.rb +16 -0
- data/lib/perforce2svn/logging.rb +35 -0
- data/lib/perforce2svn/mapping/analyzer.rb +30 -0
- data/lib/perforce2svn/mapping/branch_mapping.rb +32 -0
- data/lib/perforce2svn/mapping/commands.rb +75 -0
- data/lib/perforce2svn/mapping/help.txt +139 -0
- data/lib/perforce2svn/mapping/lexer.rb +101 -0
- data/lib/perforce2svn/mapping/mapping_file.rb +65 -0
- data/lib/perforce2svn/mapping/operation.rb +8 -0
- data/lib/perforce2svn/mapping/parser.rb +145 -0
- data/lib/perforce2svn/migrator.rb +71 -0
- data/lib/perforce2svn/perforce/commit_builder.rb +159 -0
- data/lib/perforce2svn/perforce/p4_depot.rb +69 -0
- data/lib/perforce2svn/perforce/perforce_file.rb +81 -0
- data/lib/perforce2svn/subversion/svn_repo.rb +156 -0
- data/lib/perforce2svn/subversion/svn_transaction.rb +136 -0
- data/lib/perforce2svn/version_range.rb +43 -0
- data/mjt.map +7 -0
- data/perforce2svn.gemspec +49 -0
- data/spec/integration/hamlet.txt +7067 -0
- data/spec/integration/madmen_icon_bigger.jpg +0 -0
- data/spec/integration/perforce/p4_depot_spec.rb +16 -0
- data/spec/integration/perforce/perforce_file.yml +4 -0
- data/spec/integration/perforce/perforce_file_spec.rb +19 -0
- data/spec/integration/subversion/svn_repo_spec.rb +93 -0
- data/spec/integration/subversion/svn_transaction_spec.rb +112 -0
- data/spec/perforce2svn/cli_spec.rb +61 -0
- data/spec/perforce2svn/mapping/analyzer_spec.rb +41 -0
- data/spec/perforce2svn/mapping/branch_mapping_spec.rb +40 -0
- data/spec/perforce2svn/mapping/lexer_spec.rb +43 -0
- data/spec/perforce2svn/mapping/parser_spec.rb +140 -0
- data/spec/perforce2svn/perforce/commit_builder_spec.rb +74 -0
- data/spec/perforce2svn/version_range_spec.rb +42 -0
- data/spec/spec_helpers.rb +44 -0
- metadata +230 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'perforce2svn/logging'
|
2
|
+
require 'perforce2svn/errors'
|
3
|
+
require 'perforce2svn/environment'
|
4
|
+
require 'perforce2svn/mapping/mapping_file'
|
5
|
+
require 'perforce2svn/perforce/commit_builder'
|
6
|
+
require 'perforce2svn/subversion/svn_repo'
|
7
|
+
require 'perforce2svn/version_range'
|
8
|
+
require 'choosy/terminal'
|
9
|
+
|
10
|
+
module Perforce2Svn
|
11
|
+
class Migrator
|
12
|
+
include Logging
|
13
|
+
include Choosy::Terminal
|
14
|
+
|
15
|
+
def initialize(migrator_file, options)
|
16
|
+
Logging.configure(options[:debug])
|
17
|
+
Environment.new.check!
|
18
|
+
|
19
|
+
@migration_file = Mapping::MappingFile.new(migrator_file, options)
|
20
|
+
@svnRepo = Perforce2Svn::Subversion::SvnRepo.new(options[:repository])
|
21
|
+
@commit_builder = Perforce::CommitBuilder.new(@migration_file.mappings)
|
22
|
+
@version_range = options[:changes] || VersionRange.new(1)
|
23
|
+
@options = options
|
24
|
+
end
|
25
|
+
|
26
|
+
def run!
|
27
|
+
begin
|
28
|
+
@commit_builder.commits_in(@version_range) do |commit|
|
29
|
+
migrate_commit(commit)
|
30
|
+
end unless @options[:skip_perforce]
|
31
|
+
|
32
|
+
execute_commands unless @options[:skip_commands]
|
33
|
+
rescue SystemExit
|
34
|
+
raise
|
35
|
+
rescue Interrupt
|
36
|
+
@svnRepo.clean_transactions!
|
37
|
+
die "Interrupted. Not continuing."
|
38
|
+
rescue Exception => e
|
39
|
+
puts e.backtrace
|
40
|
+
log.error e
|
41
|
+
die "Unable to complete migration."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def migrate_commit(commit)
|
47
|
+
commit.log!
|
48
|
+
@svnRepo.transaction(commit.author, commit.time, commit.message) do |txn|
|
49
|
+
commit.files.each do |file|
|
50
|
+
if file.deleted?
|
51
|
+
txn.delete(file.dest)
|
52
|
+
elsif file.symlink?
|
53
|
+
txn.symlink(file.dest, file.symlink_target)
|
54
|
+
else
|
55
|
+
file.streamed_contents do |fstream|
|
56
|
+
txn.update(file.dest, fstream, file.binary?)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def execute_commands
|
64
|
+
@svnRepo.transaction(@migration_file.author, Time.now, @migration_file.message) do |txn|
|
65
|
+
@migration_file.commands.each do |command|
|
66
|
+
command.execute!(txn)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'perforce2svn/logging'
|
2
|
+
require 'perforce2svn/perforce/perforce_file'
|
3
|
+
require 'perforce2svn/perforce/p4_depot'
|
4
|
+
require 'perforce2svn/mapping/branch_mapping'
|
5
|
+
require 'iconv'
|
6
|
+
require 'svn/core'
|
7
|
+
|
8
|
+
module Perforce2Svn::Perforce
|
9
|
+
# The standard commit message
|
10
|
+
class PerforceCommit
|
11
|
+
include Perforce2Svn::Logging
|
12
|
+
|
13
|
+
attr_reader :author, :message, :time, :revision, :files
|
14
|
+
|
15
|
+
def initialize(revision, author, message, time, files)
|
16
|
+
@author = author
|
17
|
+
@revision = revision
|
18
|
+
@message = message
|
19
|
+
@time = time
|
20
|
+
@files = files
|
21
|
+
end
|
22
|
+
|
23
|
+
def log!
|
24
|
+
to_s.each_line do |line|
|
25
|
+
log.info line.chomp
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
header = <<EOF
|
31
|
+
================================================================
|
32
|
+
Perforce Revision: #{revision}
|
33
|
+
Time: #{time}
|
34
|
+
User: #{author}
|
35
|
+
Message:
|
36
|
+
EOF
|
37
|
+
@message.each_line do |line|
|
38
|
+
header << " #{line}"
|
39
|
+
end
|
40
|
+
|
41
|
+
header << "\nTotal Files: #{@files.length}\n"
|
42
|
+
@files.each do |file|
|
43
|
+
header << " #{file}\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
header
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Used to build commit information from the pretty much
|
51
|
+
# crazy data the P4 library returns
|
52
|
+
class CommitBuilder
|
53
|
+
include Perforce2Svn::Logging
|
54
|
+
|
55
|
+
def initialize(mappings)
|
56
|
+
@mappings = mappings
|
57
|
+
@log_converter = Iconv.new('UTF-8//IGNORE/TRANSLIT', 'UTF-8')
|
58
|
+
end
|
59
|
+
|
60
|
+
def commits_in(version_range, &block)
|
61
|
+
raise ArgumentError, "Requires a block" unless block_given?
|
62
|
+
if version_range.synced_to_head?
|
63
|
+
version_range.reset_to_head(P4Depot.instance.latest_revision)
|
64
|
+
end
|
65
|
+
|
66
|
+
skipped_previous = false
|
67
|
+
version_range.min.upto(version_range.max) do |revision|
|
68
|
+
commit = commit_at(revision)
|
69
|
+
if commit
|
70
|
+
if skipped_previous
|
71
|
+
print "\n"
|
72
|
+
end
|
73
|
+
skipped_previous = false
|
74
|
+
yield commit
|
75
|
+
else
|
76
|
+
if log.debug?
|
77
|
+
log.info "Skipping irrelevant revision: #{revision}"
|
78
|
+
elsif skipped_previous
|
79
|
+
print "\r[INFO] Skipping irrelevant revision: #{revision}"
|
80
|
+
else
|
81
|
+
print "[INFO] Skipping irrelevant revision: #{revision}"
|
82
|
+
end
|
83
|
+
skipped_previous = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def commit_at(revision)
|
89
|
+
P4Depot.instance.query do |p4|
|
90
|
+
log.debug "PERFORCE: Inspecting revision: #{revision}"
|
91
|
+
raw_commit = p4.run('describe', '-s', "#{revision}")[0]
|
92
|
+
return build_from(raw_commit)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Builds from a raw P4 library return
|
97
|
+
def build_from(raw_commit)
|
98
|
+
changes = unpack_file_changes(raw_commit)
|
99
|
+
return nil unless changes
|
100
|
+
|
101
|
+
revision = raw_commit['change'].to_i
|
102
|
+
author = raw_commit['user']
|
103
|
+
commit_log = @log_converter.iconv(raw_commit['desc'].gsub(/\r/, ''))
|
104
|
+
time = Time.at(raw_commit['time'].to_i)
|
105
|
+
|
106
|
+
PerforceCommit.new(revision, author, commit_log, time, changes)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
# The data structures returned from the P4 library
|
111
|
+
# are pretty much unusable, so we have to munge them
|
112
|
+
# into better objects
|
113
|
+
def unpack_file_changes(raw_commit)
|
114
|
+
depot_files = raw_commit['depotFile']
|
115
|
+
if depot_files.nil? || depot_files.length == 0
|
116
|
+
log.debug "No files present"
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
|
120
|
+
file_changes = []
|
121
|
+
actions = raw_commit['action']
|
122
|
+
types = raw_commit['type']
|
123
|
+
revisions = raw_commit['rev']
|
124
|
+
|
125
|
+
filter_changes(depot_files) do |i, src, dest|
|
126
|
+
action = actions[i]
|
127
|
+
type = types[i]
|
128
|
+
rev = revisions[i].to_i
|
129
|
+
file_changes << PerforceFile.new(rev, src, dest, type, action)
|
130
|
+
end
|
131
|
+
|
132
|
+
if file_changes.empty?
|
133
|
+
log.debug "No relevant files"
|
134
|
+
nil
|
135
|
+
else
|
136
|
+
file_changes
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def filter_changes(depot_files, &block)
|
141
|
+
depot_files.each_index do |i|
|
142
|
+
src = depot_files[i]
|
143
|
+
dest = find_svn_path(src)
|
144
|
+
yield i, src, dest if dest
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# TODO: Add regular expression joining
|
149
|
+
def find_svn_path(perforce_path)
|
150
|
+
@mappings.each do |mapping|
|
151
|
+
log.debug "Checking path: #{perforce_path}"
|
152
|
+
if mapping.matches_perforce_path? perforce_path
|
153
|
+
return mapping.to_svn_path perforce_path
|
154
|
+
end
|
155
|
+
end
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
end#CommitBuilder
|
159
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'perforce2svn/logging'
|
2
|
+
require 'perforce2svn/errors'
|
3
|
+
require 'P4'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
module Perforce2Svn::Perforce
|
7
|
+
class P4Depot
|
8
|
+
include Singleton
|
9
|
+
include Perforce2Svn::Logging
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@p4 = P4.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def connect!
|
16
|
+
handle_errors do
|
17
|
+
@p4.connect unless @p4.connected?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def disconnect!
|
22
|
+
begin
|
23
|
+
@p4.disconnect if @p4.connected?
|
24
|
+
rescue Exception => e
|
25
|
+
log.fatal(e)
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def query(&block)
|
31
|
+
raise Perforce2Svn::P4Error, "Requires a block" unless block_given?
|
32
|
+
connect!
|
33
|
+
handle_errors do
|
34
|
+
yield @p4
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Retrieves the latest revision on the Perforce server
|
39
|
+
def latest_revision
|
40
|
+
if @latest_revision.nil?
|
41
|
+
query do |p4|
|
42
|
+
log.debug "Retrieving latest perforce revision"
|
43
|
+
output = p4.run("changes", "-m1")[0]
|
44
|
+
@latest_revision = output['change'].to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@latest_revision
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def handle_errors(&block)
|
52
|
+
begin
|
53
|
+
yield
|
54
|
+
rescue P4Exception => e
|
55
|
+
@p4.warnings.each do |warning|
|
56
|
+
log.debug "PERFORCE: Skipping warning: #{warning}"
|
57
|
+
end
|
58
|
+
if @p4.errors.length > 0
|
59
|
+
log.error e
|
60
|
+
@p4.errors.each do |error|
|
61
|
+
log.error "PERFORCE: #{error}"
|
62
|
+
end
|
63
|
+
log.fatal "PERFORCE: Are you currently logged into the Perforce server? "
|
64
|
+
raise Perforce2Svn::P4Error, "Error while interacting with the Perforce server"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end # P4Depot
|
69
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'perforce2svn/errors'
|
2
|
+
require 'perforce2svn/logging'
|
3
|
+
require 'perforce2svn/perforce/p4_depot'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
module Perforce2Svn::Perforce
|
7
|
+
# The collection of properties about a Perforce file
|
8
|
+
class PerforceFile
|
9
|
+
include Perforce2Svn::Logging
|
10
|
+
|
11
|
+
attr_reader :revision, :src, :dest, :type, :action
|
12
|
+
|
13
|
+
def initialize(revision, src, dest, type, action)
|
14
|
+
@revision = revision
|
15
|
+
@src = src
|
16
|
+
@dest = dest
|
17
|
+
@type = type
|
18
|
+
@action = action
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is this a binary file?
|
22
|
+
def binary?
|
23
|
+
type =~ /binary/
|
24
|
+
end
|
25
|
+
|
26
|
+
# Is this a symlink
|
27
|
+
def symlink?
|
28
|
+
type =~ /symlink/
|
29
|
+
end
|
30
|
+
|
31
|
+
# Was it deleted in the last commit?
|
32
|
+
def deleted?
|
33
|
+
action == 'delete'
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"(#{@action}:#{@type}\##{@revision})\t#{@src}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieves the target of a symlink
|
41
|
+
def symlink_target
|
42
|
+
p4query do |p4|
|
43
|
+
return p4.run('print', '-q', "#{@src}\##{@revision}")[0].strip
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pulls the file contents for a given path for the
|
48
|
+
# specific file revision
|
49
|
+
def contents
|
50
|
+
streamed_contents do |stream|
|
51
|
+
return stream.read
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Pull a stream from a file at a specified file revision
|
56
|
+
def streamed_contents(&block)
|
57
|
+
raise Perforce2Svn::P4Error, "Requires a block to pull the file stream" unless block_given?
|
58
|
+
log.debug { "PERFORCE: Reading file: #{@src}\##{@revision}" }
|
59
|
+
|
60
|
+
tmpfile = File.join(Dir.tmpdir, ".p4file-#{rand}")
|
61
|
+
begin
|
62
|
+
P4Depot.instance.query do |p4|
|
63
|
+
p4.run('print', '-o', tmpfile, '-q', "#{@src}\##{@revision}")
|
64
|
+
end
|
65
|
+
|
66
|
+
if !File.file? tmpfile
|
67
|
+
raise Perforce2Svn::P4Error, "Unable to retrieve the file contents: #{src}\##{revision}"
|
68
|
+
end
|
69
|
+
|
70
|
+
mode = binary? ? 'rb' : 'r'
|
71
|
+
File.open(tmpfile, mode) do |file|
|
72
|
+
yield file
|
73
|
+
end
|
74
|
+
ensure
|
75
|
+
if File.file? tmpfile
|
76
|
+
File.delete(tmpfile)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end#PerforceFile
|
81
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'perforce2svn/logging'
|
2
|
+
require 'perforce2svn/errors'
|
3
|
+
require 'perforce2svn/subversion/svn_transaction'
|
4
|
+
require 'svn/repos'
|
5
|
+
require 'svn/core'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'iconv'
|
8
|
+
|
9
|
+
module Perforce2Svn::Subversion
|
10
|
+
class SvnRepo
|
11
|
+
include Perforce2Svn::Logging
|
12
|
+
|
13
|
+
attr_reader :repository_path
|
14
|
+
|
15
|
+
# Initializes a repository at a given path.
|
16
|
+
# IF that repository does not exist, it creates one.
|
17
|
+
def initialize(repository_path)
|
18
|
+
raise ArgumentError, "No path given" if repository_path.nil?
|
19
|
+
|
20
|
+
@repository_path = repository_path
|
21
|
+
|
22
|
+
unless File.directory? repository_path
|
23
|
+
fs_config = {
|
24
|
+
Svn::Fs::CONFIG_FS_TYPE => Svn::Fs::TYPE_FSFS
|
25
|
+
}
|
26
|
+
Svn::Repos.create(@repository_path, {}, fs_config)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def transaction(author, time, log_message, &block)
|
31
|
+
if not block_given?
|
32
|
+
raise Perforce2Svn::SvnTransactionError, "Transactions must be block-scoped"
|
33
|
+
end
|
34
|
+
|
35
|
+
if author.nil? or author == '' or time.nil?
|
36
|
+
raise "The author or time was empty"
|
37
|
+
end
|
38
|
+
|
39
|
+
props = {
|
40
|
+
Svn::Core::PROP_REVISION_AUTHOR => author,
|
41
|
+
Svn::Core::PROP_REVISION_DATE => time.to_svn_format,
|
42
|
+
Svn::Core::PROP_REVISION_LOG => sanitize(log_message)
|
43
|
+
}
|
44
|
+
|
45
|
+
begin
|
46
|
+
# Yield the transaction
|
47
|
+
txn = repository.transaction_for_commit(props)
|
48
|
+
svn_txn = SvnTransaction.new(txn, repository.fs.root)
|
49
|
+
yield svn_txn
|
50
|
+
# Finalize the transaction
|
51
|
+
svn_txn.send(:finalize!)
|
52
|
+
|
53
|
+
if not repository.fs.transactions.include?(txn.name)
|
54
|
+
log.fatal "Unable to commit the transaction to the repository (#{txn.name}): #{author} #{time}"
|
55
|
+
raise Perforce2Svn::SvnTransactionError, "Transaction doesn't exist in repository."
|
56
|
+
end
|
57
|
+
|
58
|
+
svn_revision = repository.commit(txn)
|
59
|
+
# It doesn't look like the 'svn:date'
|
60
|
+
# property gets set correctly during
|
61
|
+
# a commit, so we need to update it now
|
62
|
+
repository.set_prop(author, # Person authorizing change
|
63
|
+
Svn::Core::PROP_REVISION_DATE, # Property to modify
|
64
|
+
time.to_svn_format, # value
|
65
|
+
svn_revision, # revision
|
66
|
+
nil, # callbacks
|
67
|
+
false, # run pre-commit hooks
|
68
|
+
false) # run post-commit hooks
|
69
|
+
log.info("Committed Subversion revision: #{svn_revision}")
|
70
|
+
return svn_revision
|
71
|
+
rescue Exception => e
|
72
|
+
clean_transactions!
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Deletes a repository
|
78
|
+
def delete!
|
79
|
+
if File.exists? @repository_path
|
80
|
+
FileUtils.rm_rf @repository_path
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Occasionally, we may interrupt a transaction in
|
85
|
+
# process. In that case, we should make sure
|
86
|
+
# to clean up after we are done.
|
87
|
+
def clean_transactions!
|
88
|
+
`svnadmin lstxns #{@repository_path}`.each do |txn|
|
89
|
+
`svnadmin rmtxns #{@repository_path} #{txn}`
|
90
|
+
if $? != 0
|
91
|
+
log.error "Unable to clean transaction: #{txn}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Retrieves the current contents of the file
|
97
|
+
# in the SVN repository at the given path.
|
98
|
+
# You can optionally supply the revision number
|
99
|
+
#
|
100
|
+
# Raises a Svn::Error::FsNotFound if the specific file path cannot
|
101
|
+
# be found.
|
102
|
+
#
|
103
|
+
# Raises a Svn::Error::FsNoSuchRevision if the revision cannot
|
104
|
+
# be found.
|
105
|
+
def pull_contents(file_path, revision = nil)
|
106
|
+
repository.fs.root(revision).file_contents(file_path){|f| f.read}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Checks that a path exists at a revision
|
110
|
+
def exists?(path, revision = nil)
|
111
|
+
repository.fs.root(revision).check_path(path) != 0
|
112
|
+
end
|
113
|
+
|
114
|
+
# Retrieve the commit log for a given revision number
|
115
|
+
def commit_log(revision)
|
116
|
+
author = repository.prop(Svn::Core::PROP_REVISION_AUTHOR, revision)
|
117
|
+
date = repository.prop(Svn::Core::PROP_REVISION_DATE, revision)
|
118
|
+
commit_log = repository.prop(Svn::Core::PROP_REVISION_LOG, revision)
|
119
|
+
timestamp = Time.parse_svn_format(date)
|
120
|
+
|
121
|
+
SvnCommitInfo.new(revision, author, timestamp, commit_log)
|
122
|
+
end
|
123
|
+
|
124
|
+
def prop_get(path, prop_name, revision = nil)
|
125
|
+
repository.fs.root(revision).node_prop(path, prop_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
def children(path, revision = nil)
|
129
|
+
repository.fs.root(revision).dir_entries(path).keys
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
def repository
|
134
|
+
@repository ||= Svn::Repos.open(@repository_path)
|
135
|
+
end
|
136
|
+
|
137
|
+
# There can be weird stuff in the log messages, so
|
138
|
+
# we make sure that it doesn't bork when committing
|
139
|
+
# FIXME: This is replicated in the PerforceCommit, refactor!
|
140
|
+
def sanitize(text)
|
141
|
+
@sanitizer ||= Iconv.new('UTF-8//IGNORE/TRANSLIT', 'UTF-8')
|
142
|
+
@sanitizer.iconv(text.gsub(/\r/, ''))
|
143
|
+
end
|
144
|
+
end # SvnRepo
|
145
|
+
|
146
|
+
class SvnCommitInfo
|
147
|
+
attr_reader :revision, :author, :timestamp, :log
|
148
|
+
|
149
|
+
def initialize(revision, author, timestamp, log)
|
150
|
+
@revision = revision
|
151
|
+
@author = author
|
152
|
+
@timestamp = timestamp
|
153
|
+
@log = log
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|