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() {