blob: ea79d4c356355069831483161451d2cbd218f39d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Ericsson AB.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Ericsson - initial API and implementation
*******************************************************************************/
package org.eclipse.egerrit.internal.ui.editors;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.egerrit.internal.core.EGerritCorePlugin;
import org.eclipse.egerrit.internal.core.GerritClient;
import org.eclipse.egerrit.internal.model.ChangeInfo;
import org.eclipse.egerrit.internal.model.RevisionInfo;
import org.eclipse.egerrit.internal.ui.table.model.BranchMatch;
import org.eclipse.egerrit.internal.ui.utils.ActiveWorkspaceRevision;
import org.eclipse.egerrit.internal.ui.utils.Messages;
import org.eclipse.egerrit.internal.ui.utils.UIUtils;
import org.eclipse.egit.ui.internal.dialogs.CheckoutConflictDialog;
import org.eclipse.egit.ui.internal.fetch.FetchGerritChangeWizard;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CheckoutResult;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RenameBranchCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
public class CheckoutRevision extends Action {
private RevisionInfo revisionCheckedOut;
private GerritClient gerritClient;
private ChangeInfo changeInfo;
private String selectedBranch;
private static final String RENAME_KEY = "branchRenameTip"; //$NON-NLS-1$
public CheckoutRevision(RevisionInfo revision, GerritClient gerritClient) {
this.revisionCheckedOut = revision;
this.gerritClient = gerritClient;
this.changeInfo = this.revisionCheckedOut.getChangeInfo();
setText(Messages.CheckoutRevision_0);
}
@Override
public void run() {
Repository localRepo = new FindLocalRepository(gerritClient, changeInfo.getProject()).getRepository();
boolean reActivateWorkspaceRevision = true;
if (localRepo == null) {
Status status = new Status(IStatus.ERROR, EGerritCorePlugin.PLUGIN_ID, Messages.CheckoutRevision_1);
ErrorDialog.openError(getShell(), Messages.CheckoutRevision_2, Messages.CheckoutRevision_3, status);
return;
}
String refSelected = revisionCheckedOut.getRef();
if ((refSelected == null) || refSelected.isEmpty()) {
Status status = new Status(IStatus.ERROR, EGerritCorePlugin.PLUGIN_ID, Messages.CheckoutRevision_4);
ErrorDialog.openError(getShell(), Messages.CheckoutRevision_2, Messages.CheckoutRevision_3, status);
}
Map<String, BranchMatch> potentialBranches = findAllPotentialBranches(localRepo);
reActivateWorkspaceRevision = selectAndCheckoutBranch(localRepo, refSelected, potentialBranches);
//Verify if the user wants to rename the selected branch
shouldRenameBranch(potentialBranches, refSelected, localRepo);
if (reActivateWorkspaceRevision) {
ActiveWorkspaceRevision.getInstance().activateCurrentRevision(gerritClient, revisionCheckedOut);
}
}
/**
* @param localRepo
* @param refSelected
* @param potentialBranches
* @return
*/
private boolean selectAndCheckoutBranch(Repository localRepo, String refSelected,
Map<String, BranchMatch> potentialBranches) {
boolean reActivateWorkspaceRevision = true;
if (potentialBranches.size() > 1) {
reActivateWorkspaceRevision = branchUiSelection(localRepo, potentialBranches);
} else {
if (potentialBranches.isEmpty()) {
//New selected
FetchGerritChangeWizard var = new FetchGerritChangeWizard(localRepo, refSelected);
WizardDialog w = new WizardDialog(getShell(), var);
w.create();
int ret = w.open();
if (ret == Window.CANCEL) {
reActivateWorkspaceRevision = false;
}
} else if (potentialBranches.entrySet().iterator().next().getValue().equals(BranchMatch.PERFECT_MATCH)) {
setSelectedBranch(potentialBranches.keySet().iterator().next()); //Get the only element PERFECT_MATCH
try {
checkoutBranch(getSelectedBranch(), localRepo);
} catch (Exception e) {
}
} else {
//Only one branch exist, but it is not the perfect match, so we need to allow the end-user to choose
reActivateWorkspaceRevision = branchUiSelection(localRepo, potentialBranches);
}
}
return reActivateWorkspaceRevision;
}
/**
* Test the selected branch match the branch to check-out and see if the user would like to rename it or not
*
* @param potentialBranches
* @param refSelected
* @param localRepo
*/
private void shouldRenameBranch(Map<String, BranchMatch> potentialBranches, String refSelected,
Repository localRepo) {
if (getSelectedBranch() == null) {
return;
}
//Test to make sure it was a perfect match
BranchMatch branchMatch = potentialBranches.get(getSelectedBranch());
boolean isPerfect = branchMatch != null ? branchMatch.equals(BranchMatch.PERFECT_MATCH) : false;
if (isPerfect && !getSelectedBranch().trim().isEmpty()) {
//What would the branch name be from refspec:
//Parse the ref from the revision to checkout
Change revisionRef = Change.fromRef(refSelected);
if (revisionRef != null && !revisionRef.getBranchNameLabel().contains(getSelectedBranch())) {
//test to see if we can rename the branch or not by removing the patch set
int lastSlash = getSelectedBranch().lastIndexOf('/');
String perfectMatchModified = lastSlash == -1
? getSelectedBranch()
: getSelectedBranch().substring(0, lastSlash);
checkBranchWithNoPatchSet(localRepo, revisionRef, perfectMatchModified);
}
}
}
/**
* Verify if we should allow to rename the selected branch to a branch showing the proper patch-set
*
* @param localRepo
* @param revisionRef
* @param branchNoPatchSet
*/
private void checkBranchWithNoPatchSet(Repository localRepo, Change revisionRef, String branchNoPatchSet) {
if (revisionRef.getBranchNameLabel().contains(branchNoPatchSet)) {
String newName = revisionRef.getBranchNameLabel();
//Question whether to modify the branch name or keep the old name
if (UIUtils.renameBranch(RENAME_KEY, Display.getDefault().getActiveShell(),
Messages.CheckoutRevisionRenameBranchTitle,
NLS.bind(Messages.CheckoutRevisionRenameBranch,
new String[] { Integer.toString(revisionRef.getPatchSet()),
Integer.toString(revisionRef.getChangeNumber()), getSelectedBranch(),
newName })) == IDialogConstants.YES_ID) {
renameBranch(newName, localRepo);
}
}
}
public Map<String, BranchMatch> findAllPotentialBranches(Repository localRepo) {
Git gitRepo = new Git(localRepo);
//Map <Key,value> = Map<Short branch name, commit id>
Map<String, String> mapBranches = new HashMap<>();
Map<String, BranchMatch> potentialBranches = null;
//Map <Key,Map<keycommit, ListChangeIdvalue> = Map<Short branch name, commit id, list of changeId>
Map<String, Map<String, List<String>>> mapBranchesChangeId = new HashMap<>();
try (RevWalk walk = new RevWalk(localRepo)) {
mapBranchNameWithCommitId(gitRepo, mapBranches, mapBranchesChangeId, walk);
//Get only potential branches
potentialBranches = mapPotentialBranch(mapBranchesChangeId);
} catch (GitAPIException e) {
EGerritCorePlugin.logError("find All Potential Branches()" + e.getMessage()); //$NON-NLS-1$
}
gitRepo.close();
return potentialBranches;
}
//Build a map of all branches --> commit ID -> List of changeId
//This goes over all the branches in the repo
private void mapBranchNameWithCommitId(Git gitRepo, Map<String, String> mapBranches,
Map<String, Map<String, List<String>>> mapBranchesChangeId, RevWalk walk) throws GitAPIException {
for (Ref current : gitRepo.branchList().call()) {
RevCommit commit;
try {
commit = walk.parseCommit(current.getObjectId());
mapBranches.put(Repository.shortenRefName(current.getName()), commit.getName());
//Map branch -> commitId -> List of changeId
Map<String, List<String>> mapCommitChangeid = new HashMap<String, List<String>>();
List<String> footerLines = commit.getFooterLines("Change-Id"); //$NON-NLS-1$
mapCommitChangeid.put(commit.getName(), footerLines);
mapBranchesChangeId.put(Repository.shortenRefName(current.getName()), mapCommitChangeid);
} catch (IOException e) {
EGerritCorePlugin.logError(gerritClient.getRepository().formatGerritVersion() + e.getMessage());
}
}
}
/**
* Open the dialog to let the user selects his options
*
* @param localRepo
* @param potentialBranches
*/
private boolean branchUiSelection(Repository localRepo, Map<String, BranchMatch> potentialBranches) {
BranchSelectionDialog branchSelectDialog = new BranchSelectionDialog(null, potentialBranches, changeInfo,
revisionCheckedOut);
int result = branchSelectDialog.open();
boolean returnOK = true;
if (result == Window.CANCEL) {
returnOK = false;
}
if (result == IDialogConstants.OK_ID) {
//New selected
String psSelected = revisionCheckedOut.getRef();
FetchGerritChangeWizard var = new FetchGerritChangeWizard(localRepo, psSelected);
WizardDialog w = new WizardDialog(getShell(), var);
w.create();
int retCode = w.open();
if (retCode == Window.CANCEL) {
returnOK = false;
}
} else if (result == IDialogConstants.CLIENT_ID) { // SWITCH
try {
setSelectedBranch(branchSelectDialog.getSelectedBranch());
if (getSelectedBranch() != null) {
checkoutBranch(getSelectedBranch(), localRepo);
}
} catch (Exception e) {
}
}
return returnOK;
}
//Filter the branches that need to be shown and indicate the level of matching for these branches
private Map<String, BranchMatch> mapPotentialBranch(Map<String, Map<String, List<String>>> mapBranchesChangeId) {
Map<String, BranchMatch> mapBranches = new TreeMap<>();
String defaultBranchName = changeInfo.get_number() + "/" //$NON-NLS-1$
+ revisionCheckedOut.get_number();
Iterator<Entry<String, Map<String, List<String>>>> iterBranch = mapBranchesChangeId.entrySet().iterator();
while (iterBranch.hasNext()) {
Entry<String, Map<String, List<String>>> entryBranch = iterBranch.next();
Map<String, List<String>> mapCommitId = entryBranch.getValue();
Iterator<Entry<String, List<String>>> iteratorcommitId = mapCommitId.entrySet().iterator();
while (iteratorcommitId.hasNext()) {
Entry<String, List<String>> entryCommitIds = iteratorcommitId.next();
List<String> listChangeIds = entryCommitIds.getValue();
Iterator<String> iterChangeId = listChangeIds.iterator();
while (iterChangeId.hasNext()) {
fillMappForPotentialBranch(mapBranches, defaultBranchName, entryBranch, entryCommitIds,
iterChangeId);
}
}
}
return mapBranches;
}
/**
* @param mapBranches
* @param defaultBranchName
* @param entryBranch
* @param entryCommitIds
* @param iterChangeId
*/
private void fillMappForPotentialBranch(Map<String, BranchMatch> mapBranches, String defaultBranchName,
Entry<String, Map<String, List<String>>> entryBranch, Entry<String, List<String>> entryCommitIds,
Iterator<String> iterChangeId) {
String lookingChangeId = revisionCheckedOut.getChangeInfo().getChange_id().trim();
String lookingCommitIdForRevision = revisionCheckedOut.getCommit().getCommit().trim();
String changeId = iterChangeId.next().trim();
if (matchesPerfectlyAnotherRevision(revisionCheckedOut, entryCommitIds.getKey())) {
return;
}
if (lookingCommitIdForRevision.equals(entryCommitIds.getKey())) {
mapBranches.put(entryBranch.getKey(), BranchMatch.PERFECT_MATCH);//Perfect match branch with commit Id
return;
}
if (lookingChangeId.equals(changeId)) {
mapBranches.put(entryBranch.getKey(), BranchMatch.CHANGE_ID_MATCH);//Potential branch for this changeId, but with some modification on the branch
return;
}
if (entryBranch.getKey().contains(defaultBranchName)) {
mapBranches.put(entryBranch.getKey(), BranchMatch.BRANCH_NAME_MATCH);//Perfect match branch
return;
}
}
//Check if the given commitId matches the commitId of a revision that is not the current one
private boolean matchesPerfectlyAnotherRevision(RevisionInfo revisionToCheckOut, String commitId) {
ChangeInfo review = revisionToCheckOut.getChangeInfo();
for (RevisionInfo aRevision : review.getRevisions().values()) {
if (aRevision == revisionToCheckOut) {
continue;
}
if (commitId.equals(aRevision.getCommit().getCommit().trim())) {
return true;
}
}
return false;
}
private void checkoutBranch(String branchName, Repository repo) throws Exception {
CheckoutCommand command = null;
try (Git gitRepo = new Git(repo)) {
command = gitRepo.checkout();
command.setCreateBranch(false);
command.setName(branchName);
command.setForce(false);
command.call();
} catch (Throwable t) {
if (command != null) {
CheckoutResult result = command.getResult();
if (result != null) {
new CheckoutConflictDialog(Display.getDefault().getActiveShell(), repo, result.getConflictList())
.open();
} else {
EGerritCorePlugin.logError(gerritClient.getRepository().formatGerritVersion() + t.getMessage());
}
}
}
}
private Shell getShell() {
return Display.getDefault().getActiveShell();
}
private void setSelectedBranch(String selectedBranch) {
this.selectedBranch = selectedBranch;
}
private String getSelectedBranch() {
return selectedBranch;
}
/**
* Rename the branch using the default naming: change/<reviewId>/<patch-set>. Catching exception for
* RefNotFoundException, InvalidRefNameException, RefAlreadyExistsException, DetachedHeadException, GitAPIException
*
* @param newName
* @param repo
*/
private void renameBranch(String newName, Repository repo) {
RenameBranchCommand command = null;
try (Git gitRepo = new Git(repo)) {
command = gitRepo.branchRename();
command.setNewName(newName);
command.call();
} catch (Exception e) {
UIUtils.displayInformation(Messages.CheckoutRevisionRenameBranchTitle,
NLS.bind(Messages.CheckoutRevisionRenameException, e.getMessage()));
}
}
/**
* Private class reading the information associated to a ref change
*/
private static final class Change {
private final String refName;
private final Integer changeNumber;
private final Integer patchSetNumber;
private static final String REF_CHANGES = "refs/changes/"; //$NON-NLS-1$
static Change fromRef(String refName) {
try {
if (!refName.startsWith(REF_CHANGES)) {
return null;
}
int reflen = REF_CHANGES.length();
String[] tokens = refName.substring(reflen).split("/"); //$NON-NLS-1$
if (tokens.length != 3) {
return null;
}
Integer changeNumber = Integer.valueOf(tokens[1]);
Integer patchSetNumber = Integer.valueOf(tokens[2]);
return new Change(refName, changeNumber, patchSetNumber);
} catch (NumberFormatException e) {
// if we can't parse this, just return null
return null;
} catch (IndexOutOfBoundsException e) {
// if we can't parse this, just return null
return null;
}
}
private Change(String refName, Integer changeNumber, Integer patchSetNumber) {
this.refName = refName;
this.changeNumber = changeNumber;
this.patchSetNumber = patchSetNumber;
}
public String getBranchNameLabel() {
return "change/" + changeNumber + "/" + patchSetNumber; //$NON-NLS-1$ //$NON-NLS-2$
}
public Integer getPatchSet() {
return patchSetNumber;
}
public Integer getChangeNumber() {
return changeNumber;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return refName;
}
}
}