Skip to content

Commit ad73552

Browse files
committed
feat: add repository metrics dashboard with branch counter and commit statistics
- Implement branch counter showing local/remote branches (7/12 format) - Add commit statistics with today/week/month activity tracking (T:4 W:48 M:82) - Create 2-row status panel to prevent overcrowding - Use real Git commands for accurate data (no simulations) - Add error handling and auto-refresh on branch updates - Document lessons learned using "Teile und Herrsche" methodology The features provide developers with at-a-glance repository insights: - Branch distribution overview - Team activity metrics - Productivity tracking - 100% read-only operations for safety
1 parent 38221b5 commit ad73552

14 files changed

+748
-425
lines changed

CLAUDE.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,35 @@ Popups inherit from `ViewModels.Popup` and use a consistent pattern:
9898
- `Popup.InvokeAsync()` shows the popup and returns result
9999

100100
### Testing Git Operations
101-
The application supports portable mode by creating a `data` folder next to the executable. This allows testing without affecting the system-wide installation.
101+
The application supports portable mode by creating a `data` folder next to the executable. This allows testing without affecting the system-wide installation.
102+
103+
## Lessons Learned
104+
105+
### Development Methodology
106+
**Divide and Conquer (Teile und Herrsche)**: When implementing new features, follow these principles:
107+
1. **Start Small**: Implement ONE small, working feature completely before expanding
108+
2. **Use Real Data**: Never use simulated/fake data - always work with actual Git commands
109+
3. **Backend First**: Build the Git command wrapper and data model before any UI
110+
4. **Test Early**: Verify functionality with real repositories before adding complexity
111+
5. **Incremental Enhancement**: Add features one at a time, testing each addition
112+
113+
### Common Pitfalls to Avoid
114+
- **Script-Kiddy Approach**: Don't try to implement everything at once with simulated data
115+
- **Missing Validation**: Always check if methods/properties exist before using them
116+
- **Protection Levels**: Respect access modifiers - don't try to access internal/protected members
117+
- **Converter Dependencies**: Verify all converters exist before referencing them in XAML
118+
- **Namespace Conflicts**: Use fully qualified names when there are ambiguous references
119+
120+
### Shutdown Performance
121+
When dealing with background operations and UI updates:
122+
- Use `CancellationTokenSource` for all long-running operations
123+
- Implement `_isUnloading` flag to prevent dispatcher operations during shutdown
124+
- Clean up event handlers properly in `OnUnloaded()`
125+
- Cancel pending operations before disposal to prevent 30+ second hangs
126+
127+
### Git Command Integration
128+
- All Git operations must inherit from `Command` class
129+
- Use `Command.Exec()` for fire-and-forget, `Command.ExecAsync()` for awaitable operations
130+
- Parse command output in `ParseResult()` override
131+
- Log all commands through `ICommandLog` interface
132+
- Handle errors gracefully with proper exception handling

src/Commands/CountBranches.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Collections.Generic;
2+
3+
namespace SourceGit.Commands
4+
{
5+
/// <summary>
6+
/// Count branches in the repository
7+
/// </summary>
8+
public class CountBranches : Command
9+
{
10+
public class BranchCount
11+
{
12+
public int Total { get; set; }
13+
public int Local { get; set; }
14+
public int Remote { get; set; }
15+
public Dictionary<string, int> ByType { get; set; } = new Dictionary<string, int>();
16+
}
17+
18+
public CountBranches(string repo)
19+
{
20+
WorkingDirectory = repo;
21+
Context = repo;
22+
Args = "branch -a --no-column";
23+
}
24+
25+
public new BranchCount Result()
26+
{
27+
var output = ReadToEnd();
28+
if (output.IsSuccess)
29+
{
30+
var count = new BranchCount();
31+
var lines = output.StdOut.Split('\n');
32+
33+
foreach (var line in lines)
34+
{
35+
if (string.IsNullOrWhiteSpace(line))
36+
continue;
37+
38+
var branch = line.Trim();
39+
if (branch.StartsWith("*"))
40+
branch = branch.Substring(1).Trim();
41+
42+
if (branch.StartsWith("remotes/"))
43+
{
44+
count.Remote++;
45+
46+
// Count by remote name
47+
var parts = branch.Split('/');
48+
if (parts.Length > 1)
49+
{
50+
var remoteName = parts[1];
51+
if (!count.ByType.ContainsKey($"remote/{remoteName}"))
52+
count.ByType[$"remote/{remoteName}"] = 0;
53+
count.ByType[$"remote/{remoteName}"]++;
54+
}
55+
}
56+
else
57+
{
58+
count.Local++;
59+
60+
// Detect GitFlow branch types
61+
if (branch.StartsWith("feature/"))
62+
{
63+
if (!count.ByType.ContainsKey("feature"))
64+
count.ByType["feature"] = 0;
65+
count.ByType["feature"]++;
66+
}
67+
else if (branch.StartsWith("release/"))
68+
{
69+
if (!count.ByType.ContainsKey("release"))
70+
count.ByType["release"] = 0;
71+
count.ByType["release"]++;
72+
}
73+
else if (branch.StartsWith("hotfix/"))
74+
{
75+
if (!count.ByType.ContainsKey("hotfix"))
76+
count.ByType["hotfix"] = 0;
77+
count.ByType["hotfix"]++;
78+
}
79+
else if (branch == "develop" || branch == "master" || branch == "main")
80+
{
81+
if (!count.ByType.ContainsKey("main"))
82+
count.ByType["main"] = 0;
83+
count.ByType["main"]++;
84+
}
85+
else
86+
{
87+
if (!count.ByType.ContainsKey("other"))
88+
count.ByType["other"] = 0;
89+
count.ByType["other"]++;
90+
}
91+
}
92+
93+
count.Total++;
94+
}
95+
96+
return count;
97+
}
98+
99+
return new BranchCount();
100+
}
101+
}
102+
}

