All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unibremen.informatik.st.libvcs4j.RevisionRange Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
package de.unibremen.informatik.st.libvcs4j;

import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static de.unibremen.informatik.st.libvcs4j.FileChange.Type.*;

/**
 * This class represents the state transition between two revisions. The "from"
 * revision (if any) is available with {@link #getPredecessorRevision()}. The
 * "to" revision is available with {@link #getRevision()}.
 *
 * A single range may subsume several commits to merge commits on, for
 * instance, a monthly basis.
 */
public interface RevisionRange extends VCSModelElement {

	/**
	 * Returns the ordinal of this range. Ordinals are used to identify
	 * individual ranges with a serial number when processing a VCS. The origin
	 * is 1.
	 *
	 * @return
	 * 		The ordinal of this range ({@code >= 1}).
	 */
	int getOrdinal();

	/**
	 * Returns the "to" revision the file changes of this range belong to.
	 *
	 * @return
	 * 		The "to" revision the file changes of this range belong to.
	 */
	Revision getRevision();

	/**
	 * Returns the "from" revision the file changes of this range belong to.
	 *
	 * @return
	 * 		The "from" revision the file changes of this range belong to or an
	 * 		empty {@link Optional} if this is the first range.
	 */
	Optional getPredecessorRevision();

	/**
	 * Returns the commits that have been applied to
	 * {@link #getPredecessorRevision()} so that {@link #getRevision()} results
	 * from it. The order of the returned list is from oldest to latest.
	 * Contains at least one commit.
	 *
	 * @return
	 * 		The list of commits.
	 */
	List getCommits();

	/**
	 * Returns the latest commit of {@link #getCommits()}.
	 *
	 * @return
	 * 		The latest commit of {@link #getCommits()}.
	 */
	default Commit getLatestCommit() {
		final List commits = getCommits();
		Validate.validateState(!commits.isEmpty(),
				"Unexpected empty list of commits");
		return commits.get(commits.size() - 1);
	}

	/**
	 * Returns all files that have changed between
	 * {@link #getPredecessorRevision()} and {@link #getRevision()}. The
	 * default implementation, if necessary, merges the file changes of all
	 * commits listed in {@link #getCommits()}.
	 *
	 * @return
	 * 		The list of file changes.
	 */
	default List getFileChanges() {
		if (getCommits().size() == 1) {
			return getCommits().get(0).getFileChanges();
		} else {
			// Accumulates the result
			final List accum = new ArrayList<>(
					getCommits().get(0).getFileChanges());
			// Merge subsequent commits one after another
			for (int i = 1; i < getCommits().size(); i++) {
				// Try to find a match in this list
				final List toProcess = new ArrayList<>(
						getCommits().get(i).getFileChanges());
				// Use a ListIterator to remove/set the current value
				final ListIterator iter = accum.listIterator();
				while (iter.hasNext()) {
					final FileChange change = iter.next();
					final FileChange.Type type = change.getType();
					// Used to export the match from within a lambda
					final Deque matches = new ArrayDeque<>();
					if (type == ADD || type == REMOVE) {
						final Path path = type == ADD
								? change.getNewFile()
									.orElseThrow(IllegalStateException::new)
									.toRelativePath()
								: change.getOldFile()
									.orElseThrow(IllegalStateException::new)
									.toRelativePath();
						toProcess.stream().filter(c ->
								(type == ADD
										? c.getOldFile()
										: c.getNewFile())
								.map(VCSFile::toRelativePath)
								.map(p -> p.equals(path))
								.orElse(false))
						.findAny().ifPresent(match -> {
							final FileChange.Type oType = match.getType();
							Validate.validateState(
									!(type == ADD && oType == ADD),
									"'%s' has been added after being added",
									path);
							Validate.validateState(
									!(type == REMOVE && oType == REMOVE),
									"'%s' has been removed after being removed",
									path);
							toProcess.remove(match);
							if (oType == REMOVE) {
								iter.remove();
							} else {
								matches.add(match);
							}
						});
					} else {
						final Path path = change.getNewFile()
								.orElseThrow(IllegalStateException::new)
								.toRelativePath();
						toProcess.stream().filter(c ->
								(c.getType() != ADD
										? c.getOldFile()
										: c.getNewFile()) // < indicates a bug
								.map(VCSFile::toRelativePath)
								.map(p -> p.equals(path))
								.orElse(false)
						).findAny().ifPresent(match -> {
							final FileChange.Type oType = match.getType();
							Validate.validateState(
									!(type == MODIFY && oType == ADD),
									"'%s' has been added after being modified",
									path);
							Validate.validateState(
									!(type == RELOCATE && oType == ADD),
									"'%s' has been added after being relocated to this path",
									path);
							toProcess.remove(match);
							matches.add(match);
						});
					}
					Validate.validateState(matches.size() <= 1,
							"Unexpected number of matches (%d)",
							matches.size());
					if (!matches.isEmpty()) {
						final FileChange match = matches.poll();
						final VCSEngine engine = getVCSEngine();
						iter.set(engine.getModelFactory().createFileChange(
								change.getOldFile().orElse(null),
								match.getNewFile().orElse(null),
								engine));
					}
				}
				accum.addAll(toProcess);
			}
			// Postprocessing: Replace accumulated file changes such that the
			// revisions of the referenced files match with the predecessor and
			// successor revision of this range.
			final Revision predRev = getPredecessorRevision().orElse(null);
			final Revision rev = getRevision();
			final VCSEngine engine = getVCSEngine();
			final VCSModelFactory factory = engine.getModelFactory();
			final ListIterator it = accum.listIterator();
			while (it.hasNext()) {
				final FileChange change = it.next();
				final VCSFile newOldFile = change.getOldFile()
						.map(file -> {
							Validate.validateState(predRev != null);
							final String relPath = file.getRelativePath();
							final boolean revMatch = file.getRevision()
									.getId().equals(predRev.getId());
							return revMatch
									? file
									: factory.createVCSFile(
											relPath, predRev, engine);
						})
						.orElse(null);
				final VCSFile newNewFile = change.getNewFile()
						.map(file -> {
							final String relPath = file.getRelativePath();
							final boolean revMatch = file.getRevision()
									.getId().equals(rev.getId());
							return revMatch
									? file
									: factory.createVCSFile(
											relPath, rev, engine);
						})
						.orElse(null);
				final FileChange newChange = factory.createFileChange(
						newOldFile, newNewFile, engine);
				it.set(newChange);
			}
			return accum;
		}
	}

