/* * Copyright (C) 2010, 2022 Chris Aniszczyk and others * * 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. * * 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; import org.eclipse.jgit.transport.Transport; /** * A class used to execute a {@code Push} command. It has setters for all * supported options and arguments of this command and a {@link #call()} method * to finally execute the command. * * @see Git documentation about Push */ public class PushCommand extends TransportCommand> { private String remote; private final List refSpecs; private final Map 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 pushOptions; // Legacy behavior as default. Use setPushDefault(null) to determine the // value from the git config. private PushDefault pushDefault = PushDefault.CURRENT; /** *

* Constructor for PushCommand. *

* * @param repo * the {@link org.eclipse.jgit.lib.Repository} */ protected PushCommand(Repository repo) { super(repo); refSpecs = new ArrayList<>(3); refLeaseSpecs = new HashMap<>(); } /** * {@inheritDoc} *

* 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()}) */ @Override public Iterable call() throws GitAPIException, InvalidRemoteException, org.eclipse.jgit.api.errors.TransportException { checkCallable(); setCallable(false); ArrayList pushResults = new ArrayList<>(3); try { Config config = repo.getConfig(); remote = determineRemote(config, remote); if (refSpecs.isEmpty()) { RemoteConfig rc = new RemoteConfig(config, getRemote()); refSpecs.addAll(rc.getPushRefSpecs()); if (refSpecs.isEmpty()) { determineDefaultRefSpecs(config); } } if (force) { for (int i = 0; i < refSpecs.size(); i++) refSpecs.set(i, refSpecs.get(i).setForceUpdate(true)); } List 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 toPush = transport .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); } finally { transport.close(); } } } catch (URISyntaxException e) { throw new InvalidRemoteException( MessageFormat.format(JGitText.get().invalidRemote, remote), e); } catch (TransportException e) { throw new org.eclipse.jgit.api.errors.TransportException( e.getMessage(), e); } catch (NotSupportedException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfPushCommand, e); } catch (IOException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfPushCommand, e); } 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)); } } /** * The remote (uri or name) used for the push operation. If no remote is * set, the default value of Constants.DEFAULT_REMOTE_NAME will * be used. * * @see Constants#DEFAULT_REMOTE_NAME * @param remote * the remote name * @return {@code this} */ public PushCommand setRemote(String remote) { checkCallable(); this.remote = remote; return this; } /** * Get remote name * * @return the remote used for the remote operation */ public String getRemote() { return remote; } /** * Sets a {@link PrintStream} a "pre-push" hook may write its stdout to. If * not set, {@link System#out} will be used. *

* When pushing to several remote repositories the stream is shared for all * pushes. *

* * @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. *

* When pushing to several remote repositories the stream is shared for all * pushes. *

* * @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 * RemoteConfig.DEFAULT_RECEIVE_PACK 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) { checkCallable(); this.receivePack = receivePack; return this; } /** * Get the name of the remote executable providing the receive-pack service * * @return the receive-pack used for the remote operation */ public String getReceivePack() { return receivePack; } /** * Get timeout used for push operation * * @return the timeout used for the push operation */ public int getTimeout() { return timeout; } /** * Get the progress monitor * * @return the progress monitor for the push operation */ public ProgressMonitor getProgressMonitor() { return monitor; } /** * The progress monitor associated with the push operation. By default, this * is set to NullProgressMonitor * * @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 RefLeaseSpecs. * * @return the RefLeaseSpecs * @since 4.7 */ public List 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 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 getRefSpecs() { return refSpecs; } /** * The ref specs to be used in the push operation * * @param specs a {@link org.eclipse.jgit.transport.RefSpec} object. * @return {@code this} */ public PushCommand setRefSpecs(RefSpec... specs) { checkCallable(); this.refSpecs.clear(); Collections.addAll(refSpecs, specs); return this; } /** * 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 specs) { checkCallable(); this.refSpecs.clear(); this.refSpecs.addAll(specs); return this; } /** * 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} */ public PushCommand setPushAll() { refSpecs.add(Transport.REFSPEC_PUSH_ALL); return this; } /** * Push all tags under refs/tags/*. * * @return {@code this} */ public PushCommand setPushTags() { refSpecs.add(Transport.REFSPEC_TAGS); return this; } /** * Add a reference to push. * * @param ref * the source reference. The remote name will match. * @return {@code this}. */ public PushCommand add(Ref ref) { refSpecs.add(new RefSpec(ref.getLeaf().getName())); return this; } /** * Add a reference to push. * * @param nameOrSpec * any reference name, or a reference specification. * @return {@code this}. * @throws JGitInternalException * the reference name cannot be resolved. */ public PushCommand add(String nameOrSpec) { if (0 <= nameOrSpec.indexOf(':')) { refSpecs.add(new RefSpec(nameOrSpec)); } else { Ref src; try { src = repo.findRef(nameOrSpec); } catch (IOException e) { throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfPushCommand, e); } if (src != null) add(src); } return this; } /** * Whether to run the push operation as a dry run * * @return the dry run preference for the push operation */ public boolean isDryRun() { return dryRun; } /** * Sets whether the push operation should be a dry run * * @param dryRun a boolean. * @return {@code this} */ public PushCommand setDryRun(boolean dryRun) { checkCallable(); this.dryRun = dryRun; return this; } /** * Get the thin-pack preference * * @return the thin-pack preference for push operation */ public boolean isThin() { return thin; } /** * Set the thin-pack preference for push operation. * * Default setting is Transport.DEFAULT_PUSH_THIN * * @param thinPack * the thin-pack preference value * @return {@code this} */ public PushCommand setThin(boolean thinPack) { checkCallable(); 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() { return force; } /** * Sets the force preference for push operation. * * @param force * whether to push forcefully * @return {@code this} */ public PushCommand setForce(boolean force) { checkCallable(); this.force = force; return this; } /** * Sets the output stream to write sideband messages to * * @param out * an {@link java.io.OutputStream} * @return {@code this} * @since 3.0 */ public PushCommand setOutputStream(OutputStream out) { this.out = out; return this; } /** * Get push options * * @return the option strings associated with the push operation * @since 4.5 */ public List 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 pushOptions) { this.pushOptions = pushOptions; return this; } }