Skip to content

Commit cddf801

Browse files
committed
Conflict resolutions are now possible.
1 parent d77b08b commit cddf801

9 files changed

Lines changed: 1214 additions & 4 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#include "MergeConflictResolve.h"
2+
3+
namespace gitReview
4+
{
5+
namespace
6+
{
7+
void trimTrailingCr(std::string &s)
8+
{
9+
while (!s.empty() && s.back() == '\r')
10+
s.pop_back();
11+
}
12+
13+
bool lineStartsWithMarker(std::string_view line, std::string_view marker)
14+
{
15+
while (!line.empty() && line.front() == ' ')
16+
line.remove_prefix(1);
17+
return line.size() >= marker.size() && line.compare(0, marker.size(), marker) == 0;
18+
}
19+
20+
/// First index >= \p from at a line start where the line begins with \p marker (after optional spaces / CR).
21+
size_t findMarkerLine(std::string_view doc, size_t from, std::string_view marker)
22+
{
23+
size_t i = from;
24+
while (i < doc.size())
25+
{
26+
const size_t lineEnd = doc.find('\n', i);
27+
const std::string_view line = doc.substr(i, (lineEnd == std::string::npos ? doc.size() : lineEnd) - i);
28+
if (lineStartsWithMarker(line, marker))
29+
return i;
30+
if (lineEnd == std::string::npos)
31+
break;
32+
i = lineEnd + 1;
33+
}
34+
return std::string::npos;
35+
}
36+
37+
/// Byte offset of the first character after the line that starts at \p lineBegin (past `\n`, or `doc.size()`).
38+
size_t pastLine(std::string_view doc, size_t lineBegin)
39+
{
40+
const size_t lineEnd = doc.find('\n', lineBegin);
41+
if (lineEnd == std::string::npos)
42+
return doc.size();
43+
return lineEnd + 1;
44+
}
45+
46+
std::string concatBlocks(const std::string &a, const std::string &b)
47+
{
48+
if (a.empty())
49+
return b;
50+
if (b.empty())
51+
return a;
52+
if (a.back() == '\n')
53+
return a + b;
54+
return a + '\n' + b;
55+
}
56+
} // namespace
57+
58+
std::vector<MergeConflictHunk> listMergeConflictHunks(std::string_view doc)
59+
{
60+
static constexpr std::string_view kOpen = "<<<<<<<";
61+
static constexpr std::string_view kMid = "=======";
62+
static constexpr std::string_view kClose = ">>>>>>>";
63+
64+
std::vector<MergeConflictHunk> out;
65+
size_t search = 0;
66+
for (;;)
67+
{
68+
const size_t p = doc.find("<<<<<<<", search);
69+
if (p == std::string::npos)
70+
break;
71+
if (p > 0 && doc[p - 1] != '\n')
72+
{
73+
search = p + 7;
74+
continue;
75+
}
76+
const size_t openLineBegin = p;
77+
78+
const size_t oursRegionStart = pastLine(doc, openLineBegin);
79+
if (oursRegionStart > doc.size())
80+
break;
81+
82+
const size_t sepLineBegin = findMarkerLine(doc, oursRegionStart, kMid);
83+
if (sepLineBegin == std::string::npos)
84+
break;
85+
86+
const size_t theirsRegionStart = pastLine(doc, sepLineBegin);
87+
if (theirsRegionStart > doc.size())
88+
break;
89+
90+
const size_t closeLineBegin = findMarkerLine(doc, theirsRegionStart, kClose);
91+
if (closeLineBegin == std::string::npos)
92+
break;
93+
94+
MergeConflictHunk h;
95+
h.openLineBegin = openLineBegin;
96+
h.sepLineBegin = sepLineBegin;
97+
h.closeLineBegin = closeLineBegin;
98+
h.afterClose = pastLine(doc, closeLineBegin);
99+
out.push_back(h);
100+
search = h.afterClose;
101+
}
102+
return out;
103+
}
104+
105+
static int byteOffsetToLine0(std::string_view doc, size_t byteOff)
106+
{
107+
if (byteOff > doc.size())
108+
byteOff = doc.size();
109+
int ln = 0;
110+
for (size_t i = 0; i < byteOff; ++i)
111+
{
112+
if (doc[i] == '\n')
113+
++ln;
114+
}
115+
return ln;
116+
}
117+
118+
std::vector<MergeConflictHunkLines> listMergeConflictHunkLines(const std::string &doc)
119+
{
120+
const std::vector<MergeConflictHunk> hunks = listMergeConflictHunks(doc);
121+
std::vector<MergeConflictHunkLines> out;
122+
out.reserve(hunks.size());
123+
for (size_t i = 0; i < hunks.size(); ++i)
124+
{
125+
const MergeConflictHunk &h = hunks[i];
126+
MergeConflictHunkLines hl;
127+
hl.hunkIndex = i;
128+
hl.firstLine0 = byteOffsetToLine0(doc, h.openLineBegin);
129+
const size_t lastByte = h.afterClose > 0 ? h.afterClose - 1 : h.openLineBegin;
130+
hl.lastLine0 = byteOffsetToLine0(doc, lastByte);
131+
out.push_back(hl);
132+
}
133+
return out;
134+
}
135+
136+
int mergeConflictHunkAtLine(const std::vector<MergeConflictHunkLines> &ranges, int line0)
137+
{
138+
for (size_t i = 0; i < ranges.size(); ++i)
139+
{
140+
if (line0 >= ranges[i].firstLine0 && line0 <= ranges[i].lastLine0)
141+
return static_cast<int>(i);
142+
}
143+
return -1;
144+
}
145+
146+
bool applyMergeConflictPick(std::string &doc, size_t hunkIndex, MergeConflictPick pick, std::string *outError)
147+
{
148+
if (outError)
149+
outError->clear();
150+
std::vector<MergeConflictHunk> hunks = listMergeConflictHunks(doc);
151+
if (hunkIndex >= hunks.size())
152+
{
153+
if (outError)
154+
*outError = "Conflict hunk index out of range.";
155+
return false;
156+
}
157+
const MergeConflictHunk &h = hunks[hunkIndex];
158+
159+
const size_t oursStart = pastLine(doc, h.openLineBegin);
160+
if (oursStart > h.sepLineBegin)
161+
{
162+
if (outError)
163+
*outError = "Malformed conflict (ours region).";
164+
return false;
165+
}
166+
const size_t theirsStart = pastLine(doc, h.sepLineBegin);
167+
if (theirsStart > h.closeLineBegin)
168+
{
169+
if (outError)
170+
*outError = "Malformed conflict (theirs region).";
171+
return false;
172+
}
173+
174+
std::string ours(doc.substr(oursStart, h.sepLineBegin - oursStart));
175+
std::string theirs(doc.substr(theirsStart, h.closeLineBegin - theirsStart));
176+
trimTrailingCr(ours);
177+
trimTrailingCr(theirs);
178+
179+
std::string replacement;
180+
switch (pick)
181+
{
182+
case MergeConflictPick::Ours:
183+
replacement = ours;
184+
break;
185+
case MergeConflictPick::Theirs:
186+
replacement = theirs;
187+
break;
188+
case MergeConflictPick::OursThenTheirs:
189+
replacement = concatBlocks(ours, theirs);
190+
break;
191+
case MergeConflictPick::TheirsThenOurs:
192+
replacement = concatBlocks(theirs, ours);
193+
break;
194+
}
195+
196+
doc.replace(h.openLineBegin, h.afterClose - h.openLineBegin, replacement);
197+
return true;
198+
}
199+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <string>
5+
#include <string_view>
6+
#include <vector>
7+
8+
namespace gitReview
9+
{
10+
enum class MergeConflictPick
11+
{
12+
Ours,
13+
Theirs,
14+
OursThenTheirs,
15+
TheirsThenOurs,
16+
};
17+
18+
/// One Git-style conflict block: `<<<<<<<` … `=======` … `>>>>>>>`.
19+
struct MergeConflictHunk
20+
{
21+
/// Byte offset of the first character of the line containing `<<<<<<<`.
22+
size_t openLineBegin = 0;
23+
/// Byte offset of the first character of the line containing `=======`.
24+
size_t sepLineBegin = 0;
25+
/// Byte offset of the first character of the line containing `>>>>>>>`.
26+
size_t closeLineBegin = 0;
27+
/// First byte after the `>>>>>>>` line (past the newline, or end of string).
28+
size_t afterClose = 0;
29+
};
30+
31+
std::vector<MergeConflictHunk> listMergeConflictHunks(std::string_view doc);
32+
33+
/// Replaces hunk \p hunkIndex (0-based in \c listMergeConflictHunks order) using \p pick. Returns false on failure.
34+
bool applyMergeConflictPick(std::string &doc, size_t hunkIndex, MergeConflictPick pick, std::string *outError);
35+
36+
/// 0-based inclusive line range in \p doc (split by `\n`) covered by each conflict hunk (including marker lines).
37+
struct MergeConflictHunkLines
38+
{
39+
size_t hunkIndex = 0;
40+
int firstLine0 = 0;
41+
int lastLine0 = 0;
42+
};
43+
44+
std::vector<MergeConflictHunkLines> listMergeConflictHunkLines(const std::string &doc);
45+
46+
/// Returns hunk index, or \c -1 if none.
47+
int mergeConflictHunkAtLine(const std::vector<MergeConflictHunkLines> &ranges, int line0);
48+
49+
}

Examples/ExampleGitReview/RepoModel.cpp

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ namespace gitReview
179179
return false;
180180
}
181181

