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