From c517e9eb64b8a1d29c0337790e387bbeb255d00d Mon Sep 17 00:00:00 2001
From: Maxwell Weru <mburumaxwell@gmail.com>
Date: Wed, 24 Jul 2024 11:06:45 +0300
Subject: [PATCH] Sync files for updater version 0.266.0

Follow up to https://github.com/tinglesoftware/dependabot-azure-devops/pull/1235
---
 updater/lib/dependabot/dependency_change.rb   | 23 +++--
 updater/lib/dependabot/dependency_snapshot.rb |  2 +-
 updater/lib/dependabot/environment.rb         | 63 +++++++++---
 .../lib/dependabot/file_fetcher_command.rb    | 15 +--
 .../updater/dependency_group_change_batch.rb  | 51 ++--------
 .../spec/dependabot/dependency_change_spec.rb | 71 ++++++++-----
 .../dependency_group_change_batch_spec.rb     | 99 -------------------
 7 files changed, 116 insertions(+), 208 deletions(-)

diff --git a/updater/lib/dependabot/dependency_change.rb b/updater/lib/dependabot/dependency_change.rb
index e821b1db..161cbb50 100644
--- a/updater/lib/dependabot/dependency_change.rb
+++ b/updater/lib/dependabot/dependency_change.rb
@@ -165,24 +165,32 @@ def matches_existing_pr?
       if grouped_update?
         # We only want PRs for the same group that have the same versions
         job.existing_group_pull_requests.any? do |pr|
+          directories_in_use = pr["dependencies"].all? { |dep| dep["directory"] }
+
           pr["dependency-group-name"] == dependency_group&.name &&
-            Set.new(pr["dependencies"]) == updated_dependencies_set
+            Set.new(pr["dependencies"]) == updated_dependencies_set(should_consider_directory: directories_in_use)
         end
       else
-        job.existing_pull_requests.any? { |pr| Set.new(pr) == updated_dependencies_set }
+        job.existing_pull_requests.any? do |pr|
+          directories_in_use = pr.all? { |dep| dep["directory"] }
+
+          Set.new(pr) == updated_dependencies_set(should_consider_directory: directories_in_use)
+        end
       end
     end
 
     private
 
