Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ All pull requests must meet these requirements:
### Unit tests
* All changes must be accompanied by new or modified RSpec unit tests
* The entire test suite must pass when `bundle exec rake spec` is run from the
project's local working copy
project's local working tree
* The unit test suite must maintain 100% code coverage to pass

### Documentation
Expand Down
56 changes: 40 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,34 @@
[![Test Coverage](https://api.codeclimate.com/v1/badges/5403e4613b7518f70da7/test_coverage)](https://codeclimate.com/github/main-branch/ruby_git/test_coverage)
[![Slack](https://img.shields.io/badge/slack-main--branch/ruby__git-yellow.svg?logo=slack)](https://main-branch.slack.com/archives/C01CHR7TMM2)

RubyGit is an object-oriented wrapper for the `git` command line tool for working with Worktrees
and Repositories. It tries to make more sense out of the Git command line. See the object model
in [this Lucid chart diagram](https://app.lucidchart.com/invitations/accept/7df13bab-3383-4683-8cb4-e76d539de93d)
(requires sign in).
Git Is Hard™ but it doesn't have to be that way. Git has this reputation because it has an
underlying model that is more complex than other popular revision control systems
such as CVS or Subversion. To make matters worse, the `git` command line is vast,
inconsistently implemented, and does not have a clear mapping between the command-line
actions and Git's underlying model.

Because of this complexity, beginners tend to memorize a few `git` commands in
order to get by with a simple workflow without really understanding how Git works
and the rich set of features it offers.

The RubyGit module provides a Ruby API that is an object-oriented wrapper around
the `git` command line. It is intended to make automating both simple and complex Git
interactions easier. To accomplish this, it ties each action you can do with `git` to
the type of object that action operates on.

There are three main objects in RubyGit:
* [WorkingTree](lib/ruby_git/working_tree.rb): The directory tree of actual checked
out files. The working tree normally contains the contents of the HEAD commit’s
tree, plus any local changes that you have made but not yet committed.
* [Index](lib/ruby_git/index.rb): The index is used as a staging area between your
working tree and your repository. You can use the index to build up a set of changes
that you want to commit together. When you create a commit, what is committed is what is
currently in the index, not what is in your working directory.
* [Repository](lib/ruby_git/repository.rb): The repository stores the files in a project,
their history, and other meta data like commit information, tags, and branches.

The [RubyGit Class Diagram](RubyGit%20Class%20Diagram.svg) shows the main abstractions in
RubyGit, how they are related, and what actions each can perform.

## Installation

Expand Down Expand Up @@ -41,24 +65,24 @@ RubyGit.git.path #=> '/usr/local/bin/git'
RubyGit.git.version #=> [2,28,0]
```

To work with an existing Worktree:
To work with an existing WorkingTree:

```Ruby
worktree = RubyGit.open(worktree_path)
worktree.append_to_file('README.md', 'New line in README.md')
worktree.add('README.md')
worktree.commit('Add a line to the README.md')
worktree.push
working_tree = RubyGit.open(working_tree_path)
working_tree.append_to_file('README.md', 'New line in README.md')
working_tree.add('README.md')
working_tree.commit('Add a line to the README.md')
working_tree.push
```

To create a new Worktree:
To create a new WorkingTree:

```Ruby
worktree = RubyGit.init(worktree_path)
worktree.write_to_file('README.md', '# My New Project')
worktree.add('README.md')
worktree.repository.add_remote(remote_name: 'origin', url: 'https://github.com/jcouball/test', default_branch: 'main')
worktree.push(remote_name: 'origin')
working_tree = RubyGit.init(working_tree_path)
working_tree.write_to_file('README.md', '# My New Project')
working_tree.add('README.md')
working_tree.repository.add_remote(remote_name: 'origin', url: 'https://github.com/jcouball/test', default_branch: 'main')
working_tree.push(remote_name: 'origin')
```

To tell what version of Git is being used:
Expand Down
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ select `Draft a new release`
## Build and release the gem

Clone [main-branch/ruby_git](https://github.com/main-branch/ruby_git) directly (not a
fork) and ensure your local working copy is on the main branch
fork) and ensure your local working tree is on the main 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
Expand Down
1 change: 1 addition & 0 deletions RubyGit Class Diagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 42 additions & 29 deletions lib/ruby_git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@
require 'ruby_git/file_helpers'
require 'ruby_git/git_binary'
require 'ruby_git/version'
require 'ruby_git/worktree'
require 'ruby_git/working_tree'

require 'null_logger'

# RubyGit is an object-oriented wrapper for the `git` command line tool for
# working with Worktrees and Repositories. It tries to make more sense out
# of the Git command line.
# The RubyGit module provides a Ruby API that is an object-oriented wrapper around
# the `git` command line. It is intended to make automating both simple and complex Git
# interactions easier. To accomplish this, it ties each action you can do with `git` to
# the type of object that action operates on.
#
# There are three main objects in RubyGit:
# * {WorkingTree}: The directory tree of actual checked
# out files. The working tree normally contains the contents of the HEAD commit's
# tree, plus any local changes that you have made but not yet committed.
# * Index: The index is used as a staging area between your
# working tree and your repository. You can use the index to build up a set of changes
# that you want to commit together. When you create a commit, what is committed is what is
# currently in the index, not what is in your working directory.
# * Repository: The repository stores the files in a project,
# their history, and other meta data like commit information, tags, and branches.
#
# @api public
#
Expand Down Expand Up @@ -55,78 +67,79 @@ class << self
attr_accessor :logger
end

# Create an empty Git repository under the root worktree `path`
# Create an empty Git repository under the root working tree `path`
#
# If the repository already exists, it will not be overwritten.
#
# @see https://git-scm.com/docs/git-init git-init
#
# @example
# worktree = Worktree.init(worktree_path)
# working_tree = WorkingTree.init(working_tree_path)
#
# @param [String] worktree_path the root path of a worktree
# @param [String] working_tree_path the root path of a working_tree
#
# @raise [RubyGit::Error] if worktree_path is not a directory
# @raise [RubyGit::Error] if working_tree_path is not a directory
#
# @return [RubyGit::Worktree] the worktree whose root is at `path`
# @return [RubyGit::WorkingTree] the working_tree whose root is at `path`
#
def self.init(worktree_path)
RubyGit::Worktree.init(worktree_path)
def self.init(working_tree_path)
RubyGit::WorkingTree.init(working_tree_path)
end

# Open an existing Git worktree that contains worktree_path
# Open an existing Git working tree that contains working_tree_path
#
# @see https://git-scm.com/docs/git-open git-open
#
# @example
# worktree = Worktree.open(worktree_path)
# working_tree = WorkingTree.open(working_tree_path)
#
# @param [String] worktree_path the root path of a worktree
# @param [String] working_tree_path the root path of a working_tree
#
# @raise [RubyGit::Error] if `worktree_path` does not exist, is not a directory, or is not within a Git worktree.
# @raise [RubyGit::Error] if `working_tree_path` does not exist, is not a directory, or is not within
# a Git working_tree.
#
# @return [RubyGit::Worktree] the worktree that contains `worktree_path`
# @return [RubyGit::WorkingTree] the working_tree that contains `working_tree_path`
#
def self.open(worktree_path)
RubyGit::Worktree.open(worktree_path)
def self.open(working_tree_path)
RubyGit::WorkingTree.open(working_tree_path)
end

# Copy the remote repository and checkout the default branch
#
# Clones the repository referred to by `repository_url` into a newly created
# directory, creates remote-tracking branches for each branch in the cloned repository,
# and checks out the default branch in the worktree whose root directory is `to_path`.
# and checks out the default branch in the working_tree whose root directory is `to_path`.
#
# @see https://git-scm.com/docs/git-clone git-clone
#
# @example Using default for Worktree path
# @example Using default for WorkingTree path
# FileUtils.pwd
# => "/Users/jsmith"
# worktree = Worktree.clone('https://github.com/main-branch/ruby_git.git')
# worktree.path
# working_tree = WorkingTree.clone('https://github.com/main-branch/ruby_git.git')
# working_tree.path
# => "/Users/jsmith/ruby_git"
#
# @example Using a specified worktree_path
# @example Using a specified working_tree_path
# FileUtils.pwd
# => "/Users/jsmith"
# worktree_path = '/tmp/project'
# worktree = Worktree.clone('https://github.com/main-branch/ruby_git.git', to_path: worktree_path)
# worktree.path
# working_tree_path = '/tmp/project'
# working_tree = WorkingTree.clone('https://github.com/main-branch/ruby_git.git', to_path: working_tree_path)
# working_tree.path
# => "/tmp/project"
#
# @param [String] repository_url a reference to a Git repository
#
# @param [String] to_path where to put the checked out worktree once the repository is cloned
# @param [String] to_path where to put the checked out working tree once the repository is cloned
#
# `to_path` will be created if it does not exist. An error is raised if `to_path` exists and
# not an empty directory.
#
# @raise [RubyGit::Error] if (1) `repository_url` is not valid or does not point to a valid repository OR
# (2) `to_path` is not an empty directory.
#
# @return [RubyGit::Worktree] the worktree checked out from the cloned repository
# @return [RubyGit::WorkingTree] the working tree checked out from the cloned repository
#
def self.clone(repository_url, to_path: '')
RubyGit::Worktree.clone(repository_url, to_path: to_path)
RubyGit::WorkingTree.clone(repository_url, to_path: to_path)
end
end
138 changes: 138 additions & 0 deletions lib/ruby_git/working_tree.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# frozen_string_literal: true

require 'open3'

module RubyGit
# The working tree is a directory tree consisting of the checked out files that
# you are currently working on.
#
# Create a new WorkingTree using {.init}, {.clone}, or {.open}.
#
class WorkingTree
# The root path of the working tree
#
# @example
# working_tree_path = '/Users/James/myproject'
# working_tree = WorkingTree.open(working_tree_path)
# working_tree.path
# => '/Users/James/myproject'
#
# @return [Pathname] the root path of the working_tree
#
attr_reader :path

# Create an empty Git repository under the root working tree `path`
#
# If the repository already exists, it will not be overwritten.
#
# @see https://git-scm.com/docs/git-init git-init
#
# @example
# working_tree = WorkingTree.init(working_tree_path)
#
# @param [String] working_tree_path the root path of a Git working tree
#
# @raise [RubyGit::Error] if working_tree_path is not a directory
#
# @return [RubyGit::WorkingTree] the working tree whose root is at `path`
#
def self.init(working_tree_path)
raise RubyGit::Error, "Path '#{working_tree_path}' not valid." unless File.directory?(working_tree_path)

command = [RubyGit.git.path.to_s, 'init']
_out, err, status = Open3.capture3(*command, chdir: working_tree_path)
raise RubyGit::Error, err unless status.success?

WorkingTree.new(working_tree_path)
end

# Open an existing Git working tree that contains working_tree_path
#
# @see https://git-scm.com/docs/git-open git-open
#
# @example
# working_tree = WorkingTree.open(working_tree_path)
#
# @param [String] working_tree_path the root path of a Git working tree
#
# @raise [RubyGit::Error] if `working_tree_path` does not exist, is not a directory, or is not within
# a Git working tree.
#
# @return [RubyGit::WorkingTree] the Git working tree that contains `working_tree_path`
#
def self.open(working_tree_path)
new(working_tree_path)
end

# Copy the remote repository and checkout the default branch
#
# Clones the repository referred to by `repository_url` into a newly created
# directory, creates remote-tracking branches for each branch in the cloned repository,
# and checks out the default branch in the Git working tree whose root directory is `to_path`.
#
# @see https://git-scm.com/docs/git-clone git-clone
#
# @example Using default for WorkingTree path
# FileUtils.pwd
# => "/Users/jsmith"
# working_tree = WorkingTree.clone('https://github.com/main-branch/ruby_git.git')
# working_tree.path
# => "/Users/jsmith/ruby_git"
#
# @example Using a specified working_tree_path
# FileUtils.pwd
# => "/Users/jsmith"
# working_tree_path = '/tmp/project'
# working_tree = WorkingTree.clone('https://github.com/main-branch/ruby_git.git', to_path: working_tree_path)
# working_tree.path
# => "/tmp/project"
#
# @param [String] repository_url a reference to a Git repository
#
# @param [String] to_path where to put the checked out Git working tree once the repository is cloned
#
# `to_path` will be created if it does not exist. An error is raised if `to_path` exists and
# not an empty directory.
#
# @raise [RubyGit::Error] if (1) `repository_url` is not valid or does not point to a valid repository OR
# (2) `to_path` is not an empty directory.
#
# @return [RubyGit::WorkingTree] the Git working tree checked out from the cloned repository
#
def self.clone(repository_url, to_path: '')
command = [RubyGit.git.path.to_s, 'clone', '--', repository_url, to_path]
_out, err, status = Open3.capture3(*command)
raise RubyGit::Error, err unless status.success?

new(to_path)
end

private

# Create a WorkingTree object
# @api private
#
def initialize(working_tree_path)
raise RubyGit::Error, "Path '#{working_tree_path}' not valid." unless File.directory?(working_tree_path)

@path = root_path(working_tree_path)
RubyGit.logger.debug("Created #{inspect}")
end

# Find the root path of a Git working tree containing `path`
#
# @raise [RubyGit::Error] if the path is not in a Git working tree
#
# @return [String] the root path of the Git working tree containing `path`
#
# @api private
#
def root_path(working_tree_path)
command = [RubyGit.git.path.to_s, 'rev-parse', '--show-toplevel']
out, err, status = Open3.capture3(*command, chdir: working_tree_path)
raise RubyGit::Error, err unless status.success?

out.chomp
end
end
end
Loading