diff --git a/CHANGELOG.md b/CHANGELOG.md index bc708094..e53dd79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## 1.10.0 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.0 + ## 1.9.1 See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.1 diff --git a/lib/git.rb b/lib/git.rb index eb4c7cce..6e93957c 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -212,6 +212,9 @@ def self.global_config(name = nil, value = nil) # `"#{directory}/.git"`, create a bare repository at `"#{directory}"`. # See [what is a bare repository?](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbarerepositoryabarerepository). # + # @option options [String] :initial_branch Use the specified name for the + # initial branch in the newly created repository. + # # @option options [Pathname] :repository the path to put the newly initialized # Git repository. The default for non-bare repository is `"#{directory}/.git"`. # diff --git a/lib/git/base.rb b/lib/git/base.rb index 6e7c7a10..13edf848 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -12,40 +12,35 @@ class Base # (see Git.bare) def self.bare(git_dir, options = {}) - self.new({:repository => git_dir}.merge(options)) + normalize_paths(options, default_repository: git_dir, bare: true) + self.new(options) end # (see Git.clone) def self.clone(repository, name, options = {}) - self.new(Git::Lib.new(nil, options[:log]).clone(repository, name, options)) + new_options = Git::Lib.new(nil, options[:log]).clone(repository, name, options) + normalize_paths(new_options, bare: options[:bare] || options[:mirror]) + self.new(new_options) end # Returns (and initialize if needed) a Git::Config instance # # @return [Git::Config] the current config instance. def self.config - return @@config ||= Config.new + @@config ||= Config.new end # (see Git.init) - def self.init(directory, options = {}) - options[:working_directory] ||= directory - options[:repository] ||= File.join(options[:working_directory], '.git') - - FileUtils.mkdir_p(options[:working_directory]) if options[:working_directory] && !File.directory?(options[:working_directory]) + def self.init(directory = '.', options = {}) + normalize_paths(options, default_working_directory: directory, default_repository: directory, bare: options[:bare]) - init_options = { :bare => options[:bare] } + init_options = { + :bare => options[:bare], + :initial_branch => options[:initial_branch], + } - options.delete(:working_directory) if options[:bare] - - # Submodules have a .git *file* not a .git folder. - # This file's contents point to the location of - # where the git refs are held (In the parent repo) - if options[:working_directory] && File.file?(File.join(options[:working_directory], '.git')) - git_file = File.open('.git').read[8..-1].strip - options[:repository] = git_file - options[:index] = git_file + '/index' - end + directory = options[:bare] ? options[:repository] : options[:working_directory] + FileUtils.mkdir_p(directory) unless File.exist?(directory) # TODO: this dance seems awkward: this creates a Git::Lib so we can call # init so we can create a new Git::Base which in turn (ultimately) @@ -63,21 +58,8 @@ def self.init(directory, options = {}) end # (see Git.open) - def self.open(working_dir, options={}) - # TODO: move this to Git.open? - - options[:working_directory] ||= working_dir - options[:repository] ||= File.join(options[:working_directory], '.git') - - # Submodules have a .git *file* not a .git folder. - # This file's contents point to the location of - # where the git refs are held (In the parent repo) - if options[:working_directory] && File.file?(File.join(options[:working_directory], '.git')) - git_file = File.open('.git').read[8..-1].strip - options[:repository] = git_file - options[:index] = git_file + '/index' - end - + def self.open(working_dir, options = {}) + normalize_paths(options, default_working_directory: working_dir) self.new(options) end @@ -287,6 +269,7 @@ def reset_hard(commitish = nil, opts = {}) # options: # :force # :d + # :ff # def clean(opts = {}) self.lib.clean(opts) @@ -567,7 +550,6 @@ def with_temp_working &blk with_working(temp_dir, &blk) end - # runs git rev-parse to convert the objectish to a full sha # # @example @@ -592,6 +574,93 @@ def current_branch self.lib.branch_current end - end + private + + # Normalize options before they are sent to Git::Base.new + # + # Updates the options parameter by setting appropriate values for the following keys: + # * options[:working_directory] + # * options[:repository] + # * options[:index] + # + # All three values will be set to absolute paths. An exception is that + # :working_directory will be set to nil if bare is true. + # + private_class_method def self.normalize_paths( + options, default_working_directory: nil, default_repository: nil, bare: false + ) + normalize_working_directory(options, default: default_working_directory, bare: bare) + normalize_repository(options, default: default_repository, bare: bare) + normalize_index(options) + end + + # Normalize options[:working_directory] + # + # If working with a bare repository, set to `nil`. + # Otherwise, set to the first non-nil value of: + # 1. `options[:working_directory]`, + # 2. the `default` parameter, or + # 3. the current working directory + # + # Finally, if options[:working_directory] is a relative path, convert it to an absoluite + # path relative to the current directory. + # + private_class_method def self.normalize_working_directory(options, default:, bare: false) + working_directory = + if bare + nil + else + File.expand_path(options[:working_directory] || default || Dir.pwd) + end + options[:working_directory] = working_directory + end + + # Normalize options[:repository] + # + # If working with a bare repository, set to the first non-nil value out of: + # 1. `options[:repository]` + # 2. the `default` parameter + # 3. the current working directory + # + # Otherwise, set to the first non-nil value of: + # 1. `options[:repository]` + # 2. `.git` + # + # Next, if options[:repository] refers to a *file* and not a *directory*, set + # options[:repository] to the contents of that file. This is the case when + # working with a submodule or a secondary working tree (created with git worktree + # add). In these cases the repository is actually contained/nested within the + # parent's repository directory. + # + # Finally, if options[:repository] is a relative path, convert it to an absolute + # path relative to: + # 1. the current directory if working with a bare repository or + # 2. the working directory if NOT working with a bare repository + # + private_class_method def self.normalize_repository(options, default:, bare: false) + repository = + if bare + File.expand_path(options[:repository] || default || Dir.pwd) + else + File.expand_path(options[:repository] || '.git', options[:working_directory]) + end + + if File.file?(repository) + repository = File.expand_path(File.open(repository).read[8..-1].strip, options[:working_directory]) + end + + options[:repository] = repository + end + + # Normalize options[:index] + # + # If options[:index] is a relative directory, convert it to an absolute + # directory relative to the repository directory + # + private_class_method def self.normalize_index(options) + index = File.expand_path(options[:index] || 'index', options[:repository]) + options[:index] = index + end + end end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 57220f07..5641e4eb 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -71,10 +71,12 @@ def initialize(base = nil, logger = nil) # options: # :bare # :working_directory + # :initial_branch # def init(opts={}) arr_opts = [] arr_opts << '--bare' if opts[:bare] + arr_opts << "--initial-branch=#{opts[:initial_branch]}" if opts[:initial_branch] command('init', arr_opts) end @@ -682,6 +684,7 @@ def reset(commit, opts = {}) def clean(opts = {}) arr_opts = [] arr_opts << '--force' if opts[:force] + arr_opts << '-ff' if opts[:ff] arr_opts << '-d' if opts[:d] arr_opts << '-x' if opts[:x] @@ -772,6 +775,7 @@ def checkout_file(version, file) def merge(branch, message = nil, opts = {}) arr_opts = [] + arr_opts << '--no-commit' if opts[:no_commit] arr_opts << '--no-ff' if opts[:no_ff] arr_opts << '-m' << message if message arr_opts += [branch] @@ -879,6 +883,7 @@ def fetch(remote, opts) arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] arr_opts << '--unshallow' if opts[:unshallow] + arr_opts << '--depth' << opts[:depth] if opts[:depth] command('fetch', arr_opts) end diff --git a/lib/git/version.rb b/lib/git/version.rb index b4cde914..ea10ef11 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -1,5 +1,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='1.9.1' + VERSION='1.10.0' end diff --git a/tests/units/test_index_ops.rb b/tests/units/test_index_ops.rb index fd47e609..c033735b 100644 --- a/tests/units/test_index_ops.rb +++ b/tests/units/test_index_ops.rb @@ -49,6 +49,11 @@ def test_clean g.add g.commit("first commit") + FileUtils.mkdir_p("nested") + Dir.chdir('nested') do + Git.init + end + new_file('file-to-clean', 'blablahbla') FileUtils.mkdir_p("dir_to_clean") @@ -76,6 +81,11 @@ def test_clean g.clean(:force => true, :x => true) assert(!File.exist?('ignored_file')) + + assert(File.exist?('nested')) + + g.clean(:ff => true, :d => true) + assert(!File.exist?('nested')) end end end diff --git a/tests/units/test_init.rb b/tests/units/test_init.rb index bbb04b94..4ec4771d 100644 --- a/tests/units/test_init.rb +++ b/tests/units/test_init.rb @@ -38,14 +38,14 @@ def test_git_init assert(File.directory?(File.join(path, '.git'))) assert(File.exist?(File.join(path, '.git', 'config'))) assert_equal('false', repo.config('core.bare')) + assert_equal("ref: refs/heads/master\n", File.read("#{path}/.git/HEAD")) end end def test_git_init_bare in_temp_dir do |path| repo = Git.init(path, :bare => true) - assert(File.directory?(File.join(path, '.git'))) - assert(File.exist?(File.join(path, '.git', 'config'))) + assert(File.exist?(File.join(path, 'config'))) assert_equal('true', repo.config('core.bare')) end end @@ -61,6 +61,16 @@ def test_git_init_remote_git end end + def test_git_init_initial_branch + in_temp_dir do |path| + repo = Git.init(path, initial_branch: 'main') + assert(File.directory?(File.join(path, '.git'))) + assert(File.exist?(File.join(path, '.git', 'config'))) + assert_equal('false', repo.config('core.bare')) + assert_equal("ref: refs/heads/main\n", File.read("#{path}/.git/HEAD")) + end + end + def test_git_clone in_temp_dir do |path| g = Git.clone(@wbare, 'bare-co') diff --git a/tests/units/test_merge.rb b/tests/units/test_merge.rb index 8fd10712..21e9ee78 100644 --- a/tests/units/test_merge.rb +++ b/tests/units/test_merge.rb @@ -130,4 +130,33 @@ def test_no_ff_merge end end end + + def test_merge_no_commit + in_temp_dir do |path| + g = Git.clone(@wbare, 'branch_merge_test') + g.chdir do + g.branch('new_branch_1').in_branch('first commit message') do + new_file('new_file_1', 'foo') + g.add + true + end + + g.branch('new_branch_2').in_branch('first commit message') do + new_file('new_file_2', 'bar') + g.add + true + end + + g.checkout('new_branch_2') + before_merge = g.show + g.merge('new_branch_1', nil, no_commit: true) + # HEAD is the same as before. + assert_equal(before_merge, g.show) + # File has not been merged in. + status = g.status['new_file_1'] + assert_equal('new_file_1', status.path) + assert_equal('A', status.type) + end + end + end end diff --git a/tests/units/test_thread_safety.rb b/tests/units/test_thread_safety.rb index 401659a5..d2500f10 100644 --- a/tests/units/test_thread_safety.rb +++ b/tests/units/test_thread_safety.rb @@ -24,7 +24,7 @@ def test_git_init_bare threads.each(&:join) dirs.each do |dir| - Git.bare("#{dir}/.git").ls_files + Git.bare(dir).ls_files end end end diff --git a/tests/units/test_worktree.rb b/tests/units/test_worktree.rb index f5141c8d..c0a81dcb 100644 --- a/tests/units/test_worktree.rb +++ b/tests/units/test_worktree.rb @@ -1,5 +1,6 @@ #!/usr/bin/env ruby require 'fileutils' +require 'pathname' require File.dirname(__FILE__) + '/../test_helper' SAMPLE_LAST_COMMIT = '5e53019b3238362144c2766f02a2c00d91fcc023'