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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +2733 -0
- data/.github/pull_request_template.md +12 -3
- data/.github/workflows/continuous_integration.yml +2 -2
- data/.github/workflows/enforce_conventional_commits.yml +1 -0
- data/.github/workflows/release.yml +2 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop_todo.yml +1 -1
- data/.yardopts +4 -0
- data/AI_POLICY.md +24 -0
- data/CHANGELOG.md +75 -0
- data/CODE_OF_CONDUCT.md +25 -0
- data/CONTRIBUTING.md +35 -2
- data/GOVERNANCE.md +106 -0
- data/MAINTAINERS.md +17 -4
- data/README.md +433 -315
- data/git.gemspec +1 -0
- data/lib/git/args_builder.rb +8 -0
- data/lib/git/base.rb +138 -14
- data/lib/git/diff.rb +40 -2
- data/lib/git/diff_path_status.rb +1 -1
- data/lib/git/fsck_object.rb +48 -0
- data/lib/git/fsck_result.rb +121 -0
- data/lib/git/lib.rb +269 -17
- data/lib/git/log.rb +6 -1
- data/lib/git/path.rb +7 -1
- data/lib/git/version.rb +1 -1
- data/lib/git.rb +41 -0
- data/redesign/index.md +34 -0
- metadata +25 -4
data/README.md
CHANGED
|
@@ -7,20 +7,30 @@
|
|
|
7
7
|
|
|
8
8
|
[](https://badge.fury.io/rb/git)
|
|
9
9
|
[](https://rubydoc.info/gems/git/)
|
|
10
|
-
[](https://rubydoc.info/gems/git/file/CHANGELOG.md)
|
|
12
|
+
[](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI)
|
|
14
|
+
[](https://conventionalcommits.org)
|
|
16
|
+
[](AI_POLICY.md)
|
|
13
17
|
|
|
14
18
|
- [Summary](#summary)
|
|
15
19
|
- [Install](#install)
|
|
16
|
-
- [
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [Examples](#examples)
|
|
22
|
+
- [Configuration](#configuration)
|
|
23
|
+
- [Read Operations](#read-operations)
|
|
24
|
+
- [Write Operations](#write-operations)
|
|
25
|
+
- [Index and Tree Operations](#index-and-tree-operations)
|
|
17
26
|
- [Errors Raised By This Gem](#errors-raised-by-this-gem)
|
|
18
27
|
- [Specifying And Handling Timeouts](#specifying-and-handling-timeouts)
|
|
19
28
|
- [Deprecations](#deprecations)
|
|
20
|
-
- [
|
|
21
|
-
- [Ruby
|
|
22
|
-
- [
|
|
29
|
+
- [Project Policies](#project-policies)
|
|
30
|
+
- [Ruby Version Support Policy](#ruby-version-support-policy)
|
|
31
|
+
- [Git Version Support Policy](#git-version-support-policy)
|
|
23
32
|
- [📢 Project Announcements 📢](#-project-announcements-)
|
|
33
|
+
- [2026-01-07: AI Policy Introduced](#2026-01-07-ai-policy-introduced)
|
|
24
34
|
- [2025-07-09: Architectural Redesign](#2025-07-09-architectural-redesign)
|
|
25
35
|
- [2025-07-07: We Now Use RuboCop](#2025-07-07-we-now-use-rubocop)
|
|
26
36
|
- [2025-06-06: Default Branch Rename](#2025-06-06-default-branch-rename)
|
|
@@ -33,11 +43,15 @@ command line.
|
|
|
33
43
|
|
|
34
44
|
Get started by obtaining a repository object by:
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
- opening an existing working copy with
|
|
47
|
+
[Git.open](https://rubydoc.info/gems/git/Git#open-class_method)
|
|
48
|
+
- initializing a new repository with
|
|
49
|
+
[Git.init](https://rubydoc.info/gems/git/Git#init-class_method)
|
|
50
|
+
- cloning a repository with
|
|
51
|
+
[Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method)
|
|
39
52
|
|
|
40
|
-
Methods that can be called on a repository object are documented in
|
|
53
|
+
Methods that can be called on a repository object are documented in
|
|
54
|
+
[Git::Base](https://rubydoc.info/gems/git/Git/Base)
|
|
41
55
|
|
|
42
56
|
## Install
|
|
43
57
|
|
|
@@ -65,199 +79,147 @@ to install version 1.x:
|
|
|
65
79
|
gem install git --version "~> 1.19"
|
|
66
80
|
```
|
|
67
81
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
**Git::Base** - The object returned from a `Git.open` or `Git.clone`. Most major actions are called from this object.
|
|
71
|
-
|
|
72
|
-
**Git::Object** - The base object for your tree, blob and commit objects, returned from `@git.gtree` or `@git.object` calls. the `Git::AbstractObject` will have most of the calls in common for all those objects.
|
|
82
|
+
## Quick Start
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
All functionality for this gem starts with the top-level
|
|
85
|
+
[`Git`](https://rubydoc.info/gems/git/Git) module. This module can be used to run
|
|
86
|
+
non-repo scoped `git` commands such as `config`.
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
`Git
|
|
78
|
-
|
|
88
|
+
The `Git` module also has factory methods such as `open`, `clone`, and `init` which
|
|
89
|
+
return a [`Git::Base`](https://rubydoc.info/gems/git/Git/Base) object. The
|
|
90
|
+
`Git::Base` object is used to run repo-specific `git` commands such as `add`,
|
|
91
|
+
`commit`, `push`, and `log`.
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
**Git::Remote**- A reference to a remote repository that is tracked by this repository.
|
|
83
|
-
|
|
84
|
-
**Git::Log** - An Enumerable object that references all the `Git::Object::Commit`
|
|
85
|
-
objects that encompass your log query, which can be constructed through methods on
|
|
86
|
-
the `Git::Log object`, like:
|
|
93
|
+
Clone, read status, and log:
|
|
87
94
|
|
|
88
95
|
```ruby
|
|
89
|
-
git
|
|
90
|
-
.max_count(:all)
|
|
91
|
-
.object('README.md')
|
|
92
|
-
.since('10 years ago')
|
|
93
|
-
.between('v1.0.7', 'HEAD')
|
|
94
|
-
.map { |commit| commit.sha }
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
A maximum of 30 commits are returned if `max_count` is not called. To get all commits
|
|
98
|
-
that match the log query, call `max_count(:all)`.
|
|
99
|
-
|
|
100
|
-
Note that `git.log.all` adds the `--all` option to the underlying `git log` command.
|
|
101
|
-
This asks for the logs of all refs (basically all commits reachable by HEAD,
|
|
102
|
-
branches, and tags). This does not control the maximum number of commits returned. To
|
|
103
|
-
control how many commits are returned, you should call `max_count`.
|
|
104
|
-
|
|
105
|
-
**Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`.
|
|
106
|
-
|
|
107
|
-
## Errors Raised By This Gem
|
|
96
|
+
require 'git'
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
repo = Git.clone('https://github.com/ruby-git/ruby-git.git', 'ruby-git')
|
|
99
|
+
repo.status.changed.each { |f| puts "changed: #{f.path}" }
|
|
100
|
+
repo.log(5).each { |c| puts c.message }
|
|
101
|
+
```
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
this gem unless you need more specific error handling.
|
|
103
|
+
Open an existing repo and commit:
|
|
114
104
|
|
|
115
105
|
```ruby
|
|
116
|
-
|
|
117
|
-
# some git operation
|
|
118
|
-
rescue Git::Error => e
|
|
119
|
-
puts "An error occurred: #{e.message}"
|
|
120
|
-
end
|
|
121
|
-
```
|
|
106
|
+
require 'git'
|
|
122
107
|
|
|
123
|
-
|
|
108
|
+
repo = Git.open('/path/to/repo')
|
|
109
|
+
repo.add(all: true)
|
|
110
|
+
repo.commit('chore: update files')
|
|
111
|
+
repo.push
|
|
112
|
+
```
|
|
124
113
|
|
|
125
|
-
|
|
114
|
+
Initialize a new repo and make the first commit:
|
|
126
115
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
A timeout for git command line operations can be set either globally or for specific
|
|
130
|
-
method calls that accept a `:timeout` parameter.
|
|
116
|
+
```ruby
|
|
117
|
+
require 'git'
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
119
|
+
repo = Git.init('my_project')
|
|
120
|
+
repo.add(all: true)
|
|
121
|
+
repo.commit('initial commit')
|
|
122
|
+
```
|
|
136
123
|
|
|
137
|
-
|
|
138
|
-
`Git::TimeoutError` is raised. This error derives from the `Git::SignaledError` and
|
|
139
|
-
`Git::Error`.
|
|
124
|
+
## Examples
|
|
140
125
|
|
|
141
|
-
|
|
126
|
+
Beyond the basics covered in Quick Start, these examples show the full range of
|
|
127
|
+
options and variations for each operation.
|
|
142
128
|
|
|
143
|
-
|
|
144
|
-
of this parameter will override the global timeout value. In this context, a value of
|
|
145
|
-
`nil` (which is usually the default) will use the global timeout value and a value of
|
|
146
|
-
`0` will turn off timeout enforcement for that method call no matter what the global
|
|
147
|
-
value is.
|
|
129
|
+
### Configuration
|
|
148
130
|
|
|
149
|
-
|
|
131
|
+
Configure the `git` command line:
|
|
150
132
|
|
|
151
133
|
```ruby
|
|
152
|
-
|
|
153
|
-
|
|
134
|
+
# Global config (in ~/.gitconfig)
|
|
135
|
+
settings = Git.global_config # returns a Hash
|
|
136
|
+
username = Git.global_config('user.email')
|
|
137
|
+
Git.global_config('user.email', 'user@example.com')
|
|
138
|
+
|
|
139
|
+
# Repository config
|
|
140
|
+
repo = Git.open('path/to/repo')
|
|
141
|
+
settings = repo.config # returns a Hash
|
|
142
|
+
username = repo.config('user.email')
|
|
143
|
+
repo.config('user.email', 'anotheruser@example.com')
|
|
154
144
|
```
|
|
155
145
|
|
|
156
|
-
|
|
157
|
-
`:timeout` parameter:
|
|
146
|
+
Configure the git gem:
|
|
158
147
|
|
|
159
148
|
```ruby
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
Git.clone(repo_url, timeout: 0) # Do not enforce a timeout
|
|
164
|
-
Git.clone(repo_url, timeout: 10.5) # Timeout after 10.5 seconds raising Git::SignaledError
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
If the command takes too long, a `Git::TimeoutError` will be raised:
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
begin
|
|
171
|
-
Git.clone(repo_url, timeout: 10)
|
|
172
|
-
rescue Git::TimeoutError => e
|
|
173
|
-
e.result.tap do |r|
|
|
174
|
-
r.class #=> Git::CommandLineResult
|
|
175
|
-
r.status #=> #<Process::Status: pid 62173 SIGKILL (signal 9)>
|
|
176
|
-
r.status.timeout? #=> true
|
|
177
|
-
r.git_cmd # The git command ran as an array of strings
|
|
178
|
-
r.stdout # The command's output to stdout until it was terminated
|
|
179
|
-
r.stderr # The command's output to stderr until it was terminated
|
|
180
|
-
end
|
|
149
|
+
Git.configure do |config|
|
|
150
|
+
config.binary_path = '/usr/local/bin/git'
|
|
151
|
+
config.git_ssh = 'ssh -i ~/.ssh/id_rsa'
|
|
181
152
|
end
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
## Deprecations
|
|
185
|
-
|
|
186
|
-
This gem uses ActiveSupport's deprecation mechanism to report deprecation warnings.
|
|
187
153
|
|
|
188
|
-
|
|
154
|
+
# or
|
|
189
155
|
|
|
190
|
-
|
|
191
|
-
Git
|
|
156
|
+
Git.config.binary_path = '/usr/local/bin/git'
|
|
157
|
+
Git.config.git_ssh = 'ssh -i ~/.ssh/id_rsa'
|
|
192
158
|
```
|
|
193
159
|
|
|
194
|
-
|
|
195
|
-
documentation](https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html)
|
|
196
|
-
for more details.
|
|
160
|
+
**How SSH configuration is determined:**
|
|
197
161
|
|
|
198
|
-
If
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Here are a bunch of examples of how to use the Ruby/Git package.
|
|
162
|
+
- If `git_ssh` is not specified in the API call, the global config (`Git.configure {
|
|
163
|
+
|c| c.git_ssh = ... }`) is used.
|
|
164
|
+
- If `git_ssh: nil` is specified, SSH is disabled for that instance (no SSH key or
|
|
165
|
+
script will be used).
|
|
166
|
+
- If `git_ssh` is a non-empty string, it is used for that instance (overriding the
|
|
167
|
+
global config).
|
|
205
168
|
|
|
206
|
-
|
|
169
|
+
You can also specify a custom SSH script on a per-repository basis:
|
|
207
170
|
|
|
208
171
|
```ruby
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Git env config
|
|
172
|
+
# Use a specific SSH key for a single repository
|
|
173
|
+
git = Git.open('/path/to/repo', git_ssh: 'ssh -i /path/to/private_key')
|
|
213
174
|
|
|
214
|
-
|
|
215
|
-
Git.
|
|
216
|
-
|
|
217
|
-
config.binary_path = '/git/bin/path'
|
|
175
|
+
# Or when cloning
|
|
176
|
+
git = Git.clone('git@github.com:user/repo.git', 'local-dir',
|
|
177
|
+
git_ssh: 'ssh -i /path/to/private_key')
|
|
218
178
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
end
|
|
179
|
+
# Or when initializing
|
|
180
|
+
git = Git.init('new-repo', git_ssh: 'ssh -i /path/to/private_key')
|
|
222
181
|
```
|
|
223
182
|
|
|
224
|
-
|
|
183
|
+
This is especially useful in multi-threaded applications where different repositories
|
|
184
|
+
require different SSH credentials.
|
|
185
|
+
|
|
186
|
+
### Read Operations
|
|
225
187
|
|
|
226
|
-
Here are the operations that need read permission only
|
|
188
|
+
Here are the operations that need read permission only:
|
|
227
189
|
|
|
228
190
|
```ruby
|
|
229
|
-
|
|
191
|
+
repo = Git.open(working_dir, :log => Logger.new(STDOUT))
|
|
230
192
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
193
|
+
repo.index
|
|
194
|
+
repo.index.readable?
|
|
195
|
+
repo.index.writable?
|
|
196
|
+
repo.repo
|
|
197
|
+
repo.dir
|
|
236
198
|
|
|
237
199
|
# ls-tree with recursion into subtrees (list files)
|
|
238
|
-
|
|
200
|
+
repo.ls_tree("HEAD", recursive: true)
|
|
239
201
|
|
|
240
202
|
# log - returns a Git::Log object, which is an Enumerator of Git::Commit objects
|
|
241
203
|
# default configuration returns a max of 30 commits
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
204
|
+
repo.log
|
|
205
|
+
repo.log(200) # 200 most recent commits
|
|
206
|
+
repo.log.since('2 weeks ago') # default count of commits since 2 weeks ago.
|
|
207
|
+
repo.log(200).since('2 weeks ago') # commits since 2 weeks ago, limited to 200.
|
|
208
|
+
repo.log.between('v2.5', 'v2.6')
|
|
209
|
+
repo.log.each {|l| puts l.sha }
|
|
210
|
+
repo.gblob('v2.5:Makefile').log.since('2 weeks ago')
|
|
249
211
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
212
|
+
repo.object('HEAD^').to_s # git show / git rev-parse
|
|
213
|
+
repo.object('HEAD^').contents
|
|
214
|
+
repo.object('v2.5:Makefile').size
|
|
215
|
+
repo.object('v2.5:Makefile').sha
|
|
254
216
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
217
|
+
repo.gtree(treeish)
|
|
218
|
+
repo.gblob(treeish)
|
|
219
|
+
repo.gcommit(treeish)
|
|
258
220
|
|
|
259
221
|
|
|
260
|
-
commit =
|
|
222
|
+
commit = repo.gcommit('1cc8667014381')
|
|
261
223
|
|
|
262
224
|
commit.gtree
|
|
263
225
|
commit.parent.sha
|
|
@@ -269,59 +231,74 @@ commit.committer.name
|
|
|
269
231
|
commit.date.strftime("%m-%d-%y")
|
|
270
232
|
commit.message
|
|
271
233
|
|
|
272
|
-
tree =
|
|
234
|
+
tree = repo.gtree("HEAD^{tree}")
|
|
273
235
|
|
|
274
236
|
tree.blobs
|
|
275
237
|
tree.subtrees
|
|
276
238
|
tree.children # blobs and subtrees
|
|
277
239
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
240
|
+
repo.rev_parse('v2.0.0:README.md')
|
|
241
|
+
|
|
242
|
+
repo.branches # returns Git::Branch objects
|
|
243
|
+
repo.branches.local
|
|
244
|
+
repo.current_branch
|
|
245
|
+
repo.branches.remote
|
|
246
|
+
repo.branches[:main].gcommit
|
|
247
|
+
repo.branches['origin/main'].gcommit
|
|
248
|
+
|
|
249
|
+
repo.grep('hello') # implies HEAD
|
|
250
|
+
repo.blob('v2.5:Makefile').grep('hello')
|
|
251
|
+
repo.tag('v2.5').grep('hello', 'docs/')
|
|
252
|
+
repo.describe()
|
|
253
|
+
repo.describe('0djf2aa')
|
|
254
|
+
repo.describe('HEAD', {:all => true, :tags => true})
|
|
255
|
+
|
|
256
|
+
repo.diff(commit1, commit2).size
|
|
257
|
+
repo.diff(commit1, commit2).stats
|
|
258
|
+
repo.diff(commit1, commit2).name_status
|
|
259
|
+
repo.gtree('v2.5').diff('v2.6').insertions
|
|
260
|
+
repo.diff('gitsearch1', 'v2.5').path('lib/')
|
|
261
|
+
repo.diff('gitsearch1', 'v2.5').path('lib/', 'docs/', 'README.md') # multiple paths
|
|
262
|
+
repo.diff('gitsearch1', repo.gtree('v2.5'))
|
|
263
|
+
repo.diff('gitsearch1', 'v2.5').path('docs/').patch
|
|
264
|
+
repo.gtree('v2.5').diff('v2.6').patch
|
|
265
|
+
|
|
266
|
+
repo.gtree('v2.5').diff('v2.6').each do |file_diff|
|
|
304
267
|
puts file_diff.path
|
|
305
268
|
puts file_diff.patch
|
|
306
269
|
puts file_diff.blob(:src).contents
|
|
307
270
|
end
|
|
308
271
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
272
|
+
repo.worktrees # returns Git::Worktree objects
|
|
273
|
+
repo.worktrees.count
|
|
274
|
+
repo.worktrees.each do |worktree|
|
|
312
275
|
worktree.dir
|
|
313
276
|
worktree.gcommit
|
|
314
277
|
worktree.to_s
|
|
315
278
|
end
|
|
316
279
|
|
|
317
|
-
|
|
318
|
-
|
|
280
|
+
# Check repository integrity with fsck
|
|
281
|
+
result = repo.fsck
|
|
282
|
+
result.dangling.each { |obj| puts "dangling #{obj.type}: #{obj.sha}" }
|
|
283
|
+
result.missing.each { |obj| puts "missing #{obj.type}: #{obj.sha}" }
|
|
284
|
+
|
|
285
|
+
# Check if repository has any issues
|
|
286
|
+
puts "Repository is clean" if result.empty?
|
|
287
|
+
|
|
288
|
+
# fsck with options
|
|
289
|
+
result = repo.fsck(unreachable: true, strict: true)
|
|
290
|
+
|
|
291
|
+
# Suppress dangling object output
|
|
292
|
+
result = repo.fsck(dangling: false)
|
|
293
|
+
|
|
294
|
+
repo.config('user.name') # returns 'Scott Chacon'
|
|
295
|
+
repo.config # returns whole config hash
|
|
319
296
|
|
|
320
297
|
# Configuration can be set when cloning using the :config option.
|
|
321
298
|
# This option can be an single configuration String or an Array
|
|
322
299
|
# if multiple config items need to be set.
|
|
323
300
|
#
|
|
324
|
-
|
|
301
|
+
repo = Git.clone(
|
|
325
302
|
git_uri, destination_path,
|
|
326
303
|
:config => [
|
|
327
304
|
'core.sshCommand=ssh -i /home/user/.ssh/id_rsa',
|
|
@@ -329,11 +306,11 @@ g = Git.clone(
|
|
|
329
306
|
]
|
|
330
307
|
)
|
|
331
308
|
|
|
332
|
-
|
|
309
|
+
repo.tags # returns array of Git::Tag objects
|
|
333
310
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
311
|
+
repo.show()
|
|
312
|
+
repo.show('HEAD')
|
|
313
|
+
repo.show('v2.8', 'README.md')
|
|
337
314
|
|
|
338
315
|
Git.ls_remote('https://github.com/ruby-git/ruby-git.git') # returns a hash containing the available references of the repo.
|
|
339
316
|
Git.ls_remote('/path/to/local/repo')
|
|
@@ -342,207 +319,347 @@ Git.ls_remote() # same as Git.ls_remote('.')
|
|
|
342
319
|
Git.default_branch('https://github.com/ruby-git/ruby-git') #=> 'main'
|
|
343
320
|
```
|
|
344
321
|
|
|
322
|
+
### Write Operations
|
|
323
|
+
|
|
345
324
|
And here are the operations that will need to write to your git repository.
|
|
346
325
|
|
|
347
326
|
```ruby
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
327
|
+
repo = Git.init # default is the current directory
|
|
328
|
+
repo = Git.init('project')
|
|
329
|
+
repo = Git.init(
|
|
330
|
+
'/home/schacon/proj',
|
|
331
|
+
{ :repository => '/opt/git/proj.git', :index => '/tmp/index'}
|
|
332
|
+
)
|
|
353
333
|
|
|
354
334
|
# Clone from a git url
|
|
355
335
|
git_url = 'https://github.com/ruby-git/ruby-git.git'
|
|
356
|
-
|
|
357
|
-
g = Git.clone(git_url)
|
|
336
|
+
repo = Git.clone(git_url)
|
|
358
337
|
|
|
359
338
|
# Clone into /tmp/clone/ruby-git-clean
|
|
360
339
|
name = 'ruby-git-clean'
|
|
361
340
|
path = '/tmp/clone'
|
|
362
|
-
|
|
363
|
-
|
|
341
|
+
repo = Git.clone(git_url, name, :path => path)
|
|
342
|
+
repo.dir #=> /tmp/clone/ruby-git-clean
|
|
364
343
|
|
|
365
|
-
|
|
366
|
-
|
|
344
|
+
repo.config('user.name', 'Scott Chacon')
|
|
345
|
+
repo.config('user.email', 'email@email.com')
|
|
367
346
|
|
|
368
347
|
# Clone can take a filter to tell the serve to send a partial clone
|
|
369
|
-
|
|
348
|
+
repo = Git.clone(git_url, name, :path => path, :filter => 'tree:0')
|
|
349
|
+
|
|
350
|
+
# Clone can control single-branch behavior (nil default keeps current git behavior)
|
|
351
|
+
repo = Git.clone(git_url, name, :path => path, :depth => 1, :single_branch => false)
|
|
370
352
|
|
|
371
353
|
# Clone can take an optional logger
|
|
372
|
-
logger = Logger.new
|
|
373
|
-
|
|
354
|
+
logger = Logger.new(STDOUT)
|
|
355
|
+
repo = Git.clone(git_url, 'my-repo', :log => logger)
|
|
374
356
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
357
|
+
repo.add # git add -- "."
|
|
358
|
+
repo.add(:all=>true) # git add --all -- "."
|
|
359
|
+
repo.add('file_path') # git add -- "file_path"
|
|
360
|
+
repo.add(['file_path_1', 'file_path_2']) # git add -- "file_path_1" "file_path_2"
|
|
379
361
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
362
|
+
repo.remove() # git rm -f -- "."
|
|
363
|
+
repo.remove('file.txt') # git rm -f -- "file.txt"
|
|
364
|
+
repo.remove(['file.txt', 'file2.txt']) # git rm -f -- "file.txt" "file2.txt"
|
|
365
|
+
repo.remove('file.txt', :recursive => true) # git rm -f -r -- "file.txt"
|
|
366
|
+
repo.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt"
|
|
385
367
|
|
|
386
|
-
|
|
387
|
-
|
|
368
|
+
repo.commit('message')
|
|
369
|
+
repo.commit_all('message')
|
|
388
370
|
|
|
389
371
|
# Sign a commit using the gpg key configured in the user.signingkey config setting
|
|
390
|
-
|
|
391
|
-
|
|
372
|
+
repo.config('user.signingkey', '0A46826A')
|
|
373
|
+
repo.commit('message', gpg_sign: true)
|
|
392
374
|
|
|
393
375
|
# Sign a commit using a specified gpg key
|
|
394
376
|
key_id = '0A46826A'
|
|
395
|
-
|
|
377
|
+
repo.commit('message', gpg_sign: key_id)
|
|
396
378
|
|
|
397
379
|
# Skip signing a commit (overriding any global gpgsign setting)
|
|
398
|
-
|
|
380
|
+
repo.commit('message', no_gpg_sign: true)
|
|
399
381
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
end
|
|
382
|
+
repo = Git.clone(git_url, 'myrepo')
|
|
383
|
+
repo.chdir do
|
|
384
|
+
File.write('test-file', 'blahblahblah')
|
|
385
|
+
repo.status.changed.each do |file|
|
|
386
|
+
puts file.blob(:index).contents
|
|
387
|
+
end
|
|
406
388
|
end
|
|
407
389
|
|
|
408
|
-
|
|
409
|
-
|
|
390
|
+
repo.reset # defaults to HEAD
|
|
391
|
+
repo.reset_hard(Git::Commit)
|
|
410
392
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
393
|
+
repo.branch('new_branch') # creates new or fetches existing
|
|
394
|
+
repo.branch('new_branch').checkout
|
|
395
|
+
repo.branch('new_branch').delete
|
|
396
|
+
repo.branch('existing_branch').checkout
|
|
397
|
+
repo.branch('main').contains?('existing_branch')
|
|
416
398
|
|
|
417
399
|
# delete remote branch
|
|
418
|
-
|
|
400
|
+
repo.push('origin', 'remote_branch_name', force: true, delete: true)
|
|
419
401
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
402
|
+
repo.checkout('new_branch')
|
|
403
|
+
repo.checkout('new_branch', new_branch: true, start_point: 'main')
|
|
404
|
+
repo.checkout(repo.branch('new_branch'))
|
|
423
405
|
|
|
424
|
-
|
|
425
|
-
|
|
406
|
+
repo.branch(name).merge(branch2)
|
|
407
|
+
repo.branch(branch2).merge # merges HEAD with branch2
|
|
426
408
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
409
|
+
repo.branch(name).in_branch(message) { # add files } # auto-commits
|
|
410
|
+
repo.merge('new_branch')
|
|
411
|
+
repo.merge('new_branch', 'merge commit message', no_ff: true)
|
|
412
|
+
repo.merge('origin/remote_branch')
|
|
413
|
+
repo.merge(repo.branch('main'))
|
|
414
|
+
repo.merge([branch1, branch2])
|
|
433
415
|
|
|
434
|
-
|
|
416
|
+
repo.merge_base('branch1', 'branch2')
|
|
435
417
|
|
|
436
|
-
r =
|
|
437
|
-
r =
|
|
418
|
+
r = repo.add_remote(name, uri) # Git::Remote
|
|
419
|
+
r = repo.add_remote(name, Git::Base) # Git::Remote
|
|
438
420
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
421
|
+
repo.remotes # array of Git::Remotes
|
|
422
|
+
repo.remote(name).fetch
|
|
423
|
+
repo.remote(name).remove
|
|
424
|
+
repo.remote(name).merge
|
|
425
|
+
repo.remote(name).merge(branch)
|
|
444
426
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
g.fetch('origin', {:ref => 'some/ref/head'} )
|
|
448
|
-
g.fetch(all: true, force: true, depth: 2)
|
|
449
|
-
g.fetch('origin', {:'update-head-ok' => true})
|
|
427
|
+
repo.remote_set_branches('origin', '*', add: true) # append additional fetch refspecs
|
|
428
|
+
repo.remote_set_branches('origin', 'feature', 'release/*') # replace fetch refspecs
|
|
450
429
|
|
|
451
|
-
|
|
452
|
-
|
|
430
|
+
repo.fetch
|
|
431
|
+
repo.fetch(repo.remotes.first)
|
|
432
|
+
repo.fetch('origin', {:ref => 'some/ref/head'} )
|
|
433
|
+
repo.fetch(all: true, force: true, depth: 2)
|
|
434
|
+
repo.fetch('origin', {:'update-head-ok' => true})
|
|
453
435
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
g.add_tag('tag_name', 'object_reference', {:options => 'here'})
|
|
457
|
-
g.add_tag('tag_name', {:options => 'here'})
|
|
436
|
+
repo.pull
|
|
437
|
+
repo.pull(Git::Repo, Git::Branch) # fetch and a merge
|
|
458
438
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
:m | :message
|
|
464
|
-
:s
|
|
439
|
+
repo.add_tag('tag_name') # returns Git::Tag
|
|
440
|
+
repo.add_tag('tag_name', 'object_reference')
|
|
441
|
+
repo.add_tag('tag_name', 'object_reference', {:options => 'here'})
|
|
442
|
+
repo.add_tag('tag_name', {:options => 'here'})
|
|
465
443
|
|
|
466
|
-
|
|
444
|
+
repo.delete_tag('tag_name')
|
|
467
445
|
|
|
468
|
-
|
|
446
|
+
repo.repack
|
|
469
447
|
|
|
470
|
-
|
|
471
|
-
|
|
448
|
+
repo.push
|
|
449
|
+
repo.push(repo.remote('name'))
|
|
472
450
|
|
|
473
451
|
# delete remote branch
|
|
474
|
-
|
|
452
|
+
repo.push('origin', 'remote_branch_name', force: true, delete: true)
|
|
475
453
|
|
|
476
454
|
# push all branches to remote at one time
|
|
477
|
-
|
|
455
|
+
repo.push('origin', all: true)
|
|
478
456
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
457
|
+
repo.worktree('/tmp/new_worktree').add
|
|
458
|
+
repo.worktree('/tmp/new_worktree', 'branch1').add
|
|
459
|
+
repo.worktree('/tmp/new_worktree').remove
|
|
460
|
+
repo.worktrees.prune
|
|
483
461
|
```
|
|
484
462
|
|
|
463
|
+
### Index and Tree Operations
|
|
464
|
+
|
|
485
465
|
Some examples of more low-level index and tree operations
|
|
486
466
|
|
|
487
467
|
```ruby
|
|
488
|
-
|
|
468
|
+
repo.with_temp_index do
|
|
489
469
|
|
|
490
|
-
|
|
491
|
-
|
|
470
|
+
repo.read_tree(tree3) # calls self.index.read_tree
|
|
471
|
+
repo.read_tree(tree1, :prefix => 'hi/')
|
|
492
472
|
|
|
493
|
-
c =
|
|
473
|
+
c = repo.commit_tree('message')
|
|
494
474
|
# or #
|
|
495
|
-
t =
|
|
496
|
-
c =
|
|
475
|
+
t = repo.write_tree
|
|
476
|
+
c = repo.commit_tree(t, :message => 'message', :parents => [sha1, sha2])
|
|
497
477
|
|
|
498
|
-
|
|
499
|
-
|
|
478
|
+
repo.branch('branch_name').update_ref(c)
|
|
479
|
+
repo.update_ref(branch, c)
|
|
500
480
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
481
|
+
repo.with_temp_working do # new blank working directory
|
|
482
|
+
repo.checkout
|
|
483
|
+
repo.checkout(another_index)
|
|
484
|
+
repo.commit # commits to temp_index
|
|
505
485
|
end
|
|
506
486
|
end
|
|
507
487
|
|
|
508
|
-
|
|
509
|
-
|
|
488
|
+
repo.set_index('/path/to/index')
|
|
510
489
|
|
|
511
|
-
|
|
490
|
+
repo.with_index(path) do
|
|
512
491
|
# calls set_index, then switches back after
|
|
513
492
|
end
|
|
514
493
|
|
|
515
|
-
|
|
494
|
+
repo.with_working(dir) do
|
|
516
495
|
# calls set_working, then switches back after
|
|
517
496
|
end
|
|
518
497
|
|
|
519
|
-
|
|
520
|
-
|
|
498
|
+
repo.with_temp_working(dir) do
|
|
499
|
+
repo.checkout_index(:prefix => dir, :path_limiter => path)
|
|
521
500
|
# do file work
|
|
522
|
-
|
|
501
|
+
repo.commit # commits to index
|
|
523
502
|
end
|
|
524
503
|
```
|
|
525
504
|
|
|
526
|
-
##
|
|
505
|
+
## Errors Raised By This Gem
|
|
527
506
|
|
|
528
|
-
|
|
507
|
+
The git gem will only raise an `ArgumentError` or an error that is a subclass of
|
|
508
|
+
`Git::Error`. It does not explicitly raise any other types of errors.
|
|
529
509
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
- The latest version of Truffle Ruby on Linus
|
|
510
|
+
It is recommended to rescue `Git::Error` to catch any runtime error raised by this
|
|
511
|
+
gem unless you need more specific error handling.
|
|
533
512
|
|
|
534
|
-
|
|
535
|
-
|
|
513
|
+
```ruby
|
|
514
|
+
begin
|
|
515
|
+
# some git operation
|
|
516
|
+
rescue Git::Error => e
|
|
517
|
+
puts "An error occurred: #{e.message}"
|
|
518
|
+
end
|
|
519
|
+
```
|
|
536
520
|
|
|
537
|
-
|
|
521
|
+
See [`Git::Error`](https://rubydoc.info/gems/git/Git/Error) for more information.
|
|
538
522
|
|
|
539
|
-
##
|
|
523
|
+
## Specifying And Handling Timeouts
|
|
540
524
|
|
|
541
|
-
|
|
542
|
-
|
|
525
|
+
The timeout feature was added in git gem version `2.0.0`.
|
|
526
|
+
|
|
527
|
+
A timeout for git command line operations can be set either globally or for specific
|
|
528
|
+
method calls that accept a `:timeout` parameter.
|
|
529
|
+
|
|
530
|
+
The timeout value must be a real, non-negative `Numeric` value that specifies a
|
|
531
|
+
number of seconds a `git` command will be given to complete before being sent a KILL
|
|
532
|
+
signal. This library may hang if the `git` command does not terminate after receiving
|
|
533
|
+
the KILL signal.
|
|
534
|
+
|
|
535
|
+
When a command times out, it is killed by sending it the `SIGKILL` signal and a
|
|
536
|
+
`Git::TimeoutError` is raised. This error derives from the `Git::SignaledError` and
|
|
537
|
+
`Git::Error`.
|
|
538
|
+
|
|
539
|
+
If the timeout value is `0` or `nil`, no timeout will be enforced.
|
|
540
|
+
|
|
541
|
+
If a method accepts a `:timeout` parameter and a receives a non-nil value, the value
|
|
542
|
+
of this parameter will override the global timeout value. In this context, a value of
|
|
543
|
+
`nil` (which is usually the default) will use the global timeout value and a value of
|
|
544
|
+
`0` will turn off timeout enforcement for that method call no matter what the global
|
|
545
|
+
value is.
|
|
546
|
+
|
|
547
|
+
To set a global timeout, use the `Git.config` object:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
Git.config.timeout = nil # a value of nil or 0 means no timeout is enforced
|
|
551
|
+
Git.config.timeout = 1.5 # can be any real, non-negative Numeric interpreted as number of seconds
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
The global timeout can be overridden for a specific method if the method accepts a
|
|
555
|
+
`:timeout` parameter:
|
|
556
|
+
|
|
557
|
+
```ruby
|
|
558
|
+
repo_url = 'https://github.com/ruby-git/ruby-git.git'
|
|
559
|
+
Git.clone(repo_url) # Use the global timeout value
|
|
560
|
+
Git.clone(repo_url, timeout: nil) # Also uses the global timeout value
|
|
561
|
+
Git.clone(repo_url, timeout: 0) # Do not enforce a timeout
|
|
562
|
+
Git.clone(repo_url, timeout: 10.5) # Timeout after 10.5 seconds raising Git::SignaledError
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
If the command takes too long, a `Git::TimeoutError` will be raised:
|
|
566
|
+
|
|
567
|
+
```ruby
|
|
568
|
+
begin
|
|
569
|
+
Git.clone(repo_url, timeout: 10)
|
|
570
|
+
rescue Git::TimeoutError => e
|
|
571
|
+
e.result.tap do |r|
|
|
572
|
+
r.class #=> Git::CommandLineResult
|
|
573
|
+
r.status #=> #<Process::Status: pid 62173 SIGKILL (signal 9)>
|
|
574
|
+
r.status.timeout? #=> true
|
|
575
|
+
r.git_cmd # The git command ran as an array of strings
|
|
576
|
+
r.stdout # The command's output to stdout until it was terminated
|
|
577
|
+
r.stderr # The command's output to stderr until it was terminated
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Deprecations
|
|
583
|
+
|
|
584
|
+
This gem uses ActiveSupport's deprecation mechanism to report deprecation warnings.
|
|
585
|
+
|
|
586
|
+
You can silence deprecation warnings by adding this line to your source code:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
Git::Deprecation.behavior = :silence
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
See [the Active Support Deprecation
|
|
593
|
+
documentation](https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html)
|
|
594
|
+
for more details.
|
|
595
|
+
|
|
596
|
+
If deprecation warnings are silenced, you should reenable them before upgrading the
|
|
597
|
+
git gem to the next major version. This will make it easier to identify changes
|
|
598
|
+
needed for the upgrade.
|
|
599
|
+
|
|
600
|
+
## Project Policies
|
|
601
|
+
|
|
602
|
+
These documents set expectations for behavior, contribution workflows, AI-assisted
|
|
603
|
+
changes, decision making, maintainer roles, and licensing. Please review them before
|
|
604
|
+
opening issues or pull requests.
|
|
605
|
+
|
|
606
|
+
| Document | Description |
|
|
607
|
+
| -------- | ----------- |
|
|
608
|
+
| [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) | We follow the Ruby community Code of Conduct; expect respectful, harassment-free participation and report concerns to maintainers. |
|
|
609
|
+
| [CONTRIBUTING](CONTRIBUTING.md) | How to report issues, submit PRs with Conventional Commits, meet coding/testing standards, and follow the Code of Conduct. |
|
|
610
|
+
| [AI_POLICY](AI_POLICY.md) | AI-assisted contributions are welcome. Contributors are expected to read and apply the AI Policy, and ensure any AI-assisted work meets our quality, security, and licensing standards. |
|
|
611
|
+
| [Ruby version support policy](#ruby-version-support-policy) | Supported Ruby runtimes and platforms; bump decisions and CI coverage expectations. |
|
|
612
|
+
| [Git version support policy](#git-version-support-policy) | Minimum supported git version and how version bumps are communicated and enforced. |
|
|
613
|
+
| [GOVERNANCE](GOVERNANCE.md) | Principles-first governance defining maintainer/project lead roles, least-privilege access, consensus/majority decisions, and nomination/emeritus steps. |
|
|
614
|
+
| [MAINTAINERS](MAINTAINERS.md) | Lists active maintainers (Project Lead noted) and emeritus alumni with links; see governance for role scope. |
|
|
615
|
+
| [LICENSE](LICENSE) | MIT License terms for using, modifying, and redistributing this project. |
|
|
616
|
+
|
|
617
|
+
### Ruby Version Support Policy
|
|
618
|
+
|
|
619
|
+
This gem is expected to function correctly on:
|
|
620
|
+
|
|
621
|
+
- All [non-EOL versions](https://www.ruby-lang.org/en/downloads/branches/) of the MRI
|
|
622
|
+
Ruby on Mac, Linux, and Windows
|
|
623
|
+
- The latest version of JRuby 9.4+ on Linux
|
|
624
|
+
- The latest version of TruffleRuby 24+ on Linux
|
|
625
|
+
|
|
626
|
+
It is this project's intent to support the latest version of JRuby on Windows once
|
|
627
|
+
the [process_executer](https://github.com/main-branch/process_executer) gem properly
|
|
628
|
+
supports subprocess status reporting on JRuby for Windows (see
|
|
629
|
+
[main-branch/process_executer#156](https://github.com/main-branch/process_executer/issues/156)).
|
|
630
|
+
|
|
631
|
+
### Git Version Support Policy
|
|
632
|
+
|
|
633
|
+
This gem requires git version 2.28.0 or greater as specified in the gemspec. This
|
|
634
|
+
requirement reflects:
|
|
635
|
+
|
|
636
|
+
- The minimum git version necessary to support all features provided by this gem
|
|
637
|
+
- A reasonable balance between supporting older systems and leveraging modern git
|
|
638
|
+
capabilities
|
|
639
|
+
- The practical limitations of testing across multiple git versions in CI
|
|
640
|
+
|
|
641
|
+
Git 2.28.0 was released on July 27, 2020. While this gem may work with earlier
|
|
642
|
+
versions of git, compatibility with versions prior to 2.28.0 is not tested or
|
|
643
|
+
guaranteed. Users on older git versions should upgrade to at least 2.28.0.
|
|
644
|
+
|
|
645
|
+
The supported git version may be increased in future major or minor releases of this
|
|
646
|
+
gem as new git features are adopted or as maintaining backward compatibility becomes
|
|
647
|
+
impractical. Such changes will be clearly documented in the CHANGELOG and release
|
|
648
|
+
notes.
|
|
543
649
|
|
|
544
650
|
## 📢 Project Announcements 📢
|
|
545
651
|
|
|
652
|
+
### 2026-01-07: AI Policy Introduced
|
|
653
|
+
|
|
654
|
+
We have adopted a formal [AI Policy](AI_POLICY.md) to clarify expectations for
|
|
655
|
+
AI-assisted contributions. Please review it before opening a PR to ensure your
|
|
656
|
+
changes are fully understood, meet our quality bar, and respect licensing
|
|
657
|
+
requirements.
|
|
658
|
+
|
|
659
|
+
We chose a principles-based policy to respect contributors’ time and expertise. It’s
|
|
660
|
+
quick to read, easy to remember, and avoids unnecessary policy overhead while still
|
|
661
|
+
setting clear expectations.
|
|
662
|
+
|
|
546
663
|
### 2025-07-09: Architectural Redesign
|
|
547
664
|
|
|
548
665
|
The git gem is undergoing a significant architectural redesign for the upcoming
|
|
@@ -594,11 +711,12 @@ rake rubocop
|
|
|
594
711
|
RuboCop is also run as part of the default rake task (by running `rake`) that is run
|
|
595
712
|
in our Continuous Integration workflow.
|
|
596
713
|
|
|
597
|
-
Going forward, any PRs that have any Robocop offenses will not be merged. In
|
|
598
|
-
|
|
599
|
-
|
|
714
|
+
Going forward, any PRs that have any Robocop offenses will not be merged. In certain
|
|
715
|
+
rare cases, it might be acceptable to disable a RuboCop check for the most limited
|
|
716
|
+
scope possible.
|
|
600
717
|
|
|
601
|
-
If you have a problem fixing a RuboCop offense, don't be afraid to ask a
|
|
718
|
+
If you have a problem fixing a RuboCop offense, don't be afraid to ask a
|
|
719
|
+
contributor.
|
|
602
720
|
|
|
603
721
|
### 2025-06-06: Default Branch Rename
|
|
604
722
|
|