class History implements Serializable {

    private static final long serialVersionUID = -1;

    static final String TAGS_SEPARATOR = ", ";

    /** Entries in the log. The first entry is the most recent one. */
    private List<HistoryEntry> entries;
    /**
     * Track renamed files, so they can be treated in special way (for some SCMs) during cache creation.
     * These are relative to repository root.
     */
    private final Set<String> renamedFiles;

    /**
     * Revision of the newest change. Used in history cache.
     */
    private String latestRev;

    // revision to tag list. Individual tags are joined via TAGS_SEPARATOR.
    private Map<String, String> tags = new HashMap<>();

    public History() {
        this(new ArrayList<>());
    }

    public History(List<HistoryEntry> entries) {
        this(entries, Collections.emptyList());
    }

    History(List<HistoryEntry> entries, List<String> renamed) {
        this.entries = entries;
        this.renamedFiles = new HashSet<>(renamed);
    }

    History(List<HistoryEntry> entries, Set<String> renamed) {
        this.entries = entries;
        this.renamedFiles = renamed;
    }

    History(List<HistoryEntry> entries, Set<String> renamed, String latestRev) {
        this(entries, renamed);
        this.latestRev = latestRev;
    }

    public String getLatestRev() {
        return latestRev;
    }

    // Needed for serialization.
    public Map<String, String> getTags() {
        return tags;
    }

    // Needed for serialization.
    public void setTags(Map<String, String> tags) {
        this.tags = tags;
    }

    public void setEntries(List<HistoryEntry> entries) {
        this.entries = entries;
    }

    /**
     * Set the list of log entries for the file. The first entry is the most
     * recent one.
     *
     * @param entries The entries to add to the list
     */
    public void setHistoryEntries(List<HistoryEntry> entries) {
        this.setEntries(entries);
    }

    /**
     * Get the list of log entries, most recent first.
     *
     * @return The list of entries in this history
     */
    public List<HistoryEntry> getHistoryEntries() {
        return entries;
    }

    /**
     * Get the list of log entries, most recent first.
     * With parameters
     * @param limit max number of entries
     * @param offset starting position
     *
     * @return The list of entries in this history
     */
    public List<HistoryEntry> getHistoryEntries(int limit, int offset) {<FILL_FUNCTION_BODY>}

    /**
     * Check if at least one history entry has a file list.
     *
     * @return {@code true} if at least one of the entries has a non-empty
     * file list, {@code false} otherwise
     */
    public boolean hasFileList() {
        return entries.stream()
                .map(HistoryEntry::getFiles)
                .anyMatch(files -> !files.isEmpty());
    }

    /**
     * Check if at least one history entry has a tag list.
     *
     * @return {@code true} if at least one of the entries has a non-empty
     * tag list, {@code false} otherwise
     */
    public boolean hasTags() {
        return !tags.isEmpty();
    }

    public void addTags(HistoryEntry entry, String newTags) {
        tags.merge(entry.getRevision(), newTags, (a, b) -> a + TAGS_SEPARATOR + b);
    }

    /**
     * Gets a value indicating if {@code file} is in the list of renamed files.
     * @param file file path
     * @return is file renamed
     */
    public boolean isRenamed(String file) {
        return renamedFiles.contains(file);
    }

    public Set<String> getRenamedFiles() {
        return renamedFiles;
    }

    /**
     * @return list of revisions
     */
    public List<String> getRevisionList() {
        return getHistoryEntries().stream().
                map(HistoryEntry::getRevision).collect(Collectors.toList());
    }

    /**
     * Strip files and tags.
     * @see HistoryEntry#strip()
     */
    public void strip() {
        for (HistoryEntry ent : this.getHistoryEntries()) {
            ent.strip();
        }

        tags.clear();
    }

    /**
     * @return last (newest) history entry or null
     */
    public @Nullable HistoryEntry getLastHistoryEntry() {
        List<HistoryEntry> historyEntries = getHistoryEntries();
        if (historyEntries == null || historyEntries.isEmpty()) {
            return null;
        }

        return historyEntries.get(0);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        History that = (History) o;
        return Objects.equals(this.getHistoryEntries(), that.getHistoryEntries()) &&
                Objects.equals(this.getTags(), that.getTags()) &&
                Objects.equals(this.getRenamedFiles(), that.getRenamedFiles());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getHistoryEntries(), getTags(), getRenamedFiles());
    }

    @Override
    public String toString() {
        return this.getHistoryEntries().toString() + ", renamed files: " + this.getRenamedFiles().toString() +
                " , tags: " + getTags();
    }
}

        offset = Math.max(offset, 0);
        limit = offset + limit > entries.size() ? entries.size() - offset : limit;
        return entries.subList(offset, offset + limit);
    
<no_super_class>