Skip to content

Commit c8b07f6

Browse files
committed
refactor: make path normalization for Worktree and Repository optional
This will make testing easier since the path normalization will not need to be mocked.
1 parent b03a7ae commit c8b07f6

File tree

8 files changed

+297
-147
lines changed

8 files changed

+297
-147
lines changed

lib/ruby_git/repository.rb

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,50 @@ class Repository
2525
# RubyGit::Repository.new('/path/to/repository') #=> #<RubyGit::Repository ...>
2626
#
2727
# @param [String] repository_path the path to the repository
28+
# @param normalize_path [Boolean] if true, path is converted to an absolute path to the root of the working tree
2829
#
29-
def initialize(repository_path)
30-
@path = File.realpath(repository_path)
30+
# The purpose of this flag is to allow tests to not have to mock the
31+
# normalization of the path. This allows testing that the right git command
32+
# is contructed based on the options passed any particular method.
33+
#
34+
# @raise [ArgumentError] if the path is not a directory
35+
#
36+
def initialize(repository_path, normalize_path: true)
37+
@normalize_path = normalize_path
38+
39+
@path =
40+
if normalize_path?
41+
normalize_path(repository_path)
42+
else
43+
repository_path
44+
end
45+
end
46+
47+
private
48+
49+
# true if the path should be expanded and converted to a absolute, real path
50+
# @return [Boolean]
51+
# @api private
52+
def normalize_path? = @normalize_path
53+
54+
# Expand and convert the given path to an absolute, real path
55+
#
56+
# @example Expand the path
57+
# normalize_path('~/repository.git') #=> '/Users/james/repository.git'
58+
#
59+
# @example Convert to an absolute path
60+
# File.chdir('/User/james/repository/.git')
61+
# normalize_path('.') #=> '/User/james/repository/.git'
62+
#
63+
# @param path [String] the path to normalize
64+
#
65+
# @return [String]
66+
#
67+
# @api private
68+
def normalize_path(path)
69+
raise ArgumentError, "Directory '#{path}' does not exist." unless File.directory?(path)
70+
71+
File.realpath(File.expand_path(path))
3172
end
3273
end
3374
end

lib/ruby_git/worktree.rb

Lines changed: 82 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ class Worktree
3434
#
3535
# @return [RubyGit::Worktree] the working tree whose root is at `path`
3636
#
37-
def self.init(worktree_path)
37+
def self.init(worktree_path, normalize_path: true)
3838
raise RubyGit::Error, "Path '#{worktree_path}' not valid." unless File.directory?(worktree_path)
3939

4040
command = ['init']
4141
options = { chdir: worktree_path, out: StringIO.new, err: StringIO.new }
4242
RubyGit::CommandLine.run(*command, **options)
4343

44-
new(worktree_path)
44+
new(worktree_path, normalize_path:)
4545
end
4646

4747
# Open an existing Git working tree that contains worktree_path
@@ -58,8 +58,8 @@ def self.init(worktree_path)
5858
#
5959
# @return [RubyGit::Worktree] the Git working tree that contains `worktree_path`
6060
#
61-
def self.open(worktree_path)
62-
new(worktree_path)
61+
def self.open(worktree_path, normalize_path: true)
62+
new(worktree_path, normalize_path:)
6363
end
6464

