dependabot-common 0.385.0 → 0.387.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58033d13e6a5c0bee14915505c98d752b8a6f2e26ffb96a4ba6d95ca66057bc3
4
- data.tar.gz: 0a6c8eebbffb115434da53fbd171236690f1594a617f16fb4e29b1addfc81308
3
+ metadata.gz: 1af9eae7cb945bd312cf8a9fee63b2f4e42adcbc3d137208e2043b659052013d
4
+ data.tar.gz: f815b9a6961d5ecbd9c77ffdf296a9a048dbfadf21fae5ddebc4bbd8db6fbd2b
5
5
  SHA512:
6
- metadata.gz: 5defea48ad000e82511496f0cb413945bf6ba2a5070361af5f1fa5e6411a11679a24de9e852c478f48fba3299d3678865e2faf97158c310ee429f6ac368fee5c
7
- data.tar.gz: fcf737b08310353b51bc3b32a53f24546e8bf30aa58a9a0b9e91e025921728bdcba378da8b68f9dc592b1bec349ff487a5d21a2683ab1ad76fa1d8b748ca066b
6
+ metadata.gz: 34737076727da04d065d0163c93fe1058cfbb3a86d79d2cfef2be4cf126d2fd4d335e204087d3c337e2d97f5ec0220878a1110e139f553805c4d36574e33cb5d
7
+ data.tar.gz: 5885a478a01996a6f382b3c2bedb29ff59ca557c0316582f14e9136bd3881dac04d20874862bc76a93a16119653a85c868d3d08c65e8cabb9fff1ae257124323
@@ -3,6 +3,8 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
 
6
+ require "dependabot/dependency_file"
7
+
6
8
  module Dependabot
7
9
  module DependencyGraphers
8
10
  # This is a small value class that specifies the information we expect to be returned for each
@@ -20,6 +22,29 @@ module Dependabot
20
22
  const :dependencies, T::Array[String]
21
23
  end
22
24
 
25
+ # A manifest group is a subset of a directory's dependency files that stands on its own as a valid
26
+ # parser input, plus the single file that the group's dependencies should be attributed to.
27
+ #
28
+ # - `primary` is the attribution target (the file that owns the group's dependencies in the snapshot).
29
+ # - `files` is everything the parser needs to resolve the group, including the primary and any sibling
30
+ # files pulled in only to satisfy cross-references.
31
+ #
32
+ # Most ecosystems have exactly one group per directory (a manifest + optional lockfile).
33
+ #
34
+ # Ecosystems where multiple independent manifests routinely share a directory override `manifest_groups`
35
+ # to return one group per independent manifest using ecosystem-specific rules (e.g. Python layered requirements)
36
+ class ManifestGroup < T::ImmutableStruct
37
+ const :primary, Dependabot::DependencyFile
38
+ const :files, T::Array[Dependabot::DependencyFile]
39
+ end
40
+
41
+ # The resolved output for a single manifest group: the file to attribute to, and the dependencies
42
+ # resolved from that group's files.
43
+ class ManifestGroupSnapshot < T::ImmutableStruct
44
+ const :manifest_file, Dependabot::DependencyFile
45
+ const :resolved_dependencies, T::Hash[String, ResolvedDependency]
46
+ end
47
+
23
48
  class Base
24
49
  extend T::Sig
25
50
  extend T::Helpers
@@ -78,8 +103,86 @@ module Dependabot
78
103
  end
79
104
  end
80
105
 
106
+ # Partitions the parsed directory into one or more manifest groups.
107
+ #
108
+ # The default is a single group spanning the whole directory, attributed to `relevant_dependency_file`.
109
+ #
110
+ # Ecosystems where multiple independent manifests share a directory should override this to return
111
+ # one group per manifest.
112
+ sig { overridable.returns(T::Array[ManifestGroup]) }
113
+ def manifest_groups
114
+ [ManifestGroup.new(primary: relevant_dependency_file, files: dependency_files)]
115
+ end
116
+
117
+ # Resolves each manifest group into a snapshot.
118
+ #
119
+ # When there is a single group, the common case for most ecosystems, we attribute all resolved dependencies
120
+ # to the group's primary without further work required.
121
+ #
122
+ # When there are multiple groups, we instantiate a new scoped grapher for the group's files and resolve each
123
+ # independently.
124
+ sig { returns(T::Array[ManifestGroupSnapshot]) }
125
+ def manifest_group_snapshots
126
+ @manifest_group_snapshots ||= T.let(
127
+ build_manifest_group_snapshots,
128
+ T.nilable(T::Array[ManifestGroupSnapshot])
129
+ )
130
+ end
131
+
81
132
  private
82
133
 
