PushOperation is designed to handle push-to-one and push-to-many URIs, using some supporting classes. Failure of connection to any of repositories doesn't cause to fail whole operation - they are independent that way. It is also possible to specify different expected old object id for each remote repository. Signed-off-by: Marek Zawirski <marek.zawirski@xxxxxxxxx> --- .../src/org/spearce/egit/core/CoreText.java | 15 + .../src/org/spearce/egit/core/coretext.properties | 6 + .../org/spearce/egit/core/op/PushOperation.java | 148 +++++++++++ .../spearce/egit/core/op/PushOperationResult.java | 273 ++++++++++++++++++++ .../egit/core/op/PushOperationSpecification.java | 82 ++++++ 5 files changed, 524 insertions(+), 0 deletions(-) create mode 100644 org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperation.java create mode 100644 org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationResult.java create mode 100644 org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationSpecification.java diff --git a/org.spearce.egit.core/src/org/spearce/egit/core/CoreText.java b/org.spearce.egit.core/src/org/spearce/egit/core/CoreText.java index 5974a5f..35e17b9 100644 --- a/org.spearce.egit.core/src/org/spearce/egit/core/CoreText.java +++ b/org.spearce.egit.core/src/org/spearce/egit/core/CoreText.java @@ -98,6 +98,21 @@ public class CoreText extends NLS { /** */ public static String ListRemoteOperation_title; + /** */ + public static String PushOperation_resultCancelled; + + /** */ + public static String PushOperation_resultNotSupported; + + /** */ + public static String PushOperation_resultTransportError; + + /** */ + public static String PushOperation_taskNameDryRun; + + /** */ + public static String PushOperation_taskNameNormalRun; + static { final Class c = CoreText.class; initializeMessages(c.getPackage().getName() + ".coretext", c); diff --git a/org.spearce.egit.core/src/org/spearce/egit/core/coretext.properties b/org.spearce.egit.core/src/org/spearce/egit/core/coretext.properties index c412161..94cf4aa 100644 --- a/org.spearce.egit.core/src/org/spearce/egit/core/coretext.properties +++ b/org.spearce.egit.core/src/org/spearce/egit/core/coretext.properties @@ -57,3 +57,9 @@ CheckpointJob_failed=Failed to write modified objects. CloneOperation_title=Cloning from {0} ListRemoteOperation_title=Getting remote branches information + +PushOperation_resultCancelled=Operation was cancelled. +PushOperation_resultNotSupported=Can't push to {0} +PushOperation_resultTransportError=Transport error occured during push operation: {0} +PushOperation_taskNameDryRun=Trying pushing to remote repositories +PushOperation_taskNameNormalRun=Pushing to remote repositories diff --git a/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperation.java b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperation.java new file mode 100644 index 0000000..853bcfc --- /dev/null +++ b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperation.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@xxxxxxxxx> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * See LICENSE for the full license text, also available. + *******************************************************************************/ +package org.spearce.egit.core.op; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.SubProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.osgi.util.NLS; +import org.spearce.egit.core.CoreText; +import org.spearce.egit.core.EclipseGitProgressTransformer; +import org.spearce.jgit.errors.NotSupportedException; +import org.spearce.jgit.errors.TransportException; +import org.spearce.jgit.lib.Repository; +import org.spearce.jgit.transport.PushResult; +import org.spearce.jgit.transport.RemoteConfig; +import org.spearce.jgit.transport.Transport; +import org.spearce.jgit.transport.URIish; + +/** + * Push operation: pushing from local repository to one or many remote ones. + */ +public class PushOperation implements IRunnableWithProgress { + private static final int WORK_UNITS_PER_TRANSPORT = 10; + + private final Repository localDb; + + private final PushOperationSpecification specification; + + private final boolean dryRun; + + private final RemoteConfig rc; + + private final PushOperationResult operationResult = new PushOperationResult(); + + /** + * Create push operation for provided specification. + * <p> + * Operation is not performed within constructor, + * {@link #run(IProgressMonitor)} method must be called for that. + * + * @param localDb + * local repository. + * @param specification + * specification of ref updates for remote repositories. + * @param rc + * optional remote config to apply on used transports. May be + * null. + * @param dryRun + * true if push operation should just check for possible result + * and not really update remote refs, false otherwise - when push + * should act normally. + */ + public PushOperation(final Repository localDb, + final PushOperationSpecification specification, + final boolean dryRun, final RemoteConfig rc) { + this.localDb = localDb; + this.specification = specification; + this.dryRun = dryRun; + this.rc = rc; + } + + /** + * @return push operation result. + */ + public PushOperationResult getOperationResult() { + return operationResult; + } + + /** + * @return operation specification, as provided in constructor. + */ + public PushOperationSpecification getSpecification() { + return specification; + } + + /** + * Execute operation and store result. Operation is executed independently + * on each remote repository. + * <p> + * + * @throws InvocationTargetException + * Cause of this exceptions may include + * {@link TransportException}, {@link NotSupportedException} or + * some unexpected {@link RuntimeException}. + * @see IRunnableWithProgress#run(IProgressMonitor) + */ + public void run(IProgressMonitor monitor) throws InvocationTargetException { + if (monitor == null) + monitor = new NullProgressMonitor(); + + final int totalWork = specification.getURIsNumber() + * WORK_UNITS_PER_TRANSPORT; + if (dryRun) + monitor.beginTask(CoreText.PushOperation_taskNameDryRun, totalWork); + else + monitor.beginTask(CoreText.PushOperation_taskNameNormalRun, + totalWork); + + for (final URIish uri : specification.getURIs()) { + final SubProgressMonitor subMonitor = new SubProgressMonitor( + monitor, WORK_UNITS_PER_TRANSPORT, + SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK); + Transport transport = null; + try { + if (monitor.isCanceled()) { + operationResult.addOperationResult(uri, + CoreText.PushOperation_resultCancelled); + continue; + } + transport = Transport.open(localDb, uri); + + if (rc != null) + transport.applyConfig(rc); + transport.setDryRun(dryRun); + final EclipseGitProgressTransformer gitSubMonitor = new EclipseGitProgressTransformer( + subMonitor); + final PushResult pr = transport.push(gitSubMonitor, + specification.getRefUpdates(uri)); + operationResult.addOperationResult(uri, pr); + } catch (final TransportException e) { + operationResult.addOperationResult(uri, NLS.bind( + CoreText.PushOperation_resultTransportError, e + .getMessage())); + } catch (final NotSupportedException e) { + operationResult.addOperationResult(uri, NLS.bind( + CoreText.PushOperation_resultNotSupported, e + .getMessage())); + } finally { + if (transport != null) { + transport.close(); + } + // Dirty trick to get things always working. + subMonitor.beginTask("", WORK_UNITS_PER_TRANSPORT); //$NON-NLS-1$ + subMonitor.done(); + subMonitor.done(); + } + } + monitor.done(); + } +} diff --git a/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationResult.java b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationResult.java new file mode 100644 index 0000000..d78b79c --- /dev/null +++ b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationResult.java @@ -0,0 +1,273 @@ +/******************************************************************************* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@xxxxxxxxx> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * See LICENSE for the full license text, also available. + *******************************************************************************/ +package org.spearce.egit.core.op; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Set; + +import org.spearce.jgit.lib.ObjectId; +import org.spearce.jgit.lib.Ref; +import org.spearce.jgit.transport.PushResult; +import org.spearce.jgit.transport.RemoteRefUpdate; +import org.spearce.jgit.transport.URIish; + +/** + * Data class for storing push operation results for each remote repository/URI + * being part of push operation. + * <p> + * One instance of this class is dedicated for result of one push operation: + * either to one URI or to many URIs. + * + * @see PushOperation + */ +public class PushOperationResult { + private LinkedHashMap<URIish, Entry> urisEntries; + + /** + * Construct empty push operation result. + */ + public PushOperationResult() { + this.urisEntries = new LinkedHashMap<URIish, Entry>(); + } + + /** + * Add push result for the repository (URI) with successful connection. + * + * @param uri + * remote repository URI. + * @param result + * push result. + */ + public void addOperationResult(final URIish uri, final PushResult result) { + urisEntries.put(uri, new Entry(result)); + } + + /** + * Add error message for the repository (URI) with unsuccessful connection. + * + * @param uri + * remote repository URI. + * @param errorMessage + * failure error message. + */ + public void addOperationResult(final URIish uri, final String errorMessage) { + urisEntries.put(uri, new Entry(errorMessage)); + } + + /** + * @return set of remote repositories URIis. Set is ordered in addition + * sequence, which is usually the same as that from + * {@link PushOperationSpecification}. + */ + public Set<URIish> getURIs() { + return Collections.unmodifiableSet(urisEntries.keySet()); + } + + /** + * @param uri + * remote repository URI. + * @return true if connection was successful for this repository (URI), + * false if this operation ended with unsuccessful connection. + */ + public boolean isSuccessfulConnection(final URIish uri) { + return urisEntries.get(uri).isSuccessfulConnection(); + } + + /** + * @return true if connection was successful for any repository (URI), false + * otherwise. + */ + public boolean isSuccessfulConnectionForAnyURI() { + for (final URIish uri : getURIs()) { + if (isSuccessfulConnection(uri)) + return true; + } + return false; + } + + /** + * @param uri + * remote repository URI. + * @return push result for this repository (URI) or null if operation ended + * with unsuccessful connection for this URI. + */ + public PushResult getPushResult(final URIish uri) { + return urisEntries.get(uri).getResult(); + } + + /** + * @param uri + * remote repository URI. + * @return error message for this repository (URI) or null if operation + * ended with successful connection for this URI. + */ + public String getErrorMessage(final URIish uri) { + return urisEntries.get(uri).getErrorMessage(); + } + + /** + * @return string being list of failed URIs with their error messages. + */ + public String getErrorStringForAllURis() { + final StringBuilder sb = new StringBuilder(); + boolean first = true; + for (final URIish uri : getURIs()) { + if (first) + first = false; + else + sb.append(", "); + sb.append(uri); + sb.append(" ("); + sb.append(getErrorMessage(uri)); + sb.append(")"); + } + return sb.toString(); + } + + /** + * Derive push operation specification from this push operation result. + * <p> + * Specification is created basing on URIs of remote repositories in this + * result that completed without connection errors, and remote ref updates + * from push results. + * <p> + * This method is targeted to provide support for 2-stage push, where first + * operation is dry run for user confirmation and second one is a real + * operation. + * + * @param requireUnchanged + * if true, newly created copies of remote ref updates have + * expected old object id set to previously advertised ref value + * (remote ref won't be updated if it change in the mean time), + * if false, newly create copies of remote ref updates have + * expected object id set up as in this result source + * specification. + * @return derived specification for another push operation. + * @throws IOException + * when some previously locally available source ref is not + * available anymore, or some error occurred during creation + * locally tracking ref update. + * + */ + public PushOperationSpecification deriveSpecification( + final boolean requireUnchanged) throws IOException { + final PushOperationSpecification spec = new PushOperationSpecification(); + for (final URIish uri : getURIs()) { + final PushResult pr = getPushResult(uri); + if (pr == null) + continue; + + final Collection<RemoteRefUpdate> oldUpdates = pr + .getRemoteUpdates(); + final ArrayList<RemoteRefUpdate> newUpdates = new ArrayList<RemoteRefUpdate>( + oldUpdates.size()); + for (final RemoteRefUpdate rru : oldUpdates) { + final ObjectId expectedOldObjectId; + if (requireUnchanged) { + final Ref advertisedRef = getPushResult(uri) + .getAdvertisedRef(rru.getRemoteName()); + if (advertisedRef == null) + expectedOldObjectId = ObjectId.zeroId(); + else + expectedOldObjectId = advertisedRef.getObjectId(); + } else + expectedOldObjectId = rru.getExpectedOldObjectId(); + final RemoteRefUpdate newRru = new RemoteRefUpdate(rru, + expectedOldObjectId); + newUpdates.add(newRru); + } + spec.addURIRefUpdates(uri, newUpdates); + } + return spec; + } + + /** + * This implementation returns true if all following conditions are met: + * <ul> + * <li>both objects result have the same set successfully connected + * repositories (URIs) - unsuccessful connections are discarded, AND <li> + * remote ref updates must match for each successful connection in sense of + * equal remoteName, equal status and equal newObjectId value.</li> + * </ul> + * + * @see Object#equals(Object) + * @param obj + * other push operation result to compare to. + * @return true if object is equal to this one in terms of conditions + * described above, false otherwise. + */ + @Override + public boolean equals(final Object obj) { + if (!(obj instanceof PushOperationResult)) + return false; + + final PushOperationResult other = (PushOperationResult) obj; + + // Check successful connections/URIs two-ways: + final Set<URIish> otherURIs = other.getURIs(); + for (final URIish uri : getURIs()) { + if (isSuccessfulConnection(uri) + && (!otherURIs.contains(uri) || !other + .isSuccessfulConnection(uri))) + return false; + } + for (final URIish uri : other.getURIs()) { + if (other.isSuccessfulConnection(uri) + && (!urisEntries.containsKey(uri) || !isSuccessfulConnection(uri))) + return false; + } + + for (final URIish uri : getURIs()) { + if (!isSuccessfulConnection(uri)) + continue; + + final PushResult otherPushResult = other.getPushResult(uri); + for (final RemoteRefUpdate rru : getPushResult(uri) + .getRemoteUpdates()) { + final RemoteRefUpdate otherRru = otherPushResult + .getRemoteUpdate(rru.getRemoteName()); + if (otherRru == null) + return false; + if (otherRru.getStatus() != rru.getStatus() + || otherRru.getNewObjectId() != rru.getNewObjectId()) + return false; + } + } + return true; + } + + private static class Entry { + private String errorMessage; + + private PushResult result; + + Entry(final PushResult result) { + this.result = result; + } + + Entry(final String errorMessage) { + this.errorMessage = errorMessage; + } + + boolean isSuccessfulConnection() { + return result != null; + } + + String getErrorMessage() { + return errorMessage; + } + + PushResult getResult() { + return result; + } + } +} diff --git a/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationSpecification.java b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationSpecification.java new file mode 100644 index 0000000..0e3f3b4 --- /dev/null +++ b/org.spearce.egit.core/src/org/spearce/egit/core/op/PushOperationSpecification.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (C) 2008, Marek Zawirski <marek.zawirski@xxxxxxxxx> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * See LICENSE for the full license text, also available. + *******************************************************************************/ +package org.spearce.egit.core.op; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Set; + +import org.spearce.jgit.transport.RemoteRefUpdate; +import org.spearce.jgit.transport.URIish; + +/** + * Data class storing push operation update specifications for each remote + * repository. + * <p> + * One instance is dedicated for one push operation: either push to one URI or + * to many URIs. + * + * @see PushOperation + */ +public class PushOperationSpecification { + private LinkedHashMap<URIish, Collection<RemoteRefUpdate>> urisRefUpdates; + + /** + * Create empty instance of specification. + * <p> + * URIs and ref updates should be configured + * {@link #addURIRefUpdates(URIish, Collection)} method. + */ + public PushOperationSpecification() { + this.urisRefUpdates = new LinkedHashMap<URIish, Collection<RemoteRefUpdate>>(); + } + + /** + * Add remote repository URI with ref updates specification. + * <p> + * Ref updates are not in constructor - pay attention to not share them + * between different URIs ref updates or push operations. + * <p> + * Note that refUpdates can differ between URIs <b>only</b> by expected old + * object id field: {@link RemoteRefUpdate#getExpectedOldObjectId()}. + * + * @param uri + * remote repository URI. + * @param refUpdates + * collection of remote ref updates specifications. + */ + public void addURIRefUpdates(final URIish uri, + Collection<RemoteRefUpdate> refUpdates) { + urisRefUpdates.put(uri, refUpdates); + } + + /** + * @return set of remote repositories URIis. Set is ordered in addition + * sequence. + */ + public Set<URIish> getURIs() { + return Collections.unmodifiableSet(urisRefUpdates.keySet()); + } + + /** + * @return number of remote repositories URI for this push operation. + */ + public int getURIsNumber() { + return urisRefUpdates.keySet().size(); + } + + /** + * @param uri + * remote repository URI. + * @return remote ref updates as specified by user for this URI. + */ + public Collection<RemoteRefUpdate> getRefUpdates(final URIish uri) { + return Collections.unmodifiableCollection(urisRefUpdates.get(uri)); + } +} -- 1.5.6.3 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html