class ChangesetInfo {
        CommitInfo commit;
        public SortedSet<String> files;
        public Set<String> renamedFiles;
        public Set<String> deletedFiles;
        public Boolean isMerge;

        ChangesetInfo(CommitInfo commit) {
            this.commit = commit;
            this.files = new TreeSet<>();
            this.renamedFiles = new HashSet<>();
            this.deletedFiles = new HashSet<>();
        }

        ChangesetInfo(CommitInfo commit, SortedSet<String> files, Set<String> renamedFiles, Set<String> deletedFiles,
                      Boolean isMerge) {
            this.commit = commit;
            this.files = files;
            this.renamedFiles = renamedFiles;
            this.deletedFiles = deletedFiles;
            this.isMerge = isMerge;
        }
    }

    /**
     * Traverse history of given file/directory.
     * @param file File object
     * @param sinceRevision start revision (non-inclusive)
     * @param tillRevision end revision (inclusive)
     * @param numCommits maximum number of commits to traverse (use {@code null} as unlimited)
     * @param visitors list of {@link ChangesetVisitor} objects
     * @throws HistoryException on error
     */
    public abstract void traverseHistory(File file, String sinceRevision, @Nullable String tillRevision,
                         Integer numCommits, List<ChangesetVisitor> visitors) throws HistoryException;

    public History getHistory(File file, String sinceRevision, String tillRevision,
                              Integer numCommits) throws HistoryException {

        if (numCommits != null && numCommits <= 0) {
            return null;
        }

        HistoryCollector historyCollector = new HistoryCollector(isMergeCommitsEnabled());
        traverseHistory(file, sinceRevision, tillRevision, numCommits, List.of(historyCollector));
        History history = new History(historyCollector.entries, historyCollector.renamedFiles,
                historyCollector.latestRev);

        // Assign tags to changesets they represent.
        if (RuntimeEnvironment.getInstance().isTagsEnabled() && hasFileBasedTags()) {
            assignTagsInHistory(history);
        }

        return history;
    }

    @Override
    protected void doCreateCache(HistoryCache cache, String sinceRevision, File directory) throws HistoryException, CacheException {<FILL_FUNCTION_BODY>

        RuntimeEnvironment env = RuntimeEnvironment.getInstance();

        FileCollector fileCollector = null;
        Project project = Project.getProject(directory);
        if (project != null && isHistoryBasedReindex() && isHistoryEnabled()) {
            // The fileCollector has to go through merge changesets no matter what the configuration says
            // in order to detect the files that need to be indexed.
            fileCollector = new FileCollector(true);
        }

        if (!env.isHistoryCachePerPartesEnabled()) {
            LOGGER.log(Level.INFO, "repository {0} supports per partes history cache creation however " +
                    "it is disabled in the configuration. Generating history cache as whole.", this);

            HistoryCollector historyCollector = new HistoryCollector(isMergeCommitsEnabled());
            List<ChangesetVisitor> visitors = new ArrayList<>();
            visitors.add(historyCollector);
            if (fileCollector != null) {
                visitors.add(fileCollector);
            }
            try (Progress progress = new Progress(LOGGER, String.format("changesets traversed of %s", this),
                    Level.FINER)) {
                ProgressVisitor progressVisitor = new ProgressVisitor(progress);
                visitors.add(progressVisitor);
                traverseHistory(directory, sinceRevision, null, null, visitors);
            }
            History history = new History(historyCollector.entries, historyCollector.renamedFiles,
                    historyCollector.latestRev);

            // Assign tags to changesets they represent.
            if (env.isTagsEnabled() && hasFileBasedTags()) {
                assignTagsInHistory(history);
            }

            finishCreateCache(cache, history, null);

            updateFileCollector(fileCollector, project);

            return;
        }

        // For repositories that supports this, avoid storing complete History in memory
        // (which can be sizeable, at least for the initial indexing, esp. if merge changeset support is enabled),
        // by splitting the work into multiple chunks.
        BoundaryChangesets boundaryChangesets = new BoundaryChangesets(this);
        List<String> boundaryChangesetList = new ArrayList<>(boundaryChangesets.getBoundaryChangesetIDs(sinceRevision));
        boundaryChangesetList.add(null);    // to finish the last step in the cycle below
        LOGGER.log(Level.FINE, "boundary changesets: {0}", boundaryChangesetList);
        int cnt = 0;
        for (String tillRevision: boundaryChangesetList) {
            Statistics stat = new Statistics();
            LOGGER.log(Level.FINEST, "storing history cache for revision range ({0}, {1})",
                    new Object[]{sinceRevision, tillRevision});

            HistoryCollector historyCollector = new HistoryCollector(isMergeCommitsEnabled());
            List<ChangesetVisitor> visitors = new ArrayList<>();
            visitors.add(historyCollector);
            if (fileCollector != null) {
                visitors.add(fileCollector);
            }

            try (Progress progress = new Progress(LOGGER,
                    String.format("changesets traversed of %s (range %s %s)", this, sinceRevision, tillRevision),
                    Level.FINER)) {
                ProgressVisitor progressVisitor = new ProgressVisitor(progress);
                visitors.add(progressVisitor);
                traverseHistory(directory, sinceRevision, tillRevision, null, visitors);
            }
            History history = new History(historyCollector.entries, historyCollector.renamedFiles,
                    historyCollector.latestRev);

            // Assign tags to changesets they represent.
            if (env.isTagsEnabled() && hasFileBasedTags()) {
                assignTagsInHistory(history);
            }

            finishCreateCache(cache, history, tillRevision);
            sinceRevision = tillRevision;
            stat.report(LOGGER, Level.FINE, String.format("Finished chunk %d/%d of history cache for repository '%s'",
                    ++cnt, boundaryChangesetList.size(), this.getDirectoryName()));
        }

        updateFileCollector(fileCollector, project);
    
<methods>public non-sealed void <init>() ,public abstract void accept(java.lang.String, Consumer<org.opengrok.indexer.history.BoundaryChangesets.IdWithProgress>, org.opengrok.indexer.util.Progress) throws org.opengrok.indexer.history.HistoryException,public int getPerPartesCount() <variables>private static final java.util.logging.Logger LOGGER,public static final int MAX_CHANGESETS,private static final long serialVersionUID