-    sig { returns(T::Set[T::Hash[String, T.any(String, T::Boolean)]]) }
-    def updated_dependencies_set
+    # Older PRs will not have a directory key, in that case do not consider directory in the comparison. This will
+    # allow rebases to continue working for those, but for multi-directory configs we do compare with the directory.
+    sig { params(should_consider_directory: T::Boolean).returns(T::Set[T::Hash[String, T.any(String, T::Boolean)]]) }
+    def updated_dependencies_set(should_consider_directory:)
       Set.new(
         updated_dependencies.map do |dep|
           {
             "dependency-name" => dep.name,
             "dependency-version" => dep.version,
-            "directory" => should_consider_directory? ? dep.directory : nil,
+            "directory" => should_consider_directory ? dep.directory : nil,
             "dependency-removed" => dep.removed? ? true : nil
           }.compact
         end
@@ -202,10 +210,5 @@ def directory
 
       T.must(updated_dependency_files.first).directory
     end
-
-    sig { returns(T::Boolean) }
-    def should_consider_directory?
-      grouped_update? && Dependabot::Experiments.enabled?("dependency_has_directory")
-    end
   end
 end
diff --git a/updater/lib/dependabot/dependency_snapshot.rb b/updater/lib/dependabot/dependency_snapshot.rb
index ebd950ed..d347a2c2 100644
--- a/updater/lib/dependabot/dependency_snapshot.rb
+++ b/updater/lib/dependabot/dependency_snapshot.rb
@@ -28,7 +28,7 @@ def self.create_from_job_definition(job:, job_definition:)
         file
       end
 
-      if Dependabot::Experiments.enabled?(:globs) && job.source.directories
+      if job.source.directories
         # The job.source.directory may contain globs, so we use the directories from the fetched files
         job.source.directories = decoded_dependency_files.flat_map(&:directory).uniq
       end
diff --git a/updater/lib/dependabot/environment.rb b/updater/lib/dependabot/environment.rb
index 7069d877..230423dd 100644
--- a/updater/lib/dependabot/environment.rb
+++ b/updater/lib/dependabot/environment.rb
@@ -1,64 +1,97 @@
-# typed: true
+# typed: strict
 # frozen_string_literal: true
 
+require "sorbet-runtime"
+
 module Dependabot
   module Environment
+    extend T::Sig
+    extend T::Generic
+
+    sig { returns(String) }
     def self.job_id
-      @job_id ||= environment_variable("DEPENDABOT_JOB_ID")
+      @job_id ||= T.let(environment_variable("DEPENDABOT_JOB_ID"), T.nilable(String))
     end
 
+    sig { returns(String) }
     def self.job_token
-      @job_token ||= environment_variable("DEPENDABOT_JOB_TOKEN")
+      @job_token ||= T.let(environment_variable("DEPENDABOT_JOB_TOKEN"), T.nilable(String))
     end
 
+    sig { returns(T::Boolean) }
     def self.debug_enabled?
-      @debug_enabled ||= job_debug_enabled? || environment_debug_enabled?
+      @debug_enabled ||= T.let(job_debug_enabled? || environment_debug_enabled?, T.nilable(T::Boolean))
     end
 
+    sig { returns(Symbol) }
     def self.log_level
       debug_enabled? ? :debug : :info
     end
 
+    sig { returns(String) }
     def self.api_url
-      @api_url ||= environment_variable("DEPENDABOT_API_URL", "http://localhost:3001")
+      @api_url ||= T.let(environment_variable("DEPENDABOT_API_URL", "http://localhost:3001"), T.nilable(String))
     end
 
+    sig { returns(String) }
     def self.job_path
-      @job_path ||= environment_variable("DEPENDABOT_JOB_PATH")
+      @job_path ||= T.let(environment_variable("DEPENDABOT_JOB_PATH"), T.nilable(String))
     end
 
+    sig { returns(String) }
     def self.output_path
-      @output_path ||= environment_variable("DEPENDABOT_OUTPUT_PATH")
+      @output_path ||= T.let(environment_variable("DEPENDABOT_OUTPUT_PATH"), T.nilable(String))
     end
 
+    sig { returns(T.nilable(String)) }
     def self.repo_contents_path
-      @repo_contents_path ||= environment_variable("DEPENDABOT_REPO_CONTENTS_PATH", nil)
+      @repo_contents_path ||= T.let(environment_variable("DEPENDABOT_REPO_CONTENTS_PATH", nil), T.nilable(String))
     end
 
+    sig { returns(T::Boolean) }
     def self.github_actions?
-      @github_actions ||= environment_variable("GITHUB_ACTIONS", false)
+      b = T.cast(environment_variable("GITHUB_ACTIONS", false), T::Boolean)
+      @github_actions ||= T.let(b, T.nilable(T::Boolean))
     end
 
+    sig { returns(T::Boolean) }
     def self.deterministic_updates?
-      @deterministic_updates ||= environment_variable("UPDATER_DETERMINISTIC", false)
+      b = T.cast(environment_variable("UPDATER_DETERMINISTIC", false), T::Boolean)
+      @deterministic_updates ||= T.let(b, T.nilable(T::Boolean))
     end
 
+    sig { returns(T::Hash[String, T.untyped]) }
     def self.job_definition
-      @job_definition ||= JSON.parse(File.read(job_path))
+      @job_definition ||= T.let(JSON.parse(File.read(job_path)), T.nilable(T::Hash[String, T.untyped]))
     end
 
+    sig do
+      type_parameters(:T)
+        .params(variable_name: String, default: T.any(Symbol, T.type_parameter(:T)))
+        .returns(T.any(String, T.type_parameter(:T)))
+    end
     private_class_method def self.environment_variable(variable_name, default = :_undefined)
-      return ENV.fetch(variable_name, default) unless default == :_undefined
-
-      ENV.fetch(variable_name) do
-        raise ArgumentError, "Missing environment variable #{variable_name}"
+      case default
+      when :_undefined
+        ENV.fetch(variable_name) do
+          raise ArgumentError, "Missing environment variable #{variable_name}"
+        end
+      else
+        val = ENV.fetch(variable_name, T.cast(default, T.type_parameter(:T)))
+        case val
+        when String
+          val = T.must(val.casecmp("true")).zero? if [true, false].include? default
+        end
+        T.cast(val, T.type_parameter(:T))
       end
     end
 
+    sig { returns(T::Boolean) }
     private_class_method def self.job_debug_enabled?
       !!job_definition.dig("job", "debug")
     end
 
+    sig { returns(T::Boolean) }
     private_class_method def self.environment_debug_enabled?
       !!environment_variable("DEPENDABOT_DEBUG", false)
     end
diff --git a/updater/lib/dependabot/file_fetcher_command.rb b/updater/lib/dependabot/file_fetcher_command.rb
index 21bc2d3f..e18898ea 100644
--- a/updater/lib/dependabot/file_fetcher_command.rb
+++ b/updater/lib/dependabot/file_fetcher_command.rb
@@ -100,19 +100,8 @@ def file_fetcher_for_directory(directory)
     end
 
     def dependency_files_for_multi_directories
-      if Dependabot::Experiments.enabled?(:globs)
-        return @dependency_files_for_multi_directories ||= dependency_files_for_globs
-      end
-
-      @dependency_files_for_multi_directories ||= job.source.directories.flat_map do |dir|
-        ff = with_retries { file_fetcher_for_directory(dir) }
-        files = ff.files
-        post_ecosystem_versions(ff) if should_record_ecosystem_versions?
-        files
-      end
-    end
+      return @dependency_files_for_multi_directories if defined?(@dependency_files_for_multi_directories)
 
-    def dependency_files_for_globs
       has_glob = T.let(false, T::Boolean)
       directories = Dir.chdir(job.repo_contents_path) do
         job.source.directories.map do |dir|
@@ -124,7 +113,7 @@ def dependency_files_for_globs
         end.flatten
       end.uniq
 
-      directories.flat_map do |dir|
+      @dependency_files_for_multi_directories = directories.flat_map do |dir|
         ff = with_retries { file_fetcher_for_directory(dir) }
 
         begin
diff --git a/updater/lib/dependabot/updater/dependency_group_change_batch.rb b/updater/lib/dependabot/updater/dependency_group_change_batch.rb
index bbbc248a..96f542b7 100644
--- a/updater/lib/dependabot/updater/dependency_group_change_batch.rb
+++ b/updater/lib/dependabot/updater/dependency_group_change_batch.rb
@@ -14,7 +14,7 @@ def initialize(initial_dependency_files:)
         @updated_dependencies = []
 
         @dependency_file_batch = initial_dependency_files.each_with_object({}) do |file, hsh|
-          hsh[file.path] = { file: file, updated_dependencies: [], changed: false, changes: 0 }
+          hsh[file.path] = { file: file, changed: false, changes: 0 }
         end
 
         @vendored_dependency_batch = {}
@@ -45,18 +45,8 @@ def updated_dependency_files
       end
 
       def merge(dependency_change)
-        # FIXME: we shouldn't have to rely on this but because CreateGroupUpdatePullRequest explicitly checks
-        # the DependencyChange.updated_dependencies, we need to add the updated dependencies to the global list
         merge_dependency_changes(dependency_change.updated_dependencies)
-
-        if Dependabot::Experiments.enabled?(:dependency_has_directory)
-          merge_file_and_dependency_changes(
-            dependency_change.updated_dependencies,
-            dependency_change.updated_dependency_files
-          )
-        else
-          merge_file_changes(dependency_change.updated_dependency_files)
-        end
+        merge_file_changes(dependency_change.updated_dependency_files)
 
         Dependabot.logger.debug("Dependencies updated:")
         debug_updated_dependencies
@@ -72,7 +62,9 @@ def add_updated_dependency(dependency)
 
       private
 
-      # We should retain a list of all dependencies that we change.
+      # We should retain a list of all dependencies that we change, in future we may need to account for the folder
+      # in which these changes are made to permit-cross folder updates of the same dependency.
+      #
       # This list may contain duplicates if we make iterative updates to a Dependency within a single group, but
       # rather than re-write the Dependency objects to account for the changes from the lowest previous version
       # to the final version, we should defer it to the Dependabot::PullRequestCreator::MessageBuilder as a
@@ -100,38 +92,7 @@ def merge_file_to_batch(file, batch)
                          0
                        end
 
-        batch[file.path] = {
-          file: file,
-          updated_dependencies: batch.dig(file.path, :updated_dependencies) || [],
-          changed: true,
-          changes: change_count + 1
-        }
-      end
-
-      def merge_file_and_dependency_changes(updated_dependencies, updated_dependency_files)
-        updated_dependency_files.each do |updated_file|
-          if updated_file.vendored_file?
-            merge_file_and_dependency_changes_to_batch(updated_file, @vendored_dependency_batch, updated_dependencies)
-          else
-            merge_file_and_dependency_changes_to_batch(updated_file, @dependency_file_batch, updated_dependencies)
-          end
-        end
-      end
-
-      def merge_file_and_dependency_changes_to_batch(file, batch, updated_dependencies)
-        change_count = if (existing_file = batch[file.path])
-                         existing_file.fetch(:change_count, 0)
-                       else
-                         # The file is newly encountered
-                         Dependabot.logger.debug("File #{file.operation}d: '#{file.path}'")
-                         0
-                       end
-
-        previous_updated_dependencies = batch.dig(file.path, :updated_dependencies) || []
-        updated_dependencies_list = previous_updated_dependencies.concat(updated_dependencies)
-
-        batch[file.path] =
-          { file: file, updated_dependencies: updated_dependencies_list, changed: true, changes: change_count + 1 }
+        batch[file.path] = { file: file, changed: true, changes: change_count + 1 }
       end
 
       def debug_updated_dependencies
diff --git a/updater/spec/dependabot/dependency_change_spec.rb b/updater/spec/dependabot/dependency_change_spec.rb
index b467876a..61d6fe0e 100644
--- a/updater/spec/dependabot/dependency_change_spec.rb
+++ b/updater/spec/dependabot/dependency_change_spec.rb
@@ -33,7 +33,8 @@
         ],
         previous_requirements: [
           { file: "Gemfile", requirement: "~> 1.7.0", groups: [], source: nil }
-        ]
+        ],
+        directory: "/"
       )
     ]
   end
