From 2b1974c1c9e6829ec0b498fb1739788555ae3e9e Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 1 Mar 2023 17:18:58 -0800 Subject: [PATCH 1/8] Make it easier to run test files from the command line (#635) Signed-off-by: James Couball --- CONTRIBUTING.md | 6 +++--- bin/test | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c0526f8e..8b9d7bf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,11 +84,11 @@ In order to ensure high quality, all pull requests must meet these requirements: While working on specific features you can run individual test files or a group of tests using `bin/test`: - # run a single file: - $ bin/test tests/units/test_object.rb + # run a single file (from tests/units): + $ bin/test test_object # run multiple files: - $ bin/test tests/units/test_object.rb tests/units/test_archive.rb + $ bin/test test_object test_archive # run all unit tests: $ bin/test diff --git a/bin/test b/bin/test index 10115417..8024c5ab 100755 --- a/bin/test +++ b/bin/test @@ -11,10 +11,11 @@ project_root = File.expand_path(File.join(__dir__, '..')) $LOAD_PATH.unshift(File.join(project_root, 'tests')) -if ARGV.empty? - paths = Dir.glob(File.join(project_root, 'tests/**/test_*.rb')) -else - paths = ARGV.map { |p| File.join(project_root, p) } -end +paths = + if ARGV.empty? + Dir.glob('tests/units/test_*.rb').map { |p| File.basename(p) } + else + ARGV + end.map { |p| File.join(project_root, 'tests/units', p) } paths.each { |p| require p } From 0c908da9098079abff3a4cfe55f3c33629c375ae Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 2 Mar 2023 08:09:35 -0800 Subject: [PATCH 2/8] #push without args should do same as `git push` with no args (#636) Signed-off-by: James Couball --- lib/git/base.rb | 7 +-- lib/git/lib.rb | 24 +++++++-- tests/units/test_push.rb | 110 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 tests/units/test_push.rb diff --git a/lib/git/base.rb b/lib/git/base.rb index 40b0a559..91ec6d1c 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -374,11 +374,8 @@ def fetch(remote = 'origin', opts = {}) # # @git.config('remote.remote-name.push', 'refs/heads/master:refs/heads/master') # - def push(remote = 'origin', branch = 'master', opts = {}) - # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature. - opts = {:tags => opts} if [true, false].include?(opts) - - self.lib.push(remote, branch, opts) + def push(*args, **options) + self.lib.push(*args, **options) end # merges one or more branches into the current working branch diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 4e768b97..7820c5d8 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -908,20 +908,36 @@ def fetch(remote, opts) command('fetch', *arr_opts) end - def push(remote, branch = 'master', opts = {}) + def push(remote = nil, branch = nil, opts = nil) + if opts.nil? && branch.instance_of?(Hash) + opts = branch + branch = nil + end + + if opts.nil? && remote.instance_of?(Hash) + opts = remote + remote = nil + end + + opts ||= {} + # Small hack to keep backwards compatibility with the 'push(remote, branch, tags)' method signature. opts = {:tags => opts} if [true, false].include?(opts) + raise ArgumentError, "You must specify a remote if a branch is specified" if remote.nil? && !branch.nil? + arr_opts = [] arr_opts << '--mirror' if opts[:mirror] arr_opts << '--delete' if opts[:delete] arr_opts << '--force' if opts[:force] || opts[:f] - arr_opts << remote + arr_opts << remote if remote + arr_opts_with_branch = arr_opts.dup + arr_opts_with_branch << branch if branch if opts[:mirror] - command('push', *arr_opts) + command('push', *arr_opts_with_branch) else - command('push', *arr_opts, branch) + command('push', *arr_opts_with_branch) command('push', '--tags', *arr_opts) if opts[:tags] end end diff --git a/tests/units/test_push.rb b/tests/units/test_push.rb new file mode 100644 index 00000000..0f579d36 --- /dev/null +++ b/tests/units/test_push.rb @@ -0,0 +1,110 @@ +require 'test_helper' + +class TestPush < Test::Unit::TestCase + test 'push with no args' do + expected_command_line = ['push'] + git_cmd = :push + git_cmd_args = [] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with no args and options' do + expected_command_line = ['push', '--force'] + git_cmd = :push + git_cmd_args = [force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with only a remote name' do + expected_command_line = ['push', 'origin'] + git_cmd = :push + git_cmd_args = ['origin'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with only a remote name and options' do + expected_command_line = ['push', '--force', 'origin'] + git_cmd = :push + git_cmd_args = ['origin', force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with only a branch name' do + expected_command_line = ['push', 'master'] + git_cmd = :push + git_cmd_args = [nil, 'origin'] + + in_temp_dir do + git = Git.init('.', initial_branch: 'master') + assert_raises(ArgumentError) { git.push(nil, 'master') } + end + end + + test 'push with both remote and branch name' do + expected_command_line = ['push', 'origin', 'master'] + git_cmd = :push + git_cmd_args = ['origin', 'master'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with force: true' do + expected_command_line = ['push', '--force', 'origin', 'master'] + git_cmd = :push + git_cmd_args = ['origin', 'master', force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with f: true' do + expected_command_line = ['push', '--force', 'origin', 'master'] + git_cmd = :push + git_cmd_args = ['origin', 'master', f: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with mirror: true' do + expected_command_line = ['push', '--force', 'origin', 'master'] + git_cmd = :push + git_cmd_args = ['origin', 'master', f: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with delete: true' do + expected_command_line = ['push', '--delete', 'origin', 'master'] + git_cmd = :push + git_cmd_args = ['origin', 'master', delete: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'push with tags: true' do + expected_command_line = ['push', '--tags', 'origin'] + git_cmd = :push + git_cmd_args = ['origin', nil, tags: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'when push succeeds an error should not be raised' do + in_temp_dir do + Git.init('remote.git', initial_branch: 'master', bare: true) + + git = Git.clone('remote.git', 'local') + Dir.chdir 'local' do + File.write('File2.txt', 'hello world') + git.add('File2.txt') + git.commit('Second commit') + assert_nothing_raised { git.push } + end + end + end + + test 'when push fails a Git::FailedError should be raised' do + in_temp_dir do + Git.init('remote.git', initial_branch: 'master', bare: true) + + git = Git.clone('remote.git', 'local') + Dir.chdir 'local' do + # Pushing when there is nothing to push fails + assert_raises(Git::FailedError) { git.push } + end + end + end +end From d33d5638a1468d48be3c4acf56aed3ca9ce3f172 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 2 Mar 2023 11:58:07 -0800 Subject: [PATCH 3/8] #checkout without args should do same as `git checkout` with no args (#637) Signed-off-by: James Couball --- lib/git/base.rb | 4 +- lib/git/lib.rb | 9 +++- tests/units/test_checkout.rb | 82 ++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tests/units/test_checkout.rb diff --git a/lib/git/base.rb b/lib/git/base.rb index 91ec6d1c..c89b8315 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -350,8 +350,8 @@ def commit_all(message, opts = {}) end # checks out a branch as the new git working directory - def checkout(branch = 'master', opts = {}) - self.lib.checkout(branch, opts) + def checkout(*args, **options) + self.lib.checkout(*args, **options) end # checks out an old version of a file diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 7820c5d8..741083df 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -772,11 +772,16 @@ def branch_delete(branch) # # @param [String] branch # @param [Hash] opts - def checkout(branch, opts = {}) + def checkout(branch = nil, opts = {}) + if branch.is_a?(Hash) && opts == {} + opts = branch + branch = nil + end + arr_opts = [] arr_opts << '-b' if opts[:new_branch] || opts[:b] arr_opts << '--force' if opts[:force] || opts[:f] - arr_opts << branch + arr_opts << branch if branch arr_opts << opts[:start_point] if opts[:start_point] && arr_opts.include?('-b') command('checkout', *arr_opts) diff --git a/tests/units/test_checkout.rb b/tests/units/test_checkout.rb new file mode 100644 index 00000000..4c7ea59d --- /dev/null +++ b/tests/units/test_checkout.rb @@ -0,0 +1,82 @@ +require 'test_helper' + + # Runs checkout command to checkout or create branch + # + # accepts options: + # :new_branch + # :force + # :start_point + # + # @param [String] branch + # @param [Hash] opts + # def checkout(branch, opts = {}) + +class TestCheckout < Test::Unit::TestCase + test 'checkout with no args' do + expected_command_line = ['checkout'] + git_cmd = :checkout + git_cmd_args = [] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with no args and options' do + expected_command_line = ['checkout', '--force'] + git_cmd = :checkout + git_cmd_args = [force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with branch' do + expected_command_line = ['checkout', 'feature1'] + git_cmd = :checkout + git_cmd_args = ['feature1'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with branch and options' do + expected_command_line = ['checkout', '--force', 'feature1'] + git_cmd = :checkout + git_cmd_args = ['feature1', force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with branch name and new_branch: true' do + expected_command_line = ['checkout', '-b', 'feature1'] + git_cmd = :checkout + git_cmd_args = ['feature1', new_branch: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with force: true' do + expected_command_line = ['checkout', '--force', 'feature1'] + git_cmd = :checkout + git_cmd_args = ['feature1', force: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + test 'checkout with branch name and new_branch: true and start_point: "sha"' do + expected_command_line = ['checkout', '-b', 'feature1', 'sha'] + git_cmd = :checkout + git_cmd_args = ['feature1', new_branch: true, start_point: 'sha'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + + test 'when checkout succeeds an error should not be raised' do + in_temp_dir do + git = Git.init('.', initial_branch: 'master') + File.write('file1.txt', 'file1') + git.add('file1.txt') + git.commit('commit1') + assert_nothing_raised { git.checkout('master') } + end + end + + test 'when checkout fails a Git::FailedError should be raised' do + in_temp_dir do + git = Git.init('.', initial_branch: 'master') + # fails because there are no commits + assert_raises(Git::FailedError) { git.checkout('master') } + end + end +end From 3dda0408c518a1d393503db631d56fc7dee37a02 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 2 Mar 2023 12:59:01 -0800 Subject: [PATCH 4/8] `#branch` name should default to current branch instead of `master` (#638) Signed-off-by: James Couball --- lib/git/base/factory.rb | 4 +--- tests/units/test_branch.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/git/base/factory.rb b/lib/git/base/factory.rb index 7b601306..25cb1090 100644 --- a/lib/git/base/factory.rb +++ b/lib/git/base/factory.rb @@ -3,9 +3,8 @@ module Git class Base module Factory - # @return [Git::Branch] an object for branch_name - def branch(branch_name = 'master') + def branch(branch_name = self.current_branch) Git::Branch.new(self, branch_name) end @@ -93,7 +92,6 @@ def merge_base(*args) shas = self.lib.merge_base(*args) shas.map { |sha| gcommit(sha) } end - end end diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index e3acd0d2..96585bdc 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -14,6 +14,18 @@ def setup @branches = @git.branches end + test 'Git::Lib#branch with no args should return current branch' do + in_temp_dir do + git = Git.init('.', initial_branch: 'my_branch') + File.write('file.txt', 'hello world') + git.add('file.txt') + git.commit('Initial commit') + + b = git.branch + assert_equal('my_branch', b.name) + end + end + def test_branches_all assert(@git.branches[:master].is_a?(Git::Branch)) assert(@git.branches.size > 5) From 7d8848c5257ca319e4587b35865889a816f0665b Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 3 Mar 2023 11:55:41 -0800 Subject: [PATCH 5/8] Remote#branch and #merge should default to current branch instead of "master" (#639) Signed-off-by: James Couball --- lib/git/remote.rb | 28 +++++++++++---------- tests/units/test_remotes.rb | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/lib/git/remote.rb b/lib/git/remote.rb index 73556a7c..9b2f3958 100644 --- a/lib/git/remote.rb +++ b/lib/git/remote.rb @@ -1,8 +1,8 @@ module Git class Remote < Path - + attr_accessor :name, :url, :fetch_opts - + def initialize(base, name) @base = base config = @base.lib.config_remote(name) @@ -10,27 +10,29 @@ def initialize(base, name) @url = config['url'] @fetch_opts = config['fetch'] end - + def fetch(opts={}) @base.fetch(@name, opts) end - + # merge this remote locally - def merge(branch = 'master') - @base.merge("#{@name}/#{branch}") + def merge(branch = @base.current_branch) + remote_tracking_branch = "#{@name}/#{branch}" + @base.merge(remote_tracking_branch) end - - def branch(branch = 'master') - Git::Branch.new(@base, "#{@name}/#{branch}") + + def branch(branch = @base.current_branch) + remote_tracking_branch = "#{@name}/#{branch}" + Git::Branch.new(@base, remote_tracking_branch) end - + def remove - @base.lib.remote_remove(@name) + @base.lib.remote_remove(@name) end - + def to_s @name end - + end end diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index ce0ed507..d119754e 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -232,4 +232,53 @@ def test_push assert(rem.tag('test-tag')) end end + + test 'Remote#branch with no args' do + in_temp_dir do + Dir.mkdir 'git' + Git.init('git', initial_branch: 'first', bare: true) + r1 = Git.clone('git', 'r1') + File.write('r1/file1.txt', 'hello world') + r1.add('file1.txt') + r1.commit('first commit') + r1.push + + r2 = Git.clone('git', 'r2') + + File.write('r1/file2.txt', 'hello world') + r1.add('file2.txt') + r1.commit('second commit') + r1.push + + branch = r2.remote('origin').branch + + assert_equal('origin/first', branch.full) + end + end + + test 'Remote#merge with no args' do + in_temp_dir do + Dir.mkdir 'git' + Git.init('git', initial_branch: 'first', bare: true) + r1 = Git.clone('git', 'r1') + File.write('r1/file1.txt', 'hello world') + r1.add('file1.txt') + r1.commit('first commit') + r1.push + + r2 = Git.clone('git', 'r2') + + File.write('r1/file2.txt', 'hello world') + r1.add('file2.txt') + r1.commit('second commit') + r1.push + + remote = r2.remote('origin') + + remote.fetch + remote.merge + + assert(File.exist?('r2/file2.txt')) + end + end end From 5c6833f487f2ca43240c0e11ac3621ec2559b031 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 3 Mar 2023 15:25:12 -0800 Subject: [PATCH 6/8] Fix parsing of symbolic refs in `Git::Lib#branches_all` (#640) Signed-off-by: James Couball --- lib/git/lib.rb | 34 +++++++++++++++++++++++++++++----- tests/units/test_branch.rb | 25 ++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 741083df..108b1035 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -347,13 +347,37 @@ def change_head_branch(branch_name) command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}") end + BRANCH_LINE_REGEXP = / + ^ + # Prefix indicates if this branch is checked out. The prefix is one of: + (?: + (?\*[[:blank:]]) | # Current branch (checked out in the current worktree) + (?\+[[:blank:]]) | # Branch checked out in a different worktree + [[:blank:]]{2} # Branch not checked out + ) + + # The branch's full refname + (?[^[[:blank:]]]+) + + # Optional symref + # If this ref is a symbolic reference, this is the ref referenced + (?: + [[:blank:]]->[[:blank:]](?.*) + )? + $ + /x + def branches_all - arr = [] - command_lines('branch', '-a').each do |b| - current = (b[0, 2] == '* ') - arr << [b.gsub('* ', '').strip, current] + command_lines('branch', '-a').map do |line| + match_data = line.match(BRANCH_LINE_REGEXP) + raise GitExecuteError, 'Unexpected branch line format' unless match_data + [ + match_data[:refname], + !match_data[:current].nil?, + !match_data[:worktree].nil?, + match_data[:symref] + ] end - arr end def worktrees_all diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index 96585bdc..c7a12aee 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -26,9 +26,28 @@ def setup end end - def test_branches_all - assert(@git.branches[:master].is_a?(Git::Branch)) - assert(@git.branches.size > 5) + test 'Git::Base#branches' do + in_temp_dir do + remote_git = Git.init('remote_git', initial_branch: 'master') + File.write('remote_git/file.txt', 'hello world') + remote_git.add('file.txt') + remote_git.commit('Initial commit') + remote_branches = remote_git.branches + assert_equal(1, remote_branches.size) + assert(remote_branches.first.current) + assert_equal('master', remote_branches.first.name) + + # Test that remote tracking branches are handled correctly + # + local_git = Git.clone('remote_git/.git', 'local_git') + local_branches = assert_nothing_raised { local_git.branches } + assert_equal(3, local_branches.size) + assert(remote_branches.first.current) + local_branch_refs = local_branches.map(&:full) + assert_include(local_branch_refs, 'master') + assert_include(local_branch_refs, 'remotes/origin/master') + assert_include(local_branch_refs, 'remotes/origin/HEAD') + end end def test_branches_local From 536df087cfa488534b19183ad28c0e60590ab726 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 3 Mar 2023 16:32:11 -0800 Subject: [PATCH 7/8] Fix parsing when in detached HEAD state in Git::Lib#branches_all (#641) Signed-off-by: James Couball --- lib/git/lib.rb | 9 +++++++-- tests/units/test_branch.rb | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 108b1035..4a685944 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -357,7 +357,11 @@ def change_head_branch(branch_name) ) # The branch's full refname - (?[^[[:blank:]]]+) + (?: + (?\(not[[:blank:]]a[[:blank:]]branch\)) | + (?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?[^\)]+)\)) | + (?[^[[:blank:]]]+) + ) # Optional symref # If this ref is a symbolic reference, this is the ref referenced @@ -371,13 +375,14 @@ def branches_all command_lines('branch', '-a').map do |line| match_data = line.match(BRANCH_LINE_REGEXP) raise GitExecuteError, 'Unexpected branch line format' unless match_data + next nil if match_data[:not_a_branch] || match_data[:detached_ref] [ match_data[:refname], !match_data[:current].nil?, !match_data[:worktree].nil?, match_data[:symref] ] - end + end.compact end def worktrees_all diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index c7a12aee..08707b63 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -50,6 +50,26 @@ def setup end end + test 'Git::Base#branchs with detached head' do + in_temp_dir do + git = Git.init('.', initial_branch: 'master') + File.write('file1.txt', 'hello world') + git.add('file1.txt') + git.commit('Initial commit') + git.add_tag('v1.0.0') + File.write('file2.txt', 'hello world') + git.add('file2.txt') + git.commit('Second commit') + + # This will put us in a detached head state + git.checkout('v1.0.0') + + branches = assert_nothing_raised { git.branches } + assert_equal(1, branches.size) + assert_equal('master', branches.first.name) + end + end + def test_branches_local bs = @git.branches.local assert(bs.size > 4) From 734e085c2a448db312b3bf7fef414708b7a741dc Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 3 Mar 2023 16:46:55 -0800 Subject: [PATCH 8/8] Release v1.16.0 Signed-off-by: James Couball --- CHANGELOG.md | 14 ++++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a6eac9..e355919e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ # Change Log +## v1.16.0 (2023-03-03) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v1.15.0..v1.16.0) + +Changes since v1.15.0: + +* 536d Fix parsing when in detached HEAD state in Git::Lib#branches_all (#641) +* 5c68 Fix parsing of symbolic refs in `Git::Lib#branches_all` (#640) +* 7d88 Remote#branch and #merge should default to current branch instead of "master" (#639) +* 3dda0 `#branch` name should default to current branch instead of `master` (#638) +* d33d #checkout without args should do same as `git checkout` with no args (#637) +* 0c90 #push without args should do same as `git push` with no args (#636) +* 2b19 Make it easier to run test files from the command line (#635) + ## v1.15.0 (2023-03-01) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v1.14.0..v1.15.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index dad40436..976add24 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.15.0' + VERSION='1.16.0' end