realityforge-piston 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "piston"
2
+
3
+ module Piston
4
+ # Raised whenever an argument is not correct during processing.
5
+ class CommandError < ArgumentError; end
6
+ end
@@ -0,0 +1,80 @@
1
+ require "piston"
2
+ require "piston/command"
3
+ require "piston/commands/import"
4
+
5
+ module Piston
6
+ module Commands
7
+ class Convert < Piston::Command
8
+ def run
9
+ if args.empty? then
10
+ svn(:propget, '--recursive', 'svn:externals').each_line do |line|
11
+ next unless line =~ /^([^ ]+)\s-\s/
12
+ args << $1
13
+ end
14
+ end
15
+
16
+ return logging_stream.puts("No svn:externals defined in this folder or any of it's subfolders") if args.empty?
17
+
18
+ args.each do |dir|
19
+ externals = svn(:propget, 'svn:externals', dir)
20
+ next skip_no_externals(dir) if externals.chomp.empty?
21
+
22
+ operations = Array.new
23
+ externals.each_line do |external|
24
+ external.chomp!
25
+ next if external.empty?
26
+ next skip_no_match(external) unless external =~ /^([^ ]+)\s+(?:-r\s*(\d+)\s+)?(.*)$/
27
+
28
+ local, revision, repos = $1, $2, $3
29
+ lock = true if revision
30
+ local_dir = File.join(dir, local)
31
+ if File.exists?(local_dir)
32
+ raise Piston::CommandError, "#{local_dir.inspect} is not a directory" unless File.directory?(local_dir)
33
+ status = svn(:status, local_dir)
34
+ raise Piston::CommandError, "#{local_dir.inspect} has local modifications:\n#{status}\nYour must revert or commit before trying again." unless status.empty?
35
+ info = YAML::load(svn(:info, local_dir))
36
+ revision = info['Last Changed Rev'] unless revision
37
+ FileUtils.rm_rf(local_dir)
38
+ end
39
+
40
+ operations << [local_dir, revision, repos, lock]
41
+ end
42
+
43
+ operations.each do |local_dir, revision, repos, lock|
44
+ logging_stream.puts "Importing '#{repos}' to #{local_dir} (-r #{revision || 'HEAD'}#{' locked' if lock})"
45
+ import = Piston::Commands::Import.new([repos, local_dir], {})
46
+ import.revision = revision
47
+ import.verbose, import.quiet, import.logging_stream = self.verbose, self.quiet, self.logging_stream
48
+ import.lock = lock
49
+ import.run
50
+ logging_stream.puts
51
+ end
52
+ end
53
+
54
+ svn :propdel, 'svn:externals', *args
55
+ logging_stream.puts "Done converting existing svn:externals to Piston"
56
+ end
57
+
58
+ def skip_no_externals(dir)
59
+ logging_stream.puts "Skipping '#{dir}' - no svn:externals definition"
60
+ end
61
+
62
+ def skip_no_match(external)
63
+ logging_stream.puts "#{external.inspect} did not match Regexp"
64
+ end
65
+
66
+ def self.help
67
+ "Converts existing svn:externals into Piston managed folders"
68
+ end
69
+
70
+ def self.detailed_help
71
+ <<EOF
72
+ usage: convert [DIR [...]]
73
+
74
+ Converts folders which have the svn:externals property set to Piston managed
75
+ folders.
76
+ EOF
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,55 @@
1
+ require "piston"
2
+ require "piston/command"
3
+ require 'find'
4
+
5
+ module Piston
6
+ module Commands
7
+ class Diff < Piston::Command
8
+ def run
9
+ (args.empty? ? find_targets : args).each do |dir|
10
+ diff dir
11
+ end
12
+ end
13
+
14
+ def diff(dir)
15
+ return unless File.directory?(dir)
16
+ logging_stream.puts "Processing '#{dir}'..."
17
+ repos = svn(:propget, Piston::ROOT, dir).chomp
18
+ uuid = svn(:propget, Piston::UUID, dir).chomp
19
+ remote_revision = svn(:propget, Piston::REMOTE_REV, dir).chomp.to_i
20
+
21
+ logging_stream.puts " Fetching remote repository's latest revision and UUID"
22
+ info = YAML::load(svn(:info, repos))
23
+ return skip(dir, "Repository UUID changed\n Expected #{uuid}\n Found #{info['Repository UUID']}\n Repository: #{repos}") unless uuid == info['Repository UUID']
24
+
25
+ logging_stream.puts " Checking out repository at revision #{remote_revision}"
26
+ svn :checkout, '--ignore-externals', '--quiet', '--revision', remote_revision, repos, dir.tmp
27
+
28
+ puts run_diff(dir.tmp, dir)
29
+
30
+ logging_stream.puts " Removing temporary files / folders"
31
+ FileUtils.rm_rf dir.tmp
32
+
33
+ end
34
+
35
+ def run_diff(dir1, dir2)
36
+ `diff -urN --exclude=.svn #{dir1} #{dir2}`
37
+ end
38
+
39
+ def self.help
40
+ "Shows the differences between the local repository and the pristine upstream"
41
+ end
42
+
43
+ def self.detailed_help
44
+ <<EOF
45
+ usage: diff [DIR [...]]
46
+
47
+ This operation has the effect of producing a diff between the pristine upstream
48
+ (at the last updated revision) and your local version. In other words, it
49
+ gives you the changes you have made in your repository that have not been
50
+ incorporated upstream.
51
+ EOF
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,75 @@
1
+ require "piston"
2
+ require "piston/command"
3
+
4
+ module Piston
5
+ module Commands
6
+ class Import < Piston::Command
7
+ def run
8
+ raise Piston::CommandError, "Missing REPOS_URL argument" if args.empty?
9
+
10
+ repos, dir = args.shift, args.shift
11
+ raise Piston::CommandError, "Too many arguments" unless args.empty?
12
+ dir = File.basename(URI.parse(repos).path) unless dir
13
+
14
+ if File.exists?(dir) then
15
+ raise Piston::CommandError, "Target folder already exists" unless force
16
+ svn :revert, '--recursive', dir
17
+ FileUtils.rm_rf(dir)
18
+ end
19
+
20
+ my_info = YAML::load(svn(:info, File.join(dir, '..')))
21
+ my_revision = YAML::load(svn(:info, my_info['URL']))['Revision']
22
+ raise Piston::CommandError, "#{File.expand_path(File.join(dir, '..'))} is out of date - run svn update" unless my_info['Revision'] == my_revision
23
+
24
+ info = YAML::load(svn(:info, repos))
25
+ his_revision = revision || info['Revision']
26
+ options = [:export]
27
+ options << ['--revision', his_revision]
28
+ options << '--quiet'
29
+ options << repos
30
+ options << dir
31
+ export = svn options
32
+ export.each_line do |line|
33
+ next unless line =~ /Exported revision (\d+)./i
34
+ @revision = $1
35
+ break
36
+ end
37
+
38
+ # Add so we can set properties
39
+ svn :add, '--non-recursive', '--force', '--quiet', dir
40
+
41
+ # Set the properties
42
+ svn :propset, Piston::ROOT, repos, dir
43
+ svn :propset, Piston::UUID, info['Repository UUID'], dir
44
+ svn :propset, Piston::REMOTE_REV, his_revision, dir
45
+ svn :propset, Piston::LOCAL_REV, my_revision, dir
46
+ svn :propset, Piston::LOCKED, revision, dir if lock
47
+
48
+ # Finish adding. If we get an error, at least the properties will be
49
+ # set and the user can handle the rest
50
+ svn :add, '--force', '--quiet', dir
51
+
52
+ logging_stream.puts "Exported r#{his_revision} from '#{repos}' to '#{dir}'"
53
+ end
54
+
55
+ def self.help
56
+ "Prepares a folder for merge tracking"
57
+ end
58
+
59
+ def self.detailed_help
60
+ <<EOF
61
+ usage: import REPOS_URL [DIR]
62
+
63
+ Exports the specified REPOS_URL (which must be a Subversion repository) to
64
+ DIR, defaulting to the last component of REPOS_URL if DIR is not present.
65
+
66
+ If the local folder already exists, this command will abort with an error.
67
+ EOF
68
+ end
69
+
70
+ def self.aliases
71
+ %w(init)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,30 @@
1
+ require "piston"
2
+ require "piston/command"
3
+
4
+ module Piston
5
+ module Commands
6
+ class Lock < Piston::Command
7
+ def run
8
+ raise Piston::CommandError, "No targets to run against" if args.empty?
9
+
10
+ args.each do |dir|
11
+ remote_rev = svn(:propget, Piston::REMOTE_REV, dir).chomp.to_i
12
+ svn :propset, Piston::LOCKED, remote_rev, dir
13
+ logging_stream.puts "'#{dir}' locked at revision #{remote_rev}"
14
+ end
15
+ end
16
+
17
+ def self.help
18
+ "Lock one or more folders to their current revision"
19
+ end
20
+
21
+ def self.detailed_help
22
+ <<EOF
23
+ usage: lock DIR [DIR [...]]
24
+
25
+ Locked folders will not be updated to the latest revision when updating.
26
+ EOF
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,82 @@
1
+ require "piston"
2
+ require "piston/command"
3
+ require 'pp'
4
+
5
+ module Piston
6
+ module Commands
7
+ class Status < Piston::Command
8
+ def run
9
+ # First, find the list of pistoned folders
10
+ folders = svn(:propget, '--recursive', Piston::ROOT, *args)
11
+ repos = Hash.new
12
+ folders.each_line do |line|
13
+ next unless line =~ /(\w.*) - /
14
+ repos[$1] = Hash.new
15
+ end
16
+
17
+ # Then, get their properties
18
+ repo = nil
19
+ last_piston_key = nil
20
+ svn(:proplist, '--verbose', *repos.keys).each_line do |line|
21
+ case line
22
+ when /'([^']+)'/
23
+ repo = repos[$1]
24
+ when /(piston:[-\w]+)\s*:\s*(.*)$/
25
+ repo[$1] = $2
26
+ when /(piston:[-\w]+)\s$/
27
+ last_piston_key = $1
28
+ when /^\s*(.*)\s*$/
29
+ repo[last_piston_key] = $1 if last_piston_key
30
+ last_piston_key = nil
31
+ else
32
+ last_piston_key = nil
33
+ end
34
+ end
35
+
36
+ # Determine their local status
37
+ repos.each_pair do |path, props|
38
+ log = svn(:log, '--revision', "#{props[Piston::LOCAL_REV]}:HEAD", '--quiet', '--limit', '2', path)
39
+ props[:locally_modified] = 'M' if log.count("\n") > 3
40
+ end
41
+
42
+ # And their remote status, if required
43
+ repos.values.each do |props|
44
+ log = svn(:log, '--revision', "#{props[Piston::REMOTE_REV]}:HEAD", '--quiet', '--limit', '2', props[Piston::ROOT])
45
+ props[:remotely_modified] = 'M' if log.count("\n") > 3
46
+ end if show_updates
47
+
48
+ # Display the results
49
+ repos.each_pair do |path, props|
50
+ logging_stream.printf "%1s%1s %5s %s (%s)\n", props[:locally_modified],
51
+ props[:remotely_modified], props[Piston::LOCKED], path, props[Piston::ROOT]
52
+ end
53
+
54
+ logging_stream.puts "No pistoned folders found" if repos.empty?
55
+ end
56
+
57
+ def self.help
58
+ "Determines the current status of each pistoned directory"
59
+ end
60
+
61
+ def self.detailed_help
62
+ <<EOF
63
+ usage: status [DIR [DIR...]]
64
+
65
+ Shows the status of one, many or all pistoned folders. The status is
66
+ returned in columns.
67
+
68
+ The first column's values are:
69
+ : Locally unchanged (space)
70
+ M: Locally modified since importing
71
+
72
+ The second column's values are blanks, unless the --show-updates is passed.
73
+ M: Remotely modified since importing
74
+ EOF
75
+ end
76
+
77
+ def self.aliases
78
+ %w(st)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,139 @@
1
+ require "piston"
2
+ require "piston/command"
3
+
4
+ module Piston
5
+ module Commands
6
+ class Switch < Piston::Command
7
+ def run
8
+ new_root, dir = args.shift, args.shift
9
+ raise Piston::CommandError, "Expected two arguments only to switch. Unrecognized arguments: #{args.inspect}" unless args.empty?
10
+ raise Piston::CommandError, "Expected a new vendor repository URL." if new_root.nil?
11
+ raise Piston::CommandError, "Expected a directory to update." if dir.nil?
12
+ switch(dir, new_root)
13
+ end
14
+
15
+ def switch(dir, new_repos)
16
+ return unless File.directory?(dir)
17
+ return skip(dir, "locked") unless svn(:propget, LOCKED, dir) == ''
18
+ status = svn(:status, '--show-updates', dir)
19
+ new_local_rev = nil
20
+ new_status = Array.new
21
+ status.each_line do |line|
22
+ if line =~ /status.+\s(\d+)$/i then
23
+ new_local_rev = $1.to_i
24
+ else
25
+ new_status << line unless line =~ /^\?/
26
+ end
27
+ end
28
+ raise "Unable to parse status\n#{status}" unless new_local_rev
29
+ return skip(dir, "pending updates -- run \"svn update #{dir}\"\n#{new_status}") if new_status.size > 0
30
+
31
+ logging_stream.puts "Processing '#{dir}'..."
32
+ repos = svn(:propget, Piston::ROOT, dir).chomp
33
+ uuid = svn(:propget, Piston::UUID, dir).chomp
34
+ remote_revision = svn(:propget, Piston::REMOTE_REV, dir).chomp.to_i
35
+ local_revision = svn(:propget, Piston::LOCAL_REV, dir).chomp.to_i
36
+ local_revision = local_revision.succ
37
+
38
+ new_info = YAML::load(svn(:info, new_repos))
39
+ raise Piston::CommandError, "Switching repositories is not supported at this time\nYou initially imported from #{uuid}, but are now importing from #{new_info['Repository UUID']}" unless uuid == new_info['Repository UUID']
40
+
41
+ logging_stream.puts " Fetching remote repository's latest revision and UUID"
42
+ info = YAML::load(svn(:info, "#{repos}@#{remote_revision}"))
43
+ return skip(dir, "Repository UUID changed\n Expected #{uuid}\n Found #{info['Repository UUID']}\n Repository: #{repos}") unless uuid == info['Repository UUID']
44
+
45
+ new_remote_rev = new_info['Last Changed Rev'].to_i
46
+ revisions = (remote_revision .. (revision || new_remote_rev))
47
+
48
+ logging_stream.puts " Restoring remote repository to known state at r#{revisions.first}"
49
+ svn :checkout, '--ignore-externals', '--quiet', '--revision', revisions.first, "#{repos}@#{remote_revision}", dir.tmp
50
+
51
+ logging_stream.puts " Updating remote repository to #{new_repos}@#{revisions.last}"
52
+ updates = svn :switch, '--revision', revisions.last, new_repos, dir.tmp
53
+
54
+ logging_stream.puts " Processing adds/deletes"
55
+ merges = Array.new
56
+ changes = 0
57
+ updates.each_line do |line|
58
+ next unless line =~ %r{^([A-Z]).*\s+#{Regexp.escape(dir.tmp)}[\\/](.+)$}
59
+ op, file = $1, $2
60
+ changes += 1
61
+
62
+ case op
63
+ when 'A'
64
+ if File.directory?(File.join(dir.tmp, file)) then
65
+ svn :mkdir, '--quiet', File.join(dir, file)
66
+ else
67
+ copy(dir, file)
68
+ svn :add, '--quiet', '--force', File.join(dir, file)
69
+ end
70
+ when 'D'
71
+ svn :remove, '--quiet', '--force', File.join(dir, file)
72
+ else
73
+ copy(dir, file)
74
+ merges << file
75
+ end
76
+ end
77
+
78
+ # Determine if there are any local changes in the pistoned directory
79
+ log = svn(:log, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn, '--limit', '2', dir)
80
+
81
+ # If none, we skip the merge process
82
+ if local_revision < new_local_rev && log.count("\n") > 3 then
83
+ logging_stream.puts " Merging local changes back in"
84
+ merges.each do |file|
85
+ begin
86
+ svn(:merge, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn,
87
+ File.join(dir, file), File.join(dir, file))
88
+ rescue RuntimeError
89
+ next if $!.message =~ /Unable to find repository location for/
90
+ end
91
+ end
92
+ end
93
+
94
+ logging_stream.puts " Removing temporary files / folders"
95
+ FileUtils.rm_rf dir.tmp
96
+
97
+ logging_stream.puts " Updating Piston properties"
98
+ svn :propset, Piston::ROOT, new_repos, dir
99
+ svn :propset, Piston::REMOTE_REV, revisions.last, dir
100
+ svn :propset, Piston::LOCAL_REV, new_local_rev, dir
101
+ svn :propset, Piston::LOCKED, revisions.last, dir if lock
102
+
103
+ logging_stream.puts " Updated to r#{revisions.last} (#{changes} changes)"
104
+ end
105
+
106
+ def copy(dir, file)
107
+ FileUtils.cp(File.join(dir.tmp, file), File.join(dir, file))
108
+ end
109
+
110
+ def skip(dir, msg, header=true)
111
+ logging_stream.print "Skipping '#{dir}': " if header
112
+ logging_stream.puts msg
113
+ end
114
+
115
+ def self.help
116
+ "Switches a single directory to a new repository root"
117
+ end
118
+
119
+ def self.detailed_help
120
+ <<EOF
121
+ usage: switch NEW_REPOSITORY_ROOT DIR
122
+
123
+ This operation changes the remote location from A to B, keeping local
124
+ changes. If any local modifications were done, they will be preserved.
125
+ If merge conflicts occur, they will not be taken care of, and your subsequent
126
+ commit will fail.
127
+
128
+ Piston will refuse to update a folder if it has pending updates. Run
129
+ 'svn update' on the target folder to update it before running Piston
130
+ again.
131
+ EOF
132
+ end
133
+
134
+ def self.aliases
135
+ %w(sw)
136
+ end
137
+ end
138
+ end
139
+ end