diff --git a/app/Commands/SyncGiteaData.php b/app/Commands/SyncGiteaData.php
new file mode 100644
index 0000000..2a8dbc1
--- /dev/null
+++ b/app/Commands/SyncGiteaData.php
@@ -0,0 +1,43 @@
+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');
+ }
+ }
+ }
+}
diff --git a/app/Commands/SyncGiteaIncremental.php b/app/Commands/SyncGiteaIncremental.php
new file mode 100644
index 0000000..f34ee74
--- /dev/null
+++ b/app/Commands/SyncGiteaIncremental.php
@@ -0,0 +1,50 @@
+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');
+ }
+ }
+ }
+}
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index af914b5..17bf968 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -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');
diff --git a/app/Controllers/Api/GitApi.php b/app/Controllers/Api/GitApi.php
new file mode 100644
index 0000000..37322b4
--- /dev/null
+++ b/app/Controllers/Api/GitApi.php
@@ -0,0 +1,236 @@
+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);
+ }
+}
diff --git a/app/Controllers/Gitea.php b/app/Controllers/Gitea.php
index dc1ce8a..286bf04 100644
--- a/app/Controllers/Gitea.php
+++ b/app/Controllers/Gitea.php
@@ -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);
}
}
\ No newline at end of file
diff --git a/app/Database/Migrations/2026-04-22-000001_CreateGitTables.php b/app/Database/Migrations/2026-04-22-000001_CreateGitTables.php
new file mode 100644
index 0000000..7c1c78b
--- /dev/null
+++ b/app/Database/Migrations/2026-04-22-000001_CreateGitTables.php
@@ -0,0 +1,339 @@
+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);
+ }
+}
diff --git a/app/Libraries/GiteaSyncService.php b/app/Libraries/GiteaSyncService.php
new file mode 100644
index 0000000..c112661
--- /dev/null
+++ b/app/Libraries/GiteaSyncService.php
@@ -0,0 +1,414 @@
+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;
+ }
+ }
+}
diff --git a/app/Models/GitCommitsModel.php b/app/Models/GitCommitsModel.php
new file mode 100644
index 0000000..feb7342
--- /dev/null
+++ b/app/Models/GitCommitsModel.php
@@ -0,0 +1,29 @@
+extend('layouts/main.php') ?>
+
+= $this->section('content') ?>
+
+
+
+
+
Gitea Dashboard (DB)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Latest Commits
+
Total Commits: 0
+
+
+
+
+
+ | Date |
+ Repository |
+ User |
+ SHA |
+ Message |
+
+
+
+
+
+
+
+
+
+
+
+
+
Latest Pull Requests
+
+
+
+
+ | Updated |
+ Repository |
+ User |
+ PR |
+ State |
+
+
+
+
+
+
+
+
+
+
+
+= $this->endSection() ?>
+
+= $this->section('script') ?>
+
+= $this->endSection() ?>
diff --git a/app/Views/gitea_index.php b/app/Views/gitea_index.php
index 0de8d36..3b93761 100644
--- a/app/Views/gitea_index.php
+++ b/app/Views/gitea_index.php
@@ -42,9 +42,9 @@
-
+
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -78,7 +81,7 @@
- Please select a User -> Repository -> Branch first
+ Please select User and Date Range first (Repository is optional)
@@ -89,142 +92,234 @@
= $this->section('script') ?>
diff --git a/app/Views/layouts/_sidebar.php b/app/Views/layouts/_sidebar.php
index 62818c2..b4e30bd 100644
--- a/app/Views/layouts/_sidebar.php
+++ b/app/Views/layouts/_sidebar.php
@@ -113,7 +113,7 @@
Activity Text
-
+