gitra 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ ## Git repository analyser
2
+
3
+ (The too-soon version)
4
+
5
+ [![Build Status](https://travis-ci.org/sldblog/gitra.png?branch=master)](https://travis-ci.org/sldblog/gitra)
6
+
7
+ ### Usage
8
+
9
+ ```
10
+ Usage: gitra [options]
11
+ -a, --analyse [BRANCH] Analyze the repository in relation the the specified branch (defaults to master).
12
+ -l, --log SINCE_REV Shows the stories and bugs committed since the specified revision.
13
+ ```
14
+
15
+ ### Sample use case
16
+
17
+ Given the following git repository:
18
+
19
+ ```
20
+ $ git log --oneline --graph --all --decorate
21
+ * 0fbdc16 (HEAD, master) master #3
22
+ * 08a0df3 Merge branch 'test'
23
+ |\
24
+ * | 23e7058 master #2
25
+ * | b4c12ca master #1
26
+ | | * 08e7339 (test) test #3
27
+ | |/
28
+ | * af75469 test #2
29
+ | * de14754 test #1
30
+ |/
31
+ * 6530d98 first
32
+ ```
33
+
34
+ You can analyse the ahead/behind information using `gitra`.
35
+
36
+ By default, it analyses the active branch in the repository it is in (you have to be physically in the repo directory for now):
37
+
38
+ ```
39
+ $ git branch
40
+ * master
41
+ test
42
+ ```
43
+
44
+ ```
45
+ $ gitra -a
46
+ ---- Analyzing branches in relation to master ----
47
+ Analysing 2 branches: ..
48
+ master merged
49
+ test 1 ahead (unmerged), (but 2 behind)
50
+ ```
51
+
52
+ You can specify any branches as part of the command line:
53
+
54
+ ```
55
+ $ gitra -a test
56
+ ---- Analyzing branches in relation to test ----
57
+ Analysing 2 branches: ..
58
+ test merged
59
+ master 2 ahead (unmerged), (but 1 behind)
60
+ ```
61
+
62
+ As you can see, the reported information is in line compared to the repository graph displayed above:
63
+ - `master` is indeed ahead by 2 commits (`08a0df3`, `0fbdc16`), but missing 1 (`08e7339`), when viewed from `test` branch
64
+ - the other way around if viewed from the `master` branch
65
+
66
+ This kind of analysis really helps our current team to get on top of the current branching strategy we inherited.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => [:test]
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'spec/support'
7
+ t.test_files = FileList['spec/**/*_spec.rb']
8
+ t.verbose = true
9
+ end
data/bin/gitra ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'gitra'
5
+ require 'optparse'
6
+ require 'ostruct'
7
+ require 'term/ansicolor'
8
+
9
+ class String; include Term::ANSIColor; end
10
+
11
+ options = OpenStruct.new
12
+ parser = OptionParser.new do |opts|
13
+ opts.banner = "Usage: gitra [options]"
14
+
15
+ opts.on("-a [BRANCH]", "--analyse [BRANCH]", "Analyze the repository in relation the the specified branch (defaults to master).") do |branch|
16
+ options.command = :analyze
17
+ options.parameter = branch || 'master'
18
+ end
19
+ opts.on("-l SINCE_REV", "--log SINCE_REV", "Shows the stories and bugs committed since the specified revision.") do |since_rev|
20
+ options.command = :history
21
+ options.parameter = since_rev
22
+ end
23
+ end
24
+
25
+ begin
26
+ parser.parse!
27
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
28
+ $stderr.puts e.to_s.red
29
+ end
30
+
31
+ if options.command
32
+ cli = Gitra::CLI.new
33
+ cli.send(options.command, options.parameter)
34
+ else
35
+ puts parser
36
+ end
data/lib/gitra.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'gitra/tracker'
2
+ require 'gitra/cli'
3
+ require 'gitra/git_patches'
4
+ require 'gitra/parser'
data/lib/gitra/cli.rb ADDED
@@ -0,0 +1,64 @@
1
+ require 'term/ansicolor'
2
+
3
+ class String
4
+ include Term::ANSIColor
5
+ end
6
+
7
+ module Gitra
8
+
9
+ class CLI
10
+ def initialize
11
+ @tracker = Tracker.new '.'
12
+ end
13
+
14
+ def analyze(reference_branch)
15
+ reference_branch ||= @tracker.current_branch
16
+
17
+ $stdout.puts "---- Analyzing branches in relation to #{reference_branch.yellow} ----"
18
+ branches = ([reference_branch] + @tracker.branches).uniq!
19
+
20
+ $stdout.print "Analysing #{branches.size} branches: "
21
+ branch_analysis = branches.map do |branch|
22
+ $stdout.print '.'
23
+ $stdout.flush
24
+ unmerged = @tracker.branch(branch).commits_since(reference_branch).collect { |c| c.sha }
25
+ behind = @tracker.branch(reference_branch).commits_since(branch).collect { |c| c.sha }
26
+ {:name => branch, :behind => behind.size, :unmerged => unmerged.size}
27
+ end
28
+ $stdout.puts
29
+
30
+ name_max_size = branch_analysis.map { |item| item[:name].size }.sort.last
31
+ branch_analysis.each do |item|
32
+ state = []
33
+ state << "#{item[:unmerged]} ahead (unmerged)".red if item[:unmerged] > 0
34
+ state << "(but #{item[:behind]} behind)" if item[:unmerged] > 0 and item[:behind] > 0
35
+ state << 'merged'.green if state.empty?
36
+ $stdout.puts "%#{name_max_size}s %s" % [item[:name], state.join(', ')]
37
+ end
38
+ end
39
+
40
+ def history(since_revision)
41
+ current = @tracker.current_branch
42
+
43
+ $stdout.puts "---- Analyzing commit history from #{since_revision.yellow} to #{current.yellow} ----"
44
+ commits = @tracker.branch(current).commits_since(since_revision)
45
+
46
+ $stdout.print "Analysing #{commits.size} commits: "
47
+ commit_parser = Gitra::Parser.new('.gitra-rules.yml')
48
+ commits.each do |commit|
49
+ $stdout.print '.'
50
+ $stdout.flush
51
+ commit_parser.use(commit)
52
+ end
53
+ $stdout.puts
54
+
55
+ commit_parser.result.each_pair do |type, ids|
56
+ ids.sort.each do |id, commits|
57
+ description = commits.map { |c| c.sha.slice(0..10) }.join(', ')
58
+ $stdout.puts "%28s - %s" % ["#{type.to_s} #{id.to_s}".yellow, description]
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,27 @@
1
+ require 'git'
2
+
3
+ module Git
4
+ class Base
5
+ def merge_base(commit1, commit2)
6
+ self.lib.merge_base(commit1, commit2)
7
+ end
8
+
9
+ def log_ancestry(from, to)
10
+ self.lib.log_ancestry(from, to).map { |c| Git::Object::Commit.new(self, c['sha'], c) }
11
+ end
12
+ end
13
+
14
+ class Lib
15
+ def merge_base(commit1, commit2)
16
+ command('merge-base', [commit1, commit2])
17
+ end
18
+
19
+ def log_ancestry(from, to)
20
+ arr_opts = ['--pretty=raw']
21
+ arr_opts << "#{from.to_s}..#{to.to_s}"
22
+ arr_opts << '--ancestry-path'
23
+ full_log = command_lines('log', arr_opts, true)
24
+ process_commit_data(full_log)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ require 'yaml'
2
+
3
+ module Gitra
4
+ class Parser
5
+ def initialize(rule_file)
6
+ rule_file ||= '.gitra-rules.yml'
7
+ @rules = YAML.load_file(rule_file)
8
+
9
+ @uses = {}
10
+ @rules.each_pair do |name, patterns|
11
+ @uses[name] = {}
12
+ patterns.map! { |p| Regexp.new p }
13
+ end
14
+ end
15
+
16
+ def use(commit)
17
+ @rules.each_pair do |name, patterns|
18
+ patterns.each do |pattern|
19
+ match = pattern.match commit.message
20
+ next unless match
21
+
22
+ id = match[1].to_i
23
+ (@uses[name][id] ||= []) << commit
24
+ end
25
+ end
26
+ end
27
+
28
+ def result
29
+ @uses
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ require 'git'
2
+
3
+ module Gitra
4
+
5
+ class Tracker
6
+ def initialize(repository)
7
+ @git = Git.open(repository)
8
+ end
9
+
10
+ def current_branch
11
+ @git.current_branch
12
+ end
13
+
14
+ def branches
15
+ @git.branches.select do |branch|
16
+ name = branch.full.gsub(%r{^remotes/}, '')
17
+ next if name =~ / -> /
18
+ block_given? ? yield(name) : true
19
+ end.map { |branch| branch.full.gsub(%r{^remotes/}, '') }
20
+ end
21
+
22
+ def branch(branch)
23
+ TrackedBranch.new(@git, branch)
24
+ end
25
+
26
+ def method_missing(name, *args, &block)
27
+ raise NoMethodError, "method `#{name}' should be used as `<tracker>.branch(reference).#{name}(<args>)'" if TrackedBranch.method_defined? name
28
+ super
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ class TrackedBranch
35
+ def initialize(git, branch)
36
+ @git = git
37
+ @branch = branch.to_s
38
+ end
39
+
40
+ def commits_since(reference, options = {:ancestry => true})
41
+ since = @git.object(reference.to_s)
42
+ base = @git.merge_base(@branch, since)
43
+ if options[:ancestry]
44
+ @git.log_ancestry(base, @branch).reverse
45
+ else
46
+ @git.log(2**16).between(base, @branch).to_a.reverse
47
+ end
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,3 @@
1
+ module Gitra
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,71 @@
1
+ require 'git'
2
+ require 'fileutils'
3
+
4
+ class GitHelper
5
+
6
+ def initialize
7
+ path = File.join(Dir.tmpdir, "#{Time.now.to_i}_#{rand(1000)}")
8
+ FileUtils.mkdir_p path
9
+ Dir.chdir path do
10
+ @git = Git.init
11
+ @git.config('user.name', 'John Testable')
12
+ @git.config('user.email', 'john.testable@testing.it')
13
+
14
+ File.new(".gitignore", "w")
15
+ @git.add(".gitignore")
16
+ @git.commit "Initial commit."
17
+ end
18
+ end
19
+
20
+ def path
21
+ @git.dir.path
22
+ end
23
+
24
+ def cleanup
25
+ FileUtils.rm_rf(path, :secure => true)
26
+ end
27
+
28
+ def commit_to(branch_sym, parent_sym = :master)
29
+ branch = branch_sym.to_s
30
+ parent = parent_sym.to_s
31
+
32
+ @git.checkout parent
33
+ @git.branch(branch).create
34
+ @git.checkout branch
35
+ file = File.new(File.join(path, "file_of_#{branch}"), "a+")
36
+ file.puts("additional line")
37
+ file.close
38
+ @git.add file.path
39
+ @git.commit "Update #{branch}"
40
+ @git.checkout 'master'
41
+
42
+ @git.object(branch).sha
43
+ end
44
+
45
+ def branch_off(branch_hash)
46
+ branch_hash.collect do |branch_sym, into_sym|
47
+ branch = branch_sym.to_s
48
+ into = into_sym.to_s
49
+
50
+ @git.checkout branch
51
+ @git.branch(into).create
52
+ @git.checkout 'master'
53
+
54
+ @git.object(into).sha
55
+ end
56
+ end
57
+
58
+ def merge(merge_hash)
59
+ merge_hash.collect do |branch_sym, into_sym|
60
+ branch = branch_sym.to_s
61
+ into = into_sym.to_s
62
+
63
+ @git.checkout into
64
+ @git.merge branch, "Merge #{branch}"
65
+ @git.checkout 'master'
66
+
67
+ @git.object(into).sha
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,3 @@
1
+ require 'minitest/autorun'
2
+ require 'gitra'
3
+ require 'git_helper'
@@ -0,0 +1,80 @@
1
+ require 'minitest_helper'
2
+ require 'git'
3
+
4
+ describe Gitra::Tracker do
5
+ let(:git) { GitHelper.new }
6
+ let(:tracker) { Gitra::Tracker.new(git.path) }
7
+
8
+ after do
9
+ git.cleanup
10
+ end
11
+
12
+ describe 'Selects branches' do
13
+ it 'should list all branches without block' do
14
+ git.commit_to :b1
15
+ git.commit_to :b2
16
+ tracker.branches.sort.must_equal %w{master b1 b2}.sort
17
+ end
18
+
19
+ it 'should list matching branches with block' do
20
+ git.commit_to :b1
21
+ git.commit_to :b2
22
+ tracker.branches { |name| name =~ /^b/ }.sort.must_equal %w{b1 b2}.sort
23
+ end
24
+ end
25
+
26
+ describe 'Shows commits from merge base' do
27
+ it 'should contain all commits from branching point' do
28
+ git.commit_to :master
29
+ git.branch_off :master => :release
30
+ git.commit_to :release
31
+ after_branch_off_commits = [
32
+ git.commit_to(:master),
33
+ git.commit_to(:master)
34
+ ]
35
+ tracker.branch(:master).commits_since(:release).map { |c| c.sha }.must_equal after_branch_off_commits
36
+ end
37
+
38
+ it 'should only contain commits with real ancestry' do
39
+ git.commit_to :master
40
+ git.branch_off :master => :release
41
+ 2.times { git.commit_to :release }
42
+ 2.times { git.commit_to :master }
43
+
44
+ only_on_master = []
45
+ only_on_master += git.merge(:release => :master)
46
+ only_on_master << git.commit_to(:master)
47
+ git.commit_to :release
48
+
49
+ tracker.branch(:master).commits_since(:release).map { |c| c.sha }.must_equal only_on_master
50
+ end
51
+
52
+ it 'should be able to show more than the default log limit (30)' do
53
+ git.commit_to :master
54
+ git.branch_off :master => :release
55
+
56
+ limit = 40
57
+ limit.times { git.commit_to :release }
58
+ tracker.branch(:release).commits_since(:master).size.must_equal limit
59
+ end
60
+
61
+ it 'should fail if "since" branch is not resolvable' do
62
+ proc { tracker.branch(:master).commits_since(:new_branch) }.must_raise Git::GitExecuteError
63
+ end
64
+
65
+ it 'should fail if "from" branch is not resolvable' do
66
+ proc { tracker.branch(:neverwhere).commits_since(:master) }.must_raise Git::GitExecuteError
67
+ end
68
+ end
69
+
70
+ describe 'Shows unmerged commits between two branches' do
71
+ it 'should contain all unmerged commits' do
72
+ git.commit_to :master
73
+ git.branch_off :master => :release
74
+ missing_commit = git.commit_to :release
75
+
76
+ tracker.branch(:release).commits_since(:master).map { |c| c.sha }.must_equal [missing_commit]
77
+ end
78
+ end
79
+
80
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitra
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - David Lantos
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2013-07-16 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: git
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: term-ansicolor
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :development
75
+ version_requirements: *id004
76
+ description: Analyze branches and continuity in a git repository.
77
+ email:
78
+ - david.lantos@gmail.com
79
+ executables:
80
+ - gitra
81
+ extensions: []
82
+
83
+ extra_rdoc_files: []
84
+
85
+ files:
86
+ - Rakefile
87
+ - bin/gitra
88
+ - lib/gitra/cli.rb
89
+ - lib/gitra/git_patches.rb
90
+ - lib/gitra/parser.rb
91
+ - lib/gitra/tracker.rb
92
+ - lib/gitra/version.rb
93
+ - lib/gitra.rb
94
+ - spec/support/git_helper.rb
95
+ - spec/support/minitest_helper.rb
96
+ - spec/tracker_spec.rb
97
+ - README.md
98
+ homepage: http://github.com/sldblog/gitra
99
+ licenses: []
100
+
101
+ post_install_message:
102
+ rdoc_options: []
103
+
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 57
112
+ segments:
113
+ - 1
114
+ - 8
115
+ - 7
116
+ version: 1.8.7
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ hash: 3
123
+ segments:
124
+ - 0
125
+ version: "0"
126
+ requirements: []
127
+
128
+ rubyforge_project:
129
+ rubygems_version: 1.8.25
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Git Repository Analyzer
133
+ test_files:
134
+ - spec/support/git_helper.rb
135
+ - spec/support/minitest_helper.rb
136
+ - spec/tracker_spec.rb