blob: b25e11087f2ce32ce5b8ca1a0db6fb94f60a4676 [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// 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.googlesource.gerrit.plugins.batch.ssh;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.util.cli.Options;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.batch.Batch;
import com.googlesource.gerrit.plugins.batch.BatchCloser;
import com.googlesource.gerrit.plugins.batch.cli.FastForwardOptions;
import com.googlesource.gerrit.plugins.batch.cli.MergeStrategyOption;
import com.googlesource.gerrit.plugins.batch.cli.PatchSetArgument;
import com.googlesource.gerrit.plugins.batch.util.MergeBranch;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@CommandMetaData(
name = "merge-change",
description = "Merge changes in the git repository to a batch")
public class MergeChangeCommand extends SshCommand {
@Inject @Options public MergeStrategyOption strategy;
@Inject @Options public FastForwardOptions fastForward;
@Option(
name = "--message",
aliases = "-m",
metaVar = "MESSAGE",
usage = "commit message to use when applying a change")
public String message;
@Option(name = "--close", usage = "close batch on merge success")
public boolean close;
protected LinkedHashMap<PatchSet.Id, PatchSetArgument> patchSetArgumentsByPatchSet =
new LinkedHashMap<PatchSet.Id, PatchSetArgument>();
@Argument(
index = 0,
required = true,
multiValued = true,
metaVar = "{CHANGE,PATCHSET}",
usage = "list of patch sets to merge")
protected void addPatchSetId(final String token) {
PatchSetArgument psa = patchSetArgumentFactory.createForArgument(token);
psa.ensureLatest();
patchSetArgumentsByPatchSet.put(psa.patchSet.getId(), psa);
}
@Inject protected PatchSetArgument.Factory patchSetArgumentFactory;
@Inject protected MergeBranch.Factory mergeBranchFactory;
@Inject protected BatchCloser batchCloser;
@Inject protected ReviewDb db;
@Inject protected IdentifiedUser user;
@Inject protected GitRepositoryManager repoManager;
protected Map<PatchSet.Id, List<ObjectId>> parentsByPsarg =
new HashMap<PatchSet.Id, List<ObjectId>>();
@Override
public void run() throws Exception {
parseCommandLine();
Batch batch = new Batch(user.getAccountId());
String err = null;
try {
Resolver resolver = new Resolver(patchSetArgumentsByPatchSet.values());
for (PatchSetArgument psarg : resolver.resolved) {
err = "Couldn't merge change(" + psarg.patchSet + ") to batch(" + batch.id + ")";
merge(batch, psarg.change, psarg.patchSet);
}
if (close) {
err = "Could not close batch(" + batch.id + ")";
batchCloser.close(batch);
}
} catch (Exception e) {
String msg = e.getMessage();
if (msg != null) {
err += ": " + msg;
}
throw die(err);
}
batch.version = null;
out.write((OutputFormat.JSON.newGson().toJson(batch) + "\n").getBytes(ENC));
out.flush();
}
protected boolean isParentMergedInto(PatchSetArgument psarg, Iterable<ObjectId> sha1s)
throws IOException, OrmException, RepositoryNotFoundException {
for (ObjectId sha1 : sha1s) {
if (isParentMergedInto(psarg, sha1)) {
return true;
}
}
return false;
}
protected boolean isParentMergedInto(PatchSetArgument psarg, ObjectId sha1)
throws IOException, OrmException, RepositoryNotFoundException {
List<ObjectId> parents = getParents(psarg);
if (parents.isEmpty()) {
return true;
}
for (ObjectId parent : parents) {
Project.NameKey project = psarg.change.getDest().getParentKey();
Boolean isMergedInto = isMergedInto(project, parent, sha1);
if (isMergedInto == null) {
throw new IOException();
}
if (isMergedInto) {
return true;
}
}
return false;
}
public boolean isMergedInto(Project.NameKey project, ObjectId needle, ObjectId haystack)
throws IOException {
try (Repository repo = repoManager.openRepository(project);
RevWalk walk = new RevWalk(repo)) {
return walk.isMergedInto(walk.parseCommit(needle), walk.parseCommit(haystack));
}
}
protected ObjectId getTip(Branch.NameKey branch)
throws IOException, NoSuchRefException, RepositoryNotFoundException {
try (Repository repo = repoManager.openRepository(branch.getParentKey())) {
Ref ref = repo.getRefDatabase().getRef(branch.get());
if (ref == null) {
throw new NoSuchRefException(branch.toString());
}
return ref.getObjectId();
}
}
protected void merge(Batch batch, Change change, PatchSet ps)
throws Exception, IOException, NoSuchRefException, OrmException, UnloggedFailure {
Branch.NameKey branch = change.getDest();
Batch.Destination dest = batch.getDestination(branch);
dest.sha1 =
mergeBranchFactory
.create(
branch,
dest.sha1,
ps.getRefName(),
strategy.getMergeStrategy(),
fastForward.getFastForwardMode(),
message)
.call()
.getName();
dest.add(ps.getId());
}
/* A Resolver which ensures that changes are eligible to merge before
* resolving them. Once resolved, changes are ordered to minimize the
* amount of merge commits required to merge them.
*/
protected class Resolver {
protected class ParentsNotOnBranchException extends Exception {
protected ParentsNotOnBranchException(PatchSetArgument psarg) {
super(
"No Parent of "
+ psarg.patchSet
+ " is on its destination branch("
+ psarg.change.getDest()
+ ")");
}
}
protected class Destination {
List<PatchSetArgument> remaining = new ArrayList<PatchSetArgument>();
Set<ObjectId> sources = new HashSet<ObjectId>();
Destination(Branch.NameKey branch) throws IOException, NoSuchRefException {
sources.add(getTip(branch));
}
}
protected Map<Branch.NameKey, Destination> destinationsByBranches =
new HashMap<Branch.NameKey, Destination>();
protected List<PatchSetArgument> resolved = new ArrayList<PatchSetArgument>();
protected Resolver(Iterable<PatchSetArgument> psargs)
throws Exception, IOException, OrmException, NoSuchRefException,
RepositoryNotFoundException {
add(psargs);
while (resolve()) {}
for (Destination dest : destinationsByBranches.values()) {
if (!dest.remaining.isEmpty()) {
throw new ParentsNotOnBranchException(dest.remaining.get(0));
}
}
Collections.reverse(resolved); // Reduces merges
}
protected boolean resolve()
throws IOException, OrmException, NoSuchRefException, RepositoryNotFoundException {
boolean found = false;
for (Destination dest : destinationsByBranches.values()) {
// If more dependencies are destined for the same branch than not,
// then resolving a branch as much as possible will reduce the
// total iterations required.
while (resolve(dest)) {
found = true;
}
}
return found;
}
protected boolean resolve(Destination dest)
throws IOException, OrmException, NoSuchRefException, RepositoryNotFoundException {
boolean found = false;
for (PatchSetArgument psarg : dest.remaining) {
if (isParentMergedInto(psarg, dest.sources)) {
found = true;
resolved.add(psarg);
dest.sources.add(ObjectId.fromString(psarg.patchSet.getRevision().get()));
}
}
dest.remaining.removeAll(resolved);
return found;
}
protected void add(Iterable<PatchSetArgument> psargs) throws IOException, NoSuchRefException {
for (PatchSetArgument psarg : psargs) {
add(psarg);
}
}
protected void add(PatchSetArgument psarg) throws IOException, NoSuchRefException {
Branch.NameKey branch = psarg.change.getDest();
Destination dest = getDestination(branch);
dest.remaining.add(psarg);
}
protected Destination getDestination(Branch.NameKey b) throws IOException, NoSuchRefException {
Destination dest = destinationsByBranches.get(b);
if (dest == null) {
dest = new Destination(b);
destinationsByBranches.put(b, dest);
}
return dest;
}
}
protected List<ObjectId> getParents(PatchSetArgument psarg) throws IOException {
PatchSet.Id id = psarg.patchSet.getId();
List<ObjectId> parents = parentsByPsarg.get(id);
if (parents == null) {
parents = loadParents(psarg);
parentsByPsarg.put(id, parents);
}
return parents;
}
protected List<ObjectId> loadParents(PatchSetArgument psarg) throws IOException {
try (Repository repo = repoManager.openRepository(psarg.change.getProject());
RevWalk revWalk = new RevWalk(repo)) {
List<ObjectId> parents = new ArrayList<ObjectId>();
ObjectId id = ObjectId.fromString(psarg.patchSet.getRevision().get());
RevCommit c = revWalk.parseCommit(id);
for (RevCommit parent : c.getParents()) {
parents.add(parent);
}
return parents;
}
}
}