Skip to content

Commit

Permalink
#5662: More pre-merge analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
codereader committed Jul 16, 2021
1 parent abdb29e commit 4121212
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 18 deletions.
122 changes: 110 additions & 12 deletions plugins/vcs/Algorithm.h
Expand Up @@ -3,19 +3,44 @@
#include "imap.h"
#include "i18n.h"
#include "Repository.h"
#include "GitException.h"
#include "Diff.h"
#include <git2.h>

namespace vcs
{

namespace git
{

enum class RequiredMergeStrategy
{
// The local branch is up to date
NoMergeRequired,

// Only the local branch got commits they can be pushed
JustPush,

// The local branch can be fast-forwarded since there are no local changes
FastForward,

// At most one branch changed the map, the branches can be merged
MergeRecursively,

// Both branches changed the loaded map, a merge is required
MergeMap,

// The local map has uncommitted changes, and the remote is trying to change it
MergeMapWithUncommittedChanges,
};

struct RemoteStatus
{
std::size_t localAheadCount;
std::size_t remoteAheadCount;
std::string label;

RequiredMergeStrategy strategy;
};

inline RemoteStatus analyseRemoteStatus(const std::shared_ptr<Repository>& repository)
Expand All @@ -28,12 +53,18 @@ inline RemoteStatus analyseRemoteStatus(const std::shared_ptr<Repository>& repos
}

auto status = repository->getSyncStatusOfBranch(*repository->getHead());
auto mapFileHasUncommittedChanges = repository->fileHasUncommittedChanges(mapPath);

if (status.remoteCommitsAhead == 0)
{
return status.localCommitsAhead == 0 ?
RemoteStatus{ status.localCommitsAhead, 0, _("Up to date") } :
RemoteStatus{ status.localCommitsAhead, 0, _("Pending Upload") };
RemoteStatus{ status.localCommitsAhead, 0, _("Up to date"), RequiredMergeStrategy::NoMergeRequired } :
RemoteStatus{ status.localCommitsAhead, 0, _("Pending Upload"), RequiredMergeStrategy::JustPush };
}
else if (status.localCommitsAhead == 0 && !mapFileHasUncommittedChanges)
{
// No local commits and no uncommitted changes, we can fast-forward
return RemoteStatus{ 0, status.remoteCommitsAhead, _("Integrate"), RequiredMergeStrategy::FastForward };
}

// Check the incoming commits for modifications of the loaded map
Expand All @@ -44,28 +75,95 @@ inline RemoteStatus analyseRemoteStatus(const std::shared_ptr<Repository>& repos
auto mergeBase = repository->findMergeBase(*head, *upstream);

auto remoteDiffAgainstBase = repository->getDiff(*upstream, *mergeBase);

bool remoteDiffContainsMap = remoteDiffAgainstBase->containsFile(mapPath);

if (!remoteDiffAgainstBase->containsFile(mapPath))
if (!remoteDiffContainsMap)
{
// Remote didn't change, we can integrate it without conflicting the loaded map
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Integrate") };
// Remote didn't change the map, we can integrate it without conflicting the loaded map
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Integrate"),
RequiredMergeStrategy::MergeRecursively };
}

auto localDiffAgainstBase = repository->getDiff(*head, *mergeBase);

if (repository->fileHasUncommittedChanges(mapPath))
if (mapFileHasUncommittedChanges)
{
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Commit, then integrate ") };
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Commit, then integrate "),
RequiredMergeStrategy::MergeMapWithUncommittedChanges };
}

if (!localDiffAgainstBase->containsFile(mapPath))
auto localDiffAgainstBase = repository->getDiff(*head, *mergeBase);
bool localDiffContainsMap = localDiffAgainstBase->containsFile(mapPath);

if (!localDiffContainsMap)
{
// The local diff doesn't include the map, the remote changes can be integrated
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Integrate") };
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Integrate"),
RequiredMergeStrategy::MergeRecursively };
}

// Both the local and the remote diff are affecting the map file, this needs resolution
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Resolve") };
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Resolve"),
RequiredMergeStrategy::MergeMap };
}

