diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index 04455ea..7c2a8f4 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -2,7 +2,7 @@ import gitbucket.core.account.html import gitbucket.core.helper -import gitbucket.core.model.{AccountWebHook, GroupMember, RepositoryWebHook, RepositoryWebHookEvent, Role, WebHook, WebHookContentType} +import gitbucket.core.model.{AccountWebHook, GroupMember, RepositoryWebHook, Role, WebHook, WebHookContentType} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.service._ import gitbucket.core.service.WebHookService._ @@ -12,7 +12,6 @@ import gitbucket.core.util.Implicits._ import gitbucket.core.util.StringUtil._ import gitbucket.core.util._ -import org.apache.commons.io.FileUtils import org.scalatra.i18n.Messages import org.scalatra.BadRequest import org.scalatra.forms._ @@ -87,15 +86,16 @@ "clearImage" -> trim(label("Clear image" ,boolean())) )(EditGroupForm.apply) - case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, initOption: String, sourceUrl: Option[String]) case class ForkRepositoryForm(owner: String, name: String) val newRepositoryForm = mapping( - "owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))), - "name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))), - "description" -> trim(label("Description" , optional(text()))), - "isPrivate" -> trim(label("Repository Type", boolean())), - "createReadme" -> trim(label("Create README" , boolean())) + "owner" -> trim(label("Owner", text(required, maxlength(100), identifier, existsAccount))), + "name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))), + "description" -> trim(label("Description", optional(text()))), + "isPrivate" -> trim(label("Repository Type", boolean())), + "initOption" -> trim(label("Initialize option", text(required))), + "sourceUrl" -> trim(label("Source URL", optionalRequired(_.value("initOption") == "COPY", text()))) )(RepositoryCreationForm.apply) val forkRepositoryForm = mapping( @@ -461,7 +461,6 @@ get("/:groupName/_editgroup")(managersOnly { defining(params("groupName")){ groupName => - // TODO Don't use Option.get getAccountByUserName(groupName, true).map { account => html.editgroup(account, getGroupMembers(groupName), flash.get("info")) } getOrElse NotFound() @@ -528,11 +527,7 @@ post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}"){ if(getRepository(form.owner, form.name).isEmpty){ - // Create the repository - createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme) - - // Call hooks - PluginRegistry().getRepositoryHooks.foreach(_.created(form.owner, form.name)) + createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.initOption, form.sourceUrl) } } @@ -566,66 +561,15 @@ val loginUserName = loginAccount.userName val accountName = form.accountName - LockUtil.lock(s"${accountName}/${repository.name}"){ - if(getRepository(accountName, repository.name).isDefined || - (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ - // redirect to the repository if repository already exists - redirect(s"/${accountName}/${repository.name}") - } else { - // Insert to the database at first - val originUserName = repository.repository.originUserName.getOrElse(repository.owner) - val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - - insertRepository( - repositoryName = repository.name, - userName = accountName, - description = repository.repository.description, - isPrivate = repository.repository.isPrivate, - originRepositoryName = Some(originRepositoryName), - originUserName = Some(originUserName), - parentRepositoryName = Some(repository.name), - parentUserName = Some(repository.owner) - ) - - // Set default collaborators for the private fork - if(repository.repository.isPrivate){ - // Copy collaborators from the source repository - getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) => - addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role) - } - // Register an owner of the source repository as a collaborator - addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name) - } - - // Insert default labels - insertDefaultLabels(accountName, repository.name) - // Insert default priorities - insertDefaultPriorities(accountName, repository.name) - - // clone repository actually - JGitUtil.cloneRepository( - getRepositoryDir(repository.owner, repository.name), - FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name))) - - // Create Wiki repository - JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name), - FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name))) - - // Copy LFS files - val lfsDir = getLfsDir(repository.owner, repository.name) - if(lfsDir.exists){ - FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name))) - } - - // Record activity - recordForkActivity(repository.owner, repository.name, loginUserName, accountName) - - // Call hooks - PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name)) - - // redirect to the repository - redirect(s"/${accountName}/${repository.name}") - } + if (getRepository(accountName, repository.name).isDefined || + (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))) { + // redirect to the repository if repository already exists + redirect(s"/${accountName}/${repository.name}") + } else { + // fork repository asynchronously + forkRepository(accountName, repository, loginUserName) + // redirect to the repository + redirect(s"/${accountName}/${repository.name}") } } else BadRequest() }) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 1199921..d7a6e75 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -16,6 +16,8 @@ import org.scalatra.{Created, NoContent, UnprocessableEntity} import scala.collection.JavaConverters._ +import scala.concurrent.Await +import scala.concurrent.duration.Duration class ApiController extends ApiControllerBase with RepositoryService @@ -249,7 +251,8 @@ } yield { LockUtil.lock(s"${owner}/${data.name}") { if(getRepository(owner, data.name).isEmpty){ - createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init) + val f = createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init) + Await.result(f, Duration.Inf) val repository = getRepository(owner, data.name).get JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get))) } else { @@ -273,7 +276,8 @@ } yield { LockUtil.lock(s"${groupName}/${data.name}") { if(getRepository(groupName, data.name).isEmpty){ - createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init) + val f = createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init) + Await.result(f, Duration.Inf) val repository = getRepository(groupName, data.name).get JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get))) } else { diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 311dbac..8fb6370 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -25,6 +25,7 @@ import org.eclipse.jgit.errors.MissingObjectException import org.eclipse.jgit.lib._ import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} +import org.json4s.jackson.Serialization import org.scalatra._ import org.scalatra.i18n.Messages @@ -148,14 +149,31 @@ * Displays the file list of the repository root and the default branch. */ get("/:owner/:repository") { - params.get("go-get") match { - case Some("1") => defining(request.paths){ paths => - getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound() + val owner = params("owner") + val repository = params("repository") + + if (RepositoryCreationService.isCreating(owner, repository)) { + gitbucket.core.repo.html.creating(owner, repository) + } else { + params.get("go-get") match { + case Some("1") => defining(request.paths) { paths => + getRepository(owner, repository).map(gitbucket.core.html.goget(_)) getOrElse NotFound() + } + case _ => referrersOnly(fileList(_)) } - case _ => referrersOnly(fileList(_)) } } + ajaxGet("/:owner/:repository/creating") { + val owner = params("owner") + val repository = params("repository") + contentType = formats("json") + Serialization.write(Map( + "creating" -> RepositoryCreationService.isCreating(owner, repository), + "error" -> RepositoryCreationService.getCreationError(owner, repository) + )) + } + /** * Displays the file list of the specified path and branch. */ @@ -403,7 +421,7 @@ contentType = formats("json") using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name - Map( + Serialization.write(Map( "root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}", "id" -> id, "path" -> path, @@ -418,8 +436,9 @@ "prevPath" -> blame.prevPath, "commited" -> blame.commitTime.getTime, "message" -> blame.message, - "lines" -> blame.lines) - }) + "lines" -> blame.lines + ) + })) } }) diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala index 2aa4196..7b9bffd 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -1,71 +1,206 @@ package gitbucket.core.service +import java.nio.file.Files +import java.util.concurrent.ConcurrentHashMap + import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.Directory._ -import gitbucket.core.util.JGitUtil -import gitbucket.core.model.Account +import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil} +import gitbucket.core.model.{Account, Role} +import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.service.RepositoryService.RepositoryInfo +import gitbucket.core.servlet.Database +import org.apache.commons.io.FileUtils import org.eclipse.jgit.api.Git import org.eclipse.jgit.dircache.DirCache -import org.eclipse.jgit.lib.{FileMode, Constants} +import org.eclipse.jgit.lib.{Constants, FileMode} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +object RepositoryCreationService { + + private val Creating = new ConcurrentHashMap[String, Option[String]]() + + def isCreating(owner: String, repository: String): Boolean = { + Option(Creating.get(s"${owner}/${repository}")).map(_.isEmpty).getOrElse(false) + } + + def startCreation(owner: String, repository: String): Unit = { + Creating.put(s"${owner}/${repository}", None) + } + + def endCreation(owner: String, repository: String, error: Option[String]): Unit = { + error match { + case None => Creating.remove(s"${owner}/${repository}") + case Some(error) => Creating.put(s"${owner}/${repository}", Some(error)) + } + } + + def getCreationError(owner: String, repository: String): Option[String] = { + Option(Creating.remove(s"${owner}/${repository}")).getOrElse(None) + } + +} trait RepositoryCreationService { self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService with PrioritiesService => - def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) - (implicit s: Session) { - val ownerAccount = getAccountByUserName(owner).get - val loginUserName = loginAccount.userName + def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], + isPrivate: Boolean, createReadme: Boolean): Future[Unit] = { + createRepository(loginAccount, owner, name, description, isPrivate, if (createReadme) "README" else "EMPTY", None) + } - // Insert to the database at first - insertRepository(name, owner, description, isPrivate) + def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], + isPrivate: Boolean, initOption: String, sourceUrl: Option[String]): Future[Unit] = Future { + RepositoryCreationService.startCreation(owner, name) + try { + Database() withTransaction { implicit session => + val ownerAccount = getAccountByUserName(owner).get + val loginUserName = loginAccount.userName -// // Add collaborators for group repository -// if(ownerAccount.isGroupAccount){ -// getGroupMembers(owner).foreach { member => -// addCollaborator(owner, name, member.userName) -// } -// } + val copyRepositoryDir = if (initOption == "COPY") { + sourceUrl.flatMap { url => + val dir = Files.createTempDirectory(s"gitbucket-${owner}-${name}").toFile + Git.cloneRepository().setBare(true).setURI(url).setDirectory(dir).setCloneAllBranches(true).call() + Some(dir) + } + } else None - // Insert default labels - insertDefaultLabels(owner, name) - // Insert default priorities - insertDefaultPriorities(owner, name) + // Insert to the database at first + insertRepository(name, owner, description, isPrivate) - // Create the actual repository - val gitdir = getRepositoryDir(owner, name) - JGitUtil.initRepository(gitdir) + // // Add collaborators for group repository + // if(ownerAccount.isGroupAccount){ + // getGroupMembers(owner).foreach { member => + // addCollaborator(owner, name, member.userName) + // } + // } - if(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(description.nonEmpty){ - name + "\n" + - "===============\n" + - "\n" + - description.get - } else { - name + "\n" + - "===============\n" + // Insert default labels + insertDefaultLabels(owner, name) + + // Insert default priorities + insertDefaultPriorities(owner, name) + + // Create the actual repository + val gitdir = getRepositoryDir(owner, name) + JGitUtil.initRepository(gitdir) + + if (initOption == "README") { + 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 (description.nonEmpty) { + name + "\n" + + "===============\n" + + "\n" + + description.get + } else { + 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), + Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } } - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() + copyRepositoryDir.foreach { dir => + try { + using(Git.open(dir)) { git => + git.push().setRemote(gitdir.toURI.toString).setPushAll().setPushTags().call() + } + } finally { + FileUtils.deleteQuietly(dir) + } + } - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + // Create Wiki repository + createWikiRepository(loginAccount, owner, name) + + // Record activity + recordCreateRepositoryActivity(owner, name, loginUserName) + + // Call hooks + PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name)) } + + RepositoryCreationService.endCreation(owner, name, None) + + } catch { + case ex: Exception => RepositoryCreationService.endCreation(owner, name, Some(ex.toString)) } + } - // Create Wiki repository - createWikiRepository(loginAccount, owner, name) + def forkRepository(accountName: String, repository: RepositoryInfo, loginUserName: String): Future[Unit] = Future { + RepositoryCreationService.startCreation(accountName, repository.name) + try { + LockUtil.lock(s"${accountName}/${repository.name}") { + Database() withTransaction { implicit session => + val originUserName = repository.repository.originUserName.getOrElse(repository.owner) + val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - // Record activity - recordCreateRepositoryActivity(owner, name, loginUserName) + insertRepository( + repositoryName = repository.name, + userName = accountName, + description = repository.repository.description, + isPrivate = repository.repository.isPrivate, + originRepositoryName = Some(originRepositoryName), + originUserName = Some(originUserName), + parentRepositoryName = Some(repository.name), + parentUserName = Some(repository.owner) + ) + + // Set default collaborators for the private fork + if (repository.repository.isPrivate) { + // Copy collaborators from the source repository + getCollaborators(repository.owner, repository.name).foreach { case (collaborator, _) => + addCollaborator(accountName, repository.name, collaborator.collaboratorName, collaborator.role) + } + // Register an owner of the source repository as a collaborator + addCollaborator(accountName, repository.name, repository.owner, Role.ADMIN.name) + } + + // Insert default labels + insertDefaultLabels(accountName, repository.name) + // Insert default priorities + insertDefaultPriorities(accountName, repository.name) + + // clone repository actually + JGitUtil.cloneRepository( + getRepositoryDir(repository.owner, repository.name), + FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name))) + + // Create Wiki repository + JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name), + FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name))) + + // Copy LFS files + val lfsDir = getLfsDir(repository.owner, repository.name) + if (lfsDir.exists) { + FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name))) + } + + // Record activity + recordForkActivity(repository.owner, repository.name, loginUserName, accountName) + + // Call hooks + PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name)) + + RepositoryCreationService.endCreation(accountName, repository.name, None) + } + } + } catch { + case ex: Exception => RepositoryCreationService.endCreation(accountName, repository.name, Some(ex.toString)) + } } def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = { diff --git a/src/main/twirl/gitbucket/core/account/newrepo.scala.html b/src/main/twirl/gitbucket/core/account/newrepo.scala.html index 6f6397b..32ae645 100644 --- a/src/main/twirl/gitbucket/core/account/newrepo.scala.html +++ b/src/main/twirl/gitbucket/core/account/newrepo.scala.html @@ -39,7 +39,7 @@
- +
-
+ +
@@ -83,4 +99,8 @@ $('#owner-dropdown span.strong').html($(this).find('span').html()); }); + +$('input[name=initOption]').click(function () { + $('#sourceUrl').prop('disabled', $('input[name=initOption]:checked').val() != 'COPY'); +}); diff --git a/src/main/twirl/gitbucket/core/repo/creating.scala.html b/src/main/twirl/gitbucket/core/repo/creating.scala.html new file mode 100644 index 0000000..715a5b5 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/creating.scala.html @@ -0,0 +1,39 @@ +@(owner: String, repository: String)(implicit context: gitbucket.core.controller.Context) +@gitbucket.core.html.main("Creating...") { +
+
+ +
+

Creating repository...

+ +
+ + +
+
+} +