devtools-jdiff 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +5 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +34 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/devtools-jdiff.gemspec +33 -0
- data/exe/jdiff +7 -0
- data/lib/jira_diff.rb +44 -0
- data/lib/jira_diff/git.rb +30 -0
- data/lib/jira_diff/globals.rb +4 -0
- data/lib/jira_diff/options.rb +68 -0
- data/lib/jira_diff/stories.rb +122 -0
- data/lib/jira_diff/story.rb +53 -0
- metadata +177 -0
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Donovan Young
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/dyoung522/devtools-jdiff.svg?branch=master)](https://travis-ci.org/dyoung522/devtools-jdiff)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/dyoung522/devtools-jdiff/badges/gpa.svg)](https://codeclimate.com/github/dyoung522/devtools-jdiff)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/dyoung522/devtools-jdiff/badges/coverage.svg)](https://codeclimate.com/github/dyoung522/devtools-jdiff/coverage)
|
4
|
+
|
5
|
+
# JIRADiff
|
6
|
+
|
7
|
+
Compares JIRA stories from two git branches, displaying the difference
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Install it from the command line
|
12
|
+
|
13
|
+
$ gem install devtools-jdiff
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Once installed, this gem provides the `jdiff` command line utility.
|
18
|
+
|
19
|
+
Please run `jdiff --help` for more information.
|
20
|
+
|
21
|
+
## Development
|
22
|
+
|
23
|
+
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.
|
24
|
+
|
25
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
26
|
+
|
27
|
+
## Contributing
|
28
|
+
|
29
|
+
Bug reports and pull requests are welcome on [our GitHub page](https://github.com/dyoung522/devtools)
|
30
|
+
|
31
|
+
## License
|
32
|
+
|
33
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
34
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "jira_diff"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "pry"
|
14
|
+
Pry.start
|
15
|
+
|
data/bin/setup
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "jira_diff/globals"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "devtools-jdiff"
|
8
|
+
spec.version = JIRADiff::VERSION
|
9
|
+
spec.authors = ["Donovan Young"]
|
10
|
+
spec.email = ["dyoung522@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Compares git branches, searching for JIRA stories}
|
13
|
+
spec.description = %q{Looks for JIRA stories within the commits in a git repository} +
|
14
|
+
%q{and compares branches to find stories included in one branch} +
|
15
|
+
%q{but not another}
|
16
|
+
spec.homepage = "https://github.com/dyoung522/devtools-jdiff"
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_dependency "config", "~> 1.3"
|
25
|
+
spec.add_dependency "octokit", "~> 4.0"
|
26
|
+
spec.add_dependency "devtools-base", "~> 2.0"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.13"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
31
|
+
spec.add_development_dependency "pry", "~> 0.4"
|
32
|
+
spec.add_development_dependency "factory_girl", "~> 4.0"
|
33
|
+
end
|
data/exe/jdiff
ADDED
data/lib/jira_diff.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require "devtools"
|
2
|
+
require "jira_diff/globals"
|
3
|
+
require "jira_diff/options"
|
4
|
+
require "jira_diff/stories"
|
5
|
+
|
6
|
+
module JIRADiff
|
7
|
+
def self.not_implemented(feature)
|
8
|
+
puts "Sorry, #{feature} has not yet been implemented"
|
9
|
+
exit 2
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.run!
|
13
|
+
begin
|
14
|
+
opts = OptParse.parse ARGV
|
15
|
+
rescue => error
|
16
|
+
puts error
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
puts opts.inspect if opts.debug
|
21
|
+
|
22
|
+
begin
|
23
|
+
puts 'Searching for stories...' if opts.verbose
|
24
|
+
stories = Stories.new(opts)
|
25
|
+
rescue RuntimeError => error
|
26
|
+
puts error
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
|
30
|
+
if opts.verbose
|
31
|
+
puts "From #{stories.directory}"
|
32
|
+
puts "-> All stories from #{stories.source.join(', ')}"
|
33
|
+
puts "-> Which are not in #{stories.master}"
|
34
|
+
stories.diff.each do |story|
|
35
|
+
puts "%-120.120s" % story.to_s
|
36
|
+
end
|
37
|
+
else
|
38
|
+
puts stories.diff.shas
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module JIRADiff
|
4
|
+
class Git
|
5
|
+
|
6
|
+
def initialize(dir = '.')
|
7
|
+
raise StandardError, "Directory '#{dir}' is not valid" unless Dir.exist?(dir)
|
8
|
+
raise RuntimeError, "Doesn't look like '#{dir}' is a Git repository" unless Dir.exist?(File.join(dir, '.git'))
|
9
|
+
|
10
|
+
@working_dir = dir
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(branch)
|
14
|
+
raise RuntimeError, "Invalid branch: #{branch}" unless branch_valid? branch
|
15
|
+
run_command("\\git --no-pager log --no-merges --pretty='%H|%s' #{branch}")
|
16
|
+
end
|
17
|
+
|
18
|
+
def branch_valid?(branch)
|
19
|
+
run_command("\\git branch --all --list #{branch}")[0] =~ /#{branch}/
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def run_command(cmd)
|
25
|
+
Open3.popen3(cmd, chdir: @working_dir) do |_i, o, _e, _t|
|
26
|
+
o.read.split("\n")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "devtools"
|
2
|
+
|
3
|
+
module JIRADiff
|
4
|
+
class OptParse
|
5
|
+
|
6
|
+
def self.default_options
|
7
|
+
{
|
8
|
+
debug: false,
|
9
|
+
directory: ".",
|
10
|
+
dryrun: false,
|
11
|
+
master: "master",
|
12
|
+
source: [],
|
13
|
+
verbose: true
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse(argv_opts = [], unit_testing = false)
|
18
|
+
opt_parse = DevTools::OptParse.new({ name: PROGRAM_NAME,
|
19
|
+
version: VERSION,
|
20
|
+
testing: unit_testing,
|
21
|
+
defaults: default_options })
|
22
|
+
|
23
|
+
parser = opt_parse.parser
|
24
|
+
|
25
|
+
parser.banner = "Usage: #{DevTools::PROGRAM} [OPTIONS]"
|
26
|
+
|
27
|
+
parser.separator ""
|
28
|
+
parser.separator "[OPTIONS]"
|
29
|
+
|
30
|
+
parser.separator ""
|
31
|
+
parser.separator "Specific Options:"
|
32
|
+
|
33
|
+
parser.on("-d", "--directory DIR", "Use DIR as our source directory") do |dir|
|
34
|
+
dir = File.expand_path(dir.strip)
|
35
|
+
if Dir.exist?(dir)
|
36
|
+
Options.directory = dir
|
37
|
+
else
|
38
|
+
raise ArgumentError, "ENOEXIST: Directory does not exist -> #{dir}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
parser.on("-m", "--master BRANCH", "Specify a master branch (default: master)") { |m| Options.master = m }
|
43
|
+
parser.on("-s", "--source BRANCH",
|
44
|
+
"Use BRANCH as the source to compare against (may be used more than once)") do |branch|
|
45
|
+
Options.source << branch unless Options.source.include?(branch)
|
46
|
+
end
|
47
|
+
|
48
|
+
parser.separator ""
|
49
|
+
parser.separator "Common Options:"
|
50
|
+
|
51
|
+
parser.parse!(argv_opts)
|
52
|
+
|
53
|
+
validate_options(Options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.validate_options(opts)
|
57
|
+
if opts.source.include?(opts.master)
|
58
|
+
raise RuntimeError, "Source branches cannot include the master branch"
|
59
|
+
end
|
60
|
+
|
61
|
+
opts.source = ["develop"] if opts.source.empty?
|
62
|
+
opts.master = "master" if opts.master.nil? || opts.master.strip == ""
|
63
|
+
|
64
|
+
opts
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'jira_diff/story'
|
2
|
+
require 'jira_diff/git'
|
3
|
+
|
4
|
+
module JIRADiff
|
5
|
+
class Stories
|
6
|
+
|
7
|
+
def initialize(opts = Options.defaults)
|
8
|
+
@branches = _get_branches(opts[:master], opts[:source])
|
9
|
+
@directory = opts[:directory] || '.'
|
10
|
+
@includes = _get_includes(opts.includes)
|
11
|
+
@options = opts
|
12
|
+
@stories = opts[:stories] || {}
|
13
|
+
|
14
|
+
_get_stories if @stories == {}
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :branches, :directory
|
18
|
+
attr_reader :stories, :includes
|
19
|
+
|
20
|
+
alias dir directory
|
21
|
+
|
22
|
+
def each
|
23
|
+
@stories.values.each do |stories|
|
24
|
+
stories.each { |story| yield story }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def source
|
29
|
+
@branches[1, @branches.size]
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_include(story)
|
33
|
+
@includes << story
|
34
|
+
end
|
35
|
+
|
36
|
+
def includes=(file)
|
37
|
+
@includes = _get_includes(file)
|
38
|
+
end
|
39
|
+
|
40
|
+
def master
|
41
|
+
@branches[0]
|
42
|
+
end
|
43
|
+
|
44
|
+
def master=(new_master)
|
45
|
+
@stories[master] = []
|
46
|
+
@branches[0] = new_master
|
47
|
+
_get_stories(new_master)
|
48
|
+
end
|
49
|
+
|
50
|
+
def shas
|
51
|
+
source.map do |branch|
|
52
|
+
stories[branch].map { |s| s.sha }
|
53
|
+
end.flatten.reverse
|
54
|
+
end
|
55
|
+
|
56
|
+
def source_stories
|
57
|
+
story_index = {}
|
58
|
+
|
59
|
+
source.each do |branch|
|
60
|
+
stories[branch].each { |s| story_index[s.sha] = s }
|
61
|
+
end
|
62
|
+
|
63
|
+
story_index.values
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_story(branch, story)
|
67
|
+
(@stories[branch] ||= []).push story
|
68
|
+
end
|
69
|
+
|
70
|
+
def find(branch, sha)
|
71
|
+
raise ArgumentError, "Invalid environment #{branch}" unless @branches.include?(branch)
|
72
|
+
|
73
|
+
@stories[branch].each { |story| return true if story.sha == sha }
|
74
|
+
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def diff
|
79
|
+
stories = []
|
80
|
+
opts = @options
|
81
|
+
|
82
|
+
source_stories.each do |story|
|
83
|
+
stories << story unless find(master, story.sha)
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.source = ['diff']
|
87
|
+
opts.stories = {'diff' => stories.flatten}
|
88
|
+
Stories.new opts
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def _get_branches(master, sources)
|
94
|
+
branches = [] << (master.nil? ? 'master' : master)
|
95
|
+
branches << (sources.empty? ? ['develop'] : sources)
|
96
|
+
branches.flatten
|
97
|
+
end
|
98
|
+
|
99
|
+
def _get_includes(includes_file)
|
100
|
+
lines = []
|
101
|
+
if includes_file && File.exist?(includes_file)
|
102
|
+
File.open(includes_file, 'r') do |f|
|
103
|
+
f.each_line { |line| lines << $1 if line =~ /(\w+\-\d+)/ }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
lines
|
107
|
+
end
|
108
|
+
|
109
|
+
def _get_stories(branches = @branches)
|
110
|
+
git = Git.new(@directory)
|
111
|
+
|
112
|
+
branches.to_a.each do |branch|
|
113
|
+
puts "checking #{branch}" if @options.debug
|
114
|
+
git.log(branch).each do |line|
|
115
|
+
add_story branch, Story.new(line)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module JIRADiff
|
2
|
+
|
3
|
+
class Story
|
4
|
+
def initialize( story )
|
5
|
+
unless story =~ /\S+|\S+/
|
6
|
+
raise ArgumentError, "story must follow 'SHA|description' format"
|
7
|
+
end
|
8
|
+
|
9
|
+
@sha, @description = story.split('|')
|
10
|
+
|
11
|
+
raise ArgumentError if @sha.nil? || @description.nil?
|
12
|
+
end
|
13
|
+
attr_reader :sha
|
14
|
+
|
15
|
+
def split_story( description = @description )
|
16
|
+
raise RuntimeError 'description cannot be blank' unless description
|
17
|
+
|
18
|
+
stories = []
|
19
|
+
story_pattern = /\[?(((SRMPRT|OSMCLOUD)\-\d+)|NO-JIRA)\]?[,:\-\s]+\s*(.*)$/
|
20
|
+
line = description.match(story_pattern)
|
21
|
+
|
22
|
+
if line.nil? # did not find a JIRA ticket pattern
|
23
|
+
stories.push 'NO-JIRA'
|
24
|
+
desc = description.strip
|
25
|
+
else
|
26
|
+
stories.push line.captures[0]
|
27
|
+
desc = line.captures[3].strip
|
28
|
+
end
|
29
|
+
|
30
|
+
# Perform recursion if there are multiple tickets in the description
|
31
|
+
if desc =~ story_pattern
|
32
|
+
new_story, new_desc = split_story desc
|
33
|
+
stories.push new_story
|
34
|
+
desc = new_desc
|
35
|
+
end
|
36
|
+
|
37
|
+
[stories.flatten, desc]
|
38
|
+
end
|
39
|
+
|
40
|
+
def tickets
|
41
|
+
(split_story)[0]
|
42
|
+
end
|
43
|
+
|
44
|
+
def desc
|
45
|
+
(split_story)[1]
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
'[%07.07s] %s - %s' % [sha, tickets.join(', '), desc]
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|