182-
static void decideBinaryAfterLoad(const FileEntry &entry, const std::string &left, const std::string &right, bool &outBinary)
182+
void decideBinaryAfterLoad(const FileEntry &entry, const std::string &left, const std::string &right, bool &outBinary)
183183
{
184184
const bool lc = pathLooksBinaryByContent(left);
185185
const bool rc = pathLooksBinaryByContent(right);
@@ -481,4 +481,51 @@ namespace gitReview
481481
outRightIsWorktreeFile = false;
482482
decideBinaryAfterLoad(entry, outLeft, outRight, outBinary);
483483
}
484+
485+
bool pathHasUnmergedIndex(const std::string &repoRoot, const std::string &pathInRepo)
486+
{
487+
if (repoRoot.empty() || pathInRepo.empty())
488+
return false;
489+
GitRunResult r = runGit(repoRoot, { "ls-files", "-u", "--", pathInRepo });
490+
if (r.exitCode != 0)
491+
return false;
492+
return !trim(r.standardOut).empty();
493+
}
494+
495+
bool tryLoadMergeIndexVersions(const std::string &repoRoot, const std::string &pathInRepo, std::string &outBase, std::string &outOurs,
496+
std::string &outTheirs, std::string &outError)
497+
{
498+
outError.clear();
499+
outBase.clear();
500+
outOurs.clear();
501+
outTheirs.clear();
502+
if (repoRoot.empty() || pathInRepo.empty())
503+
{
504+
outError = "Missing repository path or file path.";
505+
return false;
506+
}
507+
508+
auto showStage = [&](const char *stageNum, std::string &into) -> bool {
509+
const std::string ref = std::string(":") + stageNum + ":" + pathInRepo;
510+
GitRunResult r = runGit(repoRoot, { "show", ref });
511+
if (r.exitCode != 0)
512+
return false;
513+
into = std::move(r.standardOut);
514+
return true;
515+
};
516+
517+
if (!showStage("2", outOurs))
518+
{
519+
outError = "Could not read merge stage 2 (ours) from the index.";
520+
return false;
521+
}
522+
if (!showStage("3", outTheirs))
523+
{
524+
outError = "Could not read merge stage 3 (theirs) from the index.";
525+
return false;
526+
}
527+
if (!showStage("1", outBase))
528+
outBase.clear();
529+
return true;
530+
}
484531
}

