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.
@@ -0,0 +1,144 @@
1
+ require 'ostruct'
2
+
3
+ module Twigg
4
+ class Settings
5
+ module DSL
6
+ # Hash subclass used as a book-keeping detail, which allows us to
7
+ # differentiate settings hashes from hashes used to construct namespaces.
8
+ class Namespace < Hash; end
9
+
10
+ module ClassMethods
11
+ attr_reader :overrides
12
+
13
+ private
14
+
15
+ def namespaces
16
+ @namespaces ||= []
17
+ end
18
+
19
+ # DSL method for declaring settings underneath a namespace.
20
+ #
21
+ # Example:
22
+ #
23
+ # namespace :app do
24
+ # setting :bind, default: '0.0.0.0'
25
+ # end
26
+ #
27
+ def namespace(scope, &block)
28
+ namespaces.push scope
29
+ yield
30
+ ensure
31
+ namespaces.pop
32
+ end
33
+
34
+ # DSL method which is used to create a reader for the setting `name`. If
35
+ # the configuration file does not contain a value for the setting, return
36
+ # `options[:default]` from the `options` hash.
37
+ #
38
+ # A block maybe provided to do checking of the supplied value prior to
39
+ # returning; for an example, see the `repositories_directory` setting in
40
+ # this file.
41
+ #
42
+ # Note that this method doesn't actually create any readers; it merely
43
+ # records data about default values and validation which are then used at
44
+ # initialization time to override the readers provided by OpenStruct
45
+ # itself.
46
+ def setting(name, options = {}, &block)
47
+ options.merge!(block: block)
48
+ @overrides ||= {}
49
+
50
+ overrides = namespaces.inject(@overrides) do |overrides, namespace|
51
+ overrides[namespace] ||= Namespace.new
52
+ end
53
+
54
+ overrides[name] = options
55
+ end
56
+ end
57
+
58
+ module InstanceMethods
59
+ private
60
+
61
+ def initialize(hash = nil)
62
+ super
63
+ deep_open_structify!(self)
64
+ override_methods!(self, self.class.overrides)
65
+ end
66
+
67
+ # Recursively replace nested hash values with OpenStructs.
68
+ #
69
+ # This enables us to make nice calls like `Config.foo.bar` instead of
70
+ # the somewhat unsightly `Config.foo[:bar]`.
71
+ def deep_open_structify!(instance)
72
+ # call `to_a` here to avoid mutating the collection during iteration
73
+ instance.each_pair.to_a.each do |key, value|
74
+ if value.is_a?(Hash)
75
+ deep_open_structify!(instance.[]=(key, OpenStruct.new(value)))
76
+ end
77
+ end
78
+ end
79
+
80
+ # Recurses through the `overrides` hash, overriding OpenStruct-supplied
81
+ # methods with ones which handle defaults, perform validation, and memoize
82
+ # their results.
83
+ #
84
+ # The `overrides` hash itself is built up using the DSL as this class file
85
+ # is passed. This method is called at runtime, once the YAML file containing
86
+ # the configuration has been loaded from disk.
87
+ def override_methods!(instance, overrides, namespace = nil)
88
+ return unless overrides
89
+
90
+ overrides.each do |name, options|
91
+ if options.is_a?(Namespace)
92
+ process_namespace(instance, namespace, name, options) # recurses
93
+ else
94
+ define_getter(instance, namespace, name, options)
95
+ end
96
+ end
97
+ end
98
+
99
+ # Sets up the given `namespace` on `instance`, if not already set up,
100
+ # and (recursively) calls {#override_methods!} to set up `name`.
101
+ def process_namespace(instance, namespace, name, options)
102
+ nested = instance.[](name)
103
+
104
+ unless nested.is_a?(OpenStruct)
105
+ instance.[]=(name, nested = OpenStruct.new)
106
+ end
107
+
108
+ override_methods!(nested, options, name)
109
+ end
110
+
111
+ # Defines a getter on the `instance` for setting `name` within
112
+ # `namespace`.
113
+ #
114
+ # The getter:
115
+ #
116
+ # - memoizes its result
117
+ # - raises an ArgumentError if the option is required by not
118
+ # configured
119
+ # - supplies a default value if the option is not configured and a
120
+ # default setting is available
121
+ # - optionally calls a supplied block to perform validation of the
122
+ # configured value
123
+ #
124
+ def define_getter(instance, namespace, name, options)
125
+ instance.define_singleton_method name do
126
+ value = instance_variable_get("@#{namespace}__#{name}")
127
+ return value if value
128
+
129
+ if options[:required] &&
130
+ !instance.each_pair.to_a.map(&:first).include?(name)
131
+ raise ArgumentError, "#{name} not set"
132
+ end
133
+
134
+ value = instance.[](name) || options[:default]
135
+
136
+ options[:block].call(name, value) if options[:block]
137
+
138
+ instance_variable_set("@#{namespace}__#{name}", value)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,69 @@
1
+ require 'ostruct'
2
+
3
+ module Twigg
4
+ # Simple OpenStruct subclass with some methods overridden in order to provide
5
+ # more detailed feedback in the event of a misconfiguration, or to provide
6
+ # reasonable default values.
7
+ #
8
+ # For example, we override `repositories_directory` so that we can report to
9
+ # the user that:
10
+ #
11
+ # - the setting is missing from the configuration file
12
+ # - the setting does not point at a directory
13
+ #
14
+ # We override `app.bind` so that we can supply a reasonable default when the
15
+ # configuration file has no value set.
16
+ #
17
+ # In order to keep this class a simple and readable manifest of the different
18
+ # options, the real logic is all implemented in the {Twigg::Settings::DSL}
19
+ # module, leaving only the DSL declarations here.
20
+ class Settings < OpenStruct
21
+ autoload :DSL, 'twigg/settings/dsl'
22
+ extend DSL::ClassMethods
23
+ include DSL::InstanceMethods
24
+
25
+ namespace :app do
26
+ setting :bind, default: '0.0.0.0'
27
+
28
+ namespace :gerrit do
29
+ setting :enabled, default: false
30
+ end
31
+ end
32
+
33
+ setting :default_days, default: 7
34
+
35
+ namespace :gerrit do
36
+ setting :host, default: 'localhost'
37
+ setting :port, default: 29418
38
+ setting :user, default: ENV['USER']
39
+
40
+ namespace :web do
41
+ setting :host, default: 'https://localhost'
42
+ end
43
+
44
+ namespace :db do
45
+ setting :adapter, default: 'mysql2'
46
+ setting :database, required: true
47
+ setting :host, default: 'localhost'
48
+ setting :password
49
+ setting :port, default: 3306
50
+ setting :user, default: ENV['USER']
51
+ end
52
+ end
53
+
54
+ namespace :github do
55
+ setting :organization, required: true
56
+ setting :token, required: true
57
+ end
58
+
59
+ setting :repositories_directory, required: true do |name, value|
60
+ raise ArgumentError, "#{name} not a directory" unless File.directory?(value)
61
+ end
62
+
63
+ setting :teams, default: {} do |name, value|
64
+ if !value.respond_to?(:to_h) || value.to_h.values.any? { |team| !team.is_a?(Array) }
65
+ raise ArgumentError, "#{name} should be a hash of arrays"
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/twigg/team.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Twigg
2
+ class Team
3
+ # Catch-all team name for authors who aren't assigned to a particular team.
4
+ OTHER_TEAM_NAME = 'Other'
5
+
6
+ class << self
7
+ # Returns a hash where the keys are author names and the values are team
8
+ # names.
9
+ #
10
+ # As there is only one value per key, an author must be in one team only;
11
+ # if the author is assigned to multiple teams, this method will pick the
12
+ # first team the author is assigned to as his or her team.
13
+ def author_to_team_map
14
+ Config.
15
+ teams.
16
+ each_pair.
17
+ each_with_object(Hash.new(OTHER_TEAM_NAME)) do |(team, members), map|
18
+ members.each do |member|
19
+ map[member] = team.to_s unless map.has_key?(member)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/twigg/util.rb ADDED
@@ -0,0 +1,68 @@
1
+ module Twigg
2
+ module Util
3
+ private
4
+
5
+ # Returns the age of `time` relative to now in hours (for short intervals)
6
+ # or days (for intervals longer than 24 hours).
7
+ def age(time)
8
+ delta = Time.now - time
9
+ return 'future' if delta < 0
10
+ hours = (delta / (60 * 60)).to_i
11
+ days = hours / 24
12
+ (hours > 24 ? "#{pluralize days, 'day'}" : "#{pluralize hours, 'hour'}") +
13
+ ' ago'
14
+ end
15
+
16
+ def number_with_delimiter(integer)
17
+ # Regex based on one in `ActiveSupport::NumberHelper#number_to_delimited`;
18
+ # this method is simpler because it only needs to handle integers.
19
+ integer.to_s.tap do |string|
20
+ string.gsub!(/(\d)(?=(\d{3})+(?!\d))/, '\\1,')
21
+ end
22
+ end
23
+
24
+ # Dumb implementation of a Rails-style `#pluralize` helper.
25
+ #
26
+ # As a default, it pluralizes by adding an "s" to the singular form, and
27
+ # will use `#number_with_delimiter` to insert delimiters, unless passed
28
+ # `delimit: false`.
29
+ #
30
+ # If you pass a plural inflection, it is remembered for subsequent calls.
31
+ #
32
+ # Example:
33
+ #
34
+ # pluralize(1, 'octopus', 'octopi') # => "1 octopus"
35
+ # pluralize(2, 'octopus') # => "2 octopi"
36
+ # pluralize(1_200, 'commit') # => "1,200 commits"
37
+ # pluralize(1_200, 'commit', delimit: false) # => "1200 commits"
38
+ #
39
+ def pluralize(count, singular, plural = nil, delimit: true)
40
+ @inflections ||= Hash.new do |hash, key|
41
+ hash[key] = [key, plural ? plural : key + 's']
42
+ end
43
+
44
+ (delimit ? number_with_delimiter(count) : count.to_s) + ' ' +
45
+ @inflections[singular][count == 1 ? 0 : 1]
46
+ end
47
+
48
+ # Returns a per-repo breakdown (repo names, commit counts) of commits in
49
+ # `commit_set`.
50
+ #
51
+ # Returns HTML output by default, or plain-text when `html` is `false`.
52
+ def breakdown(commit_set, html: true)
53
+ commit_set.count_by_repo.map do |data|
54
+ if html && (link = data[:repo].link)
55
+ name = %{<a href="#{link}">#{data[:repo].name}</a>}
56
+ else
57
+ name = data[:repo].name
58
+ end
59
+
60
+ if html
61
+ "<i>#{name}:</i> <b>#{number_with_delimiter data[:count]}</b>"
62
+ else
63
+ "#{name}: #{number_with_delimiter data[:count]}"
64
+ end
65
+ end.join(', ')
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Twigg
2
+ VERSION = '0.0.1'
3
+ end
data/lib/twigg.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'pathname'
2
+
3
+ module Twigg
4
+ autoload :Command, 'twigg/command'
5
+ autoload :Commit, 'twigg/commit'
6
+ autoload :CommitSet, 'twigg/commit_set'
7
+ autoload :Config, 'twigg/config'
8
+ autoload :Console, 'twigg/console'
9
+ autoload :Dependency, 'twigg/dependency'
10
+ autoload :Flesch, 'twigg/flesch'
11
+ autoload :Gatherer, 'twigg/gatherer'
12
+ autoload :PairMatrix, 'twigg/pair_matrix'
13
+ autoload :Repo, 'twigg/repo'
14
+ autoload :RepoSet, 'twigg/repo_set'
15
+ autoload :RussianNovel, 'twigg/russian_novel'
16
+ autoload :Settings, 'twigg/settings'
17
+ autoload :Team, 'twigg/team'
18
+ autoload :Util, 'twigg/util'
19
+ autoload :VERSION, 'twigg/version'
20
+
21
+ # Returns a Pathname instance corresponding to the root directory of the gem
22
+ # (ie. the directory containing the `files` and `templates` directories).
23
+ def self.root
24
+ Pathname.new(__dir__) + '..'
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twigg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Causes Engineering
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: factory_girl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rr
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Twigg provides stats for activity in Git repositories.
84
+ email:
85
+ - eng@causes.com
86
+ executables:
87
+ - twigg
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - bin/twigg
92
+ - files/github.pem
93
+ - lib/twigg/command/git.rb
94
+ - lib/twigg/command/git_host.rb
95
+ - lib/twigg/command/git_hub.rb
96
+ - lib/twigg/command/help.rb
97
+ - lib/twigg/command/init.rb
98
+ - lib/twigg/command/russian.rb
99
+ - lib/twigg/command/stats.rb
100
+ - lib/twigg/command.rb
101
+ - lib/twigg/commit.rb
102
+ - lib/twigg/commit_set.rb
103
+ - lib/twigg/config.rb
104
+ - lib/twigg/console.rb
105
+ - lib/twigg/dependency.rb
106
+ - lib/twigg/flesch.rb
107
+ - lib/twigg/gatherer.rb
108
+ - lib/twigg/pair_matrix.rb
109
+ - lib/twigg/repo.rb
110
+ - lib/twigg/repo_set.rb
111
+ - lib/twigg/russian_novel.rb
112
+ - lib/twigg/settings/dsl.rb
113
+ - lib/twigg/settings.rb
114
+ - lib/twigg/team.rb
115
+ - lib/twigg/util.rb
116
+ - lib/twigg/version.rb
117
+ - lib/twigg.rb
118
+ homepage: https://github.com/causes/twigg
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - '>='
129
+ - !ruby/object:Gem::Version
130
+ version: 2.0.0
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.0.3
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Statistics for Git repositories
142
+ test_files: []