contributions 0.1.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.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ .sass-cache
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ b/
20
+ .rbenv-version
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in contributions.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Charlie Tanksley
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # Contributions
2
+
3
+ Get the detailed information about all your OSS contributions in one
4
+ place.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'contributions'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install contributions
19
+
20
+ ## Fair Warning:
21
+
22
+ Right now, Contributions doesn't know what to do if you have more than
23
+ 100 public repositories. I'll fix that later. Consider yourself
24
+ warned.
25
+
26
+ ## Requirements
27
+
28
+ [![Build Status](https://secure.travis-ci.org/charlietanksley/contributions.png)](http://travis-ci.org/charlietanksley/contributions)
29
+
30
+ Contributions is known to work on:
31
+
32
+ * MRI 1.9.2
33
+ * MRI 1.9.3
34
+
35
+ At present it does not work on:
36
+
37
+ * Rubinius 1.2.4
38
+
39
+ I don't know about any other Rubys.
40
+
41
+ ## Usage
42
+
43
+ ### Finding contributions
44
+
45
+ The main idea behind contributions is to make it easy to add your open
46
+ source contributions to a resume or website.
47
+
48
+ A Contributions object takes a hash as an argument. The only required
49
+ key is `:username`. This is the user's github username. From here you
50
+ have a few options. You can manually update the list of repositories
51
+ (see below). When you are ready you can use the
52
+ `.contributions_as_hash` method to return the user's contributions.
53
+
54
+ Finding all a user's contributions is very computationally intensive: we
55
+ actually create a clone (in a temporary directory :)) of every forked
56
+ repository and look for contributions via a `git log --author=username`
57
+ command. The first time you call this method it may take a while to
58
+ return the hash (especially if you have forks of really big projects).
59
+ Since you might want to access this hash multiple times, the result is
60
+ cached. If you want or need to update this for some reason, use the
61
+ `.reload_contributions` method and then `.contributions_as_hash` to get
62
+ the new results.
63
+
64
+ ### But which projects?
65
+
66
+ By default, contributions assumes that a user has contributed to every
67
+ forked OSS project in his or her github account. When you generate your
68
+ list of contributions, contributions will look for contributions in
69
+ every forked repository in your account. (Note: it will look not for
70
+ additions you have made locally, but for additions that have been merged
71
+ into the forked project; it looks for commits in that project for which
72
+ you are either an author or committer.)
73
+
74
+ Some people have lots of forks that they don't contribute to. In that
75
+ case, you can pass an array of projects to ignore to contributions:
76
+
77
+ Contributions::Contributions.new(:username => 'u', :remove => ['homebrew'])
78
+
79
+ If you have contributed to projects that you have not forked (perhaps
80
+ you keep a tidy github account :)), you can add those in by passing an
81
+ array of projects to contributions:
82
+
83
+ Contributions::Contributions.new(:username => 'u', :add => ['rubinius/rubinius'])
84
+
85
+ Notice that you must pass both the repository name and the username in
86
+ this case.
87
+
88
+ Finally, you might want to only get your contributions to a certain set
89
+ of repositories, ignoring any others (e.g., any others your forked).
90
+
91
+ Contributions::Contributions.new(:username => 'u', :only => ['rubinius/rubinius'])
92
+
93
+ The envisioned use case for this command is when you have lots of forked
94
+ repositories, perhaps even ones you have contributed to, but only care
95
+ to get your contributions for one.
96
+
97
+ ### Adding or subtracting contributions
98
+
99
+ Before you determine the contributions for a user, you might need to
100
+ alter the list of repositories you are looking at. You can find out
101
+ which repositories are currently being considered with the
102
+ `repositories` method:
103
+
104
+ c = Contributions::Contributions.new(:username => 'u')
105
+ c.repositories
106
+ # ['rubinius/rubinius', 'mxcl/homebrew']
107
+
108
+ You can find out just the project names with the `project_names` method
109
+
110
+ c = Contributions::Contributions.new(:username => 'u')
111
+ c.project_names
112
+ # ['rubinius', 'homebrew']
113
+
114
+ You can subtract a repository using the `remove` method. The `remove`
115
+ method takes either a string (in the case of a single repository) or an
116
+ array of strings as an argument. The repositories should be specified
117
+ in the username/repository pattern.
118
+
119
+ c = Contributions::Contributions.new(:username => 'u')
120
+ c.repositories
121
+ # ['sinatra/sinatra', 'rubinius/rubinius', 'mxcl/homebrew']
122
+ c.remove('sinatra/sinatra')
123
+ # ['rubinius/rubinius', 'mxcl/homebrew']
124
+ c.remove(['rubinius/rubinius', 'mxcl/homebrew'])
125
+ # []
126
+
127
+ You can add a repository using the `add` command. It takes arguments
128
+ just like `remove`.
129
+
130
+ c = Contributions::Contributions.new(:username => 'u')
131
+ c.repositories
132
+ # []
133
+ c.add('sinatra/sinatra')
134
+ # ['sinatra/sinatra']
135
+ c.add(['rubinius/rubinius', 'mxcl/homebrew'])
136
+ # ['sinatra/sinatra', 'rubinius/rubinius', 'mxcl/homebrew']
137
+
138
+ ### Determining your contributions
139
+
140
+ c = Contributions::Contributions.new(:username => 'u')
141
+ # => # does not determine the contributions
142
+ c.contributions_as_hash
143
+ # => {:project1 => [...], :project2 => [...] ... }
144
+
145
+ ### Getting the information out
146
+
147
+ You access commits via `contributions_as_hash`:
148
+
149
+ c = Contributions::Contributions.new(:username => 'u')
150
+ c.repositories
151
+ # ['sinatra/sinatra', 'rubinius/rubinius']
152
+ c.contributions_as_hash
153
+ # => {:'sinatra/sinatra' => ["commit data", "commit data"], :'rubinius/rubinius' => ["commit data"]}
154
+
155
+
156
+ ## Contributing
157
+
158
+ 1. Fork it
159
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
160
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
161
+ 4. Push to the branch (`git push origin my-new-feature`)
162
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ desc "Run all our tests"
8
+ task :test do
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "test"
11
+ t.pattern = "test/**/*_test.rb"
12
+ t.verbose = false
13
+ end
14
+ end
15
+
16
+ task :default => :test
data/TODO.markdown ADDED
@@ -0,0 +1,6 @@
1
+ # Things to add
2
+
3
+ * Some sort of progress option for `.load_contributions`. It takes
4
+ forever. It would be nice if you knew what it was working on.
5
+ * Silence the 'unpacking objects: ...' output that comes from somewhere.
6
+ Where is that comming from?
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/contributions/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Charlie Tanksley"]
6
+ gem.email = ["charlie.tanksley@gmail.com"]
7
+ gem.description = %q{Gather your contributions to OSS projects (hosted on github!).}
8
+ gem.summary = %q{A gem to find all your contributions to OSS projects on github and make that information easy to access.}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "contributions"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Contributions::VERSION
17
+
18
+ gem.add_dependency('json')
19
+
20
+ gem.add_development_dependency('riot')
21
+ gem.add_development_dependency('rake')
22
+ end
@@ -0,0 +1,153 @@
1
+ require 'json'
2
+
3
+ module Contributions
4
+ class Contributions
5
+
6
+ # opts - a Hash with, at the very least, a username. Optional
7
+ # arguments include :remove (to ignore some repository),
8
+ # :add (to add), and :only (to focus).
9
+ def initialize(opts={})
10
+ @username = opts.delete(:username)
11
+ setup_repositories(opts)
12
+ end
13
+
14
+ # Public: Add a repository (or array of repositories).
15
+ #
16
+ # repos - a 'username/repository' String or Array of such strings.
17
+ #
18
+ # Returns the updated array of repositories.
19
+ def add(repos)
20
+ @repositories.add(repos)
21
+
22
+ repositories
23
+ end
24
+
25
+ # Public: Return a user's OSS contributions as a hash. If the hash
26
+ # hasn't already been determined, the contributions are all looked
27
+ # up and stashed in an ivar: @contributions. If @contributions
28
+ # already exists, it is returned without the costly lookup being
29
+ # performed.
30
+ #
31
+ # Returns a Hash.
32
+ def contributions_as_hash
33
+ load_contributions unless @contributions
34
+ @contributions
35
+ end
36
+
37
+ # Public: Determine a user's contributions and load the
38
+ # @contributions ivar.
39
+ #
40
+ # Returns a Hash.
41
+ def load_contributions
42
+ @contributions = Hash.new
43
+ repositories.each do |f|
44
+ conts = get_contributions(f)
45
+ @contributions[f] = conts unless conts.empty?
46
+ end
47
+
48
+ @contributions
49
+ end
50
+
51
+ # Public: Replace the user's forked repositories with the specified
52
+ # repositories.
53
+ #
54
+ # repos - a 'username/repository_name' string, or an array of such
55
+ # strings.
56
+ #
57
+ # Returns the updated array of repositories.
58
+ def only(repos)
59
+ @repositories.only repos
60
+
61
+ repositories
62
+ end
63
+
64
+ # Public: Provide the names for all the forked projects.
65
+ #
66
+ # Example:
67
+ #
68
+ # user.repositories
69
+ # # => ['r/r', 's/s']
70
+ # user.project_names
71
+ # # => ['r', 's']
72
+ #
73
+ # Returns an Array.
74
+ def project_names
75
+ repositories.map { |s| s.match(/[^\/]*$/)[0] }
76
+ end
77
+
78
+ # Public: Remove a repository (or array of repositories).
79
+ #
80
+ # repos - a 'username/repository' String or Array of such strings.
81
+ #
82
+ # Returns the updated array of repositories.
83
+ def remove(repos)
84
+ @repositories.remove(repos)
85
+
86
+ repositories
87
+ end
88
+
89
+ # Public: Accessor method for the @repositories ivar.
90
+ #
91
+ # array - an array of 'username/repository_name' strings.
92
+ #
93
+ # Returns nothing.
94
+ def repositories=(array)
95
+ @repositories = RepositoryList.new(array)
96
+ end
97
+
98
+ # Public: Accessor method for the @repositories ivar.
99
+ #
100
+ # Returns an Array of 'username/repository_name' strings.
101
+ def repositories
102
+ @repositories.list
103
+ end
104
+
105
+ # Internal: attr_accessor for @contributions. This method really only
106
+ # exists for testing.
107
+ #
108
+ # hash - a hash.
109
+ #
110
+ # Returns a Hash.
111
+ def contributions=(hash)
112
+ @contributions = hash
113
+ end
114
+
115
+ # Internal: attr_reader for @contributions. This method really only
116
+ # exists for testing.
117
+ #
118
+ # Returns a Hash.
119
+ def contributions
120
+ @contributions
121
+ end
122
+
123
+ # Internal: Generate an array of forked repositories for the user.
124
+ # This array is set as the @repositories variable.
125
+ #
126
+ # opts - an array with, possible, keys for :only, :remove, and :add.
127
+ #
128
+ # Returns an Array of repositories.
129
+ def setup_repositories(opts)
130
+ @repositories = RepositoryList.new(GithubAPI.forks(@username))
131
+ update(opts)
132
+ end
133
+
134
+ # Internal: Get the user's contributions to the repository.
135
+ #
136
+ # Returns a Hash.
137
+ def get_contributions(repository)
138
+ Git.contributions GithubAPI.name(@username), repository
139
+ end
140
+
141
+ # Internal: Combine the user's explicit preferences with an array of
142
+ # forks.
143
+ #
144
+ # Returns an Array.
145
+ def update(opts)
146
+ opts.each_pair do |k,v|
147
+ @repositories.send(k.to_sym, v)
148
+ end
149
+
150
+ repositories
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,66 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+ require 'contributions/string_utils'
4
+
5
+ module Contributions
6
+ class Git
7
+
8
+ ENDING = " CONTRIBUTIONS_ENDING "
9
+ KEYS = [:sha, :date, :subject, :body]
10
+ SEPARATOR = " CONTRIBUTIONS_SEPARATOR "
11
+
12
+ # Public: Get all the contributions in a repository by a user
13
+ # (contributions for which the user is the *author*).
14
+ #
15
+ # user - a user's name (the name that shows up as the committer or
16
+ # author---e.g., 'John Smith'
17
+ # repository - a 'username/repository_name' string.
18
+ #
19
+ # Returns an Array of Hashes with keys for :sha, :date, :subject, :body
20
+ def self.contributions(user, repository)
21
+ log = self.clone(repository) { self.read_log(user) }
22
+ StringUtils.string_to_hash(log, KEYS, SEPARATOR, ENDING)
23
+ end
24
+
25
+ # Public: Clone a repository and run the block passed inside the
26
+ # newly cloned repository.
27
+ #
28
+ # repository - a 'username/repository_name' string.
29
+ # block - a block to be executed inside the newly cloned
30
+ # directory.
31
+ #
32
+ # Returns the return value of the block.
33
+ def self.clone(repository, &block)
34
+ value = ''
35
+ repo_name = repository.match(/([^\/]*$)/)[1]
36
+ Dir.mktmpdir(repo_name) do |dir|
37
+ cloned_repo_name = dir + '/' + repo_name
38
+ system "git clone -q https://github.com/#{repository} #{cloned_repo_name}"
39
+ Dir.chdir(cloned_repo_name) do
40
+ value = yield
41
+ end
42
+
43
+ FileUtils.rm_rf(cloned_repo_name)
44
+ end
45
+
46
+ value
47
+ end
48
+
49
+ # Internal: The command to read the git log.
50
+ #
51
+ # user - the user's name.
52
+ #
53
+ # Returns nothing.
54
+ def self.read_log(user)
55
+ # We want a string returned (for parsing); so use read over
56
+ # readlines.
57
+ IO.popen("git log --author='#{user}' --format='#{self.log_format}' --no-color") { |io| io.read }
58
+ end
59
+
60
+ def self.log_format
61
+ ["%h", "%ci", "%s", "%b"].join(SEPARATOR) << ENDING
62
+ end
63
+
64
+
65
+ end
66
+ end
@@ -0,0 +1,62 @@
1
+ require 'open-uri'
2
+ require 'json'
3
+
4
+ module Contributions
5
+ class GithubAPI
6
+
7
+ # Public: Get just the user's repositories that are forks.
8
+ #
9
+ # Returns an Array.
10
+ def self.forks(username)
11
+ forks = self.repos(username).select { |r| r["fork"] == true }
12
+ repo_names = forks.map { |r| r["owner"]["login"] + '/' + r["name"] }
13
+ repo_names.map { |r| self.parent(r) }
14
+ end
15
+
16
+ # Public: Get the name of the user.
17
+ #
18
+ # username - github username.
19
+ #
20
+ # Returns a String.
21
+ def self.name(username)
22
+ self.user(username)["name"]
23
+ end
24
+
25
+ # Public: Get the name of the forked repository.
26
+ #
27
+ # repository - a 'username/repository_name' string.
28
+ #
29
+ # Returns a String.
30
+ def self.parent(repository)
31
+ username, repo_name = repository.split('/')
32
+ repo_info = self.repository(repository)
33
+ repo_info["parent"]["owner"]["login"] + '/' + repo_name
34
+ end
35
+
36
+ # Public: Get all the user's repositories.
37
+ #
38
+ # Returns an Array.
39
+ def self.repos(username)
40
+ JSON.parse(open("https://api.github.com/users/#{username}/repos?per_page=100") { |f| f.read } )
41
+ end
42
+
43
+ # Internal: Get the user info (all of it) from github.
44
+ #
45
+ # username - github username.
46
+ #
47
+ # Returns a Hash.
48
+ def self.user(username)
49
+ JSON.parse(open("https://api.github.com/users/#{username}") { |f| f.read } )
50
+ end
51
+
52
+ # Internal: Get the repository info (all of it) from github.
53
+ #
54
+ # repository - a 'username/repository_name' string.
55
+ #
56
+ # Returns a Hash.
57
+ def self.repository(repository)
58
+ JSON.parse(open("https://api.github.com/repos/#{repository}") { |f| f.read } )
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,56 @@
1
+ module Contributions
2
+ class RepositoryList
3
+
4
+ attr_reader :list
5
+
6
+ def initialize(*args)
7
+ @list = [args].flatten
8
+ end
9
+
10
+ # Public: Add a string or array of strings to the repository list.
11
+ #
12
+ # repos - a string or an array of strings (each of which is a
13
+ # 'username/repo')
14
+ #
15
+ # Returns a RepositoryList
16
+ def add(repos)
17
+ @list.push(repos).flatten!
18
+ self
19
+ end
20
+
21
+ # Public: Turn repositories into key value pairs.
22
+ #
23
+ # Returns an Array of Hashes {:username, :repository}
24
+ def key_value_pairs
25
+ results = []
26
+ @list.each do |e|
27
+ p = e.split('/')
28
+ results.push Hash[:username => p[0], :repository => p[1]]
29
+ end
30
+
31
+ results
32
+ end
33
+
34
+ # Public: Replace list of repositories with the list provided.
35
+ #
36
+ # repos - a string or an array of strings (each of which is a
37
+ # 'username/repo')
38
+ #
39
+ # Returns a RepositoryList
40
+ def only(repos)
41
+ @list = [repos].flatten
42
+ self
43
+ end
44
+
45
+ # Public: Remove a string or array of strings from the repository
46
+ # list.
47
+ # repos - a string or an array of strings (each of which is a
48
+ # 'username/repo')
49
+ #
50
+ # Returns a RepositoryList
51
+ def remove(repos)
52
+ @list.delete_if { |e| [repos].flatten.include? e }
53
+ self
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,133 @@
1
+ module Contributions
2
+ class StringUtils
3
+
4
+ # Public: Read a long string of commit data and turn it into a
5
+ # hash.
6
+ #
7
+ # string - a string of commit data.
8
+ # separator - the separator between values in an entry.
9
+ # ending - the separator between entries.
10
+ # keys - an Array of keys for the final hash.
11
+ #
12
+ # Returns a Hash.
13
+ def self.string_to_hash(string, keys, separator, *ending)
14
+ s = string.dup
15
+ s_as_array = ending.empty? ? [s] : self.split!(s, ending[0])
16
+
17
+ s_as_array.map! { |e| self.split!(e, separator) }
18
+ self.remove_empty(s_as_array)
19
+
20
+ s_as_array.map! do |e|
21
+ e.map! { |line| line.strip }
22
+ self.zip_to_hash(keys, e)
23
+ end
24
+
25
+ s_as_array.map do |e|
26
+ self.short_dates(e)
27
+ end
28
+
29
+ # s_as_array.map do |e|
30
+ # self.zip_to_hash(keys, e)
31
+ # end
32
+
33
+
34
+ # self.split!(s, ending).each do |e|
35
+ # self.remove_empty(self.split!(e, separator))
36
+ # end
37
+
38
+ # Now we need the zip move, then to a hash, then replace nils with
39
+ # ''
40
+
41
+ # s = string.dup
42
+ # array = string.split()
43
+ # small_arrays = array.map { |e| e.split(separator).map { |l| l.strip } }
44
+ # small_arrays.delete_if { |a| a[0] == "" and a[1] == nil }
45
+ # small_arrays.map! { |a| [:sha, :date, :subject, :body].zip a }
46
+ # small_arrays
47
+
48
+
49
+
50
+
51
+ # results = []
52
+ # s = string.dup
53
+ # s.gsub!(/(\d{4}-\d{2}-\d{2})/) { |f| " BREAK " + f }
54
+ # s = s.split(" BREAK ")
55
+ # s.delete_if { |l| l.empty? }
56
+ # s.each do |line|
57
+ # m = /(?<date>\d{4}-\d{2}-\d{2})/.match line
58
+ # results.push m["date"]
59
+ # end
60
+
61
+ # results
62
+ end
63
+
64
+ # Public: Split the string on the give separator.
65
+ #
66
+ # separator - the character(s) on which to split the string.
67
+ #
68
+ # Returns a modified version of the string.
69
+ def self.split!(string, separator)
70
+ string.split(separator)
71
+ end
72
+
73
+ # Internal: Remove any empty arrays after a split.
74
+ #
75
+ # array - an array of Strings.
76
+ #
77
+ # Returns an Array of Strings (modified)
78
+ def self.remove_empty(array)
79
+ array.delete_if { |a| self.practically_empty?(a[0]) && a[1].nil? }
80
+ end
81
+
82
+ # Internal: Determine whether a string has any content.
83
+ #
84
+ # Examples:
85
+ #
86
+ # StringUtils.practically_empty?('')
87
+ # # => true
88
+ # StringUtils.practically_empty?("\n\n")
89
+ # # => true
90
+ # StringUtils.practically_empty?("a\n")
91
+ # # => false
92
+ #
93
+ # Returns a Boolean.
94
+ def self.practically_empty?(arg)
95
+ if arg.empty?
96
+ return true
97
+ elsif !arg.match /\w/
98
+ return true
99
+ else
100
+ return false
101
+ end
102
+ end
103
+
104
+ # Internal: Convert a pair of arrays into a hash with the first as
105
+ # keys.
106
+ #
107
+ # keys - an Array of keys.
108
+ # values - an Array of values.
109
+ #
110
+ # Returns a Hash.
111
+ def self.zip_to_hash(keys, values)
112
+ value = Hash.new
113
+ zipped = keys.zip values
114
+ zipped.each do |pair|
115
+ value[pair.first] = pair.last || ''
116
+ end
117
+
118
+ value
119
+ end
120
+
121
+ # Internal: Convert date format to a simpler one.
122
+ #
123
+ # hash - a hash with a :date key
124
+ #
125
+ # Returns a Hash.
126
+ def self.short_dates(hash)
127
+ old_date = hash[:date]
128
+ hash[:date] = old_date.match(/(\d{4}-\d{2}-\d{2})/)[1]
129
+
130
+ hash
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,3 @@
1
+ module Contributions
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "contributions/version"
2
+ require 'contributions/contributions'
3
+ require 'contributions/git'
4
+ require 'contributions/github_api'
5
+ require 'contributions/repository_list'
6
+ require 'contributions/string_utils'
7
+
8
+ module Contributions
9
+ end
@@ -0,0 +1,78 @@
1
+ require 'teststrap'
2
+ require 'contributions'
3
+
4
+ context "Contributions::Contributions" do
5
+ hookup { RR.reset }
6
+ helper(:full) { Contributions::Contributions.new(:username => 'charlietanksley') }
7
+
8
+ context ".new finds out about all forks" do
9
+ setup { full }
10
+ asserts(:repositories).includes "msanders/snipmate.vim"
11
+ asserts(:repositories).includes "thumblemonks/riot"
12
+ end
13
+
14
+ context ".new with an :only modifies the forks" do
15
+ setup { Contributions::Contributions.new(:username => 'charlietanksley', :only => 'thumblemonks/riot') }
16
+ asserts(:repositories).equals ['thumblemonks/riot']
17
+ end
18
+
19
+ context ".new with a :remove subtracts a fork" do
20
+ setup { Contributions::Contributions.new(:username => 'charlietanksley', :remove => 'thumblemonks/riot') }
21
+ denies(:repositories).includes 'thumblemonks/riot'
22
+ asserts("we have subtracted 1") { full.repositories.count - topic.repositories.count == 1 }.equals true
23
+ end
24
+
25
+ context ".new will :remove with an array as an argument" do
26
+ setup { Contributions::Contributions.new(:username => 'charlietanksley', :remove => ['thumblemonks/riot', 'rubinius/rubinius']) }
27
+ denies(:repositories).includes 'thumblemonks/riot'
28
+ denies(:repositories).includes 'rubinius/rubinius'
29
+ end
30
+
31
+ context ".new with an :add adds a fork" do
32
+ setup { Contributions::Contributions.new(:username => 'charlietanksley', :add => 'thumblemonks/chicago') }
33
+ asserts(:repositories).includes 'thumblemonks/chicago'
34
+ asserts("we have added 1") { topic.repositories.count - full.repositories.count == 1 }.equals true
35
+ end
36
+
37
+ context ".new will :add with an array as an argument" do
38
+ setup { Contributions::Contributions.new(:username => 'charlietanksley', :add => ['t/r', 'r/r']) }
39
+ asserts(:repositories).includes 't/r'
40
+ asserts(:repositories).includes 'r/r'
41
+ end
42
+ end
43
+
44
+ context "a full run of Contributions::Contributions" do
45
+ setup { Contributions::Contributions.new(:username => 'charlietanksley',
46
+ :only => ['thumblemonks/riot', 'msanders/snipmate.vim', 'davetron5000/methadone']) }
47
+
48
+ context "starts with a list of repositories" do
49
+ asserts(:repositories).equals ['thumblemonks/riot', 'msanders/snipmate.vim', 'davetron5000/methadone']
50
+ end
51
+
52
+ context "does not start with any contributions" do
53
+ asserts(:contributions).nil
54
+ end
55
+
56
+ context "grabs the contributions when asked" do
57
+
58
+ context "with the right keys" do
59
+ setup { topic.contributions_as_hash.keys }
60
+ asserts_topic.includes 'thumblemonks/riot'
61
+ asserts_topic.includes 'davetron5000/methadone'
62
+ end
63
+
64
+ context "and saves them off" do
65
+ hookup { topic.contributions_as_hash }
66
+ setup { topic.contributions.keys }
67
+ asserts_topic.includes 'thumblemonks/riot'
68
+ asserts_topic.includes 'davetron5000/methadone'
69
+ end
70
+ end
71
+
72
+ context "when the list is updated" do
73
+ hookup { topic.remove ['thumblemonks/riot', 'davetron5000/methadone'] }
74
+ hookup { topic.load_contributions }
75
+ asserts { topic.contributions_as_hash.keys }.equals []
76
+ asserts(:contributions_as_hash).equals Hash[]
77
+ end
78
+ end
@@ -0,0 +1,112 @@
1
+ require 'teststrap'
2
+ require 'contributions'
3
+
4
+ context "Contributions public api with mocked .new" do
5
+ hookup do
6
+ stub(Contributions::GithubAPI).forks(anything) { ['r/r', 's/s'] }
7
+ end
8
+
9
+ setup { Contributions::Contributions.new(:username => 'charlietanksley') }
10
+
11
+ context ".new" do
12
+ context "assignments" do
13
+ asserts_topic.assigns :username
14
+ asserts_topic.assigns :repositories
15
+ end
16
+
17
+ asserts(:repositories).equals ['r/r', 's/s']
18
+ end
19
+
20
+ context ".add will add repositories" do
21
+ context "if given a string" do
22
+ setup { topic.add 'added/added' }
23
+ asserts_topic.includes 'added/added'
24
+ end
25
+
26
+ context "if given an array" do
27
+ setup { topic.add ['added/first', 'added/second'] }
28
+ asserts_topic.includes 'added/first'
29
+ asserts_topic.includes 'added/second'
30
+ end
31
+ end
32
+
33
+ context ".contributions_as_hash" do
34
+ context "does not reload hash if it already exists" do
35
+ hookup do
36
+ topic.contributions = Hash[:k => 'v']
37
+ dont_allow(topic).load_contributions
38
+ end
39
+
40
+ asserts(:contributions_as_hash).equals Hash[:k => 'v']
41
+ end
42
+
43
+ context "calls #load_contributions if there aren't any contributions yet" do
44
+ hookup { mock(topic).load_contributions { topic.contributions = Hash[:k => 'v'] } }
45
+ asserts(:contributions_as_hash).equals Hash[:k => 'v']
46
+ end
47
+ end
48
+
49
+ context ".only will trade out repositories" do
50
+ context "if given a string" do
51
+ setup { topic.only 'added/added' }
52
+ asserts_topic.includes 'added/added'
53
+ denies_topic.includes 'r/r'
54
+ denies_topic.includes 's/s'
55
+ end
56
+
57
+ context "if given an array" do
58
+ setup { topic.only ['added/first', 'added/second'] }
59
+ asserts_topic.includes 'added/first'
60
+ asserts_topic.includes 'added/second'
61
+ denies_topic.includes 'r/r'
62
+ denies_topic.includes 's/s'
63
+ end
64
+ end
65
+
66
+ context ".project_names returns an array of project names" do
67
+ asserts(:project_names).equals { ['r', 's'] }
68
+ end
69
+
70
+ context ".load_contributions" do
71
+ context "actually calls out to get the contributions for each fork" do
72
+ hookup do
73
+ mock(Contributions::Git).contributions('Charlie Tanksley', 'r/r') { Hash[:sha => 1] }
74
+ mock(Contributions::Git).contributions('Charlie Tanksley', 's/s') { Hash[:sha => 2] }
75
+ end
76
+
77
+ denies(:load_contributions).nil
78
+ end
79
+
80
+ context "returns a hash with repository names as keys" do
81
+ hookup do
82
+ stub(Contributions::Git).contributions('Charlie Tanksley', 'r/r') { Hash[:sha => 1] }
83
+ stub(Contributions::Git).contributions('Charlie Tanksley', 's/s') { Hash[:sha => 2] }
84
+ end
85
+
86
+ asserts(:load_contributions).equals Hash['r/r' => Hash[:sha => 1], 's/s' => Hash[:sha => 2]]
87
+ end
88
+
89
+ context "stashes the results in an instance variable" do
90
+ hookup do
91
+ stub(Contributions::Git).contributions('Charlie Tanksley', 'r/r') { Hash[:sha => 1] }
92
+ stub(Contributions::Git).contributions('Charlie Tanksley', 's/s') { Hash[:sha => 2] }
93
+ topic.load_contributions
94
+ end
95
+
96
+ asserts(:contributions).equals Hash['r/r' => Hash[:sha => 1], 's/s' => Hash[:sha => 2]]
97
+ end
98
+ end
99
+
100
+ context ".remove will drop repositories" do
101
+ context "if given a string" do
102
+ setup { topic.remove 'r/r' }
103
+ denies_topic.includes 'added/added'
104
+ end
105
+
106
+ context "if given an array" do
107
+ setup { topic.remove ['r/r', 's/s'] }
108
+ denies_topic.includes 'r/r'
109
+ denies_topic.includes 's/s'
110
+ end
111
+ end
112
+ end
data/test/git_test.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'teststrap'
2
+ require 'contributions/git'
3
+
4
+ context "Contributions::Git" do
5
+ context ".contributions" do
6
+ context 'determines all the user\'s contributions' do
7
+ setup { Contributions::Git.contributions('Charlie Tanksley', 'thumblemonks/riot') }
8
+
9
+ asserts_topic.size 6
10
+ end
11
+ end
12
+
13
+ context ".clone creates a clone and runs the block you pass it" do
14
+ helper(:command) { "git log --author='Charlie Tanksley' --format='%h %ci %s %b' --no-color" }
15
+ setup do
16
+ Contributions::Git.clone('thumblemonks/riot') { IO.popen(command) { |io| io.readlines } }
17
+ end
18
+
19
+ asserts_topic.size 6
20
+ end
21
+
22
+ context ".log_format" do
23
+ setup { Contributions::Git.log_format }
24
+
25
+ asserts_topic.equals "%h CONTRIBUTIONS_SEPARATOR %ci CONTRIBUTIONS_SEPARATOR %s CONTRIBUTIONS_SEPARATOR %b CONTRIBUTIONS_ENDING "
26
+ end
27
+
28
+ end
@@ -0,0 +1,41 @@
1
+ require 'teststrap'
2
+ require 'contributions/github_api'
3
+
4
+ context "Contributions::GithubAPI" do
5
+ context ".forks" do
6
+ setup { Contributions::GithubAPI.forks('charlietanksley') }
7
+
8
+ context "gets a list of all the forks" do
9
+ setup { topic.count }
10
+ denies_topic.equals 0
11
+ end
12
+
13
+ context "gets the original names, not the name of the fork" do
14
+ denies_topic.includes 'charlietanksley/riot'
15
+ asserts_topic.includes 'thumblemonks/riot'
16
+ end
17
+ end
18
+
19
+ context ".repos gets all the repositories when there are less than 100" do
20
+ setup { Contributions::GithubAPI.repos('rubinius').count }
21
+
22
+ asserts_topic.equals JSON.parse(open("https://api.github.com/users/rubinius") { |f| f.read })["public_repos"]
23
+ end
24
+
25
+ # Pending
26
+ context ".repos gets all the repositories when there are tons" do
27
+ # setup { Contributions::GithubAPI.repos('vim-scripts').count }
28
+
29
+ # asserts_topic.equals JSON.parse(open("https://api.github.com/users/vim-scripts") { |f| f.read })["public_repos"]
30
+ end
31
+
32
+ context ".name returns the name of the user" do
33
+ setup { Contributions::GithubAPI.name('charlietanksley') }
34
+ asserts_topic.equals 'Charlie Tanksley'
35
+ end
36
+
37
+ context ".parent returns the name of the forked repository" do
38
+ setup { Contributions::GithubAPI.parent('charlietanksley/riot') }
39
+ asserts_topic.equals 'thumblemonks/riot'
40
+ end
41
+ end
@@ -0,0 +1,73 @@
1
+ require 'teststrap'
2
+ require 'contributions/repository_list'
3
+
4
+ context "RepositoryList" do
5
+ context "#add" do
6
+ context "with a blank canvas" do
7
+ setup { Contributions::RepositoryList.new }
8
+
9
+ context "adds a single string" do
10
+ hookup { topic.add('rubinius/rubinius') }
11
+ asserts(:list).equals ['rubinius/rubinius']
12
+ end
13
+
14
+ context "adds an array of strings" do
15
+ hookup { topic.add ['rubinius/rubinius', 'vim-scripts/test.vim'] }
16
+ asserts(:list).equals ['rubinius/rubinius', 'vim-scripts/test.vim']
17
+ end
18
+ end
19
+
20
+ context "with a starting array" do
21
+ setup { Contributions::RepositoryList.new(['s/s']) }
22
+
23
+ context "adds a single string" do
24
+ hookup { topic.add 'rubinius/rubinius' }
25
+ asserts(:list).equals ['s/s', 'rubinius/rubinius']
26
+ end
27
+
28
+ context "adds an array of strings" do
29
+ hookup { topic.add ['rubinius/rubinius', 'vim-scripts/test.vim'] }
30
+ asserts(:list).equals ['s/s', 'rubinius/rubinius', 'vim-scripts/test.vim']
31
+ end
32
+ end
33
+ end
34
+
35
+ context "#remove" do
36
+ setup { Contributions::RepositoryList.new(['s/s', 's/ss', 'o/other']) }
37
+
38
+ context "selectively removes a single repo" do
39
+ hookup { topic.remove 's/s' }
40
+ asserts(:list).equals ['s/ss', 'o/other']
41
+ end
42
+
43
+ context "selectively removes an array of repos" do
44
+ hookup { topic.remove ['s/s', 's/ss'] }
45
+ asserts(:list).equals ['o/other']
46
+ end
47
+ end
48
+
49
+ context "#only completely replaces the list" do
50
+ setup { Contributions::RepositoryList.new(['s/s', 's/ss', 'o/other']) }
51
+
52
+ context "when given a single repo" do
53
+ hookup { topic.only 'a/a' }
54
+ asserts(:list).equals ['a/a']
55
+ end
56
+
57
+ context "when given an array" do
58
+ hookup { topic.only ['a/a', 'b/b'] }
59
+ asserts(:list).equals ['a/a', 'b/b']
60
+ end
61
+ end
62
+
63
+ context "#key_value_pairs splits into key value pairs" do
64
+ setup { Contributions::RepositoryList.new(['s/s', 's/ss', 'o/other']) }
65
+ asserts(:key_value_pairs).equals [{:username => 's', :repository => 's'},
66
+ {:username => 's', :repository => 'ss'},
67
+ {:username => 'o', :repository => 'other'}]
68
+ end
69
+
70
+ end
71
+
72
+
73
+
@@ -0,0 +1,62 @@
1
+ require 'teststrap'
2
+ require 'contributions/string_utils'
3
+
4
+ context "StringUtils" do
5
+ helper(:string) { "1b90c5e CONTRIBUTIONS_SEPARATOR 2011-09-13 07:49:43 -0500 CONTRIBUTIONS_SEPARATOR remove mention of deprecated exists from README CONTRIBUTIONS_SEPARATOR CONTRIBUTIONS_ENDING \n1620141 CONTRIBUTIONS_SEPARATOR 2011-12-16 19:26:58 -0500 CONTRIBUTIONS_SEPARATOR adjust input boxes to be same height as buttons CONTRIBUTIONS_SEPARATOR There was this weird thing happening where the inline submit button on a\nform would be like twice as tall as the text input area (notably on the\nuser_search form, but in a few other places it looked weird). This\nmakes all those a standard height (the same as the default Twitter\nbutton height). CONTRIBUTIONS_ENDING \n\n" }
6
+ helper(:ending) { " CONTRIBUTIONS_ENDING " }
7
+ helper(:separator) { " CONTRIBUTIONS_SEPARATOR " }
8
+
9
+ context ".split!" do
10
+ context "splits the string in place" do
11
+ setup { Contributions::StringUtils.split!(string, ending) }
12
+
13
+ asserts_topic.size 3
14
+ end
15
+ end
16
+
17
+ context ".remove_empty throws out any 'blank' arrays" do
18
+ setup { Contributions::StringUtils.remove_empty([['s'], ['', 's'], [''], ["\n\n"]]) }
19
+ asserts_topic.size 2
20
+ end
21
+
22
+ context ".zip_to_hash" do
23
+ context "creates a hash with the first array as the key" do
24
+ setup { Contributions::StringUtils.zip_to_hash([:k, :v, :e], ['key', 'value', 'empty']) }
25
+ asserts_topic.equals Hash[:k => 'key', :v => 'value', :e => 'empty']
26
+ end
27
+
28
+ context "creates a hash with the first array as the key and '' for any empty values" do
29
+ setup { Contributions::StringUtils.zip_to_hash([:k, :v, :e], ['key']) }
30
+ asserts_topic.equals Hash[:k => 'key', :v => '', :e => '']
31
+ end
32
+ end
33
+
34
+ context ".string_to_hash turns a hash of commit info into a useful hash" do
35
+ setup do
36
+ one = "1b90c5e CONTRIBUTIONS_SEPARATOR 2011-09-13 07:49:43 -0500 CONTRIBUTIONS_SEPARATOR remove mention of deprecated exists from README CONTRIBUTIONS_SEPARATOR CONTRIBUTIONS_ENDING \n"
37
+ two = "7201941 CONTRIBUTIONS_SEPARATOR 2011-09-13 07:48:53 -0500 CONTRIBUTIONS_SEPARATOR add deprecation warning to exists macro CONTRIBUTIONS_SEPARATOR CONTRIBUTIONS_ENDING \n"
38
+ three = "1620141 CONTRIBUTIONS_SEPARATOR 2011-12-16 19:26:58 -0500 CONTRIBUTIONS_SEPARATOR adjust input boxes to be same height as buttons CONTRIBUTIONS_SEPARATOR There was this weird thing happening where the inline submit button on a\nform would be like twice as tall as the text input area (notably on the\nuser_search form, but in a few other places it looked weird). This\nmakes all those a standard height (the same as the default Twitter\nbutton height). CONTRIBUTIONS_ENDING \n\n"
39
+ Contributions::StringUtils.string_to_hash([one, two, three].join, [:sha, :date, :subject, :body], separator, ending)
40
+ end
41
+
42
+ asserts_topic.equals [{:sha => "1b90c5e",
43
+ :date => "2011-09-13",
44
+ :subject => "remove mention of deprecated exists from README",
45
+ :body => ''},
46
+ {:sha=>"7201941",
47
+ :date=>"2011-09-13",
48
+ :subject=>"add deprecation warning to exists macro",
49
+ :body=>""},
50
+ {:sha=>"1620141",
51
+ :date=>"2011-12-16",
52
+ :subject=>"adjust input boxes to be same height as buttons",
53
+ :body=>"There was this weird thing happening where the inline submit button on a\nform would be like twice as tall as the text input area (notably on the\nuser_search form, but in a few other places it looked weird). This\nmakes all those a standard height (the same as the default Twitter\nbutton height)."}]
54
+ end
55
+
56
+ context ".short_dates gives you cleaner dates" do
57
+ setup { Contributions::StringUtils.short_dates(Hash[:sha=>"7201941", :date=>"2011-09-13 07:48:53 -0500", :subject=>"add deprecation warning to exists macro", :body=>""]) }
58
+
59
+ asserts_topic.equals Hash[:sha=>"7201941", :date=>"2011-09-13", :subject=>"add deprecation warning to exists macro", :body=>""]
60
+ end
61
+
62
+ end
data/test/teststrap.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'riot'
2
+ require 'riot/rr'
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: contributions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Charlie Tanksley
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: &70340630508160 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70340630508160
25
+ - !ruby/object:Gem::Dependency
26
+ name: riot
27
+ requirement: &70340630505820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70340630505820
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70340630514680 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70340630514680
47
+ description: Gather your contributions to OSS projects (hosted on github!).
48
+ email:
49
+ - charlie.tanksley@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - .travis.yml
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - TODO.markdown
61
+ - contributions.gemspec
62
+ - lib/contributions.rb
63
+ - lib/contributions/contributions.rb
64
+ - lib/contributions/git.rb
65
+ - lib/contributions/github_api.rb
66
+ - lib/contributions/repository_list.rb
67
+ - lib/contributions/string_utils.rb
68
+ - lib/contributions/version.rb
69
+ - test/contributions_integration_test.rb
70
+ - test/contributions_test.rb
71
+ - test/git_test.rb
72
+ - test/github_api_test.rb
73
+ - test/repository_list_test.rb
74
+ - test/string_utils_test.rb
75
+ - test/teststrap.rb
76
+ homepage: ''
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.11
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A gem to find all your contributions to OSS projects on github and make that
100
+ information easy to access.
101
+ test_files:
102
+ - test/contributions_integration_test.rb
103
+ - test/contributions_test.rb
104
+ - test/git_test.rb
105
+ - test/github_api_test.rb
106
+ - test/repository_list_test.rb
107
+ - test/string_utils_test.rb
108
+ - test/teststrap.rb