twigg 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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