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
|
contributionMap[day.date] = day.count
|
||||||
})
|
})
|
||||||
|
|
||||||
// Debug: Log contribution map
|
|
||||||
console.log('Contribution map sample:', Object.entries(contributionMap).slice(0, 10))
|
|
||||||
|
|
||||||
let col = 1
|
let col = 1
|
||||||
let currentDate = new Date(firstSunday)
|
let currentDate = new Date(firstSunday)
|
||||||
const lastDay = new Date(currentYear.value, 11, 31)
|
const lastDay = new Date(currentYear.value, 11, 31)
|
||||||
@ -173,11 +170,6 @@ const contributionGrid = computed(() => {
|
|||||||
currentDate.setDate(currentDate.getDate() + 1)
|
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
|
return grid
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -309,11 +301,6 @@ async function loadContributions() {
|
|||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
contributionData.value = data.contributions || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading contributions:', err)
|
console.error('Error loading contributions:', err)
|
||||||
error.value = err.message
|
error.value = err.message
|
||||||
@ -433,6 +420,11 @@ defineExpose({
|
|||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
min-height: 13px;
|
min-height: 13px;
|
||||||
min-width: 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 {
|
.contribution-cell:not(.intensity-empty):hover {
|
||||||
|
|||||||
@ -711,6 +711,15 @@
|
|||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
class="w-10rem"
|
class="w-10rem"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="selectedRepository"
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
label="Cache aktualisieren"
|
||||||
|
size="small"
|
||||||
|
outlined
|
||||||
|
:loading="refreshingCache"
|
||||||
|
@click="refreshRepositoryCache"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contribution Chart -->
|
<!-- Contribution Chart -->
|
||||||
@ -782,6 +791,7 @@ const gitRepositories = ref([])
|
|||||||
const selectedRepository = ref(null)
|
const selectedRepository = ref(null)
|
||||||
const selectedBranch = ref('main')
|
const selectedBranch = ref('main')
|
||||||
const selectedYear = ref(new Date().getFullYear())
|
const selectedYear = ref(new Date().getFullYear())
|
||||||
|
const refreshingCache = ref(false)
|
||||||
|
|
||||||
// Git Repository Management
|
// Git Repository Management
|
||||||
const editGitRepositories = ref([])
|
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) {
|
function formatDate(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
"phpstan/phpdoc-parser": "^2.3",
|
"phpstan/phpdoc-parser": "^2.3",
|
||||||
"symfony/asset": "7.1.*",
|
"symfony/asset": "7.1.*",
|
||||||
"symfony/asset-mapper": "7.1.*",
|
"symfony/asset-mapper": "7.1.*",
|
||||||
|
"symfony/cache": "7.1.*",
|
||||||
"symfony/console": "7.1.*",
|
"symfony/console": "7.1.*",
|
||||||
"symfony/doctrine-messenger": "7.1.*",
|
"symfony/doctrine-messenger": "7.1.*",
|
||||||
"symfony/dotenv": "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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "af57c523401fba0e523501b76e0629f0",
|
"content-hash": "c70cc2a152b707d0dcf4c562bd06f8d5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/core",
|
"name": "api-platform/core",
|
||||||
|
|||||||
@ -12,6 +12,8 @@ use Symfony\Component\HttpFoundation\JsonResponse;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Contracts\Cache\CacheInterface;
|
||||||
|
use Symfony\Contracts\Cache\ItemInterface;
|
||||||
|
|
||||||
#[Route('/api/git-repos')]
|
#[Route('/api/git-repos')]
|
||||||
class GitRepositoryController extends AbstractController
|
class GitRepositoryController extends AbstractController
|
||||||
@ -20,7 +22,8 @@ class GitRepositoryController extends AbstractController
|
|||||||
private GitService $gitService,
|
private GitService $gitService,
|
||||||
private GitHubService $githubService,
|
private GitHubService $githubService,
|
||||||
private GiteaService $giteaService,
|
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);
|
$limit = (int)$request->query->get('limit', 100);
|
||||||
$branch = $request->query->get('branch', $gitRepo->getBranch() ?? 'main');
|
$branch = $request->query->get('branch', $gitRepo->getBranch() ?? 'main');
|
||||||
|
|
||||||
// Choose service based on provider
|
// Cache key unique per repository, branch, and limit
|
||||||
if ($gitRepo->getProvider() === 'github') {
|
$cacheKey = sprintf(
|
||||||
$commits = $this->getCommitsFromGitHub($gitRepo, $branch, $limit);
|
'git_commits_%d_%s_%d',
|
||||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
$gitRepo->getId(),
|
||||||
$commits = $this->getCommitsFromGitea($gitRepo, $branch, $limit);
|
md5($branch),
|
||||||
} elseif ($gitRepo->getLocalPath()) {
|
$limit
|
||||||
$commits = $this->gitService->getCommits(
|
);
|
||||||
$gitRepo->getLocalPath(),
|
|
||||||
$branch,
|
$commits = $this->cache->get($cacheKey, function (ItemInterface $item) use ($gitRepo, $branch, $limit) {
|
||||||
$limit
|
// Cache for 15 minutes (commits change frequently during active development)
|
||||||
);
|
$item->expiresAfter(900);
|
||||||
} else {
|
|
||||||
return $this->json([
|
// Choose service based on provider
|
||||||
'error' => 'No data source configured (neither provider API nor local path)'
|
if ($gitRepo->getProvider() === 'github') {
|
||||||
], Response::HTTP_BAD_REQUEST);
|
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
|
// Update last sync timestamp
|
||||||
$gitRepo->setLastSync(new \DateTime());
|
$gitRepo->setLastSync(new \DateTime());
|
||||||
@ -89,23 +103,35 @@ class GitRepositoryController extends AbstractController
|
|||||||
$author = $request->query->get('author');
|
$author = $request->query->get('author');
|
||||||
$year = (int)$request->query->get('year', date('Y'));
|
$year = (int)$request->query->get('year', date('Y'));
|
||||||
|
|
||||||
// Choose service based on provider
|
// Cache key unique per repository, branch, author, and year
|
||||||
if ($gitRepo->getProvider() === 'github') {
|
$cacheKey = sprintf(
|
||||||
$contributions = $this->getContributionsFromGitHub($gitRepo, $year);
|
'git_contributions_%d_%s_%s_%d',
|
||||||
} elseif ($gitRepo->getProvider() === 'gitea') {
|
$gitRepo->getId(),
|
||||||
$contributions = $this->getContributionsFromGitea($gitRepo, $branch, $author, $year);
|
md5($branch),
|
||||||
} elseif ($gitRepo->getLocalPath()) {
|
md5($author ?? 'all'),
|
||||||
$contributions = $this->gitService->getContributions(
|
$year
|
||||||
$gitRepo->getLocalPath(),
|
);
|
||||||
$branch,
|
|
||||||
$author,
|
$contributions = $this->cache->get($cacheKey, function (ItemInterface $item) use ($gitRepo, $branch, $author, $year) {
|
||||||
$year
|
// Cache for 1 hour (contributions data is relatively stable)
|
||||||
);
|
$item->expiresAfter(3600);
|
||||||
} else {
|
|
||||||
return $this->json([
|
// Choose service based on provider
|
||||||
'error' => 'No data source configured (neither provider API nor local path)'
|
if ($gitRepo->getProvider() === 'github') {
|
||||||
], Response::HTTP_BAD_REQUEST);
|
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
|
// Update last sync timestamp
|
||||||
$gitRepo->setLastSync(new \DateTime());
|
$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);
|
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'])]
|
#[Route('/{id}/branches', name: 'api_git_repo_branches', methods: ['GET'])]
|
||||||
public function getBranches(GitRepository $gitRepo): JsonResponse
|
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