/**
* Copyright 2005-2014 Red Hat, Inc.
*
* Red Hat licenses this file to you 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 io.fabric8.git.internal;
import io.fabric8.api.GitContext;
import io.fabric8.git.PullPushPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.MergeResult.MergeStatus;
import org.eclipse.jgit.api.RebaseCommand.Operation;
import org.eclipse.jgit.api.RebaseResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.jboss.gravia.utils.IllegalStateAssertion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default {@link PullPushPolicy}.
*/
public final class DefaultPullPushPolicy implements PullPushPolicy {
private static final transient Logger LOGGER = LoggerFactory.getLogger(DefaultPullPushPolicy.class);
private final Git git;
private final String remoteRef;
private final int gitTimeout;
DefaultPullPushPolicy(Git git, String remoteRef, int gitTimeout) {
this.git = git;
this.remoteRef = remoteRef;
this.gitTimeout = gitTimeout;
}
@Override
public synchronized PullPolicyResult doPull(GitContext context, CredentialsProvider credentialsProvider, boolean allowVersionDelete) {
Repository repository = git.getRepository();
StoredConfig config = repository.getConfig();
String remoteUrl = config.getString("remote", remoteRef, "url");
if (remoteUrl == null) {
LOGGER.debug("No remote repository defined, so not doing a pull");
return new AbstractPullPolicyResult();
}
LOGGER.info("Performing a pull on remote URL: {}", remoteUrl);
Exception lastException = null;
try {
git.fetch().setTimeout(gitTimeout).setCredentialsProvider(credentialsProvider).setRemote(remoteRef).call();
} catch (GitAPIException | JGitInternalException ex) {
lastException = ex;
}
// No meaningful processing after GitAPIException
if (lastException != null) {
LOGGER.warn("Pull failed because of: {}", lastException.toString());
return new AbstractPullPolicyResult(lastException);
}
// Get local and remote branches
Map<String, Ref> localBranches = new HashMap<String, Ref>();
Map<String, Ref> remoteBranches = new HashMap<String, Ref>();
Set<String> allBranches = new HashSet<String>();
try {
for (Ref ref : git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call()) {
if (ref.getName().startsWith("refs/remotes/" + remoteRef + "/")) {
String name = ref.getName().substring(("refs/remotes/" + remoteRef + "/").length());
remoteBranches.put(name, ref);
allBranches.add(name);
} else if (ref.getName().startsWith("refs/heads/")) {
String name = ref.getName().substring(("refs/heads/").length());
localBranches.put(name, ref);
allBranches.add(name);
}
}
boolean localUpdate = false;
boolean remoteUpdate = false;
Set<String> versions = new TreeSet<>();
// Remote repository has no branches, force a push
if (remoteBranches.isEmpty()) {
LOGGER.debug("Pulled from an empty remote repository");
return new AbstractPullPolicyResult(versions, false, !localBranches.isEmpty(), null);
} else {
LOGGER.debug("Processing remote branches: {}", remoteBranches);
}
// Verify master branch and do a checkout of it when we have it locally (already)
IllegalStateAssertion.assertTrue(remoteBranches.containsKey(GitHelpers.MASTER_BRANCH), "Remote repository does not have a master branch");
if (localBranches.containsKey(GitHelpers.MASTER_BRANCH)) {
git.checkout().setName(GitHelpers.MASTER_BRANCH).setForce(true).call();
}
// Iterate over all local/remote branches
for (String branch : allBranches) {
// Delete a local branch that does not exist remotely, but not master
boolean allowDelete = allowVersionDelete && !GitHelpers.MASTER_BRANCH.equals(branch);
if (localBranches.containsKey(branch) && !remoteBranches.containsKey(branch)) {
if (allowDelete) {
LOGGER.debug("Deleting local branch: {}", branch);
git.branchDelete().setBranchNames(branch).setForce(true).call();
localUpdate = true;
} else {
remoteUpdate = true;
}
}
// Create a local branch that exists remotely
else if (!localBranches.containsKey(branch) && remoteBranches.containsKey(branch)) {
LOGGER.debug("Adding local branch: {}", branch);
git.checkout().setCreateBranch(true).setName(branch).setStartPoint(remoteRef + "/" + branch).setUpstreamMode(SetupUpstreamMode.TRACK).setForce(true).call();
versions.add(branch);
localUpdate = true;
}
// Update a local branch that also exists remotely
else if (localBranches.containsKey(branch) && remoteBranches.containsKey(branch)) {
ObjectId localObjectId = localBranches.get(branch).getObjectId();
ObjectId remoteObjectId = remoteBranches.get(branch).getObjectId();
String localCommit = localObjectId.getName();
String remoteCommit = remoteObjectId.getName();
if (!localCommit.equals(remoteCommit)) {
git.clean().setCleanDirectories(true).call();
git.checkout().setName("HEAD").setForce(true).call();
git.checkout().setName(branch).setForce(true).call();
MergeResult mergeResult = git.merge().setFastForward(FastForwardMode.FF_ONLY).include(remoteObjectId).call();
MergeStatus mergeStatus = mergeResult.getMergeStatus();
LOGGER.debug("Updating local branch {} with status: {}", branch, mergeStatus);
if (mergeStatus == MergeStatus.FAST_FORWARD) {
localUpdate = true;
} else if (mergeStatus == MergeStatus.ALREADY_UP_TO_DATE) {
remoteUpdate = true;
} else if (mergeStatus == MergeStatus.ABORTED) {
LOGGER.debug("Cannot fast forward branch {}, attempting rebase", branch);
RebaseResult rebaseResult = git.rebase().setUpstream(remoteCommit).call();
RebaseResult.Status rebaseStatus = rebaseResult.getStatus();
if (rebaseStatus == RebaseResult.Status.OK) {
localUpdate = true;
remoteUpdate = true;
} else {
LOGGER.warn("Rebase on branch {} failed, restoring remote branch", branch);
git.rebase().setOperation(Operation.ABORT).call();
git.checkout().setName(GitHelpers.MASTER_BRANCH).setForce(true).call();
git.branchDelete().setBranchNames(branch).setForce(true).call();
git.checkout().setCreateBranch(true).setName(branch).setStartPoint(remoteRef + "/" + branch).setUpstreamMode(SetupUpstreamMode.TRACK).setForce(true).call();
localUpdate = true;
}
}
}
versions.add(branch);
}
}
PullPolicyResult result = new AbstractPullPolicyResult(versions, localUpdate, remoteUpdate, null);
LOGGER.info("Pull result: {}", result);
return result;
} catch (Exception ex) {
return new AbstractPullPolicyResult(ex);
}
}
@Override
public synchronized PushPolicyResult doPush(GitContext context, CredentialsProvider credentialsProvider) {
StoredConfig config = git.getRepository().getConfig();
String remoteUrl = config.getString("remote", remoteRef, "url");
if (remoteUrl == null) {
LOGGER.debug("No remote repository defined, so not doing a push");
return new AbstractPushPolicyResult();
}
LOGGER.info("Pushing last change to: {}", remoteUrl);
Iterator<PushResult> resit = null;
Exception lastException = null;
try {
resit = git.push().setTimeout(gitTimeout).setCredentialsProvider(credentialsProvider).setPushAll().call().iterator();
} catch (GitAPIException | JGitInternalException ex) {
lastException = ex;
}
// Allow the commit to stay in the repository in case of push failure
if (lastException != null) {
LOGGER.warn("Cannot push because of: {}", lastException.toString());
return new AbstractPushPolicyResult(lastException);
}
List<PushResult> pushResults = new ArrayList<>();
List<RemoteRefUpdate> acceptedUpdates = new ArrayList<>();
List<RemoteRefUpdate> rejectedUpdates = new ArrayList<>();
// Collect the updates that are not ok
while (resit.hasNext()) {
PushResult pushResult = resit.next();
pushResults.add(pushResult);
for (RemoteRefUpdate refUpdate : pushResult.getRemoteUpdates()) {
Status status = refUpdate.getStatus();
if (status == Status.OK || status == Status.UP_TO_DATE) {
acceptedUpdates.add(refUpdate);
} else {
rejectedUpdates.add(refUpdate);
}
}
}
// Reset to the last known good rev and make the commit/push fail
for (RemoteRefUpdate rejectedRef : rejectedUpdates) {
LOGGER.warn("Rejected push: {}" + rejectedRef);
String refName = rejectedRef.getRemoteName();
String branch = refName.substring(refName.lastIndexOf('/') + 1);
try {
GitHelpers.checkoutBranch(git, branch);
FetchResult fetchResult = git.fetch().setTimeout(gitTimeout).setCredentialsProvider(credentialsProvider).setRemote(remoteRef).setRefSpecs(new RefSpec("refs/heads/" + branch)).call();
Ref fetchRef = fetchResult.getAdvertisedRef("refs/heads/" + branch);
git.branchRename().setOldName(branch).setNewName(branch + "-tmp").call();
git.checkout().setCreateBranch(true).setName(branch).setStartPoint(fetchRef.getObjectId().getName()).call();
git.branchDelete().setBranchNames(branch + "-tmp").setForce(true).call();
} catch (GitAPIException ex) {
LOGGER.warn("Cannot reset branch {}, because of: {}", branch, ex.toString());
}
}
PushPolicyResult result = new AbstractPushPolicyResult(pushResults, acceptedUpdates, rejectedUpdates, lastException);
LOGGER.info("Push result: {}", result);
return result;
}
static class AbstractPullPolicyResult implements PullPolicyResult {
private final Set<String> versions = new TreeSet<>();
private final boolean localUpdate;
private final boolean remoteUpdate;
private final Exception lastException;
AbstractPullPolicyResult() {
this(Collections.<String>emptySet(), false, false, null);
}
AbstractPullPolicyResult(Exception lastException) {
this(Collections.<String>emptySet(), false, false, lastException);
}
AbstractPullPolicyResult(Set<String> versions, boolean localUpdate, boolean remoteUpdate, Exception lastException) {
this.versions.addAll(versions);
this.localUpdate = localUpdate;
this.remoteUpdate = remoteUpdate;
this.lastException = lastException;
}
@Override
public boolean localUpdateRequired() {
return localUpdate;
}
@Override
public boolean remoteUpdateRequired() {
return remoteUpdate;
}
@Override
public Set<String> getVersions() {
return Collections.unmodifiableSet(versions);
}
@Override
public Exception getLastException() {
return lastException;
}
@Override
public String toString() {
return "[localUpdate=" + localUpdate + ",remoteUpdate=" + remoteUpdate + ",versions=" + versions + ",error=" + lastException + "]";
}
}
static class AbstractPushPolicyResult implements PushPolicyResult {
private final List<PushResult> pushResults = new ArrayList<>();
private final List<RemoteRefUpdate> acceptedUpdates = new ArrayList<>();
private final List<RemoteRefUpdate> rejectedUpdates = new ArrayList<>();
private final Exception lastException;
AbstractPushPolicyResult() {
this(Collections.<PushResult>emptyList(), Collections.<RemoteRefUpdate>emptyList(), Collections.<RemoteRefUpdate>emptyList(), null);
}
AbstractPushPolicyResult(Exception lastException) {
this(Collections.<PushResult>emptyList(), Collections.<RemoteRefUpdate>emptyList(), Collections.<RemoteRefUpdate>emptyList(), lastException);
}
AbstractPushPolicyResult(List<PushResult> pushResults, List<RemoteRefUpdate> acceptedUpdates, List<RemoteRefUpdate> rejectedUpdates, Exception lastException) {
this.pushResults.addAll(pushResults);
this.acceptedUpdates.addAll(acceptedUpdates);
this.rejectedUpdates.addAll(rejectedUpdates);
this.lastException = lastException;
}
@Override
public List<PushResult> getPushResults() {
return Collections.unmodifiableList(pushResults);
}
@Override
public List<RemoteRefUpdate> getAcceptedUpdates() {
return Collections.unmodifiableList(acceptedUpdates);
}
@Override
public List<RemoteRefUpdate> getRejectedUpdates() {
return Collections.unmodifiableList(rejectedUpdates);
}
@Override
public Exception getLastException() {
return lastException;
}
@Override
public String toString() {
return "[accepted=" + acceptedUpdates.size() + ",rejected=" + rejectedUpdates.size() + ",error=" + lastException + "]";
}
}
}