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,32 @@
1
+ require 'perforce2svn/errors'
2
+ require 'perforce2svn/mapping/operation'
3
+
4
+ module Perforce2Svn::Mapping
5
+ class BranchMapping < Operation
6
+ attr_reader :p4_path, :svn_path
7
+
8
+ def initialize(tok, p4_path, svn_path)
9
+ super(tok)
10
+ @p4_path = p4_path
11
+ @svn_path = svn_path
12
+ @path_match = /^#{@p4_path}/
13
+ end
14
+
15
+ def p4_dotted
16
+ @p4_path + '...'
17
+ end
18
+
19
+ def matches_perforce_path?(other_p4_path)
20
+ (other_p4_path =~ @path_match) != nil
21
+ end
22
+
23
+ def to_svn_path(other_p4_path)
24
+ opath = other_p4_path.gsub(p4_path, svn_path)
25
+ opath.gsub!("%40", "@")
26
+ opath.gsub!("%23", "#")
27
+ opath.gsub!("%2a", "*")
28
+ opath.gsub!("%25", "%")
29
+ opath
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,75 @@
1
+ require 'perforce2svn/mapping/operation'
2
+
3
+ module Perforce2Svn::Mapping
4
+ class Command < Operation
5
+ # Must be implemented by subclasses
6
+ def execute!(svn_txn)
7
+ raise Exception, "Not implemented"
8
+ end
9
+ end
10
+
11
+ class Copy < Command
12
+ attr_reader :svn_from, :svn_to
13
+ def initialize(tok,svn_from, svn_to)
14
+ super(tok)
15
+ @svn_from = svn_from
16
+ @svn_to = svn_to
17
+ end
18
+
19
+ def execute!(svn_txn)
20
+ svn_txn.copy(@svn_from, @svn_to)
21
+ end
22
+ end
23
+
24
+ class Mkdir < Command
25
+ attr_reader :svn_path
26
+ def initialize(tok, svn_path)
27
+ super(tok)
28
+ @svn_path = svn_path
29
+ end
30
+
31
+ def execute!(svn_txn)
32
+ svn_txn.mkdir(@svn_path)
33
+ end
34
+ end
35
+
36
+ class Move < Command
37
+ attr_reader :svn_from, :svn_to
38
+ def initialize(tok, svn_from, svn_to)
39
+ super(tok)
40
+ @svn_from = svn_from
41
+ @svn_to = svn_to
42
+ end
43
+
44
+ def execute!(svn_txn)
45
+ svn_txn.move(@svn_from, @svn_to)
46
+ end
47
+ end
48
+
49
+ class Update < Command
50
+ attr_reader :svn_path, :live_path
51
+ def initialize(tok, svn_path, live_path)
52
+ super(tok)
53
+ @svn_path = svn_path
54
+ @live_path = live_path
55
+ end
56
+
57
+ def execute!(svn_txn)
58
+ File.open(@live_path, 'r') do |fstream|
59
+ svn_txn.update(@svn_path, fstream)
60
+ end
61
+ end
62
+ end
63
+
64
+ class Delete < Command
65
+ attr_reader :svn_path
66
+ def initialize(tok, svn_path)
67
+ super(tok)
68
+ @svn_path = svn_path
69
+ end
70
+
71
+ def execute!(svn_txn)
72
+ svn_txn.delete(@svn_path)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,139 @@
1
+ ######################################################################
2
+ # MAPPING FILE
3
+ ######################################################################
4
+ #
5
+ # This file has been colored for your convenience. Comments are in blue;
6
+ # commands in green.
7
+ #
8
+ # This is an example mapping file for describing a migration from Perforce
9
+ # into Subversion. Blank lines are ignored, and everything after the
10
+ # '#' character in a line is ignored.
11
+
12
+ ######################################################################
13
+ # Migration Command
14
+ #
15
+ # There are several commands to be aware of, the most central of which is:
16
+ migrate //depot/perforce/path /trunk/absolute/path
17
+
18
+ # As you can see, the migrate command takes 2 arguments: the path to migrate
19
+ # from and the path to migrate to. It cannot handle individual files -- only
20
+ # directory structures.
21
+ #
22
+ # Multiple migrate commands can be given in this file, mapping from Perforce
23
+ # path to Subversion path. This is useful when migrating both 'trunk' and
24
+ # 'branches' inside a repository when a standard layout has not been supplied,
25
+ # or for more significant code refactorings.
26
+ #
27
+ # Each part of the command is separated by spaces, and multiple spaces are
28
+ # ignored.
29
+ #
30
+ # Commands don't necessarily need to be on the same line. If to continue a
31
+ # command on the next line, you can add a '\' character to the end of the
32
+ # line. So, the above command could have been written:
33
+ migrate //depot/perforce/path \
34
+ /trunk/absolute/path
35
+
36
+ # If you have spaces in your path names, those spaces need to be escaped in a
37
+ # similar manner:
38
+ migrate //depot/another\ path\ with/spaces \
39
+ /to\ a\ path/with/spaces
40
+
41
+ ######################################################################
42
+ # Directives
43
+ #
44
+ # There are several directives, all of which are optional.
45
+
46
+ ## author
47
+ # The first directive is 'author'. This directive informs the migration
48
+ # that when the commands (discussed below) are committed to the repository,
49
+ # the name of the author should be used.
50
+ author gabe.mcarthur
51
+ # Thus, when the log is scanned in the future in the SVN repository, this
52
+ # will be the author of the commands. If no author is given, the system
53
+ # will put in 'Perforce2Svn Migration Tool'
54
+
55
+ ## message
56
+ # The next directive is like the first. It attaches the given log message
57
+ # to the commit that occurs when the commands (discussed below) are
58
+ # executed.
59
+ message This is a message about all of the cool things that will be \
60
+ will be done to the repository after the primary Perforce \
61
+ migration is complete. No need to worry about the multiple lines \
62
+ here, as long as the line is terminated with a '\' character.
63
+ # If no message directive is given, the system supplies 'Perforce migration.'
64
+
65
+ ## svn-prefix
66
+ # There is a special directive that you need to be aware of:
67
+ svn-prefix /project/trunk
68
+
69
+ # This directive informs the mapping system that every SVN path farther down
70
+ # in the file that don't start with a '/' character will have this path prefix
71
+ # pre-pended.
72
+ #
73
+ # Paths that start with a '/' are considered absolute and will not be altered.
74
+ #
75
+ # Thus, given the previous directive, the following commands are now
76
+ # equivalent:
77
+ migrate //depot/perforce/path /project/trunk/src/path
78
+ migrate //depot/perforce/path src/path
79
+
80
+ # This feature is a convenience so that you don't have to type out the full
81
+ # path each time, which can become a hassle in a big migration.
82
+ #
83
+ # Note that you can call the 'svn-prefix' directive multiple times, and each
84
+ # relative SVN path that follows will have the directly preceeding directive
85
+ # take precedence.
86
+
87
+ ######################################################################
88
+ # Additional Commands
89
+ #
90
+ # The 'migrate' command is obviously the most essential, but this tool
91
+ # provides several other convenience commands to help you migrate more
92
+ # complex directory structures.
93
+ #
94
+ # Each of these commands is pretty much the same as their SVN counterparts
95
+ # (with a few noted exceptions).
96
+
97
+ ## move
98
+ # Moves paths internally within SVN after the migration:
99
+ move original/svn/path.txt \
100
+ new/svn/path.txt
101
+
102
+ ## delete
103
+ # Deletes paths internally after the migration:
104
+ delete some/perforce/file/thats/no/longer/needed.sh
105
+
106
+ ## copy
107
+ # Copies SVN paths, retaining their history within the tree after a
108
+ # migration
109
+ copy /some/absolute/file.txt \
110
+ to/another/needed/location.txt
111
+
112
+ ## mkdir
113
+ # Creates empty directories. By default, directory structures are created for
114
+ # you when you copy or move files, but this is occasionally useful when you
115
+ # want to set up the standard Subversion layout.
116
+ mkdir /project/branches
117
+ mkdir /project/tags
118
+
119
+ ## update
120
+ # The 'update' command is a bit weird, as it will look at the 'live' sources that
121
+ # you have edited externally and passed in via the '--live-path' flag.
122
+ # This migration tool will add the sources listed below that live path to the
123
+ # SVN repository. If the file doesn't exist in the Subversion repository, it
124
+ # will be created.
125
+ #
126
+ # Obviously, this is an optional command, and if you don't need it, you don't
127
+ # need to supply the '--live-path' flag. If you have this directive, and the
128
+ # '--live-path' flag isn't present, the migration will fail and ask you for the
129
+ # live path
130
+ #
131
+ # The use case for this option lies in that not all migrations happen at once.
132
+ # In particularly large refactorings, it may be necessary to update certain
133
+ # files that rely upon the new source control layout. This is a facility for
134
+ # adding those files transparently at the end of a migration, so that you don't
135
+ # have to add them manually. The mapping analysis tool will flag if any addition
136
+ # doesn't exist before the migration continues.
137
+ update some/live/path/pom.xml
138
+ update some/path/that/exists/in/live/and/in/svn/Klass.java
139
+
@@ -0,0 +1,101 @@
1
+
2
+ module Perforce2Svn
3
+ module Mapping
4
+ class Token
5
+ attr_reader :name, :args, :line_number
6
+
7
+ def initialize(name, args, line_number)
8
+ @name = name.gsub(/-/, '_')
9
+ @args = args
10
+ @line_number = line_number
11
+ end
12
+
13
+ def [](index)
14
+ @args[index]
15
+ end
16
+
17
+ def to_s
18
+ "{@(#{line_number}) name: #{name}; args: #{args.join(' ')} }"
19
+ end
20
+ end
21
+
22
+ class Lexer
23
+ def initialize(content)
24
+ @content = content
25
+ end
26
+
27
+ # Yields each parsed line of the configuration
28
+ def each(&block)
29
+ if not block_given?
30
+ raise ArgumentError, "Requires a block"
31
+ end
32
+
33
+ lines = @content.readlines
34
+ i = 1
35
+ continues_from_previous = false
36
+ previous = []
37
+
38
+ lines.each do |line|
39
+ parts= tokenize(line)
40
+
41
+ will_continue_on_next_line = false
42
+ if line =~ /\\\s*$/
43
+ will_continue_on_next_line = true
44
+ end
45
+
46
+ if parts.length > 0
47
+ if continues_from_previous
48
+ previous << parts
49
+ else
50
+ previous = []
51
+ name = parts.shift
52
+ yield Token.new(name, parts, i)
53
+ end
54
+ end
55
+
56
+ continues_from_previous = will_continue_on_next_line
57
+ i += 1
58
+ end
59
+ end
60
+
61
+ # Reads a line for all of the possible tokens
62
+ def tokenize(line)
63
+ chars = line.scan(/./)
64
+ parts = []
65
+
66
+ part = ""
67
+ i = 0
68
+ while i < chars.length
69
+ current = chars[i]
70
+ if current == '\\'
71
+ nxt = chars[i + 1]
72
+ if nxt == ' '
73
+ i += 1
74
+ part << ' '
75
+ elsif nxt == "\n"
76
+ break
77
+ end
78
+ elsif current == ' '
79
+ if part.length > 0
80
+ parts << part
81
+ part = ""
82
+ end
83
+ elsif current == '#'
84
+ break
85
+ else
86
+ part << current
87
+ end
88
+
89
+ i += 1
90
+ end
91
+
92
+ if not parts[-1].eql?(part)
93
+ parts << part
94
+ end
95
+
96
+ parts.delete_if {|p| p =~ /^\s*$/}
97
+ parts
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,65 @@
1
+ require 'perforce2svn/logging'
2
+ require 'perforce2svn/mapping/parser'
3
+ require 'perforce2svn/mapping/analyzer'
4
+ require 'choosy/terminal'
5
+ require 'choosy/printing/color'
6
+
7
+ module Perforce2Svn::Mapping
8
+ class MappingFile
9
+
10
+ def self.help_file
11
+ map_file = File.join(File.dirname(__FILE__), 'help.txt')
12
+ contents = ""
13
+ color = Choosy::Printing::Color.new
14
+ File.open(map_file, 'r') do |file|
15
+ file.each_line do |line|
16
+ contents << if line =~ /^#/
17
+ color.blue(line)
18
+ elsif line =~ /^([\w-]+)(.*)/
19
+ color.green($1) + $2 + "\n"
20
+ else
21
+ line
22
+ end
23
+ end
24
+ end
25
+
26
+ contents
27
+ end
28
+
29
+ attr_reader :mappings, :commands, :author, :message
30
+
31
+ def initialize(mapping_file, options)
32
+ load_mapping_file(mapping_file, options)
33
+ analyze(mapping_file)
34
+
35
+ if options[:analysis_only]
36
+ exit 0
37
+ end
38
+ end
39
+
40
+ private
41
+ def load_mapping_file(migration_file, options)
42
+ migration = nil
43
+ File.open(migration_file, 'r') do |file|
44
+ parser = Parser.new
45
+ migration = parser.parse!(file, options[:live_path])
46
+ end
47
+
48
+ if migration[:failed]
49
+ Choosy::Terminal.die "Parsing the mapping file failed"
50
+ end
51
+
52
+ @mappings = migration[:mappings]
53
+ @commands = migration[:commands]
54
+ @author = migration[:author]
55
+ @message = migration[:message]
56
+ end
57
+
58
+ def analyze(mapping_file)
59
+ analyzer = Analyzer.new(File.dirname(mapping_file))
60
+ if !analyzer.check(@commands) || !analyzer.check(@mappings)
61
+ Choosy::Terminal.die "Analysis of mapping file failed"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,8 @@
1
+ module Perforce2Svn::Mapping
2
+ class Operation
3
+ attr_reader :line_number
4
+ def initialize(tok)
5
+ @line_number = tok.line_number
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,145 @@
1
+ require 'perforce2svn/logging'
2
+ require 'perforce2svn/mapping/lexer'
3
+ require 'perforce2svn/mapping/commands'
4
+ require 'perforce2svn/mapping/branch_mapping'
5
+
6
+ module Perforce2Svn::Mapping
7
+ class Parser
8
+ include Perforce2Svn::Logging
9
+
10
+ def parse!(content, live_path)
11
+ raise ArgumentError, "The content must respond to 'readlines'" unless content.respond_to? :readlines
12
+
13
+ live_path = live_path.dup
14
+ if !live_path.nil? && live_path !~ /\/$/
15
+ live_path << '/'
16
+ end
17
+
18
+ ctx = {
19
+ :failed => false,
20
+ :commands => [],
21
+ :mappings => [],
22
+ :svn_prefix => '',
23
+ :live_path => live_path,
24
+ :author => 'Perforce2Svn Migration Tool',
25
+ :message => 'Perforce Migration'
26
+ }
27
+
28
+ Lexer.new(content).each do |tok|
29
+ handle(tok, ctx)
30
+ end
31
+ ctx
32
+ end
33
+
34
+ private
35
+ def handle(tok, ctx)
36
+ if private_methods.include? tok.name
37
+ send(tok.name, tok, ctx)
38
+ else
39
+ log.error "(line: #{tok.line_number}) Unknown directive: '#{tok.name}'"
40
+ ctx[:failed] = true
41
+ end
42
+ end
43
+
44
+ def author(tok, ctx)
45
+ ctx[:author] = tok.args.join(' ')
46
+ end
47
+
48
+ def message(tok, ctx)
49
+ ctx[:message] = tok.args.join(' ')
50
+ end
51
+
52
+ def copy(tok, ctx)
53
+ return unless args_ok?(2, tok, ctx)
54
+ ctx[:commands] << Copy.new(tok, format_svn_path(tok, tok[0], ctx), format_svn_path(tok, tok[1], ctx))
55
+ end
56
+
57
+ def delete(tok, ctx)
58
+ return unless args_ok?(1, tok, ctx)
59
+ ctx[:commands] << Delete.new(tok, format_svn_path(tok, tok[0], ctx))
60
+ end
61
+
62
+ def mkdir(tok, ctx)
63
+ return unless args_ok?(1, tok, ctx)
64
+ ctx[:commands] << Mkdir.new(tok, format_svn_path(tok, tok[0], ctx))
65
+ end
66
+
67
+ def move(tok, ctx)
68
+ return unless args_ok?(2, tok, ctx)
69
+ ctx[:commands] << Move.new(tok, format_svn_path(tok, tok[0], ctx), format_svn_path(tok, tok[1], ctx))
70
+ end
71
+
72
+ def update(tok, ctx)
73
+ return unless args_ok?(1, tok, ctx)
74
+ ctx[:commands] << Update.new(tok, format_svn_path(tok, tok[0], ctx), format_live_path(tok, tok[0], ctx))
75
+ end
76
+
77
+ def migrate(tok, ctx)
78
+ return unless args_ok?(2, tok, ctx)
79
+
80
+ p4_path = tok[0]
81
+ p4_path << '/' if not p4_path[-1].chr == '/'
82
+ svn_path = format_svn_path(tok, tok[1], ctx)
83
+ svn_path << '/' if not svn_path[-1].chr == '/'
84
+
85
+ if p4_path !~ %r|^//([^/]+/?)*$|
86
+ log.error "(line #{tok.line_number}) Perforce path was malformed: '#{p4_path}'"
87
+ ctx[:failed] = true
88
+ end
89
+
90
+ if svn_path !~ %r|^/([^/]+/?)*$|
91
+ log.error "(line #{tok.line_number}) Subversion path was malformed: '#{svn_path}'"
92
+ ctx[:failed] = true
93
+ end
94
+
95
+ ctx[:mappings] << BranchMapping.new(tok, p4_path, svn_path)
96
+ end
97
+
98
+ def svn_prefix(tok, ctx)
99
+ return unless args_ok?(1, tok, ctx)
100
+
101
+ svn_prefix = tok[0]
102
+ if svn_prefix !~ /^\//
103
+ log.error "(line: #{tok.line_number}) 'svn-prefix' directive must start with a '/'"
104
+ ctx[:failed] = true
105
+ elsif svn_prefix !~ /\/$/
106
+ svn_prefix << '/'
107
+ end
108
+
109
+ ctx[:svn_prefix] = svn_prefix
110
+ end
111
+
112
+ def args_ok?(required_count, tok, ctx)
113
+ if tok.args.size != required_count
114
+ log.error "(line: #{tok.line_number}) '#{tok.name}' requires #{required_count} argument(s), but found #{tok.args.size}"
115
+ ctx[:failed] = true
116
+ return false
117
+ end
118
+ return true
119
+ end
120
+
121
+ def format_svn_path(tok, path, ctx)
122
+ if ctx[:svn_prefix].empty? && path !~ /^\//
123
+ log.error "(line: #{tok.line_number}) No 'svn-prefix' defined, but a relative SVN path was used"
124
+ ctx[:failed] = true
125
+ nil
126
+ else
127
+ "#{ctx[:svn_prefix]}#{path}"
128
+ end
129
+ end
130
+
131
+ def format_live_path(tok, path, ctx)
132
+ if ctx[:live_path].nil?
133
+ log.error "(line: #{tok.line_number}) The command '#{tok.name}' requires a live path, but none was given."
134
+ ctx[:failed] = true
135
+ nil
136
+ else
137
+ if path =~ /^\//
138
+ "#{ctx[:live_path].chomp('/')}#{path}"
139
+ else
140
+ "#{ctx[:live_path].chomp('/')}#{ctx[:svn_prefix]}#{path}"
141
+ end
142
+ end
143
+ end
144
+ end#Parser
145
+ end