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,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