src/Commands/QueryCommitStats.cs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text.RegularExpressions;
4+
5+
namespace SourceGit.Commands
6+
{
7+
/// <summary>
8+
/// Query commit statistics for different time periods
9+
/// </summary>
10+
public class QueryCommitStats : Command
11+
{
12+
public class CommitStats
13+
{
14+
public int TotalCommits { get; set; }
15+
public Dictionary<string, int> CommitsByAuthor { get; set; } = new Dictionary<string, int>();
16+
public Dictionary<int, int> CommitsByHour { get; set; } = new Dictionary<int, int>();
17+
public Dictionary<string, int> CommitsByBranch { get; set; } = new Dictionary<string, int>();
18+
public DateTime Since { get; set; }
19+
public DateTime Until { get; set; }
20+
}
21+
22+
private readonly DateTime _since;
23+
private readonly DateTime _until;
24+
25+
public QueryCommitStats(string repo, DateTime since, DateTime until = default)
26+
{
27+
WorkingDirectory = repo;
28+
Context = repo;
29+
_since = since;
30+
_until = until == default ? DateTime.Now : until;
31+
32+
// Query commits with author and date
33+
Args = $"log --since=\"{_since:yyyy-MM-dd HH:mm:ss}\" --until=\"{_until:yyyy-MM-dd HH:mm:ss}\" --format=\"%aN|%aI\" --all";
34+
}
35+
36+
public new CommitStats Result()
37+
{
38+
var stats = new CommitStats
39+
{
40+
Since = _since,
41+
Until = _until
42+
};
43+
44+
var output = ReadToEnd();
45+
if (output.IsSuccess && !string.IsNullOrWhiteSpace(output.StdOut))
46+
{
47+
var lines = output.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
48+
stats.TotalCommits = lines.Length;
49+
50+
// Initialize hour buckets
51+
for (int i = 0; i < 24; i++)
52+
{
53+
stats.CommitsByHour[i] = 0;
54+
}
55+
56+
foreach (var line in lines)
57+
{
58+
var parts = line.Split('|');
59+
if (parts.Length >= 2)
60+
{
61+
// Count by author
62+
var author = parts[0].Trim();
63+
if (!string.IsNullOrEmpty(author))
64+
{
65+
if (!stats.CommitsByAuthor.ContainsKey(author))
66+
stats.CommitsByAuthor[author] = 0;
67+
stats.CommitsByAuthor[author]++;
68+
}
69+
70+
// Count by hour
71+
if (DateTime.TryParse(parts[1], out var commitDate))
72+
{
73+
stats.CommitsByHour[commitDate.Hour]++;
74+
}
75+
}
76+
}
77+
}
78+
79+
// Get branch statistics separately
80+
QueryBranchStats(stats);
81+
82+
return stats;
83+
}
84+
85+
private void QueryBranchStats(CommitStats stats)
86+
{
87+
// Query commits per branch
88+
var branchCmd = new QueryCommitStats(WorkingDirectory, _since, _until)
89+
{
90+
Args = $"log --since=\"{_since:yyyy-MM-dd HH:mm:ss}\" --until=\"{_until:yyyy-MM-dd HH:mm:ss}\" --format=\"%H\" --all"
91+
};
92+
93+
var branchOutput = branchCmd.ReadToEnd();
94+
if (branchOutput.IsSuccess && !string.IsNullOrWhiteSpace(branchOutput.StdOut))
95+
{
96+
var commits = branchOutput.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
97+
98+
foreach (var commit in commits)
99+
{
100+
// Find which branches contain this commit
101+
var containsCmd = new QueryCommitStats(WorkingDirectory, _since, _until)
102+
{
103+
Args = $"branch --contains {commit.Trim()} --format=\"%(refname:short)\""
104+
};
105+
106+
var containsOutput = containsCmd.ReadToEnd();
107+
if (containsOutput.IsSuccess && !string.IsNullOrWhiteSpace(containsOutput.StdOut))
108+
{
109+
var branches = containsOutput.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries);
110+
foreach (var branch in branches)
111+
{
112+
var branchName = branch.Trim();
113+
if (!string.IsNullOrEmpty(branchName))
114+
{
115+
if (!stats.CommitsByBranch.ContainsKey(branchName))
116+
stats.CommitsByBranch[branchName] = 0;
117+
stats.CommitsByBranch[branchName]++;
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
/// <summary>
126+
/// Get stats for today
127+
/// </summary>
128+
public static CommitStats GetTodayStats(string repo)
129+
{
130+
var today = DateTime.Today;
131+
var cmd = new QueryCommitStats(repo, today);
132+
return cmd.Result();
133+
}
134+
135+
/// <summary>
136+
/// Get stats for this week
137+
/// </summary>
138+
public static CommitStats GetWeekStats(string repo)
139+
{
140+
var today = DateTime.Today;
141+
var dayOfWeek = (int)today.DayOfWeek;
142+
var weekStart = today.AddDays(-dayOfWeek);
143+
var cmd = new QueryCommitStats(repo, weekStart);
144+
return cmd.Result();
145+
}
146+
147+
/// <summary>
148+
/// Get stats for this month
149+
/// </summary>
150+
public static CommitStats GetMonthStats(string repo)
151+
{
152+
var today = DateTime.Today;
153+
var monthStart = new DateTime(today.Year, today.Month, 1);
154+
var cmd = new QueryCommitStats(repo, monthStart);
155+
return cmd.Result();
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)