inline void mergeRemoteChanges(const std::shared_ptr<Repository>& repository)
{
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
auto mapFileHasUncommittedChanges = !mapPath.empty() && repository->fileHasUncommittedChanges(mapPath);

if (mapFileHasUncommittedChanges)
{
throw git::GitException(_("The map file has uncommitted changes, cannot merge yet."));
}

RemoteStatus status = analyseRemoteStatus(repository);

if (!repository->isReadyForMerge())
{
throw git::GitException(_("Repository is not ready for a merge at this point"));
}

if (!repository->getHead() || !repository->getHead()->getUpstream())
{
throw git::GitException(_("Cannot resolve HEAD and the corresponding upstream branch"));
}

git_merge_analysis_t analysis;
git_merge_preference_t preference = GIT_MERGE_PREFERENCE_NONE;

git_annotated_commit* mergeHead;

auto upstream = repository->getHead()->getUpstream();
git_oid upstreamOid;
auto error = git_reference_name_to_id(&upstreamOid, repository->_get(), upstream->getName().c_str());

error = git_annotated_commit_lookup(&mergeHead, repository->_get(), &upstreamOid);
GitException::ThrowOnError(error);

std::vector<const git_annotated_commit*> mergeHeads;
mergeHeads.push_back(mergeHead);

error = git_merge_analysis(&analysis, &preference, repository->_get(), mergeHeads.data(), mergeHeads.size());

if (error < 0)
{
git_annotated_commit_free(mergeHead);
GitException::ThrowOnError(error);
}

git_merge_options mergeOptions = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkoutOptions = GIT_CHECKOUT_OPTIONS_INIT;
checkoutOptions.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_ALLOW_CONFLICTS;

error = git_merge(repository->_get(), mergeHeads.data(), mergeHeads.size(), &mergeOptions, &checkoutOptions);

if (error < 0)
{
git_annotated_commit_free(mergeHead);
GitException::ThrowOnError(error);
}


}

}
Expand Down
6 changes: 6 additions & 0 deletions plugins/vcs/Repository.cpp
Expand Up @@ -238,6 +238,12 @@ bool Repository::fileHasUncommittedChanges(const std::string& relativePath)
return (getFileStatus(relativePath) & GIT_STATUS_WT_MODIFIED) != 0;
}

bool Repository::isReadyForMerge()
{
auto state = git_repository_state(_repository);
return state == GIT_REPOSITORY_STATE_NONE;
}

Commit::Ptr Repository::findMergeBase(const Reference& first, const Reference& second)
{
git_oid firstOid;
Expand Down
2 changes: 2 additions & 0 deletions plugins/vcs/Repository.h
Expand Up @@ -59,6 +59,8 @@ class Repository final
// returns the number of commits each of them is ahead of the other one.
RefSyncStatus getSyncStatusOfBranch(const Reference& reference);

bool isReadyForMerge();

// Finds a common ancestor of the two refs, to base a merge operation on
std::shared_ptr<Commit> findMergeBase(const Reference& first, const Reference& second);

Expand Down
23 changes: 17 additions & 6 deletions plugins/vcs/ui/VcsStatus.cpp
Expand Up @@ -31,12 +31,6 @@ VcsStatus::VcsStatus(wxWindow* parent) :
_fetchInProgress(false),
_popupMenu(new wxutil::PopupMenu)
{
_popupMenu->addItem(std::make_shared<wxutil::CommandMenuItem>(
new wxMenuItem(nullptr, wxID_ANY, _("Check for Changes"), "Load"),
"GitFetch",
[this]() { return !_fetchInProgress; }
));

_mapStatus = findNamedObject<wxStaticText>(_panel, "MapStatusLabel");
_remoteStatus = findNamedObject<wxStaticText>(_panel, "RemoteStatusLabel");

Expand Down Expand Up @@ -64,6 +58,8 @@ VcsStatus::VcsStatus(wxWindow* parent) :
GlobalMapModule().signal_mapEvent().connect(
sigc::mem_fun(this, &VcsStatus::onMapEvent)
);

createPopupMenu();
}

VcsStatus::~VcsStatus()
Expand All @@ -86,6 +82,21 @@ wxWindow* VcsStatus::getWidget()
return _panel;
}

void VcsStatus::createPopupMenu()
{
_popupMenu->addItem(std::make_shared<wxutil::CommandMenuItem>(
new wxMenuItem(nullptr, wxID_ANY, _("Check for Changes"), ""),
"GitFetch",
[this]() { return !_fetchInProgress; }
));

_popupMenu->addItem(std::make_shared<wxutil::MenuItem>(
new wxMenuItem(nullptr, wxID_ANY, _("Integrate Changes from Server"), ""),
[this]() { if (_repository) { mergeRemoteChanges(_repository); } },
[this]() { return !_fetchInProgress; }
));
}

void VcsStatus::setRepository(const std::shared_ptr<git::Repository>& repository)
{
_repository = repository;
Expand Down
1 change: 1 addition & 0 deletions plugins/vcs/ui/VcsStatus.h
Expand Up @@ -51,6 +51,7 @@ class VcsStatus final :
void setRepository(const std::shared_ptr<git::Repository>& repository);

private:
void createPopupMenu();
void startFetchTask();
void restartTimer();
void onIntervalReached(wxTimerEvent& ev);
Expand Down

0 comments on commit 4121212

Please sign in to comment.