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.
Files changed (44) hide show
  1. data/Gemfile +2 -0
  2. data/Gemfile.lock +38 -0
  3. data/LICENSE +23 -0
  4. data/README.markdown +66 -0
  5. data/Rakefile +24 -0
  6. data/bin/perforce2svn +11 -0
  7. data/lib/VERSION.yml +6 -0
  8. data/lib/perforce2svn/cli.rb +117 -0
  9. data/lib/perforce2svn/environment.rb +66 -0
  10. data/lib/perforce2svn/errors.rb +16 -0
  11. data/lib/perforce2svn/logging.rb +35 -0
  12. data/lib/perforce2svn/mapping/analyzer.rb +30 -0
  13. data/lib/perforce2svn/mapping/branch_mapping.rb +32 -0
  14. data/lib/perforce2svn/mapping/commands.rb +75 -0
  15. data/lib/perforce2svn/mapping/help.txt +139 -0
  16. data/lib/perforce2svn/mapping/lexer.rb +101 -0
  17. data/lib/perforce2svn/mapping/mapping_file.rb +65 -0
  18. data/lib/perforce2svn/mapping/operation.rb +8 -0
  19. data/lib/perforce2svn/mapping/parser.rb +145 -0
  20. data/lib/perforce2svn/migrator.rb +71 -0
  21. data/lib/perforce2svn/perforce/commit_builder.rb +159 -0
  22. data/lib/perforce2svn/perforce/p4_depot.rb +69 -0
  23. data/lib/perforce2svn/perforce/perforce_file.rb +81 -0
  24. data/lib/perforce2svn/subversion/svn_repo.rb +156 -0
  25. data/lib/perforce2svn/subversion/svn_transaction.rb +136 -0
  26. data/lib/perforce2svn/version_range.rb +43 -0
  27. data/mjt.map +7 -0
  28. data/perforce2svn.gemspec +49 -0
  29. data/spec/integration/hamlet.txt +7067 -0
  30. data/spec/integration/madmen_icon_bigger.jpg +0 -0
  31. data/spec/integration/perforce/p4_depot_spec.rb +16 -0
  32. data/spec/integration/perforce/perforce_file.yml +4 -0
  33. data/spec/integration/perforce/perforce_file_spec.rb +19 -0
  34. data/spec/integration/subversion/svn_repo_spec.rb +93 -0
  35. data/spec/integration/subversion/svn_transaction_spec.rb +112 -0
  36. data/spec/perforce2svn/cli_spec.rb +61 -0
  37. data/spec/perforce2svn/mapping/analyzer_spec.rb +41 -0
  38. data/spec/perforce2svn/mapping/branch_mapping_spec.rb +40 -0
  39. data/spec/perforce2svn/mapping/lexer_spec.rb +43 -0
  40. data/spec/perforce2svn/mapping/parser_spec.rb +140 -0
  41. data/spec/perforce2svn/perforce/commit_builder_spec.rb +74 -0
  42. data/spec/perforce2svn/version_range_spec.rb +42 -0
  43. data/spec/spec_helpers.rb +44 -0
  44. 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