drupid 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/drupid +270 -0
- data/lib/drupid/component.rb +236 -0
- data/lib/drupid/download_strategy.rb +585 -0
- data/lib/drupid/drush.rb +185 -0
- data/lib/drupid/extend/pathname.rb +114 -0
- data/lib/drupid/library.rb +52 -0
- data/lib/drupid/makefile.rb +423 -0
- data/lib/drupid/patch.rb +92 -0
- data/lib/drupid/platform.rb +234 -0
- data/lib/drupid/platform_project.rb +91 -0
- data/lib/drupid/project.rb +563 -0
- data/lib/drupid/updater.rb +683 -0
- data/lib/drupid/utils.rb +301 -0
- data/lib/drupid/version.rb +230 -0
- data/lib/drupid.rb +56 -0
- metadata +76 -0
@@ -0,0 +1,563 @@
|
|
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
|
+
# A convenience class that encapsulates methods for detecting
|
26
|
+
# some project's properties from a local copy of a project.
|
27
|
+
class ProjectInfo
|
28
|
+
include Drupid::Utils
|
29
|
+
|
30
|
+
# The project's name.
|
31
|
+
attr :project_name
|
32
|
+
# The project's core compatibility number. See Drupid::VersionCore.
|
33
|
+
attr :project_core
|
34
|
+
# The project's version. See Drupid::Version.
|
35
|
+
attr :project_version
|
36
|
+
# The project's type ('core', 'module', 'theme', 'profile').
|
37
|
+
attr :project_type
|
38
|
+
# The full path to the project's directory.
|
39
|
+
attr :project_dir
|
40
|
+
# The full path to the main .info file of this project
|
41
|
+
attr :info_file
|
42
|
+
|
43
|
+
# The argument must be the path to a .info file or a project directory.
|
44
|
+
# If the path to a directory is passed, then this object will try
|
45
|
+
# automatically detect the .info file inside the directory, which is
|
46
|
+
# not a trivial task because, among the rest,
|
47
|
+
#
|
48
|
+
# - the .info file may be located in some sub-directory;
|
49
|
+
# - there may be more than one .info file;
|
50
|
+
# - the .info file name may be unrelated to the name of its containing
|
51
|
+
# directories.
|
52
|
+
#
|
53
|
+
# Faithful to its name, Drupid won't try to be too keen, and it will raise
|
54
|
+
# an exception if it cannot reliably detect a .info file.
|
55
|
+
def initialize(project_or_info_path)
|
56
|
+
p = Pathname.new(project_or_info_path).realpath # must exist
|
57
|
+
@project_core = nil
|
58
|
+
@project_type = nil
|
59
|
+
@project_version = nil
|
60
|
+
if '.info' == p.extname
|
61
|
+
@project_name = p.basename('.info').to_s
|
62
|
+
@info_file = p
|
63
|
+
grandparent = @info_file.parent.parent
|
64
|
+
if grandparent.basename.to_s == @project_name
|
65
|
+
@project_dir = grandparent
|
66
|
+
else
|
67
|
+
@project_dir = @info_file.parent
|
68
|
+
end
|
69
|
+
else
|
70
|
+
@project_name = p.basename.to_s
|
71
|
+
@project_dir = p
|
72
|
+
@info_file = _identify_main_info_file
|
73
|
+
end
|
74
|
+
debug "Parsing project info from #{@info_file}"
|
75
|
+
_parse_info_file
|
76
|
+
end
|
77
|
+
|
78
|
+
def core_project?
|
79
|
+
@is_core_project
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Returns the absolute path of the "main" .info file of this project.
|
85
|
+
# The first file satisfying one of the following heuristics is returned:
|
86
|
+
#
|
87
|
+
# 1. './#name.info' exists.
|
88
|
+
# 2. './#name/#name.info' exists.
|
89
|
+
# 3. './<any name>.info' exists and no other .info file exists at the top-level.
|
90
|
+
# 4. './#name/<any name>.info' exists and no other .info file exists inside './#name'.
|
91
|
+
# 5. There is a unique .info file, anywhere inside the project's folder.
|
92
|
+
#
|
93
|
+
# If none of the above is satisfied, pick any .info file and set the
|
94
|
+
# project's name after the .info file's 'project' field (if any). Then
|
95
|
+
# return that .info file.
|
96
|
+
#
|
97
|
+
# Finally, if the .info file has no 'project' field, give up
|
98
|
+
# hoping that one day Drupal will have better specifications and that people
|
99
|
+
# will eventually follow the specifications—but complain fiercely
|
100
|
+
# by raising an exception.
|
101
|
+
def _identify_main_info_file
|
102
|
+
attempts = [
|
103
|
+
@project_dir + (@project_name + '.info'),
|
104
|
+
@project_dir + @project_name + (@project_name + '.info'),
|
105
|
+
@project_dir + '*.info',
|
106
|
+
@project_dir + @project_name + '*.info',
|
107
|
+
@project_dir+'**/*.info'
|
108
|
+
]
|
109
|
+
attempts.each do |p|
|
110
|
+
list = Pathname.glob(p)
|
111
|
+
if 1 == list.size and list.first.exist?
|
112
|
+
# Set the project's name after the .info file name
|
113
|
+
@project_name = list.first.basename('.info').to_s
|
114
|
+
return list.first
|
115
|
+
end
|
116
|
+
end
|
117
|
+
# We get here if all the above has failed.
|
118
|
+
Pathname.glob(@project_dir+'**/*.info').each do |p|
|
119
|
+
data = p.open("r").read
|
120
|
+
match = data.match(/project\s*=\s*["']?(.+)["']?/)
|
121
|
+
unless match.nil?
|
122
|
+
@project_name = match[1].strip
|
123
|
+
return p
|
124
|
+
end
|
125
|
+
end
|
126
|
+
# Give up :/
|
127
|
+
raise "The .info file for #{@project_name} cannot be reliably detected"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Extracts the relevant information from the .info file.
|
131
|
+
def _parse_info_file
|
132
|
+
_read_info_file
|
133
|
+
_check_project_name
|
134
|
+
_set_project_version
|
135
|
+
_set_project_type
|
136
|
+
end
|
137
|
+
|
138
|
+
# Reads the content of the .info file into a hash.
|
139
|
+
# Parses only 'simple' key-value pairs (of the form X = v).
|
140
|
+
# Then, check for some other keys useful to determine the project's type
|
141
|
+
# (e.g, 'stylesheets')
|
142
|
+
def _read_info_file
|
143
|
+
@info_data = Hash.new
|
144
|
+
data = @info_file.open("r").read
|
145
|
+
data.each_line do |l|
|
146
|
+
next if l =~ /^\s*$/
|
147
|
+
next if l =~ /^\s*;/
|
148
|
+
if l.match(/^(.+)=(.+)$/)
|
149
|
+
key = $~[1].strip
|
150
|
+
value = $~[2].strip.gsub(/\A["']|["']\Z/, '')
|
151
|
+
@info_data[key] = value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
@info_data['stylesheets'] = true if data.match(/^\s*stylesheets */)
|
155
|
+
@info_data['regions'] = true if data.match(/^\s*regions */)
|
156
|
+
end
|
157
|
+
|
158
|
+
# If the .info file name differs from the name of the containing directory
|
159
|
+
# and the .info file contains a 'project' field, do the following:
|
160
|
+
#
|
161
|
+
# - if <project name>.info does not exist and if the 'project' field is
|
162
|
+
# the same as the .info file name or the directory name, update the project's
|
163
|
+
# name accordingly.
|
164
|
+
#
|
165
|
+
# This check will fix, for example, the project's name for a project like
|
166
|
+
# Google Analytics, whose project's name is 'google_analytics' but the .info
|
167
|
+
# file is called 'googleanalytics.info'.
|
168
|
+
# It will also fix the project's name when the directory name has been
|
169
|
+
# changed and this object has been passed the path to the project's directory
|
170
|
+
# rather than the path to the .info file.
|
171
|
+
#
|
172
|
+
# Testing that <project name>.info does not exist is necessary to avoid
|
173
|
+
# renaming projects when more than one .info file exists in the same directory
|
174
|
+
# (see for example the Entity module).
|
175
|
+
def _check_project_name
|
176
|
+
dirname = @project_dir.basename.to_s
|
177
|
+
if @project_name != dirname and # E.g., 'featured_news' != 'featured_news_feature'
|
178
|
+
!(@info_file.dirname+(dirname+'.info')).exist? and # E.g., '.../featured_news_feature/featured_news_feature.info' does not exist
|
179
|
+
@info_data.has_key?('project')
|
180
|
+
pn = @info_data['project']
|
181
|
+
if pn == @info_file.basename('.info').to_s or pn == dirname
|
182
|
+
@project_name = pn
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def _set_project_core
|
188
|
+
@info_data['core'].match(/^(\d+)\.x$/)
|
189
|
+
raise "Missing mandatory core compatibility for #{@project_name}" unless $1
|
190
|
+
@project_core = VersionCore.new($1)
|
191
|
+
end
|
192
|
+
|
193
|
+
def _set_project_version
|
194
|
+
_set_project_core
|
195
|
+
if @info_data.has_key?('version')
|
196
|
+
v = @info_data['version']
|
197
|
+
v = @project_core.to_s + '-' + v if v !~ /^#{@project_core}-/
|
198
|
+
@project_version = Version.from_s(v)
|
199
|
+
else
|
200
|
+
@project_version = nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# *Requires:* @info_data must not be nil
|
205
|
+
def _set_project_type
|
206
|
+
# Determine whether this is a core project
|
207
|
+
if (@info_data.has_key?('package') and @info_data['package'] =~ /Core/i) or
|
208
|
+
(@info_data.has_key?('project') and @info_data['project'] =~ /drupal/i)
|
209
|
+
@is_core_project = true
|
210
|
+
else
|
211
|
+
@is_core_project = false
|
212
|
+
end
|
213
|
+
# Determine the project's type (module, profile or theme)
|
214
|
+
if @info_file.sub_ext('.profile').exist?
|
215
|
+
@project_type = 'profile'
|
216
|
+
elsif @info_file.sub_ext('.module').exist?
|
217
|
+
@project_type = 'module'
|
218
|
+
elsif @info_data.has_key?('engine') or
|
219
|
+
@info_data.has_key?('Base theme') or
|
220
|
+
@info_data.has_key?('base theme') or
|
221
|
+
@info_data.has_key?('stylesheets') or
|
222
|
+
@info_data.has_key?('regions')
|
223
|
+
@project_type = 'theme'
|
224
|
+
end
|
225
|
+
# If the above didn't work, examine the path the project is in as a last resort.
|
226
|
+
# This is needed, at least, to avoid "type cannot be determined" errors
|
227
|
+
# for some test directories in Drupal Core, which contain an .info file
|
228
|
+
# but no other file :/
|
229
|
+
unless project_type
|
230
|
+
@project_dir.each_filename do |p|
|
231
|
+
case p
|
232
|
+
when 'modules'
|
233
|
+
@project_type = 'module'
|
234
|
+
when 'themes'
|
235
|
+
@project_type = 'theme'
|
236
|
+
when 'profiles'
|
237
|
+
@project_type = 'profile'
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
raise "The project's type for #{@project_name} cannot be determined" unless @project_type
|
242
|
+
end
|
243
|
+
|
244
|
+
end # ProjectInfo
|
245
|
+
|
246
|
+
|
247
|
+
# Base class for projects.
|
248
|
+
class Project < Component
|
249
|
+
include Comparable
|
250
|
+
|
251
|
+
attr :core
|
252
|
+
attr_accessor :location
|
253
|
+
# The type of this project, which is one among 'drupal', 'module', 'theme'
|
254
|
+
# and 'profile', or nil if the type has not been determined or assigned.
|
255
|
+
# Note that this does not coincide with the 'type' field in a Drush makefile,
|
256
|
+
# whose feasible values are 'core', 'module', 'theme', 'profile'.
|
257
|
+
attr_accessor :proj_type
|
258
|
+
attr_accessor :l10n_path
|
259
|
+
attr_accessor :l10n_url
|
260
|
+
|
261
|
+
# Creates a new project with a given name and compatibility number.
|
262
|
+
# Optionally, specify a short version string (i.e., a version string
|
263
|
+
# without core compatibility number).
|
264
|
+
#
|
265
|
+
# Examples:
|
266
|
+
# p = Drupid::Project.new('cck', 6)
|
267
|
+
# p = Drupid::Project.new('views', 7, '1.2')
|
268
|
+
def initialize name, core_num, vers = nil
|
269
|
+
super(name)
|
270
|
+
@core = VersionCore.new(core_num)
|
271
|
+
@core_project = ('drupal' == @name) ? true : nil
|
272
|
+
@version = vers ? Version.from_s(@core.to_s + '-' + vers) : nil
|
273
|
+
@proj_type = ('drupal' == @name) ? 'drupal' : nil
|
274
|
+
@info_file = nil
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns true if a version is specified for this project, false otherwise.
|
278
|
+
def has_version?
|
279
|
+
nil != @version
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns the version of this project as a Drupid::Version object,
|
283
|
+
# or nil if this project has not been assigned a version.
|
284
|
+
def version
|
285
|
+
@version
|
286
|
+
end
|
287
|
+
|
288
|
+
# Assigns a version to this project.
|
289
|
+
# The argument must be a String object or a Drupid::Version object.
|
290
|
+
# For the syntax of the String argument, see Drupid::Version.
|
291
|
+
def version=(new_version)
|
292
|
+
if new_version.is_a?(Version)
|
293
|
+
temp_version = new_version
|
294
|
+
elsif new_version.is_a?(String)
|
295
|
+
v = new_version
|
296
|
+
temp_version = Version.from_s(v)
|
297
|
+
else
|
298
|
+
raise NotDrupalVersionError
|
299
|
+
end
|
300
|
+
raise NotDrupalVersionError, "Incompatible version for project #{extended_name}: #{temp_version.long}" if temp_version.core != core
|
301
|
+
@version = temp_version
|
302
|
+
end
|
303
|
+
|
304
|
+
# Updates the version of this project to the latest recommended release;
|
305
|
+
# if no recommended release is available, tries to retrieve the version
|
306
|
+
# of the latest supported release; if no supported release is found, the version
|
307
|
+
# of this project is not modified.
|
308
|
+
# Raises an error if no release history for the project is found (presumably
|
309
|
+
# because no such project exists on drupal.org).
|
310
|
+
#
|
311
|
+
# *Requires:* a network connection.
|
312
|
+
def update_version
|
313
|
+
v = recommended_release
|
314
|
+
v = supported_release unless v
|
315
|
+
if v
|
316
|
+
self.version = core.to_s + '-' + v
|
317
|
+
debug "Version updated: #{extended_name}"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Returns true if this object corresponds to Drupal core;
|
322
|
+
# returns false otherwise.
|
323
|
+
def drupal?
|
324
|
+
'drupal' == proj_type
|
325
|
+
end
|
326
|
+
|
327
|
+
# Returns true if this is a profile; returns false otherwise.
|
328
|
+
def profile?
|
329
|
+
'profile' == proj_type
|
330
|
+
end
|
331
|
+
|
332
|
+
# Returns true if this is a core project; returns false otherwise.
|
333
|
+
def core_project?
|
334
|
+
@core_project
|
335
|
+
end
|
336
|
+
|
337
|
+
def core_project=(c)
|
338
|
+
@core_project = c
|
339
|
+
end
|
340
|
+
|
341
|
+
# See Version for the reason why we define == explicitly.
|
342
|
+
def ==(other)
|
343
|
+
@name == other.name and
|
344
|
+
@core == other.core and
|
345
|
+
@version == other.version
|
346
|
+
end
|
347
|
+
|
348
|
+
# Compares this project with another to determine which is newer.
|
349
|
+
# The comparison returns nil if the two projects have different names
|
350
|
+
# or at least one of them has no version;
|
351
|
+
# otherwise, returns -1 if this project is older than the other,
|
352
|
+
# 1 if this project is more recent than the other,
|
353
|
+
# 0 if this project has the same version as the other.
|
354
|
+
def <=>(other)
|
355
|
+
return nil if @name != other.name
|
356
|
+
c = core <=> other.core
|
357
|
+
if 0 == c
|
358
|
+
return nil unless has_version? and other.has_version?
|
359
|
+
return version <=> other.version
|
360
|
+
else
|
361
|
+
return c
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Returns the name and the version of this project as a string, e.g.,
|
366
|
+
# 'media-7.x-2.0-unstable2' or 'drupal-7.14'.
|
367
|
+
# If no version is specified for this project,
|
368
|
+
# returns only the project's name and core compatibility number.
|
369
|
+
def extended_name
|
370
|
+
if has_version?
|
371
|
+
return name + '-' + ((drupal?) ? version.short : version.long)
|
372
|
+
else
|
373
|
+
return name + '-' + core.to_s
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Returns a list of the names of the extensions (modules and themes) upon
|
378
|
+
# which this project and its subprojects (the projects contained within
|
379
|
+
# this one) depend.
|
380
|
+
# Returns an empty list if no local copy of this project exists.
|
381
|
+
#
|
382
|
+
# If :subprojects is set to false, subprojects' dependencies are not computed.
|
383
|
+
#
|
384
|
+
# Options: subprojects
|
385
|
+
def dependencies options = {}
|
386
|
+
return [] unless exist?
|
387
|
+
deps = Array.new
|
388
|
+
if options.has_key?(:subprojects) and (not options[:subprojects])
|
389
|
+
reload_project_info unless @info_file and @info_file.exist?
|
390
|
+
info_files = [@info_file]
|
391
|
+
else
|
392
|
+
info_files = Dir["#{local_path}/**/*.info"]
|
393
|
+
end
|
394
|
+
info_files.each do |info|
|
395
|
+
f = File.open(info, "r").read
|
396
|
+
f.each_line do |l|
|
397
|
+
matchdata = l.match(/^\s*dependencies\s*\[\s*\]\s*=\s*["']?([^\s("']+)/)
|
398
|
+
if nil != matchdata
|
399
|
+
deps << matchdata[1].strip
|
400
|
+
end
|
401
|
+
matchdata = l.match(/^\s*base +theme\s*=\s*(.+)$/)
|
402
|
+
if nil != matchdata
|
403
|
+
d = matchdata[1].strip
|
404
|
+
deps << d.gsub(/\A["']|["']\Z/, '') # Strip leading and trailing quotes
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
# Remove duplicates and self-dependency
|
409
|
+
deps.uniq!
|
410
|
+
deps.delete(name)
|
411
|
+
return deps
|
412
|
+
end
|
413
|
+
|
414
|
+
# Returns a list of the names of the extensions (modules and themes)
|
415
|
+
# contained in this project.
|
416
|
+
# Returns a list containing only the project's name
|
417
|
+
# if no local copy of this project exists.
|
418
|
+
def extensions
|
419
|
+
return [name] unless exist?
|
420
|
+
# Note that the project's name may be different from the name of the .info file.
|
421
|
+
ext = [name]
|
422
|
+
Dir["#{local_path}/**/*.info"].map do |p|
|
423
|
+
ext << File.basename(p, '.info')
|
424
|
+
end
|
425
|
+
ext.uniq!
|
426
|
+
return ext
|
427
|
+
end
|
428
|
+
|
429
|
+
def reload_project_info
|
430
|
+
project_info = ProjectInfo.new(@local_path)
|
431
|
+
raise "Inconsistent naming: expected #{@name}, got #{project_info.project_name}" unless @name == project_info.project_name
|
432
|
+
raise "Inconsistent core: expected #{@core}, got #{project_info.project_core}" unless @core == project_info.project_core
|
433
|
+
@proj_type = project_info.project_type
|
434
|
+
@core_project = project_info.core_project?
|
435
|
+
@version = project_info.project_version
|
436
|
+
@info_file = project_info.info_file
|
437
|
+
end
|
438
|
+
|
439
|
+
def fetch
|
440
|
+
# Try to get the latest version if:
|
441
|
+
# (1) the project is not local;
|
442
|
+
# (2) it does not have a version already;
|
443
|
+
# (3) no download type has been explicitly given.
|
444
|
+
unless has_version? or download_url =~ /file:\/\// or download_type
|
445
|
+
update_version
|
446
|
+
end
|
447
|
+
# If the project has no version we fetch it even if it is cached.
|
448
|
+
# If the project has a download type, we fetch it even if it is cached
|
449
|
+
# (say the download type is 'git' and the revision is changed in the
|
450
|
+
# makefile, then the cached project must be updated accordingly).
|
451
|
+
if has_version? and !download_type and cached_location.exist?
|
452
|
+
@local_path = cached_location
|
453
|
+
debug "#{extended_name} is cached"
|
454
|
+
else
|
455
|
+
blah "Fetching #{extended_name}"
|
456
|
+
if download_type
|
457
|
+
if download_url
|
458
|
+
src = download_url
|
459
|
+
elsif 'git' == download_type # Download from git.drupal.org
|
460
|
+
src = "http://git.drupal.org/project/#{name}.git"
|
461
|
+
else
|
462
|
+
raise "No download URL specified for #{extended_name}" unless download_url
|
463
|
+
end
|
464
|
+
else
|
465
|
+
src = extended_name
|
466
|
+
end
|
467
|
+
downloader = Drupid.makeDownloader src, cached_location.dirname.to_s, cached_location.basename.to_s, download_specs.merge({:type => download_type})
|
468
|
+
downloader.fetch
|
469
|
+
downloader.stage
|
470
|
+
@local_path = downloader.staged_path
|
471
|
+
end
|
472
|
+
reload_project_info unless drupal?
|
473
|
+
end
|
474
|
+
|
475
|
+
# Returns the relative path where this project should be installed
|
476
|
+
# within a platform.
|
477
|
+
# For example, for a module called 'Foo', it might be something like
|
478
|
+
# 'modules/contrib/foo'.
|
479
|
+
def target_path
|
480
|
+
case proj_type
|
481
|
+
when 'drupal'
|
482
|
+
return Pathname.new('.')
|
483
|
+
when nil
|
484
|
+
raise "Undefined project type for #{name}."
|
485
|
+
else
|
486
|
+
return Pathname.new(proj_type + 's') + subdir + directory_name
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# Returns the path to a makefile contained in this project, if any.
|
491
|
+
# Returns nil if this project does not contain any makefile.
|
492
|
+
# For an embedded makefile to be recognized, the makefile
|
493
|
+
# itself must be named '#name.make' or 'drupal-org.make'.
|
494
|
+
#
|
495
|
+
# *Requires:* a local copy of this project.
|
496
|
+
def makefile
|
497
|
+
return nil unless self.exist?
|
498
|
+
paths = [
|
499
|
+
local_path + "#{name}.make",
|
500
|
+
local_path + 'drupal-org.make' # Used in Drupal distributions
|
501
|
+
]
|
502
|
+
paths.each do |p|
|
503
|
+
return p if p.exist?
|
504
|
+
end
|
505
|
+
return nil
|
506
|
+
end
|
507
|
+
|
508
|
+
# Compares this project with another, returning an array of differences.
|
509
|
+
# If this project contains a makefile, ignore the content of the following
|
510
|
+
# directories inside the project: libraries, modules, profiles and themes.
|
511
|
+
def file_level_compare_with tgt
|
512
|
+
args = Array.new
|
513
|
+
if makefile
|
514
|
+
args << '-f' << '- /libraries/***' # this syntax requires rsync >=2.6.7.
|
515
|
+
args << '-f' << '- /modules/***'
|
516
|
+
args << '-f' << '- /profiles/***'
|
517
|
+
args << '-f' << '- /themes/***'
|
518
|
+
end
|
519
|
+
if drupal?
|
520
|
+
args << '-f' << '+ /profiles/default/***' # D6
|
521
|
+
args << '-f' << '+ /profiles/minimal/***' # D7
|
522
|
+
args << '-f' << '+ /profiles/standard/***' # D7
|
523
|
+
args << '-f' << '+ /profiles/testing/***' # D7
|
524
|
+
args << '-f' << '- /profiles/***'
|
525
|
+
args << '-f' << '+ /sites/all/README.txt'
|
526
|
+
args << '-f' << '+ /sites/default/default.settings.php'
|
527
|
+
args << '-f' << '- /sites/***'
|
528
|
+
end
|
529
|
+
super(tgt, args)
|
530
|
+
end
|
531
|
+
|
532
|
+
# Returns the version of the latest recommended release of this project
|
533
|
+
# as a string, or nil if no recommended release can be found
|
534
|
+
# or the project does not exist at drupal.org.
|
535
|
+
#
|
536
|
+
# *Requires:* a network connection.
|
537
|
+
#
|
538
|
+
# See also: Drupid::Drush.pm_releases, Drupid::Project.supported_release
|
539
|
+
def recommended_release
|
540
|
+
output = Drush.pm_releases(name + '-' + core.to_s)
|
541
|
+
return unless output =~ /RELEASES FOR/
|
542
|
+
rl = Drush.recommended_release(output)
|
543
|
+
return rl.sub(/^#{core}-/, '') if rl
|
544
|
+
return nil
|
545
|
+
end
|
546
|
+
|
547
|
+
# Returns the version of the latest supported release
|
548
|
+
# as a string, or nil if no supported release can be found
|
549
|
+
# or the project does not exist at drupal.org.
|
550
|
+
#
|
551
|
+
# *Requires:* a network connection.
|
552
|
+
#
|
553
|
+
# See also: Drupid::Drush.pm_releases, Drupid::Project.recommended_release
|
554
|
+
def supported_release
|
555
|
+
output = Drush.pm_releases(name + '-' + core.to_s)
|
556
|
+
return nil unless output =~ /RELEASES FOR/
|
557
|
+
rl = Drush.supported_release(output)
|
558
|
+
return rl.sub(/^#{core}-/, '') if rl
|
559
|
+
return nil
|
560
|
+
end
|
561
|
+
|
562
|
+
end # Project
|
563
|
+
end # Drupid
|