All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.patch.AutoMerger Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.google.gerrit.server.patch;

import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeFormatter;
import org.eclipse.jgit.merge.MergeResult;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AutoMerger {
  private static final Logger log = LoggerFactory.getLogger(AutoMerger.class);

  public static boolean cacheAutomerge(Config cfg) {
    return cfg.getBoolean("change", null, "cacheAutomerge", true);
  }

  private final PersonIdent gerritIdent;
  private final boolean save;

  @Inject
  AutoMerger(@GerritServerConfig Config cfg, @GerritPersonIdent PersonIdent gerritIdent) {
    save = cacheAutomerge(cfg);
    this.gerritIdent = gerritIdent;
  }

  /**
   * Perform an auto-merge of the parents of the given merge commit.
   *
   * @return auto-merge commit or {@code null} if an auto-merge commit couldn't be created. Headers
   *     of the returned RevCommit are parsed.
   */
  public RevCommit merge(
      Repository repo,
      RevWalk rw,
      final ObjectInserter ins,
      RevCommit merge,
      ThreeWayMergeStrategy mergeStrategy)
      throws IOException {
    checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
    InMemoryInserter tmpIns = null;
    if (ins instanceof InMemoryInserter) {
      // Caller gave us an in-memory inserter, so ensure anything we write from
      // this method is visible to them.
      tmpIns = (InMemoryInserter) ins;
    } else if (!save) {
      // If we don't plan on saving results, use a fully in-memory inserter.
      // Using just a non-flushing wrapper is not sufficient, since in
      // particular DfsInserter might try to write to storage after exceeding an
      // internal buffer size.
      tmpIns = new InMemoryInserter(rw.getObjectReader());
    }

    rw.parseHeaders(merge);
    String refName = RefNames.refsCacheAutomerge(merge.name());
    Ref ref = repo.getRefDatabase().exactRef(refName);
    if (ref != null && ref.getObjectId() != null) {
      RevObject obj = rw.parseAny(ref.getObjectId());
      if (obj instanceof RevCommit) {
        return (RevCommit) obj;
      }
      return commit(repo, rw, tmpIns, ins, refName, obj, merge);
    }

    ResolveMerger m = (ResolveMerger) mergeStrategy.newMerger(repo, true);
    DirCache dc = DirCache.newInCore();
    m.setDirCache(dc);
    m.setObjectInserter(tmpIns == null ? new NonFlushingWrapper(ins) : tmpIns);

    boolean couldMerge;
    try {
      couldMerge = m.merge(merge.getParents());
    } catch (IOException e) {
      // It is not safe to continue further down in this method as throwing
      // an exception most likely means that the merge tree was not created
      // and m.getMergeResults() is empty. This would mean that all paths are
      // unmerged and Gerrit UI would show all paths in the patch list.
      log.warn("Error attempting automerge " + refName, e);
      return null;
    }

    ObjectId treeId;
    if (couldMerge) {
      treeId = m.getResultTreeId();

    } else {
      RevCommit ours = merge.getParent(0);
      RevCommit theirs = merge.getParent(1);
      rw.parseBody(ours);
      rw.parseBody(theirs);
      String oursMsg = ours.getShortMessage();
      String theirsMsg = theirs.getShortMessage();

      String oursName =
          String.format(
              "HEAD   (%s %s)",
              ours.abbreviate(6).name(), oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
      String theirsName =
          String.format(
              "BRANCH (%s %s)",
              theirs.abbreviate(6).name(),
              theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));

      MergeFormatter fmt = new MergeFormatter();
      Map> r = m.getMergeResults();
      Map resolved = new HashMap<>();
      for (Map.Entry> entry : r.entrySet()) {
        MergeResult p = entry.getValue();
        try (TemporaryBuffer buf = new TemporaryBuffer.LocalFile(null, 10 * 1024 * 1024)) {
          fmt.formatMerge(buf, p, "BASE", oursName, theirsName, UTF_8.name());
          buf.close();

          try (InputStream in = buf.openInputStream()) {
            resolved.put(entry.getKey(), ins.insert(Constants.OBJ_BLOB, buf.length(), in));
          }
        }
      }

      DirCacheBuilder builder = dc.builder();
      int cnt = dc.getEntryCount();
      for (int i = 0; i < cnt; ) {
        DirCacheEntry entry = dc.getEntry(i);
        if (entry.getStage() == 0) {
          builder.add(entry);
          i++;
          continue;
        }

        int next = dc.nextEntry(i);
        String path = entry.getPathString();
        DirCacheEntry res = new DirCacheEntry(path);
        if (resolved.containsKey(path)) {
          // For a file with content merge conflict that we produced a result
          // above on, collapse the file down to a single stage 0 with just
          // the blob content, and a randomly selected mode (the lowest stage,
          // which should be the merge base, or ours).
          res.setFileMode(entry.getFileMode());
          res.setObjectId(resolved.get(path));

        } else if (next == i + 1) {
          // If there is exactly one stage present, shouldn't be a conflict...
          res.setFileMode(entry.getFileMode());
          res.setObjectId(entry.getObjectId());

        } else if (next == i + 2) {
          // Two stages suggests a delete/modify conflict. Pick the higher
          // stage as the automatic result.
          entry = dc.getEntry(i + 1);
          res.setFileMode(entry.getFileMode());
          res.setObjectId(entry.getObjectId());

        } else {
          // 3 stage conflict, no resolve above
          // Punt on the 3-stage conflict and show the base, for now.
          res.setFileMode(entry.getFileMode());
          res.setObjectId(entry.getObjectId());
        }
        builder.add(res);
        i = next;
      }
      builder.finish();
      treeId = dc.writeTree(ins);
    }

    return commit(repo, rw, tmpIns, ins, refName, treeId, merge);
  }

  private RevCommit commit(
      Repository repo,
      RevWalk rw,
      @Nullable InMemoryInserter tmpIns,
      ObjectInserter ins,
      String refName,
      ObjectId tree,
      RevCommit merge)
      throws IOException {
    rw.parseHeaders(merge);
    // For maximum stability, choose a single ident using the committer time of
    // the input commit, using the server name and timezone.
    PersonIdent ident =
        new PersonIdent(
            gerritIdent, merge.getCommitterIdent().getWhen(), gerritIdent.getTimeZone());
    CommitBuilder cb = new CommitBuilder();
    cb.setAuthor(ident);
    cb.setCommitter(ident);
    cb.setTreeId(tree);
    cb.setMessage("Auto-merge of " + merge.name() + '\n');
    for (RevCommit p : merge.getParents()) {
      cb.addParentId(p);
    }

    if (!save) {
      checkArgument(tmpIns != null);
      try (ObjectReader tmpReader = tmpIns.newReader();
          RevWalk tmpRw = new RevWalk(tmpReader)) {
        return tmpRw.parseCommit(tmpIns.insert(cb));
      }
    }

    checkArgument(tmpIns == null);
    checkArgument(!(ins instanceof InMemoryInserter));
    ObjectId commitId = ins.insert(cb);
    ins.flush();

    RefUpdate ru = repo.updateRef(refName);
    ru.setNewObjectId(commitId);
    ru.disableRefLog();
    ru.forceUpdate();
    return rw.parseCommit(commitId);
  }

  private static class NonFlushingWrapper extends ObjectInserter.Filter {
    private final ObjectInserter ins;

    private NonFlushingWrapper(ObjectInserter ins) {
      this.ins = ins;
    }

    @Override
    protected ObjectInserter delegate() {
      return ins;
    }

    @Override
    public void flush() {}

    @Override
    public void close() {}
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy