francois-piston 2.0.0

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.
Files changed (73) hide show
  1. data/History.txt +19 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +109 -0
  4. data/README.txt +136 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/piston +5 -0
  7. data/lib/piston.rb +18 -0
  8. data/lib/piston/cli.rb +391 -0
  9. data/lib/piston/commands.rb +4 -0
  10. data/lib/piston/commands/base.rb +44 -0
  11. data/lib/piston/commands/convert.rb +26 -0
  12. data/lib/piston/commands/diff.rb +12 -0
  13. data/lib/piston/commands/import.rb +43 -0
  14. data/lib/piston/commands/info.rb +14 -0
  15. data/lib/piston/commands/lock_unlock.rb +21 -0
  16. data/lib/piston/commands/status.rb +40 -0
  17. data/lib/piston/commands/update.rb +34 -0
  18. data/lib/piston/commands/upgrade.rb +20 -0
  19. data/lib/piston/git.rb +13 -0
  20. data/lib/piston/git/client.rb +76 -0
  21. data/lib/piston/git/commit.rb +114 -0
  22. data/lib/piston/git/repository.rb +63 -0
  23. data/lib/piston/git/working_copy.rb +142 -0
  24. data/lib/piston/repository.rb +61 -0
  25. data/lib/piston/revision.rb +83 -0
  26. data/lib/piston/svn.rb +15 -0
  27. data/lib/piston/svn/client.rb +88 -0
  28. data/lib/piston/svn/repository.rb +67 -0
  29. data/lib/piston/svn/revision.rb +112 -0
  30. data/lib/piston/svn/working_copy.rb +182 -0
  31. data/lib/piston/version.rb +9 -0
  32. data/lib/piston/working_copy.rb +334 -0
  33. data/lib/subclass_responsibility_error.rb +2 -0
  34. data/test/integration_helpers.rb +35 -0
  35. data/test/spec_suite.rb +4 -0
  36. data/test/test_helper.rb +83 -0
  37. data/test/unit/git/commit/test_checkout.rb +31 -0
  38. data/test/unit/git/commit/test_each.rb +30 -0
  39. data/test/unit/git/commit/test_rememberance.rb +22 -0
  40. data/test/unit/git/commit/test_validation.rb +34 -0
  41. data/test/unit/git/repository/test_at.rb +23 -0
  42. data/test/unit/git/repository/test_basename.rb +12 -0
  43. data/test/unit/git/repository/test_branchanme.rb +15 -0
  44. data/test/unit/git/repository/test_guessing.rb +32 -0
  45. data/test/unit/git/working_copy/test_copying.rb +25 -0
  46. data/test/unit/git/working_copy/test_creation.rb +22 -0
  47. data/test/unit/git/working_copy/test_existence.rb +18 -0
  48. data/test/unit/git/working_copy/test_finalization.rb +15 -0
  49. data/test/unit/git/working_copy/test_guessing.rb +35 -0
  50. data/test/unit/git/working_copy/test_rememberance.rb +22 -0
  51. data/test/unit/svn/repository/test_at.rb +19 -0
  52. data/test/unit/svn/repository/test_basename.rb +24 -0
  53. data/test/unit/svn/repository/test_guessing.rb +45 -0
  54. data/test/unit/svn/revision/test_checkout.rb +28 -0
  55. data/test/unit/svn/revision/test_each.rb +22 -0
  56. data/test/unit/svn/revision/test_rememberance.rb +38 -0
  57. data/test/unit/svn/revision/test_validation.rb +50 -0
  58. data/test/unit/svn/working_copy/test_copying.rb +26 -0
  59. data/test/unit/svn/working_copy/test_creation.rb +16 -0
  60. data/test/unit/svn/working_copy/test_existence.rb +23 -0
  61. data/test/unit/svn/working_copy/test_externals.rb +56 -0
  62. data/test/unit/svn/working_copy/test_finalization.rb +17 -0
  63. data/test/unit/svn/working_copy/test_guessing.rb +18 -0
  64. data/test/unit/svn/working_copy/test_rememberance.rb +26 -0
  65. data/test/unit/test_info.rb +37 -0
  66. data/test/unit/test_lock_unlock.rb +47 -0
  67. data/test/unit/test_repository.rb +51 -0
  68. data/test/unit/test_revision.rb +31 -0
  69. data/test/unit/working_copy/test_guessing.rb +35 -0
  70. data/test/unit/working_copy/test_info.rb +14 -0
  71. data/test/unit/working_copy/test_rememberance.rb +42 -0
  72. data/test/unit/working_copy/test_validate.rb +63 -0
  73. metadata +178 -0
