Skip to content

Commit cdbe4e5

Browse files
authored
add a new 'dart bin/report.dart contributors' command (#311)
- add a new 'dart bin/report.dart contributors' command This will collect the top contributors to dart-lang git repos from the last 365 days. --- - [x] I’ve reviewed the contributor guide and applied the relevant portions to this PR. <details> <summary>Contribution guidelines:</summary><br> - See our [contributor guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md) for general expectations for PRs. - Larger or significant changes should be discussed in an issue before creating a PR. - Contributions to our repos should follow the [Dart style guide](https://dart.dev/guides/language/effective-dart) and use `dart format`. - Most changes should add an entry to the changelog and may need to [rev the pubspec package version](https://github.com/dart-lang/sdk/blob/main/docs/External-Package-Maintenance.md#making-a-change). - Changes to packages require [corresponding tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing). Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback. </details>
1 parent 52e4ceb commit cdbe4e5

File tree

2 files changed

+275
-2
lines changed

2 files changed

+275
-2
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:github/github.dart';
8+
import 'package:graphql/client.dart';
9+
10+
import 'src/common.dart';
11+
12+
class ContributorsCommand extends ReportCommand {
13+
ContributorsCommand()
14+
: super(
15+
'contributors',
16+
'Run a report on repo contributors.\n'
17+
'Defaults to the last 365 days.') {
18+
argParser
19+
..addFlag(
20+
'dart-lang',
21+
negatable: false,
22+
help: 'Return stats for all dart-lang repos (otherwise, this defaults '
23+
'to the main Dart and Flutter repos).',
24+
)
25+
..addFlag(
26+
'monthly',
27+
negatable: false,
28+
help: 'Last 30 days.',
29+
);
30+
}
31+
32+
@override
33+
Future<int> run() async {
34+
final args = argResults!;
35+
final byMonth = args['monthly'] as bool;
36+
final allDartLang = args['dart-lang'] as bool;
37+
38+
late final DateTime firstReportingDay;
39+
40+
if (byMonth) {
41+
final now = DateTime.now();
42+
firstReportingDay = now.subtract(const Duration(days: 31));
43+
} else {
44+
// by year
45+
final now = DateTime.now();
46+
firstReportingDay = now.subtract(const Duration(days: 365));
47+
}
48+
49+
var repos = noteableRepos
50+
.map(RepositorySlug.full)
51+
.where((repo) => repo.owner == 'dart-lang')
52+
.toList();
53+
54+
if (allDartLang) {
55+
repos = (await getReposForOrg('dart-lang')).map((r) => r.slug()).toList();
56+
}
57+
58+
print('Reporting from ${iso8601String(firstReportingDay)}...');
59+
60+
final aggregates = <String, List<Commit>>{};
61+
62+
for (final repo in repos) {
63+
final commits = await queryCommits(repo: repo, from: firstReportingDay);
64+
65+
print('$repo: ${commits.length} commits');
66+
67+
for (final commit in commits) {
68+
aggregates.putIfAbsent(commit.user, () => []).add(commit);
69+
}
70+
}
71+
72+
print('');
73+
74+
final contributors = <Contributor>[];
75+
76+
for (final entry in aggregates.entries) {
77+
contributors.add(
78+
Contributor(github: entry.key, count: entry.value.length),
79+
);
80+
}
81+
82+
contributors.sort((a, b) {
83+
return b.count - a.count;
84+
});
85+
86+
for (var i = 0; i < contributors.length; i++) {
87+
final contributor = contributors[i];
88+
print('[$i] @${contributor.github}: ${contributor.count} commits');
89+
}
90+
91+
return 0;
92+
}
93+
94+
Future<List<Commit>> queryCommits({
95+
required RepositorySlug repo,
96+
required DateTime from,
97+
}) async {
98+
var result = await query(
99+
QueryOptions(document: gql(commitQueryString(repo: repo, from: from))),
100+
);
101+
if (result.hasException) throw result.exception!;
102+
103+
final commits = <Commit>[];
104+
105+
while (true) {
106+
if (result.hasException) throw result.exception!;
107+
108+
commits.addAll(_getCommitsFromResult(result));
109+
110+
final pageInfo = _pageInfoFromResult(result);
111+
final hasNextPage = (pageInfo['hasNextPage'] as bool?) ?? false;
112+
113+
if (hasNextPage) {
114+
final endCursor = pageInfo['endCursor'] as String?;
115+
result = await query(
116+
QueryOptions(
117+
document: gql(
118+
commitQueryString(repo: repo, from: from, endCursor: endCursor),
119+
),
120+
),
121+
);
122+
} else {
123+
break;
124+
}
125+
}
126+
127+
return commits;
128+
}
129+
}
130+
131+
Iterable<Commit> _getCommitsFromResult(QueryResult result) {
132+
// ignore: avoid_dynamic_calls
133+
final history = result.data!['repository']['defaultBranchRef']['target']
134+
['history'] as Map;
135+
var edges = (history['edges'] as List).cast<Map>();
136+
137+
return edges.map<Commit>((Map edge) {
138+
var node = edge['node'] as Map<String, dynamic>;
139+
return Commit.fromQuery(node);
140+
});
141+
}
142+
143+
Map<String, dynamic> _pageInfoFromResult(QueryResult result) {
144+
// pageInfo {
145+
// endCursor
146+
// startCursor
147+
// hasNextPage
148+
// hasPreviousPage
149+
// }
150+
151+
// ignore: avoid_dynamic_calls
152+
final history = result.data!['repository']['defaultBranchRef']['target']
153+
['history'] as Map;
154+
155+
return (history['pageInfo'] as Map).cast();
156+
}
157+
158+
String commitQueryString({
159+
required RepositorySlug repo,
160+
required DateTime from,
161+
String? endCursor,
162+
}) {
163+
final since = from.toIso8601String();
164+
final cursor = endCursor == null ? '' : ', after: "$endCursor"';
165+
166+
// https://docs.github.com/en/graphql/reference/objects#commit
167+
return '''{
168+
repository(owner: "${repo.owner}", name: "${repo.name}") {
169+
defaultBranchRef {
170+
target {
171+
... on Commit {
172+
history(first: 100, since: "$since" $cursor) {
173+
edges {
174+
node {
175+
oid
176+
messageHeadline
177+
committedDate
178+
author {
179+
user {
180+
login
181+
}
182+
}
183+
committer {
184+
user {
185+
login
186+
}
187+
}
188+
}
189+
}
190+
pageInfo {
191+
endCursor
192+
startCursor
193+
hasNextPage
194+
hasPreviousPage
195+
}
196+
}
197+
}
198+
}
199+
}
200+
}
201+
}
202+
''';
203+
}
204+
205+
class Commit implements Comparable<Commit> {
206+
final String oid;
207+
final String message;
208+
final String user;
209+
final DateTime committedDate;
210+
211+
Commit({
212+
required this.oid,
213+
required this.message,
214+
required this.user,
215+
required this.committedDate,
216+
});
217+
218+
factory Commit.fromQuery(Map<String, dynamic> node) {
219+
var oid = node['oid'] as String;
220+
var messageHeadline = node['messageHeadline'] as String;
221+
// ignore: avoid_dynamic_calls
222+
var user = (node['author']['user'] ?? node['committer']['user']) as Map?;
223+
var login = user == null ? '' : user['login'] as String;
224+
// 2021-07-23T18:37:57Z
225+
var committedDate = node['committedDate'] as String;
226+
227+
if (login.isEmpty) {
228+
final json = jsonEncode(node);
229+
print('[$json]');
230+
}
231+
232+
return Commit(
233+
oid: oid,
234+
message: messageHeadline,
235+
user: login,
236+
committedDate: DateTime.parse(committedDate),
237+
);
238+
}
239+
240+
@override
241+
int compareTo(Commit other) {
242+
return other.committedDate.compareTo(committedDate);
243+
}
244+
245+
@override
246+
String toString() => '${oid.substring(0, 8)} $_shortDate $user $message';
247+
248+
String get _shortDate => committedDate.toIso8601String().substring(0, 10);
249+
}
250+
251+
class PageInfo {
252+
final String endCursor;
253+
final bool hasNextPage;
254+
255+
PageInfo({required this.endCursor, required this.hasNextPage});
256+
257+
static PageInfo parse(Map<String, dynamic> json) {
258+
return PageInfo(
259+
endCursor: json['endCursor'] as String,
260+
hasNextPage: json['hasNextPage'] as bool,
261+
);
262+
}
263+
}
264+
265+
class Contributor {
266+
final String github;
267+
final int count;
268+
269+
Contributor({required this.github, required this.count});
270+
}

pkgs/repo_manage/lib/src/common.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:github/github.dart';
1010
import 'package:graphql/client.dart';
1111

1212
import '../branches.dart';
13+
import '../contributors.dart';
1314
import '../issue_transfer.dart';
1415
import '../labels.dart';
1516
import '../labels_update.dart';
@@ -79,10 +80,11 @@ class ReportCommandRunner extends CommandRunner<int> {
7980
: super('report',
8081
'Run various reports on Dart and Flutter related repositories.') {
8182
addCommand(BranchesCommand());
83+
addCommand(ContributorsCommand());
8284
addCommand(LabelsCommand());
8385
addCommand(LabelsUpdateCommand());
84-
addCommand(WeeklyCommand());
8586
addCommand(TransferIssuesCommand());
87+
addCommand(WeeklyCommand());
8688
}
8789

8890
GitHub get github =>
@@ -147,11 +149,12 @@ class RepoInfo {
147149
// These are monorepos, high-traffic repos, or otherwise noteable repos.
148150
final List<String> noteableRepos = [
149151
'dart-lang/build',
152+
'dart-lang/core',
150153
'dart-lang/ecosystem',
151154
'dart-lang/ffi',
152155
'dart-lang/http',
156+
'dart-lang/i18n',
153157
'dart-lang/language',
154-
'dart-lang/linter',
155158
'dart-lang/native',
156159
'dart-lang/pub',
157160
'dart-lang/sdk',

0 commit comments

Comments
 (0)