134
+ sig { returns(T::Array[ManifestGroupSnapshot]) }
135
+ def build_manifest_group_snapshots
136
+ groups = manifest_groups
137
+
138
+ if groups.one?
139
+ group = T.must(groups.first)
140
+ return [ManifestGroupSnapshot.new(
141
+ manifest_file: group.primary,
142
+ resolved_dependencies: resolved_dependencies
143
+ )]
144
+ end
145
+
146
+ groups.map do |group|
147
+ scoped = scoped_grapher(group.files)
148
+ snapshot = ManifestGroupSnapshot.new(
149
+ manifest_file: group.primary,
150
+ resolved_dependencies: scoped.resolved_dependencies
151
+ )
152
+ # Ensure we propagate any error flags from the scoped grapher.
153
+ absorb_error_state(scoped)
154
+ snapshot
155
+ end
156
+ end
157
+
158
+ # Propagates a scoped grapher's subdependency error state onto this grapher so callers can inspect the
159
+ # aggregate result of resolving every group.
160
+ sig { params(scoped: Dependabot::DependencyGraphers::Base).void }
161
+ def absorb_error_state(scoped)
162
+ return unless scoped.errored_fetching_subdependencies
163
+
164
+ errored_fetching_subdependencies!
165
+ # We keep the last subdependency error we see - the full error dialogue will be present in the logs,
166
+ # this just ensures job summary dialogues have something to show.
167
+ @subdependency_error = scoped.subdependency_error unless scoped.subdependency_error.nil?
168
+ end
169
+
170
+ # Builds a grapher of the same class scoped to a subset of the directory's files, reusing the current
171
+ # file parser's configuration. Used to resolve a single manifest group in isolation.
172
+ sig { params(files: T::Array[Dependabot::DependencyFile]).returns(Dependabot::DependencyGraphers::Base) }
173
+ def scoped_grapher(files)
174
+ scoped_parser = file_parser.class.new(
175
+ dependency_files: files,
176
+ source: file_parser.source,
177
+ repo_contents_path: file_parser.repo_contents_path,
178
+ credentials: file_parser.credentials,
179
+ reject_external_code: file_parser.reject_external_code?,
180
+ options: file_parser.options
181
+ )
182
+
183
+ self.class.new(file_parser: scoped_parser)
184
+ end
185
+
83
186
  sig { returns(Dependabot::FileParsers::Base) }
84
187
  attr_reader :file_parser
85
188
 
@@ -0,0 +1,42 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ class GitCommitChecker
8
+ # Typed view over the source hash attached to a git dependency.
9
+ class SourceDetails < T::ImmutableStruct
10
+ extend T::Sig
11
+
12
+ const :type, T.nilable(String)
13
+ const :url, T.nilable(String)
14
+ const :branch, T.nilable(String)
15
+ const :ref, T.nilable(String)
16
+
17
+ sig do
18
+ params(details: T::Hash[T.any(String, Symbol), Object]).returns(SourceDetails)
19
+ end
20
+ def self.from_hash(details)
21
+ new(
22
+ type: string_value(details, :type),
23
+ url: string_value(details, :url),
24
+ branch: string_value(details, :branch),
25
+ ref: string_value(details, :ref)
26
+ )
27
+ end
28
+
29
+ sig do
30
+ params(
31
+ details: T::Hash[T.any(String, Symbol), Object],
32
+ key: Symbol
33
+ ).returns(T.nilable(String))
34
+ end
35
+ def self.string_value(details, key)
36
+ value = details.key?(key) ? details[key] : details[key.to_s]
37
+ value.is_a?(String) ? value : nil
38
+ end
39
+ private_class_method :string_value
40
+ end
41
+ end
42
+ end
@@ -15,6 +15,7 @@ require "dependabot/source"
15
15
  require "dependabot/dependency"
16
16
  require "dependabot/credential"
17
17
  require "dependabot/git_metadata_fetcher"
18
+ require "dependabot/git_commit_checker/source_details"
18
19
  require "dependabot/git_tag_details"
19
20
  require "dependabot/package/package_release"
20
21
  require "dependabot/package/release_cooldown_options"
@@ -46,7 +47,7 @@ module Dependabot
46
47
  ignored_versions: T::Array[String],
47
48
  raise_on_ignored: T::Boolean,
48
49
  consider_version_branches_pinned: T::Boolean,
49
- dependency_source_details: T.nilable(T::Hash[Symbol, String])
50
+ dependency_source_details: T.nilable(T::Hash[Symbol, Object])
50
51
  )
51
52
  .void
52
53
  end
@@ -63,14 +64,15 @@ module Dependabot
63
64
  @ignored_versions = ignored_versions
64
65
  @raise_on_ignored = raise_on_ignored
65
66
  @consider_version_branches_pinned = consider_version_branches_pinned
66
- @dependency_source_details = dependency_source_details
67
+ @dependency_source_details = T.let(
68
+ dependency_source_details && SourceDetails.from_hash(dependency_source_details),
69
+ T.nilable(SourceDetails)
70
+ )
67
71
  end
68
72
 
69
73
  sig { returns(T::Boolean) }
70
74
  def git_dependency?
71
- return false if dependency_source_details.nil?
72
-
73
- dependency_source_details&.fetch(:type) == "git"
75
+ dependency_source_details&.type == "git"
74
76
  end
75
77
 
76
78
  # rubocop:disable Metrics/PerceivedComplexity
@@ -78,7 +80,7 @@ module Dependabot
78
80
  def pinned?
79
81
  raise "Not a git dependency!" unless git_dependency?
80
82
 
81
- branch = dependency_source_details&.fetch(:branch)
83
+ branch = dependency_source_details&.branch
82
84
 
83
85
  return false if ref.nil?
84
86
  return false if branch == ref
@@ -118,7 +120,7 @@ module Dependabot
118
120
  sig { returns(T::Array[GitRef]) }
119
121
  def tags
120
122
  GitMetadataFetcher.new(
121
- url: dependency.source_details&.fetch(:url, nil),
123
+ url: T.must(source_details&.url),
122
124
  credentials: credentials
123
125
  ).tags
124
126
  end