	/**
	 * Returns all files that have been added.
	 *
	 * @return
	 * 		All files that have been added.
	 */
	default List getAddedFiles() {
		return getFileChanges().stream()
				.filter(fc -> fc.getType() == ADD)
				.collect(Collectors.toList());
	}

	/**
	 * Returns all files that have been removed.
	 *
	 * @return
	 * 		All files that have been removed.
	 */
	default List getRemovedFiles() {
		return getFileChanges().stream()
				.filter(fc -> fc.getType() == REMOVE)
				.collect(Collectors.toList());
	}

	/**
	 * Returns all files that have been modified.
	 *
	 * @return
	 * 		All files that have been modified.
	 */
	default List getModifiedFiles() {
		return getFileChanges().stream()
				.filter(fc -> fc.getType() == MODIFY)
				.collect(Collectors.toList());
	}

	/**
	 * Returns all files that have been relocated.
	 *
	 * @return
	 * 		All files that have been relocated.
	 */
	default List getRelocatedFiles() {
		return getFileChanges().stream()
				.filter(fc -> fc.getType() == RELOCATE)
				.collect(Collectors.toList());
	}

	/**
	 * Filters the list of file changes returned by {@link #getFileChanges()}
	 * and returns only those whose old or the new relative path end with
	 * {@code suffix}.
	 *
	 * You may use this method to analyze file changes affecting a certain file
	 * type only. For instance, call {@code getFileChangesBySuffix(".java")} to
	 * get only the file changes affecting Java files.
	 *
	 * @param suffix
	 * 		The suffix used to filter the file changes.
	 * @return
	 * 		All file changes whose old or the new relative path end with
	 * 		{@code suffix}.
	 */
	default List getFileChangesBySuffix(final String suffix) {
		return getFileChanges().stream()
				.filter(fc -> {
					final boolean old = fc.getOldFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.endsWith(suffix))
							.orElse(false);
					final boolean nev = fc.getNewFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.endsWith(suffix))
							.orElse(false);
					return old || nev;
				})
				.collect(Collectors.toList());
	}

	/**
	 * Filters the list of file changes returned by {@link #getFileChanges()}
	 * and returns only those whose old or new relative path start with
	 * {@code prefix}.
	 *
	 * You may use this method to analyze file changes affecting a certain
	 * directory (and its sub-directories) only. For instance, call
	 * {@code getFileChangesByPrefix("src/main/java")} to get only the file
	 * changes affecting files located in "src/main/java".
	 *
	 * @param prefix
	 * 		The prefix used to filter the file changes.
	 * @return
	 * 		All file changes whose old or new relative file path start with
	 * 		{@code prefix}.
	 */
	default List getFileChangesByPrefix(final String prefix) {
		return getFileChanges().stream()
				.filter(fc -> {
					final boolean old = fc.getOldFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.startsWith(prefix))
							.orElse(false);
					final boolean nev = fc.getNewFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.startsWith(prefix))
							.orElse(false);
					return old || nev;
				})
				.collect(Collectors.toList());
	}

	/**
	 * Filters the list of file changes returned by {@link #getFileChanges()}
	 * and returns only those whose old or new relative path match
	 * {@code regex}. Paths are matched using {@link String#matches(String)}.
	 *
	 * @param regex
	 * 		The regular expression used to filter the file changes.
	 * @return
	 * 		All file changes whose old or new relative path match
	 * 		{@code regex}.
	 */
	default List getFileChangesByRegex(final String regex) {
		return getFileChanges().stream()
				.filter(fc -> {
					final boolean old = fc.getOldFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.matches(regex))
							.orElse(false);
					final boolean nev = fc.getNewFile()
							.map(VCSFile::getRelativePath)
							.map(p -> p.matches(regex))
							.orElse(false);
					return old || nev;
				})
				.collect(Collectors.toList());
	}

	/**
	 * Returns the issues referenced by the commits of this range. To enable
	 * this feature when processing a repository, set an appropriate
	 * {@link ITEngine} with {@link VCSEngine#setITEngine(ITEngine)}. The
	 * returned list does not contain the same issue (according to
	 * {@link Issue#getId()}) twice.
	 *
	 * @return
	 * 		The issues referenced by the commits of this range.
	 */
	default List getIssues() {
		final List issues = getCommits().stream()
				.map(Commit::getIssues)
				.flatMap(Collection::stream)
				.collect(Collectors.toList());
		final Map idToIssue = new HashMap<>();
		issues.forEach(i -> idToIssue.put(i.getId(), i));
		return new ArrayList<>(idToIssue.values());
	}

	/**
	 * Returns whether this range is the first one. That is, there is no
	 * predecessor revision and, consequently, all changes returned by
	 * {@link #getFileChanges()} are additions. The default implementation
	 * simply checks whether {@link #getPredecessorRevision()} returns an empty
	 * {@link Optional}.
	 *
	 * @return
	 * 		{@code true} if this range is the first one, {@code false}
	 * 		otherwise.
	 */
	default boolean isFirst() {
		return getPredecessorRevision().isEmpty();
	}

	/**
	 * Runs the given action if this range is the first one.
	 *
	 * @param action
	 * 		The action to run if this is the first range.
	 */
	default void ifFirst(final Consumer action) {
		if (isFirst()) {
			action.accept(this);
		}
	}

	/**
	 * Runs the given action if this range is not the first one.
	 *
	 * @param action
	 * 		The action to run if this is not the first range.
	 */
	default void ifNotFirst(final Consumer action) {
		if (!isFirst()) {
			action.accept(this);
		}
	}

	/**
	 * Merges this range into {@code predecessor} and returns a new instance
	 * that represents the state transition from
	 * {@code predecessor.getPredecessorRevision()} to
	 * {@code this.getRevision()}. The commits of {@code predecessor} and
	 * {@code this} are combined such that the commits of {@code this} are
	 * applied onto the commits of {@code predecessor}.
	 *
	 * @param predecessor
	 * 		The predecessor range to merge this range into.
	 * @return
	 * 		A new revision range representing the state transition from
	 * 		{@code predecessor.getPredecessorRevision()} to
	 * 		{@code this.getRevision()}.
	 * @throws NullPointerException
	 * 		If {@code predecessor} is {@code null}.
	 */
	default RevisionRange merge(final RevisionRange predecessor)
			throws NullPointerException {
		Validate.notNull(predecessor);
		final VCSEngine engine = getVCSEngine();
		final List commits = predecessor.getCommits();
		commits.addAll(getCommits());
		return engine.getModelFactory().createRevisionRange(getOrdinal(),
				getRevision(),
				predecessor.getPredecessorRevision().orElse(null), commits,
				getVCSEngine());
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy