Line data Source code
1 : // Copyright (C) 2009 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; 16 : 17 : import static java.util.Comparator.comparingInt; 18 : import static java.util.stream.Collectors.toSet; 19 : 20 : import com.google.common.collect.Ordering; 21 : import com.google.common.io.BaseEncoding; 22 : import com.google.gerrit.common.FooterConstants; 23 : import com.google.gerrit.entities.Change; 24 : import com.google.gerrit.entities.PatchSet; 25 : import com.google.gerrit.extensions.restapi.BadRequestException; 26 : import com.google.gerrit.extensions.restapi.ResourceConflictException; 27 : import com.google.gerrit.server.config.UrlFormatter; 28 : import com.google.gerrit.server.util.CommitMessageUtil; 29 : import com.google.inject.Singleton; 30 : import java.io.IOException; 31 : import java.security.SecureRandom; 32 : import java.util.Collection; 33 : import java.util.List; 34 : import java.util.Optional; 35 : import java.util.Random; 36 : import java.util.Set; 37 : import java.util.regex.Matcher; 38 : import java.util.regex.Pattern; 39 : import java.util.stream.Stream; 40 : import org.eclipse.jgit.lib.Constants; 41 : import org.eclipse.jgit.lib.ObjectId; 42 : import org.eclipse.jgit.lib.Ref; 43 : import org.eclipse.jgit.lib.Repository; 44 : import org.eclipse.jgit.revwalk.RevCommit; 45 : 46 : @Singleton 47 : public class ChangeUtil { 48 : public static final int TOPIC_MAX_LENGTH = 2048; 49 : 50 108 : private static final Random UUID_RANDOM = new SecureRandom(); 51 108 : private static final BaseEncoding UUID_ENCODING = BaseEncoding.base16().lowerCase(); 52 : 53 108 : public static final Ordering<PatchSet> PS_ID_ORDER = 54 108 : Ordering.from(comparingInt(PatchSet::number)); 55 : 56 : /** Returns a new unique identifier for change message entities. */ 57 : public static String messageUuid() { 58 29 : byte[] buf = new byte[8]; 59 29 : UUID_RANDOM.nextBytes(buf); 60 29 : return UUID_ENCODING.encode(buf, 0, 4) + '_' + UUID_ENCODING.encode(buf, 4, 4); 61 : } 62 : 63 : /** 64 : * Get the next patch set ID from a previously-read map of refs below the change prefix. 65 : * 66 : * @param changeRefNames existing full change ref names with the same change ID as {@code id}. 67 : * @param id previous patch set ID. 68 : * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref 69 : * names appear in the {@code changeRefs} map. 70 : */ 71 : public static PatchSet.Id nextPatchSetIdFromChangeRefs( 72 : Collection<String> changeRefNames, PatchSet.Id id) { 73 17 : return nextPatchSetIdFromChangeRefs(changeRefNames.stream(), id); 74 : } 75 : 76 : private static PatchSet.Id nextPatchSetIdFromChangeRefs( 77 : Stream<String> changeRefNames, PatchSet.Id id) { 78 34 : Set<PatchSet.Id> existing = 79 : changeRefNames 80 34 : .map(PatchSet.Id::fromRef) 81 34 : .filter(psId -> psId != null && psId.changeId().equals(id.changeId())) 82 34 : .collect(toSet()); 83 34 : PatchSet.Id next = nextPatchSetId(id); 84 34 : while (existing.contains(next)) { 85 1 : next = nextPatchSetId(next); 86 : } 87 34 : return next; 88 : } 89 : 90 : /** 91 : * Get the next patch set ID just looking at a single previous patch set ID. 92 : * 93 : * <p>This patch set ID may or may not be available in the database. 94 : * 95 : * @param id previous patch set ID. 96 : * @return next patch set ID for the same change, incrementing by 1. 97 : */ 98 : public static PatchSet.Id nextPatchSetId(PatchSet.Id id) { 99 52 : return PatchSet.id(id.changeId(), id.get() + 1); 100 : } 101 : 102 : /** 103 : * Get the next patch set ID from scanning refs in the repo. 104 : * 105 : * @param git repository to scan for patch set refs. 106 : * @param id previous patch set ID. 107 : * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref 108 : * names appear in the repository. 109 : */ 110 : public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) throws IOException { 111 27 : return nextPatchSetIdFromChangeRefs( 112 27 : git.getRefDatabase().getRefsByPrefix(id.changeId().toRefPrefix()).stream() 113 27 : .map(Ref::getName), 114 : id); 115 : } 116 : 117 : /** 118 : * Make sure that the change commit message has a correct footer. 119 : * 120 : * @param requireChangeId true if Change-Id is a mandatory footer for the project 121 : * @param currentChangeId current Change-Id value before the commit message is updated 122 : * @param newCommitMessage new commit message for the change 123 : * @throws ResourceConflictException if the new commit message has a missing or invalid Change-Id 124 : * @throws BadRequestException if the new commit message is null or empty 125 : */ 126 : public static void ensureChangeIdIsCorrect( 127 : boolean requireChangeId, String currentChangeId, String newCommitMessage) 128 : throws ResourceConflictException, BadRequestException { 129 : RevCommit revCommit = 130 10 : RevCommit.parse( 131 10 : Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage)); 132 : 133 : // Check that the commit message without footers is not empty 134 10 : CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage()); 135 : 136 10 : List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID); 137 10 : if (requireChangeId && changeIdFooters.isEmpty()) { 138 1 : throw new ResourceConflictException("missing Change-Id footer"); 139 : } 140 10 : if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) { 141 2 : throw new ResourceConflictException("wrong Change-Id footer"); 142 : } 143 10 : if (changeIdFooters.size() > 1) { 144 0 : throw new ResourceConflictException("multiple Change-Id footers"); 145 : } 146 10 : } 147 : 148 : public static String status(Change c) { 149 103 : return c != null ? c.getStatus().name().toLowerCase() : "deleted"; 150 : } 151 : 152 108 : private static final Pattern LINK_CHANGE_ID_PATTERN = Pattern.compile("I[0-9a-f]{40}"); 153 : 154 : public static List<String> getChangeIdsFromFooter(RevCommit c, UrlFormatter urlFormatter) { 155 108 : List<String> changeIds = c.getFooterLines(FooterConstants.CHANGE_ID); 156 108 : Optional<String> webUrl = urlFormatter.getWebUrl(); 157 108 : if (!webUrl.isPresent()) { 158 0 : return changeIds; 159 : } 160 : 161 108 : String prefix = webUrl.get() + "id/"; 162 108 : for (String link : c.getFooterLines(FooterConstants.LINK)) { 163 3 : if (!link.startsWith(prefix)) { 164 3 : continue; 165 : } 166 3 : String changeId = link.substring(prefix.length()); 167 3 : Matcher m = LINK_CHANGE_ID_PATTERN.matcher(changeId); 168 3 : if (m.matches()) { 169 3 : changeIds.add(changeId); 170 : } 171 3 : } 172 : 173 108 : return changeIds; 174 : } 175 : 176 : private ChangeUtil() {} 177 : }