@@ -269,14 +270,6 @@
   end
 
   describe "#matches_existing_pr?" do
-    before do
-      Dependabot::Experiments.register("dependency_has_directory", true)
-    end
-
-    after do
-      Dependabot::Experiments.reset!
-    end
-
     context "when no existing pull requests are found" do
       let(:job) do
         instance_double(Dependabot::Job,
@@ -306,7 +299,8 @@
         [
           updated_dependencies.map do |dep|
             { "dependency-name" => dep.name,
-              "dependency-version" => dep.version }
+              "dependency-version" => dep.version,
+              "directory" => dep.directory }
           end
         ]
       end
@@ -321,6 +315,21 @@
       it "returns true" do
         expect(dependency_change.matches_existing_pr?).to be true
       end
+
+      context "when there's no directory in an existing PR that otherwise matches" do
+        let(:existing_pull_requests) do
+          [
+            updated_dependencies.map do |dep|
+              { "dependency-name" => dep.name,
+                "dependency-version" => dep.version }
+            end
+          ]
+        end
+
+        it "returns true" do
+          expect(dependency_change.matches_existing_pr?).to be true
+        end
+      end
     end
 
     context "when updating a grouped pull request with the same dependencies" do
@@ -332,13 +341,11 @@
       let(:existing_group_pull_requests) do
         [
           { "dependency-group-name" => "foo",
-            "dependencies" => [
-              updated_dependencies.map do |dep|
-                { "dependency-name" => dep.name.to_s,
-                  "dependency-version" => dep.version.to_s,
-                  "directory" => dep.directory.to_s }
-              end
-            ] }
+            "dependencies" => updated_dependencies.map do |dep|
+                                { "dependency-name" => dep.name.to_s,
+                                  "dependency-version" => dep.version.to_s,
+                                  "directory" => dep.directory.to_s }
+                              end }
         ]
       end
 
