Commit 0198019c619ca78fa89c3ce08fdaa432dbb76201

Added Piston::Commands::Update specs and implementation.

git-svn-id: svn+ssh://rubyforge.org/var/svn/piston/trunk@135 d6c2ea82-c31b-0410-8381-e9c44f9824c5

Commit diff

lib/piston.rb

 
1212 end
1313 end
1414
15 class RepositoryUuidChanged < PistonError
16 def initialize(url, expected_uuid, actual_uuid)
17 super "The repository at #{url} changed UUID from #{expected_uuid} to #{actual_uuid} -- Piston will not update/switch"
18 end
19 end
20
21 class MissingRepositoryLocation < PistonError
22 def initialize(url)
23 super "Repository location #{url} does not exist anymore"
24 end
25 end
26
1527 REMOTE_UUID = "piston:remote-uuid"
1628 REMOTE_ROOT = "piston:remote-root"
1729 REMOTE_REVISION = "piston:remote-revision"
toggle raw diff

lib/piston/commands/update.rb

 
11require "piston"
2require "piston/command"
3require 'find'
42
53module Piston
64 module Commands
7 class Update < Piston::Command
8 def run
9 (args.empty? ? find_targets : args).each do |dir|
10 update dir
11 end
12 end
13
14 def find_targets
15 targets = Array.new
16 svn(:propget, '--recursive', Piston::ROOT).each_line do |line|
17 next unless line =~ /^([^ ]+)\s-\s.*$/
18 targets << $1
19 end
20
21 targets
5 class Update
6 def initialize(paths=nil, options={})
7 @options = options
8 @paths = paths.kind_of?(Enumerable) ? paths : [paths]
9 @wcs = @paths.map {|path| Piston::WorkingCopy.new(path, :logger => options[:logger])}
2210 end
2311
24 def update(dir)
25 return unless File.directory?(dir)
26 return skip(dir, "locked") unless svn(:propget, Piston::LOCKED, dir) == ''
27 status = svn(:status, '--show-updates', dir)
28 new_local_rev = nil
29 new_status = Array.new
30 status.each_line do |line|
31 if line =~ /status.+\s(\d+)$/i then
32 new_local_rev = $1.to_i
33 else
34 new_status << line unless line =~ /^\?/
12 def run
13 @wcs.each do |wc|
14 begin
15 update(wc)
16 rescue
17 raise if @wcs.size == 1
18 warn {$!.message}
3519 end
3620 end
37 raise "Unable to parse status\n#{status}" unless new_local_rev
38 return skip(dir, "pending updates -- run \"svn update #{dir}\"\n#{new_status}") if new_status.size > 0
21 end
3922
40 logging_stream.puts "Processing '#{dir}'..."
41 repos = svn(:propget, Piston::ROOT, dir).chomp
42 uuid = svn(:propget, Piston::UUID, dir).chomp
43 remote_revision = svn(:propget, Piston::REMOTE_REV, dir).chomp.to_i
44 local_revision = svn(:propget, Piston::LOCAL_REV, dir).chomp.to_i
45 local_revision = local_revision.succ
23 def update(wc)
24 info {"Updating #{wc}"}
25 vrepos = Piston::Repository.new(wc.propget(Piston::REMOTE_ROOT), :logger => @options[:logger])
4626
47 logging_stream.puts " Fetching remote repository's latest revision and UUID"
48 info = YAML::load(svn(:info, repos))
49 return skip(dir, "Repository UUID changed\n Expected #{uuid}\n Found #{info['Repository UUID']}\n Repository: #{repos}") unless uuid == info['Repository UUID']
27 check_preconditions!(vrepos, wc)
5028
51 new_remote_rev = info['Last Changed Rev'].to_i
52 return skip(dir, "unchanged from revision #{remote_revision}", false) if remote_revision == new_remote_rev
29 new_vendor_head = vrepos.last_changed_rev
30 vwc = Piston::WorkingCopy.new(wc.up.path + ".#{File.basename(wc.path)}.tmp", :logger => @options[:logger])
31 prior_vendor_head = wc.propget(Piston::REMOTE_REVISION)
32 vwc.checkout(vrepos.url, prior_vendor_head)
33 vwc.update(new_vendor_head)
34 changes = vwc.log(prior_vendor_head.succ .. new_vendor_head)
35 debug { changes.inspect }
36 changes.each do |op, filename|
37 case op
38 when :add
39 if File.directory?(vwc.wc_path(filename)) then
40 wc.mkdir(filename)
41 else
42 wc.create!(filename, File.read(vwc.wc_path(filename)))
43 end
5344
54 revisions = (remote_revision .. (revision || new_remote_rev))
45 when :modify
46 wc.edit!(filename, File.read(vwc.wc_path(filename)))
5547
56 logging_stream.puts " Restoring remote repository to known state at r#{revisions.first}"
57 svn :checkout, '--ignore-externals', '--quiet', '--revision', revisions.first, repos, dir.tmp
48 when :delete
49 wc.delete(filename)
5850
59 logging_stream.puts " Updating remote repository to r#{revisions.last}"
60 updates = svn :update, '--ignore-externals', '--revision', revisions.last, dir.tmp
51 when :move
52 from, to = filename.to_a.flatten
53 wc.move(from, to)
6154
62 logging_stream.puts " Processing adds/deletes"
63 merges = Array.new
64 changes = 0
65 updates.each_line do |line|
66 next unless line =~ %r{^([A-Z]).*\s+#{Regexp.escape(dir.tmp)}[\\/](.+)$}
67 op, file = $1, $2
68 changes += 1
55 when :copy
56 from, to = filename.to_a.flatten
57 wc.copy(from, to)
6958
70 case op
71 when 'A'
72 if File.directory?(File.join(dir.tmp, file)) then
73 svn :mkdir, '--quiet', File.join(dir, file)
74 else
75 copy(dir, file)
76 svn :add, '--quiet', '--force', File.join(dir, file)
77 end
78 when 'D'
79 svn :remove, '--quiet', '--force', File.join(dir, file)
8059 else
81 copy(dir, file)
82 merges << file
60 raise SyntaxError, "Unknown repository operation: #{op.inspect}"
8361 end
8462 end
8563
86 # Determine if there are any local changes in the pistoned directory
87 log = svn(:log, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn, '--limit', '2', dir)
88
89 # If none, we skip the merge process
90 if local_revision < new_local_rev && log.count("\n") > 3 then
91 logging_stream.puts " Merging local changes back in"
92 merges.each do |file|
93 begin
94 svn(:merge, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn,
95 File.join(dir, file), File.join(dir, file))
96 rescue RuntimeError
97 next if $!.message =~ /Unable to find repository location for/
98 end
99 end
100 end
64 prior_local_head = wc.propget(Piston::LOCAL_REVISION).to_i.succ
65 new_local_head = wc.youngest
66 changes.map {|op, filename| filename if op == :modify}.compact.each do |filename|
67 wc.merge(filename, prior_local_head .. new_local_head)
68 end unless prior_local_head == new_local_head
10169
102 logging_stream.puts " Removing temporary files / folders"
103 FileUtils.rm_rf dir.tmp
70 wc.propset(Piston::REMOTE_REVISION, new_vendor_head)
71 wc.propset(Piston::LOCAL_REVISION, new_local_head) unless prior_local_head == new_local_head
72 vwc.destroy!
73 end
10474
105 logging_stream.puts " Updating Piston properties"
106 svn :propset, Piston::REMOTE_REV, revisions.last, dir
107 svn :propset, Piston::LOCAL_REV, new_local_rev, dir
108 svn :propset, Piston::LOCKED, revisions.last, dir if lock
75 def check_preconditions!(vrepos, wc)
76 local_vendor_uuid = wc.propget(Piston::REMOTE_UUID)
77 raise UnknownRepository.new(vrepos.url) unless vrepos.exists?
10978
110 logging_stream.puts " Updated to r#{revisions.last} (#{changes} changes)"
79 vendor_uuid = vrepos.uuid
80 raise RepositoryUuidChanged.new(vrepos.url, local_vendor_uuid, vendor_uuid) unless local_vendor_uuid == vendor_uuid
11181 end
11282
113 def copy(dir, file)
114 FileUtils.cp(File.join(dir.tmp, file), File.join(dir, file))
83 def debug(&block)
84 self.logger.debug(&block) if self.logger
11585 end
11686
117 def skip(dir, msg, header=true)
118 logging_stream.print "Skipping '#{dir}': " if header
119 logging_stream.puts msg
87 def info(&block)
88 self.logger.info(&block) if self.logger
12089 end
12190
122 def self.help
123 "Updates all or specified folders to the latest revision"
91 def warn(&block)
92 self.logger.warn(&block) if self.logger
12493 end
12594
126 def self.detailed_help
127 <<EOF
128usage: update [DIR [...]]
129
130 This operation has the effect of downloading all remote changes back to our
131 working copy. If any local modifications were done, they will be preserved.
132 If merge conflicts occur, they will not be taken care of, and your subsequent
133 commit will fail.
95 def error(&block)
96 self.logger.error(&block) if self.logger
97 end
13498
135 Piston will refuse to update a folder if it has pending updates. Run
136 'svn update' on the target folder to update it before running Piston
137 again.
138EOF
99 def fatal(&block)
100 self.logger.fatal(&block) if self.logger
139101 end
140102
141 def self.aliases
142 %w(up)
103 def logger
104 @options[:logger]
143105 end
144106 end
145107 end
toggle raw diff

spec/update_spec.rb

 
1require File.dirname(__FILE__) + "/spec_helper"
2require "piston/commands/import"
3require "piston/commands/update"
4
5describe Piston::Commands::Update, "#run" do
6 it_should_behave_like "An upstream repository with no copies/renames"
7
8 it "should find all pistonized folders and update them in turn"
9end
10
11describe Piston::Commands::Update, "#run('vendor')" do
12 it_should_behave_like "An upstream repository with no copies/renames"
13 it_should_behave_like "A working copy against a local repository"
14
15 before do
16 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
17 @wc.commit "Pistonized vendor@r1"
18 @vendor = @wc.down("vendor")
19
20 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
21 end
22
23 after do
24 @wc.destroy!
25 end
26
27 it "should receive new folders" do
28 @upwc.mkdir("lib")
29 @upwc.create!("lib/calc.c", "int calc() { /* TBD */ }")
30 @upwc.commit
31 @command.run
32 status = @wc.status.map {|st| st.sub(@wc.path, "")}
33 status.sort.should == [
34 " M /vendor",
35 "A /vendor/lib",
36 "A /vendor/lib/calc.c",
37 "A /vendor/LICENSE",
38 "A /vendor/README",
39 "M /vendor/main.c",
40 "M /vendor/CHANGELOG"].sort
41 end
42end
43
44describe Piston::Commands::Update, "#run('vendor')" do
45 it_should_behave_like "An upstream repository with no copies/renames"
46 it_should_behave_like "A working copy against a local repository"
47
48 before do
49 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
50 @wc.commit "Pistonized vendor@r1"
51 @vendor = @wc.down("vendor")
52
53 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
54 end
55
56 after do
57 @wc.destroy!
58 end
59
60 it "should delete deleted files" do
61 @upwc.delete("CHANGELOG")
62 @upwc.commit
63 @command.run
64 status = @wc.status.map {|st| st.sub(@wc.path, "")}
65 status.should include("D /vendor/CHANGELOG")
66 end
67end
68
69describe Piston::Commands::Update, "#run('vendor')" do
70 it_should_behave_like "An upstream repository with no copies/renames"
71 it_should_behave_like "A working copy against a local repository"
72
73 before do
74 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
75 @wc.commit "Pistonized vendor@r1"
76 @vendor = @wc.down("vendor")
77
78 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
79 end
80
81 after do
82 @wc.destroy!
83 end
84
85 it "should move files" do
86 @upwc.move("CHANGELOG", "Changelog.txt")
87 @upwc.commit
88 @command.run
89 status = @wc.status.map {|st| st.sub(@wc.path, "")}
90 status.should include("D /vendor/CHANGELOG")
91 status.should include("A + /vendor/Changelog.txt")
92 end
93end
94
95describe Piston::Commands::Update, "#run('vendor')" do
96 it_should_behave_like "An upstream repository with no copies/renames"
97 it_should_behave_like "A working copy against a local repository"
98
99 before do
100 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
101 @wc.commit "Pistonized vendor@r1"
102 @vendor = @wc.down("vendor")
103
104 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
105 end
106
107 after do
108 @wc.destroy!
109 end
110
111 it "should copy files" do
112 @upwc.copy("CHANGELOG", "old-changelog.txt")
113 @upwc.commit
114 @command.run
115 status = @wc.status.map {|st| st.sub(@wc.path, "")}
116 status.should include("A + /vendor/old-changelog.txt")
117 end
118end
119
120describe Piston::Commands::Update, "#run('vendor')" do
121 it_should_behave_like "An upstream repository with no copies/renames"
122 it_should_behave_like "A working copy against a local repository"
123
124 before do
125 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
126 @wc.commit "Pistonized vendor@r1"
127 @vendor = @wc.down("vendor")
128
129 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
130 end
131
132 after do
133 @wc.destroy!
134 end
135
136 it "should reject the update if the repository UUID is not as expected" do
137 @vendor.propset(Piston::REMOTE_UUID, "some-random-string")
138 lambda { @command.run }.should raise_error(Piston::RepositoryUuidChanged)
139 end
140
141 it "should reject the update when the repository is gone" do
142 @upstream.destroy!
143 lambda { @command.run }.should raise_error(Piston::UnknownRepository)
144 end
145end
146
147describe Piston::Commands::Update, "#run('vendor')" do
148 it_should_behave_like "An upstream repository with no copies/renames"
149 it_should_behave_like "A working copy against a local repository"
150
151 before do
152 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
153 @wc.commit "Pistonized vendor@r1"
154 @vendor = @wc.down("vendor")
155
156 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
157 @command.run
158 end
159
160 after do
161 @wc.destroy!
162 end
163
164 it "should receive vendor's HEAD changes" do
165 status = @wc.status
166 status.map! do |st|
167 st.sub(@wc.path, "")
168 end
169 status.sort.should == [
170 " M /vendor",
171 "A /vendor/LICENSE",
172 "A /vendor/README",
173 "M /vendor/main.c",
174 "M /vendor/CHANGELOG"].sort
175 end
176
177 it "should record new vendor's HEAD" do
178 @vendor.propget(Piston::REMOTE_REVISION).to_i.should == @upstream.youngest
179 end
180
181 it "should record new local HEAD (unchanged, since no changes in local repos)" do
182 @vendor.propget(Piston::LOCAL_REVISION).to_i.should == 0
183 end
184end
185
186describe Piston::Commands::Update, "#run('vendor') when local modifications have been made" do
187 it_should_behave_like "An upstream repository with no copies/renames"
188 it_should_behave_like "A working copy against a local repository"
189
190 before do
191 Piston::Commands::Import.new(@upstream.url, @wc.wc_path("vendor"), :revision => 1, :logger => logger).run
192 @wc.commit "Pistonized vendor@r1"
193 @vendor = @wc.down("vendor")
194
195 @vendor.edit!("main.c", <<EOF
196/**
197 *
198 * main.c: the main program file.
199 *
200 * This program does absolutely nothing.
201 *
202 */
203#include <stdio.h>
204
205int main(int argc, char** argv) {
206 return 0;
207}
208EOF
209)
210 @vendor.commit
211 @vendor.update
212
213 @command = Piston::Commands::Update.new(@wc.wc_path("vendor"), :logger => logger)
214 @command.run
215 end
216
217 it "should record new local HEAD" do
218 @vendor.propget(Piston::LOCAL_REVISION).to_i.should == 2
219 end
220
221 it "should merge local modifications" do
222 File.read(@vendor.wc_path("main.c")).should == <<EOF
223/**
224 *
225 * main.c: the main program file.
226 *
227 * This program does absolutely nothing.
228 *
229 */
230#include <stdio.h>
231
232/** Main program file */
233int main(int argc, char** argv) {
234 return do_work(argc);
235}
236
237int do_work(int argc) {
238 return 2*argc;
239}
240EOF
241 end
242end
toggle raw diff