lib/between_meals/changes/cookbook.rb (105 lines of code) (raw):
# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
# Copyright 2013-present Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module BetweenMeals
module Changes
# Changeset aware cookbook
class Cookbook < Change
def self.meaningful_cookbook_file?(path)
!explode_path(path).nil?
end
def self.explode_path(path)
@cookbook_dirs.each do |dir|
re = %r{^#{dir}/([^/]+)/.*}
debug("[cookbook] Matching #{path} against ^#{re}")
m = path.match(re)
next unless m
info("Cookbook is #{m[1]}")
return {
:cookbook_dir => dir,
:name => m[1],
}
end
nil
end
def self.map_symlinks(files)
# For each symlink get the source path, if any files have changed under
# the source path, fake them as coming from the symlink path. This
# allows the normal cookbook logic to just work.
symlinks = {}
@cookbook_dirs.each do |dir|
dir = File.join(@repo_dir, dir)
# Find symlinks in each cookbook_dir
links = Dir.foreach(dir).select do |d|
File.symlink?(File.join(dir, d))
end
links.each do |link|
link = File.join(dir, link)
next if symlinks[link]
source = File.realpath(link)
repo = File.join(@repo_dir, '/')
# maps absolute symlink path to relative source and link paths
symlinks[link] = {
'source' => source.gsub(repo, ''),
'link' => link.gsub(repo, ''),
}
end
end
# Create the file hash expected for each file that is a link or coming
# from a linked directory but fake the source path as a symlink path.
# Hacky but works :)
links_to_append = []
symlinks.each_value do |lrp| # link_abs_path, link_relative_path
files.each do |f|
# a symlink will never have trailing '/', add one.
f[:path] += '/' if f[:path] == lrp['link']
# If a metadata file in the path of a symlink target directory has a
# deleted status, check if a metadata file exists in the symlink
# source directory. If so, mark it as modified to prevent deletion.
symlink_source_dir = File.join(@repo_dir, lrp['source'])
if (f[:path] == File.join(lrp['link'], 'metadata.rb') ||
f[:path] == File.join(lrp['link'], 'metadata.json')) &&
f[:status] == :deleted &&
(File.file?(File.join(symlink_source_dir, 'metadata.rb')) ||
File.file?(File.join(symlink_source_dir, 'metadata.json')))
f[:status] = :modified
end
next unless f[:path].start_with?(lrp['source'])
# This make a deep dup of the file hash
l = Marshal.load(Marshal.dump(f))
l[:path].gsub!(lrp['source'], lrp['link'])
links_to_append << l
end
end
links_to_append
end
def initialize(files, cookbook_dirs)
@files = files
@cookbook_dirs = cookbook_dirs
@name = self.class.explode_path(files.sample[:path])[:name]
# if metadata.(json|rb) is being deleted and we aren't also
# adding/modifying one of those two,
# cookbook is marked for deletion
# otherwise it was modified
# and will be re-uploaded
if files.
select { |x| x[:status] == :deleted }.
map do |x|
x[:path].match(
%{^(#{cookbook_dirs.join('|')})/[^/]+/metadata\.(rb|json)$},
)
end.
compact.
any? &&
files.reject { |x| x[:status] == :deleted }.
map do |x|
x[:path].match(
%{^(#{cookbook_dirs.join('|')})/[^/]+/metadata\.(rb|json)$},
)
end.none?
@status = :deleted
else
@status = :modified
end
end
# Given a list of changed files
# create a list of Cookbook objects
def self.find(list, cookbook_dirs, logger, repo, track_symlinks = false)
# rubocop:disable ClassVars
@@logger = logger
# rubocop:enable ClassVars
return [] if list.nil? || list.empty?
# rubocop:disable MultilineBlockChain
@repo_dir = File.realpath(repo.repo_path)
@cookbook_dirs = cookbook_dirs
list += map_symlinks(list) if track_symlinks
list.
group_by do |x|
# Group by prefix of cookbok_dir + cookbook_name
# so that we treat deletes and modifications across
# two locations separately
g = self.explode_path(x[:path])
g[:cookbook_dir] + '/' + g[:name] if g
end.
map do |_, change|
# Confirm we're dealing with a cookbook
# Changes to OWNERS or other stuff that might end up
# in [core, other, secure] dirs are ignored
is_cookbook = change.select do |c|
self.meaningful_cookbook_file?(c[:path])
end.any?
if is_cookbook
BetweenMeals::Changes::Cookbook.new(change, @cookbook_dirs)
end
end.compact
# rubocop:enable MultilineBlockChain
end
end
end
end