Skip to content

Commit

Permalink
Create tool for getting DCO sign off emails
Browse files Browse the repository at this point in the history
This tool digs through commit messages and parses out names and email
addresses from the `Signed-off-by:` tags, collects all unique email
addresses and outputs a CSV of name/email pairs.

Signed-off-by: Andrew Ross <[email protected]>
  • Loading branch information
andrross committed Dec 15, 2022
1 parent 998fd60 commit 9b45bd2
Show file tree
Hide file tree
Showing 13 changed files with 931 additions and 20 deletions.
26 changes: 6 additions & 20 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2022-12-01 14:05:15 UTC using RuboCop version 1.36.0.
# on 2022-12-06 18:50:08 UTC using RuboCop version 1.36.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -113,37 +113,23 @@ RSpec/EmptyExampleGroup:
RSpec/ExampleLength:
Max: 6

# Offense count: 14
# Offense count: 17
# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
# Include: **/*_spec*rb*, **/spec/**/*
RSpec/FilePath:
Exclude:
- 'spec/github/contributor_spec.rb'
- 'spec/github/contributors_spec.rb'
- 'spec/github/data_spec.rb'
- 'spec/github/issue_spec.rb'
- 'spec/github/issues_spec.rb'
- 'spec/github/organization_spec.rb'
- 'spec/github/progress_spec.rb'
- 'spec/github/pull_request_spec.rb'
- 'spec/github/pull_requests_spec.rb'
- 'spec/github/rate_limited_spec.rb'
- 'spec/github/repos_spec.rb'
- 'spec/github/searchable_spec.rb'
- 'spec/github/user_spec.rb'
- 'spec/github/users_spec.rb'
Enabled: false

# Offense count: 1
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Exclude:
- 'spec/github/progress_spec.rb'

# Offense count: 7
# Offense count: 12
RSpec/MultipleExpectations:
Max: 5

# Offense count: 28
# Offense count: 31
# Configuration parameters: IgnoreSharedExamples.
RSpec/NamedSubject:
Exclude:
Expand All @@ -152,7 +138,7 @@ RSpec/NamedSubject:
- 'spec/github/progress_spec.rb'
- 'spec/github/pull_requests_spec.rb'

