/* * Copyright (C) 2016 Google Inc. * * Licensed 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 com.google.copybara; import static com.google.common.collect.Queues.newArrayDeque; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.copybara.authoring.Authoring; import com.google.copybara.exception.EmptyChangeException; import com.google.copybara.exception.RepoException; import com.google.copybara.exception.ValidationException; import com.google.copybara.util.Glob; import com.google.copybara.util.console.Console; import com.google.devtools.build.lib.skylarkinterface.StarlarkBuiltin; import com.google.devtools.build.lib.skylarkinterface.StarlarkDocumentationCategory; import com.google.devtools.build.lib.syntax.StarlarkValue; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; /** * A {@code Origin} represents a source control repository from which source is copied. * * @param the origin type of the references/revisions this origin handles */ @StarlarkBuiltin( name = "origin", doc = "A Origin represents a source control repository from which source is copied.", category = StarlarkDocumentationCategory.TOP_LEVEL_TYPE, documented = false) public interface Origin extends ConfigItemDescription, StarlarkValue { /** * Resolves a migration reference into a revision. For example for git it would resolve 'master' * to the SHA-1. * * @throws RepoException if any error happens during the resolve. */ R resolve(String reference) throws RepoException, ValidationException; /** * Show different changes between two references. Returns null if the origin doesn't * support generating differences. * * @throws RepoException */ @Nullable default String showDiff(R revisionFrom, R revisionTo) throws RepoException { return null; } /** * An object which is capable of checking out code from the origin at particular paths. This can * also enumerate changes in the history and transform authorship information. */ interface Reader extends ChangeVisitable { /** * Checks out the revision {@code ref} from the repository into {@code workdir} directory. This * method is not on {@link Revision} in order to prevent {@link Destination} implementations * from getting access to the code pre-transformation. * * @throws RepoException if any error happens during the checkout or workdir preparation. */ void checkout(R ref, Path workdir) throws RepoException, ValidationException; /** * Returns the changes that happen in the interval (fromRef, toRef]. * *

If {@code fromRef} is null, returns all the changes from the first commit of the parent * branch to {@code toRef}, both included. * * @param fromRef the revision used in the latest invocation. If null it means that no previous * ref could be found or that the destination didn't store the ref. * @param toRef current revision to transform. * @throws RepoException if any error happens during the computation of the diff. */ ChangesResponse changes(@Nullable R fromRef, R toRef) throws RepoException, ValidationException; class ChangesResponse { private final ImmutableList> changes; @Nullable private final EmptyReason emptyReason; /** * Changes in key will only be included if the value is included. The usage is for non-linear * histories like git where including a change depends if we end up including the merge * commit. */ private final ImmutableMap, Change> conditionalChanges; private ChangesResponse(ImmutableList> changes, ImmutableMap, Change> conditionalChanges, @Nullable EmptyReason emptyReason) { this.changes = Preconditions.checkNotNull(changes); this.conditionalChanges = Preconditions.checkNotNull(conditionalChanges); this.emptyReason = emptyReason; Preconditions.checkArgument(changes.isEmpty() ^ emptyReason == null, "Either we have" + " changes or we have an empty reason"); } public static ChangesResponse forChanges( Iterable> changes) { Preconditions.checkArgument(!Iterables.isEmpty(changes), "Empty changes not allowed"); return new ChangesResponse<>(ImmutableList.copyOf(changes), ImmutableMap.copyOf(ImmutableMap.of()), /*emptyReason=*/ null); } /** * Build a ChangeResponse object with changes where some of them are conditional to their * closest first-parent root being included (merge commit). */ public static ChangesResponse forChangesWithMerges( Iterable> changes) { Preconditions.checkArgument(!Iterables.isEmpty(changes), "Shouldn't be called for empty changes"); Map> byRevision = new HashMap<>(); List> all = new ArrayList<>(); for (Change e : changes) { all.add(e); byRevision.put(e.getRevision(), e); } List> firstParents = new ArrayList<>(); Set toSkip = new HashSet<>(); Change latest = Iterables.getLast(changes); // Compute first parents and add them to toSkip so that they are not counted as conditional // changes later. while (true) { firstParents.add(latest); // We don't want to add first parents as conditional changes toSkip.add(latest.getRevision()); if (parents(latest).isEmpty()) { break; } R firstParent = parents(latest).get(0); Change firstParentChange = byRevision.get(firstParent); if (firstParentChange == null) { break; } latest = firstParentChange; } Map, Change> conditionalChanges = new HashMap<>(); // Traverse from old to new so that we use oldest first-parent as the conditional change. for (Change firstParent : Lists.reverse(firstParents)) { // Skip non-merges if (parents(firstParent).size() < 2) { continue; } Deque toVisit = newArrayDeque(Iterables.skip(parents(firstParent), 1)); while (!toVisit.isEmpty()) { R revision = toVisit.poll(); // Don't traverse again non-first parents already visited: This is for performance and // correctness, the conditional changes is the oldest merge first-parent. if (!toSkip.add(revision)) { continue; } Change change = byRevision.get(revision); if (change == null) { continue; } conditionalChanges.put(change, firstParent); toVisit.addAll(parents(change)); } } return new ChangesResponse<>(ImmutableList.copyOf((Iterable>) all), ImmutableMap.copyOf(conditionalChanges), /*emptyReason=*/ null); } private static ImmutableList parents(Change change) { return Preconditions.checkNotNull(change.getParents(), "Don't use forChangesWithParents for changes that don't support parents: ", change); } /** * Create a {@code ChangeResponse} that doesn't contain any change */ public static ChangesResponse noChanges(EmptyReason emptyReason) { Preconditions.checkNotNull(emptyReason); return new ChangesResponse<>(ImmutableList.of(), ImmutableMap.of(), emptyReason); } /** * Returns true if there are no changes. */ public boolean isEmpty() { return changes.isEmpty(); } public EmptyReason getEmptyReason() { Preconditions.checkNotNull(emptyReason, "Use isEmpty() first"); return emptyReason; } /** * The changes that happen in the interval (fromRef, toRef]. * *

The list might include changes that shouldn't be included in the final list of changes. * Check conditionalChanges for changes that might not be included. */ public ImmutableList> getChanges() { Preconditions.checkNotNull(changes, "Use isEmpty() first"); return changes; } /** * Changes that should only be included if the change in the value is also included. */ ImmutableMap, Change> getConditionalChanges() { return conditionalChanges; } /** Reason why {@link Origin.Reader#changes(Revision, Revision)} didn't return any change */ public enum EmptyReason { /** 'from' is ancestor of 'to' but all changes are for irrelevant files */ NO_CHANGES, /** There is no parent/child relationship between 'from' and 'to' */ UNRELATED_REVISIONS, /* 'to' is equal or ancestor of 'from' */ TO_IS_ANCESTOR, } } /** * Returns true if the origin repository supports maintaining a history of changes. Generally * this should be true */ default boolean supportsHistory() { return true; } /** * Returns a change identified by {@code ref}. * * @param ref current revision to transform. * @throws RepoException if any error happens during the computation of the diff. */ Change change(R ref) throws RepoException, EmptyChangeException; /** * Finds the baseline of startRevision. Most of the implementations will use the label to * look for the closest parent with that label, but there might be other kind of implementations * that ignore it. * *

If the the label is present in a change multiple times it generally uses the last * appearance. */ default Optional> findBaseline(R startRevision, String label) throws RepoException, ValidationException { FindLatestWithLabel visitor = new FindLatestWithLabel<>(startRevision, label); visitChanges(startRevision, visitor); return visitor.getBaseline(); } /** * Find the baseline of the change without using a label. That means that it will use the * specific system information to compute the parent. For example for GH PR, it will return the * baseline submitted SHA-1. * *

The order is chronologically reversed. First element is the most recent one. In other * words, the best suitable baseline should be element 0, then 1, etc. */ default ImmutableList findBaselinesWithoutLabel(R startRevision, int limit) throws RepoException, ValidationException { throw new ValidationException("Origin does't support this workflow mode"); } class FindLatestWithLabel implements ChangesVisitor { private final R startRevision; private final String label; @Nullable private Baseline baseline; public FindLatestWithLabel(R startRevision, String label) { this.startRevision = Preconditions.checkNotNull(startRevision); this.label = Preconditions.checkNotNull(label); } public Optional> getBaseline() { return Optional.ofNullable(baseline); } @SuppressWarnings("unchecked") @Override public VisitResult visit(Change input) { if (input.getRevision().asString().equals(startRevision.asString())) { return VisitResult.CONTINUE; } ImmutableMap> labels = input.getLabels().asMap(); if (!labels.containsKey(label)) { return VisitResult.CONTINUE; } baseline = new Baseline<>(Iterables.getLast(labels.get(label)), (R) input.getRevision()); return VisitResult.TERMINATE; } } /** * Utility endpoint for accessing and adding feedback data. * @param console */ default Endpoint getFeedbackEndPoint(Console console) throws ValidationException { return Endpoint.NOOP_ENDPOINT; } } /** * Creates a new reader of this origin. * * @param originFiles indicates which files in the origin repository need to be read. Note that * the reader does not necessarily need to remove files after checking them out according to * the glob - that is actually done automatically by the {@link Workflow}. However, some * {@link Origin} implementations may choose to optimize operations on the repo based on the * glob. * @param authoring the authoring object used for constructing the Author objects. * @throws ValidationException if the reader could not be created because of a user error. For * instance, the origin cannot be used with the given {@code originFiles}. */ Reader newReader(Glob originFiles, Authoring authoring) throws ValidationException; /** * Label name to be used in when creating a commit message in the destination to refer to a * revision. For example "Git-RevId". */ String getLabelName(); /** * Represents a baseline pointer in the origin * @param */ class Baseline { private final String baseline; private final R originRevision; public Baseline(String baseline, @Nullable R originRevision) { this.baseline = Preconditions.checkNotNull(baseline); this.originRevision = originRevision; } /** * The baseline reference that will be used in the destination. */ public String getBaseline() { return baseline; } /** * A reference to the origin revision where the baseline was found. */ @Nullable public R getOriginRevision() { return originRevision; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("baseline", baseline) .add("originRevision", originRevision) .toString(); } } }