diff --git a/README.md b/README.md index ff9acf1f..96ee81fd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ Migrate your projects from JIRA to [Backlog]. * Backlog * [https://backlog.com](https://backlog.com/) + +## DEMO + +![Demo](https://www.backlog.jp/backlog-migration/backlog-jira-migration.gif) + + ## Requirements * **Java 8** * The Backlog Space's **administrator** roles. @@ -19,7 +25,7 @@ Download Please download the jar file from this link, and run from the command line as follows. -https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b1/backlog-migration-jira-0.1.0b1.jar +https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b2/backlog-migration-jira-0.1.0b2.jar java -jar backlog-migration-jira-[latest version].jar @@ -227,7 +233,7 @@ https://github.com/nulab/BacklogMigration-Jira/releases こちらのリンクからjarファイルをダウンロードし、以下のようにコマンドラインから実行します。 -https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b1/backlog-migration-jira-0.1.0b1.jar +https://github.com/nulab/BacklogMigration-Jira/releases/download/0.1.0b2/backlog-migration-jira-0.1.0b2.jar java -jar backlog-migration-jira-[最新バージョン].jar diff --git a/build.sbt b/build.sbt index eec44634..c1db3913 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ import sbt.Keys._ -lazy val projectVersion = "0.1.0b1" +lazy val projectVersion = "0.1.0b2" lazy val commonSettings = Seq( organization := "com.nulabinc", diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogIssueLinkConverter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogIssueLinkConverter.scala new file mode 100644 index 00000000..a267913e --- /dev/null +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/ChangeLogIssueLinkConverter.scala @@ -0,0 +1,44 @@ +package com.nulabinc.backlog.j2b.exporter + +import com.nulabinc.backlog.migration.common.domain.BacklogIssue +import com.nulabinc.jira.client.domain.changeLog._ + +object ChangeLogIssueLinkConverter { + + def convert(changeLogs: Seq[ChangeLog], backlogIssue: BacklogIssue): Seq[ChangeLog] = { + val displayStrings = changeLogs.flatMap { changeLog => + changeLog.items + .filter { item => item.field == DefaultField("link_issue") } + .flatMap { _.toDisplayString } + } + + val linkedIssueChangeLogItems = if (displayStrings.nonEmpty) { + val lastDescription = changeLogs.reverse.flatMap { changeLog => + changeLog.items.reverse.find(_.field == DescriptionChangeLogItemField) + }.headOption.flatMap(_.toDisplayString) + + Seq( + ChangeLog( + id = 0, // TODO: Check + author = changeLogs.last.author, + createdAt = changeLogs.last.createdAt, + items = Seq( + ChangeLogItem( + field = DescriptionChangeLogItemField, + fieldType = ChangeLogItem.FieldType.JIRA, + fieldId = Some(DescriptionFieldId), + from = None, + fromDisplayString = lastDescription, + to = None, + toDisplayString = Some(lastDescription.getOrElse("") + backlogIssue.description + "\n\n" + displayStrings.mkString("\n")) + ) + ) + ) + ) + } else { + Seq.empty[ChangeLog] + } + + changeLogs ++ linkedIssueChangeLogItems + } +} diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala index 79e0ba59..76c1aca2 100644 --- a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/Exporter.scala @@ -2,6 +2,7 @@ package com.nulabinc.backlog.j2b.exporter import javax.inject.Inject +import com.nulabinc.backlog.j2b.exporter.console.RemainingTimeCalculator import com.nulabinc.backlog.j2b.jira.conf.JiraBacklogPaths import com.nulabinc.backlog.j2b.jira.domain.mapping.MappingCollectDatabase import com.nulabinc.backlog.j2b.jira.domain.{CollectData, JiraProjectKey} @@ -65,7 +66,8 @@ class Exporter @Inject()(projectKey: JiraProjectKey, val statuses = statusService.all() val total = issueService.count() val fields = fieldService.all() - fetchIssue(statuses, categories, versions, fields, 1, total, 0, 100) + val calculator = new RemainingTimeCalculator(total) + fetchIssue(calculator, statuses, categories, versions, fields, 1, total, 0, 100) // version & milestone versionsWriter.write(versions, mappingCollectDatabase.milestones) @@ -86,7 +88,8 @@ class Exporter @Inject()(projectKey: JiraProjectKey, collectedData } - private def fetchIssue(statuses: Seq[Status], + private def fetchIssue(calculator: RemainingTimeCalculator, + statuses: Seq[Status], components: Seq[Component], versions: Seq[Version], fields: Seq[Field], @@ -155,10 +158,14 @@ class Exporter @Inject()(projectKey: JiraProjectKey, // export issue comments val categoryPlayedChangeLogs = ChangeLogsPlayer.play(ComponentChangeLogItemField, initializedBacklogIssue.categoryNames, issueWithFilteredChangeLogs.changeLogs) val versionPlayedChangeLogs = ChangeLogsPlayer.play(FixVersion, initializedBacklogIssue.versionNames, categoryPlayedChangeLogs) - val changeLogs = ChangeLogStatusConverter.convert(versionPlayedChangeLogs, statuses) + val statusPlayedChangeLogs = ChangeLogStatusConverter.convert(versionPlayedChangeLogs, statuses) + val changeLogs = ChangeLogIssueLinkConverter.convert(statusPlayedChangeLogs, initializedBacklogIssue) + commentWriter.write(initializedBacklogIssue, comments, changeLogs, issue.attachments) - console(i + index.toInt, total.toInt) +// console(i + index.toInt, total.toInt) + + calculator.progress(i + index.toInt, total.toInt, issue.summary) val changeLogUsers = changeLogs.map(u => Some(u.author.name)) val changeLogItemUsers = changeLogs.flatMap { changeLog => @@ -194,7 +201,7 @@ class Exporter @Inject()(projectKey: JiraProjectKey, } } } - fetchIssue(statuses, components, versions, fields, index + issues.length , total, startAt + maxResults, maxResults) + fetchIssue(calculator, statuses, components, versions, fields, index + issues.length , total, startAt + maxResults, maxResults) } } diff --git a/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/console/RemainingTimeCalculator.scala b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/console/RemainingTimeCalculator.scala new file mode 100644 index 00000000..4296e740 --- /dev/null +++ b/exporter/src/main/scala/com/nulabinc/backlog/j2b/exporter/console/RemainingTimeCalculator.scala @@ -0,0 +1,63 @@ +package com.nulabinc.backlog.j2b.exporter.console + +import java.util.Date + +import com.nulabinc.backlog.migration.common.utils.{ConsoleOut, DateUtil, Logging, ProgressBar} +import com.osinka.i18n.Messages +import org.fusesource.jansi.Ansi +import org.fusesource.jansi.Ansi.ansi + +class RemainingTimeCalculator(totalSize: Long) extends Logging { + + private var failed = 0 + private var date = "" + + private case class RemainingTime(totalSize: Long, lastTime: Long = System.currentTimeMillis(), totalElapsedTime: Long = 0, count: Long = 0) { + + def action(): RemainingTime = { + val elapsedTime: Long = System.currentTimeMillis() - this.lastTime + this.copy( + lastTime = System.currentTimeMillis(), + totalElapsedTime = totalElapsedTime + elapsedTime, + count = count + 1 + ) + } + + def average: Float = totalElapsedTime.toFloat / count.toFloat + + def remaining: Long = totalSize - count + + def remainingTime: Long = (remaining * average).toLong + } + + private var remainingTime = RemainingTime(totalSize) + + private[this] var newLine = false + private[this] var isMessageMode = false + + def progress(indexOfDate: Int, totalOfDate: Int, summary: String): Unit = { + newLine = indexOfDate == 1 + clear() + remainingTime = remainingTime.action() + val message = remaining() + ConsoleOut.outStream.println(message) + isMessageMode = false + } + + private def clear(): Unit = { + if (newLine && !isMessageMode) { + ConsoleOut.outStream.println() + } + (0 until 3).foreach { _ => + ConsoleOut.outStream.print(ansi.cursorLeft(999).cursorUp(1).eraseLine(Ansi.Erase.ALL)) + } + ConsoleOut.outStream.flush() + newLine = false + } + + private def remaining(): String = { + val progressBar = ProgressBar.progressBar(remainingTime.count.toInt, remainingTime.totalSize.toInt) + val time = Messages("export.remaining_time", DateUtil.timeFormat(new Date(remainingTime.remainingTime))) + s"$progressBar $time" + } +} diff --git a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala index 92877737..8c98572a 100644 --- a/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala +++ b/jira-client/src/main/scala/com/nulabinc/jira/client/domain/changeLog/ChangeLog.scala @@ -35,6 +35,7 @@ case object TimeSpentChangeLogItemField extends ChangeLogItemField("timespent") case object WorkIdChangeLogItemField extends ChangeLogItemField("WorklogId") case object TimeEstimateChangeLogItemField extends ChangeLogItemField("timeestimate") case object SprintChangeLogItemField extends ChangeLogItemField("Sprint") +case object DescriptionChangeLogItemField extends ChangeLogItemField("description") case class DefaultField(name: String) extends ChangeLogItemField(name) object ChangeLogItemField { @@ -53,6 +54,7 @@ object ChangeLogItemField { case WorkIdChangeLogItemField.value => WorkIdChangeLogItemField case TimeEstimateChangeLogItemField.value => TimeEstimateChangeLogItemField case SprintChangeLogItemField.value => SprintChangeLogItemField + case DescriptionChangeLogItemField.value => DescriptionChangeLogItemField case v => DefaultField(v) } } diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index c9c3ae22..4cb8642e 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -1,9 +1,10 @@ application { name = "Backlog Migration for Jira" - version = "0.1.0b1" + version = "0.1.0b2" title = ${application.name} ${application.version} (c) nulab.inc export-limit-at-once = 100 mixpanel.token = "5be8b628b7103858164142d02cb38347" + mixpanel.backlogtool.token = "0512c52e553b9283143bed99e61c27e4" mixpanel.product = "jira" language=default akka.mailbox-pool = 100 diff --git a/src/main/resources/messages.txt b/src/main/resources/messages.txt index c1e64255..39945cd1 100644 --- a/src/main/resources/messages.txt +++ b/src/main/resources/messages.txt @@ -53,6 +53,7 @@ cli.help.projectKey=Your JIRA project identifier.(required) Example:--projectKey cli.confirm_recreate=The mapping file {0} already exists. Do you want to overwrite it? (y/n [n]): cli.backlog_project_already_exist=Project "{0}" already exists. Do you want to import issues and wikis to project "{0}"? (Check the README file for details.) (y/n [n]): cli.error.unknown=Unknown error +cli.error.args=Invalid arguments. Please check the following. cli.cancel=Import has been canceled. cli.invalid_setup=Setup is incomplete. Please complete the set up with the sub-command "export". Add "--help" option to know about the "export" command. cli.warn.not.latest=The latest version [{0}] has been released. The current version is [{1}]. @@ -168,6 +169,8 @@ import.error.failed.comment=Could not register comment on issue [{0}]. : {1} export.start=Start export. export.finish=Export completed. Next step \n\n1. Edit mapping files.\n2. Execute the following command to import (replace with your JIRA password).\n\n--------------------------------------\n\n{0}\n\n-------------------------------------- export.attachment.empty=Attachment: {0} -> {1} +export.remaining_time=[Remaining time:{0}] +export.date.execute={2} {1} about {0}. # ----------------------------------------------------------------------------- # Convert diff --git a/src/main/resources/messages_ja.txt b/src/main/resources/messages_ja.txt index d7027560..0025fce3 100644 --- a/src/main/resources/messages_ja.txt +++ b/src/main/resources/messages_ja.txt @@ -54,6 +54,7 @@ cli.help.projectKey=移行したいJIRAプロジェクトのプロジェクト cli.confirm_recreate={0}マッピングファイルが既にあります。上書きしますか? (y/n [n]): cli.backlog_project_already_exist=プロジェクト[{0}]はBacklog内に既に存在します。\nプロジェクト[{0}]に課題とWikiをインポートしますか?(追加インポートの仕様については、詳細READMEを参照ください) (y/n [n]): cli.error.unknown=予期しないエラーが発生しました。 +cli.error.args=不正な引数です。以下をご確認下さい。 cli.cancel=インポートを中止しました。 cli.invalid_setup=エクスポートが不十分です。サブコマンド[export]を使用し再度エクスポートしてください。[export]コマンドについて知るには、[--help]オプションをつけて実行してください。 cli.warn.not.latest=最新バージョン[{0}]がリリースされています。現在のバージョンは[{1}]です。 @@ -169,6 +170,8 @@ import.error.failed.comment=コメントを課題[{0}]に登録できません export.start=エクスポートを開始します。 export.finish=エクスポートが完了しました。 次のステップは \n\n1. マッピングファイルを編集します。\n2. インポートするために以下のコメントを実行します。 (JIRAのパスワードは修正してください)\n\n--------------------------------------\n\n{0}\n\n-------------------------------------- export.attachment.empty=添付ファイル: {0} -> {1} +export.remaining_time=[残り時間:{0}] +export.date.execute={0}の{1}を{2} # ----------------------------------------------------------------------------- # Convert diff --git a/src/main/scala/com/nulabinc/backlog/j2b/Main.scala b/src/main/scala/com/nulabinc/backlog/j2b/Main.scala index ab336871..e894acd4 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/Main.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/Main.scala @@ -12,6 +12,8 @@ import com.osinka.i18n.Messages import org.fusesource.jansi.AnsiConsole import org.rogach.scallop.{ScallopConf, Subcommand} +import scala.util.{Failure, Success, Try} + class CommandLineInterface(arguments: Seq[String]) extends ScallopConf(arguments) with BacklogConfiguration with Logging { banner( @@ -105,15 +107,22 @@ object J2B extends BacklogConfiguration with Logging { // Run try { val cli = new CommandLineInterface(args) - val config = getConfiguration(cli) val nextCommand = new NextCommand(args) - cli.subcommand match { - case Some(cli.importCommand) => J2BCli.`import`(config) - case Some(cli.exportCommand) => J2BCli.export(config, nextCommand) - case _ => J2BCli.help() + getConfiguration(cli) match { + case Success(config) => + cli.subcommand match { + case Some(cli.importCommand) => J2BCli.`import`(config) + case Some(cli.exportCommand) => J2BCli.export(config, nextCommand) + case _ => J2BCli.help() + } + exit(0) + case Failure(failure) => + ConsoleOut.error(s"${Messages("cli.error.args")}") + logger.error(failure.getMessage) + J2BCli.help() + exit(1) } - exit(0) } catch { case e: Throwable => logger.error(e.getMessage, e) @@ -122,7 +131,7 @@ object J2B extends BacklogConfiguration with Logging { } } - private[this] def getConfiguration(cli: CommandLineInterface) = { + private[this] def getConfiguration(cli: CommandLineInterface) = Try { val keys: Array[String] = cli.importCommand.projectKey().split(":") val jira: String = keys(0) val backlog: String = if (keys.length == 2) keys(1) else keys(0).toUpperCase.replaceAll("-", "_") diff --git a/src/main/scala/com/nulabinc/backlog/j2b/cli/Tracker.scala b/src/main/scala/com/nulabinc/backlog/j2b/cli/Tracker.scala index 0eb9c9d9..96c90de3 100644 --- a/src/main/scala/com/nulabinc/backlog/j2b/cli/Tracker.scala +++ b/src/main/scala/com/nulabinc/backlog/j2b/cli/Tracker.scala @@ -12,17 +12,23 @@ trait Tracker extends BacklogConfiguration { def tracking(config: AppConfiguration, spaceService: SpaceService, userService: UserService) = { Try { val environment = spaceService.environment() - val data = TrackingData(product = mixpanelProduct, - envname = environment.name, - spaceId = environment.spaceId, - userId = userService.myself().id, - srcUrl = config.jiraConfig.url, - dstUrl = config.backlogConfig.url, - srcProjectKey = config.jiraConfig.projectKey, - dstProjectKey = config.backlogConfig.projectKey, + val data = TrackingData( + product = mixpanelProduct, + envname = environment.name, + spaceId = environment.spaceId, + userId = userService.myself().id, + srcUrl = config.jiraConfig.url, + dstUrl = config.backlogConfig.url, + srcProjectKey = config.jiraConfig.projectKey, + dstProjectKey = config.backlogConfig.projectKey, srcSpaceCreated = "", dstSpaceCreated = spaceService.space().created) - MixpanelUtil.track(token = mixpanelToken, data = data) + val backlogToolEnvNames = Seq( + "backlogtool", + "us-6" + ) + val token = if (backlogToolEnvNames.contains(environment.name)) mixpanelBacklogtoolToken else mixpanelToken + MixpanelUtil.track(token = token, data = data) } } } diff --git a/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala b/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala index 4bada080..82914d48 100644 --- a/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala +++ b/src/test/scala/com/nulabinc/backlog/j2b/CompareSpec.scala @@ -9,6 +9,7 @@ import com.nulabinc.backlog.migration.common.convert.writes.UserWrites import com.nulabinc.backlog4j.{CustomFieldSetting, IssueComment, ResponseList} import com.nulabinc.backlog4j.api.option.{GetIssuesParams, QueryParams} import com.nulabinc.backlog4j.internal.json.customFields._ +import com.nulabinc.jira.client.domain.changeLog.LinkChangeLogItemField import com.nulabinc.jira.client.domain.field._ import com.nulabinc.jira.client.domain.issue._ import org.scalatest.{DiagrammedAssertions, FlatSpec, Matchers} @@ -132,9 +133,6 @@ class CompareSpec extends FlatSpec maybeBacklogIssue.map { backlogIssue => - // description - jiraIssue.description.getOrElse("") should equal(backlogIssue.getDescription) - // issue type jiraIssue.issueType.name should equal(backlogIssue.getIssueType.getName) @@ -270,7 +268,7 @@ class CompareSpec extends FlatSpec jiraCommentService.issueComments(jiraIssue).filterNot { jiraComment => attachmentCommentPattern.findFirstIn(jiraComment.body).isDefined }.map { jiraComment => - val backlogComment = backlogAllComments.find(_.getContent == jiraComment.body) + val backlogComment = backlogAllComments.find(_.getContent == jiraComment.body.trim) backlogComment should not be empty assertUser(jiraComment.author, backlogComment.get.getCreatedUser) timestampToString(jiraComment.createdAt.toDate) should be(timestampToString(backlogComment.get.getCreated)) @@ -280,7 +278,8 @@ class CompareSpec extends FlatSpec // Test // - creator is same // - created at is same - jiraIssueService.changeLogs(jiraIssue).map { jiraChangeLog => + val jiraChangeLogs = jiraIssueService.changeLogs(jiraIssue) + jiraChangeLogs.map { jiraChangeLog => val backlogChangelog = backlogAllComments.find { backlogComment => timestampToString(backlogComment.getCreated) == timestampToString(jiraChangeLog.createdAt.toDate) } @@ -288,6 +287,12 @@ class CompareSpec extends FlatSpec assertUser(jiraChangeLog.author, backlogChangelog.get.getCreatedUser) } + // description + val links = jiraChangeLogs.flatMap(_.items.filter(_.field == LinkChangeLogItemField).flatMap(_.toDisplayString)) + val linksReplace = "\n\n" + links.mkString("\n") + val backlogDescription = backlogIssue.getDescription.replace(linksReplace, "") + jiraIssue.description.getOrElse("") should equal(backlogDescription) + } }