git-process 0.9.1.pre3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +16 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile +22 -0
  5. data/Gemfile.lock +56 -0
  6. data/LICENSE +22 -0
  7. data/README.md +80 -0
  8. data/Rakefile +16 -0
  9. data/bin/git-new-fb +21 -0
  10. data/bin/git-pull-request +21 -0
  11. data/bin/git-sync +21 -0
  12. data/bin/git-to-master +21 -0
  13. data/git-process.gemspec +21 -0
  14. data/lib/git-process/abstract-error-builder.rb +46 -0
  15. data/lib/git-process/git-abstract-merge-error-builder.rb +115 -0
  16. data/lib/git-process/git-branch.rb +86 -0
  17. data/lib/git-process/git-branches.rb +53 -0
  18. data/lib/git-process/git-lib.rb +413 -0
  19. data/lib/git-process/git-merge-error.rb +31 -0
  20. data/lib/git-process/git-new-fb-options.rb +34 -0
  21. data/lib/git-process/git-process-error.rb +10 -0
  22. data/lib/git-process/git-process-options.rb +82 -0
  23. data/lib/git-process/git-process.rb +194 -0
  24. data/lib/git-process/git-pull-request-options.rb +42 -0
  25. data/lib/git-process/git-rebase-error.rb +31 -0
  26. data/lib/git-process/git-status.rb +72 -0
  27. data/lib/git-process/git-sync-options.rb +34 -0
  28. data/lib/git-process/git-to-master-options.rb +18 -0
  29. data/lib/git-process/github-client.rb +73 -0
  30. data/lib/git-process/github-service.rb +156 -0
  31. data/lib/git-process/parked-changes-error.rb +32 -0
  32. data/lib/git-process/pull-request.rb +38 -0
  33. data/lib/git-process/uncommitted-changes-error.rb +15 -0
  34. data/lib/git-process/version.rb +12 -0
  35. data/spec/FileHelpers.rb +18 -0
  36. data/spec/GitRepoHelper.rb +86 -0
  37. data/spec/git-abstract-merge-error-builder_spec.rb +113 -0
  38. data/spec/git-lib_spec.rb +118 -0
  39. data/spec/git-process_spec.rb +328 -0
  40. data/spec/git-status_spec.rb +101 -0
  41. data/spec/github-service_spec.rb +209 -0
  42. data/spec/pull-request_spec.rb +57 -0
  43. data/spec/spec_helper.rb +1 -0
  44. metadata +133 -0
