1+ using System . Threading . Tasks ;
2+ using Interfaces ;
3+ using Octokit ;
4+ using Storage . Remote . GitHub ;
5+ using System . Text . RegularExpressions ;
6+ using System ;
7+ using System . Linq ;
8+ using System . Security . Cryptography ;
9+
10+ namespace Platform . Bot . Triggers
11+ {
12+ using TContext = Issue ;
13+ /// <summary>
14+ /// <para>
15+ /// Represents the Codacy badge update trigger.
16+ /// </para>
17+ /// <para></para>
18+ /// </summary>
19+ /// <seealso cref="ITrigger{TContext}"/>
20+ internal class CodacyBadgeUpdateTrigger : ITrigger < TContext >
21+ {
22+ private readonly GitHubStorage _storage ;
23+
24+ /// <summary>
25+ /// <para>
26+ /// Initializes a new <see cref="CodacyBadgeUpdateTrigger"/> instance.
27+ /// </para>
28+ /// <para></para>
29+ /// </summary>
30+ /// <param name="storage">
31+ /// <para>A git hub storage.</para>
32+ /// <para></para>
33+ /// </param>
34+ public CodacyBadgeUpdateTrigger ( GitHubStorage storage )
35+ {
36+ this . _storage = storage ;
37+ }
38+
39+ /// <summary>
40+ /// <para>
41+ /// Actions the context.
42+ /// </para>
43+ /// <para></para>
44+ /// </summary>
45+ /// <param name="context">
46+ /// <para>The context.</para>
47+ /// <para></para>
48+ /// </param>
49+ public async Task Action ( TContext context )
50+ {
51+ var repository = context . Repository ;
52+ var readmeContent = await GetReadmeContent ( repository ) ;
53+
54+ if ( readmeContent == null ) return ;
55+
56+ var updatedContent = UpdateCodacyBadges ( readmeContent , repository . Owner . Login , repository . Name ) ;
57+
58+ if ( updatedContent != readmeContent )
59+ {
60+ await _storage . CreateOrUpdateFile ( updatedContent , repository , repository . DefaultBranch , "README.md" , "Codacy badge is updated." ) ;
61+ }
62+
63+ _storage . CloseIssue ( context ) ;
64+ }
65+
66+ /// <summary>
67+ /// <para>
68+ /// Determines whether this instance condition.
69+ /// </para>
70+ /// <para></para>
71+ /// </summary>
72+ /// <param name="context">
73+ /// <para>The context.</para>
74+ /// <para></para>
75+ /// </param>
76+ /// <returns>
77+ /// <para>The bool</para>
78+ /// <para></para>
79+ /// </returns>
80+ public async Task < bool > Condition ( TContext context ) =>
81+ context . Title . ToLower ( ) . Contains ( "codacy" ) &&
82+ context . Title . ToLower ( ) . Contains ( "badge" ) &&
83+ context . Title . ToLower ( ) . Contains ( "update" ) ;
84+
85+ private async Task < string ? > GetReadmeContent ( Repository repository )
86+ {
87+ try
88+ {
89+ var readmeFiles = await _storage . Client . Repository . Content . GetAllContents ( repository . Id , "README.md" ) ;
90+ return readmeFiles . FirstOrDefault ( ) ? . Content ;
91+ }
92+ catch
93+ {
94+ return null ;
95+ }
96+ }
97+
98+ private string UpdateCodacyBadges ( string content , string owner , string repositoryName )
99+ {
100+ // Pattern to match Codacy badges - matches both old and new formats
101+ var codacyBadgePattern = @"\[\!\[Codacy Badge\]\(https://api\.codacy\.com/project/badge/Grade/[a-f0-9]+\)\]\(https://[^)]+\)" ;
102+
103+ // Find all Codacy badges
104+ var matches = Regex . Matches ( content , codacyBadgePattern ) ;
105+
106+ if ( matches . Count == 0 ) return content ;
107+
108+ // Extract the project ID from the first badge found
109+ var firstMatch = matches [ 0 ] . Value ;
110+ var projectIdMatch = Regex . Match ( firstMatch , @"Grade/([a-f0-9]+)" ) ;
111+ string projectId = projectIdMatch . Success ? projectIdMatch . Groups [ 1 ] . Value : GenerateProjectId ( owner , repositoryName ) ;
112+
113+ // Remove all existing Codacy badges
114+ var cleanedContent = Regex . Replace ( content , codacyBadgePattern , "" ) ;
115+
116+ // Remove extra empty lines that might have been left behind
117+ cleanedContent = Regex . Replace ( cleanedContent , @"\n\s*\n\s*\n" , "\n \n " ) ;
118+
119+ // Create the new standardized Codacy badge with modern URL format
120+ var newBadge = $ "[](https://app.codacy.com/gh/{ owner } /{ repositoryName } ?utm_source=github.com&utm_medium=referral&utm_content={ owner } /{ repositoryName } &utm_campaign=Badge_Grade_Settings)";
121+
122+ // Insert the badge at the beginning of the file (before any other content)
123+ return newBadge + "\n " + cleanedContent . TrimStart ( '\n ' ) ;
124+ }
125+
126+ private string GenerateProjectId ( string owner , string repositoryName )
127+ {
128+ // Generate a consistent 32-character hex string based on repository info
129+ var input = $ "{ owner . ToLower ( ) } /{ repositoryName . ToLower ( ) } ";
130+ var hash = System . Security . Cryptography . MD5 . Create ( ) . ComputeHash ( System . Text . Encoding . UTF8 . GetBytes ( input ) ) ;
131+ return BitConverter . ToString ( hash ) . Replace ( "-" , "" ) . ToLower ( ) ;
132+ }
133+ }
134+ }
0 commit comments