• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

samsmithnz / RepoGovernance / 16895891586

12 Aug 2025 12:47AM UTC coverage: 44.71% (-26.7%) from 71.373%
16895891586

Pull #1001

github

web-flow
Merge 666b2ba0a into a12c1cae9
Pull Request #1001: Add repository details page with recommendation ignore functionality

401 of 1078 branches covered (37.2%)

Branch coverage included in aggregate %.

75 of 272 new or added lines in 6 files covered. (27.57%)

4 existing lines in 1 file now uncovered.

1023 of 2107 relevant lines covered (48.55%)

37.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.21
/src/RepoGovernance.Web/Controllers/HomeController.cs
1
using Microsoft.AspNetCore.Mvc;
2
using RepoAutomation.Core.Models;
3
using RepoGovernance.Core.Models;
4
using RepoGovernance.Web.Models;
5
using RepoGovernance.Web.Services;
6
using System.Diagnostics;
7
using System.Text.RegularExpressions;
8

9
namespace RepoGovernance.Web.Controllers;
10

11
public class HomeController : Controller
12
{
13
    private readonly ISummaryItemsServiceApiClient _ServiceApiClient;
14

15
    public HomeController(ISummaryItemsServiceApiClient ServiceApiClient)
7✔
16
    {
7✔
17
        _ServiceApiClient = ServiceApiClient;
7✔
18
    }
7✔
19

20
    /// <summary>
21
    /// Validates that a repository name is safe for use in URL fragments.
22
    /// GitHub repository names can only contain alphanumeric characters, hyphens, underscores, and periods.
23
    /// </summary>
24
    /// <param name="repoName">The repository name to validate</param>
25
    /// <returns>True if the repository name is safe, false otherwise</returns>
26
    private static bool IsValidRepoName(string? repoName)
27
    {
×
28
        if (string.IsNullOrEmpty(repoName))
×
29
            return false;
×
30
        
31
        // GitHub repository names can only contain alphanumeric characters, hyphens, underscores, and periods
32
        // and cannot start or end with special characters
33
        return Regex.IsMatch(repoName, @"^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$");
×
34
    }
×
35

36
    public async Task<IActionResult> Index(bool isContributor = false)
37
    {
×
38
        if (!ModelState.IsValid)
×
39
        {
×
40
            return RedirectToAction("Index");
×
41
        }
42

43
        string currentUser = "samsmithnz";
×
44
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
×
45
        List<RepoLanguage> repoLanguages = new();
×
46
        Dictionary<string, int> repoLanguagesDictonary = new();
×
47
        int total = 0;
×
48
        foreach (SummaryItem summaryItem in summaryItems)
×
49
        {
×
50
            foreach (RepoLanguage repoLanguage in summaryItem.RepoLanguages)
×
51
            {
×
52
                total += repoLanguage.Total;
×
53
                if (repoLanguage.Name != null)
×
54
                {
×
55
                    if (repoLanguagesDictonary.ContainsKey(repoLanguage.Name))
×
56
                    {
×
57
                        repoLanguagesDictonary[repoLanguage.Name] += repoLanguage.Total;
×
58
                    }
×
59
                    else
60
                    {
×
61
                        repoLanguagesDictonary.Add(repoLanguage.Name, repoLanguage.Total);
×
62
                    }
×
63
                    if (repoLanguages.Find(x => x.Name == repoLanguage.Name) == null)
×
64
                    {
×
65
                        repoLanguages.Add(new RepoLanguage
×
66
                        {
×
67
                            Name = repoLanguage.Name,
×
68
                            Total = repoLanguage.Total,
×
69
                            Color = repoLanguage.Color,
×
70
                            Percent = repoLanguage.Percent
×
71
                        });
×
72
                    }
×
73
                }
×
74
            }
×
75
        }
×
76
        //Update the percent
77
        foreach (KeyValuePair<string, int> sortedLanguage in repoLanguagesDictonary.OrderByDescending(x => x.Value))
×
78
        {
×
79
            RepoLanguage? repoLanguage = repoLanguages.Find(x => x.Name == sortedLanguage.Key);
×
80
            if (repoLanguage != null)
×
81
            {
×
82
                repoLanguage.Total = sortedLanguage.Value;
×
83
                repoLanguage.Percent = Math.Round((decimal)repoLanguage.Total / (decimal)total * 100M, 1);
×
84
            }
×
85
        }
×
86

87
        SummaryItemsIndex summaryItemsIndex = new()
×
88
        {
×
89
            SummaryItems = summaryItems,
×
90
            SummaryRepoLanguages = repoLanguages.OrderByDescending(x => x.Total).ToList(),
×
91
            IsContributor = isContributor
×
92
        };
×
93
        return View(summaryItemsIndex);
×
94
    }
×
95

96
    public async Task<IActionResult> Details(string user, string owner, string repo, bool isContributor = false)
97
    {
×
98
        if (!ModelState.IsValid)
×
99
        {
×
100
            return RedirectToAction("Index");
×
101
        }
102

103
        SummaryItem result = null;
×
104
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(user);
×
105
        foreach (SummaryItem summaryItem in summaryItems)
×
106
        {
×
107
            if (summaryItem.Owner == owner && summaryItem.Repo == repo)
×
108
            {
×
109
                result = summaryItem;
×
110
                break;
×
111
            }
112
        }
×
113
        return View(result);
×
114
    }
×
115

116

117
    public async Task<IActionResult> UpdateRow(string user, string owner, string repo, bool isContributor = false)
118
    {
×
119
        if (!ModelState.IsValid)
×
120
        {
×
121
            return RedirectToAction("Index");
×
122
        }
123

124
        await _ServiceApiClient.UpdateSummaryItem(user, owner, repo);
×
125

126
        // Safely pass repo name as query parameter for client-side scrolling
127
        // Validate repository name to prevent injection attacks
128
        object routeValues;
129
        if (IsValidRepoName(repo))
×
130
        {
×
131
            if (isContributor)
×
132
            {
×
133
                routeValues = new { isContributor = true, scrollTo = repo };
×
134
            }
×
135
            else
136
            {
×
137
                routeValues = new { scrollTo = repo };
×
138
            }
×
139
        }
×
140
        else
141
        {
×
142
            if (isContributor)
×
143
            {
×
144
                routeValues = new { isContributor = true };
×
145
            }
×
146
            else
147
            {
×
148
                routeValues = new { };
×
149
            }
×
150
        }
×
151

152
        return RedirectToAction("Index", routeValues);
×
153
    }
×
154

155
    public async Task<IActionResult> UpdateAll(bool isContributor = false)
156
    {
×
157
        if (!ModelState.IsValid)
×
158
        {
×
159
            return RedirectToAction("Index");
×
160
        }
161

162
        string currentUser = "samsmithnz";
×
163
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
×
164
        foreach (SummaryItem summaryItem in summaryItems)
×
165
        {
×
166
            await _ServiceApiClient.UpdateSummaryItem(summaryItem.User, summaryItem.Owner, summaryItem.Repo);
×
167
        }
×
168

169
        //This is a hack for now - hide controls behind this iscontributor flag, but never show iscontributor=false in query string
170
        if (isContributor)
×
171
        {
×
172
            return RedirectToAction("Index", new { isContributor = true });
×
173
        }
174
        else
175
        {
×
176
            return RedirectToAction("Index");
×
177
        }
178
    }
×
179

180
    public async Task<IActionResult> ApprovePRsForAllRepos(bool isContributor = false)
181
    {
×
182
        if (!ModelState.IsValid)
×
183
        {
×
184
            return RedirectToAction("Index");
×
185
        }
186

187
        string currentUser = "samsmithnz";
×
188
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
×
189
        foreach (SummaryItem summaryItem in summaryItems)
×
190
        {
×
191
            await _ServiceApiClient.ApproveSummaryItemPRs(summaryItem.Owner, summaryItem.Repo, currentUser);
×
192
        }
×
193

194
        //This is a hack for now - hide controls behind this iscontributor flag, but never show iscontributor=false in query string
195
        if (isContributor)
×
196
        {
×
197
            return RedirectToAction("Index", new { isContributor = true });
×
198
        }
199
        else
200
        {
×
201
            return RedirectToAction("Index");
×
202
        }
203
    }
×
204

205
    public async Task<IActionResult> Config(string user, string owner, string repo, bool isContributor = false)
206
    {
×
207
        if (!ModelState.IsValid)
×
208
        {
×
209
            return RedirectToAction("Index");
×
210
        }
211

212
        SummaryItem? summaryItem = await _ServiceApiClient.GetSummaryItem(user, owner, repo);
×
213

214
        SummaryItemConfig summaryItemConfig = new()
×
215
        {
×
216
            SummaryItem = summaryItem,
×
217
            IsContributor = isContributor
×
218
        };
×
219

220
        return View(summaryItemConfig);
×
221
    }
×
222

223
    public async Task<IActionResult> TaskList(bool isContributor = false)
224
    {
×
225
        if (!ModelState.IsValid)
×
226
        {
×
227
            return RedirectToAction("Index");
×
228
        }
229

230
        string currentUser = "samsmithnz";
×
231
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
×
232
        
233
        List<TaskItem> tasks = new List<TaskItem>();
×
234
        
235
        foreach (SummaryItem item in summaryItems)
×
236
        {
×
237
            // Add repository settings recommendations
238
            foreach (string recommendation in item.RepoSettingsRecommendations)
×
239
            {
×
240
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Repository Settings", recommendation));
×
241
            }
×
242
            
243
            // Add branch policies recommendations  
244
            foreach (string recommendation in item.BranchPoliciesRecommendations)
×
245
            {
×
246
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Branch Policies", recommendation));
×
247
            }
×
248
            
249
            // Add action recommendations
250
            foreach (string recommendation in item.ActionRecommendations)
×
251
            {
×
252
                tasks.Add(new TaskItem(item.Owner, item.Repo, "GitHub Actions", recommendation));
×
253
            }
×
254
            
255
            // Add dependabot recommendations
256
            foreach (string recommendation in item.DependabotRecommendations)
×
257
            {
×
258
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Dependabot", recommendation));
×
259
            }
×
260
            
261
            // Add git version recommendations
262
            foreach (string recommendation in item.GitVersionRecommendations)
×
263
            {
×
264
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Git Version", recommendation));
×
265
            }
×
266
            
267
            // Add .NET framework recommendations
268
            foreach (string recommendation in item.DotNetFrameworksRecommendations)
×
269
            {
×
270
                tasks.Add(new TaskItem(item.Owner, item.Repo, ".NET Frameworks", recommendation));
×
271
            }
×
272
            
273
            // Add NuGet package upgrades
274
            if (item.NuGetPackages != null && item.NuGetPackages.Count > 0)
×
275
            {
×
276
                tasks.Add(new TaskItem(item.Owner, item.Repo, "NuGet Packages", 
×
277
                    $"{item.NuGetPackages.Count} NuGet packages require upgrades"));
×
278
            }
×
279
            
280
            // Add security issues
281
            if (item.SecurityIssuesCount > 0)
×
282
            {
×
283
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Security", 
×
284
                    $"{item.SecurityIssuesCount} Security alerts detected"));
×
285
            }
×
286
        }
