git-multi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/git/hub.rb ADDED
@@ -0,0 +1,95 @@
1
+ require 'octokit'
2
+
3
+ begin
4
+ require 'net/http/persistent'
5
+ Octokit.middleware.swap(
6
+ Faraday::Adapter::NetHttp, # default Faraday adapter
7
+ Faraday::Adapter::NetHttpPersistent # experimental Faraday adapter
8
+ )
9
+ rescue LoadError
10
+ # NOOP - `Net::HTTP::Persistent` is optional, so
11
+ # if the gem isn't installed, then we run with the
12
+ # default `Net::HTTP` Faraday adapter; if however
13
+ # the gem is installed then we make Faraday use it
14
+ # to benefit from persistent HTTP connections
15
+ end
16
+
17
+ module Git
18
+ module Hub
19
+
20
+ module_function
21
+
22
+ def client
23
+ @client ||= Octokit::Client.new(
24
+ :access_token => Git::Multi::TOKEN,
25
+ :auto_paginate => true,
26
+ )
27
+ end
28
+
29
+ class << self
30
+ private :client
31
+ end
32
+
33
+ def connected?
34
+ @connected ||= begin
35
+ client.validate_credentials
36
+ true
37
+ rescue Faraday::ConnectionFailed
38
+ false
39
+ end
40
+ end
41
+
42
+ # FIXME update login as part of `--refresh`
43
+
44
+ def login
45
+ @login ||= begin
46
+ client.user.login
47
+ rescue Octokit::Unauthorized, Faraday::ConnectionFailed
48
+ nil
49
+ end
50
+ end
51
+
52
+ # FIXME update orgs as part of `--refresh`
53
+
54
+ def orgs
55
+ @orgs ||= begin
56
+ client.organizations.map(&:login)
57
+ rescue Octokit::Unauthorized, Faraday::ConnectionFailed
58
+ []
59
+ end
60
+ end
61
+
62
+ #
63
+ # https://developer.github.com/v3/repos/#list-user-repositories
64
+ #
65
+
66
+ @user_repositories = Hash.new { |repos, (user, type)|
67
+ repos[[user, type]] = begin
68
+ notify("Refreshing #{type} '#{user}' repositories from GitHub")
69
+ client.repositories(user, :type => type).
70
+ sort_by { |repo| repo[:name].downcase }
71
+ end
72
+ }
73
+
74
+ def user_repositories(user, type = :owner) # all, owner, member
75
+ @user_repositories[[user, type]]
76
+ end
77
+
78
+ #
79
+ # https://developer.github.com/v3/repos/#list-organization-repositories
80
+ #
81
+
82
+ @org_repositories = Hash.new { |repos, (org, type)|
83
+ repos[[org, type]] = begin
84
+ notify("Refreshing #{type} '#{org}' repositories from GitHub")
85
+ client.org_repositories(org, :type => type).
86
+ sort_by { |repo| repo[:name].downcase }
87
+ end
88
+ }
89
+
90
+ def org_repositories(org, type = :owner) # all, public, private, forks, sources, member
91
+ @org_repositories[[org, type]]
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,189 @@
1
+ module Git
2
+ module Multi
3
+ module Commands
4
+
5
+ module_function
6
+
7
+ def version
8
+ puts Git::Multi::LONG_VERSION
9
+ end
10
+
11
+ def check
12
+ Settings.user_status(Git::Multi::USER)
13
+ Settings.organization_status(Git::Multi::ORGANIZATIONS)
14
+ Settings.token_status(Git::Multi::TOKEN)
15
+ Settings.home_status(Git::Multi::HOME)
16
+ Settings.main_workarea_status(Git::Multi::WORKAREA)
17
+ Settings.user_workarea_status(Git::Multi::USER)
18
+ Settings.organization_workarea_status(Git::Multi::ORGANIZATIONS)
19
+ Settings.file_status(Git::Multi::REPOSITORIES)
20
+ end
21
+
22
+ def help
23
+ # instead of maintaining a list of valid query args in the help-
24
+ # file, we determine it at runtime... less is more, and all that
25
+ # TODO remove attributes we 'adorned' the repos with on line 95?
26
+ query_args = Git::Multi.repositories.sample.fields.sort.each_slice(3).map {
27
+ |foo, bar, qux| '%-20s %-20s %-20s' % [foo, bar, qux]
28
+ }
29
+ puts File.read(Git::Multi::MAN_PAGE) % {
30
+ :version => Git::Multi::VERSION,
31
+ :query_args => query_args.join("\n "),
32
+ }
33
+ end
34
+
35
+ def report
36
+ if (missing_repos = Git::Multi::missing_repositories).any?
37
+ notify(missing_repos.map(&:full_name), :subtitle => "#{missing_repos.count} missing repos")
38
+ end
39
+ end
40
+
41
+ def list
42
+ puts Git::Multi.repositories.map(&:full_name)
43
+ end
44
+
45
+ def archived
46
+ puts Git::Multi.archived_repositories.map(&:full_name)
47
+ end
48
+
49
+ def forked
50
+ puts Git::Multi.forked_repositories.map(&:full_name)
51
+ end
52
+
53
+ def private
54
+ puts Git::Multi.private_repositories.map(&:full_name)
55
+ end
56
+
57
+ def paths
58
+ puts Git::Multi.repositories.map(&:local_path)
59
+ end
60
+
61
+ def missing
62
+ puts Git::Multi.missing_repositories.map(&:full_name)
63
+ end
64
+
65
+ def excess
66
+ puts Git::Multi.excess_repositories.map(&:full_name)
67
+ end
68
+
69
+ def stale
70
+ puts Git::Multi.stale_repositories.map(&:full_name)
71
+ end
72
+
73
+ def spurious
74
+ puts Git::Multi.spurious_repositories.map(&:full_name)
75
+ end
76
+
77
+ def count
78
+ # https://developer.github.com/v3/repos/#list-user-repositories
79
+ user = Git::Multi::USER
80
+ %w{ all owner member }.each { |type|
81
+ puts ["#{user}/#{type}", Git::Hub.user_repositories(user, type).count].join("\t")
82
+ }
83
+ # https://developer.github.com/v3/repos/#list-organization-repositories
84
+ for org in Git::Multi::ORGANIZATIONS
85
+ %w{ all public private forks sources member }.each { |type|
86
+ puts ["#{org}/#{type}", Git::Hub.org_repositories(org, type).count].join("\t")
87
+ }
88
+ end
89
+ end
90
+
91
+ def refresh
92
+ Git::Multi.refresh_repositories
93
+ end
94
+
95
+ def json
96
+ puts Git::Multi.repositories.to_json
97
+ end
98
+
99
+ def clone
100
+ Git::Multi.missing_repositories.each do |repo|
101
+ FileUtils.mkdir_p repo.parent_dir
102
+ repo.just_do_it(
103
+ ->(project) {
104
+ notify "Cloning '#{repo.full_name}' repo into #{repo.parent_dir.parent}"
105
+ Kernel.system "git clone -q #{project.rels[:ssh].href.shellescape}"
106
+ },
107
+ ->(project) {
108
+ Kernel.system "git clone -q #{project.rels[:ssh].href.shellescape}"
109
+ },
110
+ :in_dir => :parent_dir
111
+ )
112
+ end
113
+ end
114
+
115
+ def query args = []
116
+ Git::Multi.repositories.each do |repo|
117
+ repo.just_do_it(
118
+ ->(project) {
119
+ args.each do |attribute|
120
+ puts "#{attribute}: #{project[attribute]}"
121
+ end
122
+ },
123
+ ->(project) {
124
+ print "#{project.full_name}: "
125
+ puts args.map { |attribute| project[attribute] }.join(' ')
126
+ },
127
+ )
128
+ end
129
+ end
130
+
131
+ def system args = []
132
+ args.map!(&:shellescape)
133
+ Git::Multi.cloned_repositories.each do |repo|
134
+ repo.just_do_it(
135
+ ->(project) {
136
+ Kernel.system "#{args.join(' ')}"
137
+ },
138
+ ->(project) {
139
+ Kernel.system "#{args.join(' ')} 2>&1 | sed -e 's#^##{project.full_name.shellescape}: #'"
140
+ },
141
+ :in_dir => :local_path
142
+ )
143
+ end
144
+ end
145
+
146
+ def raw args
147
+ args.unshift ['sh', '-c']
148
+ system args.flatten
149
+ end
150
+
151
+ def exec command, args = []
152
+ args.unshift ['git', '--no-pager', command]
153
+ system args.flatten
154
+ end
155
+
156
+ def find commands
157
+ Git::Multi.cloned_repositories.each do |repo|
158
+ Dir.chdir(repo.local_path) do
159
+ begin
160
+ if repo.instance_eval(commands.join(' && '))
161
+ repo.just_do_it(
162
+ ->(project) { nil ; },
163
+ ->(project) { puts project.full_name ; },
164
+ )
165
+ end
166
+ rescue Octokit::NotFound
167
+ # project no longer exists on github.com
168
+ # consider running "git multi --stale"...
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ def eval commands
175
+ Git::Multi.cloned_repositories.each do |repo|
176
+ Dir.chdir(repo.local_path) do
177
+ begin
178
+ repo.instance_eval(commands.join(' ; '))
179
+ rescue Octokit::NotFound
180
+ # project no longer exists on github.com
181
+ # consider running "git multi --stale"...
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,89 @@
1
+ module Git
2
+ module Multi
3
+ module Settings
4
+
5
+ TICK = ["2714".hex].pack("U*").green.freeze
6
+ CROSS = ["2718".hex].pack("U*").red.freeze
7
+ ARROW = ["2794".hex].pack("U*").blue.freeze
8
+
9
+ module_function
10
+
11
+ def setting_status messages, valid, optional = false
12
+ fields = messages.compact.join(' - ')
13
+ icon = valid ? TICK : optional ? ARROW : CROSS
14
+ if $INTERACTIVE
15
+ print " #{fields}" ; sleep 0.75 ; puts "\x0d#{icon}"
16
+ else
17
+ puts "#{icon} #{fields}"
18
+ end
19
+ end
20
+
21
+ def file_status file, message = 'File'
22
+ setting_status(
23
+ [
24
+ message,
25
+ abbreviate(file),
26
+ File.file?(file) ? "#{File.size(file).commify} bytes" : nil,
27
+ ],
28
+ file && !file.empty? && File.file?(file),
29
+ false
30
+ )
31
+ end
32
+
33
+ def directory_status messages, directory
34
+ setting_status(
35
+ messages,
36
+ directory && !directory.empty? && File.directory?(directory),
37
+ false
38
+ )
39
+ end
40
+
41
+ def workarea_status message, workarea, owner
42
+ directory_status(
43
+ [
44
+ message,
45
+ File.join(abbreviate(workarea, :workarea), owner),
46
+ File.directory?(workarea) ? "#{Dir.new(workarea).git_repos(owner).count.commify} repos" : nil
47
+ ],
48
+ workarea
49
+ )
50
+ end
51
+
52
+ def user_status user
53
+ setting_status(["User", user], user && !user.empty?)
54
+ end
55
+
56
+ def organization_status orgs
57
+ for org in orgs
58
+ setting_status(["Organization", org], org && !org.empty?, true)
59
+ setting_status(["Organization", "member?"], Git::Hub.orgs.include?(org), !Git::Hub.connected?)
60
+ end
61
+ end
62
+
63
+ def token_status token
64
+ setting_status(["Token", symbolize(token), describe(token)], !token.nil? && !token.empty?)
65
+ setting_status(["Token", "valid?"], !token.nil? && !token.empty? && Git::Hub.login, !Git::Hub.connected?)
66
+ setting_status(["Token", "owned by #{Git::Multi::USER}?"], Git::Hub.login == Git::Multi::USER, !Git::Hub.connected?)
67
+ end
68
+
69
+ def home_status home
70
+ directory_status(["Home", home], home)
71
+ end
72
+
73
+ def main_workarea_status workarea
74
+ directory_status(["Workarea (main)", abbreviate(workarea, :home)], workarea)
75
+ end
76
+
77
+ def user_workarea_status user
78
+ workarea_status("Workarea (user: #{user})", Git::Multi::WORKAREA, user)
79
+ end
80
+
81
+ def organization_workarea_status orgs
82
+ for org in orgs
83
+ workarea_status("Workarea (org: #{org})", Git::Multi::WORKAREA, org)
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,32 @@
1
+ require 'octokit'
2
+ require 'sawyer'
3
+ require 'psych'
4
+
5
+ module Git
6
+ module Multi
7
+ NAME = 'git-multi'
8
+ VERSION = '1.0.0'
9
+
10
+ LONG_VERSION = "%s v%s (%s v%s, %s v%s, %s v%s)" % [
11
+ NAME,
12
+ VERSION,
13
+ 'octokit.rb',
14
+ Octokit::VERSION,
15
+ 'sawyer',
16
+ Sawyer::VERSION,
17
+ 'psych',
18
+ Psych::VERSION,
19
+ ]
20
+
21
+ PIM = <<~"EOPIM" # gem post_install_message
22
+
23
+ The required settings are as follows:
24
+
25
+ git config --global --add github.user <your_github_username>
26
+ git config --global --add github.organizations <your_github_orgs>
27
+ git config --global --add github.token <your_github_token>
28
+ git config --global --add gitmulti.workarea <your_root_workarea>
29
+
30
+ EOPIM
31
+ end
32
+ end
data/lib/git/multi.rb ADDED
@@ -0,0 +1,190 @@
1
+ require 'etc'
2
+ require 'json'
3
+ require 'pathname'
4
+ require 'fileutils'
5
+ require 'shellwords'
6
+
7
+ require 'octokit'
8
+ require 'sawyer'
9
+
10
+ require 'ext/dir'
11
+ require 'ext/utils'
12
+ require 'ext/string'
13
+ require 'ext/notify'
14
+ require 'ext/commify'
15
+ require 'ext/sawyer/resource'
16
+
17
+ require 'git/hub'
18
+
19
+ require 'git/multi/version'
20
+ require 'git/multi/settings'
21
+ require 'git/multi/commands'
22
+
23
+ module Git
24
+ module Multi
25
+
26
+ HOME = Dir.home
27
+
28
+ DEFAULT_WORKAREA = File.join(HOME, 'Workarea')
29
+ WORKAREA = git_option('gitmulti.workarea', DEFAULT_WORKAREA)
30
+
31
+ DEFAULT_TOKEN = env_var('OCTOKIT_ACCESS_TOKEN') # same as Octokit
32
+ TOKEN = git_option('github.token', DEFAULT_TOKEN)
33
+
34
+ CACHE = File.join(HOME, '.git', 'multi')
35
+ REPOSITORIES = File.join(CACHE, 'repositories.byte')
36
+
37
+ USER = git_option('github.user')
38
+ ORGANIZATIONS = git_option('github.organizations').split(/\s*,\s*/)
39
+
40
+ MAN_PAGE = File.expand_path('../../doc/git-multi.man', __dir__)
41
+
42
+ module_function
43
+
44
+ #
45
+ # local repositories (in WORKAREA)
46
+ #
47
+
48
+ @local_user_repositories = Hash.new { |repos, user|
49
+ repos[user] = Dir.new(WORKAREA).git_repos(user)
50
+ }
51
+
52
+ @local_org_repositories = Hash.new { |repos, org|
53
+ repos[org] = Dir.new(WORKAREA).git_repos(org)
54
+ }
55
+
56
+ def local_repositories
57
+ (
58
+ @local_user_repositories[USER] +
59
+ ORGANIZATIONS.map { |org| @local_org_repositories[org] }
60
+ ).flatten
61
+ end
62
+
63
+ #
64
+ # remote repositories (on GitHub)
65
+ #
66
+
67
+ def github_repositories
68
+ @github_repositories ||= (
69
+ Git::Hub.user_repositories(USER) +
70
+ ORGANIZATIONS.map { |org| Git::Hub.org_repositories(org) }
71
+ ).flatten
72
+ end
73
+
74
+ def refresh_repositories
75
+ File.directory?(CACHE) || FileUtils.mkdir_p(CACHE)
76
+
77
+ File.open(REPOSITORIES, 'wb') do |file|
78
+ Marshal.dump(github_repositories, file)
79
+ end
80
+ end
81
+
82
+ #
83
+ # the main `Git::Multi` capabilities
84
+ #
85
+
86
+ module Nike
87
+
88
+ def just_do_it interactive, pipeline, options = {}
89
+ working_dir = case options[:in_dir]
90
+ when :parent_dir then self.parent_dir
91
+ when :local_path then self.local_path
92
+ else Dir.pwd
93
+ end
94
+ Dir.chdir(working_dir) do
95
+ if $INTERACTIVE
96
+ puts "%s (%s)" % [
97
+ self.full_name.invert,
98
+ self.fractional_index
99
+ ]
100
+ interactive.call(self)
101
+ else
102
+ pipeline.call(self)
103
+ end
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ def repositories
110
+ if File.size?(REPOSITORIES)
111
+ @repositories ||= Marshal.load(File.read(REPOSITORIES)).tap do |projects|
112
+ notify "Finished loading #{REPOSITORIES}"
113
+ projects.each_with_index do |project, index|
114
+ # ensure 'project' has handle on an Octokit client
115
+ project.client = Git::Hub.send(:client)
116
+ # adorn 'project', which is a Sawyer::Resource
117
+ project.parent_dir = Pathname.new(File.join(WORKAREA, project.owner.login))
118
+ project.local_path = Pathname.new(File.join(WORKAREA, project.full_name))
119
+ project.fractional_index = "#{index + 1}/#{projects.count}"
120
+ # fix 'project' => https://github.com/octokit/octokit.rb/issues/727
121
+ project.compliant_ssh_url = 'ssh://%s/%s' % project.ssh_url.split(':', 2)
122
+ # extend 'project' with 'just do it' capabilities
123
+ project.extend Nike
124
+ end
125
+ end
126
+ else
127
+ refresh_repositories and repositories
128
+ end
129
+ end
130
+
131
+ #
132
+ # lists of repositories with a given state
133
+ #
134
+
135
+ def archived_repositories
136
+ repositories.find_all(&:archived)
137
+ end
138
+
139
+ def forked_repositories
140
+ repositories.find_all(&:fork)
141
+ end
142
+
143
+ def private_repositories
144
+ repositories.find_all(&:private)
145
+ end
146
+
147
+ #
148
+ # derived lists of repositories
149
+ #
150
+
151
+ def excess_repositories
152
+ repository_full_names = repositories.map(&:full_name)
153
+ local_repositories.reject { |project|
154
+ repository_full_names.include? project.full_name
155
+ }
156
+ end
157
+
158
+ def stale_repositories
159
+ repository_full_names = github_repositories.map(&:full_name)
160
+ repositories.reject { |project|
161
+ repository_full_names.include? project.full_name
162
+ }
163
+ end
164
+
165
+ def spurious_repositories
166
+ cloned_repositories.find_all { |project|
167
+ origin_url = `git -C #{project.local_path} config --get remote.origin.url`.chomp
168
+ ![
169
+ project.clone_url,
170
+ project.ssh_url,
171
+ project.compliant_ssh_url,
172
+ project.git_url,
173
+ ].include? origin_url
174
+ }
175
+ end
176
+
177
+ def missing_repositories
178
+ repositories.find_all { |project|
179
+ !File.directory? project.local_path
180
+ }
181
+ end
182
+
183
+ def cloned_repositories
184
+ repositories.find_all { |project|
185
+ File.directory? project.local_path
186
+ }
187
+ end
188
+
189
+ end
190
+ end