flux 0.0.3 → 0.0.5
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/.flux.local.rb.sample +3 -0
- data/.flux.rb.sample +4 -0
- data/Gemfile +2 -0
- data/README.md +52 -2
- data/VERSION +1 -1
- data/flux.gemspec +18 -11
- data/lib/flux.rb +27 -31
- data/lib/flux/cli.rb +3 -0
- data/lib/flux/cli/feature.rb +39 -0
- data/lib/flux/cli/pivotal_tracker.rb +72 -0
- data/lib/flux/cli/review.rb +190 -0
- data/lib/flux/git.rb +81 -0
- data/lib/flux/pivotal_tracker.rb +81 -0
- data/lib/flux/rcs.rb +1 -0
- data/lib/flux/rcs/git.rb +132 -15
- data/lib/flux/util.rb +0 -1
- data/lib/flux/workflows/mojotech.rb +23 -17
- data/spec/flux_spec.rb +17 -28
- metadata +51 -28
- data/.flux +0 -9
- data/.flux.local.sample +0 -3
- data/lib/flux/trackers/pivotal_tracker.rb +0 -146
- data/lib/flux/util/table.rb +0 -99
- data/spec/flux/rcs/git_spec.rb +0 -47
- data/spec/flux/trackers/pivotal_tracker_spec.rb +0 -164
- data/spec/flux/util/table_spec.rb +0 -47
- data/spec/support/matchers/print_table.rb +0 -46
data/lib/flux/git.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Flux
|
4
|
+
module Git
|
5
|
+
def self.git(path = repo_path)
|
6
|
+
Grit::Git.new(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.repo(path = repo_path)
|
10
|
+
Grit::Repo.new(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.repo_path(start_dir = Dir.pwd)
|
14
|
+
Flux.find_upwards('.git', start_dir) or
|
15
|
+
raise RCSError, "Couldn't find git repo starting at #{start_dir}."
|
16
|
+
end
|
17
|
+
|
18
|
+
class Branch
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
def self.current(repo_path = Git.repo_path)
|
22
|
+
Branch.local(Git.repo(repo_path).head.name, repo_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.remote(remote, name, repo_path = Git.repo_path)
|
26
|
+
new(repo_path, remote, name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.local(name, repo_path = Git.repo_path)
|
30
|
+
new(repo_path, nil, name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def_delegator :head, :commit
|
34
|
+
|
35
|
+
attr_reader :name, :remote
|
36
|
+
|
37
|
+
def initialize(repo_path, remote, name)
|
38
|
+
@git = Git.git(repo_path)
|
39
|
+
@remote = remote
|
40
|
+
@name = name
|
41
|
+
@repo = Git.repo(repo_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def config(var, value = nil)
|
45
|
+
gitvar = "branch.#{name}.#{var.gsub('_', '')}"
|
46
|
+
|
47
|
+
if value
|
48
|
+
@git.config({:add => true}, gitvar, value)
|
49
|
+
else
|
50
|
+
@git.config({}, gitvar).chomp
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create(parent)
|
55
|
+
tap { @git.branch({}, @name, parent.name) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def checkout
|
59
|
+
tap { @git.checkout({}, @name) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def fqdn
|
63
|
+
"#{remote}/#{name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def publish(upstream)
|
67
|
+
tap {
|
68
|
+
@git.push({}, upstream.remote, "+#{@name}:#{upstream.name}")
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def track(upstream)
|
73
|
+
tap { @git.branch({:set_upstream => true}, @name, upstream.fqdn) }
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def_delegator :@repo, :head
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Flux
|
2
|
+
module PT
|
3
|
+
class Project
|
4
|
+
attr_reader :id
|
5
|
+
|
6
|
+
def initialize(id)
|
7
|
+
@id = id
|
8
|
+
end
|
9
|
+
|
10
|
+
def members
|
11
|
+
::PivotalTracker::Membership.all(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def stories
|
15
|
+
Story::Finders.new(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Story
|
20
|
+
STATES_W_ESTIMATE = %w(started finished delivered accepted rejected)
|
21
|
+
STATES = %w(unscheduled unstarted) + STATES_W_ESTIMATE
|
22
|
+
|
23
|
+
extend Forwardable
|
24
|
+
|
25
|
+
class Finders
|
26
|
+
def initialize(project)
|
27
|
+
@project = project
|
28
|
+
end
|
29
|
+
|
30
|
+
def find(id)
|
31
|
+
story = ::PivotalTracker::Story.find(id, @project.id) or
|
32
|
+
raise TrackerError, "Couldn't find story #{id}."
|
33
|
+
|
34
|
+
Story.new(story)
|
35
|
+
end
|
36
|
+
|
37
|
+
def scheduled
|
38
|
+
::PivotalTracker::Iteration.
|
39
|
+
current_backlog(@project).
|
40
|
+
map(&:stories).
|
41
|
+
flatten.map { |s| Story.new(s) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def_delegators :@story, :id, :name, :owned_by, :current_state, :url
|
46
|
+
|
47
|
+
alias_method :state, :current_state
|
48
|
+
|
49
|
+
def self.id_from_url(url)
|
50
|
+
URI.parse(url).path.split('/').last
|
51
|
+
end
|
52
|
+
|
53
|
+
def initialize(story)
|
54
|
+
@story = story
|
55
|
+
end
|
56
|
+
|
57
|
+
def estimate
|
58
|
+
(@story.estimate && @story.estimate < 0) ? nil : @story.estimate
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :est, :estimate
|
62
|
+
|
63
|
+
def update(attrs)
|
64
|
+
if attrs[:current_state]
|
65
|
+
unless STATES.include?(attrs[:current_state])
|
66
|
+
raise TrackerError, "Invalid state: #{attrs[:current_state]}"
|
67
|
+
end
|
68
|
+
|
69
|
+
if STATES_W_ESTIMATE.include?(attrs[:current_state]) &&
|
70
|
+
! estimate &&
|
71
|
+
! attrs[:estimate]
|
72
|
+
raise TrackerError,
|
73
|
+
"Need an estimate for state `#{attrs[:current_state]}'."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@story.update attrs
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/flux/rcs.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'flux/rcs/git'
|
data/lib/flux/rcs/git.rb
CHANGED
@@ -1,33 +1,150 @@
|
|
1
1
|
require 'grit'
|
2
|
+
require 'json'
|
2
3
|
|
3
4
|
module Flux
|
4
5
|
module RCS
|
5
6
|
class Branches < Thor
|
6
7
|
namespace :branches
|
7
8
|
|
8
|
-
desc "
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
desc "review BRANCH_NAME", "review a branch"
|
10
|
+
method_option :parent, :type => :string, :default => "master", :desc => "upstream"
|
11
|
+
method_option :close, :type => :boolean, :default => false, :desc => "whether or not to close previous review requests for this branch", :aliases => '-c'
|
12
|
+
def review(branch_name=nil)
|
13
|
+
# note: we ignore any closed review requests for this branch
|
14
|
+
# so, if there are no open review requests, this one will start out at version 1.
|
15
|
+
related = open_reviews_for(branch_name)
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
+
if branch_name
|
18
|
+
# we use the sha instead of the branch name in order to prevent GH from
|
19
|
+
# updating the pull-request when someone pushed to the branch again.
|
20
|
+
head = repo.get_head(branch_name).commit.sha
|
21
|
+
else
|
22
|
+
head = repo.head.commit.sha
|
23
|
+
end
|
24
|
+
|
25
|
+
if related.last
|
26
|
+
previous_title = related.last["title"]
|
27
|
+
previous_version = previous_title.match(/Please review .+ \(v(\d+)\)/)[1].to_i
|
28
|
+
new_version = previous_version + 1
|
29
|
+
else
|
30
|
+
new_version = 1
|
31
|
+
end
|
32
|
+
history = related.map {|pr| pr["title"] }.join("\n")
|
33
|
+
history = "PRIOR VERSIONS: \n" + history unless history.empty?
|
34
|
+
data = {
|
35
|
+
:title => "Please review #{branch_name} (v#{new_version}) (#{head[0..8]})",
|
36
|
+
:head => head,
|
37
|
+
:base => options[:parent],
|
38
|
+
:body => history
|
39
|
+
}.to_json
|
40
|
+
cmd = "curl -s #{auth_string} #{pulls_url} -d '#{data}'"
|
17
41
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
42
|
+
puts cmd if @debug
|
43
|
+
new_pr = JSON.parse(`#{cmd}`)
|
44
|
+
if new_pr["number"].to_i > 0
|
45
|
+
puts "Created a new review request: #{new_pr["html_url"]}"
|
46
|
+
else
|
47
|
+
puts "Error: "
|
48
|
+
puts new_pr
|
49
|
+
return
|
50
|
+
end
|
22
51
|
|
23
|
-
if options
|
24
|
-
|
25
|
-
|
52
|
+
if options.close?
|
53
|
+
# close previous pull requests
|
54
|
+
related.each {|pr| close_pull_request(pr) }
|
26
55
|
end
|
56
|
+
new_pr
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "all_reviews", "list all pending reviews"
|
60
|
+
def all_reviews
|
61
|
+
reviews("open").group_by {|pr|
|
62
|
+
pr["title"].match(/Please review (.+) \(v.+\)/)[1]
|
63
|
+
}.each_pair {|branch_name, prs|
|
64
|
+
puts "\nReviews for #{branch_name}:"
|
65
|
+
prior_head = nil
|
66
|
+
pr = nil
|
67
|
+
our_reviews = reviews_for(branch_name)
|
68
|
+
our_reviews.each {|pr|
|
69
|
+
head = pr["title"].match(/Please review .+ \(v.+\) \((.+)\)/)[1]
|
70
|
+
title = pr["title"].sub("Please review #{branch_name}", "")
|
71
|
+
line = " #{title} : #{pulls_url}/#{pr["number"]}"
|
72
|
+
if prior_head
|
73
|
+
line << " : #{compare_cmd(prior_head, head)}"
|
74
|
+
end
|
75
|
+
puts line
|
76
|
+
prior_head = head
|
77
|
+
}
|
78
|
+
puts " Latest review request: #{our_reviews.last["html_url"]}"
|
79
|
+
}
|
27
80
|
end
|
28
81
|
|
29
82
|
private
|
30
83
|
|
84
|
+
def compare_cmd(prior_head, head)
|
85
|
+
"git diff #{prior_head}..#{head}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def reviews(state = "open")
|
89
|
+
@cached ||= {}
|
90
|
+
return @cached[state] if @cached[state]
|
91
|
+
cmd = "curl -s #{auth_string} #{pulls_url}?state=#{state}"
|
92
|
+
puts cmd if @debug
|
93
|
+
pull_requests = JSON.parse(`#{cmd}`)
|
94
|
+
@cached[state] = pull_requests.select {|pr|
|
95
|
+
pr["title"] =~ /Please review .+ \(v.+\)/
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def filter_for_branch(pull_requests, branch_name)
|
100
|
+
pull_requests.select {|pr|
|
101
|
+
pr["title"] =~ /Please review #{branch_name} \(v.+\)/
|
102
|
+
}.sort_by {|pr| pr["title"] }
|
103
|
+
end
|
104
|
+
|
105
|
+
def open_reviews_for(branch_name)
|
106
|
+
filter_for_branch(reviews("open"), branch_name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def closed_reviews_for(branch_name)
|
110
|
+
filter_for_branch(reviews("closed"), branch_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
def reviews_for(branch_name)
|
114
|
+
filter_for_branch(reviews("closed"), branch_name) + filter_for_branch(reviews("open"), branch_name)
|
115
|
+
end
|
116
|
+
|
117
|
+
def close_pull_request(pr)
|
118
|
+
puts "closing review request: #{pr["title"]}"
|
119
|
+
data = {
|
120
|
+
"state" => "closed",
|
121
|
+
"body" => "This review request has been replaced by: \n" + pr["body"]
|
122
|
+
}.to_json
|
123
|
+
cmd = "curl -s -X PATCH #{auth_string} #{pulls_url}/#{pr["number"]} -d '#{data}'"
|
124
|
+
puts cmd if @debug
|
125
|
+
`#{cmd}`
|
126
|
+
end
|
127
|
+
|
128
|
+
def api_host
|
129
|
+
"https://api.github.com"
|
130
|
+
end
|
131
|
+
|
132
|
+
def pulls_url
|
133
|
+
repo_user = config['repo_user']
|
134
|
+
repo_name = config['repo_name']
|
135
|
+
"#{api_host}/repos/#{repo_user}/#{repo_name}/pulls"
|
136
|
+
end
|
137
|
+
|
138
|
+
def auth_string
|
139
|
+
username = config['username']
|
140
|
+
password = config['password']
|
141
|
+
"-u \"#{username}:#{password}\""
|
142
|
+
end
|
143
|
+
|
144
|
+
def config
|
145
|
+
Flux.environment['github']
|
146
|
+
end
|
147
|
+
|
31
148
|
def git
|
32
149
|
@git ||= Grit::Git.new(repo_path)
|
33
150
|
end
|
data/lib/flux/util.rb
CHANGED
@@ -1,32 +1,38 @@
|
|
1
|
+
require 'flux/git'
|
2
|
+
|
1
3
|
module Flux
|
2
4
|
module Workflows
|
3
5
|
module MojoTech
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
namespace :dev
|
8
|
-
|
9
|
-
desc "link STORY_ID", "link a story to a branch"
|
10
|
-
def link(story_id)
|
11
|
-
invoke 'stories:update',
|
12
|
-
[story_id],
|
13
|
-
:attributes => {'branch' => current_branch_id}
|
14
|
-
end
|
6
|
+
class Feature < Thor
|
7
|
+
namespace :feature
|
15
8
|
|
16
9
|
desc "start STORY_ID BRANCH_ID", "start working on a story"
|
17
|
-
method_option :estimate,
|
10
|
+
method_option :estimate, :type => :numeric
|
11
|
+
method_option :parent_branch, :type => :string,
|
12
|
+
:default => 'master',
|
13
|
+
:aliases => '-b'
|
18
14
|
def start(story_id, branch_id)
|
19
15
|
invoke 'stories:grab', [story_id], :estimate => options[:estimate]
|
20
16
|
invoke 'stories:start', [story_id]
|
21
|
-
|
22
|
-
|
23
|
-
invoke :link, [story_id]
|
17
|
+
|
18
|
+
create_branch branch_id, options
|
24
19
|
end
|
25
20
|
|
26
21
|
private
|
27
22
|
|
28
|
-
def
|
29
|
-
|
23
|
+
def create_branch(branch_id, options = {})
|
24
|
+
up = Branch.remote(repo_path, 'origin', branch_id)
|
25
|
+
parent = Branch.local(repo_path, options[:parent_branch])
|
26
|
+
branch = Branch.local(repo_path, branch_id).
|
27
|
+
create(parent).
|
28
|
+
publish(up).
|
29
|
+
track(up).
|
30
|
+
checkout
|
31
|
+
end
|
32
|
+
|
33
|
+
def repo_path
|
34
|
+
Flux.find_upwards('.git', Dir.pwd) or
|
35
|
+
raise RCSError, "Couldn't find git repo starting at #{Dir.pwd}."
|
30
36
|
end
|
31
37
|
end
|
32
38
|
end
|
data/spec/flux_spec.rb
CHANGED
@@ -3,41 +3,30 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
3
3
|
require 'flux'
|
4
4
|
|
5
5
|
describe Flux do
|
6
|
-
|
7
|
-
let(:
|
8
|
-
let(:
|
9
|
-
let(:flux_env_l) { File.join(root, Flux::RC_LOCAL) }
|
6
|
+
describe "project files" do
|
7
|
+
let(:current_dir) { File.expand_path(File.dirname(__FILE__)) }
|
8
|
+
let(:project_dir) { File.expand_path(File.join(current_dir, '..')) }
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
mock(File).exist?(flux_env) { true }
|
15
|
-
}
|
16
|
-
|
17
|
-
it "merges the environment" do
|
18
|
-
mock(File).exist?(flux_env_l) { true }
|
19
|
-
|
20
|
-
mock(YAML).load_file(flux_env) {
|
21
|
-
{'trackers' => {'project_id' => 123},
|
22
|
-
'workflow' => 'mojotech'}
|
23
|
-
}
|
24
|
-
mock(YAML).load_file(flux_env_l) {
|
25
|
-
{'trackers' => {'token' => 'mytoken'}}
|
10
|
+
it "uses an rc file called #{Flux::RC}" do
|
11
|
+
Dir.chdir(current_dir) {
|
12
|
+
Flux.rc.should == File.join(project_dir, Flux::RC)
|
26
13
|
}
|
14
|
+
end
|
27
15
|
|
28
|
-
|
29
|
-
|
30
|
-
|
16
|
+
it "requires #{Flux::RC} to exist" do
|
17
|
+
lambda {
|
18
|
+
Dir.chdir('/') { Flux.rc }
|
19
|
+
}.should raise_error(Flux::FluxError)
|
31
20
|
end
|
32
21
|
|
33
|
-
it "
|
34
|
-
|
35
|
-
|
22
|
+
it "allows the user to keep a local rc file called #{Flux::RC_LOCAL}" do
|
23
|
+
Dir.chdir(current_dir) {
|
24
|
+
Flux.rc_local.should == File.join(project_dir, Flux::RC_LOCAL)
|
36
25
|
}
|
26
|
+
end
|
37
27
|
|
38
|
-
|
39
|
-
|
40
|
-
defined?(Flux::Trackers::PivotalTracker).should be_true
|
28
|
+
it "does not require #{Flux::RC_LOCAL} to exist" do
|
29
|
+
lambda { Dir.chdir('/') { Flux.rc_local } }.should_not raise_error
|
41
30
|
end
|
42
31
|
end
|
43
32
|
end
|