flux 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.flux +9 -0
- data/.flux.local.sample +3 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +20 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/bin/flux +23 -0
- data/lib/flux.rb +65 -0
- data/lib/flux/ext/pivotal-tracker.rb +13 -0
- data/lib/flux/rcs/git.rb +26 -0
- data/lib/flux/trackers/pivotal_tracker.rb +129 -0
- data/lib/flux/util.rb +2 -0
- data/lib/flux/util/table.rb +99 -0
- data/lib/flux/workflows/mojotech.rb +31 -0
- data/spec/flux/trackers/pivotal_tracker_spec.rb +155 -0
- data/spec/flux/util/table_spec.rb +47 -0
- data/spec/flux_spec.rb +43 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/matchers/print_table.rb +46 -0
- data/spec/support/rr.rb +8 -0
- metadata +167 -0
data/.document
ADDED
data/.flux
ADDED
data/.flux.local.sample
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem "thor", "~> 0.14.0"
|
6
|
+
gem "pivotal-tracker", "~> 0.4.0"
|
7
|
+
|
8
|
+
# Add dependencies to develop your gem here.
|
9
|
+
# Include everything needed to run rake, tests, features, etc.
|
10
|
+
group :development do
|
11
|
+
gem "grit", "~> 2.4.0"
|
12
|
+
gem "rspec", "~> 2.0"
|
13
|
+
gem "bundler", "~> 1.0.0"
|
14
|
+
gem "jeweler", "~> 1.6.2"
|
15
|
+
gem "rcov", ">= 0"
|
16
|
+
gem "rr", "~> 1.0.0"
|
17
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 MojoTech
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "flux"
|
18
|
+
gem.homepage = "http://github.com/mojotech/flux"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Command line workflow manager.}
|
21
|
+
gem.email = "david@mojotech.com"
|
22
|
+
gem.authors = ["David Leal"]
|
23
|
+
# dependencies defined in Gemfile
|
24
|
+
end
|
25
|
+
Jeweler::RubygemsDotOrgTasks.new
|
26
|
+
|
27
|
+
require 'rspec/core'
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
30
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
43
|
+
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
45
|
+
rdoc.title = "flux #{version}"
|
46
|
+
rdoc.rdoc_files.include('README*')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/flux
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor/runner'
|
4
|
+
|
5
|
+
require 'flux'
|
6
|
+
|
7
|
+
class Flux::Runner < Thor::Runner
|
8
|
+
alias_method :help, :list
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def initialize_thorfiles(whocares = nil, nevermind = nil)
|
13
|
+
# birds chirping
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
$thor_runner = true
|
19
|
+
Flux::Runner.start(ARGV, {'environment' => Flux.setup})
|
20
|
+
rescue Flux::FluxError => e
|
21
|
+
$stderr.puts e.message
|
22
|
+
exit 1
|
23
|
+
end
|
data/lib/flux.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'pathname'
|
3
|
+
require 'thor'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'flux/util'
|
7
|
+
|
8
|
+
module Flux
|
9
|
+
RC = '.flux'
|
10
|
+
RC_LOCAL = RC + '.local'
|
11
|
+
|
12
|
+
class FluxError < StandardError; end
|
13
|
+
class TrackerError < FluxError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :environment
|
17
|
+
|
18
|
+
def setup
|
19
|
+
self.environment = load_environment
|
20
|
+
|
21
|
+
environment.each { |k, v|
|
22
|
+
load_adapter k, v['adapter'] if v.is_a?(Hash) && v['adapter']
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_upwards(object, start_dir)
|
27
|
+
p = Pathname(start_dir)
|
28
|
+
f = (p + object).expand_path.to_s
|
29
|
+
|
30
|
+
if File.exist?(f)
|
31
|
+
f
|
32
|
+
elsif p == p.parent
|
33
|
+
nil
|
34
|
+
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
|
+
end
|
49
|
+
end
|
50
|
+
|
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) : {}
|
59
|
+
|
60
|
+
env_l.each { |k, v| env[k].merge!(v) if env[k].respond_to?(:merge) }
|
61
|
+
|
62
|
+
env
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Flux
|
2
|
+
module PivotalTracker
|
3
|
+
module IterationExt
|
4
|
+
def current_backlog(project, options = {})
|
5
|
+
params = ::PivotalTracker.encode_options(options)
|
6
|
+
path = "/projects/#{project.id}/iterations/current_backlog#{params}"
|
7
|
+
parse(::PivotalTracker::Client.connection[path].get)
|
8
|
+
end
|
9
|
+
|
10
|
+
::PivotalTracker::Iteration.send(:extend, self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/flux/rcs/git.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'grit'
|
2
|
+
|
3
|
+
module Flux
|
4
|
+
module RCS
|
5
|
+
class Branches < Thor
|
6
|
+
namespace :branches
|
7
|
+
|
8
|
+
desc "current", "show the current branch"
|
9
|
+
def current
|
10
|
+
repo.head.name.tap { |h| $stdout.puts h }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def repo
|
16
|
+
@repo ||=
|
17
|
+
begin
|
18
|
+
repo = Flux.find_upwards('.git', Dir.pwd) or
|
19
|
+
raise RCSError, "Couldn't find git repo starting at #{Dir.pwd}."
|
20
|
+
|
21
|
+
Grit::Repo.new(repo)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'pivotal-tracker'
|
2
|
+
require 'flux/ext/pivotal-tracker'
|
3
|
+
|
4
|
+
module Flux
|
5
|
+
module Trackers
|
6
|
+
class PivotalTracker < Thor
|
7
|
+
include Flux::Util
|
8
|
+
|
9
|
+
namespace :stories
|
10
|
+
|
11
|
+
default_task :list
|
12
|
+
|
13
|
+
desc "list", "list stories, excluding icebox by default"
|
14
|
+
def list
|
15
|
+
list_stories pt::Iteration.
|
16
|
+
current_backlog(fake_project).map { |i| i.stories }.flatten
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "finish STORY_ID", "finish the given story"
|
20
|
+
method_option :estimate, :type => :numeric, :aliases => '-e'
|
21
|
+
def finish(story_id)
|
22
|
+
update_state story_id, 'finished'
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "grab STORY_ID", "assign yourself the given story"
|
26
|
+
def grab(story_id)
|
27
|
+
invoke :update, [story_id], :attributes => {:owner => me.name}
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "start STORY_ID", "start the given story"
|
31
|
+
method_option :estimate, :type => :numeric, :aliases => '-e'
|
32
|
+
def start(story_id)
|
33
|
+
update_state story_id, 'started'
|
34
|
+
end
|
35
|
+
|
36
|
+
STATES_W_ESTIMATE = %w(started finished delivered accepted rejected)
|
37
|
+
STATES = %w(unscheduled unstarted) + STATES_W_ESTIMATE
|
38
|
+
|
39
|
+
MAPPINGS = Hash.new { |h, k| h[k] = k }
|
40
|
+
MAPPINGS['owner'] = 'owned_by'
|
41
|
+
MAPPINGS['state'] = 'current_state'
|
42
|
+
|
43
|
+
desc "update STORY_ID", "update a story's attributes"
|
44
|
+
method_option :attributes, :type => :hash, :required => true
|
45
|
+
def update(story_id)
|
46
|
+
story = story!(story_id)
|
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
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def config
|
79
|
+
Flux.environment['trackers']
|
80
|
+
end
|
81
|
+
|
82
|
+
def fake_project
|
83
|
+
@fp ||= OpenStruct.new(:id => config['project_id'])
|
84
|
+
end
|
85
|
+
|
86
|
+
def fake_story(id)
|
87
|
+
pt::Story.new(:id => id,
|
88
|
+
:project_id => config['project_id'])
|
89
|
+
end
|
90
|
+
|
91
|
+
STORY_LIST_HEADERS = %w(ID STATE ASSIGNEE STORY)
|
92
|
+
|
93
|
+
def list_stories(stories)
|
94
|
+
puts_table [STORY_LIST_HEADERS] + stories.map { |s|
|
95
|
+
[s.id, s.current_state, s.owned_by, s.name]
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def login
|
100
|
+
::PivotalTracker::Client.token = config['token']
|
101
|
+
end
|
102
|
+
|
103
|
+
def me
|
104
|
+
ms = pt::Membership.all(fake_project)
|
105
|
+
|
106
|
+
ms.find { |m| m.email == config['email'] }
|
107
|
+
end
|
108
|
+
|
109
|
+
def pt
|
110
|
+
login and return ::PivotalTracker
|
111
|
+
end
|
112
|
+
|
113
|
+
def story(id)
|
114
|
+
pt::Story.find(id, config['project_id'])
|
115
|
+
end
|
116
|
+
|
117
|
+
def story!(id)
|
118
|
+
story(id) or raise TrackerError, "Couldn't find story #{id}."
|
119
|
+
end
|
120
|
+
|
121
|
+
def update_state(story_id, state)
|
122
|
+
attrs = {:state => state}
|
123
|
+
attrs[:estimate] = options[:estimate] if options[:estimate]
|
124
|
+
|
125
|
+
invoke :update, [story_id], :attributes => attrs
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/flux/util.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Flux
|
2
|
+
module Util
|
3
|
+
# Initialize a new Table.
|
4
|
+
#
|
5
|
+
# @param [Array<Array>] raw_data the table's annotated data
|
6
|
+
# @param field_sep the string used to separate columns
|
7
|
+
# @param [Integer] max_width the maximum width for this table.
|
8
|
+
# Lines exceeding this width will be truncated.
|
9
|
+
# @param shell the shell object used by the current task
|
10
|
+
def puts_table(raw_data, field_sep = ' ', max_width = nil)
|
11
|
+
$stdout.puts Table.new(shell, raw_data, field_sep, max_width)
|
12
|
+
end
|
13
|
+
|
14
|
+
# A table pretty printer that allows us to take a peek at the data after
|
15
|
+
# it's been stripped of formatting information. It exists to make testing
|
16
|
+
# easier.
|
17
|
+
#
|
18
|
+
# If any cell in the first row is prefixed with '>', then that column's
|
19
|
+
# contents will be right-aligned. This is useful when showing numbers.
|
20
|
+
class Table
|
21
|
+
ALIGN_RIGHT = '>'
|
22
|
+
|
23
|
+
attr_reader :field_sep, :raw_data, :shell, :max_width
|
24
|
+
|
25
|
+
# @see Flux::Util#puts_table
|
26
|
+
def initialize(shell, raw_data, field_sep, max_width)
|
27
|
+
@shell = shell
|
28
|
+
@raw_data = raw_data
|
29
|
+
@field_sep = field_sep
|
30
|
+
@max_width = max_width || terminal_width
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return whether a given column's contents will be aligned to the right
|
34
|
+
def align_right?(index)
|
35
|
+
meta[index].include?(ALIGN_RIGHT)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return all rows but the first
|
39
|
+
def body
|
40
|
+
@body ||= raw_data[1..-1]
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return the table data, stripped of format information
|
44
|
+
def data
|
45
|
+
@data ||= body.unshift(headers)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return the table's first row
|
49
|
+
def headers
|
50
|
+
@headers = raw_data.first.each_with_index.map { |e, i|
|
51
|
+
align_right?(i) ? e[1..-1] : e
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return the table's format information
|
56
|
+
def meta
|
57
|
+
@meta ||= @raw_data.first.inject([]) { |a, e|
|
58
|
+
a << (e =~ /^>/ ? ALIGN_RIGHT : '')
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
@s ||=
|
64
|
+
begin
|
65
|
+
widths = data.transpose.inject([]) { |a, col|
|
66
|
+
a << col.max { |a, b| a.to_s.size <=> b.to_s.size }.to_s.size
|
67
|
+
}
|
68
|
+
|
69
|
+
# we don't need the last column's width if it is to be left-aligned
|
70
|
+
widths[-1] = nil unless align_right?(-1)
|
71
|
+
|
72
|
+
format = meta.each_with_index.inject('') { |a, (e, i)|
|
73
|
+
w = widths[i] ? widths[i].to_s : ''
|
74
|
+
align = align_right?(i) ? '' : '-'
|
75
|
+
|
76
|
+
a << "#{field_sep}%" << align << w << 's'
|
77
|
+
}.strip
|
78
|
+
|
79
|
+
max_width
|
80
|
+
|
81
|
+
data.inject('') { |a, r|
|
82
|
+
l =
|
83
|
+
a << truncate(format % r, max_width) << "\n"
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def truncate(string, width)
|
91
|
+
shell.send(:truncate, string, width)
|
92
|
+
end
|
93
|
+
|
94
|
+
def terminal_width
|
95
|
+
shell.send(:terminal_width)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Flux
|
2
|
+
module Workflows
|
3
|
+
module MojoTech
|
4
|
+
class Developer < Thor
|
5
|
+
namespace :dev
|
6
|
+
|
7
|
+
desc "link STORY_ID", "link a story to a branch"
|
8
|
+
def link(story_id)
|
9
|
+
invoke 'stories:update',
|
10
|
+
[story_id],
|
11
|
+
:attributes => {'branch' => current_branch_id}
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def current_branch_id
|
17
|
+
silence { @current_branch_id ||= invoke('branches:current', []) }
|
18
|
+
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
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'flux/trackers/pivotal_tracker'
|
4
|
+
|
5
|
+
describe Flux::Trackers::PivotalTracker do
|
6
|
+
let(:environment) {
|
7
|
+
{'trackers' => {'project_id' => PROJECT.id,
|
8
|
+
'token' => PROJECT.token,
|
9
|
+
'email' => 'david@mojotech.com'}}
|
10
|
+
}
|
11
|
+
|
12
|
+
before { Flux.environment = environment }
|
13
|
+
|
14
|
+
it "lists stories by iteration" do
|
15
|
+
mock(PivotalTracker::Iteration).current_backlog(fake_project) {
|
16
|
+
[OpenStruct.new(:stories => [
|
17
|
+
OpenStruct.new(:id => 1,
|
18
|
+
:current_state => 'unstarted',
|
19
|
+
:owned_by => '',
|
20
|
+
:name => 'first story')]),
|
21
|
+
OpenStruct.new(:stories => [
|
22
|
+
OpenStruct.new(:id => 2,
|
23
|
+
:current_state => 'started',
|
24
|
+
:owned_by => 'David Leal',
|
25
|
+
:name => 'second story')])]
|
26
|
+
}
|
27
|
+
|
28
|
+
t = [['ID', 'STATE', 'ASSIGNEE', 'STORY'],
|
29
|
+
[1, 'unstarted', '', 'first story'],
|
30
|
+
[2, 'started', 'David Leal', 'second story']]
|
31
|
+
|
32
|
+
lambda { subject.invoke :list }.should print_table(t)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "grabs a story" do
|
36
|
+
prepare_myself
|
37
|
+
|
38
|
+
mock_update(123, 'owned_by' => 'David Leal')
|
39
|
+
|
40
|
+
subject.invoke :grab, [123]
|
41
|
+
end
|
42
|
+
|
43
|
+
[%w(starts started start),
|
44
|
+
%w(finishes finished finish)].each do |(action, state, task)|
|
45
|
+
it "#{action} a story that was already estimated" do
|
46
|
+
s = mock_update(123, 'current_state' => state)
|
47
|
+
s.estimate = 2
|
48
|
+
|
49
|
+
subject.invoke task, [123]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "#{action} a story that needs to be estimated" do
|
53
|
+
mock_update(123, 'current_state' => state, 'estimate' => 2)
|
54
|
+
|
55
|
+
subject.invoke task, [123], :estimate => 2
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "attributes" do
|
60
|
+
it "updates custom attributes" do
|
61
|
+
attrs = {'branch' => 'blah', 'other' => 'yay'}
|
62
|
+
|
63
|
+
mock_find(123)
|
64
|
+
mock(PivotalTracker::Note).new(observe { |h|
|
65
|
+
h[:owner].id == 123 &&
|
66
|
+
h[:text] == YAML.dump(attrs)
|
67
|
+
}).mock!.create
|
68
|
+
|
69
|
+
subject.invoke :update, [123], :attributes => attrs
|
70
|
+
end
|
71
|
+
|
72
|
+
it "updates native attributes" do
|
73
|
+
attrs = {'owner' => 'David Leal',
|
74
|
+
'state' => 'started',
|
75
|
+
'estimate' => 2,
|
76
|
+
'name' => 'My story'}
|
77
|
+
|
78
|
+
mock_update(123,
|
79
|
+
'owned_by' => 'David Leal',
|
80
|
+
'current_state' => 'started',
|
81
|
+
'estimate' => 2,
|
82
|
+
'name' => 'My story')
|
83
|
+
|
84
|
+
subject.invoke :update, [123], :attributes => attrs
|
85
|
+
end
|
86
|
+
|
87
|
+
it "updates valid states only" do
|
88
|
+
attrs = {'state' => 'sitting'}
|
89
|
+
|
90
|
+
s = mock_find(123)
|
91
|
+
|
92
|
+
lambda { subject.invoke :update, [123], :attributes => attrs }.
|
93
|
+
should raise_error(Flux::TrackerError, /Invalid state/)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "updates state when story has estimate" do
|
97
|
+
attrs = {'state' => 'started'}
|
98
|
+
|
99
|
+
s = mock_update(123, 'current_state' => 'started')
|
100
|
+
s.estimate = 2
|
101
|
+
|
102
|
+
subject.invoke :update, [123], :attributes => attrs
|
103
|
+
end
|
104
|
+
|
105
|
+
it "updates state when story estimate is given" do
|
106
|
+
attrs = {'state' => 'started', 'estimate' => 2}
|
107
|
+
|
108
|
+
s = mock_update(123, 'current_state' => 'started',
|
109
|
+
'estimate' => 2)
|
110
|
+
|
111
|
+
subject.invoke :update, [123], :attributes => attrs
|
112
|
+
end
|
113
|
+
|
114
|
+
Flux::Trackers::PivotalTracker::STATES_W_ESTIMATE.each do |state|
|
115
|
+
it "fails to update state `#{state}' if no estimate given" do
|
116
|
+
attrs = {'state' => state}
|
117
|
+
|
118
|
+
s = mock_find(123)
|
119
|
+
|
120
|
+
lambda { subject.invoke :update, [123], :attributes => attrs }.
|
121
|
+
should raise_error(Flux::TrackerError, /Need an estimate/)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def iteration(which, data)
|
127
|
+
mock(PivotalTracker::Iteration).__send__(which, fake_project) {
|
128
|
+
OpenStruct.new(:stories => data.map { |d| OpenStruct.new(d) })
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
def fake_project
|
133
|
+
OpenStruct.new(:id => PROJECT.id)
|
134
|
+
end
|
135
|
+
|
136
|
+
def fake_story(id)
|
137
|
+
PivotalTracker::Story.new(:id => id, :project_id => PROJECT.id)
|
138
|
+
end
|
139
|
+
|
140
|
+
def mock_find(story_id)
|
141
|
+
fake_story(story_id).tap { |s|
|
142
|
+
mock(PivotalTracker::Story).find(story_id, PROJECT.id) { s }
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def mock_update(story_id, attrs)
|
147
|
+
mock_find(story_id).tap { |s| mock(s).update(attrs) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def prepare_myself
|
151
|
+
mock(PivotalTracker::Membership).all(fake_project) {
|
152
|
+
[OpenStruct.new(:name => 'David Leal', :email => 'david@mojotech.com')]
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Flux::Util::Table do
|
4
|
+
let(:body) {
|
5
|
+
[[123, 'unstarted', 'David Leal', 'First story'],
|
6
|
+
[456, 'unscheduled', '', 'Second story']]
|
7
|
+
}
|
8
|
+
|
9
|
+
it "is converted to a string" do
|
10
|
+
t = Flux::Util::Table.new(Thor::Shell::Basic.new,
|
11
|
+
[%w(ID STATE ASSIGNEE STORY)] + body,
|
12
|
+
' ',
|
13
|
+
nil)
|
14
|
+
|
15
|
+
t.to_s.should == <<EOT
|
16
|
+
ID STATE ASSIGNEE STORY
|
17
|
+
123 unstarted David Leal First story
|
18
|
+
456 unscheduled Second story
|
19
|
+
EOT
|
20
|
+
end
|
21
|
+
|
22
|
+
it "aligns its columns to the right" do
|
23
|
+
t = Flux::Util::Table.new(Thor::Shell::Basic.new,
|
24
|
+
[%w(>ID >STATE >ASSIGNEE >STORY)] + body,
|
25
|
+
' ',
|
26
|
+
nil)
|
27
|
+
|
28
|
+
t.to_s.should == <<EOT
|
29
|
+
ID STATE ASSIGNEE STORY
|
30
|
+
123 unstarted David Leal First story
|
31
|
+
456 unscheduled Second story
|
32
|
+
EOT
|
33
|
+
end
|
34
|
+
|
35
|
+
it "truncates lines to a given width" do
|
36
|
+
t = Flux::Util::Table.new(Thor::Shell::Basic.new,
|
37
|
+
[%w(ID STATE ASSIGNEE STORY)] + body,
|
38
|
+
' ',
|
39
|
+
7)
|
40
|
+
|
41
|
+
t.to_s.should == <<EOT
|
42
|
+
ID ...
|
43
|
+
123 ...
|
44
|
+
456 ...
|
45
|
+
EOT
|
46
|
+
end
|
47
|
+
end
|
data/spec/flux_spec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'flux'
|
4
|
+
|
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) }
|
10
|
+
|
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'}}
|
26
|
+
}
|
27
|
+
|
28
|
+
Flux.setup.should ==
|
29
|
+
{'trackers' => {'project_id' => 123, 'token' => 'mytoken'},
|
30
|
+
'workflow' => 'mojotech'}
|
31
|
+
end
|
32
|
+
|
33
|
+
it "instantiates an adapter" do
|
34
|
+
mock(YAML).load_file(flux_env) {
|
35
|
+
{'trackers' => {'adapter' => 'pivotal_tracker'}}
|
36
|
+
}
|
37
|
+
|
38
|
+
Flux.setup
|
39
|
+
|
40
|
+
defined?(Flux::Trackers::PivotalTracker).should be_true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'flux'
|
5
|
+
|
6
|
+
PROJECT = OpenStruct.new(:id => 1, :token => 'tok')
|
7
|
+
|
8
|
+
# Requires supporting files with custom matchers and macros, etc,
|
9
|
+
# in ./support/ and its subdirectories.
|
10
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.mock_with :rr
|
14
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
RSpec::Matchers.define :print_table do |data|
|
2
|
+
class FakeStdout
|
3
|
+
attr_reader :table
|
4
|
+
|
5
|
+
def initialize(stdout)
|
6
|
+
@stdout = stdout
|
7
|
+
end
|
8
|
+
|
9
|
+
def puts(*args)
|
10
|
+
case args.first
|
11
|
+
when Flux::Util::Table
|
12
|
+
@table = args.first
|
13
|
+
else
|
14
|
+
@stdout.puts *args
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(data)
|
19
|
+
@stdout.write data
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
match do |block|
|
25
|
+
stdout = $stdout
|
26
|
+
$stdout = @stdout = FakeStdout.new(stdout)
|
27
|
+
|
28
|
+
begin
|
29
|
+
block.call
|
30
|
+
|
31
|
+
@stdout.table.should_not be_nil
|
32
|
+
@stdout.table.data.should == data
|
33
|
+
ensure
|
34
|
+
$stdout = stdout
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
failure_message_for_should do |block|
|
39
|
+
"Expected table:\n\n#{Flux::Util::Table.new(data, ' ', nil)}\n" <<
|
40
|
+
"but got:\n\n#{@stdout.table ? @stdout.table : 'nil'}"
|
41
|
+
end
|
42
|
+
|
43
|
+
failure_message_for_should_not do |block|
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
end
|
data/spec/support/rr.rb
ADDED
metadata
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flux
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David Leal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-09-05 00:00:00 +01:00
|
14
|
+
default_executable: flux
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: thor
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.14.0
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pivotal-tracker
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.4.0
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: grit
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.4.0
|
46
|
+
type: :development
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rspec
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "2.0"
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: bundler
|
62
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ~>
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 1.0.0
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: jeweler
|
73
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.6.2
|
79
|
+
type: :development
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rcov
|
84
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: *id007
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: rr
|
95
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ~>
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 1.0.0
|
101
|
+
type: :development
|
102
|
+
prerelease: false
|
103
|
+
version_requirements: *id008
|
104
|
+
description:
|
105
|
+
email: david@mojotech.com
|
106
|
+
executables:
|
107
|
+
- flux
|
108
|
+
extensions: []
|
109
|
+
|
110
|
+
extra_rdoc_files:
|
111
|
+
- LICENSE.txt
|
112
|
+
files:
|
113
|
+
- .document
|
114
|
+
- .flux
|
115
|
+
- .flux.local.sample
|
116
|
+
- .rspec
|
117
|
+
- Gemfile
|
118
|
+
- LICENSE.txt
|
119
|
+
- Rakefile
|
120
|
+
- VERSION
|
121
|
+
- bin/flux
|
122
|
+
- lib/flux.rb
|
123
|
+
- lib/flux/ext/pivotal-tracker.rb
|
124
|
+
- lib/flux/rcs/git.rb
|
125
|
+
- lib/flux/trackers/pivotal_tracker.rb
|
126
|
+
- lib/flux/util.rb
|
127
|
+
- lib/flux/util/table.rb
|
128
|
+
- lib/flux/workflows/mojotech.rb
|
129
|
+
- spec/flux/trackers/pivotal_tracker_spec.rb
|
130
|
+
- spec/flux/util/table_spec.rb
|
131
|
+
- spec/flux_spec.rb
|
132
|
+
- spec/spec_helper.rb
|
133
|
+
- spec/support/matchers/print_table.rb
|
134
|
+
- spec/support/rr.rb
|
135
|
+
has_rdoc: true
|
136
|
+
homepage: http://github.com/mojotech/flux
|
137
|
+
licenses:
|
138
|
+
- MIT
|
139
|
+
post_install_message:
|
140
|
+
rdoc_options: []
|
141
|
+
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 692717009
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
version: "0"
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: "0"
|
159
|
+
requirements: []
|
160
|
+
|
161
|
+
rubyforge_project:
|
162
|
+
rubygems_version: 1.6.2
|
163
|
+
signing_key:
|
164
|
+
specification_version: 3
|
165
|
+
summary: Command line workflow manager.
|
166
|
+
test_files: []
|
167
|
+
|