drupid 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/drupid ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Copyright (c) 2012 Lifepillar
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ require 'drupid'
25
+
26
+ module Drupid
27
+
28
+ class Fools
29
+ include Drupid::Utils
30
+
31
+ def initialize
32
+ @options = { # Defaults
33
+ :command => :sync,
34
+ :directory => nil,
35
+ :dry => false,
36
+ :edit => nil,
37
+ :force => false,
38
+ :nocore => false,
39
+ :nodeps => false,
40
+ :nolibs => false,
41
+ :out => nil,
42
+ :site => nil
43
+ }
44
+ @updater = nil
45
+ end
46
+
47
+ def preflight_dependencies
48
+ odie 'Drupid requires Ruby 1.8.7 or later' if RUBY_VERSION < '1.8.7'
49
+ ['diff','curl','git','drush','mktemp','patch','rsync'].each do |cmd|
50
+ if `which #{cmd} 2>/dev/null`.chomp.empty?
51
+ odie "Drupid requires '#{cmd}', but '#{cmd}' was not found in your PATH."
52
+ end
53
+ end
54
+ end
55
+
56
+ def parse_options
57
+ require 'optparse'
58
+
59
+ begin
60
+ OptionParser.new do |o|
61
+ o.banner = "Drupid synchronizes a Drush makefile" +
62
+ " with a Drupal platform, and more!\n" +
63
+ "Usage:\n" +
64
+ "drupid -s <MAKEFILE> -p <DIRECTORY> [-cdDflnSv]\n" +
65
+ "drupid --clear-cache [-nv]\n" +
66
+ "drupid --edit [<URL>] [-v] [-o <FILE>]\n" +
67
+ "drupid --graph -p <DIRECTORY> [-Sv]\n" +
68
+ "drupid --help\n" +
69
+ "drupid --version"
70
+ o.on('-s', '--spec MAKEFILE', 'Path to a drush .make file.') do |p|
71
+ begin
72
+ @options[:makefile] = Pathname.new(p).realpath
73
+ rescue
74
+ odie "#{p} does not exist."
75
+ end
76
+ end
77
+ o.on('-c', '--no-core', 'Do not synchronize Drupal core.') { @options[:nocore] = true }
78
+ o.on('-C', '--clear-cache', 'Clear Drupid\'s cache and exit.') { @options[:command] = :clear }
79
+ o.on('-d', '--no-deps', 'Do not follow dependencies...',
80
+ '...and miss one of the coolest features of Drupid :)') { @options[:nodeps] = true }
81
+ o.on('-D', '--debug', 'Enable debugging.') { $DEBUG = true; $VERBOSE = true }
82
+ o.on('-e', '--edit [URL]', 'Create patches interactively.',
83
+ 'With no URL, edit the current directory.') { |u| @options[:command] = :edit; @options[:edit] = u }
84
+ o.on('-f', '--force', 'Force completion, even if there are errors.') { |b| @options[:force] = b }
85
+ o.on('-g', '--graph', 'Generate a dependency graph and exit.') { @options[:command] = :graph }
86
+ o.on('-h', '--help', 'Print help and exit.') {
87
+ puts o
88
+ puts "\nGleefully brought to you by Lifepillar! http://lifepillar.com"
89
+ exit 0
90
+ }
91
+ o.on('-l', '--no-libs', 'Do not synchronize libraries.') { @options[:nolibs] = true }
92
+ o.on('-n', '--dry', 'Dry run.') { |b| @options[:dry] = b }
93
+ o.on('-o', '--out FILE', 'Name of the output patch.') { |o| @options[:out] = o }
94
+ o.on('-p', '--path DIRECTORY', 'Path to a Drupal platform.') do |p|
95
+ @options[:directory] = Pathname.new(p).expand_path
96
+ @options[:directory].mkpath
97
+ end
98
+ o.on('-S', '--site NAME', 'Process the given site.',
99
+ '(For multi-site platforms.)') { |s| @options[:site] = s }
100
+ o.on('-v', '--verbose', 'Be verbose.') { $VERBOSE = true }
101
+ o.on('-V', '--version', 'Print version and exit.') { puts DRUPID_USER_AGENT; exit 0 }
102
+ o.parse!
103
+ end
104
+ rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption => ex
105
+ odie "#{ex}\nTry 'drupid --help' to see the available options."
106
+ rescue OptionParser::MissingArgument, OptionParser::NeedlessArgument => ex
107
+ odie "#{ex}\nTry 'drupid --help' for the correct syntax."
108
+ end
109
+ case @options[:command]
110
+ when :graph
111
+ odie "Please specify the path to a Drupal platform." unless @options[:directory]
112
+ when :clear
113
+ when :edit
114
+ when :sync
115
+ odie "Please specify a makefile." unless @options[:makefile]
116
+ odie "Please specify a destination." unless @options[:directory]
117
+ else
118
+ odie "Unknown command: #{@options[:command]}"
119
+ end
120
+ end
121
+
122
+ def sync!
123
+ blah "Caching files in #{Drupid.cache_path}"
124
+ begin
125
+ mf = Drupid::Makefile.new(@options[:makefile])
126
+ rescue ParseMakefileError => ex
127
+ odie "Could not parse makefile: #{ex}"
128
+ end
129
+ begin
130
+ pl = Drupid::Platform.new(@options[:directory])
131
+ pl.contrib_path = pl.sites_dir + @options[:site] if @options[:site]
132
+ rescue => ex
133
+ odie "Could not analyze platform: #{ex}"
134
+ end
135
+ @updater = Drupid::Updater.new(mf, pl, @options[:site])
136
+ blah 'Syncing (this may take a while)...'
137
+ ohai 'Preflighting changes...'
138
+ @updater.sync(
139
+ :nocore => @options[:nocore],
140
+ :nofollow => @options[:nodeps],
141
+ :nolibs => @options[:nolibs]
142
+ )
143
+
144
+ # Check outcome and apply changes
145
+ failed = @updater.log.errors?
146
+ if failed
147
+ puts
148
+ ohai "The following errors should be fixed for a successful update:"
149
+ @updater.log.errors.each { |e| ofail e }
150
+ end
151
+
152
+ if @updater.pending_actions?
153
+ if (failed and (not @options[:force])) or @options[:dry]
154
+ ohai 'No changes applied.'
155
+ else
156
+ ohai 'Applying changes' + (failed ? ' despite errors...' : '...')
157
+ @updater.apply_changes(:force => @options[:force])
158
+ ohai 'Success!'
159
+ end
160
+ else
161
+ ohai "The platform is in sync with the makefile." unless failed
162
+ end
163
+ # Write .lock makefile
164
+ if !(@options[:no_lockfile] or @options[:dry]) and
165
+ (!(failed) or @options[:force]) and
166
+ '.lock' != @updater.makefile.path.extname
167
+ @updater.makefile.save(@updater.makefile.path.sub_ext('.make.lock'))
168
+ end
169
+ end
170
+
171
+ def clear_cache
172
+ FileUtils.rmtree Drupid.cache_path.to_s, :noop => @options[:dry], :verbose => $VERBOSE
173
+ ohai "Cache cleared."
174
+ end
175
+
176
+ def graph
177
+ platform = Drupid::Platform.new(@options[:directory])
178
+ platform.contrib_path = platform.sites_dir + @options[:site] if @options[:site]
179
+ outfile = platform.dependency_graph
180
+ ohai "#{outfile} created in the current directory."
181
+ end
182
+
183
+ def patch_interactive
184
+ patch = ''
185
+ if @options[:edit]
186
+ begin
187
+ tmp = Pathname.new `mktemp -d /tmp/temp_item-XXXXXX`.strip
188
+ dl = Drupid.makeDownloader @options[:edit], tmp, File.basename(@options[:edit])
189
+ ohai "Fetching #{@options[:edit]}"
190
+ dl.fetch
191
+ dl.stage
192
+ wd = dl.staged_path
193
+ rescue => ex
194
+ odie "Error retrieving #{@options[:edit]}: #{ex}"
195
+ end
196
+ else
197
+ wd = Pathname.pwd
198
+ end
199
+ git_repo = (wd + '.git').exist?
200
+ Dir.chdir wd.to_s do
201
+ if git_repo
202
+ blah "This directory appears to be a git repo"
203
+ unless git('status', '-s').empty?
204
+ odie "This git repo is not in a clean state"
205
+ end
206
+ else
207
+ begin
208
+ blah "Initializing temporary git repo inside #{wd}"
209
+ git 'init'
210
+ git 'add', '-A'
211
+ git 'commit', '-m', 'Temporary commit'
212
+ rescue
213
+ odie "Unable to create temporary git repo."
214
+ end
215
+ end
216
+ begin
217
+ ohai 'Make any changes you wish, then exit from the shell.'
218
+ interactive_shell
219
+ git 'add', '-A'
220
+ patch = git 'diff', '--binary', 'HEAD'
221
+ ensure
222
+ git 'reset', '--hard'
223
+ FileUtils.rmtree '.git' unless git_repo
224
+ end
225
+ end
226
+ # Show/write patch
227
+ if patch.empty?
228
+ ohai "No changes made"
229
+ else
230
+ if @options[:out]
231
+ writeFile @options[:out], patch
232
+ ohai "Patch written to #{@options[:out]}"
233
+ else
234
+ ohai 'May I interest you in a patch?'
235
+ puts patch
236
+ end
237
+ end
238
+ end
239
+
240
+ def go!
241
+ preflight_dependencies
242
+ parse_options
243
+ ohai "Dry run" if @options[:dry]
244
+ case @options[:command]
245
+ when :clear then clear_cache
246
+ when :graph then graph
247
+ when :edit then patch_interactive
248
+ else sync!
249
+ end
250
+ end
251
+
252
+ def self.rush_in!
253
+ begin
254
+ drupid = Fools.new
255
+ drupid.go!
256
+ rescue Interrupt
257
+ puts
258
+ drupid.ohai "Drupid interrupted"
259
+ rescue => ex
260
+ puts
261
+ drupid.debug 'Backtrace:', ex.backtrace.join("\n")
262
+ drupid.odie "Unexpected exception raised:\n#{ex}"
263
+ end
264
+ end
265
+
266
+ end # Fools
267
+
268
+ Fools.rush_in!
269
+
270
+ end # Drupid
@@ -0,0 +1,236 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2012 Lifepillar
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ module Drupid
24
+
25
+ if RUBY_PLATFORM =~ /darwin/
26
+ @@cache_path = Pathname.new(ENV['HOME']) + 'Library/Caches/Drupid'
27
+ else
28
+ @@cache_path = Pathname.new(ENV['HOME']) + '.drupid_cache'
29
+ end
30
+
31
+ def Drupid.cache_path
32
+ @@cache_path
33
+ end
34
+
35
+ def Drupid.cache_path=(new_path)
36
+ raise "Invalid cache path" unless new_path.to_s =~ /cache/i
37
+ @@cache_path = Pathname.new(new_path).realpath # must exist
38
+ end
39
+
40
+ class Component
41
+ include Drupid::Utils
42
+
43
+ attr :name
44
+ attr_accessor :download_url
45
+ attr_accessor :download_type
46
+ attr_accessor :download_specs
47
+ attr_accessor :overwrite
48
+ attr_accessor :local_path
49
+ attr :ignore_paths
50
+
51
+ def initialize name
52
+ @name = name
53
+ @download_url = nil
54
+ @download_type = nil
55
+ @download_specs = Hash.new
56
+ @overwrite = false
57
+ @subdir = nil
58
+ @directory_name = nil
59
+ @local_path = nil
60
+ @ignore_paths = Array.new
61
+ @patches = Array.new
62
+ end
63
+
64
+ # Performs a deep copy of this object.
65
+ def clone
66
+ Marshal.load(Marshal.dump(self))
67
+ end
68
+
69
+ def extended_name
70
+ @name
71
+ end
72
+
73
+ # A synonym for #extended_name.
74
+ def to_s
75
+ extended_name
76
+ end
77
+
78
+ # Returns a path to a subdirectory where this component
79
+ # should be installed. The path is meant to be relative
80
+ # to the 'default' installation path (e.g., 'sites/all/modules' for modules,
81
+ # 'sites/all/themes' for themes, 'profiles' for profiles, etc...).
82
+ # For example, if a module 'foobar' must be installed under 'sites/all/modules'
83
+ # and this property is set, say, to 'contrib', then the module will be installed
84
+ # at 'sites/all/modules/contrib/foobar'.
85
+ def subdir
86
+ (@subdir) ? Pathname.new(@subdir) : Pathname.new('.')
87
+ end
88
+
89
+ # Sets the path to a subdirectory where this component should be installed,
90
+ # relative to the default installation path.
91
+ def subdir=(d)
92
+ @subdir = d
93
+ end
94
+
95
+ # Returns the directory name for this component.
96
+ def directory_name
97
+ return @directory_name.to_s if @directory_name
98
+ return local_path.basename.to_s if exist?
99
+ return name
100
+ end
101
+
102
+ # Sets the directory name for this component.
103
+ def directory_name=(d)
104
+ @directory_name = d
105
+ end
106
+
107
+ # Returns true if this project is associated to a local copy on disk;
108
+ # returns false otherwise.
109
+ def exist?
110
+ @local_path and @local_path.exist?
111
+ end
112
+
113
+ def add_download_spec(spec, ref)
114
+ @download_specs.merge!({spec => ref})
115
+ end
116
+
117
+ # Downloads to local cache.
118
+ def fetch
119
+ if cached_location.exist?
120
+ @local_path = cached_location
121
+ debug "#{extended_name} is cached"
122
+ else
123
+ raise "No download URL specified for #{extended_name}" unless download_url
124
+ blah "Fetching #{extended_name}"
125
+ downloader = Drupid.makeDownloader download_url.to_s, cached_location.dirname.to_s, cached_location.basename.to_s, download_specs
126
+ downloader.fetch
127
+ downloader.stage
128
+ @local_path = downloader.staged_path
129
+ end
130
+ end
131
+
132
+ # Applies the patches associated to this component.
133
+ # Raises an exception if a patch cannot be applied.
134
+ def patch
135
+ fetch unless exist?
136
+ return unless has_patches?
137
+ patched_location.rmtree if patched_location.exist? # Ensure no previous patched copy exists
138
+ @local_path.ditto patched_location
139
+ @local_path = patched_location
140
+ # Download patches
141
+ patched_location.dirname.cd do
142
+ each_patch do |p|
143
+ p.fetch
144
+ end
145
+ end
146
+ # Apply patches
147
+ patched_location.cd do
148
+ each_patch do |p|
149
+ p.apply
150
+ end
151
+ end
152
+ end
153
+
154
+ # Removes all the patches from this component.
155
+ def clear_patches
156
+ @patches.clear
157
+ end
158
+
159
+ # Returns true if this component has been patched;
160
+ # returns false otherwise.
161
+ def patched?
162
+ @local_path == patched_location
163
+ end
164
+
165
+ # Iterates over each patch associated to this component,
166
+ # yielding a Drupid::Patch object.
167
+ def each_patch
168
+ @patches.each do |p|
169
+ yield p
170
+ end
171
+ end
172
+
173
+ # Returns true if patches are associated to this component,
174
+ # returns false otherwise.
175
+ def has_patches?
176
+ !@patches.empty?
177
+ end
178
+
179
+ def add_patch(url, descr, md5 = nil)
180
+ @patches << Patch.new(url, descr, md5)
181
+ end
182
+
183
+ # Returns the first patch with the given description, or
184
+ # nil if no such patch exists.
185
+ def get_patch descr
186
+ @patches.each do |p|
187
+ return p if descr == p.descr
188
+ end
189
+ end
190
+
191
+ # Full path to the location where a cached copy of this component is located.
192
+ def cached_location
193
+ dlt = (download_type) ? download_type : 'default'
194
+ Drupid.cache_path + self.class.to_s.split(/::/).last + extended_name + dlt + name
195
+ end
196
+
197
+ # Full path to the directory where a patched copy of this component is located.
198
+ def patched_location
199
+ cached_location.dirname + '__patches' + name
200
+ end
201
+
202
+ # Ignores the given path relative to this component's path.
203
+ # This is useful, for example, when an external library is installed
204
+ # inside a module's folder (rather than in the libraries folder).
205
+ def ignore_path(relative_path)
206
+ @ignore_paths << Pathname.new(relative_path)
207
+ end
208
+
209
+ # Performs a file-by-file comparison of this component with another.
210
+ # Returns a list of files that are different between the two copies.
211
+ # If the directories of the two projects look the same, returns an empty array.
212
+ # Local copies must exist for both projects, otherwise this method raises an error.
213
+ #
214
+ # If one of the projects has a makefile, the content of the following directories
215
+ # is ignored: libraries, modules, themes.
216
+ # Version control directories (.git) are always ignored.
217
+ def file_level_compare_with tgt, additional_rsync_args = []
218
+ raise "#{extended_name} does not exist at #{local_path}" unless exist?
219
+ raise "#{tgt.extended_name} does not exist at #{tgt.local_path}" unless tgt.exist?
220
+ args = Array.new
221
+ default_exclusions = [
222
+ '.DS_Store',
223
+ '.git/',
224
+ '.bzr/',
225
+ '.hg/',
226
+ '.svn/'
227
+ ]
228
+ default_exclusions.each { |e| args << "--exclude=#{e}" }
229
+ ignore_paths.each { |p| args << "--exclude=#{p}" }
230
+ tgt.ignore_paths.each { |p| args << "--exclude=#{p}" }
231
+ args += additional_rsync_args
232
+ compare_paths local_path, tgt.local_path, args
233
+ end
234
+
235
+ end # Component
236
+ end # Drupid