×
287

288
        // Filter out ignored recommendations
NEW
289
        List<IgnoredRecommendation> ignoredRecommendations = await _ServiceApiClient.GetAllIgnoredRecommendations(currentUser);
×
NEW
290
        List<string> ignoredIds = ignoredRecommendations.Select(ir => ir.GetUniqueId()).ToList();
×
291
        
NEW
292
        List<TaskItem> filteredTasks = tasks.Where(t => !ignoredIds.Contains(t.Id)).ToList();
×
293

294
        TaskList taskList = new TaskList()
×
295
        {
×
NEW
296
            Tasks = filteredTasks.OrderBy(t => t.Owner).ThenBy(t => t.Repository).ThenBy(t => t.RecommendationType).ToList(),
×
297
            IsContributor = isContributor
×
298
        };
×
299

300
        return View(taskList);
×
301
    }
×
302

303
    public async Task<IActionResult> RepoDetails(string owner, string repo, bool isContributor = false)
304
    {
3✔
305
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repo))
3✔
306
        {
2✔
307
            return RedirectToAction("TaskList");
2✔
308
        }
309

310
        if (!ModelState.IsValid)
1!
NEW
311
        {
×
NEW
312
            return RedirectToAction("TaskList");
×
313
        }
314

315
        string currentUser = "samsmithnz";
