git 4.0.7 → 4.1.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "4.0.7"
2
+ ".": "4.1.0"
3
3
  }
data/.rubocop_todo.yml CHANGED
@@ -9,4 +9,4 @@
9
9
  # Offense count: 2
10
10
  # Configuration parameters: CountComments, CountAsOne.
11
11
  Metrics/ClassLength:
12
- Max: 1039
12
+ Max: 1150
data/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@
5
5
 
6
6
  # Change Log
7
7
 
8
+ ## [4.1.0](https://github.com/ruby-git/ruby-git/compare/v4.0.7...v4.1.0) (2026-01-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add per-instance git_ssh configuration support ([26c1199](https://github.com/ruby-git/ruby-git/commit/26c119969ec71c23c965f55f0570471f8ddf333a))
14
+ * **clone:** Add single_branch option ([a6929bb](https://github.com/ruby-git/ruby-git/commit/a6929bb0bfd51cba3a595e47740897ca619da468))
15
+ * **diff:** Allow multiple paths in diff path limiter ([c663b62](https://github.com/ruby-git/ruby-git/commit/c663b62a0c9075a18c112e2cda3744f88f42ab7e))
16
+ * **remote:** Add remote set-branches helper ([a7dab2b](https://github.com/ruby-git/ruby-git/commit/a7dab2bdf9088f0610dfbf3e3b78677b90195f75))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * Prevent GIT_INDEX_FILE from corrupting worktree indexes ([27c0f16](https://github.com/ruby-git/ruby-git/commit/27c0f1629927ae23a5bb8efc4df79756a9e4406b))
22
+ * **test:** Use larger timeout values on JRuby to prevent flaky tests ([aa8fd8b](https://github.com/ruby-git/ruby-git/commit/aa8fd8b0435246f70579bfab3cde8d45bc23233a))
23
+
24
+
25
+ ### Other Changes
26
+
27
+ * Add git version support policy ([fbb0c60](https://github.com/ruby-git/ruby-git/commit/fbb0c60c56a01222133b61eb5267148773b4239c))
28
+ * **clone:** Simplify single_branch validator ([3900233](https://github.com/ruby-git/ruby-git/commit/39002330d42c4a2b3f0413ba920e6fd534880e03))
29
+ * Expand AI instructions with comprehensive workflows ([04907ed](https://github.com/ruby-git/ruby-git/commit/04907edd89dd716d85f190d828cbf6a0c43d47f6))
30
+ * Make env_overrides more flexible and idiomatic ([dc0b43b](https://github.com/ruby-git/ruby-git/commit/dc0b43bccbc9c57c445efc303a3e0f6a71cbd66f))
31
+
8
32
  ## [4.0.7](https://github.com/ruby-git/ruby-git/compare/v4.0.6...v4.0.7) (2025-12-29)
9
33
 
10
34
 
data/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - [Deprecations](#deprecations)
20
20
  - [Examples](#examples)
21
21
  - [Ruby version support policy](#ruby-version-support-policy)
22
+ - [Git version support policy](#git-version-support-policy)
22
23
  - [License](#license)
23
24
  - [📢 Project Announcements 📢](#-project-announcements-)
24
25
  - [2025-07-09: Architectural Redesign](#2025-07-09-architectural-redesign)
@@ -33,9 +34,9 @@ command line.
33
34
 
34
35
  Get started by obtaining a repository object by:
35
36
 
36
- * opening an existing working copy with [Git.open](https://rubydoc.info/gems/git/Git#open-class_method)
37
- * initializing a new repository with [Git.init](https://rubydoc.info/gems/git/Git#init-class_method)
38
- * cloning a repository with [Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method)
37
+ - opening an existing working copy with [Git.open](https://rubydoc.info/gems/git/Git#open-class_method)
38
+ - initializing a new repository with [Git.init](https://rubydoc.info/gems/git/Git#init-class_method)
39
+ - cloning a repository with [Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method)
39
40
 
40
41
  Methods that can be called on a repository object are documented in [Git::Base](https://rubydoc.info/gems/git/Git/Base)
41
42
 
@@ -223,6 +224,28 @@ end
223
224
 
224
225
  _NOTE: Another way to specify where is the `git` binary is through the environment variable `GIT_PATH`_
225
226
 
227
+ **How SSH configuration is determined:**
228
+
229
+ - If `git_ssh` is not specified in the API call, the global config (`Git.configure { |c| c.git_ssh = ... }`) is used.
230
+ - If `git_ssh: nil` is specified, SSH is disabled for that instance (no SSH key or script will be used).
231
+ - If `git_ssh` is a non-empty string, it is used for that instance (overriding the global config).
232
+
233
+ You can also specify a custom SSH script on a per-repository basis:
234
+
235
+ ```ruby
236
+ # Use a specific SSH key for a single repository
237
+ git = Git.open('/path/to/repo', git_ssh: 'ssh -i /path/to/private_key')
238
+
239
+ # Or when cloning
240
+ git = Git.clone('git@github.com:user/repo.git', 'local-dir',
241
+ git_ssh: 'ssh -i /path/to/private_key')
242
+
243
+ # Or when initializing
244
+ git = Git.init('new-repo', git_ssh: 'ssh -i /path/to/private_key')
245
+ ```
246
+
247
+ This is especially useful in multi-threaded applications where different repositories require different SSH credentials.
248
+
226
249
  Here are the operations that need read permission only.
227
250
 
228
251
  ```ruby
@@ -296,6 +319,7 @@ g.diff(commit1, commit2).stats
296
319
  g.diff(commit1, commit2).name_status
297
320
  g.gtree('v2.5').diff('v2.6').insertions
298
321
  g.diff('gitsearch1', 'v2.5').path('lib/')
322
+ g.diff('gitsearch1', 'v2.5').path('lib/', 'docs/', 'README.md') # multiple paths
299
323
  g.diff('gitsearch1', @git.gtree('v2.5'))
300
324
  g.diff('gitsearch1', 'v2.5').path('docs/').patch
301
325
  g.gtree('v2.5').diff('v2.6').patch
@@ -368,6 +392,9 @@ g.config('user.email', 'email@email.com')
368
392
  # Clone can take a filter to tell the serve to send a partial clone
369
393
  g = Git.clone(git_url, name, :path => path, :filter => 'tree:0')
370
394
 
395
+ # Clone can control single-branch behavior (nil default keeps current git behavior)
396
+ g = Git.clone(git_url, name, :path => path, :depth => 1, :single_branch => false)
397
+
371
398
  # Clone can take an optional logger
372
399
  logger = Logger.new
373
400
  g = Git.clone(git_url, NAME, :log => logger)
@@ -442,6 +469,9 @@ g.remote(name).remove
442
469
  g.remote(name).merge
443
470
  g.remote(name).merge(branch)
444
471
 
472
+ g.remote_set_branches('origin', '*', add: true) # append additional fetch refspecs
473
+ g.remote_set_branches('origin', 'feature', 'release/*') # replace fetch refspecs
474
+
445
475
  g.fetch
446
476
  g.fetch(g.remotes.first)
447
477
  g.fetch('origin', {:ref => 'some/ref/head'} )
@@ -536,6 +566,25 @@ once the following JRuby bug is fixed:
536
566
 
537
567
  jruby/jruby#7515
538
568
 
569
+ ## Git version support policy
570
+
571
+ This gem requires git version 2.28.0 or greater as specified in the gemspec. This
572
+ requirement reflects:
573
+
574
+ - The minimum git version necessary to support all features provided by this gem
575
+ - A reasonable balance between supporting older systems and leveraging modern git
576
+ capabilities
577
+ - The practical limitations of testing across multiple git versions in CI
578
+
579
+ Git 2.28.0 was released on July 27, 2020. While this gem may work with earlier
580
+ versions of git, compatibility with versions prior to 2.28.0 is not tested or
581
+ guaranteed. Users on older git versions should upgrade to at least 2.28.0.
582
+
583
+ The supported git version may be increased in future major or minor releases of this
584
+ gem as new git features are adopted or as maintaining backward compatibility becomes
585
+ impractical. Such changes will be clearly documented in the CHANGELOG and release
586
+ notes.
587
+
539
588
  ## License
540
589
 
541
590
  Licensed under MIT License Copyright (c) 2008 Scott Chacon. See LICENSE for further
data/lib/git/base.rb CHANGED
@@ -21,7 +21,9 @@ module Git
21
21
 
22
22
  # (see Git.clone)
23
23
  def self.clone(repository_url, directory, options = {})
24
- new_options = Git::Lib.new(nil, options[:log]).clone(repository_url, directory, options)
24
+ lib_options = {}
25
+ lib_options[:git_ssh] = options[:git_ssh] if options.key?(:git_ssh)
26
+ new_options = Git::Lib.new(lib_options, options[:log]).clone(repository_url, directory, options)
25
27
  normalize_paths(new_options, bare: options[:bare] || options[:mirror])
26
28
  new(new_options)
27
29
  end
@@ -148,12 +150,20 @@ module Git
148
150
  # commands are logged at the `:info` level. Additional logging is done
149
151
  # at the `:debug` level.
150
152
  #
153
+ # @option options [String, nil] :git_ssh Path to a custom SSH executable or script.
154
+ # Controls how SSH is configured for this {Git::Base} instance:
155
+ # - If this option is not provided, the global Git::Base.config.git_ssh setting is used.
156
+ # - If this option is explicitly set to nil, SSH is disabled for this instance.
157
+ # - If this option is a non-empty String, that value is used as the SSH command for
158
+ # this instance, overriding the global Git::Base.config.git_ssh setting.
159
+ #
151
160
  # @return [Git::Base] an object that can execute git commands in the context
152
161
  # of the opened working copy or bare repository
153
162
  #
154
163
  def initialize(options = {})
155
164
  options = default_paths(options)
156
165
  setup_logger(options[:log])
166
+ @git_ssh = options.key?(:git_ssh) ? options[:git_ssh] : :use_global_config
157
167
  initialize_components(options)
158
168
  end
159
169
 
@@ -328,6 +338,17 @@ module Git
328
338
  @lib ||= Git::Lib.new(self, @logger)
329
339
  end
330
340
 
341
+ # Returns the per-instance git_ssh configuration value.
342
+ #
343
+ # This may be:
344
+ # * a [String] path when an explicit git_ssh command has been configured
345
+ # * the Symbol `:use_global_config` when this instance is using the global config
346
+ # * `nil` when SSH has been explicitly disabled for this instance
347
+ #
348
+ # @return [String, Symbol, nil] the git_ssh configuration value for this instance
349
+ # @api private
350
+ attr_reader :git_ssh
351
+
331
352
  # Run a grep for 'string' on the HEAD of the git repository
332
353
  #
333
354
  # @example Limit grep's scope by calling grep() from a specific object:
@@ -342,7 +363,8 @@ module Git
342
363
  # end
343
364
  #
344
365
  # @param string [String] the string to search for
345
- # @param path_limiter [String, Array] a path or array of paths to limit the search to or nil for no limit
366
+ # @param path_limiter [String, Pathname, Array<String, Pathname>] a path or array
367
+ # of paths to limit the search to or nil for no limit
346
368
  # @param opts [Hash] options to pass to the underlying `git grep` command
347
369
  #
348
370
  # @option opts [Boolean] :ignore_case (false) ignore case when matching
@@ -508,8 +530,8 @@ module Git
508
530
  # @param branch [String] the branch to pull from
509
531
  # @param opts [Hash] options to pass to the pull command
510
532
  #
511
- # @option opts [Boolean] :allow_unrelated_histories (false) Merges histories of two projects that started their
512
- # lives independently
533
+ # @option opts [Boolean] :allow_unrelated_histories (false) Merges histories of
534
+ # two projects that started their lives independently
513
535
  # @example pulls from origin/master
514
536
  # @git.pull
515
537
  # @example pulls from upstream/master
@@ -541,6 +563,43 @@ module Git
541
563
  Git::Remote.new(self, name)
542
564
  end
543
565
 
566
+ # Configures which branches are fetched for a remote
567
+ #
568
+ # Uses `git remote set-branches` to set or append fetch refspecs. When the `add:`
569
+ # option is not given, the `--add` option is not passed to the git command
570
+ #
571
+ # @example Replace fetched branches with a single glob pattern
572
+ # git = Git.open('/path/to/repo')
573
+ # # Only fetch branches matching "feature/*" from origin
574
+ # git.remote_set_branches('origin', 'feature/*')
575
+ #
576
+ # @example Append a glob pattern to existing fetched branches
577
+ # git = Git.open('/path/to/repo')
578
+ # # Keep existing fetch refspecs and add all release branches
579
+ # git.remote_set_branches('origin', 'release/*', add: true)
580
+ #
581
+ # @example Configure multiple explicit branches
582
+ # git = Git.open('/path/to/repo')
583
+ # git.remote_set_branches('origin', 'main', 'development', 'hotfix')
584
+ #
585
+ # @param name [String] the remote name (for example, "origin")
586
+ # @param branches [Array<String>] branch names or globs (for example, '*')
587
+ # @param add [Boolean] when true, append to existing refspecs instead of replacing them
588
+ #
589
+ # @return [nil]
590
+ #
591
+ # @raise [ArgumentError] if no branches are provided @raise [Git::FailedError] if
592
+ # the underlying git command fails
593
+ #
594
+ def remote_set_branches(name, *branches, add: false)
595
+ branch_list = branches.flatten
596
+ raise ArgumentError, 'branches are required' if branch_list.empty?
597
+
598
+ lib.remote_set_branches(name, branch_list, add: add)
599
+
600
+ nil
601
+ end
602
+
544
603
  # removes a remote from this repository
545
604
  #
546
605
  # @git.remove_remote('scott_git')
@@ -812,18 +871,32 @@ module Git
812
871
  #
813
872
  # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
814
873
  # @param obj2 [String, nil] The second commit or object to compare.
815
- # @return [Git::Diff::Stats]
816
- def diff_stats(objectish = 'HEAD', obj2 = nil)
817
- Git::DiffStats.new(self, objectish, obj2)
874
+ # @param opts [Hash] Options to filter the diff.
875
+ # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit stats to specified path(s).
876
+ # @return [Git::DiffStats]
877
+ def diff_stats(objectish = 'HEAD', obj2 = nil, opts = {})
878
+ Git::DiffStats.new(self, objectish, obj2, opts[:path_limiter])
818
879
  end
819
880
 
820
881
  # Returns a Git::Diff::PathStatus object for accessing the name-status report.
821
882
  #
822
883
  # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
823
884
  # @param obj2 [String, nil] The second commit or object to compare.
824
- # @return [Git::Diff::PathStatus]
825
- def diff_path_status(objectish = 'HEAD', obj2 = nil)
826
- Git::DiffPathStatus.new(self, objectish, obj2)
885
+ # @param opts [Hash] Options to filter the diff.
886
+ # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit status to specified path(s).
887
+ # @option opts [String, Pathname, Array<String, Pathname>] :path (deprecated) Legacy alias for :path_limiter.
888
+ # @return [Git::DiffPathStatus]
889
+ def diff_path_status(objectish = 'HEAD', obj2 = nil, opts = {})
890
+ path_limiter = if opts.key?(:path_limiter)
891
+ opts[:path_limiter]
892
+ elsif opts.key?(:path)
893
+ Git::Deprecation.warn(
894
+ 'Git::Base#diff_path_status :path option is deprecated. Use :path_limiter instead.'
895
+ )
896
+ opts[:path]
897
+ end
898
+
899
+ Git::DiffPathStatus.new(self, objectish, obj2, path_limiter)
827
900
  end
828
901
 
829
902
  # Provided for backwards compatibility
data/lib/git/diff.rb CHANGED
@@ -18,8 +18,38 @@ module Git
18
18
  end
19
19
  attr_reader :from, :to
20
20
 
21
- def path(path)
22
- @path = path
21
+ # Limits the diff to the specified path(s)
22
+ #
23
+ # When called with no arguments (or only nil arguments), removes any existing
24
+ # path filter, showing all files in the diff. Internally stores a single path
25
+ # as a String and multiple paths as an Array for efficiency.
26
+ #
27
+ # @example Limit diff to a single path
28
+ # git.diff('HEAD~3', 'HEAD').path('lib/')
29
+ #
30
+ # @example Limit diff to multiple paths
31
+ # git.diff('HEAD~3', 'HEAD').path('src/', 'docs/', 'README.md')
32
+ #
33
+ # @example Remove path filtering (show all files)
34
+ # diff.path # or diff.path(nil)
35
+ #
36
+ # @param paths [String, Pathname] one or more paths to filter the diff. Pass no arguments to remove filtering.
37
+ # @return [self] returns self for method chaining
38
+ # @raise [ArgumentError] if any path is an Array (use splatted arguments instead)
39
+ #
40
+ def path(*paths)
41
+ validate_paths_not_arrays(paths)
42
+
43
+ cleaned_paths = paths.compact
44
+
45
+ @path = if cleaned_paths.empty?
46
+ nil
47
+ elsif cleaned_paths.length == 1
48
+ cleaned_paths.first
49
+ else
50
+ cleaned_paths
51
+ end
52
+
23
53
  self
24
54
  end
25
55
 
@@ -102,6 +132,14 @@ module Git
102
132
 
103
133
  private
104
134
 
135
+ def validate_paths_not_arrays(paths)
136
+ return unless paths.any? { |p| p.is_a?(Array) }
137
+
138
+ raise ArgumentError,
139
+ 'path expects individual arguments, not arrays. ' \
140
+ "Use path('lib/', 'docs/') not path(['lib/', 'docs/'])"
141
+ end
142
+
105
143
  def process_full
106
144
  return if @full_diff_files
107
145
 
@@ -39,7 +39,7 @@ module Git
39
39
  # Lazily fetches and caches the path status from the git lib.
40
40
  def fetch_path_status
41
41
  @fetch_path_status ||= @base.lib.diff_path_status(
42
- @from, @to, { path: @path_limiter }
42
+ @from, @to, { path_limiter: @path_limiter }
43
43
  )
44
44
  end
45
45
  end