@@ -128,7 +130,7 @@ module Dependabot
128
130
  T.must(
129
131
  T.let(
130
132
  GitMetadataFetcher.new(
131
- url: dependency.source_details&.fetch(:url, nil),
133
+ url: T.must(source_details&.url),
132
134
  credentials: credentials
133
135
  ).ref_details_for_pinned_ref(ref),
134
136
  T.nilable(Excon::Response)
@@ -282,9 +284,12 @@ module Dependabot
282
284
  false
283
285
  end
284
286
 
285
- sig { returns(T.nilable(T::Hash[T.any(Symbol, String), T.untyped])) }
287
+ sig { returns(T.nilable(SourceDetails)) }
286
288
  def dependency_source_details
287
- @dependency_source_details || dependency.source_details(allowed_types: ["git"])
289
+ @dependency_source_details ||= begin
290
+ details = dependency.source_details(allowed_types: ["git"])
291
+ SourceDetails.from_hash(details) if details
292
+ end
288
293
  end
289
294
 
290
295
  sig { returns(T::Array[Dependabot::GitTagWithDetail]) }
@@ -333,7 +338,7 @@ module Dependabot
333
338
 
334
339
  sig { override.returns(T.nilable(String)) }
335
340
  def cooldown_source_url
336
- dependency_source_details&.fetch(:url, nil)
341
+ dependency_source_details&.url
337
342
  end
338
343
 
339
344
  sig { override.returns(T::Array[Dependabot::Credential]) }
@@ -478,7 +483,7 @@ module Dependabot
478
483
 
479
484
  sig { params(tags: T::Array[Dependabot::GitRef]).returns(T::Array[Dependabot::GitRef]) }
480
485
  def handle_tag_prefix(tags)
481
- if dependency_source_details&.fetch(:ref, nil)&.start_with?("tags/")
486
+ if dependency_source_details&.ref&.start_with?("tags/")
482
487
  tags = tags.map do |tag|
483
488
  tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
484
489
  end
@@ -558,12 +563,12 @@ module Dependabot
558
563
 
559
564
  sig { returns(T.nilable(String)) }
560
565
  def ref_or_branch
561
- ref || dependency_source_details&.fetch(:branch)
566
+ ref || dependency_source_details&.branch
562
567
  end
563
568
 
564
569
  sig { returns(T.nilable(String)) }
565
570
  def ref
566
- dependency_source_details&.fetch(:ref)
571
+ dependency_source_details&.ref
567
572
  end
568
573
 
569
574
  sig { params(tag: String).returns(T::Boolean) }
@@ -667,7 +672,7 @@ module Dependabot
667
672
  begin
668
673
  tags = listing_repo_git_metadata_fetcher.tags
669
674
 
670
- if dependency_source_details&.fetch(:ref, nil)&.start_with?("tags/")
675
+ if dependency_source_details&.ref&.start_with?("tags/")
671
676
  tags = tags.map do |tag|
672
677
  tag.dup.tap { |t| t.name = "tags/#{tag.name}" }
673
678
  end
@@ -695,7 +700,7 @@ module Dependabot
695
700
 
696
701
  sig { returns(T::Boolean) }
697
702
  def wants_prerelease?
698
- return false unless dependency_source_details&.fetch(:ref, nil)
703
+ return false unless dependency_source_details&.ref
699
704
  return false unless pinned_ref_looks_like_version?
700
705
 
701
706
  version = version_from_ref(T.must(ref))
@@ -892,7 +897,7 @@ module Dependabot
892
897
  @local_repo_git_metadata_fetcher ||=
893
898
  T.let(
894
899
  GitMetadataFetcher.new(
895
- url: dependency_source_details&.fetch(:url),
900
+ url: T.must(dependency_source_details&.url),
896
901
  credentials: credentials
897
902
  ),
898
903
  T.nilable(Dependabot::GitMetadataFetcher)
@@ -913,8 +918,14 @@ module Dependabot
913
918
 
914
919
  sig { returns(String) }
915
920
  def ref_pinned
916
- dependency.source_details&.fetch(:ref, nil) ||
917
- dependency.source_details&.fetch(:branch, nil) || "HEAD"
921
+ details = source_details
922
+ details&.ref || details&.branch || "HEAD"
923
+ end
924
+
925
+ sig { returns(T.nilable(SourceDetails)) }
926
+ def source_details
927
+ details = dependency.source_details
928
+ SourceDetails.from_hash(details) if details
918
929
  end
919
930
  end
920
931
  # rubocop:enable Metrics/ClassLength
@@ -147,12 +147,6 @@ module Dependabot
147
147
  raise "Unexpected response: #{response_with_git.status} - #{response_with_git.body}"
148
148
  end
149
149
 
150
- if uri.match?(/github\.com/i)
151
- response = response_with_git.data
152
- response[:response_headers] = response[:headers] unless response.nil?
153
- raise Octokit::Error.from_response(response)
154
- end
155
-
156
150
  raise "Server error at #{uri}: #{response_with_git.body}" if response_with_git.status >= 500
157
151
 
158
152
  raise Dependabot::GitDependenciesNotReachable, [uri]
@@ -213,7 +207,7 @@ module Dependabot
213
207
  )
214
208
  end
215
209
 
216
- sig { params(uri: String).returns(T.untyped) }
210
+ sig { params(uri: String).returns(GitResponse) }
217
211
  def fetch_raw_upload_pack_with_git_for(uri)
218
212
  service_pack_uri = uri
219
213
  service_pack_uri += ".git" unless service_pack_uri.end_with?(".git") || skip_git_suffix(uri)
@@ -329,7 +323,7 @@ module Dependabot
329
323
  T.must(line.split.first).chars.last(40).join
330
324
  end
331
325
 
332
- sig { returns(T::Hash[Symbol, T.untyped]) }
326
+ sig { returns(T::Hash[Symbol, Object]) }
333
327
  def excon_defaults
334
328
  # Some git hosts are slow when returning a large number of tags
335
329
  SharedHelpers.excon_defaults(read_timeout: 20)
@@ -344,7 +338,7 @@ module Dependabot
344
338
 
345
339
  # Added method to fetch tags with their creation dates from a git repository. In case
346
340
  # private registry is used, it will clone the repository and fetch tags with their creation dates.
347
- sig { params(uri: String).returns(T.untyped) }
341
+ sig { params(uri: String).returns(GitResponse) }
348
342
  def fetch_tags_with_detail_from_git_for(uri)
349
343
  uri_ending_with_git = uri
350
344
  uri_ending_with_git += ".git" unless uri_ending_with_git.end_with?(".git") || skip_git_suffix(uri)
@@ -0,0 +1,138 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/errors"
7
+ require "dependabot/pull_request_creator"
8
+
9
+ module Dependabot
10
+ class PullRequestCreator
11
+ class BranchNameTemplate
12
+ extend T::Sig
13
+
14
+ class Error < Dependabot::DependabotError; end
15
+
16
+ SOLO_PLACEHOLDERS = T.let(
17
+ %w(prefix package_manager directory target_branch dependency version name).freeze,
18
+ T::Array[String]
19
+ )
20
+ GROUP_PLACEHOLDERS = T.let(
21
+ %w(prefix package_manager directory target_branch group_name name).freeze,
22
+ T::Array[String]
23
+ )
24
+ MULTI_ECO_PLACEHOLDERS = T.let(
25
+ %w(prefix target_branch group_name name).freeze,
26
+ T::Array[String]
27
+ )
28
+
29
+ # ---------------------------------------------------------------
30
+ # 1. validate_template(template, strategy:)
31
+ # Ensures every {token} is allowed for the given strategy,
32
+ # brackets are well-formed, and no forbidden placeholders used.
33
+ # ---------------------------------------------------------------
34
+ sig { params(template: String, strategy: Symbol).returns(T::Boolean) }
35
+ def self.validate_template(template, strategy:) # rubocop:disable Naming/PredicateMethod
36
+ raise Error, "Template must be a non-empty string." if template.empty?
37
+
38
+ allowed = allowed_placeholders(strategy)
39
+ messages = collect_template_errors(template, allowed, strategy)
40
+
41
+ raise Error, "#{messages.join("\n")}\nAllowed: #{allowed.map { |v| "{#{v}}" }.join(', ')}" if messages.any?
42
+
43
+ true
44
+ end
45
+
46
+ sig { params(strategy: Symbol).returns(T::Array[String]) }
47
+ def self.allowed_placeholders(strategy)
48
+ case strategy
49
+ when :solo then SOLO_PLACEHOLDERS
50
+ when :group then GROUP_PLACEHOLDERS
51
+ when :multi_ecosystem then MULTI_ECO_PLACEHOLDERS
52
+ else
53
+ raise Error, "Unknown strategy: #{strategy}"
54
+ end
55
+ end
56
+
57
+ sig { params(template: String, allowed: T::Array[String], strategy: Symbol).returns(T::Array[String]) }
58
+ def self.collect_template_errors(template, allowed, strategy)
59
+ used = template.scan(/\{(\w+)\}/).flatten
60
+ unknown = used.uniq.reject { |t| allowed.include?(t) }
61
+ unknown -= ["package_manager"] if strategy == :multi_ecosystem && used.include?("package_manager")
62
+ malformed = template.gsub(/\{\w+\}/, "").match?(/[{}]/)
63
+
64
+ messages = T.let([], T::Array[String])
65
+ messages << "Unknown placeholder(s): #{unknown.map { |v| "{#{v}}" }.join(', ')}" unless unknown.empty?
66
+ messages << "Malformed or unclosed braces detected." if malformed
67
+
68
+ if strategy == :multi_ecosystem && used.include?("package_manager")
69
+ messages << "{package_manager} is not available for multi-ecosystem groups (spans multiple ecosystems)."
70
+ end
71
+
72
+ messages
73
+ end
74
+
75
+ # ---------------------------------------------------------------
76
+ # 2. validate_ref_name(name)
77
+ # Validates the final branch name against Git ref rules.
78
+ # ---------------------------------------------------------------
79
+ sig { params(name: String).returns(T::Boolean) }
80
+ def self.validate_ref_name(name) # rubocop:disable Naming/PredicateMethod
81
+ illegal = %r{
82
+ [\x00-\x1F\x7F\ ~^:?*\[\\] |
83
+ \.\. |
84
+ @\{ |
85
+ // |
86
+ /\. |
87
+ \A/ |
88
+ /\z |
89
+ \.lock\z |
90
+ \A- |
91
+ \.\z |
92
+ \A@\z
93
+ }x
94
+
95
+ raise Error, "Resolved branch name \"#{name}\" is not a valid Git ref." if name.match?(illegal)
96
+
97
+ true
98
+ end
99
+
100
+ # ---------------------------------------------------------------
101
+ # 3. render(template, vars, strategy:, digest: nil)
102
+ # Full pipeline: validate template -> substitute -> append digest
103
+ # -> validate ref.
104
+ # ---------------------------------------------------------------
105
+ sig do
106
+ params(
107
+ template: String,
108
+ vars: T::Hash[String, String],
109
+ strategy: Symbol,
110
+ digest: T.nilable(String)
111
+ ).returns(String)
112
+ end
113
+ def self.render(template, vars, strategy:, digest: nil)
114
+ validate_template(template, strategy: strategy)
115
+
116
+ # Substitute placeholders
117
+ name = template.gsub(/\{(\w+)\}/) do
118
+ key = T.must(Regexp.last_match(1))
119
+ raise Error, "Missing value for placeholder \"{#{key}}\"." unless vars.key?(key)
120
+
121
+ T.must(vars[key])
122
+ end
123
+
124
+ # Auto-append digest for Group/MultiEcosystem
125
+ name = "#{name}-#{digest}" if digest && strategy != :solo
126
+
127
+ # Collapse empty path segments (e.g. {target_branch} when nil) and strip edge slashes
128
+ name = name.squeeze("/").gsub(%r{\A/|/\z}, "")
129
+ raise Error, "Resolved branch name is empty." if name.empty?
130
+
131
+ # Validate git ref after all transformations
132
+ validate_ref_name(name)
133
+
134
+ name
135
+ end
136
+ end
137
+ end
138
+ end
@@ -4,6 +4,8 @@
4
4
  require "digest"
5
5
  require "sorbet-runtime"
6
6
 
7
+ require "dependabot/pull_request_creator/branch_name_template"
8
+
7
9
  module Dependabot
8
10
  class PullRequestCreator
9
11
  class BranchNamer
@@ -28,6 +30,15 @@ module Dependabot
28
30
  sig { returns(T.nilable(Integer)) }
29
31
  attr_reader :max_length
30
32
 
33
+ sig { returns(T.nilable(String)) }
34
+ attr_reader :word_separator
35
+
36
+ sig { returns(T.nilable(String)) }
37
+ attr_reader :branch_name_case
38
+
39
+ sig { returns(T.nilable(String)) }
40
+ attr_reader :template
41
+
31
42
  sig do
32
43
  params(
33
44
  dependencies: T::Array[Dependency],
@@ -35,7 +46,10 @@ module Dependabot
35
46
  target_branch: T.nilable(String),
36
47
  separator: String,
37
48
  prefix: String,
38
- max_length: T.nilable(Integer)
49
+ max_length: T.nilable(Integer),
50
+ word_separator: T.nilable(String),
51
+ branch_name_case: T.nilable(String),
52
+ template: T.nilable(String)
39
53
  )
40
54
  .void
41
55
  end
@@ -45,7 +59,10 @@ module Dependabot
45
59
  target_branch:,
46
60
  separator: "/",
47
61
  prefix: "dependabot",
48
- max_length: nil
62
+ max_length: nil,
63
+ word_separator: nil,
64
+ branch_name_case: nil,
65
+ template: nil
49
66
  )
50
67
  @dependencies = dependencies
51
68
  @files = files
@@ -53,6 +70,9 @@ module Dependabot
53
70
  @separator = separator
54
71
  @prefix = prefix
55
72
  @max_length = max_length
73
+ @word_separator = word_separator
74
+ @branch_name_case = branch_name_case
75
+ @template = template
56
76
  end
57
77
 
58
78
  sig { overridable.returns(String) }
@@ -62,6 +82,25 @@ module Dependabot
62
82
 
63
83
  private
64
84
 
85
+ sig do
86
+ params(
87
+ vars: T::Hash[String, String],
88
+ strategy: Symbol,
89
+ digest: T.nilable(String)
90
+ ).returns(String)
91
+ end
92
+ def render_from_template(vars:, strategy:, digest: nil)
93
+ rendered = BranchNameTemplate.render(
94
+ T.must(template),
95
+ vars,
96
+ strategy: strategy,
97
+ digest: digest
98
+ )
99
+
100
+ # Apply post-processing (separator, word_separator, case) and max-length
101
+ sanitize_branch_name(rendered)
102
+ end
103
+
65
104
  sig { params(ref_name: String).returns(String) }
66
105
  def sanitize_branch_name(ref_name)
67
106
  # General git ref validation
@@ -70,6 +109,27 @@ module Dependabot
70
109
  # Some users need branch names without slashes
71
110
  sanitized_name = sanitized_name.gsub("/", separator)
72
111
 
112
+ # Apply word_separator and case transformation only to content after the prefix,
113
+ # preserving the user-configured prefix as-is.
114
+ if word_separator || branch_name_case
115
+ prefix_with_sep = "#{prefix}#{separator}"
116
+ prefix_part = sanitized_name.start_with?(prefix_with_sep) ? prefix_with_sep : ""
117
+ content = sanitized_name.delete_prefix(prefix_with_sep)
118
+
119
+ # Replace underscores with word_separator in the content after prefix
120
+ content = content.gsub("_", T.must(word_separator)) if word_separator
121
+
122
+ # Apply case transformation to content after prefix
123
+ case branch_name_case
124
+ when "lowercase"
125
+ content = content.downcase
126
+ when "uppercase"
127
+ content = content.upcase
128
+ end
129
+
130
+ sanitized_name = "#{prefix_part}#{content}"
131
+ end
132
+
73
133
  # Shorten the ref in case users refs have length limits
74
134
  branch_name_max_length = max_length
75
135
  if branch_name_max_length && (sanitized_name.length > branch_name_max_length)
@@ -19,7 +19,10 @@ module Dependabot
19
19
  includes_security_fixes: T::Boolean,
20
20
  separator: String,
21
21
  prefix: String,
22
- max_length: T.nilable(Integer)
22
+ max_length: T.nilable(Integer),
23
+ word_separator: T.nilable(String),
24
+ branch_name_case: T.nilable(String),
25
+ template: T.nilable(String)
23
26
  )
24
27
  .void
25
28
  end
@@ -31,7 +34,10 @@ module Dependabot
31
34
  includes_security_fixes:,
32
35
  separator: "/",
33
36
  prefix: "dependabot",
34
- max_length: nil
37
+ max_length: nil,
38
+ word_separator: nil,
39
+ branch_name_case: nil,
40
+ template: nil
35
41
  )
36
42
  super(
37
43
  dependencies: dependencies,
@@ -40,6 +46,9 @@ module Dependabot
40
46
  separator: separator,
41
47
  prefix: prefix,
42
48
  max_length: max_length,
49
+ word_separator: word_separator,
50
+ branch_name_case: branch_name_case,
51
+ template: template,
43
52
  )
44
53
 
45
54
  @dependency_group = dependency_group
@@ -48,6 +57,14 @@ module Dependabot
48
57
 
49
58
  sig { override.returns(String) }
50
59
  def new_branch_name
60
+ if template
61
+ return render_from_template(
62
+ vars: template_vars,
63
+ strategy: :group,
64
+ digest: dependency_digest
65
+ )
66
+ end
67
+
51
68
  sanitize_branch_name(File.join(prefixes, group_name_with_dependency_digest))
52
69
  end
53
70
 
@@ -56,6 +73,24 @@ module Dependabot
56
73
  sig { returns(Dependabot::DependencyGroup) }
57
74
  attr_reader :dependency_group
58
75
 
76
+ sig { returns(T::Hash[String, String]) }
77
+ def template_vars
78
+ directory_part = (directory || "/").sub(%r{^/}, "")
79
+ directory_part = "root" if directory_part.empty?
80
+
81
+ group_name = sanitize_ref(dependency_group.name.tr(" ", "-"))
82
+
83
+ vars = {
84
+ "prefix" => prefix,
85
+ "package_manager" => package_manager,
86
+ "directory" => directory_part,
87
+ "group_name" => group_name,
88
+ "name" => group_name,
89
+ "target_branch" => target_branch || ""
90
+ }
91
+ vars
92
+ end
93
+
59
94
  sig { returns(T::Array[String]) }
60
95
  def prefixes
61
96
  [
@@ -19,7 +19,10 @@ module Dependabot
19
19
  multi_ecosystem_name: String,
20
20
  separator: String,
21
21
  prefix: String,
22
- max_length: T.nilable(Integer)
22
+ max_length: T.nilable(Integer),
23
+ word_separator: T.nilable(String),
24
+ branch_name_case: T.nilable(String),
25
+ template: T.nilable(String)
23
26
  )
24
27
  .void
25
28
  end
@@ -31,7 +34,10 @@ module Dependabot
31
34
  multi_ecosystem_name:,
32
35
  separator: "/",
33
36
  prefix: "dependabot",
34
- max_length: nil
37
+ max_length: nil,
38
+ word_separator: nil,
39
+ branch_name_case: nil,
40
+ template: nil
35
41
  )
36
42
  super(
37
43
  dependencies: dependencies,
@@ -40,6 +46,9 @@ module Dependabot
40
46
  separator: separator,
41
47
  prefix: prefix,
42
48
  max_length: max_length,
49
+ word_separator: word_separator,
50
+ branch_name_case: branch_name_case,
51
+ template: template,
43
52
  )
44
53
 
45
54
  @multi_ecosystem_name = multi_ecosystem_name
@@ -48,6 +57,14 @@ module Dependabot
48
57
 
49
58
  sig { override.returns(String) }
50
59
  def new_branch_name
60
+ if template
61
+ return render_from_template(
62
+ vars: template_vars,
63
+ strategy: :multi_ecosystem,
64
+ digest: dependency_digest
65
+ )
66
+ end
67
+
51
68
  sanitize_branch_name(File.join(prefixes, group_name_with_dependency_digest))
52
69
  end
53
70
 
@@ -56,6 +73,19 @@ module Dependabot
56
73
  sig { returns(String) }
57
74
  attr_reader :multi_ecosystem_name
58
75
 
76
+ sig { returns(T::Hash[String, String]) }
77
+ def template_vars
78
+ multi_name = sanitize_ref(multi_ecosystem_name.tr(" ", "-"))
79
+
80
+ vars = {
81
+ "prefix" => prefix,
82
+ "group_name" => multi_name,
83
+ "name" => multi_name,
84
+ "target_branch" => target_branch || ""
85
+ }
86
+ vars
87
+ end
88
+
59
89
  sig { returns(T::Array[String]) }
60
90
  def prefixes
61
91
  [
@@ -15,26 +15,18 @@ module Dependabot
15
15
 
16
16
  sig { override.returns(String) }
17
17
  def new_branch_name
18
+ if template
19
+ return render_from_template(
20
+ vars: template_vars,
21
+ strategy: :solo
22
+ )
23
+ end
24
+
18
25
  return short_branch_name if branch_name_might_be_long?
19
26
 
20
27
  @name ||=
21
28
  T.let(
22
- begin
23
- dependency_name_part =
24
- if dependencies.count > 1 && updating_a_property?
25
- property_name
26
- elsif dependencies.count > 1 && updating_a_dependency_set?
27
- dependency_set.fetch(:group)
28
- else
29
- dependencies
30
- .map(&:name)
31
- .join("-and-")
32
- .tr(":[]", "-")
33
- .tr("@", "")
34
- end
35
-
36
- "#{dependency_name_part}-#{branch_version_suffix}"
37
- end,
29
+ "#{template_dependency_name}-#{branch_version_suffix}",
38
30
  T.nilable(String)
39
31
  )
40
32
 
@@ -53,6 +45,40 @@ module Dependabot
53
45
  ].compact
54
46
  end
55
47
 
48
+ sig { returns(T::Hash[String, String]) }
49
+ def template_vars
50
+ dep_name = template_dependency_name
51
+ version = branch_version_suffix || ""
52
+
53
+ vars = {
54
+ "prefix" => prefix,
55
+ "package_manager" => package_manager,
56
+ "directory" => sanitized_directory,
57
+ "dependency" => dep_name,
58
+ "version" => version,
59
+ "name" => "#{dep_name}-#{version}",
60
+ "target_branch" => target_branch || ""
61
+ }
62
+ vars
63
+ end
64
+
65
+ sig { returns(String) }
66
+ def template_dependency_name
67
+ if dependencies.count > 1 && updating_a_property?
68
+ property_name
69
+ elsif dependencies.count > 1 && updating_a_dependency_set?
70
+ dependency_set.fetch(:group)
71
+ else
72
+ dependencies.map(&:name).join("-and-").tr(":[]", "-").tr("@", "")
73
+ end
74
+ end
75
+
76
+ sig { returns(String) }
77
+ def sanitized_directory
78
+ dir = (files.first&.directory&.tr(" ", "-") || "/").sub(%r{^/}, "")
79
+ dir.empty? ? "root" : dir
80
+ end
81
+
56
82
  sig { returns(String) }
57
83
  def package_manager
58
84
  T.must(dependencies.first).package_manager
@@ -9,6 +9,7 @@ require "dependabot/pull_request_creator"
9
9
  require "dependabot/pull_request_creator/branch_namer/solo_strategy"
10
10
  require "dependabot/pull_request_creator/branch_namer/dependency_group_strategy"
11
11
  require "dependabot/pull_request_creator/branch_namer/multi_ecosystem_strategy"
12
+ require "dependabot/pull_request_creator/branch_name_template"
12
13
 
13
14
  module Dependabot
14
15
  class PullRequestCreator
@@ -33,6 +34,15 @@ module Dependabot
33
34
  sig { returns(T.nilable(Integer)) }
34
35
  attr_reader :max_length
35
36
 
37
+ sig { returns(T.nilable(String)) }
38
+ attr_reader :word_separator
39
+
40
+ sig { returns(T.nilable(String)) }
41
+ attr_reader :branch_name_case
42
+
43
+ sig { returns(T.nilable(String)) }
44
+ attr_reader :template
45
+
36
46
  sig { returns(T.nilable(Dependabot::DependencyGroup)) }
37
47
  attr_reader :dependency_group
38
48
 
@@ -51,6 +61,9 @@ module Dependabot
51
61
  separator: String,
52
62
  prefix: String,
53
63
  max_length: T.nilable(Integer),
64
+ word_separator: T.nilable(String),
65
+ branch_name_case: T.nilable(String),
66
+ template: T.nilable(String),
54
67
  includes_security_fixes: T::Boolean,
55
68
  multi_ecosystem_name: T.nilable(String)
56
69
  )
@@ -64,6 +77,9 @@ module Dependabot
64
77
  separator: "/",
65
78
  prefix: "dependabot",
66
79
  max_length: nil,
80
+ word_separator: nil,
81
+ branch_name_case: nil,
82
+ template: nil,
67
83
  includes_security_fixes: false,
68
84
  multi_ecosystem_name: nil
69
85
  )