@@ -0,0 +1,194 @@
1
+ require 'git-lib'
2
+ require 'uncommitted-changes-error'
3
+ require 'git-rebase-error'
4
+ require 'git-merge-error'
5
+ require 'parked-changes-error'
6
+ require 'pull-request'
7
+ require 'shellwords'
8
+ require 'highline/import'
9
+
10
+
11
+ module Git
12
+
13
+ class Process
14
+ attr_reader :lib
15
+
16
+ @@server_name = 'origin'
17
+ @@master_branch = 'master'
18
+
19
+ def initialize(dir = nil, gitlib = nil, options = {})
20
+ @lib = gitlib || Git::GitLib.new(dir, options)
21
+ end
22
+
23
+
24
+ def Process.remote_master_branch
25
+ "#{@@server_name}/#{@@master_branch}"
26
+ end
27
+
28
+
29
+ def Process.server_name
30
+ @@server_name
31
+ end
32
+
33
+
34
+ def Process.master_branch
35
+ @@master_branch
36
+ end
37
+
38
+
39
+ def rebase_to_master
40
+ raise UncommittedChangesError.new unless lib.status.clean?
41
+ raise ParkedChangesError.new(lib) if is_parked?
42
+
43
+ if lib.has_a_remote?
44
+ lib.fetch
45
+ rebase(Process::remote_master_branch)
46
+ lib.push(Process::server_name, lib.branches.current, Process::master_branch)
47
+ remove_feature_branch
48
+ else
49
+ rebase("master")
50
+ end
51
+ end
52
+
53
+
54
+ def sync_with_server(rebase, force)
55
+ raise UncommittedChangesError.new unless lib.status.clean?
56
+ raise ParkedChangesError.new(lib) if is_parked?
57
+
58
+ current_branch = lib.branches.current
59
+ remote_branch = "#{Process::server_name}/#{current_branch}"
60
+
61
+ lib.fetch
62
+
63
+ if rebase
64
+ rebase(Process::remote_master_branch)
65
+ else
66
+ merge(Process::remote_master_branch)
67
+ end
68
+
69
+ old_sha = lib.command('rev-parse', remote_branch) rescue ''
70
+
71
+ unless current_branch == Process::master_branch
72
+ lib.fetch
73
+ new_sha = lib.command('rev-parse', remote_branch) rescue ''
74
+ unless old_sha == new_sha
75
+ logger.warn("'#{current_branch}' changed on '#{Process::server_name}'"+
76
+ " [#{old_sha[0..5]}->#{new_sha[0..5]}]; trying sync again.")
77
+ sync_with_server(rebase, force)
78
+ end
79
+ lib.push(Process::server_name, current_branch, current_branch, :force => rebase || force)
80
+ else
81
+ logger.warn("Not pushing to the server because the current branch is the master branch.")
82
+ end
83
+ end
84
+
85
+
86
+ def new_feature_branch(branch_name)
87
+ branches = lib.branches
88
+ on_parking = (branches.parking == branches.current)
89
+
90
+ if on_parking
91
+ new_branch = lib.checkout(branch_name, :new_branch => '_parking_')
92
+ branches.parking.delete
93
+ new_branch
94
+ else
95
+ lib.checkout(branch_name, :new_branch => 'origin/master')
96
+ end
97
+ end
98
+
99
+
100
+ def bad_parking_branch_msg
101
+ hl = HighLine.new
102
+ hl.color("\n***********************************************************************************************\n\n"+
103
+ "There is an old '_parking_' branch with unacounted changes in it.\n"+
104
+ "It has been renamed to '_parking_OLD_'.\n"+
105
+ "Please rename the branch to what the changes are about (`git branch -m _parking_OLD_ my_fb_name`),\n"+
106
+ " or remove it altogher (`git branch -D _parking_OLD_`).\n\n"+
107
+ "***********************************************************************************************\n", :red, :bold)
108
+ end
109
+
110
+
111
+ def remove_feature_branch
112
+ branches = lib.branches
113
+
114
+ remote_master = branches[Process::remote_master_branch]
115
+ current_branch = branches.current
116
+
117
+ unless remote_master.contains_all_of(current_branch.name)
118
+ raise GitProcessError.new("Branch '#{current_branch.name}' has not been merged into '#{Process::remote_master_branch}'")
119
+ end
120
+
121
+ parking_branch = branches['_parking_']
122
+ if parking_branch
123
+ if (parking_branch.is_ahead_of(remote_master.name) and
124
+ !current_branch.contains_all_of(parking_branch.name))
125
+
126
+ parking_branch.rename('_parking_OLD_')
127
+
128
+ logger.warn {bad_parking_branch_msg}
129
+ else
130
+ parking_branch.delete
131
+ end
132
+ end
133
+ remote_master.checkout_to_new('_parking_', :no_track => true)
134
+
135
+ current_branch.delete(true)
136
+ if branches["#{Process.server_name}/#{current_branch.name}"]
137
+ lib.push(Process.server_name, nil, nil, :delete => current_branch.name)
138
+ end
139
+ end
140
+
141
+
142
+ def is_parked?
143
+ branches = lib.branches
144
+ branches.parking == branches.current
145
+ end
146
+
147
+
148
+ def rebase(base)
149
+ begin
150
+ lib.rebase(base)
151
+ rescue Git::GitExecuteError => rebase_error
152
+ raise RebaseError.new(rebase_error.message, lib)
153
+ end
154
+ end
155
+
156
+
157
+ def merge(base)
158
+ begin
159
+ lib.merge(base)
160
+ rescue Git::GitExecuteError => merge_error
161
+ raise MergeError.new(merge_error.message, lib)
162
+ end
163
+ end
164
+
165
+
166
+ def pull_request(repo_name, base, head, title, body, opts = {})
167
+ repo_name ||= lib.repo_name
168
+ base ||= @@master_branch
169
+ head ||= lib.branches.current
170
+ title ||= ask_for_pull_title
171
+ body ||= ask_for_pull_body
172
+ GitHub::PullRequest.new(lib, repo_name, opts).pull_request(base, head, title, body)
173
+ end
174
+
175
+
176
+ def ask_for_pull_title
177
+ ask("What <%= color('title', [:bold]) %> do you want to give the pull request? ") do |q|
178
+ q.validate = /^\w+.*/
179
+ end
180
+ end
181
+
182
+
183
+ def ask_for_pull_body
184
+ ask("What <%= color('description', [:bold]) %> do you want to give the pull request? ")
185
+ end
186
+
187
+
188
+ def logger
189
+ @lib.logger
190
+ end
191
+
192
+ end
193
+
194
+ end
@@ -0,0 +1,42 @@
1
+ require 'git-process-options'
2
+
3
+ module Git
4
+
5
+ class Process
6
+
7
+ class PullRequestOptions
8
+ include GitProcessOptions
9
+
10
+ attr_reader :user, :password, :description, :title, :filename
11
+
12
+ def initialize(filename, argv)
13
+ @filename = filename
14
+ parse(filename, argv)
15
+ end
16
+
17
+ def extend_opts(opts)
18
+ opts.banner = "Usage: #{filename} [ options ] [pull_request_title]"
19
+
20
+ opts.on("-u", "--user name", String, "GitHub account username") do |user|
21
+ @user = user
22
+ end
23
+
24
+ opts.on("-p", "--password pw", String, "GitHub account password") do |password|
25
+ @password = password
26
+ end
27
+
28
+ opts.on(nil, "--desc description", String, "Description of the changes.") do |desc|
29
+ @description = desc
30
+ end
31
+ end
32
+
33
+
34
+ def extend_args(argv)
35
+ @title = argv.pop unless argv.empty?
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,31 @@
1
+ require 'git-process-error'
2
+ require 'git-abstract-merge-error-builder'
3
+
4
+ module Git
5
+
6
+ class Process
7
+
8
+ class RebaseError < GitProcessError
9
+ include Git::AbstractMergeErrorBuilder
10
+
11
+ attr_reader :error_message, :lib
12
+
13
+ def initialize(rebase_error_message, lib)
14
+ @lib = lib
15
+ @error_message = rebase_error_message
16
+
17
+ msg = build_message
18
+
19
+ super(msg)
20
+ end
21
+
22
+
23
+ def continue_command
24
+ 'git rebase --continue'
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,72 @@
1
+ module Git
2
+
3
+ #
4
+ # The status of the Git repository.
5
+ #
6
+ # @!attribute [r] unmerged
7
+ # @return [Enumerable] a sorted list of unmerged files
8
+ # @!attribute [r] modified
9
+ # @return [Enumerable] a sorted list of modified files
10
+ # @!attribute [r] deleted
11
+ # @return [Enumerable] a sorted list of deleted files
12
+ # @!attribute [r] added
13
+ # @return [Enumerable] a sorted list of files that have been added
14
+ # @!attribute [r] unknown
15
+ # @return [Enumerable] a sorted list of unknown files
16
+ class GitStatus
17
+ attr_reader :unmerged, :modified, :deleted, :added, :unknown
18
+
19
+ def initialize(lib)
20
+ unmerged = []
21
+ modified = []
22
+ deleted = []
23
+ added = []
24
+ unknown = []
25
+
26
+ stats = lib.porcelain_status.split("\n")
27
+
28
+ stats.each do |s|
29
+ stat = s[0..1]
30
+ file = s[3..-1]
31
+ #puts "stat #{stat} - #{file}"
32
+ case stat
33
+ when 'U ', ' U'
34
+ unmerged << file
35
+ when 'UU'
36
+ unmerged << file
37
+ modified << file
38
+ when 'M ', ' M'
39
+ modified << file
40
+ when 'D ', ' D'
41
+ deleted << file
42
+ when 'DU', 'UD'
43
+ deleted << file
44
+ unmerged << file
45
+ when 'A ', ' A'
46
+ added << file
47
+ when 'AA'
48
+ added << file
49
+ unmerged << file
50
+ when '??'
51
+ unknown << file
52
+ else
53
+ raise "Do not know what to do with status #{stat} - #{file}"
54
+ end
55
+ end
56
+
57
+ @unmerged = unmerged.sort.uniq.freeze
58
+ @modified = modified.sort.uniq.freeze
59
+ @deleted = deleted.sort.uniq.freeze
60
+ @added = added.sort.uniq.freeze
61
+ @unknown = unknown.sort.uniq.freeze
62
+ end
63
+
64
+
65
+ # @return [Boolean] are there any changes in the index or working directory?
66
+ def clean?
67
+ @unmerged.empty? and @modified.empty? and @deleted.empty? and @added.empty? and @unknown.empty?
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,34 @@
1
+ require 'optparse'
2
+ require 'git-process-options'
3
+
4
+ module Git
5
+
6
+ class Process
7
+
8
+ class SyncOptions
9
+ include GitProcessOptions
10
+
11
+ attr_reader :rebase, :force
12
+
13
+
14
+ def initialize(filename, argv)
15
+ @rebase = false
16
+ @force = false
17
+ parse(filename, argv)
18
+ end
19
+
20
+
21
+ def extend_opts(opts)
22
+ opts.on("-r", "--rebase", "Rebase instead of merge") do |v|
23
+ @rebase = true
24
+ end
25
+
26
+ opts.on("-f", "--force", "Force the push") do |v|
27
+ @force = true
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,18 @@
1
+ require 'optparse'
2
+ require 'git-process-options'
3
+
4
+ module Git
5
+
6
+ class Process
7
+
8
+ class ToMasterOptions
9
+ include GitProcessOptions
10
+
11
+ def initialize(filename, argv)
12
+ parse(filename, argv)
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,73 @@
1
+ require 'octokit'
2
+
3
+ module Octokit
4
+
5
+ module Connection
6
+
7
+ #
8
+ # Unfortunately, there's no way to change the URL except by completely replacing
9
+ # this method.
10
+ #
11
+ def connection(authenticate=true, raw=false, version=3, force_urlencoded=false)
12
+ if site
13
+ url = site
14
+ else
15
+ case version
16
+ when 2
17
+ url = "https://github.com"
18
+ when 3
19
+ url = "https://api.github.com"
20
+ end
21
+ end
22
+
23
+ options = {
24
+ :proxy => proxy,
25
+ :ssl => { :verify => false },
26
+ :url => url,
27
+ }
28
+
29
+ options.merge!(:params => {:access_token => oauth_token}) if oauthed? && !authenticated?
30
+
31
+ connection = Faraday.new(options) do |builder|
32
+ if version >= 3 && !force_urlencoded
33
+ builder.request :json
34
+ else
35
+ builder.request :url_encoded
36
+ end
37
+ builder.use Faraday::Response::RaiseOctokitError
38
+ unless raw
39
+ builder.use FaradayMiddleware::Mashify
40
+ builder.use FaradayMiddleware::ParseJson
41
+ end
42
+ builder.adapter(adapter)
43
+ end
44
+ connection.basic_auth authentication[:login], authentication[:password] if authenticate and authenticated?
45
+ connection
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+
52
+ class GitHubClient < Octokit::Client
53
+
54
+ def site
55
+ @site
56
+ end
57
+
58
+
59
+ def site=(new_site)
60
+ @site = new_site
61
+ end
62
+
63
+
64
+ alias :old_request :request
65
+
66
+ def request(method, path, options, version, authenticate, raw, force_urlencoded)
67
+ if /api.github.com/ !~ site
68
+ path = "/api/v3#{path}"
69
+ end
70
+ old_request(method, path, options, version, authenticate, raw, force_urlencoded)
71
+ end
72
+
73
+ end
@@ -0,0 +1,156 @@
1
+ require 'highline/import'
2
+ require 'github-client'
3
+ require 'uri'
4
+
5
+
6
+ module GitHubService
7
+
8
+ def client
9
+ unless @client
10
+ auth_token
11
+ logger.debug { "Creating GitHub client for user #{user} using token '#{auth_token}'" }
12
+ @client = GitHubClient.new(:login => user, :oauth_token=> auth_token)
13
+ @client.site = site
14
+ end
15
+ @client
16
+ end
17
+
18
+
19
+ def site(opts = {})
20
+ @site ||= compute_site(opts)
21
+ end
22
+
23
+
24
+ def compute_site(opts = {})
25
+ origin_url = lib.config('remote.origin.url')
26
+
27
+ raise GitHubService::NoRemoteRepository.new("There is no value set for 'remote.origin.url'") if origin_url.empty?
28
+
29
+ if /^git\@/ =~ origin_url
30
+ host = origin_url.sub(/^git\@(.*?):.*$/, '\1')
31
+ site = host_to_site(host, false)
32
+ else
33
+ uri = URI.parse(origin_url)
34
+ host = uri.host
35
+ scheme = uri.scheme
36
+
37
+ raise URI::InvalidURIError.new("Need a scheme in URI: '#{origin_url}'") unless scheme
38
+
39
+ unless host
40
+ # assume that the 'scheme' is the named configuration in ~/.ssh/config
41
+ host = hostname_from_ssh_config(scheme, opts[:ssh_config_file] ||= "#{ENV['HOME']}/.ssh/config")
42
+ end
43
+
44
+ site = host_to_site(host, scheme == 'https')
45
+ end
46
+ site
47
+ end
48
+
49
+
50
+ def hostname_from_ssh_config(host_alias, config_file)
51
+ config_lines = File.new(config_file).readlines
52
+
53
+ in_host_section = false
54
+ host_name = nil
55
+
56
+ sections = config_lines.each do |line|
57
+ line.chop!
58
+ if /^\s*Host\s+#{host_alias}\s*$/ =~ line
59
+ in_host_section = true
60
+ next
61
+ end
62
+ if in_host_section and (/^\s*HostName\s+\S+\s*$/ =~ line)
63
+ host_name = line.sub(/^\s*HostName\s+(\S+)\s*$/, '\1')
64
+ break
65
+ end
66
+ end
67
+ host_name
68
+ end
69
+
70
+
71
+ def host_to_site(host, ssl)
72
+ if /github.com$/ =~ host
73
+ 'https://api.github.com'
74
+ else
75
+ "http#{ssl ? 's' : ''}://#{host}"
76
+ end
77
+ end
78
+
79
+
80
+ private :host_to_site, :compute_site
81
+
82
+
83
+ def pw_client
84
+ unless @pw_client
85
+ logger.debug { "Creating GitHub client for user #{user} using password #{password}" }
86
+ @pw_client = GitHubClient.new(:login => user, :password => password)
87
+ @pw_client.site = site
88
+ end
89
+ @pw_client
90
+ end
91
+
92
+
93
+ def user
94
+ unless @user
95
+ user = lib.config('github.user')
96
+ if user.empty?
97
+ user = ask("Your <%= color('GitHub', [:bold, :blue]) %> username: ") do |q|
98
+ q.validate = /^\w\w+$/
99
+ end
100
+ lib.config('github.user', user)
101
+ end
102
+ @user = user
103
+ end
104
+ @user
105
+ end
106
+
107
+
108
+ def password
109
+ unless @password
110
+ @password = ask("Your <%= color('GitHub', [:bold, :blue]) %> password: ") do |q|
111
+ q.validate = /^\w\w+$/
112
+ q.echo = 'x'
113
+ end
114
+ end
115
+ @password
116
+ end
117
+
118
+
119
+ def auth_token
120
+ @auth_token ||= config_auth_token || create_authorization
121
+ end
122
+
123
+
124
+ def create_authorization
125
+ logger.info("Authorizing this to work with your repos.")
126
+ auth = pw_client.create_authorization(:scopes => ['repo', 'user', 'gist'],
127
+ :note => 'Git-Process',
128
+ :note_url => 'http://jdigger.github.com/git-process')
129
+ config_auth_token = auth['token']
130
+ lib.config('gitProcess.github.authToken', config_auth_token)
131
+ config_auth_token
132
+ end
133
+
134
+
135
+ def config_auth_token
136
+ unless @auth_token
137
+ c_auth_token = lib.config('gitProcess.github.authToken')
138
+ @auth_token = c_auth_token.empty? ? nil : c_auth_token
139
+ end
140
+ @auth_token
141
+ end
142
+
143
+
144
+ def logger
145
+ @lib.logger
146
+ end
147
+
148
+
149
+ class GithubServiceError < StandardError
150
+ end
151
+
152
+
153
+ class NoRemoteRepository < GithubServiceError
154
+ end
155
+
156
+ end
@@ -0,0 +1,32 @@
1
+ require 'git-process-error'
2
+
3
+ module Git
4
+
5
+ class Process
6
+
7
+ class ParkedChangesError < GitProcessError
8
+ include Git::AbstractErrorBuilder
9
+
10
+ attr_reader :error_message, :lib
11
+
12
+ def initialize(lib)
13
+ @lib = lib
14
+ msg = build_message
15
+ super(msg)
16
+ end
17
+
18
+
19
+ def human_message
20
+ "You made your changes on the the '_parking_' branch instead of a feature branch.\n"+"Please rename the branch to be a feature branch."
21
+ end
22
+
23
+
24
+ def build_commands
25
+ ['git branch -m _parking_ my_feature_branch']
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,38 @@
1
+ require 'github-service'
2
+ require 'octokit'
3
+
4
+
5
+ module GitHub
6
+
7
+ class PullRequest
8
+ include GitHubService
9
+
10
+ attr_reader :lib, :repo
11
+
12
+ def initialize(lib, repo, opts = {})
13
+ @lib = lib
14
+ @repo = repo
15
+ @user = opts[:user]
16
+ @password = opts[:password]
17
+ end
18
+
19
+
20
+ def pull_requests
21
+ @pull_requests ||= client.pull_requests(repo)
22
+ end
23
+
24
+
25
+ def create(base, head, title, body)
26
+ logger.info { "Creating a pull request asking for '#{head}' to be merged into '#{base}' on #{repo}." }
27
+ begin
28
+ client.create_pull_request(repo, base, head, title, body)
29
+ rescue Octokit::UnprocessableEntity => exp
30
+ pull = pull_requests.find {|p| p[:head][:ref] == head and p[:base][:ref] == base}
31
+ logger.warn { "Pull request already exists. See #{pull[:html_url]}" }
32
+ pull
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'git-process-error'
2
+
3
+ module Git
4
+
5
+ class Process
6
+
7
+ class UncommittedChangesError < GitProcessError
8
+ def initialize()
9
+ super("There are uncommitted changes.\nPlease either commit your changes, or use 'git stash' to set them aside.")
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ end