diff --git a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java index b769b33..67064e7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/JiraConfig.java @@ -92,6 +92,30 @@ interface JiraProjectGroup { * for the timeframe to end and will get process */ EventProcessing processing(); + + /** + * Allows customizing formatting options. + */ + Formatting formatting(); + } + + interface Formatting { + + /** + * Specify how the label is formatted for a downstream issue. + *

+ * Template receives a single String token that is an upstream label. {@code %s} + * must be used in the template as it will be replaced by a {@code .+} + * regex to find matches in the already synced labels. + */ + @WithDefault("upstream-%s") + String labelTemplate(); + + /** + * Specify how {@link java.time.ZonedDateTime} is formatted to string. + */ + @WithDefault("EEEE, MMMM dd, yyyy 'at' HH:mm:ss z(Z)") + String timestampFormat(); } interface EventProcessing { diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java index d2b9387..5dbf5f6 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/HandlerProjectContext.java @@ -1,9 +1,12 @@ package org.hibernate.infra.replicate.jira.service.jira; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.infra.replicate.jira.service.jira.client.JiraRestClient; @@ -37,6 +40,8 @@ public final class HandlerProjectContext implements AutoCloseable { private final JiraUser notMappedAssignee; private final Map allProjectsContextMap; + private final Pattern sourceLabelPattern; + private final DateTimeFormatter formatter; public HandlerProjectContext(String projectName, String projectGroupName, JiraRestClient sourceJiraClient, JiraRestClient destinationJiraClient, HandlerProjectGroupContext projectGroupContext, @@ -55,6 +60,9 @@ public HandlerProjectContext(String projectName, String projectGroupName, JiraRe .map(v -> new JiraUser(projectGroup().users().mappedPropertyName(), v)).orElse(null); this.allProjectsContextMap = allProjectsContextMap; + this.sourceLabelPattern = Pattern + .compile(projectGroupContext.projectGroup().formatting().labelTemplate().formatted(".+")); + this.formatter = DateTimeFormatter.ofPattern(projectGroupContext.projectGroup().formatting().timestampFormat()); } public JiraConfig.JiraProject project() { @@ -217,4 +225,12 @@ public Optional contextForProjectInSameGroup(String proje } return Optional.ofNullable(allProjectsContextMap.get(project)); } + + public boolean isSourceLabel(String label) { + return sourceLabelPattern.matcher(label).matches(); + } + + public String formatTimestamp(ZonedDateTime time) { + return time != null ? time.format(formatter) : ""; + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java index 7574f3b..17e9563 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraCommentUpsertEventHandler.java @@ -57,11 +57,19 @@ private JiraComment prepareComment(JiraIssue issue, JiraComment source) { private String prepareCommentQuote(JiraIssue issue, JiraComment comment) { URI jiraCommentUri = createJiraCommentUri(issue, comment); UserData userData = userData(comment.self, comment.author, "the user %s"); + UserData editUserData = userData(comment.self, comment.updateAuthor, "the user %s"); String content = """ - {quote}This [comment|%s] was posted by [%s|%s].{quote} + {quote}This [comment|%s] was posted by [%s|%s] on %s.%s{quote} - """.formatted(jiraCommentUri, userData.name(), userData.uri()); + """.formatted(jiraCommentUri, userData.name(), userData.uri(), context.formatTimestamp(comment.created), + comment.isUpdatedSameAsCreated() + ? "" + : """ + + [%s|%s] edited the comment on %s. + """.formatted(editUserData.name(), editUserData.uri(), + context.formatTimestamp(comment.updated))); return truncateContent(content); } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java index 93957c4..52e7ec7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/handler/JiraIssueAbstractEventHandler.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import org.hibernate.infra.replicate.jira.JiraConfig; import org.hibernate.infra.replicate.jira.service.jira.HandlerProjectContext; @@ -20,6 +21,8 @@ abstract class JiraIssueAbstractEventHandler extends JiraEventHandler { + private static final Pattern FIX_VERSION_PATTERN = Pattern.compile("Fix_version:.++"); + public JiraIssueAbstractEventHandler(ReportingConfig reportingConfig, HandlerProjectContext context, Long id) { super(reportingConfig, context, id); } @@ -30,7 +33,8 @@ protected void applyTransition(JiraIssue sourceIssue, String destinationKey) { } protected void updateIssueBody(JiraIssue sourceIssue, String destinationKey) { - JiraIssue issue = issueToCreate(sourceIssue); + JiraIssue destIssue = context.destinationJiraClient().getIssue(destinationKey); + JiraIssue issue = issueToCreate(sourceIssue, destIssue); updateIssue(destinationKey, issue, sourceIssue, context.notMappedAssignee()); } @@ -83,7 +87,7 @@ protected JiraRemoteLink remoteSelfLink(JiraIssue sourceIssue) { return link; } - protected JiraIssue issueToCreate(JiraIssue sourceIssue) { + protected JiraIssue issueToCreate(JiraIssue sourceIssue, JiraIssue downstreamIssue) { JiraIssue destinationIssue = new JiraIssue(); destinationIssue.fields = new JiraFields(); @@ -93,17 +97,7 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue) { Objects.toString(sourceIssue.fields.description, "")); destinationIssue.fields.description = truncateContent(destinationIssue.fields.description); - destinationIssue.fields.labels = sourceIssue.fields.labels; - // let's also add fix versions to the labels - if (sourceIssue.fields.fixVersions != null) { - if (destinationIssue.fields.labels == null) { - destinationIssue.fields.labels = List.of(); - } - destinationIssue.fields.labels = new ArrayList<>(destinationIssue.fields.labels); - for (JiraSimpleObject fixVersion : sourceIssue.fields.fixVersions) { - destinationIssue.fields.labels.add("Fix version:%s".formatted(fixVersion.name).replace(' ', '_')); - } - } + destinationIssue.fields.labels = prepareLabels(sourceIssue, downstreamIssue); // if we can map the priority - great we'll do that, if no: we'll keep it blank // and let Jira use its default instead: @@ -155,6 +149,38 @@ protected JiraIssue issueToCreate(JiraIssue sourceIssue) { return destinationIssue; } + private List prepareLabels(JiraIssue sourceIssue, JiraIssue downstreamIssue) { + List labelsToSet = new ArrayList<>(); + + for (String label : sourceIssue.fields.labels) { + labelsToSet.add(asUpstreamLabel(label)); + } + + // let's also add fix versions to the labels + if (sourceIssue.fields.fixVersions != null) { + for (JiraSimpleObject fixVersion : sourceIssue.fields.fixVersions) { + String fixVersionLabel = "Fix version:%s".formatted(fixVersion.name).replace(' ', '_'); + labelsToSet.add(fixVersionLabel); + } + } + + for (String label : downstreamIssue.fields.labels) { + if (!(context.isSourceLabel(label) || isFixVersion(label))) { + labelsToSet.add(label); + } + } + + return labelsToSet; + } + + private boolean isFixVersion(String label) { + return FIX_VERSION_PATTERN.matcher(label).matches(); + } + + private String asUpstreamLabel(String label) { + return context.projectGroup().formatting().labelTemplate().formatted(label); + } + private JiraUser toUser(String value) { return new JiraUser(context.projectGroup().users().mappedPropertyName(), value); } @@ -218,13 +244,19 @@ private String prepareDescriptionQuote(JiraIssue issue) { Reported by: %s. - Upstream status: %s.{quote} + Upstream status: %s. + + Created: %s. + + Last updated: %s.{quote} """.formatted(issue.key, issueUri, assignee == null ? " Unassigned" : "[%s|%s]".formatted(assignee.name(), assignee.uri()), reporter == null ? " Unknown" : "[%s|%s]".formatted(reporter.name(), reporter.uri()), - issue.fields.status != null ? issue.fields.status.name : "Unknown"); + issue.fields.status != null ? issue.fields.status.name : "Unknown", + context.formatTimestamp(issue.fields.created), + context.formatTimestamp(issue.fields.updated != null ? issue.fields.updated : issue.fields.created)); } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraComment.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraComment.java index d7c64d1..36272c7 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraComment.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraComment.java @@ -1,6 +1,7 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; import java.net.URI; +import java.time.ZonedDateTime; import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; @@ -9,7 +10,10 @@ public class JiraComment extends JiraBaseObject { public String id; public URI self; public JiraUser author = new JiraUser(); + public JiraUser updateAuthor; public String body; + public ZonedDateTime created; + public ZonedDateTime updated; public JiraComment() { } @@ -17,4 +21,8 @@ public JiraComment() { public JiraComment(String id) { this.id = id; } + + public boolean isUpdatedSameAsCreated() { + return updated != null && updated.equals(created); + } } diff --git a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java index d5620d4..3e367d2 100644 --- a/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java +++ b/src/main/java/org/hibernate/infra/replicate/jira/service/jira/model/rest/JiraFields.java @@ -1,5 +1,6 @@ package org.hibernate.infra.replicate.jira.service.jira.model.rest; +import java.time.ZonedDateTime; import java.util.List; import org.hibernate.infra.replicate.jira.service.jira.model.JiraBaseObject; @@ -22,6 +23,8 @@ public class JiraFields extends JiraBaseObject { public List issuelinks; public JiraComments comment; public JiraIssue parent; + public ZonedDateTime created; + public ZonedDateTime updated; @Override public String toString() {