scrumninja-git-cli 0.0.2

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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Renewable Funding, LLC
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/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = scrumninja-git-cli
2
+
3
+ A command-line interface to integrate the ScrumNinja (http://scrumninja.com) API with a git workflow.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Renewable Funding, LLC. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "scrumninja-git-cli"
8
+ gem.summary = %Q{Workflow tools for git/ScrumNinja integration}
9
+ gem.description = %Q{A command-line interface to integrate the ScrumNinja (http://scrumninja.com) API with a git workflow.}
10
+ gem.email = "johnwilger@gmail.com"
11
+ gem.homepage = "http://github.com/jwilger/scrumninja-git-cli"
12
+ gem.authors = ["John Wilger", "Sam Livingston-Gray", "JD Huntington"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_development_dependency "mocha", "= 0.9.8"
15
+ gem.add_dependency "grit", "= 2.0.0"
16
+ gem.add_dependency "activesupport", "= 2.3.5"
17
+ gem.add_dependency "xml-simple", "= 1.0.12"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'rake/testtask'
26
+ Rake::TestTask.new(:test) do |test|
27
+ test.libs << 'lib' << 'test'
28
+ test.pattern = 'test/**/test_*.rb'
29
+ test.verbose = true
30
+ end
31
+
32
+ begin
33
+ require 'rcov/rcovtask'
34
+ Rcov::RcovTask.new do |test|
35
+ test.libs << 'test'
36
+ test.pattern = 'test/**/test_*.rb'
37
+ test.verbose = true
38
+ end
39
+ rescue LoadError
40
+ task :rcov do
41
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
42
+ end
43
+ end
44
+
45
+ task :test => :check_dependencies
46
+
47
+ task :default => :test
48
+
49
+ require 'rake/rdoctask'
50
+ Rake::RDocTask.new do |rdoc|
51
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "scrumninja-git-cli #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'scrum_ninja_git_cli'
3
+ ScrumNinjaGitCli.run_command(*ARGV)
@@ -0,0 +1,164 @@
1
+ require 'grit'
2
+ require 'shell_cmd'
3
+
4
+ module GitWrapper
5
+ module_function
6
+
7
+ class WorkingFolderDirtyException < Exception; end
8
+ class WrongBranchException < Exception; end
9
+ class OutOfDateGitException < Exception; end
10
+
11
+ def git
12
+ @git ||= Grit::Repo.new(locate_git_repo_root)
13
+ end
14
+
15
+ def locate_git_repo_root(start_dir = nil)
16
+ pwd = start_dir || ENV['PWD']
17
+ if Dir.new(pwd).entries.include?('.git')
18
+ pwd
19
+ else
20
+ parent = File.dirname(pwd)
21
+ return nil if parent == pwd
22
+ locate_git_repo_root(parent)
23
+ end
24
+ end
25
+
26
+ def current_branch_name
27
+ git.head.name
28
+ end
29
+
30
+ def is_git_current_enough?
31
+ (git_version <=> [1, 7]) != -1
32
+ end
33
+
34
+ def git_version
35
+ @git_version ||= run_git('--version').match(/^git version (.*)$/).captures.first.split('.').map(&:to_i)
36
+ end
37
+
38
+ def branches(remote_name = nil)
39
+ if remote_name
40
+ git.remotes.select { |e| e.name =~ Regexp.new("^#{remote_name}") }
41
+ else
42
+ git.branches
43
+ end
44
+ end
45
+
46
+ def local_branch_exists?(name)
47
+ git.branches.any? { |e| e.name == name }
48
+ end
49
+
50
+ def remote_branch_exists?(name, remote='origin')
51
+ branches(remote).any? { |b| b.name == "#{remote}/#{name}" }
52
+ end
53
+
54
+ def is_state_clean?
55
+ current_status = run_git 'status'
56
+ !!(current_status =~ /nothing to commit \(working directory clean\)/)
57
+ end
58
+
59
+ ##### Modification commands: these just shell out to git (directly or indirectly) #####
60
+ def checkout(branch_name, remote='origin')
61
+ fetch(remote)
62
+ create_branch(branch_name) unless local_branch_exists?(branch_name)
63
+ publish_remote_branch(branch_name, remote) unless remote_branch_exists?(branch_name, remote)
64
+ track_remote_branch(branch_name, remote)
65
+ checkout_branch(branch_name, remote)
66
+ end
67
+
68
+ def fetch(remote='origin')
69
+ run_git "fetch #{remote}"
70
+ end
71
+
72
+ def create_branch(branch_name)
73
+ run_git "branch #{branch_name}".strip
74
+ end
75
+
76
+ def publish_remote_branch(branch_name, remote='origin')
77
+ run_git "push #{remote} #{branch_name}:refs/heads/#{branch_name}"
78
+ end
79
+
80
+ def track_remote_branch(branch_name, remote='origin')
81
+ # Note that this could also be written as:
82
+ # run_git "config branch.#{branch_name}.remote #{remote}"
83
+ # run_git "config branch.#{branch_name}.merge refs/heads/#{branch_name}"
84
+ # ...but that doesn't read nearly as well. Just upgrade your damn git install already.
85
+ run_git "branch --set-upstream #{branch_name} #{remote}/#{branch_name}"
86
+ end
87
+
88
+ def checkout_branch(branch_name, remote='origin')
89
+ run_git "checkout #{branch_name}"
90
+ end
91
+
92
+ def merge_from_master
93
+ require_clean_story_branch
94
+ story_branch = current_branch_name
95
+ run_git 'checkout master'
96
+ run_git 'pull'
97
+ run_git "checkout #{story_branch}"
98
+ run_git 'merge master'
99
+ end
100
+
101
+ def push_master
102
+ require_clean_master_branch
103
+ run_git 'pull --rebase'
104
+ run_git "push origin master"
105
+ end
106
+
107
+ def push_branch
108
+ require_clean_story_branch
109
+ run_git 'pull --rebase'
110
+ run_git "push origin #{current_branch_name}"
111
+ end
112
+
113
+ def merge_branch_into_master(branch=nil)
114
+ if branch
115
+ require_clean_master_branch
116
+ branch_to_be_squashed = branch
117
+ else
118
+ require_clean_story_branch
119
+ branch_to_be_squashed = current_branch_name
120
+ run_git 'checkout master'
121
+ end
122
+ run_git 'pull'
123
+ run_git "merge #{branch_to_be_squashed}"
124
+ end
125
+
126
+ def require_clean_story_branch
127
+ raise WorkingFolderDirtyException.new unless is_state_clean?
128
+ raise WrongBranchException.new if 'master' == current_branch_name
129
+ end
130
+
131
+ def require_clean_master_branch
132
+ raise WorkingFolderDirtyException.new unless is_state_clean?
133
+ raise WrongBranchException.new unless 'master' == current_branch_name
134
+ end
135
+
136
+ def add(*args)
137
+ run_git "add #{args.join(" ")}"
138
+ end
139
+
140
+ def commit(default_message = "")
141
+ run_git "commit -m \"#{default_message}\" -e", true
142
+ end
143
+
144
+ def run_git(cmd, interactive=false)
145
+ puts "*" * 80
146
+ puts "* git #{cmd}"
147
+ puts "*" * 80
148
+ old_env = ENV['I_AM_A_GIT_GURU']
149
+ ENV['I_AM_A_GIT_GURU'] = '1'
150
+ if interactive
151
+ ShellCmd.run "git #{cmd}"
152
+ else
153
+ out = `git #{cmd}`
154
+ puts out
155
+ return out
156
+ end
157
+ ensure
158
+ ENV['I_AM_A_GIT_GURU'] = old_env
159
+ end
160
+
161
+ def string_hyphenize(str)
162
+ str.gsub(/[^A-Za-z ]/, '').gsub(/\s+/, '-').downcase
163
+ end
164
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_support'
2
+ Dir[File.dirname(__FILE__)+ '/scrum_ninja/*.rb'].each {|x| require x.sub(/\.rb$/,'') }
@@ -0,0 +1,34 @@
1
+ require 'tempfile'
2
+
3
+ module ScrumNinja
4
+ module Server
5
+ module_function
6
+
7
+ def get_stories(api_key, project_id)
8
+ get_page_contents "http://scrumninja.com/projects/#{project_id}/stories.xml?api_key=#{api_key}"
9
+ end
10
+
11
+ def get_story(api_key, project_id, story_id)
12
+ get_page_contents "http://scrumninja.com/projects/#{project_id}/stories/#{story_id}.xml?api_key=#{api_key}"
13
+ end
14
+
15
+
16
+ def update_story(api_key, project_id, story_id, xml)
17
+ url = "http://scrumninja.com/projects/#{project_id}/stories/#{story_id}.xml?api_key=#{api_key}"
18
+ # raise [story_id, url].inspect
19
+ post_xml(url, xml)
20
+ end
21
+
22
+
23
+ def get_page_contents(url)
24
+ `curl -s "#{url}"`
25
+ end
26
+
27
+ def post_xml(url, xml)
28
+ f = Tempfile.new('curl_hackery')
29
+ f << xml
30
+ f.close
31
+ `curl -s -H "Content-Type: application/xml; charset=utf-8" -X PUT -d @#{f.path} "#{url}"`
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,87 @@
1
+ require 'xmlsimple'
2
+
3
+ module ScrumNinja
4
+ class Session
5
+ attr_reader :api_key, :project_id, :user_id, :options
6
+
7
+ def initialize(opts={})
8
+ @options = opts_maybe_with_defaults_from_config_files(opts)
9
+
10
+ @api_key = options[:api_key ]
11
+ @project_id = options[:project_id]
12
+ @user_id = options[:user_id ]
13
+
14
+ raise ArgumentError.new("Your ScrumNinja API key could not be determined!") if @api_key.blank?
15
+ end
16
+
17
+ @read_config_from_filesystem = true
18
+ class << self
19
+ attr_accessor :read_config_from_filesystem
20
+ end
21
+
22
+ protected
23
+ def opts_maybe_with_defaults_from_config_files(opts)
24
+ opts = HashWithIndifferentAccess.new(opts)
25
+ return opts unless self.class.read_config_from_filesystem
26
+
27
+ search_dirs = [File.expand_path('~')]
28
+ search_dirs << opts[:project_dir] if opts[:project_dir].present?
29
+ opt_hashes = search_dirs.reverse.map { |dirname|
30
+ fname = File.join(dirname, '.scrumninja.yml')
31
+ YAML.load_file(fname)
32
+ }
33
+
34
+ return opt_hashes.inject(opts) { |mem, var| mem.reverse_merge(var) }
35
+ rescue Errno::ENOENT => e
36
+ opts
37
+ end
38
+ public
39
+
40
+ def get_stories
41
+ xml = Server.get_stories(api_key, project_id)
42
+ h = XmlSimple.xml_in(xml)
43
+ h['story'].map { |hh| story_from_xml_hash(hh) } #rescue raise h.inspect
44
+ end
45
+
46
+ def get_story_xml(story_id)
47
+ Server.get_story(api_key, project_id, story_id)
48
+ end
49
+
50
+ def get_story(story_id)
51
+ xml = get_story_xml(story_id)
52
+ h = XmlSimple.xml_in(xml)
53
+ story_from_xml_hash(h)
54
+ end
55
+
56
+ def story_from_xml_hash(h)
57
+ type = 'Story'
58
+ id = h['id'].first['content'].to_i
59
+ owner_id = h['owner-user-id'].first['content'].to_i
60
+ name = h['name'].first
61
+ Story.new(type, id, name, owner_id)
62
+ end
63
+ protected :story_from_xml_hash
64
+
65
+ def update_ownership(story_id, new_owner_id = nil, opts={})
66
+ return if opts[:abort_if_already_owned] && get_story(story_id).has_owner?
67
+ new_owner_id ||= user_id
68
+ Server.update_story api_key, project_id, story_id, <<-XML
69
+ <story>
70
+ <owner-user-id type="integer">#{new_owner_id}</owner-user-id>
71
+ </story>
72
+ XML
73
+ end
74
+
75
+ def update_status(story_id, new_status)
76
+ allowed_statuses = ['in process', 'delivered', 'accepted', 'deployed']
77
+ unless allowed_statuses.include?(new_status)
78
+ raise ArgumentError.new("Allowed statuses are #{allowed_statuses.join(', ')}")
79
+ end
80
+ Server.update_story api_key, project_id, story_id, <<-XML
81
+ <story>
82
+ <status>#{new_status}</status>
83
+ </story>
84
+ XML
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,25 @@
1
+ module ScrumNinja
2
+ Story = Struct.new(:type, :story_number, :name, :owner_id) do
3
+ def to_s
4
+ '%-5s %6d: %s' % [type, story_number, name]
5
+ end
6
+
7
+ def to_i
8
+ story_number.to_i
9
+ end
10
+
11
+ def has_owner?
12
+ @owner_id
13
+ end
14
+
15
+ def branch_name
16
+ [story_number, hyphenized_name] * '-'
17
+ end
18
+
19
+ def hyphenized_name
20
+ GitWrapper.string_hyphenize(name)
21
+ rescue
22
+ "indeterminate-branch-name-#{rand(100000000000)}"
23
+ end
24
+ end
25
+ end