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.
@@ -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
@@ -0,0 +1 @@
1
+ require 'flux/rcs/git'
@@ -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 "current", "show the current branch"
9
- def current
10
- repo.head.name.tap { |h| $stdout.puts h }
11
- end
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
- desc "checkout BRANCH_ID", "checkout a branch"
14
- def checkout(branch_id)
15
- git.checkout({}, branch_id)
16
- end
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
- desc "create BRANCH_ID", "create a branch"
19
- method_option :public, :type => :boolean, :aliases => '-p'
20
- def create(branch_id)
21
- git.branch({}, branch_id)
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[:public]
24
- git.push({}, 'origin', "+#{branch_id}:#{branch_id}")
25
- git.branch({:set_upstream => true}, branch_id, "origin/#{branch_id}")
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
@@ -1,3 +1,2 @@
1
- require 'flux/util/table'
2
1
  require 'flux/util/output'
3
2
 
@@ -1,32 +1,38 @@
1
+ require 'flux/git'
2
+
1
3
  module Flux
2
4
  module Workflows
3
5
  module MojoTech
4
- class Developer < Thor
5
- include Util::Output
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, :type => :numeric
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
- invoke 'branches:create', [branch_id]
22
- invoke 'branches:checkout', [branch_id]
23
- invoke :link, [story_id]
17
+
18
+ create_branch branch_id, options
24
19
  end
25
20
 
26
21
  private
27
22
 
28
- def current_branch_id
29
- silence { @current_branch_id ||= invoke('branches:current', []) }
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
@@ -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
- context "with environment files" do
7
- let(:root) { File.expand_path(File.dirname(__FILE__ + '/../../..')) }
8
- let(:flux_env) { File.join(root, Flux::RC) }
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
- before {
12
- mock(File).exist?(is_a(String)).any_times { false }
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
- Flux.setup.should ==
29
- {'trackers' => {'project_id' => 123, 'token' => 'mytoken'},
30
- 'workflow' => 'mojotech'}
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 "instantiates an adapter" do
34
- mock(YAML).load_file(flux_env) {
35
- {'trackers' => {'adapter' => 'pivotal_tracker'}}
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
- Flux.setup
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