gitcycle 0.1.0

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