/
Algorithm.h
358 lines (281 loc) · 11.8 KB
/
Algorithm.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
#pragma once
#include "imap.h"
#include "i18n.h"
#include "itextstream.h"
#include "Repository.h"
#include "gamelib.h"
#include "GitException.h"
#include "GitModule.h"
#include "Commit.h"
#include "Diff.h"
#include "VersionControlLib.h"
#include "command/ExecutionFailure.h"
#include "wxutil/dialog/MessageBox.h"
#include "ui/CommitDialog.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,
// Merge is not possible at this point
MergeAlreadyInProgress,
};
struct RemoteStatus
{
std::size_t localAheadCount;
std::size_t remoteAheadCount;
std::string label;
RequiredMergeStrategy strategy;
};
inline RemoteStatus analyseRemoteStatus(const std::shared_ptr<Repository>& repository)
{
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
if (mapPath.empty())
{
return RemoteStatus{ 0, 0, _("-") };
}
auto status = repository->getSyncStatusOfBranch(*repository->getHead());
if (repository->mergeIsInProgress())
{
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead,
_("Merge in progress"), RequiredMergeStrategy::MergeAlreadyInProgress };
}
auto mapFileHasUncommittedChanges = repository->fileHasUncommittedChanges(mapPath);
if (status.remoteCommitsAhead == 0)
{
return status.localCommitsAhead == 0 ?
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
auto head = repository->getHead();
auto upstream = head->getUpstream();
// Find the merge base for this ref and its upstream
auto mergeBase = repository->findMergeBase(*head, *upstream);
auto remoteDiffAgainstBase = repository->getDiff(*upstream, *mergeBase);
bool remoteDiffContainsMap = remoteDiffAgainstBase->containsFile(mapPath);
if (!remoteDiffContainsMap)
{
// Remote didn't change the map, we can integrate it without conflicting the loaded map
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Integrate"),
status.localCommitsAhead == 0 ? RequiredMergeStrategy::FastForward : RequiredMergeStrategy::MergeRecursively };
}
if (mapFileHasUncommittedChanges)
{
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Commit, then integrate "),
RequiredMergeStrategy::MergeMapWithUncommittedChanges };
}
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"),
RequiredMergeStrategy::MergeRecursively };
}
// Both the local and the remote diff are affecting the map file, this needs resolution
return RemoteStatus{ status.localCommitsAhead, status.remoteCommitsAhead, _("Resolve"),
RequiredMergeStrategy::MergeMap };
}
inline std::string getInfoFilePath(const std::string& mapPath)
{
auto format = GlobalMapFormatManager().getMapFormatForFilename(mapPath);
if (format && format->allowInfoFileCreation())
{
return os::replaceExtension(mapPath, game::current::getInfoFileExtension());
}
return std::string();
}
inline void resolveMapFileConflictUsingOurs(const std::shared_ptr<Repository>& repository)
{
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
auto index = repository->getIndex();
// Remove the conflict state of the map file after save
if (!mapPath.empty() && index->fileIsConflicted(mapPath))
{
index->resolveByUsingOurs(mapPath);
auto infoFilePath = getInfoFilePath(mapPath);
if (!infoFilePath.empty())
{
index->resolveByUsingOurs(infoFilePath);
}
index->write();
}
}
inline void tryToFinishMerge(const std::shared_ptr<Repository>& repository)
{
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
auto index = repository->getIndex();
if (index->hasConflicts())
{
// Remove the conflict state from the map
resolveMapFileConflictUsingOurs(repository);
// If the index still has conflicts, notify the user
if (index->hasConflicts())
{
wxutil::Messagebox::Show(_("Conflicts"),
_("There are still unresolved conflicts in the repository.\nPlease use your Git client to resolve them and try again."),
::ui::IDialog::MessageType::MESSAGE_CONFIRM);
return;
}
}
auto head = repository->getHead();
if (!head) throw git::GitException("Cannot resolve repository HEAD");
auto upstream = head->getUpstream();
if (!upstream) throw git::GitException("Cannot resolve upstream ref from HEAD");
// We need the commit metadata to be valid
git::CommitMetadata metadata;
metadata.name = repository->getConfigValue("user.name");
metadata.email = repository->getConfigValue("user.email");
metadata.message = "Integrated remote changes from " + upstream->getShorthandName();
if (metadata.name.empty() || metadata.email.empty())
{
metadata = ui::CommitDialog::RunDialog(metadata);
}
if (metadata.isValid())
{
repository->createCommit(metadata, upstream);
repository->cleanupState();
}
}
inline void performFastForward(const std::shared_ptr<Repository>& repository)
{
auto head = repository->getHead();
auto upstream = head->getUpstream();
// Find the merge base for this ref and its upstream
auto mergeBase = repository->findMergeBase(*head, *upstream);
auto remoteDiffAgainstBase = repository->getDiff(*upstream, *mergeBase);
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
bool remoteDiffContainsMap = remoteDiffAgainstBase->containsFile(mapPath);
repository->fastForwardToTrackedRemote();
if (!remoteDiffContainsMap)
{
return;
}
// The map has been modified on disk, so it might be a good choice to reload the map
if (wxutil::Messagebox::Show(_("Map has been updated"),
_("The map file has been updated on disk, reload the map file now?"),
::ui::IDialog::MessageType::MESSAGE_ASK) == ::ui::IDialog::RESULT_YES)
{
GlobalCommandSystem().executeCommand("OpenMap", GlobalMapModule().getMapName());
}
}
inline void syncWithRemote(const std::shared_ptr<Repository>& repository)
{
if (repository->mergeIsInProgress())
{
throw GitException(_("Merge in progress"));
}
if (GlobalMapModule().isModified())
{
throw GitException(_("The map file has unsaved changes, please save before merging."));
}
auto mapPath = repository->getRepositoryRelativePath(GlobalMapModule().getMapName());
RemoteStatus status = analyseRemoteStatus(repository);
switch (status.strategy)
{
case RequiredMergeStrategy::NoMergeRequired:
rMessage() << "No merge required." << std::endl;
return;
case RequiredMergeStrategy::JustPush:
repository->pushToTrackedRemote();
return;
case RequiredMergeStrategy::FastForward:
performFastForward(repository);
return;
}
// All the other merge strategies include a recursive merge, prepare it
if (status.strategy == RequiredMergeStrategy::MergeMapWithUncommittedChanges)
{
// Commit the current map changes
wxutil::Messagebox::Show(_("Pending Commit"),
_("The map file has uncommitted changes, please commit first before integrating."),
::ui::IDialog::MessageType::MESSAGE_CONFIRM);
return;
}
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;
auto upstream = repository->getHead()->getUpstream();
git_oid upstreamOid;
auto error = git_reference_name_to_id(&upstreamOid, repository->_get(), upstream->getName().c_str());
git_annotated_commit* mergeHead;
error = git_annotated_commit_lookup(&mergeHead, repository->_get(), &upstreamOid);
GitException::ThrowOnError(error);
try
{
std::vector<const git_annotated_commit*> mergeHeads;
mergeHeads.push_back(mergeHead);
error = git_merge_analysis(&analysis, &preference, repository->_get(), mergeHeads.data(), mergeHeads.size());
GitException::ThrowOnError(error);
// The result of the analysis must be that a three-way merge is required
if (analysis != GIT_MERGE_ANALYSIS_NORMAL)
{
throw GitException("The repository state doesn't require a regular merge, cannot proceed.");
}
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);
GitException::ThrowOnError(error);
// Check if the loaded map is affected by the merge
if (status.strategy != RequiredMergeStrategy::MergeMap)
{
tryToFinishMerge(repository);
return;
}
// A map merge is required
wxutil::Messagebox::Show(_("Map Merge Required"),
_("The map has been changed both on the server and locally.\n"
"DarkRadiant will now switch to Merge Mode to highlight the differences.\n"
"Please have a look, resolve possible conflicts and finish the merge."),
::ui::IDialog::MessageType::MESSAGE_CONFIRM);
auto mergeBase = repository->findMergeBase(*repository->getHead(), *upstream);
auto baseUri = constructVcsFileUri(GitModule::UriPrefix, Reference::OidToString(mergeBase->getOid()), mapPath);
auto sourceUri = constructVcsFileUri(GitModule::UriPrefix, Reference::OidToString(&upstreamOid), mapPath);
try
{
// The loaded map merge needs to be confirmed by the user
GlobalMapModule().startMergeOperation(sourceUri, baseUri);
// Done here, wait until the user finishes or aborts the merge
return;
}
catch (const cmd::ExecutionFailure& ex)
{
throw GitException(ex.what());
}
}
catch (const GitException& ex)
{
git_annotated_commit_free(mergeHead);
throw ex;
}
}
}
}