@@ -0,0 +1,67 @@
1
+ require "uri"
2
+
3
+ module Piston
4
+ module Svn
5
+ class Repository < Piston::Repository
6
+ # Register ourselves as a repository handler
7
+ Piston::Repository.add_handler self
8
+
9
+ class << self
10
+ def understands_url?(url)
11
+ uri = URI.parse(url)
12
+ case uri.scheme
13
+ when "svn", /^svn\+/
14
+ true
15
+ when "http", "https", "file"
16
+ # Have to contact server to know
17
+ result = svn(:info, url) rescue :failed
18
+ result == :failed ? false : true
19
+ else
20
+ # Don't know how to handle this scheme.
21
+ # Let someone else handle it
22
+ end
23
+ end
24
+
25
+ def client
26
+ @@client ||= Piston::Svn::Client.instance
27
+ end
28
+
29
+ def svn(*args)
30
+ client.svn(*args)
31
+ end
32
+
33
+ def repository_type
34
+ 'svn'
35
+ end
36
+ end
37
+
38
+ def svn(*args)
39
+ self.class.svn(*args)
40
+ end
41
+
42
+ def at(revision)
43
+ if revision.respond_to?(:keys) then
44
+ rev = revision[Piston::Svn::REMOTE_REV]
45
+ Piston::Svn::Revision.new(self, rev, revision)
46
+ else
47
+ case
48
+ when revision == :head
49
+ Piston::Svn::Revision.new(self, "HEAD")
50
+ when revision.to_i != 0
51
+ Piston::Svn::Revision.new(self, revision.to_i)
52
+ else
53
+ raise ArgumentError, "Invalid revision argument: #{revision.inspect}"
54
+ end
55
+ end
56
+ end
57
+
58
+ def basename
59
+ if self.url =~ /trunk|branches|tags/ then
60
+ self.url.sub(%r{/(?:trunk|branches|tags).*$}, "").split("/").last
61
+ else
62
+ self.url.split("/").last
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,112 @@
1
+ require "piston/revision"
2
+ require "fileutils"
3
+
4
+ module Piston
5
+ module Svn
6
+ class Revision < Piston::Revision
7
+ class InvalidRevision < RuntimeError; end
8
+ class RepositoryMoved < InvalidRevision; end
9
+ class UuidChanged < InvalidRevision; end
10
+
11
+ def client
12
+ @client ||= Piston::Svn::Client.instance
13
+ end
14
+
15
+ def svn(*args)
16
+ client.svn(*args)
17
+ end
18
+
19
+ def name
20
+ "r#{revision}"
21
+ end
22
+
23
+ def checkout_to(dir)
24
+ super
25
+ answer = svn(:checkout, "--revision", revision, repository.url, dir)
26
+ if answer =~ /Checked out revision (\d+)[.]/ then
27
+ if revision == "HEAD" then
28
+ @revision = $1.to_i
29
+ elsif revision != $1.to_i then
30
+ raise InvalidRevision, "Did not get the revision I wanted to checkout. Subversion checked out #{$1}, I wanted #{revision}"
31
+ end
32
+ else
33
+ raise InvalidRevision, "Could not checkout revision #{revision} from #{repository.url} to #{dir}\n#{answer}"
34
+ end
35
+ end
36
+
37
+ def update_to(revision)
38
+ raise ArgumentError, "Revision #{self.revision} of #{repository.url} was never checked out -- can't update" unless @dir
39
+
40
+ answer = svn(:update, "--non-interactive", "--revision", revision, @dir)
41
+ if answer =~ /(Updated to|At) revision (\d+)[.]/ then
42
+ if revision == "HEAD" then
43
+ @revision = $2.to_i
44
+ elsif revision != $2.to_i then
45
+ raise InvalidRevision, "Did not get the revision I wanted to update. Subversion update to #{$1}, I wanted #{revision}"
46
+ end
47
+ else
48
+ raise InvalidRevision, "Could not update #{@dir} to revision #{revision} from #{repository.url}\n#{answer}"
49
+ end
50
+ added = relative_paths(answer.scan(/^A\s+(.*)$/).flatten)
51
+ deleted = relative_paths(answer.scan(/^D\s+(.*)$/).flatten)
52
+ renamed = []
53
+ [added, deleted, renamed]
54
+ end
55
+
56
+ def remember_values
57
+ str = svn(:info, "--revision", revision, repository.url)
58
+ raise Failed, "Could not get 'svn info' from #{repository.url} at revision #{revision}" if str.nil? || str.chomp.strip.empty?
59
+ info = YAML.load(str)
60
+ { Piston::Svn::UUID => info["Repository UUID"],
61
+ Piston::Svn::REMOTE_REV => info["Revision"]}
62
+ end
63
+
64
+ def each
65
+ raise ArgumentError, "Revision #{revision} of #{repository.url} was never checked out -- can't iterate over files" unless @dir
66
+
67
+ svn(:ls, "--recursive", @dir).each do |relpath|
68
+ next if relpath =~ %r{/$}
69
+ yield relpath.chomp
70
+ end
71
+ end
72
+
73
+ def validate!
74
+ data = svn(:info, "--revision", revision, repository.url)
75
+ info = YAML.load(data)
76
+ actual_uuid = info["Repository UUID"]
77
+ raise RepositoryMoved, "Repository at #{repository.url} does not exist anymore:\n#{data}" if actual_uuid.blank?
78
+ raise UuidChanged, "Expected repository at #{repository.url} to have UUID #{recalled_uuid} but found #{actual_uuid}" if recalled_uuid != actual_uuid
79
+ end
80
+
81
+ def recalled_uuid
82
+ recalled_values[Piston::Svn::UUID]
83
+ end
84
+
85
+ def remotely_modified
86
+ logger.debug {"Get last revision in #{repository.url}"}
87
+ data = svn(:info, repository.url)
88
+ info = YAML.load(data)
89
+ latest_revision = info["Last Changed Rev"].to_i
90
+ revision < latest_revision
91
+ end
92
+
93
+ def exclude_for_diff
94
+ Piston::Svn::EXCLUDE
95
+ end
96
+
97
+ def resolve!
98
+ logger.debug {"Resolving #{@revision} to it's real value"}
99
+ return if @revision.to_i == @revision && !@revision.blank?
100
+ data = YAML.load(svn(:info, repository.url))
101
+ @revision = data["Last Changed Rev"].to_i
102
+ logger.debug {"Resolved #{@revision}"}
103
+ @revision
104
+ end
105
+
106
+ private
107
+ def relative_paths(paths)
108
+ paths.map { |item| Pathname.new(item).relative_path_from(@dir) }
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,182 @@
1
+ require "yaml"
2
+
3
+ module Piston
4
+ module Svn
5
+ class WorkingCopy < Piston::WorkingCopy
6
+ # Register ourselves as a handler for working copies
7
+ Piston::WorkingCopy.add_handler self
8
+
9
+ class << self
10
+ def understands_dir?(dir)
11
+ result = svn(:info, dir) rescue :failed
12
+ result == :failed ? false : true
13
+ end
14
+
15
+ def client
16
+ @@client ||= Piston::Svn::Client.instance
17
+ end
18
+
19
+ def svn(*args)
20
+ client.svn(*args)
21
+ end
22
+
23
+ def old_repositories(*directories)
24
+ repositories = []
25
+ unless directories.empty?
26
+ folders = svn(:propget, '--recursive', Piston::Svn::ROOT, *directories)
27
+ folders.each_line do |line|
28
+ next unless line =~ /^(.+) - \S+/
29
+ logger.debug {"Found repository #{$1}"}
30
+ repositories << $1
31
+ end
32
+ end
33
+ repositories
34
+ end
35
+ end
36
+
37
+ def svn(*args)
38
+ self.class.svn(*args)
39
+ end
40
+
41
+ def exist?
42
+ result = svn(:info, path) rescue :failed
43
+ logger.debug {"result: #{result.inspect}"}
44
+ return false if result == :failed
45
+ return false if result.nil? || result.chomp.strip.empty?
46
+ return true if YAML.load(result).has_key?("Path")
47
+ end
48
+
49
+ def create
50
+ logger.debug {"Creating #{path}"}
51
+ begin
52
+ svn(:mkdir, path)
53
+ rescue Piston::Svn::Client::CommandError
54
+ logger.error do
55
+ "Folder #{path} could not be created. Is #{path.parent} a working copy? (Tip: svn mkdir it)"
56
+ end
57
+ raise
58
+ end
59
+ end
60
+
61
+ def after_remember(path)
62
+ begin
63
+ info = svn(:info, path)
64
+ rescue Piston::Svn::Client::CommandError
65
+ ensure
66
+ return unless info =~ /\(not a versioned resource\)/i || info =~ /\(is not under version control\)/i || info.blank?
67
+ svn(:add, path)
68
+ end
69
+ end
70
+
71
+ def finalize
72
+ targets = []
73
+ Dir[path + "*"].each do |item|
74
+ svn(:add, item)
75
+ end
76
+ end
77
+
78
+ def add(added)
79
+ added.each do |item|
80
+ svn(:add, path + item)
81
+ end
82
+ end
83
+
84
+ def delete(deleted)
85
+ deleted.each do |item|
86
+ svn(:rm, path + item)
87
+ end
88
+ end
89
+
90
+ def rename(renamed)
91
+ renamed.each do |from, to|
92
+ svn(:mv, path + from, path + to)
93
+ end
94
+ end
95
+
96
+ def downgrade_to(revision)
97
+ logger.debug {"Downgrading to revision when last update was made"}
98
+ svn(:update, '--revision', revision, path)
99
+ end
100
+
101
+ def merge_local_changes(revision)
102
+ logger.debug {"Update to #{revision} in order to merge local changes"}
103
+ svn(:update, "--non-interactive", path)
104
+ end
105
+
106
+ def status(subpath=nil)
107
+ svn(:status, path + subpath.to_s).split("\n").inject([]) do |memo, line|
108
+ next memo unless line =~ /^\w.+\s(.*)$/
109
+ memo << [$1, $2]
110
+ end
111
+ end
112
+
113
+ # Returns all defined externals (recursively) of this WC.
114
+ # Returns a Hash:
115
+ # {"vendor/rails" => {:revision => :head, :url => "http://dev.rubyonrails.org/svn/rails/trunk"},
116
+ # "vendor/plugins/will_paginate" => {:revision => 1234, :url => "http://will_paginate.org/svn/trunk"}}
117
+ def externals
118
+ externals = svn(:proplist, "--recursive", "--verbose")
119
+ return Hash.new if externals.blank?
120
+ returning(Hash.new) do |result|
121
+ YAML.load(externals).each_pair do |dir, props|
122
+ next if props["svn:externals"].blank?
123
+ next unless dir =~ /Properties on '([^']+)'/
124
+ basedir = self.path + $1
125
+ exts = props["svn:externals"]
126
+ exts.split("\n").each do |external|
127
+ data = external.match(/^([^\s]+)\s+(?:(?:-r|--revision)\s*(\d+)\s+)?(.+)$/)
128
+ case data.length
129
+ when 4
130
+ subdir, rev, url = data[1], data[2].nil? ? :head : data[2].to_i, data[3]
131
+ else
132
+ raise SyntaxError, "Could not parse svn:externals on #{basedir}: #{external}"
133
+ end
134
+
135
+ result[basedir + subdir] = {:revision => rev, :url => url}
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def remove_external_references(*targets)
142
+ svn(:propdel, "svn:externals", *targets)
143
+ end
144
+
145
+ def locally_modified
146
+ logger.debug {"Get last changed revision for #{yaml_path}"}
147
+ # get latest revision for .piston.yml
148
+ initial_revision = last_changed_revision(yaml_path)
149
+ logger.debug {"Get last log line for #{path} after #{initial_revision}"}
150
+ # get latest revisions for this working copy since last update
151
+ log = svn(:log, '--revision', "#{initial_revision}:HEAD", '--quiet', '--limit', '2', path)
152
+ log.count("\n") > 3
153
+ end
154
+
155
+ def exclude_for_diff
156
+ Piston::Svn::EXCLUDE
157
+ end
158
+
159
+ def upgrade
160
+ props = Hash.new
161
+ svn(:proplist, '--verbose', path).each_line do |line|
162
+ if line =~ /(piston:[-\w]+)\s*:\s*(.*)$/
163
+ props[$1] = $2
164
+ svn(:propdel, $1, path)
165
+ end
166
+ end
167
+ remember({:repository_url => props[Piston::Svn::ROOT], :lock => props[Piston::Svn::LOCKED] || false, :repository_class => Piston::Svn::Repository.name}, {Piston::Svn::REMOTE_REV => props[Piston::Svn::REMOTE_REV], Piston::Svn::UUID => props[Piston::Svn::UUID]})
168
+ end
169
+
170
+ protected
171
+ def current_revision
172
+ data = svn(:info, path)
173
+ YAML.load(data)["Revision"].to_i
174
+ end
175
+
176
+ def last_changed_revision(path)
177
+ data = svn(:info, yaml_path)
178
+ YAML.load(data)["Last Changed Rev"].to_i
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,9 @@
1
+ module Piston #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 1
4
+ MINOR = 9
5
+ TINY = 5
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join(".")
8
+ end
9
+ end
@@ -0,0 +1,334 @@
1
+ module Piston
2
+ class WorkingCopy
3
+ class UnhandledWorkingCopy < RuntimeError; end
4
+ class NotWorkingCopy < RuntimeError; end
5
+
6
+ class << self
7
+ def logger
8
+ @@logger ||= Log4r::Logger["handler"]
9
+ end
10
+
11
+ def guess(path)
12
+ path = path.kind_of?(Pathname) ? path : Pathname.new(path.to_s)
13
+ try_path = path.exist? ? path : path.parent
14
+ logger.info {"Guessing the working copy type of #{try_path.inspect}"}
15
+ handler = handlers.detect do |handler|
16
+ logger.debug {"Asking #{handler.name} if it understands #{try_path}"}
17
+ handler.understands_dir?(try_path)
18
+ end
19
+
20
+ raise UnhandledWorkingCopy, "Don't know what working copy type #{path} is." if handler.nil?
21
+ handler.new(File.expand_path(path))
22
+ end
23
+
24
+ @@handlers = Array.new
25
+ def add_handler(handler)
26
+ @@handlers << handler
27
+ end
28
+
29
+ def handlers
30
+ @@handlers
31
+ end
32
+ private :handlers
33
+ end
34
+
35
+ attr_reader :path
36
+
37
+ def initialize(path)
38
+ if path.kind_of?(Pathname)
39
+ raise ArgumentError, "#{path} must be absolute" unless path.absolute?
40
+ @path = path
41
+ else
42
+ @path = Pathname.new(File.expand_path(path))
43
+ end
44
+ logger.debug {"Initialized on #{@path}"}
45
+ end
46
+
47
+ def logger
48
+ self.class.logger
49
+ end
50
+
51
+ def to_s
52
+ "Piston::WorkingCopy(#{@path})"
53
+ end
54
+
55
+ def exist?
56
+ @path.exist? && @path.directory?
57
+ end
58
+
59
+ def pistonized?
60
+ yaml_path.exist? && yaml_path.file?
61
+ end
62
+
63
+ def validate!
64
+ raise NotWorkingCopy unless self.pistonized?
65
+ self
66
+ end
67
+
68
+ def repository
69
+ values = self.recall
70
+ repository_class = values["repository_class"]
71
+ repository_url = values["repository_url"]
72
+ repository_class.constantize.new(repository_url)
73
+ end
74
+
75
+ # Creates the initial working copy for pistonizing a new repository.
76
+ def create
77
+ logger.debug {"Creating working copy at #{path}"}
78
+ end
79
+
80
+ # Copy files from +revision+. +revision+ must
81
+ # #respond_to?(:each), and return each file that is to be copied.
82
+ # Only files must be returned.
83
+ #
84
+ # Each item yielded by Revision#each must be a relative path.
85
+ #
86
+ # WorkingCopy will call Revision#copy_to with the full path to where the
87
+ # file needs to be copied.
88
+ def copy_from(revision)
89
+ revision.each do |relpath|
90
+ target = path + relpath
91
+ target.dirname.mkdir rescue nil
92
+
93
+ logger.debug {"Copying #{relpath} to #{target}"}
94
+ revision.copy_to(relpath, target)
95
+ end
96
+ end
97
+
98
+ # Copy files to +revision+ to keep local changes. +revision+ must
99
+ # #respond_to?(:each), and return each file that is to be copied.
100
+ # Only files must be returned.
101
+ #
102
+ # Each item yielded by Revision#each must be a relative path.
103
+ #
104
+ # WorkingCopy will call Revision#copy_from with the full path from where the
105
+ # file needs to be copied.
106
+ def copy_to(revision)
107
+ revision.each do |relpath|
108
+ source = path + relpath
109
+
110
+ logger.debug {"Copying #{source} to #{relpath}"}
111
+ revision.copy_from(source, relpath)
112
+ end
113
+ end
114
+
115
+ # add some files to working copy
116
+ def add(added)
117
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#add should be implemented by a subclass."
118
+ end
119
+
120
+ # delete some files from working copy
121
+ def delete(deleted)
122
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#delete should be implemented by a subclass."
123
+ end
124
+
125
+ # rename some files in working copy
126
+ def rename(renamed)
127
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#rename should be implemented by a subclass."
128
+ end
129
+
130
+ # Downgrade this working copy to +revision+.
131
+ def downgrade_to(revision)
132
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#downgrade_to should be implemented by a subclass."
133
+ end
134
+
135
+ # Merge remote changes with local changes in +revision+.
136
+ def merge_local_changes(revision)
137
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#merge_local_changes should be implemented by a subclass."
138
+ end
139
+
140
+ # Stores a Hash of values that can be retrieved later.
141
+ def remember(values, handler_values)
142
+ values["format"] = 1
143
+
144
+ # Stringify keys
145
+ values.keys.each do |key|
146
+ values[key.to_s] = values.delete(key)
147
+ end
148
+
149
+ logger.debug {"Remembering #{values.inspect} as well as #{handler_values.inspect}"}
150
+ File.open(yaml_path, "w+") do |f|
151
+ f.write(values.merge("handler" => handler_values).to_yaml)
152
+ end
153
+
154
+ logger.debug {"Calling \#after_remember on #{yaml_path}"}
155
+ after_remember(yaml_path)
156
+ end
157
+
158
+ # Callback after #remember is done, to do whatever the
159
+ # working copy needs to do with the file.
160
+ def after_remember(path)
161
+ end
162
+
163
+ # Recalls a Hash of values from the working copy.
164
+ def recall
165
+ YAML.load_file(yaml_path)
166
+ end
167
+
168
+ def finalize
169
+ logger.debug {"Finalizing #{path}"}
170
+ end
171
+
172
+ # Returns basic information about this working copy.
173
+ def info
174
+ recall
175
+ end
176
+
177
+ def import(revision, lock)
178
+ lock ||= false
179
+ repository = revision.repository
180
+ tmpdir = temp_dir_name
181
+ begin
182
+ logger.info {"Checking out the repository"}
183
+ revision.checkout_to(tmpdir)
184
+
185
+ logger.debug {"Creating the local working copy"}
186
+ create
187
+
188
+ logger.info {"Copying from #{revision}"}
189
+ copy_from(revision)
190
+
191
+ logger.debug {"Remembering values"}
192
+ remember(
193
+ {:repository_url => repository.url, :lock => lock, :repository_class => repository.class.name},
194
+ revision.remember_values
195
+ )
196
+
197
+ logger.debug {"Finalizing working copy"}
198
+ finalize
199
+
200
+ logger.info {"Checked out #{repository.url.inspect} #{revision.name} to #{path.to_s}"}
201
+ ensure
202
+ logger.debug {"Removing temporary directory: #{tmpdir}"}
203
+ tmpdir.rmtree rescue nil
204
+ end
205
+ end
206
+
207
+ # Update this working copy from +from+ to +to+, which means merging local changes back in
208
+ # Return true if changed, false if not
209
+ def update(revision, to, lock)
210
+ lock ||= false
211
+ tmpdir = temp_dir_name
212
+ begin
213
+ logger.info {"Checking out the repository at #{revision.revision}"}
214
+ revision.checkout_to(tmpdir)
215
+
216
+ revision_to_return = current_revision
217
+ revision_to_downgrade = last_changed_revision(yaml_path)
218
+ logger.debug {"Downgrading to #{revision_to_downgrade}"}
219
+ downgrade_to(revision_to_downgrade)
220
+
221
+ logger.debug {"Copying old changes to temporary directory in order to keep them"}
222
+ copy_to(revision)
223
+
224
+ logger.info {"Looking changes from #{revision.revision} to #{to.revision}"}
225
+ added, deleted, renamed = revision.update_to(to.revision)
226
+
227
+ logger.info {"Updating working copy"}
228
+
229
+ # rename before copy because copy_from will copy these files
230
+ logger.debug {"Renaming files"}
231
+ rename(renamed)
232
+
233
+ logger.debug {"Copying files from temporary directory"}
234
+ copy_from(revision)
235
+
236
+ logger.debug {"Adding new files to version control"}
237
+ add(added)
238
+
239
+ logger.debug {"Deleting files from version control"}
240
+ delete(deleted)
241
+
242
+ # merge local changes updating to revision before downgrade was made
243
+ logger.debug {"Merging local changes"}
244
+ merge_local_changes(revision_to_return)
245
+
246
+ remember(recall.merge(:lock => lock), to.remember_values)
247
+
248
+ status = status(path)
249
+ logger.debug { {:added => added, :deleted => deleted, :renamed => renamed, :status => status}.to_yaml }
250
+ !status.empty?
251
+ ensure
252
+ logger.debug {"Removing temporary directory: #{tmpdir}"}
253
+ tmpdir.rmtree rescue nil
254
+ end
255
+ end
256
+
257
+ def diff
258
+ tmpdir = temp_dir_name
259
+ begin
260
+ logger.info {"Checking out the repository at #{revision.revision}"}
261
+ revision = repository.at(:head)
262
+ revision.checkout_to(tmpdir)
263
+
264
+ excludes = (['.piston.yml'] + exclude_for_diff + revision.exclude_for_diff).uniq.collect {|pattern| "--exclude=#{pattern}"}.join ' '
265
+ system("diff -urN #{excludes} '#{tmpdir}' '#{path}'")
266
+ ensure
267
+ logger.debug {"Removing temporary directory: #{tmpdir}"}
268
+ tmpdir.rmtree rescue nil
269
+ end
270
+ end
271
+
272
+ def diff
273
+ tmpdir = temp_dir_name
274
+ begin
275
+ logger.info {"Checking out the repository at #{revision.revision}"}
276
+ revision = repository.at(:head)
277
+ revision.checkout_to(tmpdir)
278
+
279
+ excludes = (['.piston.yml'] + exclude_for_diff + revision.exclude_for_diff).uniq.collect {|pattern| "--exclude=#{pattern}"}.join ' '
280
+ system("diff -urN #{excludes} '#{tmpdir}' '#{path}'")
281
+ ensure
282
+ logger.debug {"Removing temporary directory: #{tmpdir}"}
283
+ tmpdir.rmtree rescue nil
284
+ end
285
+ end
286
+
287
+ def diff
288
+ tmpdir = temp_dir_name
289
+ begin
290
+ logger.info {"Checking out the repository at #{revision.revision}"}
291
+ revision = repository.at(:head)
292
+ revision.checkout_to(tmpdir)
293
+
294
+ excludes = (['.piston.yml'] + exclude_for_diff + revision.exclude_for_diff).uniq.collect {|pattern| "--exclude=#{pattern}"}.join ' '
295
+ system("diff -urN #{excludes} '#{tmpdir}' '#{path}'")
296
+ ensure
297
+ logger.debug {"Removing temporary directory: #{tmpdir}"}
298
+ tmpdir.rmtree rescue nil
299
+ end
300
+ end
301
+
302
+ def temp_dir_name
303
+ path.parent + ".#{path.basename}.tmp"
304
+ end
305
+
306
+ def locally_modified
307
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#locally_modified should be implemented by a subclass."
308
+ end
309
+
310
+ def remotely_modified
311
+ repository.at(recall["handler"]).remotely_modified
312
+ end
313
+
314
+ def exclude_for_diff
315
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#exclude_for_diff should be implemented by a subclass."
316
+ end
317
+
318
+ protected
319
+ # The path to the piston YAML file.
320
+ def yaml_path
321
+ path + ".piston.yml"
322
+ end
323
+
324
+ # The current revision of this working copy.
325
+ def current_revision
326
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#current_revision should be implemented by a subclass."
327
+ end
328
+
329
+ # The last revision which +path+ was changed in
330
+ def last_changed_revision(path)
331
+ raise SubclassResponsibilityError, "Piston::WorkingCopy#last_changed_revision should be implemented by a subclass."
332
+ end
333
+ end
334
+ end