@@ -352,7 +359,23 @@
       end
 
       it "returns true" do
-        expect(dependency_change.matches_existing_pr?).to be false
+        expect(dependency_change.matches_existing_pr?).to be true
+      end
+
+      context "when there's no directory in a PR that otherwise matches" do
+        let(:existing_group_pull_requests) do
+          [
+            { "dependency-group-name" => "foo",
+              "dependencies" => updated_dependencies.map do |dep|
+                                  { "dependency-name" => dep.name.to_s,
+                                    "dependency-version" => dep.version.to_s }
+                                end }
+          ]
+        end
+
+        it "returns true" do
+          expect(dependency_change.matches_existing_pr?).to be true
+        end
       end
     end
 
@@ -365,13 +388,11 @@
       let(:existing_group_pull_requests) do
         [
           { "dependency-group-name" => "foo",
-            "dependencies" => [
-              updated_dependencies.map do |dep|
-                { "dependency-name" => dep.name.to_s,
-                  "dependency-version" => dep.version.to_s,
-                  "directory" => "/foo" }
-              end
-            ] }
+            "dependencies" => updated_dependencies.map do |dep|
+                                { "dependency-name" => dep.name.to_s,
+                                  "dependency-version" => dep.version.to_s,
+                                  "directory" => "/foo" }
+                              end }
         ]
       end
 
