flux 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ use :pivotal_tracker, token: 'abcdef1234567890',
2
+ email: 'david@mojotech.com'
3
+ use :github, username: 'david', password: 'password'
@@ -0,0 +1,4 @@
1
+ use :pivotal_tracker, project_id: 128808
2
+ use :git
3
+ use :github, repo_user: 'mojotech', repo_name: 'flux'
4
+ use :mojotech
data/Gemfile CHANGED
@@ -5,6 +5,8 @@ source "http://rubygems.org"
5
5
  gem "thor", "~> 0.14.0"
6
6
  gem "pivotal-tracker", "~> 0.4.0"
7
7
  gem "grit", "~> 2.4.0"
8
+ gem 'hirb'
9
+ gem 'github_api'
8
10
 
9
11
  # Add dependencies to develop your gem here.
10
12
  # Include everything needed to run rake, tests, features, etc.
data/README.md CHANGED
@@ -46,8 +46,8 @@ This project's `.flux` file is as follows:
46
46
  The `.flux` file should be used for project-wide configuration and kept under
47
47
  version control. Obviously you aren't going to store your account credentials
48
48
  there, so you need a different place where you can store them. That place is
49
- `.flux.local`. If you're using Git, this file should be added to the project's
50
- `.gitignore`.
49
+ `.flux.local`. If you're using Git (and really, why wouldn't you?), this file
50
+ should be added to the project's `.gitignore`.
51
51
 
52
52
  Here's an example of a `.flux.local` file.
53
53
 
@@ -56,3 +56,53 @@ Here's an example of a `.flux.local` file.
56
56
  email: david@mojotech.com
57
57
 
58
58
  Note for Pivotal Tracker: Your API token is at https://www.pivotaltracker.com/profile
59
+
60
+
61
+ Code Reviews Workflow
62
+ ---------------------
63
+
64
+ # start your feature on a branch
65
+ $ git checkout feature-branch
66
+
67
+ # code your feature
68
+ $ git commit # rinse, repeat
69
+
70
+ # make sure your code to review is available
71
+ $ git push
72
+
73
+ # request a review
74
+ $ flux branches:review feature-branch [ --parent master ]
75
+ # Note: currently, you must explicitly list your feature branch name
76
+ # This feature branch name will be used to keep track of multiple rounds
77
+ # of the review.
78
+
79
+ # flux will generate a pull request and print its URL
80
+
81
+ # Github takes sends out notices.
82
+ # other devs review and comment
83
+
84
+ # You fix up your commits.
85
+ $ git rebase -i origin/master
86
+ # I'm in your branch... rewriting your history.
87
+
88
+ # Time to resubmit for review
89
+ $ git push
90
+ $ flux request-review feature-branch [ --parent master ] [ --close ]
91
+
92
+ # flux closes old review request for feature-branch (optionally)
93
+ # flux opens a new review request for feature-branch
94
+ # The process repeats.
95
+
96
+ # Now the reviewers want to see if you addressed their concerns.
97
+ # They run:
98
+ $ flux branches:all_reviews
99
+ # (TODO: should be able to limit this report to one feature branch)
100
+
101
+ # They see: the most recent review request for each branch, but also links
102
+ # to the diffs between each previous review request for that branch.
103
+
104
+ # So, instead of repeating the entire review, they can look at just the "inter-diff"
105
+
106
+
107
+
108
+
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.0.5
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "flux"
8
- s.version = "0.0.3"
8
+ s.version = "0.0.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["David Leal"]
12
- s.date = "2011-10-07"
12
+ s.date = "2012-02-17"
13
13
  s.email = "david@mojotech.com"
14
14
  s.executables = ["flux"]
