Skip to content

Commit 54de9e1

Browse files
committed
feat: add git integration for 3-way merge and fix path handling
- Add base content support for 3-way merge operations - Integrate git commands to fetch base and ours content - Make filename required for git operations - Fix file paths to use relative instead of absolute paths - Improve conflict detection for identical ours/theirs cases
1 parent 7e0394e commit 54de9e1

File tree

3 files changed

+44
-11
lines changed

3 files changed

+44
-11
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"git-json-resolver": minor
3+
---
4+
5+
Add Git integration for 3-way merge support and fix file path handling
6+
7+
- Add base content support to ParsedConflict interface for 3-way merges
8+
- Integrate Git commands to fetch base and ours content when parsing conflicts
9+
- Make filename parameter required in ParseConflictOptions for Git operations
10+
- Fix file path handling in utils to return relative paths instead of absolute paths
11+
- Improve conflict detection for cases where ours and theirs are identical

lib/src/file-parser.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { execFile } from "node:child_process";
12
import { Config, SupportedParsers } from "./types";
3+
import { promisify } from "node:util";
4+
5+
const execFileAsync = promisify(execFile);
26

37
/**
48
* Represents a parsed conflict from a file with `ours` and `theirs` versions.
@@ -10,6 +14,8 @@ export interface ParsedConflict<T = unknown> {
1014
ours: T;
1115
/** Parsed content from the "theirs" side of the conflict. */
1216
theirs: T;
17+
/** Parsed content from the "base" side of the conflict (optional). */
18+
base?: T;
1319
/** Format used to parse the content (`json`, `yaml`, `toml`, `xml`, or `custom`). */
1420
format: string;
1521
}
@@ -19,14 +25,14 @@ export interface ParsedConflict<T = unknown> {
1925
*/
2026
export interface ParseConflictOptions extends Pick<Config, "parsers"> {
2127
/**
22-
* Optional filename hint to prioritize parser choice.
28+
* filename hint to prioritize parser choice as well as get base and ours from git.
2329
* Example:
2430
* - `config.yaml` → try `yaml` first.
2531
* - `data.toml` → try `toml` first.
2632
*
2733
* If extension is unknown, falls back to `parsers` or `"json"`.
2834
*/
29-
filename?: string;
35+
filename: string;
3036
}
3137

3238
/**
@@ -48,7 +54,7 @@ export interface ParseConflictOptions extends Pick<Config, "parsers"> {
4854
*/
4955
export const parseConflictContent = async <T = unknown>(
5056
content: string,
51-
options: ParseConflictOptions = {},
57+
options: ParseConflictOptions,
5258
): Promise<ParsedConflict<T>> => {
5359
const lines = content.split("\n");
5460
const oursLines: string[] = [];
@@ -87,8 +93,23 @@ export const parseConflictContent = async <T = unknown>(
8793
}
8894
}
8995

90-
const oursRaw = oursLines.join("\n");
96+
let oursRaw = oursLines.join("\n");
9197
const theirsRaw = theirsLines.join("\n");
98+
const baseRaw = await execFileAsync("git", ["show", `:1:${options.filename}`], {
99+
maxBuffer: 1024 * 1024 * 50,
100+
})
101+
.then(({ stdout }) => stdout)
102+
.catch(() => null);
103+
104+
// No conflict
105+
if (oursRaw === theirsRaw) {
106+
oursRaw =
107+
(await execFileAsync("git", ["show", `HEAD:${options.filename}`], {
108+
maxBuffer: 1024 * 1024 * 50,
109+
})
110+
.then(({ stdout }) => stdout)
111+
.catch(() => null)) ?? oursRaw;
112+
}
92113

93114
if (!oursRaw || !theirsRaw) {
94115
throw new Error("Conflict parsing resulted in empty content.");
@@ -98,11 +119,14 @@ export const parseConflictContent = async <T = unknown>(
98119
const parsers = normalizeParsers(options);
99120

100121
const [oursParsed, format] = await runParser(oursRaw, parsers);
101-
const [theirsParsed] = await runParser(theirsRaw, [format]);
122+
const [[theirsParsed], baseParsed] = await Promise.all(
123+
(baseRaw ? [theirsRaw, baseRaw] : [theirsRaw]).map(raw => runParser(raw, [format])),
124+
);
102125

103126
return {
104127
ours: oursParsed as T,
105128
theirs: theirsParsed as T,
129+
base: baseParsed?.[0] as T | undefined,
106130
format: typeof format === "string" ? format : format.name,
107131
};
108132
};

lib/src/utils.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,19 @@ export const listMatchingFiles = async ({
7575

7676
for (const entry of entries) {
7777
const fullPath = path.join(dir, entry.name);
78+
const relativePath = path.relative(root, fullPath);
7879

7980
if (entry.isDirectory()) {
8081
/* v8 ignore next */
81-
if (
82-
!/node_modules|\.git/.test(entry.name) &&
83-
!skipDirMatcher(path.relative(root, fullPath))
84-
) {
82+
if (!/node_modules|\.git/.test(entry.name) && !skipDirMatcher(relativePath)) {
8583
await walk(fullPath);
8684
}
87-
} else if (fileMatcher(path.relative(root, fullPath))) {
85+
} else if (fileMatcher(relativePath)) {
8886
try {
8987
const content = await fs.readFile(fullPath, "utf8");
9088

9189
if (includeNonConflicted || hasConflict(content)) {
92-
fileEntries.push({ filePath: fullPath, content });
90+
fileEntries.push({ filePath: relativePath, content });
9391
/* v8 ignore next 6 -- Logging and warning only */
9492
} else if (debug) {
9593
console.info(`Skipped (no conflicts): ${fullPath}`);

0 commit comments

Comments
 (0)