| // Copyright (C) 2008 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.server; |
| |
| import com.google.gerrit.client.admin.ProjectAdminService; |
| import com.google.gerrit.client.admin.ProjectDetail; |
| import com.google.gerrit.client.data.ProjectCache; |
| import com.google.gerrit.client.reviewdb.Account; |
| import com.google.gerrit.client.reviewdb.AccountGroup; |
| import com.google.gerrit.client.reviewdb.ApprovalCategory; |
| import com.google.gerrit.client.reviewdb.ApprovalCategoryValue; |
| import com.google.gerrit.client.reviewdb.Branch; |
| import com.google.gerrit.client.reviewdb.Project; |
| import com.google.gerrit.client.reviewdb.ProjectRight; |
| import com.google.gerrit.client.reviewdb.ReviewDb; |
| import com.google.gerrit.client.rpc.BaseServiceImplementation; |
| import com.google.gerrit.client.rpc.Common; |
| import com.google.gerrit.client.rpc.InvalidNameException; |
| import com.google.gerrit.client.rpc.InvalidRevisionException; |
| import com.google.gerrit.client.rpc.NoSuchEntityException; |
| import com.google.gerrit.git.InvalidRepositoryException; |
| import com.google.gerrit.git.PushQueue; |
| import com.google.gwt.user.client.rpc.AsyncCallback; |
| import com.google.gwtjsonrpc.client.VoidResult; |
| import com.google.gwtorm.client.OrmException; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.spearce.jgit.errors.IncorrectObjectTypeException; |
| import org.spearce.jgit.errors.MissingObjectException; |
| import org.spearce.jgit.lib.Constants; |
| import org.spearce.jgit.lib.LockFile; |
| import org.spearce.jgit.lib.ObjectId; |
| import org.spearce.jgit.lib.Ref; |
| import org.spearce.jgit.lib.RefUpdate; |
| import org.spearce.jgit.lib.Repository; |
| import org.spearce.jgit.revwalk.ObjectWalk; |
| import org.spearce.jgit.revwalk.RevCommit; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.servlet.http.HttpServletRequest; |
| |
| public class ProjectAdminServiceImpl extends BaseServiceImplementation |
| implements ProjectAdminService { |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| private final GerritServer server; |
| |
| ProjectAdminServiceImpl(final GerritServer gs) { |
| server = gs; |
| } |
| |
| public void ownedProjects(final AsyncCallback<List<Project>> callback) { |
| run(callback, new Action<List<Project>>() { |
| public List<Project> run(ReviewDb db) throws OrmException { |
| final List<Project> result; |
| if (Common.getGroupCache().isAdministrator(Common.getAccountId())) { |
| result = db.projects().all().toList(); |
| } else { |
| result = myOwnedProjects(db); |
| Collections.sort(result, new Comparator<Project>() { |
| public int compare(final Project a, final Project b) { |
| return a.getName().compareTo(b.getName()); |
| } |
| }); |
| } |
| return result; |
| } |
| }); |
| } |
| |
| public void projectDetail(final Project.Id projectId, |
| final AsyncCallback<ProjectDetail> callback) { |
| run(callback, new Action<ProjectDetail>() { |
| public ProjectDetail run(ReviewDb db) throws OrmException, Failure { |
| assertAmProjectOwner(db, projectId); |
| final ProjectCache.Entry p = Common.getProjectCache().get(projectId); |
| if (p == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final ProjectDetail d = new ProjectDetail(); |
| d.load(db, p); |
| return d; |
| } |
| }); |
| } |
| |
| public void changeProjectDescription(final Project.Id projectId, |
| final String description, final AsyncCallback<VoidResult> callback) { |
| run(callback, new Action<VoidResult>() { |
| public VoidResult run(final ReviewDb db) throws OrmException, Failure { |
| assertAmProjectOwner(db, projectId); |
| final Project proj = db.projects().get(projectId); |
| if (proj == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| proj.setDescription(description); |
| db.projects().update(Collections.singleton(proj)); |
| Common.getProjectCache().invalidate(proj); |
| |
| if (!ProjectRight.WILD_PROJECT.equals(projectId)) { |
| // Update git's description file, in case gitweb is being used |
| // |
| try { |
| final Repository e; |
| final LockFile f; |
| |
| e = server.getRepositoryCache().get(proj.getName()); |
| f = new LockFile(new File(e.getDirectory(), "description")); |
| if (f.lock()) { |
| String d = proj.getDescription(); |
| if (d != null) { |
| d = d.trim() + "\n"; |
| } else { |
| d = ""; |
| } |
| f.write(Constants.encode(d)); |
| f.commit(); |
| } |
| } catch (IOException e) { |
| log.error("Cannot update description for " + proj.getName(), e); |
| } catch (InvalidRepositoryException e) { |
| log.error("Cannot update description for " + proj.getName(), e); |
| } |
| } |
| |
| return VoidResult.INSTANCE; |
| } |
| }); |
| } |
| |
| public void changeProjectOwner(final Project.Id projectId, |
| final String newOwnerName, final AsyncCallback<VoidResult> callback) { |
| run(callback, new Action<VoidResult>() { |
| public VoidResult run(final ReviewDb db) throws OrmException, Failure { |
| assertAmProjectOwner(db, projectId); |
| final Project project = db.projects().get(projectId); |
| if (project == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| if (ProjectRight.WILD_PROJECT.equals(projectId)) { |
| // This is *not* a good idea to change away from administrators. |
| // |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final AccountGroup owner = |
| db.accountGroups().get(new AccountGroup.NameKey(newOwnerName)); |
| if (owner == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| project.setOwnerGroupId(owner.getId()); |
| db.projects().update(Collections.singleton(project)); |
| Common.getProjectCache().invalidate(project); |
| return VoidResult.INSTANCE; |
| } |
| }); |
| } |
| |
| public void changeProjectSubmitType(final Project.Id projectId, |
| final Project.SubmitType newSubmitType, |
| final AsyncCallback<VoidResult> callback) { |
| run(callback, new Action<VoidResult>() { |
| public VoidResult run(final ReviewDb db) throws OrmException, Failure { |
| assertAmProjectOwner(db, projectId); |
| final Project project = db.projects().get(projectId); |
| if (project == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| project.setSubmitType(newSubmitType); |
| db.projects().update(Collections.singleton(project)); |
| Common.getProjectCache().invalidate(project); |
| return VoidResult.INSTANCE; |
| } |
| }); |
| } |
| |
| public void deleteRight(final Set<ProjectRight.Key> keys, |
| final AsyncCallback<VoidResult> callback) { |
| run(callback, new Action<VoidResult>() { |
| public VoidResult run(final ReviewDb db) throws OrmException, Failure { |
| final Set<Project.Id> owned = ids(myOwnedProjects(db)); |
| Boolean amAdmin = null; |
| for (final ProjectRight.Key k : keys) { |
| if (!owned.contains(k.getProjectId())) { |
| if (amAdmin == null) { |
| amAdmin = |
| Common.getGroupCache().isAdministrator(Common.getAccountId()); |
| } |
| if (!amAdmin) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| } |
| } |
| for (final ProjectRight.Key k : keys) { |
| final ProjectRight m = db.projectRights().get(k); |
| if (m != null) { |
| db.projectRights().delete(Collections.singleton(m)); |
| Common.getProjectCache().invalidate(k.getProjectId()); |
| } |
| } |
| return VoidResult.INSTANCE; |
| } |
| }); |
| } |
| |
| public void addRight(final Project.Id projectId, |
| final ApprovalCategory.Id categoryId, final String groupName, |
| final short amin, final short amax, |
| final AsyncCallback<ProjectDetail> callback) { |
| final short min, max; |
| if (amin <= amax) { |
| min = amin; |
| max = amax; |
| } else { |
| min = amax; |
| max = amin; |
| } |
| |
| run(callback, new Action<ProjectDetail>() { |
| public ProjectDetail run(ReviewDb db) throws OrmException, Failure { |
| assertAmProjectOwner(db, projectId); |
| final Project proj = db.projects().get(projectId); |
| if (proj == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final ApprovalCategory cat = db.approvalCategories().get(categoryId); |
| if (cat == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| if (db.approvalCategoryValues().get( |
| new ApprovalCategoryValue.Id(categoryId, min)) == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| if (db.approvalCategoryValues().get( |
| new ApprovalCategoryValue.Id(categoryId, max)) == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final AccountGroup group = |
| db.accountGroups().get(new AccountGroup.NameKey(groupName)); |
| if (group == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final ProjectRight.Key key = |
| new ProjectRight.Key(projectId, categoryId, group.getId()); |
| ProjectRight pr = db.projectRights().get(key); |
| if (pr == null) { |
| pr = new ProjectRight(key); |
| pr.setMinValue(min); |
| pr.setMaxValue(max); |
| db.projectRights().insert(Collections.singleton(pr)); |
| } else { |
| pr.setMinValue(min); |
| pr.setMaxValue(max); |
| db.projectRights().update(Collections.singleton(pr)); |
| } |
| |
| Common.getProjectCache().invalidate(proj); |
| final ProjectDetail d = new ProjectDetail(); |
| d.load(db, Common.getProjectCache().get(projectId)); |
| return d; |
| } |
| }); |
| } |
| |
| public void listBranches(final Project.Id project, |
| final AsyncCallback<List<Branch>> callback) { |
| run(callback, new Action<List<Branch>>() { |
| public List<Branch> run(ReviewDb db) throws OrmException, Failure { |
| final ProjectCache.Entry e = Common.getProjectCache().get(project); |
| if (e == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| assertCanRead(e.getProject().getNameKey()); |
| return db.branches().byProject(e.getProject().getNameKey()).toList(); |
| } |
| }); |
| } |
| |
| public void deleteBranch(final Set<Branch.NameKey> ids, |
| final AsyncCallback<Set<Branch.NameKey>> callback) { |
| run(callback, new Action<Set<Branch.NameKey>>() { |
| public Set<Branch.NameKey> run(ReviewDb db) throws OrmException, Failure { |
| final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>(); |
| final Set<Project.Id> owned = ids(myOwnedProjects(db)); |
| Boolean amAdmin = null; |
| for (final Branch.NameKey k : ids) { |
| final ProjectCache.Entry e; |
| |
| e = Common.getProjectCache().get(k.getParentKey()); |
| if (e == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| if (!owned.contains(e.getProject().getId())) { |
| if (amAdmin == null) { |
| amAdmin = |
| Common.getGroupCache().isAdministrator(Common.getAccountId()); |
| } |
| if (!amAdmin) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| } |
| } |
| for (final Branch.NameKey k : ids) { |
| final Branch m = db.branches().get(k); |
| if (m == null) { |
| continue; |
| } |
| final Repository r; |
| |
| try { |
| r = server.getRepositoryCache().get(k.getParentKey().get()); |
| } catch (InvalidRepositoryException e) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| final RefUpdate.Result result; |
| try { |
| final RefUpdate u = r.updateRef(m.getName()); |
| u.setForceUpdate(true); |
| result = u.delete(); |
| } catch (IOException e) { |
| log.error("Cannot delete " + k, e); |
| continue; |
| } |
| |
| final Branch.NameKey mKey = m.getNameKey(); |
| switch (result) { |
| case NEW: |
| case NO_CHANGE: |
| case FAST_FORWARD: |
| case FORCED: |
| db.branches().delete(Collections.singleton(m)); |
| deleted.add(mKey); |
| PushQueue.scheduleUpdate(mKey.getParentKey(), m.getName()); |
| break; |
| |
| case REJECTED_CURRENT_BRANCH: |
| log.warn("Cannot delete " + k + ": " + result.name()); |
| break; |
| |
| default: |
| log.error("Cannot delete " + k + ": " + result.name()); |
| break; |
| } |
| } |
| return deleted; |
| } |
| }); |
| } |
| |
| public void addBranch(final Project.Id projectId, final String branchName, |
| final String startingRevision, final AsyncCallback<List<Branch>> callback) { |
| run(callback, new Action<List<Branch>>() { |
| public List<Branch> run(ReviewDb db) throws OrmException, Failure { |
| String refname = branchName; |
| if (!refname.startsWith(Constants.R_REFS)) { |
| refname = Constants.R_HEADS + refname; |
| } |
| if (!Repository.isValidRefName(refname)) { |
| throw new Failure(new InvalidNameException()); |
| } |
| |
| final Account me = Common.getAccountCache().get(Common.getAccountId()); |
| if (me == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| final ProjectCache.Entry pce = Common.getProjectCache().get(projectId); |
| if (pce == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| assertAmProjectOwner(db, projectId); |
| |
| final String repoName = pce.getProject().getName(); |
| final Repository repo; |
| try { |
| repo = server.getRepositoryCache().get(repoName); |
| } catch (InvalidRepositoryException e1) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| |
| // Convert the name given by the user into a valid object. |
| // |
| final ObjectId revid; |
| try { |
| revid = repo.resolve(startingRevision); |
| if (revid == null) { |
| throw new Failure(new InvalidRevisionException()); |
| } |
| } catch (IOException err) { |
| log.error("Cannot resolve \"" + startingRevision + "\" in " |
| + repoName, err); |
| throw new Failure(new InvalidRevisionException()); |
| } |
| |
| // Ensure it is fully connected in this repository. If not, |
| // we can't safely create a ref to it as objects are missing |
| // |
| final RevCommit revcommit; |
| final ObjectWalk rw = new ObjectWalk(repo); |
| try { |
| try { |
| revcommit = rw.parseCommit(revid); |
| rw.markStart(revcommit); |
| } catch (IncorrectObjectTypeException err) { |
| throw new Failure(new InvalidRevisionException()); |
| } |
| for (final Ref r : repo.getAllRefs().values()) { |
| try { |
| rw.markUninteresting(rw.parseAny(r.getObjectId())); |
| } catch (MissingObjectException err) { |
| continue; |
| } |
| } |
| rw.checkConnectivity(); |
| } catch (IncorrectObjectTypeException err) { |
| throw new Failure(new InvalidRevisionException()); |
| } catch (MissingObjectException err) { |
| throw new Failure(new InvalidRevisionException()); |
| } catch (IOException err) { |
| log.error("Repository " + repoName + " possibly corrupt", err); |
| throw new Failure(new InvalidRevisionException()); |
| } |
| |
| final HttpServletRequest hreq = |
| GerritJsonServlet.getCurrentCall().getHttpServletRequest(); |
| final Branch.NameKey name = |
| new Branch.NameKey(pce.getProject().getNameKey(), refname); |
| try { |
| final RefUpdate u = repo.updateRef(refname); |
| u.setExpectedOldObjectId(ObjectId.zeroId()); |
| u.setNewObjectId(revid); |
| u.setRefLogIdent(ChangeUtil.toReflogIdent(me, new InetSocketAddress( |
| hreq.getRemoteHost(), hreq.getRemotePort()))); |
| u.setRefLogMessage("created via web from " + startingRevision, false); |
| final RefUpdate.Result result = u.update(rw); |
| switch (result) { |
| case FAST_FORWARD: |
| case NEW: |
| case NO_CHANGE: |
| PushQueue.scheduleUpdate(name.getParentKey(), refname); |
| break; |
| default: |
| log.error("Cannot create branch " + name + ": " + result.name()); |
| throw new Failure(new IOException(result.name())); |
| } |
| } catch (IOException err) { |
| log.error("Cannot create branch " + name, err); |
| throw new Failure(err); |
| } |
| |
| final Branch.Id id = new Branch.Id(db.nextBranchId()); |
| final Branch newBranch = new Branch(name, id); |
| db.branches().insert(Collections.singleton(newBranch)); |
| return db.branches().byProject(pce.getProject().getNameKey()).toList(); |
| } |
| }); |
| } |
| |
| private void assertAmProjectOwner(final ReviewDb db, |
| final Project.Id projectId) throws Failure { |
| final ProjectCache.Entry p = Common.getProjectCache().get(projectId); |
| if (p == null) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| final Account.Id me = Common.getAccountId(); |
| if (!Common.getGroupCache().isInGroup(me, p.getProject().getOwnerGroupId()) |
| && !Common.getGroupCache().isAdministrator(me)) { |
| throw new Failure(new NoSuchEntityException()); |
| } |
| } |
| |
| private List<Project> myOwnedProjects(final ReviewDb db) throws OrmException { |
| final Account.Id me = Common.getAccountId(); |
| final List<Project> own = new ArrayList<Project>(); |
| for (final AccountGroup.Id groupId : Common.getGroupCache() |
| .getEffectiveGroups(me)) { |
| for (final Project g : db.projects().ownedByGroup(groupId)) { |
| own.add(g); |
| } |
| } |
| return own; |
| } |
| |
| private static Set<Project.Id> ids(final Collection<Project> projectList) { |
| final HashSet<Project.Id> r = new HashSet<Project.Id>(); |
| for (final Project project : projectList) { |
| r.add(project.getId()); |
| } |
| return r; |
| } |
| } |