contributions 0.1.1

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