diff --git a/src/main/resources/update/1_12.sql b/src/main/resources/update/1_12.sql new file mode 100644 index 0000000..0030169 --- /dev/null +++ b/src/main/resources/update/1_12.sql @@ -0,0 +1 @@ +ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index de3c40d..21c213d 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -20,7 +20,6 @@ context.mount(new DashboardController, "/*") context.mount(new UserManagementController, "/*") context.mount(new SystemSettingsController, "/*") - context.mount(new CreateRepositoryController, "/*") context.mount(new AccountController, "/*") context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index df24aad..d35379b 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -1,17 +1,25 @@ package app import service._ -import util.{FileUtil, OneselfAuthenticator} +import util._ import util.StringUtil._ import util.Directory._ +import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.{FileMode, Constants} +import org.eclipse.jgit.dircache.DirCache +import model.GroupMember class AccountController extends AccountControllerBase - with AccountService with RepositoryService with ActivityService with OneselfAuthenticator + with AccountService with RepositoryService with ActivityService with WikiService with LabelsService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator trait AccountControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with ActivityService with OneselfAuthenticator => + self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, url: Option[String], fileId: Option[String]) @@ -37,6 +45,40 @@ "clearImage" -> trim(label("Clear image" , boolean())) )(AccountEditForm.apply) + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())) + )(EditGroupForm.apply) + + case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + case class ForkRepositoryForm(owner: String, name: String) + + val newRepositoryForm = mapping( + "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), + "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))), + "description" -> trim(label("Description" , optional(text()))), + "isPrivate" -> trim(label("Repository Type", boolean())), + "createReadme" -> trim(label("Create README" , boolean())) + )(RepositoryCreationForm.apply) + + val forkRepositoryForm = mapping( + "owner" -> trim(label("Repository owner", text(required))), + "name" -> trim(label("Repository name", text(required))) + )(ForkRepositoryForm.apply) + /** * Displays user information. */ @@ -51,14 +93,20 @@ getActivitiesByUser(userName, true)) // Members - case "members" if(account.isGroupAccount) => - _root_.account.html.members(account, getGroupMembers(account.userName)) + case "members" if(account.isGroupAccount) => { + val members = getGroupMembers(account.userName) + _root_.account.html.members(account, members.map(_.userName), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } // Repositories - case _ => + case _ => { + val members = getGroupMembers(account.userName) _root_.account.html.repositories(account, if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getVisibleRepositories(context.loginAccount, baseUrl, Some(userName))) + getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } } } getOrElse NotFound } @@ -134,4 +182,228 @@ } else NotFound } + get("/groups/new")(usersOnly { + account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true))) + }) + + post("/groups/new", newGroupForm)(usersOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect(s"/${form.groupName}") + }) + + get("/:groupName/_editgroup")(managersOnly { + defining(params("groupName")){ groupName => + account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + get("/:groupName/_deletegroup")(managersOnly { + defining(params("groupName")){ groupName => + // Remove from GROUP_MEMBER + updateGroupMembers(groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } + redirect("/") + }) + + post("/:groupName/_editgroup", editGroupForm)(managersOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, false) + + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect(s"/${form.groupName}") + + } getOrElse NotFound + } + }) + + /** + * Show the new repository form. + */ + get("/new")(usersOnly { + account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName)) + }) + + /** + * Create new repository. + */ + post("/new", newRepositoryForm)(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) + + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(form.owner).foreach { member => + addCollaborator(form.owner, form.name, member.userName) + } + } + + // Insert default labels + insertDefaultLabels(form.owner, form.name) + + // Create the actual repository + val gitdir = getRepositoryDir(form.owner, form.name) + JGitUtil.initRepository(gitdir) + + if(form.createReadme){ + using(Git.open(gitdir)){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + val content = if(form.description.nonEmpty){ + form.name + "\n" + + "===============\n" + + "\n" + + form.description.get + } else { + form.name + "\n" + + "===============\n" + } + + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } + } + + // 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}") + } + }) + + get("/:owner/:repository/fork")(readableUsersOnly { repository => + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ + if(repository.owner == loginUserName){ + // redirect to the repository + redirect(s"/${repository.owner}/${repository.name}") + } else { + getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => + // redirect to the repository + redirect(s"/${owner}/${name}") + } getOrElse { + // 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 + using(Git.open(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"/${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(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None + } + + private def uniqueRepository: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } } diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala deleted file mode 100644 index 16cd05c..0000000 --- a/src/main/scala/app/CreateRepositoryController.scala +++ /dev/null @@ -1,199 +0,0 @@ -package app - -import util.Directory._ -import util.ControlUtil._ -import util._ -import service._ -import org.eclipse.jgit.api.Git -import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.lib.{FileMode, Constants} -import org.eclipse.jgit.dircache.DirCache -import org.scalatra.i18n.Messages - -class CreateRepositoryController extends CreateRepositoryControllerBase - with RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator with ReadableUsersAuthenticator - -/** - * Creates new repository. - */ -trait CreateRepositoryControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WikiService with LabelsService with ActivityService - with UsersAuthenticator with ReadableUsersAuthenticator => - - case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) - - 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()))), - "isPrivate" -> trim(label("Repository Type", boolean())), - "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. - */ - get("/new")(usersOnly { - html.newrepo(getGroupsByUserName(context.loginAccount.get.userName)) - }) - - /** - * Create new repository. - */ - 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) - - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(form.owner).foreach { userName => - addCollaborator(form.owner, form.name, userName) - } - } - - // Insert default labels - insertDefaultLabels(form.owner, form.name) - - // Create the actual repository - val gitdir = getRepositoryDir(form.owner, form.name) - JGitUtil.initRepository(gitdir) - - if(form.createReadme){ - using(Git.open(gitdir)){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - val content = if(form.description.nonEmpty){ - form.name + "\n" + - "===============\n" + - "\n" + - form.description.get - } else { - form.name + "\n" + - "===============\n" - } - - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - loginAccount.fullName, loginAccount.mailAddress, "Initial commit") - } - } - - // 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}") - } - }) - - get("/:owner/:repository/fork")(readableUsersOnly { repository => - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - LockUtil.lock(s"${loginUserName}/${repository.name}/create"){ - if(repository.owner == loginUserName){ - // redirect to the repository - redirect(s"/${repository.owner}/${repository.name}") - } else { - getForkedRepositories(repository.owner, repository.name).find(_._1 == loginUserName).map { case (owner, name) => - // redirect to the repository - redirect(s"/${owner}/${name}") - } getOrElse { - // 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 - using(Git.open(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"/${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(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None - } - - /** - * Duplicate check for the repository name. - */ - private def unique: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - -} diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index be47ee9..47c0614 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -105,9 +105,9 @@ } getOrElse NotFound }) - get("/:owner/:repository/pull/:id/delete/:branchName")(collaboratorsOnly { repository => + get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => params("id").toIntOpt.map { issueId => - val branchName = params("branchName") + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 6eb9296..d68dc53 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -95,7 +95,7 @@ } map { objectId => if(raw){ // Download - defining(JGitUtil.getContent(git, objectId, false).get){ bytes => + defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => contentType = FileUtil.getContentType(path, bytes) bytes } @@ -103,7 +103,7 @@ // Viewer val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" - val bytes = if(viewer == "other") JGitUtil.getContent(git, objectId, false) else None + val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None val content = if(viewer == "other"){ if(bytes.isDefined && FileUtil.isText(bytes.get)){ @@ -159,8 +159,8 @@ /** * Deletes branch. */ - get("/:owner/:repository/delete/:branchName")(collaboratorsOnly { repository => - val branchName = params("branchName") + get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -208,7 +208,7 @@ while(walk.next){ val name = walk.getPathString val mode = walk.getFileMode(0) - if(mode != FileMode.TREE){ + if(mode == FileMode.REGULAR_FILE){ walk.getObjectId(objectId, 0) val entry = new ZipEntry(name) val loader = reader.open(objectId) @@ -277,7 +277,8 @@ val readme = files.find { file => readmeFiles.contains(file.name.toLowerCase) }.map { file => - file -> StringUtil.convertFromByteArray(JGitUtil.getContent(Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) + file -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( + Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) } repo.html.files(revision, repository, diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala index 50ffb3c..dc3543c 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -5,6 +5,7 @@ import util.StringUtil._ import util.ControlUtil._ import jp.sf.amateras.scalatra.forms._ +import org.scalatra.i18n.Messages import org.apache.commons.io.FileUtils import util.Directory._ @@ -23,10 +24,10 @@ fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String]) + members: String) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - memberNames: Option[String], clearImage: Boolean, isRemoved: Boolean) + members: String, clearImage: Boolean, isRemoved: Boolean) val newUserForm = mapping( "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), @@ -51,28 +52,28 @@ )(EditUserForm.apply) val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) )(NewGroupForm.apply) val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "memberNames" -> trim(label("Member Names" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) )(EditGroupForm.apply) get("/admin/users")(adminOnly { val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - - val members = users.collect { case account if(account.isGroupAccount) => - account.userName -> getGroupMembers(account.userName) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.isGroupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) }.toMap + admin.users.html.list(users, members, includeRemoved) }) @@ -127,7 +128,11 @@ post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.memberNames.map(_.split(",").toList).getOrElse(Nil)) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) updateImage(form.groupName, form.fileId, false) redirect("/admin/users") }) @@ -139,7 +144,11 @@ }) post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.memberNames.map(_.split(",").toList).getOrElse(Nil)){ case (groupName, memberNames) => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => getAccountByUserName(groupName, true).map { account => updateGroup(groupName, form.url, form.isRemoved) @@ -155,11 +164,11 @@ } } else { // Update GROUP_MEMBER - updateGroupMembers(form.groupName, memberNames) + updateGroupMembers(form.groupName, members) // Update COLLABORATOR for group repositories getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => removeCollaborators(form.groupName, repositoryName) - memberNames.foreach { userName => + members.foreach { case (userName, isManager) => addCollaborator(form.groupName, repositoryName, userName) } } @@ -172,8 +181,17 @@ } }) - post("/admin/users/_usercheck")(adminOnly { + // TODO Move to other generic controller? + post("/admin/users/_usercheck"){ getAccountByUserName(params("userName")).isDefined - }) + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } } diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala index 0bcd0af..a2a38f3 100644 --- a/src/main/scala/model/GroupMembers.scala +++ b/src/main/scala/model/GroupMembers.scala @@ -5,10 +5,12 @@ object GroupMembers extends Table[GroupMember]("GROUP_MEMBER") { def groupName = column[String]("GROUP_NAME", O PrimaryKey) def userName = column[String]("USER_NAME", O PrimaryKey) - def * = groupName ~ userName <> (GroupMember, GroupMember.unapply _) + def isManager = column[Boolean]("MANAGER") + def * = groupName ~ userName ~ isManager <> (GroupMember, GroupMember.unapply _) } case class GroupMember( groupName: String, - userName: String + userName: String, + isManager: Boolean ) \ No newline at end of file diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index d00c4cd..1b021ea 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -128,18 +128,17 @@ def updateGroup(groupName: String, url: Option[String], removed: Boolean): Unit = Accounts.filter(_.userName is groupName.bind).map(t => t.url.? ~ t.removed).update(url, removed) - def updateGroupMembers(groupName: String, members: List[String]): Unit = { + def updateGroupMembers(groupName: String, members: List[(String, Boolean)]): Unit = { Query(GroupMembers).filter(_.groupName is groupName.bind).delete - members.foreach { userName => - GroupMembers insert GroupMember (groupName, userName) + members.foreach { case (userName, isManager) => + GroupMembers insert GroupMember (groupName, userName, isManager) } } - def getGroupMembers(groupName: String): List[String] = + def getGroupMembers(groupName: String): List[GroupMember] = Query(GroupMembers) .filter(_.groupName is groupName.bind) .sortBy(_.userName) - .map(_.userName) .list def getGroupsByUserName(userName: String): List[String] = diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala index ac4f177..57265c8 100644 --- a/src/main/scala/service/RepositorySearchService.scala +++ b/src/main/scala/service/RepositorySearchService.scala @@ -63,8 +63,8 @@ val list = new ListBuffer[(String, String)] while (treeWalk.next()) { - if(treeWalk.getFileMode(0) != FileMode.TREE){ - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).foreach { bytes => + if(treeWalk.getFileMode(0) == FileMode.REGULAR_FILE){ + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes => if(FileUtil.isText(bytes)){ val text = StringUtil.convertFromByteArray(bytes) val lowerText = text.toLowerCase diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 74bd139..9038ea5 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -147,7 +147,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -162,7 +163,8 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } @@ -195,10 +197,18 @@ getForkedCount( repository.originUserName.getOrElse(repository.userName), repository.originRepositoryName.getOrElse(repository.repositoryName) - )) + ), + getRepositoryManagers(repository.userName)) } } + private def getRepositoryManagers(userName: String): Seq[String] = + if(getAccountByUserName(userName).exists(_.isGroupAccount)){ + getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } + } else { + Seq(userName) + } + /** * Updates the last activity date of the repository. */ @@ -280,19 +290,19 @@ case class RepositoryInfo(owner: String, name: String, url: String, repository: Repository, issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, - branchList: List[String], tags: List[util.JGitUtil.TagInfo]){ + branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){ /** * Creates instance with issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) /** * Creates instance without issue count and pull request count. */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int) = - this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags) + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) } case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 00fbabd..f4e3ac5 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -234,7 +234,7 @@ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } else { created = false - updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) + updated = JGitUtil.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) } } } @@ -268,35 +268,35 @@ */ def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, mailAddress: String, message: String): Unit = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") - var removed = false + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var removed = false - using(new RevWalk(git.getRepository)){ revWalk => - using(new TreeWalk(git.getRepository)){ treeWalk => - val index = treeWalk.addTree(revWalk.parseTree(headId)) - treeWalk.setRecursive(true) - while(treeWalk.next){ - val path = treeWalk.getPathString - val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) - if(path != pageName + ".md"){ - builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) - } else { - removed = true - } + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true } } + } - if(removed){ - builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) - } + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) } } } + } } } diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index bfa6992..830ca29 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -50,6 +50,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 12), Version(1, 11), Version(1, 10), Version(1, 9), diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index 9efefd3..00cd04f 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -8,7 +8,7 @@ import javax.servlet.ServletConfig import javax.servlet.ServletContext -import javax.servlet.http.HttpServletRequest +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import util.{StringUtil, Keys, JGitUtil, Directory} import util.ControlUtil._ import util.Implicits._ @@ -23,7 +23,7 @@ * This servlet provides only Git repository functionality. * Authentication is provided by [[servlet.BasicAuthenticationFilter]]. */ -class GitRepositoryServlet extends GitServlet { +class GitRepositoryServlet extends GitServlet with SystemSettingsService { private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) @@ -47,7 +47,19 @@ super.init(config) } - + + override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = { + val agent = req.getHeader("USER-AGENT") + if(agent == null || !agent.startsWith("git/")){ + // redirect for browsers + val paths = req.getRequestURI.split("/") + val baseUrl = loadSystemSettings().baseUrl.getOrElse(req.getServletContext.getContextPath) + res.sendRedirect(baseUrl + "/" + paths.dropRight(1).last + "/" + paths.last.replaceFirst("\\.git$", "")) + } else { + // response for git client + super.service(req, res) + } + } } class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { diff --git a/src/main/scala/util/Authenticator.scala b/src/main/scala/util/Authenticator.scala index c524713..f40af7b 100644 --- a/src/main/scala/util/Authenticator.scala +++ b/src/main/scala/util/Authenticator.scala @@ -29,7 +29,7 @@ /** * Allows only the repository owner and administrators. */ -trait OwnerAuthenticator { self: ControllerBase with RepositoryService => +trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -40,6 +40,9 @@ context.loginAccount match { case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists { member => + member.userName == x.userName && member.isManager == true + }) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -106,7 +109,7 @@ } /** - * Allows only the repository owner and administrators. + * Allows only the repository owner (or manager for group repository) and administrators. */ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } @@ -155,3 +158,24 @@ } } } + +/** + * Allows only the group managers. + */ +trait GroupManagerAuthenticator { self: ControllerBase with AccountService => + protected def managersOnly(action: => Any) = { authenticate(action) } + protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } + + private def authenticate(action: => Any) = { + { + defining(request.paths){ paths => + context.loginAccount match { + case Some(x) if(getGroupMembers(paths(0)).exists { member => + member.userName == x.userName && member.isManager + }) => action + case _ => Unauthorized() + } + } + } + } +} diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 57fd421..0e2cb40 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -11,17 +11,20 @@ import org.eclipse.jgit.treewalk._ import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.diff.DiffEntry.ChangeType -import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import java.util.Date import org.eclipse.jgit.api.errors.NoHeadException import service.RepositoryService import org.eclipse.jgit.dircache.DirCacheEntry +import org.slf4j.LoggerFactory /** * Provides complex JGit operations. */ object JGitUtil { + private val logger = LoggerFactory.getLogger(JGitUtil.getClass) + /** * The repository data. * @@ -45,9 +48,10 @@ * @param commitId the last commit id * @param committer the last committer name * @param mailAddress the committer's mail address + * @param linkUrl the url of submodule */ case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, - committer: String, mailAddress: String) + committer: String, mailAddress: String, linkUrl: Option[String]) /** * The commit data. @@ -105,6 +109,15 @@ case class TagInfo(name: String, time: Date, id: String) /** + * The submodule data + * + * @param name the module name + * @param path the path in the repository + * @param url the repository url of this module + */ + case class SubmoduleInfo(name: String, path: String, url: String) + + /** * Returns RevCommit from the commit or tag id. * * @param git the Git object @@ -162,7 +175,7 @@ * @return HTML of the file list */ def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { - val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String)] + val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(revision) @@ -195,22 +208,28 @@ }) } while (treeWalk.next()) { - list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString)) + // submodule + val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ + getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) + } else None + + list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) } } } val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) - list.map { case (objectId, fileMode, path, name) => + list.map { case (objectId, fileMode, path, name, linkUrl) => FileInfo( objectId, - fileMode == FileMode.TREE, + fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, name, commits(path).getCommitterIdent.getWhen, commits(path).getShortMessage, commits(path).getName, commits(path).getCommitterIdent.getName, - commits(path).getCommitterIdent.getEmailAddress) + commits(path).getCommitterIdent.getEmailAddress, + linkUrl) }.sortWith { (file1, file2) => (file1.isDirectory, file2.isDirectory) match { case (true , false) => true @@ -326,27 +345,6 @@ } /** - * Get object content of the given id as String from the Git repository. - * - * @param git the Git object - * @param id the object id - * @param large if false then returns None for the large file - * @return the object or None if object does not exist - */ - def getContent(git: Git, id: ObjectId, large: Boolean): Option[Array[Byte]] = try { - val loader = git.getRepository.getObjectDatabase.open(id) - if(large == false && FileUtil.isLarge(loader.getSize)){ - None - } else { - using(git.getRepository.getObjectDatabase){ db => - Some(db.open(id).getBytes) - } - } - } catch { - case e: MissingObjectException => None - } - - /** * Returns the tuple of diff of the given commit and the previous commit id. */ def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { @@ -377,7 +375,7 @@ DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) } else { DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, - JGitUtil.getContent(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) })) } (buffer.toList, None) @@ -400,8 +398,8 @@ 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(convertFromByteArray), - JGitUtil.getContent(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) + JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) } }.toList } @@ -494,4 +492,73 @@ newHeadId.getName } + /** + * Read submodule information from .gitmodules + */ + def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = { + val repository = git.getRepository + getContentFromPath(git, tree, ".gitmodules", true).map { bytes => + (try { + val config = new BlobBasedConfig(repository.getConfig(), bytes) + config.getSubsections("submodule").asScala.map { module => + val path = config.getString("submodule", module, "path") + val url = config.getString("submodule", module, "url") + SubmoduleInfo(module, path, url) + } + } catch { + case e: ConfigInvalidException => { + logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e) + Nil + } + }).toList + } getOrElse Nil + } + + /** + * Get object content of the given path as byte array from the Git repository. + * + * @param git the Git object + * @param revTree the rev tree + * @param path the path + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = { + @scala.annotation.tailrec + def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => getPathObjectId(path, walk) + case false => None + } + + using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.addTree(revTree) + treeWalk.setRecursive(true) + getPathObjectId(path, treeWalk) + } flatMap { objectId => + getContentFromId(git, objectId, fetchLargeFile) + } + } + + /** + * Get object content of the given object id as byte array from the Git repository. + * + * @param git the Git object + * @param id the object id + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { + val loader = git.getRepository.getObjectDatabase.open(id) + if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ + None + } else { + using(git.getRepository.getObjectDatabase){ db => + Some(db.open(id).getBytes) + } + } + } catch { + case e: MissingObjectException => None + } + } diff --git a/src/main/twirl/account/group.scala.html b/src/main/twirl/account/group.scala.html new file mode 100644 index 0000000..4826352 --- /dev/null +++ b/src/main/twirl/account/group.scala.html @@ -0,0 +1,140 @@ +@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(if(account.isEmpty) "Create group" else "Edit group"){ +