Skip to content

Commit 301c7b7

Browse files
committed
feat: support git add via Worktree#add
1 parent 72020b9 commit 301c7b7

File tree

2 files changed

+242
-5
lines changed

2 files changed

+242
-5
lines changed

lib/ruby_git/worktree.rb

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,57 @@ def self.cloned_to(clone_output)
146146
#
147147
# @return [RubyGit::Status::Report] the status of the working tree
148148
#
149-
def status(*path_specs, untracked_files: :all, ignored: :no, ignore_submodules: :all) # rubocop:disable Metrics/MethodLength
149+
def status(*path_specs, untracked_files: :all, ignored: :no, ignore_submodules: :all)
150150
command = %w[status --porcelain=v2 --branch --show-stash --ahead-behind --renames -z]
151151
command << "--untracked-files=#{untracked_files}"
152152
command << "--ignored=#{ignored}"
153153
command << "--ignore-submodules=#{ignore_submodules}"
154-
unless path_specs.empty?
155-
command << '--'
156-
command.concat(path_specs)
157-
end
154+
command << '--' unless path_specs.empty?
155+
command.concat(path_specs)
158156
options = { out: StringIO.new, err: StringIO.new }
159157
status_output = run(*command, **options).stdout
160158
RubyGit::Status.parse(status_output)
161159
end
162160