@@ -74,6 +90,9 @@ module Dependabot
74
90
  @separator = separator
75
91
  @prefix = prefix
76
92
  @max_length = max_length
93
+ @word_separator = word_separator
94
+ @branch_name_case = branch_name_case
95
+ @template = template
77
96
  @includes_security_fixes = includes_security_fixes
78
97
  @multi_ecosystem_name = multi_ecosystem_name
79
98
  end
@@ -109,6 +128,9 @@ module Dependabot
109
128
  separator: separator,
110
129
  prefix: prefix,
111
130
  max_length: max_length,
131
+ word_separator: word_separator,
132
+ branch_name_case: branch_name_case,
133
+ template: template,
112
134
  multi_ecosystem_name: T.must(multi_ecosystem_name)
113
135
  )
114
136
  end
@@ -121,7 +143,10 @@ module Dependabot
121
143
  target_branch: target_branch,
122
144
  separator: separator,
123
145
  prefix: prefix,
124
- max_length: max_length
146
+ max_length: max_length,
147
+ word_separator: word_separator,
148
+ branch_name_case: branch_name_case,
149
+ template: template
125
150
  )
126
151
  end
127
152
 
@@ -135,7 +160,10 @@ module Dependabot
135
160
  includes_security_fixes: includes_security_fixes,
