Contributing to the git gem
- Summary
- How to contribute
- How to report an issue or request a feature
- Local development setup
- How to submit a code or documentation change
- Branch strategy
- AI-assisted contributions
- Design philosophy
- Command-layer neutrality
- Wrapping a git command
- Coding standards
- Building a specific version of the Git command-line
- Licensing
Summary
Thank you for your interest in contributing to the ruby-git project.
This document provides guidelines for contributing to the ruby-git project. While
these guidelines may not cover every situation, we encourage you to use your best
judgment when contributing.
If you have suggestions for improving these guidelines, please propose changes via a pull request.
Please also review and adhere to our Code of Conduct when participating in the project. Governance and maintainer expectations are described in GOVERNANCE.md.
How to contribute
You can contribute in the following ways:
How to report an issue or request a feature
ruby-git utilizes GitHub
Issues
for issue tracking and feature requests.
To report an issue or request a feature, please create a ruby-git GitHub
issue. Fill in the template as
thoroughly as possible to describe the issue or feature request.
Local development setup
Before submitting a change, set up a working local development environment.
bin/setup automates the bootstrap and fails fast with a clear message when a
prerequisite is missing.
Prerequisites
| Tool | Required version | Notes |
|---|---|---|
| Ruby | >= 3.2.0 (matches required_ruby_version in git.gemspec) |
A version manager such as rbenv, asdf, chruby, or rvm is recommended so you can match the project's CI matrix. |
| Bundler | Any 2.x or 4.x | Install with gem install bundler. |
| git | >= 2.28.0 (matches git.gemspec requirements) |
Older git versions are not supported and the test suite will not pass against them. |
| Node.js / npm | Optional | Required only to install the local Conventional Commit commit-msg hook (Husky + commitlint). If npm is missing, bin/setup will warn and continue — CI will still validate commit messages. |
Bootstrap the project
From the project root, run:
bin/setup
bin/setup will:
- Verify the prerequisites above and exit with a non-zero status if any are missing or out of date.
- Run
bundle installto install Ruby gem dependencies. - Run
npm install(when npm is available) to install the Conventional Commitcommit-msghook used by this project (Husky + commitlint). A separatepre-commithook is also installed that blocks direct commits to the protected branches (main,4.x). - Verify the toolchain by running
bundle exec rake --tasks.
Verify the toolchain
Once bin/setup succeeds, confirm the full test and lint suite passes locally:
bundle exec rake
This is the same default task that runs in CI and is the canonical way to validate a change before requesting review.
Contributor validation policy
Contributors are expected to run bundle exec rake locally and confirm it
passes before requesting review on a pull request — trivial documentation-only
fixes (e.g., typo corrections in markdown files) are excepted. "CI passed" is
not a substitute for local validation; it is a backstop. This applies equally to
human-authored and AI-assisted contributions — see
AI-assisted contributions.
How to submit a code or documentation change
There is a three-step process for submitting code or documentation changes:
- Commit your changes to a fork of
ruby-gitusing Conventional Commits - Create a pull request
- Get your pull request reviewed
Commit your changes to a fork of ruby-git
Make your changes in a fork of the ruby-git repository.
Create a pull request
If you are not familiar with GitHub Pull Requests, please refer to this article.
Follow the instructions in the pull request template.
Get your pull request reviewed
Code review takes place in a GitHub pull request using the GitHub pull request review feature.
Once your pull request is ready for review, request a review from at least one maintainer and any other contributors you deem necessary.
During the review process, you may need to make additional commits, which should be
squashed. Additionally, you will need to rebase your branch to the latest version of
the target branch (e.g., main or 4.x) before merging.
At least one approval from a project maintainer is required before your pull request can be merged. The maintainer is responsible for ensuring that the pull request meets the project's coding standards.
Before requesting review
Before moving a pull request out of draft or requesting a review, confirm:
- [ ]
bundle exec rakepasses locally on your branch (see Local development setup). - [ ] New or changed code has accompanying tests under
spec/(see Unit tests). - [ ] Every commit message follows Conventional Commits.
- [ ] User-facing changes are documented in
README.mdand/or YARD as appropriate.
These checks mirror what reviewers and CI will look for; running them locally first keeps the review cycle short.
Branch strategy
This project maintains two active branches:
main: Active development for the next major version (v5.0.0+). This branch may contain breaking changes.4.x: Maintenance branch for the v4.x release series. This branch receives bug fixes and backward-compatible improvements only.
Important: Never commit directly to main or 4.x. All changes must be
submitted via pull requests from feature branches. This ensures proper code review,
CI validation, and maintains a clean commit history.
When submitting a pull request:
- New features and breaking changes: Target the
mainbranch - Bug fixes: Target
main, and maintainers will backport to4.xif applicable - Security fixes: Target both branches or
4.xif the issue only affects v4.x
AI-assisted contributions
AI-assisted contributions are welcome. Please review and apply our AI Policy before submitting changes. You are responsible for understanding and verifying any AI-assisted work included in PRs and ensuring it meets our standards for quality, security, and licensing.
The human submitter — not the AI agent — is responsible for ensuring that
bundle exec rake passes locally before requesting review. This is true even
when the change was authored end-to-end by an agent. "The agent ran the tests"
and "CI is green" are not substitutes for the submitter running
the local validation step themselves; CI is a
backstop, not a primary validation surface.
Design philosophy
The git gem is designed as a lightweight wrapper around the git command-line
tool, providing Ruby developers with a simple and intuitive interface for
programmatically interacting with Git.
This gem adheres to the "principle of least surprise," ensuring that it does not
introduce unnecessary abstraction layers or modify Git's core functionality. Instead,
the gem maintains a close alignment with the existing git command-line interface,
avoiding extensions or alterations that could lead to unexpected behaviors.
By following this philosophy, the git gem allows users to leverage their existing
knowledge of Git while benefiting from the expressiveness and power of Ruby's syntax
and paradigms.
Command-layer neutrality
Command classes (Git::Commands::*) are faithful, neutral representations of the
git CLI. They declare every option via the DSL but never embed policy choices —
output-control flags, editor suppression, progress output, verbose mode, etc.
Policy decisions belong to the facade (Git::Lib), which sets safe defaults at
each call site. Callers may override those defaults when they have a legitimate
reason (e.g., running in a TTY-attached environment where an editor is desired).
This principle serves two purposes: it keeps the command layer a reusable, unbiased interface to git, and it supports this gem's non-interactive execution model (git is never allowed to prompt for input, open an editor, or wait for TTY interaction by default). The execution layer provides an unconditional safety net regardless of what the caller passes.
The three architectural layers each play a distinct role:
| Layer | Responsibility | Mechanism |
|---|---|---|
Command (Git::Commands::*) |
Neutral git CLI interface | Declares options via DSL (e.g. flag_option :edit, negatable: true) — no policy |
Facade (Git::Lib) |
Safe defaults | Sets policy options at each call site (e.g. edit: false); callers may override |
Execution (Git::CommandLine) |
Unconditional safety net | GIT_EDITOR='true' in every subprocess environment |
Anti-pattern:
literal '--no-edit',literal '--verbose',literal '--no-progress'inside a command class — embeds policy in the wrong layer.Correct pattern:
flag_option :edit, negatable: truein the command;edit: falsepassed from the facade call site.
Wrapping a git command
Note: This documentation reflects Phase 2 (Strangler Fig) of the architectural redesign. It will be updated in Phase 3 when
Git::Repositorybecomes the primary public API andGit::Libis bypassed. Currently,Git::Baseremains the public API andGit::Libacts as the delegation layer.
This section guides you through wrapping a git command. The first subsections focus on API design: where methods belong, how to name them, and how to handle parameters and output. These describe the public interface that gem users will see.
From design to implementation then shows how to
structure your code using the gem's three-layer architecture. Note that while we are
transitioning to Git::Repository, the current public API is Git::Base, which
delegates to Git::Lib, which in turn delegates to internal command classes.
Note: When adding new git command wrappers, always use the new architecture described in "From design to implementation" with
Git::Commands::*classes and the Arguments DSL. The gem is being incrementally migrated fromGit::Libto this pattern. Do not add new methods directly toGit::Lib.
Method placement
When implementing a git command, first determine what type of command it is. This determines where to implement it in the Ruby API:
Note: These placement guidelines define the public API. Always add public methods to
Gitmodule orGit::Base(which acts as the current facade forGit::Repository), even though the implementation will be in aGit::Commands::*class.
Repository factory methods are implemented on the Git module. Use these to
obtain a repository object for subsequent operations:
repo = Git.clone('https://github.com/user/repo.git', 'local_path')
repo = Git.init('new_repo')
repo = Git.open('.')
Repository-scoped commands operate within a repository context. Implement these
Git::Base instance methods:
repo.add('file.txt')
repo.commit('Add file')
repo.log
Non-repository commands do not require a repository context. Implement these as
methods on the Git module:
Git.config_get('user.name', global: true)
Git.config_set('user.email', 'user@example.com', global: true)
Some commands, like git config, can operate in multiple contexts:
- On the
Gitmodule: A scope parameter (global: true,system: true) orfile:parameter is required. Thelocal:andworktree:options are not allowed since they require a repository. - On a
Git::Baseinstance: The command defaults to the repository's local scope. Theworktree: trueoption is also available.
Method naming
Each method corresponds directly to a git command. For example, the git add
command is implemented as Git::Base#add, and the git ls-files command is
implemented as Git::Base#ls_files.
When a single Git command serves multiple distinct purposes, method names should use the git command name as a prefix, followed by a descriptive suffix indicating the specific function. The suffix should correspond to the git option that distinguishes the behavior.
For example, git config supports --get, --set, --list, --unset, and other
options. These are implemented as separate methods:
repo.config_get('user.name') # git config --get user.name
repo.config_set('user.name', 'Scott') # git config user.name Scott
repo.config_list # git config --list
repo.config_unset('user.name') # git config --unset user.name
repo.config_get_all('remote.origin.url') # git config --get-all remote.origin.url
To enhance usability, aliases may be introduced to provide more user-friendly method names where appropriate.
See also Output processing for when different output formats require separate methods.
Result class naming
Parsed result objects returned from facade methods follow a reserved suffix convention:
*Info— a parsed metadata struct returned from a query (e.g.,BranchInfo,TagInfo,StashInfo,DiffInfo). Always lives in the top-levelGit::namespace.*Result— the outcome of a mutating or destructive operation (e.g.,BranchDeleteResult,TagDeleteResult). Also lives inGit::.
Do not use these suffixes on Git::Commands::* command classes — those are
subprocess runners, not data objects. A reader seeing Commands::Foo::BarInfo
expects a parsed struct, not a class that shells out to git.
Parameter naming
Parameters within the git gem methods are named after their corresponding long
command-line options, ensuring familiarity and ease of use for developers already
accustomed to Git.
For example, git config --global becomes global: true, and git config --file
becomes file: '/path/to/config'.
As a lightweight wrapper, the gem passes options directly to the git command-line. This means git itself will validate option combinations and report errors. This approach is preferred as long as the error messages returned by git are actionable and understandable for users of the gem.
When multiple options are mutually exclusive (like --global, --local,
--system), only one may be specified. Providing more than one will raise an
ArgumentError.
Note that not all Git command options are supported.
Parameter values
This section defines how git command-line options and positional arguments map to Ruby method parameters. Contributors must follow these conventions:
Options
Git command-line options are passed as keyword arguments in the Ruby API. Methods
accept these via an options splat parameter (e.g., def replace(object, replacement,
**options)). Each option is mapped to a keyword argument as described below.
Boolean flags: Git options like
--globalor--bareare mapped toglobal: trueorbare: true. Omit the key or usefalseto leave the flag unset.git config --global→global: true
Negated boolean flags: Options like
--no-reflogsare mapped tono_reflogs: true.git branch --no-reflogs→no_reflogs: true
Value options: Options that take a value, such as
--file <path>or--author <name>, are mapped asfile: '/path',author: 'Name'.git config --file /tmp/config→file: '/tmp/config'
Options with optional values: If a git option can be used as a flag or with a value (e.g.,
--coloror--color=always), usecolor: truefor the flag form, orcolor: 'always'for the value form.git log --color→color: truegit log --color=always→color: 'always'
List/array options: Options that can be repeated or take multiple values (e.g.,
--exclude <pattern>,--pathspec-from-file <file>) are mapped to arrays:exclude: ['foo', 'bar'].git ls-files --exclude=foo --exclude=bar→exclude: ['foo', 'bar']
Key-value pair options: Options like
-c key=valueare mapped asc: { 'key' => 'value' }or as an array of pairs if multiple are allowed.git -c user.name=Scott→c: { 'user.name' => 'Scott' }
Mutually exclusive options: If options are mutually exclusive (e.g.,
--global,--local,--system), only one may be used at a time. Setting more than one raisesArgumentError. The DSL enforces this viaconflictsdeclarations at bind time. For negatable flag options (negatable: true), passingfalse(which emits--no-flag) also counts as using that option in the conflict check; non-negatablefalseis treated as absent.Forbidden value combinations (negatable flags): When two negatable flags may both be present but only certain value pairings are contradictory, use
forbid_valuesdeclarations instead of (or in addition to)conflicts.conflictsis presence-based and blocks all co-presence;forbid_valuesblocks only the exactname: valuetuples listed, leaving semantically equivalent pairs valid. For example,--all --no-ignore-removaland--no-all --ignore-removalare equivalent and should remain allowed, while--all --ignore-removaland--no-all --no-ignore-removalare contradictory and should be rejected:
forbid_values all: true, ignore_removal: true # contradictory
forbid_values all: false, ignore_removal: false # contradictory
Unknown names raise ArgumentError at definition time. Alias names are
canonicalized automatically.
Exactly-one required from a mutually exclusive group: When exactly one of a group of arguments must be provided (e.g., a command that accepts exactly one of
--mode-a,--mode-b, or--mode-c), omitting all of them or supplying more than one raisesArgumentError. The DSL enforces this viarequires_exactly_one_ofdeclarations, which combinerequires_one_of(at-least-one) andconflicts(at-most-one) in a single declaration.At-least-one required: When at least one of a group of arguments (options or positional) must be provided, but the group is not mutually exclusive, omitting all of them raises
ArgumentError. The DSL enforces this viarequires_one_ofdeclarations at bind time.
Positional arguments
Arguments that are not options (e.g., file names, branch names) are passed as method arguments, not as keyword arguments.
Only single-valued positional arguments: If a command has one or more single-valued positional arguments (e.g.,
<arg1>or<arg1> <arg2>), pass each as a separate method argument, in the order they appear in the official git documentation and CLI usage. Optional arguments (indicated by[<arg>]) should default tonil.git cmd <object>→def cmd(object)(fictitious command)git replace <object> <replacement>→def replace(object, replacement)git clone <repository> [<directory>]→def clone(repository, directory = nil)
Single multi-valued positional argument: If a command has a single multi-valued positional argument (e.g.,
<pathspec>...or[<pathspec>...]), use a splat parameter to accept zero or more values (optional) or one or more values (required).git add [<pathspec>...]→def add(*paths)
Mixed single-valued and multi-valued positional arguments —
--separated (independently reachable groups): When a git command separates two optional groups with--(e.g.,[<tree-ish>] [-- <pathspec>...]), callers may want to supply the post---group without supplying the first group. Use the single-valued argument as a regular optional parameter and the multi-valued group as a keyword argument with an empty array default. The keyword argument should accept a single value or an array; wrap a single value in an array internally.git checkout [<branch>] [-- <pathspec>...]→def checkout(branch = nil, pathspecs: [])git diff [<tree-ish>] [-- <pathspec>...]→def diff(tree_ish = nil, pathspec: [])- Callers can then do
checkout(pathspecs: ['file.rb'])(no branch) ordiff('HEAD~3', pathspec: ['file.rb'])(both), with no ambiguity.
Multiple optional single-valued positional arguments — pure nesting (second only meaningful with first): When the git SYNOPSIS shows nested optional brackets and the inner operand is only useful in the presence of the outer one, both arguments may be regular optional parameters in left-to-right order. A caller would never supply the second without the first.
git diff [<commit1> [<commit2>]]→def diff(commit1 = nil, commit2 = nil)- Callers can do
diff(no args),diff('HEAD~3'), ordiff('HEAD~3', 'HEAD'). There is no case where someone would passcommit2withoutcommit1.
These conventions ensure the API is predictable and closely aligned with the git CLI. If a new option type is encountered, extend this section to document the mapping.
Output processing
The git gem translates the output of many Git commands into Ruby objects, making it
easier to work with programmatically.
These Ruby objects often include methods that allow for further Git operations where useful, providing additional functionality while staying true to the underlying Git behavior.
When a single git command can produce distinctly different output types based on its options, implement separate methods for each output type. Follow the same naming convention used for commands with multiple purposes: use the git command name as a prefix, followed by a suffix that describes the specific output type or functionality.
For example, git diff can produce full diffs, statistical summaries, or path status
information depending on the options used. These are implemented as separate methods:
repo.diff_full('HEAD~1', 'HEAD') # Full diff output (git diff -p)
repo.diff_stats('HEAD~1', 'HEAD') # Statistical summary (git diff --numstat)
repo.diff_path_status('HEAD~1', 'HEAD') # File paths and status (git diff --name-status)
This approach ensures each method has a clear, predictable return type and allows for targeted parsing logic appropriate to each output format.
From design to implementation
Note: Use this architecture for all new commands. The gem is being incrementally migrated using the "Strangler Fig" pattern:
- Phase 1 (completed): Foundation work to introduce the new command architecture and prepare the codebase for incremental migration.
- Phase 2 (current): New
Git::Commands::*classes are created, andGit::Libmethods delegate to them.Git::Libremains but becomes a thin wrapper.- Phase 3 (planned): Public API (
Git::Base) will be refactored to useGit::Commands::*directly, bypassingGit::Lib.- Phase 4 (planned):
Git::Libwill be removed entirely.When adding new commands, create the
Git::Commands::*class and have the correspondingGit::Libmethod delegate to it (seeGit::Lib#addfor an example). When you encounter existing commands, you may optionally refactor them to this pattern following the TDD workflow.
The gem uses a three-layer architecture that separates the public API from internal implementation:
Facade layer (
Git::BaseandGitmodule) — The current public interface. Methods here are thin wrappers that delegate toGit::Lib, which in turn delegates to internal command classes.Command layer (
Git::Commands::*) — Internal classes that implement git commands. Each command class handles argument building and output parsing.Execution layer (
Git::ExecutionContext) — Runs raw git commands. Command classes use this to execute git and receive output.
When wrapping a new git command:
Design the public API using the guidelines in this section (placement, naming, parameters, output)
Create a command class in
lib/git/commands/that:- Accepts an
ExecutionContextand any required arguments - Defines arguments using the Arguments DSL
- Parses the output into Ruby objects
- Accepts an
Add the facade method to
Git::Base(orGitmodule) that delegates toGit::Lib.
Example structure for git add:
# lib/git/commands/add.rb (internal)
require 'git/commands/base'
module Git
module Commands
class Add < Base
arguments do
literal 'add'
flag_option :all
flag_option :force
operand :paths, repeatable: true, default: [], separator: '--'
end
# Execute the git add command
#
# @overload call(*paths, all: nil, force: nil)
#
# @param paths [Array<String>] files to be added
#
# @param all [Boolean] Add, modify, and remove index entries to match the worktree
#
# @param force [Boolean] Allow adding otherwise ignored files
#
# @return [Git::CommandLineResult] the result of the command
#
def call(...) = super # rubocop:disable Lint/UselessMethodDefinition
end
end
end
How Base works: Base provides default #initialize (accepts an
execution_context) and #call (binds arguments via the DSL, calls
execution_context.command, validates exit status). Simple commands only need to
declare arguments do … end and write def call(...) = super (which also serves as
the YARD documentation anchor for per-command documentation). That forwarding method
looks "useless" to RuboCop, so we disable Lint/UselessMethodDefinition on it; the
method body must exist for YARD to attach command-specific docs, even though all work
is delegated to Base#call.
Method Signature Convention: Most commands use def call(...) = super, which
forwards all arguments to Base#call. Base#call binds them via the Arguments DSL,
calls @execution_context.command(*args, **args.execution_options,
raise_on_failure: false), and validates the exit status.
Override call explicitly in three situations:
- Input validation — guard
ArgumentErrorfor invalid option combinations that the DSL cannot express (e.g., empty operands without a compensating flag). - Stdin via IO pipe — commands using the
--batch/--batch-checkprotocol must feed object names to the subprocess's stdin. Use the inheritedBase#with_stdin(content), which opens anIO.pipe, writes the string content, and yields the read end asin:. Do not open a pipe manually —StringIOis not accepted byProcess.spawn(it has no file descriptor). - Non-trivial option routing — when multiple call shapes require different argument sets built separately before dispatching.
When overriding, work with args_definition.bind(...) directly and delegate
exit-status handling to the inherited validate_exit_status!. Extract bulk logic
into private helpers to satisfy Rubocop Metrics thresholds:
def call(*objects, **)
raise ArgumentError, '...' if objects.empty? && ![:batch_all_objects]
bound = args_definition.bind(**)
with_stdin(objects.map { |o| "#{o}\n" }.join) { |reader| run_batch(bound, reader) }
end
private
def run_batch(bound, reader)
result = @execution_context.command(*bound, in: reader, **bound., raise_on_failure: false)
validate_exit_status!(result)
result
end
Validation of supported options is handled by the Arguments DSL, which raises
ArgumentError for unsupported keywords. The public API in Git::Lib handles the
translation from single values or arrays to the splat format.
YARD Documentation Note: When using anonymous keyword forwarding (
**), YARD cannot infer the method signature. Use the@overloaddirective with explicit keyword parameters (e.g.,@overload call(paths, all: nil, force: nil)) and document each keyword with its own@paramtag. Do not use@optionwith@overload. See the example above for the pattern.Testing Requirement: When defining arguments with the DSL, you must write RSpec tests that verify each argument handles valid values correctly (booleans, strings, arrays) and handles invalid values appropriately. Use a separate
contextblock for testing each option to ensure clarity and isolation. Seespec/unit/git/commands/add_spec.rbfor examples of comprehensive argument testing.
# lib/git/lib.rb (delegation)
class Git::Lib
# Git::Lib may accept an options hash for backward compatibility
def add(paths = '.', = {})
# Convert to splat + keyword arguments when calling the command class
Git::Commands::Add.new(self).call(*Array(paths), **)
end
end
# lib/git/base.rb (public facade)
class Git::Base
def add(paths = '.', **)
lib.add(paths, )
end
end
For factory methods and non-repository commands, the pattern is similar but differs
in how the ExecutionContext is obtained:
# Factory method (Git.clone) — creates context, runs command, returns repository
module Git
def self.clone(url, path = nil, **)
# logic to call Git::Commands::Clone via Git::Lib
end
end
# Non-repository command (Git.global_config) — standalone context
module Git
def self.global_config(name, value = nil)
Git::Lib.new.global_config(name, value)
end
end
Note: The
Git::Libclass currently acts as the execution context. In the new architecture,Git::Libmethods delegate toGit::Commands::*classes, passingself(theGit::Libinstance) as the execution context.
Example implementations
The following command classes demonstrate patterns for implementing new commands.
See lib/git/commands/ and spec/unit/git/commands/ for the full implementations:
- Simple command:
Git::Commands::Add— straightforward argument building with the Arguments DSL - Command with output parsing:
Git::Commands::Fsck— parses git output into structured Ruby objects - Factory command:
Git::Commands::Clone— returns data for creating a repository object - Multiple outputs:
Git::Commands::Diff::*— subclasses for different output formats (planned) - Multi-context:
Git::Commands::Config— handles both module and instance variants (planned)
Coding standards
To ensure high-quality contributions, all pull requests must meet the following requirements:
Commit message guidelines
To enhance our development workflow, enable automated changelog generation, and pave
the way for Continuous Delivery, the ruby-git project has adopted the Conventional
Commits standard for all commit
messages.
This structured approach to commit messages allows us to:
- Automate versioning and releases: Tools can now automatically determine the semantic version bump (patch, minor, major) based on the types of commits merged.
- Generate accurate changelogs: We can automatically create and update a
CHANGELOG.mdfile, providing a clear history of changes for users and contributors. - Improve commit history readability: A standardized format makes it easier for everyone to understand the nature of changes at a glance.
What does this mean for contributors?
Going forward, all commits to this repository MUST adhere to the Conventional Commits standard. Commits not adhering to this standard will cause the CI build to fail. PRs will not be merged if they include non-conventional commits.
A git commit-msg hook (Husky + commitlint) that validates your Conventional
Commit messages locally is installed automatically as part of the project
bootstrap — see Local development setup. The hook
depends on Node.js and npm; if those are not installed, bin/setup will warn
and skip the hook, and commit-message validation will only run in CI.
What to know about Conventional Commits
The simplist conventional commit is in the form type: description where type
indicates the type of change and description is your usual commit message (with
some limitations).
- Types include:
feat,fix,docs,test,refactor, andchore. See the full list of types supported in .commitlintrc.yml. - The description must (1) not start with an upper case letter, (2) be no more than 100 characters, and (3) not end with punctuation.
Examples of valid commits:
feat: add the --merges option to Git::Lib.logfix: exception thrown by Git::Lib.log when repo has no commitsdocs: add conventional commit announcement to README.md
Commits that include breaking changes must include an exclaimation mark before the colon:
feat!: removed Git::Repository#commit_force
The commit messages will drive how the version is incremented for each release:
- a release containing a breaking change will do a major version increment
- a release containing a new feature will do a minor increment
- a release containing neither a breaking change nor a new feature will do a patch version increment
The full conventional commit format is:
<type>[optional scope][!]: <description>
[optional body]
[optional footer(s)]
optional bodymay include multiple lines of descriptive text limited to 100 chars eachoptional footersonly usesBREAKING CHANGE: <description>where description should describe the nature of the backward incompatibility.
Use of the BREAKING CHANGE: footer flags a backward incompatible change even if it
is not flagged with an exclaimation mark after the type. Other footers are allowed
by not acted upon.
See the Conventional Commits specification for more details.
Issue and PR references
Due to a parser limitation in commitlint, using #<number> anywhere in the commit
body causes everything from that line onward to be treated as a footer, which
triggers a footer-leading-blank error.
To avoid this:
- In the body, omit the
#when mentioning an issue or PR — writeissue 1000notissue #1000. - In the footer, always include
#for closing references:Closes #1000,Fixes #1000, orResolves #1000. - If you only want to mention an issue for context (not close it), omit the
#in the body — no footer line is needed.
To validate a commit message before committing:
npx commitlint --format @commitlint/format < commit_msg.txt
To see how commitlint has parsed a commit message:
cat commit_msg.txt | node -e "
const parse = require('@commitlint/parse');
let msg = '';
process.stdin.on('data', d => msg += d);
process.stdin.on('end', () =>
parse.default(msg.trim()).then(r => console.log(JSON.stringify(r, null, 2)))
);
" | jq
Unit tests
- All changes must be accompanied by new or modified unit tests.
- The entire test suite must pass when
bundle exec rake defaultis run from the project's local working copy.
This project uses two test frameworks:
- RSpec (
spec/) - Primary framework for all new tests. - Test::Unit (
tests/units/) - Legacy test suite. Maintained for existing coverage but should not be extended for new features unless absolutely necessary.
RSpec best practices
- Public methods: Use a separate
describe '#method_name'block for each public method. - Contexts: Use separate
contextblocks for different scenarios. - Options: For methods accepting options (like commands), use a separate
contextfor each option to ensure isolation and comprehensiveness. - One assertion per test: Each test should verify one specific aspect of behavior. Exceptions include: (a) testing that an object has expected attributes after creation (e.g., verifying multiple fields of a returned object), (b) verifying expected side effects of a single operation (e.g., a method that both returns a value and modifies state), (c) testing that multiple related assertions hold for the same setup (e.g., boundary conditions).
Unit tests vs Integration tests
This project uses two types of RSpec tests, organized by directory:
Unit tests (
spec/unit/) - Test individual classes and methods with mocked execution context. These verify that the gem builds correct git command arguments and properly handles git output. Unit tests should mock@execution_contextto avoid calling real git commands.Integration tests (
spec/integration/) - Test the gem's behavior against real git repositories. These verify that mocked assumptions in unit tests match actual git behavior. Integration tests create temporary repositories usingDir.mktmpdirand run real git commands through the gem's public API.
Purpose of integration tests: Integration tests validate that the gem correctly interacts with git, not that git itself works correctly. They should verify:
- That the gem's mocked command expectations match real git output format
- That the gem correctly handles real git behavior (e.g., unicode in branch names)
- That command options produce expected git behavior
- Edge cases that are difficult to mock reliably
Integration test guidelines:
- Keep tests minimal and purposeful - only create what's needed for the test
- Focus on key behaviors that unit tests can't verify
- Don't test git's functionality - test the gem's interaction with git
- Use the shared context
'in an empty repository'for temporary repo setup - Use
Git::IntegrationTestHelpersmethods for file operations - Each test should verify one specific git interaction pattern
Example: An integration test for branch listing should verify that the gem correctly parses git's branch list format, not that git can create branches.
While working on specific features, you can run tests using:
# Run all tests (TestUnit + RSpec):
$ bundle exec rake test_all
# Run only TestUnit integration tests:
$ bundle exec rake test
# Run only RSpec tests (unit + integration):
$ bundle exec rake spec
# Run only RSpec unit tests:
$ bundle exec rake spec:unit
# Run only RSpec integration tests:
$ bundle exec rake spec:integration
# Run a single TestUnit file (from tests/units):
$ bin/test test_object
# Run multiple TestUnit files:
$ bin/test test_object test_archive
# Run a specific RSpec file:
$ bundle exec rspec spec/unit/git/commands/add_spec.rb
# Run TestUnit tests with a different version of the git command line:
$ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test
New and updated public-facing features should be documented in the project's README.md.
Building a specific version of the Git command-line
To test with a specific version of the Git command-line, you may need to build that version from source code. The following instructions are adapted from Atlassian’s How to install Git page for building Git on macOS.
Install pre-requisites
Prerequisites only need to be installed if they are not already present.
From your terminal, install Xcode’s Command Line Tools:
xcode-select --install
Install Homebrew by following the instructions on the Homebrew page.
Using Homebrew, install OpenSSL:
brew install openssl
Obtain Git source code
Download and extract the source tarball for the desired Git version from this source code mirror.
Build git
From your terminal, change to the root directory of the extracted source code and run the build with following command:
NO_GETTEXT=1 make CFLAGS="-I/usr/local/opt/openssl/include" LDFLAGS="-L/usr/local/opt/openssl/lib"
The build script will place the newly compiled Git executables in the bin-wrappers
directory (e.g., bin-wrappers/git).
Use the new Git version
To configure programs that use the Git gem to utilize the newly built version, do the following:
require 'git'
# Set the binary path
Git.configure { |c| c.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' }
# Validate the version (if desired)
assert_equal(Git::Version.new(2, 30, 2), Git.git_version)
Tests can be run using the newly built Git version as follows:
GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test
Note: GIT_PATH refers to the directory containing the git executable.
Licensing
ruby-git uses the MIT license as
declared in the LICENSE file.
Licensing is critical to open-source projects as it ensures the software remains available under the terms desired by the author.