Line data Source code
1 : // Copyright (C) 2013 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 com.google.auto.value.AutoValue; 18 : import com.google.gerrit.entities.Account; 19 : import com.google.gerrit.entities.AttentionSetUpdate; 20 : import com.google.gerrit.json.OutputFormat; 21 : import com.google.gerrit.server.config.GerritServerId; 22 : import com.google.gson.Gson; 23 : import com.google.inject.Inject; 24 : import java.time.Instant; 25 : import java.util.Optional; 26 : import org.eclipse.jgit.lib.PersonIdent; 27 : import org.eclipse.jgit.revwalk.RevCommit; 28 : import org.eclipse.jgit.util.RawParseUtils; 29 : 30 : public class ChangeNoteUtil { 31 : 32 : static final String GERRIT_USER_TEMPLATE = "Gerrit User %d"; 33 : 34 142 : private static final Gson gson = OutputFormat.JSON_COMPACT.newGson(); 35 : 36 : private final ChangeNoteJson changeNoteJson; 37 : private final String serverId; 38 : 39 : @Inject 40 142 : public ChangeNoteUtil(ChangeNoteJson changeNoteJson, @GerritServerId String serverId) { 41 142 : this.serverId = serverId; 42 142 : this.changeNoteJson = changeNoteJson; 43 142 : } 44 : 45 : public ChangeNoteJson getChangeNoteJson() { 46 64 : return changeNoteJson; 47 : } 48 : 49 : /** 50 : * Generates a user identifier that contains the account ID, but not the user's name or email 51 : * address. 52 : * 53 : * @return The passed in {@link StringBuilder} instance to which the identifier has been appended. 54 : */ 55 : StringBuilder appendAccountIdIdentString(StringBuilder stringBuilder, Account.Id accountId) { 56 75 : return stringBuilder 57 75 : .append(getAccountIdAsUsername(accountId)) 58 75 : .append(" <") 59 75 : .append(getAccountIdAsEmailAddress(accountId)) 60 75 : .append('>'); 61 : } 62 : 63 : public static String formatAccountIdentString(Account.Id account, String accountIdAsEmail) { 64 1 : return String.format( 65 1 : "%s <%s>", ChangeNoteUtil.getAccountIdAsUsername(account), accountIdAsEmail); 66 : } 67 : 68 : /** 69 : * Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email 70 : * address. 71 : */ 72 : public PersonIdent newAccountIdIdent( 73 : Account.Id accountId, Instant when, PersonIdent serverIdent) { 74 103 : return new PersonIdent( 75 103 : getAccountIdAsUsername(accountId), 76 103 : getAccountIdAsEmailAddress(accountId), 77 : when, 78 103 : serverIdent.getZoneId()); 79 : } 80 : 81 : /** Returns the string {@code "Gerrit User " + accountId}, to pseudonymize user names. */ 82 : public static String getAccountIdAsUsername(Account.Id accountId) { 83 103 : return String.format(GERRIT_USER_TEMPLATE, accountId.get()); 84 : } 85 : 86 : public String getAccountIdAsEmailAddress(Account.Id accountId) { 87 103 : return accountId.get() + "@" + serverId; 88 : } 89 : 90 : public static Optional<CommitMessageRange> parseCommitMessageRange(RevCommit commit) { 91 103 : byte[] raw = commit.getRawBuffer(); 92 103 : int size = raw.length; 93 : 94 103 : int subjectStart = RawParseUtils.commitMessage(raw, 0); 95 103 : if (subjectStart < 0 || subjectStart >= size) { 96 0 : return Optional.empty(); 97 : } 98 : 99 103 : int subjectEnd = RawParseUtils.endOfParagraph(raw, subjectStart); 100 103 : if (subjectEnd == size) { 101 1 : return Optional.empty(); 102 : } 103 : 104 : int changeMessageStart; 105 : 106 103 : if (raw[subjectEnd] == '\n') { 107 103 : changeMessageStart = subjectEnd + 2; // \n\n ends paragraph 108 0 : } else if (raw[subjectEnd] == '\r') { 109 0 : changeMessageStart = subjectEnd + 4; // \r\n\r\n ends paragraph 110 : } else { 111 0 : return Optional.empty(); 112 : } 113 : 114 103 : int ptr = size - 1; 115 103 : int changeMessageEnd = -1; 116 103 : while (ptr > changeMessageStart) { 117 103 : ptr = RawParseUtils.prevLF(raw, ptr, '\r'); 118 103 : if (ptr == -1) { 119 0 : break; 120 : } 121 103 : if (raw[ptr] == '\n') { 122 103 : changeMessageEnd = ptr - 1; 123 103 : break; 124 103 : } else if (raw[ptr] == '\r') { 125 0 : changeMessageEnd = ptr - 3; 126 0 : break; 127 : } 128 : } 129 : 130 103 : if (ptr <= changeMessageStart) { 131 : // Return with subject, ChangeMessage is empty 132 44 : return Optional.of( 133 44 : CommitMessageRange.builder() 134 44 : .subjectStart(subjectStart) 135 44 : .subjectEnd(subjectEnd) 136 44 : .changeMessageStart(changeMessageStart) 137 44 : .changeMessageEnd(changeMessageStart) 138 44 : .build()); 139 : } 140 : 141 : CommitMessageRange range = 142 103 : CommitMessageRange.builder() 143 103 : .subjectStart(subjectStart) 144 103 : .subjectEnd(subjectEnd) 145 103 : .changeMessageStart(changeMessageStart) 146 103 : .changeMessageEnd(changeMessageEnd) 147 103 : .build(); 148 : 149 103 : return Optional.of(range); 150 : } 151 : 152 : @AutoValue 153 103 : public abstract static class CommitMessageRange { 154 : 155 : public abstract int subjectStart(); 156 : 157 : public abstract int subjectEnd(); 158 : 159 : public abstract int changeMessageStart(); 160 : 161 : public abstract int changeMessageEnd(); 162 : 163 : public boolean hasChangeMessage() { 164 103 : return changeMessageStart() < changeMessageEnd(); 165 : } 166 : 167 : public static Builder builder() { 168 103 : return new AutoValue_ChangeNoteUtil_CommitMessageRange.Builder(); 169 : } 170 : 171 : @AutoValue.Builder 172 103 : public abstract static class Builder { 173 : 174 : abstract Builder subjectStart(int subjectStart); 175 : 176 : abstract Builder subjectEnd(int subjectEnd); 177 : 178 : abstract Builder changeMessageStart(int changeMessageStart); 179 : 180 : abstract Builder changeMessageEnd(int changeMessageEnd); 181 : 182 : abstract CommitMessageRange build(); 183 : } 184 : } 185 : 186 : /** Helper class for JSON serialization. Timestamp is taken from the commit. */ 187 : public static class AttentionStatusInNoteDb { 188 : 189 : final String personIdent; 190 : final AttentionSetUpdate.Operation operation; 191 : final String reason; 192 : 193 : AttentionStatusInNoteDb( 194 52 : String personIndent, AttentionSetUpdate.Operation operation, String reason) { 195 52 : this.personIdent = personIndent; 196 52 : this.operation = operation; 197 52 : this.reason = reason; 198 52 : } 199 : } 200 : 201 : /** The returned {@link Optional} holds the parsed entity or is empty if parsing failed. */ 202 : static Optional<AttentionSetUpdate> attentionStatusFromJson( 203 : Instant timestamp, String attentionString) { 204 52 : AttentionStatusInNoteDb inNoteDb = 205 52 : gson.fromJson(attentionString, AttentionStatusInNoteDb.class); 206 52 : PersonIdent personIdent = RawParseUtils.parsePersonIdent(inNoteDb.personIdent); 207 52 : if (personIdent == null) { 208 0 : return Optional.empty(); 209 : } 210 52 : Optional<Account.Id> account = NoteDbUtil.parseIdent(personIdent); 211 52 : return account.map( 212 : id -> 213 52 : AttentionSetUpdate.createFromRead(timestamp, id, inNoteDb.operation, inNoteDb.reason)); 214 : } 215 : 216 : String attentionSetUpdateToJson(AttentionSetUpdate attentionSetUpdate) { 217 52 : StringBuilder stringBuilder = new StringBuilder(); 218 52 : appendAccountIdIdentString(stringBuilder, attentionSetUpdate.account()); 219 52 : return gson.toJson( 220 : new AttentionStatusInNoteDb( 221 52 : stringBuilder.toString(), attentionSetUpdate.operation(), attentionSetUpdate.reason())); 222 : } 223 : }