package com.atlassian.stash.internal.scm.git;

import com.atlassian.bitbucket.commit.Changeset;
import com.atlassian.bitbucket.commit.Commit;
import com.atlassian.bitbucket.commit.CommitCallback;
import com.atlassian.bitbucket.commit.graph.TraversalCallback;
import com.atlassian.bitbucket.content.*;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.io.TypeAwareOutputSupplier;
import com.atlassian.bitbucket.io.TypeDetectingCopyOutputHandler;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.scm.*;
import com.atlassian.bitbucket.scm.git.GitRefPattern;
import com.atlassian.bitbucket.scm.git.command.*;
import com.atlassian.bitbucket.scm.git.command.diff.*;
import com.atlassian.bitbucket.scm.git.command.foreachref.GitForEachRefBuilder;
import com.atlassian.bitbucket.scm.git.command.log.GitLogBuilder;
import com.atlassian.bitbucket.scm.git.command.revlist.*;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.validation.ArgumentValidationException;
import com.atlassian.stash.internal.scm.git.command.*;
import com.atlassian.stash.internal.scm.git.command.blame.IncrementalBlameOutputHandler;
import com.atlassian.stash.internal.scm.git.command.blame.PorcelainBlameOutputHandler;
import com.atlassian.stash.internal.scm.git.command.catfile.CallbackCatFileOutputHandler;
import com.atlassian.stash.internal.scm.git.command.catfile.TypeCatFileOutputHandler;
import com.atlassian.stash.internal.scm.git.command.diff.*;
import com.atlassian.stash.internal.scm.git.command.foreachref.*;
import com.atlassian.stash.internal.scm.git.command.log.LogCommandExitHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.*;
import com.atlassian.stash.internal.scm.git.submodule.GitSubmoduleHelper;
import com.atlassian.stash.internal.scm.git.submodule.Submodules;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Function;

/**
 * @since 1.3
 */
@SuppressWarnings("deprecation")
public class DefaultGitCommandFactory implements GitCommandFactory {

    private static final Logger log = LoggerFactory.getLogger(DefaultGitCommandFactory.class);

    private final InternalGitAgent agent;
    private final GitCommandBuilderFactory builderFactory;
    private final InternalGitScmConfig config;
    private final ScheduledExecutorService executorService;
    private final GitExtendedCommandFactory extendedCommandFactory;
    private final I18nService i18nService;
    private final GitRepositoryConfig repositoryConfig;
    private final GitSubmoduleHelper submoduleHelper;

    public DefaultGitCommandFactory(InternalGitAgent agent, GitCommandBuilderFactory builderFactory,
                                    InternalGitScmConfig config, ScheduledExecutorService executorService,
                                    GitExtendedCommandFactory extendedCommandFactory, I18nService i18nService,
                                    GitRepositoryConfig repositoryConfig, GitSubmoduleHelper submoduleHelper) {
        this.agent = agent;
        this.builderFactory = builderFactory;
        this.executorService = executorService;
        this.config = config;
        this.extendedCommandFactory = extendedCommandFactory;
        this.i18nService = i18nService;
        this.repositoryConfig = repositoryConfig;
        this.submoduleHelper = submoduleHelper;
    }

    @Nonnull
    @Override
    public GitCommand<List<Blame>> blame(@Nonnull Repository repository, @Nonnull BlameCommandParameters parameters,
                                         @Nonnull PageRequest pageRequest) {
        return builder(repository).blame()
                .incremental()
                .file(parameters.getPath())
                .range(pageRequest)
                .rev(parameters.getCommitId())
                .build(new IncrementalBlameOutputHandler());
    }

    @Nonnull
    @Override
    public GitCommand<Void> branches(@Nonnull Repository repository, @Nonnull BranchesCommandParameters parameters,
                                     @Nonnull BranchCallback callback) {
        return createForEachRefCommand(repository, parameters, GitRefPattern.HEADS,
                new CallbackForEachRefOutputHandler(
                        new ForEachBranchCallback(callback, parameters.getFilterText(), agent.getHead(repository))));
    }

