git 4.0.6 → 4.3.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.
data/git.gemspec CHANGED
@@ -44,6 +44,7 @@ Gem::Specification.new do |spec|
44
44
  spec.add_development_dependency 'test-unit', '~> 3.6'
45
45
 
46
46
  unless RUBY_PLATFORM == 'java'
47
+ spec.add_development_dependency 'irb', '~> 1.6'
47
48
  spec.add_development_dependency 'redcarpet', '~> 3.6'
48
49
  spec.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28'
49
50
  spec.add_development_dependency 'yardstick', '~> 0.9'
@@ -12,6 +12,14 @@ module Git
12
12
  ARG_BUILDERS = {
13
13
  boolean: ->(config, value) { value ? config[:flag] : [] },
14
14
 
15
+ boolean_negatable: lambda do |config, value|
16
+ case value
17
+ when true then config[:flag]
18
+ when false then config[:flag].sub('--', '--no-')
19
+ else []
20
+ end
21
+ end,
22
+
15
23
  valued_equals: ->(config, value) { "#{config[:flag]}=#{value}" if value },
16
24
 
17
25
  valued_space: ->(config, value) { [config[:flag], value.to_s] if value },
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
 
@@ -185,7 +195,7 @@ module Git
185
195
  # :fetch => true
186
196
  # :track => <branch_name>
187
197
  def add_remote(name, url, opts = {})
188
- url = url.repo.path if url.is_a?(Git::Base)
198
+ url = url.repo.to_s if url.is_a?(Git::Base)
189
199
  lib.remote_add(name, url, opts)
190
200
  Git::Remote.new(self, name)
191
201
  end
@@ -200,8 +210,8 @@ module Git
200
210
  # @git.commit('message')
201
211
  # end
202
212
  def chdir # :yields: the Git::Path
203
- Dir.chdir(dir.path) do
204
- yield dir.path
213
+ Dir.chdir(dir.to_s) do
214
+ yield dir.to_s
205
215
  end
206
216
  end
207
217
 
@@ -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
@@ -536,11 +558,48 @@ module Git
536
558
  # @git.set_remote_url('scotts_git', 'git://repo.or.cz/rubygit.git')
537
559
  #
538
560
  def set_remote_url(name, url)
539
- url = url.repo.path if url.is_a?(Git::Base)
561
+ url = url.repo.to_s if url.is_a?(Git::Base)
540
562
  lib.remote_set_url(name, url)
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')
@@ -595,6 +654,57 @@ module Git
595
654
  lib.gc
596
655
  end
597
656
 
657
+ # Verifies the connectivity and validity of objects in the database
658
+ #
659
+ # Runs `git fsck` to check repository integrity and identify dangling,
660
+ # missing, or unreachable objects.
661
+ #
662
+ # @overload fsck(objects = [], options = {})
663
+ # @param objects [Array<String>] specific objects to treat as heads for unreachability trace.
664
+ # If no objects are given, git fsck defaults to using the index file, all SHA-1
665
+ # references in the refs namespace, and all reflogs.
666
+ # @param [Hash] options options to pass to the underlying `git fsck` command
667
+ #
668
+ # @option options [Boolean] :unreachable print unreachable objects
669
+ # @option options [Boolean] :strict enable strict checking
670
+ # @option options [Boolean] :connectivity_only check only connectivity (faster)
671
+ # @option options [Boolean] :root report root nodes
672
+ # @option options [Boolean] :tags report tags
673
+ # @option options [Boolean] :cache consider objects in the index
674
+ # @option options [Boolean] :no_reflogs do not consider reflogs
675
+ # @option options [Boolean] :lost_found write dangling objects to .git/lost-found
676
+ # (note: this modifies the repository by creating files)
677
+ # @option options [Boolean, nil] :dangling print dangling objects (true/false/nil for default)
678
+ # @option options [Boolean, nil] :full check objects in alternate pools (true/false/nil for default)
679
+ # @option options [Boolean, nil] :name_objects name objects by refs (true/false/nil for default)
680
+ # @option options [Boolean, nil] :references check refs database consistency (true/false/nil for default)
681
+ #
682
+ # @return [Git::FsckResult] categorized objects flagged by fsck
683
+ #
684
+ # @example Check repository integrity
685
+ # result = git.fsck
686
+ # result.dangling.each { |obj| puts "#{obj.type}: #{obj.sha}" }
687
+ #
688
+ # @example Check with strict mode and suppress dangling output
689
+ # result = git.fsck(strict: true, dangling: false)
690
+ #
691
+ # @example Check if repository has any issues
692
+ # result = git.fsck
693
+ # puts "Repository is clean" if result.empty?
694
+ #
695
+ # @example List root commits
696
+ # result = git.fsck(root: true)
697
+ # result.root.each { |obj| puts obj.sha }
698
+ #
699
+ # @example Check specific objects
700
+ # result = git.fsck('abc1234', 'def5678')
701
+ #
702
+ # rubocop:disable Style/ArgumentsForwarding
703
+ def fsck(*objects, **opts)
704
+ lib.fsck(*objects, **opts)
705
+ end
706
+ # rubocop:enable Style/ArgumentsForwarding
707
+
598
708
  def apply(file)
599
709
  return unless File.exist?(file)
600
710
 
@@ -812,18 +922,32 @@ module Git
812
922
  #
813
923
  # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
814
924
  # @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)
925
+ # @param opts [Hash] Options to filter the diff.
926
+ # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit stats to specified path(s).
927
+ # @return [Git::DiffStats]
928
+ def diff_stats(objectish = 'HEAD', obj2 = nil, opts = {})
929
+ Git::DiffStats.new(self, objectish, obj2, opts[:path_limiter])
818
930
  end
819
931
 
820
932
  # Returns a Git::Diff::PathStatus object for accessing the name-status report.
821
933
  #
822
934
  # @param objectish [String] The first commit or object to compare. Defaults to 'HEAD'.
823
935
  # @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)
936
+ # @param opts [Hash] Options to filter the diff.
937
+ # @option opts [String, Pathname, Array<String, Pathname>] :path_limiter Limit status to specified path(s).
938
+ # @option opts [String, Pathname, Array<String, Pathname>] :path (deprecated) Legacy alias for :path_limiter.
939
+ # @return [Git::DiffPathStatus]
940
+ def diff_path_status(objectish = 'HEAD', obj2 = nil, opts = {})
941
+ path_limiter = if opts.key?(:path_limiter)
942
+ opts[:path_limiter]
943
+ elsif opts.key?(:path)
944
+ Git::Deprecation.warn(
945
+ 'Git::Base#diff_path_status :path option is deprecated. Use :path_limiter instead.'
946
+ )
947
+ opts[:path]
948
+ end
949
+
950
+ Git::DiffPathStatus.new(self, objectish, obj2, path_limiter)
827
951
  end
828
952
 
829
953
  # 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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents an object returned by `git fsck`
5
+ #
6
+ # This class provides information about dangling, missing, unreachable, or
7
+ # problematic Git objects found during repository integrity checks.
8
+ #
9
+ # @api public
10
+ #
11
+ class FsckObject
12
+ # The type of the Git object
13
+ # @return [Symbol] one of :commit, :tree, :blob, or :tag
14
+ attr_reader :type
15
+
16
+ # The SHA-1 hash of the object
17
+ # @return [String] the 40-character SHA-1 hash
18
+ attr_reader :sha
19
+
20
+ # A warning or error message associated with this object
21
+ # @return [String, nil] the message, or nil if no message
22
+ attr_reader :message
23
+
24
+ # A name describing how the object is reachable (from --name-objects)
25
+ # @return [String, nil] the name, or nil if not provided
26
+ attr_reader :name
27
+
28
+ # Create a new FsckObject
29
+ #
30
+ # @param type [Symbol] the object type (:commit, :tree, :blob, or :tag)
31
+ # @param sha [String] the 40-character SHA-1 hash
32
+ # @param message [String, nil] optional warning/error message
33
+ # @param name [String, nil] optional name from --name-objects (e.g., "HEAD~2^2:src/")
34
+ #
35
+ def initialize(type:, sha:, message: nil, name: nil)
36
+ @type = type
37
+ @sha = sha
38
+ @message = message
39
+ @name = name
40
+ end
41
+
42
+ # Returns the SHA as the string representation
43
+ # @return [String] the SHA-1 hash
44
+ def to_s
45
+ sha
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents the result of running `git fsck`
5
+ #
6
+ # This class provides structured access to the objects found during a
7
+ # repository integrity check, categorized by their status.
8
+ #
9
+ # @api public
10
+ #
11
+ class FsckResult
12
+ # Objects not referenced by any other object
13
+ # @return [Array<Git::FsckObject>]
14
+ attr_reader :dangling
15
+
16
+ # Objects that are referenced but not present in the repository
17
+ # @return [Array<Git::FsckObject>]
18
+ attr_reader :missing
19
+
20
+ # Objects not reachable from any ref
21
+ # @return [Array<Git::FsckObject>]
22
+ attr_reader :unreachable
23
+
24
+ # Objects with warnings (each includes a message)
25
+ # @return [Array<Git::FsckObject>]
26
+ attr_reader :warnings
27
+
28
+ # Root nodes (commits with no parents) when --root is used
29
+ # @return [Array<Git::FsckObject>]
30
+ attr_reader :root
31
+
32
+ # Tagged objects when --tags is used
33
+ # @return [Array<Git::FsckObject>]
34
+ attr_reader :tagged
35
+
36
+ # rubocop:disable Metrics/ParameterLists
37
+
38
+ # Create a new FsckResult
39
+ #
40
+ # @param dangling [Array<Git::FsckObject>] dangling objects
41
+ # @param missing [Array<Git::FsckObject>] missing objects
42
+ # @param unreachable [Array<Git::FsckObject>] unreachable objects
43
+ # @param warnings [Array<Git::FsckObject>] objects with warnings
44
+ # @param root [Array<Git::FsckObject>] root nodes
45
+ # @param tagged [Array<Git::FsckObject>] tagged objects
46
+ #
47
+ def initialize(dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [])
48
+ @dangling = dangling
49
+ @missing = missing
50
+ @unreachable = unreachable
51
+ @warnings = warnings
52
+ @root = root
53
+ @tagged = tagged
54
+ end
55
+
56
+ # rubocop:enable Metrics/ParameterLists
57
+
58
+ # Returns true if any issues were found
59
+ #
60
+ # @return [Boolean]
61
+ #
62
+ # @example
63
+ # result = git.fsck
64
+ # puts "Repository has issues!" if result.any_issues?
65
+ #
66
+ def any_issues?
67
+ [dangling, missing, unreachable, warnings].any?(&:any?)
68
+ end
69
+
70
+ # Returns true if no issues were found
71
+ #
72
+ # @return [Boolean]
73
+ #
74
+ # @example
75
+ # result = git.fsck
76
+ # puts "Repository is clean" if result.empty?
77
+ #
78
+ def empty?
79
+ !any_issues?
80
+ end
81
+
82
+ # Returns all objects from all categories (excluding informational root/tagged)
83
+ #
84
+ # @return [Array<Git::FsckObject>]
85
+ #
86
+ # @example
87
+ # result = git.fsck
88
+ # result.all_objects.each { |obj| puts obj.sha }
89
+ #
90
+ def all_objects
91
+ dangling + missing + unreachable + warnings
92
+ end
93
+
94
+ # Returns the total number of issues found
95
+ #
96
+ # @return [Integer]
97
+ #
98
+ # @example
99
+ # result = git.fsck
100
+ # puts "Found #{result.count} issues"
101
+ #
102
+ def count
103
+ all_objects.size
104
+ end
105
+
106
+ # Returns a hash representation of the result
107
+ #
108
+ # @return [Hash{Symbol => Array<Git::FsckObject>}]
109
+ #
110
+ # @example
111
+ # result = git.fsck
112
+ # result.to_h # => { dangling: [...], missing: [...], ... }
113
+ #
114
+ def to_h
115
+ {
116
+ dangling: dangling, missing: missing, unreachable: unreachable,
117
+ warnings: warnings, root: root, tagged: tagged
118
+ }
119
+ end
120
+ end
121
+ end