15
15
  s.extra_rdoc_files = [
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
21
- ".flux",
22
- ".flux.local.sample",
21
+ ".flux.local.rb.sample",
22
+ ".flux.rb.sample",
23
23
  ".rspec",
24
24
  "Gemfile",
25
25
  "LICENSE.txt",
@@ -29,25 +29,26 @@ Gem::Specification.new do |s|
29
29
  "bin/flux",
30
30
  "flux.gemspec",
31
31
  "lib/flux.rb",
32
+ "lib/flux/cli.rb",
33
+ "lib/flux/cli/feature.rb",
34
+ "lib/flux/cli/pivotal_tracker.rb",
35
+ "lib/flux/cli/review.rb",
32
36
  "lib/flux/ext/pivotal-tracker.rb",
37
+ "lib/flux/git.rb",
38
+ "lib/flux/pivotal_tracker.rb",
39
+ "lib/flux/rcs.rb",
33
40
  "lib/flux/rcs/git.rb",
34
- "lib/flux/trackers/pivotal_tracker.rb",
35
41
  "lib/flux/util.rb",
36
42
  "lib/flux/util/output.rb",
37
- "lib/flux/util/table.rb",
38
43
  "lib/flux/workflows/mojotech.rb",
39
- "spec/flux/rcs/git_spec.rb",
40
- "spec/flux/trackers/pivotal_tracker_spec.rb",
41
- "spec/flux/util/table_spec.rb",
42
44
  "spec/flux_spec.rb",
43
45
  "spec/spec_helper.rb",
44
- "spec/support/matchers/print_table.rb",
45
46
  "spec/support/rr.rb"
46
47
  ]
47
48
  s.homepage = "http://github.com/mojotech/flux"
48
49
  s.licenses = ["MIT"]
49
50
  s.require_paths = ["lib"]
50
- s.rubygems_version = "1.8.10"
51
+ s.rubygems_version = "1.8.11"
51
52
  s.summary = "Command line workflow manager."
52
53
 
53
54
  if s.respond_to? :specification_version then
@@ -57,6 +58,8 @@ Gem::Specification.new do |s|
57
58
  s.add_runtime_dependency(%q<thor>, ["~> 0.14.0"])
58
59
  s.add_runtime_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
59
60
  s.add_runtime_dependency(%q<grit>, ["~> 2.4.0"])
61
+ s.add_runtime_dependency(%q<hirb>, [">= 0"])
62
+ s.add_runtime_dependency(%q<github_api>, [">= 0"])
60
63
  s.add_development_dependency(%q<rspec>, ["~> 2.0"])
61
64
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
62
65
  s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
@@ -66,6 +69,8 @@ Gem::Specification.new do |s|
66
69
  s.add_dependency(%q<thor>, ["~> 0.14.0"])
67
70
  s.add_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
68
71
  s.add_dependency(%q<grit>, ["~> 2.4.0"])
72
+ s.add_dependency(%q<hirb>, [">= 0"])
73
+ s.add_dependency(%q<github_api>, [">= 0"])
69
74
  s.add_dependency(%q<rspec>, ["~> 2.0"])
70
75
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
76
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
@@ -76,6 +81,8 @@ Gem::Specification.new do |s|
76
81
  s.add_dependency(%q<thor>, ["~> 0.14.0"])
77
82
  s.add_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
78
83
  s.add_dependency(%q<grit>, ["~> 2.4.0"])
84
+ s.add_dependency(%q<hirb>, [">= 0"])
85
+ s.add_dependency(%q<github_api>, [">= 0"])
79
86
  s.add_dependency(%q<rspec>, ["~> 2.0"])
80
87
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
81
88
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
@@ -6,21 +6,34 @@ require 'yaml'
6
6
  require 'flux/util'
7
7
 
8
8
  module Flux
9
- RC = '.flux'
10
- RC_LOCAL = RC + '.local'
9
+ NAME = '.flux'
10
+ RC = "#{NAME}.rb"
11
+ RC_LOCAL = "#{NAME}.local.rb"
11
12
 
12
13
  class FluxError < StandardError; end
13
14
  class TrackerError < FluxError; end
14
15
 
15
16
  class << self
16
- attr_accessor :environment
17
+ def environment
18
+ @project ||= {}
19
+ end
17
20
 
