twigg 0.0.1

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