flux 0.0.1
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.
- 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
|
+
|