feat: Implement cache management for Git repository contributions and commits
This commit is contained in:
parent
404dc2c73a
commit
c1278d2e45
@ -130,9 +130,6 @@ const contributionGrid = computed(() => {
|
||||
contributionMap[day.date] = day.count
|
||||
})
|
||||
|
||||
// Debug: Log contribution map
|
||||
console.log('Contribution map sample:', Object.entries(contributionMap).slice(0, 10))
|
||||
|
||||
let col = 1
|
||||
let currentDate = new Date(firstSunday)
|
||||
const lastDay = new Date(currentYear.value, 11, 31)
|
||||
@ -173,11 +170,6 @@ const contributionGrid = computed(() => {
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
|
||||
// Debug: Log grid sample
|
||||
const gridWithCommits = grid.filter(g => g.count > 0)
|
||||
console.log('Grid cells with commits:', gridWithCommits.length)
|
||||
console.log('Sample cells with commits:', gridWithCommits.slice(0, 5))
|
||||
|
||||
return grid
|
||||
})
|
||||
|
||||
@ -309,11 +301,6 @@ async function loadContributions() {
|
||||
|
||||
const data = await response.json()
|
||||
contributionData.value = data.contributions || []
|
||||
|
||||
// Debug: Log the data to check format
|
||||
console.log('Contribution data received:', contributionData.value.slice(0, 5))
|
||||
console.log('Total days:', contributionData.value.length)
|
||||
console.log('Days with commits:', contributionData.value.filter(d => d.count > 0).length)
|
||||
} catch (err) {
|
||||
console.error('Error loading contributions:', err)
|
||||
error.value = err.message
|
||||
@ -433,6 +420,11 @@ defineExpose({
|
||||
transition: all 0.15s ease;
|
||||
min-height: 13px;
|
||||
min-width: 13px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .contribution-cell {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.contribution-cell:not(.intensity-empty):hover {
|
||||
|
||||
@ -711,6 +711,15 @@
|
||||
:use-grouping="false"
|
||||
class="w-10rem"
|
||||
/>
|
||||
<Button
|
||||
v-if="selectedRepository"
|
||||
icon="pi pi-refresh"
|
||||
label="Cache aktualisieren"
|
||||
size="small"
|
||||
outlined
|
||||
:loading="refreshingCache"
|
||||
@click="refreshRepositoryCache"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Contribution Chart -->
|
||||
@ -782,6 +791,7 @@ const gitRepositories = ref([])
|
||||
const selectedRepository = ref(null)
|
||||
const selectedBranch = ref('main')
|
||||
const selectedYear = ref(new Date().getFullYear())
|
||||
const refreshingCache = ref(false)
|
||||
|
||||
// Git Repository Management
|
||||
const editGitRepositories = ref([])
|
||||
@ -1302,6 +1312,48 @@ async function deleteProject() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRepositoryCache() {
|
||||
if (!selectedRepository.value) return
|
||||
|
||||
refreshingCache.value = true
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/git-repos/${selectedRepository.value.id}/refresh-cache`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Cache konnte nicht aktualisiert werden')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Erfolg',
|
||||
detail: result.message || 'Cache wurde aktualisiert',
|
||||
life: 3000
|
||||
})
|
||||
|
||||
// Reload git data
|
||||
await loadGitRepositories(viewingProject.value.id)
|
||||
} catch (error) {
|
||||
console.error('Error refreshing cache:', error)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Fehler',
|
||||
detail: error.message || 'Cache konnte nicht aktualisiert werden',
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
refreshingCache.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"phpstan/phpdoc-parser": "^2.3",
|
||||
"symfony/asset": "7.1.*",
|
||||
"symfony/asset-mapper": "7.1.*",
|
||||
"symfony/cache": "7.1.*",
|
||||
"symfony/console": "7.1.*",
|
||||
"symfony/doctrine-messenger": "7.1.*",
|
||||
"symfony/dotenv": "7.1.*",
|
||||
|
||||
2
composer.lock
generated
2
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "af57c523401fba0e523501b76e0629f0",
|
||||
"content-hash": "c70cc2a152b707d0dcf4c562bd06f8d5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
|
||||
@ -12,6 +12,8 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api/git-repos')]
|
||||
class GitRepositoryController extends AbstractController
|
||||
@ -20,7 +22,8 @@ class GitRepositoryController extends AbstractController
|
||||
private GitService $gitService,
|
||||
private GitHubService $githubService,
|
||||
private GiteaService $giteaService,
|
||||
private EntityManagerInterface $em
|
||||
private EntityManagerInterface $em,
|
||||
private CacheInterface $cache
|
||||
) {
|
||||
}
|
||||
|
||||
@ -31,22 +34,33 @@ class GitRepositoryController extends AbstractController
|
||||
$limit = (int)$request->query->get('limit', 100);
|
||||
$branch = $request->query->get('branch', $gitRepo->getBranch() ?? 'main');
|
||||
|
||||
// Choose service based on provider
|
||||
if ($gitRepo->getProvider() === 'github') {
|
||||
$commits = $this->getCommitsFromGitHub($gitRepo, $branch, $limit);
|
||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
||||
$commits = $this->getCommitsFromGitea($gitRepo, $branch, $limit);
|
||||
} elseif ($gitRepo->getLocalPath()) {
|
||||
$commits = $this->gitService->getCommits(
|
||||
$gitRepo->getLocalPath(),
|
||||
$branch,
|
||||
$limit
|
||||
);
|
||||
} else {
|
||||
return $this->json([
|
||||
'error' => 'No data source configured (neither provider API nor local path)'
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
// Cache key unique per repository, branch, and limit
|
||||
$cacheKey = sprintf(
|
||||
'git_commits_%d_%s_%d',
|
||||
$gitRepo->getId(),
|
||||
md5($branch),
|
||||
$limit
|
||||
);
|
||||
|
||||
$commits = $this->cache->get($cacheKey, function (ItemInterface $item) use ($gitRepo, $branch, $limit) {
|
||||
// Cache for 15 minutes (commits change frequently during active development)
|
||||
$item->expiresAfter(900);
|
||||
|
||||
// Choose service based on provider
|
||||
if ($gitRepo->getProvider() === 'github') {
|
||||
return $this->getCommitsFromGitHub($gitRepo, $branch, $limit);
|
||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
||||
return $this->getCommitsFromGitea($gitRepo, $branch, $limit);
|
||||
} elseif ($gitRepo->getLocalPath()) {
|
||||
return $this->gitService->getCommits(
|
||||
$gitRepo->getLocalPath(),
|
||||
$branch,
|
||||
$limit
|
||||
);
|
||||
}
|
||||
|
||||
throw new \Exception('No data source configured (neither provider API nor local path)');
|
||||
});
|
||||
|
||||
// Update last sync timestamp
|
||||
$gitRepo->setLastSync(new \DateTime());
|
||||
@ -89,23 +103,35 @@ class GitRepositoryController extends AbstractController
|
||||
$author = $request->query->get('author');
|
||||
$year = (int)$request->query->get('year', date('Y'));
|
||||
|
||||
// Choose service based on provider
|
||||
if ($gitRepo->getProvider() === 'github') {
|
||||
$contributions = $this->getContributionsFromGitHub($gitRepo, $year);
|
||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
||||
$contributions = $this->getContributionsFromGitea($gitRepo, $branch, $author, $year);
|
||||
} elseif ($gitRepo->getLocalPath()) {
|
||||
$contributions = $this->gitService->getContributions(
|
||||
$gitRepo->getLocalPath(),
|
||||
$branch,
|
||||
$author,
|
||||
$year
|
||||
);
|
||||
} else {
|
||||
return $this->json([
|
||||
'error' => 'No data source configured (neither provider API nor local path)'
|
||||
], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
// Cache key unique per repository, branch, author, and year
|
||||
$cacheKey = sprintf(
|
||||
'git_contributions_%d_%s_%s_%d',
|
||||
$gitRepo->getId(),
|
||||
md5($branch),
|
||||
md5($author ?? 'all'),
|
||||
$year
|
||||
);
|
||||
|
||||
$contributions = $this->cache->get($cacheKey, function (ItemInterface $item) use ($gitRepo, $branch, $author, $year) {
|
||||
// Cache for 1 hour (contributions data is relatively stable)
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
// Choose service based on provider
|
||||
if ($gitRepo->getProvider() === 'github') {
|
||||
return $this->getContributionsFromGitHub($gitRepo, $year);
|
||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
||||
return $this->getContributionsFromGitea($gitRepo, $branch, $author, $year);
|
||||
} elseif ($gitRepo->getLocalPath()) {
|
||||
return $this->gitService->getContributions(
|
||||
$gitRepo->getLocalPath(),
|
||||
$branch,
|
||||
$author,
|
||||
$year
|
||||
);
|
||||
}
|
||||
|
||||
throw new \Exception('No data source configured (neither provider API nor local path)');
|
||||
});
|
||||
|
||||
// Update last sync timestamp
|
||||
$gitRepo->setLastSync(new \DateTime());
|
||||
@ -141,6 +167,55 @@ class GitRepositoryController extends AbstractController
|
||||
return $this->giteaService->getContributions($parsed['baseUrl'], $parsed['owner'], $parsed['repo'], $branch, $author, $year);
|
||||
}
|
||||
|
||||
#[Route('/{id}/refresh-cache', name: 'api_git_repo_refresh_cache', methods: ['POST'])]
|
||||
public function refreshCache(GitRepository $gitRepo): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
|
||||
try {
|
||||
// Invalidate all cache entries for this repository
|
||||
$repoId = $gitRepo->getId();
|
||||
$years = range(date('Y') - 2, date('Y') + 1); // Last 2 years + current + next
|
||||
$deletedKeys = [];
|
||||
|
||||
// Delete contribution caches for multiple years
|
||||
foreach ($years as $year) {
|
||||
$patterns = [
|
||||
sprintf('git_contributions_%d_*_%d', $repoId, $year),
|
||||
];
|
||||
foreach ($patterns as $pattern) {
|
||||
// Note: Pattern deletion requires specific cache adapter support
|
||||
// For filesystem cache, we need to delete specific known keys
|
||||
$key = sprintf('git_contributions_%d_%s_all_%d', $repoId, md5('main'), $year);
|
||||
if ($this->cache->delete($key)) {
|
||||
$deletedKeys[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete commit caches
|
||||
$commitKey = sprintf('git_commits_%d_%s_100', $repoId, md5('main'));
|
||||
if ($this->cache->delete($commitKey)) {
|
||||
$deletedKeys[] = $commitKey;
|
||||
}
|
||||
|
||||
// Update last sync timestamp
|
||||
$gitRepo->setLastSync(new \DateTime());
|
||||
$this->em->flush();
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'Cache erfolgreich invalidiert. Nächste Anfrage lädt frische Daten.',
|
||||
'deletedKeys' => count($deletedKeys)
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'error' => 'Cache-Invalidierung fehlgeschlagen: ' . $e->getMessage()
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/{id}/branches', name: 'api_git_repo_branches', methods: ['GET'])]
|
||||
public function getBranches(GitRepository $gitRepo): JsonResponse
|
||||
{
|
||||
|
||||
62
src/EventListener/GitRepositoryCacheListener.php
Normal file
62
src/EventListener/GitRepositoryCacheListener.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\GitRepository;
|
||||
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
|
||||
use Doctrine\ORM\Event\PreUpdateEventArgs;
|
||||
use Doctrine\ORM\Event\PreRemoveEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
#[AsEntityListener(event: Events::preUpdate, entity: GitRepository::class)]
|
||||
#[AsEntityListener(event: Events::preRemove, entity: GitRepository::class)]
|
||||
class GitRepositoryCacheListener
|
||||
{
|
||||
public function __construct(
|
||||
private CacheInterface $cache
|
||||
) {
|
||||
}
|
||||
|
||||
public function preUpdate(GitRepository $gitRepo, PreUpdateEventArgs $event): void
|
||||
{
|
||||
// Only invalidate if relevant fields changed
|
||||
if ($event->hasChangedField('branch') ||
|
||||
$event->hasChangedField('url') ||
|
||||
$event->hasChangedField('localPath') ||
|
||||
$event->hasChangedField('provider')) {
|
||||
$this->invalidateRepositoryCache($gitRepo);
|
||||
}
|
||||
}
|
||||
|
||||
public function preRemove(GitRepository $gitRepo, PreRemoveEventArgs $event): void
|
||||
{
|
||||
$this->invalidateRepositoryCache($gitRepo);
|
||||
}
|
||||
|
||||
private function invalidateRepositoryCache(GitRepository $gitRepo): void
|
||||
{
|
||||
// Delete all cache entries for this repository
|
||||
$repoId = $gitRepo->getId();
|
||||
$years = range(date('Y') - 2, date('Y') + 1);
|
||||
|
||||
// Delete contribution caches
|
||||
foreach ($years as $year) {
|
||||
// Delete for common branches and author combinations
|
||||
$branches = ['main', 'master', 'develop'];
|
||||
foreach ($branches as $branch) {
|
||||
$key = sprintf('git_contributions_%d_%s_all_%d', $repoId, md5($branch), $year);
|
||||
$this->cache->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete commit caches
|
||||
$limits = [50, 100, 200];
|
||||
foreach ($limits as $limit) {
|
||||
foreach (['main', 'master', 'develop'] as $branch) {
|
||||
$key = sprintf('git_commits_%d_%s_%d', $repoId, md5($branch), $limit);
|
||||
$this->cache->delete($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user