git 5.0.0.beta.1 → 5.0.0.beta.2

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +6 -0
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  5. data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
  6. data/.github/skills/facade-implementation/SKILL.md +14 -0
  7. data/.github/skills/facade-test-conventions/SKILL.md +14 -0
  8. data/.rubocop.yml +5 -0
  9. data/README.md +51 -11
  10. data/UPGRADING.md +141 -0
  11. data/git.gemspec +5 -0
  12. data/lib/git/branch.rb +7 -18
  13. data/lib/git/branches.rb +2 -10
  14. data/lib/git/command_line/base.rb +10 -0
  15. data/lib/git/command_line/capturing.rb +5 -3
  16. data/lib/git/command_line/streaming.rb +5 -3
  17. data/lib/git/command_line.rb +3 -3
  18. data/lib/git/commands/base.rb +7 -6
  19. data/lib/git/commands/cat_file/batch.rb +6 -1
  20. data/lib/git/commands/cat_file/raw.rb +7 -1
  21. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
  22. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  23. data/lib/git/commands/update_ref/batch.rb +1 -1
  24. data/lib/git/commands/version.rb +5 -0
  25. data/lib/git/commands.rb +5 -7
  26. data/lib/git/config.rb +17 -0
  27. data/lib/git/config_entry_info.rb +106 -0
  28. data/lib/git/configuring.rb +665 -0
  29. data/lib/git/deprecation.rb +9 -0
  30. data/lib/git/diff.rb +4 -8
  31. data/lib/git/diff_path_status.rb +2 -13
  32. data/lib/git/diff_stats.rb +1 -9
  33. data/lib/git/execution_context/global.rb +3 -28
  34. data/lib/git/execution_context/repository.rb +30 -41
  35. data/lib/git/execution_context.rb +43 -24
  36. data/lib/git/log.rb +3 -9
  37. data/lib/git/object.rb +14 -21
  38. data/lib/git/parsers/config_entry.rb +110 -0
  39. data/lib/git/parsers/ls_remote.rb +79 -0
  40. data/lib/git/remote.rb +7 -20
  41. data/lib/git/repository/branching.rb +183 -12
  42. data/lib/git/repository/committing.rb +64 -68
  43. data/lib/git/repository/configuring.rb +208 -13
  44. data/lib/git/repository/context_helpers.rb +264 -0
  45. data/lib/git/repository/factories.rb +682 -0
  46. data/lib/git/repository/inspecting.rb +99 -0
  47. data/lib/git/repository/maintenance.rb +65 -0
  48. data/lib/git/repository/merging.rb +63 -1
  49. data/lib/git/repository/object_operations.rb +133 -35
  50. data/lib/git/repository/path_resolver.rb +1 -1
  51. data/lib/git/repository/remote_operations.rb +166 -21
  52. data/lib/git/repository/staging.rb +187 -23
  53. data/lib/git/repository/stashing.rb +39 -3
  54. data/lib/git/repository/status_operations.rb +21 -0
  55. data/lib/git/repository.rb +68 -129
  56. data/lib/git/stash.rb +2 -9
  57. data/lib/git/stashes.rb +2 -7
  58. data/lib/git/status.rb +8 -17
  59. data/lib/git/version.rb +2 -2
  60. data/lib/git/worktree.rb +2 -15
  61. data/lib/git/worktrees.rb +2 -15
  62. data/lib/git.rb +180 -77
  63. data/redesign/3_architecture_implementation.md +148 -111
  64. data/redesign/Phase 4 - Step A.md +360 -0
  65. data/redesign/beta_release.md +107 -0
  66. data/redesign/c1c2_audit.md +566 -0
  67. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  68. data/redesign/config_design.rb +501 -0
  69. metadata +19 -5
  70. data/lib/git/base.rb +0 -1204
  71. data/lib/git/lib.rb +0 -2855
data/lib/git/branch.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/base'
4
3
  require_relative 'branch_info'
5
4
 
6
5
  module Git
7
6
  # Represents a Git branch
8
7
  #
9
8
  # Branch objects provide access to branch metadata and operations like checkout,
10
- # delete, and merge. They should be obtained via {Git::Base#branch} or
11
- # {Git::Base#branches}, not constructed directly.
9
+ # delete, and merge. They should be obtained via {Git::Repository#branch} or
10
+ # {Git::Repository#branches}, not constructed directly.
12
11
  #
13
12
  # @example Getting a branch
14
13
  # git = Git.open('.')