136
161
  separator: separator,
137
162
  prefix: prefix,
138
- max_length: max_length
163
+ max_length: max_length,
164
+ word_separator: word_separator,
165
+ branch_name_case: branch_name_case,
166
+ template: template
139
167
  )
140
168
  end
141
169
  end
@@ -126,6 +126,15 @@ module Dependabot
126
126
  sig { returns(T.nilable(Integer)) }
127
127
  attr_reader :branch_name_max_length
128
128
 
129
+ sig { returns(T.nilable(String)) }
130
+ attr_reader :branch_name_word_separator
131
+
132
+ sig { returns(T.nilable(String)) }
133
+ attr_reader :branch_name_case
134
+
135
+ sig { returns(T.nilable(String)) }
136
+ attr_reader :branch_name_template
137
+
129
138
  sig { returns(String) }
130
139
  attr_reader :github_redirection_service
131
140
 
@@ -144,7 +153,7 @@ module Dependabot
144
153
  sig { returns(T.nilable(Encoding)) }
145
154
  attr_reader :pr_message_encoding
146
155
 
147
- sig do
156
+ sig do # rubocop:disable Metrics/BlockLength
148
157
  params(
149
158
  source: Dependabot::Source,
150
159
  base_commit: String,
@@ -164,6 +173,9 @@ module Dependabot
164
173
  branch_name_separator: String,
165
174
  branch_name_prefix: String,
166
175
  branch_name_max_length: T.nilable(Integer),
176
+ branch_name_word_separator: T.nilable(String),
177
+ branch_name_case: T.nilable(String),
178
+ branch_name_template: T.nilable(String),
167
179
  label_language: T::Boolean,
168
180
  automerge_candidate: T::Boolean,
169
181
  github_redirection_service: String,
@@ -198,6 +210,9 @@ module Dependabot
198
210
  branch_name_separator: "/",
199
211
  branch_name_prefix: "dependabot",
200
212
  branch_name_max_length: 100,
213
+ branch_name_word_separator: nil,
214
+ branch_name_case: nil,
215
+ branch_name_template: nil,
201
216
  label_language: false,
202
217
  automerge_candidate: false,
203
218
  github_redirection_service: DEFAULT_GITHUB_REDIRECTION_SERVICE,
@@ -227,6 +242,9 @@ module Dependabot
227
242
  @branch_name_separator = branch_name_separator
228
243
  @branch_name_prefix = branch_name_prefix
229
244
  @branch_name_max_length = branch_name_max_length
245
+ @branch_name_word_separator = branch_name_word_separator
246
+ @branch_name_case = branch_name_case
247
+ @branch_name_template = branch_name_template
230
248
  @label_language = label_language
231
249
  @automerge_candidate = automerge_candidate
232
250
  @github_redirection_service = github_redirection_service
@@ -420,6 +438,9 @@ module Dependabot
420
438
  separator: branch_name_separator,
421
439
  prefix: branch_name_prefix,
422
440
  max_length: branch_name_max_length,
441
+ word_separator: branch_name_word_separator,
442
+ branch_name_case: branch_name_case,
443
+ template: branch_name_template,
423
444
  includes_security_fixes: includes_security_fixes?
424
445
  ),
