LCOV - code coverage report
Current view: top level - server/notedb - ChangeDraftUpdate.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 88 90 97.8 %
Date: 2022-11-19 15:00:39 Functions: 19 19 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2014 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.notedb;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
      20             : 
      21             : import com.google.auto.value.AutoValue;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.Account;
      24             : import com.google.gerrit.entities.Change;
      25             : import com.google.gerrit.entities.Comment;
      26             : import com.google.gerrit.entities.HumanComment;
      27             : import com.google.gerrit.entities.Project;
      28             : import com.google.gerrit.entities.RefNames;
      29             : import com.google.gerrit.exceptions.StorageException;
      30             : import com.google.gerrit.server.GerritPersonIdent;
      31             : import com.google.gerrit.server.config.AllUsersName;
      32             : import com.google.inject.assistedinject.Assisted;
      33             : import com.google.inject.assistedinject.AssistedInject;
      34             : import java.io.IOException;
      35             : import java.time.Instant;
      36             : import java.util.ArrayList;
      37             : import java.util.Arrays;
      38             : import java.util.HashMap;
      39             : import java.util.List;
      40             : import java.util.Map;
      41             : import org.eclipse.jgit.errors.ConfigInvalidException;
      42             : import org.eclipse.jgit.lib.CommitBuilder;
      43             : import org.eclipse.jgit.lib.ObjectId;
      44             : import org.eclipse.jgit.lib.ObjectInserter;
      45             : import org.eclipse.jgit.lib.PersonIdent;
      46             : import org.eclipse.jgit.notes.NoteMap;
      47             : import org.eclipse.jgit.revwalk.RevWalk;
      48             : 
      49             : /**
      50             :  * A single delta to apply atomically to a change.
      51             :  *
      52             :  * <p>This delta contains only draft comments on a single patch set of a change by a single author.
      53             :  * This delta will become a single commit in the All-Users repository.
      54             :  *
      55             :  * <p>This class is not thread safe.
      56             :  */
      57             : public class ChangeDraftUpdate extends AbstractChangeUpdate {
      58             :   public interface Factory {
      59             :     ChangeDraftUpdate create(
      60             :         ChangeNotes notes,
      61             :         @Assisted("effective") Account.Id accountId,
      62             :         @Assisted("real") Account.Id realAccountId,
      63             :         PersonIdent authorIdent,
      64             :         Instant when);
      65             : 
      66             :     ChangeDraftUpdate create(
      67             :         Change change,
      68             :         @Assisted("effective") Account.Id accountId,
      69             :         @Assisted("real") Account.Id realAccountId,
      70             :         PersonIdent authorIdent,
      71             :         Instant when);
      72             :   }
      73             : 
      74             :   @AutoValue
      75          29 :   abstract static class Key {
      76             :     abstract ObjectId commitId();
      77             : 
      78             :     abstract Comment.Key key();
      79             :   }
      80             : 
      81          27 :   enum DeleteReason {
      82          27 :     DELETED,
      83          27 :     PUBLISHED,
      84          27 :     FIXED
      85             :   }
      86             : 
      87             :   private static Key key(HumanComment c) {
      88          29 :     return new AutoValue_ChangeDraftUpdate_Key(c.getCommitId(), c.key);
      89             :   }
      90             : 
      91             :   private final AllUsersName draftsProject;
      92             : 
      93          29 :   private List<HumanComment> put = new ArrayList<>();
      94          29 :   private Map<Key, DeleteReason> delete = new HashMap<>();
      95             : 
      96             :   @SuppressWarnings("UnusedMethod")
      97             :   @AssistedInject
      98             :   private ChangeDraftUpdate(
      99             :       @GerritPersonIdent PersonIdent serverIdent,
     100             :       AllUsersName allUsers,
     101             :       ChangeNoteUtil noteUtil,
     102             :       @Assisted ChangeNotes notes,
     103             :       @Assisted("effective") Account.Id accountId,
     104             :       @Assisted("real") Account.Id realAccountId,
     105             :       @Assisted PersonIdent authorIdent,
     106             :       @Assisted Instant when) {
     107          29 :     super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
     108          29 :     this.draftsProject = allUsers;
     109          29 :   }
     110             : 
     111             :   @AssistedInject
     112             :   private ChangeDraftUpdate(
     113             :       @GerritPersonIdent PersonIdent serverIdent,
     114             :       AllUsersName allUsers,
     115             :       ChangeNoteUtil noteUtil,
     116             :       @Assisted Change change,
     117             :       @Assisted("effective") Account.Id accountId,
     118             :       @Assisted("real") Account.Id realAccountId,
     119             :       @Assisted PersonIdent authorIdent,
     120             :       @Assisted Instant when) {
     121          26 :     super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
     122          26 :     this.draftsProject = allUsers;
     123          26 :   }
     124             : 
     125             :   public void putComment(HumanComment c) {
     126          21 :     checkState(!put.contains(c), "comment already added");
     127          21 :     verifyComment(c);
     128          21 :     put.add(c);
     129          21 :   }
     130             : 
     131             :   /**
     132             :    * Marks a comment for deletion. Called when the comment is deleted because the user published it.
     133             :    */
     134             :   public void markCommentPublished(HumanComment c) {
     135          26 :     checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
     136          26 :     verifyComment(c);
     137          26 :     delete.put(key(c), DeleteReason.PUBLISHED);
     138          26 :   }
     139             : 
     140             :   /**
     141             :    * Marks a comment for deletion. Called when the comment is deleted because the user removed it.
     142             :    */
     143             :   public void deleteComment(HumanComment c) {
     144          10 :     checkState(!delete.containsKey(key(c)), "comment already marked for deletion");
     145          10 :     verifyComment(c);
     146          10 :     delete.put(key(c), DeleteReason.DELETED);
     147          10 :   }
     148             : 
     149             :   /**
     150             :    * Marks a comment for deletion. Called when the comment should have been deleted previously, but
     151             :    * wasn't, so we're fixing it up.
     152             :    */
     153             :   public void deleteComment(ObjectId commitId, Comment.Key key) {
     154          16 :     Key commentKey = new AutoValue_ChangeDraftUpdate_Key(commitId, key);
     155          16 :     checkState(!delete.containsKey(commentKey), "comment already marked for deletion");
     156          16 :     delete.put(commentKey, DeleteReason.FIXED);
     157          16 :   }
     158             : 
     159             :   /**
     160             :    * Returns true if all we do in this operations is deletes caused by publishing or fixing up
     161             :    * comments.
     162             :    */
     163             :   public boolean canRunAsync() {
     164          29 :     return put.isEmpty()
     165          27 :         && delete.values().stream()
     166          29 :             .allMatch(r -> r == DeleteReason.PUBLISHED || r == DeleteReason.FIXED);
     167             :   }
     168             : 
     169             :   /**
     170             :    * Returns a copy of the current {@link ChangeDraftUpdate} that contains references to all
     171             :    * deletions. Copying of {@link ChangeDraftUpdate} is only allowed if it contains no new comments.
     172             :    */
     173             :   ChangeDraftUpdate copy() {
     174          26 :     checkState(
     175          26 :         put.isEmpty(),
     176             :         "copying ChangeDraftUpdate is allowed only if it doesn't contain new comments");
     177          26 :     ChangeDraftUpdate clonedUpdate =
     178             :         new ChangeDraftUpdate(
     179             :             authorIdent,
     180             :             draftsProject,
     181             :             noteUtil,
     182          26 :             new Change(getChange()),
     183             :             accountId,
     184             :             realAccountId,
     185             :             authorIdent,
     186             :             when);
     187          26 :     clonedUpdate.delete.putAll(delete);
     188          26 :     return clonedUpdate;
     189             :   }
     190             : 
     191             :   @Nullable
     192             :   private CommitBuilder storeCommentsInNotes(
     193             :       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
     194             :       throws ConfigInvalidException, IOException {
     195          29 :     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
     196          29 :     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
     197             : 
     198          29 :     for (HumanComment c : put) {
     199          21 :       if (!delete.keySet().contains(key(c))) {
     200          21 :         cache.get(c.getCommitId()).putComment(c);
     201             :       }
     202          21 :     }
     203          29 :     for (Key k : delete.keySet()) {
     204          27 :       cache.get(k.commitId()).deleteComment(k.key());
     205          27 :     }
     206             : 
     207             :     // keyed by commit ID.
     208          29 :     Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     209          29 :     boolean touchedAnyRevs = false;
     210          29 :     for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
     211          29 :       ObjectId id = e.getKey();
     212          29 :       byte[] data = e.getValue().build(noteUtil.getChangeNoteJson());
     213          29 :       if (!Arrays.equals(data, e.getValue().baseRaw)) {
     214          21 :         touchedAnyRevs = true;
     215             :       }
     216          29 :       if (data.length == 0) {
     217          27 :         rnm.noteMap.remove(id);
     218             :       } else {
     219          21 :         ObjectId dataBlob = ins.insert(OBJ_BLOB, data);
     220          21 :         rnm.noteMap.set(id, dataBlob);
     221             :       }
     222          29 :     }
     223             : 
     224             :     // If we didn't touch any notes, tell the caller this was a no-op update. We
     225             :     // couldn't have done this in isEmpty() below because we hadn't read the old
     226             :     // data yet.
     227          29 :     if (!touchedAnyRevs) {
     228          21 :       return NO_OP_UPDATE;
     229             :     }
     230             : 
     231             :     // If there are no comments left, tell the
     232             :     // caller to delete the entire ref.
     233          21 :     if (!rnm.noteMap.iterator().hasNext()) {
     234          17 :       return null;
     235             :     }
     236             : 
     237          21 :     ObjectId treeId = rnm.noteMap.writeTree(ins);
     238          21 :     cb.setTreeId(treeId);
     239          21 :     return cb;
     240             :   }
     241             : 
     242             :   private RevisionNoteMap<ChangeRevisionNote> getRevisionNoteMap(RevWalk rw, ObjectId curr)
     243             :       throws ConfigInvalidException, IOException {
     244             :     // The old DraftCommentNotes already parsed the revision notes. We can reuse them as long as
     245             :     // the ref hasn't advanced.
     246          29 :     ChangeNotes changeNotes = getNotes();
     247          29 :     if (changeNotes != null) {
     248          21 :       DraftCommentNotes draftNotes = changeNotes.load().getDraftCommentNotes();
     249          21 :       if (draftNotes != null) {
     250           9 :         ObjectId idFromNotes = firstNonNull(draftNotes.getRevision(), ObjectId.zeroId());
     251           9 :         RevisionNoteMap<ChangeRevisionNote> rnm = draftNotes.getRevisionNoteMap();
     252           9 :         if (idFromNotes.equals(curr) && rnm != null) {
     253           9 :           return rnm;
     254             :         }
     255             :       }
     256             :     }
     257             :     NoteMap noteMap;
     258          29 :     if (!curr.equals(ObjectId.zeroId())) {
     259          13 :       noteMap = NoteMap.read(rw.getObjectReader(), rw.parseCommit(curr));
     260             :     } else {
     261          29 :       noteMap = NoteMap.newEmptyMap();
     262             :     }
     263             :     // Even though reading from changes might not be enabled, we need to
     264             :     // parse any existing revision notes so we can merge them.
     265          29 :     return RevisionNoteMap.parse(
     266          29 :         noteUtil.getChangeNoteJson(), rw.getObjectReader(), noteMap, HumanComment.Status.DRAFT);
     267             :   }
     268             : 
     269             :   @Override
     270             :   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
     271             :       throws IOException {
     272          29 :     CommitBuilder cb = new CommitBuilder();
     273          29 :     cb.setMessage("Update draft comments");
     274             :     try {
     275          29 :       return storeCommentsInNotes(rw, ins, curr, cb);
     276           0 :     } catch (ConfigInvalidException e) {
     277           0 :       throw new StorageException(e);
     278             :     }
     279             :   }
     280             : 
     281             :   @Override
     282             :   protected Project.NameKey getProjectName() {
     283          29 :     return draftsProject;
     284             :   }
     285             : 
     286             :   @Override
     287             :   protected String getRefName() {
     288          29 :     return RefNames.refsDraftComments(getId(), accountId);
     289             :   }
     290             : 
     291             :   @Override
     292             :   protected void setParentCommit(CommitBuilder cb, ObjectId parentCommitId) {
     293          21 :     cb.setParentIds(); // Draft updates should not keep history of parent commits
     294          21 :   }
     295             : 
     296             :   @Override
     297             :   public boolean isEmpty() {
     298          29 :     return delete.isEmpty() && put.isEmpty();
     299             :   }
     300             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750