# Offense count: 8
# Offense count: 10
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 5
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Pull Request Stats](#pull-request-stats)
- [Issues](#issues)
- [Member Bios](#member-bios)
- [DCO Signers](#dco-signers)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [Security](#security)
Expand Down Expand Up @@ -288,6 +289,14 @@ Shows users in [data/users/members.txt](data/users/members.txt) that do not have
./bin/project members check
```

#### DCO Signers

Shows name and email address from all contributors that have signed a developer certificate of origin on any commit.

```
./bin/project dco-signers --from=2022-01-01 --to=2022-01-31 --org=opensearch-project --repo=OpenSearch
```

## Contributing

See [how to contribute to this project](CONTRIBUTING.md).
Expand Down
11 changes: 11 additions & 0 deletions bin/commands/contributors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@
end
end
end

g.desc 'Create a list of all DCO signers'
g.command 'dco-signers' do |c|
c.action do |_global_options, options, _args|
org = GitHub::Organization.new(options)
signers = org.commits(options).dco_signers
signers.sort_for_display.each do |signer|
puts signer.to_s
end
end
end
end
13 changes: 13 additions & 0 deletions lib/github/commit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module GitHub
class Commit < Item
# Creates an array of Signers from all 'Signed-off-by' tags included in the
# commit message
def dco_signers
commit.message.scan(/Signed-off-by: (.+) <([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]+)>/).map do |signer|
Signer.new(signer[0], signer[1])
end
end
end
end
31 changes: 31 additions & 0 deletions lib/github/commits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module GitHub
class Commits < Items
def initialize(arr_or_options)
super arr_or_options, GitHub::Commit
end

# Gets all unique DCO signers (by email address) from all commits
def dco_signers
Signers.new(each.map(&:dco_signers).flatten)
end

def page(options)
data = $github.search_commits(query(options), per_page: 1000).items
raise 'There are 1000+ commits returned from a single query, reduce --page.' if data.size >= 1000

data.reject do |commit|
commit.commit.author.email.include?('[bot]')
end
end

def query(options = {})
GitHub::Searchables.new(options).to_a.concat(
[
"committer-date:#{options[:from]}..#{options[:to]}"
]
).compact.join(' ')
end
end
end
4 changes: 4 additions & 0 deletions lib/github/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def pull_requests(options = {})
@pull_requests ||= GitHub::PullRequests.new({ org: name, status: :merged }.merge(options))
end

def commits(options = {})
@commits ||= GitHub::Commits.new({ org: name }.merge(options))
end

def issues(options = {})
@issues ||= GitHub::Issues.new({ org: name }.merge(options))
end
Expand Down
16 changes: 16 additions & 0 deletions lib/github/signer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module GitHub
class Signer
attr_reader :email, :name

def initialize(name, email)
@name = name
@email = email
end

def to_s
"#{name},#{email}"
end
end
end
32 changes: 32 additions & 0 deletions lib/github/signers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module GitHub
class Signers < Array
def initialize(arr)
# De-dupe by email address, choosing the "best" name
by_email = {}
arr.each do |signer|
by_email[signer.email] = best_signer(by_email[signer.email], signer)
end
super by_email.values
end

# Sort all "noreply" email addresses to the bottom (for manual curation), then sort by name
def sort_for_display
Signers.new(sort_by { |signer| [signer.email.include?('noreply') ? 1 : 0, signer.name.downcase] })
end

private

def best_signer(left, right)
# The "best" name is defined by the name with the most words. For example,
# if both "dblock" and "Daniel (dB.) Doubrovkine" are encountered, then
# "Daniel (dB.) Doubrovkine" will be chosen.
if left.nil? || right.name.split.length > left.name.split.length
right
else
left
end
end
end
end
4 changes: 4 additions & 0 deletions lib/tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
require_relative 'github/repos'
require_relative 'github/pull_requests'
require_relative 'github/pull_request'
require_relative 'github/commits'
require_relative 'github/commit'
require_relative 'github/contributors'
require_relative 'github/contributor'
require_relative 'github/maintainers'
require_relative 'github/signers'
require_relative 'github/signer'
require_relative 'github/users'
require_relative 'github/user'
require_relative 'github/issues'
Expand Down

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions spec/github/commit_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

describe GitHub::Commit do
subject(:commit) do
message = %{Bump opencensus-contrib-http-util from 0.18.0 to 0.31.1 in /plugins/repository-gcs (#3633)
* Bump opencensus-contrib-http-util in /plugins/repository-gcs
Bumps [opencensus-contrib-http-util](https://github.com/census-instrumentation/opencensus-java) from 0.18.0 to 0.31.1.
- [Release notes](https://github.com/census-instrumentation/opencensus-java/releases)
- [Changelog](https://github.com/census-instrumentation/opencensus-java/blob/master/CHANGELOG.md)
- [Commits](census-instrumentation/[email protected])
---
updated-dependencies:
- dependency-name: io.opencensus:opencensus-contrib-http-util
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <[email protected]>
* Updating SHAs
Signed-off-by: dependabot[bot] <[email protected]>
* Adding missing classes
Signed-off-by: Vacha Shah <[email protected]>
* changelog change
Signed-off-by: Poojita Raj <[email protected]>
Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Vacha Shah <[email protected]>
Signed-off-by: Poojita Raj <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <dependabot[bot]@users.noreply.github.com>
Co-authored-by: Vacha Shah <[email protected]>
Co-authored-by: Poojita Raj <[email protected]>
}
resource = Sawyer::Resource.new(Sawyer::Agent.new('fake'), { commit: { message: message } })
described_class.new(resource)
end

it 'parses signers from a commit message' do
expect(commit.dco_signers.count).to eq 7
expect(commit.dco_signers.map(&:name)).to eq ['dependabot[bot]', 'dependabot[bot]', 'Vacha Shah', 'Poojita Raj', 'dependabot[bot]', 'Vacha Shah', 'Poojita Raj']
end
end
25 changes: 25 additions & 0 deletions spec/github/commits_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

describe GitHub::Commits do
context 'with contributors' do
context 'with org' do
context 'with january 2022' do
context 'with OpenSearch commits', vcr: { cassette_name: 'search/opensearch-project/commits_2022-01-01_2022-01-31' } do
subject(:commits) do
described_class.new(org: 'opensearch-project', repo: 'OpenSearch', from: Date.new(2022, 1, 1), to: Date.new(2022, 1, 31), page: 7)
end

it 'fetches commits between two dates' do
expect(commits.count).to eq 62
expect(commits.first['sha']).to eq 'db23f72a2a5da1f21d674bde3a9d1cbe4fb74b19'
end

it 'collects DCO signers from commits' do
expect(commits.dco_signers.count).to eq 25
expect(commits.dco_signers.first.name).to eq 'Tianli Feng'
end
end
end
end
end
end
33 changes: 33 additions & 0 deletions spec/github/signers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

describe GitHub::Signers do
context 'with duplicate emails' do
subject(:signers) do
described_class.new([
GitHub::Signer.new('dev', '[email protected]'),
GitHub::Signer.new('Dev Eloper', '[email protected]'),
GitHub::Signer.new('Dev Eloper, Esq.', '[email protected]')
])
end

it 'chooses the best name' do
expect(signers.size).to eq(1)
expect(signers.first.name).to eq('Dev Eloper, Esq.')
end
end

context 'with noreply email addresses' do
subject(:signers) do
described_class.new([
GitHub::Signer.new('Mis Configurer', '[email protected]'),
GitHub::Signer.new('dev', '[email protected]')
])
end

it 'sorts noreply email addresses to the end' do
expect(signers.size).to eq(2)
expect(signers.sort_for_display.first.email).to eq('[email protected]')
expect(signers.sort_for_display.last.email).to eq('[email protected]')
end
end
end

0 comments on commit 9b45bd2

Please sign in to comment.