425
446
  T.nilable(Dependabot::PullRequestCreator::BranchNamer)
@@ -126,16 +126,36 @@ module Dependabot
126
126
  )
127
127
  end
128
128
 
129
+ @github_enterprise_cache = T.let({}, T::Hash[String, T::Boolean])
130
+
131
+ sig { void }
132
+ def self.reset_github_enterprise_cache!
133
+ @github_enterprise_cache.clear
134
+ end
135
+
129
136
  sig { params(base_url: String).returns(T::Boolean) }
130
137
  def self.github_enterprise?(base_url)
138
+ return T.must(@github_enterprise_cache[base_url]) if @github_enterprise_cache.key?(base_url)
139
+
140
+ result = detect_github_enterprise(base_url)
141
+ @github_enterprise_cache[base_url] = result unless result.nil?
142
+ result || false
143
+ end
144
+
145
+ sig { params(base_url: String).returns(T.nilable(T::Boolean)) }
146
+ def self.detect_github_enterprise(base_url)
131
147
  resp = Excon.get(File.join(base_url, "status"))
132
- resp.status == 200 &&
133
- # Alternatively: resp.headers["Server"] == "GitHub.com", but this
134
- # currently doesn't work with development environments
135
- ((resp.headers["X-GitHub-Request-Id"] && !resp.headers["X-GitHub-Request-Id"]&.empty?) || false)
148
+ return false if resp.status == 404
149
+
150
+ return unless resp.status == 200
151
+
152
+ # Alternatively: resp.headers["Server"] == "GitHub.com", but this
153
+ # currently doesn't work with development environments
154
+ (resp.headers["X-GitHub-Request-Id"] && !resp.headers["X-GitHub-Request-Id"]&.empty?) || false
136
155
  rescue StandardError
