Skip to content

Commit c6c2558

Browse files
Apply literate coding style to C# source files (#8)
* Initial plan * Add literate coding style comments to C# source files - Added leading comments to all logical code blocks - Separated code paragraphs with blank lines - Made method logic deducible from comments alone - Comments explain WHAT the code does, not HOW - Preserved all existing XML documentation comments - Applied to all source files in src/ and test/ directories * Remove unnecessary blank lines and expand external tool comments Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
1 parent 4e34a30 commit c6c2558

File tree

13 files changed

+155
-72
lines changed

13 files changed

+155
-72
lines changed

src/DemaConsulting.BuildMark/BuildInformation.cs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -56,88 +56,91 @@ public record BuildInformation(
5656
/// <exception cref="InvalidOperationException">Thrown if version cannot be determined.</exception>
5757
public static async Task<BuildInformation> CreateAsync(IRepoConnector connector, Version? version = null)
5858
{
59-
// Get tag history and current hash
59+
// Retrieve tag history and current commit hash from the repository
6060
var tags = await connector.GetTagHistoryAsync();
6161
var currentHash = await connector.GetHashForTagAsync(null);
6262

63-
// Determine the "To" version
63+
// Determine the target version and hash for build information
6464
Version toTagInfo;
6565
string toHash;
66-
6766
if (version != null)
6867
{
69-
// Use the provided version
68+
// Use explicitly specified version as target
7069
toTagInfo = version;
7170
toHash = currentHash;
7271
}
7372
else if (tags.Count > 0)
7473
{
75-
// Check if current commit matches the most recent tag
74+
// Verify current commit matches latest tag when no version specified
7675
var latestTag = tags[^1];
7776
var latestTagHash = await connector.GetHashForTagAsync(latestTag.Tag);
7877

7978
if (latestTagHash.Trim() == currentHash.Trim())
8079
{
80+
// Current commit matches latest tag, use it as target
8181
toTagInfo = latestTag;
8282
toHash = currentHash;
8383
}
8484
else
8585
{
86+
// Current commit doesn't match any tag, cannot determine version
8687
throw new InvalidOperationException(
8788
"Target version not specified and current commit does not match any tag. " +
8889
"Please provide a version parameter.");
8990
}
9091
}
9192
else
9293
{
94+
// No tags in repository and no version provided
9395
throw new InvalidOperationException(
9496
"No tags found in repository and no version specified. " +
9597
"Please provide a version parameter.");
9698
}
9799

98-
// Determine the "From" version
100+
// Determine the starting version for comparing changes
99101
Version? fromTagInfo = null;
100102
string? fromHash = null;
101-
102103
if (tags.Count > 0)
103104
{
105+
// Find the position of target version in tag history
104106
var toIndex = FindTagIndex(tags, toTagInfo.FullVersion);
105107

106108
if (toTagInfo.IsPreRelease)
107109
{
108-
// For pre-release: use the previous tag (any type)
110+
// Pre-release versions use the immediately previous tag as baseline
109111
if (toIndex > 0)
110112
{
111-
// The to version exists in tag history, use the previous tag
113+
// Target version exists in history, use previous tag
112114
fromTagInfo = tags[toIndex - 1];
113115
}
114116
else if (toIndex == -1)
115117
{
116-
// The to version doesn't exist in tag history, use the most recent tag
118+
// Target version not in history, use most recent tag as baseline
117119
fromTagInfo = tags[^1];
118120
}
119-
// If toIndex == 0, fromTagInfo stays null (first release)
121+
// If toIndex == 0, this is the first tag, no baseline
120122
}
121123
else
122124
{
123-
// For release: use the previous release tag (skip pre-releases)
125+
// Release versions skip pre-releases and use previous release as baseline
124126
int startIndex;
125127
if (toIndex > 0)
126128
{
127-
// The to version exists in tag history
129+
// Target version exists in history, start search from previous position
128130
startIndex = toIndex - 1;
129131
}
130132
else if (toIndex == -1)
131133
{
132-
// The to version doesn't exist in tag history, use the most recent tag
134+
// Target version not in history, start from most recent tag
133135
startIndex = tags.Count - 1;
134136
}
135137
else
136138
{
137-
// toIndex == 0, this is the first tag, no previous release
139+
// Target is first tag, no previous release exists
138140
startIndex = -1;
139141
}
140142

143+
// Search backward for previous non-pre-release version
141144
for (var i = startIndex; i >= 0; i--)
142145
{
143146
if (!tags[i].IsPreRelease)
@@ -148,37 +151,45 @@ public static async Task<BuildInformation> CreateAsync(IRepoConnector connector,
148151
}
149152
}
150153

154+
// Get commit hash for baseline version if one was found
151155
if (fromTagInfo != null)
152156
{
153157
fromHash = await connector.GetHashForTagAsync(fromTagInfo.Tag);
154158
}
155159
}
156160

157-
// Get pull requests and issues between versions
161+
// Collect all pull requests and their associated issues in version range
158162
var pullRequests = await connector.GetPullRequestsBetweenTagsAsync(fromTagInfo, toTagInfo);
159-
160163
var allIssues = new HashSet<string>();
161164
var bugIssues = new List<IssueInfo>();
162165
var changeIssues = new List<IssueInfo>();
163166

167+
// Process each pull request to extract and categorize issues
164168
foreach (var pr in pullRequests)
165169
{
170+
// Get all issues referenced by this pull request
166171
var issueIds = await connector.GetIssuesForPullRequestAsync(pr);
172+
167173
foreach (var issueId in issueIds)
168174
{
175+
// Skip issues already processed
169176
if (allIssues.Contains(issueId))
170177
{
171178
continue;
172179
}
173180

181+
// Mark issue as processed
174182
allIssues.Add(issueId);
175183

184+
// Fetch issue details
176185
var title = await connector.GetIssueTitleAsync(issueId);
177186
var url = await connector.GetIssueUrlAsync(issueId);
178187
var type = await connector.GetIssueTypeAsync(issueId);
179188

189+
// Create issue record
180190
var issueInfo = new IssueInfo(issueId, title, url);
181191

192+
// Categorize issue by type
182193
if (type == "bug")
183194
{
184195
bugIssues.Add(issueInfo);
@@ -190,18 +201,18 @@ public static async Task<BuildInformation> CreateAsync(IRepoConnector connector,
190201
}
191202
}
192203

193-
// Get known issues (open bugs that are not already fixed in this build)
204+
// Collect known issues (open bugs not fixed in this build)
194205
var knownIssues = new List<IssueInfo>();
195206
var openIssueIds = await connector.GetOpenIssuesAsync();
196-
197207
foreach (var issueId in openIssueIds)
198208
{
199-
// Skip if already included in fixed bugs
209+
// Skip issues already fixed in this build
200210
if (allIssues.Contains(issueId))
201211
{
202212
continue;
203213
}
204214

215+
// Only include bugs in known issues list
205216
var type = await connector.GetIssueTypeAsync(issueId);
206217
if (type == "bug")
207218
{
@@ -211,6 +222,7 @@ public static async Task<BuildInformation> CreateAsync(IRepoConnector connector,
211222
}
212223
}
213224

225+
// Create and return build information with all collected data
214226
return new BuildInformation(
215227
fromTagInfo,
216228
toTagInfo,
@@ -229,6 +241,7 @@ public static async Task<BuildInformation> CreateAsync(IRepoConnector connector,
229241
/// <returns>Index of the tag, or -1 if not found.</returns>
230242
private static int FindTagIndex(List<Version> tags, string normalizedVersion)
231243
{
244+
// Search for tag matching the normalized version
232245
for (var i = 0; i < tags.Count; i++)
233246
{
234247
if (tags[i].FullVersion.Equals(normalizedVersion, StringComparison.OrdinalIgnoreCase))
@@ -237,6 +250,7 @@ private static int FindTagIndex(List<Version> tags, string normalizedVersion)
237250
}
238251
}
239252

253+
// Tag not found in history
240254
return -1;
241255
}
242256
}

src/DemaConsulting.BuildMark/Program.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ public static string Version
3434
{
3535
get
3636
{
37+
// Get the assembly containing this program
3738
var assembly = typeof(Program).Assembly;
39+
40+
// Try to get version from assembly attributes, fallback to AssemblyVersion, or default to 0.0.0
3841
return assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
3942
?? assembly.GetName().Version?.ToString()
4043
?? "0.0.0";
@@ -48,14 +51,14 @@ public static string Version
4851
/// <returns>Exit code: 0 for success, non-zero for failure.</returns>
4952
private static int Main(string[] args)
5053
{
51-
// Print version if --version is specified
54+
// Handle version display request
5255
if (args.Length > 0 && args[0] == "--version")
5356
{
5457
Console.WriteLine($"BuildMark version {Version}");
5558
return 0;
5659
}
5760

58-
// Print help if --help is specified or no arguments
61+
// Handle help display request or missing arguments
5962
if (args.Length == 0 || args[0] == "--help")
6063
{
6164
Console.WriteLine("BuildMark - Tool to generate Markdown Build Notes");
@@ -68,6 +71,7 @@ private static int Main(string[] args)
6871
return 0;
6972
}
7073

74+
// Display placeholder message for unhandled arguments
7175
Console.WriteLine("Hello from BuildMark!");
7276
return 0;
7377
}

src/DemaConsulting.BuildMark/RepoConnectors/GitHubRepoConnector.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public partial class GitHubRepoConnector : RepoConnectorBase
4646
/// <exception cref="ArgumentException">Thrown if tag name is invalid.</exception>
4747
private static string ValidateTag(string tag)
4848
{
49+
// Ensure tag name matches allowed pattern to prevent injection attacks
4950
if (!TagNameRegex().IsMatch(tag))
5051
{
5152
throw new ArgumentException($"Invalid tag name: {tag}", nameof(tag));
@@ -63,6 +64,7 @@ private static string ValidateTag(string tag)
6364
/// <exception cref="ArgumentException">Thrown if ID is invalid.</exception>
6465
private static string ValidateId(string id, string paramName)
6566
{
67+
// Ensure ID is numeric to prevent injection attacks
6668
if (!NumericIdRegex().IsMatch(id))
6769
{
6870
throw new ArgumentException($"Invalid ID: {id}", paramName);
@@ -77,12 +79,18 @@ private static string ValidateId(string id, string paramName)
7779
/// <returns>List of tags in chronological order.</returns>
7880
public override async Task<List<Version>> GetTagHistoryAsync()
7981
{
82+
// Get all tags merged into current branch, sorted by creation date
83+
// Arguments: --sort=creatordate (chronological order), --merged HEAD (reachable from HEAD)
84+
// Output format: one tag name per line
8085
var output = await RunCommandAsync("git", "tag --sort=creatordate --merged HEAD");
86+
87+
// Split output into individual tag names
8188
var tagNames = output
8289
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
8390
.Select(t => t.Trim())
8491
.ToList();
85-
// Filter out non-version tags
92+
93+
// Parse and filter to valid version tags only
8694
return tagNames
8795
.Select(Version.TryCreate)
8896
.Where(t => t != null)
@@ -98,30 +106,40 @@ public override async Task<List<Version>> GetTagHistoryAsync()
98106
/// <returns>List of pull request IDs.</returns>
99107
public override async Task<List<string>> GetPullRequestsBetweenTagsAsync(Version? from, Version? to)
100108
{
109+
// Build git log range based on provided versions
101110
string range;
102111
if (from == null && to == null)
103112
{
113+
// No versions specified, use all of HEAD
104114
range = "HEAD";
105115
}
106116
else if (from == null && to != null)
107117
{
118+
// Only end version specified
108119
range = ValidateTag(to.Tag);
109120
}
110121
else if (to == null && from != null)
111122
{
123+
// Only start version specified, range to HEAD
112124
range = $"{ValidateTag(from.Tag)}..HEAD";
113125
}
114126
else
115127
{
116-
// Both from and to are not null (verified by the preceding conditions)
128+
// Both versions specified
117129
if (from == null || to == null)
118130
{
119131
throw new InvalidOperationException("Unexpected null version");
120132
}
121133
range = $"{ValidateTag(from.Tag)}..{ValidateTag(to.Tag)}";
122134
}
123135

136+
// Get merge commits in range using git log
137+
// Arguments: --oneline (one line per commit), --merges (only merge commits)
138+
// Output format: "<short-hash> Merge pull request #<number> from <branch>"
124139
var output = await RunCommandAsync("git", $"log --oneline --merges {range}");
140+
141+
// Extract pull request numbers from merge commit messages
142+
// Each line is parsed for "#<number>" pattern to identify the PR
125143
var pullRequests = new List<string>();
126144
var regex = NumberReferenceRegex();
127145

@@ -144,8 +162,13 @@ public override async Task<List<string>> GetPullRequestsBetweenTagsAsync(Version
144162
/// <returns>List of issue IDs.</returns>
145163
public override async Task<List<string>> GetIssuesForPullRequestAsync(string pullRequestId)
146164
{
165+
// Validate and fetch PR body using GitHub CLI
166+
// Arguments: --json body (get body field), --jq .body (extract body value)
167+
// Output: raw PR description text which may contain issue references
147168
var validatedId = ValidateId(pullRequestId, nameof(pullRequestId));
148169
var output = await RunCommandAsync("gh", $"pr view {validatedId} --json body --jq .body");
170+
171+
// Extract issue references (e.g., #123, #456) from PR body text
149172
var issues = new List<string>();
150173
var regex = NumberReferenceRegex();
151174

@@ -164,6 +187,9 @@ public override async Task<List<string>> GetIssuesForPullRequestAsync(string pul
164187
/// <returns>Issue title.</returns>
165188
public override async Task<string> GetIssueTitleAsync(string issueId)
166189
{
190+
// Validate and fetch issue title using GitHub CLI
191+
// Arguments: --json title (get title field), --jq .title (extract title value)
192+
// Output: issue title as plain text
167193
var validatedId = ValidateId(issueId, nameof(issueId));
168194
return await RunCommandAsync("gh", $"issue view {validatedId} --json title --jq .title");
169195
}
@@ -175,11 +201,14 @@ public override async Task<string> GetIssueTitleAsync(string issueId)
175201
/// <returns>Issue type.</returns>
176202
public override async Task<string> GetIssueTypeAsync(string issueId)
177203
{
204+
// Validate and fetch issue labels using GitHub CLI
205+
// Arguments: --json labels (get labels array), --jq '.labels[].name' (extract label names)
206+
// Output: one label name per line
178207
var validatedId = ValidateId(issueId, nameof(issueId));
179208
var output = await RunCommandAsync("gh", $"issue view {validatedId} --json labels --jq '.labels[].name'");
180209
var labels = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
181210

182-
// Look for common type labels
211+
// Map labels to standardized issue types
183212
foreach (var label in labels)
184213
{
185214
var lowerLabel = label.ToLowerInvariant();
@@ -192,6 +221,7 @@ public override async Task<string> GetIssueTypeAsync(string issueId)
192221
}
193222
}
194223

224+
// Default type when no recognized label found
195225
return "other";
196226
}
197227

@@ -202,6 +232,9 @@ public override async Task<string> GetIssueTypeAsync(string issueId)
202232
/// <returns>Git hash.</returns>
203233
public override async Task<string> GetHashForTagAsync(string? tag)
204234
{
235+
// Get commit hash for tag or HEAD using git rev-parse
236+
// Arguments: tag name or "HEAD" for current commit
237+
// Output: full 40-character commit SHA
205238
var refName = tag == null ? "HEAD" : ValidateTag(tag);
206239
return await RunCommandAsync("git", $"rev-parse {refName}");
207240
}
@@ -213,6 +246,9 @@ public override async Task<string> GetHashForTagAsync(string? tag)
213246
/// <returns>Issue URL.</returns>
214247
public override async Task<string> GetIssueUrlAsync(string issueId)
215248
{
249+
// Validate and fetch issue URL using GitHub CLI
250+
// Arguments: --json url (get url field), --jq .url (extract url value)
251+
// Output: full HTTPS URL to the issue
216252
var validatedId = ValidateId(issueId, nameof(issueId));
217253
return await RunCommandAsync("gh", $"issue view {validatedId} --json url --jq .url");
218254
}
@@ -223,7 +259,12 @@ public override async Task<string> GetIssueUrlAsync(string issueId)
223259
/// <returns>List of open issue IDs.</returns>
224260
public override async Task<List<string>> GetOpenIssuesAsync()
225261
{
262+
// Fetch all open issue numbers using GitHub CLI
263+
// Arguments: --state open (open issues only), --json number (get number field), --jq '.[].number' (extract numbers from array)
264+
// Output: one issue number per line
226265
var output = await RunCommandAsync("gh", "issue list --state open --json number --jq '.[].number'");
266+
267+
// Parse output into list of issue IDs
227268
return output
228269
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
229270
.Select(n => n.Trim())

0 commit comments

Comments
 (0)