@@ -24,7 +23,7 @@ module Git
24
23
  # The full refname of this branch
25
24
  #
26
25
  # For local branches this is the short name (e.g. `'main'`). For
27
- # remote-tracking branches obtained via {Git::Base#branches} this includes
26
+ # remote-tracking branches obtained via {Git::Repository#branches} this includes
28
27
  # the `remotes/` prefix (e.g. `'remotes/origin/main'`). Branches constructed
29
28
  # by {Git::Remote#branch} use the `<remote>/<branch>` form (e.g.
30
29
  # `'origin/main'`) which does **not** populate {#remote}.
@@ -68,17 +67,13 @@ module Git
68
67
 
69
68
  # Initialize a new Branch object
70
69
  #
71
- # @param base [Git::Base, Git::Repository] the git repository
72
- #
73
- # Accepts either a {Git::Base} (legacy) or a {Git::Repository} (new form).
74
- # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
75
- # in Phase 4.
70
+ # @param base [Git::Repository] the git repository
76
71
  #
77
72
  # @param branch_info_or_name [Git::BranchInfo, String] branch info object or name string
78
73
  #
79
74
  # Passing a BranchInfo is preferred; String support is for backward compatibility.
80
75
  #
81
- # @note Use {Git::Base#branch} or {Git::Base#branches} instead of constructing directly
76
+ # @note Use {Git::Repository#branch} or {Git::Repository#branches} instead of constructing directly
82
77
  #
83
78
  # @api private
84
79
  #
@@ -151,7 +146,7 @@ module Git
151
146
  #
152
147
  # @param file [String] path to the destination archive file
153
148
  #
154
- # @param opts [Hash] archive options (see {Git::Base#archive})
149
+ # @param opts [Hash] archive options (see {Git::Repository#archive})
155
150
  #
156
151
  # @return [String] the path to the written archive file
157
152
  #
@@ -464,18 +459,12 @@ module Git
464
459
  nil
465
460
  end
466
461
 
467
- # Resolves the {Git::Repository} for this branch
468
- #
469
- # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
470
- # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
471
- # in Phase 4.
472
- #
473
462
  # @return [Git::Repository]
474
463
  #
475
464
  # @api private
476
465
  #
477
466
  def branch_repository
478
- @base.is_a?(Git::Base) ? @base.facade_repository : @base
467
+ @base
479
468
  end
480
469
  end
481
470
  end
data/lib/git/branches.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/base'
4
-
5
3
  module Git
6
4
  # Collection of all Git branches in a repository
7
5
  #
@@ -19,7 +17,7 @@ module Git
19
17
 
20
18
  # Creates a new Branches collection populated from the given repository
21
19
  #
22
- # @param base [Git::Base, Git::Repository] the repository to enumerate
20
+ # @param base [Git::Repository] the repository to enumerate
23
21
  # branches from
24
22
  #
25
23
  # @return [void]
@@ -136,18 +134,12 @@ module Git
136
134
 
137
135
  private
138
136
 
139
- # Resolves the {Git::Repository} for this collection of branches
140
- #
141
- # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
142
- # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
143
- # in Phase 4.
144
- #
145
137
  # @return [Git::Repository] the repository used to enumerate branches
146
138
  #
147
139
  # @api private
148
140
  #
149
141
  def branch_repository
150
- @base.is_a?(Git::Base) ? @base.facade_repository : @base
142
+ @base
151
143
  end
152
144
 
153
145
  # Indexes all supported lookup keys for a branch without mutating
@@ -156,6 +156,11 @@ module Git
156
156
  #
157
157
  # @raise [Git::ProcessIOError] in place of ProcessExecuter::ProcessIOError
158
158
  #
159
+ # @raise [Git::ProcessIOError] when a timeout race causes Errno::ESRCH. On Ruby 4.0+,
160
+ # `Process.kill` raises `Errno::ESRCH` when the spawned process exits between the
161
+ # timeout firing and the kill signal being delivered. This is a race in
162
+ # process_executer's timeout handling and is semantically equivalent to a timeout.
163
+ #
159
164
  # @return [Object] the return value of the block
160
165
  #
161
166
  # @api private
@@ -168,6 +173,11 @@ module Git
168
173
  raise Git::Error, e.message, cause: e.cause
169
174
  rescue ProcessExecuter::ProcessIOError => e
170
175
  raise Git::ProcessIOError, e.message, cause: e.cause