    @Nonnull
    @Override
    public GitCommand<Page<Branch>> branches(@Nonnull Repository repository,
                                             @Nonnull BranchesCommandParameters parameters,
                                             @Nonnull PageRequest pageRequest) {
        return createForEachRefCommand(repository, parameters, GitRefPattern.HEADS,
                new PagedForEachRefOutputHandler<>(new ForEachBranchParser(agent.getHead(repository)),
                        parameters.getFilterText(), pageRequest));
    }

    @Nonnull
    @Override
    public GitCommand<Void> changes(@Nonnull Repository repository, @Nonnull ChangesCommandParameters parameters,
                                    @Nonnull ChangeCallback callback) {
        return createChangesCommand(repository, parameters,
                new CallbackDiffTreeOutputHandler(callback, parameters.toContext()));
    }

    @Nonnull
    @Override
    public GitCommand<Page<Change>> changes(@Nonnull Repository repository,
                                            @Nonnull ChangesCommandParameters parameters,
                                            @Nonnull PageRequest pageRequest) {
        return createChangesCommand(repository, parameters, new ChangeDiffTreeOutputHandler(pageRequest));
    }

    @Nonnull
    @Override
    public GitCommand<Page<Changeset>> changesets(@Nonnull Repository repository,
                                                  @Nonnull ChangesetsCommandParameters parameters,
                                                  @Nonnull PageRequest pageRequest) {
        BatchChangeDiffTreeHandler handler = new BatchChangeDiffTreeHandler(i18nService, repository, parameters, pageRequest);

        GitDiffTreeBuilder builder = builder(repository).diffTree()
                .always(true)
                .format(handler.getFormat())
                .inputHandler(handler)
                .recursive(true)
                .root(true);
        return repositoryConfig.getDiffStrategy(repository).apply(builder)
                .build(handler);
    }

    @Nonnull
    @Override
    public GitCommand<Commit> commit(@Nonnull Repository repository, @Nonnull CommitCommandParameters parameters) {
        String commitId = parameters.getCommitId();
        String path = parameters.getPath();
        SingleCommitRevListOutputHandler handler = new SingleCommitRevListOutputHandler(
                i18nService, repository, commitId, parameters.hasPath() ? path : null);

        //--no-walk=unsorted is not supported by the type-safe rev-list builder, since it isn't available in 1.7.6,
        //so we have to use the free-form builder to allow specifying the arguments directly. This can be removed in
        //4.0 when the minimum Git version is raised to 1.8.0
        GitScmCommandBuilder builder = builder(repository).command("rev-list")
                .argument("--format=" + handler.getFormat());
        if (parameters.hasPath()) {
            //Because there is a path we _cannot_ use --no-walk here. If the starting commit does not modify the
            //specified path, we need to walk until we find one that does. Instead we limit the output to the
            //first matching commit
            builder.argument("-1")
                    .argument(commitId)
                    .argument("--")
                    .argument(path);
        } else {
            //Git 1.8.0 and higher offer a variant of --no-walk that allows us to disable sorting. This ensures
            //the commits are always returned in the order we asked for them, regardless of their commit dates,
            //which means we can safely read them in order they're output
            builder.argument("--no-walk=unsorted");

            //If there is no path provided, use the ^@ syntax to include the parents (if any) of the requested commit,
            //paired with --no-walk to ensure we only list those commits (as opposed to limit, because we don't know,
            //here, how many parents there might be).
            builder.argument(commitId)
                    .argument(commitId + "^@")
                    .argument("--");
        }

        return builder.build(handler);
    }

    @Nonnull
    @Override
    public GitCommand<Page<Commit>> commits(@Nonnull Repository repository,
                                            @Nonnull CommitsCommandParameters parameters,
                                            @Nonnull PageRequest pageRequest) {
        PagedCommitRevListOutputHandler handler = new PagedCommitRevListOutputHandler(repository, parameters, pageRequest);

        return parameters.isFollowingRenames() ?
                createLogCommand(repository, parameters, handler) :
                createRevListCommand(repository, parameters, handler);
    }

    @Nonnull
    @Override
    public GitCommand<Void> commits(@Nonnull Repository repository, @Nonnull CommitsCommandParameters parameters,
                                    @Nonnull CommitCallback callback) {
        RevListOutputHandler<Void> handler = new CallbackRevListOutputHandler(repository, callback, parameters);

        return parameters.isFollowingRenames() ?
                createLogCommand(repository, parameters, handler) :
                createRevListCommand(repository, parameters, handler);
    }