diff --git a/updater/spec/dependabot/updater/dependency_group_change_batch_spec.rb b/updater/spec/dependabot/updater/dependency_group_change_batch_spec.rb
index 44e8dc6c..08a2ab82 100644
--- a/updater/spec/dependabot/updater/dependency_group_change_batch_spec.rb
+++ b/updater/spec/dependabot/updater/dependency_group_change_batch_spec.rb
@@ -54,104 +54,5 @@
           .current_dependency_files(job).map(&:name)).to eq(%w(Gemfile Gemfile.lock))
       end
     end
-
-    context "when the :dependency_has_directory ff is enabled" do
-      before do
-        Dependabot::Experiments.register(:dependency_has_directory, true)
-      end
-
-      after do
-        Dependabot::Experiments.reset!
-      end
-
-      let(:dependency_change) do
-        Dependabot::DependencyChange.new(
-          job: job,
-          updated_dependency_files: updated_dependency_files,
-          updated_dependencies: updated_dependencies,
-          dependency_group: group
-        )
-      end
-
-      let(:updated_dependency_files) do
-        [
-          Dependabot::DependencyFile.new(
-            name: "Gemfile", content: "mock-updated-gemfile", directory: "/"
-          ),
-          Dependabot::DependencyFile.new(
-            name: "Gemfile.lock", content: "mock-updated-gemfile-lock", directory: "/hello/.."
-          ),
-          Dependabot::DependencyFile.new(
-            name: "Gemfile", content: "mock-updated-package-json", directory: "/elsewhere"
-          ),
-          Dependabot::DependencyFile.new(
-            name: "Gemfile", content: "mock-package-json", directory: "unknown"
-          ),
-          Dependabot::DependencyFile.new(
-            name: "Gemfile", content: "mock-package-json", directory: "../../oob"
-          )
-        ]
-      end
-
-      let(:updated_dependencies) do
-        [
-          Dependabot::Dependency.new(
-            name: "test-dependency-1",
-            package_manager: "bundler",
-            version: "1.1.0",
-            requirements: [
-              {
-                file: "Gemfile",
-                requirement: "~> 1.1.0",
-                groups: ["test"],
-                source: nil
-              }
-            ],
-            directory: "/"
-          ),
-          Dependabot::Dependency.new(
-            name: "test-dependency-2",
-            package_manager: "bundler",
-            version: "1.1.0",
-            requirements: [
-              {
-                file: "Gemfile",
-                requirement: "~> 1.1.0",
-                groups: ["test"],
-                source: nil
-              }
-            ],
-            directory: "/hello/.."
-          )
-        ]
-      end
-
-      let(:group) do
-        Dependabot::DependencyGroup.new(
-          name: "test",
-          rules: { patterns: ["test-*"] }
-        )
-      end
-
-      it "still tracks the updated_dependencies in the global list" do
-        batch = described_class.new(initial_dependency_files: files)
-        batch.merge(dependency_change)
-
-        expect(batch.updated_dependencies).to eq(updated_dependencies)
-      end
-
-      it "tracks the updated dependencies in the appropriate updated_dependency_files" do
-        batch = described_class.new(initial_dependency_files: files)
-        batch.merge(dependency_change)
-
-        # FIXME: All of the files in the dependency_file_batch will contain a copy of the updated dependencies.
-        # Eventually this should be narrowed down so that only the files that were modified
-        # by the newly merged dependency_change have their updated_dependencies updated.
-        dependency_file_batch = batch.instance_variable_get(:@dependency_file_batch)
-        expect(dependency_file_batch["/Gemfile"][:updated_dependencies]).to eq(updated_dependencies)
-        expect(dependency_file_batch["/Gemfile.lock"][:updated_dependencies]).to eq(updated_dependencies)
-        expect(dependency_file_batch["/unknown/Gemfile"][:updated_dependencies]).to eq(updated_dependencies)
-      end
-    end
   end
 end