176
+ rescue Errno::ESRCH => e
177
+ # Ruby 4.0+: Process.kill raises Errno::ESRCH when the spawned process exits
178
+ # in the narrow window between the timeout firing and the kill signal being sent.
179
+ # This is a known race in process_executer's timeout handling.
180
+ raise Git::ProcessIOError, "Git process no longer exists (timeout race): #{e.message}", cause: e
171
181
  end
172
182
 
173
183
  # Build the git command line from the available sources to send to `Process.spawn`
@@ -9,7 +9,7 @@ module Git
9
9
  # {Git::CommandLine::Capturing} is the buffering strategy: it calls
10
10
  # `ProcessExecuter.run_with_capture`, which reads all subprocess output into
11
11
  # `String` objects before returning. Use this class (via
12
- # {Git::Lib#command_capturing}) for the vast majority of git subcommands whose
12
+ # {Git::ExecutionContext#command_capturing}) for the vast majority of git subcommands whose
13
13
  # output fits comfortably in memory.
14
14
  #
15
15
  # {Git::CommandLine::Streaming} is the complementary strategy for commands
@@ -23,7 +23,7 @@ module Git
23
23
  # result.stdout # => "abc1234 Initial commit\n..."
24
24
  # result.stderr # => ""
25
25
  #
26
- # @see Git::Lib#command_capturing
26
+ # @see Git::ExecutionContext#command_capturing
27
27
  #
28
28
  # @see Git::CommandLine::Streaming
29
29
  #
@@ -149,7 +149,9 @@ module Git
149
149
  # @raise [Git::FailedError] if the command returned a non-zero exit status
150
150
  #
151
151
  # @raise [Git::ProcessIOError] if an exception was raised while collecting
152
- # subprocess output
152
+ # subprocess output, or (Ruby 4.0+) if a timeout-handling race causes
153
+ # `Errno::ESRCH` when the spawned process exits between the timeout
154
+ # firing and the kill signal being delivered
153
155
  #
154
156
  # @raise [Git::TimeoutError] if the command times out
155
157
  #
@@ -12,7 +12,7 @@ module Git
12
12
  # IO object. Stderr is always captured internally in a `StringIO` for error
13
13
  # diagnostics and is available as `result.stderr`.
14
14
  #
15
- # Use this class (via {Git::Lib#command_streaming}) for commands such as
15
+ # Use this class (via {Git::ExecutionContext#command_streaming}) for commands such as
16
16
  # `cat-file -p <blob>` whose stdout may be too large to buffer in memory.
17
17
  #
18
18
  # {Git::CommandLine::Capturing} is the complementary strategy for the common case
@@ -26,7 +26,7 @@ module Git
26
26
  # streaming.run('cat-file', 'blob', sha, out: f)
27
27
  # end
28
28
  #
29
- # @see Git::Lib#command_streaming
29
+ # @see Git::ExecutionContext#command_streaming
30
30
  #
31
31
  # @see Git::CommandLine::Capturing
32
32
  #
@@ -102,7 +102,9 @@ module Git
102
102
  # @raise [Git::FailedError] if the command returned a non-zero exit status
103
103
  #
104
104
  # @raise [Git::ProcessIOError] if an exception was raised while collecting
105
- # subprocess output
105
+ # subprocess output, or (Ruby 4.0+) if a timeout-handling race causes
106
+ # `Errno::ESRCH` when the spawned process exits between the timeout
107
+ # firing and the kill signal being delivered
106
108
  #
107
109
  # @raise [Git::TimeoutError] if the command times out
108
110
  #
@@ -14,9 +14,9 @@ module Git
14
14
  # Use this for commands (e.g. `cat-file -p <blob>`) whose output may be
15
15
  # too large to buffer.
16
16
  #
17
- # Both classes inherit from {Git::CommandLine::Base} and are instantiated
18
- # via factory helpers in {Git::Lib}: {Git::Lib#command_capturing} and
19
- # {Git::Lib#command_streaming}.
17
+ # Both classes inherit from {Git::CommandLine::Base} and are used internally
18
+ # by {Git::ExecutionContext#command_capturing} and
19
+ # {Git::ExecutionContext#command_streaming}.
20
20
  #
21
21
  # Results are returned as {Git::CommandLine::Result} objects (also accessible
22
22
  # as {Git::CommandLineResult} for backward compatibility).
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'git/commands/arguments'
4
+ require 'git/version_constraint'
4
5
 
5
6
  module Git
6
7
  module Commands
@@ -173,8 +174,8 @@ module Git
173
174
  end
174
175
  end
175
176
 
