scrumninja-git-cli 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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