perforce2svn 0.7.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/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
|