176
- # @param execution_context [Git::ExecutionContext, Git::Lib] context that provides
177
- # {Git::Lib#command_capturing} and {Git::Lib#command_streaming}
177
+ # @param execution_context [Git::ExecutionContext] context that provides
178
+ # {Git::ExecutionContext#command_capturing} and {Git::ExecutionContext#command_streaming}
178
179
  def initialize(execution_context)
179
180
  @execution_context = execution_context
180
181
  end
@@ -210,7 +211,7 @@ module Git
210
211
  # @raise [Git::VersionError] if the installed git version doesn't meet requirements
211
212
  def call(*, **)
212
213
  bound = args_definition.bind(*, **)
213
- validate_version!
214
+ validate_version!(bound.execution_options)
214
215
  result = execute_command(bound)
215
216
  validate_exit_status!(result)
216
217
  result
@@ -312,10 +313,10 @@ module Git
312
313
  #
313
314
  # Floor check always runs first and fails fast.
314
315
  #
315
- def validate_version!
316
+ def validate_version!(exec_opts = {})
316
317
  return if self.class.skip_version_validation?
317
318
 
318
- actual_version = @execution_context.git_version
319
+ actual_version = @execution_context.git_version(timeout: exec_opts[:timeout])
319
320
 
320
321
  # Floor check: fail-fast if git is too old for the gem itself
321
322
  validate_floor_version!(actual_version)
@@ -351,7 +352,7 @@ module Git
351
352
  # the read end. The write and close happen concurrently with the block.
352
353
  #
353
354
  # The read end can be passed as the `in:` keyword to
354
- # {Git::Lib#command_capturing} / {Git::CommandLine#run_with_capture}, connecting it directly to
355
+ # {Git::ExecutionContext#command_capturing} / {Git::CommandLine#run_with_capture}, connecting it directly to
355
356
  # the spawned git process's stdin without an intermediate file or shell
356
357
  # heredoc. This is required because `Process.spawn` only accepts real IO
357
358
  # objects with a file descriptor — `StringIO` does not work.
@@ -103,6 +103,9 @@ module Git
103
103
  # When provided, {#call} dispatches to the streaming execution path.
104
104
  execution_option :out
105
105
 
106
+ # Abort the command after this many seconds.
107
+ execution_option :timeout
108
+
106
109
  # Object names (or batch-command lines) are written to stdin, not argv.
107
110
  # Using skip_cli: true because these values are fed via stdin — git never
108
111
  # sees them as CLI arguments so Ruby must enforce the cross-argument
@@ -302,6 +305,8 @@ module Git
302
305
  # @option options [#write, nil] :out (nil) stream stdout to this IO object
303
306
  # instead of buffering in memory; when given, `result.stdout` will be `''`
304
307
  #
308
+ # @option options [Numeric, nil] :timeout (nil) abort the command after this many seconds
309
+ #
305
310
  # @return [Git::CommandLineResult] the result of calling `git cat-file`
306
311
  #
307
312
  # Stdout contains one metadata line per object (or `''` when `out:` is given)
@@ -311,7 +316,7 @@ module Git
311
316
  # @raise [Git::FailedError] if git exits non-zero
312
317
  def call(*objects, **)
313
318
  bound = args_definition.bind(*objects, **)
314
- validate_version!
319
+ validate_version!(bound.execution_options)
315
320
  # `-Z` puts git into NUL I/O mode: input objects must be NUL-terminated.
316
321
  # Without `-Z`, the standard newline delimiter is used.
317
322
  delimiter = bound.Z? ? "\0" : "\n"
@@ -62,6 +62,9 @@ module Git
62
62
  # When provided, {#call} dispatches to the streaming execution path.
63
63
  execution_option :out
64
64
 
65
+ # Abort the command after this many seconds.
66
+ execution_option :timeout
67
+
65
68
  end_of_options
66
69
 
67
70
  # Expected object type — one of `commit`, `tree`, `blob`, or `tag`.
@@ -191,9 +194,12 @@ module Git
191
194
  #
192
195
  # @raise [Git::FailedError] if the object does not exist or is not of the
193
196
  # given type
197
+ #
198
+ # @option options [Numeric, nil] :timeout (nil) abort the command after this many seconds
199
+ #
194
200
  def call(*, **)
195
201
  bound = args_definition.bind(*, **)
196
- validate_version!
202
+ validate_version!(bound.execution_options)
197
203
  result = execute_command(bound)
198
204
 
199
205
  # `-e` treats exit 1 as a meaningful result (object not found), but any other
