git-multi 1.0.0

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