1313import java .net .http .HttpRequest ;
1414import java .net .http .HttpResponse ;
1515import java .nio .charset .StandardCharsets ;
16+ import java .security .MessageDigest ;
17+ import java .security .NoSuchAlgorithmException ;
1618import java .time .Duration ;
1719import java .time .Instant ;
1820import java .util .*;
@@ -947,4 +949,117 @@ static String fetchUpstreamSource(String className) {
947949 FETCH_CACHE .put (className , source );
948950 return source ;
949951 }
952+
953+ /// Generates a SHA256 fingerprint of the differences (first 7 chars)
954+ /// Used for deduplicating GitHub issues
955+ /// @param report the full comparison report
956+ /// @return 7-character fingerprint or "0000000" if no differences
957+ static String generateFingerprint (JsonObject report ) {
958+ final var summary = (JsonObject ) report .members ().get ("summary" );
959+ final var differentApi = ((JsonNumber ) summary .members ().get ("differentApi" )).toNumber ().longValue ();
960+
961+ if (differentApi == 0 ) {
962+ return "0000000" ;
963+ }
964+
965+ // Extract just the differences array for fingerprinting
966+ final var differences = (JsonArray ) report .members ().get ("differences" );
967+ final var diffsOnly = differences .values ().stream ()
968+ .filter (v -> {
969+ final var obj = (JsonObject ) v ;
970+ final var status = ((JsonString ) obj .members ().get ("status" )).value ();
971+ return "DIFFERENT" .equals (status );
972+ })
973+ .toList ();
974+
975+ // Serialize to stable JSON string for hashing
976+ final var jsonString = JsonArray .of (diffsOnly ).toString ();
977+
978+ try {
979+ final var digest = MessageDigest .getInstance ("SHA-256" );
980+ final var hash = digest .digest (jsonString .getBytes (StandardCharsets .UTF_8 ));
981+ final var hexString = new StringBuilder ();
982+ for (final var b : hash ) {
983+ hexString .append (String .format ("%02x" , b ));
984+ }
985+ return hexString .substring (0 , 7 );
986+ } catch (NoSuchAlgorithmException e ) {
987+ LOGGER .warning ("SHA-256 not available, using fallback fingerprint" );
988+ return String .format ("%07d" , jsonString .hashCode () & 0xFFFFFFF );
989+ }
990+ }
991+
992+ /// Generates a terse human-readable summary of the API differences
993+ /// Suitable for GitHub issue body
994+ /// @param report the full comparison report
995+ /// @return markdown-formatted summary
996+ static String generateSummary (JsonObject report ) {
997+ final var sb = new StringBuilder ();
998+ final var summary = (JsonObject ) report .members ().get ("summary" );
999+ final var differences = (JsonArray ) report .members ().get ("differences" );
1000+
1001+ final var totalClasses = ((JsonNumber ) summary .members ().get ("totalClasses" )).toNumber ().longValue ();
1002+ final var matchingClasses = ((JsonNumber ) summary .members ().get ("matchingClasses" )).toNumber ().longValue ();
1003+ final var differentApi = ((JsonNumber ) summary .members ().get ("differentApi" )).toNumber ().longValue ();
1004+ final var missingUpstream = ((JsonNumber ) summary .members ().get ("missingUpstream" )).toNumber ().longValue ();
1005+
1006+ sb .append ("## API Comparison Summary\n \n " );
1007+ sb .append ("| Metric | Count |\n " );
1008+ sb .append ("|--------|-------|\n " );
1009+ sb .append ("| Total Classes | " ).append (totalClasses ).append (" |\n " );
1010+ sb .append ("| Matching | " ).append (matchingClasses ).append (" |\n " );
1011+ sb .append ("| Different | " ).append (differentApi ).append (" |\n " );
1012+ sb .append ("| Missing Upstream | " ).append (missingUpstream ).append (" |\n \n " );
1013+
1014+ if (differentApi > 0 ) {
1015+ sb .append ("## Changes Detected\n \n " );
1016+
1017+ for (final var diff : differences .values ()) {
1018+ final var diffObj = (JsonObject ) diff ;
1019+ final var status = ((JsonString ) diffObj .members ().get ("status" )).value ();
1020+
1021+ if (!"DIFFERENT" .equals (status )) continue ;
1022+
1023+ final var className = ((JsonString ) diffObj .members ().get ("className" )).value ();
1024+ sb .append ("### " ).append (className ).append ("\n \n " );
1025+
1026+ final var classDiffs = (JsonArray ) diffObj .members ().get ("differences" );
1027+ if (classDiffs != null ) {
1028+ for (final var change : classDiffs .values ()) {
1029+ final var changeObj = (JsonObject ) change ;
1030+ final var type = ((JsonString ) changeObj .members ().get ("type" )).value ();
1031+ final var methodValue = changeObj .members ().get ("method" );
1032+ final var method = methodValue instanceof JsonString js ? js .value () : "unknown" ;
1033+
1034+ final var emoji = switch (type ) {
1035+ case "methodRemoved" -> "➖" ;
1036+ case "methodAdded" -> "➕" ;
1037+ case "methodChanged" -> "🔄" ;
1038+ case "inheritanceChanged" -> "🔗" ;
1039+ case "fieldsChanged" -> "📦" ;
1040+ case "constructorsChanged" -> "🏗️" ;
1041+ default -> "❓" ;
1042+ };
1043+
1044+ sb .append ("- " ).append (emoji ).append (" **" ).append (type ).append ("**: `" ).append (method ).append ("`\n " );
1045+ }
1046+ }
1047+ sb .append ("\n " );
1048+ }
1049+ }
1050+
1051+ sb .append ("---\n " );
1052+ sb .append ("*Generated by API Tracker on " ).append (Instant .now ().toString ().split ("T" )[0 ]).append ("*\n " );
1053+
1054+ return sb .toString ();
1055+ }
1056+
1057+ /// Checks if there are any API differences in the report
1058+ /// @param report the comparison report
1059+ /// @return true if differentApi > 0
1060+ static boolean hasDifferences (JsonObject report ) {
1061+ final var summary = (JsonObject ) report .members ().get ("summary" );
1062+ final var differentApi = ((JsonNumber ) summary .members ().get ("differentApi" )).toNumber ().longValue ();
1063+ return differentApi > 0 ;
1064+ }
9501065}
0 commit comments