diff --git a/src/main/resources/update/1_5.sql b/src/main/resources/update/1_5.sql new file mode 100644 index 0000000..35131fc --- /dev/null +++ b/src/main/resources/update/1_5.sql @@ -0,0 +1,22 @@ +ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100); +ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100); +ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100); +ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100); + +CREATE TABLE PULL_REQUEST( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + ISSUE_ID INT NOT NULL, + BRANCH VARCHAR(100) NOT NULL, + REQUEST_USER_NAME VARCHAR(100) NOT NULL, + REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL, + REQUEST_BRANCH VARCHAR(100) NOT NULL, + COMMIT_ID_FROM VARCHAR(40) NOT NULL, + COMMIT_ID_TO VARCHAR(40) NOT NULL +); + +ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID); +ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID); +ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK1 FOREIGN KEY (REQUEST_USER_NAME, REQUEST_REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME); + +ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 1233a28..874bbbd 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -18,6 +18,7 @@ context.mount(new LabelsController, "/*") context.mount(new MilestonesController, "/*") context.mount(new IssuesController, "/*") + context.mount(new PullRequestsController, "/*") context.mount(new RepositorySettingsController, "/*") val dir = new java.io.File(_root_.util.Directory.GitBucketHome) diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala index 04cc719..3446d16 100644 --- a/src/main/scala/app/CreateRepositoryController.scala +++ b/src/main/scala/app/CreateRepositoryController.scala @@ -1,27 +1,30 @@ package app import util.Directory._ -import util.{JGitUtil, UsersAuthenticator} +import util.{LockUtil, JGitUtil, UsersAuthenticator, ReferrerAuthenticator} import service._ import java.io.File import org.eclipse.jgit.api.Git import org.apache.commons.io._ import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.lib.PersonIdent class CreateRepositoryController extends CreateRepositoryControllerBase with RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator + with UsersAuthenticator with ReferrerAuthenticator /** * Creates new repository. */ trait CreateRepositoryControllerBase extends ControllerBase { self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator => + with UsersAuthenticator with ReferrerAuthenticator => case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) - val form = mapping( + case class ForkRepositoryForm(owner: String, name: String) + + val newForm = mapping( "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, unique))), "description" -> trim(label("Description" , optional(text()))), @@ -29,6 +32,11 @@ "createReadme" -> trim(label("Create README" , boolean())) )(RepositoryCreationForm.apply) + val forkForm = mapping( + "owner" -> trim(label("Repository owner", text(required))), + "name" -> trim(label("Repository name", text(required))) + )(ForkRepositoryForm.apply) + /** * Show the new repository form. */ @@ -39,77 +47,142 @@ /** * Create new repository. */ - post("/new", form)(usersOnly { form => - val ownerAccount = getAccountByUserName(form.owner).get - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName + post("/new", newForm)(usersOnly { form => + LockUtil.lock(s"${form.owner}/${form.name}/create"){ + if(getRepository(form.owner, form.name, baseUrl).isEmpty){ + val ownerAccount = getAccountByUserName(form.owner).get + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName - // Insert to the database at first - createRepository(form.name, form.owner, form.description, form.isPrivate) + // Insert to the database at first + createRepository(form.name, form.owner, form.description, form.isPrivate) - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(form.owner).foreach { userName => - addCollaborator(form.owner, form.name, userName) + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(form.owner).foreach { userName => + addCollaborator(form.owner, form.name, userName) + } + } + + // Insert default labels + insertDefaultLabels(loginUserName, form.name) + + // Create the actual repository + val gitdir = getRepositoryDir(form.owner, form.name) + JGitUtil.initRepository(gitdir) + + if(form.createReadme){ + val tmpdir = getInitRepositoryDir(form.owner, form.name) + try { + // Clone the repository + Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call + + // Create README.md + FileUtils.writeStringToFile(new File(tmpdir, "README.md"), + if(form.description.nonEmpty){ + form.name + "\n" + + "===============\n" + + "\n" + + form.description.get + } else { + form.name + "\n" + + "===============\n" + }, "UTF-8") + + val git = Git.open(tmpdir) + git.add.addFilepattern("README.md").call + git.commit + .setCommitter(new PersonIdent(loginUserName, loginAccount.mailAddress)) + .setMessage("Initial commit").call + git.push.call + + } finally { + FileUtils.deleteDirectory(tmpdir) + } + } + + // Create Wiki repository + createWikiRepository(loginAccount, form.owner, form.name) + + // Record activity + recordCreateRepositoryActivity(form.owner, form.name, loginUserName) } + + // redirect to the repository + redirect(s"/${form.owner}/${form.name}") } - - // Insert default labels - createLabel(form.owner, form.name, "bug", "fc2929") - createLabel(form.owner, form.name, "duplicate", "cccccc") - createLabel(form.owner, form.name, "enhancement", "84b6eb") - createLabel(form.owner, form.name, "invalid", "e6e6e6") - createLabel(form.owner, form.name, "question", "cc317c") - createLabel(form.owner, form.name, "wontfix", "ffffff") - - // Create the actual repository - val gitdir = getRepositoryDir(form.owner, form.name) - JGitUtil.initRepository(gitdir) - - if(form.createReadme){ - val tmpdir = getInitRepositoryDir(form.owner, form.name) - try { - // Clone the repository - Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call - - // Create README.md - FileUtils.writeStringToFile(new File(tmpdir, "README.md"), - if(form.description.nonEmpty){ - form.name + "\n" + - "===============\n" + - "\n" + - form.description.get - } else { - form.name + "\n" + - "===============\n" - }, "UTF-8") - - val git = Git.open(tmpdir) - git.add.addFilepattern("README.md").call - git.commit.setCommitter(loginAccount.userName, loginAccount.mailAddress).setMessage("Initial commit").call - git.push.call - - } finally { - FileUtils.deleteDirectory(tmpdir) - } - } - - // Create Wiki repository - createWikiRepository(loginAccount, form.owner, form.name) - - // Record activity - recordCreateRepositoryActivity(form.owner, form.name, loginUserName) - - // redirect to the repository - redirect(s"/${form.owner}/${form.name}") }) + post("/:owner/:repository/_fork")(referrersOnly { repository => + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ + if(getRepository(loginUserName, repository.name, baseUrl).isEmpty){ + // Insert to the database at first + val originUserName = repository.repository.originUserName.getOrElse(repository.owner) + val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) + + createRepository( + repositoryName = repository.name, + userName = loginUserName, + description = repository.repository.description, + isPrivate = repository.repository.isPrivate, + originRepositoryName = Some(originRepositoryName), + originUserName = Some(originUserName), + parentRepositoryName = Some(repository.name), + parentUserName = Some(repository.owner) + ) + + // Insert default labels + insertDefaultLabels(loginUserName, repository.name) + + // clone repository actually + JGitUtil.cloneRepository( + getRepositoryDir(repository.owner, repository.name), + getRepositoryDir(loginUserName, repository.name)) + + // Create Wiki repository + JGitUtil.cloneRepository( + getWikiRepositoryDir(repository.owner, repository.name), + getWikiRepositoryDir(loginUserName, repository.name)) + + // insert commit id + JGitUtil.withGit(getRepositoryDir(loginUserName, repository.name)){ git => + JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => + JGitUtil.getCommitLog(git, branch) match { + case Right((commits, _)) => commits.foreach { commit => + if(!existsCommitId(loginUserName, repository.name, commit.id)){ + insertCommitId(loginUserName, repository.name, commit.id) + } + } + case Left(_) => ??? + } + } + } + + // Record activity + recordForkActivity(repository.owner, repository.name, loginUserName) + } + // redirect to the repository + redirect("/%s/%s".format(loginUserName, repository.name)) + } + }) + + private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { + createLabel(userName, repositoryName, "bug", "fc2929") + createLabel(userName, repositoryName, "duplicate", "cccccc") + createLabel(userName, repositoryName, "enhancement", "84b6eb") + createLabel(userName, repositoryName, "invalid", "e6e6e6") + createLabel(userName, repositoryName, "question", "cc317c") + createLabel(userName, repositoryName, "wontfix", "ffffff") + } + private def existsAccount: Constraint = new Constraint(){ def validate(name: String, value: String): Option[String] = if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None } - /** * Duplicate check for the repository name. */ diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index 7186d40..2728b5e 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -32,26 +32,26 @@ else IssueSearchCondition(request) session.put(sessionKey, condition) - + val userName = context.loginAccount.get.userName val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) val filterUser = Map(filter -> userName) - val page = IssueSearchCondition.page(request) + val page = IssueSearchCondition.page(request) // dashboard.html.issues( issues.html.listparts( - searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, repositories: _*), + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, repositories: _*), page, - countIssue(condition.copy(state = "open"), filterUser, repositories: _*), - countIssue(condition.copy(state = "closed"), filterUser, repositories: _*), + countIssue(condition.copy(state = "open"), filterUser, false, repositories: _*), + countIssue(condition.copy(state = "closed"), filterUser, false, repositories: _*), condition), - countIssue(condition, Map.empty, repositories: _*), - countIssue(condition, Map("assigned" -> userName), repositories: _*), - countIssue(condition, Map("created_by" -> userName), repositories: _*), + countIssue(condition, Map.empty, false, repositories: _*), + countIssue(condition, Map("assigned" -> userName), false, repositories: _*), + countIssue(condition, Map("created_by" -> userName), false, repositories: _*), countIssueGroupByRepository(condition, filterUser, repositories: _*), condition, filter) } -} +} diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index af8da84..ccebc5e 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -128,14 +128,22 @@ }) post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { id => - redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}") + handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => + if(issue.isPullRequest){ + redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}") + } else { + redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}") + } } getOrElse NotFound }) post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { id => - redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}") + handleComment(form.issueId, form.content, repository)() map { case (issue, id) => + if(issue.isPullRequest){ + redirect(s"/${repository.owner}/${repository.name}/pull/${form.issueId}#comment-${id}") + } else { + redirect(s"/${repository.owner}/${repository.name}/issues/${form.issueId}#comment-${id}") + } } getOrElse NotFound }) @@ -294,7 +302,7 @@ content foreach ( recordCommentIssueActivity(owner, name, userName, issueId, _) ) recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - commentId + (issue, commentId) } } @@ -313,16 +321,16 @@ session.put(sessionKey, condition) issues.html.list( - searchIssue(condition, filterUser, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), page, (getCollaborators(owner, repoName) :+ owner).sorted, getMilestones(owner, repoName), getLabels(owner, repoName), - countIssue(condition.copy(state = "open"), filterUser, owner -> repoName), - countIssue(condition.copy(state = "closed"), filterUser, owner -> repoName), - countIssue(condition, Map.empty, owner -> repoName), - context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), owner -> repoName)), - context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), owner -> repoName)), + countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), + countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), + countIssue(condition, Map.empty, false, owner -> repoName), + context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), + context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), countIssueGroupByLabels(owner, repoName, condition, filterUser), condition, filter, diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala new file mode 100644 index 0000000..d361d44 --- /dev/null +++ b/src/main/scala/app/PullRequestsController.scala @@ -0,0 +1,400 @@ +package app + +import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator} +import util.Directory._ +import util.Implicits._ +import service._ +import org.eclipse.jgit.api.Git +import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.transport.RefSpec +import org.apache.commons.io.FileUtils +import scala.collection.JavaConverters._ +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.api.MergeCommand.FastForwardMode +import service.IssuesService._ +import service.PullRequestService._ +import util.JGitUtil.DiffInfo +import scala.Some +import service.RepositoryService.RepositoryTreeNode +import util.JGitUtil.CommitInfo + +class PullRequestsController extends PullRequestsControllerBase + with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService + with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait PullRequestsControllerBase extends ControllerBase { + self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService + with ReferrerAuthenticator with CollaboratorsAuthenticator => + + val pullRequestForm = mapping( + "title" -> trim(label("Title" , text(required, maxlength(100)))), + "content" -> trim(label("Content", optional(text()))), + "targetUserName" -> trim(text(required, maxlength(100))), + "targetBranch" -> trim(text(required, maxlength(100))), + "requestUserName" -> trim(text(required, maxlength(100))), + "requestBranch" -> trim(text(required, maxlength(100))), + "commitIdFrom" -> trim(text(required, maxlength(40))), + "commitIdTo" -> trim(text(required, maxlength(40))) + )(PullRequestForm.apply) + + val mergeForm = mapping( + "message" -> trim(label("Message", text(required))) + )(MergeForm.apply) + + case class PullRequestForm( + title: String, + content: Option[String], + targetUserName: String, + targetBranch: String, + requestUserName: String, + requestBranch: String, + commitIdFrom: String, + commitIdTo: String) + + case class MergeForm(message: String) + + get("/:owner/:repository/pulls")(referrersOnly { repository => + searchPullRequests(None, repository) + }) + + get("/:owner/:repository/pulls/:userName")(referrersOnly { repository => + searchPullRequests(Some(params("userName")), repository) + }) + + get("/:owner/:repository/pull/:id")(referrersOnly { repository => + val owner = repository.owner + val name = repository.name + val issueId = params("id").toInt + + getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + JGitUtil.withGit(getRepositoryDir(owner, name)){ git => + val requestCommitId = git.getRepository.resolve(pullreq.requestBranch) + + val (commits, diffs) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + + pulls.html.pullreq( + issue, pullreq, + getComments(owner, name, issueId.toInt), + (getCollaborators(owner, name) :+ owner).sorted, + getMilestonesWithIssueCount(owner, name), + commits, + diffs, + requestCommitId.getName, + if(issue.closed){ + false + } else { + checkConflict(owner, name, pullreq.branch, owner, name, pullreq.requestBranch) + }, + hasWritePermission(owner, name, context.loginAccount), + repository, + s"${baseUrl}${context.path}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") + } + + } getOrElse NotFound + }) + + post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => + LockUtil.lock(s"${repository.owner}/${repository.name}/merge"){ + val issueId = params("id").toInt + + getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) => + val remote = getRepositoryDir(repository.owner, repository.name) + val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}") + val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call + + try { + // mark issue as merged and close. + val loginAccount = context.loginAccount.get + createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Merge", "merge") + createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close") + updateClosed(repository.owner, repository.name, issueId, true) + recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message) + + // fetch pull request to working repository + val pullRequestBranchName = s"gitbucket-pullrequest-${issueId}" + + git.fetch + .setRemote(getRepositoryDir(repository.owner, repository.name).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/pull/${issueId}/head:refs/heads/${pullRequestBranchName}")).call + + // merge pull request + git.checkout.setName(pullreq.branch).call + + val result = git.merge + .include(git.getRepository.resolve(pullRequestBranchName)) + .setFastForward(FastForwardMode.NO_FF) + .setCommit(false) + .call + + if(result.getConflicts != null){ + throw new RuntimeException("This pull request can't merge automatically.") + } + + // merge commit + git.getRepository.writeMergeCommitMsg( + s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n" + + form.message) + + git.commit + .setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress)) + .call + + // push + git.push.call + + val (commits, _) = getRequestCompareInfo(repository.owner, repository.name, pullreq.commitIdFrom, + pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) + + commits.flatten.foreach { commit => + if(!existsCommitId(repository.owner, repository.name, commit.id)){ + insertCommitId(repository.owner, repository.name, commit.id) + } + } + + redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") + + } finally { + git.getRepository.close + FileUtils.deleteDirectory(tmpdir) + } + } getOrElse NotFound + } + }) + + /** + * Checks whether conflict will be caused in merging. + * Returns true if conflict will be caused. + */ + private def checkConflict(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { + // TODO Are there more quick way? + LockUtil.lock(s"${userName}/${repositoryName}/merge-check"){ + val remote = getRepositoryDir(userName, repositoryName) + val tmpdir = new java.io.File(getTemporaryDir(userName, repositoryName), "merge-check") + if(tmpdir.exists()){ + FileUtils.deleteDirectory(tmpdir) + } + + val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call + try { + git.checkout.setName(branch).call + + git.fetch + .setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/heads/${requestBranch}")).call + + val result = git.merge + .include(git.getRepository.resolve("FETCH_HEAD")) + .setCommit(false).call + + result.getConflicts != null + + } finally { + git.getRepository.close + FileUtils.deleteDirectory(tmpdir) + } + } + } + + get("/:owner/:repository/compare")(collaboratorsOnly { forkedRepository => + (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { + case (Some(originUserName), Some(originRepositoryName)) => { + getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository => + withGit( + getRepositoryDir(originUserName, originRepositoryName), + getRepositoryDir(forkedRepository.owner, forkedRepository.name) + ){ (oldGit, newGit) => + val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 + val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 + + redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") + } + } getOrElse NotFound + } + case _ => { + JGitUtil.withGit(getRepositoryDir(forkedRepository.owner, forkedRepository.name)){ git => + val defaultBranch = JGitUtil.getDefaultBranch(git, forkedRepository).get._2 + redirect(s"${context.path}/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") + } + } + } + }) + + get("/:owner/:repository/compare/*...*")(collaboratorsOnly { repository => + val Seq(origin, forked) = multiParams("splat") + val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, repository.owner) + val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, repository.owner) + + (getRepository(originOwner, repository.name, baseUrl), + getRepository(forkedOwner, repository.name, baseUrl)) match { + case (Some(originRepository), Some(forkedRepository)) => { + withGit( + getRepositoryDir(originOwner, repository.name), + getRepositoryDir(forkedOwner, repository.name) + ){ case (oldGit, newGit) => + val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 + val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 + + val forkedId = getForkedCommitId(oldGit, newGit, + originOwner, repository.name, originBranch, + forkedOwner, repository.name, forkedBranch) + + val oldId = oldGit.getRepository.resolve(forkedId) + val newId = newGit.getRepository.resolve(forkedBranch) + + val (commits, diffs) = getRequestCompareInfo( + originOwner, repository.name, oldId.getName, + forkedOwner, repository.name, newId.getName) + + pulls.html.compare( + commits, + diffs, + repository.repository.originUserName.map { userName => + getRepositoryNames(getForkedRepositoryTree(userName, repository.name)) + } getOrElse Nil, + originBranch, + forkedBranch, + oldId.getName, + newId.getName, + checkConflict(originOwner, repository.name, originBranch, forkedOwner, repository.name, forkedBranch), + repository, + originRepository, + forkedRepository) + } + } + case _ => NotFound + } + }) + + post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => + val loginUserName = context.loginAccount.get.userName + + val issueId = createIssue( + owner = repository.owner, + repository = repository.name, + loginUser = loginUserName, + title = form.title, + content = form.content, + assignedUserName = None, + milestoneId = None, + isPullRequest = true) + + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = issueId, + originBranch = form.targetBranch, + requestUserName = form.requestUserName, + requestRepositoryName = repository.name, + requestBranch = form.requestBranch, + commitIdFrom = form.commitIdFrom, + commitIdTo = form.commitIdTo) + + // fetch requested branch + JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => + git.fetch + .setRemote(getRepositoryDir(form.requestUserName, repository.name).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) + .call + } + + recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title) + + redirect(s"/${repository.owner}/${repository.name}/pulls/${issueId}") + }) + + /** + * Handles w Git object simultaneously. + */ + private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = { + val oldGit = Git.open(oldDir) + val newGit = Git.open(newDir) + try { + action(oldGit, newGit) + } finally { + oldGit.getRepository.close + newGit.getRepository.close + } + } + + /** + * Parses branch identifier and extracts owner and branch name as tuple. + * + * - "owner:branch" to ("owner", "branch") + * - "branch" to ("defaultOwner", "branch") + */ + private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) = + if(value.contains(':')){ + val array = value.split(":") + (array(0), array(1)) + } else { + (defaultOwner, value) + } + + /** + * Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list. + */ + private def getRepositoryNames(node: RepositoryTreeNode): List[String] = + node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten + + /** + * Returns the identifier of the root commit (or latest merge commit) of the specified branch. + */ + private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String): String = + JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit => + existsCommitId(userName, repositoryName, commit.getName) && + JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch) + }.head.id + + private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = { + + withGit( + getRepositoryDir(userName, repositoryName), + getRepositoryDir(requestUserName, requestRepositoryName) + ){ (oldGit, newGit) => + val oldId = oldGit.getRepository.resolve(branch) + val newId = newGit.getRepository.resolve(requestCommitId) + + val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => + new CommitInfo(revCommit) + }.toList.splitWith{ (commit1, commit2) => + view.helpers.date(commit1.time) == view.helpers.date(commit2.time) + } + + val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) + + (commits, diffs) + } + } + + private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = { + val owner = repository.owner + val repoName = repository.name + val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "") + val page = IssueSearchCondition.page(request) + val sessionKey = s"${owner}/${repoName}/pulls" + + // retrieve search condition + val condition = if(request.getQueryString == null){ + session.get(sessionKey).getOrElse(IssueSearchCondition()).asInstanceOf[IssueSearchCondition] + } else IssueSearchCondition(request) + + session.put(sessionKey, condition) + + pulls.html.list( + searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), + getPullRequestCount(condition.state == "closed", Some(owner, repoName)), + userName, + page, + countIssue(condition.copy(state = "open"), filterUser, true, owner -> repoName), + countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), + countIssue(condition, Map.empty, true, owner -> repoName), + condition, + repository, + hasWritePermission(owner, repoName, context.loginAccount)) + } + +} diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index 780a7f3..14c4f91 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -45,7 +45,15 @@ * Save the repository options. */ post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => - saveRepositoryOptions(repository.owner, repository.name, form.description, form.defaultBranch, form.isPrivate) + saveRepositoryOptions( + repository.owner, + repository.name, + form.description, + form.defaultBranch, + repository.repository.parentUserName.map { _ => + repository.repository.isPrivate + } getOrElse form.isPrivate + ) flash += "info" -> "Repository settings has been updated." redirect(s"/${repository.owner}/${repository.name}/settings/options") }) diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 3a510c8..f5e7dc7 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -182,6 +182,14 @@ } }) + get("/:owner/:repository/network/members")(referrersOnly { repository => + repo.html.forked( + getForkedRepositoryTree( + repository.repository.originUserName.getOrElse(repository.owner), + repository.repository.originRepositoryName.getOrElse(repository.name)), + repository) + }) + private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = { val id = repository.branchList.collectFirst { case branch if(path == branch || path.startsWith(branch + "/")) => branch @@ -207,7 +215,7 @@ JGitUtil.withGit(getRepositoryDir(repository.owner, repository.name)){ git => val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) // get specified commit - revisions.map { rev => (git.getRepository.resolve(rev), rev)}.find(_._1 != null).map { case (objectId, revision) => + JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => val revCommit = JGitUtil.getRevCommitFromId(git, objectId) // get files diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala index 932f9e2..c1c1feb 100644 --- a/src/main/scala/app/WikiController.scala +++ b/src/main/scala/app/WikiController.scala @@ -59,7 +59,7 @@ val commitId = params("commitId").split("\\.\\.\\.") JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => - wiki.html.compare(Some(pageName), getWikiDiffs(git, commitId(0), commitId(1)), repository) + wiki.html.compare(Some(pageName), JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) } }) @@ -67,7 +67,7 @@ val commitId = params("commitId").split("\\.\\.\\.") JGitUtil.withGit(getWikiRepositoryDir(repository.owner, repository.name)){ git => - wiki.html.compare(None, getWikiDiffs(git, commitId(0), commitId(1)), repository) + wiki.html.compare(None, JGitUtil.getDiffs(git, commitId(0), commitId(1), true), repository) } }) @@ -105,9 +105,10 @@ }) get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) + val pageName = StringUtil.urlDecode(params("page")) + val account = context.loginAccount.get - deleteWikiPage(repository.owner, repository.name, pageName, context.loginAccount.get.userName, s"Delete ${pageName}") + deleteWikiPage(repository.owner, repository.name, pageName, account.userName, account.mailAddress, s"Delete ${pageName}") updateLastActivityDate(repository.owner, repository.name) redirect(s"/${repository.owner}/${repository.name}/wiki") diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala index 769b059..d5ce8a3 100644 --- a/src/main/scala/model/Issue.scala +++ b/src/main/scala/model/Issue.scala @@ -20,7 +20,8 @@ def closed = column[Boolean]("CLOSED") def registeredDate = column[java.util.Date]("REGISTERED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate <> (Issue, Issue.unapply _) + def pullRequest = column[Boolean]("PULL_REQUEST") + def * = userName ~ repositoryName ~ issueId ~ openedUserName ~ milestoneId.? ~ assignedUserName.? ~ title ~ content.? ~ closed ~ registeredDate ~ updatedDate ~ pullRequest <> (Issue, Issue.unapply _) def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) } @@ -36,4 +37,5 @@ content: Option[String], closed: Boolean, registeredDate: java.util.Date, - updatedDate: java.util.Date) \ No newline at end of file + updatedDate: java.util.Date, + isPullRequest: Boolean) \ No newline at end of file diff --git a/src/main/scala/model/PullRequest.scala b/src/main/scala/model/PullRequest.scala new file mode 100644 index 0000000..0fb3205 --- /dev/null +++ b/src/main/scala/model/PullRequest.scala @@ -0,0 +1,28 @@ +package model + +import scala.slick.driver.H2Driver.simple._ + +object PullRequests extends Table[PullRequest]("PULL_REQUEST") with IssueTemplate { + def branch = column[String]("BRANCH") + def requestUserName = column[String]("REQUEST_USER_NAME") + def requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME") + def requestBranch = column[String]("REQUEST_BRANCH") + def commitIdFrom = column[String]("COMMIT_ID_FROM") + def commitIdTo = column[String]("COMMIT_ID_TO") + def * = userName ~ repositoryName ~ issueId ~ branch ~ requestUserName ~ requestRepositoryName ~ requestBranch ~ commitIdFrom ~ commitIdTo <> (PullRequest, PullRequest.unapply _) + + def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId) + def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId) +} + +case class PullRequest( + userName: String, + repositoryName: String, + issueId: Int, + branch: String, + requestUserName: String, + requestRepositoryName: String, + requestBranch: String, + commitIdFrom: String, + commitIdTo: String +) \ No newline at end of file diff --git a/src/main/scala/model/Repository.scala b/src/main/scala/model/Repository.scala index fcfb336..215b670 100644 --- a/src/main/scala/model/Repository.scala +++ b/src/main/scala/model/Repository.scala @@ -9,7 +9,11 @@ def registeredDate = column[java.util.Date]("REGISTERED_DATE") def updatedDate = column[java.util.Date]("UPDATED_DATE") def lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE") - def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate <> (Repository, Repository.unapply _) + def originUserName = column[String]("ORIGIN_USER_NAME") + def originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") + def parentUserName = column[String]("PARENT_USER_NAME") + def parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") + def * = userName ~ repositoryName ~ isPrivate ~ description.? ~ defaultBranch ~ registeredDate ~ updatedDate ~ lastActivityDate ~ originUserName.? ~ originRepositoryName.? ~ parentUserName.? ~ parentRepositoryName.? <> (Repository, Repository.unapply _) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) } @@ -22,5 +26,9 @@ defaultBranch: String, registeredDate: java.util.Date, updatedDate: java.util.Date, - lastActivityDate: java.util.Date + lastActivityDate: java.util.Date, + originUserName: Option[String], + originRepositoryName: Option[String], + parentUserName: Option[String], + parentRepositoryName: Option[String] ) diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala index 0b7e261..389be4a 100644 --- a/src/main/scala/service/ActivityService.scala +++ b/src/main/scala/service/ActivityService.scala @@ -102,7 +102,28 @@ s"[user:${activityUserName}] created branch [tag:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", None, currentDate) - + + def recordForkActivity(userName: String, repositoryName: String, activityUserName: String) = + Activities.autoInc insert(userName, repositoryName, activityUserName, + "fork", + s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${activityUserName}/${repositoryName}]", + None, + currentDate) + + def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String): Unit = + Activities.autoInc insert(userName, repositoryName, activityUserName, + "open_pullreq", + s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String): Unit = + Activities.autoInc insert(userName, repositoryName, activityUserName, + "merge_pullreq", + s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(message), + currentDate) + def insertCommitId(userName: String, repositoryName: String, commitId: String) = { CommitLog insert (userName, repositoryName, commitId) } diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 543c795..46706d0 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -44,14 +44,16 @@ * * @param condition the search condition * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param repos Tuple of the repository owner and the repository name * @return the count of the search result */ - def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], repos: (String, String)*): Int = { + def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + repos: (String, String)*): Int = { // TODO It must be _.length instead of map (_.issueId) list).length. // But it does not work on Slick 1.0.1 (worked on Slick 1.0.0). // https://github.com/slick/slick/issues/170 - (searchIssueQuery(repos, condition, filterUser) map (_.issueId) list).length + (searchIssueQuery(repos, condition, filterUser, onlyPullRequest) map (_.issueId) list).length } /** * Returns the Map which contains issue count for each labels. @@ -65,7 +67,7 @@ def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, filterUser: Map[String, String]): Map[String, Int] = { - searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser) + searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false) .innerJoin(IssueLabels).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } @@ -89,9 +91,9 @@ * @param repos Tuple of the repository owner and the repository name * @return list which contains issue count for each repository */ - def countIssueGroupByRepository(condition: IssueSearchCondition, filterUser: Map[String, String], - repos: (String, String)*): List[(String, String, Int)] = { - searchIssueQuery(repos, condition.copy(repo = None), filterUser) + def countIssueGroupByRepository( + condition: IssueSearchCondition, filterUser: Map[String, String], repos: (String, String)*): List[(String, String, Int)] = { + searchIssueQuery(repos, condition.copy(repo = None), filterUser, false) .groupBy { t => t.userName ~ t.repositoryName } @@ -107,16 +109,17 @@ * * @param condition the search condition * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) + * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param offset the offset for pagination * @param limit the limit for pagination * @param repos Tuple of the repository owner and the repository name * @return the search result (list of tuples which contain issue, labels and comment count) */ - def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], + def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*): List[(Issue, List[Label], Int)] = { // get issues and comment count and labels - searchIssueQuery(repos, condition, filterUser) + searchIssueQuery(repos, condition, filterUser, onlyPullRequest) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } @@ -156,7 +159,8 @@ /** * Assembles query for conditional issue searching. */ - private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, filterUser: Map[String, String]) = + private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, + filterUser: Map[String, String], onlyPullRequest: Boolean) = Query(Issues) filter { t1 => condition.repo .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } @@ -168,6 +172,7 @@ (t1.milestoneId isNull, condition.milestoneId == Some(None)) && (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && + (t1.pullRequest is true.bind, onlyPullRequest) && (IssueLabels filter { t2 => (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.labelId in @@ -179,7 +184,7 @@ } def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int]) = + assignedUserName: Option[String], milestoneId: Option[Int], isPullRequest: Boolean = false) = // next id number sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] .firstOption.filter { id => @@ -194,7 +199,8 @@ content, false, currentDate, - currentDate) + currentDate, + isPullRequest) // increment issue id IssueId diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala new file mode 100644 index 0000000..ab28675 --- /dev/null +++ b/src/main/scala/service/PullRequestService.scala @@ -0,0 +1,57 @@ +package service + +import scala.slick.driver.H2Driver.simple._ +import Database.threadLocalSession + +import model._ + +trait PullRequestService { self: IssuesService => + import PullRequestService._ + + def getPullRequest(owner: String, repository: String, issueId: Int): Option[(Issue, PullRequest)] = { + val issue = getIssue(owner, repository, issueId.toString) + if(issue.isDefined){ + Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).firstOption match { + case Some(pullreq) => Some((issue.get, pullreq)) + case None => None + } + } else None + } + + def getPullRequestCount(closed: Boolean, repository: Option[(String, String)]): List[PullRequestCount] = + Query(PullRequests) + .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } + .filter { case (t1, t2) => + (t2.closed is closed.bind) && + (t1.userName is repository.get._1, repository.isDefined) && + (t1.repositoryName is repository.get._2, repository.isDefined) + } + .groupBy { case (t1, t2) => t2.openedUserName } + .map { case (userName, t) => userName ~ t.length } + .list + .map { x => PullRequestCount(x._1, x._2) } + .sortBy(_.count).reverse + + def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, + originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, + commitIdFrom: String, commitIdTo: String): Unit = + PullRequests insert (PullRequest( + originUserName, + originRepositoryName, + issueId, + originBranch, + requestUserName, + requestRepositoryName, + requestBranch, + commitIdFrom, + commitIdTo)) + +} + +object PullRequestService { + + val PullRequestLimit = 25 + + case class PullRequestCount(userName: String, count: Int) + +} \ No newline at end of file diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index c0e1d0c..b03a0b5 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -15,19 +15,27 @@ * @param userName the user name of the repository owner * @param description the repository description * @param isPrivate the repository type (private is true, otherwise false) + * @param originRepositoryName specify for the forked repository. (default is None) + * @param originUserName specify for the forked repository. (default is None) */ - def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean): Unit = { + def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, + originRepositoryName: Option[String] = None, originUserName: Option[String] = None, + parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None): Unit = { Repositories insert Repository( - userName = userName, - repositoryName = repositoryName, - isPrivate = isPrivate, - description = description, - defaultBranch = "master", - registeredDate = currentDate, - updatedDate = currentDate, - lastActivityDate = currentDate) - + userName = userName, + repositoryName = repositoryName, + isPrivate = isPrivate, + description = description, + defaultBranch = "master", + registeredDate = currentDate, + updatedDate = currentDate, + lastActivityDate = currentDate, + originUserName = originUserName, + originRepositoryName = originRepositoryName, + parentUserName = parentUserName, + parentRepositoryName = parentRepositoryName) + IssueId insert (userName, repositoryName, 0) } @@ -63,27 +71,35 @@ */ def getRepository(userName: String, repositoryName: String, baseUrl: String): Option[RepositoryInfo] = { (Query(Repositories) filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => - new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository) + new RepositoryInfo( + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), + repository, + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + )) } } - /** - * Returns the list of specified user's repositories. - * It contains own repositories and collaboration repositories. - */ def getUserRepositories(userName: String, baseUrl: String): List[RepositoryInfo] = { Query(Repositories).filter { t1 => (t1.userName is userName.bind) || (Query(Collaborators).filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) }.sortBy(_.lastActivityDate desc).list.map{ repository => - new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository) + new RepositoryInfo( + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), + repository, + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + )) } } /** * Returns the list of visible repositories for the specified user. * If repositoryUserName is given then filters results by repository owner. - * + * * @param loginAccount the logged in account * @param baseUrl the base url of this application * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) @@ -103,7 +119,13 @@ }).filter { t => repositoryUserName.map { userName => t.userName is userName.bind } getOrElse ConstColumn.TRUE }.sortBy(_.lastActivityDate desc).list.map{ repository => - new RepositoryInfo(JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), repository) + new RepositoryInfo( + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), + repository, + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + )) } } @@ -170,17 +192,39 @@ } } + // TODO It must be _.length instead of map (_.issueId) list).length. + // But it does not work on Slick 1.0.1 (worked on Slick 1.0.0). + // https://github.com/slick/slick/issues/170 + private def getForkedCount(userName: String, repositoryName: String): Int = + Query(Repositories).filter { t => + (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) + }.list.length + + + def getForkedRepositoryTree(userName: String, repositoryName: String): RepositoryTreeNode = { + RepositoryTreeNode(userName, repositoryName, + Query(Repositories).filter { t => + (t.parentUserName is userName.bind) && (t.parentRepositoryName is repositoryName.bind) + }.map { t => + t.userName ~ t.repositoryName + }.list.map { case (userName, repositoryName) => + getForkedRepositoryTree(userName, repositoryName) + } + ) + } + } object RepositoryService { case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, - commitCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ + commitCount: Int, forkedCount: Int, branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ - def this(repo: JGitUtil.RepositoryInfo, model: Repository) = { - this(repo.owner, repo.name, repo.url, model, repo.commitCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = { + this(repo.owner, repo.name, repo.url, model, repo.commitCount, forkedCount, repo.branchList, repo.tags) } - } + case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) + } \ No newline at end of file diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index a9be43d..49f30aa 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -4,10 +4,7 @@ import java.util.Date import org.eclipse.jgit.api.Git import org.apache.commons.io.FileUtils -import util.JGitUtil.DiffInfo -import util.{Directory, JGitUtil} -import org.eclipse.jgit.treewalk.CanonicalTreeParser -import java.util.concurrent.ConcurrentHashMap +import util.{Directory, JGitUtil, LockUtil} object WikiService { @@ -31,40 +28,13 @@ */ case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) - /** - * lock objects - */ - private val locks = new ConcurrentHashMap[String, AnyRef]() - - /** - * Returns the lock object for the specified repository. - */ - private def getLockObject(owner: String, repository: String): AnyRef = synchronized { - val key = owner + "/" + repository - if(!locks.containsKey(key)){ - locks.put(key, new AnyRef()) - } - locks.get(key) - } - - /** - * Synchronizes a given function which modifies the working copy of the wiki repository. - * - * @param owner the repository owner - * @param repository the repository name - * @param f the function which modifies the working copy of the wiki repository - * @tparam T the return type of the given function - * @return the result of the given function - */ - def lock[T](owner: String, repository: String)(f: => T): T = getLockObject(owner, repository).synchronized(f) - } trait WikiService { import WikiService._ def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = { - lock(owner, repository){ + LockUtil.lock(s"${owner}/${repository}/wiki"){ val dir = Directory.getWikiRepositoryDir(owner, repository) if(!dir.exists){ try { @@ -126,7 +96,7 @@ def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, content: String, committer: model.Account, message: String): Unit = { - lock(owner, repository){ + LockUtil.lock(s"${owner}/${repository}/wiki"){ // clone working copy val workDir = Directory.getWikiWorkDir(owner, repository) cloneOrPullWorkingCopy(workDir, owner, repository) @@ -162,8 +132,9 @@ /** * Delete the wiki page. */ - def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, message: String): Unit = { - lock(owner, repository){ + def deleteWikiPage(owner: String, repository: String, pageName: String, + committer: String, mailAddress: String, message: String): Unit = { + LockUtil.lock(s"${owner}/${repository}/wiki"){ // clone working copy val workDir = Directory.getWikiWorkDir(owner, repository) cloneOrPullWorkingCopy(workDir, owner, repository) @@ -175,34 +146,12 @@ git.rm.addFilepattern(pageName + ".md").call // commit and push - // TODO committer's mail address - git.commit.setAuthor(committer, committer + "@devnull").setMessage(message).call + git.commit.setAuthor(committer, mailAddress).setMessage(message).call git.push.call } } } - /** - * Returns differences between specified commits. - */ - def getWikiDiffs(git: Git, commitId1: String, commitId2: String): List[DiffInfo] = { - // get diff between specified commit and its previous commit - val reader = git.getRepository.newObjectReader - - val oldTreeIter = new CanonicalTreeParser - oldTreeIter.reset(reader, git.getRepository.resolve(commitId1 + "^{tree}")) - - val newTreeIter = new CanonicalTreeParser - newTreeIter.reset(reader, git.getRepository.resolve(commitId2 + "^{tree}")) - - import scala.collection.JavaConverters._ - git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContent(git, diff.getOldId.toObjectId, false).map(new String(_, "UTF-8")), - JGitUtil.getContent(git, diff.getNewId.toObjectId, false).map(new String(_, "UTF-8"))) - }.toList - } - private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { if(!workDir.exists){ val git = diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 601af9b..51f8c2c 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -49,6 +49,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 5), Version(1, 4), new Version(1, 3){ override def update(conn: Connection): Unit = { diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 0526bfe..b647d53 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -15,6 +15,7 @@ import org.eclipse.jgit.errors.MissingObjectException import java.util.Date import org.eclipse.jgit.api.errors.NoHeadException +import service.RepositoryService /** * Provides complex JGit operations. @@ -155,12 +156,7 @@ withGit(getRepositoryDir(owner, repository)){ git => try { // get commit count - val i = git.log.all.call.iterator - var commitCount = 0 - while(i.hasNext && commitCount <= 1000){ - i.next - commitCount = commitCount + 1 - } + val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(1000).sum RepositoryInfo( owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", @@ -297,6 +293,32 @@ Right(commits) } } + + def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false) + (endCondition: RevCommit => Boolean): List[CommitInfo] = { + @scala.annotation.tailrec + def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] = + i.hasNext match { + case true => { + val revCommit = i.next + if(endCondition(revCommit)){ + if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs + } else { + getCommitLog(i, logs :+ new CommitInfo(revCommit)) + } + } + case false => logs + } + + val revWalk = new RevWalk(git.getRepository) + revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin))) + + val commits = getCommitLog(revWalk.iterator, Nil) + revWalk.release + + commits.reverse + } + /** * Returns the commit list between two revisions. @@ -306,30 +328,9 @@ * @param to the to revision * @return the commit list */ - def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = { - @scala.annotation.tailrec - def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] = - i.hasNext match { - case true => { - val revCommit = i.next - if(revCommit.name == from){ - logs - } else { - getCommitLog(i, logs :+ new CommitInfo(revCommit)) - } - } - case false => logs - } - - val revWalk = new RevWalk(git.getRepository) - revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(to))) - - val commits = getCommitLog(revWalk.iterator, Nil) - revWalk.release - - commits.reverse - } - + // TODO swap parameters 'from' and 'to'!? + def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = + getCommitLogs(git, to)(_.getName == from) /** * Returns the latest RevCommit of the specified path. @@ -389,7 +390,7 @@ case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next) case _ => logs } - + val revWalk = new RevWalk(git.getRepository) revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) @@ -401,26 +402,8 @@ if(commits.length >= 2){ // not initial commit val oldCommit = commits(1) - - // get diff between specified commit and its previous commit - val reader = git.getRepository.newObjectReader - - val oldTreeIter = new CanonicalTreeParser - oldTreeIter.reset(reader, git.getRepository.resolve(oldCommit.name + "^{tree}")) - - val newTreeIter = new CanonicalTreeParser - newTreeIter.reset(reader, git.getRepository.resolve(id + "^{tree}")) - - import scala.collection.JavaConverters._ - git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => - if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){ - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) - } else { - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")), - JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8"))) - } - }.toList + getDiffs(git, oldCommit.getName, id, fetchContent) + } else { // initial commit val walk = new TreeWalk(git.getRepository) @@ -439,6 +422,27 @@ } } + def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = { + val reader = git.getRepository.newObjectReader + val oldTreeIter = new CanonicalTreeParser + oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) + + val newTreeIter = new CanonicalTreeParser + newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) + + import scala.collection.JavaConverters._ + git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => + if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){ + DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) + } else { + DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, + JGitUtil.getContent(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")), + JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8"))) + } + }.toList + } + + /** * Returns the list of branch names of the specified commit. */ @@ -487,6 +491,15 @@ } } + def cloneRepository(from: java.io.File, to: java.io.File): Unit = { + val git = Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call + try { + setReceivePack(git.getRepository) + } finally { + git.getRepository.close + } + } + def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = { @@ -495,4 +508,14 @@ config.save } + def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo, + revstr: String = ""): Option[(ObjectId, String)] = { + Seq( + if(revstr.isEmpty) repository.repository.defaultBranch else revstr, + repository.branchList.head + ).map { rev => + (git.getRepository.resolve(rev), rev) + }.find(_._1 != null) + } + } diff --git a/src/main/scala/util/LockUtil.scala b/src/main/scala/util/LockUtil.scala new file mode 100644 index 0000000..3b6c796 --- /dev/null +++ b/src/main/scala/util/LockUtil.scala @@ -0,0 +1,36 @@ +package util + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.{ReentrantLock, Lock} + +object LockUtil { + + /** + * lock objects + */ + private val locks = new ConcurrentHashMap[String, Lock]() + + /** + * Returns the lock object for the specified repository. + */ + private def getLockObject(key: String): Lock = synchronized { + if(!locks.containsKey(key)){ + locks.put(key, new ReentrantLock()) + } + locks.get(key) + } + + /** + * Synchronizes a given function which modifies the working copy of the wiki repository. + */ + def lock[T](key: String)(f: => T): T = { + val lock = getLockObject(key) + try { + lock.lock() + f + } finally { + lock.unlock() + } + } + +} diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 2d9c0e0..6613a5f 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -51,9 +51,17 @@ def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html = Html(convertRefsLinks(value, repository)) + def cut(value: String, length: Int): String = + if(value.length > length){ + value.substring(0, length) + "..." + } else { + value + } + def activityMessage(message: String)(implicit context: app.Context): Html = Html(message .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") + .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""$$1/$$2""") .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", s"""$$3""") .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , s"""$$3""") diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html index de47a3d..97045d9 100644 --- a/src/main/twirl/account/repositories.scala.html +++ b/src/main/twirl/account/repositories.scala.html @@ -15,6 +15,9 @@ } + @if(repository.repository.originUserName.isDefined){ +
+ } @if(repository.repository.description.isDefined){Issues | ++ Pull Requests + | Wiki | @@ -25,11 +41,20 @@ }
---|