161+
# Add changed files to the index to stage for the next commit
162+
#
163+
# @example
164+
# worktree = Worktree.open(worktree_path)
165+
# worktree.add('file1.txt', 'file2.txt')
166+
# worktree.add('.')
167+
# worktree.add(all: true)
168+
#
169+
# @param pathspecs [Array<String>] paths to add to the index
170+
# @param all [Boolean] adds, updates, and removes index entries to match the working tree (entire repo)
171+
# @param force [Boolean] add files even if they are ignored
172+
# @param refresh [Boolean] only refresh each files stat information in the index
173+
# @param update [Boolean] add all updated and deleted files to the index but does not add any files
174+
#
175+
# @see https://git-scm.com/docs/git-add git-add
176+
#
177+
# @return [RubyGit::CommandLineResult] the result of the git add command
178+
#
179+
# @raise [ArgumentError] if any of the options are not valid
180+
#
181+
def add(*pathspecs, all: false, force: false, refresh: false, update: false) # rubocop:disable Metrics/MethodLength
182+
validate_boolean_option(name: :all, value: all)
183+
validate_boolean_option(name: :force, value: force)
184+
validate_boolean_option(name: :refresh, value: refresh)
185+
validate_boolean_option(name: :update, value: update)
186+
187+
command = %w[add]
188+
command << '--all' if all
189+
command << '--force' if force
190+
command << '--update' if update
191+
command << '--refresh' if refresh
192+
command << '--' unless pathspecs.empty?
193+
command.concat(pathspecs)
194+
195+
options = { out: StringIO.new, err: StringIO.new }
196+
197+
run(*command, **options)
198+
end
199+
163200
# Return the repository associated with the worktree
164201
#
165202
# @example
@@ -221,5 +258,20 @@ def root_path(worktree_path)
221258
def run(*command, **options)
222259
RubyGit::CommandLine.run(*command, repository_path: repository.path, worktree_path: path, **options)
223260
end
261+
262+
# Raise an error if an option is not a Boolean (or optionally nil) value
263+
# @param name [String] the name of the option
264+
# @param value [Object] the value of the option
265+
# @param nullable [Boolean] whether the option can be nil (default is false)
266+
# @return [void]
267+
# @raise [ArgumentError] if the option is not a Boolean (or optionally nil) value
268+
# @api private
269+
def validate_boolean_option(name:, value:, nullable: false)
270+
return if nullable && value.nil?
271+
272+
return if [true, false].include?(value)
273+
274+
raise ArgumentError, "The '#{name}:' option must be a Boolean value but was #{value.inspect}"
275+
end
224276
end
225277
end
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RubyGit::Worktree do
4+
let(:worktree) { described_class.open(worktree_path) }
5+
let(:worktree_path) { @worktree_path }
6+
7+
describe '#add' do
8+
subject { worktree.add(*pathspecs, **options) }
9+
10+
around do |example|
11+
in_temp_dir do |path|
12+
@worktree_path = path
13+
run %w[git init --initial-branch=main]
14+
File.write('file1.txt', 'file1 contents')
15+
File.write('file2.txt', 'file2 contents')
16+
Dir.mkdir 'subdir'
17+
File.write('subdir/file3.txt', 'file3 contents')
18+
example.run
19+
end
20+
end
21+
22+
describe 'adding changes to the index' do
23+
context 'when told to add all changes into the index' do
24+
def untracked_entries
25+
worktree.status.entries.select { |entry| entry.is_a?(RubyGit::Status::UntrackedEntry) }
26+
end
27+
28+
it 'should add files to the index' do
29+
expect { worktree.add('.') }.to change { untracked_entries.count }.from(3).to(0)
30+
end
31+
end
32+
end
33+
34+
describe 'calling the git add command line' do
35+
let(:result) { instance_double(RubyGit::CommandLine::Result, stdout: '') }
36+
37+
context 'with called with no arguments' do
38+
let(:pathspecs) { [] }
39+
let(:options) { {} }
40+
41+
let(:expected_command) { %w[add] }
42+
43+
it 'should build the correct command' do
44+
expect(worktree).to(
45+
receive(:run).with(*expected_command, Hash)
46+
).and_return(result)
47+
48+
subject
49+
end
50+
end
51+
52+
RSpec.shared_examples 'the git command' do |expected_command|
53+
it 'should build the correct command' do
54+
expect(worktree).to(
55+
receive(:run).with(*expected_command, Hash)
56+
).and_return(result)
57+
58+
subject
59+
end
60+
end
61+
62+
context 'with with a pathspec' do
63+
let(:pathspecs) { %w[file1.txt] }
64+
let(:options) { {} }
65+
66+
it_behaves_like 'the git command', %w[add -- file1.txt]
67+
end
68+
69+
context 'with two pathspecs' do
70+
let(:pathspecs) { %w[file1.txt file2.txt] }
71+
let(:options) { {} }
72+
73+
it_behaves_like 'the git command', %w[add -- file1.txt file2.txt]
74+
end
75+
76+
context 'with the all option' do
77+
context 'all: true' do
78+
let(:pathspecs) { [] }
79+
let(:options) { { all: true } }
80+
81+
it_behaves_like 'the git command', %w[add --all]
82+
end
83+
84+
context 'all: false' do
85+
let(:pathspecs) { [] }
86+
let(:options) { { all: false } }
87+
88+
it_behaves_like 'the git command', %w[add]
89+
end
90+
91+
context 'all: invalid' do
92+
let(:pathspecs) { [] }
93+
let(:options) { { all: 'invalid' } }
94+
95+
it 'should raise an error' do
96+
expect { subject }.to(
97+
raise_error(ArgumentError, %(The 'all:' option must be a Boolean value but was "invalid"))
98+
)
99+
end
100+
end
101+
end
102+
103+
context 'with the force option' do
104+
context 'force: true' do
105+
let(:pathspecs) { [] }
106+
let(:options) { { force: true } }
107+
108+
it_behaves_like 'the git command', %w[add --force]
109+
end
110+
111+
context 'force: false' do
112+
let(:pathspecs) { [] }
113+
let(:options) { { force: false } }
114+
115+
it_behaves_like 'the git command', %w[add]
116+
end
117+
118+
context 'force: invalid' do
119+
let(:pathspecs) { [] }
120+
let(:options) { { force: 'invalid' } }
121+
122+
it 'should raise an error' do
123+
expect { subject }.to(
124+
raise_error(ArgumentError, %(The 'force:' option must be a Boolean value but was "invalid"))
125+
)
126+
end
127+
end
128+
end
129+
130+
context 'with the update option' do
131+
context 'update: true' do
132+
let(:pathspecs) { [] }
133+
let(:options) { { update: true } }
134+
135+
it_behaves_like 'the git command', %w[add --update]
136+
end
137+
138+
context 'update: false' do
139+
let(:pathspecs) { [] }
140+
let(:options) { { update: false } }
141+
142+
it_behaves_like 'the git command', %w[add]
143+
end
144+
145+
context 'update: invalid' do
146+
let(:pathspecs) { [] }
147+
let(:options) { { update: 'invalid' } }
148+
149+
it 'should raise an error' do
150+
expect { subject }.to(
151+
raise_error(ArgumentError, %(The 'update:' option must be a Boolean value but was "invalid"))
152+
)
153+
end
154+
end
155+
end
156+
157+
context 'with the refresh option' do
158+
context 'refresh: true' do
159+
let(:pathspecs) { [] }
160+
let(:options) { { refresh: true } }
161+
162+
it_behaves_like 'the git command', %w[add --refresh]
163+
end
164+
165+
context 'refresh: false' do
166+
let(:pathspecs) { [] }
167+
let(:options) { { refresh: false } }
168+
169+
it_behaves_like 'the git command', %w[add]
170+
end
171+
172+
context 'refresh: invalid' do
173+
let(:pathspecs) { [] }
174+
let(:options) { { refresh: 'invalid' } }
175+
176+
it 'should raise an error' do
177+
expect { subject }.to(
178+
raise_error(ArgumentError, %(The 'refresh:' option must be a Boolean value but was "invalid"))
179+
)
180+
end
181+
end
182+
end
183+
end
184+
end
185+
end

0 commit comments

Comments
 (0)