    @Nonnull
    @Override
    public GitCommand<Void> create(@Nonnull Repository repository) {
        return new CreateCommand(executorService, builderFactory, config, i18nService, repository);
    }

    @Nonnull
    @Override
    public GitCommand<Branch> defaultBranch(@Nonnull final Repository repository) {
        return new SimpleGitCommand<Branch>(executorService) {

            @Override
            public Branch call() {
                return agent.resolveHead(repository);
            }
        };
    }

    @Nonnull
    @Override
    public GitCommand<Void> delete(@Nonnull Repository repository, @Nonnull DeleteCommandParameters parameters) {
        return new DeleteCommand(config, executorService, repository, parameters);
    }

    @Nonnull
    @Override
    public GitCommand<Void> diff(@Nonnull Repository repository, @Nonnull DiffCommandParameters parameters,
                                 @Nonnull DiffContentCallback callback) {
        GitDiffBuilder builder = builder(repository).diff()
                .ancestor(parameters.getSinceId())
                .color(GitDiffColor.NEVER)
                .dstPrefix(DiffOutputHandler.DST_PREFIX)
                .whitespace(GitDiffWhitespace.fromDiffWhitespace(parameters.getWhitespace()))
                .paths(parameters.getPaths())
                .rev(parameters.getUntilId())
                .srcPrefix(DiffOutputHandler.SRC_PREFIX);
        if (parameters.hasContextLines()) {
            builder.contextLines(parameters.getContextLines());
        }
        return repositoryConfig.getDiffStrategy(repository).apply(builder)
                .build(new DiffOutputHandler(callback, parameters.toContext()));
    }

    @Nonnull
    @Override
    public GitCommand<Void> directory(@Nonnull Repository repository, @Nonnull DirectoryCommandParameters parameters,
                                      @Nonnull ContentTreeCallback callback, @Nonnull PageRequest pageRequest) {
        Submodules submodules = submoduleHelper.getSubmodules(repository, parameters.getCommitId(), parameters.getPath());

        return new DirectoryCommand(executorService, builderFactory, config,
                repository, parameters, callback, pageRequest, submodules);
    }

    @Nonnull
    @Override
    public GitCommand<Void> file(@Nonnull Repository repository, @Nonnull FileCommandParameters parameters,
                                 @Nonnull FileContentCallback callback, @Nonnull PageRequest pageRequest) {
        FileContext context = parameters.toContext();

        if (parameters.isAnnotated()) {
            log.debug("Using git blame to retrieve content and blame for {} at {}",
                    parameters.getPath(), parameters.getCommitId());
            return builder(repository).blame()
                    .porcelain()
                    .file(parameters.getPath())
                    .range(pageRequest)
                    .rev(parameters.getCommitId())
                    .build(new PorcelainBlameOutputHandler(callback, context, pageRequest));
        }

        log.debug("Using git cat-file to retrieve content for {} at {}", parameters.getPath(), parameters.getCommitId());
        return builder(repository).catFile()
                .pretty()
                .object(parameters.getCommitId(), parameters.getPath())
                .build(new CallbackCatFileOutputHandler(callback, context, pageRequest));
    }

    @Deprecated
    @Nonnull
    @Override
    public GitCommand<Void> fork(@Nonnull Repository repository, @Nonnull Repository fork) {
        return extendedCommandFactory.fork(repository, new ForkCommandParameters.Builder(fork).build());
    }

    @Nonnull
    @Override
    public GitCommand<Void> heads(@Nonnull Repository repository, @Nonnull RefCallback callback) {
        HeadsForEachRefOutputHandler handler = new HeadsForEachRefOutputHandler(callback, agent.getHead(repository));

        return builder(repository).forEachRef()
                .format(handler.getFormat())
                .patterns(GitRefPattern.HEADS.getPath(), GitRefPattern.TAGS.getPath())
                .build(handler);
    }

    @Deprecated
    @Nonnull
    @Override
    public GitCommand<Branch> merge(@Nonnull Repository toRepository, @Nonnull MergeCommandParameters parameters) {
        return extendedCommandFactory.merge(toRepository, parameters);
    }