137
- false
156
+ nil
138
157
  end
158
+ private_class_method :detect_github_enterprise
139
159
 
140
160
  sig do
141
161
  params(
data/lib/dependabot.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Dependabot
5
- VERSION = "0.385.0"
5
+ VERSION = "0.387.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-common
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.385.0
4
+ version: 0.387.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -552,6 +552,7 @@ files:
552
552
  - lib/dependabot/file_updaters/base.rb
553
553
  - lib/dependabot/file_updaters/vendor_updater.rb
554
554
  - lib/dependabot/git_commit_checker.rb
555
+ - lib/dependabot/git_commit_checker/source_details.rb
555
556
  - lib/dependabot/git_cooldown_date_resolver.rb
556
557
  - lib/dependabot/git_metadata_fetcher.rb
557
558
  - lib/dependabot/git_ref.rb
@@ -574,6 +575,7 @@ files:
574
575
  - lib/dependabot/pull_request_creator.rb
575
576
  - lib/dependabot/pull_request_creator/azure.rb
576
577
  - lib/dependabot/pull_request_creator/bitbucket.rb
578
+ - lib/dependabot/pull_request_creator/branch_name_template.rb
577
579
  - lib/dependabot/pull_request_creator/branch_namer.rb
578
580
  - lib/dependabot/pull_request_creator/branch_namer/base.rb
579
581
  - lib/dependabot/pull_request_creator/branch_namer/dependency_group_strategy.rb
@@ -621,7 +623,7 @@ licenses:
621
623
  - MIT
622
624
  metadata:
623
625
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
624
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.385.0
626
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.387.0
625
627
  rdoc_options: []
626
628
  require_paths:
627
629
  - lib