diff --git a/config/mariadb/init.sql b/config/mariadb/init.sql index 31ad11e303f929088fa61bf49731b554e15cf18a..f6e9c9e39fa9623c11ac325d0ebbb91225430950 100644 --- a/config/mariadb/init.sql +++ b/config/mariadb/init.sql @@ -3,6 +3,7 @@ CREATE TABLE `CommitValidationMessage` ( `commit_id` bigint(20) NOT NULL, `providerId` varchar(100) DEFAULT NULL, `authorEmail` varchar(100) DEFAULT NULL, + `committerEmail` varchar(255) DEFAULT NULL, `eclipseId` varchar(255) DEFAULT NULL, `statusCode` int(11) NOT NULL, PRIMARY KEY (`id`) @@ -23,4 +24,6 @@ CREATE TABLE `CommitValidationStatusGrouping` ( `fingerprint` varchar(255) NOT NULL, `commit_id` bigint(20) NOT NULL, PRIMARY KEY (`commit_id`,`fingerprint`) -); \ No newline at end of file +); + +ALTER TABLE CommitValidationMessage ADD COLUMN IF NOT EXISTS `committerEmail` varchar(255) DEFAULT NULL; diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java b/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java new file mode 100644 index 0000000000000000000000000000000000000000..80c280368fcad132066359f16dd95ac1bf96488d --- /dev/null +++ b/src/main/java/org/eclipsefoundation/git/eca/config/EclipseQuteTemplateExtensions.java @@ -0,0 +1,87 @@ +package org.eclipsefoundation.git.eca.config; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipsefoundation.git.eca.dto.CommitValidationMessage; +import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; + +import io.quarkus.qute.TemplateExtension; + +@TemplateExtension +public class EclipseQuteTemplateExtensions { + public static final Pattern EMAIL_OBFUSCATION_PATTERN = Pattern.compile("^(.).*?(@.*)$"); + public static final String EMAIL_OBFUSCATION_REPLACEMENT = "$1*****$2"; + + /** + * Made to count nested errors in a list of commit status objects. + * + * @param statuses the statuses to expand and count errors for + * @return the number of errors in the list of statuses + */ + static int getErrorCount(List<CommitValidationStatus> statuses) { + return statuses.stream().mapToInt(s -> s.getErrors().size()).sum(); + } + + /** + * Formats and flattens a list of statuses to a list of errors encountered while validation a set of commits. + * + * @param statuses a list of commit validation statuses to retrieve errors from + * @return a list of flattened errors, or an empty list if (none are found. + */ + static List<CommitValidationMessage> getValidationErrors(List<CommitValidationStatus> statuses) { + return statuses.stream().flatMap(s -> s.getErrors().stream()).collect(Collectors.toList()); + } + + /** + * <p> + * Obfuscates an email for public consumption, showing the first letter of the email address, and the domain of the + * address for identification, and stripping out the rest of the address. + * </p> + * + * <p> + * <strong>Example:</strong> <br /> + * Source: sample.address+123@eclipse.org <br /> + * Output: s*****@eclipse.org + * </p> + * + * @param email the address to obfuscate + * @return the obfuscated address, or empty string if (the string could not be parsed as an email address. + */ + static String obfuscateEmail(String email) { + Matcher m = EMAIL_OBFUSCATION_PATTERN.matcher(email); + return m.matches() ? m.replaceAll(EMAIL_OBFUSCATION_REPLACEMENT) : ""; + } + + /** + * Standardized conversion of error codes into messages to be consumed downstream. + * + * @param message the validation error to convert into a meaningful message + * @return the message string for the given validation error. + */ + static String getMessageForError(CommitValidationMessage message) { + String out = ""; + if (message.getStatusCode() == -406) { + out = String.format("Committer does not have permission to push on behalf of another user (Legacy). (%s)", + EclipseQuteTemplateExtensions.obfuscateEmail(message.getCommitterEmail())); + } else if (message.getStatusCode() == -405) { + out = String.format("Committer did not have a signed ECA on file. (%s)", + EclipseQuteTemplateExtensions.obfuscateEmail(message.getCommitterEmail())); + } else if (message.getStatusCode() == -404) { + out = String.format("Author did not have a signed ECA on file. (%s)", + EclipseQuteTemplateExtensions.obfuscateEmail(message.getAuthorEmail())); + } else if (message.getStatusCode() == -403) { + out = String.format("Committer does not have permission to commit on specification projects. (%s)", + EclipseQuteTemplateExtensions.obfuscateEmail(message.getCommitterEmail())); + } else if (message.getStatusCode() == -402) { + out = "Sign-off not detected in the commit message (Legacy)."; + } else if (message.getStatusCode() == -401) { + out = "Request format/state error detected."; + } else { + out = "Unaccounted for error detected, please contact administrators."; + } + return out; + } +} diff --git a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java index 9d1267af1a053208aeffdd0f8d96b73afe3ca119..f00509cba0f841fda89aa6493842c4f3de823022 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java +++ b/src/main/java/org/eclipsefoundation/git/eca/dto/CommitValidationMessage.java @@ -38,6 +38,7 @@ public class CommitValidationMessage extends BareNode { private int statusCode; private String eclipseId; private String authorEmail; + private String committerEmail; private String providerId; @Override @@ -124,6 +125,20 @@ public class CommitValidationMessage extends BareNode { this.authorEmail = authorEmail; } + /** + * @return the committerEmail + */ + public String getCommitterEmail() { + return committerEmail; + } + + /** + * @param committerEmail the committerEmail to set + */ + public void setCommitterEmail(String committerEmail) { + this.committerEmail = committerEmail; + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java index 7c55f68c51a1af3184a80c8c19c6cf01e751f1dd..889e02134b81b950bef85f9cab7b541fe9cbd556 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java +++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java @@ -155,9 +155,10 @@ public class ValidationResource { if (statuses.isEmpty()) { return Response.status(404).build(); } - return Response.ok().entity( - membershipTemplateWeb.data("statuses", statuses, "repoUrl", statuses.get(0).getRepoUrl()).render()) - .build(); + List<Project> projects = retrieveProjectsForRepoURL(statuses.get(0).getRepoUrl(), + statuses.get(0).getProvider()); + return Response.ok().entity(membershipTemplateWeb.data("statuses", statuses, "repoUrl", + statuses.get(0).getRepoUrl(), "project", projects.isEmpty() ? null : projects.get(0)).render()).build(); } /** @@ -372,6 +373,21 @@ public class ValidationResource { */ private List<Project> retrieveProjectsForRequest(ValidationRequest req) { String repoUrl = req.getRepoUrl().getPath(); + if (repoUrl == null) { + LOGGER.warn("Can not match null repo URL to projects"); + return Collections.emptyList(); + } + return retrieveProjectsForRepoURL(repoUrl, req.getProvider()); + } + + /** + * Retrieves projects for given provider, using the repo URL to match to a stored repository. + * + * @param repoUrl the repo URL to match + * @param provider the provider that is being served for the request. + * @return a list of matching projects, or an empty list if none are found. + */ + private List<Project> retrieveProjectsForRepoURL(String repoUrl, ProviderType provider) { if (repoUrl == null) { LOGGER.warn("Can not match null repo URL to projects"); return Collections.emptyList(); @@ -386,19 +402,19 @@ public class ValidationResource { // filter the projects based on the repo URL. At least one repo in project must // match the repo URL to be valid - if (ProviderType.GITLAB.equals(req.getProvider())) { + if (ProviderType.GITLAB.equals(provider)) { // get the path of the project, removing the leading slash String projectNamespace = URI.create(repoUrl).getPath().substring(1).toLowerCase(); return availableProjects.stream() .filter(p -> projectNamespace.startsWith(p.getGitlab().getProjectGroup() + "/") && p.getGitlab() .getIgnoredSubGroups().stream().noneMatch(sg -> projectNamespace.startsWith(sg + "/"))) .collect(Collectors.toList()); - } else if (ProviderType.GITHUB.equals(req.getProvider())) { + } else if (ProviderType.GITHUB.equals(provider)) { return availableProjects.stream() .filter(p -> p.getGithubRepos().stream() .anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl))) .collect(Collectors.toList()); - } else if (ProviderType.GERRIT.equals(req.getProvider())) { + } else if (ProviderType.GERRIT.equals(provider)) { return availableProjects.stream() .filter(p -> p.getGerritRepos().stream() .anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl))) diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java index 83cc07b4353438523b031096887714b303addfd6..106df30334cee4c8686728605fa754f66cdff3a6 100644 --- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java +++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java @@ -114,6 +114,7 @@ public class DefaultValidationService implements ValidationService { .map(err -> { CommitValidationMessage m = new CommitValidationMessage(); m.setAuthorEmail(c.getAuthor().getMail()); + m.setCommitterEmail(c.getCommitter().getMail()); m.setStatusCode(err.getCode().getValue()); // TODO add a checked way to set this m.setEclipseId(null); diff --git a/src/main/resources/templates/eclipse_header.html b/src/main/resources/templates/eclipse_header.html index b03554d80bf03a58e00881ba94e91baea6ca4b39..488237ddbcf222611104dbb50da7f5f9a2b3bb45 100644 --- a/src/main/resources/templates/eclipse_header.html +++ b/src/main/resources/templates/eclipse_header.html @@ -13,20 +13,20 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= <!-- End Google Tag Manager --> <meta name="author" content="Christopher Guindon"/> <meta name="keywords" content="eclipse.org, Eclipse Foundation"/> <link rel="shortcut icon" href="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/favicon.ico"/> - <title>HTML Template | The Eclipse Foundation</title> + <title>Git ECA Validation | The Eclipse Foundation</title> <link rel="preconnect stylesheet" href="//www.eclipse.org/eclipse.org-common/themes/solstice/public/stylesheets/quicksilver.min.css?v0.180"/> <meta name="description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/> <meta property="og:description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/> <meta property="og:image" content="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-200x200.png"/> -<meta property="og:title" content="HTML Template | The Eclipse Foundation"/> +<meta property="og:title" content="Git ECA Validation | The Eclipse Foundation"/> <meta property="og:image:width" content="200"/> <meta property="og:image:height" content="200"/> -<meta itemprop="name" content="HTML Template | The Eclipse Foundation"/> +<meta itemprop="name" content="Git ECA Validation | The Eclipse Foundation"/> <meta itemprop="description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/> <meta itemprop="image" content="//www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-400x400.png"/> <meta name="twitter:site" content="@EclipseFdn"/> <meta name="twitter:card" content="summary"/> -<meta name="twitter:title" content="HTML Template | The Eclipse Foundation"/> +<meta name="twitter:title" content="Git ECA Validation | The Eclipse Foundation"/> <meta name="twitter:url" content="https://www.eclipse.org/eclipse.org-common/themes/solstice/html_template/index.php?theme=default&layout=default-header"/> <meta name="twitter:description" content="The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 415 open source projects, including runtimes, tools and frameworks."/> <meta name="twitter:image" content="https://www.eclipse.org/eclipse.org-common/themes/solstice/public/images/logo/eclipse-foundation-400x400.png"/> diff --git a/src/main/resources/templates/simple_fingerprint_ui.html b/src/main/resources/templates/simple_fingerprint_ui.html index bea82497f860b0b95772aeb22e305b3a9dbb8e02..1ffdcb05bd11fd3e07a307bd26d1902be4a69404 100644 --- a/src/main/resources/templates/simple_fingerprint_ui.html +++ b/src/main/resources/templates/simple_fingerprint_ui.html @@ -9,43 +9,112 @@ background-color: #ffd8cc; color: #c74922; border-color: #c74922; +} +@media (min-width: 992px){ +.main-sidebar-default-margin { + margin-top: 0px; +} +} +.icon-toggle.glyphicon { + top: 3px; }</style>|} -<section class="container"> - <h1>Repo: {repoUrl}</h1> - <div id="accordion"> - <div class="panel list-group"> - {#for status in statuses.orEmpty} - <a href="#{status.commitHash}" data-parent="#accordion" data-toggle="collapse" class="list-group-item"> - {#if status.errors.size > 0} - <span class="badge">{status.errors.size}</span> - {/if} - <p>SHA: <strong>{status.commitHash}</strong></p> +<div class="container" id="main-page-content"> + <div id="main-content-row" class="row"> + <section id="main-content" class="col-md-18 col-sm-16 margin-bottom-20"> + <h1>Git ECA Validation Status</h1> + <div class="row"> + <div class="col-md-24 margin-bottom-20"> + <h2 class="h4 margin-top-0">About this project</h2> + <ul> + <li><strong>Eclipse Foundation Project:</strong> + <i class="fa {project ? 'fa-check brand-success' : 'fa-times brand-danger'}" aria-hidden="true"></i></li> + <li><strong>Eclipse Foundation Specification Project:</strong> + <i class="fa {project.getSpecWorkingGroup ? 'fa-check brand-success' : 'fa-times brand-danger'}" aria-hidden="true"></i></li> + </ul> + <a href="{repoUrl}" target="_blank" class="btn btn-primary margin-top-10">Project repository</a> + </div> + </div> + {#if statuses.getErrorCount > 0} + <div class="row"> + <div class="col-md-24 margin-bottom-20"> + <div class="panel panel-danger"> + <div class="panel-heading"><i class="fa fa-times" aria-hidden="true"></i> + Author(s) covered by necessary legal agreements + </div> + <div class="panel-body background-white"> + <ul class="list-group-item-text"> + {#for error in statuses.getValidationErrors} + <li>{error.getMessageForError}</li> + {/for} + </ul> + </div> + </div> + </div> + </div> + <a id="more-details" href="#accordion" class="big" data-toggle="collapse">More details + <i class="margin-left-10 icon-toggle glyphicon glyphicon-plus"></i> </a> - - <div class="padding-30 collapse" id="{status.commitHash}"> - <div>Last validated: <em>{status.lastModified.format('d MMM uuuu')}</em></div> - <div>Errors: <em>{status.errors.size}</em></div> - <ul class="list-group-item-text"> - {#for error in status.errors.orEmpty} - {#if error.statusCode == -406} - <li>Committer does not have permission to push on behalf of another user (Legacy).</li> - {#else if error.statusCode == -405} - <li>Committer did not have a signed ECA on file.</li> - {#else if error.statusCode == -404} - <li>Author did not have a signed ECA on file.</li> - {#else if error.statusCode == -403} - <li>Committer does not have permission to commit on specification projects.</li> - {#else if error.statusCode == -402} - <li>Sign-off not detected in the commit message (Legacy).</li> - {#else if error.statusCode == -401} - <li>Request format/state error detected.</li> - {#else} - <li>Unaccounted for error detected, please contact administrators.</li> - {/if} + <div id="accordion" class="collapse"> + <div class="panel list-group"> + {#for status in statuses.orEmpty} + <a href="#{status.commitHash}" data-parent="#accordion" data-toggle="collapse" class="list-group-item"> + {#if status.errors.size > 0} + <span class="badge">{status.errors.size}</span> + {/if} + <p>SHA: <strong>{status.commitHash}</strong></p> + </a> + + <div class="padding-30 collapse" id="{status.commitHash}"> + <div>Last validated: <em>{status.lastModified.format('d MMM uuuu')}</em></div> + <div>Errors: <em>{status.errors.size}</em></div> + <ul class="list-group-item-text"> + {#for error in status.errors.orEmpty} + <li>{error.getMessageForError}</li> + {/for} + </ul> + </div> {/for} - </ul> </div> - {/for} + {#else} + <div class="panel panel-success"> + <div class="panel-heading"><i class="fa fa-check" aria-hidden="true"></i> + Author(s) covered by necessary legal agreements + </div> + </div> + {/if} + </section> + + <aside id="main-sidebar-secondy" role="complementary" class="col-md-6 col-sm-8"> + <div class="region region-sidebar-second solstice-region-element-count-1"> + <section id="block-eclipse-api-github-eclipse-api-github-links" class="block block-eclipse-api-github contextual-links-region block-region-sidebar-second solstice-block block-eclipse-api-github-links clearfix"> + <div class="block-content"> + <aside class="main-sidebar-default-margin" id="main-sidebar"> + <ul id="leftnav" class="ul-left-nav fa-ul hidden-print"> + <li class="separator"><a class="separator" href="https://www.eclipse.org/legal/ECA.php"> ECA </a></li> + <li><i class="fa fa-caret-right fa-fw"></i> <a href="https://accounts.eclipse.org/user/eca" target="_self"> Sign + </a></li> + <li class="separator"><a class="separator" href="https://www.eclipse.org/legal/licenses.php"> Licenses </a></li> + <li><i class="fa fa-caret-right fa-fw"></i> <a href="https://www.eclipse.org/legal/licenses.php#approved" target="_self"> Approved 3rd Party </a></li> + <li><i class="fa fa-caret-right fa-fw"></i> <a href="https://www.eclipse.org/legal/licenses.php#nonapproved" target="_self"> Non Approved </a></li> + <li><i class="fa fa-caret-right fa-fw"></i> <a href="https://www.eclipse.org/legal/noncodelicenses.php" target="_self"> Docs & examples </a></li> + </ul> + </aside> + </div> + </section> <!-- /.block --> + </div> + </aside><!-- /#sidebar-second --> </div> -</section> +</div> +{|<script> + document.addEventListener("DOMContentLoaded", function() { + function toggleIcon(e) { + $(e.target) + .prev() + .find('.icon-toggle') + .toggleClass('glyphicon-plus glyphicon-minus'); + } + $('#accordion').on('hidden.bs.collapse', toggleIcon); + $('#accordion').on('shown.bs.collapse', toggleIcon); + }); +</script>|} {#include eclipse_footer /} \ No newline at end of file