6565
# Copy the remote repository and checkout the default branch
@@ -97,23 +97,12 @@ def self.open(worktree_path)
9797
#
9898
# @return [RubyGit::Worktree] the Git working tree checked out from the cloned repository
9999
#
100-
def self.clone(repository_url, to_path: nil)
100+
def self.clone(repository_url, to_path: nil, normalize_path: true)
101101
command = ['clone', '--', repository_url]
102102
command << to_path if to_path
103103
options = { out: StringIO.new, err: StringIO.new }
104104
clone_output = RubyGit::CommandLine.run(*command, **options).stderr
105-
new(cloned_to(clone_output))
106-
end
107-
108-
# Get path of the cloned worktree from `git clone` stderr output
109-
#
110-
# @param clone_output [String] the stderr output of the `git clone` command
111-
#
112-
# @return [String] the path of the cloned worktree
113-
#
114-
# @api private
115-
def self.cloned_to(clone_output)
116-
clone_output.match(/Cloning into ['"](.+)['"]\.\.\./)[1]
105+
new(cloned_to(clone_output), normalize_path:)
117106
end
118107

119108
# Show the working tree and index status
@@ -154,7 +143,7 @@ def status(*path_specs, untracked_files: :all, ignored: :no, ignore_submodules:
154143
command << '--' unless path_specs.empty?
155144
command.concat(path_specs)
156145
options = { out: StringIO.new, err: StringIO.new }
157-
status_output = run(*command, **options).stdout
146+
status_output = run_with_context(*command, **options).stdout
158147
RubyGit::Status.parse(status_output)
159148
end
160149

@@ -194,7 +183,7 @@ def add(*pathspecs, all: false, force: false, refresh: false, update: false) # r
194183

195184
options = { out: StringIO.new, err: StringIO.new }
196185

197-
run(*command, **options)
186+
run_with_context(*command, **options)
198187
end
199188

200189
# Return the repository associated with the worktree
@@ -211,7 +200,7 @@ def repository
211200
options = { chdir: path, chomp: true, out: StringIO.new, err: StringIO.new }
212201
# rev-parse path might be relative to the worktree, thus the need to expand it
213202
git_dir = File.realpath(RubyGit::CommandLine.run(*command, **options).stdout, path)
214-
Repository.new(git_dir)
203+
Repository.new(git_dir, normalize_path: normalize_path?)
215204
end
216205
end
217206

@@ -220,28 +209,94 @@ def repository
220209
# Create a Worktree object
221210
#
222211
# @param worktree_path [String] a path anywhere in the worktree
212+
# @param normalize_path [Boolean] if true, path is converted to an absolute path to the root of the working tree
213+
#
214+
# The purpose of this flag is to allow tests to not have to mock the
215+
# normalization of the path. This allows testing that the right git command
216+
# is contructed based on the options passed any particular method.
223217
#
218+
# @raise [ArgumentError] if the path is not a directory or the path is not in a
219+
# git working tree
220+
#
221+
# @return [RubyGit::Worktree] the worktree whose root is at `path`
224222
# @api private
225223
#
226-
def initialize(worktree_path)
227-
raise RubyGit::Error, "Path '#{worktree_path}' not valid." unless File.directory?(worktree_path)
224+
def initialize(worktree_path, normalize_path: true)
225+
@normalize_path = normalize_path
226+
227+
@path =
228+
if normalize_path?
229+
normalize_worktree_path(worktree_path)
230+
else
231+
worktree_path
232+
end
228233

229-
@path = root_path(worktree_path)
230234
RubyGit.logger.debug("Created #{inspect}")
231235
end
232236

233-
# Find the root path of a Git working tree containing `path`
237+
# Get path of the cloned worktree from `git clone` stderr output
238+
#
239+
# @param clone_output [String] the stderr output of the `git clone` command
240+
#
241+
# @return [String] the path of the cloned worktree
242+
#
243+
# @api private
244+
private_class_method def self.cloned_to(clone_output)
245+
clone_output.match(/Cloning into ['"](.+)['"]\.\.\./)[1]
246+
end
247+
248+
# True if the path should be normalized
249+
#
250+
# This means that the path should be expanded and converted to a absolute, real
251+
# path to the working tree root dir.
252+
#
253+
# @return [Boolean]
254+
#
255+
# @api private
256+
#
257+
def normalize_path? = @normalize_path
258+
259+
# Return the absolute path to the root of the working tree containing path
260+
#
261+
# @example Expand the path
262+
# normalize_path('~/worktree') #=> '/Users/james/worktree'
263+
#
264+
# @example Convert to an absolute path
265+
# File.chdir('/User/james/worktree')
266+
# normalize_path('.') #=> '/User/james/worktree'
234267
#
235-
# @raise [RubyGit::FailedError] if the path is not in a Git working tree
268+
# @param path [String] a (possibly relative) path within the worktree
269+
#
270+
# @return [String]
271+
#
272+
# @raise [ArgumentError] if the path is not a directory or the path is not in a
273+
# git working tree
274+
#
275+
# @api private
276+
#
277+
def normalize_worktree_path(path)
278+
raise ArgumentError, "Directory '#{path}' does not exist." unless File.directory?(path)
279+
280+
begin
281+
root_path(path)
282+
rescue RubyGit::FailedError => e
283+
raise ArgumentError, e.message
284+
end
285+
end
286+
287+
# Find the root path of a Git working tree containing `path`
236288
#
237289
# @return [String] the root path of the Git working tree containing `path`
238290
#
291+
# @raise [ArgumentError] if the path is not in a Git working tree
292+
#
239293
# @api private
240294
#
241295
def root_path(worktree_path)
242296
command = %w[rev-parse --show-toplevel]
243297
options = { chdir: worktree_path, chomp: true, out: StringIO.new, err: StringIO.new }
244-
File.realpath(RubyGit::CommandLine.run(*command, **options).stdout)
298+
root_path = RubyGit::CommandLine.run(*command, **options).stdout
299+
File.realpath(File.expand_path(root_path))
245300
end
246301

247302
# Run a Git command in this worktree
@@ -255,7 +310,7 @@ def root_path(worktree_path)
255310
#
256311
# @api private
257312
#
258-
def run(*command, **options)
313+
def run_with_context(*command, **options)
259314
RubyGit::CommandLine.run(*command, repository_path: repository.path, worktree_path: path, **options)
260315
end
261316

spec/lib/ruby_git/repository_spec.rb

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
# frozen_string_literal: true
22

33
RSpec.describe RubyGit::Repository do
4-
let(:repository) { RubyGit::Repository.new(repository_path) }
5-
let(:repository_path) { File.realpath(@repository_path) }
4+
let(:repository) { RubyGit::Repository.new(repository_path, normalize_path:) }
5+
let(:repository_path) { '.' }
66

77
describe '#initialize' do
8-
context 'when given a repository path' do
9-
around do |example|
10-
in_temp_dir do |repository_path|
11-
@repository_path = repository_path
12-
example.run
13-
end
8+
subject { repository }
9+
10+
around do |example|
11+
in_temp_dir do |_repository_path|
12+
example.run
1413
end
14+
end
1515

16-
subject { repository }
16+
context 'when normalize_path is true' do
17+
let(:normalize_path) { true }
18+
it 'path should be the absolute path to the repository' do
19+
expected_path = File.realpath(File.expand_path(repository_path))
20+
expect(subject).to have_attributes(path: expected_path)
21+
end
22+
end
1723

18-
it 'should set the path to the given repository path' do
24+
context 'when normalize_path is false' do
25+
let(:normalize_path) { false }
26+
it 'path should be as given' do
1927
expect(subject).to have_attributes(path: repository_path)
2028
end
2129
end

0 commit comments

Comments
 (0)