18
21
  def setup
19
- self.environment = load_environment
22
+ load rc
23
+ load rc_local if File.exist?(rc_local)
24
+
25
+ require 'flux/rcs'
26
+ require 'flux/cli'
27
+ end
28
+
29
+ def rc
30
+ find_upwards(RC, Dir.pwd) or
31
+ raise FluxError, "Could not find a '#{RC}' " <<
32
+ "file in the current filesystem hierarchy."
33
+ end
20
34
 
21
- environment.each { |k, v|
22
- load_adapter k, v['adapter'] if v.is_a?(Hash) && v['adapter']
23
- }
35
+ def rc_local
36
+ find_upwards(RC_LOCAL, Dir.pwd)
24
37
  end
25
38
 
26
39
  def find_upwards(object, start_dir)
@@ -32,34 +45,17 @@ module Flux
32
45
  elsif p == p.parent
33
46
  nil
34
47
  else
35
- find_upwards(p.parent)
36
- end
37
- end
38
-
39
- private
40
-
41
- def load_adapter(kind, adapter)
42
- adapter_path = "flux/#{kind}/#{adapter}"
43
-
44
- begin
45
- require adapter_path
46
- rescue LoadError
47
- raise FluxError, "Could not load `#{adapter_path}'."
48
+ find_upwards(object, p.parent)
48
49
  end
49
50
  end
51
+ end
52
+ end
50
53
 
51
- def load_environment
52
- rc = find_upwards(RC, Dir.pwd) or
53
- raise FluxError, "Could not find a '#{RC}' " <<
54
- "file in the current filesystem hierarchy."
55
- rc_l = File.join(File.dirname(rc), RC_LOCAL)
56
-
57
- env = YAML.load_file(rc)
58
- env_l = File.exist?(rc_l) ? YAML.load_file(rc_l) : {}
54
+ module Kernel
55
+ def use(mod, options = {})
56
+ (Flux.environment[mod.to_s] ||= {}).
57
+ merge! Hash[*options.map { |k, v| [k.to_s, v] }.flatten]
59
58
 
60
- env_l.each { |k, v| env[k].merge!(v) if env[k].respond_to?(:merge) }
61
59
 
62
- env
63
- end
64
60
  end
65
61
  end