1✔
316
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
1✔
317
        
318
        // Find the specific repository
319
        SummaryItem? repoItem = summaryItems.FirstOrDefault(s => s.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase) 
2!
320
                                                                && s.Repo.Equals(repo, StringComparison.OrdinalIgnoreCase));
2✔
321
        
322
        if (repoItem == null)
1!
NEW
323
        {
×
NEW
324
            return RedirectToAction("TaskList");
×
325
        }
326

327
        List<TaskItem> allRecommendations = new List<TaskItem>();
1✔
328
        
329
        // Generate all recommendations for this repository (same logic as TaskList)
330
        foreach (string recommendation in repoItem.RepoSettingsRecommendations)
5✔
331
        {
1✔
332
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Repository Settings", recommendation));
1✔
333
        }
1✔
334
        
335
        foreach (string recommendation in repoItem.BranchPoliciesRecommendations)
5✔
336
        {
1✔
337
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Branch Policies", recommendation));
1✔
338
        }
1✔
339
        
340
        foreach (string recommendation in repoItem.ActionRecommendations)
5✔
341
        {
1✔
342
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "GitHub Actions", recommendation));
1✔
343
        }
1✔
344
        
345
        foreach (string recommendation in repoItem.DependabotRecommendations)
5✔
346
        {
1✔
347
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Dependabot", recommendation));
1✔
348
        }
