| // Copyright (c) 2013 VMware, Inc. All Rights Reserved. |
| // Copyright (C) 2017 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.googlesource.gerrit.owners.common; |
| |
| import static com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace.IGNORE_NONE; |
| |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.Project.NameKey; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.extensions.annotations.Listen; |
| import com.google.gerrit.extensions.api.GerritApi; |
| import com.google.gerrit.extensions.api.changes.ChangeApi; |
| import com.google.gerrit.extensions.api.changes.Changes; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.server.ChangeMessagesUtil; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.git.GitRepositoryManager; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.notedb.ChangeNotesCommit; |
| import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; |
| import com.google.gerrit.server.patch.DiffSummary; |
| import com.google.gerrit.server.patch.DiffSummaryKey; |
| import com.google.gerrit.server.patch.PatchListCache; |
| import com.google.gerrit.server.patch.PatchListKey; |
| import com.google.gerrit.server.patch.PatchListNotAvailableException; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.util.ManualRequestContext; |
| import com.google.gerrit.server.util.OneOffRequestContext; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.errors.MissingObjectException; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.FooterKey; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| @Listen |
| public class GitRefListener implements GitReferenceUpdatedListener { |
| private static final Logger logger = LoggerFactory.getLogger(GitRefListener.class); |
| |
| private static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress"); |
| |
| private final GerritApi api; |
| |
| private final PatchListCache patchListCache; |
| private final ProjectCache projectCache; |
| private final GitRepositoryManager repositoryManager; |
| private final Accounts accounts; |
| private final ReviewerManager reviewerManager; |
| |
| private final OneOffRequestContext oneOffReqCtx; |
| |
| private Provider<CurrentUser> currentUserProvider; |
| |
| private ChangeNotes.Factory notesFactory; |
| |
| private final AutoAssignConfig cfg; |
| |
| private final PathOwnersEntriesCache cache; |
| |
| @Inject |
| public GitRefListener( |
| GerritApi api, |
| PatchListCache patchListCache, |
| ProjectCache projectCache, |
| GitRepositoryManager repositoryManager, |
| Accounts accounts, |
| ReviewerManager reviewerManager, |
| OneOffRequestContext oneOffReqCtx, |
| Provider<CurrentUser> currentUserProvider, |
| ChangeNotes.Factory notesFactory, |
| AutoAssignConfig cfg, |
| PathOwnersEntriesCache cache) { |
| this.api = api; |
| this.patchListCache = patchListCache; |
| this.projectCache = projectCache; |
| this.repositoryManager = repositoryManager; |
| this.accounts = accounts; |
| this.reviewerManager = reviewerManager; |
| this.oneOffReqCtx = oneOffReqCtx; |
| this.currentUserProvider = currentUserProvider; |
| this.notesFactory = notesFactory; |
| this.cfg = cfg; |
| this.cache = cache; |
| } |
| |
| @Override |
| public void onGitReferenceUpdated(Event event) { |
| if (event.isDelete()) { |
| logger.debug("Ref-update event on ref %s is a deletion: ignoring", event.getRefName()); |
| return; |
| } |
| |
| try { |
| AccountInfo updaterAccountInfo = event.getUpdater(); |
| CurrentUser currentUser = currentUserProvider.get(); |
| if (currentUser.isIdentifiedUser()) { |
| handleGitReferenceUpdated(event); |
| } else if (updaterAccountInfo != null) { |
| handleGitReferenceUpdatedAsUser(event, Account.id(updaterAccountInfo._accountId)); |
| } else { |
| handleGitReferenceUpdatedAsServer(event); |
| } |
| } catch (StorageException | NoSuchProjectException e) { |
| logger.warn("Unable to process event {} on project {}", event, event.getProjectName(), e); |
| } |
| } |
| |
| private void handleGitReferenceUpdatedAsUser(Event event, Account.Id updaterAccountId) |
| throws NoSuchProjectException { |
| try (ManualRequestContext ctx = oneOffReqCtx.openAs(updaterAccountId)) { |
| handleGitReferenceUpdated(event); |
| } |
| } |
| |
| private void handleGitReferenceUpdatedAsServer(Event event) throws NoSuchProjectException { |
| try (ManualRequestContext ctx = oneOffReqCtx.open()) { |
| handleGitReferenceUpdated(event); |
| } |
| } |
| |
| private void handleGitReferenceUpdated(Event event) throws NoSuchProjectException { |
| String projectName = event.getProjectName(); |
| Repository repository; |
| try { |
| NameKey projectNameKey = Project.NameKey.parse(projectName); |
| boolean autoAssignWip = cfg.autoAssignWip(projectNameKey); |
| repository = repositoryManager.openRepository(projectNameKey); |
| try { |
| String refName = event.getRefName(); |
| Change.Id changeId = Change.Id.fromRef(refName); |
| if (changeId != null) { |
| ChangeNotes changeNotes = notesFactory.createChecked(projectNameKey, changeId); |
| if ((!RefNames.isNoteDbMetaRef(refName) |
| && isChangeToBeProcessed(changeNotes.getChange(), autoAssignWip)) |
| || isChangeSetReadyForReview(repository, changeNotes, event.getNewObjectId())) { |
| processEvent(projectNameKey, repository, event, changeId); |
| } |
| } |
| } finally { |
| repository.close(); |
| } |
| } catch (IOException e) { |
| logger.warn("Couldn't open repository: {}", projectName, e); |
| } |
| } |
| |
| private boolean isChangeToBeProcessed(Change change, boolean autoAssignWip) { |
| return !change.isWorkInProgress() || autoAssignWip; |
| } |
| |
| private boolean isChangeSetReadyForReview( |
| Repository repository, ChangeNotes changeNotes, String metaObjectId) |
| throws MissingObjectException, IncorrectObjectTypeException, IOException { |
| if (changeNotes.getChange().isWorkInProgress()) { |
| return false; |
| } |
| |
| if (changeNotes.getChangeMessages().stream() |
| .filter(message -> message.getKey().uuid().equals(metaObjectId)) |
| .map(message -> message.getTag()) |
| .filter(Predicates.notNull()) |
| .anyMatch(tag -> tag.contains(ChangeMessagesUtil.TAG_SET_READY))) { |
| return true; |
| } |
| |
| try (ChangeNotesRevWalk revWalk = ChangeNotesCommit.newRevWalk(repository)) { |
| ChangeNotesCommit metaCommit = revWalk.parseCommit(ObjectId.fromString(metaObjectId)); |
| if (metaCommit.getParentCount() == 0) { |
| // The first commit cannot be a 'Set ready' operation |
| return false; |
| } |
| List<String> wipFooterLines = metaCommit.getFooterLines(FOOTER_WORK_IN_PROGRESS); |
| return wipFooterLines != null |
| && !wipFooterLines.isEmpty() |
| && Boolean.FALSE.toString().equalsIgnoreCase(wipFooterLines.get(0)); |
| } |
| } |
| |
| public void processEvent( |
| Project.NameKey projectNameKey, Repository repository, Event event, Change.Id cId) |
| throws NoSuchProjectException { |
| Changes changes = api.changes(); |
| // The provider injected by Gerrit is shared with other workers on the |
| // same local thread and thus cannot be closed in this event listener. |
| try { |
| ChangeApi cApi = changes.id(cId.get()); |
| ChangeInfo change = cApi.get(); |
| List<NameKey> parentProjectsNameKeys = |
| projectCache |
| .get(NameKey.parse(change.project)) |
| .map( |
| p -> |
| p.parents().stream() |
| .map(ProjectState::getNameKey) |
| .collect(Collectors.toList())) |
| .orElse(Collections.emptyList()); |
| |
| DiffSummary patchList = getDiffSummary(repository, event, change); |
| if (patchList != null) { |
| PathOwners owners = |
| new PathOwners( |
| accounts, |
| repositoryManager, |
| repository, |
| parentProjectsNameKeys, |
| cfg.isBranchDisabled(change.branch) ? Optional.empty() : Optional.of(change.branch), |
| patchList, |
| cfg.expandGroups(), |
| projectNameKey.get(), |
| cache); |
| Set<Account.Id> allReviewers = Sets.newHashSet(); |
| allReviewers.addAll(owners.get().values()); |
| allReviewers.addAll(owners.getReviewers().values()); |
| for (Matcher matcher : owners.getMatchers().values()) { |
| allReviewers.addAll(matcher.getOwners()); |
| allReviewers.addAll(matcher.getReviewers()); |
| } |
| logger.debug("Autoassigned reviewers are: {}", allReviewers.toString()); |
| reviewerManager.addReviewers(projectNameKey, cApi, allReviewers); |
| } |
| } catch (RestApiException e) { |
| logger.warn("Could not open change: {}", cId, e); |
| } catch (ReviewerManagerException e) { |
| logger.warn("Could not add reviewers for change: {}", cId, e); |
| } |
| } |
| |
| private DiffSummary getDiffSummary(Repository repository, Event event, ChangeInfo change) { |
| ObjectId newId = null; |
| PatchListKey plKey; |
| try { |
| if (RefNames.isNoteDbMetaRef(event.getRefName())) { |
| newId = ObjectId.fromString(change.currentRevision); |
| RevCommit revCommit = repository.parseCommit(newId); |
| plKey = PatchListKey.againstBase(newId, revCommit.getParentCount()); |
| } else { |
| if (event.getNewObjectId() != null) { |
| newId = ObjectId.fromString(event.getNewObjectId()); |
| } |
| plKey = PatchListKey.againstCommit(null, newId, IGNORE_NONE); |
| } |
| return patchListCache.getDiffSummary( |
| DiffSummaryKey.fromPatchListKey(plKey), Project.nameKey(change.project)); |
| } catch (PatchListNotAvailableException | IOException e) { |
| logger.warn("Could not load patch list for change {}", change.id, e); |
| } |
| return null; |
| } |
| } |