realityforge-piston 1.4.1

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.
@@ -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