|  | // Copyright (C) 2013 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.entities; | 
|  |  | 
|  | import com.google.common.base.Splitter; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.gerrit.common.UsedAt; | 
|  | import java.util.List; | 
|  |  | 
|  | /** Constants and utilities for Gerrit-specific ref names. */ | 
|  | public class RefNames { | 
|  | public static final String HEAD = "HEAD"; | 
|  |  | 
|  | public static final String REFS = "refs/"; | 
|  |  | 
|  | public static final String REFS_HEADS = "refs/heads/"; | 
|  |  | 
|  | public static final String REFS_TAGS = "refs/tags/"; | 
|  |  | 
|  | public static final String REFS_CHANGES = "refs/changes/"; | 
|  |  | 
|  | public static final String REFS_META = "refs/meta/"; | 
|  |  | 
|  | /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */ | 
|  | public static final String REFS_REJECT_COMMITS = "refs/meta/reject-commits"; | 
|  |  | 
|  | /** Configuration settings for a project {@code refs/meta/config} */ | 
|  | public static final String REFS_CONFIG = "refs/meta/config"; | 
|  |  | 
|  | /** Note tree listing external IDs */ | 
|  | public static final String REFS_EXTERNAL_IDS = "refs/meta/external-ids"; | 
|  |  | 
|  | /** Magic user branch in All-Users {@code refs/users/self} */ | 
|  | public static final String REFS_USERS_SELF = "refs/users/self"; | 
|  |  | 
|  | /** Default user preference settings */ | 
|  | public static final String REFS_USERS_DEFAULT = RefNames.REFS_USERS + "default"; | 
|  |  | 
|  | /** Configurations of project-specific dashboards (canned search queries). */ | 
|  | public static final String REFS_DASHBOARDS = "refs/meta/dashboards/"; | 
|  |  | 
|  | /** Sequence counters in NoteDb. */ | 
|  | public static final String REFS_SEQUENCES = "refs/sequences/"; | 
|  |  | 
|  | /** NoteDb schema version number. */ | 
|  | public static final String REFS_VERSION = "refs/meta/version"; | 
|  |  | 
|  | /** | 
|  | * Prefix applied to merge commit base nodes. | 
|  | * | 
|  | * <p>References in this directory should take the form {@code refs/cache-automerge/xx/yyyy...} | 
|  | * where xx is the first two digits of the merge commit's object name, and yyyyy... is the | 
|  | * remaining 38. The reference should point to a treeish that is the automatic merge result of the | 
|  | * merge commit's parents. | 
|  | */ | 
|  | public static final String REFS_CACHE_AUTOMERGE = "refs/cache-automerge/"; | 
|  |  | 
|  | /** Suffix of a meta ref in the NoteDb. */ | 
|  | public static final String META_SUFFIX = "/meta"; | 
|  |  | 
|  | /** Suffix of a ref that stores robot comments in the NoteDb. */ | 
|  | public static final String ROBOT_COMMENTS_SUFFIX = "/robot-comments"; | 
|  |  | 
|  | public static final String EDIT_PREFIX = "edit-"; | 
|  |  | 
|  | /* | 
|  | * The following refs contain an account ID and should be visible only to that account. | 
|  | * | 
|  | * Parsing the account ID from the ref is implemented in Account.Id#fromRef(String). This ensures | 
|  | * that VisibleRefFilter hides those refs from other users. | 
|  | * | 
|  | * This applies to: | 
|  | * - User branches (e.g. 'refs/users/23/1011123') | 
|  | * - Draft comment refs (e.g. 'refs/draft-comments/73/67473/1011123') | 
|  | * - Starred changes refs (e.g. 'refs/starred-changes/73/67473/1011123') | 
|  | */ | 
|  |  | 
|  | /** Preference settings for a user {@code refs/users} */ | 
|  | public static final String REFS_USERS = "refs/users/"; | 
|  |  | 
|  | /** NoteDb ref for a group {@code refs/groups} */ | 
|  | public static final String REFS_GROUPS = "refs/groups/"; | 
|  |  | 
|  | /** NoteDb ref for the NoteMap of all group names */ | 
|  | public static final String REFS_GROUPNAMES = "refs/meta/group-names"; | 
|  |  | 
|  | /** | 
|  | * NoteDb ref for deleted groups {@code refs/deleted-groups}. This ref namespace is foreseen as an | 
|  | * attic for deleted groups (it's reserved but not used yet) | 
|  | */ | 
|  | public static final String REFS_DELETED_GROUPS = "refs/deleted-groups/"; | 
|  |  | 
|  | /** Draft inline comments of a user on a change */ | 
|  | public static final String REFS_DRAFT_COMMENTS = "refs/draft-comments/"; | 
|  |  | 
|  | /** A change starred by a user */ | 
|  | public static final String REFS_STARRED_CHANGES = "refs/starred-changes/"; | 
|  |  | 
|  | /** | 
|  | * List of refs managed by Gerrit. Covers all Gerrit internal refs. | 
|  | * | 
|  | * <p><b>Caution</b> Any ref not in this list will be served if the user was granted a READ | 
|  | * permission on it using Gerrit's permission model. | 
|  | */ | 
|  | public static final ImmutableList<String> GERRIT_REFS = | 
|  | ImmutableList.of( | 
|  | REFS_CHANGES, | 
|  | REFS_EXTERNAL_IDS, | 
|  | REFS_CACHE_AUTOMERGE, | 
|  | REFS_DRAFT_COMMENTS, | 
|  | REFS_DELETED_GROUPS, | 
|  | REFS_SEQUENCES, | 
|  | REFS_GROUPS, | 
|  | REFS_GROUPNAMES, | 
|  | REFS_USERS, | 
|  | REFS_STARRED_CHANGES, | 
|  | REFS_REJECT_COMMITS); | 
|  |  | 
|  | private static final Splitter SPLITTER = Splitter.on("/"); | 
|  |  | 
|  | public static String fullName(String ref) { | 
|  | return (ref.startsWith(REFS) || ref.equals(HEAD)) ? ref : REFS_HEADS + ref; | 
|  | } | 
|  |  | 
|  | public static final String shortName(String ref) { | 
|  | if (ref.startsWith(REFS_HEADS)) { | 
|  | return ref.substring(REFS_HEADS.length()); | 
|  | } else if (ref.startsWith(REFS_TAGS)) { | 
|  | return ref.substring(REFS_TAGS.length()); | 
|  | } | 
|  | return ref; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Warning: Change refs have to manually be advertised in {@code | 
|  | * com.google.gerrit.server.permissions.DefaultRefFilter}; this should be done when adding new | 
|  | * change refs. | 
|  | */ | 
|  | public static String changeMetaRef(Change.Id id) { | 
|  | StringBuilder r = newStringBuilder().append(REFS_CHANGES); | 
|  | return shard(id.get(), r).append(META_SUFFIX).toString(); | 
|  | } | 
|  |  | 
|  | public static String patchSetRef(PatchSet.Id id) { | 
|  | StringBuilder r = newStringBuilder().append(REFS_CHANGES); | 
|  | return shard(id.changeId().get(), r).append('/').append(id.get()).toString(); | 
|  | } | 
|  |  | 
|  | public static String changeRefPrefix(Change.Id id) { | 
|  | StringBuilder r = newStringBuilder().append(REFS_CHANGES); | 
|  | return shard(id.get(), r).append('/').toString(); | 
|  | } | 
|  |  | 
|  | public static String robotCommentsRef(Change.Id id) { | 
|  | StringBuilder r = newStringBuilder().append(REFS_CHANGES); | 
|  | return shard(id.get(), r).append(ROBOT_COMMENTS_SUFFIX).toString(); | 
|  | } | 
|  |  | 
|  | public static boolean isNoteDbMetaRef(String ref) { | 
|  | if (ref.startsWith(REFS_CHANGES) | 
|  | && (ref.endsWith(META_SUFFIX) || ref.endsWith(ROBOT_COMMENTS_SUFFIX))) { | 
|  | return true; | 
|  | } | 
|  | if (ref.startsWith(REFS_DRAFT_COMMENTS) || ref.startsWith(REFS_STARRED_CHANGES)) { | 
|  | return true; | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | /** True if the provided ref is in {@code refs/changes/*}. */ | 
|  | public static boolean isRefsChanges(String ref) { | 
|  | return ref.startsWith(REFS_CHANGES); | 
|  | } | 
|  |  | 
|  | public static String refsGroups(AccountGroup.UUID groupUuid) { | 
|  | return REFS_GROUPS + shardUuid(groupUuid.get()); | 
|  | } | 
|  |  | 
|  | public static String refsDeletedGroups(AccountGroup.UUID groupUuid) { | 
|  | return REFS_DELETED_GROUPS + shardUuid(groupUuid.get()); | 
|  | } | 
|  |  | 
|  | public static String refsUsers(Account.Id accountId) { | 
|  | StringBuilder r = newStringBuilder().append(REFS_USERS); | 
|  | return shard(accountId.get(), r).toString(); | 
|  | } | 
|  |  | 
|  | public static String refsDraftComments(Change.Id changeId, Account.Id accountId) { | 
|  | return buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get()).append(accountId.get()).toString(); | 
|  | } | 
|  |  | 
|  | public static String refsDraftCommentsPrefix(Change.Id changeId) { | 
|  | return buildRefsPrefix(REFS_DRAFT_COMMENTS, changeId.get()).toString(); | 
|  | } | 
|  |  | 
|  | public static String refsStarredChanges(Change.Id changeId, Account.Id accountId) { | 
|  | return buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get()).append(accountId.get()).toString(); | 
|  | } | 
|  |  | 
|  | public static String refsStarredChangesPrefix(Change.Id changeId) { | 
|  | return buildRefsPrefix(REFS_STARRED_CHANGES, changeId.get()).toString(); | 
|  | } | 
|  |  | 
|  | private static StringBuilder buildRefsPrefix(String prefix, int id) { | 
|  | StringBuilder r = newStringBuilder().append(prefix); | 
|  | return shard(id, r).append('/'); | 
|  | } | 
|  |  | 
|  | public static String refsCacheAutomerge(String hash) { | 
|  | return REFS_CACHE_AUTOMERGE + hash.substring(0, 2) + '/' + hash.substring(2); | 
|  | } | 
|  |  | 
|  | public static String shard(int id) { | 
|  | if (id < 0) { | 
|  | return null; | 
|  | } | 
|  | return shard(id, newStringBuilder()).toString(); | 
|  | } | 
|  |  | 
|  | private static StringBuilder shard(int id, StringBuilder sb) { | 
|  | int n = id % 100; | 
|  | if (n < 10) { | 
|  | sb.append('0'); | 
|  | } | 
|  | sb.append(n); | 
|  | sb.append('/'); | 
|  | sb.append(id); | 
|  | return sb; | 
|  | } | 
|  |  | 
|  | @UsedAt(UsedAt.Project.PLUGINS_ALL) | 
|  | public static String shardUuid(String uuid) { | 
|  | if (uuid == null || uuid.length() < 2) { | 
|  | throw new IllegalArgumentException("UUIDs must consist of at least two characters"); | 
|  | } | 
|  | return uuid.substring(0, 2) + '/' + uuid; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns reference for this change edit with sharded user and change number: | 
|  | * refs/users/UU/UUUU/edit-CCCC/P. | 
|  | * | 
|  | * @param accountId account id | 
|  | * @param changeId change number | 
|  | * @param psId patch set number | 
|  | * @return reference for this change edit | 
|  | */ | 
|  | public static String refsEdit(Account.Id accountId, Change.Id changeId, PatchSet.Id psId) { | 
|  | return refsEditPrefix(accountId, changeId) + psId.get(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns reference prefix for this change edit with sharded user and change number: | 
|  | * refs/users/UU/UUUU/edit-CCCC/. | 
|  | * | 
|  | * @param accountId account id | 
|  | * @param changeId change number | 
|  | * @return reference prefix for this change edit | 
|  | */ | 
|  | public static String refsEditPrefix(Account.Id accountId, Change.Id changeId) { | 
|  | return refsEditPrefix(accountId) + changeId.get() + '/'; | 
|  | } | 
|  |  | 
|  | public static String refsEditPrefix(Account.Id accountId) { | 
|  | return refsUsers(accountId) + '/' + EDIT_PREFIX; | 
|  | } | 
|  |  | 
|  | public static boolean isRefsEdit(String ref) { | 
|  | return ref != null && ref.startsWith(REFS_USERS) && ref.contains(EDIT_PREFIX); | 
|  | } | 
|  |  | 
|  | public static boolean isRefsUsers(String ref) { | 
|  | return ref.startsWith(REFS_USERS); | 
|  | } | 
|  |  | 
|  | public static boolean isRefsUsersSelf(String ref, boolean isAllUsers) { | 
|  | return isAllUsers && REFS_USERS_SELF.equals(ref); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Whether the ref is a group branch that stores NoteDb data of a group. Returns {@code true} for | 
|  | * all refs that start with {@code refs/groups/}. | 
|  | */ | 
|  | public static boolean isRefsGroups(String ref) { | 
|  | return ref.startsWith(REFS_GROUPS); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Whether the ref is a group branch that stores NoteDb data of a deleted group. Returns {@code | 
|  | * true} for all refs that start with {@code refs/deleted-groups/}. | 
|  | */ | 
|  | public static boolean isRefsDeletedGroups(String ref) { | 
|  | return ref.startsWith(REFS_DELETED_GROUPS); | 
|  | } | 
|  |  | 
|  | /** Returns true if the provided ref is for draft comments. */ | 
|  | public static boolean isRefsDraftsComments(String ref) { | 
|  | return ref.startsWith(REFS_DRAFT_COMMENTS); | 
|  | } | 
|  |  | 
|  | /** Returns true if the provided ref is for starred changes. */ | 
|  | public static boolean isRefsStarredChanges(String ref) { | 
|  | return ref.startsWith(REFS_STARRED_CHANGES); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Whether the ref is used for storing group data in NoteDb. Returns {@code true} for all group | 
|  | * branches, refs/meta/group-names and deleted group branches. | 
|  | */ | 
|  | public static boolean isGroupRef(String ref) { | 
|  | return isRefsGroups(ref) || isRefsDeletedGroups(ref) || REFS_GROUPNAMES.equals(ref); | 
|  | } | 
|  |  | 
|  | /** Whether the ref is the configuration branch, i.e. {@code refs/meta/config}, for a project. */ | 
|  | public static boolean isConfigRef(String ref) { | 
|  | return REFS_CONFIG.equals(ref); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Whether the ref is managed by Gerrit. Covers all Gerrit-internal refs like refs/cache-automerge | 
|  | * and refs/meta as well as refs/changes. Does not cover user-created refs like branches or custom | 
|  | * ref namespaces like refs/my-company. | 
|  | * | 
|  | * <p>Any ref for which this method evaluates to true will be served to users who have the {@code | 
|  | * ACCESS_DATABASE} capability. | 
|  | * | 
|  | * <p><b>Caution</b> Any ref not in this list will be served if the user was granted a READ | 
|  | * permission on it using Gerrit's permission model. | 
|  | */ | 
|  | public static boolean isGerritRef(String ref) { | 
|  | return GERRIT_REFS.stream().anyMatch(internalRef -> ref.startsWith(internalRef)); | 
|  | } | 
|  |  | 
|  | static Integer parseShardedRefPart(String name) { | 
|  | if (name == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | List<String> parts = SPLITTER.splitToList(name); | 
|  | int n = parts.size(); | 
|  | if (n < 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Last 2 digits. | 
|  | int le; | 
|  | for (le = 0; le < parts.get(0).length(); le++) { | 
|  | if (!Character.isDigit(parts.get(0).charAt(le))) { | 
|  | return null; | 
|  | } | 
|  | } | 
|  | if (le != 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Full ID. | 
|  | int ie; | 
|  | for (ie = 0; ie < parts.get(1).length(); ie++) { | 
|  | if (!Character.isDigit(parts.get(1).charAt(ie))) { | 
|  | if (ie == 0) { | 
|  | return null; | 
|  | } | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | int shard = Integer.parseInt(parts.get(0)); | 
|  | int id = Integer.parseInt(parts.get(1).substring(0, ie)); | 
|  |  | 
|  | if (id % 100 != shard) { | 
|  | return null; | 
|  | } | 
|  | return id; | 
|  | } | 
|  |  | 
|  | @UsedAt(UsedAt.Project.PLUGINS_ALL) | 
|  | public static String parseShardedUuidFromRefPart(String name) { | 
|  | if (name == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | List<String> parts = SPLITTER.splitToList(name); | 
|  | int n = parts.size(); | 
|  | if (n != 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // First 2 chars. | 
|  | if (parts.get(0).length() != 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Full UUID. | 
|  | String uuid = parts.get(1); | 
|  | if (!uuid.startsWith(parts.get(0))) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | return uuid; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Skips a sharded ref part at the beginning of the name. | 
|  | * | 
|  | * <p>E.g.: "01/1" -> "", "01/1/" -> "/", "01/1/2" -> "/2", "01/1-edit" -> "-edit" | 
|  | * | 
|  | * @param name ref part name | 
|  | * @return the rest of the name, {@code null} if the ref name part doesn't start with a valid | 
|  | *     sharded ID | 
|  | */ | 
|  | static String skipShardedRefPart(String name) { | 
|  | if (name == null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | List<String> parts = SPLITTER.splitToList(name); | 
|  | int n = parts.size(); | 
|  | if (n < 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Last 2 digits. | 
|  | int le; | 
|  | for (le = 0; le < parts.get(0).length(); le++) { | 
|  | if (!Character.isDigit(parts.get(0).charAt(le))) { | 
|  | return null; | 
|  | } | 
|  | } | 
|  | if (le != 2) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Full ID. | 
|  | int ie; | 
|  | for (ie = 0; ie < parts.get(1).length(); ie++) { | 
|  | if (!Character.isDigit(parts.get(1).charAt(ie))) { | 
|  | if (ie == 0) { | 
|  | return null; | 
|  | } | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | int shard = Integer.parseInt(parts.get(0)); | 
|  | int id = Integer.parseInt(parts.get(1).substring(0, ie)); | 
|  |  | 
|  | if (id % 100 != shard) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | return name.substring(2 + 1 + ie); // 2 for the length of the shard, 1 for the '/' | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Parses an ID that follows a sharded ref part at the beginning of the name. | 
|  | * | 
|  | * <p>E.g.: "01/1/2" -> 2, "01/1/2/4" -> 2, ""01/1/2-edit" -> 2 | 
|  | * | 
|  | * @param name ref part name | 
|  | * @return ID that follows the sharded ref part at the beginning of the name, {@code null} if the | 
|  | *     ref name part doesn't start with a valid sharded ID or if no valid ID follows the sharded | 
|  | *     ref part | 
|  | */ | 
|  | static Integer parseAfterShardedRefPart(String name) { | 
|  | String rest = skipShardedRefPart(name); | 
|  | if (rest == null || !rest.startsWith("/")) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | rest = rest.substring(1); | 
|  |  | 
|  | int ie; | 
|  | for (ie = 0; ie < rest.length(); ie++) { | 
|  | if (!Character.isDigit(rest.charAt(ie))) { | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (ie == 0) { | 
|  | return null; | 
|  | } | 
|  | return Integer.parseInt(rest.substring(0, ie)); | 
|  | } | 
|  |  | 
|  | public static Integer parseRefSuffix(String name) { | 
|  | if (name == null) { | 
|  | return null; | 
|  | } | 
|  | int i = name.length(); | 
|  | while (i > 0) { | 
|  | char c = name.charAt(i - 1); | 
|  | if (c == '/') { | 
|  | break; | 
|  | } else if (!Character.isDigit(c)) { | 
|  | return null; | 
|  | } | 
|  | i--; | 
|  | } | 
|  | if (i == 0) { | 
|  | return null; | 
|  | } | 
|  | return Integer.valueOf(name.substring(i)); | 
|  | } | 
|  |  | 
|  | private static StringBuilder newStringBuilder() { | 
|  | // Many refname types in this file are always are longer than the default of 16 chars, so | 
|  | // presize StringBuilders larger by default. This hurts readability less than accurate | 
|  | // calculations would, at a negligible cost to memory overhead. | 
|  | return new StringBuilder(64); | 
|  | } | 
|  |  | 
|  | private RefNames() {} | 
|  | } |