@@ -45,6 +45,9 @@ module Git
45
45
  # General read options
46
46
  flag_option :includes, negatable: true
47
47
 
48
+ # Output modifiers
49
+ flag_option :show_scope
50
+
48
51
  # Operands
49
52
  end_of_options
50
53
  operand :name, required: true
@@ -91,6 +94,8 @@ module Git
91
94
  # @option options [Boolean, nil] :no_includes (nil) disable include directives
92
95
  # in config files (`--no-includes`)
93
96
  #
97
+ # @option options [Boolean, nil] :show_scope (nil) show the scope of each config entry
98
+ #
94
99
  # @return [Git::CommandLineResult] the result of calling `git config --get-urlmatch`
95
100
  #
96
101
  # @raise [ArgumentError] if unsupported options are provided
@@ -86,7 +86,7 @@ module Git
86
86
  end
87
87
 
88
88
  bound = args_definition.bind(*, exclude_existing: exclude_existing, **)
89
- validate_version!
89
+ validate_version!(bound.execution_options)
90
90
  stdin = Array(bound.ref).map { |r| "#{r}\n" }.join
91
91
  with_stdin(stdin) { |reader| run_filter(bound, reader) }
92
92
  end
@@ -118,7 +118,7 @@ module Git
118
118
  # @raise [Git::FailedError] if git exits with a non-zero exit status
119
119
  def call(*, **)
120
120
  bound = args_definition.bind(*, **)
121
- validate_version!
121
+ validate_version!(bound.execution_options)
122
122
  with_stdin(build_stdin(bound)) do |reader|
