Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

233: Merge bot should be able to multiple branches #365

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -39,20 +39,41 @@
class MergeBot implements Bot, WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final Path storage;
private final HostedRepository from;
private final Branch fromBranch;
private final HostedRepository to;
private final Branch toBranch;
private final HostedRepository toFork;

MergeBot(Path storage, HostedRepository from, Branch fromBranch,
HostedRepository to, Branch toBranch, HostedRepository toFork) {

private final HostedRepository target;
private final HostedRepository fork;
private final List<Spec> specs;

MergeBot(Path storage, HostedRepository target, HostedRepository fork,
List<Spec> specs) {
this.storage = storage;
this.from = from;
this.fromBranch = fromBranch;
this.to = to;
this.toBranch = toBranch;
this.toFork = toFork;
this.target = target;
this.fork = fork;
this.specs = specs;
}

final static class Spec {
private final HostedRepository fromRepo;
private final Branch fromBranch;
private final Branch toBranch;

Spec(HostedRepository fromRepo, Branch fromBranch, Branch toBranch) {
this.fromRepo = fromRepo;
this.fromBranch = fromBranch;
this.toBranch = toBranch;
}

HostedRepository fromRepo() {
return fromRepo;
}

Branch fromBranch() {
return fromBranch;
}

Branch toBranch() {
return toBranch;
}
}

@Override
@@ -61,132 +82,150 @@ public boolean concurrentWith(WorkItem other) {
return true;
}
var otherBot = (MergeBot) other;
return !to.name().equals(otherBot.to.name());
return !target.name().equals(otherBot.target.name());
}

