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 +7 -0
- data/bin/twigg +5 -0
- data/files/github.pem +97 -0
- data/lib/twigg/command/git.rb +23 -0
- data/lib/twigg/command/git_host.rb +98 -0
- data/lib/twigg/command/git_hub.rb +67 -0
- data/lib/twigg/command/help.rb +113 -0
- data/lib/twigg/command/init.rb +15 -0
- data/lib/twigg/command/russian.rb +18 -0
- data/lib/twigg/command/stats.rb +75 -0
- data/lib/twigg/command.rb +108 -0
- data/lib/twigg/commit.rb +59 -0
- data/lib/twigg/commit_set.rb +137 -0
- data/lib/twigg/config.rb +95 -0
- data/lib/twigg/console.rb +68 -0
- data/lib/twigg/dependency.rb +12 -0
- data/lib/twigg/flesch.rb +65 -0
- data/lib/twigg/gatherer.rb +15 -0
- data/lib/twigg/pair_matrix.rb +83 -0
- data/lib/twigg/repo.rb +134 -0
- data/lib/twigg/repo_set.rb +31 -0
- data/lib/twigg/russian_novel.rb +40 -0
- data/lib/twigg/settings/dsl.rb +144 -0
- data/lib/twigg/settings.rb +69 -0
- data/lib/twigg/team.rb +25 -0
- data/lib/twigg/util.rb +68 -0
- data/lib/twigg/version.rb +3 -0
- data/lib/twigg.rb +26 -0
- metadata +142 -0
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
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,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
|
data/lib/twigg/commit.rb
ADDED
@@ -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
|