forked from mahdahar/crm-summit
feat(gitea): add database-backed sync, API, and dashboard views
Add Gitea sync service with full and incremental modes, paged API fetch, upsert logic for users/repos/commits/PRs, and error aggregation. Add migration for git_users, git_repositories, git_commits, git_pull_requests with indexes and unique constraints; add models and sync scripts for full/incremental jobs. Update Gitea UI and dashboard filters (user/repo/date), aggregate commit loading across repositories, and wire routes/controllers/sidebar for dashboard and sync endpoints.
This commit is contained in:
parent
6956b3235c
commit
ec5f2fc385
43
app/Commands/SyncGiteaData.php
Normal file
43
app/Commands/SyncGiteaData.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Libraries\GiteaSyncService;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
class SyncGiteaData extends BaseCommand
|
||||
{
|
||||
protected $group = 'Gitea';
|
||||
protected $name = 'gitea:sync';
|
||||
protected $description = 'Full sync all users, repositories, commits, and pull requests from Gitea into local database.';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
CLI::write('Starting Gitea full sync...', 'yellow');
|
||||
|
||||
$service = new GiteaSyncService();
|
||||
$result = $service->syncFull();
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
CLI::error('Sync failed: ' . ($result['message'] ?? 'Unknown error'));
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $result['stats'] ?? [];
|
||||
CLI::write('Sync done.', 'green');
|
||||
CLI::write('Users: ' . ($stats['users_synced'] ?? 0));
|
||||
CLI::write('Repositories: ' . ($stats['repositories_synced'] ?? 0));
|
||||
CLI::write('Commits: ' . ($stats['commits_synced'] ?? 0));
|
||||
CLI::write('Pull Requests: ' . ($stats['pull_requests_synced'] ?? 0));
|
||||
|
||||
$errors = $stats['errors'] ?? [];
|
||||
if (!empty($errors)) {
|
||||
CLI::newLine();
|
||||
CLI::write('Warnings / Errors:', 'yellow');
|
||||
foreach ($errors as $error) {
|
||||
CLI::write('- ' . $error, 'light_red');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
app/Commands/SyncGiteaIncremental.php
Normal file
50
app/Commands/SyncGiteaIncremental.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Libraries\GiteaSyncService;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
|
||||
class SyncGiteaIncremental extends BaseCommand
|
||||
{
|
||||
protected $group = 'Gitea';
|
||||
protected $name = 'gitea:sync-incremental';
|
||||
protected $description = 'Incremental sync from Gitea (default last 1 day). Usage: php spark gitea:sync-incremental [days]';
|
||||
|
||||
public function run(array $params)
|
||||
{
|
||||
$days = isset($params[0]) ? (int) $params[0] : 1;
|
||||
if ($days < 1) {
|
||||
$days = 1;
|
||||
}
|
||||
|
||||
CLI::write('Starting Gitea incremental sync (last ' . $days . ' day)...', 'yellow');
|
||||
|
||||
$service = new GiteaSyncService();
|
||||
$result = $service->syncIncremental($days);
|
||||
|
||||
if (!($result['success'] ?? false)) {
|
||||
CLI::error('Sync failed: ' . ($result['message'] ?? 'Unknown error'));
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $result['stats'] ?? [];
|
||||
CLI::write('Sync done.', 'green');
|
||||
CLI::write('Mode: ' . ($stats['mode'] ?? 'incremental'));
|
||||
CLI::write('Since: ' . ($stats['since'] ?? '-'));
|
||||
CLI::write('Users: ' . ($stats['users_synced'] ?? 0));
|
||||
CLI::write('Repositories: ' . ($stats['repositories_synced'] ?? 0));
|
||||
CLI::write('Commits: ' . ($stats['commits_synced'] ?? 0));
|
||||
CLI::write('Pull Requests: ' . ($stats['pull_requests_synced'] ?? 0));
|
||||
|
||||
$errors = $stats['errors'] ?? [];
|
||||
if (!empty($errors)) {
|
||||
CLI::newLine();
|
||||
CLI::write('Warnings / Errors:', 'yellow');
|
||||
foreach ($errors as $error) {
|
||||
CLI::write('- ' . $error, 'light_red');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,19 +268,22 @@ $routes->match(['get','post'],'/invtrans/reportusage/', 'InvTrans::reportusage/$
|
||||
// Khusus Untuk Gitea
|
||||
$routes->get('/gitea', 'Gitea::index');
|
||||
$routes->group('api/gitea', function($routes) {
|
||||
// Mendapatkan detail user
|
||||
// Legacy proxy endpoints (tetap dipertahankan)
|
||||
$routes->get('getuser/(:segment)', 'Gitea::getUser/$1');
|
||||
|
||||
// Mendapatkan daftar repositori milik user
|
||||
$routes->get('getrepos/(:segment)', 'Gitea::getRepositories/$1');
|
||||
|
||||
// Mendapatkan daftar branch dari sebuah repositori
|
||||
$routes->get('getbranches/(:segment)/(:segment)', 'Gitea::getBranches/$1/$2');
|
||||
|
||||
// Mendapatkan daftar commit dari sebuah repositori
|
||||
$routes->get('getcommits/(:segment)/(:segment)', 'Gitea::getCommits/$1/$2');
|
||||
});
|
||||
|
||||
$routes->group('api/git', function($routes) {
|
||||
$routes->get('summary', 'Api\GitApi::summary');
|
||||
$routes->get('users', 'Api\GitApi::users');
|
||||
$routes->get('repositories', 'Api\GitApi::repositories');
|
||||
$routes->get('commits', 'Api\GitApi::commits');
|
||||
$routes->get('pull-requests', 'Api\GitApi::pullRequests');
|
||||
$routes->post('sync', 'Api\GitApi::sync');
|
||||
});
|
||||
|
||||
//LQMS
|
||||
/*
|
||||
$routes->match(['get','post'],'/lqms', 'Lqms::index');
|
||||
|
||||
236
app/Controllers/Api/GitApi.php
Normal file
236
app/Controllers/Api/GitApi.php
Normal file
@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\GiteaSyncService;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class GitApi extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
private function ensureLoggedIn()
|
||||
{
|
||||
if (!session()->get('userid')) {
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'message' => 'Unauthorized',
|
||||
], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function ensureAdmin()
|
||||
{
|
||||
$level = (int) session()->get('level');
|
||||
if (!in_array($level, [0, 1, 2], true)) {
|
||||
return $this->respond([
|
||||
'status' => 'error',
|
||||
'message' => 'Forbidden. Admin only.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function summary()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$usersCount = $db->table('git_users')->countAllResults();
|
||||
$repositoriesCount = $db->table('git_repositories')->countAllResults();
|
||||
$commitsCount = $db->table('git_commits')->countAllResults();
|
||||
$pullRequestsCount = $db->table('git_pull_requests')->countAllResults();
|
||||
|
||||
$latestCommit = $db->table('git_commits')->selectMax('committed_at', 'latest')->get()->getRowArray();
|
||||
$latestPullRequest = $db->table('git_pull_requests')->selectMax('updated_at_gitea', 'latest')->get()->getRowArray();
|
||||
$latestRepoSync = $db->table('git_repositories')->selectMax('last_synced_at', 'latest')->get()->getRowArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Summary fetched',
|
||||
'data' => [
|
||||
'users' => $usersCount,
|
||||
'repositories' => $repositoriesCount,
|
||||
'commits' => $commitsCount,
|
||||
'pull_requests' => $pullRequestsCount,
|
||||
'latest_commit_at' => $latestCommit['latest'] ?? null,
|
||||
'latest_pull_request_at' => $latestPullRequest['latest'] ?? null,
|
||||
'last_synced_at' => $latestRepoSync['latest'] ?? null,
|
||||
],
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$rows = $db->table('git_users')
|
||||
->select('id, gitea_user_id, username, full_name, email, is_active, is_admin, last_synced_at')
|
||||
->orderBy('username', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Users fetched',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function repositories()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$rows = $db->table('git_repositories r')
|
||||
->select('r.id, r.gitea_repo_id, r.name, r.full_name, r.owner_username, r.default_branch, r.is_private, r.last_pushed_at, r.last_synced_at, u.username as owner_login')
|
||||
->join('git_users u', 'u.id = r.owner_user_id', 'left')
|
||||
->orderBy('r.full_name', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Repositories fetched',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function commits()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$repoId = $this->request->getGet('repo_id');
|
||||
$userId = $this->request->getGet('user_id');
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 200);
|
||||
if ($limit <= 0 || $limit > 1000) {
|
||||
$limit = 200;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('git_commits c')
|
||||
->select('c.id, c.sha, c.short_sha, c.message, c.author_name, c.author_email, c.committed_at, c.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
->join('git_repositories r', 'r.id = c.repository_id', 'inner')
|
||||
->join('git_users u', 'u.id = c.author_user_id', 'left');
|
||||
|
||||
if (!empty($repoId)) {
|
||||
$builder->where('c.repository_id', (int) $repoId);
|
||||
}
|
||||
|
||||
if (!empty($userId)) {
|
||||
$builder->where('c.author_user_id', (int) $userId);
|
||||
}
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$builder->where('c.committed_at >=', $startDate . ' 00:00:00');
|
||||
}
|
||||
|
||||
if (!empty($endDate)) {
|
||||
$builder->where('c.committed_at <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
->orderBy('c.committed_at', 'DESC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Commits fetched',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function pullRequests()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$repoId = $this->request->getGet('repo_id');
|
||||
$userId = $this->request->getGet('user_id');
|
||||
$state = $this->request->getGet('state');
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 200);
|
||||
if ($limit <= 0 || $limit > 1000) {
|
||||
$limit = 200;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('git_pull_requests p')
|
||||
->select('p.id, p.number, p.title, p.state, p.is_draft, p.is_merged, p.created_at_gitea, p.updated_at_gitea, p.merged_at, p.closed_at, p.html_url, r.id as repository_id, r.full_name as repository_full_name, u.id as user_id, u.username as user_username')
|
||||
->join('git_repositories r', 'r.id = p.repository_id', 'inner')
|
||||
->join('git_users u', 'u.id = p.author_user_id', 'left');
|
||||
|
||||
if (!empty($repoId)) {
|
||||
$builder->where('p.repository_id', (int) $repoId);
|
||||
}
|
||||
|
||||
if (!empty($userId)) {
|
||||
$builder->where('p.author_user_id', (int) $userId);
|
||||
}
|
||||
|
||||
if (!empty($state)) {
|
||||
$builder->where('p.state', $state);
|
||||
}
|
||||
|
||||
if (!empty($startDate)) {
|
||||
$builder->where('p.updated_at_gitea >=', $startDate . ' 00:00:00');
|
||||
}
|
||||
|
||||
if (!empty($endDate)) {
|
||||
$builder->where('p.updated_at_gitea <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
$rows = $builder
|
||||
->orderBy('p.updated_at_gitea', 'DESC')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Pull requests fetched',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function sync()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($response = $this->ensureAdmin()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$service = new GiteaSyncService();
|
||||
$result = $service->syncAll();
|
||||
|
||||
$statusCode = $result['success'] ? 200 : 500;
|
||||
return $this->respond([
|
||||
'status' => $result['success'] ? 'success' : 'error',
|
||||
'message' => $result['message'],
|
||||
'data' => $result['stats'] ?? [],
|
||||
], $statusCode);
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,15 @@ class Gitea extends BaseController
|
||||
*/
|
||||
private function fetchFromGitea(string $endpoint)
|
||||
{
|
||||
$token = env('GITEA_TOKEN') ?: '0eca3f7e42e0992ecc9af9d947bbbd7cfc6ce2e1';
|
||||
$token = env('GITEA_TOKEN');
|
||||
if (!$token) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'GITEA_TOKEN missing in .env',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
|
||||
$url = $this->baseUrl . $endpoint;
|
||||
|
||||
$client = \Config\Services::curlrequest();
|
||||
@ -131,6 +139,9 @@ class Gitea extends BaseController
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('gitea_index');
|
||||
$level = (int) session()->get('level');
|
||||
$data['isAdmin'] = in_array($level, [0, 1, 2], true);
|
||||
|
||||
return view('gitea_dashboard', $data);
|
||||
}
|
||||
}
|
||||
339
app/Database/Migrations/2026-04-22-000001_CreateGitTables.php
Normal file
339
app/Database/Migrations/2026-04-22-000001_CreateGitTables.php
Normal file
@ -0,0 +1,339 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateGitTables extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'gitea_user_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'username' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
],
|
||||
'full_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'email' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'avatar_url' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'is_active' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 1,
|
||||
],
|
||||
'is_admin' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'last_synced_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('gitea_user_id');
|
||||
$this->forge->addUniqueKey('username');
|
||||
$this->forge->createTable('git_users', true);
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'gitea_repo_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'owner_user_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'owner_username' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
'null' => true,
|
||||
],
|
||||
'name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 150,
|
||||
],
|
||||
'full_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'html_url' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'clone_url' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'default_branch' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 100,
|
||||
'null' => true,
|
||||
],
|
||||
'is_private' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_archived' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_fork' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'open_issues_count' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'stars_count' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'forks_count' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'watchers_count' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'default' => 0,
|
||||
],
|
||||
'last_pushed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'last_synced_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('gitea_repo_id');
|
||||
$this->forge->addUniqueKey('full_name');
|
||||
$this->forge->addKey('owner_user_id');
|
||||
$this->forge->createTable('git_repositories', true);
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'repository_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'author_user_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'sha' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 64,
|
||||
],
|
||||
'short_sha' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 12,
|
||||
'null' => true,
|
||||
],
|
||||
'message' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'author_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'author_email' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'committed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'html_url' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['repository_id', 'sha']);
|
||||
$this->forge->addKey('repository_id');
|
||||
$this->forge->addKey('author_user_id');
|
||||
$this->forge->addKey('committed_at');
|
||||
$this->forge->createTable('git_commits', true);
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'gitea_pr_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'repository_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
'author_user_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'number' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
],
|
||||
'title' => [
|
||||
'type' => 'TEXT',
|
||||
],
|
||||
'body' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'state' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 30,
|
||||
'default' => 'open',
|
||||
],
|
||||
'is_draft' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'is_merged' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
'merged_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'closed_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at_gitea' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at_gitea' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'html_url' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('gitea_pr_id');
|
||||
$this->forge->addKey('repository_id');
|
||||
$this->forge->addKey('author_user_id');
|
||||
$this->forge->addKey('state');
|
||||
$this->forge->addKey('updated_at_gitea');
|
||||
$this->forge->createTable('git_pull_requests', true);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('git_pull_requests', true);
|
||||
$this->forge->dropTable('git_commits', true);
|
||||
$this->forge->dropTable('git_repositories', true);
|
||||
$this->forge->dropTable('git_users', true);
|
||||
}
|
||||
}
|
||||
414
app/Libraries/GiteaSyncService.php
Normal file
414
app/Libraries/GiteaSyncService.php
Normal file
@ -0,0 +1,414 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Models\GitCommitsModel;
|
||||
use App\Models\GitPullRequestsModel;
|
||||
use App\Models\GitRepositoriesModel;
|
||||
use App\Models\GitUsersModel;
|
||||
|
||||
class GiteaSyncService
|
||||
{
|
||||
private string $baseUrl;
|
||||
private string $token;
|
||||
private int $perPage = 100;
|
||||
|
||||
private GitUsersModel $usersModel;
|
||||
private GitRepositoriesModel $repositoriesModel;
|
||||
private GitCommitsModel $commitsModel;
|
||||
private GitPullRequestsModel $pullRequestsModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->baseUrl = rtrim((string) (env('GITEA_BASE_URL') ?: 'https://gitea.services-summit.my.id/api/v1'), '/');
|
||||
$this->token = (string) env('GITEA_TOKEN');
|
||||
|
||||
$this->usersModel = new GitUsersModel();
|
||||
$this->repositoriesModel = new GitRepositoriesModel();
|
||||
$this->commitsModel = new GitCommitsModel();
|
||||
$this->pullRequestsModel = new GitPullRequestsModel();
|
||||
}
|
||||
|
||||
public function syncAll(): array
|
||||
{
|
||||
return $this->syncFull();
|
||||
}
|
||||
|
||||
public function syncFull(): array
|
||||
{
|
||||
return $this->performSync();
|
||||
}
|
||||
|
||||
public function syncIncremental(int $days = 1): array
|
||||
{
|
||||
if ($days < 1) {
|
||||
$days = 1;
|
||||
}
|
||||
|
||||
$sinceIso = (new \DateTimeImmutable('now'))
|
||||
->modify('-' . $days . ' days')
|
||||
->format(DATE_ATOM);
|
||||
|
||||
return $this->performSync($sinceIso, $days);
|
||||
}
|
||||
|
||||
private function performSync(?string $sinceIso = null, ?int $days = null): array
|
||||
{
|
||||
if ($this->token === '') {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'GITEA_TOKEN missing in .env',
|
||||
'stats' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'mode' => $sinceIso === null ? 'full' : 'incremental',
|
||||
'since' => $sinceIso,
|
||||
'users_synced' => 0,
|
||||
'repositories_synced' => 0,
|
||||
'commits_synced' => 0,
|
||||
'pull_requests_synced' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
$users = $this->fetchPaged('/users/search', [], ['data']);
|
||||
foreach ($users['items'] as $user) {
|
||||
$this->upsertUser($user);
|
||||
$stats['users_synced']++;
|
||||
}
|
||||
$stats['errors'] = array_merge($stats['errors'], $users['errors']);
|
||||
|
||||
$repositories = $this->fetchPaged('/repos/search', [], ['data']);
|
||||
$stats['errors'] = array_merge($stats['errors'], $repositories['errors']);
|
||||
|
||||
foreach ($repositories['items'] as $repo) {
|
||||
$localRepoId = $this->upsertRepository($repo);
|
||||
$stats['repositories_synced']++;
|
||||
|
||||
if ($localRepoId === null) {
|
||||
$stats['errors'][] = 'Skip repo because upsert failed: ' . ($repo['full_name'] ?? 'unknown');
|
||||
continue;
|
||||
}
|
||||
|
||||
$owner = $repo['owner']['username'] ?? $repo['owner']['login'] ?? null;
|
||||
$name = $repo['name'] ?? null;
|
||||
if ($owner === null || $name === null) {
|
||||
$stats['errors'][] = 'Skip repo because owner/name missing: ' . ($repo['full_name'] ?? 'unknown');
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitQuery = [];
|
||||
if ($sinceIso !== null) {
|
||||
$commitQuery['since'] = $sinceIso;
|
||||
}
|
||||
$commits = $this->fetchPaged("/repos/{$owner}/{$name}/commits", $commitQuery, []);
|
||||
$stats['errors'] = array_merge($stats['errors'], $commits['errors']);
|
||||
foreach ($commits['items'] as $commit) {
|
||||
if ($this->upsertCommit($localRepoId, $commit)) {
|
||||
$stats['commits_synced']++;
|
||||
}
|
||||
}
|
||||
|
||||
$pullRequests = $this->fetchPaged("/repos/{$owner}/{$name}/pulls", ['state' => 'all', 'sort' => 'recentupdate', 'direction' => 'desc'], []);
|
||||
$stats['errors'] = array_merge($stats['errors'], $pullRequests['errors']);
|
||||
foreach ($pullRequests['items'] as $pullRequest) {
|
||||
if ($sinceIso !== null && !$this->isOnOrAfterSince($pullRequest['updated_at'] ?? null, $sinceIso)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->upsertPullRequest($localRepoId, $pullRequest)) {
|
||||
$stats['pull_requests_synced']++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->repositoriesModel->update($localRepoId, [
|
||||
'last_synced_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
$message = $sinceIso === null
|
||||
? 'Gitea full sync completed'
|
||||
: 'Gitea incremental sync completed (last ' . ($days ?? 1) . ' day)';
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'stats' => $stats,
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchPaged(string $endpoint, array $query = [], array $listKeys = ['data']): array
|
||||
{
|
||||
$page = 1;
|
||||
$items = [];
|
||||
$errors = [];
|
||||
|
||||
do {
|
||||
$payload = array_merge($query, [
|
||||
'limit' => $this->perPage,
|
||||
'page' => $page,
|
||||
]);
|
||||
|
||||
$response = $this->request('GET', $endpoint, $payload);
|
||||
if ($response['success'] === false) {
|
||||
$errors[] = $response['message'];
|
||||
break;
|
||||
}
|
||||
|
||||
$chunk = $this->extractList($response['data'], $listKeys);
|
||||
if (empty($chunk)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$items = array_merge($items, $chunk);
|
||||
$page++;
|
||||
} while (count($chunk) >= $this->perPage);
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function extractList($data, array $listKeys): array
|
||||
{
|
||||
if (is_array($data) && array_is_list($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($listKeys as $key) {
|
||||
if (isset($data[$key]) && is_array($data[$key])) {
|
||||
return $data[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['items']) && is_array($data['items'])) {
|
||||
return $data['items'];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function upsertUser(array $user): ?int
|
||||
{
|
||||
$giteaUserId = isset($user['id']) ? (int) $user['id'] : null;
|
||||
$username = $user['username'] ?? $user['login'] ?? null;
|
||||
|
||||
if ($giteaUserId === null || $username === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'gitea_user_id' => $giteaUserId,
|
||||
'username' => $username,
|
||||
'full_name' => $user['full_name'] ?? null,
|
||||
'email' => $user['email'] ?? null,
|
||||
'avatar_url' => $user['avatar_url'] ?? null,
|
||||
'is_active' => isset($user['active']) ? (int) $user['active'] : 1,
|
||||
'is_admin' => isset($user['is_admin']) ? (int) $user['is_admin'] : 0,
|
||||
'last_synced_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$existing = $this->usersModel->where('gitea_user_id', $giteaUserId)->first();
|
||||
if ($existing) {
|
||||
$this->usersModel->update((int) $existing['id'], $data);
|
||||
return (int) $existing['id'];
|
||||
}
|
||||
|
||||
$this->usersModel->insert($data);
|
||||
return (int) $this->usersModel->getInsertID();
|
||||
}
|
||||
|
||||
private function upsertRepository(array $repo): ?int
|
||||
{
|
||||
$giteaRepoId = isset($repo['id']) ? (int) $repo['id'] : null;
|
||||
if ($giteaRepoId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ownerUserId = null;
|
||||
if (isset($repo['owner']) && is_array($repo['owner'])) {
|
||||
$ownerUserId = $this->upsertUser($repo['owner']);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'gitea_repo_id' => $giteaRepoId,
|
||||
'owner_user_id' => $ownerUserId,
|
||||
'owner_username' => $repo['owner']['username'] ?? $repo['owner']['login'] ?? null,
|
||||
'name' => $repo['name'] ?? '',
|
||||
'full_name' => $repo['full_name'] ?? '',
|
||||
'description' => $repo['description'] ?? null,
|
||||
'html_url' => $repo['html_url'] ?? null,
|
||||
'clone_url' => $repo['clone_url'] ?? null,
|
||||
'default_branch' => $repo['default_branch'] ?? null,
|
||||
'is_private' => isset($repo['private']) ? (int) $repo['private'] : 0,
|
||||
'is_archived' => isset($repo['archived']) ? (int) $repo['archived'] : 0,
|
||||
'is_fork' => isset($repo['fork']) ? (int) $repo['fork'] : 0,
|
||||
'open_issues_count' => (int) ($repo['open_issues_count'] ?? 0),
|
||||
'stars_count' => (int) ($repo['stars_count'] ?? 0),
|
||||
'forks_count' => (int) ($repo['forks_count'] ?? 0),
|
||||
'watchers_count' => (int) ($repo['watchers_count'] ?? 0),
|
||||
'last_pushed_at' => $this->normalizeDate($repo['updated_at'] ?? $repo['pushed_at'] ?? null),
|
||||
'last_synced_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$existing = $this->repositoriesModel->where('gitea_repo_id', $giteaRepoId)->first();
|
||||
if ($existing) {
|
||||
$this->repositoriesModel->update((int) $existing['id'], $data);
|
||||
return (int) $existing['id'];
|
||||
}
|
||||
|
||||
$this->repositoriesModel->insert($data);
|
||||
return (int) $this->repositoriesModel->getInsertID();
|
||||
}
|
||||
|
||||
private function upsertCommit(int $repositoryId, array $commit): bool
|
||||
{
|
||||
$sha = $commit['sha'] ?? null;
|
||||
if ($sha === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorUserId = null;
|
||||
if (isset($commit['author']) && is_array($commit['author']) && isset($commit['author']['id'])) {
|
||||
$authorUserId = $this->upsertUser($commit['author']);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'repository_id' => $repositoryId,
|
||||
'author_user_id' => $authorUserId,
|
||||
'sha' => $sha,
|
||||
'short_sha' => substr($sha, 0, 10),
|
||||
'message' => $commit['commit']['message'] ?? null,
|
||||
'author_name' => $commit['commit']['author']['name'] ?? null,
|
||||
'author_email' => $commit['commit']['author']['email'] ?? null,
|
||||
'committed_at' => $this->normalizeDate($commit['commit']['author']['date'] ?? null),
|
||||
'html_url' => $commit['html_url'] ?? null,
|
||||
];
|
||||
|
||||
$existing = $this->commitsModel
|
||||
->where('repository_id', $repositoryId)
|
||||
->where('sha', $sha)
|
||||
->first();
|
||||
if ($existing) {
|
||||
$this->commitsModel->update((int) $existing['id'], $data);
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->commitsModel->insert($data);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function upsertPullRequest(int $repositoryId, array $pullRequest): bool
|
||||
{
|
||||
$giteaPrId = isset($pullRequest['id']) ? (int) $pullRequest['id'] : null;
|
||||
if ($giteaPrId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$authorUserId = null;
|
||||
if (isset($pullRequest['user']) && is_array($pullRequest['user']) && isset($pullRequest['user']['id'])) {
|
||||
$authorUserId = $this->upsertUser($pullRequest['user']);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'gitea_pr_id' => $giteaPrId,
|
||||
'repository_id' => $repositoryId,
|
||||
'author_user_id' => $authorUserId,
|
||||
'number' => (int) ($pullRequest['number'] ?? 0),
|
||||
'title' => $pullRequest['title'] ?? '',
|
||||
'body' => $pullRequest['body'] ?? null,
|
||||
'state' => $pullRequest['state'] ?? 'open',
|
||||
'is_draft' => isset($pullRequest['draft']) ? (int) $pullRequest['draft'] : 0,
|
||||
'is_merged' => isset($pullRequest['merged']) ? (int) $pullRequest['merged'] : 0,
|
||||
'merged_at' => $this->normalizeDate($pullRequest['merged_at'] ?? null),
|
||||
'closed_at' => $this->normalizeDate($pullRequest['closed_at'] ?? null),
|
||||
'created_at_gitea' => $this->normalizeDate($pullRequest['created_at'] ?? null),
|
||||
'updated_at_gitea' => $this->normalizeDate($pullRequest['updated_at'] ?? null),
|
||||
'html_url' => $pullRequest['html_url'] ?? null,
|
||||
];
|
||||
|
||||
$existing = $this->pullRequestsModel->where('gitea_pr_id', $giteaPrId)->first();
|
||||
if ($existing) {
|
||||
$this->pullRequestsModel->update((int) $existing['id'], $data);
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->pullRequestsModel->insert($data);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isOnOrAfterSince(?string $value, string $sinceIso): bool
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new \DateTimeImmutable($value)) >= (new \DateTimeImmutable($sinceIso));
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function request(string $method, string $endpoint, array $query = []): array
|
||||
{
|
||||
$url = $this->baseUrl . $endpoint;
|
||||
if (!empty($query)) {
|
||||
$url .= '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
$client = \Config\Services::curlrequest();
|
||||
|
||||
try {
|
||||
$response = $client->request($method, $url, [
|
||||
'headers' => [
|
||||
'Authorization' => 'token ' . $this->token,
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'http_errors' => false,
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = json_decode($response->getBody(), true);
|
||||
|
||||
if ($statusCode < 200 || $statusCode >= 300) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Gitea request failed [' . $statusCode . '] ' . $endpoint,
|
||||
'data' => $body,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'ok',
|
||||
'data' => $body,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Gitea request exception: ' . $e->getMessage(),
|
||||
'data' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeDate(?string $value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return (new \DateTime($value))->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
app/Models/GitCommitsModel.php
Normal file
29
app/Models/GitCommitsModel.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class GitCommitsModel extends Model
|
||||
{
|
||||
protected $table = 'git_commits';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'repository_id',
|
||||
'author_user_id',
|
||||
'sha',
|
||||
'short_sha',
|
||||
'message',
|
||||
'author_name',
|
||||
'author_email',
|
||||
'committed_at',
|
||||
'html_url',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
34
app/Models/GitPullRequestsModel.php
Normal file
34
app/Models/GitPullRequestsModel.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class GitPullRequestsModel extends Model
|
||||
{
|
||||
protected $table = 'git_pull_requests';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'gitea_pr_id',
|
||||
'repository_id',
|
||||
'author_user_id',
|
||||
'number',
|
||||
'title',
|
||||
'body',
|
||||
'state',
|
||||
'is_draft',
|
||||
'is_merged',
|
||||
'merged_at',
|
||||
'closed_at',
|
||||
'created_at_gitea',
|
||||
'updated_at_gitea',
|
||||
'html_url',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
38
app/Models/GitRepositoriesModel.php
Normal file
38
app/Models/GitRepositoriesModel.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class GitRepositoriesModel extends Model
|
||||
{
|
||||
protected $table = 'git_repositories';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'gitea_repo_id',
|
||||
'owner_user_id',
|
||||
'owner_username',
|
||||
'name',
|
||||
'full_name',
|
||||
'description',
|
||||
'html_url',
|
||||
'clone_url',
|
||||
'default_branch',
|
||||
'is_private',
|
||||
'is_archived',
|
||||
'is_fork',
|
||||
'open_issues_count',
|
||||
'stars_count',
|
||||
'forks_count',
|
||||
'watchers_count',
|
||||
'last_pushed_at',
|
||||
'last_synced_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
28
app/Models/GitUsersModel.php
Normal file
28
app/Models/GitUsersModel.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class GitUsersModel extends Model
|
||||
{
|
||||
protected $table = 'git_users';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'gitea_user_id',
|
||||
'username',
|
||||
'full_name',
|
||||
'email',
|
||||
'avatar_url',
|
||||
'is_active',
|
||||
'is_admin',
|
||||
'last_synced_at',
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $dateFormat = 'datetime';
|
||||
protected $createdField = 'created_at';
|
||||
protected $updatedField = 'updated_at';
|
||||
}
|
||||
259
app/Views/gitea_dashboard.php
Normal file
259
app/Views/gitea_dashboard.php
Normal file
@ -0,0 +1,259 @@
|
||||
<?= $this->extend('layouts/main.php') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
<div class="page-wrapper">
|
||||
<div class="container-fluid">
|
||||
<div class="row page-titles">
|
||||
<div class="col-md-6 align-self-center">
|
||||
<h4 class="text-themecolor">Gitea Dashboard (DB)</h4>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<?php if (!empty($isAdmin)): ?>
|
||||
<button id="btnSync" class="btn btn-primary">Sync Now</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">User</label>
|
||||
<select id="filterUser" class="form-select"><option value="">All User</option></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Repository</label>
|
||||
<select id="filterRepo" class="form-select"><option value="">All Repository</option></select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Start Date</label>
|
||||
<input type="date" id="filterStart" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">End Date</label>
|
||||
<input type="date" id="filterEnd" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button id="btnApplyFilter" class="btn btn-success w-100">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Latest Commits</h5>
|
||||
<div><strong>Total Commits:</strong> <span id="totalCommits">0</span></div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered" id="tableCommits">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Repository</th>
|
||||
<th>User</th>
|
||||
<th>SHA</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5>Latest Pull Requests</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered" id="tablePrs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Updated</th>
|
||||
<th>Repository</th>
|
||||
<th>User</th>
|
||||
<th>PR</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('script') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const totalCommits = document.getElementById('totalCommits');
|
||||
|
||||
const filterRepo = document.getElementById('filterRepo');
|
||||
const filterUser = document.getElementById('filterUser');
|
||||
const filterStart = document.getElementById('filterStart');
|
||||
const filterEnd = document.getElementById('filterEnd');
|
||||
const btnApplyFilter = document.getElementById('btnApplyFilter');
|
||||
const btnSync = document.getElementById('btnSync');
|
||||
|
||||
setDefaultDateRange();
|
||||
await loadUsers();
|
||||
await loadRepos();
|
||||
await loadTables();
|
||||
|
||||
btnApplyFilter.addEventListener('click', loadTables);
|
||||
|
||||
if (btnSync) {
|
||||
btnSync.addEventListener('click', async () => {
|
||||
btnSync.disabled = true;
|
||||
btnSync.innerText = 'Syncing...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`<?= base_url('api/git/sync') ?>`, {
|
||||
method: 'POST'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert('Sync complete');
|
||||
await loadTables();
|
||||
} else {
|
||||
alert(result.message || 'Sync failed');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Sync request failed');
|
||||
}
|
||||
|
||||
btnSync.disabled = false;
|
||||
btnSync.innerText = 'Sync Now';
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultDateRange() {
|
||||
const today = new Date();
|
||||
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
||||
filterStart.value = toInputDate(startOfYear);
|
||||
filterEnd.value = toInputDate(today);
|
||||
}
|
||||
|
||||
function toInputDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const response = await fetch(`<?= base_url('api/git/users') ?>`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.data.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.username;
|
||||
filterUser.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRepos() {
|
||||
const response = await fetch(`<?= base_url('api/git/repositories') ?>`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.data.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.id;
|
||||
option.textContent = item.full_name;
|
||||
filterRepo.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTables() {
|
||||
await loadCommits();
|
||||
await loadPullRequests();
|
||||
}
|
||||
|
||||
function buildParams() {
|
||||
const params = new URLSearchParams();
|
||||
if (filterRepo.value) params.set('repo_id', filterRepo.value);
|
||||
if (filterUser.value) params.set('user_id', filterUser.value);
|
||||
if (filterStart.value) params.set('start_date', filterStart.value);
|
||||
if (filterEnd.value) params.set('end_date', filterEnd.value);
|
||||
params.set('limit', '200');
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function loadCommits() {
|
||||
const tbody = document.querySelector('#tableCommits tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="5">Loading...</td></tr>';
|
||||
|
||||
const response = await fetch(`<?= base_url('api/git/commits') ?>?${buildParams()}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
totalCommits.innerText = '0';
|
||||
tbody.innerHTML = '<tr><td colspan="5">Failed loading commits</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
totalCommits.innerText = String(result.data.length);
|
||||
|
||||
if (!result.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5">No commits found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = result.data.map(item => {
|
||||
const msg = (item.message || '').replace(/</g, '<').replace(/>/g, '>').slice(0, 120);
|
||||
const sha = item.short_sha || '';
|
||||
const link = item.html_url ? `<a href="${item.html_url}" target="_blank">${sha}</a>` : sha;
|
||||
return `<tr>
|
||||
<td>${item.committed_at || ''}</td>
|
||||
<td>${item.repository_full_name || ''}</td>
|
||||
<td>${item.user_username || item.author_name || ''}</td>
|
||||
<td>${link}</td>
|
||||
<td>${msg}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function loadPullRequests() {
|
||||
const tbody = document.querySelector('#tablePrs tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="5">Loading...</td></tr>';
|
||||
|
||||
const response = await fetch(`<?= base_url('api/git/pull-requests') ?>?${buildParams()}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
tbody.innerHTML = '<tr><td colspan="5">Failed loading pull requests</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5">No pull requests found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = result.data.map(item => {
|
||||
const title = (item.title || '').replace(/</g, '<').replace(/>/g, '>').slice(0, 100);
|
||||
const prText = `#${item.number} ${title}`;
|
||||
const link = item.html_url ? `<a href="${item.html_url}" target="_blank">${prText}</a>` : prText;
|
||||
return `<tr>
|
||||
<td>${item.updated_at_gitea || ''}</td>
|
||||
<td>${item.repository_full_name || ''}</td>
|
||||
<td>${item.user_username || ''}</td>
|
||||
<td>${link}</td>
|
||||
<td>${item.state || ''}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
@ -42,9 +42,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4 bg-light p-3 rounded shadow-sm">
|
||||
<div class="row mb-4 bg-light p-3 rounded shadow-sm g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">1. Select User</label>
|
||||
<label class="form-label fw-bold">1. Select User <span class="text-danger">*</span></label>
|
||||
<select id="selectUser" class="form-select">
|
||||
<option value="">-- Select User --</option>
|
||||
<option value="alamdh22">alamdh22</option>
|
||||
@ -55,22 +55,25 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">2. Select Repository</label>
|
||||
<label class="form-label fw-bold">2. Select Repository (Optional)</label>
|
||||
<select id="selectRepo" class="form-select" disabled>
|
||||
<option value="">-- Select Repository --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">3. Select Branch</label>
|
||||
<select id="selectBranch" class="form-select" disabled>
|
||||
<option value="">-- Select Branch --</option>
|
||||
<option value="">-- All Repositories --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold">3. Start Date <span class="text-danger">*</span></label>
|
||||
<input type="date" id="startDate" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<label class="form-label fw-bold">4. End Date <span class="text-danger">*</span></label>
|
||||
<input type="date" id="endDate" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button id="btnLoadCommits" class="btn btn-success w-100" disabled>
|
||||
Show All Commit
|
||||
Filter Commits
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,7 +81,7 @@
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12" id="commits-container">
|
||||
<div class="alert alert-secondary text-center">
|
||||
Please select a User -> Repository -> Branch first
|
||||
Please select User and Date Range first (Repository is optional)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -89,142 +92,234 @@
|
||||
<?= $this->section('script') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Ambil elemen
|
||||
const selectUser = document.getElementById('selectUser');
|
||||
const selectRepo = document.getElementById('selectRepo');
|
||||
const selectBranch = document.getElementById('selectBranch');
|
||||
const startDate = document.getElementById('startDate');
|
||||
const endDate = document.getElementById('endDate');
|
||||
const btnLoadCommits = document.getElementById('btnLoadCommits');
|
||||
const commitsContainer = document.getElementById('commits-container');
|
||||
|
||||
// 1. Cari Repo otomatis saat User dipilih di Dropdown
|
||||
let availableRepos = [];
|
||||
|
||||
setDefaultDateRange();
|
||||
updateButtonState();
|
||||
|
||||
selectUser.addEventListener('change', async () => {
|
||||
const username = selectUser.value;
|
||||
|
||||
// Reset pilihan di bawahnya jika user kembali memilih "-- Select User --"
|
||||
|
||||
if (!username) {
|
||||
selectRepo.innerHTML = '<option value="">-- Select Repository --</option>';
|
||||
selectRepo.disabled = true;
|
||||
selectBranch.innerHTML = '<option value="">-- Select Branch --</option>';
|
||||
selectBranch.disabled = true;
|
||||
btnLoadCommits.disabled = true;
|
||||
availableRepos = [];
|
||||
resetRepoSelect('-- All Repositories --', true);
|
||||
updateButtonState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Tampilkan loading
|
||||
selectRepo.innerHTML = '<option value="">Loading...</option>';
|
||||
selectRepo.disabled = true;
|
||||
selectBranch.innerHTML = '<option value="">-- Select Branch --</option>';
|
||||
selectBranch.disabled = true;
|
||||
btnLoadCommits.disabled = true;
|
||||
resetRepoSelect('Loading repositories...', true);
|
||||
updateButtonState();
|
||||
|
||||
try {
|
||||
// PERBAIKAN: Menggunakan getrepos sesuai route kamu
|
||||
const url = `<?= base_url('api/gitea/getrepos') ?>/${username}`;
|
||||
const res = await fetch(url);
|
||||
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
availableRepos = (response.success && Array.isArray(response.data))
|
||||
? response.data.map(repo => repo.name)
|
||||
: [];
|
||||
|
||||
if (response.success && response.data.length > 0) {
|
||||
selectRepo.innerHTML = '<option value="">-- Select Repository --</option>';
|
||||
response.data.forEach(repo => {
|
||||
selectRepo.innerHTML += `<option value="${repo.name}">${repo.name}</option>`;
|
||||
});
|
||||
selectRepo.disabled = false;
|
||||
} else {
|
||||
selectRepo.innerHTML = '<option value="">Repository not found</option>';
|
||||
}
|
||||
renderRepoOptions(availableRepos);
|
||||
} catch (error) {
|
||||
console.error('Error fetching repos:', error);
|
||||
selectRepo.innerHTML = '<option value="">Error loading repository</option>';
|
||||
availableRepos = [];
|
||||
resetRepoSelect('Error loading repository', true);
|
||||
alert('Failed to fetch repository data from the server.');
|
||||
} finally {
|
||||
updateButtonState();
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Ambil Branch ketika Repo dipilih
|
||||
selectRepo.addEventListener('change', async () => {
|
||||
const username = selectUser.value;
|
||||
const repo = selectRepo.value;
|
||||
if (!repo) return;
|
||||
startDate.addEventListener('change', updateButtonState);
|
||||
endDate.addEventListener('change', updateButtonState);
|
||||
|
||||
selectBranch.innerHTML = '<option value="">Loading...</option>';
|
||||
selectBranch.disabled = true;
|
||||
btnLoadCommits.disabled = true;
|
||||
|
||||
try {
|
||||
// PERBAIKAN: Menggunakan getbranches sesuai route kamu
|
||||
const url = `<?= base_url('api/gitea/getbranches') ?>/${username}/${repo}`;
|
||||
const res = await fetch(url);
|
||||
const response = await res.json();
|
||||
|
||||
if (response.success && response.data.length > 0) {
|
||||
selectBranch.innerHTML = '<option value="">-- Select Branch --</option>';
|
||||
response.data.forEach(branch => {
|
||||
selectBranch.innerHTML += `<option value="${branch.name}">${branch.name}</option>`;
|
||||
});
|
||||
selectBranch.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching branches:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Enable tombol Commit ketika branch dipilih
|
||||
selectBranch.addEventListener('change', () => {
|
||||
btnLoadCommits.disabled = !selectBranch.value;
|
||||
});
|
||||
|
||||
// 4. Ambil Commits dan Render HTML
|
||||
btnLoadCommits.addEventListener('click', async () => {
|
||||
const username = selectUser.value;
|
||||
const repo = selectRepo.value;
|
||||
const branch = selectBranch.value;
|
||||
|
||||
commitsContainer.innerHTML = '<div class="text-center"><div class="spinner-border text-primary" role="status"></div><p>Fetching commits...</p></div>';
|
||||
const selectedRepo = selectRepo.value;
|
||||
|
||||
if (!username || !startDate.value || !endDate.value) {
|
||||
commitsContainer.innerHTML = `<div class="alert alert-warning">User and date range are required.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const start = new Date(`${startDate.value}T00:00:00`);
|
||||
const end = new Date(`${endDate.value}T23:59:59.999`);
|
||||
|
||||
if (start > end) {
|
||||
commitsContainer.innerHTML = `<div class="alert alert-warning">Start date must be earlier than or equal to end date.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRepos = selectedRepo ? [selectedRepo] : availableRepos;
|
||||
|
||||
if (!targetRepos.length) {
|
||||
commitsContainer.innerHTML = `<div class="alert alert-info">No repositories found for selected user.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
commitsContainer.innerHTML = `<div class="text-center"><div class="spinner-border text-primary" role="status"></div><p>Fetching commits from ${targetRepos.length} repos...</p></div>`;
|
||||
|
||||
const limit = 100;
|
||||
const failedRepos = [];
|
||||
|
||||
try {
|
||||
// set limit=50
|
||||
const limit = 50;
|
||||
const url = `<?= base_url('api/gitea/getcommits') ?>/${username}/${repo}?sha=${encodeURIComponent(branch)}&limit=${limit}`;
|
||||
const res = await fetch(url);
|
||||
const response = await res.json();
|
||||
const results = await Promise.all(targetRepos.map(async (repoName) => {
|
||||
try {
|
||||
const url = `<?= base_url('api/gitea/getcommits') ?>/${username}/${repoName}?limit=${limit}`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (response.success) {
|
||||
renderCommits(response.data);
|
||||
} else {
|
||||
commitsContainer.innerHTML = `<div class="alert alert-danger">${response.message}</div>`;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
if (!response.success || !Array.isArray(response.data)) {
|
||||
throw new Error(response.message || 'Invalid response');
|
||||
}
|
||||
|
||||
return response.data.map(item => ({ ...item, __repo: repoName }));
|
||||
} catch (error) {
|
||||
failedRepos.push(repoName);
|
||||
console.error(`Error fetching commits for repo ${repoName}:`, error);
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
|
||||
const mergedCommits = results.flat();
|
||||
const filteredCommits = mergedCommits
|
||||
.filter(item => commitMatchesUser(item, username))
|
||||
.filter(item => isDateInRange(item, start, end))
|
||||
.sort((a, b) => {
|
||||
const dateA = getCommitDate(a)?.getTime() ?? 0;
|
||||
const dateB = getCommitDate(b)?.getTime() ?? 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
renderCommits(filteredCommits, failedRepos);
|
||||
} catch (error) {
|
||||
console.error('Error filtering commits:', error);
|
||||
commitsContainer.innerHTML = `<div class="alert alert-danger">Failed to contact the server.</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Fungsi untuk me-render HTML persis seperti template PHP yang kamu buat
|
||||
function renderCommits(commits) {
|
||||
function setDefaultDateRange() {
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const januaryFirst = new Date(currentYear, 0, 1);
|
||||
|
||||
startDate.value = formatDateInput(januaryFirst);
|
||||
endDate.value = formatDateInput(today);
|
||||
}
|
||||
|
||||
function formatDateInput(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function resetRepoSelect(message, disabled = true) {
|
||||
selectRepo.innerHTML = `<option value="">${message}</option>`;
|
||||
selectRepo.disabled = disabled;
|
||||
}
|
||||
|
||||
function renderRepoOptions(repos) {
|
||||
selectRepo.innerHTML = '<option value="">-- All Repositories --</option>';
|
||||
|
||||
if (!repos.length) {
|
||||
selectRepo.innerHTML = '<option value="">Repository not found</option>';
|
||||
selectRepo.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
repos.forEach(repoName => {
|
||||
selectRepo.innerHTML += `<option value="${repoName}">${repoName}</option>`;
|
||||
});
|
||||
|
||||
selectRepo.disabled = false;
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
const hasUser = Boolean(selectUser.value);
|
||||
const hasStartDate = Boolean(startDate.value);
|
||||
const hasEndDate = Boolean(endDate.value);
|
||||
btnLoadCommits.disabled = !(hasUser && hasStartDate && hasEndDate);
|
||||
}
|
||||
|
||||
function getCommitDate(item) {
|
||||
const rawDate = item.commit?.author?.date || item.commit?.committer?.date;
|
||||
if (!rawDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = new Date(rawDate);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
function isDateInRange(item, start, end) {
|
||||
const commitDate = getCommitDate(item);
|
||||
if (!commitDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return commitDate >= start && commitDate <= end;
|
||||
}
|
||||
|
||||
function commitMatchesUser(item, selectedUsername) {
|
||||
const username = (selectedUsername || '').toLowerCase();
|
||||
const authorUsername = (item.author?.username || '').toLowerCase();
|
||||
const commitAuthorName = (item.commit?.author?.name || '').toLowerCase();
|
||||
const commitAuthorEmail = (item.commit?.author?.email || '').toLowerCase();
|
||||
const emailPrefix = commitAuthorEmail.includes('@') ? commitAuthorEmail.split('@')[0] : commitAuthorEmail;
|
||||
|
||||
return [authorUsername, commitAuthorName, emailPrefix].some(value => value && value === username);
|
||||
}
|
||||
|
||||
function renderCommits(commits, failedRepos = []) {
|
||||
if (!commits || commits.length === 0) {
|
||||
commitsContainer.innerHTML = `
|
||||
const warning = failedRepos.length
|
||||
? `<div class="alert alert-warning text-center shadow-sm"><i class="bi bi-exclamation-triangle me-2"></i> Failed to load ${failedRepos.length} repository data.</div>`
|
||||
: '';
|
||||
|
||||
commitsContainer.innerHTML = `${warning}
|
||||
<div class="alert alert-info text-center shadow-sm">
|
||||
<i class="bi bi-info-circle me-2"></i> No commit history available for this branch.
|
||||
<i class="bi bi-info-circle me-2"></i> No commits found for selected filters.
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
if (failedRepos.length) {
|
||||
html += `<div class="alert alert-warning text-center shadow-sm"><i class="bi bi-exclamation-triangle me-2"></i> Failed to load ${failedRepos.length} repository data: ${escapeHtml(failedRepos.join(', '))}</div>`;
|
||||
}
|
||||
|
||||
commits.forEach((item, index) => {
|
||||
const message = item.commit?.message || 'No commit message';
|
||||
const authorName = item.author?.username || item.commit?.author?.name || 'Unknown';
|
||||
const avatar = item.author?.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(authorName)}&background=random`;
|
||||
|
||||
const dateObj = new Date(item.commit?.author?.date);
|
||||
const dateStr = dateObj.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) + ', ' +
|
||||
dateObj.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
const dateObj = getCommitDate(item);
|
||||
const dateStr = dateObj
|
||||
? dateObj.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) + ', ' +
|
||||
dateObj.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })
|
||||
: '-';
|
||||
|
||||
const shaShort = item.sha ? item.sha.substring(0, 10) : '';
|
||||
const commitUrl = item.html_url || '#';
|
||||
|
||||
const repoName = item.__repo || '-';
|
||||
|
||||
const additions = item.stats?.additions || 0;
|
||||
const deletions = item.stats?.deletions || 0;
|
||||
const files = item.files || [];
|
||||
@ -233,10 +328,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="card shadow-sm mb-3 border-0 commit-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-sm-row flex-column">
|
||||
|
||||
|
||||
<div class="d-flex align-items-start flex-grow-1 overflow-hidden w-100">
|
||||
<img src="${avatar}" class="rounded-circle avatar border me-3 flex-shrink-0" alt="Avatar ${authorName}" width="40" height="40">
|
||||
|
||||
|
||||
<div class="overflow-hidden w-100">
|
||||
<a href="${commitUrl}" target="_blank" class="commit-message text-truncate d-block fw-bold text-decoration-none text-dark" title="${escapeHtml(message)}">
|
||||
${escapeHtml(message)}
|
||||
@ -244,9 +339,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="text-muted small mt-1 text-truncate">
|
||||
<i class="bi bi-person-fill"></i> <strong>${escapeHtml(authorName)}</strong> melakukan commit pada ${dateStr}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<span class="badge bg-primary-subtle text-primary border">Repo: ${escapeHtml(repoName)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-sm-end flex-shrink-0 mt-3 mt-sm-0">
|
||||
<a href="${commitUrl}" target="_blank" class="badge bg-light text-dark border font-monospace text-decoration-none fs-6 mb-2 d-inline-block">
|
||||
<i class="bi bi-hash"></i>${shaShort}
|
||||
@ -269,12 +367,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="collapse mt-2" id="files-${index}">
|
||||
<div class="card card-body bg-light border-0 p-3 file-list">
|
||||
<ul class="list-unstyled mb-0 small font-monospace">`;
|
||||
|
||||
|
||||
files.forEach(file => {
|
||||
let statusClass = 'text-secondary';
|
||||
let icon = 'bi-file-earmark';
|
||||
if (file.status === 'added') { statusClass = 'text-success'; icon = 'bi-file-earmark-plus-fill'; }
|
||||
else if (file.status === 'modified') { statusClass = 'text-warning'; icon = 'bi-file-earmark-diff-fill'; }
|
||||
if (file.status === 'added') { statusClass = 'text-success'; icon = 'bi-file-earmark-plus-fill'; }
|
||||
else if (file.status === 'modified') { statusClass = 'text-warning'; icon = 'bi-file-earmark-diff-fill'; }
|
||||
else if (file.status === 'removed' || file.status === 'deleted') { statusClass = 'text-danger'; icon = 'bi-file-earmark-minus-fill'; }
|
||||
|
||||
html += `
|
||||
@ -285,7 +383,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
|
||||
html += ` </ul>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -300,13 +398,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
if(!unsafe) return '';
|
||||
if (!unsafe) return '';
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
<li><a href="<?=base_url();?>acttext">Activity Text</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li> <a class="waves-effect waves-dark" href='<?=base_url();?>gitea' aria-expanded="false"> <i class="fa-brands fa-git-alt"></i><span class='hide-menu'>Commits</span></a></li>
|
||||
<li> <a class="waves-effect waves-dark" href='<?=base_url();?>gitea' aria-expanded="false"> <i class="fa-brands fa-git-alt"></i><span class='hide-menu'>Gitea Dashboard</span></a></li>
|
||||
|
||||
<?php elseif ($isPS): ?>
|
||||
<!-- <li> <a class="has-arrow waves-effect waves-dark" href="javascript:void(0)" aria-expanded="false"> <i class="fas fa-user"></i><span class="hide-menu">User</span> </a>
|
||||
|
||||
7
scripts/gitea-sync-full.sh
Normal file
7
scripts/gitea-sync-full.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
php spark gitea:sync
|
||||
8
scripts/gitea-sync-incremental.sh
Normal file
8
scripts/gitea-sync-incremental.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
DAYS="${1:-1}"
|
||||
php spark gitea:sync-incremental "$DAYS"
|
||||
Loading…
x
Reference in New Issue
Block a user