lib/dependabot/file_updaters/artifact_updater.rb
# typed: strict # frozen_string_literal: true require "sorbet-runtime" require "dependabot/dependency_file" # This class provides a utility to check for arbitrary modified files within a # git directory that need to be wrapped as Dependabot::DependencyFile object # and returned as along with anything managed by the FileUpdater itself. module Dependabot module FileUpdaters class ArtifactUpdater extend T::Sig extend T::Helpers # @param repo_contents_path [String, nil] the path we cloned the repository into # @param target_directory [String, nil] the path within a project directory we should inspect for changes sig { params(repo_contents_path: T.nilable(String), target_directory: T.nilable(String)).void } def initialize(repo_contents_path:, target_directory:) @repo_contents_path = repo_contents_path @target_directory = target_directory end # Returns any files that have changed within the path composed from: # :repo_contents_path/:base_directory/:target_directory # # @param base_directory [String] Update config base directory # @param only_paths [Array<String>, nil] An optional list of specific paths to check, if this is nil we will # return every change we find within the `base_directory` # @return [Array<Dependabot::DependencyFile>] sig do params(base_directory: String, only_paths: T.nilable(T::Array[String])) .returns(T::Array[Dependabot::DependencyFile]) end def updated_files(base_directory:, only_paths: nil) return [] unless repo_contents_path && target_directory Dir.chdir(T.must(repo_contents_path)) do # rubocop:disable Performance/DeletePrefix relative_dir = Pathname.new(base_directory).sub(%r{\A/}, "").join(T.must(target_directory)) # rubocop:enable Performance/DeletePrefix status = T.let( SharedHelpers.run_shell_command( "git status --untracked-files all --porcelain v1 #{relative_dir}", fingerprint: "git status --untracked-files all --porcelain v1 <relative_dir>" ), String ) changed_paths = status.split("\n").map(&:split) changed_paths.filter_map do |type, path| project_root = Pathname.new(File.expand_path(File.join(Dir.pwd, base_directory))) file_path = Pathname.new(path).expand_path.relative_path_from(project_root) # Skip this file if we are looking for specific paths and this isn't on the list next if only_paths && !only_paths.include?(file_path.to_s) # The following types are possible to be returned: # M = Modified = Default for DependencyFile # D = Deleted # ?? = Untracked = Created operation = Dependabot::DependencyFile::Operation::UPDATE operation = Dependabot::DependencyFile::Operation::DELETE if type == "D" operation = Dependabot::DependencyFile::Operation::CREATE if type == "??" encoded_content, encoding = get_encoded_file_contents(T.must(path), operation) create_dependency_file( name: file_path.to_s, content: encoded_content, directory: base_directory, operation: operation, content_encoding: encoding ) end end end private TEXT_ENCODINGS = T.let(%w(us-ascii utf-8).freeze, T::Array[String]) sig { returns(T.nilable(String)) } attr_reader :repo_contents_path sig { returns(T.nilable(String)) } attr_reader :target_directory sig do params( path: String, operation: String ).returns([T.nilable(String), String]) end def get_encoded_file_contents(path, operation) encoded_content = nil encoding = "" return encoded_content, encoding if operation == Dependabot::DependencyFile::Operation::DELETE encoded_content = File.read(path) if binary_file?(path) encoding = Dependabot::DependencyFile::ContentEncoding::BASE64 encoded_content = Base64.encode64(encoded_content) end [encoded_content, encoding] end sig { params(path: String).returns(T::Boolean) } def binary_file?(path) return false unless File.exist?(path) command = SharedHelpers.escape_command("file -b --mime-encoding #{path}") encoding = `#{command}`.strip !TEXT_ENCODINGS.include?(encoding) end sig do overridable .params(parameters: T::Hash[Symbol, T.untyped]) .returns(Dependabot::DependencyFile) end def create_dependency_file(parameters) Dependabot::DependencyFile.new(**T.unsafe(parameters)) end end end end