dependabot-dep 0.90.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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