diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java | 463 |
1 files changed, 389 insertions, 74 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java index c719f0a546..e9d1a3213b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -1,68 +1,49 @@ /* - * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2010, 2022 Chris Aniszczyk <caniszczyk@gmail.com> and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.api; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintStream; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TooLargeObjectInPackException; +import org.eclipse.jgit.errors.TooLargePackException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BranchConfig; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushConfig; +import org.eclipse.jgit.transport.PushConfig.PushDefault; import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RefLeaseSpec; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.RemoteRefUpdate; @@ -79,60 +60,75 @@ import org.eclipse.jgit.transport.Transport; public class PushCommand extends TransportCommand<PushCommand, Iterable<PushResult>> { - private String remote = Constants.DEFAULT_REMOTE_NAME; + private String remote; private final List<RefSpec> refSpecs; + private final Map<String, RefLeaseSpec> refLeaseSpecs; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; private String receivePack = RemoteConfig.DEFAULT_RECEIVE_PACK; private boolean dryRun; - + private boolean atomic; private boolean force; - private boolean thin = Transport.DEFAULT_PUSH_THIN; + private boolean useBitmaps = Transport.DEFAULT_PUSH_USE_BITMAPS; + + private PrintStream hookOutRedirect; + + private PrintStream hookErrRedirect; private OutputStream out; + private List<String> pushOptions; + + // Legacy behavior as default. Use setPushDefault(null) to determine the + // value from the git config. + private PushDefault pushDefault = PushDefault.CURRENT; + /** + * <p> + * Constructor for PushCommand. + * </p> + * * @param repo + * the {@link org.eclipse.jgit.lib.Repository} */ protected PushCommand(Repository repo) { super(repo); - refSpecs = new ArrayList<RefSpec>(3); + refSpecs = new ArrayList<>(3); + refLeaseSpecs = new HashMap<>(); } /** - * Executes the {@code push} command with all the options and parameters + * {@inheritDoc} + * <p> + * Execute the {@code push} command with all the options and parameters * collected by the setter methods of this class. Each instance of this * class should only be used for one invocation of the command (means: one * call to {@link #call()}) - * - * @return an iteration over {@link PushResult} objects - * @throws InvalidRemoteException - * when called with an invalid remote uri - * @throws org.eclipse.jgit.api.errors.TransportException - * when an error occurs with the transport - * @throws GitAPIException */ + @Override public Iterable<PushResult> call() throws GitAPIException, InvalidRemoteException, org.eclipse.jgit.api.errors.TransportException { checkCallable(); + setCallable(false); - ArrayList<PushResult> pushResults = new ArrayList<PushResult>(3); + ArrayList<PushResult> pushResults = new ArrayList<>(3); try { + Config config = repo.getConfig(); + remote = determineRemote(config, remote); if (refSpecs.isEmpty()) { - RemoteConfig config = new RemoteConfig(repo.getConfig(), + RemoteConfig rc = new RemoteConfig(config, getRemote()); - refSpecs.addAll(config.getPushRefSpecs()); - } - if (refSpecs.isEmpty()) { - Ref head = repo.getRef(Constants.HEAD); - if (head != null && head.isSymbolic()) - refSpecs.add(new RefSpec(head.getLeaf().getName())); + refSpecs.addAll(rc.getPushRefSpecs()); + if (refSpecs.isEmpty()) { + determineDefaultRefSpecs(config); + } } if (force) { @@ -140,22 +136,34 @@ public class PushCommand extends refSpecs.set(i, refSpecs.get(i).setForceUpdate(true)); } - final List<Transport> transports; - transports = Transport.openAll(repo, remote, Transport.Operation.PUSH); - for (final Transport transport : transports) { + List<Transport> transports = Transport.openAll(repo, remote, + Transport.Operation.PUSH); + for (@SuppressWarnings("resource") // Explicitly closed in finally + final Transport transport : transports) { transport.setPushThin(thin); + transport.setPushAtomic(atomic); if (receivePack != null) transport.setOptionReceivePack(receivePack); transport.setDryRun(dryRun); + transport.setPushOptions(pushOptions); + transport.setPushUseBitmaps(useBitmaps); + transport.setHookOutputStream(hookOutRedirect); + transport.setHookErrorStream(hookErrRedirect); configure(transport); final Collection<RemoteRefUpdate> toPush = transport - .findRemoteRefUpdatesFor(refSpecs); + .findRemoteRefUpdatesFor(refSpecs, refLeaseSpecs); try { PushResult result = transport.push(monitor, toPush, out); pushResults.add(result); + } catch (TooLargePackException e) { + throw new org.eclipse.jgit.api.errors.TooLargePackException( + e.getMessage(), e); + } catch (TooLargeObjectInPackException e) { + throw new org.eclipse.jgit.api.errors.TooLargeObjectInPackException( + e.getMessage(), e); } catch (TransportException e) { throw new org.eclipse.jgit.api.errors.TransportException( e.getMessage(), e); @@ -165,8 +173,9 @@ public class PushCommand extends } } catch (URISyntaxException e) { - throw new InvalidRemoteException(MessageFormat.format( - JGitText.get().invalidRemote, remote)); + throw new InvalidRemoteException( + MessageFormat.format(JGitText.get().invalidRemote, remote), + e); } catch (TransportException e) { throw new org.eclipse.jgit.api.errors.TransportException( e.getMessage(), e); @@ -181,7 +190,102 @@ public class PushCommand extends } return pushResults; + } + + private String determineRemote(Config config, String remoteName) + throws IOException { + if (remoteName != null) { + return remoteName; + } + Ref head = repo.exactRef(Constants.HEAD); + String effectiveRemote = null; + BranchConfig branchCfg = null; + if (head != null && head.isSymbolic()) { + String currentBranch = head.getLeaf().getName(); + branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + effectiveRemote = branchCfg.getPushRemote(); + } + if (effectiveRemote == null) { + effectiveRemote = config.getString( + ConfigConstants.CONFIG_REMOTE_SECTION, null, + ConfigConstants.CONFIG_KEY_PUSH_DEFAULT); + if (effectiveRemote == null && branchCfg != null) { + effectiveRemote = branchCfg.getRemote(); + } + } + if (effectiveRemote == null) { + effectiveRemote = Constants.DEFAULT_REMOTE_NAME; + } + return effectiveRemote; + } + private String getCurrentBranch() + throws IOException, DetachedHeadException { + Ref head = repo.exactRef(Constants.HEAD); + if (head != null && head.isSymbolic()) { + return head.getLeaf().getName(); + } + throw new DetachedHeadException(); + } + + private void determineDefaultRefSpecs(Config config) + throws IOException, GitAPIException { + if (pushDefault == null) { + pushDefault = config.get(PushConfig::new).getPushDefault(); + } + switch (pushDefault) { + case CURRENT: + refSpecs.add(new RefSpec(getCurrentBranch())); + break; + case MATCHING: + refSpecs.add(new RefSpec(":")); //$NON-NLS-1$ + break; + case NOTHING: + throw new InvalidRefNameException( + JGitText.get().pushDefaultNothing); + case SIMPLE: + case UPSTREAM: + String currentBranch = getCurrentBranch(); + BranchConfig branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + String fetchRemote = branchCfg.getRemote(); + if (fetchRemote == null) { + fetchRemote = Constants.DEFAULT_REMOTE_NAME; + } + boolean isTriangular = !fetchRemote.equals(remote); + if (isTriangular) { + if (PushDefault.UPSTREAM.equals(pushDefault)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultTriangularUpstream, + remote, fetchRemote)); + } + // Strange, but consistent with C git: "simple" doesn't even + // check whether there is a configured upstream, and if so, that + // it is equal to the local branch name. It just becomes + // "current". + refSpecs.add(new RefSpec(currentBranch)); + } else { + String trackedBranch = branchCfg.getMerge(); + if (branchCfg.isRemoteLocal() || trackedBranch == null + || !trackedBranch.startsWith(Constants.R_HEADS)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultNoUpstream, + currentBranch)); + } + if (PushDefault.SIMPLE.equals(pushDefault) + && !trackedBranch.equals(currentBranch)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultSimple, currentBranch, + trackedBranch)); + } + refSpecs.add(new RefSpec(currentBranch + ':' + trackedBranch)); + } + break; + default: + throw new InvalidRefNameException(MessageFormat + .format(JGitText.get().pushDefaultUnknown, pushDefault)); + } } /** @@ -191,6 +295,7 @@ public class PushCommand extends * * @see Constants#DEFAULT_REMOTE_NAME * @param remote + * the remote name * @return {@code this} */ public PushCommand setRemote(String remote) { @@ -200,6 +305,8 @@ public class PushCommand extends } /** + * Get remote name + * * @return the remote used for the remote operation */ public String getRemote() { @@ -207,12 +314,54 @@ public class PushCommand extends } /** + * Sets a {@link PrintStream} a "pre-push" hook may write its stdout to. If + * not set, {@link System#out} will be used. + * <p> + * When pushing to several remote repositories the stream is shared for all + * pushes. + * </p> + * + * @param redirect + * {@link PrintStream} to use; if {@code null}, + * {@link System#out} will be used + * @return {@code this} + * @since 6.4 + */ + public PushCommand setHookOutputStream(PrintStream redirect) { + checkCallable(); + hookOutRedirect = redirect; + return this; + } + + /** + * Sets a {@link PrintStream} a "pre-push" hook may write its stderr to. If + * not set, {@link System#err} will be used. + * <p> + * When pushing to several remote repositories the stream is shared for all + * pushes. + * </p> + * + * @param redirect + * {@link PrintStream} to use; if {@code null}, + * {@link System#err} will be used + * @return {@code this} + * @since 6.4 + */ + public PushCommand setHookErrorStream(PrintStream redirect) { + checkCallable(); + hookErrRedirect = redirect; + return this; + } + + /** * The remote executable providing receive-pack service for pack transports. * If no receive-pack is set, the default value of * <code>RemoteConfig.DEFAULT_RECEIVE_PACK</code> will be used. * * @see RemoteConfig#DEFAULT_RECEIVE_PACK * @param receivePack + * name of the remote executable providing the receive-pack + * service * @return {@code this} */ public PushCommand setReceivePack(String receivePack) { @@ -222,6 +371,8 @@ public class PushCommand extends } /** + * Get the name of the remote executable providing the receive-pack service + * * @return the receive-pack used for the remote operation */ public String getReceivePack() { @@ -229,6 +380,8 @@ public class PushCommand extends } /** + * Get timeout used for push operation + * * @return the timeout used for the push operation */ public int getTimeout() { @@ -236,6 +389,8 @@ public class PushCommand extends } /** + * Get the progress monitor + * * @return the progress monitor for the push operation */ public ProgressMonitor getProgressMonitor() { @@ -247,17 +402,63 @@ public class PushCommand extends * is set to <code>NullProgressMonitor</code> * * @see NullProgressMonitor - * * @param monitor + * a {@link org.eclipse.jgit.lib.ProgressMonitor} * @return {@code this} */ public PushCommand setProgressMonitor(ProgressMonitor monitor) { checkCallable(); + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } this.monitor = monitor; return this; } /** + * Get the <code>RefLeaseSpec</code>s. + * + * @return the <code>RefLeaseSpec</code>s + * @since 4.7 + */ + public List<RefLeaseSpec> getRefLeaseSpecs() { + return new ArrayList<>(refLeaseSpecs.values()); + } + + /** + * The ref lease specs to be used in the push operation, for a + * force-with-lease push operation. + * + * @param specs + * a {@link org.eclipse.jgit.transport.RefLeaseSpec} object. + * @return {@code this} + * @since 4.7 + */ + public PushCommand setRefLeaseSpecs(RefLeaseSpec... specs) { + return setRefLeaseSpecs(Arrays.asList(specs)); + } + + /** + * The ref lease specs to be used in the push operation, for a + * force-with-lease push operation. + * + * @param specs + * list of {@code RefLeaseSpec}s + * @return {@code this} + * @since 4.7 + */ + public PushCommand setRefLeaseSpecs(List<RefLeaseSpec> specs) { + checkCallable(); + this.refLeaseSpecs.clear(); + for (RefLeaseSpec spec : specs) { + refLeaseSpecs.put(spec.getRef(), spec); + } + return this; + } + + /** + * Get {@code RefSpec}s. + * * @return the ref specs */ public List<RefSpec> getRefSpecs() { @@ -267,7 +468,7 @@ public class PushCommand extends /** * The ref specs to be used in the push operation * - * @param specs + * @param specs a {@link org.eclipse.jgit.transport.RefSpec} object. * @return {@code this} */ public PushCommand setRefSpecs(RefSpec... specs) { @@ -281,6 +482,7 @@ public class PushCommand extends * The ref specs to be used in the push operation * * @param specs + * list of {@link org.eclipse.jgit.transport.RefSpec}s * @return {@code this} */ public PushCommand setRefSpecs(List<RefSpec> specs) { @@ -291,9 +493,37 @@ public class PushCommand extends } /** + * Retrieves the {@link PushDefault} currently set. + * + * @return the {@link PushDefault}, or {@code null} if not set + * @since 6.1 + */ + public PushDefault getPushDefault() { + return pushDefault; + } + + /** + * Sets an explicit {@link PushDefault}. The default used if this is not + * called is {@link PushDefault#CURRENT} for compatibility reasons with + * earlier JGit versions. + * + * @param pushDefault + * {@link PushDefault} to set; if {@code null} the value defined + * in the git config will be used. + * + * @return {@code this} + * @since 6.1 + */ + public PushCommand setPushDefault(PushDefault pushDefault) { + checkCallable(); + this.pushDefault = pushDefault; + return this; + } + + /** * Push all branches under refs/heads/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushAll() { refSpecs.add(Transport.REFSPEC_PUSH_ALL); @@ -303,7 +533,7 @@ public class PushCommand extends /** * Push all tags under refs/tags/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushTags() { refSpecs.add(Transport.REFSPEC_TAGS); @@ -337,7 +567,7 @@ public class PushCommand extends } else { Ref src; try { - src = repo.getRef(nameOrSpec); + src = repo.findRef(nameOrSpec); } catch (IOException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfPushCommand, @@ -350,6 +580,8 @@ public class PushCommand extends } /** + * Whether to run the push operation as a dry run + * * @return the dry run preference for the push operation */ public boolean isDryRun() { @@ -359,7 +591,7 @@ public class PushCommand extends /** * Sets whether the push operation should be a dry run * - * @param dryRun + * @param dryRun a boolean. * @return {@code this} */ public PushCommand setDryRun(boolean dryRun) { @@ -369,6 +601,8 @@ public class PushCommand extends } /** + * Get the thin-pack preference + * * @return the thin-pack preference for push operation */ public boolean isThin() { @@ -376,20 +610,76 @@ public class PushCommand extends } /** - * Sets the thin-pack preference for push operation. + * Set the thin-pack preference for push operation. * * Default setting is Transport.DEFAULT_PUSH_THIN * - * @param thin + * @param thinPack + * the thin-pack preference value * @return {@code this} */ - public PushCommand setThin(boolean thin) { + public PushCommand setThin(boolean thinPack) { checkCallable(); - this.thin = thin; + this.thin = thinPack; return this; } /** + * Whether to use bitmaps for push. + * + * @return true if push use bitmaps. + * @since 6.4 + */ + public boolean isUseBitmaps() { + return useBitmaps; + } + + /** + * Set whether to use bitmaps for push. + * + * Default setting is {@value Transport#DEFAULT_PUSH_USE_BITMAPS} + * + * @param useBitmaps + * false to disable use of bitmaps for push, true otherwise. + * @return {@code this} + * @since 6.4 + */ + public PushCommand setUseBitmaps(boolean useBitmaps) { + checkCallable(); + this.useBitmaps = useBitmaps; + return this; + } + + /** + * Whether this push should be executed atomically (all references updated, + * or none) + * + * @return true if all-or-nothing behavior is requested. + * @since 4.2 + */ + public boolean isAtomic() { + return atomic; + } + + /** + * Requests atomic push (all references updated, or no updates). + * + * Default setting is false. + * + * @param atomic + * whether to run the push atomically + * @return {@code this} + * @since 4.2 + */ + public PushCommand setAtomic(boolean atomic) { + checkCallable(); + this.atomic = atomic; + return this; + } + + /** + * Whether to push forcefully + * * @return the force preference for push operation */ public boolean isForce() { @@ -400,6 +690,7 @@ public class PushCommand extends * Sets the force preference for push operation. * * @param force + * whether to push forcefully * @return {@code this} */ public PushCommand setForce(boolean force) { @@ -412,6 +703,7 @@ public class PushCommand extends * Sets the output stream to write sideband messages to * * @param out + * an {@link java.io.OutputStream} * @return {@code this} * @since 3.0 */ @@ -419,4 +711,27 @@ public class PushCommand extends this.out = out; return this; } + + /** + * Get push options + * + * @return the option strings associated with the push operation + * @since 4.5 + */ + public List<String> getPushOptions() { + return pushOptions; + } + + /** + * Set the option strings associated with the push operation. + * + * @param pushOptions + * a {@link java.util.List} of push option strings + * @return {@code this} + * @since 4.5 + */ + public PushCommand setPushOptions(List<String> pushOptions) { + this.pushOptions = pushOptions; + return this; + } } |