/* * Copyright (C) 2010, Mathias Kinzler * Copyright (C) 2010, 2023 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.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NotMergedException; 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.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; /** * Used to delete one or several branches. * * The result of {@link #call()} is a list with the (full) names of the deleted * branches. * * Note that we don't have a setter corresponding to the -r option; remote * tracking branches are simply deleted just like local branches. * * @see Git documentation about Branch */ public class DeleteBranchCommand extends GitCommand> { private final Set branchNames = new HashSet<>(); private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; private boolean force; /** * Constructor for DeleteBranchCommand * * @param repo * the {@link org.eclipse.jgit.lib.Repository} */ protected DeleteBranchCommand(Repository repo) { super(repo); } @Override public List call() throws GitAPIException, NotMergedException, CannotDeleteCurrentBranchException { checkCallable(); List result = new ArrayList<>(); Set shortNames = new HashSet<>(); if (branchNames.isEmpty()) { return result; } Exception error = null; try { deleteBranches(result, shortNames); } catch (Exception e) { error = e; } monitor.beginTask(JGitText.get().updatingConfig, 1); try { updateConfig(shortNames, error); } finally { monitor.update(1); monitor.endTask(); } return result; } private void deleteBranches(List result, Set shortNames) throws GitAPIException, NotMergedException, CannotDeleteCurrentBranchException { try { String currentBranch = repo.getFullBranch(); if (!force) { // check if the branches to be deleted // are all merged into the current branch try (RevWalk walk = new RevWalk(repo)) { RevCommit tip = walk .parseCommit(repo.resolve(Constants.HEAD)); for (String branchName : branchNames) { if (branchName == null) { continue; } Ref currentRef = repo.findRef(branchName); if (currentRef == null) { continue; } RevCommit base = walk .parseCommit(repo.resolve(branchName)); if (!walk.isMergedInto(base, tip)) { throw new NotMergedException(); } } } } setCallable(false); monitor.start(2); monitor.beginTask(JGitText.get().deletingBranches, branchNames.size()); try { for (String branchName : branchNames) { if (branchName == null) { monitor.update(1); continue; } Ref currentRef = repo.findRef(branchName); if (currentRef == null) { monitor.update(1); continue; } String fullName = currentRef.getName(); if (fullName.equals(currentBranch)) { throw new CannotDeleteCurrentBranchException( MessageFormat.format(JGitText .get().cannotDeleteCheckedOutBranch, branchName)); } RefUpdate update = repo.updateRef(fullName); update.setRefLogMessage("branch deleted", false); //$NON-NLS-1$ update.setForceUpdate(true); Result deleteResult = update.delete(); switch (deleteResult) { case IO_FAILURE: case LOCK_FAILURE: case REJECTED: throw new JGitInternalException(MessageFormat.format( JGitText.get().deleteBranchUnexpectedResult, deleteResult.name())); default: result.add(fullName); if (fullName.startsWith(Constants.R_HEADS)) { shortNames.add(fullName .substring(Constants.R_HEADS.length())); } break; } monitor.update(1); if (monitor.isCancelled()) { break; } } } finally { monitor.endTask(); } } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } } private void updateConfig(Set shortNames, Exception error) throws GitAPIException { IOException configError = null; if (!shortNames.isEmpty()) { try { // Remove upstream configurations if any StoredConfig cfg = repo.getConfig(); boolean changed = false; for (String branchName : shortNames) { changed |= cfg.removeSection( ConfigConstants.CONFIG_BRANCH_SECTION, branchName); } if (changed) { cfg.save(); } } catch (IOException e) { configError = e; } } if (error == null) { if (configError != null) { throw new JGitInternalException(configError.getMessage(), configError); } } else if (error instanceof GitAPIException) { if (configError != null) { error.addSuppressed(configError); } throw (GitAPIException) error; } else if (error instanceof RuntimeException) { if (configError != null) { error.addSuppressed(configError); } throw (RuntimeException) error; } else { JGitInternalException internal = new JGitInternalException( error.getMessage(), error); if (configError != null) { internal.addSuppressed(configError); } throw internal; } } /** * Set the names of the branches to delete * * @param branchnames * the names of the branches to delete; if not set, this will do * nothing; invalid branch names will simply be ignored * @return this instance */ public DeleteBranchCommand setBranchNames(String... branchnames) { checkCallable(); this.branchNames.clear(); this.branchNames.addAll(Arrays.asList(branchnames)); return this; } /** * Sets the names of the branches to delete * * @param branchNames * the names of the branches to delete; if not set, this will do * nothing; invalid branch names will simply be ignored * @return {@code this} * @since 6.8 */ public DeleteBranchCommand setBranchNames(Collection branchNames) { checkCallable(); this.branchNames.clear(); this.branchNames.addAll(branchNames); return this; } /** * Set whether to forcefully delete branches * * @param force * true corresponds to the -D option, * false to the -d option (default)
* if false a check will be performed whether the * branch to be deleted is already merged into the current branch * and deletion will be refused in this case * @return this instance */ public DeleteBranchCommand setForce(boolean force) { checkCallable(); this.force = force; return this; } /** * Retrieves the progress monitor. * * @return the {@link ProgressMonitor} for the delete operation * @since 6.8 */ public ProgressMonitor getProgressMonitor() { return monitor; } /** * Sets the progress monitor associated with the delete operation. By * default, this is set to NullProgressMonitor * * @see NullProgressMonitor * @param monitor * a {@link ProgressMonitor} * @return {@code this} * @since 6.8 */ public DeleteBranchCommand setProgressMonitor(ProgressMonitor monitor) { checkCallable(); if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } this.monitor = monitor; return this; } }