@@ -0,0 +1,3 @@
1
+ require 'flux/cli/feature'
2
+ require 'flux/cli/pivotal_tracker'
3
+ require 'flux/cli/review'
@@ -0,0 +1,39 @@
1
+ require 'flux/git'
2
+
3
+ module Flux
4
+ module CLI
5
+ class Feature < Thor
6
+ namespace :feature
7
+
8
+ desc "start STORY_ID BRANCH_ID", "start working on a story"
9
+ method_option :estimate, :type => :numeric
10
+ method_option :parent_branch, :type => :string,
11
+ :default => 'master',
12
+ :aliases => '-b'
13
+ def start(story_id, branch_id)
14
+ invoke 'pt:grab', [story_id], :estimate => options[:estimate]
15
+ invoke 'pt:start', [story_id]
16
+
17
+ create_branch branch_id, story_id, options
18
+ end
19
+
20
+ private
21
+
22
+ def create_branch(branch_id, story_id, options = {})
23
+ up = Git::Branch.remote('origin', branch_id)
24
+ parent = Git::Branch.local(options[:parent_branch])
25
+ branch = Git::Branch.local(branch_id).
26
+ create(parent).
27
+ publish(up).
28
+ track(up).
29
+ checkout
30
+
31
+ link_branch_to_story branch_id, story_id
32
+ end
33
+
34
+ def link_branch_to_story(branch_id, story_id)
35
+ Git::Branch.local(branch_id).config('story_id', story_id)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,72 @@
1
+ require 'pivotal-tracker'
2
+ require 'hirb'
3
+ require 'flux/pivotal_tracker'
4
+ require 'flux/ext/pivotal-tracker'
5
+ require 'forwardable'
6
+
7
+ module Flux
8
+ module CLI
9
+ class PT < Thor
10
+ extend Forwardable
11
+ include Flux::Util
12
+
13
+ namespace :pt
14
+
15
+ default_task :list
16
+
17
+ class << self
18
+ def config
19
+ Flux.environment['pivotal_tracker']
20
+ end
21
+
22
+ def login
23
+ ::PivotalTracker::Client.token = config['token']
24
+ end
25
+
26
+ def story_update(name, attrs = nil, &get_attrs)
27
+ desc "#{name} STORY_ID", "#{name} a story"
28
+ method_option :estimate, :type => :numeric, :aliases => '-e'
29
+ define_method name do |story_id|
30
+ a = attrs || instance_eval(&get_attrs)
31
+
32
+ a[:estimate] = options[:estimate] if options[:estimate]
33
+
34
+ story(story_id).update a
35
+ end
36
+ end
37
+ end
38
+
39
+ no_tasks {
40
+ def_delegator self, :config
41
+ }
42
+
43
+ login if config
44
+
45
+ desc "list", "list stories, excluding icebox by default"
46
+ def list
47
+ puts Hirb::Helpers::ObjectTable.render(
48
+ current_project.stories.scheduled,
49
+ :fields => [:id, :state, :owned_by, :est, :name]
50
+ )
51
+ end
52
+
53
+ story_update :finish, :current_state => 'finished'
54
+ story_update(:grab) {{:owned_by => me.name}}
55
+ story_update :start, :current_state => 'started'
56
+
57
+ private
58
+
59
+ def current_project
60
+ Flux::PT::Project.new(config['project_id'])
61
+ end
62
+
63
+ def me
64
+ current_project.members.find { |m| m.email == config['email'] }
65
+ end
66
+
67
+ def story(id)
68
+ current_project.stories.find(id)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,190 @@
1
+ require 'open-uri'
2
+ require 'flux/git'
3
+ require 'github_api'
4
+
5
+ module Flux
6
+ module CLI
7
+ class Review < Thor
8
+ namespace 'review'
9
+
10
+ desc "diff", "show interdiff between last 2 reviews"
11
+ method_option :from, :type => :numeric, :aliases => '-f'
12
+ method_option :to, :type => :numeric, :aliases => '-t'
13
+ method_option :color, :type => :boolean, :aliases => '-c'
14
+ def diff(branch_name = Git::Branch.current.name)
15
+ reqs = pull_requests(:state => 'all', :branch_name => branch_name)
16
+
17
+ if options[:from] && options[:to]
18
+ to = reqs.find { |r| r[:number].to_i == options[:to].to_i }
19
+ from = reqs.find { |r| r[:number].to_i == options[:from].to_i }
20
+ else
21
+ to, from = reqs.first(2)
22
+ end
23
+
24
+ if from
25
+ auth = [gh_config['username'], gh_config['password']]
26
+
27
+ to_diff = Tempfile.new("to_diff")
28
+ to_diff.write(open(to[:diff_url],
29
+ :http_basic_authentication => auth).read)
30
+
31
+ from_diff = Tempfile.new("from_diff")
32
+ from_diff.write(open(from[:diff_url],
33
+ :http_basic_authentication => auth).read)
34
+
35
+ cmd = "interdiff #{from_diff.path} #{to_diff.path}"
36
+ cmd << "| colordiff"
37
+
38
+ system cmd
39
+ end
40
+ end
41
+
42
+ desc "request", "request a review for the current feature"
43
+ def request
44
+ branch = Git::Branch.current
45
+ story = current_project.stories.find(branch.config('story_id'))
46
+ body = {:branch_name => branch.name,
47
+ :story_url => story.url,
48
+ :iterations => iterations(branch.name)}
49
+ data = {:base => 'master',
50
+ :head => branch.commit.sha,
51
+ :title => story.name,
52
+ :body => PullRequestBody.to_markdown(body)}
53
+
54
+ gh.pull_requests.create_request gh_config['repo_user'],
55
+ gh_config['repo_name'],
56
+ data
57
+ end
58
+
59
+ desc "list", "list pull requests for the current project"
60
+ def list
61
+ list_pull_requests pull_requests
62
+ end
63
+
64
+ desc "history BRANCH_ID", "show review history for branch"
65
+ def history(branch_id = Git::Branch.current.name)
66
+ reqs = pull_requests(:branch_name => branch_id, :state => 'all')
67
+
68
+ list_pull_requests reqs
69
+ end
70
+
71
+ private
72
+
73
+ def config
74
+ Flux.environment
75
+ end
76
+
77
+ def current_project
78
+ Flux::PT::Project.new(pt_config['project_id'])
79
+ end
80
+
81
+ def gh
82
+ Github.new :login => gh_config['username'],
83
+ :password => gh_config['password']
84
+ end
85
+
86
+ def gh_config
87
+ config['github']
88
+ end
89
+
90
+ def pt_config
91
+ config['pivotal_tracker']
92
+ end
93
+
94
+ def iterations(branch_name)
95
+ last = pull_requests(:branch_name => branch_name).first
96
+
97
+ if last
98
+ [last[:url]].concat(last[:body][:iterations])
99
+ else
100
+ []
101
+ end
102
+ end
103
+
104
+ def pull_requests(opts = {})
105
+ if opts[:state] == 'all'
106
+ return pull_requests(opts.merge(:state => 'open')) +
107
+ pull_requests(opts.merge(:state => 'closed'))
108
+ end
109
+
110
+ branch_name = opts.delete(:branch_name)
111
+
112
+ reqs = gh.pull_requests.pull_requests(gh_config['repo_user'],
113
+ gh_config['repo_name'],
114
+ opts).each { |r|
115
+ r[:author] = r[:user][:login]
116
+ r[:body] = PullRequestBody.from_markdown(r[:body])
117
+ r[:story_id] = Flux::PT::Story.id_from_url(r[:body][:story_url])
118
+ }
119
+
120
+ if branch_name
121
+ reqs.select { |r| r[:body][:branch_name] == branch_name }
122
+ else
123
+ reqs
124
+ end
125
+ end
126
+
127
+ def list_pull_requests(pull_requests)
128
+ puts Hirb::Helpers::Table.render(
129
+ pull_requests,
130
+ :fields => [:number, :title, :story_id, :author, :created_at]
131
+ )
132
+ end
133
+
134
+ module PullRequestBody
135
+ def self.from_markdown(md)
136
+ if (md || '').include?(signoff)
137
+ _, branch_name, story_url, iter = md.
138
+ gsub(signoff, '').
139
+ split(/### [^\n]+/).map(&:strip)
140
+
141
+ if iter
142
+ _, iterations = iter.split('*').map(&:strip)
143
+
144
+ iterations = Array(iterations)
145
+ else
146
+ iterations = []
147
+ end
148
+ else
149
+ branch_name = nil
150
+ story_url = "http://mojotech.com"
151
+ iterations = []
152
+ end
153
+
154
+ {:branch_name => branch_name,
155
+ :story_url => story_url,
156
+ :iterations => iterations}
157
+ end
158
+
159
+ def self.to_markdown(data)
160
+ tmpl = <<MARKDOWN
161
+ ### Branch
162
+
163
+ #{data[:branch_name]}
164
+
165
+ ### Story
166
+
167
+ #{data[:story_url]}
168
+
169
+ MARKDOWN
170
+
171
+ if data[:iterations].empty?
172
+ tmpl << signoff << "\n"
173
+ else
174
+ tmpl << <<ITERATIONS
175
+ ### Prior Versions
176
+
177
+ #{"* " + data[:iterations].join("\n* ") + "\n"}
178
+
179
+ #{signoff}
180
+ ITERATIONS
181
+ end
182
+ end
183
+
184
+ def self.signoff
185
+ "*Your friendly neighborhood Flux*"
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end