Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create tool for getting DCO sign off emails #34

Merged
merged 1 commit into from Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 6 additions & 20 deletions .rubocop_todo.yml
@@ -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
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 contributors 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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an issue for exporting CSV in a generic way, #18.

How about we change this to be called "emails" and output something non-structured, then implement the CSV thing generically?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still looking at what it would take to wire in more generic csv support.

end
13 changes: 13 additions & 0 deletions lib/github/commit.rb
@@ -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
@@ -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
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
@@ -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
@@ -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
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
@@ -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/opencensus-java@v0.18.0...v0.31.1)

---
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] <support@github.com>

* Updating SHAs

Signed-off-by: dependabot[bot] <support@github.com>

* Adding missing classes

Signed-off-by: Vacha Shah <vachshah@amazon.com>

* changelog change

Signed-off-by: Poojita Raj <poojiraj@amazon.com>

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Vacha Shah <vachshah@amazon.com>
Signed-off-by: Poojita Raj <poojiraj@amazon.com>
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 <vachshah@amazon.com>
Co-authored-by: Poojita Raj <poojiraj@amazon.com>
}
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
@@ -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
@@ -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', 'dev@opensearch.org'),
GitHub::Signer.new('Dev Eloper', 'dev@opensearch.org'),
GitHub::Signer.new('Dev Eloper, Esq.', 'dev@opensearch.org')
])
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', 'noreply@github.com'),
GitHub::Signer.new('dev', 'dev@opensearch.org')
])
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('dev@opensearch.org')
expect(signers.sort_for_display.last.email).to eq('noreply@github.com')
end
end
end