autowow 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,78 +0,0 @@
1
- require 'open3'
2
-
3
- module Autowow
4
- class Command
5
-
6
- include EasyLogging
7
-
8
- def self.run(*args)
9
- Command.new(*args).check.explain.chronic_execute
10
- end
11
-
12
- def self.run_dry(*args)
13
- Command.new(*args).silent_check.execute
14
- end
15
-
16
- def self.popen3_reader(*args)
17
- args.each do |arg|
18
- reader = <<-EOF
19
- def #{arg}
20
- @#{arg} = @#{arg}.read.rstrip unless @#{arg}.is_a?(String)
21
- return @#{arg}
22
- end
23
- EOF
24
- class_eval(reader)
25
- end
26
- end
27
-
28
- popen3_reader :stdin, :stdout, :stderr
29
- attr_reader :wait_thr
30
-
31
- def initialize(*args)
32
- @cmd = args
33
- end
34
-
35
- def explain
36
- logger.debug(@cmd.join(' ')) unless @cmd.empty?
37
- self
38
- end
39
-
40
- def execute
41
- @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(*@cmd) unless @cmd.empty?
42
- self
43
- end
44
-
45
- def chronic_execute
46
- @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(*@cmd) unless @cmd.empty?
47
- logger.error(stderr) unless stderr.empty?
48
- self
49
- end
50
-
51
- def silent_check
52
- @stdin, @stdout, @stderr, @wait_thr = Open3.popen3('which', @cmd[0])
53
- unless output_matches?(not_empty_matcher)
54
- yield if block_given?
55
- @cmd = []
56
- end
57
- self
58
- end
59
-
60
- def check
61
- silent_check do
62
- logger.info("Skipping '#{@cmd.join(' ')}' because command '#{@cmd[0]}' is not found.")
63
- end
64
- end
65
-
66
- def output_matches?(matcher)
67
- stdout.match(matcher)
68
- end
69
-
70
- def output_does_not_match?(matcher)
71
- !output_matches?(matcher)
72
- end
73
-
74
- def not_empty_matcher
75
- %r{.*\S.*}
76
- end
77
- end
78
- end
data/lib/autowow/fs.rb DELETED
@@ -1,41 +0,0 @@
1
- require_relative 'time_difference'
2
-
3
- module Autowow
4
- class Fs
5
- using RefinedTimeDifference
6
-
7
- def self.ls_dirs
8
- Dir.glob(File.expand_path('./*/')).select {|f| File.directory? f}
9
- end
10
-
11
- def self.latest(files)
12
- files.sort_by{ |f| File.mtime(f) }.reverse!.first
13
- end
14
-
15
- def self.older_than(files, quantity, unit)
16
- files.select do |dir|
17
- TimeDifference.between(File.mtime(dir), Time.now).public_send("in_#{unit}") > quantity
18
- end
19
- end
20
-
21
- def self.for_dirs(dirs)
22
- dirs.each do |working_dir|
23
- # TODO: add handling of directories via extra param to popen3
24
- # https://stackoverflow.com/a/10148084/2771889
25
- Dir.chdir(working_dir) do
26
- yield working_dir
27
- end
28
- end
29
- end
30
-
31
- def self.in_place_or_subdirs(in_place)
32
- if in_place
33
- yield
34
- else
35
- for_dirs(ls_dirs) do
36
- yield
37
- end
38
- end
39
- end
40
- end
41
- end
data/lib/autowow/gem.rb DELETED
@@ -1,32 +0,0 @@
1
- require_relative 'vcs'
2
- require_relative 'command'
3
-
4
- module Autowow
5
- class Gem
6
- include EasyLogging
7
-
8
- def self.gem_release
9
- start_status = Vcs.status
10
- logger.info(start_status)
11
- working_branch = Vcs.current_branch
12
- logger.error("Not on master.") and return unless working_branch.eql?('master')
13
- Vcs.push
14
-
15
- Vcs.on_branch('release') do
16
- Vcs.pull
17
- Vcs.rebase(working_branch)
18
- release
19
- end
20
-
21
- logger.info(Vcs.status)
22
- end
23
-
24
- def self.release
25
- Command.run('rake', 'release')
26
- end
27
-
28
- def self.clean
29
- Command.run('gem', 'clean')
30
- end
31
- end
32
- end
data/lib/autowow/ruby.rb DELETED
@@ -1,38 +0,0 @@
1
- module Autowow
2
- class Ruby
3
- include EasyLogging
4
-
5
- def self.used_versions
6
- rubies = []
7
- Fs.in_place_or_subdirs(Vcs.is_git?(Vcs.status_dry)) do
8
- rubies.push(version)
9
- end
10
- rubies.uniq
11
- end
12
-
13
- def self.version
14
- Command.run_dry('rbenv', 'local').stdout
15
- end
16
-
17
- def self.installed_versions
18
- Command.run_dry('rbenv', 'versions', '--bare', '--skip-aliases').stdout.each_line.map(&:strip)
19
- end
20
-
21
- def self.aliases
22
- aliases = {}
23
- Command.run_dry('rbenv', 'alias').stdout.each_line do |line|
24
- aliases[line.strip.split(' => ')[0]] = line.strip.split(' => ')[1]
25
- end
26
- aliases
27
- end
28
-
29
- def self.obsolete_versions
30
- alias_map = aliases
31
- used_versions_and_aliases = used_versions
32
- used_versions.each do |v|
33
- used_versions_and_aliases.push(alias_map[v]) if alias_map.has_key?(v)
34
- end
35
- installed_versions - used_versions_and_aliases
36
- end
37
- end
38
- end
data/lib/autowow/vcs.rb DELETED
@@ -1,304 +0,0 @@
1
- require 'uri'
2
- require 'net/https'
3
- require 'net/http'
4
- require 'json'
5
- require 'launchy'
6
-
7
- require_relative 'command'
8
- require_relative 'decorators/string_decorator'
9
- require_relative 'fs'
10
- require_relative 'time_difference'
11
- require_relative 'gem'
12
- require_relative 'ruby'
13
-
14
- module Autowow
15
- class Vcs
16
- include EasyLogging
17
- include StringDecorator
18
-
19
- using RefinedTimeDifference
20
-
21
- def self.branch_merged
22
- start_status = status
23
- logger.info(start_status)
24
- working_branch = current_branch
25
- logger.error("Nothing to do.") and return if working_branch.eql?('master')
26
-
27
- keep_changes do
28
- checkout('master')
29
- pull
30
- end
31
- branch_force_delete(working_branch)
32
-
33
- logger.info(status)
34
- end
35
-
36
- def self.update_projects
37
- Fs.in_place_or_subdirs(is_git?(status_dry)) do
38
- update_project
39
- end
40
- end
41
-
42
- def self.update_project
43
- start_status = status_dry
44
- return unless is_git?(start_status)
45
- logger.info("Updating #{File.expand_path('.')} ...")
46
- logger.warn("Skipped: uncommitted changes on master.") and return if uncommitted_changes?(start_status) and current_branch.eql?('master')
47
-
48
- on_branch('master') do
49
- has_upstream?(remotes.stdout) ? pull_upstream : pull
50
- end
51
- end
52
-
53
- def self.clear_branches
54
- logger.info(branch.stdout)
55
- working_branch = current_branch
56
- master_branch = 'master'
57
-
58
- (branches - [master_branch, working_branch]).each do |branch|
59
- branch_force_delete(branch) if branch_pushed(branch)
60
- end
61
-
62
- logger.info(branch.stdout)
63
- end
64
-
65
- def self.add_upstream
66
- start_status = status_dry
67
- logger.error("Not a git repository.") and return unless is_git?(start_status)
68
- remote_list = remotes.stdout
69
- logger.warn("Already has upstream.") and return if has_upstream?(remote_list)
70
- logger.info(remote_list)
71
-
72
- url = URI.parse(origin_push_url(remote_list))
73
- host = "api.#{url.host}"
74
- path = "/repos#{url.path}"
75
- request = Net::HTTP.new(host, url.port)
76
- request.verify_mode = OpenSSL::SSL::VERIFY_NONE
77
- request.use_ssl = url.scheme == 'https'
78
- response = request.get(path)
79
-
80
- if response.kind_of?(Net::HTTPRedirection)
81
- logger.error('Repository moved / renamed. Update remote or implement redirect handling. :)')
82
- elsif response.kind_of?(Net::HTTPNotFound)
83
- logger.error('Repository not found. Maybe it is private.')
84
- elsif response.kind_of?(Net::HTTPSuccess)
85
- parsed_response = JSON.parse(response.body)
86
- logger.warn('Not a fork.') and return unless parsed_response['fork']
87
- parent_url = parsed_response.dig('parent', 'html_url')
88
- add_remote('upstream', parent_url) unless parent_url.to_s.empty?
89
- logger.info(remotes.stdout)
90
- else
91
- logger.error("Github API (#{url.scheme}://#{host}#{path}) could not be reached: #{response.body}")
92
- end
93
- end
94
-
95
- def self.greet(latest_project_info = nil)
96
- logger.info("\nGood morning!\n\n")
97
- if is_git?(status_dry)
98
- logger.error('Inside repo, cannot show report about all repos.')
99
- else
100
- latest_project_info ||= get_latest_project_info
101
- logger.info(latest_project_info)
102
- check_projects_older_than(1, :months)
103
- end
104
- logger.info("\nThe following Ruby versions are not used by any projects, maybe consider removing them?\n #{Ruby.obsolete_versions.join("\n ")}")
105
- end
106
-
107
- def self.hi
108
- logger.error("In a git repository. Try 1 level higher.") && return if is_git?(status_dry)
109
- latest_project_info = get_latest_project_info
110
- logger.info("\nHang on, updating your local projects and remote forks...\n\n")
111
- git_projects.each do |project|
112
- Dir.chdir(project) do
113
- logger.info("\nGetting #{project} in shape...")
114
- yield if block_given?
115
- update_project
116
- end
117
- end
118
- greet(latest_project_info)
119
- end
120
-
121
- def self.hi!
122
- logger.error("In a git repository. Try 1 level higher.") && return if is_git?(status_dry)
123
- hi do
124
- logger.info('Removing unused branches...')
125
- clear_branches
126
- logger.info('Adding upstream...')
127
- add_upstream
128
- logger.info('Removing unused gems...')
129
- logger.info(Gem.clean.stdout)
130
- end
131
- end
132
-
133
- def self.open
134
- Launchy.open(origin_push_url(remotes.stdout))
135
- end
136
-
137
- def self.get_latest_project_info
138
- latest = latest_repo
139
- time_diff = TimeDifference.between(File.mtime(latest), Time.now).humanize_higher_than(:days).downcase
140
- time_diff_text = time_diff.empty? ? 'recently' : "#{time_diff} ago"
141
- "It looks like you were working on #{File.basename(latest)} #{time_diff_text}.\n\n"
142
- end
143
-
144
- def self.latest_repo
145
- Fs.latest(git_projects)
146
- end
147
-
148
- def self.check_projects_older_than(quantity, unit)
149
- old_projects = Fs.older_than(git_projects, quantity, unit)
150
- deprecated_projects = old_projects.reject do |project|
151
- Dir.chdir(project) { branches.reject{ |branch| branch_pushed(branch) }.any? }
152
- end
153
-
154
- logger.info("The following projects have not been touched for more than #{quantity} #{unit} and all changes have been pushed, maybe consider removing them?") unless deprecated_projects.empty?
155
- deprecated_projects.each do |project|
156
- time_diff = TimeDifference.between(File.mtime(project), Time.now).humanize_higher_than(:weeks).downcase
157
- logger.info(" #{File.basename(project)} (#{time_diff})")
158
- end
159
- end
160
-
161
- def self.stash
162
- Command.run('git', 'stash').output_does_not_match?(%r{No local changes to save})
163
- end
164
-
165
- def self.current_branch
166
- Command.run_dry('git', 'symbolic-ref', '--short', 'HEAD').stdout
167
- end
168
-
169
- def self.status
170
- status = Command.run('git', 'status')
171
- status.stdout + status.stderr
172
- end
173
-
174
- def self.status_dry
175
- status = Command.run_dry('git', 'status')
176
- status.stdout + status.stderr
177
- end
178
-
179
- def self.checkout(existing_branch)
180
- Command.run('git', 'checkout', existing_branch)
181
- end
182
-
183
- def self.create(branch)
184
- Command.run('git', 'checkout', '-b', branch)
185
- Command.run('git', 'push', '--set-upstream', 'origin', branch)
186
- end
187
-
188
- def self.pull
189
- Command.run('git', 'pull')
190
- end
191
-
192
- def self.pull_upstream
193
- Command.run('git', 'fetch', 'upstream')
194
- Command.run('git', 'merge', 'upstream/master')
195
- Command.run('git', 'push', 'origin', 'master')
196
- end
197
-
198
- def self.branch
199
- Command.run('git', 'branch')
200
- end
201
-
202
- def self.stash_pop
203
- Command.run('git', 'stash', 'pop')
204
- end
205
-
206
- def self.branch_force_delete(branch)
207
- Command.run('git', 'branch', '-D', branch)
208
- end
209
-
210
- def self.remotes
211
- Command.run_dry('git', 'remote', '-v')
212
- end
213
-
214
- def self.has_upstream?(remotes)
215
- remotes.include?('upstream')
216
- end
217
-
218
- def self.uncommitted_changes?(start_status)
219
- !(start_status.include?('nothing to commit, working tree clean') or start_status.include?('nothing added to commit but untracked files present'))
220
- end
221
-
222
- def self.is_git?(start_status)
223
- !start_status.include?('Not a git repository')
224
- end
225
-
226
- def self.origin_push_url(remotes)
227
- # Order is important: first try to match "url" in "#{url}.git" as non-dot_git matchers would include ".git" in the match
228
- origin_push_url_ssl_dot_git(remotes) or
229
- origin_push_url_ssl(remotes)or
230
- origin_push_url_https_dot_git(remotes) or
231
- origin_push_url_https(remotes)
232
- end
233
-
234
- def self.origin_push_url_https(remotes)
235
- remotes[%r{(?<=origin(\s))http(s?)://[a-zA-Z\-_./]*(?=(\s)\(push\))}]
236
- end
237
-
238
- def self.origin_push_url_https_dot_git(remotes)
239
- remotes[%r{(?<=origin(\s))http(s?)://[a-zA-Z\-_./]*(?=(\.)git(\s)\(push\))}]
240
- end
241
-
242
- def self.origin_push_url_ssl_dot_git(remotes)
243
- url = remotes[%r{(?<=origin(\s)git@)[a-zA-Z\-_./:]*(?=(\.)git(\s)\(push\))}]
244
- "https://#{url.gsub(':', '/')}" if url
245
- end
246
-
247
- def self.origin_push_url_ssl(remotes)
248
- url = remotes[%r{(?<=origin(\s)git@)[a-zA-Z\-_./:]*(?=(\s)\(push\))}]
249
- "https://#{url.gsub(':', '/')}" if url
250
- end
251
-
252
- def self.add_remote(name, url)
253
- Command.run('git', 'remote', 'add', name, url)
254
- end
255
-
256
- def self.on_branch(branch)
257
- keep_changes do
258
- working_branch = current_branch
259
- switch_needed = !working_branch.eql?(branch)
260
- if switch_needed
261
- result = checkout(branch)
262
- create(branch) if result.stderr.eql?("error: pathspec '#{branch}' did not match any file(s) known to git.")
263
- end
264
-
265
- yield
266
-
267
- checkout(working_branch) if switch_needed
268
- end
269
- end
270
-
271
- def self.keep_changes
272
- status = status_dry
273
- pop_stash = uncommitted_changes?(status)
274
- stash if pop_stash
275
- yield
276
- stash_pop if pop_stash
277
- end
278
-
279
- def self.git_projects
280
- Fs.ls_dirs.select do |dir|
281
- Dir.chdir(dir) do
282
- is_git?(status_dry)
283
- end
284
- end
285
- end
286
-
287
- def self.branch_pushed(branch)
288
- Command.run_dry('git', 'log', branch, '--not', '--remotes').stdout.empty?
289
- end
290
-
291
- def self.branches
292
- branches = Command.run_dry('git', 'for-each-ref', "--format='%(refname)'", 'refs/heads/').stdout
293
- branches.each_line.map { |line| line.strip[%r{(?<='refs/heads/)(.*)(?=')}] }
294
- end
295
-
296
- def self.push
297
- Command.run('git', 'push')
298
- end
299
-
300
- def self.rebase(branch)
301
- Command.run('git', 'rebase', branch)
302
- end
303
- end
304
- end