Examples/ExampleGitReview/RepoModel.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ namespace gitReview
6262
void loadDiffPair(const std::string &repoRoot, ReviewMode mode, const FileEntry &entry, std::string &outLeft, std::string &outRight,
6363
bool &outRightIsWorktreeFile, bool &outBinary, std::string &outError);
6464

65+
/// True when the index has multiple stages for \p pathInRepo (merge conflict).
66+
bool pathHasUnmergedIndex(const std::string &repoRoot, const std::string &pathInRepo);
67+
68+
/// Reads merge stages \c :1: (base), \c :2: (ours), \c :3: (theirs). Missing stage 1 yields empty base.
69+
/// Returns false if stages 2 and 3 cannot both be read (caller falls back to \c loadDiffPair).
70+
bool tryLoadMergeIndexVersions(const std::string &repoRoot, const std::string &pathInRepo, std::string &outBase, std::string &outOurs,
71+
std::string &outTheirs, std::string &outError);
72+
6573
/// Content-based heuristic: checks for NUL bytes and high density of
6674
/// non-text control characters in the first 8 KB.
6775
bool pathLooksBinaryByContent(const std::string &text);
@@ -71,4 +79,7 @@ namespace gitReview
7179

7280
/// Source / markup extensions we prefer to treat as text when content looks printable.
7381
bool pathLooksTextByExtension(const std::string &path);
82+
83+
/// Sets \p outBinary using the same rules as \c loadDiffPair (content + extension heuristics).
84+
void decideBinaryAfterLoad(const FileEntry &entry, const std::string &left, const std::string &right, bool &outBinary);
7485
}

0 commit comments

Comments
 (0)