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