twigg 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3e52b0f0bd210a32cf32644f0b9eb53c19b24c5
4
+ data.tar.gz: aa750f08ceb9795ef9e8d2f3b970470bf0c3e869
5
+ SHA512:
6
+ metadata.gz: 2d2cda3048d6a225c48d0e62ac26b3dd012b2be379e45584c088381c3f4d5d901ae154e1565d5f0d4af74b8f11e3c99cc3025bb66196bdfe1f9c65705695e5e4
7
+ data.tar.gz: 66d36587d5b4e8339517cc1f78ade317a77f132cb634f409b689744ba932c85787f54350a6868b57fa0688ff12cfef82ad259ec3071834057db606c3ff72a756
data/bin/twigg ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'twigg'
4
+
5
+ Twigg::Command.run(ARGV.shift, *ARGV)
data/files/github.pem ADDED
@@ -0,0 +1,97 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
3
+ MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
4
+ d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
5
+ ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
6
+ MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
7
+ LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
8
+ RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
9
+ +9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
10
+ PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
11
+ xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
12
+ Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
13
+ hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
14
+ EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
15
+ MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
16
+ FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
17
+ nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
18
+ eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
19
+ hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
20
+ Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
21
+ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
22
+ +OkuE6N36B9K
23
+ -----END CERTIFICATE-----
24
+ -----BEGIN CERTIFICATE-----
25
+ MIIGWDCCBUCgAwIBAgIQCl8RTQNbF5EX0u/UA4w/OzANBgkqhkiG9w0BAQUFADBs
26
+ MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
27
+ d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
28
+ ZSBFViBSb290IENBMB4XDTA4MDQwMjEyMDAwMFoXDTIyMDQwMzAwMDAwMFowZjEL
29
+ MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
30
+ LmRpZ2ljZXJ0LmNvbTElMCMGA1UEAxMcRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
31
+ Q0EtMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9hCikQH17+NDdR
32
+ CPge+yLtYb4LDXBMUGMmdRW5QYiXtvCgFbsIYOBC6AUpEIc2iihlqO8xB3RtNpcv
33
+ KEZmBMcqeSZ6mdWOw21PoF6tvD2Rwll7XjZswFPPAAgyPhBkWBATaccM7pxCUQD5
34
+ BUTuJM56H+2MEb0SqPMV9Bx6MWkBG6fmXcCabH4JnudSREoQOiPkm7YDr6ictFuf
35
+ 1EutkozOtREqqjcYjbTCuNhcBoz4/yO9NV7UfD5+gw6RlgWYw7If48hl66l7XaAs
36
+ zPw82W3tzPpLQ4zJ1LilYRyyQLYoEt+5+F/+07LJ7z20Hkt8HEyZNp496+ynaF4d
37
+ 32duXvsCAwEAAaOCAvowggL2MA4GA1UdDwEB/wQEAwIBhjCCAcYGA1UdIASCAb0w
38
+ ggG5MIIBtQYLYIZIAYb9bAEDAAIwggGkMDoGCCsGAQUFBwIBFi5odHRwOi8vd3d3
39
+ LmRpZ2ljZXJ0LmNvbS9zc2wtY3BzLXJlcG9zaXRvcnkuaHRtMIIBZAYIKwYBBQUH
40
+ AgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQBy
41
+ AHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBj
42
+ AGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAg
43
+ AEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQ
44
+ AGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBt
45
+ AGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBj
46
+ AG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBl
47
+ AHIAZQBuAGMAZQAuMBIGA1UdEwEB/wQIMAYBAf8CAQAwNAYIKwYBBQUHAQEEKDAm
48
+ MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wgY8GA1UdHwSB
49
+ hzCBhDBAoD6gPIY6aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0SGln
50
+ aEFzc3VyYW5jZUVWUm9vdENBLmNybDBAoD6gPIY6aHR0cDovL2NybDQuZGlnaWNl
51
+ cnQuY29tL0RpZ2lDZXJ0SGlnaEFzc3VyYW5jZUVWUm9vdENBLmNybDAfBgNVHSME
52
+ GDAWgBSxPsNpA/i/RwHUmCYaCALvY2QrwzAdBgNVHQ4EFgQUUOpzidsp+xCPnuUB
53
+ INTeeZlIg/cwDQYJKoZIhvcNAQEFBQADggEBAB7ipUiebNtTOA/vphoqrOIDQ+2a
54
+ vD6OdRvw/S4iWawTwGHi5/rpmc2HCXVUKL9GYNy+USyS8xuRfDEIcOI3ucFbqL2j
55
+ CwD7GhX9A61YasXHJJlIR0YxHpLvtF9ONMeQvzHB+LGEhtCcAarfilYGzjrpDq6X
56
+ dF3XcZpCdF/ejUN83ulV7WkAywXgemFhM9EZTfkI7qA5xSU1tyvED7Ld8aW3DiTE
57
+ JiiNeXf1L/BXunwH1OH8zVowV36GEEfdMR/X/KLCvzB8XSSq6PmuX2p0ws5rs0bY
58
+ Ib4p1I5eFdZCSucyb6Sxa1GDWL4/bcf72gMhy2oWGU4K8K2Eyl2Us1p292E=
59
+ -----END CERTIFICATE-----
60
+ -----BEGIN CERTIFICATE-----
61
+ MIIGpjCCBY6gAwIBAgIQCc4q8gtIK3iiFj+bqb1z9jANBgkqhkiG9w0BAQUFADBm
62
+ MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
63
+ d3cuZGlnaWNlcnQuY29tMSUwIwYDVQQDExxEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
64
+ ZSBDQS0zMB4XDTEyMDQzMDAwMDAwMFoXDTE0MDcwOTEyMDAwMFowaDELMAkGA1UE
65
+ BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lz
66
+ Y28xFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEVMBMGA1UEAwwMKi5naXRodWIuY29t
67
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA70XN+uwT7z4M04aFUwEJ
68
+ ypo6Tlr5gMvBvVFQnJlEqM3rYeqVHU8wU8af1tNV7dE6OkPpbdQ3yYgT2kWOXJnw
69
+ gR16Bzbqsd8OPGAFkKIS2zVm1uB3qKN5UaZTEE83vEbjjtwQE5OZDWqzEB2HFN14
70
+ YHtUezSp8unjO19RtWqnYiC/neEqdX2UJFJKjc0tm1yWL9jf6P04vYCsEWBh57O3
71
+ v4GugyHJ63SI8n0RZgNCX6dV9OwAq/Eju1qr+8p8E6sojA7BIvmUJMoGpNKoRtbU
72
+ RhjlzyG2udbZUYY5UGYEqQZgDx1vqKCbgq9xQ2RVd6ZWsW017HyvSK0BLnYtFubX
73
+ wQIDAQABo4IDTDCCA0gwHwYDVR0jBBgwFoAUUOpzidsp+xCPnuUBINTeeZlIg/cw
74
+ HQYDVR0OBBYEFIFnymFWE2hKxPmD1vRNyJ+hiKP6MCMGA1UdEQQcMBqCDCouZ2l0
75
+ aHViLmNvbYIKZ2l0aHViLmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI
76
+ KwYBBQUHAwEGCCsGAQUFBwMCMF8GA1UdHwRYMFYwKaAnoCWGI2h0dHA6Ly9jcmwz
77
+ LmRpZ2ljZXJ0LmNvbS9jYTMtZzguY3JsMCmgJ6AlhiNodHRwOi8vY3JsNC5kaWdp
78
+ Y2VydC5jb20vY2EzLWc4LmNybDCCAcQGA1UdIASCAbswggG3MIIBswYJYIZIAYb9
79
+ bAEBMIIBpDA6BggrBgEFBQcCARYuaHR0cDovL3d3dy5kaWdpY2VydC5jb20vc3Ns
80
+ LWNwcy1yZXBvc2l0b3J5Lmh0bTCCAWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkA
81
+ IAB1AHMAZQAgAG8AZgAgAHQAaABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUA
82
+ IABjAG8AbgBzAHQAaQB0AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAA
83
+ bwBmACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEA
84
+ bgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBnAHIA
85
+ ZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkAYQBiAGkA
86
+ bABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBvAHIAcABvAHIAYQB0AGUA
87
+ ZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQByAGUAbgBjAGUALjB7Bggr
88
+ BgEFBQcBAQRvMG0wJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNv
89
+ bTBFBggrBgEFBQcwAoY5aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lD
90
+ ZXJ0SGlnaEFzc3VyYW5jZUNBLTMuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN
91
+ AQEFBQADggEBABArBePXElSsd5Ec5IIqfb7MBAafKK1jbFstNTyf175xDJAUPh3p
92
+ 5fsB72UOpm1rdfn5/aDc1rgTKiL3t+1ALcHvyIhaxkf2pe39W9zN2bAy0Yx+sSOf
93
+ GEi0LHMfzeDkmaeHrQXUjWBQoE4QOIon7AoBsylNK6gbsLryiab0rJ565/BEhC16
94
+ BfZt0XgSRJHF18enz5vjf2fK6CgtM1poVVEPdxfr9qxdJ7A9MBBw+OcB+JwWdE89
95
+ IzV94FOVPClMm1G87wcFx0cIqYvA3j8g6UvIOeG63PWLmXhAeHCGmVEoIjQz96S/
96
+ Ox6cL5JR0RfxxBvKCzONM8XS2pU1dvUlcpw=
97
+ -----END CERTIFICATE-----
@@ -0,0 +1,23 @@
1
+ module Twigg
2
+ class Command
3
+ class Git < GitHost
4
+ private
5
+
6
+ def sub_subcommands
7
+ %w[gc]
8
+ end
9
+
10
+ def projects
11
+ @projects ||= RepoSet.new(@repositories_directory).repos.map(&:name)
12
+ end
13
+
14
+ def gc
15
+ for_each_repo do |project|
16
+ Dir.chdir project do
17
+ git 'gc', '--quiet'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ module Twigg
2
+ class Command
3
+ # This is an abstract superclass the holds code common to the "gerrit" and
4
+ # "github" subcommands.
5
+ class GitHost < Command
6
+ def initialize(*args)
7
+ super
8
+ @sub_subcommand = @args.first
9
+
10
+ unless (1..2).cover?(@args.size) &&
11
+ sub_subcommands.include?(@sub_subcommand)
12
+ # eg. "Twigg::Command::{Gerrit,GitHub}" -> "gerrit/github"
13
+ Help.new(self.class.to_s.split('::').last.downcase).run!
14
+ end
15
+
16
+ @repositories_directory = @args[1] || Config.repositories_directory
17
+ end
18
+
19
+ def run
20
+ send @sub_subcommand
21
+ end
22
+
23
+ private
24
+
25
+ def sub_subcommands
26
+ %w[clone update]
27
+ end
28
+
29
+ def for_each_repo(&block)
30
+ Dir.chdir @repositories_directory do
31
+ projects.each do |project|
32
+ print @verbose ? "#{project}: " : '.'
33
+ block.call(project)
34
+ puts ' done' if @verbose
35
+ end
36
+ puts
37
+ end
38
+ end
39
+
40
+ def clone
41
+ for_each_repo do |project|
42
+ if File.directory?(project)
43
+ print 'skipping (already present);' if @verbose
44
+ else
45
+ print 'cloning...' if @verbose
46
+ git_clone(project)
47
+ end
48
+ end
49
+ end
50
+
51
+ def update
52
+ for_each_repo do |project|
53
+ if File.directory?(project)
54
+ Dir.chdir project do
55
+ print 'pulling...' if @verbose
56
+ git_pull
57
+ end
58
+ else
59
+ print 'skipping (not present);' if @verbose
60
+ end
61
+ end
62
+ end
63
+
64
+ # Convenience method for running a Git command that is expected to succeed
65
+ # (raises an error if a non-zero exit code is produced).
66
+ def git(*args)
67
+ Process.wait(IO.popen(%w[git] + args).pid)
68
+ raise unless $?.success?
69
+ end
70
+
71
+ # Runs `git clone` to obtain the specified `project`.
72
+ def git_clone(project)
73
+ git 'clone', '--quiet', address(project)
74
+ end
75
+
76
+ # Runs `git fetch --quiet` followed by `git merge --ff-only --quiet
77
+ # FETCH_HEAD`.
78
+ #
79
+ # We do this as two commands rather than a `git pull` because the latter
80
+ # is much fussier about tracking information being in place.
81
+ def git_pull
82
+ git 'fetch', '--quiet'
83
+ git 'merge', '--ff-only', '--quiet', 'FETCH_HEAD'
84
+ rescue => e
85
+ # could die here if remote doesn't contain any commits yet
86
+ end
87
+
88
+ def address(*args)
89
+ raise NotImplementedError # subclass responsibility
90
+ end
91
+
92
+ # Returns the list of all projects hosted within a Git host.
93
+ def projects
94
+ raise NotImplementedError # subclass responsibility
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,67 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'json'
4
+
5
+ module Twigg
6
+ class Command
7
+ # The "github" subcommand can be used to conveniently initialize a set of
8
+ # repos and keep them up-to-date.
9
+ class GitHub < GitHost
10
+ private
11
+
12
+ def address(project)
13
+ "git@github.com:#{Config.github.organization}/#{project}.git"
14
+ end
15
+
16
+ API_HOST = 'api.github.com'
17
+ API_PORT = 443
18
+ ORG_REPOS_ENDPOINT = '/orgs/%s/repos'
19
+
20
+ # Returns the list of all projects hosted within a GitHub organization.
21
+ def projects
22
+ @projects ||= begin
23
+ http = Net::HTTP.new(API_HOST, API_PORT)
24
+ http.use_ssl = true
25
+ http.ca_file = (Twigg.root + 'files' + 'github.pem').to_s
26
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
27
+ uri = ORG_REPOS_ENDPOINT % Config.github.organization
28
+ headers = { 'Authorization' => "token #{Config.github.token}" }
29
+
30
+ [].tap do |names|
31
+ begin # loop: page through project list
32
+ request = Net::HTTP::Get.new(uri, headers)
33
+ response = http.request(request)
34
+ raise "Bad response #{response.inspect}" unless response.is_a?(Net::HTTPOK)
35
+ names.concat JSON[response.body].map { |repo| repo['name'] }
36
+ uri = parse_link(response['Link'])
37
+ end until uri.nil?
38
+ end
39
+ end
40
+ end
41
+
42
+ # Parse the next page's URI out of a Link header, which will be of the
43
+ # form:
44
+ #
45
+ # <https://api.github.com/organizations/1234/repos?page=2>; rel="next",
46
+ # <https://api.github.com/organizations/1234/repos?page=N>; rel="last"
47
+ #
48
+ # (Linebreak included for readability; in the real headers there are no
49
+ # linebreaks.)
50
+ #
51
+ # We split on "," to get a list of links, find the first link labeled as
52
+ # `rel="next'`, and then extract the URI from inside the corresponding
53
+ # angle brackets.
54
+ #
55
+ # Returns a `URI` object on success, and `nil` if no suitable link was
56
+ # present.
57
+ def parse_link(header)
58
+ link = header.split(',').find do |link|
59
+ rel = link.split(';').last
60
+ rel && rel =~ /rel="next"/
61
+ end
62
+
63
+ URI(link.split(';').first.gsub(/\A<|>\z/, '')) if link
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,113 @@
1
+ require 'shellwords'
2
+
3
+ module Twigg
4
+ class Command
5
+ class Help < Command
6
+ PUBLIC_HELP_TOPICS = PUBLIC_SUBCOMMANDS + %w[commands usage]
7
+ HELP_TOPICS = PUBLIC_HELP_TOPICS + EASTER_EGGS
8
+
9
+ def initialize(*args)
10
+ super
11
+ @topic = @args.shift
12
+ ignore @args
13
+ end
14
+
15
+ def run
16
+ if HELP_TOPICS.include?(@topic)
17
+ show_help(@topic)
18
+ else
19
+ PUBLIC_HELP_TOPICS.each { |topic| show_help(topic) }
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def executable
26
+ Shellwords.escape($0)
27
+ end
28
+
29
+ TOPIC_HEADERS = Hash.new { |h, k| h[k] = k.capitalize }.merge(
30
+ # header = subcommand with first letter capitalized; exceptions:
31
+ 'app' => 'Web application',
32
+ 'github' => 'GitHub',
33
+ )
34
+
35
+ def show_help(topic)
36
+ puts TOPIC_HEADERS[topic] + ':'
37
+ stderr strip_heredoc(send(topic)) + "\n"
38
+ end
39
+
40
+ def app
41
+ <<-DOC
42
+ #{executable} app
43
+ DOC
44
+ end
45
+
46
+ def commands
47
+ <<-DOC
48
+ #{executable} app # run the Twigg web app
49
+ #{executable} gerrit # clone/update/report from Gerrit
50
+ #{executable} git # perform operations on Git repos
51
+ #{executable} github # clone/update from GitHub
52
+ #{executable} init # generate a .twiggrc file
53
+ #{executable} help # this help information
54
+ #{executable} stats # show statistics about repos
55
+ DOC
56
+ end
57
+
58
+ def gerrit
59
+ <<-DOC
60
+ #{executable} gerrit clone [repos dir] # clone repos into repos dir
61
+ #{executable} gerrit update [repos dir] # update repos in repos dir
62
+ #{executable} gerrit stats [repos dir] # show stats for repos in dir
63
+ DOC
64
+ end
65
+
66
+ def git
67
+ <<-DOC
68
+ #{executable} git gc [repos dir] # garbage collect repos in repos dir
69
+ DOC
70
+ end
71
+
72
+ def github
73
+ <<-DOC
74
+ #{executable} github clone [repos dir] # clone repos into repos dir
75
+ #{executable} github update [repos dir] # update repos in repos dir
76
+ DOC
77
+ end
78
+
79
+ def help
80
+ <<-DOC
81
+ #{executable} help # this help information
82
+ #{executable} help <subcommand> # help for a specific subcommand
83
+ #{executable} help commands # list all subcommands
84
+ DOC
85
+ end
86
+
87
+ def init
88
+ <<-DOC
89
+ #{executable} init # emit a sample .twiggrc file to standard out
90
+ DOC
91
+ end
92
+
93
+ def russian
94
+ <<-DOC
95
+ #{executable} russian <repos dir> <number of days> # easter egg
96
+ DOC
97
+ end
98
+
99
+ def stats
100
+ <<-DOC
101
+ #{executable} stats [--verbose|-v] <repos dir> <number of days>
102
+ DOC
103
+ end
104
+
105
+ def usage
106
+ <<-DOC
107
+ #{executable} <subcommand> [options] <arguments...>
108
+ #{executable} help
109
+ DOC
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,15 @@
1
+ module Twigg
2
+ class Command
3
+ class Init < Command
4
+ def initialize(*args)
5
+ super
6
+ ignore @args
7
+ end
8
+
9
+ def run
10
+ path = Twigg.root + 'templates' + 'twiggrc.yml'
11
+ IO.copy_stream(path, STDOUT)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module Twigg
2
+ class Command
3
+ class Russian < Command
4
+ def initialize(*args)
5
+ super
6
+ Help.new('russian').run! if @args.size > 2
7
+
8
+ @repositories_directory = @args[0] || Config.repositories_directory
9
+ @days = (@args[1] || Config.default_days).to_i
10
+ end
11
+
12
+ def run
13
+ commit_set = Gatherer.gather(@repositories_directory, @days)
14
+ puts RussianNovel.new(commit_set).data['children'].to_yaml
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ module Twigg
2
+ class Command
3
+ class Stats < Command
4
+ include Util
5
+
6
+ def initialize(*args)
7
+ super
8
+ Help.new('stats').run! if @args.size > 2
9
+
10
+ @repositories_directory = @args[0] || Config.repositories_directory
11
+ @days = (@args[1] || Config.default_days).to_i
12
+ end
13
+
14
+ def run
15
+ master_set = Twigg::Gatherer.gather(@repositories_directory, @days)
16
+ w0, w1, w2 = stats_widths(master_set)
17
+
18
+ master_set.authors.each do |author_data|
19
+ author = author_data[:author]
20
+ commit_set = author_data[:commit_set]
21
+ puts '%5s %-24s %s' % [
22
+ number_with_delimiter(commit_set.count),
23
+ author,
24
+ breakdown(commit_set, html: false),
25
+ ]
26
+
27
+ if @verbose
28
+ puts
29
+ commit_set.each do |commit|
30
+ puts (' ' * w0) + " %#{w1}s, %#{w2}s %s [%s]" % [
31
+ "+#{number_with_delimiter commit.stat[:additions]}",
32
+ "-#{number_with_delimiter commit.stat[:deletions]}",
33
+ commit.subject,
34
+ commit.repo.name,
35
+ ]
36
+ end
37
+
38
+ totals = (' ' * w0) + " %#{w1}s, %#{w2}s" % [
39
+ "+#{number_with_delimiter commit_set.additions}",
40
+ "-#{number_with_delimiter commit_set.deletions}",
41
+ ]
42
+ puts '-' * totals.length
43
+ puts totals
44
+ puts
45
+ end
46
+ end
47
+
48
+ if @verbose
49
+ totals = "%-#{w0}s %#{w1}s, %#{w2}s" % [
50
+ number_with_delimiter(master_set.count),
51
+ "+#{number_with_delimiter master_set.additions}",
52
+ "-#{number_with_delimiter master_set.deletions}",
53
+ ]
54
+ puts '=' * totals.length
55
+ puts totals
56
+ else
57
+ totals = "%#{w0}s" % number_with_delimiter(master_set.count)
58
+ puts '-' * totals.length
59
+ puts totals
60
+ end
61
+ end
62
+
63
+ # Returns a tuple of "column" widths with sufficient space to represent
64
+ # the commit count, addition count and deletion count for the given
65
+ # {CommitSet}, `master_set`.
66
+ def stats_widths(master_set)
67
+ [
68
+ number_with_delimiter(master_set.count).length,
69
+ number_with_delimiter(master_set.additions).length + 1, # room for sign
70
+ number_with_delimiter(master_set.deletions).length + 1, # room for sign
71
+ ]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,108 @@
1
+ require 'forwardable'
2
+
3
+ module Twigg
4
+ class Command
5
+ # Subcommands, in the order they should appear in the help output.
6
+ PUBLIC_SUBCOMMANDS = %w[help init app stats gerrit github git]
7
+
8
+ EASTER_EGGS = %w[russian]
9
+ SUBCOMMANDS = PUBLIC_SUBCOMMANDS + EASTER_EGGS
10
+
11
+ autoload :Git, 'twigg/command/git'
12
+ autoload :GitHost, 'twigg/command/git_host'
13
+ autoload :GitHub, 'twigg/command/git_hub'
14
+ autoload :Init, 'twigg/command/init'
15
+ autoload :Help, 'twigg/command/help'
16
+ autoload :Russian, 'twigg/command/russian'
17
+ autoload :Stats, 'twigg/command/stats'
18
+
19
+ extend Console
20
+ include Console
21
+
22
+ class << self
23
+ include Dependency # for with_dependency
24
+
25
+ def run(subcommand, *args)
26
+ Help.new('usage').run! unless SUBCOMMANDS.include?(subcommand)
27
+
28
+ if args.include?('-h') || args.include?('--help')
29
+ Help.new(subcommand).run
30
+ exit
31
+ end
32
+
33
+ begin
34
+ send(subcommand, *args)
35
+ rescue => e
36
+ raise if args.include?('-d') || args.include?('--debug')
37
+
38
+ error e.message
39
+ stderr '[run with -d or --debug flag to see full stack trace]'
40
+ die
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def ignore(args)
47
+ warn "unsupported extra arguments #{args.inspect} ignored" if args.any?
48
+ end
49
+
50
+ def app(*args)
51
+ ignore args
52
+ with_dependency('twigg-app') { App::Server.run! }
53
+ end
54
+
55
+ def gerrit(*args)
56
+ with_dependency('twigg-gerrit') { Gerrit.new(*args).run }
57
+ end
58
+
59
+ def git(*args)
60
+ Git.new(*args).run
61
+ end
62
+
63
+ def github(*args)
64
+ GitHub.new(*args).run
65
+ end
66
+
67
+ def help(*args)
68
+ Help.new(*args).run
69
+ end
70
+
71
+ def init(*args)
72
+ Init.new(*args).run
73
+ end
74
+
75
+ def russian(*args)
76
+ Russian.new(*args).run
77
+ end
78
+
79
+ def stats(*args)
80
+ Stats.new(*args).run
81
+ end
82
+ end
83
+
84
+ extend Forwardable
85
+ def_delegators 'self.class', :ignore
86
+
87
+ def initialize(*args)
88
+ Config.config # ensure `-c`/`--config` option is applied
89
+ consume_option(%w[-c --config], args) # ensure consumed
90
+
91
+ @debug = true if args.delete('-d') || args.delete('--debug')
92
+ @verbose = true if args.delete('-v') || args.delete('--verbose')
93
+ @args = args
94
+ end
95
+
96
+ # Run and then die.
97
+ def run!
98
+ run
99
+ die
100
+ end
101
+
102
+ # Abstract implementation of a "run" method; subclasses are expected to
103
+ # override this method.
104
+ def run
105
+ raise NotImplementedError
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,59 @@
1
+ module Twigg
2
+ class Commit
3
+ attr_reader :repo, :commit, :subject, :body, :author, :date, :stat
4
+
5
+ def initialize(options)
6
+ raise ArgumentError unless @repo = options[:repo]
7
+ raise ArgumentError unless @commit = options[:commit]
8
+ raise ArgumentError unless @subject = options[:subject]
9
+ raise ArgumentError unless @body = options[:body]
10
+ raise ArgumentError unless @author = options[:author]
11
+ raise ArgumentError unless @date = options[:date]
12
+ raise ArgumentError unless @stat = options[:stat]
13
+ end
14
+
15
+ def link
16
+ if Config.github.organization
17
+ "https://github.com/#{Config.github.organization}/#{repo.name}/commit/#{commit}"
18
+ end
19
+ end
20
+
21
+ def author_names
22
+ @author.split(/\+|&|,|\band\b/).map(&:strip)
23
+ end
24
+
25
+ def eql?(other)
26
+ other.is_a?(Commit) &&
27
+ other.repo == @repo &&
28
+ other.commit == @commit &&
29
+ other.subject == @subject &&
30
+ other.body == @body &&
31
+ other.author == @author &&
32
+ other.date == @date &&
33
+ other.stat == @stat
34
+ end
35
+
36
+ def filtered_commit_message
37
+ @filtered_commit_message ||= @body.lines.reject do |line|
38
+ line =~ /^[a-z-]+: /i # filter out Change-Id:, Signed-off-by: etc
39
+ end.concat([@subject]).join("\n").chomp
40
+ end
41
+
42
+ def flesch_reading_ease
43
+ @flesch_reading_ease ||= Flesch.new(filtered_commit_message).reading_ease
44
+ end
45
+
46
+ # Return the length of the commit message in lines.
47
+ def russianness
48
+ filtered_commit_message.lines.count
49
+ end
50
+
51
+ def inspect
52
+ "repo: #{@repo.name}\n" +
53
+ "commit: #{@commit}\n" +
54
+ "subject: #{@subject}\n" +
55
+ "author: #{@author}\n" +
56
+ "stat: +#{@stat[:additions]}, -#{@stat[:deletions]}"
57
+ end
58
+ end
59
+ end