package bluej.groupwork.git;
import bluej.groupwork.TeamworkCommandError;
import bluej.groupwork.TeamworkCommandResult;
import bluej.groupwork.UpdateListener;
import bluej.groupwork.UpdateResults;
import bluej.utility.Debug;
import bluej.utility.DialogManager;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javafx.application.Platform;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CheckoutCommand.Stage;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.merge.MergeStrategy;
import threadchecker.OnThread;
import threadchecker.Tag;
| Git command to pull project changes from the upstream repository.
|
| @author Fabio Heday
|
@OnThread(Tag.Worker)
public class GitUpdateToCommand
extends GitCommand implements UpdateResults{
private final Set<File> forceFiles;
private final UpdateListener listener;
@OnThread(Tag.Any)
private final List<File> conflicts = new ArrayList<>();
@OnThread(Tag.Any)
private final Set<File> binaryConflicts = new HashSet<>();
| Construct a GitUpdateToCommand command object.
|
| <p>Note that this always "updates to" (merges with) origin/master. The fetch should have
|
* been performed previously.
*
* @param repository the repository
* @param listener the listener for notification of file changes
| @param forceFiles the files to "force update"
|
*/
@OnThread(Tag.Any)
public GitUpdateToCommand(GitRepository repository, UpdateListener listener, Set<File> forceFiles)
{
super(repository);
this.forceFiles = forceFiles;
|
|
this.listener = listener;
|
|
}
|
|
@Override
|
|
@OnThread(Tag.Worker)
|
|
public TeamworkCommandResult getResult()
|
|
{
|
|
try (Git repo = Git.open(this.getRepository().getProjectPath()))
|
|
{
|
|
File gitPath = this.getRepository().getProjectPath();
|
|
MergeCommand merge = repo.merge();
|
|
merge.setCommit(true);
|
|
merge.setFastForward(MergeCommand.FastForwardMode.FF);
|
|
merge.setStrategy(MergeStrategy.RECURSIVE);
|
|
if (! forceFiles.isEmpty())
|
|
{
|
|
Path basePath = Paths.get(this.getRepository().getProjectPath().toString());
|
|
CheckoutCommand ccommand = repo.checkout();
|
|
for (File f : forceFiles)
|
|
{
|
|
ccommand.addPath(GitUtilities.getRelativeFileName(basePath, f));
|
|
}
|
|
ccommand.call();
|
|
}
|
|
repo.getRepository().getListenerList().addWorkingTreeModifiedListener(e -> {
|
|
for (String modified : e.getModified())
|
|
{
|
|
Platform.runLater(() -> {
|
|
// Oddly (perhaps a bug), JGit reports removed files as "modified" if
// there is a conflict on update. We detect if a file is actually
// removed by checking if it exists:
File f = new File(getRepository().getProjectPath(), modified);
|
|
if (f.exists())
|
|
{
|
|
listener.fileModified(f);
|
|
}
|
|
else
|
|
{
|
|
listener.fileRemoved(f);
|
|
}
|
|
});
|
|
}
|
|
for (String deleted : e.getDeleted())
|
|
{
|
|
Platform.runLater(() -> {
|
|
listener.fileRemoved(new File(getRepository().getProjectPath(), deleted));
|
|
});
|
|
}
|
|
});
|
|
merge.include(repo.getRepository().resolve("origin/master")); // merge with remote repository.
MergeResult mergeResult = merge.call();
Map<String, int[][]> allConflicts;
switch (mergeResult.getMergeStatus()) {
case FAST_FORWARD:
|
|
// No conflicts; this was a fast-forward.
|
|
break;
|
|
case CONFLICTING:
|
|
// Update the conflicts list.
|
|
allConflicts = mergeResult.getConflicts();
|
|
allConflicts.keySet().stream().map((path) -> new File(gitPath, path)).forEach((f) -> {
|
|
conflicts.add(f);
|
|
});
|
|
break;
|
|
case FAILED:
|
|
// Proceed with conflict resolution if jGit managed to identify conflicts.
|
|
allConflicts = mergeResult.getConflicts();
|
|
if (allConflicts != null)
|
|
{
|
|
// I am not convinced this can actually happen, but this code exists so I
|
|
// will leave it for now and log the occurrence:
|
|
Debug.log("Git merge FAILED with conflicts list? conflicts = " + allConflicts);
allConflicts.keySet().stream().forEach(path -> {
conflicts.add(new File(gitPath, path));
});
}
else
|
|
{
|
|
// The cause(s) can be found via getFailingPaths(), in practice the reason
|
|
// is that either the tree or index is dirty (contains uncommitted
|
|
// changes).
|
|
String conflictMessage = DialogManager.getMessage("team-commit-needed");
return new TeamworkCommandError(conflictMessage, conflictMessage);
}
break;
case MERGED:
case ALREADY_UP_TO_DATE:
|
|
// Changes merged and committed as a merge, or nothing changed.
|
|
break;
|
|
default:
|
|
Debug.reportError("Unknown/unhandled Git merge status: " + mergeResult.getMergeStatus());
}
if (!conflicts.isEmpty() || !binaryConflicts.isEmpty()) {
Platform.runLater(() -> listener.handleConflicts(this));
}
}
|
|
catch (IOException ex)
|
|
{
|
|
return new TeamworkCommandError(ex.getMessage(), ex.getLocalizedMessage());
|
|
}
|
|
catch (CheckoutConflictException ex)
|
|
{
|
|
// This occurs when the working tree has modifications to files that would be updated.
|
|
String conflictMessage = DialogManager.getMessage("team-commit-needed");
return new TeamworkCommandError(conflictMessage, conflictMessage);
}
catch (GitAPIException ex)
{
return new TeamworkCommandError(ex.getMessage(), ex.getLocalizedMessage());
|
|
}
|
|
return new TeamworkCommandResult();
|
|
}
|
|
@Override
|
|
@OnThread(Tag.Any)
|
|
public boolean mergeCommitNeeded()
|
|
{
|
|
// Note that we only issue a handle-conflicts call if there were conflicts, in which case
|
|
// a merge commit is certainly needed.
|
|
return true;
|
|
}
|
|
@Override
|
|
@OnThread(Tag.Any)
|
|
public List<File> getConflicts()
|
|
{
|
|
return conflicts;
|
|
}
|
|
@Override
|
|
@OnThread(Tag.Any)
|
|
public Set<File> getBinaryConflicts()
|
|
{
|
|
return binaryConflicts;
|
|
}
|
|
@Override
|
|
@OnThread(Tag.FXPlatform)
|
|
public void overrideFiles(Set<File> files)
|
|
{
|
|
if (! files.isEmpty())
|
|
{
|
|
try (Git repo = Git.open(this.getRepository().getProjectPath()))
|
|
{
|
|
Path basePath = Paths.get(this.getRepository().getProjectPath().toString());
|
|
CheckoutCommand ccommand = repo.checkout();
|
|
for (File f : files)
|
|
{
|
|
ccommand.addPath(GitUtilities.getRelativeFileName(basePath, f));
|
|
}
|
|
ccommand.setStage(Stage.THEIRS);
|
|
ccommand.setForce(true);
|
|
ccommand.call();
|
|
}
|
|
catch (IOException | GitAPIException exc)
|
|
{
|
|
Debug.reportError("Git override files failed", exc);
}
}
}
}