crystalball 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +20 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE +674 -0
- data/README.md +196 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/crystalball +5 -0
- data/bin/setup +8 -0
- data/crystalball.gemspec +48 -0
- data/lib/crystalball.rb +38 -0
- data/lib/crystalball/case_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +55 -0
- data/lib/crystalball/git_repo.rb +58 -0
- data/lib/crystalball/map_generator.rb +81 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +44 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +44 -0
- data/lib/crystalball/map_generator/base_strategy.rb +21 -0
- data/lib/crystalball/map_generator/configuration.rb +54 -0
- data/lib/crystalball/map_generator/coverage_strategy.rb +34 -0
- data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
- data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
- data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
- data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
- data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
- data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
- data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
- data/lib/crystalball/map_storage/yaml_storage.rb +68 -0
- data/lib/crystalball/prediction.rb +34 -0
- data/lib/crystalball/predictor.rb +56 -0
- data/lib/crystalball/predictor/associated_specs.rb +40 -0
- data/lib/crystalball/predictor/modified_execution_paths.rb +21 -0
- data/lib/crystalball/predictor/modified_specs.rb +27 -0
- data/lib/crystalball/predictor_evaluator.rb +55 -0
- data/lib/crystalball/rails.rb +10 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +88 -0
- data/lib/crystalball/rspec/prediction_builder.rb +43 -0
- data/lib/crystalball/rspec/runner.rb +95 -0
- data/lib/crystalball/rspec/runner/configuration.rb +70 -0
- data/lib/crystalball/simple_predictor.rb +18 -0
- data/lib/crystalball/source_diff.rb +37 -0
- data/lib/crystalball/source_diff/file_diff.rb +53 -0
- data/lib/crystalball/version.rb +5 -0
- metadata +263 -0
data/README.md
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# Crystalball
|
2
|
+
|
3
|
+
Crystalball is a Ruby library which implements [Regression Test Selection mechanism](https://tenderlovemaking.com/2015/02/13/predicting-test-failues.html) originally published by Aaron Patterson. Its main purpose is to select a subset of your test suite which should be run to ensure your changes didn't break anything.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/toptal/crystalball.svg?branch=master)](https://travis-ci.org/toptal/crystalball)
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/c8bfc25a43a1a2ecf964/maintainability)](https://codeclimate.com/github/toptal/crystalball/maintainability)
|
7
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/c8bfc25a43a1a2ecf964/test_coverage)](https://codeclimate.com/github/toptal/crystalball/test_coverage)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
group :test do
|
15
|
+
gem 'crystalball'
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install crystalball
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
1. Start MapGenerator in your `spec_helper` before you loaded any file of your app. E.g.
|
30
|
+
```ruby
|
31
|
+
Crystalball::MapGenerator.start! do |config|
|
32
|
+
config.register Crystalball::MapGenerator::CoverageStrategy.new
|
33
|
+
end
|
34
|
+
```
|
35
|
+
1. Run your test suite on clean branch with green build. This step will generate file `execution_map.yml` in your project root
|
36
|
+
1. Make some changes to your app code
|
37
|
+
1. Run `bundle exec crystalball` to build a prediction and run RSpec with it. Check out [RSpec runner section](#rspec-runner) for customization details.
|
38
|
+
|
39
|
+
Keep in mind that as your target branch (usually master) code changes your execution maps will become outdated,
|
40
|
+
so you need to regenerate execution maps regularly.
|
41
|
+
|
42
|
+
## Map Generator
|
43
|
+
|
44
|
+
There are different map generator strategies that can (and should) be used together for better predictions. Each one has its own benefits and drawbacks, so they should be configured to best fit your needs.
|
45
|
+
|
46
|
+
### CoverageStrategy
|
47
|
+
|
48
|
+
Uses coverage information to detect which files are covered by the given spec (i.e. the files that, if changed, may potentially break the spec);
|
49
|
+
To customize the way the execution detection works, pass an object that responds to #detect and returns the paths to the strategy initialization:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# ...
|
53
|
+
config.register Crystalball::MapGenerator::CoverageStrategy.new(MyDetector)
|
54
|
+
```
|
55
|
+
|
56
|
+
By default, the execution detector is a `Crystalball::MapGenerator::CoverageStrategy::ExecutionDetector`, which filters out the paths outside the root and converts absolute paths to relative.
|
57
|
+
|
58
|
+
### AllocatedObjectsStrategy
|
59
|
+
|
60
|
+
Looks for the files in which the objects allocated during the spec execution are defined. It is considerably slower than `CoverageStrategy`.
|
61
|
+
To use this strategy, use the convenient method `.build` which takes two optional keyword arguments: `only`, used to define the classes or modules to have their descendants tracked (defaults to `[]`); and `root`, which is the path where the detection will take place (defaults to `Dir.pwd`).
|
62
|
+
Here's an example that tracks allocation of `ActiveRecord::Base` objects:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# ...
|
66
|
+
config.register Crystalball::MapGenerator::AllocatedObjectsStrategy.build(only: ['ActiveRecord::Base'])
|
67
|
+
```
|
68
|
+
|
69
|
+
That method is fine for most uses, but if you need to further customize the behavior of the strategy, you can directly instantiate the class.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# ...
|
73
|
+
config.register Crystalball::MapGenerator::AllocatedObjectsStrategy
|
74
|
+
.new(execution_detector: MyCustomDetector, object_tracker: MyCustomTracker)
|
75
|
+
```
|
76
|
+
|
77
|
+
The initialization takes two keyword arguments: `execution_detector` and `object_tracker`.
|
78
|
+
`execution_detector` must be an object that responds to `#detect` receiving a list of objects and returning the paths affected by said objects. `object_tracker` is something that responds to `#used_classes_during` which yields to the caller and returns the array of classes of objects allocated during the execution of the block.
|
79
|
+
|
80
|
+
### DescribedClassStrategy
|
81
|
+
|
82
|
+
This strategy will take each example that has a `described_class` (i.e. examples inside `describe` blocks of classes and not strings) and add the paths where the described class and its ancestors are defined to the case map of the example;
|
83
|
+
|
84
|
+
To use it, add to your `Crystalball::MapGenerator.start!` block:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# ...
|
88
|
+
config.register Crystalball::MapGenerator::DescribedClassStrategy.new
|
89
|
+
```
|
90
|
+
|
91
|
+
As with `AllocatedObjectsStrategy`, you can pass a custom execution detector (an object that responds to `#detect` and returns the paths) to the initialization:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
# ...
|
95
|
+
config.register Crystalball::MapGenerator::DescribedClassStrategy.new(MyDetector)
|
96
|
+
```
|
97
|
+
|
98
|
+
### Rails specific strategies
|
99
|
+
|
100
|
+
To use Rails specific strategies you must first `require 'crystalball/rails'`.
|
101
|
+
|
102
|
+
#### ActionViewStrategy
|
103
|
+
|
104
|
+
This strategy patches `ActionView::Template#compile!` to map the examples to affected views. Use it as follows:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# ...
|
108
|
+
config.register Crystalball::MapGenerator::ActionViewStrategy.new
|
109
|
+
```
|
110
|
+
|
111
|
+
#### I18nStrategy
|
112
|
+
|
113
|
+
Patches I18n to have access to the path where the locales are defined, so that those paths can be added to the case map.
|
114
|
+
To use it, add to your config:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# ...
|
118
|
+
config.register Crystalball::MapGenerator::I18nStrategy.new
|
119
|
+
```
|
120
|
+
|
121
|
+
### Custom strategies
|
122
|
+
|
123
|
+
You can create your own strategy and use it with the map generator. Any object that responds to `#call(case_map, example)` (where `case_map` is a `Crystalball::CaseMap` and `example` a `RSpec::Core::Example`) and augmenting its list of affected files using `case_map.push(*paths_to_files)`.
|
124
|
+
Check out the [implementation](https://github.com/toptal/crystalball/tree/master/lib/crystalball/map_generator) of the default strategies for details.
|
125
|
+
|
126
|
+
Keep in mind that all the strategies configured for the map generator will run for each example of your test suite, so it may slow down the generation process considerably.
|
127
|
+
|
128
|
+
## Predictor
|
129
|
+
|
130
|
+
The predictor can also be customized with different strategies:
|
131
|
+
|
132
|
+
### AssociatedSpecs
|
133
|
+
|
134
|
+
Needs to be configured with rules for detecting which specs should be on the prediction.
|
135
|
+
`predictor.use Crystalball::Predictor::AssociatedSpecs.new(from: %r{models/(.*).rb}, to: "./spec/models/%s_spec.rb")`
|
136
|
+
will add `./spec/models/foo_spec.rb` to prediction when `models/foo.rb` changes.
|
137
|
+
This strategy does not depend on a previoulsy generated case map.
|
138
|
+
|
139
|
+
### ModifiedExecutionPaths
|
140
|
+
|
141
|
+
Checks the case map and the diff to see which specs are affected by the new or modified files.
|
142
|
+
|
143
|
+
### ModifiedSpecs
|
144
|
+
|
145
|
+
As the name implies, checks for modified specs. The scope can be modified by passing a regex as argument, which defaults to `%r{spec/.*_spec\.rb\z}`.
|
146
|
+
This strategy does not depend on a previously generated case map.
|
147
|
+
|
148
|
+
### Custom strategies
|
149
|
+
|
150
|
+
As with the map generator you may define custom strategies for prediction. It must be an object that responds to `#call(diff, case_map)` (where `diff` is a `Crystalball::SourceDiff` and `case_map` is a `Crystalball::CaseMap`) and returns an array of paths.
|
151
|
+
|
152
|
+
Check out [default strategies implementation](https://github.com/toptal/crystalball/tree/master/lib/crystalball/predictor) for details.
|
153
|
+
|
154
|
+
## Under the hood
|
155
|
+
|
156
|
+
TODO: Write good description for anyone who wants to customize behavior
|
157
|
+
|
158
|
+
## Spring integration
|
159
|
+
|
160
|
+
It's very easy to integrate Crystalball with [Spring](https://github.com/rails/spring). Check out [spring-commands-crystalball](https://github.com/pluff/spring-commands-crystalball) for details.
|
161
|
+
|
162
|
+
## Plans
|
163
|
+
|
164
|
+
1. RSpec parallel integration
|
165
|
+
1. Map size optimization
|
166
|
+
1. Different strategies for execution map
|
167
|
+
1. Different strategies for failure predictor
|
168
|
+
1. Integration for git hook
|
169
|
+
|
170
|
+
## RSpec Runner
|
171
|
+
|
172
|
+
There is a custom RSpec runner you can use in your development with `bundle exec crystalball` command. It builds a prediction and runs it.
|
173
|
+
|
174
|
+
### Runner Configuration
|
175
|
+
|
176
|
+
#### Config file
|
177
|
+
|
178
|
+
Create a YAML file for the runner. Default locations are `./crystalball.yml` and `./config/crystalball.yml`. You can override config path with `CRYSTALBALL_CONFIG` env variable.
|
179
|
+
Please check an [example of a config file](https://github.com/toptal/crystalball/blob/master/spec/fixtures/crystalball.yml) for available options
|
180
|
+
|
181
|
+
#### Environment variables
|
182
|
+
|
183
|
+
`CRYSTALBALL_CONFIG=path/to/crystalball.yml` if you want to override default path to config file.
|
184
|
+
`CRYSTALBALL_SKIP_MAP_CHECK=true` if you want to skip maps expiration period check.
|
185
|
+
`CRYSTALBALL_SKIP_EXAMPLES_LIMIT=true` if you want to skip examples limit check.
|
186
|
+
|
187
|
+
## Development
|
188
|
+
|
189
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
190
|
+
|
191
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
192
|
+
|
193
|
+
## Contributing
|
194
|
+
|
195
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/toptal/crystalball. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
196
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "crystalball"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/crystalball
ADDED
data/bin/setup
ADDED
data/crystalball.gemspec
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'crystalball/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "crystalball"
|
9
|
+
spec.version = Crystalball::VERSION
|
10
|
+
spec.authors = ["Pavel Shutsin"]
|
11
|
+
spec.email = ["publicshady@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = 'A library for RSpec regression test selection'
|
14
|
+
spec.description = 'Provides simple way to integrate regression test selection approach to your RSpec test suite'
|
15
|
+
spec.homepage = 'https://github.com/toptal/crystalball'
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
23
|
+
"public gem pushes."
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
spec.bindir = "bin"
|
30
|
+
spec.executables = [File.basename('bin/crystalball')]
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_dependency 'git'
|
34
|
+
|
35
|
+
spec.required_ruby_version = '> 2.3.0'
|
36
|
+
|
37
|
+
spec.add_development_dependency 'actionview'
|
38
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
39
|
+
spec.add_development_dependency 'i18n'
|
40
|
+
spec.add_development_dependency 'pry'
|
41
|
+
spec.add_development_dependency 'pry-byebug'
|
42
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
43
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
44
|
+
spec.add_development_dependency 'rubocop'
|
45
|
+
spec.add_development_dependency 'rubocop-rspec'
|
46
|
+
spec.add_development_dependency 'simplecov'
|
47
|
+
spec.add_development_dependency 'yard'
|
48
|
+
end
|
data/lib/crystalball.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/git_repo'
|
4
|
+
require 'crystalball/rspec/prediction_builder'
|
5
|
+
require 'crystalball/rspec/runner'
|
6
|
+
require 'crystalball/prediction'
|
7
|
+
require 'crystalball/predictor'
|
8
|
+
require 'crystalball/predictor/modified_execution_paths'
|
9
|
+
require 'crystalball/predictor/modified_specs'
|
10
|
+
require 'crystalball/predictor/associated_specs'
|
11
|
+
require 'crystalball/case_map'
|
12
|
+
require 'crystalball/execution_map'
|
13
|
+
require 'crystalball/map_generator'
|
14
|
+
require 'crystalball/map_generator/configuration'
|
15
|
+
require 'crystalball/map_generator/coverage_strategy'
|
16
|
+
require 'crystalball/map_generator/allocated_objects_strategy'
|
17
|
+
require 'crystalball/map_generator/described_class_strategy'
|
18
|
+
require 'crystalball/map_storage/yaml_storage'
|
19
|
+
require 'crystalball/version'
|
20
|
+
|
21
|
+
# Main module for the library
|
22
|
+
module Crystalball
|
23
|
+
# Prints the list of specs which might fail
|
24
|
+
#
|
25
|
+
# @param [String] workdir - path to the root directory of repository (usually contains .git folder inside). Default: current directory
|
26
|
+
# @param [String] map_path - path to the execution map. Default: execution_map.yml
|
27
|
+
# @param [Proc] block - used to configure predictors
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# Crystalball.foresee do |predictor|
|
31
|
+
# predictor.use Crystalball::Predictor::ModifiedExecutionPaths.new
|
32
|
+
# predictor.use Crystalball::Predictor::ModifiedSpecs.new
|
33
|
+
# end
|
34
|
+
def self.foresee(workdir: '.', map_path: 'execution_map.yml', &block)
|
35
|
+
map = MapStorage::YAMLStorage.load(Pathname(map_path))
|
36
|
+
Predictor.new(map, GitRepo.open(Pathname(workdir)), from: map.commit, &block).prediction.compact
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
# Data object to store execution map for specific example
|
5
|
+
class CaseMap
|
6
|
+
attr_reader :uid, :file_path, :affected_files
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
delegate %i[push] => :affected_files
|
10
|
+
|
11
|
+
# @param [String] example - id of example
|
12
|
+
# @param [Array<String>] affected_files - list of files affected by example
|
13
|
+
def initialize(example, affected_files = [])
|
14
|
+
@uid = example.id
|
15
|
+
@file_path = example.file_path
|
16
|
+
@affected_files = affected_files
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
# Storage for execution map
|
5
|
+
class ExecutionMap
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
# Simple data object for map metadata information
|
9
|
+
class Metadata
|
10
|
+
attr_accessor :commit, :type, :version
|
11
|
+
|
12
|
+
# @param [String] commit - SHA of commit
|
13
|
+
# @param [String] type - type of execution map
|
14
|
+
# @param [Numeric] version - map generator version number
|
15
|
+
def initialize(commit: nil, type: nil, version: nil)
|
16
|
+
@commit = commit
|
17
|
+
@type = type
|
18
|
+
@version = version
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{type: type, commit: commit, version: version}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :cases, :metadata
|
27
|
+
|
28
|
+
delegate %i[commit commit= version version=] => :metadata
|
29
|
+
delegate %i[size] => :cases
|
30
|
+
|
31
|
+
# @param [Hash] metadata - add or override metadata of execution map
|
32
|
+
# @param [Hash] cases - initial list of cases
|
33
|
+
def initialize(metadata: {}, cases: {})
|
34
|
+
@cases = cases
|
35
|
+
|
36
|
+
@metadata = Metadata.new(type: self.class.name, **metadata)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Adds case map to the list
|
40
|
+
#
|
41
|
+
# @param [Crystalball::CaseMap] case_map
|
42
|
+
def <<(case_map)
|
43
|
+
cases[case_map.uid] = case_map.affected_files.uniq
|
44
|
+
end
|
45
|
+
|
46
|
+
# Remove all cases
|
47
|
+
def clear!
|
48
|
+
self.cases = {}
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_writer :cases, :metadata
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'git'
|
4
|
+
require 'crystalball/source_diff'
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
# Wrapper class representing Git repository
|
8
|
+
class GitRepo
|
9
|
+
attr_reader :repo_path
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# @return [Crystalball::GitRepo] instance for given path
|
13
|
+
def open(repo_path)
|
14
|
+
path = Pathname(repo_path)
|
15
|
+
new(path) if exists?(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if given path is under git control (contains .git folder)
|
19
|
+
def exists?(path)
|
20
|
+
path.join('.git').directory?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Pathname] repo_path path to repository root folder
|
25
|
+
def initialize(repo_path)
|
26
|
+
@repo_path = repo_path
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check if repository has no uncommitted changes
|
30
|
+
def pristine?
|
31
|
+
diff.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Proxy all unknown calls to `Git` object
|
35
|
+
def method_missing(method, *args, &block)
|
36
|
+
repo.public_send(method, *args, &block) || super
|
37
|
+
end
|
38
|
+
|
39
|
+
def respond_to_missing?(method, *)
|
40
|
+
repo.respond_to?(method, false) || super
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates diff
|
44
|
+
#
|
45
|
+
# @param [String] from starting commit to build a diff. Default: HEAD
|
46
|
+
# @param [String] to ending commit to build a diff. Default: nil, will build diff of uncommitted changes
|
47
|
+
# @return [SourceDiff]
|
48
|
+
def diff(from = 'HEAD', to = nil)
|
49
|
+
SourceDiff.new(repo.diff(from, to))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def repo
|
55
|
+
@repo ||= Git.open(repo_path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|