diff --git a/src/main/resources/update/2_9.sql b/src/main/resources/update/2_9.sql index 7be56ab..1ac303b 100644 --- a/src/main/resources/update/2_9.sql +++ b/src/main/resources/update/2_9.sql @@ -11,3 +11,32 @@ ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME) ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH); + + +DROP TABLE IF EXISTS COMMIT_STATUS; +CREATE TABLE COMMIT_STATUS( + COMMIT_STATUS_ID INT AUTO_INCREMENT, + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + COMMIT_ID VARCHAR(40) NOT NULL, + CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters) + STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure + TARGET_URL VARCHAR(200), + DESCRIPTION TEXT, + CREATOR VARCHAR(100) NOT NULL, + REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT + UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT +); +ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID); +ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1 + UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT); +ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1 + FOREIGN KEY (USER_NAME, REPOSITORY_NAME) + REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2 + FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME) + ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3 + FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/scala/model/BasicTemplate.scala b/src/main/scala/model/BasicTemplate.scala index 52e0a11..1d012c1 100644 --- a/src/main/scala/model/BasicTemplate.scala +++ b/src/main/scala/model/BasicTemplate.scala @@ -49,6 +49,9 @@ def byCommit(owner: String, repository: String, commitId: String) = byRepository(owner, repository) && (this.commitId === commitId) + + def byCommit(owner: Column[String], repository: Column[String], commitId: Column[String]) = + byRepository(userName, repositoryName) && (this.commitId === commitId) } } diff --git a/src/main/scala/model/CommitStatus.scala b/src/main/scala/model/CommitStatus.scala new file mode 100644 index 0000000..2977b26 --- /dev/null +++ b/src/main/scala/model/CommitStatus.scala @@ -0,0 +1,71 @@ +package model + +import scala.slick.lifted.MappedTo +import scala.slick.jdbc._ + +trait CommitStatusComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + implicit val commitStateColumnType = MappedColumnType.base[CommitState, String](b => b.name , i => CommitState(i)) + + lazy val CommitStatuses = TableQuery[CommitStatuses] + class CommitStatuses(tag: Tag) extends Table[CommitStatus](tag, "COMMIT_STATUS") with CommitTemplate { + val commitStatusId = column[Int]("COMMIT_STATUS_ID", O AutoInc) + val context = column[String]("CONTEXT") + val state = column[CommitState]("STATE") + val targetUrl = column[Option[String]]("TARGET_URL") + val description = column[Option[String]]("DESCRIPTION") + val creator = column[String]("CREATOR") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply) + def byPrimaryKey(id: Int) = commitStatusId === id.bind + } +} + +case class CommitStatus( + commitStatusId: Int = 0, + userName: String, + repositoryName: String, + commitId: String, + context: String, + state: CommitState, + targetUrl: Option[String], + description: Option[String], + creator: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date +) +sealed abstract class CommitState(val name: String) +object CommitState { + object ERROR extends CommitState("error") + object FAILURE extends CommitState("failure") + object PENDING extends CommitState("pending") + object SUCCESS extends CommitState("success") + + val values: Vector[CommitState] = Vector(PENDING, SUCCESS, ERROR, FAILURE) + private val map: Map[String, CommitState] = values.map(enum => enum.name -> enum).toMap + def apply(name: String): CommitState = map(name) + def valueOf(name: String): Option[CommitState] = map.get(name) + + /** + * failure if any of the contexts report as error or failure + * pending if there are no statuses or a context is pending + * success if the latest status for all contexts is success + */ + def combine(statuses: Set[CommitState]): CommitState = { + if(statuses.isEmpty){ + PENDING + }else if(statuses.contains(CommitState.ERROR) || statuses.contains(CommitState.FAILURE)){ + FAILURE + }else if(statuses.contains(CommitState.PENDING)){ + PENDING + }else{ + SUCCESS + } + } + + implicit val getResult: GetResult[CommitState] = GetResult(r => CommitState(r.<<)) + implicit val getResultOpt: GetResult[Option[CommitState]] = GetResult(r => r.< t.byCommit(userName, repositoryName, sha) && t.context===context.bind ) + .map(_.commitStatusId).firstOption match { + case Some(id:Int) => { + CommitStatuses.filter(_.byPrimaryKey(id)).map{ + t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description) + }.update( (state, targetUrl, now, creator.userName, description) ) + id + } + case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus( + userName = userName, + repositoryName = repositoryName, + commitId = sha, + context = context, + state = state, + targetUrl = targetUrl, + description = description, + creator = creator.userName, + registeredDate = now, + updatedDate = now) + } + + def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session) :Option[CommitStatus] = + CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption + + def getCommitStatus(userName: String, repositoryName: String, sha: String, context: String)(implicit s: Session) :Option[CommitStatus] = + CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind ).firstOption + + def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] = + byCommitStatues(userName, repositoryName, sha).list + + def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] = + byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts) + .filter{ case (t,a) => t.creator === a.userName }.list + + protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) = + CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc) +} \ No newline at end of file diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index f54291b..bae3a71 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -55,6 +55,7 @@ val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list + val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => @@ -95,6 +96,7 @@ IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Convert labelId val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap diff --git a/src/test/scala/model/CommitStateSpec.scala b/src/test/scala/model/CommitStateSpec.scala new file mode 100644 index 0000000..a744d83 --- /dev/null +++ b/src/test/scala/model/CommitStateSpec.scala @@ -0,0 +1,25 @@ +package model + +import org.specs2.mutable.Specification + +import CommitState._ + +class CommitStateSpec extends Specification { + "CommitState" should { + "combine empty must eq PENDING" in { + combine(Set()) must_== PENDING + } + "combine includes ERROR must eq FAILURE" in { + combine(Set(ERROR, SUCCESS, PENDING)) must_== FAILURE + } + "combine includes FAILURE must eq peinding" in { + combine(Set(FAILURE, SUCCESS, PENDING)) must_== FAILURE + } + "combine includes PENDING must eq peinding" in { + combine(Set(PENDING, SUCCESS)) must_== PENDING + } + "combine only SUCCESS must eq SUCCESS" in { + combine(Set(SUCCESS)) must_== SUCCESS + } + } +} diff --git a/src/test/scala/service/CommitStateServiceSpec.scala b/src/test/scala/service/CommitStateServiceSpec.scala new file mode 100644 index 0000000..24ea41d --- /dev/null +++ b/src/test/scala/service/CommitStateServiceSpec.scala @@ -0,0 +1,77 @@ +package service +import org.specs2.mutable.Specification +import java.util.Date +import model._ +import model.Profile._ +import profile.simple._ +class CommitStatusServiceSpec extends Specification with ServiceSpecBase with CommitStatusService + with RepositoryService with AccountService{ + def generateNewAccount(name:String)(implicit s:Session):Account = { + createAccount(name, name, name, s"${name}@example.com", false, None) + getAccountByUserName(name).get + } + val now = new java.util.Date() + val fixture1 = CommitStatus( + userName = "root", + repositoryName = "repo", + commitId = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7", + context = "jenkins/test", + creator = "tester", + state = CommitState.PENDING, + targetUrl = Some("http://example.com/target"), + description = Some("description"), + updatedDate = now, + registeredDate = now) + def findById(id: Int)(implicit s:Session) = CommitStatuses.filter(_.byPrimaryKey(id)).firstOption + def generateFixture1(tester:Account)(implicit s:Session) = createCommitStatus( + userName = fixture1.userName, + repositoryName = fixture1.repositoryName, + sha = fixture1.commitId, + context = fixture1.context, + state = fixture1.state, + targetUrl = fixture1.targetUrl, + description = fixture1.description, + creator = tester, + now = fixture1.registeredDate) + "CommitStatusService" should { + "createCommitState can insert and update" in { withTestDB { implicit session => + val tester = generateNewAccount(fixture1.creator) + createRepository(fixture1.repositoryName,fixture1.userName,None,false) + val id = generateFixture1(tester:Account) + getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_== + Some(fixture1.copy(commitStatusId=id)) + // other one can update + val tester2 = generateNewAccount("tester2") + val time2 = new java.util.Date(); + val id2 = createCommitStatus( + userName = fixture1.userName, + repositoryName = fixture1.repositoryName, + sha = fixture1.commitId, + context = fixture1.context, + state = CommitState.SUCCESS, + targetUrl = Some("http://example.com/target2"), + description = Some("description2"), + creator = tester2, + now = time2) + getCommitStatus(fixture1.userName, fixture1.repositoryName, id2) must_== Some(fixture1.copy( + commitStatusId = id, + creator = "tester2", + state = CommitState.SUCCESS, + targetUrl = Some("http://example.com/target2"), + description = Some("description2"), + updatedDate = time2)) + }} + "getCommitStatus can find by commitId and context" in { withTestDB { implicit session => + val tester = generateNewAccount(fixture1.creator) + createRepository(fixture1.repositoryName,fixture1.userName,None,false) + val id = generateFixture1(tester:Account) + getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) must_== Some(fixture1.copy(commitStatusId=id)) + }} + "getCommitStatus can find by commitStatusId" in { withTestDB { implicit session => + val tester = generateNewAccount(fixture1.creator) + createRepository(fixture1.repositoryName,fixture1.userName,None,false) + val id = generateFixture1(tester:Account) + getCommitStatus(fixture1.userName, fixture1.repositoryName, id) must_== Some(fixture1.copy(commitStatusId=id)) + }} + } +} \ No newline at end of file diff --git a/src/test/scala/service/RepositoryServiceSpec.scala b/src/test/scala/service/RepositoryServiceSpec.scala new file mode 100644 index 0000000..b3a5faa --- /dev/null +++ b/src/test/scala/service/RepositoryServiceSpec.scala @@ -0,0 +1,37 @@ +package service +import org.specs2.mutable.Specification +import java.util.Date +import model._ +import model.Profile._ +import profile.simple._ +class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{ + def generateNewAccount(name:String)(implicit s:Session):Account = { + createAccount(name, name, name, s"${name}@example.com", false, None) + getAccountByUserName(name).get + } + "RepositoryService" should { + "renameRepository can rename CommitState" in { withTestDB { implicit session => + val tester = generateNewAccount("tester") + createRepository("repo","root",None,false) + val commitStatusService = new CommitStatusService{} + val id = commitStatusService.createCommitStatus( + userName = "root", + repositoryName = "repo", + sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7", + context = "jenkins/test", + state = CommitState.PENDING, + targetUrl = Some("http://example.com/target"), + description = Some("description"), + creator = tester, + now = new java.util.Date) + val org = commitStatusService.getCommitStatus("root","repo", id).get + renameRepository("root","repo","tester","repo2") + val neo = commitStatusService.getCommitStatus("tester","repo2", org.commitId, org.context).get + neo must_== + org.copy( + commitStatusId=neo.commitStatusId, + repositoryName="repo2", + userName="tester") + }} + } +}