    @Nonnull
    @Override
    public GitCommand<Void> rawFile(@Nonnull Repository repository, @Nonnull RawFileCommandParameters parameters,
                                    @Nonnull TypeAwareOutputSupplier outputStreamSupplier) {
        return builder(repository).catFile()
                .pretty()
                .object(parameters.getCommitId(), parameters.getPath())
                .build(new TypeDetectingCopyOutputHandler(outputStreamSupplier, parameters.getPath(), 8192));
    }

    @Override
    public GitCommand<Ref> resolveRef(@Nonnull Repository repository, @Nonnull ResolveRefCommandParameters parameters) {
        ResolveRefsCommandParameters.Builder builder = new ResolveRefsCommandParameters.Builder();
        parameters.getType()
                .<Function<String, ResolveRefsCommandParameters.Builder>>map(type -> {
                    if (StandardRefType.BRANCH == type) {
                        return builder::branchId;
                    }
                    if (StandardRefType.TAG == type) {
                        return builder::tagId;
                    }
                    return null;
                })
                .orElse(builder::refId)
                .apply(parameters.getRefId());

        return new TransformedGitCommand<>(resolveRefs(repository, builder.build()),
                refsById -> refsById == null ? null : refsById.get(parameters.getRefId()));
    }

    @Deprecated
    @Nonnull
    @Override
    public GitCommand<Ref> resolveRef(@Nonnull Repository repository, @Nonnull String refId) {
        ResolveRefsCommandParameters parameters = new ResolveRefsCommandParameters.Builder()
                .refId(refId)
                .build();

        return new TransformedGitCommand<>(resolveRefs(repository, parameters),
                refsById -> refsById == null ? null : refsById.get(refId));
    }

    @Nonnull
    @Override
    public GitCommand<Map<String, Ref>> resolveRefs(@Nonnull Repository repository,
                                                    @Nonnull ResolveRefsCommandParameters parameters) {
        return new ResolveRefsCommand(executorService, agent, builderFactory, i18nService, repository, parameters);
    }

    @Nonnull
    @Override
    public GitCommand<Page<Tag>> tags(@Nonnull Repository repository, @Nonnull TagsCommandParameters parameters,
                                      @Nonnull PageRequest pageRequest) {
        return createForEachRefCommand(repository, parameters, GitRefPattern.TAGS,
                new PagedForEachRefOutputHandler<>(ForEachTagParser.INSTANCE, parameters.getFilterText(), pageRequest));
    }

    @Nonnull
    @Override
    public GitCommand<Void> tags(@Nonnull Repository repository, @Nonnull TagsCommandParameters parameters,
                                 @Nonnull TagCallback callback) {
        return createForEachRefCommand(repository, parameters, GitRefPattern.TAGS,
                new CallbackForEachRefOutputHandler(new ForEachTagCallback(callback, parameters.getFilterText())));
    }

    @Nonnull
    @Override
    public GitCommand<ContentTreeNode.Type> type(@Nonnull Repository repository,
                                                 @Nonnull TypeCommandParameters parameters) {
        return builder(repository)
                .catFile()
                .type()
                .object(parameters.getCommitId(), parameters.getPath())
                .build(new TypeCatFileOutputHandler());
    }

    @Nonnull
    @Override
    public GitCommand<Void> traverseCommits(@Nonnull Repository repository, @Nonnull TraversalCallback callback) {
        return builder(repository)
                .revList()
                .branches(true)
                .ignoreMissing(true)
                .order(GitRevListOrder.TOPOLOGICAL)
                .parents(true)
                .tags(true)
                .build(new TraversalRevListOutputHandler(callback));
    }

    @Deprecated
    @Nonnull
    @Override
    public GitCommand<Void> updateDefaultBranch(@Nonnull Repository repository, @Nonnull String branchName) {
        return extendedCommandFactory.updateDefaultBranch(repository,
                new UpdateDefaultBranchCommandParameters.Builder(branchName).build());
    }

    @Nonnull
    private GitScmCommandBuilder builder(Repository repository) {
        return builderFactory.builder(repository);
    }

