flux 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +56 -0
- data/VERSION +1 -1
- data/flux.gemspec +87 -0
- data/lib/flux/rcs/git.rb +25 -6
- data/lib/flux/trackers/pivotal_tracker.rb +49 -32
- data/lib/flux/util.rb +1 -0
- data/lib/flux/util/output.rb +25 -0
- data/lib/flux/workflows/mojotech.rb +12 -9
- data/spec/flux/rcs/git_spec.rb +47 -0
- data/spec/flux/trackers/pivotal_tracker_spec.rb +9 -0
- metadata +8 -3
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Flux
|
2
|
+
====
|
3
|
+
|
4
|
+
Flux is a command-line tool that tries to eliminate the drudgery from your
|
5
|
+
workflow.
|
6
|
+
|
7
|
+
It provides hooks for interacting with the following:
|
8
|
+
|
9
|
+
* Tracker (currently only Pivotal Tracker is supported)
|
10
|
+
* RCS (currently, git only)
|
11
|
+
* Anything else you can think of, as long as someone implements it.
|
12
|
+
|
13
|
+
Some examples
|
14
|
+
-------------
|
15
|
+
|
16
|
+
Let's list all stories that have been scheduled (i.e., moved out of the icebox
|
17
|
+
and into some iteration):
|
18
|
+
|
19
|
+
$ flux stories
|
20
|
+
ID STATE ASSIGNEE STORY
|
21
|
+
12345678 unstarted A developer can grab a story
|
22
|
+
12345679 finished David Leal A developer can link a branch to a story
|
23
|
+
12345680 finished David Leal A developer can mark a story as started
|
24
|
+
...
|
25
|
+
|
26
|
+
[TODO Complete workflow. For now try `flux` to see a list of implemented tasks.]
|
27
|
+
|
28
|
+
Configuration
|
29
|
+
-------------
|
30
|
+
|
31
|
+
A project is configured by placing a file named `.flux` in the project root.
|
32
|
+
Each top level key represents a functional domain.
|
33
|
+
|
34
|
+
This project's `.flux` file is as follows:
|
35
|
+
|
36
|
+
trackers:
|
37
|
+
adapter: pivotal_tracker
|
38
|
+
project_id: 128808
|
39
|
+
rcs:
|
40
|
+
adapter: git
|
41
|
+
workflows:
|
42
|
+
adapter: mojotech
|
43
|
+
|
44
|
+
### Local configuration
|
45
|
+
|
46
|
+
The `.flux` file should be used for project-wide configuration and kept under
|
47
|
+
version control. Obviously you aren't going to store your account credentials
|
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`.
|
51
|
+
|
52
|
+
Here's an example of a `.flux.local` file.
|
53
|
+
|
54
|
+
trackers:
|
55
|
+
token: abcdef1234567890
|
56
|
+
email: david@mojotech.com
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/flux.gemspec
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{flux}
|
8
|
+
s.version = "0.0.2"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["David Leal"]
|
12
|
+
s.date = %q{2011-09-07}
|
13
|
+
s.default_executable = %q{flux}
|
14
|
+
s.email = %q{david@mojotech.com}
|
15
|
+
s.executables = ["flux"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE.txt",
|
18
|
+
"README.md"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
".flux",
|
23
|
+
".flux.local.sample",
|
24
|
+
".rspec",
|
25
|
+
"Gemfile",
|
26
|
+
"LICENSE.txt",
|
27
|
+
"README.md",
|
28
|
+
"Rakefile",
|
29
|
+
"VERSION",
|
30
|
+
"bin/flux",
|
31
|
+
"flux.gemspec",
|
32
|
+
"lib/flux.rb",
|
33
|
+
"lib/flux/ext/pivotal-tracker.rb",
|
34
|
+
"lib/flux/rcs/git.rb",
|
35
|
+
"lib/flux/trackers/pivotal_tracker.rb",
|
36
|
+
"lib/flux/util.rb",
|
37
|
+
"lib/flux/util/output.rb",
|
38
|
+
"lib/flux/util/table.rb",
|
39
|
+
"lib/flux/workflows/mojotech.rb",
|
40
|
+
"spec/flux/rcs/git_spec.rb",
|
41
|
+
"spec/flux/trackers/pivotal_tracker_spec.rb",
|
42
|
+
"spec/flux/util/table_spec.rb",
|
43
|
+
"spec/flux_spec.rb",
|
44
|
+
"spec/spec_helper.rb",
|
45
|
+
"spec/support/matchers/print_table.rb",
|
46
|
+
"spec/support/rr.rb"
|
47
|
+
]
|
48
|
+
s.homepage = %q{http://github.com/mojotech/flux}
|
49
|
+
s.licenses = ["MIT"]
|
50
|
+
s.require_paths = ["lib"]
|
51
|
+
s.rubygems_version = %q{1.6.2}
|
52
|
+
s.summary = %q{Command line workflow manager.}
|
53
|
+
|
54
|
+
if s.respond_to? :specification_version then
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_runtime_dependency(%q<thor>, ["~> 0.14.0"])
|
59
|
+
s.add_runtime_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
|
60
|
+
s.add_development_dependency(%q<grit>, ["~> 2.4.0"])
|
61
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.0"])
|
62
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
63
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
|
64
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
65
|
+
s.add_development_dependency(%q<rr>, ["~> 1.0.0"])
|
66
|
+
else
|
67
|
+
s.add_dependency(%q<thor>, ["~> 0.14.0"])
|
68
|
+
s.add_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
|
69
|
+
s.add_dependency(%q<grit>, ["~> 2.4.0"])
|
70
|
+
s.add_dependency(%q<rspec>, ["~> 2.0"])
|
71
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
72
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
73
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
74
|
+
s.add_dependency(%q<rr>, ["~> 1.0.0"])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
s.add_dependency(%q<thor>, ["~> 0.14.0"])
|
78
|
+
s.add_dependency(%q<pivotal-tracker>, ["~> 0.4.0"])
|
79
|
+
s.add_dependency(%q<grit>, ["~> 2.4.0"])
|
80
|
+
s.add_dependency(%q<rspec>, ["~> 2.0"])
|
81
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
82
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
83
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
84
|
+
s.add_dependency(%q<rr>, ["~> 1.0.0"])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
data/lib/flux/rcs/git.rb
CHANGED
@@ -10,16 +10,35 @@ module Flux
|
|
10
10
|
repo.head.name.tap { |h| $stdout.puts h }
|
11
11
|
end
|
12
12
|
|
13
|
+
desc "checkout BRANCH_ID", "checkout a branch"
|
14
|
+
def checkout(branch_id)
|
15
|
+
git.checkout({}, branch_id)
|
16
|
+
end
|
17
|
+
|
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)
|
22
|
+
|
23
|
+
if options[:public]
|
24
|
+
git.push({}, 'origin', "+#{branch_id}:#{branch_id}")
|
25
|
+
git.branch({:set_upstream => true}, branch_id, "origin/#{branch_id}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
13
29
|
private
|
14
30
|
|
31
|
+
def git
|
32
|
+
@git ||= Grit::Git.new(repo_path)
|
33
|
+
end
|
34
|
+
|
15
35
|
def repo
|
16
|
-
@repo ||=
|
17
|
-
|
18
|
-
repo = Flux.find_upwards('.git', Dir.pwd) or
|
19
|
-
raise RCSError, "Couldn't find git repo starting at #{Dir.pwd}."
|
36
|
+
@repo ||= Grit::Repo.new(repo_path)
|
37
|
+
end
|
20
38
|
|
21
|
-
|
22
|
-
|
39
|
+
def repo_path
|
40
|
+
Flux.find_upwards('.git', Dir.pwd) or
|
41
|
+
raise RCSError, "Couldn't find git repo starting at #{Dir.pwd}."
|
23
42
|
end
|
24
43
|
end
|
25
44
|
end
|
@@ -23,8 +23,12 @@ module Flux
|
|
23
23
|
end
|
24
24
|
|
25
25
|
desc "grab STORY_ID", "assign yourself the given story"
|
26
|
+
method_option :estimate, :type => :numeric, :aliases => '-e'
|
26
27
|
def grab(story_id)
|
27
|
-
|
28
|
+
attrs = {:owner => me.name}
|
29
|
+
attrs[:estimate] = options[:estimate] if options[:estimate]
|
30
|
+
|
31
|
+
update_attributes story_id, attrs
|
28
32
|
end
|
29
33
|
|
30
34
|
desc "start STORY_ID", "start the given story"
|
@@ -43,34 +47,7 @@ module Flux
|
|
43
47
|
desc "update STORY_ID", "update a story's attributes"
|
44
48
|
method_option :attributes, :type => :hash, :required => true
|
45
49
|
def update(story_id)
|
46
|
-
|
47
|
-
native, custom =
|
48
|
-
options[:attributes].
|
49
|
-
map { |k, v| [MAPPINGS[k.to_s], v] }.
|
50
|
-
partition { |(k, v)| story.respond_to?("#{k}=") }
|
51
|
-
|
52
|
-
native_h = Hash[*native.flatten]
|
53
|
-
|
54
|
-
if native_h['current_state']
|
55
|
-
unless STATES.include?(native_h['current_state'])
|
56
|
-
raise TrackerError, "Invalid state: #{native_h['current_state']}"
|
57
|
-
end
|
58
|
-
|
59
|
-
if STATES_W_ESTIMATE.include?(native_h['current_state']) &&
|
60
|
-
! story.estimate &&
|
61
|
-
! native_h['estimate']
|
62
|
-
raise TrackerError,
|
63
|
-
"Need an estimate for state `#{native_h['current_state']}'."
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
story.update(native_h) unless native.empty?
|
68
|
-
|
69
|
-
unless custom.empty?
|
70
|
-
str = YAML.dump(Hash[*custom.flatten])
|
71
|
-
|
72
|
-
pt::Note.new(:owner => story, :text => str).create
|
73
|
-
end
|
50
|
+
update_attributes story_id, options[:attributes]
|
74
51
|
end
|
75
52
|
|
76
53
|
private
|
@@ -88,11 +65,15 @@ module Flux
|
|
88
65
|
:project_id => config['project_id'])
|
89
66
|
end
|
90
67
|
|
91
|
-
STORY_LIST_HEADERS = %w(ID STATE ASSIGNEE STORY)
|
68
|
+
STORY_LIST_HEADERS = %w(ID STATE ASSIGNEE >EST. STORY)
|
92
69
|
|
93
70
|
def list_stories(stories)
|
94
71
|
puts_table [STORY_LIST_HEADERS] + stories.map { |s|
|
95
|
-
[s.id,
|
72
|
+
[s.id,
|
73
|
+
s.current_state,
|
74
|
+
s.owned_by,
|
75
|
+
normalize_estimate(s.estimate).to_s,
|
76
|
+
s.name.strip]
|
96
77
|
}
|
97
78
|
end
|
98
79
|
|
@@ -106,6 +87,10 @@ module Flux
|
|
106
87
|
ms.find { |m| m.email == config['email'] }
|
107
88
|
end
|
108
89
|
|
90
|
+
def normalize_estimate(estimate)
|
91
|
+
(estimate && estimate < 0) ? nil : estimate
|
92
|
+
end
|
93
|
+
|
109
94
|
def pt
|
110
95
|
login and return ::PivotalTracker
|
111
96
|
end
|
@@ -118,11 +103,43 @@ module Flux
|
|
118
103
|
story(id) or raise TrackerError, "Couldn't find story #{id}."
|
119
104
|
end
|
120
105
|
|
106
|
+
def update_attributes(story_id, attrs)
|
107
|
+
story = story!(story_id)
|
108
|
+
native, custom =
|
109
|
+
attrs.
|
110
|
+
map { |k, v| [MAPPINGS[k.to_s], v] }.
|
111
|
+
partition { |(k, v)| story.respond_to?("#{k}=") }
|
112
|
+
|
113
|
+
native_h = Hash[*native.flatten]
|
114
|
+
|
115
|
+
if native_h['current_state']
|
116
|
+
unless STATES.include?(native_h['current_state'])
|
117
|
+
raise TrackerError, "Invalid state: #{native_h['current_state']}"
|
118
|
+
end
|
119
|
+
|
120
|
+
if STATES_W_ESTIMATE.include?(native_h['current_state']) &&
|
121
|
+
! normalize_estimate(story.estimate) &&
|
122
|
+
! native_h['estimate']
|
123
|
+
raise TrackerError,
|
124
|
+
"Need an estimate for state `#{native_h['current_state']}'."
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
story.update(native_h) unless native.empty?
|
129
|
+
|
130
|
+
unless custom.empty?
|
131
|
+
str = YAML.dump(Hash[*custom.flatten])
|
132
|
+
|
133
|
+
pt::Note.new(:owner => story, :text => str).create
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
121
138
|
def update_state(story_id, state)
|
122
139
|
attrs = {:state => state}
|
123
140
|
attrs[:estimate] = options[:estimate] if options[:estimate]
|
124
141
|
|
125
|
-
|
142
|
+
update_attributes story_id, attrs
|
126
143
|
end
|
127
144
|
end
|
128
145
|
end
|
data/lib/flux/util.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flux
|
2
|
+
module Util
|
3
|
+
module Output
|
4
|
+
# Capture standard IO channel.
|
5
|
+
#
|
6
|
+
# @yield the block that encloses the channel capture
|
7
|
+
#
|
8
|
+
# @return everything that was written to the given channel
|
9
|
+
def capture(channel = :stdout)
|
10
|
+
io = StringIO.new
|
11
|
+
old_io = eval("$#{channel}")
|
12
|
+
|
13
|
+
eval("$#{channel} = io")
|
14
|
+
|
15
|
+
yield
|
16
|
+
|
17
|
+
io.string
|
18
|
+
ensure
|
19
|
+
eval("$#{channel} = old_io")
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :silence, :capture
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -2,6 +2,8 @@ module Flux
|
|
2
2
|
module Workflows
|
3
3
|
module MojoTech
|
4
4
|
class Developer < Thor
|
5
|
+
include Util::Output
|
6
|
+
|
5
7
|
namespace :dev
|
6
8
|
|
7
9
|
desc "link STORY_ID", "link a story to a branch"
|
@@ -11,20 +13,21 @@ module Flux
|
|
11
13
|
:attributes => {'branch' => current_branch_id}
|
12
14
|
end
|
13
15
|
|
16
|
+
desc "start STORY_ID BRANCH_ID", "start working on a story"
|
17
|
+
method_option :estimate, :type => :numeric
|
18
|
+
def start(story_id, branch_id)
|
19
|
+
invoke 'stories:grab', [story_id], :estimate => options[:estimate]
|
20
|
+
invoke 'stories:start', [story_id]
|
21
|
+
invoke 'branches:create', [branch_id]
|
22
|
+
invoke 'branches:checkout', [branch_id]
|
23
|
+
invoke :link, [story_id]
|
24
|
+
end
|
25
|
+
|
14
26
|
private
|
15
27
|
|
16
28
|
def current_branch_id
|
17
29
|
silence { @current_branch_id ||= invoke('branches:current', []) }
|
18
30
|
end
|
19
|
-
|
20
|
-
def silence
|
21
|
-
old_stdout = $stdout
|
22
|
-
$stdout = StringIO.new
|
23
|
-
|
24
|
-
yield
|
25
|
-
ensure
|
26
|
-
$stdout = old_stdout
|
27
|
-
end
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'flux/rcs/git'
|
4
|
+
|
5
|
+
describe Flux::RCS::Branches do
|
6
|
+
include Flux::Util::Output
|
7
|
+
|
8
|
+
it "displays the current branch" do
|
9
|
+
mock(mock_root(Grit::Repo)).head.mock!.name { 'zee_current_branch' }
|
10
|
+
|
11
|
+
retval = nil
|
12
|
+
capture { retval = subject.invoke('branches:current', []) }.
|
13
|
+
should == "zee_current_branch\n"
|
14
|
+
retval.should == 'zee_current_branch'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "creates a branch" do
|
18
|
+
mock(mock_root(Grit::Git)).branch({}, 'new_branch')
|
19
|
+
|
20
|
+
subject.invoke 'branches:create', %w(new_branch)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates a public branch" do
|
24
|
+
git = mock_root(Grit::Git)
|
25
|
+
|
26
|
+
mock(git).branch({}, 'new_branch')
|
27
|
+
mock(git).push({}, 'origin', '+new_branch:new_branch')
|
28
|
+
mock(git).branch({:set_upstream => true}, 'new_branch', 'origin/new_branch')
|
29
|
+
|
30
|
+
subject.invoke 'branches:create', %w(new_branch), :public => true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "checks out a branch" do
|
34
|
+
mock(mock_root(Grit::Git)).checkout({}, 'new_branch')
|
35
|
+
|
36
|
+
subject.invoke 'branches:checkout', %w(new_branch)
|
37
|
+
end
|
38
|
+
|
39
|
+
def mock_root(klass)
|
40
|
+
repo_path = File.expand_path(File.dirname(__FILE__) + '/../../../.git')
|
41
|
+
root = Object.new
|
42
|
+
|
43
|
+
mock(klass).new(repo_path) { root }
|
44
|
+
|
45
|
+
root
|
46
|
+
end
|
47
|
+
end
|
@@ -40,6 +40,15 @@ describe Flux::Trackers::PivotalTracker do
|
|
40
40
|
subject.invoke :grab, [123]
|
41
41
|
end
|
42
42
|
|
43
|
+
it "grabs a story and estimates it" do
|
44
|
+
prepare_myself
|
45
|
+
|
46
|
+
mock_update(123, 'owned_by' => 'David Leal',
|
47
|
+
'estimate' => 2)
|
48
|
+
|
49
|
+
subject.invoke :grab, [123], :estimate => 2
|
50
|
+
end
|
51
|
+
|
43
52
|
[%w(starts started start),
|
44
53
|
%w(finishes finished finish)].each do |(action, state, task)|
|
45
54
|
it "#{action} a story that was already estimated" do
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: flux
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- David Leal
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-09-
|
13
|
+
date: 2011-09-07 00:00:00 +01:00
|
14
14
|
default_executable: flux
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -109,6 +109,7 @@ extensions: []
|
|
109
109
|
|
110
110
|
extra_rdoc_files:
|
111
111
|
- LICENSE.txt
|
112
|
+
- README.md
|
112
113
|
files:
|
113
114
|
- .document
|
114
115
|
- .flux
|
@@ -116,16 +117,20 @@ files:
|
|
116
117
|
- .rspec
|
117
118
|
- Gemfile
|
118
119
|
- LICENSE.txt
|
120
|
+
- README.md
|
119
121
|
- Rakefile
|
120
122
|
- VERSION
|
121
123
|
- bin/flux
|
124
|
+
- flux.gemspec
|
122
125
|
- lib/flux.rb
|
123
126
|
- lib/flux/ext/pivotal-tracker.rb
|
124
127
|
- lib/flux/rcs/git.rb
|
125
128
|
- lib/flux/trackers/pivotal_tracker.rb
|
126
129
|
- lib/flux/util.rb
|
130
|
+
- lib/flux/util/output.rb
|
127
131
|
- lib/flux/util/table.rb
|
128
132
|
- lib/flux/workflows/mojotech.rb
|
133
|
+
- spec/flux/rcs/git_spec.rb
|
129
134
|
- spec/flux/trackers/pivotal_tracker_spec.rb
|
130
135
|
- spec/flux/util/table_spec.rb
|
131
136
|
- spec/flux_spec.rb
|
@@ -146,7 +151,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
151
|
requirements:
|
147
152
|
- - ">="
|
148
153
|
- !ruby/object:Gem::Version
|
149
|
-
hash:
|
154
|
+
hash: 1054297281
|
150
155
|
segments:
|
151
156
|
- 0
|
152
157
|
version: "0"
|