dependabot-dep 0.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+ require "dependabot/dependency_file"
5
+ require "dependabot/dep/file_parser"
6
+ require "dependabot/dep/update_checker"
7
+
8
+ module Dependabot
9
+ module Dep
10
+ class UpdateChecker
11
+ # This class takes a set of dependency files and prepares them for use
12
+ # in Dep::UpdateChecker.
13
+ class FilePreparer
14
+ def initialize(dependency_files:, dependency:,
15
+ remove_git_source: false,
16
+ unlock_requirement: true,
17
+ replacement_git_pin: nil,
18
+ latest_allowable_version: nil)
19
+ @dependency_files = dependency_files
20
+ @dependency = dependency
21
+ @unlock_requirement = unlock_requirement
22
+ @remove_git_source = remove_git_source
23
+ @replacement_git_pin = replacement_git_pin
24
+ @latest_allowable_version = latest_allowable_version
25
+ end
26
+
27
+ def prepared_dependency_files
28
+ files = []
29
+
30
+ files << manifest_for_update_check
31
+ files << lockfile if lockfile
32
+
33
+ files
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :dependency_files, :dependency, :replacement_git_pin,
39
+ :latest_allowable_version
40
+
41
+ def unlock_requirement?
42
+ @unlock_requirement
43
+ end
44
+
45
+ def remove_git_source?
46
+ @remove_git_source
47
+ end
48
+
49
+ def replace_git_pin?
50
+ !replacement_git_pin.nil?
51
+ end
52
+
53
+ def manifest_for_update_check
54
+ DependencyFile.new(
55
+ name: manifest.name,
56
+ content: manifest_content_for_update_check(manifest),
57
+ directory: manifest.directory
58
+ )
59
+ end
60
+
61
+ def manifest_content_for_update_check(file)
62
+ content = file.content
63
+
64
+ content = remove_git_source(content) if remove_git_source?
65
+ content = replace_git_pin(content) if replace_git_pin?
66
+ content = replace_version_constraint(content, file.name)
67
+ content = add_fsnotify_override(content)
68
+
69
+ content
70
+ end
71
+
72
+ def remove_git_source(content)
73
+ parsed_manifest = TomlRB.parse(content)
74
+
75
+ Dep::FileParser::REQUIREMENT_TYPES.each do |type|
76
+ (parsed_manifest[type] || []).each do |details|
77
+ next unless details["name"] == dependency.name
78
+
79
+ details.delete("revision")
80
+ details.delete("branch")
81
+ end
82
+ end
83
+
84
+ TomlRB.dump(parsed_manifest)
85
+ end
86
+
87
+ def replace_git_pin(content)
88
+ parsed_manifest = TomlRB.parse(content)
89
+
90
+ Dep::FileParser::REQUIREMENT_TYPES.each do |type|
91
+ (parsed_manifest[type] || []).each do |details|
92
+ next unless details["name"] == dependency.name
93
+
94
+ raise "Invalid details! #{details}" if details["branch"]
95
+
96
+ if details["version"]
97
+ details["version"] = replacement_git_pin
98
+ else
99
+ details["revision"] = replacement_git_pin
100
+ end
101
+ end
102
+ end
103
+
104
+ TomlRB.dump(parsed_manifest)
105
+ end
106
+
107
+ # Note: We don't need to care about formatting in this method, since
108
+ # we're only using the manifest to find the latest resolvable version
109
+ def replace_version_constraint(content, filename)
110
+ parsed_manifest = TomlRB.parse(content)
111
+
112
+ Dep::FileParser::REQUIREMENT_TYPES.each do |type|
113
+ (parsed_manifest[type] || []).each do |details|
114
+ next unless details["name"] == dependency.name
115
+ next if details["revision"] || details["branch"]
116
+ next if replacement_git_pin
117
+
118
+ updated_req = temporary_requirement_for_resolution(filename)
119
+
120
+ details["version"] = updated_req
121
+ end
122
+ end
123
+
124
+ TomlRB.dump(parsed_manifest)
125
+ end
126
+
127
+ # A dep bug means we have to specify a source for gopkg.in/fsnotify.v1
128
+ # or we get `panic: version queue is empty` errors
129
+ def add_fsnotify_override(content)
130
+ parsed_manifest = TomlRB.parse(content)
131
+
132
+ overrides = parsed_manifest.fetch("override", [])
133
+ dep_name = "gopkg.in/fsnotify.v1"
134
+
135
+ override = overrides.find { |s| s["name"] == dep_name }
136
+ if override.nil?
137
+ override = { "name" => dep_name }
138
+ overrides << override
139
+ end
140
+
141
+ unless override["source"]
142
+ override["source"] = "gopkg.in/fsnotify/fsnotify.v1"
143
+ end
144
+
145
+ parsed_manifest["override"] = overrides
146
+ TomlRB.dump(parsed_manifest)
147
+ end
148
+
149
+ def temporary_requirement_for_resolution(filename)
150
+ original_req = dependency.requirements.
151
+ find { |r| r.fetch(:file) == filename }&.
152
+ fetch(:requirement)
153
+
154
+ lower_bound_req =
155
+ if original_req && !unlock_requirement?
156
+ original_req
157
+ else
158
+ ">= #{lower_bound_version}"
159
+ end
160
+
161
+ unless latest_allowable_version &&
162
+ version_class.correct?(latest_allowable_version) &&
163
+ version_class.new(latest_allowable_version) >=
164
+ version_class.new(lower_bound_version)
165
+ return lower_bound_req
166
+ end
167
+
168
+ lower_bound_req + ", <= #{latest_allowable_version}"
169
+ end
170
+
171
+ def lower_bound_version
172
+ @lower_bound_version ||=
173
+ if version_from_lockfile
174
+ version_from_lockfile
175
+ else
176
+ version_from_requirement =
177
+ dependency.requirements.map { |r| r.fetch(:requirement) }.
178
+ compact.
179
+ flat_map { |req_str| requirement_class.new(req_str) }.
180
+ flat_map(&:requirements).
181
+ reject { |req_array| req_array.first.start_with?("<") }.
182
+ map(&:last).
183
+ max&.to_s
184
+
185
+ version_from_requirement || 0
186
+ end
187
+ end
188
+
189
+ def version_from_lockfile
190
+ return unless lockfile
191
+
192
+ TomlRB.parse(lockfile.content).
193
+ fetch("projects", []).
194
+ find { |p| p["name"] == dependency.name }&.
195
+ fetch("version", nil)&.
196
+ sub(/^v?/, "")
197
+ end
198
+
199
+ def version_class
200
+ Utils.version_class_for_package_manager(dependency.package_manager)
201
+ end
202
+
203
+ def requirement_class
204
+ Utils.requirement_class_for_package_manager(
205
+ dependency.package_manager
206
+ )
207
+ end
208
+
209
+ def manifest
210
+ @manifest ||= dependency_files.find { |f| f.name == "Gopkg.toml" }
211
+ end
212
+
213
+ def lockfile
214
+ @lockfile ||= dependency_files.find { |f| f.name == "Gopkg.lock" }
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "toml-rb"
5
+
6
+ require "dependabot/source"
7
+ require "dependabot/dep/update_checker"
8
+ require "dependabot/git_commit_checker"
9
+ require "dependabot/dep/path_converter"
10
+
11
+ module Dependabot
12
+ module Dep
13
+ class UpdateChecker
14
+ class LatestVersionFinder
15
+ def initialize(dependency:, dependency_files:, credentials:,
16
+ ignored_versions:)
17
+ @dependency = dependency
18
+ @dependency_files = dependency_files
19
+ @credentials = credentials
20
+ @ignored_versions = ignored_versions
21
+ end
22
+
23
+ def latest_version
24
+ @latest_version ||=
25
+ if git_dependency? then latest_version_for_git_dependency
26
+ else latest_release_tag_version
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :dependency, :dependency_files, :credentials,
33
+ :ignored_versions
34
+
35
+ def latest_release_tag_version
36
+ if @latest_release_tag_lookup_attempted
37
+ return @latest_release_tag_version
38
+ end
39
+
40
+ @latest_release_tag_lookup_attempted = true
41
+
42
+ latest_release_str = fetch_latest_release_tag&.sub(/^v?/, "")
43
+ return unless latest_release_str
44
+ return unless version_class.correct?(latest_release_str)
45
+
46
+ @latest_release_tag_version =
47
+ version_class.new(latest_release_str)
48
+ end
49
+
50
+ def fetch_latest_release_tag
51
+ # If this is a git dependency then getting the latest tag is trivial
52
+ if git_dependency?
53
+ return git_commit_checker.
54
+ local_tag_for_latest_version&.fetch(:tag)
55
+ end
56
+
57
+ # If not, we need to find the URL for the source code.
58
+ path = dependency.requirements.
59
+ map { |r| r.dig(:source, :source) }.compact.first
60
+ path ||= dependency.name
61
+
62
+ source_url = git_source(path)
63
+ return unless source_url
64
+
65
+ # Given a source, we want to find the latest tag. Piggy-back off the
66
+ # logic in GitCommitChecker to do so.
67
+ git_dep = Dependency.new(
68
+ name: dependency.name,
69
+ version: dependency.version,
70
+ requirements: [{
71
+ file: "Gopkg.toml",
72
+ groups: [],
73
+ requirement: nil,
74
+ source: { type: "git", url: source_url }
75
+ }],
76
+ package_manager: dependency.package_manager
77
+ )
78
+
79
+ GitCommitChecker.
80
+ new(dependency: git_dep, credentials: credentials).
81
+ local_tag_for_latest_version&.fetch(:tag)
82
+ end
83
+
84
+ def latest_version_for_git_dependency
85
+ latest_release = latest_release_tag_version
86
+
87
+ # If there's been a release that includes the current pinned ref or
88
+ # that the current branch is behind, we switch to that release.
89
+ return latest_release if branch_or_ref_in_release?(latest_release)
90
+
91
+ # Otherwise, if the gem isn't pinned, the latest version is just the
92
+ # latest commit for the specified branch.
93
+ unless git_commit_checker.pinned?
94
+ return git_commit_checker.head_commit_for_current_branch
95
+ end
96
+
97
+ # If the dependency is pinned to a tag that looks like a version
98
+ # then we want to update that tag.
99
+ if git_commit_checker.pinned_ref_looks_like_version?
100
+ latest_tag = git_commit_checker.local_tag_for_latest_version
101
+ return version_from_tag(latest_tag)
102
+ end
103
+
104
+ # If the dependency is pinned to a tag that doesn't look like a
105
+ # version then there's nothing we can do.
106
+ nil
107
+ end
108
+
109
+ def git_source(path)
110
+ Dependabot::Dep::PathConverter.git_url_for_path(path)
111
+ end
112
+
113
+ def version_from_tag(tag)
114
+ # To compare with the current version we either use the commit SHA
115
+ # (if that's what the parser picked up) of the tag name.
116
+ if dependency.version&.match?(/^[0-9a-f]{40}$/)
117
+ return tag&.fetch(:commit_sha)
118
+ end
119
+
120
+ tag&.fetch(:tag)
121
+ end
122
+
123
+ def branch_or_ref_in_release?(release)
124
+ return false unless release
125
+
126
+ git_commit_checker.branch_or_ref_in_release?(release)
127
+ end
128
+
129
+ def git_dependency?
130
+ git_commit_checker.git_dependency?
131
+ end
132
+
133
+ def git_commit_checker
134
+ @git_commit_checker ||=
135
+ GitCommitChecker.new(
136
+ dependency: dependency,
137
+ credentials: credentials,
138
+ ignored_versions: ignored_versions
139
+ )
140
+ end
141
+
142
+ def parsed_file(file)
143
+ @parsed_file ||= {}
144
+ @parsed_file[file.name] ||= TomlRB.parse(file.content)
145
+ end
146
+
147
+ def version_class
148
+ Utils.version_class_for_package_manager(dependency.package_manager)
149
+ end
150
+
151
+ def manifest
152
+ @manifest ||= dependency_files.find { |f| f.name == "Gopkg.toml" }
153
+ raise "No Gopkg.lock!" unless @manifest
154
+
155
+ @manifest
156
+ end
157
+
158
+ def lockfile
159
+ @lockfile = dependency_files.find { |f| f.name == "Gopkg.lock" }
160
+ raise "No Gopkg.lock!" unless @lockfile
161
+
162
+ @lockfile
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dep/update_checker"
4
+ require "dependabot/dep/requirement"
5
+ require "dependabot/dep/version"
6
+
7
+ module Dependabot
8
+ module Dep
9
+ class UpdateChecker
10
+ class RequirementsUpdater
11
+ class UnfixableRequirement < StandardError; end
12
+
13
+ VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-*]+)*/.freeze
14
+ ALLOWED_UPDATE_STRATEGIES = %i(widen_ranges bump_versions).freeze
15
+
16
+ def initialize(requirements:, updated_source:, update_strategy:,
17
+ latest_version:, latest_resolvable_version:)
18
+ @requirements = requirements
19
+ @updated_source = updated_source
20
+ @update_strategy = update_strategy
21
+
22
+ check_update_strategy
23
+
24
+ if latest_version && version_class.correct?(latest_version)
25
+ @latest_version = version_class.new(latest_version)
26
+ end
27
+
28
+ return unless latest_resolvable_version
29
+ return unless version_class.correct?(latest_resolvable_version)
30
+
31
+ @latest_resolvable_version =
32
+ version_class.new(latest_resolvable_version)
33
+ end
34
+
35
+ def updated_requirements
36
+ requirements.map do |req|
37
+ req = req.merge(source: updated_source)
38
+ next req unless latest_resolvable_version
39
+ next initial_req_after_source_change(req) unless req[:requirement]
40
+
41
+ case update_strategy
42
+ when :widen_ranges then widen_requirement(req)
43
+ when :bump_versions then update_version(req)
44
+ else raise "Unexpected update strategy: #{update_strategy}"
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :requirements, :updated_source, :update_strategy,
52
+ :latest_version, :latest_resolvable_version
53
+
54
+ def check_update_strategy
55
+ return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
56
+
57
+ raise "Unknown update strategy: #{update_strategy}"
58
+ end
59
+
60
+ def updating_from_git_to_version?
61
+ return false unless updated_source&.fetch(:type) == "default"
62
+
63
+ original_source = requirements.map { |r| r[:source] }.compact.first
64
+ original_source&.fetch(:type) == "git"
65
+ end
66
+
67
+ def initial_req_after_source_change(req)
68
+ return req unless updating_from_git_to_version?
69
+ return req unless req.fetch(:requirement).nil?
70
+
71
+ new_req =
72
+ if req.fetch(:file) == "go.mod"
73
+ "v#{latest_resolvable_version.to_s.gsub(/^v/, '')}"
74
+ else
75
+ "^#{latest_resolvable_version}"
76
+ end
77
+ req.merge(requirement: new_req)
78
+ end
79
+
80
+ def widen_requirement(req)
81
+ current_requirement = req[:requirement]
82
+ version = latest_resolvable_version
83
+
84
+ ruby_reqs = ruby_requirements(current_requirement)
85
+ return req if ruby_reqs.any? { |r| r.satisfied_by?(version) }
86
+
87
+ reqs = current_requirement.strip.split(",").map(&:strip)
88
+
89
+ updated_requirement =
90
+ if current_requirement.include?("||")
91
+ # Further widen the range by adding another OR condition
92
+ current_requirement + " || ^#{version}"
93
+ elsif reqs.any? { |r| r.match?(/(<|-\s)/) }
94
+ # Further widen the range by updating the upper bound
95
+ update_range_requirement(current_requirement)
96
+ else
97
+ # Convert existing requirement to a range
98
+ create_new_range_requirement(reqs)
99
+ end
100
+
101
+ req.merge(requirement: updated_requirement)
102
+ end
103
+
104
+ def update_version(req)
105
+ current_requirement = req[:requirement]
106
+ version = latest_resolvable_version
107
+
108
+ ruby_reqs = ruby_requirements(current_requirement)
109
+ reqs = current_requirement.strip.split(",").map(&:strip)
110
+
111
+ if ruby_reqs.any? { |r| r.satisfied_by?(version) } &&
112
+ current_requirement.match?(/(<|-\s|\|\|)/)
113
+ return req
114
+ end
115
+
116
+ updated_requirement =
117
+ if current_requirement.include?("||")
118
+ # Further widen the range by adding another OR condition
119
+ current_requirement + " || ^#{version}"
120
+ elsif reqs.any? { |r| r.match?(/(<|-\s)/) }
121
+ # Further widen the range by updating the upper bound
122
+ update_range_requirement(current_requirement)
123
+ else
124
+ update_version_requirement(reqs)
125
+ end
126
+
127
+ req.merge(requirement: updated_requirement)
128
+ end
129
+
130
+ def ruby_requirements(requirement_string)
131
+ requirement_class.requirements_array(requirement_string)
132
+ end
133
+
134
+ def update_range_requirement(req_string)
135
+ range_requirement = req_string.split(",").
136
+ find { |r| r.match?(/<|(\s+-\s+)/) }
137
+
138
+ versions = range_requirement.scan(VERSION_REGEX)
139
+ upper_bound = versions.map { |v| version_class.new(v) }.max
140
+ new_upper_bound = update_greatest_version(
141
+ upper_bound,
142
+ latest_resolvable_version
143
+ )
144
+
145
+ req_string.sub(
146
+ upper_bound.to_s,
147
+ new_upper_bound.to_s
148
+ )
149
+ end
150
+
151
+ def create_new_range_requirement(string_reqs)
152
+ version = latest_resolvable_version
153
+
154
+ lower_bound =
155
+ string_reqs.
156
+ map { |req| requirement_class.new(req) }.
157
+ flat_map { |req| req.requirements.map(&:last) }.
158
+ min.to_s
159
+
160
+ upper_bound =
161
+ if string_reqs.first.start_with?("~") &&
162
+ version.to_s.split(".").count > 1
163
+ create_upper_bound_for_tilda_req(string_reqs.first)
164
+ else
165
+ upper_bound_parts = [version.to_s.split(".").first.to_i + 1]
166
+ upper_bound_parts.
167
+ fill("0", 1..(lower_bound.split(".").count - 1)).
168
+ join(".")
169
+ end
170
+
171
+ ">= #{lower_bound}, < #{upper_bound}"
172
+ end
173
+
174
+ def update_version_requirement(string_reqs)
175
+ version = latest_resolvable_version.to_s.gsub(/^v/, "")
176
+ current_req = string_reqs.first
177
+
178
+ current_req.gsub(VERSION_REGEX, version)
179
+ end
180
+
181
+ def create_upper_bound_for_tilda_req(string_req)
182
+ tilda_version = requirement_class.new(string_req).
183
+ requirements.map(&:last).
184
+ min.to_s
185
+
186
+ upper_bound_parts = latest_resolvable_version.to_s.split(".")
187
+ upper_bound_parts.slice(0, tilda_version.to_s.split(".").count)
188
+ upper_bound_parts[-1] = "0"
189
+ upper_bound_parts[-2] = (upper_bound_parts[-2].to_i + 1).to_s
190
+
191
+ upper_bound_parts.join(".")
192
+ end
193
+
194
+ def update_greatest_version(old_version, version_to_be_permitted)
195
+ version = version_class.new(old_version)
196
+ version = version.release if version.prerelease?
197
+
198
+ index_to_update =
199
+ version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
200
+
201
+ version.segments.map.with_index do |_, index|
202
+ if index < index_to_update
203
+ version_to_be_permitted.segments[index]
204
+ elsif index == index_to_update
205
+ version_to_be_permitted.segments[index] + 1
206
+ else 0
207
+ end
208
+ end.join(".")
209
+ end
210
+
211
+ def version_class
212
+ Dep::Version
213
+ end
214
+
215
+ def requirement_class
216
+ Dep::Requirement
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end