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.
- data/CHANGELOG +81 -0
- data/LICENSE +19 -0
- data/README.rdoc +134 -0
- data/Rakefile +25 -0
- data/bin/piston +10 -0
- data/contrib/piston +43 -0
- data/lib/core_ext/range.rb +5 -0
- data/lib/core_ext/string.rb +9 -0
- data/lib/piston.rb +70 -0
- data/lib/piston/command.rb +68 -0
- data/lib/piston/command_error.rb +6 -0
- data/lib/piston/commands/convert.rb +80 -0
- data/lib/piston/commands/diff.rb +55 -0
- data/lib/piston/commands/import.rb +75 -0
- data/lib/piston/commands/lock.rb +30 -0
- data/lib/piston/commands/status.rb +82 -0
- data/lib/piston/commands/switch.rb +139 -0
- data/lib/piston/commands/unlock.rb +29 -0
- data/lib/piston/commands/update.rb +131 -0
- data/lib/piston/version.rb +9 -0
- data/lib/transat/parser.rb +189 -0
- data/piston.gemspec +22 -0
- metadata +95 -0
@@ -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
|