diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml
index ad2ea03a..c6599412 100644
--- a/.github/workflows/continuous_integration.yml
+++ b/.github/workflows/continuous_integration.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- ruby: [2.3, 2.7]
+ ruby: [2.3, 2.7, 3.0]
operating-system: [ubuntu-latest]
include:
- ruby: head
@@ -28,6 +28,9 @@ jobs:
runs-on: ${{ matrix.operating-system }}
+ env:
+ JAVA_OPTS: -Djdk.io.File.enableADS=true
+
steps:
- name: Checkout Code
uses: actions/checkout@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7e7963b..a08297c5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,37 @@
# Change Log
+## v1.11.0
+
+* 292087e Supress unneeded test output (#570)
+* 19dfe5e Add support for fetch options "--force/-f" and "--prune-tags/-P". (#563)
+* 018d919 Fix bug when grepping lines that contain numbers surrounded by colons (#566)
+* c04d16e remove from maintainer (#567)
+* 291ca09 Address command line injection in Git::Lib#fetch
+* 521b8e7 Release v1.10.2 (#561)
+
+See https://github.com/ruby-git/ruby-git/releases/tag/v1.11.0
+
+## v1.10.2
+
+See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.2
+
+## 1.10.1
+
+See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.1
+
+## 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
+
+## 1.9.0
+
+See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.0
+
## 1.8.1
See https://github.com/ruby-git/ruby-git/releases/tag/v1.8.1
diff --git a/Dockerfile.changelog-rs b/Dockerfile.changelog-rs
new file mode 100644
index 00000000..75c35d93
--- /dev/null
+++ b/Dockerfile.changelog-rs
@@ -0,0 +1,12 @@
+FROM rust
+
+# Build the docker image (from this project's root directory):
+# docker build --file Dockerfile.changelog-rs --tag changelog-rs .
+#
+# Use this image to output a changelog (from this project's root directory):
+# docker run --rm --volume "$PWD:/worktree" changelog-rs v1.9.1 v1.10.0
+
+RUN cargo install changelog-rs
+WORKDIR /worktree
+
+ENTRYPOINT ["/usr/local/cargo/bin/changelog-rs", "/worktree"]
diff --git a/Gemfile b/Gemfile
index 7054c552..b2afa573 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,4 +1,9 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org'
-gemspec :name => 'git'
+git 'https://github.com/lsegal/yard', branch: 'main' do
+ gem 'yard'
+end
+gemspec name: 'git'
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 2d8ac7b1..ef13361f 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -10,5 +10,4 @@ When making changes in this repository, one of the maintainers below must review
### Maintainers
* [Per Lundberg](https://github.com/perlun)
-* [Vern Burton](https://github.com/tarcinil)
* [James Couball](https://github.com/jcouball)
\ No newline at end of file
diff --git a/README.md b/README.md
index 0ff9a0a5..d4b68c55 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ g.index.writable?
g.repo
g.dir
-g.log # returns array of Git::Commit objects
+g.log # returns a Git::Log object, which is an Enumerator of Git::Commit objects
g.log.since('2 weeks ago')
g.log.between('v2.5', 'v2.6')
g.log.each {|l| puts l.sha }
@@ -146,6 +146,7 @@ g.revparse('v2.5:Makefile')
g.branches # returns Git::Branch objects
g.branches.local
+g.current_branch
g.branches.remote
g.branches[:master].gcommit
g.branches['origin/master'].gcommit
@@ -203,13 +204,23 @@ g = Git.init
{ :repository => '/opt/git/proj.git',
:index => '/tmp/index'} )
-g = Git.clone(URI, NAME, :path => '/tmp/checkout')
+# Clone from a git url
+git_url = 'https://github.com/ruby-git/ruby-git.git'
+# Clone into the ruby-git directory
+g = Git.clone(git_url)
+
+# Clone into /tmp/clone/ruby-git-clean
+name = 'ruby-git-clean'
+path = '/tmp/clone'
+g = Git.clone(git_url, name, :path => path)
+g.dir #=> /tmp/clone/ruby-git-clean
+
g.config('user.name', 'Scott Chacon')
g.config('user.email', 'email@email.com')
# Clone can take an optional logger
logger = Logger.new
-g = Git.clone(URI, NAME, :log => logger)
+g = Git.clone(git_url, NAME, :log => logger)
g.add # git add -- "."
g.add(:all=>true) # git add --all -- "."
@@ -225,6 +236,14 @@ g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt"
g.commit('message')
g.commit_all('message')
+# Sign a commit using the gpg key configured in the user.signingkey config setting
+g.config('user.signingkey', '0A46826A')
+g.commit('message', gpg_sign: true)
+
+# Sign a commit using a specified gpg key
+key_id = '0A46826A'
+g.commit('message', gpg_sign: key_id)
+
g = Git.clone(repo, 'myrepo')
g.chdir do
new_file('test-file', 'blahblahblah')
@@ -269,6 +288,7 @@ g.remote(name).merge(branch)
g.fetch
g.fetch(g.remotes.first)
g.fetch('origin', {:ref => 'some/ref/head'} )
+g.fetch(all: true, force: true, depth: 2)
g.pull
g.pull(Git::Repo, Git::Branch) # fetch and a merge
diff --git a/RELEASING.md b/RELEASING.md
index 7f360370..f43697da 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -6,9 +6,11 @@
# How to release a new git.gem
Releasing a new version of the `git` gem requires these steps:
- * [Prepare the release](#prepare-the-release)
- * [Create a GitHub release](#create-a-github-release)
- * [Build and release the gem](#build-and-release-the-gem)
+
+- [How to release a new git.gem](#how-to-release-a-new-gitgem)
+ - [Prepare the release](#prepare-the-release)
+ - [Create a GitHub release](#create-a-github-release)
+ - [Build and release the gem](#build-and-release-the-gem)
These instructions use an example where the current release version is `1.5.0`
and the new release version to be created is `1.6.0.pre1`.
@@ -18,45 +20,49 @@ and the new release version to be created is `1.6.0.pre1`.
From a fork of ruby-git, create a PR containing changes to (1) bump the
version number, (2) update the CHANGELOG.md, and (3) tag the release.
- * Bump the version number in lib/git/version.rb following [Semantic Versioning](https://semver.org)
- guidelines
- * Add a link in CHANGELOG.md to the release tag which will be created later
- in this guide
- * Create a new tag using [git-extras](https://github.com/tj/git-extras/blob/master/Commands.md#git-release)
- `git release` command
- * For example: `git release v1.6.0.pre1`
- * These should be the only changes in the PR
- * An example of these changes for `v1.6.0.pre1` can be found in [PR #435](https://github.com/ruby-git/ruby-git/pull/435)
- * Get the PR reviewed, approved and merged to master.
+- Bump the version number in lib/git/version.rb following [Semantic Versioning](https://semver.org)
+ guidelines
+- Add a link in CHANGELOG.md to the release tag which will be created later
+ in this guide
+- Create a new tag using [git-extras](https://github.com/tj/git-extras/blob/master/Commands.md#git-release)
+ `git release` command
+ - For example: `git release v1.6.0.pre1`
+- These should be the only changes in the PR
+- An example of these changes for `v1.6.0.pre1` can be found in [PR #435](https://github.com/ruby-git/ruby-git/pull/435)
+- Get the PR reviewed, approved and merged to master.
## Create a GitHub release
On [the ruby-git releases page](https://github.com/ruby-git/ruby-git/releases),
select `Draft a new release`
- * Select the tag corresponding to the version being released `v1.6.0.pre1`
- * The Target should be `master`
- * For the release description, use the output of [changelog-rs](https://github.com/perlun/changelog-rs)
- * Since the release has not been created yet, you will need to supply
- `changeling-rs` with the current release tag and the tag the new release
- is being created from
- * For example: `changelog-rs . v1.5.0 v1.6.0.pre1`
- * Copy the output, omitting the tag header `## v1.6.0.pre1` and paste into
- the release description
- * The release description can be edited later if needed
- * Select the appropriate value for `This is a pre-release`
- * Since `v1.6.0.pre1` is a pre-release, check `This is a pre-release`
+- Select the tag corresponding to the version being released `v1.6.0.pre1`
+- The Target should be `master`
+- For the release description, use the output of [changelog-rs](https://github.com/perlun/changelog-rs)
+ - A Docker image is provided in [Dockerfile.changelog-rs](https://github.com/ruby-git/ruby-git/blob/master/Dockerfile.changelog-rs)
+ so you don't have to install changelog-rs or the Rust tool chain. To build the
+ Docker image, run this command from this project's root directory:
+ - `docker build --file Dockerfile.changelog-rs --tag changelog-rs .`
+ - To run the changelog-rs command using this image, run the following command
+ from this project's root directory (replace the tag names appropriate for the
+ current release):
+ - `docker run --rm --volume "$PWD:/worktree" changelog-rs v1.5.0 v1.6.0.pre1`
+ - Copy the output, omitting the tag header `## v1.6.0.pre1` and paste into
+ the release description
+ - The release description can be edited later if needed
+- Select the appropriate value for `This is a pre-release`
+ - Since `v1.6.0.pre1` is a pre-release, check `This is a pre-release`
## Build and release the gem
Clone [ruby-git/ruby-git](https://github.com/ruby-git/ruby-git) directly (not a
fork) and ensure your local working copy is on the master branch
- * Verify that you are not on a fork with the command `git remote -v`
- * Verify that the version number is correct by running `rake -T` and inspecting
- the output for the `release[remote]` task
+- Verify that you are not on a fork with the command `git remote -v`
+- Verify that the version number is correct by running `rake -T` and inspecting
+ the output for the `release[remote]` task
Build the git gem and push it to rubygems.org with the command `rake release`
- * Ensure that your `gem sources list` includes `https://rubygems.org` (in my
- case, I usually have my work’s internal gem repository listed)
+- Ensure that your `gem sources list` includes `https://rubygems.org` (in my
+ case, I usually have my work’s internal gem repository listed)
diff --git a/bin/console b/bin/console
new file mode 100755
index 00000000..0199a6fc
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,15 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'bundler/setup'
+require 'git'
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require 'irb'
+IRB.start(__FILE__)
diff --git a/bin/create-release b/bin/create-release
new file mode 100755
index 00000000..fdc8aa83
--- /dev/null
+++ b/bin/create-release
@@ -0,0 +1,506 @@
+#!/usr/bin/env ruby
+
+# Run this script while in the root directory of the project with the default
+# branch checked out.
+
+require 'bump'
+require 'English'
+require 'fileutils'
+require 'optparse'
+require 'tempfile'
+
+# TODO: Right now the default branch and the remote name are hard coded
+
+class Options
+ attr_accessor :current_version, :next_version, :tag, :current_tag, :next_tag, :branch, :quiet
+
+ def initialize
+ yield self if block_given?
+ end
+
+ def release_type
+ raise "release_type not set" if @release_type.nil?
+ @release_type
+ end
+
+ VALID_RELEASE_TYPES = %w(major minor patch)
+
+ def release_type=(release_type)
+ raise 'release_type must be one of: ' + VALID_RELEASE_TYPES.join(', ') unless VALID_RELEASE_TYPES.include?(release_type)
+ @release_type = release_type
+ end
+
+ def quiet
+ @quiet = false unless instance_variable_defined?(:@quiet)
+ @quiet
+ end
+
+ def current_version
+ @current_version ||= Bump::Bump.current
+ end
+
+ def next_version
+ current_version # Save the current version before bumping
+ @next_version ||= Bump::Bump.next_version(release_type)
+ end
+
+ def tag
+ @tag ||= "v#{next_version}"
+ end
+
+ def current_tag
+ @current_tag ||= "v#{current_version}"
+ end
+
+ def next_tag
+ tag
+ end
+
+ def branch
+ @branch ||= "release-#{tag}"
+ end
+
+ def default_branch
+ @default_branch ||= `git remote show '#{remote}'`.match(/HEAD branch: (.*?)$/)[1]
+ end
+
+ def remote
+ @remote ||= 'origin'
+ end
+
+ def to_s
+ <<~OUTPUT
+ release_type='#{release_type}'
+ current_version='#{current_version}'
+ next_version='#{next_version}'
+ tag='#{tag}'
+ branch='#{branch}'
+ quiet=#{quiet}
+ OUTPUT
+ end
+end
+
+class CommandLineParser
+ attr_reader :options
+
+ def initialize
+ @option_parser = OptionParser.new
+ define_options
+ @options = Options.new
+ end
+
+ def parse(args)
+ option_parser.parse!(remaining_args = args.dup)
+ parse_remaining_args(remaining_args)
+ # puts options unless options.quiet
+ options
+ end
+
+ private
+
+ attr_reader :option_parser
+
+ def parse_remaining_args(remaining_args)
+ error_with_usage('No release type specified') if remaining_args.empty?
+ @options.release_type = remaining_args.shift || nil
+ error_with_usage('Too many args') unless remaining_args.empty?
+ end
+
+ def error_with_usage(message)
+ warn <<~MESSAGE
+ ERROR: #{message}
+ #{option_parser}
+ MESSAGE
+ exit 1
+ end
+
+ def define_options
+ option_parser.banner = 'Usage: create_release --help | release-type'
+ option_parser.separator ''
+ option_parser.separator 'Options:'
+
+ define_quiet_option
+ define_help_option
+ end
+
+ def define_quiet_option
+ option_parser.on('-q', '--[no-]quiet', 'Do not show output') do |quiet|
+ options.quiet = quiet
+ end
+ end
+
+ def define_help_option
+ option_parser.on_tail('-h', '--help', 'Show this message') do
+ puts option_parser
+ exit 0
+ end
+ end
+end
+
+class ReleaseAssertions
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def make_assertions
+ bundle_is_up_to_date
+ in_git_repo
+ in_repo_toplevel_directory
+ on_default_branch
+ no_uncommitted_changes
+ local_and_remote_on_same_commit
+ tag_does_not_exist
+ branch_does_not_exist
+ docker_is_running
+ changelog_docker_container_exists
+ gh_command_exists
+ end
+
+ private
+
+ def gh_command_exists
+ print "Checking that the gh command exists..."
+ `which gh > /dev/null 2>&1`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "The gh command was not found"
+ end
+ end
+
+ def docker_is_running
+ print "Checking that docker is installed and running..."
+ `docker info > /dev/null 2>&1`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "Docker is not installed or not running"
+ end
+ end
+
+
+ def changelog_docker_container_exists
+ print "Checking that the changelog docker container exists (might take time to build)..."
+ `docker build --file Dockerfile.changelog-rs --tag changelog-rs . 1>/dev/null`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "Failed to build the changelog-rs docker container"
+ end
+ end
+
+ def bundle_is_up_to_date
+ print "Checking that the bundle is up to date..."
+ if File.exist?('Gemfile.lock')
+ print "Running bundle update..."
+ `bundle update --quiet`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "bundle update failed"
+ end
+ else
+ print "Running bundle install..."
+ `bundle install --quiet`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "bundle install failed"
+ end
+ end
+ end
+
+ def in_git_repo
+ print "Checking that you are in a git repo..."
+ `git rev-parse --is-inside-work-tree --quiet > /dev/null 2>&1`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "You are not in a git repo"
+ end
+ end
+
+ def in_repo_toplevel_directory
+ print "Checking that you are in the repo's toplevel directory..."
+ toplevel_directory = `git rev-parse --show-toplevel`.chomp
+ if toplevel_directory == FileUtils.pwd
+ puts "OK"
+ else
+ error "You are not in the repo's toplevel directory"
+ end
+ end
+
+ def on_default_branch
+ print "Checking that you are on the default branch..."
+ current_branch = `git branch --show-current`.chomp
+ if current_branch == options.default_branch
+ puts "OK"
+ else
+ error "You are not on the default branch '#{default_branch}'"
+ end
+ end
+
+ def no_uncommitted_changes
+ print "Checking that there are no uncommitted changes..."
+ if `git status --porcelain | wc -l`.to_i == 0
+ puts "OK"
+ else
+ error "There are uncommitted changes"
+ end
+ end
+
+ def no_staged_changes
+ print "Checking that there are no staged changes..."
+ if `git diff --staged --name-only | wc -l`.to_i == 0
+ puts "OK"
+ else
+ error "There are staged changes"
+ end
+ end
+
+ def local_and_remote_on_same_commit
+ print "Checking that local and remote are on the same commit..."
+ local_commit = `git rev-parse HEAD`.chomp
+ remote_commit = `git ls-remote '#{options.remote}' '#{options.default_branch}' | cut -f 1`.chomp
+ if local_commit == remote_commit
+ puts "OK"
+ else
+ error "Local and remote are not on the same commit"
+ end
+ end
+
+ def local_tag_does_not_exist
+ print "Checking that local tag '#{options.tag}' does not exist..."
+
+ tags = `git tag --list "#{options.tag}"`.chomp
+ error 'Could not list tags' unless $CHILD_STATUS.success?
+
+ if tags.split.empty?
+ puts 'OK'
+ else
+ error "'#{options.tag}' already exists"
+ end
+ end
+
+ def remote_tag_does_not_exist
+ print "Checking that the remote tag '#{options.tag}' does not exist..."
+ `git ls-remote --tags --exit-code '#{options.remote}' #{options.tag} >/dev/null 2>&1`
+ unless $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "'#{options.tag}' already exists"
+ end
+ end
+
+ def tag_does_not_exist
+ local_tag_does_not_exist
+ remote_tag_does_not_exist
+ end
+
+ def local_branch_does_not_exist
+ print "Checking that local branch '#{options.branch}' does not exist..."
+
+ if `git branch --list "#{options.branch}" | wc -l`.to_i.zero?
+ puts "OK"
+ else
+ error "'#{options.branch}' already exists."
+ end
+ end
+
+ def remote_branch_does_not_exist
+ print "Checking that the remote branch '#{options.branch}' does not exist..."
+ `git ls-remote --heads --exit-code '#{options.remote}' '#{options.branch}' >/dev/null 2>&1`
+ unless $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "'#{options.branch}' already exists"
+ end
+ end
+
+ def branch_does_not_exist
+ local_branch_does_not_exist
+ remote_branch_does_not_exist
+ end
+
+ private
+
+ def print(*args)
+ super unless options.quiet
+ end
+
+ def puts(*args)
+ super unless options.quiet
+ end
+
+ def error(message)
+ warn "ERROR: #{message}"
+ exit 1
+ end
+end
+
+class ReleaseCreator
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def create_release
+ create_branch
+ update_changelog
+ update_version
+ make_release_commit
+ create_tag
+ push_release_commit_and_tag
+ create_github_release
+ create_release_pull_request
+ end
+
+ private
+
+ def create_branch
+ print "Creating branch '#{options.branch}'..."
+ `git checkout -b "#{options.branch}" > /dev/null 2>&1`
+ if $CHILD_STATUS.success?
+ puts "OK"
+ else
+ error "Could not create branch '#{options.branch}'" unless $CHILD_STATUS.success?
+ end
+ end
+
+ def update_changelog
+ print 'Updating CHANGELOG.md...'
+ changelog_lines = File.readlines('CHANGELOG.md')
+ first_entry = changelog_lines.index { |e| e =~ /^## / }
+ error "Could not find changelog insertion point" unless first_entry
+ FileUtils.rm('CHANGELOG.md')
+ File.write('CHANGELOG.md', <<~CHANGELOG.chomp)
+ #{changelog_lines[0..first_entry - 1].join}## #{options.tag}
+
+ See https://github.com/ruby-git/ruby-git/releases/tag/#{options.tag}
+
+ #{changelog_lines[first_entry..].join}
+ CHANGELOG
+ `git add CHANGELOG.md`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error 'Could not stage changes to CHANGELOG.md'
+ end
+ end
+
+ def update_version
+ print 'Updating version...'
+ message, status = Bump::Bump.run(options.release_type, commit: false)
+ error 'Could not bump version' unless status == 0
+ `git add lib/git/version.rb`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error 'Could not stage changes to lib/git/version.rb'
+ end
+ end
+
+ def make_release_commit
+ print 'Making release commit...'
+ `git commit -s -m 'Release #{options.tag}'`
+ error 'Could not make release commit' unless $CHILD_STATUS.success?
+ end
+
+ def create_tag
+ print "Creating tag '#{options.tag}'..."
+ `git tag '#{options.tag}'`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error "Could not create tag '#{options.tag}'"
+ end
+ end
+
+ def push_release_commit_and_tag
+ print "Pushing branch '#{options.branch}' to remote..."
+ `git push --tags --set-upstream '#{options.remote}' '#{options.branch}' > /dev/null 2>&1`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error 'Could not push release commit'
+ end
+ end
+
+ def changelog
+ @changelog ||= begin
+ print "Generating changelog..."
+ pwd = FileUtils.pwd
+ from = options.current_tag
+ to = options.next_tag
+ command = "docker run --rm --volume '#{pwd}:/worktree' changelog-rs '#{from}' '#{to}'"
+ changelog = `#{command}`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ changelog.rstrip.lines[1..].join
+ else
+ error 'Could not generate the changelog'
+ end
+ end
+ end
+
+ def create_github_release
+ Tempfile.create do |f|
+ f.write changelog
+ f.close
+
+ print "Creating GitHub release '#{options.tag}'..."
+ tag = options.tag
+ `gh release create #{tag} --title 'Release #{tag}' --notes-file '#{f.path}' --target #{options.default_branch}`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error 'Could not create release'
+ end
+ end
+ end
+
+ def create_release_pull_request
+ Tempfile.create do |f|
+ f.write <<~PR
+ ### Your checklist for this pull request
+ 🚨Please review the [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/#{options.default_branch}/CONTRIBUTING.md) to this repository.
+
+ - [X] Ensure all commits include DCO sign-off.
+ - [X] Ensure that your contributions pass unit testing.
+ - [X] Ensure that your contributions contain documentation if applicable.
+
+ ### Description
+ #{changelog}
+ PR
+ f.close
+
+ print "Creating GitHub pull request..."
+ `gh pr create --title 'Release #{options.tag}' --body-file '#{f.path}' --base '#{options.default_branch}'`
+ if $CHILD_STATUS.success?
+ puts 'OK'
+ else
+ error 'Could not create release pull request'
+ end
+ end
+ end
+
+ def error(message)
+ warn "ERROR: #{message}"
+ exit 1
+ end
+
+ def print(*args)
+ super unless options.quiet
+ end
+
+ def puts(*args)
+ super unless options.quiet
+ end
+end
+
+options = CommandLineParser.new.parse(ARGV)
+ReleaseAssertions.new(options).make_assertions
+ReleaseCreator.new(options).create_release
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 00000000..dce67d86
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/git.gemspec b/git.gemspec
index 53994aa2..ac2ff000 100644
--- a/git.gemspec
+++ b/git.gemspec
@@ -26,8 +26,10 @@ Gem::Specification.new do |s|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
s.requirements = ['git 1.6.0.0, or greater']
+ s.add_runtime_dependency 'addressable', '~> 2.8'
s.add_runtime_dependency 'rchardet', '~> 1.8'
+ s.add_development_dependency 'bump', '~> 0.10'
s.add_development_dependency 'minitar', '~> 0.9'
s.add_development_dependency 'rake', '~> 13.0'
s.add_development_dependency 'test-unit', '~> 3.3'
@@ -41,6 +43,6 @@ Gem::Specification.new do |s|
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
s.files = Dir.chdir(File.expand_path(__dir__)) do
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(tests|spec|features)/}) }
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(tests|spec|features|bin)/}) }
end
end
diff --git a/lib/git.rb b/lib/git.rb
index eb4c7cce..1da03ce5 100644
--- a/lib/git.rb
+++ b/lib/git.rb
@@ -9,6 +9,8 @@
require 'git/branches'
require 'git/config'
require 'git/diff'
+require 'git/encoding_utils'
+require 'git/escaped_path'
require 'git/index'
require 'git/lib'
require 'git/log'
@@ -19,6 +21,7 @@
require 'git/status'
require 'git/stash'
require 'git/stashes'
+require 'git/url'
require 'git/version'
require 'git/working_directory'
require 'git/worktree'
@@ -104,11 +107,23 @@ def self.bare(git_dir, options = {})
# @see https://git-scm.com/docs/git-clone git clone
# @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs
#
- # @param [URI, Pathname] repository The (possibly remote) repository to clone
+ # @param repository_url [URI, Pathname] The (possibly remote) repository url to clone
# from. See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a)
# for more information.
#
- # @param [Pathname] name The directory to clone into.
+ # @param directory [Pathname, nil] The directory to clone into
+ #
+ # If `directory` is a relative directory it is relative to the `path` option if
+ # given. If `path` is not given, `directory` is relative to the current working
+ # directory.
+ #
+ # If `nil`, `directory` will be set to the basename of the last component of
+ # the path from the `repository_url`. For example, for the URL:
+ # `https://github.com/org/repo.git`, `directory` will be set to `repo`.
+ #
+ # If the last component of the path is `.git`, the next-to-last component of
+ # the path is used. For example, for the URL `/Users/me/foo/.git`, `directory`
+ # will be set to `foo`.
#
# @param [Hash] options The options for this command (see list of valid
# options below)
@@ -155,8 +170,10 @@ def self.bare(git_dir, options = {})
# @return [Git::Base] an object that can execute git commands in the context
# of the cloned local working copy or cloned repository.
#
- def self.clone(repository, name, options = {})
- Base.clone(repository, name, options)
+ def self.clone(repository_url, directory = nil, options = {})
+ clone_to_options = options.select { |key, _value| %i[bare mirror].include?(key) }
+ directory ||= Git::URL.clone_to(repository_url, **clone_to_options)
+ Base.clone(repository_url, directory, options)
end
# Export the current HEAD (or a branch, if options[:branch]
@@ -212,6 +229,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 94e59c21..c58bafe8 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))
+ def self.clone(repository_url, directory, options = {})
+ new_options = Git::Lib.new(nil, options[:log]).clone(repository_url, directory, options)
+ normalize_paths(new_options, bare: options[:bare] || options[:mirror])
+ 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
@@ -137,13 +119,14 @@ def chdir # :yields: the Git::Path
#g.config('user.name', 'Scott Chacon') # sets value
#g.config('user.email', 'email@email.com') # sets value
+ #g.config('user.email', 'email@email.com', file: 'path/to/custom/config) # sets value in file
#g.config('user.name') # returns 'Scott Chacon'
#g.config # returns whole config hash
- def config(name = nil, value = nil)
- if(name && value)
+ def config(name = nil, value = nil, options = {})
+ if name && value
# set value
- lib.config_set(name, value)
- elsif (name)
+ lib.config_set(name, value, options)
+ elsif name
# return value
lib.config_get(name)
else
@@ -286,6 +269,7 @@ def reset_hard(commitish = nil, opts = {})
# options:
# :force
# :d
+ # :ff
#
def clean(opts = {})
self.lib.clean(opts)
@@ -352,7 +336,11 @@ def checkout_file(version, file)
# fetches changes from a remote branch - this does not modify the working directory,
# it just gets the changes from the remote if there are any
- def fetch(remote = 'origin', opts={})
+ def fetch(remote = 'origin', opts = {})
+ if remote.is_a?(Hash)
+ opts = remote
+ remote = nil
+ end
self.lib.fetch(remote, opts)
end
@@ -569,7 +557,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
@@ -594,6 +581,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/diff.rb b/lib/git/diff.rb
index e34ba82c..78f02745 100644
--- a/lib/git/diff.rb
+++ b/lib/git/diff.rb
@@ -130,8 +130,8 @@ def process_full_diff
final = {}
current_file = nil
@full_diff.split("\n").each do |line|
- if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line)
- current_file = m[1]
+ if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line)
+ current_file = Git::EscapedPath.new(m[2]).unescape
final[current_file] = defaults.merge({:patch => line, :path => current_file})
else
if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line)
diff --git a/lib/git/encoding_utils.rb b/lib/git/encoding_utils.rb
new file mode 100644
index 00000000..332b5461
--- /dev/null
+++ b/lib/git/encoding_utils.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'rchardet'
+
+module Git
+ # Method that can be used to detect and normalize string encoding
+ module EncodingUtils
+ def self.default_encoding
+ __ENCODING__.name
+ end
+
+ def self.best_guess_encoding
+ # Encoding::ASCII_8BIT.name
+ Encoding::UTF_8.name
+ end
+
+ def self.detected_encoding(str)
+ CharDet.detect(str)['encoding'] || best_guess_encoding
+ end
+
+ def self.encoding_options
+ { invalid: :replace, undef: :replace }
+ end
+
+ def self.normalize_encoding(str)
+ return str if str.valid_encoding? && str.encoding.name == default_encoding
+
+ return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding?
+
+ str.encode(default_encoding, detected_encoding(str), **encoding_options)
+ end
+ end
+end
diff --git a/lib/git/escaped_path.rb b/lib/git/escaped_path.rb
new file mode 100644
index 00000000..7519a3ac
--- /dev/null
+++ b/lib/git/escaped_path.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Git
+ # Represents an escaped Git path string
+ #
+ # Git commands that output paths (e.g. ls-files, diff), will escape usual
+ # characters in the path with backslashes in the same way C escapes control
+ # characters (e.g. \t for TAB, \n for LF, \\ for backslash) or bytes with values
+ # larger than 0x80 (e.g. octal \302\265 for "micro" in UTF-8).
+ #
+ # @example
+ # Git::GitPath.new('\302\265').unescape # => "µ"
+ #
+ class EscapedPath
+ UNESCAPES = {
+ 'a' => 0x07,
+ 'b' => 0x08,
+ 't' => 0x09,
+ 'n' => 0x0a,
+ 'v' => 0x0b,
+ 'f' => 0x0c,
+ 'r' => 0x0d,
+ 'e' => 0x1b,
+ '\\' => 0x5c,
+ '"' => 0x22,
+ "'" => 0x27
+ }.freeze
+
+ attr_reader :path
+
+ def initialize(path)
+ @path = path
+ end
+
+ # Convert an escaped path to an unescaped path
+ def unescape
+ bytes = escaped_path_to_bytes(path)
+ str = bytes.pack('C*')
+ str.force_encoding(Encoding::UTF_8)
+ end
+
+ private
+
+ def extract_octal(path, index)
+ [path[index + 1..index + 4].to_i(8), 4]
+ end
+
+ def extract_escape(path, index)
+ [UNESCAPES[path[index + 1]], 2]
+ end
+
+ def extract_single_char(path, index)
+ [path[index].ord, 1]
+ end
+
+ def next_byte(path, index)
+ if path[index] == '\\' && path[index + 1] >= '0' && path[index + 1] <= '7'
+ extract_octal(path, index)
+ elsif path[index] == '\\' && UNESCAPES.include?(path[index + 1])
+ extract_escape(path, index)
+ else
+ extract_single_char(path, index)
+ end
+ end
+
+ def escaped_path_to_bytes(path)
+ index = 0
+ [].tap do |bytes|
+ while index < path.length
+ byte, chars_used = next_byte(path, index)
+ bytes << byte
+ index += chars_used
+ end
+ end
+ end
+ end
+end
diff --git a/lib/git/lib.rb b/lib/git/lib.rb
index d69205f4..f24bf2da 100644
--- a/lib/git/lib.rb
+++ b/lib/git/lib.rb
@@ -1,4 +1,3 @@
-require 'rchardet'
require 'tempfile'
require 'zlib'
@@ -71,10 +70,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
@@ -94,9 +95,9 @@ def init(opts={})
#
# @return [Hash] the options to pass to {Git::Base.new}
#
- def clone(repository, name, opts = {})
+ def clone(repository_url, directory, opts = {})
@path = opts[:path] || '.'
- clone_dir = opts[:path] ? File.join(@path, name) : name
+ clone_dir = opts[:path] ? File.join(@path, directory) : directory
arr_opts = []
arr_opts << '--bare' if opts[:bare]
@@ -105,11 +106,11 @@ def clone(repository, name, opts = {})
arr_opts << '--config' << opts[:config] if opts[:config]
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
arr_opts << '--recursive' if opts[:recursive]
- arr_opts << "--mirror" if opts[:mirror]
+ arr_opts << '--mirror' if opts[:mirror]
arr_opts << '--'
- arr_opts << repository
+ arr_opts << repository_url
arr_opts << clone_dir
command('clone', arr_opts)
@@ -418,7 +419,7 @@ def grep(string, opts = {})
hsh = {}
command_lines('grep', grep_opts).each do |line|
- if m = /(.*)\:(\d+)\:(.*)/.match(line)
+ if m = /(.*?)\:(\d+)\:(.*)/.match(line)
hsh[m[1]] ||= []
hsh[m[1]] << [m[2].to_i, m[3]]
end
@@ -586,8 +587,12 @@ def show(objectish=nil, path=nil)
## WRITE COMMANDS ##
- def config_set(name, value)
- command('config', name, value)
+ def config_set(name, value, options = {})
+ if options[:file].to_s.empty?
+ command('config', name, value)
+ else
+ command('config', '--file', options[:file], name, value)
+ end
end
def global_config_set(name, value)
@@ -645,6 +650,7 @@ def remove(path = '.', opts = {})
# :date
# :no_verify
# :allow_empty_message
+ # :gpg_sign
#
# @param [String] message the commit message to be used
# @param [Hash] opts the commit options to be used
@@ -658,6 +664,14 @@ def commit(message, opts = {})
arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String
arr_opts << '--no-verify' if opts[:no_verify]
arr_opts << '--allow-empty-message' if opts[:allow_empty_message]
+ if opts[:gpg_sign]
+ arr_opts <<
+ if opts[:gpg_sign] == true
+ '--gpg-sign'
+ else
+ "--gpg-sign=#{opts[:gpg_sign]}"
+ end
+ end
command('commit', arr_opts)
end
@@ -672,6 +686,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]
@@ -762,6 +777,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]
@@ -865,13 +881,18 @@ def tag(name, *opts)
command('tag', arr_opts)
end
-
def fetch(remote, opts)
- arr_opts = [remote]
- arr_opts << opts[:ref] if opts[:ref]
+ arr_opts = []
+ arr_opts << '--all' if opts[:all]
arr_opts << '--tags' if opts[:t] || opts[:tags]
arr_opts << '--prune' if opts[:p] || opts[:prune]
+ arr_opts << '--prune-tags' if opts[:P] || opts[:'prune-tags']
+ arr_opts << '--force' if opts[:f] || opts[:force]
arr_opts << '--unshallow' if opts[:unshallow]
+ arr_opts << '--depth' << opts[:depth] if opts[:depth]
+ arr_opts << '--' if remote || opts[:ref]
+ arr_opts << remote if remote
+ arr_opts << opts[:ref] if opts[:ref]
command('fetch', arr_opts)
end
@@ -1077,7 +1098,8 @@ def command(cmd, *opts, &block)
end
global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil?
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
- global_opts << ["-c", "color.ui=false"]
+ global_opts << %w[-c core.quotePath=true]
+ global_opts << %w[-c color.ui=false]
opts = [opts].flatten.map {|s| escape(s) }.join(' ')
@@ -1168,35 +1190,10 @@ def log_path_options(opts)
arr_opts
end
- def default_encoding
- __ENCODING__.name
- end
-
- def best_guess_encoding
- # Encoding::ASCII_8BIT.name
- Encoding::UTF_8.name
- end
-
- def detected_encoding(str)
- CharDet.detect(str)['encoding'] || best_guess_encoding
- end
-
- def encoding_options
- { invalid: :replace, undef: :replace }
- end
-
- def normalize_encoding(str)
- return str if str.valid_encoding? && str.encoding.name == default_encoding
-
- return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding?
-
- str.encode(default_encoding, detected_encoding(str), **encoding_options)
- end
-
def run_command(git_cmd, &block)
return IO.popen(git_cmd, &block) if block_given?
- `#{git_cmd}`.lines.map { |l| normalize_encoding(l) }.join
+ `#{git_cmd}`.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join
end
def escape(s)
@@ -1208,8 +1205,9 @@ def escape_for_sh(s)
end
def escape_for_windows(s)
- # Windows does not need single quote escaping inside double quotes
- %Q{"#{s}"}
+ # Escape existing double quotes in s and then wrap the result with double quotes
+ escaped_string = s.to_s.gsub('"','\\"')
+ %Q{"#{escaped_string}"}
end
def windows_platform?
@@ -1217,6 +1215,5 @@ def windows_platform?
win_platform_regex = /mingw|mswin/
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
end
-
end
end
diff --git a/lib/git/url.rb b/lib/git/url.rb
new file mode 100644
index 00000000..af170615
--- /dev/null
+++ b/lib/git/url.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require 'addressable/uri'
+
+module Git
+ # Methods for parsing a Git URL
+ #
+ # Any URL that can be passed to `git clone` can be parsed by this class.
+ #
+ # @see https://git-scm.com/docs/git-clone#_git_urls GIT URLs
+ # @see https://github.com/sporkmonger/addressable Addresable::URI
+ #
+ # @api public
+ #
+ class URL
+ # Regexp used to match a Git URL with an alternative SSH syntax
+ # such as `user@host:path`
+ #
+ GIT_ALTERNATIVE_SSH_SYNTAX = %r{
+ ^
+ (?:(?[^@/]+)@)? # user or nil
+ (?[^:/]+) # host is required
+ :(?!/) # : serparator is required, but must not be followed by /
+ (?.*?) # path is required
+ $
+ }x.freeze
+
+ # Parse a Git URL and return an Addressable::URI object
+ #
+ # The URI returned can be converted back to a string with 'to_s'. This is
+ # guaranteed to return the same URL string that was parsed.
+ #
+ # @example
+ # uri = Git::URL.parse('https://github.com/ruby-git/ruby-git.git')
+ # #=> #
+ # uri.scheme #=> "https"
+ # uri.host #=> "github.com"
+ # uri.path #=> "/ruby-git/ruby-git.git"
+ #
+ # Git::URL.parse('/Users/James/projects/ruby-git')
+ # #=> #
+ #
+ # @param url [String] the Git URL to parse
+ #
+ # @return [Addressable::URI] the parsed URI
+ #
+ def self.parse(url)
+ if !url.start_with?('file:') && (m = GIT_ALTERNATIVE_SSH_SYNTAX.match(url))
+ GitAltURI.new(user: m[:user], host: m[:host], path: m[:path])
+ else
+ Addressable::URI.parse(url)
+ end
+ end
+
+ # The directory `git clone` would use for the repository directory for the given URL
+ #
+ # @example
+ # Git::URL.clone_to('https://github.com/ruby-git/ruby-git.git') #=> 'ruby-git'
+ #
+ # @param url [String] the Git URL containing the repository directory
+ #
+ # @return [String] the name of the repository directory
+ #
+ def self.clone_to(url, bare: false, mirror: false)
+ uri = parse(url)
+ path_parts = uri.path.split('/')
+ path_parts.pop if path_parts.last == '.git'
+ directory = path_parts.last
+ if bare || mirror
+ directory += '.git' unless directory.end_with?('.git')
+ elsif directory.end_with?('.git')
+ directory = directory[0..-5]
+ end
+ directory
+ end
+ end
+
+ # The URI for git's alternative scp-like syntax
+ #
+ # This class is necessary to ensure that #to_s returns the same string
+ # that was passed to the initializer.
+ #
+ # @api public
+ #
+ class GitAltURI < Addressable::URI
+ # Create a new GitAltURI object
+ #
+ # @example
+ # uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
+ # uri.to_s #=> 'james@github.com/james/ruby-git'
+ #
+ # @param user [String, nil] the user from the URL or nil
+ # @param host [String] the host from the URL
+ # @param path [String] the path from the URL
+ #
+ def initialize(user:, host:, path:)
+ super(scheme: 'git-alt', user: user, host: host, path: path)
+ end
+
+ # Convert the URI to a String
+ #
+ # Addressible::URI forces path to be absolute by prepending a '/' to the
+ # path. This method removes the '/' when converting back to a string
+ # since that is what is expected by git. The following is a valid git URL:
+ #
+ # `james@github.com:ruby-git/ruby-git.git`
+ #
+ # and the following (with the initial '/'' in the path) is NOT a valid git URL:
+ #
+ # `james@github.com:/ruby-git/ruby-git.git`
+ #
+ # @example
+ # uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git')
+ # uri.path #=> '/james/ruby-git'
+ # uri.to_s #=> 'james@github.com:james/ruby-git'
+ #
+ # @return [String] the URI as a String
+ #
+ def to_s
+ if user
+ "#{user}@#{host}:#{path[1..-1]}"
+ else
+ "#{host}:#{path[1..-1]}"
+ end
+ end
+ end
+end
diff --git a/lib/git/version.rb b/lib/git/version.rb
index 05b60fb1..87bffb51 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.8.1'
+ VERSION='1.11.0'
end
diff --git a/tests/files/working/colon_numbers.txt b/tests/files/working/colon_numbers.txt
new file mode 100644
index 00000000..e76778b7
--- /dev/null
+++ b/tests/files/working/colon_numbers.txt
@@ -0,0 +1 @@
+Grep regex doesn't like this:4342: because it is bad
diff --git a/tests/files/working/dot_git/index b/tests/files/working/dot_git/index
index ef22be73..9896710a 100644
Binary files a/tests/files/working/dot_git/index and b/tests/files/working/dot_git/index differ
diff --git a/tests/files/working/dot_git/logs/HEAD b/tests/files/working/dot_git/logs/HEAD
index a48f0312..cbe9b80e 100644
--- a/tests/files/working/dot_git/logs/HEAD
+++ b/tests/files/working/dot_git/logs/HEAD
@@ -79,3 +79,4 @@ a3db7143944dcfa006fefe7fb49c48793cb29ade 34a566d193dc4702f03149969a2aad144323156
6f09de178a27f7702c37907fd614c3c122d33c30 faf8d899a0f123c3c5def10857920be1c930e8ed Scott Chacon 1378910110 -0400 commit (merge): Merge commit '4ce44a75510cbfe200b131fdbcc56a86f1b2dc08' into cherry
faf8d899a0f123c3c5def10857920be1c930e8ed 5e392652a881999392c2757cf9b783c5d47b67f7 Scott Chacon 1378910135 -0400 checkout: moving from cherry to master
5e392652a881999392c2757cf9b783c5d47b67f7 5e53019b3238362144c2766f02a2c00d91fcc023 Scott Chacon 1378910138 -0400 checkout: moving from master to git_grep
+5e53019b3238362144c2766f02a2c00d91fcc023 46abbf07e3c564c723c7c039a43ab3a39e5d02dd Scott Chacon 1647231179 +1300 commit: add example for grep with colon and numbers
diff --git a/tests/files/working/dot_git/logs/refs/heads/git_grep b/tests/files/working/dot_git/logs/refs/heads/git_grep
index 0123a146..22a6f143 100644
--- a/tests/files/working/dot_git/logs/refs/heads/git_grep
+++ b/tests/files/working/dot_git/logs/refs/heads/git_grep
@@ -3,3 +3,4 @@
a3db7143944dcfa006fefe7fb49c48793cb29ade 34a566d193dc4702f03149969a2aad1443231560 scott Chacon 1194632975 -0800 commit: modified to not show up
34a566d193dc4702f03149969a2aad1443231560 935badc874edd62a8629aaf103418092c73f0a56 scott Chacon 1194633382 -0800 commit: more search help
935badc874edd62a8629aaf103418092c73f0a56 5e53019b3238362144c2766f02a2c00d91fcc023 scott Chacon 1194720731 -0800 commit: diff test
+5e53019b3238362144c2766f02a2c00d91fcc023 46abbf07e3c564c723c7c039a43ab3a39e5d02dd Scott Chacon 1647231179 +1300 commit: add example for grep with colon and numbers
diff --git a/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd b/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd
new file mode 100644
index 00000000..9675e231
--- /dev/null
+++ b/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd
@@ -0,0 +1 @@
+xQj0DSH+A('XVG0<I-ezS"YƜ2ėe#K9сuq/>&9lQMeᶲ+_!|ӌ9»֚Aal
7=Àdz,/RL
\ No newline at end of file
diff --git a/tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d b/tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d
new file mode 100644
index 00000000..8ea983cf
Binary files /dev/null and b/tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d differ
diff --git a/tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 b/tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373
new file mode 100644
index 00000000..28df1dc0
Binary files /dev/null and b/tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 differ
diff --git a/tests/files/working/dot_git/refs/heads/git_grep b/tests/files/working/dot_git/refs/heads/git_grep
index 475c8590..0392fbf4 100644
--- a/tests/files/working/dot_git/refs/heads/git_grep
+++ b/tests/files/working/dot_git/refs/heads/git_grep
@@ -1 +1 @@
-5e53019b3238362144c2766f02a2c00d91fcc023
+46abbf07e3c564c723c7c039a43ab3a39e5d02dd
diff --git a/tests/files/working/dot_git/refs/tags/grep_colon_numbers b/tests/files/working/dot_git/refs/tags/grep_colon_numbers
new file mode 100644
index 00000000..0392fbf4
--- /dev/null
+++ b/tests/files/working/dot_git/refs/tags/grep_colon_numbers
@@ -0,0 +1 @@
+46abbf07e3c564c723c7c039a43ab3a39e5d02dd
diff --git a/tests/test_helper.rb b/tests/test_helper.rb
index b04f3f4d..31ed8477 100644
--- a/tests/test_helper.rb
+++ b/tests/test_helper.rb
@@ -97,4 +97,67 @@ def with_custom_env_variables(&block)
Git::Lib::ENV_VARIABLE_NAMES.each { |k| ENV[k] = saved_env[k] }
end
end
+
+ # Assert that the expected command line args are generated for a given Git::Lib method
+ #
+ # This assertion generates an empty git repository and then runs calls
+ # Git::Base method named by `git_cmd` passing that method `git_cmd_args`.
+ #
+ # Before calling `git_cmd`, this method stubs the `Git::Lib#command` method to
+ # capture the args sent to it by `git_cmd`. These args are captured into
+ # `actual_command_line`.
+ #
+ # assert_equal is called comparing the given `expected_command_line` to
+ # `actual_command_line`.
+ #
+ # @example Fetch with no args
+ # expected_command_line = ['fetch', '--', 'origin']
+ # git_cmd = :fetch
+ # git_cmd_args = []
+ # assert_command_line(expected_command_line, git_cmd, git_cmd_args)
+ #
+ # @example Fetch with some args
+ # expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master']
+ # git_cmd = :fetch
+ # git_cmd_args = ['origin', ref: 'master', depth: '2']
+ # assert_command_line(expected_command_line, git_cmd, git_cmd_args)
+ #
+ # @example Fetch all
+ # expected_command_line = ['fetch', '--all']
+ # git_cmd = :fetch
+ # git_cmd_args = [all: true]
+ # assert_command_line(expected_command_line, git_cmd, git_cmd_args)
+ #
+ # @param expected_command_line [Array] The expected arguments to be sent to Git::Lib#command
+ # @param git_cmd [Symbol] the method to be called on the Git::Base object
+ # @param git_cmd_args [Array