com.googlesource.gerrit.plugins.uploadvalidator.DuplicatePathnameValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.googlesource.gerrit.plugins.uploadvalidator.DuplicatePathnameValidator.java

Source

// Copyright (C) 2016 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.googlesource.gerrit.plugins.uploadvalidator;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;

public class DuplicatePathnameValidator implements CommitValidationListener {

    public static AbstractModule module() {
        return new AbstractModule() {
            private List<String> getAvailableLocales() {
                return Lists.transform(Arrays.asList(Locale.getAvailableLocales()), new Function<Locale, String>() {
                    @Override
                    public String apply(Locale input) {
                        return input.toString();
                    }
                });
            }

            @Override
            protected void configure() {
                DynamicSet.bind(binder(), CommitValidationListener.class).to(DuplicatePathnameValidator.class);
                bind(ProjectConfigEntry.class).annotatedWith(Exports.named(KEY_REJECT_DUPLICATE_PATHNAMES))
                        .toInstance(new ProjectConfigEntry("Reject Duplicate Pathnames", null,
                                ProjectConfigEntryType.BOOLEAN, null, false,
                                "Pushes of commits that contain duplicate pathnames, or that "
                                        + "contain duplicates of existing pathnames will be "
                                        + "rejected. Pathnames y and z are considered to be "
                                        + "duplicates if they are equal, case-insensitive."));
                bind(ProjectConfigEntry.class).annotatedWith(Exports.named(KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE))
                        .toInstance(new ProjectConfigEntry("Reject Duplicate Pathnames Locale", "en",
                                ProjectConfigEntryType.STRING, getAvailableLocales(), false,
                                "To avoid problems caused by comparing pathnames with different "
                                        + "locales it is possible to use a specific locale. The "
                                        + "default is English (en)."));
            }
        };
    }

    public static final String KEY_REJECT_DUPLICATE_PATHNAMES = "rejectDuplicatePathnames";
    public static final String KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE = "rejectDuplicatePathnamesLocale";

    @VisibleForTesting
    static boolean isActive(PluginConfig cfg) {
        return cfg.getBoolean(KEY_REJECT_DUPLICATE_PATHNAMES, false);
    }

    @VisibleForTesting
    static Locale getLocale(PluginConfig cfg) {
        return Locale.forLanguageTag(cfg.getString(KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE, "en"));
    }

    @VisibleForTesting
    Map<String, String> allPaths(Collection<String> leafs) {
        Map<String, String> paths = new HashMap<>();
        for (String cp : leafs) {
            int n = cp.indexOf('/');
            while (n > -1) {
                String s = cp.substring(0, n);
                paths.put(s.toLowerCase(locale), s);
                n = cp.indexOf('/', n + 1);
            }
            paths.put(cp.toLowerCase(locale), cp);
        }
        return paths;
    }

    Set<String> allParentFolders(Collection<String> paths) {
        Set<String> folders = new HashSet<>();
        for (String cp : paths) {
            int n = cp.indexOf('/');
            while (n > -1) {
                String s = cp.substring(0, n);
                folders.add(s);
                n = cp.indexOf('/', n + 1);
            }
        }
        return folders;
    }

    @VisibleForTesting
    static CommitValidationMessage conflict(String f1, String f2) {
        return new CommitValidationMessage(f1 + ": pathname conflicts with " + f2, true);
    }

    private static boolean isDeleted(TreeWalk tw) {
        return FileMode.MISSING.equals(tw.getRawMode(0));
    }

    private final String pluginName;
    private final PluginConfigFactory cfgFactory;
    private final GitRepositoryManager repoManager;
    private final ValidatorConfig validatorConfig;

    private Locale locale;

    @VisibleForTesting
    void setLocale(Locale locale) {
        this.locale = locale;
    }

    @Inject
    DuplicatePathnameValidator(@PluginName String pluginName, PluginConfigFactory cfgFactory,
            GitRepositoryManager repoManager, ValidatorConfig validatorConfig) {
        this.pluginName = pluginName;
        this.cfgFactory = cfgFactory;
        this.repoManager = repoManager;
        this.validatorConfig = validatorConfig;
    }

    @Override
    public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
            throws CommitValidationException {
        try {
            PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(receiveEvent.project.getNameKey(),
                    pluginName);
            if (isActive(cfg) && validatorConfig.isEnabledForRef(receiveEvent.user,
                    receiveEvent.getProjectNameKey(), receiveEvent.getRefName(), KEY_REJECT_DUPLICATE_PATHNAMES)) {
                locale = getLocale(cfg);
                try (Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey())) {
                    List<CommitValidationMessage> messages = performValidation(repo, receiveEvent.commit,
                            receiveEvent.revWalk);
                    if (!messages.isEmpty()) {
                        throw new CommitValidationException("contains duplicate pathnames", messages);
                    }
                }
            }
        } catch (NoSuchProjectException | IOException e) {
            throw new CommitValidationException("failed to check for duplicate pathnames", e);
        }
        return Collections.emptyList();
    }

    @VisibleForTesting
    List<CommitValidationMessage> performValidation(Repository repo, RevCommit c, RevWalk revWalk)
            throws IOException {
        List<CommitValidationMessage> messages = new LinkedList<>();

        Set<String> pathnames = CommitUtils.getChangedPaths(repo, c, revWalk);
        checkForDuplicatesInSet(pathnames, messages);
        if (!messages.isEmpty() || c.getParentCount() == 0) {
            return messages;
        }

        try (TreeWalk tw = new TreeWalk(repo)) {
            tw.setRecursive(false);
            tw.addTree(c.getTree());
            checkForDuplicatesAgainstTheWholeTree(tw, pathnames, messages);
        }
        return messages;
    }

    @VisibleForTesting
    void checkForDuplicatesAgainstTheWholeTree(TreeWalk tw, Set<String> changed,
            List<CommitValidationMessage> messages) throws IOException {
        Map<String, String> all = allPaths(changed);

        while (tw.next()) {
            String currentPath = tw.getPathString();

            if (isDeleted(tw)) {
                continue;
            }

            String potentialDuplicate = all.get(currentPath.toLowerCase(locale));
            if (potentialDuplicate == null) {
                continue;
            } else if (potentialDuplicate.equals(currentPath)) {
                if (tw.isSubtree()) {
                    tw.enterSubtree();
                }
                continue;
            } else {
                messages.add(conflict(potentialDuplicate, currentPath));
            }
        }
    }

    private void checkForDuplicatesInSet(Set<String> files, List<CommitValidationMessage> messages) {
        Set<String> filesAndFolders = Sets.newHashSet(files);
        filesAndFolders.addAll(allParentFolders(files));
        Map<String, String> seen = new HashMap<>();
        for (String file : filesAndFolders) {
            String lc = file.toLowerCase(locale);
            String duplicate = seen.get(lc);
            if (duplicate != null) {
                messages.add(conflict(duplicate, file));
            } else {
                seen.put(lc, file);
            }
        }
    }
}