git-process 0.9.1.pre3

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.
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