1✔
349
        
350
        foreach (string recommendation in repoItem.GitVersionRecommendations)
5✔
351
        {
1✔
352
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Git Version", recommendation));
1✔
353
        }
1✔
354
        
355
        foreach (string recommendation in repoItem.DotNetFrameworksRecommendations)
5✔
356
        {
1✔
357
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, ".NET Frameworks", recommendation));
1✔
358
        }
1✔
359
        
360
        if (repoItem.NuGetPackages != null && repoItem.NuGetPackages.Count > 0)
1!
361
        {
1✔
362
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "NuGet Packages", 
1✔
363
                $"{repoItem.NuGetPackages.Count} NuGet packages require upgrades"));
1✔
364
        }
1✔
365
        
366
        if (repoItem.SecurityIssuesCount > 0)
1✔
367
        {
1✔
368
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Security", 
1✔
369
                $"{repoItem.SecurityIssuesCount} Security alerts detected"));
1✔
370
        }
1✔
371

372
        // Get ignored recommendations
373
        List<IgnoredRecommendation> ignoredRecommendations = await _ServiceApiClient.GetIgnoredRecommendations(currentUser, owner, repo);
1✔
374
        List<string> ignoredIds = ignoredRecommendations.Select(ir => ir.GetUniqueId()).ToList();
1✔
375
        
376
        // Separate active and ignored recommendations
377
        List<TaskItem> activeRecommendations = allRecommendations.Where(r => !ignoredIds.Contains(r.Id)).ToList();
9✔
378
        List<TaskItem> ignoredTaskItems = allRecommendations.Where(r => ignoredIds.Contains(r.Id)).ToList();
9✔
379

380
        RepoDetails repoDetails = new RepoDetails(owner, repo)
1✔
381
        {
1✔
382
            Recommendations = activeRecommendations.OrderBy(r => r.RecommendationType).ToList(),
8✔
NEW
383
            IgnoredRecommendations = ignoredTaskItems.OrderBy(r => r.RecommendationType).ToList(),
×
384
            IsContributor = isContributor
1✔
385
        };
1✔
386

387
        return View(repoDetails);
1✔
388
    }
3✔
389

390
    [HttpPost]
391
    public async Task<IActionResult> IgnoreRecommendation(string owner, string repository, string recommendationType, string recommendationDetails)
392
    {
2✔
393
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repository) || 
2✔
394
            string.IsNullOrEmpty(recommendationType) || string.IsNullOrEmpty(recommendationDetails))
2✔
395
        {
1✔
396
            return Json(new { success = false, message = "Missing required parameters" });
1✔
397
        }
398

399
        string currentUser = "samsmithnz";
1✔
400
        bool success = await _ServiceApiClient.IgnoreRecommendation(currentUser, owner, repository, recommendationType, recommendationDetails);
1✔
401
        
402
        return Json(new { success = success });
1✔
403
    }
2✔
404

405
    [HttpPost]
406
    public async Task<IActionResult> UnignoreRecommendation(string owner, string repository, string recommendationType, string recommendationDetails)
407
    {
2✔
408
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repository) || 
2✔
409
            string.IsNullOrEmpty(recommendationType) || string.IsNullOrEmpty(recommendationDetails))
2✔
410
        {
1✔
411
            return Json(new { success = false, message = "Missing required parameters" });
1✔
412
        }
413

414
        string currentUser = "samsmithnz";
1✔
415
        bool success = await _ServiceApiClient.RestoreRecommendation(currentUser, owner, repository, recommendationType, recommendationDetails);
1✔
416
        
417
        return Json(new { success = success });
1✔
418
    }
2✔
419

420
    public IActionResult Privacy()
421
    {
×
422
        return View();
×
423
    }
×
424

425
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
426
    public IActionResult Error()
427
    {
×
428
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
×
429
    }
×
430
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc