perforce2svn 0.7.0

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