blob: fd982af58b4641c95b11d466c5558dd7fe8cefee [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.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.json.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.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<>();
@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.id(), psa);
}
@Inject protected PatchSetArgument.Factory patchSetArgumentFactory;
@Inject protected MergeBranch.Factory mergeBranchFactory;
@Inject protected BatchCloser batchCloser;
@Inject protected GitRepositoryManager repoManager;
protected Map<PatchSet.Id, List<ObjectId>> parentsByPsarg = new HashMap<>();
@Override
public void run() throws Exception {
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, RepositoryNotFoundException {
for (ObjectId sha1 : sha1s) {
if (isParentMergedInto(psarg, sha1)) {
return true;
}
}
return false;
}
protected boolean isParentMergedInto(PatchSetArgument psarg, ObjectId sha1)
throws IOException, RepositoryNotFoundException {
List<ObjectId> parents = getParents(psarg);
if (parents.isEmpty()) {
return true;
}
for (ObjectId parent : parents) {
Project.NameKey project = psarg.change.getDest().project();
if (isMergedInto(project, parent, sha1)) {
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(BranchNameKey branch)
throws IOException, NoSuchRefException, RepositoryNotFoundException {
try (Repository repo = repoManager.openRepository(branch.project())) {
Ref ref = repo.getRefDatabase().exactRef(branch.branch());
if (ref == null) {
throw new NoSuchRefException(branch.toString());
}
return ref.getObjectId();
}
}
protected void merge(Batch batch, Change change, PatchSet ps)
throws Exception, IOException, NoSuchRefException, UnloggedFailure {
BranchNameKey branch = change.getDest();
Batch.Destination dest = batch.getDestination(branch);
dest.sha1 =
mergeBranchFactory
.create(
branch,
dest.sha1,
ps.refName(),
strategy.getMergeStrategy(),
fastForward.getFastForwardMode(),
message)
.call()
.getName();
dest.add(ps.id());
}
/* 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 {
private static final long serialVersionUID = 1L;
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<>();
Set<ObjectId> sources = new HashSet<>();
Destination(BranchNameKey branch) throws IOException, NoSuchRefException {
sources.add(getTip(branch));
}
}
protected Map<BranchNameKey, Destination> destinationsByBranches = new HashMap<>();
protected List<PatchSetArgument> resolved = new ArrayList<>();
protected Resolver(Iterable<PatchSetArgument> psargs)
throws Exception, IOException, 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, 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, RepositoryNotFoundException {
boolean found = false;
for (PatchSetArgument psarg : dest.remaining) {
if (isParentMergedInto(psarg, dest.sources)) {
found = true;
resolved.add(psarg);
dest.sources.add(psarg.patchSet.commitId());
}
}
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 {
BranchNameKey branch = psarg.change.getDest();
Destination dest = getDestination(branch);
dest.remaining.add(psarg);
}
protected Destination getDestination(BranchNameKey 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.id();
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 id = psarg.patchSet.commitId();
RevCommit c = revWalk.parseCommit(id);
for (RevCommit parent : c.getParents()) {
parents.add(parent);
}
return parents;
}
}
}