diff --git a/pj-core/.gitignore b/pj-core/.gitignore new file mode 100644 index 0000000..5572039 --- /dev/null +++ b/pj-core/.gitignore @@ -0,0 +1,7 @@ +/.project +/.classpath +/.settings/ +/target/ +/bulldozer-temp.json +/bulldozer-state.json +/.factorypath diff --git a/pj-core/pom.xml b/pj-core/pom.xml new file mode 100644 index 0000000..20b589a --- /dev/null +++ b/pj-core/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + pj-core + + + com.g2forge.project + pj-project + 0.0.1-SNAPSHOT + ../pj-project/pom.xml + + + + + com.g2forge.gearbox + gb-argparse + ${gearbox.version} + + + com.g2forge.gearbox + gb-jira + ${gearbox.version} + + + diff --git a/pj-create/pom.xml b/pj-create/pom.xml index 09dd7be..882f1a8 100644 --- a/pj-create/pom.xml +++ b/pj-create/pom.xml @@ -15,14 +15,9 @@ - com.g2forge.gearbox - gb-argparse - ${gearbox.version} - - - com.g2forge.gearbox - gb-jira - ${gearbox.version} + com.g2forge.project + pj-core + ${project.version} diff --git a/pj-create/src/main/java/com/g2forge/project/plan/create/Create.java b/pj-create/src/main/java/com/g2forge/project/plan/create/Create.java index ea7710b..22e9a7e 100644 --- a/pj-create/src/main/java/com/g2forge/project/plan/create/Create.java +++ b/pj-create/src/main/java/com/g2forge/project/plan/create/Create.java @@ -44,9 +44,9 @@ import com.g2forge.alexandria.java.type.ref.ITypeRef; import com.g2forge.alexandria.log.HLog; import com.g2forge.gearbox.jira.ExtendedJiraRestClient; -import com.g2forge.gearbox.jira.JIRAServer; +import com.g2forge.gearbox.jira.JiraAPI; +import com.g2forge.gearbox.jira.fields.KnownField; import com.g2forge.project.plan.create.CreateIssue.CreateIssueBuilder; -import com.g2forge.project.plan.create.field.KnownField; import com.google.common.base.Objects; import io.atlassian.util.concurrent.Promise; @@ -63,7 +63,7 @@ * The INPUTFILE must be a YAML file, which consists of a configuration one field of which is issues. The configuration must specify * at least a summary for each issue, and optionally more. The fields of the issues in the YAML file are documented below. The configuration can * also include, at the top level, an entry for any field marked "configurable" whose value will be used for any issue that does not specify a value explicitly. - * Please see {@link JIRAServer} for information on specifying the Jira server and user account. + * Please see {@link JiraAPI} for information on specifying the Jira server and user account. * * * @@ -257,7 +257,7 @@ protected Map getProjectComponents(final ExtendedJiraRes protected List implementChanges(Server server, Changes changes) throws IOException, URISyntaxException, InterruptedException, ExecutionException { HLog.getLogControl().setLogLevel(Level.INFO); - try (final ExtendedJiraRestClient client = JIRAServer.createFromPropertyInput(server == null ? null : server.getApi(), null).connect(true)) { + try (final ExtendedJiraRestClient client = JiraAPI.createFromPropertyInput(server == null ? null : server.getApi(), null).connect(true)) { final Map linkTypes = new HashMap<>(); for (IssuelinksType linkType : client.getMetadataClient().getIssueLinkTypes().get()) { linkTypes.put(linkType.getName(), new LinkType(linkType.getName(), false)); diff --git a/pj-create/src/main/java/com/g2forge/project/plan/create/Server.java b/pj-create/src/main/java/com/g2forge/project/plan/create/Server.java index 548af1d..2c9a754 100644 --- a/pj-create/src/main/java/com/g2forge/project/plan/create/Server.java +++ b/pj-create/src/main/java/com/g2forge/project/plan/create/Server.java @@ -2,9 +2,10 @@ import java.util.Map; -import com.g2forge.gearbox.jira.JIRAServer; -import com.g2forge.project.plan.create.field.Field; -import com.g2forge.project.plan.create.field.KnownField; +import com.g2forge.gearbox.jira.JiraAPI; +import com.g2forge.gearbox.jira.fields.Field; +import com.g2forge.gearbox.jira.fields.IFieldConfig; +import com.g2forge.gearbox.jira.fields.KnownField; import lombok.AllArgsConstructor; import lombok.Builder; @@ -14,7 +15,7 @@ @Data @Builder @AllArgsConstructor -public class Server { +public class Server implements IFieldConfig { @Singular protected final Map fields; @@ -23,5 +24,5 @@ public class Server { @Singular protected final Map users; - protected final JIRAServer api; + protected final JiraAPI api; } diff --git a/pj-create/src/main/java/com/g2forge/project/plan/create/field/Field.java b/pj-create/src/main/java/com/g2forge/project/plan/create/field/Field.java deleted file mode 100644 index f683f26..0000000 --- a/pj-create/src/main/java/com/g2forge/project/plan/create/field/Field.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.g2forge.project.plan.create.field; - -import lombok.Builder; -import lombok.Data; -import lombok.RequiredArgsConstructor; - -@Data -@Builder(toBuilder = true) -@RequiredArgsConstructor -public class Field implements IField { - protected final String name; - - protected final String withKey; -} diff --git a/pj-create/src/main/java/com/g2forge/project/plan/create/field/IField.java b/pj-create/src/main/java/com/g2forge/project/plan/create/field/IField.java deleted file mode 100644 index 43b70ca..0000000 --- a/pj-create/src/main/java/com/g2forge/project/plan/create/field/IField.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.g2forge.project.plan.create.field; - -import com.atlassian.jira.rest.client.api.domain.input.ComplexIssueInputFieldValue; -import com.atlassian.jira.rest.client.api.domain.input.FieldInput; -import com.g2forge.alexandria.java.adt.name.IStringNamed; - -public interface IField extends IStringNamed { - public default FieldInput createFieldInput(Object value) { - final Object fieldInputValue; - final String withKey = getWithKey(); - if (withKey == null) fieldInputValue = value; - else fieldInputValue = ComplexIssueInputFieldValue.with(withKey, value); - return new FieldInput(getName(), fieldInputValue); - } - - public String getWithKey(); -} diff --git a/pj-create/src/main/java/com/g2forge/project/plan/create/field/KnownField.java b/pj-create/src/main/java/com/g2forge/project/plan/create/field/KnownField.java deleted file mode 100644 index 4c9fd1d..0000000 --- a/pj-create/src/main/java/com/g2forge/project/plan/create/field/KnownField.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.g2forge.project.plan.create.field; - -import com.atlassian.jira.rest.client.api.domain.IssueFieldId; -import com.g2forge.project.plan.create.Server; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Getter -public enum KnownField implements IField { - Assignee(IssueFieldId.ASSIGNEE_FIELD.id, "name"), - EpicSummary("customfield_10002", null), - Parent("customfield_10000", null), - Sprint("customfield_10004", null), - Security("security", "name"); - - protected final String name; - - protected final String withKey; - - public IField get(Server server) { - if ((server == null) || (server.getFields() == null)) return this; - final Field retVal = server.getFields().get(this); - if (retVal == null) return this; - return retVal; - } -} diff --git a/pj-plan/pom.xml b/pj-plan/pom.xml index 701393f..c6a211d 100644 --- a/pj-plan/pom.xml +++ b/pj-plan/pom.xml @@ -19,14 +19,9 @@ - com.g2forge.gearbox - gb-argparse - ${gearbox.version} - - - com.g2forge.gearbox - gb-jira - ${gearbox.version} + com.g2forge.project + pj-core + ${project.version} diff --git a/pj-plan/src/main/java/com/g2forge/project/plan/Download.java b/pj-plan/src/main/java/com/g2forge/project/plan/Download.java index 3909d28..dc7ea53 100644 --- a/pj-plan/src/main/java/com/g2forge/project/plan/Download.java +++ b/pj-plan/src/main/java/com/g2forge/project/plan/Download.java @@ -22,7 +22,7 @@ import com.g2forge.alexandria.command.invocation.CommandInvocation; import com.g2forge.alexandria.log.HLog; import com.g2forge.gearbox.jira.ExtendedJiraRestClient; -import com.g2forge.gearbox.jira.JIRAServer; +import com.g2forge.gearbox.jira.JiraAPI; import lombok.Builder; import lombok.Data; @@ -67,7 +67,7 @@ public IExit invoke(CommandInvocation invocation) thro header.createCell(1).setCellValue("Summary"); } - try (final ExtendedJiraRestClient client = JIRAServer.load().connect(true)) { + try (final ExtendedJiraRestClient client = JiraAPI.load().connect(true)) { final int max = 500; int base = 0, rowNum = 1; while (true) { diff --git a/pj-report/.gitignore b/pj-report/.gitignore new file mode 100644 index 0000000..5572039 --- /dev/null +++ b/pj-report/.gitignore @@ -0,0 +1,7 @@ +/.project +/.classpath +/.settings/ +/target/ +/bulldozer-temp.json +/bulldozer-state.json +/.factorypath diff --git a/pj-report/pom.xml b/pj-report/pom.xml new file mode 100644 index 0000000..b4e4240 --- /dev/null +++ b/pj-report/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + pj-report + + + com.g2forge.project + pj-project + 0.0.1-SNAPSHOT + ../pj-project/pom.xml + + + + + com.g2forge.project + pj-core + ${project.version} + + + diff --git a/pj-report/src/main/java/com/g2forge/project/report/Billing.java b/pj-report/src/main/java/com/g2forge/project/report/Billing.java new file mode 100644 index 0000000..8c986ce --- /dev/null +++ b/pj-report/src/main/java/com/g2forge/project/report/Billing.java @@ -0,0 +1,89 @@ +package com.g2forge.project.report; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.slf4j.event.Level; + +import com.atlassian.jira.rest.client.api.IssueRestClient; +import com.atlassian.jira.rest.client.api.domain.ChangelogGroup; +import com.atlassian.jira.rest.client.api.domain.ChangelogItem; +import com.atlassian.jira.rest.client.api.domain.Issue; +import com.g2forge.alexandria.command.command.IStandardCommand; +import com.g2forge.alexandria.command.exit.IExit; +import com.g2forge.alexandria.command.invocation.CommandInvocation; +import com.g2forge.alexandria.java.adt.name.IStringNamed; +import com.g2forge.alexandria.java.core.helpers.HCollection; +import com.g2forge.alexandria.log.HLog; +import com.g2forge.gearbox.argparse.ArgumentParser; +import com.g2forge.gearbox.jira.ExtendedJiraRestClient; +import com.g2forge.gearbox.jira.JiraAPI; +import com.g2forge.gearbox.jira.fields.KnownField; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Billing implements IStandardCommand { + @Data + @Builder(toBuilder = true) + @AllArgsConstructor + protected static class Arguments { + protected final String issueKey; + } + + public static void main(String[] args) throws Throwable { + IStandardCommand.main(args, new Billing()); + } + + protected void demoLogChanges(final String issueKey) throws InterruptedException, ExecutionException, IOException, URISyntaxException { + final Set fields = HCollection.asList(KnownField.Status).stream().map(IStringNamed::getName).collect(Collectors.toSet()); + try (final ExtendedJiraRestClient client = JiraAPI.load().connect(true)) { + final Issue issue = client.getIssueClient().getIssue(issueKey, HCollection.asList(IssueRestClient.Expandos.CHANGELOG)).get(); + log.info("Created at {}", issue.getCreationDate()); + for (ChangelogGroup changelogGroup : issue.getChangelog()) { + boolean printedGroupLabel = false; + for (ChangelogItem changelogItem : changelogGroup.getItems()) { + if ((fields == null) || fields.contains(changelogItem.getField())) { + if (!printedGroupLabel) { + log.info("{} {}", changelogGroup.getCreated(), changelogGroup.getAuthor().getDisplayName()); + printedGroupLabel = true; + } + log.info("\t{}: {} -> {}", changelogItem.getField(), changelogItem.getFromString(), changelogItem.getToString()); + } + } + } + } + } + + @Override + public IExit invoke(CommandInvocation invocation) throws Throwable { + HLog.getLogControl().setLogLevel(Level.INFO); + final Arguments arguments = ArgumentParser.parse(Arguments.class, invocation.getArguments()); + + demoLogChanges(arguments.getIssueKey()); + + // Progressing: Input - API info, list of users + // TODO: Search for all relevant issues (anything updatedBy a relevant user in the given time range https://confluence.atlassian.com/jirasoftwareserver/advanced-searching-functions-reference-939938746.html, might have to search across all users) + + // TODO: I/O - Start time and end time for the report, and the exact time we ran in + // TODO: Build a status history for an issue (Limit to the queried time range, Infer initial status from first status change, and create a timestamp of "now" for the end if needed) + // TODO: Input - working hours for a person (just start/stop times & days of week for now, add support for exceptions later) + // TODO: Input - mapping of issues to accounts (e.g. by epic, by component, etc) + // TODO: Construct a per-person timeline + // what accounts were they working on at all times (what issues, then group issues by account, two accounts can be double billed, or split) + // Reduce issue timeline to "active" statuses, and project those times against working hours + // Abstract the projection, so I can add filters/exceptions/days-off later + // TODO: Report on any times where a person was not billing to anything, but was working + // TODO: Report on any times an issue changed status outside working hours + + return IStandardCommand.SUCCESS; + } +} diff --git a/pj-report/src/main/java/com/g2forge/project/report/Report.java b/pj-report/src/main/java/com/g2forge/project/report/Report.java new file mode 100644 index 0000000..9a50fb2 --- /dev/null +++ b/pj-report/src/main/java/com/g2forge/project/report/Report.java @@ -0,0 +1,10 @@ +package com.g2forge.project.report; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +public class Report {} \ No newline at end of file diff --git a/pj-report/src/main/java/com/g2forge/project/report/Request.java b/pj-report/src/main/java/com/g2forge/project/report/Request.java new file mode 100644 index 0000000..a2eac09 --- /dev/null +++ b/pj-report/src/main/java/com/g2forge/project/report/Request.java @@ -0,0 +1,20 @@ +package com.g2forge.project.report; + +import java.util.List; + +import com.g2forge.gearbox.jira.JiraAPI; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +public class Request { + protected final JiraAPI api; + + @Singular + protected final List users; +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5ef6f19..1008454 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,9 @@ pj-project + pj-core pj-create pj-plan + pj-report
Create issues issue properties and their descriptions