All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.notedb.ChangeRebuilder Maven / Gradle / Ivy

// Copyright (C) 2014 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.notedb;

import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;

import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

public class ChangeRebuilder {
  private static final long TS_WINDOW_MS =
      TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);

  private final Provider dbProvider;
  private final ChangeControl.GenericFactory controlFactory;
  private final IdentifiedUser.GenericFactory userFactory;
  private final PatchListCache patchListCache;
  private final ChangeUpdate.Factory updateFactory;
  private final ChangeDraftUpdate.Factory draftUpdateFactory;

  @Inject
  ChangeRebuilder(Provider dbProvider,
      ChangeControl.GenericFactory controlFactory,
      IdentifiedUser.GenericFactory userFactory,
      PatchListCache patchListCache,
      ChangeUpdate.Factory updateFactory,
      ChangeDraftUpdate.Factory draftUpdateFactory) {
    this.dbProvider = dbProvider;
    this.controlFactory = controlFactory;
    this.userFactory = userFactory;
    this.patchListCache = patchListCache;
    this.updateFactory = updateFactory;
    this.draftUpdateFactory = draftUpdateFactory;
  }

  public ListenableFuture rebuildAsync(final Change change,
      ListeningExecutorService executor, final BatchRefUpdate bru,
      final BatchRefUpdate bruForDrafts, final Repository changeRepo,
      final Repository allUsersRepo) {
    return executor.submit(new Callable() {
        @Override
      public Void call() throws Exception {
        rebuild(change, bru, bruForDrafts, changeRepo, allUsersRepo);
        return null;
      }
    });
  }

  public void rebuild(Change change, BatchRefUpdate bru,
      BatchRefUpdate bruForDrafts, Repository changeRepo,
      Repository allUsersRepo) throws NoSuchChangeException, IOException,
      OrmException {
    deleteRef(change, changeRepo);
    ReviewDb db = dbProvider.get();
    Change.Id changeId = change.getId();

    // We will rebuild all events, except for draft comments, in buckets based
    // on author and timestamp. However, all draft comments for a given change
    // and author will be written as one commit in the notedb.
    List events = Lists.newArrayList();
    Multimap draftCommentEvents =
        ArrayListMultimap.create();

    for (PatchSet ps : db.patchSets().byChange(changeId)) {
      events.add(new PatchSetEvent(ps));
      for (PatchLineComment c : db.patchComments().byPatchSet(ps.getId())) {
        PatchLineCommentEvent e =
            new PatchLineCommentEvent(c, change, ps, patchListCache);
        if (c.getStatus() == Status.PUBLISHED) {
          events.add(e);
        } else {
          draftCommentEvents.put(c.getAuthor(), e);
        }
      }
    }

    for (PatchSetApproval psa : db.patchSetApprovals().byChange(changeId)) {
      events.add(new ApprovalEvent(psa));
    }


    Collections.sort(events);
    BatchMetaDataUpdate batch = null;
    ChangeUpdate update = null;
    for (Event e : events) {
      if (!sameUpdate(e, update)) {
        if (update != null) {
          writeToBatch(batch, update, changeRepo);
        }
        IdentifiedUser user = userFactory.create(dbProvider, e.who);
        update = updateFactory.create(
            controlFactory.controlFor(change, user), e.when);
        update.setPatchSetId(e.psId);
        if (batch == null) {
          batch = update.openUpdateInBatch(bru);
        }
      }
      e.apply(update);
    }
    if (batch != null) {
      if (update != null) {
        writeToBatch(batch, update, changeRepo);
      }

      // Since the BatchMetaDataUpdates generated by all ChangeRebuilders on a
      // given project are backed by the same BatchRefUpdate, we need to
      // synchronize on the BatchRefUpdate. Therefore, since commit on a
      // BatchMetaDataUpdate is the only method that modifies a BatchRefUpdate,
      // we can just synchronize this call.
      synchronized (bru) {
        batch.commit();
      }
    }

    for (Account.Id author : draftCommentEvents.keys()) {
      IdentifiedUser user = userFactory.create(dbProvider, author);
      ChangeDraftUpdate draftUpdate = null;
      BatchMetaDataUpdate batchForDrafts = null;
      for (PatchLineCommentEvent e : draftCommentEvents.get(author)) {
        if (draftUpdate == null) {
          draftUpdate = draftUpdateFactory.create(
              controlFactory.controlFor(change, user), e.when);
          draftUpdate.setPatchSetId(e.psId);
          batchForDrafts = draftUpdate.openUpdateInBatch(bruForDrafts);
        }
        e.applyDraft(draftUpdate);
      }
      writeToBatch(batchForDrafts, draftUpdate, allUsersRepo);
      synchronized(bruForDrafts) {
        batchForDrafts.commit();
      }
    }
  }

  private void deleteRef(Change change, Repository changeRepo)
      throws IOException {
    String refName = ChangeNoteUtil.changeRefName(change.getId());
    RefUpdate ru = changeRepo.updateRef(refName, true);
    ru.setForceUpdate(true);
    RefUpdate.Result result = ru.delete();
    switch (result) {
      case FORCED:
      case NEW:
      case NO_CHANGE:
        break;
      default:
        throw new IOException(
            String.format("Failed to delete ref %s: %s", refName, result));
    }
  }

  private void writeToBatch(BatchMetaDataUpdate batch,
      AbstractChangeUpdate update, Repository repo) throws IOException,
      OrmException {
    try (ObjectInserter inserter = repo.newObjectInserter()) {
      update.setInserter(inserter);
      update.writeCommit(batch);
    }
  }

  private static long round(Date when) {
    return when.getTime() / TS_WINDOW_MS;
  }

  private static boolean sameUpdate(Event event, ChangeUpdate update) {
    return update != null
        && round(event.when) == round(update.getWhen())
        && event.who.equals(update.getUser().getAccountId())
        && event.psId.equals(update.getPatchSetId());
  }

  private abstract static class Event implements Comparable {
    final PatchSet.Id psId;
    final Account.Id who;
    final Timestamp when;

    protected Event(PatchSet.Id psId, Account.Id who, Timestamp when) {
      this.psId = psId;
      this.who = who;
      this.when = when;
    }

    protected void checkUpdate(AbstractChangeUpdate update) {
      checkState(Objects.equals(update.getPatchSetId(), psId),
          "cannot apply event for %s to update for %s",
          update.getPatchSetId(), psId);
      checkState(when.getTime() - update.getWhen().getTime() <= TS_WINDOW_MS,
          "event at %s outside update window starting at %s",
          when, update.getWhen());
      checkState(Objects.equals(update.getUser().getAccountId(), who),
          "cannot apply event by %s to update by %s",
          who, update.getUser().getAccountId());
    }

    abstract void apply(ChangeUpdate update) throws OrmException;

    @Override
    public int compareTo(Event other) {
      return ComparisonChain.start()
          // TODO(dborowitz): Smarter bucketing: pick a bucket start time T and
          // include all events up to T + TS_WINDOW_MS but no further.
          // Interleaving different authors complicates things.
          .compare(round(when), round(other.when))
          .compare(who.get(), other.who.get())
          .compare(psId.get(), other.psId.get())
          .result();
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("psId", psId)
          .add("who", who)
          .add("when", when)
          .toString();
    }
  }

  private static class ApprovalEvent extends Event {
    private PatchSetApproval psa;

    ApprovalEvent(PatchSetApproval psa) {
      super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted());
      this.psa = psa;
    }

    @Override
    void apply(ChangeUpdate update) {
      checkUpdate(update);
      update.putApproval(psa.getLabel(), psa.getValue());
    }
  }

  private static class PatchSetEvent extends Event {
    private final PatchSet ps;

    PatchSetEvent(PatchSet ps) {
      super(ps.getId(), ps.getUploader(), ps.getCreatedOn());
      this.ps = ps;
    }

    @Override
    void apply(ChangeUpdate update) {
      checkUpdate(update);
      if (ps.getPatchSetId() == 1) {
        update.setSubject("Create change");
      } else {
        update.setSubject("Create patch set " + ps.getPatchSetId());
      }
    }
  }

  private static class PatchLineCommentEvent extends Event {
    public final PatchLineComment c;
    private final Change change;
    private final PatchSet ps;
    private final PatchListCache cache;

    PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
        PatchListCache cache) {
      super(getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
      this.c = c;
      this.change = change;
      this.ps = ps;
      this.cache = cache;
    }

    @Override
    void apply(ChangeUpdate update) throws OrmException {
      checkUpdate(update);
      if (c.getRevId() == null) {
        setCommentRevId(c, cache, change, ps);
      }
      update.insertComment(c);
    }

    void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
      if (c.getRevId() == null) {
        setCommentRevId(c, cache, change, ps);
      }
      draftUpdate.insertComment(c);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy