gitcycle 0.1.0

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/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ features/config.yml
5
+ features/fixtures
6
+ Gemfile.lock
7
+ pkg
8
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,16 @@
1
+ Gitcycle
2
+ ========
3
+
4
+ Tame your development cycle.
5
+
6
+ Requirements
7
+ ------------
8
+
9
+ <pre>
10
+ gem install gitcycle
11
+ </pre>
12
+
13
+ Get Started
14
+ -----------
15
+
16
+ Visit [gitcycle.com](http://gitcycle.com) to get set up.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/gitc ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path("../../lib/gitcycle", __FILE__)
4
+
5
+ gitcycle = Gitcycle.new
6
+ command = ARGV.shift
7
+
8
+ if command.nil?
9
+ puts "\nNo command specified\n".red
10
+ elsif gitcycle.respond_to?(command)
11
+ gitcycle.send(command, *ARGV)
12
+ elsif ARGV.empty?
13
+ gitcycle.create_branch(command)
14
+ else
15
+ puts "\nCommand '#{command}' not found.\n".red
16
+ end
@@ -0,0 +1,9 @@
1
+ lighthouse:
2
+ account: # lighthouse subdomain
3
+ project: # lighthouse project id integer
4
+ token: # lighthouse api token hash
5
+ owner: # user of parent repo
6
+ repo: # repo name
7
+ token_dev: # gitcycle api token (dev)
8
+ token_qa: # gitcycle api token (qa)
9
+ user: # user of forked repo
@@ -0,0 +1,155 @@
1
+ Feature: gitcycle
2
+
3
+ Scenario: No command given
4
+ When I execute gitcycle with ""
5
+ Then output includes "No command specified"
6
+
7
+ Scenario: Non-existent command
8
+ When I execute gitcycle with "blah blah"
9
+ Then output includes "Command 'blah' not found"
10
+
11
+ Scenario: Setup
12
+ When I execute gitcycle setup
13
+ Then output includes "Configuration saved."
14
+ And gitcycle.yml should be valid
15
+
16
+ Scenario: Feature branch w/ custom branch name
17
+ Given a fresh set of repositories
18
+ And a new Lighthouse ticket
19
+ When I cd to the user repo
20
+ And I execute gitcycle with the Lighthouse ticket URL
21
+ And I enter "n"
22
+ And I enter "ticket.id-rename"
23
+ Then output includes "Retrieving branch information from gitcycle."
24
+ And output includes "Would you like to name your branch 'ticket.id'?"
25
+ And output includes "What would you like to name your branch?"
26
+ And output includes "Creating 'ticket.id-rename' from 'master'."
27
+ And output includes "Checking out branch 'ticket.id-rename'."
28
+ And output includes "Pushing 'ticket.id-rename'."
29
+ And output includes "Sending branch information to gitcycle."
30
+ And redis entries valid
31
+
32
+ Scenario: Feature branch
33
+ Given a fresh set of repositories
34
+ And a new Lighthouse ticket
35
+ When I cd to the user repo
36
+ And I execute gitcycle with the Lighthouse ticket URL
37
+ And I enter "y"
38
+ Then output includes "Retrieving branch information from gitcycle."
39
+ And output includes "Would you like to name your branch 'ticket.id'?"
40
+ And output does not include "What would you like to name your branch?"
41
+ And output includes "Creating 'ticket.id' from 'master'."
42
+ And output includes "Checking out branch 'ticket.id'."
43
+ And output includes "Pushing 'ticket.id'."
44
+ And output includes "Sending branch information to gitcycle."
45
+ And redis entries valid
46
+
47
+ Scenario: Checkout via ticket w/ existing branch
48
+ When I cd to the user repo
49
+ And I execute gitcycle with the Lighthouse ticket URL
50
+ Then output includes "Retrieving branch information from gitcycle."
51
+ And output does not include "Would you like to name your branch 'ticket.id'?"
52
+ And output does not include "What would you like to name your branch?"
53
+ And output does not include "Creating 'ticket.id' from 'master'."
54
+ And output includes "Checking out branch 'ticket.id'."
55
+ And output does not include "Pushing 'ticket.id'."
56
+ And output does not include "Sending branch information to gitcycle."
57
+ And current branch is "ticket.id"
58
+
59
+ Scenario: Checkout via ticket w/ fresh repo
60
+ Given a fresh set of repositories
61
+ When I cd to the user repo
62
+ And I execute gitcycle with the Lighthouse ticket URL
63
+ Then output includes "Retrieving branch information from gitcycle."
64
+ And output does not include "Would you like to name your branch 'ticket.id'?"
65
+ And output does not include "What would you like to name your branch?"
66
+ And output does not include "Creating 'ticket.id' from 'master'."
67
+ And output includes "Tracking branch 'ticket.id'."
68
+ And output does not include "Pushing 'ticket.id'."
69
+ And output does not include "Sending branch information to gitcycle."
70
+ And current branch is "ticket.id"
71
+
72
+ Scenario: Pull changes from upstream
73
+ When I cd to the owner repo
74
+ And I checkout master
75
+ And I commit something
76
+ And I cd to the user repo
77
+ And I execute gitcycle with "pull"
78
+ Then output includes "Retrieving branch information from gitcycle."
79
+ And output includes "Adding remote repo 'config.owner/config.repo'."
80
+ And output includes "Fetching remote branch 'master'."
81
+ And output includes "Merging remote branch 'master' from 'config.owner/config.repo'."
82
+ And git log should contain the last commit
83
+
84
+ Scenario: Discuss commits w/ no parameters and nothing committed
85
+ When I cd to the user repo
86
+ And I checkout ticket.id
87
+ And I execute gitcycle with "discuss"
88
+ Then output includes "Retrieving branch information from gitcycle."
89
+ And output includes "Creating GitHub pull request."
90
+ And output does not include "Branch not found."
91
+ And output does not include "Opening issue"
92
+ And output includes "You must push code before opening a pull request."
93
+ And redis entries valid
94
+
95
+ Scenario: Discuss commits w/ no parameters and something committed
96
+ When I cd to the user repo
97
+ And I checkout ticket.id
98
+ And I commit something
99
+ And I execute gitcycle with "discuss"
100
+ Then output includes "Retrieving branch information from gitcycle."
101
+ And output includes "Creating GitHub pull request."
102
+ And output does not include "Branch not found."
103
+ And output includes "Opening issue" with URL
104
+ And output does not include "You must push code before opening a pull request."
105
+ And URL is a valid issue
106
+ And redis entries valid
107
+
108
+ Scenario: Discuss commits w/ parameters
109
+ When I cd to the user repo
110
+ And I checkout ticket.id
111
+ And I execute gitcycle with "discuss issue.id"
112
+ Then output includes "Retrieving branch information from gitcycle."
113
+ And output does not include "Creating GitHub pull request."
114
+ And output does not include "Branch not found."
115
+ And output does not include "You must push code before opening a pull request."
116
+ And output includes "Opening issue" with URL
117
+ And URL is a valid issue
118
+
119
+ Scenario: Ready issue w/ no parameters
120
+ When I cd to the user repo
121
+ And I checkout ticket.id
122
+ And I execute gitcycle with "ready"
123
+ Then output includes "Labeling issue as 'Pending Review'."
124
+
125
+ Scenario: Ready issue w/ parameters
126
+ When I cd to the user repo
127
+ And I execute gitcycle with "ready issue.id"
128
+ Then output includes "Labeling issues as 'Pending Review'."
129
+
130
+ Scenario: Reviewed issue w/ no parameters
131
+ When I cd to the user repo
132
+ And I checkout ticket.id
133
+ And I execute gitcycle with "reviewed"
134
+ Then output includes "Labeling issue as 'Pending QA'."
135
+
136
+ Scenario: Reviewed issue w/ parameters
137
+ When I cd to the user repo
138
+ And I execute gitcycle with "reviewed issue.id"
139
+ Then output includes "Labeling issues as 'Pending QA'."
140
+
141
+ Scenario: QA issue
142
+ When I cd to the owner repo
143
+ And I checkout master
144
+ And I execute gitcycle with "qa issue.id"
145
+ Then output includes "Retrieving branch information from gitcycle."
146
+ And output does not include "Checking out source branch 'master'."
147
+ And output does not include "Tracking source branch 'master'."
148
+ And output does not include "Deleting old QA branch 'qa_master'."
149
+ And output includes "Creating QA branch 'qa_master'."
150
+ And output includes "Adding remote repo 'config.user/config.repo'."
151
+ And output includes "Fetching remote branch 'ticket.id'."
152
+ And output includes "Merging remote branch 'ticket.id' from 'config.user/config.repo'."
153
+ And output includes "Pushing QA branch 'qa_master'."
154
+ And output includes "Type 'gitc qa pass' to approve all issues in this branch."
155
+ And output includes "Type 'gitc qa fail' to reject all issues in this branch."
@@ -0,0 +1,225 @@
1
+ require 'aruba/cucumber'
2
+ require 'lighthouse'
3
+ require 'redis'
4
+ require 'rspec/expectations'
5
+ require 'yaml'
6
+ require 'yajl'
7
+
8
+ BASE = File.expand_path '../../../', __FILE__
9
+ BIN = "#{BASE}/bin/gitc"
10
+
11
+ ENV['CONFIG'] = GITCYCLE = "#{BASE}/features/fixtures/gitcycle.yml"
12
+ ENV['ENV'] = 'development'
13
+
14
+ $redis = Redis.new
15
+
16
+ Before do |scenario|
17
+ @aruba_timeout_seconds = 10
18
+ @scenario_title = scenario.title
19
+ end
20
+
21
+ def branches(options={})
22
+ in_current_dir do
23
+ b = `git branch#{" -a" if options[:all]}`
24
+ if options[:current]
25
+ b.match(/\*\s+(.+)/)[1]
26
+ elsif options[:match]
27
+ b.match(/([\s]+|origin\/)(#{options[:match]})/)[2] rescue nil
28
+ else
29
+ b
30
+ end
31
+ end
32
+ end
33
+
34
+ def config(reload=false)
35
+ @config = nil if reload
36
+ @config ||= YAML.load(File.read("#{BASE}/features/config.yml"))
37
+ Lighthouse.account = @config['lighthouse']['account']
38
+ Lighthouse.token = @config['lighthouse']['token']
39
+ @config
40
+ end
41
+
42
+ def gsub_variables(str)
43
+ if $ticket
44
+ str = str.gsub('ticket.id', $ticket.attributes['id'])
45
+ end
46
+ if $url
47
+ issue_id = $url.match(/https:\/\/github.com\/.+\/issues\/(\d+)/)[1]
48
+ str = str.gsub('issue.id', issue_id)
49
+ end
50
+ str = str.gsub('config.owner', config['owner'])
51
+ str = str.gsub('config.repo', config['repo'])
52
+ str = str.gsub('config.user', config['user'])
53
+ str
54
+ end
55
+
56
+ def log(match)
57
+ in_current_dir do
58
+ log = `git log --pretty=format:%s`
59
+ match = !(match =~ /^#{match}$/).nil?
60
+ end
61
+ match
62
+ end
63
+
64
+ def repos(reload=false)
65
+ if $repos && !reload
66
+ $repos
67
+ else
68
+ owner = "#{BASE}/features/fixtures/owner"
69
+ user = "#{BASE}/features/fixtures/user"
70
+
71
+ FileUtils.rm_rf(owner)
72
+ FileUtils.rm_rf(user)
73
+
74
+ system [
75
+ "cd #{BASE}/features/fixtures",
76
+ "git clone git@github.com:#{config['owner']}/#{config['repo']}.git owner",
77
+ "git clone git@github.com:#{config['user']}/#{config['repo']}.git user"
78
+ ].join(' && ')
79
+
80
+ $repos = { :owner => owner, :user => user }
81
+ end
82
+ end
83
+
84
+ def run_gitcycle(cmd, interactive=false)
85
+ cmd = [ BIN, cmd ].join(' ')
86
+ if interactive
87
+ run_interactive(unescape(cmd))
88
+ else
89
+ run_simple(unescape(cmd), false)
90
+ end
91
+ end
92
+
93
+ Given /^a fresh set of repositories$/ do
94
+ repos(true)
95
+ end
96
+
97
+ Given /^a new Lighthouse ticket$/ do
98
+ $ticket = Lighthouse::Ticket.new(
99
+ :body => "test",
100
+ :project_id => config['lighthouse']['project'],
101
+ :state => "open",
102
+ :title => "Test ticket"
103
+ )
104
+ $ticket.save
105
+ end
106
+
107
+ When /^I execute gitcycle with "([^\"]*)"$/ do |cmd|
108
+ cmd = gsub_variables(cmd)
109
+ run_gitcycle(cmd)
110
+ end
111
+
112
+ When /^I execute gitcycle setup$/ do
113
+ FileUtils.rm(GITCYCLE) if File.exists?(GITCYCLE)
114
+ run_gitcycle [
115
+ "setup",
116
+ config['user'],
117
+ config['repo'],
118
+ config['token_dev']
119
+ ].join(' ')
120
+ run_gitcycle [
121
+ "setup",
122
+ config['user'],
123
+ "#{config['owner']}/#{config['repo']}",
124
+ config['token_qa']
125
+ ].join(' ')
126
+ end
127
+
128
+ When /^I execute gitcycle with the Lighthouse ticket URL$/ do
129
+ run_gitcycle $ticket.url, true
130
+ end
131
+
132
+ When /^I cd to the (.*) repo$/ do |user|
133
+ dirs.pop
134
+ cd($repos[user.to_sym])
135
+ end
136
+
137
+ When /^I enter "([^\"]*)"$/ do |input|
138
+ input = gsub_variables(input)
139
+ type(input)
140
+ end
141
+
142
+ When /^I commit something$/ do
143
+ branch = branches(:current => true)
144
+ in_current_dir do
145
+ $commit_msg = "#{@scenario_title} - #{rand}"
146
+ File.open('README', 'w') {|f| f.write($commit_msg) }
147
+ `git add . && git add . -u && git commit -a -m '#{$commit_msg}'`
148
+ `git push origin #{branch}`
149
+ end
150
+ end
151
+
152
+ When /^I checkout (.+)$/ do |branch|
153
+ branch = gsub_variables(branch)
154
+ in_current_dir do
155
+ `git checkout #{branch}`
156
+ end
157
+ end
158
+
159
+ Then /^gitcycle\.yml should be valid$/ do
160
+ gitcycle = YAML.load(File.read(GITCYCLE))
161
+
162
+ repo = "#{config['user']}/#{config['repo']}"
163
+ gitcycle[repo].should == [ config['user'], config['token_dev'] ]
164
+
165
+ repo = "#{config['owner']}/#{config['repo']}"
166
+ gitcycle[repo].should == [ config['user'], config['token_qa'] ]
167
+ end
168
+
169
+ Then /^output includes \"([^\"]*)"$/ do |expected|
170
+ expected = gsub_variables(expected)
171
+ assert_partial_output(expected, all_output)
172
+ end
173
+
174
+ Then /^output includes \"([^\"]*)" with URL$/ do |expected|
175
+ puts all_output.inspect
176
+ expected = gsub_variables(expected)
177
+ assert_partial_output(expected, all_output)
178
+ $url = all_output.match(/^#{expected}.*(https?:\/\/[^\s]+)/)[1]
179
+ end
180
+
181
+ Then /^output does not include \"([^\"]*)"$/ do |expected|
182
+ expected = gsub_variables(expected)
183
+ assert_no_partial_output(expected, all_output)
184
+ end
185
+
186
+ Then /^redis entries valid$/ do
187
+ add = @scenario_title.include?('custom branch name') ? "-rename" : ""
188
+ branch = $redis.hget(
189
+ [
190
+ "users",
191
+ config['user'],
192
+ "repos",
193
+ "#{config['owner']}:#{config['repo']}",
194
+ "branches"
195
+ ].join('/'),
196
+ $ticket.attributes['id'] + add
197
+ )
198
+ branch = Yajl::Parser.parse(branch)
199
+ should = {
200
+ 'lighthouse_url' => $ticket.url,
201
+ 'body' => "<div><p>test</p></div>\n\n#{$ticket.url}",
202
+ 'name' => $ticket.attributes['id'] + add,
203
+ 'id' => $ticket.attributes['id'] + add,
204
+ 'title' => $ticket.title,
205
+ 'repo' => "#{config['owner']}:#{config['repo']}",
206
+ 'user' => config['user'],
207
+ 'source' => 'master'
208
+ }
209
+ if @scenario_title == 'Discuss commits w/ no parameters and something committed'
210
+ should['issue_url'] = $url
211
+ end
212
+ branch.should == should
213
+ end
214
+
215
+ Then /^current branch is \"([^\"]*)"$/ do |branch|
216
+ branches(:current => true).should == gsub_variables(branch)
217
+ end
218
+
219
+ Then /^git log should contain the last commit$/ do
220
+ log($commit_msg).should == true
221
+ end
222
+
223
+ Then /^URL is a valid issue$/ do
224
+ $url.should =~ /https:\/\/github.com\/.+\/issues\/\d+/
225
+ end
data/gitcycle.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ root = File.expand_path('../', __FILE__)
3
+ lib = "#{root}/lib"
4
+
5
+ $:.unshift lib unless $:.include?(lib)
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "gitcycle"
9
+ s.version = '0.1.0'
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = [ 'Winton Welsh' ]
12
+ s.email = [ 'mail@wintoni.us' ]
13
+ s.homepage = "https://github.com/winton/gitcycle"
14
+ s.summary = %q{Tame your development cycle}
15
+ s.description = %q{Tame your development cycle.}
16
+
17
+ s.executables = `cd #{root} && git ls-files bin/*`.split("\n").collect { |f| File.basename(f) }
18
+ s.files = `cd #{root} && git ls-files`.split("\n")
19
+ s.require_paths = %w(lib)
20
+ s.test_files = `cd #{root} && git ls-files -- {features,test,spec}/*`.split("\n")
21
+
22
+ s.add_development_dependency "aruba"
23
+ s.add_development_dependency "cucumber"
24
+ s.add_development_dependency "lighthouse"
25
+ s.add_development_dependency "redis"
26
+ s.add_development_dependency "rspec"
27
+ s.add_development_dependency "yajl-ruby"
28
+
29
+ s.add_dependency "launchy", "= 2.0.5"
30
+ s.add_dependency "yajl-ruby", "= 1.1.0"
31
+ end
data/lib/ext/string.rb ADDED
@@ -0,0 +1,20 @@
1
+ class String
2
+
3
+ # Colors
4
+
5
+ def blue
6
+ "\e[34m#{self}\e[0m"
7
+ end
8
+
9
+ def green
10
+ "\e[32m#{self}\e[0m"
11
+ end
12
+
13
+ def red
14
+ "\e[31m#{self}\e[0m"
15
+ end
16
+
17
+ def yellow
18
+ "\e[33m#{self}\e[0m"
19
+ end
20
+ end
data/lib/gitcycle.rb ADDED
@@ -0,0 +1,498 @@
1
+ require 'open-uri'
2
+ require 'uri'
3
+ require 'yaml'
4
+
5
+ gem 'launchy', '= 2.0.5'
6
+ require 'launchy'
7
+
8
+ gem 'yajl-ruby', '= 1.1.0'
9
+ require 'yajl'
10
+
11
+ $:.unshift File.dirname(__FILE__)
12
+
13
+ require "ext/string"
14
+
15
+ class Gitcycle
16
+
17
+ API =
18
+ if ENV['ENV'] == 'development'
19
+ "http://127.0.0.1:8080/api"
20
+ else
21
+ "http://gitcycle.bleacherreport.com/api"
22
+ end
23
+
24
+ def initialize
25
+ if ENV['CONFIG']
26
+ @config_path = File.expand_path(ENV['CONFIG'])
27
+ else
28
+ @config_path = File.expand_path("~/.gitcycle.yml")
29
+ end
30
+ load_config
31
+ load_git
32
+ end
33
+
34
+ def create_branch(url_or_title)
35
+ require_git && require_config
36
+
37
+ params = {}
38
+
39
+ if url_or_title.strip[0..3] == 'http'
40
+ if url_or_title.include?('lighthouseapp.com/')
41
+ params = { 'branch[lighthouse_url]' => url_or_title }
42
+ elsif url_or_title.include?('github.com/')
43
+ params = { 'branch[issue_url]' => url_or_title }
44
+ end
45
+ else
46
+ params = {
47
+ 'branch[name]' => url_or_title,
48
+ 'branch[title]' => url_or_title
49
+ }
50
+ end
51
+
52
+ puts "\nRetrieving branch information from gitcycle.\n".green
53
+ branch = get('branch', params)
54
+
55
+ name = branch['name']
56
+
57
+ unless branch['exists']
58
+ branch['source'] = branches(:current => true)
59
+
60
+ unless yes?("Would you like to name your branch '#{name}'?")
61
+ name = q("\nWhat would you like to name your branch?")
62
+ name = name.gsub(/[\s\W]/, '-')
63
+ end
64
+
65
+ unless branches(:match => name)
66
+ puts "\nCreating '#{name}' from '#{branch['source']}'.\n".green
67
+ run("git branch #{name}")
68
+ end
69
+ end
70
+
71
+ if branches(:match => name)
72
+ puts "Checking out branch '#{name}'.\n".green
73
+ run("git checkout #{name}")
74
+ else
75
+ puts "Tracking branch '#{name}'.\n".green
76
+ run("git fetch && git checkout -b #{name} origin/#{name}")
77
+ end
78
+
79
+ unless branch['exists']
80
+ puts "Pushing '#{name}'.".green
81
+ run("git push origin #{name}")
82
+
83
+ puts "Sending branch information to gitcycle.".green
84
+ get('branch',
85
+ 'branch[name]' => branch['name'],
86
+ 'branch[rename]' => name != branch['name'] ? name : nil,
87
+ 'branch[source]' => branch['source']
88
+ )
89
+ end
90
+
91
+ puts "\n"
92
+ end
93
+
94
+ def discuss(*issues)
95
+ require_git && require_config
96
+
97
+ puts "\nRetrieving branch information from gitcycle.\n".green
98
+
99
+ if issues.empty?
100
+ branch = get('branch',
101
+ 'branch[name]' => branches(:current => true),
102
+ 'create' => 0
103
+ )
104
+
105
+ if branch && !branch['issue_url']
106
+ puts "Creating GitHub pull request.\n".green
107
+ branch = get('branch',
108
+ 'branch[create_pull_request]' => true,
109
+ 'branch[name]' => branch['name'],
110
+ 'create' => 0
111
+ )
112
+ end
113
+
114
+ if branch == false
115
+ puts "Branch not found.\n".red
116
+ elsif branch['issue_url']
117
+ puts "Opening issue: #{branch['issue_url']}\n".green
118
+ Launchy.open(branch['issue_url'])
119
+ else
120
+ puts "You must push code before opening a pull request.\n".red
121
+ end
122
+ else
123
+ get('branch', 'issues' => issues, 'scope' => 'repo').each do |branch|
124
+ if branch['issue_url']
125
+ puts "Opening issue: #{branch['issue_url']}\n".green
126
+ Launchy.open(branch['issue_url'])
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def pull
133
+ require_git && require_config
134
+
135
+ puts "\nRetrieving branch information from gitcycle.\n".green
136
+ branch = get('branch',
137
+ 'branch[name]' => branches(:current => true),
138
+ 'include' => [ 'repo' ],
139
+ 'create' => 0
140
+ )
141
+
142
+ merge_remote_branch(
143
+ :owner => branch['repo']['owner'],
144
+ :repo => branch['repo']['name'],
145
+ :branch => branch['source']
146
+ )
147
+ end
148
+
149
+ def qa(*issues)
150
+ require_git && require_config
151
+
152
+ if issues.empty?
153
+ puts "\n"
154
+ get('qa_branch').each do |branches|
155
+ puts "qa_#{branches['source']}".green
156
+ branches['branches'].each do |branch|
157
+ puts " #{"issue ##{branch['issue']}".yellow}\t#{branch['user']}/#{branch['branch']}"
158
+ end
159
+ puts "\n"
160
+ end
161
+ elsif issues.first == 'fail' || issues.first == 'pass'
162
+ branch = branches(:current => true)
163
+ label = issues.first.capitalize
164
+
165
+ if branch =~ /^qa_/
166
+ puts "\nRetrieving branch information from gitcycle.\n".green
167
+ qa_branch = get('qa_branch', :source => branch.gsub(/^qa_/, ''))
168
+
169
+ puts "Checking out #{qa_branch['source']}.".green
170
+ run("git checkout #{qa_branch['source']}")
171
+
172
+ if issues[1..-1].empty?
173
+ puts "Merging '#{branch}' into '#{qa_branch['source']}'.\n".green
174
+ run("git merge #{branch}")
175
+ run("git push origin #{qa_branch['source']}")
176
+
177
+ puts "\nLabeling all issues as '#{label}'.\n".green
178
+ get('label',
179
+ 'qa_branch[source]' => branch.gsub(/^qa_/, ''),
180
+ 'labels' => [ label ]
181
+ )
182
+
183
+ branches = qa_branch['branches']
184
+ else
185
+ issues = [1..-1]
186
+
187
+ branches = qa_branch['branches'].select do |b|
188
+ issues.include?(b['issue'])
189
+ end
190
+
191
+ branches.each do |branch|
192
+ merge_remote_branch(
193
+ :user => branch['user'],
194
+ :repo => branch['repo'].split(':'),
195
+ :branch => branch['branch']
196
+ )
197
+
198
+ puts "\nLabeling issue #{branch['issue']} as '#{label}'.\n".green
199
+ get('label',
200
+ 'qa_branch[source]' => branch.gsub(/^qa_/, ''),
201
+ 'issue' => branch['issue'],
202
+ 'labels' => [ label ]
203
+ )
204
+ end
205
+ end
206
+
207
+ puts "\nMarking Lighthouse tickets as 'pending-approval'.\n".green
208
+ branches = branches.collect do |b|
209
+ { :name => b['branch'], :repo => b['repo'], :user => b['user'] }
210
+ end
211
+ get('ticket_resolve', 'branches' => Yajl::Encoder.encode(branches))
212
+ else
213
+ puts "\nYou are not in a QA branch.\n".red
214
+ end
215
+ elsif issues.first == 'resolved'
216
+ branch = branches(:current => true)
217
+
218
+ if branch =~ /^qa_/
219
+ puts "\nRetrieving branch information from gitcycle.\n".green
220
+ qa_branch = get('qa_branch', :source => branch.gsub(/^qa_/, ''))
221
+
222
+ if qa_branch
223
+ branches = qa_branch['branches']
224
+ conflict = branches.detect { |branch| branch['conflict'] }
225
+
226
+ if conflict
227
+ puts "Committing merge resolution of #{conflict['branch']} (issue ##{conflict['issue']}).\n".green
228
+ run("git add . && git add . -u && git commit -a -m 'Merge conflict resolution of #{conflict['branch']} (issue ##{conflict['issue']})'")
229
+
230
+ puts "Pushing merge resolution of #{conflict['branch']} (issue ##{conflict['issue']}).\n".green
231
+ run("git push origin qa_#{qa_branch['source']}")
232
+
233
+ create_qa_branch(
234
+ :preserve => true,
235
+ :range => (branches.index(conflict)+1..-1),
236
+ :qa_branch => qa_branch
237
+ )
238
+ else
239
+ puts "Couldn't find record of a conflicted merge.\n".red
240
+ end
241
+ else
242
+ puts "Couldn't find record of a conflicted merge.\n".red
243
+ end
244
+ else
245
+ puts "\nYou aren't on a QA branch.\n".red
246
+ end
247
+ else
248
+ create_qa_branch(:issues => issues)
249
+ end
250
+ end
251
+
252
+ def ready(*issues)
253
+ require_git && require_config
254
+
255
+ if issues.empty?
256
+ puts "\nLabeling issue as 'Pending Review'.\n".green
257
+ get('label',
258
+ 'branch[name]' => branches(:current => true),
259
+ 'labels' => [ 'Pending Review' ]
260
+ )
261
+ else
262
+ puts "\nLabeling issues as 'Pending Review'.\n".green
263
+ get('label',
264
+ 'issues' => issues,
265
+ 'labels' => [ 'Pending Review' ],
266
+ 'scope' => 'repo'
267
+ )
268
+ end
269
+ end
270
+
271
+ def reviewed(*issues)
272
+ require_git && require_config
273
+
274
+ if issues.empty?
275
+ puts "\nLabeling issue as 'Pending QA'.\n".green
276
+ get('label',
277
+ 'branch[name]' => branches(:current => true),
278
+ 'labels' => [ 'Pending QA' ]
279
+ )
280
+ else
281
+ puts "\nLabeling issues as 'Pending QA'.\n".green
282
+ get('label',
283
+ 'issues' => issues,
284
+ 'labels' => [ 'Pending QA' ],
285
+ 'scope' => 'repo'
286
+ )
287
+ end
288
+ end
289
+
290
+ def setup(login, repo, token)
291
+ repo = "#{login}/#{repo}" unless repo.include?('/')
292
+ @config[repo] = [ login, token ]
293
+ save_config
294
+ puts "\nConfiguration saved.\n".green
295
+ end
296
+
297
+ private
298
+
299
+ def branches(options={})
300
+ b = `git branch#{" -a" if options[:all]}`
301
+ if options[:current]
302
+ b.match(/\*\s+(.+)/)[1]
303
+ elsif options[:match]
304
+ b.match(/([\s]+|origin\/)(#{options[:match]})/)[2] rescue nil
305
+ else
306
+ b
307
+ end
308
+ end
309
+
310
+ def create_qa_branch(options)
311
+ issues = options[:issues]
312
+ range = options[:range] || (0..-1)
313
+
314
+ if (issues && !issues.empty?) || options[:qa_branch]
315
+ if options[:qa_branch]
316
+ qa_branch = options[:qa_branch]
317
+ else
318
+ puts "\nRetrieving branch information from gitcycle.\n".green
319
+ qa_branch = get('qa_branch', 'issues' => issues)
320
+ end
321
+
322
+ source = qa_branch['source']
323
+ name = "qa_#{source}"
324
+
325
+ unless qa_branch['branches'].empty?
326
+ unless options[:preserve]
327
+ if branches(:current => source)
328
+ # Do nothing
329
+ elsif branches(:match => source)
330
+ puts "Checking out source branch '#{source}'.\n".green
331
+ run("git checkout #{source}")
332
+ else
333
+ puts "Tracking source branch '#{source}'.\n".green
334
+ run("git fetch && git checkout -b #{source} origin/#{source}")
335
+ end
336
+
337
+ if branches(:match => name)
338
+ puts "Deleting old QA branch '#{name}'.\n".green
339
+ run("git branch -D #{name}")
340
+ run("git push origin :#{name}")
341
+ end
342
+
343
+ puts "Creating QA branch '#{name}'.\n".green
344
+ run("git branch #{name} && git checkout #{name}")
345
+
346
+ puts "\n"
347
+ end
348
+
349
+ qa_branch['branches'][range].each do |branch|
350
+ issue = branch['issue']
351
+ owner, repo = branch['repo'].split(':')
352
+ user = branch['user']
353
+ branch = branch['branch']
354
+
355
+ output = merge_remote_branch(
356
+ :owner => user,
357
+ :repo => repo,
358
+ :branch => branch
359
+ )
360
+
361
+ if output.include?('CONFLICT')
362
+ puts "Conflict occurred when merging '#{branch}' (issue ##{issue}).\n".red
363
+ answer = q("Would you like to (s)kip or (r)esolve?")
364
+
365
+ issues = qa_branch['branches'].collect { |b| b['issue'] }
366
+
367
+ if answer.downcase[0..0] == 's'
368
+ run("git reset --hard HEAD")
369
+ issues.delete(issue)
370
+
371
+ puts "\nSending QA branch information to gitcycle.\n".green
372
+ get('qa_branch', 'issues' => issues, 'conflict' => issue)
373
+ else
374
+ puts "\nSending conflict information to gitcycle.\n".green
375
+ get('qa_branch', 'issues' => issues, 'conflict' => issue)
376
+
377
+ puts "Type 'gitc qa resolved' when finished resolving.\n".yellow
378
+ exit
379
+ end
380
+ else
381
+ puts "Pushing QA branch '#{name}'.\n".green
382
+ run("git push origin #{name}")
383
+ end
384
+ end
385
+
386
+ puts "\nType '".yellow + "gitc qa pass".green + "' to approve all issues in this branch.\n".yellow
387
+ puts "Type '".yellow + "gitc qa fail".red + "' to reject all issues in this branch.\n".yellow
388
+ end
389
+ end
390
+ end
391
+
392
+ def get(path, hash={})
393
+ hash.merge!(
394
+ :login => @login,
395
+ :token => @token
396
+ )
397
+
398
+ params = ''
399
+ hash[:session] = 0
400
+ hash.each do |k, v|
401
+ if v
402
+ params << "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}&"
403
+ end
404
+ end
405
+ params.chop! # trailing &
406
+
407
+ json = open("#{API}/#{path}.json?#{params}").read
408
+ Yajl::Parser.parse(json)
409
+ end
410
+
411
+ def load_config
412
+ if File.exists?(@config_path)
413
+ @config = YAML.load(File.read(@config_path))
414
+ else
415
+ @config = {}
416
+ end
417
+ end
418
+
419
+ def load_git
420
+ path = "#{Dir.pwd}/.git/config"
421
+ if File.exists?(path)
422
+ @git_url = File.read(path).match(/\[remote "origin"\][^\[]*url = ([^\n]+)/m)[1]
423
+ @git_repo = @git_url.match(/\/(.+)\./)[1]
424
+ @git_login = @git_url.match(/:(.+)\//)[1]
425
+ @login, @token = @config["#{@git_login}/#{@git_repo}"] rescue [ nil, nil ]
426
+ end
427
+ end
428
+
429
+ def merge_remote_branch(options={})
430
+ owner = options[:owner]
431
+ repo = options[:repo]
432
+ branch = options[:branch]
433
+
434
+ $remotes ||= {}
435
+
436
+ unless $remotes[owner]
437
+ $remotes[owner] = true
438
+ puts "Adding remote repo '#{owner}/#{repo}'.\n".green
439
+ run("git remote rm #{owner}") if remotes(:match => owner)
440
+ run("git remote add #{owner} git@github.com:#{owner}/#{repo}.git")
441
+ end
442
+
443
+ puts "\nFetching remote branch '#{branch}'.\n".green
444
+ run("git fetch #{owner}")
445
+
446
+ puts "\nMerging remote branch '#{branch}' from '#{owner}/#{repo}'.\n".green
447
+ run("git merge #{owner}/#{branch}")
448
+ end
449
+
450
+ def remotes(options={})
451
+ b = `git remote`
452
+ if options[:match]
453
+ b.match(/^(#{options[:match]})$/)[1] rescue nil
454
+ else
455
+ b
456
+ end
457
+ end
458
+
459
+ def require_config
460
+ unless @login && @token
461
+ puts "\nGitcycle configuration not found.".red
462
+ puts "Are you in the right repository?".yellow
463
+ puts "Have you set up this repository at http://gitcycle.com?\n".yellow
464
+ exit
465
+ end
466
+ end
467
+
468
+ def require_git
469
+ unless @git_url && @git_repo && @git_login
470
+ puts "\norigin entry within '.git/config' not found!".red
471
+ puts "Are you sure you are in a git repository?\n".yellow
472
+ exit
473
+ end
474
+ end
475
+
476
+ def save_config
477
+ File.open(@config_path, 'w') do |f|
478
+ f.write(YAML.dump(@config))
479
+ end
480
+ end
481
+
482
+ def q(question, extra='')
483
+ puts "#{question.yellow}#{extra}"
484
+ $stdin.gets.strip
485
+ end
486
+
487
+ def run(cmd)
488
+ if ENV['RUN'] == '0'
489
+ puts cmd
490
+ else
491
+ `#{cmd}`
492
+ end
493
+ end
494
+
495
+ def yes?(question)
496
+ q(question, " (#{"y".green}/#{"n".red})").downcase[0..0] == 'y'
497
+ end
498
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gitcycle
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Winton Welsh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aruba
16
+ requirement: &70199852896960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70199852896960
25
+ - !ruby/object:Gem::Dependency
26
+ name: cucumber
27
+ requirement: &70199852895300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70199852895300
36
+ - !ruby/object:Gem::Dependency
37
+ name: lighthouse
38
+ requirement: &70199852894720 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70199852894720
47
+ - !ruby/object:Gem::Dependency
48
+ name: redis
49
+ requirement: &70199852894100 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70199852894100
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec
60
+ requirement: &70199852893240 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70199852893240
69
+ - !ruby/object:Gem::Dependency
70
+ name: yajl-ruby
71
+ requirement: &70199852892360 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70199852892360
80
+ - !ruby/object:Gem::Dependency
81
+ name: launchy
82
+ requirement: &70199852879100 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - =
86
+ - !ruby/object:Gem::Version
87
+ version: 2.0.5
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *70199852879100
91
+ - !ruby/object:Gem::Dependency
92
+ name: yajl-ruby
93
+ requirement: &70199852877580 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - =
97
+ - !ruby/object:Gem::Version
98
+ version: 1.1.0
99
+ type: :runtime
100
+ prerelease: false
101
+ version_requirements: *70199852877580
102
+ description: Tame your development cycle.
103
+ email:
104
+ - mail@wintoni.us
105
+ executables:
106
+ - gitc
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - .gitignore
111
+ - Gemfile
112
+ - LICENSE
113
+ - README.md
114
+ - Rakefile
115
+ - bin/gitc
116
+ - features/config.example.yml
117
+ - features/gitcycle.feature
118
+ - features/steps/gitcycle_steps.rb
119
+ - gitcycle.gemspec
120
+ - lib/ext/string.rb
121
+ - lib/gitcycle.rb
122
+ homepage: https://github.com/winton/gitcycle
123
+ licenses: []
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ! '>='
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 1.8.10
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: Tame your development cycle
146
+ test_files:
147
+ - features/config.example.yml
148
+ - features/gitcycle.feature
149
+ - features/steps/gitcycle_steps.rb