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.
- data/History.txt +19 -0
- data/License.txt +20 -0
- data/Manifest.txt +109 -0
- data/README.txt +136 -0
- data/VERSION.yml +4 -0
- data/bin/piston +5 -0
- data/lib/piston.rb +18 -0
- data/lib/piston/cli.rb +391 -0
- data/lib/piston/commands.rb +4 -0
- data/lib/piston/commands/base.rb +44 -0
- data/lib/piston/commands/convert.rb +26 -0
- data/lib/piston/commands/diff.rb +12 -0
- data/lib/piston/commands/import.rb +43 -0
- data/lib/piston/commands/info.rb +14 -0
- data/lib/piston/commands/lock_unlock.rb +21 -0
- data/lib/piston/commands/status.rb +40 -0
- data/lib/piston/commands/update.rb +34 -0
- data/lib/piston/commands/upgrade.rb +20 -0
- data/lib/piston/git.rb +13 -0
- data/lib/piston/git/client.rb +76 -0
- data/lib/piston/git/commit.rb +114 -0
- data/lib/piston/git/repository.rb +63 -0
- data/lib/piston/git/working_copy.rb +142 -0
- data/lib/piston/repository.rb +61 -0
- data/lib/piston/revision.rb +83 -0
- data/lib/piston/svn.rb +15 -0
- data/lib/piston/svn/client.rb +88 -0
- data/lib/piston/svn/repository.rb +67 -0
- data/lib/piston/svn/revision.rb +112 -0
- data/lib/piston/svn/working_copy.rb +182 -0
- data/lib/piston/version.rb +9 -0
- data/lib/piston/working_copy.rb +334 -0
- data/lib/subclass_responsibility_error.rb +2 -0
- data/test/integration_helpers.rb +35 -0
- data/test/spec_suite.rb +4 -0
- data/test/test_helper.rb +83 -0
- data/test/unit/git/commit/test_checkout.rb +31 -0
- data/test/unit/git/commit/test_each.rb +30 -0
- data/test/unit/git/commit/test_rememberance.rb +22 -0
- data/test/unit/git/commit/test_validation.rb +34 -0
- data/test/unit/git/repository/test_at.rb +23 -0
- data/test/unit/git/repository/test_basename.rb +12 -0
- data/test/unit/git/repository/test_branchanme.rb +15 -0
- data/test/unit/git/repository/test_guessing.rb +32 -0
- data/test/unit/git/working_copy/test_copying.rb +25 -0
- data/test/unit/git/working_copy/test_creation.rb +22 -0
- data/test/unit/git/working_copy/test_existence.rb +18 -0
- data/test/unit/git/working_copy/test_finalization.rb +15 -0
- data/test/unit/git/working_copy/test_guessing.rb +35 -0
- data/test/unit/git/working_copy/test_rememberance.rb +22 -0
- data/test/unit/svn/repository/test_at.rb +19 -0
- data/test/unit/svn/repository/test_basename.rb +24 -0
- data/test/unit/svn/repository/test_guessing.rb +45 -0
- data/test/unit/svn/revision/test_checkout.rb +28 -0
- data/test/unit/svn/revision/test_each.rb +22 -0
- data/test/unit/svn/revision/test_rememberance.rb +38 -0
- data/test/unit/svn/revision/test_validation.rb +50 -0
- data/test/unit/svn/working_copy/test_copying.rb +26 -0
- data/test/unit/svn/working_copy/test_creation.rb +16 -0
- data/test/unit/svn/working_copy/test_existence.rb +23 -0
- data/test/unit/svn/working_copy/test_externals.rb +56 -0
- data/test/unit/svn/working_copy/test_finalization.rb +17 -0
- data/test/unit/svn/working_copy/test_guessing.rb +18 -0
- data/test/unit/svn/working_copy/test_rememberance.rb +26 -0
- data/test/unit/test_info.rb +37 -0
- data/test/unit/test_lock_unlock.rb +47 -0
- data/test/unit/test_repository.rb +51 -0
- data/test/unit/test_revision.rb +31 -0
- data/test/unit/working_copy/test_guessing.rb +35 -0
- data/test/unit/working_copy/test_info.rb +14 -0
- data/test/unit/working_copy/test_rememberance.rb +42 -0
- data/test/unit/working_copy/test_validate.rb +63 -0
- 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,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
|