    private <T> GitCommand<T> createChangesCommand(Repository repository, ChangesCommandParameters parameters,
                                                   DiffTreeOutputHandler<T> handler) {
        GitDiffTreeBuilder builder = builder(repository).diffTree()
                .format(handler.getFormat())
                .paths(parameters.getPaths())
                .recursive(true)
                .rev(parameters.getUntilId());
        if (parameters.hasSinceId()) {
            builder.ancestor(parameters.getSinceId());
        } else {
            //Note: merges(true) in conjunction with always(true) does _not_ display empty diff-trees for merge commits
            //      with multiple parents if the diff between the merge and a given parent is empty. As a result, for
            //      merge commits, there is no way to know which parent the tree is relative to. As a result, we just
            //      accept diff-tree's default behaviour of showing nothing for merge commits.
            //This is a likely candidate for enhancement later. The processing done by BatchChangeDiffTreeHandler would
            //enable getting the diff to the first parent reliably, even in cases when the tree would be empty. However,
            //that approach has the caveat of not supporting branch or tag names--it requires full 40 byte hashes.
            builder.always(true)
                    .root(true);
        }
        return repositoryConfig.getDiffStrategy(repository).apply(builder)
                .build(handler);
    }

    /**
     * Creates a {@link GitForEachRefBuilder GitForEachRefBuilder} and configures
     * it with the {@link ForEachRefOutputHandler#getFormat() format} from the provided {@code handler} and the
     * {@link GitRefPattern#getPath() path} from the provided {@code pattern}. The pattern is also used to determine how
     * sorting should be applied.
     * <p>
     * A default ordering is applied if an explicit one was not requested. The default ordering is sensitive to whether
     * or not {@code filterText} was provided, using {@link RefOrder#ALPHABETICAL} when filtering and
     * {@link RefOrder#MODIFICATION} otherwise.
     *
     * @param repository the repository against which the command should run
     * @param parameters the parameters to use for configuring the command
     * @param pattern    the {@link GitRefPattern ref pattern} to determine which refs are returned
     * @param handler    the output handler to receive output when the command is {@link Command#call() called}
     * @param <T>        the object type that will be parsed from the output by the {@code handler}
     * @return a {@link Command} built from the provided output handler using {@code git for-each-ref}
     */
    private <T> GitCommand<T> createForEachRefCommand(Repository repository, AbstractRefsCommandParameters parameters,
                                                      GitRefPattern pattern, ForEachRefOutputHandler<T> handler) {
        RefOrder order = parameters.getOrder();
        if (order == null) {
            order = StringUtils.isBlank(parameters.getFilterText()) ? RefOrder.MODIFICATION : RefOrder.ALPHABETICAL;
        }

        return builder(repository).forEachRef()
                .format(handler.getFormat())
                .pattern(pattern.getPath())
                .sort(pattern.sort(order))
                .build(handler);
    }

    /**
     * Creates a {@link GitLogBuilder} and configures it with the {@link RevListOutputHandler#getFormat() format}
     * from the provided {@code handler} and other settings from the provided {@link CommitsCommandParameters}.
     *
     * @param repository the repository against which the command should run
     * @param parameters the parameters to use for configuring the command
     * @param handler    the output handler to receive output when the command is {@link Command#call() called}
     * @param <T>        the object type that will be parsed from the output by the {@code handler}
     * @return a {@link Command} built from the provided output handler using {@code git log}
     */
    private <T> GitCommand<T> createLogCommand(Repository repository, CommitsCommandParameters parameters,
                                               RevListOutputHandler<T> handler) {
        GitLogBuilder builder = builder(repository).log();
        if (parameters.isFollowingRenames()) {
            //When following renames, we also need the --name-status output so we know how the filename
            //has changed. That information allows the UI to link to the correct path when clicking on
            //an entry in the history where the file had a different name
            builder.follow(true)
                    .output(GitDiffOutput.NAME_STATUS);
        }

        return prepareRevCoreBuilder(repository, parameters, builder)
                .exitHandler(new LogCommandExitHandler(i18nService, repository, parameters.getIncludes(),
                        parameters.getExcludes(), parameters.isIgnoringMissing() == Boolean.TRUE))
                .format("commit %H%n" + handler.getFormat()) //Add a leading "commit abc123" to mimic rev-list
                .build(handler);
    }

