Cybrkyd's Git Repositories

GitGen - commit: 26d8c65

commit 26d8c65992c014b963a8f3067bd6432672e09e83630cecc16f621dc607d87ac8
author cybrkyd <git@cybrkyd.com> 2026-02-15 11:18:29 +0000
committer cybrkyd <git@cybrkyd.com> 2026-02-15 11:18:29 +0000

Commit Message

Activity graph

📊 Diffstat

gitgen.py 74
main.css 53
2 files changed, 124 insertions(+), 3 deletions(-)

Diff

diff --git a/gitgen.py b/gitgen.py
index 1dd4f98..d5eafa4 100644
--- a/gitgen.py
+++ b/gitgen.py
@@ -122,6 +122,71 @@ class GitRepoScanner:
pass
return None
+ def get_all_contributions(self) -> Dict[str, int]:
+ """Collect all commits from all repos for the past year"""
+ contributions = defaultdict(int)
+
+ for item in self.base_path.iterdir():
+ if item.is_dir() and (item / '.git').is_dir():
+ # Get all commits from the past year
+ log_output = self.run_git_command(item, [
+ 'log', '--all', '--since=1 year ago',
+ '--format=%ad', '--date=short'
+ ])
+
+ if log_output:
+ for line in log_output.strip().split('\n'):
+ date = line.strip()
+ if date: # Only count non-empty lines
+ contributions[date] += 1
+
+ return dict(contributions)
+
+ def generate_contribution_graph_html(self) -> str:
+ contributions = self.get_all_contributions()
+
+ monthly_commits = defaultdict(int)
+ for date_str, count in contributions.items():
+ year_month = date_str[:7]
+ monthly_commits[year_month] += count
+
+ today = datetime.datetime.now()
+ months_data = []
+
+ for i in range(11, -1, -1):
+ month_date = today - datetime.timedelta(days=i*30)
+ year_month = month_date.strftime('%Y-%m')
+ month_label = month_date.strftime('%b')
+ commit_count = monthly_commits.get(year_month, 0)
+ months_data.append({
+ 'label': month_label,
+ 'count': commit_count,
+ 'year_month': year_month
+ })
+
+ # Find max for scaling
+ max_commits = max((m['count'] for m in months_data), default=1)
+ if max_commits == 0:
+ max_commits = 1
+
+ graph_html = '<div class="monthly-chart">\n'
+
+ for month in months_data:
+ height_percent = (month['count'] / max_commits) * 100 if max_commits > 0 else 0
+
+ date_obj = datetime.datetime.strptime(month['year_month'], '%Y-%m')
+ formatted_date = date_obj.strftime('%b %Y')
+
+ graph_html += f'''<div class="month-col">
+ <div class="col-bar" style="height:{height_percent}%" title="{month['count']} commits in {formatted_date}"></div>
+ <div class="col-label">{month['label']}</div>
+ <div class="col-count">{month['count']}</div>
+ </div>\n'''
+
+ graph_html += '</div>'
+
+ return graph_html
+
def get_working_tree(self, repo_path: Path) -> List[Dict]:
if repo_path in self.tree_cache:
return self.tree_cache[repo_path]
@@ -181,7 +246,6 @@ class GitRepoScanner:
parts = line.split('|', 8)
if len(parts) == 9:
commit_hash = parts[0]
- # Get tags for this commit
tags_output = self.run_git_command(repo_path, [
'tag', '--points-at', commit_hash
])
@@ -271,13 +335,13 @@ class HTMLGenerator:
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
- # Generate timestamp once for all pages
self.current_time = datetime.datetime.now()
self.footer_time = datetime.datetime.now(datetime.timezone.utc)
self.time_str = self.footer_time.strftime('%Y-%m-%d %H:%M:%S %z')
def generate_index(self, repos: List[Dict]) -> str:
"""Generate main index page"""
+ graph_html = self.scanner.generate_contribution_graph_html()
html_fragments = [
f"""<!DOCTYPE html>
@@ -342,7 +406,6 @@ class HTMLGenerator:
date_style = ""
if last_commit_date != 'N/A':
try:
- # Parse just the %Y-%m-%d portion
commit_date = datetime.datetime.strptime(last_commit_date, "%Y-%m-%d")
age_days = (self.current_time - commit_date).days
if age_days <= 31:
@@ -364,6 +427,11 @@ class HTMLGenerator:
</table>
</div>
+ <div class="graph-container">
+ <h2>Activity</h2>
+ {graph_html}
+ </div>
+
<div class="footer">
<p>Made with <a href="https://git.cybrkyd.com/GitGen" target="&#95;blank" rel="noopener">GitGen</a> by Cybrkyd</p>
<p>Generated on {self.time_str}</p>
diff --git a/main.css b/main.css
index 4faa522..2df6ef5 100644
--- a/main.css
+++ b/main.css
@@ -533,6 +533,59 @@ pre code {
min-width: 100px;
}
+ /* Commit Chart */
+ .graph-container h2 {
+ text-align:center;
+ }
+
+ .monthly-chart {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-around;
+ gap: 10px;
+ max-width: 800px;
+ height: 200px;
+ margin: 20px auto;
+ padding: 20px;
+ background: #fff;
+ border-radius: 8px;
+ }
+
+ .month-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 100%;
+ }
+
+ .col-bar {
+ width: 100%;
+ max-width: 50px;
+ background: #40c463;
+ border-radius: 4px 4px 0 0;
+ min-height: 2px;
+ margin-top: auto;
+ transition: background 0.2s;
+ cursor: pointer;
+ }
+
+ .col-bar:hover {
+ background: #30a14e;
+ }
+
+ .col-label {
+ font-size: 12px;
+ color: #666;
+ font-weight: 500;
+ margin-top: 5px;
+ }
+
+ .col-count {
+ font-size: 11px;
+ color: #888;
+ }
+
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;