| /* | |
| * 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 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 static java.util.stream.Collectors.toList; | |
| import java.io.IOException; | |
| import java.net.URISyntaxException; | |
| import java.text.MessageFormat; | |
| import java.time.Instant; | |
| import java.time.OffsetDateTime; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.List; | |
| import org.eclipse.jgit.annotations.NonNull; | |
| import org.eclipse.jgit.annotations.Nullable; | |
| import org.eclipse.jgit.api.errors.GitAPIException; | |
| import org.eclipse.jgit.api.errors.InvalidConfigurationException; | |
| import org.eclipse.jgit.api.errors.InvalidRemoteException; | |
| import org.eclipse.jgit.api.errors.JGitInternalException; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.errors.NoRemoteRepositoryException; | |
| import org.eclipse.jgit.errors.NotSupportedException; | |
| import org.eclipse.jgit.errors.TransportException; | |
| import org.eclipse.jgit.internal.JGitText; | |
| import org.eclipse.jgit.lib.ConfigConstants; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.NullProgressMonitor; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ProgressMonitor; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.lib.StoredConfig; | |
| import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; | |
| import org.eclipse.jgit.revwalk.RevWalk; | |
| import org.eclipse.jgit.submodule.SubmoduleWalk; | |
| import org.eclipse.jgit.transport.FetchResult; | |
| import org.eclipse.jgit.transport.RefSpec; | |
| import org.eclipse.jgit.transport.TagOpt; | |
| import org.eclipse.jgit.transport.Transport; | |
| /** | |
| * A class used to execute a {@code Fetch} command. It has setters for all | |
| * supported options and arguments of this command and a {@link #call()} method | |
| * to finally execute the command. | |
| * | |
| * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-fetch.html" | |
| * >Git documentation about Fetch</a> | |
| */ | |
| public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> { | |
| private String remote = Constants.DEFAULT_REMOTE_NAME; | |
| private List<RefSpec> refSpecs; | |
| private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; | |
| private boolean checkFetchedObjects; | |
| private Boolean removeDeletedRefs; | |
| private boolean dryRun; | |
| private boolean thin = Transport.DEFAULT_FETCH_THIN; | |
| private TagOpt tagOption; | |
| private FetchRecurseSubmodulesMode submoduleRecurseMode = null; | |
| private Callback callback; | |
| private boolean isForceUpdate; | |
| private String initialBranch; | |
| private Integer depth; | |
| private Instant deepenSince; | |
| private List<String> shallowExcludes = new ArrayList<>(); | |
| private boolean unshallow; | |
| /** | |
| * Callback for status of fetch operation. | |
| * | |
| * @since 4.8 | |
| * | |
| */ | |
| public interface Callback { | |
| /** | |
| * Notify fetching a submodule. | |
| * | |
| * @param name | |
| * the submodule name. | |
| */ | |
| void fetchingSubmodule(String name); | |
| } | |
| /** | |
| * Constructor for FetchCommand. | |
| * | |
| * @param repo | |
| * a {@link org.eclipse.jgit.lib.Repository} object. | |
| */ | |
| protected FetchCommand(Repository repo) { | |
| super(repo); | |
| refSpecs = new ArrayList<>(3); | |
| } | |
| private FetchRecurseSubmodulesMode getRecurseMode(String path) { | |
| // Use the caller-specified mode, if set | |
| if (submoduleRecurseMode != null) { | |
| return submoduleRecurseMode; | |
| } | |
| // Fall back to submodule.name.fetchRecurseSubmodules, if set | |
| FetchRecurseSubmodulesMode mode = repo.getConfig().getEnum( | |
| FetchRecurseSubmodulesMode.values(), | |
| ConfigConstants.CONFIG_SUBMODULE_SECTION, path, | |
| ConfigConstants.CONFIG_KEY_FETCH_RECURSE_SUBMODULES); | |
| if (mode != null) { | |
| return mode; | |
| } | |
| // Fall back to fetch.recurseSubmodules, if set | |
| mode = repo.getConfig().getEnum(FetchRecurseSubmodulesMode.values(), | |
| ConfigConstants.CONFIG_FETCH_SECTION, null, | |
| ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES); | |
| if (mode != null) { | |
| return mode; | |
| } | |
| // Default to on-demand mode | |
| return FetchRecurseSubmodulesMode.ON_DEMAND; | |
| } | |
| private void fetchSubmodules(FetchResult results) | |
| throws org.eclipse.jgit.api.errors.TransportException, | |
| GitAPIException, InvalidConfigurationException { | |
| try (SubmoduleWalk walk = new SubmoduleWalk(repo); | |
| RevWalk revWalk = new RevWalk(repo)) { | |
| // Walk over submodules in the parent repository's FETCH_HEAD. | |
| ObjectId fetchHead = repo.resolve(Constants.FETCH_HEAD); | |
| if (fetchHead == null) { | |
| return; | |
| } | |
| if (revWalk.parseAny(fetchHead).getType() == Constants.OBJ_BLOB) { | |
| return; | |
| } | |
| walk.setTree(revWalk.parseTree(fetchHead)); | |
| while (walk.next()) { | |
| try (Repository submoduleRepo = walk.getRepository()) { | |
| // Skip submodules that don't exist locally (have not been | |
| // cloned), are not registered in the .gitmodules file, or | |
| // not registered in the parent repository's config. | |
| if (submoduleRepo == null || walk.getModulesPath() == null | |
| || walk.getConfigUrl() == null) { | |
| continue; | |
| } | |
| FetchRecurseSubmodulesMode recurseMode = getRecurseMode( | |
| walk.getPath()); | |
| // When the fetch mode is "yes" we always fetch. When the | |
| // mode is "on demand", we only fetch if the submodule's | |
| // revision was updated to an object that is not currently | |
| // present in the submodule. | |
| if ((recurseMode == FetchRecurseSubmodulesMode.ON_DEMAND | |
| && !submoduleRepo.getObjectDatabase() | |
| .has(walk.getObjectId())) | |
| || recurseMode == FetchRecurseSubmodulesMode.YES) { | |
| FetchCommand f = new FetchCommand(submoduleRepo) | |
| .setProgressMonitor(monitor) | |
| .setTagOpt(tagOption) | |
| .setCheckFetchedObjects(checkFetchedObjects) | |
| .setRemoveDeletedRefs(isRemoveDeletedRefs()) | |
| .setThin(thin) | |
| .setRefSpecs(applyOptions(refSpecs)) | |
| .setDryRun(dryRun) | |
| .setRecurseSubmodules(recurseMode); | |
| configure(f); | |
| if (callback != null) { | |
| callback.fetchingSubmodule(walk.getPath()); | |
| } | |
| results.addSubmodule(walk.getPath(), f.call()); | |
| } | |
| } | |
| } | |
| } catch (IOException e) { | |
| throw new JGitInternalException(e.getMessage(), e); | |
| } catch (ConfigInvalidException e) { | |
| throw new InvalidConfigurationException(e.getMessage(), e); | |
| } | |
| } | |
| /** | |
| * {@inheritDoc} | |
| * <p> | |
| * Execute the {@code fetch} 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 FetchResult call() throws GitAPIException, InvalidRemoteException, | |
| org.eclipse.jgit.api.errors.TransportException { | |
| checkCallable(); | |
| try (Transport transport = Transport.open(repo, remote)) { | |
| transport.setCheckFetchedObjects(checkFetchedObjects); | |
| transport.setRemoveDeletedRefs(isRemoveDeletedRefs()); | |
| transport.setDryRun(dryRun); | |
| if (tagOption != null) | |
| transport.setTagOpt(tagOption); | |
| transport.setFetchThin(thin); | |
| if (depth != null) { | |
| transport.setDepth(depth); | |
| } | |
| if (unshallow) { | |
| if (depth != null) { | |
| throw new IllegalStateException(JGitText.get().depthWithUnshallow); | |
| } | |
| transport.setDepth(Constants.INFINITE_DEPTH); | |
| } | |
| if (deepenSince != null) { | |
| transport.setDeepenSince(deepenSince); | |
| } | |
| transport.setDeepenNots(shallowExcludes); | |
| configure(transport); | |
| FetchResult result = transport.fetch(monitor, | |
| applyOptions(refSpecs), initialBranch); | |
| if (!repo.isBare()) { | |
| fetchSubmodules(result); | |
| } | |
| return result; | |
| } catch (NoRemoteRepositoryException 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 (URISyntaxException e) { | |
| throw new InvalidRemoteException(MessageFormat.format( | |
| JGitText.get().invalidRemote, remote), e); | |
| } catch (NotSupportedException e) { | |
| throw new JGitInternalException( | |
| JGitText.get().exceptionCaughtDuringExecutionOfFetchCommand, | |
| e); | |
| } | |
| } | |
| private List<RefSpec> applyOptions(List<RefSpec> refSpecs2) { | |
| if (!isForceUpdate()) { | |
| return refSpecs2; | |
| } | |
| List<RefSpec> updated = new ArrayList<>(3); | |
| for (RefSpec refSpec : refSpecs2) { | |
| updated.add(refSpec.setForceUpdate(true)); | |
| } | |
| return updated; | |
| } | |
| /** | |
| * Set the mode to be used for recursing into submodules. | |
| * | |
| * @param recurse | |
| * corresponds to the | |
| * --recurse-submodules/--no-recurse-submodules options. If | |
| * {@code null} use the value of the | |
| * {@code submodule.name.fetchRecurseSubmodules} option | |
| * configured per submodule. If not specified there, use the | |
| * value of the {@code fetch.recurseSubmodules} option configured | |
| * in git config. If not configured in either, "on-demand" is the | |
| * built-in default. | |
| * @return {@code this} | |
| * @since 4.7 | |
| */ | |
| public FetchCommand setRecurseSubmodules( | |
| @Nullable FetchRecurseSubmodulesMode recurse) { | |
| checkCallable(); | |
| submoduleRecurseMode = recurse; | |
| return this; | |
| } | |
| /** | |
| * The remote (uri or name) used for the fetch operation. If no remote is | |
| * set, the default value of <code>Constants.DEFAULT_REMOTE_NAME</code> will | |
| * be used. | |
| * | |
| * @see Constants#DEFAULT_REMOTE_NAME | |
| * @param remote | |
| * name of a remote | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setRemote(String remote) { | |
| checkCallable(); | |
| this.remote = remote; | |
| return this; | |
| } | |
| /** | |
| * Get the remote | |
| * | |
| * @return the remote used for the remote operation | |
| */ | |
| public String getRemote() { | |
| return remote; | |
| } | |
| /** | |
| * Get timeout | |
| * | |
| * @return the timeout used for the fetch operation | |
| */ | |
| public int getTimeout() { | |
| return timeout; | |
| } | |
| /** | |
| * Whether to check received objects for validity | |
| * | |
| * @return whether to check received objects for validity | |
| */ | |
| public boolean isCheckFetchedObjects() { | |
| return checkFetchedObjects; | |
| } | |
| /** | |
| * If set to {@code true}, objects received will be checked for validity | |
| * | |
| * @param checkFetchedObjects | |
| * whether to check objects for validity | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setCheckFetchedObjects(boolean checkFetchedObjects) { | |
| checkCallable(); | |
| this.checkFetchedObjects = checkFetchedObjects; | |
| return this; | |
| } | |
| /** | |
| * Whether to remove refs which no longer exist in the source | |
| * | |
| * @return whether to remove refs which no longer exist in the source | |
| */ | |
| public boolean isRemoveDeletedRefs() { | |
| if (removeDeletedRefs != null) { | |
| return removeDeletedRefs.booleanValue(); | |
| } | |
| // fall back to configuration | |
| boolean result = false; | |
| StoredConfig config = repo.getConfig(); | |
| result = config.getBoolean(ConfigConstants.CONFIG_FETCH_SECTION, null, | |
| ConfigConstants.CONFIG_KEY_PRUNE, result); | |
| result = config.getBoolean(ConfigConstants.CONFIG_REMOTE_SECTION, | |
| remote, ConfigConstants.CONFIG_KEY_PRUNE, result); | |
| return result; | |
| } | |
| /** | |
| * If set to {@code true}, refs are removed which no longer exist in the | |
| * source | |
| * | |
| * @param removeDeletedRefs | |
| * whether to remove deleted {@code Ref}s | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setRemoveDeletedRefs(boolean removeDeletedRefs) { | |
| checkCallable(); | |
| this.removeDeletedRefs = Boolean.valueOf(removeDeletedRefs); | |
| return this; | |
| } | |
| /** | |
| * Get progress monitor | |
| * | |
| * @return the progress monitor for the fetch operation | |
| */ | |
| public ProgressMonitor getProgressMonitor() { | |
| return monitor; | |
| } | |
| /** | |
| * The progress monitor associated with the fetch operation. By default, | |
| * this is set to <code>NullProgressMonitor</code> | |
| * | |
| * @see NullProgressMonitor | |
| * @param monitor | |
| * a {@link org.eclipse.jgit.lib.ProgressMonitor} | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setProgressMonitor(ProgressMonitor monitor) { | |
| checkCallable(); | |
| if (monitor == null) { | |
| monitor = NullProgressMonitor.INSTANCE; | |
| } | |
| this.monitor = monitor; | |
| return this; | |
| } | |
| /** | |
| * Get list of {@code RefSpec}s | |
| * | |
| * @return the ref specs | |
| */ | |
| public List<RefSpec> getRefSpecs() { | |
| return refSpecs; | |
| } | |
| /** | |
| * The ref specs to be used in the fetch operation | |
| * | |
| * @param specs | |
| * String representation of {@code RefSpec}s | |
| * @return {@code this} | |
| * @since 4.9 | |
| */ | |
| public FetchCommand setRefSpecs(String... specs) { | |
| return setRefSpecs( | |
| Arrays.stream(specs).map(RefSpec::new).collect(toList())); | |
| } | |
| /** | |
| * The ref specs to be used in the fetch operation | |
| * | |
| * @param specs | |
| * one or multiple {@link org.eclipse.jgit.transport.RefSpec}s | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setRefSpecs(RefSpec... specs) { | |
| return setRefSpecs(Arrays.asList(specs)); | |
| } | |
| /** | |
| * The ref specs to be used in the fetch operation | |
| * | |
| * @param specs | |
| * list of {@link org.eclipse.jgit.transport.RefSpec}s | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setRefSpecs(List<RefSpec> specs) { | |
| checkCallable(); | |
| this.refSpecs.clear(); | |
| this.refSpecs.addAll(specs); | |
| return this; | |
| } | |
| /** | |
| * Whether to do a dry run | |
| * | |
| * @return the dry run preference for the fetch operation | |
| */ | |
| public boolean isDryRun() { | |
| return dryRun; | |
| } | |
| /** | |
| * Sets whether the fetch operation should be a dry run | |
| * | |
| * @param dryRun | |
| * whether to do a dry run | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setDryRun(boolean dryRun) { | |
| checkCallable(); | |
| this.dryRun = dryRun; | |
| return this; | |
| } | |
| /** | |
| * Get thin-pack preference | |
| * | |
| * @return the thin-pack preference for fetch operation | |
| */ | |
| public boolean isThin() { | |
| return thin; | |
| } | |
| /** | |
| * Sets the thin-pack preference for fetch operation. | |
| * | |
| * Default setting is Transport.DEFAULT_FETCH_THIN | |
| * | |
| * @param thinPack | |
| * the thin-pack preference | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setThin(boolean thinPack) { | |
| checkCallable(); | |
| this.thin = thinPack; | |
| return this; | |
| } | |
| /** | |
| * Sets the specification of annotated tag behavior during fetch | |
| * | |
| * @param tagOpt | |
| * the {@link org.eclipse.jgit.transport.TagOpt} | |
| * @return {@code this} | |
| */ | |
| public FetchCommand setTagOpt(TagOpt tagOpt) { | |
| checkCallable(); | |
| this.tagOption = tagOpt; | |
| return this; | |
| } | |
| /** | |
| * Set the initial branch | |
| * | |
| * @param branch | |
| * the initial branch to check out when cloning the repository. | |
| * Can be specified as ref name (<code>refs/heads/master</code>), | |
| * branch name (<code>master</code>) or tag name | |
| * (<code>v1.2.3</code>). The default is to use the branch | |
| * pointed to by the cloned repository's HEAD and can be | |
| * requested by passing {@code null} or <code>HEAD</code>. | |
| * @return {@code this} | |
| * @since 5.11 | |
| */ | |
| public FetchCommand setInitialBranch(String branch) { | |
| this.initialBranch = branch; | |
| return this; | |
| } | |
| /** | |
| * Register a progress callback. | |
| * | |
| * @param callback | |
| * the callback | |
| * @return {@code this} | |
| * @since 4.8 | |
| */ | |
| public FetchCommand setCallback(Callback callback) { | |
| this.callback = callback; | |
| return this; | |
| } | |
| /** | |
| * Whether fetch --force option is enabled | |
| * | |
| * @return whether refs affected by the fetch are updated forcefully | |
| * @since 5.0 | |
| */ | |
| public boolean isForceUpdate() { | |
| return this.isForceUpdate; | |
| } | |
| /** | |
| * Set fetch --force option | |
| * | |
| * @param force | |
| * whether to update refs affected by the fetch forcefully | |
| * @return this command | |
| * @since 5.0 | |
| */ | |
| public FetchCommand setForceUpdate(boolean force) { | |
| this.isForceUpdate = force; | |
| return this; | |
| } | |
| /** | |
| * Limits fetching to the specified number of commits from the tip of each | |
| * remote branch history. | |
| * | |
| * @param depth | |
| * the depth | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand setDepth(int depth) { | |
| if (depth < 1) { | |
| throw new IllegalArgumentException(JGitText.get().depthMustBeAt1); | |
| } | |
| this.depth = Integer.valueOf(depth); | |
| return this; | |
| } | |
| /** | |
| * Deepens or shortens the history of a shallow repository to include all | |
| * reachable commits after a specified time. | |
| * | |
| * @param shallowSince | |
| * the timestammp; must not be {@code null} | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand setShallowSince(@NonNull OffsetDateTime shallowSince) { | |
| this.deepenSince = shallowSince.toInstant(); | |
| return this; | |
| } | |
| /** | |
| * Deepens or shortens the history of a shallow repository to include all | |
| * reachable commits after a specified time. | |
| * | |
| * @param shallowSince | |
| * the timestammp; must not be {@code null} | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand setShallowSince(@NonNull Instant shallowSince) { | |
| this.deepenSince = shallowSince; | |
| return this; | |
| } | |
| /** | |
| * Deepens or shortens the history of a shallow repository to exclude | |
| * commits reachable from a specified remote branch or tag. | |
| * | |
| * @param shallowExclude | |
| * the ref or commit; must not be {@code null} | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand addShallowExclude(@NonNull String shallowExclude) { | |
| shallowExcludes.add(shallowExclude); | |
| return this; | |
| } | |
| /** | |
| * Creates a shallow clone with a history, excluding commits reachable from | |
| * a specified remote branch or tag. | |
| * | |
| * @param shallowExclude | |
| * the commit; must not be {@code null} | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand addShallowExclude(@NonNull ObjectId shallowExclude) { | |
| shallowExcludes.add(shallowExclude.name()); | |
| return this; | |
| } | |
| /** | |
| * If the source repository is complete, converts a shallow repository to a | |
| * complete one, removing all the limitations imposed by shallow | |
| * repositories. | |
| * | |
| * If the source repository is shallow, fetches as much as possible so that | |
| * the current repository has the same history as the source repository. | |
| * | |
| * @param unshallow | |
| * whether to unshallow or not | |
| * @return {@code this} | |
| * | |
| * @since 6.3 | |
| */ | |
| public FetchCommand setUnshallow(boolean unshallow) { | |
| this.unshallow = unshallow; | |
| return this; | |
| } | |
| void setShallowExcludes(List<String> shallowExcludes) { | |
| this.shallowExcludes = shallowExcludes; | |
| } | |
| } |