/******************************************************************************* * Copyright (c) 2013, 2016 SAP AG and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Tobias Pfeifer (SAP AG) - initial implementation * Thomas Wolf <thomas.wolf@paranor.ch> - Bug 485511 *******************************************************************************/ package org.eclipse.egit.core.internal.rebase; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.core.runtime.Assert; import org.eclipse.egit.core.Activator; import org.eclipse.egit.core.internal.CoreText; import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry; import org.eclipse.egit.core.internal.indexdiff.IndexDiffChangedListener; import org.eclipse.egit.core.internal.indexdiff.IndexDiffData; import org.eclipse.jgit.errors.IllegalTodoFileModification; import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.events.RefsChangedListener; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RebaseTodoFile; import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.RebaseTodoLine.Action; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.GitDateFormatter; /** * Representation of the {@link RebaseTodoFile} for Rebase-Todo and * Rebase-Done-File of a {@link Repository}. * * Reparses the rebase plan when the index changes or when a {@code Ref} is * moving in order to keep the in-memory plan in sync with the one on disk. */ public class RebaseInteractivePlan implements IndexDiffChangedListener, RefsChangedListener { /** * Classes that implement this interface provide methods that deal with * changes made to a {@link RebaseInteractivePlan} * <p> * An Instance that implements this interface it can be added to a * {@link RebaseInteractivePlan} by using the * {@link RebaseInteractivePlan#addRebaseInteractivePlanChangeListener(RebaseInteractivePlanChangeListener) * addRebaseInteractivePlanChangeListener} method and removed using the * {@link RebaseInteractivePlan#removeRebaseInteractivePlanChangeListener(RebaseInteractivePlanChangeListener) * removeRebaseInteractivePlanChangeListener} method. When a change is made * to an {@link PlanElement} in this {@link RebaseInteractivePlan} (including * structural changes) the appropriate method will be invoked. */ public static interface RebaseInteractivePlanChangeListener { /** * Will be invoked if the order of either the Rebase-Todo-File or * Rebase-Done-File changes (structural change). * * @param rebaseInteractivePlan * @param element * @param oldIndex * @param newIndex */ public void planElementsOrderChanged( RebaseInteractivePlan rebaseInteractivePlan, PlanElement element, int oldIndex, int newIndex); /** * Will be invoked if the {@link ElementType} of an {@link PlanElement} * changes. * * @param rebaseInteractivePlan * @param element * @param oldType * @param newType */ public void planElementTypeChanged( RebaseInteractivePlan rebaseInteractivePlan, PlanElement element, ElementAction oldType, ElementAction newType); /** * Will be invoked after the list of {@link PlanElement Elements} has been * parsed from the {@link Repository} * * @param plan */ public void planWasUpdatedFromRepository(RebaseInteractivePlan plan); } private CopyOnWriteArrayList<RebaseInteractivePlanChangeListener> planChangeListeners = new CopyOnWriteArrayList<RebaseInteractivePlanChangeListener>(); private List<PlanElement> todoList; private List<PlanElement> doneList; private JoinedList<List<PlanElement>, PlanElement> planList; private final WeakReference<Repository> repositoryRef; private final File myGitDir; private ListenerHandle refsChangedListener; private static final Map<File, RebaseInteractivePlan> planRegistry = new HashMap<File, RebaseInteractivePlan>(); private static final String REBASE_TODO = "rebase-merge/git-rebase-todo"; //$NON-NLS-1$ private static final String REBASE_DONE = "rebase-merge/done"; //$NON-NLS-1$ /** * Provides a singleton instance of {@link RebaseInteractivePlan} for a * given {@link Repository} * <p> * If a {@link RebaseInteractivePlan} for the given {@link Repository} has * already been created and has not been disposed yet, this instance is * returned, otherwise a newly created instance is returned. * * @param repo * @return the {@link RebaseInteractivePlan} for the given * {@link Repository} */ public static RebaseInteractivePlan getPlan(Repository repo) { RebaseInteractivePlan plan = planRegistry.get(repo.getDirectory()); if (plan == null) { plan = new RebaseInteractivePlan(repo); planRegistry.put(repo.getDirectory(), plan); } return plan; } private RebaseInteractivePlan(Repository repo) { this.repositoryRef = new WeakReference<>(repo); this.myGitDir = repo.getDirectory().getAbsoluteFile(); reparsePlan(repo); registerIndexDiffChangeListener(repo); registerRefChangedListener(); } private void registerIndexDiffChangeListener(Repository repository) { IndexDiffCacheEntry entry = org.eclipse.egit.core.Activator.getDefault() .getIndexDiffCache().getIndexDiffCacheEntry(repository); if (entry != null) { entry.addIndexDiffChangedListener(this); } } private void unregisterIndexDiffChangeListener() { Repository repository = getRepository(); if (repository != null) { IndexDiffCacheEntry entry = org.eclipse.egit.core.Activator .getDefault().getIndexDiffCache() .getIndexDiffCacheEntry(repository); if (entry != null) { entry.removeIndexDiffChangedListener(this); } } } private void registerRefChangedListener() { refsChangedListener = Repository.getGlobalListenerList() .addRefsChangedListener(this); } /** * Reparse plan when {@code IndexDiff} changed */ @Override public void indexDiffChanged(Repository repo, IndexDiffData indexDiffData) { if (getRepository() == repo) reparsePlan(repo); } /** * Reparse plan when a {@code Ref} changed * * @param event */ @Override public void onRefsChanged(RefsChangedEvent event) { Repository repo = event.getRepository(); if (getRepository() == repo) reparsePlan(repo); } /** * Dispose the plan. * <p> * The next invocation of {@link RebaseInteractivePlan#getPlan(Repository)} * will create a new {@link RebaseInteractivePlan} instance. */ public void dispose() { reparsePlan(getRepository()); notifyPlanWasUpdatedFromRepository(); planRegistry.remove(myGitDir); planList.clear(); planChangeListeners.clear(); unregisterIndexDiffChangeListener(); refsChangedListener.remove(); } /** * @return a list representation of the {@link PlanElement Elements} for this * plan. */ public List<PlanElement> getList() { return planList; } /** * @return the repository */ public Repository getRepository() { return repositoryRef.get(); } /** * Adds a {@link RebaseInteractivePlanChangeListener} to this * {@link RebaseInteractivePlan} if it has not been registered yet * * @param listener * the {@link RebaseInteractivePlanChangeListener} to be added * @return true if the listener has been added, otherwise false */ public boolean addRebaseInteractivePlanChangeListener( RebaseInteractivePlanChangeListener listener) { return planChangeListeners.addIfAbsent(listener); } /** * Removes a {@link RebaseInteractivePlanChangeListener} from this * {@link RebaseInteractivePlan} if it has been registered before * * @param listener * the {@link RebaseInteractivePlanChangeListener} to be removed * @return true if the listener has been removed, otherwise false */ public boolean removeRebaseInteractivePlanChangeListener( RebaseInteractivePlanChangeListener listener) { return planChangeListeners.remove(listener); } private void notifyPlanElementsOrderChange(PlanElement element, int oldIndex, int newIndex) { persist(getRepository()); for (RebaseInteractivePlanChangeListener listener : planChangeListeners) listener.planElementsOrderChanged(this, element, oldIndex, newIndex); } private void notifyPlanElementActionChange(PlanElement element, ElementAction oldType, ElementAction newType) { persist(getRepository()); for (RebaseInteractivePlanChangeListener listener : planChangeListeners) listener.planElementTypeChanged(this, element, oldType, newType); } private void notifyPlanWasUpdatedFromRepository() { for (RebaseInteractivePlanChangeListener listener : planChangeListeners) listener.planWasUpdatedFromRepository(this); } private void reparsePlan(Repository repository) { if (repository != null) { try (RevWalk walk = new RevWalk(repository.newObjectReader())) { doneList = parseDone(repository, walk); todoList = parseTodo(repository, walk); } planList = JoinedList.wrap(doneList, todoList); notifyPlanWasUpdatedFromRepository(); } } private List<PlanElement> parseTodo(Repository repository, RevWalk walk) { List<RebaseTodoLine> rebaseTodoLines; try { rebaseTodoLines = repository.readRebaseTodo(REBASE_TODO, true); } catch (IOException e) { rebaseTodoLines = new LinkedList<RebaseTodoLine>(); } List<PlanElement> todoElements = createElementList(rebaseTodoLines, walk); return todoElements; } private List<PlanElement> parseDone(Repository repository, RevWalk walk) { List<RebaseTodoLine> rebaseDoneLines; try { rebaseDoneLines = repository.readRebaseTodo(REBASE_DONE, false); } catch (IOException e) { rebaseDoneLines = new LinkedList<RebaseTodoLine>(); } List<PlanElement> doneElements = createElementList(rebaseDoneLines, walk); return doneElements; } private List<PlanElement> createElementList( List<RebaseTodoLine> rebaseTodoLines, RevWalk walk) { List<PlanElement> planElements = new ArrayList<PlanElement>( rebaseTodoLines.size()); for (RebaseTodoLine todoLine : rebaseTodoLines) { PlanElement element = createElement(todoLine, walk); planElements.add(element); } return planElements; } private PlanElement createElement(RebaseTodoLine todoLine, RevWalk walk) { PersonIdent author = null; PersonIdent committer = null; RevCommit commit = loadCommit(todoLine.getCommit(), walk); if (commit != null) { author = commit.getAuthorIdent(); committer = commit.getCommitterIdent(); } PlanElement element = new PlanElement(todoLine, author, committer); return element; } private RevCommit loadCommit(AbbreviatedObjectId abbreviatedObjectId, RevWalk walk) { if (abbreviatedObjectId != null) { try { Collection<ObjectId> resolved = walk.getObjectReader().resolve( abbreviatedObjectId); if (resolved.size() == 1) { RevCommit commit = walk.parseCommit(resolved .iterator().next()); return commit; } } catch (IOException e) { // ignore, we assume no author/committer then } } return null; } /** * @return true if the rebase has already been started processing the plan, * otherwise false */ public boolean hasRebaseBeenStartedYet() { return isRebasingInteractive() && doneList.size() > 0; } /** * @return true if repository state is * {@link RepositoryState#REBASING_INTERACTIVE} */ public boolean isRebasingInteractive() { Repository repository = getRepository(); return repository != null && repository .getRepositoryState() == RepositoryState.REBASING_INTERACTIVE; } /** * Moves an {@link PlanElement} of Type {@link ElementType#TODO} down if * possible * * @param element * the {@link PlanElement} to move down */ public void moveTodoEntryDown(PlanElement element) { new MoveHelper(todoList, this).moveTodoEntryDown(element); } /** * Moves an {@link PlanElement} of Type {@link ElementType#TODO} up if possible * * @param element * the {@link PlanElement} to move up */ public void moveTodoEntryUp(PlanElement element) { new MoveHelper(todoList, this).moveTodoEntryUp(element); } /** * Moves a given {@link PlanElement sourceElement} of Type * {@link ElementType#TODO} to the current position of a {@link PlanElement * targetElement} in it's list representation (considering that this list * representation may be reversed). If <code>before</code> is true the * {@link PlanElement sourceElement} will be placed just before the * {@link PlanElement targetElement} * * @param sourceElement * @param targetElement * @param before */ public void moveTodoEntry(PlanElement sourceElement, PlanElement targetElement, boolean before) { new MoveHelper(todoList, this).moveTodoEntry(sourceElement, targetElement, before); } /** * Writes the plan to the FS. * <p> * Only {@link PlanElement Elements} of {@link ElementType#TODO} are * persisted. * * @param repository * the plan belongs to * * @return true if the todo file has been written successfully, otherwise * false */ private boolean persist(Repository repository) { if (repository == null || repository .getRepositoryState() != RepositoryState.REBASING_INTERACTIVE) { return false; } List<RebaseTodoLine> todoLines = new LinkedList<RebaseTodoLine>(); for (PlanElement element : planList.getSecondList()) todoLines.add(element.getRebaseTodoLine()); try { repository.writeRebaseTodoFile(REBASE_TODO, todoLines, false); } catch (IOException e) { Activator.logError(CoreText.RebaseInteractivePlan_WriteRebaseTodoFailed, e); throw new RuntimeException(e); } return true; } /** * Parses the plan from the FS by reading the todo-File and the done-File if * in state RebaseInteractive * * @throws IOException */ public void parse() throws IOException { if (!isRebasingInteractive()) return; reparsePlan(getRepository()); } /** * This class wraps a {@link RebaseTodoLine} and holds additional * information about the underlying commit, if available. */ public class PlanElement { private final RebaseTodoLine line; /** author info, may be null */ private final PersonIdent author; /** committer info, may be null */ private final PersonIdent committer; private PlanElement(RebaseTodoLine line, PersonIdent author, PersonIdent committer) { if (line == null) throw new IllegalArgumentException(); this.line = line; this.author = author; this.committer = committer; } /** * @return the {@link ElementType} for this {@link PlanElement} */ public ElementType getElementType() { if (todoList.indexOf(this) != -1) return ElementType.TODO; int indexInDone = doneList.indexOf(this); if (indexInDone != -1) { if (indexInDone == doneList.size() - 1 && isRebasingInteractive()) return ElementType.DONE_CURRENT; return ElementType.DONE; } return null; } private RebaseTodoLine getRebaseTodoLine() { return line; } /** * @return the CommitId of the wrapped {@link RebaseTodoLine} */ public AbbreviatedObjectId getCommit() { return line.getCommit(); } /** * @return the shortMessage of the wrapped {@link RebaseTodoLine} */ public String getShortMessage() { return line.getShortMessage(); } /** * @return the author name of the underlying commit */ public String getAuthor() { if (author == null) return ""; //$NON-NLS-1$ else return author.getName(); } /** * @param dateFormatter * @return the authored date of the underlying commit */ public String getAuthoredDate(GitDateFormatter dateFormatter) { if (author == null) return ""; //$NON-NLS-1$ else return dateFormatter.formatDate(author); } /** * @return the committer name of the underlying commit */ public String getCommitter() { if (committer == null) return ""; //$NON-NLS-1$ else return committer.getName(); } /** * @param dateFormatter * @return the commit date of the underlying commit */ public String getCommittedDate(GitDateFormatter dateFormatter) { if (committer == null) return ""; //$NON-NLS-1$ else return dateFormatter.formatDate(committer); } /** * This method maps the given {@link ElementAction} to the wrapped * {@link RebaseTodoLine RebaseTodoLines} {@link Action}. If the * {@link ElementAction} changes the registered * {@link RebaseInteractivePlanChangeListener * RebaseInteractivePlanChangeListeners} are notified. * * @param newAction * the {@link ElementAction} to be set */ public void setPlanElementAction(ElementAction newAction) { if (isComment()) { if (newAction == null) return; throw new IllegalArgumentException(); } ElementAction oldAction = this.getPlanElementAction(); if (oldAction == newAction) return; try { switch (newAction) { case SKIP: line.setAction(Action.COMMENT); break; case EDIT: line.setAction(Action.EDIT); break; case FIXUP: line.setAction(Action.FIXUP); break; case PICK: line.setAction(Action.PICK); break; case REWORD: line.setAction(Action.REWORD); break; case SQUASH: line.setAction(Action.SQUASH); break; default: throw new IllegalArgumentException(); } } catch (IllegalTodoFileModification e) { // shouldn't happen throw new IllegalArgumentException(e); } notifyPlanElementActionChange(this, oldAction, newAction); } /** * @return the {@link ElementAction} for this {@link PlanElement} */ public ElementAction getPlanElementAction() { if (isSkip()) return ElementAction.SKIP; if (isComment()) return null; switch (line.getAction()) { case EDIT: return ElementAction.EDIT; case FIXUP: return ElementAction.FIXUP; case PICK: return ElementAction.PICK; case SQUASH: return ElementAction.SQUASH; case REWORD: return ElementAction.REWORD; default: throw new IllegalStateException(); } } /** * @return true, if the given line is a pure comment, i.e. a comment * that doesn't hold a valid action line, otherwise false */ public boolean isComment() { return (Action.COMMENT.equals(line.getAction()) && null == line .getCommit()); } /** * @return true if this element is marked for deletion, i.e. a valid * action line has been commented out, otherwise false */ public boolean isSkip() { return (Action.COMMENT.equals(line.getAction()) && null != line .getCommit()); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PlanElement other = (PlanElement) obj; if (other.line.getCommit() == null) { if (this.line.getCommit() == null) return true; return false; } if (!other.line.getCommit().equals(this.line.getCommit())) return false; if (!other.getPlanElementAction().equals( this.getPlanElementAction())) return false; return true; } @Override public int hashCode() { return super.hashCode(); } @Override public String toString() { return line.toString(); } } /** * Wraps {@link Action} and additionally provides * {@link ElementAction#SKIP} */ public enum ElementAction { /** * The {@link PlanElement} will not be cherry-picked, i.e. changes are * lost on the new branch. Internally this is mapped to * {@link Action#COMMENT}, to comment out a {@link RebaseTodoLine} */ SKIP, /** * Equivalent to {@link Action#EDIT}; */ EDIT, /** * Equivalent to {@link Action#PICK}; */ PICK, /** * Equivalent to {@link Action#SQUASH}; */ SQUASH, /** * Equivalent to {@link Action#FIXUP}; */ FIXUP, /** * Equivalent to {@link Action#REWORD}; */ REWORD; } /** * The type of an {@link PlanElement} */ public static enum ElementType { /** * The {@link PlanElement} is present in the done-File, i.e. the * {@link RebaseTodoLine} has already been processed. */ DONE, /** * The {@link PlanElement} is present in the todo-File, i.e. the * {@link RebaseTodoLine} has not been processed yet. */ TODO, /** * Special case of {@link ElementType#DONE}. * <p> * The {@link PlanElement} is the last entry in the done-File, i.e. the * {@link RebaseTodoLine} has already been processed and furthermore the * rebase has not been finished yet */ DONE_CURRENT; } /** * Helper class for moving plan elements */ public static class MoveHelper { private List<PlanElement> todoList; private RebaseInteractivePlan plan; /** * @param todoList * @param plan */ public MoveHelper(List<PlanElement> todoList, RebaseInteractivePlan plan) { this.todoList = todoList; this.plan = plan; } /** * Move the element at the given index in the given list up if possible, * otherwise this method has no effect on the list. Moving an element up * is not possible if the list does not contain an element with the * given index or the element at the given index has no next element. * * @param index * @param list * @return true if an element has been moved up, otherwise false */ private static boolean moveUp(final int index, final List<?> list) { if (index <= 0 || index > list.size()) return false; Collections.swap(list, index, index - 1); return true; } /** * Move the element at the given index in the given list down if * possible, otherwise this method has no effect on the list. Moving an * element down is not possible if the list does not contain an element * with the given index or the element at the given index has no * previous element. * * @param index * @param list * @return true if an element has been moved down, otherwise false */ private static boolean moveDown(final int index, final List<?> list) { if (index < 0 || index >= list.size() - 1) return false; Collections.swap(list, index, index + 1); return true; } /** * Moves an {@link PlanElement} of Type {@link ElementType#TODO} down if * possible * * @param element * the {@link PlanElement} to move down */ public void moveTodoEntryDown(PlanElement element) { List<PlanElement> list = todoList; int initialIndex = list.indexOf(element); int oldIndex; do { oldIndex = list.indexOf(element); moveDown(oldIndex, list); } while (list.get(oldIndex).isComment()); int newIndex = list.indexOf(element); if (initialIndex != newIndex) plan.notifyPlanElementsOrderChange(element, initialIndex, newIndex); } /** * Moves an {@link PlanElement} of Type {@link ElementType#TODO} up if * possible * * @param element * the {@link PlanElement} to move up */ public void moveTodoEntryUp(PlanElement element) { List<PlanElement> list = todoList; int initialIndex = list.indexOf(element); int oldIndex; do { oldIndex = list.indexOf(element); moveUp(oldIndex, list); } while (list.get(oldIndex).isComment()); int newIndex = list.indexOf(element); if (initialIndex != newIndex) plan.notifyPlanElementsOrderChange(element, initialIndex, newIndex); } private static void move(int sourceIndex, int targetIndex, final List<PlanElement> list) { if (sourceIndex == targetIndex) return; if (sourceIndex < targetIndex) { Collections.rotate(list.subList(sourceIndex, targetIndex + 1), -1); } else { Collections.rotate(list.subList(targetIndex, sourceIndex + 1), 1); } } /** * Moves a given {@link PlanElement sourceElement} of Type * {@link ElementType#TODO} to the current position of a * {@link PlanElement targetElement} in it's list representation * (considering that this list representation may be reversed). If * <code>before</code> is true the {@link PlanElement sourceElement} * will be placed just before the {@link PlanElement targetElement} * * @param sourceElement * @param targetElement * @param before */ public void moveTodoEntry(PlanElement sourceElement, PlanElement targetElement, boolean before) { if (sourceElement == targetElement) return; Assert.isNotNull(sourceElement); Assert.isNotNull(targetElement); if (ElementType.TODO != sourceElement.getElementType()) throw new IllegalArgumentException(); List<PlanElement> list = todoList; int initialSourceIndex = list.indexOf(sourceElement); int targetIndex = list.indexOf(targetElement); if (targetIndex == -1 || initialSourceIndex == -1) return; if (targetIndex == initialSourceIndex) return; if (targetIndex > initialSourceIndex && before) targetIndex--; if (targetIndex < initialSourceIndex && !before) targetIndex++; move(initialSourceIndex, targetIndex, list); int newIndex = list.indexOf(sourceElement); if (initialSourceIndex != newIndex) plan.notifyPlanElementsOrderChange(sourceElement, initialSourceIndex, newIndex); } } /** * List that provides a view to two joined lists * * @param <L> * The concrete type of the two lists * @param <T> * The type of the elements in the lists */ public static class JoinedList<L extends List<T>, T> extends AbstractList<T> { private final L firstList, secondList; /** * @return the first list */ public L getFirstList() { return firstList; } /** * @return the second list */ public L getSecondList() { return secondList; } /** * @param first * @param second */ private JoinedList(L first, L second) { super(); Assert.isNotNull(first); Assert.isNotNull(second); this.firstList = first; this.secondList = second; } /** * Creates a newly List that provides a view to two joined lists. * * @param first * @param second * @return a new view on a concatenation of both lists */ public static <L extends List<T>, T> JoinedList<L, T> wrap(L first, L second) { return new JoinedList<L, T>(first, second); } private static class RelativeIndex<T> { private final int relativeIndex; private final List<T> list; public final int getRelativeIndex() { return relativeIndex; } public final List<T> getList() { return list; } RelativeIndex(int relativeIndex, List<T> list) { super(); this.relativeIndex = relativeIndex; this.list = list; } } private RelativeIndex<T> mapAbsolutIndex(int index) { if (index < firstList.size()) return new RelativeIndex<T>(index, firstList); return new RelativeIndex<T>(index - firstList.size(), secondList); } /** * if the given index is smaller than the first lists size the element * is added to the first list. if the given index points to the seam of * the joined lists, the given element will be added to the second list. * More precisely a element is added to the second list if the given * index is greater or equals the first lists size, otherwise it's added * to the first list. */ @Override public void add(int index, T element) { RelativeIndex<T> rel = mapAbsolutIndex(index); rel.getList().add(rel.getRelativeIndex(), element); modCount++; } @Override public T get(int index) { RelativeIndex<T> rel = mapAbsolutIndex(index); return rel.getList().get(rel.getRelativeIndex()); } @Override public T remove(int index) { RelativeIndex<T> rel = mapAbsolutIndex(index); modCount++; return rel.getList().remove(rel.getRelativeIndex()); } @Override public T set(int index, T element) { RelativeIndex<T> rel = mapAbsolutIndex(index); return rel.getList().set(rel.getRelativeIndex(), element); } @Override public int size() { return firstList.size() + secondList.size(); } } }