@Override
public void run(Path scratchPath) {
try {
var sanitizedUrl =
URLEncoder.encode(to.webUrl().toString(), StandardCharsets.UTF_8);
URLEncoder.encode(target.webUrl().toString(), StandardCharsets.UTF_8);
var dir = storage.resolve(sanitizedUrl);

Repository repo = null;
if (!Files.exists(dir)) {
log.info("Cloning " + to.name());
log.info("Cloning " + fork.name());
Files.createDirectories(dir);
repo = Repository.clone(toFork.url(), dir);
repo = Repository.clone(fork.url(), dir);
} else {
log.info("Found existing scratch directory for " + to.name());
log.info("Found existing scratch directory for " + fork.name());
repo = Repository.get(dir).orElseThrow(() -> {
return new RuntimeException("Repository in " + dir + " has vanished");
});
}

// Sync personal fork
var remoteBranches = repo.remoteBranches(to.url().toString());
var remoteBranches = repo.remoteBranches(target.url().toString());
for (var branch : remoteBranches) {
var fetchHead = repo.fetch(to.url(), branch.hash().hex());
repo.push(fetchHead, toFork.url(), branch.name());
var fetchHead = repo.fetch(target.url(), branch.hash().hex());
repo.push(fetchHead, fork.url(), branch.name());
}

// Checkout the branch to merge into
repo.pull(toFork.url().toString(), toBranch.name());
repo.checkout(toBranch, false);

// Check if merge conflict pull request is present
var title = "Cannot automatically merge " + from.name() + ":" + fromBranch.name();
var marker = "<!-- MERGE CONFLICTS -->";
for (var pr : to.pullRequests()) {
if (pr.title().equals(title) &&
pr.body().startsWith(marker) &&
to.forge().currentUser().equals(pr.author())) {
var lines = pr.body().split("\n");
var head = new Hash(lines[1].substring(5, 45));
if (repo.contains(toBranch, head)) {
log.info("Closing resolved merge conflict PR " + pr.id());
pr.addComment("Merge conflicts have been resolved, closing this PR");
pr.setState(PullRequest.State.CLOSED);
} else {
log.info("Outstanding unresolved merge already present");
return;
var prs = target.pullRequests();
var currentUser = target.forge().currentUser();

for (var spec : specs) {
var toBranch = spec.toBranch();
var fromRepo = spec.fromRepo();
var fromBranch = spec.fromBranch();

log.info("Trying to merge " + fromRepo.name() + ":" + fromBranch.name() + " to " + toBranch.name());

// Checkout the branch to merge into
repo.pull(fork.url().toString(), toBranch.name());
repo.checkout(toBranch, false);

// Check if merge conflict pull request is present
var isMergeConflictPRPresent = false;
var title = "Cannot automatically merge " + fromRepo.name() + ":" + fromBranch.name() + " to " + toBranch.name();
var marker = "<!-- MERGE CONFLICTS -->";
for (var pr : prs) {
if (pr.title().equals(title) &&
pr.body().startsWith(marker) &&
currentUser.equals(pr.author())) {
var lines = pr.body().split("\n");
var head = new Hash(lines[1].substring(5, 45));
if (repo.contains(toBranch, head)) {
log.info("Closing resolved merge conflict PR " + pr.id());
pr.addComment("Merge conflicts have been resolved, closing this PR");
pr.setState(PullRequest.State.CLOSED);
} else {
log.info("Outstanding unresolved merge already present");
isMergeConflictPRPresent = true;
}
break;
}
}
}

log.info("Fetching " + from.name() + ":" + fromBranch.name());
var fetchHead = repo.fetch(from.url(), fromBranch.name());
var head = repo.resolve(toBranch.name()).orElseThrow(() ->
new IOException("Could not resolve branch " + toBranch.name())
);
if (repo.contains(toBranch, fetchHead)) {
log.info("Nothing to merge");
return;
}
if (isMergeConflictPRPresent) {
continue;
}

var isAncestor = repo.isAncestor(head, fetchHead);
log.info("Fetching " + fromRepo.name() + ":" + fromBranch.name());
var fetchHead = repo.fetch(fromRepo.url(), fromBranch.name());
var head = repo.resolve(toBranch.name()).orElseThrow(() ->
new IOException("Could not resolve branch " + toBranch.name())
);
if (repo.contains(toBranch, fetchHead)) {
log.info("Nothing to merge");
continue;
}

log.info("Trying to merge into " + toBranch.name());
IOException error = null;
try {
repo.merge(fetchHead);
} catch (IOException e) {
error = e;
}
var isAncestor = repo.isAncestor(head, fetchHead);

if (error == null) {
log.info("Pushing successful merge");
if (!isAncestor) {
repo.commit("Merge", "duke", "duke@openjdk.org");
log.info("Trying to merge into " + toBranch.name());
IOException error = null;
try {
repo.merge(fetchHead);
} catch (IOException e) {
error = e;
}
repo.push(toBranch, to.url().toString(), false);
} else {
log.info("Got error: " + error.getMessage());
log.info("Aborting unsuccesful merge");
repo.abortMerge();

var fromRepoName = Path.of(from.webUrl().getPath()).getFileName();
var fromBranchDesc = fromRepoName + "/" + fromBranch.name();
repo.push(fetchHead, toFork.url(), fromBranchDesc, true);

log.info("Creating pull request to alert");
var mergeBase = repo.mergeBase(fetchHead, head);
var commits = repo.commits(mergeBase.hex() + ".." + fetchHead.hex(), true).asList();

var message = new ArrayList<String>();
message.add(marker);
message.add("<!-- " + fetchHead.hex() + " -->");
message.add("The following commits from `" + from.name() + ":" + fromBranch.name() +
"` could *not* be automatically merged into `" + toBranch.name() + "`:");
message.add("");
for (var commit : commits) {
message.add("- " + commit.hash().abbreviate() + ": " + commit.message().get(0));

if (error == null) {
log.info("Pushing successful merge");
if (!isAncestor) {
repo.commit("Merge", "duke", "duke@openjdk.org");
}
repo.push(toBranch, target.url().toString(), false);
} else {
log.info("Got error: " + error.getMessage());
log.info("Aborting unsuccesful merge");
repo.abortMerge();

var fromRepoName = Path.of(fromRepo.webUrl().getPath()).getFileName();
var branchDesc = fromRepoName + "/" + fromBranch.name() + "->" + toBranch.name();
repo.push(fetchHead, fork.url(), branchDesc, true);

log.info("Creating pull request to alert");
var mergeBase = repo.mergeBase(fetchHead, head);
var commits = repo.commits(mergeBase.hex() + ".." + fetchHead.hex(), true).asList();

var message = new ArrayList<String>();
message.add(marker);
message.add("<!-- " + fetchHead.hex() + " -->");
message.add("The following commits from `" + fromRepo.name() + ":" + fromBranch.name() +
"` could *not* be automatically merged into `" + toBranch.name() + "`:");
message.add("");
for (var commit : commits) {
message.add("- " + commit.hash().abbreviate() + ": " + commit.message().get(0));
}
message.add("");
message.add("To manually resolve these merge conflicts, please create a personal fork of " +
target.webUrl() + " and execute the following commands:");
message.add("");
message.add("```bash");
message.add("$ git checkout " + toBranch.name());
message.add("$ git pull " + fromRepo.webUrl() + " " + fromBranch.name());
message.add("```");
message.add("");
message.add("When you have resolved the conflicts resulting from the above commands, run:");
message.add("");
message.add("```bash");
message.add("$ git add paths/to/files/with/conflicts");
message.add("$ git commit -m 'Merge'");
message.add("```");
message.add("");
message.add("Push the resolved merge conflict to your personal fork and " +
"create a pull request towards this repository.");
message.add("");
message.add("This pull request will be closed automatically by a bot once " +
"the merge conflicts have been resolved.");
fork.createPullRequest(target,
toBranch.name(),
branchDesc,
title,
message);
}
message.add("");
message.add("To manually resolve these merge conflicts, please create a personal fork of " +
to.webUrl() + " and execute the following commands:");
message.add("");
message.add("```bash");
message.add("$ git checkout " + toBranch.name());
message.add("$ git pull " + from.webUrl() + " " + fromBranch.name());
message.add("```");
message.add("");
message.add("When you have resolved the conflicts resulting from the above commands, run:");
message.add("");
message.add("```bash");
message.add("$ git add paths/to/files/with/conflicts");
message.add("$ git commit -m 'Merge'");
message.add("```");
message.add("");
message.add("Push the resolved merge conflict to your personal fork and " +
"create a pull request towards this repository.");
message.add("");
message.add("This pull request will be closed automatically by a bot once " +
"the merge conflicts have been resolved.");
var pr = toFork.createPullRequest(to,
toBranch.name(),
fromBranchDesc,
title,
message);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
@@ -195,8 +234,7 @@ public void run(Path scratchPath) {

@Override
public String toString() {
return "MergeBot@(" + from.name() + ":" + fromBranch.name() + "-> "
+ to.name() + ":" + toBranch.name() + ")";
return "MergeBot@(" + target.name() + ")";
}

@Override
@@ -50,16 +50,19 @@ public List<Bot> create(BotConfiguration configuration) {

var bots = new ArrayList<Bot>();
for (var repo : specific.get("repositories").asArray()) {
var fromRepo = configuration.repository(repo.get("from").asString());
var fromBranch = new Branch(configuration.repositoryRef(repo.get("from").asString()));
var targetRepo = configuration.repository(repo.get("target").asString());
var forkRepo = configuration.repository(repo.get("fork").asString());

var toRepo = configuration.repository(repo.get("to").asString());
var toBranch = new Branch(configuration.repositoryRef(repo.get("to").asString()));
var toFork = configuration.repository(repo.get("fork").asString());
var specs = new ArrayList<MergeBot.Spec>();
for (var spec : repo.get("spec").asArray()) {
var from = spec.get("from").asString().split(":");
var fromRepo = configuration.repository(from[0]);
var fromBranch = new Branch(from[1]);
var toBranch = new Branch(spec.get("to").asString());
specs.add(new MergeBot.Spec(fromRepo, fromBranch, toBranch));
}

log.info("Setting up merging from " + fromRepo.name() + ":" + fromBranch.name() +
" to " + toRepo.name() + ":" + toBranch.name());
bots.add(new MergeBot(storage, fromRepo, fromBranch, toRepo, toBranch, toFork));
bots.add(new MergeBot(storage, targetRepo, forkRepo, specs));
}
return bots;
}