blob: db03fd63b5e0c4a747ea71fe997115d2ae16bc6a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.oak.plugins.document;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Contains commit information about a branch and its base revision.
*/
class Branch {
/**
* The commits to the branch
*/
private final ConcurrentSkipListMap<Revision, BranchCommit> commits;
/**
* The initial base revision of this branch.
*/
private final RevisionVector base;
/**
* The branch reference.
*/
private final BranchReference ref;
/**
* Create a new branch instance with an initial set of commits and a given
* base revision. The life time of this branch can be controlled with
* the {@code guard} parameter. Once the {@code guard} object becomes weakly
* reachable, the {@link BranchReference} for this branch is appended to
* the passed {@code queue}. No {@link BranchReference} is appended if the
* passed {@code guard} is {@code null}.
*
* @param commits the initial branch commits.
* @param base the base commit.
* @param queue a {@link BranchReference} to this branch will be appended to
* this queue when {@code guard} becomes weakly reachable.
* @param guard controls the life time of this branch.
* @throws IllegalArgumentException if base is a branch revision.
*/
Branch(@NotNull SortedSet<Revision> commits,
@NotNull RevisionVector base,
@NotNull ReferenceQueue<Object> queue,
@Nullable Object guard) {
checkArgument(!checkNotNull(base).isBranch(), "base is not a trunk revision: %s", base);
this.base = base;
this.commits = new ConcurrentSkipListMap<Revision, BranchCommit>(commits.comparator());
for (Revision r : commits) {
this.commits.put(r.asBranchRevision(),
new BranchCommitImpl(base, r.asBranchRevision()));
}
if (guard != null) {
this.ref = new BranchReference(queue, this, guard);
} else {
this.ref = null;
}
}
/**
* @return the initial base of this branch. This is a trunk revision.
*/
@NotNull
RevisionVector getBase() {
return base;
}
/**
* Returns the base revision for the given branch revision <code>r</code>.
*
* @param r revision of a commit in this branch.
* @return the base revision for <code>r</code>.
* @throws IllegalArgumentException if <code>r</code> is not a commit of
* this branch.
*/
@NotNull
RevisionVector getBase(@NotNull Revision r) {
BranchCommit c = commits.get(checkNotNull(r).asBranchRevision());
if (c == null) {
throw new IllegalArgumentException(
"Revision " + r + " is not a commit in this branch");
}
return c.getBase();
}
/**
* Rebases the last commit of this branch to the given revision.
*
* @param head the new head of the branch.
* @param base rebase to this revision.
* @throws IllegalArgumentException if head is a trunk revision or base is a
* branch revision.
*/
void rebase(@NotNull Revision head, @NotNull RevisionVector base) {
checkArgument(checkNotNull(head).isBranch(), "Not a branch revision: %s", head);
checkArgument(!checkNotNull(base).isBranch(), "Not a trunk revision: %s", base);
Revision last = commits.lastKey();
checkArgument(head.compareRevisionTime(last) > 0);
commits.put(head, new RebaseCommit(base, head, commits));
}
/**
* Adds a new commit with revision <code>r</code> to this branch.
*
* @param r the revision of the branch commit to add.
* @throws IllegalArgumentException if r is not a branch revision.
*/
void addCommit(@NotNull Revision r) {
checkArgument(checkNotNull(r).isBranch(), "Not a branch revision: %s", r);
Revision last = commits.lastKey();
checkArgument(commits.comparator().compare(r, last) > 0);
commits.put(r, new BranchCommitImpl(commits.get(last).getBase(), r));
}
/**
* @return the commits to this branch.
*/
SortedSet<Revision> getCommits() {
return commits.keySet();
}
/**
* @return <code>true</code> if this branch contains any commits;
* <code>false</code> otherwise.
*/
boolean hasCommits() {
return !commits.isEmpty();
}
/**
* Checks if this branch contains a commit with the given revision.
*
* @param r the revision of a commit.
* @return <code>true</code> if this branch contains a commit with the given
* revision; <code>false</code> otherwise.
*/
boolean containsCommit(@NotNull Revision r) {
return commits.containsKey(checkNotNull(r).asBranchRevision());
}
/**
* Returns the branch commit with the given or {@code null} if it does not
* exist.
*
* @param r the revision of a commit.
* @return the branch commit or {@code null} if it doesn't exist.
*/
@Nullable
BranchCommit getCommit(@NotNull Revision r) {
return commits.get(checkNotNull(r).asBranchRevision());
}
/**
* @return the branch reference or {@code null} if no guard object was
* passed to the constructor of this branch.
*/
@Nullable
BranchReference getRef() {
return ref;
}
/**
* Removes the commit with the given revision <code>r</code>. Does nothing
* if there is no such commit.
*
* @param r the revision of the commit to remove.
* @throws IllegalArgumentException if r is not a branch revision.
*/
public void removeCommit(@NotNull Revision r) {
checkArgument(checkNotNull(r).isBranch(), "Not a branch revision: %s", r);
commits.remove(r);
}
/**
* Applies all unsaved modification of this branch to the given collection
* of unsaved trunk modifications with the given merge commit revision.
*
* @param trunk the unsaved trunk modifications.
* @param mergeCommit the revision of the merge commit.
*/
public void applyTo(@NotNull UnsavedModifications trunk,
@NotNull Revision mergeCommit) {
checkNotNull(trunk);
for (BranchCommit c : commits.values()) {
c.applyTo(trunk, mergeCommit);
}
}
/**
* Gets the most recent unsaved last revision at <code>readRevision</code>
* or earlier in this branch for the given <code>path</code>. Documents with
* explicit updates are not tracked and this method may return {@code null}.
*
* @param path the path of a node.
* @param readRevision the read revision.
* @return the most recent unsaved last revision or <code>null</code> if
* there is none in this branch.
*/
@Nullable
public Revision getUnsavedLastRevision(Path path,
Revision readRevision) {
readRevision = readRevision.asBranchRevision();
for (Revision r : commits.descendingKeySet()) {
if (readRevision.compareRevisionTime(r) < 0) {
continue;
}
BranchCommit c = commits.get(r);
if (c.isModified(path)) {
return r;
}
}
return null;
}
/**
* @param rev the revision to check.
* @return {@code true} if the given revision is the head of this branch,
* {@code false} otherwise.
*/
public boolean isHead(@NotNull Revision rev) {
checkArgument(checkNotNull(rev).isBranch(),
"Not a branch revision: %s", rev);
return checkNotNull(rev).equals(commits.lastKey());
}
/**
* Returns the modified paths since the base revision of this branch until
* the given branch revision {@code r} (inclusive).
*
* @param r a commit on this branch.
* @return modified paths until {@code r}.
* @throws IllegalArgumentException if r is not a branch revision.
*/
Iterable<Path> getModifiedPathsUntil(@NotNull final Revision r) {
checkArgument(checkNotNull(r).isBranch(),
"Not a branch revision: %s", r);
if (!commits.containsKey(r)) {
return Collections.emptyList();
}
Iterable<Iterable<Path>> paths = transform(filter(commits.entrySet(),
new Predicate<Map.Entry<Revision, BranchCommit>>() {
@Override
public boolean apply(Map.Entry<Revision, BranchCommit> input) {
return !input.getValue().isRebase()
&& input.getKey().compareRevisionTime(r) <= 0;
}
}), new Function<Map.Entry<Revision, BranchCommit>, Iterable<Path>>() {
@Override
public Iterable<Path> apply(Map.Entry<Revision, BranchCommit> input) {
return input.getValue().getModifiedPaths();
}
});
return Iterables.concat(paths);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
base.toStringBuilder(sb).append("[");
String separator = "";
for (Map.Entry<Revision, BranchCommit> c : commits.entrySet()) {
sb.append(separator);
separator = ",";
sb.append(c.getKey()).append("->").append(c.getValue());
}
sb.append("]");
return sb.toString();
}
/**
* Information about a commit within a branch.
*/
abstract static class BranchCommit implements LastRevTracker {
protected final RevisionVector base;
protected final Revision commit;
BranchCommit(RevisionVector base, Revision commit) {
this.base = base;
this.commit = commit;
}
/**
* @return the branch base for this branch commit.
*/
RevisionVector getBase() {
return base;
}
abstract void applyTo(UnsavedModifications trunk, Revision commit);
abstract boolean isModified(Path path);
abstract Iterable<Path> getModifiedPaths();
protected abstract boolean isRebase();
}
/**
* Implements a regular branch commit.
*/
private static class BranchCommitImpl extends BranchCommit {
private final Set<Path> modifications = Sets.newHashSet();
BranchCommitImpl(RevisionVector base, Revision commit) {
super(base, commit);
}
@Override
void applyTo(UnsavedModifications trunk, Revision commit) {
for (Path p : modifications) {
trunk.put(p, commit);
}
}
@Override
boolean isModified(Path path) { // TODO: rather pass NodeDocument?
return modifications.contains(path);
}
@Override
Iterable<Path> getModifiedPaths() {
return modifications;
}
@Override
protected boolean isRebase() {
return false;
}
//------------------< LastRevTracker >----------------------------------
@Override
public void track(Path path) {
modifications.add(path);
}
@Override
public String toString() {
return "B (" + modifications.size() + ")";
}
}
private static class RebaseCommit extends BranchCommit {
private final NavigableMap<Revision, BranchCommit> previous;
RebaseCommit(RevisionVector base, Revision commit,
NavigableMap<Revision, BranchCommit> previous) {
super(base, commit);
this.previous = squash(previous);
}
@Override
void applyTo(UnsavedModifications trunk, Revision commit) {
for (BranchCommit c : previous.values()) {
c.applyTo(trunk, commit);
}
}
@Override
boolean isModified(Path path) {
for (BranchCommit c : previous.values()) {
if (c.isModified(path)) {
return true;
}
}
return false;
}
@Override
protected boolean isRebase() {
return true;
}
@Override
Iterable<Path> getModifiedPaths() {
Iterable<Iterable<Path>> paths = transform(previous.values(),
new Function<BranchCommit, Iterable<Path>>() {
@Override
public Iterable<Path> apply(BranchCommit branchCommit) {
return branchCommit.getModifiedPaths();
}
});
return Iterables.concat(paths);
}
/**
* Filter out the RebaseCommits as they are just container of previous BranchCommit
*
* @param previous branch commit history
* @return filtered branch history only containing non rebase commits
*/
private static NavigableMap<Revision, BranchCommit> squash(NavigableMap<Revision, BranchCommit> previous) {
NavigableMap<Revision, BranchCommit> result = new TreeMap<Revision, BranchCommit>(previous.comparator());
for (Map.Entry<Revision, BranchCommit> e : previous.entrySet()){
if (!e.getValue().isRebase()){
result.put(e.getKey(), e.getValue());
}
}
return result;
}
//------------------< LastRevTracker >----------------------------------
@Override
public void track(Path path) {
throw new UnsupportedOperationException("RebaseCommit is read-only");
}
@Override
public String toString() {
return "R (" + previous.size() + ")";
}
}
final static class BranchReference extends WeakReference<Object> {
private final Branch branch;
private BranchReference(@NotNull ReferenceQueue<Object> queue,
@NotNull Branch branch,
@NotNull Object referent) {
super(checkNotNull(referent), queue);
this.branch = checkNotNull(branch);
}
Branch getBranch() {
return branch;
}
}
}