123
123
  result = @execution_context.command_capturing(
124
124
  *bound, in: reader, **bound.execution_options, raise_on_failure: false
@@ -28,6 +28,7 @@ module Git
28
28
  skip_version_validation
29
29
 
30
30
  arguments do
31
+ execution_option :timeout
31
32
  literal 'version'
32
33
  flag_option :build_options
33
34
  end
@@ -42,6 +43,10 @@ module Git
42
43
  #
43
44
  # @option options [Boolean, nil] :build_options (nil) include build options in the output
44
45
  #
46
+ # @option options [Numeric, nil] :timeout (nil) the number of seconds to wait
47
+ # for the command to complete; if nil, uses the global timeout from
48
+ # {Git::Config}; if 0, no timeout is enforced
49
+ #
45
50
  # @return [Git::CommandLineResult] the result of calling `git version`
46
51
  #
47
52
  # @raise [ArgumentError] if unsupported options are provided
data/lib/git/commands.rb CHANGED
@@ -61,19 +61,17 @@ module Git
61
61
  # {Git::CommandLine}, and return a raw {Git::CommandLineResult}.
62
62
  #
63
63
  # Commands do **not** parse output — that responsibility belongs to the
64
- # {Git::Parsers} layer, orchestrated by the facade ({Git::Lib} / future
65
- # {Git::Repository}).
64
+ # {Git::Parsers} layer, orchestrated by the facade ({Git::Repository}).
66
65
  #
67
66
  # All classes in this namespace are internal (`@api private`). End users
68
- # should interact with the public API on {Git::Base} instead.
67
+ # should interact with the public API on {Git::Repository} instead.
69
68
  #
70
69
  # ## Architecture
71
70
  #
72
71
  # ```
73
- # Git::Base (public API)
74
- # └── Git::Lib / Git::Repository (facade orchestrates commands + parsers)
75
- # └── Git::Commands::* (defines CLI API, binds args, executes)
76
- # └── Git::CommandLine (subprocess execution)
72
+ # Git::Repository (public API / facade — orchestrates commands + parsers)
73
+ # └── Git::Commands::* (defines CLI API, binds args, executes)
74
+ # └── Git::CommandLine (subprocess execution)
77
75
  # ```
78
76
  #
79
77
  # Simple commands inherit from {Commands::Base} and only need an `arguments`
data/lib/git/config.rb CHANGED
@@ -3,6 +3,23 @@
3
3
  module Git
4
4
  # The global configuration for this gem
5
5
  class Config
6
+ # Returns the process-wide singleton {Git::Config} instance
7
+ #
8
+ # All calls to {Git.configure}, {Git.config}, and the {Git::ExecutionContext}
9
+ # classes resolve global configuration through this method.
10
+ #
11
+ # @example Read the configured binary path
12
+ # Git::Config.instance.binary_path #=> "git"
13
+ #
14
+ # @example Mutate the singleton (same as Git.configure { |c| ... })
15
+ # Git::Config.instance.binary_path = '/usr/local/bin/git'
16
+ #
17
+ # @return [Git::Config] the singleton config object
18
+ #
19
+ def self.instance
20
+ @instance ||= new
21
+ end
22
+
6
23
  attr_writer :binary_path, :git_ssh, :timeout
7
24
 
8
25
  def initialize
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents a single Git configuration entry
5
+ #
6
+ # Returned by {Git::Configuring} read operations such as {Git::Configuring#config_get},
7
+ # {Git::Configuring#config_get_all}, {Git::Configuring#config_list}, and their
8
+ # related methods.
9
+ #
10
+ # @example Create a ConfigEntryInfo
11
+ # entry = Git::ConfigEntryInfo.new(
12
+ # scope: 'local',
13
+ # origin: 'file:.git/config',
14
+ # key: 'remote.origin.url',
15
+ # value: 'https://github.com/ruby-git/ruby-git'
16
+ # )
17
+ # entry.section # => "remote"
18
+ # entry.subsection # => "origin"
19
+ # entry.variable # => "url"
20
+ #
21
+ # @api public
22
+ #
23
+ # @!attribute [r] scope
24
+ #
25
+ # The scope of the configuration entry
26
+ #
27
+ # May be one of `"system"`, `"global"`, `"local"`, `"worktree"`, `"command"`,
28
+ # `"file"`, or `"blob"`. The `"command"` scope is used for values supplied
29
+ # via the command line (including default values from `--default`).
30
+ #
31
+ # @return [String] the config scope string (e.g. `"local"`, `"global"`)
32
+ #
33
+ # @!attribute [r] origin
34
+ #
35
+ # Where the configuration entry originates
36
+ #
37
+ # The origin is in the format `<origin-type>:<actual-origin>` and is never
38
+ # blank. The origin type prefix is one of `file:`, `blob:`, `command line:`,
39
+ # or `standard input:`.
40
+ #
41
+ # `nil` when the git command used to retrieve this entry does not support
42
+ # `--show-origin` (currently only `--get-urlmatch`).
43
+ #
44
+ # @return [String, nil] the origin path in the format `<type>:<path>`, or `nil`
45
+ #
46
+ # @!attribute [r] key
47
+ #
48
+ # The full dotted key name of the configuration entry (e.g., `remote.origin.url`)
49
+ #
50
+ # @return [String] the full dotted key name (e.g. `remote.origin.url`)
51
+ #
52
+ # @!attribute [r] value
53
+ #
54
+ # The value of the configuration entry
55
+ #
56
+ # @return [String] the string value of this entry
57
+ #
58
+ ConfigEntryInfo = Data.define(:scope, :origin, :key, :value) do
59
+ # Returns the section component of the key (everything before the first dot)
60
+ #
61
+ # Returns an empty string when the key contains no dot.
62
+ #
63
+ # @example Section component of a dotted key
64
+ # entry.section # => "remote"
65
+ #
66
+ # @return [String] the section name, or an empty string when the key has no dot
67
+ #
68
+ def section = first_dot ? key[0...first_dot] : ''
69
+
70
+ # Returns the subsection component of the key (everything between the first and last dot)
71
+ #
72
+ # Returns an empty string when the key has zero or one dot (no subsection).
73
+ #
74
+ # @example Subsection component of a dotted key
75
+ # entry.subsection # => "origin"
76
+ #
77
+ # @return [String] the subsection name, or an empty string when there is no subsection
78
+ #
79
+ def subsection = first_dot && first_dot != last_dot ? key[(first_dot + 1)...last_dot] : ''
80
+
81
+ # Returns the variable component of the key (everything after the last dot)
82
+ #
83
+ # Returns the full key when the key contains no dot.
84
+ #
85
+ # @example Variable component of a dotted key
86
+ # entry.variable # => "url"
87
+ #
88
+ # @return [String] the variable name (everything after the last dot)
89
+ #
90
+ def variable = last_dot ? key[(last_dot + 1)..] : key
91
+
92
+ private
93
+
94
+ # Returns the index of the first dot in the key, or nil if none exists
95
+ #
96
+ # @return [Integer, nil] the zero-based index of the first dot, or `nil`
97
+ #
98
+ def first_dot = key.index('.')
99
+
100
+ # Returns the index of the last dot in the key, or nil if none exists
101
+ #
102
+ # @return [Integer, nil] the zero-based index of the last dot, or `nil`
103
+ #
104
+ def last_dot = key.rindex('.')
105
+ end
106
+ end