199 lines
5.8 KiB
PHP
199 lines
5.8 KiB
PHP
@php
|
|
/**
|
|
* @api-readonly
|
|
* Returns developer team statistics from GitHub
|
|
*/
|
|
use Illuminate\Support\Facades\Http;
|
|
use Carbon\Carbon;
|
|
|
|
// Configuration
|
|
$token = env('GITHUB_TOKEN');
|
|
$repoStr = env('GITHUB_REPO'); // Expected format: "owner/repo"
|
|
// Filter Configuration:
|
|
// Commits with total line changes (additions + deletions) greater than this value
|
|
// will have their line counts ignored in the stats (treated as 0 lines).
|
|
// This effectively bypasses vendor/node_modules dumps or massive removals.
|
|
$ignoredLinesThreshold = 5000;
|
|
|
|
if (!$token || !$repoStr) {
|
|
echo json_encode([
|
|
'status' => 'error',
|
|
'message' => 'GITHUB_TOKEN or GITHUB_REPO not set in .env'
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
$parts = explode('/', $repoStr);
|
|
if (count($parts) !== 2) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Invalid GITHUB_REPO format. Expected owner/repo']);
|
|
exit;
|
|
}
|
|
$owner = $parts[0];
|
|
$name = $parts[1];
|
|
|
|
// GraphQL Query to fetch Commits
|
|
// This allows us to see authors even if they are not verified GitHub users (unlike stats/contributors)
|
|
$query = <<<GRAPHQL
|
|
query(\$owner: String!, \$name: String!, \$cursor: String) {
|
|
repository(owner: \$owner, name: \$name) {
|
|
defaultBranchRef {
|
|
target {
|
|
... on Commit {
|
|
history(first: 25, after: \$cursor) {
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
nodes {
|
|
committedDate
|
|
additions
|
|
deletions
|
|
author {
|
|
name
|
|
email
|
|
user {
|
|
login
|
|
avatarUrl
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GRAPHQL;
|
|
|
|
try {
|
|
$allCommits = [];
|
|
$cursor = null;
|
|
$hasNextPage = true;
|
|
$pageCount = 0;
|
|
$maxPages = 20; // Fetch up to 1000 commits to avoid timeout
|
|
|
|
while ($hasNextPage && $pageCount < $maxPages) {
|
|
$response = Http::withToken($token)
|
|
->withHeaders([
|
|
'User-Agent' => 'Laravel/10.0',
|
|
'Accept' => 'application/json',
|
|
])
|
|
->post('https://api.github.com/graphql', [
|
|
'query' => $query,
|
|
'variables' => [
|
|
'owner' => $owner,
|
|
'name' => $name,
|
|
'cursor' => $cursor
|
|
]
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
throw new Exception("GitHub API Error: " . $response->body());
|
|
}
|
|
|
|
$json = $response->json();
|
|
|
|
if (isset($json['errors'])) {
|
|
throw new Exception("GraphQL Error: " . json_encode($json['errors']));
|
|
}
|
|
|
|
$history = $json['data']['repository']['defaultBranchRef']['target']['history'] ?? null;
|
|
|
|
if (!$history) {
|
|
break;
|
|
}
|
|
|
|
$commits = $history['nodes'];
|
|
$allCommits = array_merge($allCommits, $commits);
|
|
|
|
$hasNextPage = $history['pageInfo']['hasNextPage'];
|
|
$cursor = $history['pageInfo']['endCursor'];
|
|
$pageCount++;
|
|
}
|
|
|
|
// Process Data
|
|
$monthlyStats = [];
|
|
$monthlyTotals = [];
|
|
$totalCommitsProject = 0;
|
|
|
|
foreach ($allCommits as $commit) {
|
|
// Outlier Filter (Bypass Vendor/Large Changes)
|
|
// If a commit changes more lines than the threshold, we ignore its line counts
|
|
// but still count it as a commit activity.
|
|
$totalChanges = $commit['additions'] + $commit['deletions'];
|
|
if ($totalChanges > $ignoredLinesThreshold) {
|
|
$commit['additions'] = 0;
|
|
$commit['deletions'] = 0;
|
|
}
|
|
|
|
$date = Carbon::parse($commit['committedDate']);
|
|
$monthKey = $date->format('Y-m');
|
|
|
|
$authorData = $commit['author'];
|
|
// Prefer GitHub Login, fallback to Git Author Name
|
|
$authorName = $authorData['user']['login'] ?? $authorData['name'] ?? 'Unknown';
|
|
|
|
// Grouping key
|
|
$key = $authorName . '_' . $monthKey;
|
|
|
|
// Initialize Monthly Total for relative percentage
|
|
if (!isset($monthlyTotals[$monthKey])) {
|
|
$monthlyTotals[$monthKey] = 0;
|
|
}
|
|
$monthlyTotals[$monthKey]++;
|
|
|
|
// Initialize Developer Stats
|
|
if (!isset($monthlyStats[$key])) {
|
|
$monthlyStats[$key] = [
|
|
'author' => $authorName,
|
|
'month' => $monthKey,
|
|
'added' => 0,
|
|
'deleted' => 0,
|
|
'commits' => 0,
|
|
];
|
|
}
|
|
|
|
$monthlyStats[$key]['added'] += $commit['additions'];
|
|
$monthlyStats[$key]['deleted'] += $commit['deletions'];
|
|
$monthlyStats[$key]['commits']++;
|
|
|
|
$totalCommitsProject++;
|
|
}
|
|
|
|
// Calculate percentages and format for DevExtreme
|
|
$resultData = array_values($monthlyStats);
|
|
|
|
// Sort by month descending
|
|
usort($resultData, function($a, $b) {
|
|
return strcmp($b['month'], $a['month']);
|
|
});
|
|
|
|
// Add percentages
|
|
foreach ($resultData as &$item) {
|
|
// Global contribution percentage
|
|
$item['percentage'] = $totalCommitsProject > 0 ? round(($item['commits'] / $totalCommitsProject) * 100, 2) : 0;
|
|
|
|
// Monthly relative contribution percentage
|
|
$monthTotal = $monthlyTotals[$item['month']] ?? 0;
|
|
$item['monthly_percentage'] = $monthTotal > 0 ? round(($item['commits'] / $monthTotal) * 100, 2) : 0;
|
|
}
|
|
|
|
echo json_encode([
|
|
'status' => 'success',
|
|
'data' => $resultData,
|
|
'meta' => [
|
|
'repo' => $repoStr,
|
|
'total_commits' => $totalCommitsProject,
|
|
'fetched_commits' => count($allCommits),
|
|
'ignored_threshold' => $ignoredLinesThreshold
|
|
]
|
|
], JSON_UNESCAPED_UNICODE);
|
|
|
|
} catch (Exception $e) {
|
|
echo json_encode([
|
|
'status' => 'error',
|
|
'message' => $e->getMessage()
|
|
], JSON_UNESCAPED_UNICODE);
|
|
}
|
|
@endphp
|