    /**
     * Creates a {@link GitRevListBuilder} and configures it with the {@link RevListOutputHandler#getFormat() format}
     * from the provided {@code handler} and other settings from the provided {@link CommitsCommandParameters}.
     *
     * @param repository the repository against which the command should run
     * @param parameters the parameters to use for configuring the command
     * @param handler    the output handler to receive output when the command is {@link Command#call() called}
     * @param <T>        the object type that will be parsed from the output by the {@code handler}
     * @return a {@link Command} built from the provided output handler using {@code git rev-list}
     */
    private <T> GitCommand<T> createRevListCommand(Repository repository, CommitsCommandParameters parameters,
                                                   RevListOutputHandler<T> handler) {
        return prepareRevCoreBuilder(repository, parameters, builder(repository).revList())
                .exitHandler(new RevCoreExitHandler(i18nService, repository, parameters.getIncludes(),
                        parameters.getExcludes(), parameters.isIgnoringMissing() == Boolean.TRUE))
                .format(handler.getFormat())
                .build(handler);
    }

    private <B extends GitRevCoreBuilder<B>> B prepareRevCoreBuilder(Repository repository,
                                                                     CommitsCommandParameters parameters,
                                                                     B builder) {
        // if ignoreMissing is true, don't fail on all commits missing, but fail for ignoreMissing = null (= auto)
        boolean explicitIgnoreMissing = parameters.isIgnoringMissing() == Boolean.TRUE;

        builder = builder
                .files(parameters.getPaths())
                .ignoreMissing(explicitIgnoreMissing)
                .merges(GitRevListMerges.fromFilter(parameters.getMerges()));

        boolean autoIgnoreMissing = explicitIgnoreMissing || parameters.isIgnoringMissing() == null;
        if (parameters.isAll()) {
            //All current heads should be considered as included. This option will never receive any specific include
            //paths, but it may receive excludes. Regardless, --ignore-missing is turned on
            builder.all(true)
                    .ignoreMissing(autoIgnoreMissing);
            if (parameters.hasExcludes()) {
                builder.inputHandler(RevListInputHandler.excludingRefs(parameters.getExcludes()));
            }
        } else if (parameters.hasIncludes()) {
            //We have explicit includes, and may have excludes as well
            Set<String> excludes = parameters.getExcludes();
            Set<String> includes = parameters.getIncludes();

            builder.inputHandler(RevListInputHandler.forRefs(includes, excludes));
            if (includes.size() > 1 || !excludes.isEmpty()) {
                //If we have multiple includes, or any excludes, turn on --ignore-missing to match old behaviour
                builder.ignoreMissing(autoIgnoreMissing);
            }
            if (excludes.isEmpty() && !parameters.isTraversing()) {
                //If no excludes were provided, and a traversal was not requested, apply --no-walk. Unfortunately,
                //we can't use --no-walk=unsorted because it was introduced in git 1.7.12.
                builder.walk(GitRevListWalk.NO_WALK);
            }
        } else if (parameters.hasExcludes()) {
            //This case doesn't make sense, but the indexing service triggers it on initial commits. This code will
            //at least make that produce the same output it did prior to the introduction of the CommandFactory.
            builder.ignoreMissing(autoIgnoreMissing)
                    .inputHandler(RevListInputHandler.excludingRefs(parameters.getExcludes()));
        } else {
            //We have no all flag, no includes and no excludes. This will produce usage output from git, so we assume
            //the caller is attempting to retrieve commits for the default branch
            builder.rev("HEAD");
        }
        if (parameters.hasSecondaryRepository()) {
            Repository related = parameters.getSecondaryRepository();
            if (ObjectUtils.notEqual(repository.getId(), related.getId())) { //Same repository, no need for alternates
                //Verify that the two repositories are from the same hierarchy. This is an artificial restriction, for
                //this implementation, since as long as the alternate is present, it will happily rev-list between two
                //unrelated repositories. Applying it makes the SCMs more consistent, though
                if (ObjectUtils.notEqual(repository.getHierarchyId(), related.getHierarchyId())) {
                    throw new ArgumentValidationException(i18nService.createKeyedMessage(
                            "bitbucket.git.revlist.unrelatedrepositories",
                            repository.getProject().getKey(), repository.getSlug(),
                            related.getProject().getKey(), related.getSlug()));
                }

                //Add an alternate using an environment variable, to allow rev-list to load objects from the related
                //repository as well as the command repository
                builder.withEnvironment(InternalGitConstants.ENV_ALTERNATE_OBJECT_DIRS,
                        config.getObjectsDir(related).getAbsolutePath());
            }
        }

        return builder;
    }
}
