Skip to content

Commit da0629b

Browse files
committed
feat: enhance merge processing and debugging capabilities
1 parent 27daef2 commit da0629b

File tree

9 files changed

+117
-82
lines changed

9 files changed

+117
-82
lines changed

.changeset/tame-pumas-drop.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
---
2+
"git-json-resolver": patch
23
---
4+
5+
Improve debugging and 3-way merge support
6+
7+
- Add base conflict resolution support in merge processing
8+
- Enhance logger with debug-aware level configuration
9+
- Add default backup directory parameter to backupFile utility
10+
- Improve debug logging with structured conflict data

lib/src/index.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,6 @@ describe("resolveConflicts", () => {
142142
mockMergeObject.mockResolvedValue({});
143143

144144
await resolveConflicts(config);
145-
146-
expect(mockLogger.info).toHaveBeenCalledWith(
147-
"all",
148-
expect.stringContaining("normalizedConfig"),
149-
);
150-
expect(mockLogger.debug).toHaveBeenCalledWith("test.json", expect.stringContaining("merged"));
151145
});
152146

153147
it("processes multiple files concurrently", async () => {

lib/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export * from "./types";
1010
export const resolveConflicts = async <T extends string = InbuiltMergeStrategies>(
1111
config: Config<T>,
1212
) => {
13-
const globalLogger = await createLogger(config.loggerConfig);
13+
const globalLogger = await createLogger(config.loggerConfig, config.debug);
1414
const normalizedConfig: NormalizedConfig = await normalizeConfig<T>(config);
1515
const filesEntries = await listMatchingFiles(normalizedConfig);
1616
await Promise.all(

lib/src/logger.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,14 @@ describe("createLogger", async () => {
6363
});
6464

6565
it("writes to single file when singleFile is true", async () => {
66-
const logger = await createLogger({
67-
mode: "memory",
68-
logDir: TEST_LOG_DIR,
69-
singleFile: true,
70-
});
66+
const logger = await createLogger(
67+
{
68+
mode: "memory",
69+
logDir: TEST_LOG_DIR,
70+
singleFile: true,
71+
},
72+
true,
73+
);
7174

7275
logger.info("file1", "message1");
7376
logger.info("file2", "message2");
@@ -89,7 +92,7 @@ describe("createLogger", async () => {
8992
logDir: TEST_LOG_DIR,
9093
});
9194

92-
logger.info("test", "stream message");
95+
logger.error("test", "stream message");
9396
await logger.flush();
9497

9598
const files = fs.readdirSync(TEST_LOG_DIR);

lib/src/merge-processor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const resolveGitMergeFiles = async <T extends string = InbuiltMergeStrate
9696
theirsPath: string,
9797
config: Config<T> = {} as Config<T>,
9898
) => {
99-
const globalLogger = await createLogger(config.loggerConfig);
99+
const globalLogger = await createLogger(config.loggerConfig, config.debug);
100100
const normalizedConfig: NormalizedConfig = await normalizeConfig<T>(config);
101101

102102
if (normalizedConfig.debug) {

lib/src/merger.test.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ vi.mock("./strategy-resolver", () => ({
77
resolveStrategies: vi.fn(() => ["ours", "theirs", "merge"]),
88
}));
99
import { resolveStrategies } from "./strategy-resolver";
10-
import { StrategyStatus } from "./types";
10+
import {
11+
StrategyStatus_OK,
12+
StrategyStatus_CONTINUE,
13+
StrategyStatus_FAIL,
14+
StrategyStatus_SKIP,
15+
} from "./utils";
1116

1217
const makeCtx = (): MergeContext => ({
1318
config: { debug: false } as any,
@@ -17,10 +22,10 @@ const makeCtx = (): MergeContext => ({
1722

1823
describe("statusToString", () => {
1924
it("maps known statuses", () => {
20-
expect(statusToString(StrategyStatus.OK)).toBe("OK");
21-
expect(statusToString(StrategyStatus.CONTINUE)).toBe("CONTINUE");
22-
expect(statusToString(StrategyStatus.FAIL)).toBe("FAIL");
23-
expect(statusToString(StrategyStatus.SKIP)).toBe("SKIP");
25+
expect(statusToString(StrategyStatus_OK)).toBe("OK");
26+
expect(statusToString(StrategyStatus_CONTINUE)).toBe("CONTINUE");
27+
expect(statusToString(StrategyStatus_FAIL)).toBe("FAIL");
28+
expect(statusToString(StrategyStatus_SKIP)).toBe("SKIP");
2429
// @ts-expect-error -- testing
2530
expect(statusToString(999)).toMatch(/UNKNOWN/);
2631
});
@@ -32,17 +37,17 @@ describe("BuiltInStrategies", () => {
3237

3338
it("ours returns ours", () => {
3439
const r = BuiltInStrategies.ours(args);
35-
expect(r).toEqual({ status: StrategyStatus.OK, value: 1 });
40+
expect(r).toEqual({ status: StrategyStatus_OK, value: 1 });
3641
});
3742

3843
it("theirs returns theirs", () => {
3944
const r = BuiltInStrategies.theirs(args);
40-
expect(r).toEqual({ status: StrategyStatus.OK, value: 2 });
45+
expect(r).toEqual({ status: StrategyStatus_OK, value: 2 });
4146
});
4247

4348
it("base returns base", () => {
4449
const r = BuiltInStrategies.base(args);
45-
expect(r).toEqual({ status: StrategyStatus.OK, value: 0 });
50+
expect(r).toEqual({ status: StrategyStatus_OK, value: 0 });
4651
});
4752

4853
it("drop returns DROP symbol", () => {
@@ -53,7 +58,7 @@ describe("BuiltInStrategies", () => {
5358

5459
it("skip returns SKIP", () => {
5560
const r = BuiltInStrategies.skip(args);
56-
expect(r.status).toBe(StrategyStatus.SKIP);
61+
expect(r.status).toBe(StrategyStatus_SKIP);
5762
// @ts-expect-error -- will fix later
5863
expect(r.reason).toMatch(/Skip/);
5964
});
@@ -70,7 +75,7 @@ describe("BuiltInStrategies", () => {
7075
BuiltInStrategies["non-empty"]({ ...args, ours: "", theirs: "", base: "base" }).value,
7176
).toBe("base");
7277
expect(BuiltInStrategies["non-empty"]({ ...args, ours: "", theirs: "", base: "" }).status).toBe(
73-
StrategyStatus.CONTINUE,
78+
StrategyStatus_CONTINUE,
7479
);
7580
});
7681

@@ -93,14 +98,14 @@ describe("BuiltInStrategies", () => {
9398
path: "obj",
9499
};
95100
const r = await BuiltInStrategies.merge(objArgs);
96-
expect(r.status).toBe(StrategyStatus.OK);
101+
expect(r.status).toBe(StrategyStatus_OK);
97102
// @ts-expect-error -- will fix later
98103
expect(r.value).toEqual({ a: 1 });
99104
});
100105

101106
it("merge unmergeable types → CONTINUE", async () => {
102107
const r = await BuiltInStrategies.merge({ ...args, ours: 1, theirs: "str" });
103-
expect(r.status).toBe(StrategyStatus.CONTINUE);
108+
expect(r.status).toBe(StrategyStatus_CONTINUE);
104109
});
105110
});
106111

lib/src/merger.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { NormalizedConfig } from "./normalizer";
22
import { resolveStrategies } from "./strategy-resolver";
33
import { StrategyFn, StrategyResult, StrategyStatus } from "./types";
4-
import { DROP } from "./utils";
4+
import {
5+
DROP,
6+
StrategyStatus_OK,
7+
StrategyStatus_CONTINUE,
8+
StrategyStatus_FAIL,
9+
StrategyStatus_SKIP,
10+
} from "./utils";
511

612
/** Conflict entry (minimal by default). */
713
export interface Conflict {
@@ -23,13 +29,13 @@ export interface MergeResult {
2329
/** Helper: stringify status for logs. */
2430
export const statusToString = (s: StrategyStatus): string => {
2531
switch (s) {
26-
case StrategyStatus.OK:
32+
case StrategyStatus_OK:
2733
return "OK";
28-
case StrategyStatus.CONTINUE:
34+
case StrategyStatus_CONTINUE:
2935
return "CONTINUE";
30-
case StrategyStatus.FAIL:
36+
case StrategyStatus_FAIL:
3137
return "FAIL";
32-
case StrategyStatus.SKIP:
38+
case StrategyStatus_SKIP:
3339
return "SKIP";
3440
default:
3541
return `UNKNOWN(${s})`;
@@ -62,40 +68,40 @@ const isPlainObject = (val: unknown): val is Record<string, unknown> =>
6268
/** Built-in strategies. */
6369
export const BuiltInStrategies = {
6470
ours: <TContext>({ ours }: MergeArgs<TContext>): StrategyResult => ({
65-
status: StrategyStatus.OK,
71+
status: StrategyStatus_OK,
6672
value: ours,
6773
}),
6874

6975
theirs: <TContext>({ theirs }: MergeArgs<TContext>): StrategyResult => ({
70-
status: StrategyStatus.OK,
76+
status: StrategyStatus_OK,
7177
value: theirs,
7278
}),
7379

7480
base: <TContext>({ base }: MergeArgs<TContext>): StrategyResult => ({
75-
status: StrategyStatus.OK,
81+
status: StrategyStatus_OK,
7682
value: base,
7783
}),
7884

7985
drop: <TContext>(_skipped: MergeArgs<TContext>): StrategyResult => ({
80-
status: StrategyStatus.OK,
86+
status: StrategyStatus_OK,
8187
value: DROP,
8288
}),
8389

8490
skip: <TContext>({ path }: MergeArgs<TContext>): StrategyResult => ({
85-
status: StrategyStatus.SKIP,
91+
status: StrategyStatus_SKIP,
8692
reason: `Skip strategy applied at ${path}`,
8793
}),
8894

8995
"non-empty": <TContext>({ ours, theirs, base }: MergeArgs<TContext>): StrategyResult => {
90-
if (ours != null && ours !== "") return { status: StrategyStatus.OK, value: ours };
91-
if (theirs != null && theirs !== "") return { status: StrategyStatus.OK, value: theirs };
92-
if (base != null && base !== "") return { status: StrategyStatus.OK, value: base };
93-
return { status: StrategyStatus.CONTINUE };
96+
if (ours != null && ours !== "") return { status: StrategyStatus_OK, value: ours };
97+
if (theirs != null && theirs !== "") return { status: StrategyStatus_OK, value: theirs };
98+
if (base != null && base !== "") return { status: StrategyStatus_OK, value: base };
99+
return { status: StrategyStatus_CONTINUE };
94100
},
95101

96102
update: <TContext>({ ours, theirs }: MergeArgs<TContext>): StrategyResult => {
97-
if (ours !== undefined) return { status: StrategyStatus.OK, value: theirs };
98-
return { status: StrategyStatus.OK, value: DROP };
103+
if (ours !== undefined) return { status: StrategyStatus_OK, value: theirs };
104+
return { status: StrategyStatus_OK, value: DROP };
99105
},
100106

101107
merge: async <TContext>(args: MergeArgs<TContext>): Promise<StrategyResult> => {
@@ -116,24 +122,24 @@ export const BuiltInStrategies = {
116122
conflicts,
117123
});
118124
}
119-
return { status: StrategyStatus.OK, value: result };
125+
return { status: StrategyStatus_OK, value: result };
120126
}
121127

122-
return { status: StrategyStatus.CONTINUE, reason: "Unmergeable type" };
128+
return { status: StrategyStatus_CONTINUE, reason: "Unmergeable type" };
123129
},
124130

125131
concat: <TContext>({ ours, theirs, path }: MergeArgs<TContext>): StrategyResult => {
126132
if (Array.isArray(ours) && Array.isArray(theirs)) {
127-
return { status: StrategyStatus.OK, value: [...ours, ...theirs] };
133+
return { status: StrategyStatus_OK, value: [...ours, ...theirs] };
128134
}
129-
return { status: StrategyStatus.CONTINUE, reason: `Cannot concat at ${path}` };
135+
return { status: StrategyStatus_CONTINUE, reason: `Cannot concat at ${path}` };
130136
},
131137

132138
unique: <TContext>({ ours, theirs, path }: MergeArgs<TContext>): StrategyResult => {
133139
if (Array.isArray(ours) && Array.isArray(theirs)) {
134-
return { status: StrategyStatus.OK, value: [...new Set([...ours, ...theirs])] };
140+
return { status: StrategyStatus_OK, value: [...new Set([...ours, ...theirs])] };
135141
}
136-
return { status: StrategyStatus.CONTINUE, reason: `Cannot concat at ${path}` };
142+
return { status: StrategyStatus_CONTINUE, reason: `Cannot concat at ${path}` };
137143
},
138144
} as const;
139145

@@ -185,14 +191,14 @@ export const mergeObject = async <TContext>({
185191
});
186192

187193
switch (result.status) {
188-
case StrategyStatus.OK:
194+
case StrategyStatus_OK:
189195
return result.value;
190-
case StrategyStatus.CONTINUE:
196+
case StrategyStatus_CONTINUE:
191197
continue;
192-
case StrategyStatus.SKIP:
198+
case StrategyStatus_SKIP:
193199
conflicts.push({ path, reason: result.reason });
194200
return undefined;
195-
case StrategyStatus.FAIL:
201+
case StrategyStatus_FAIL:
196202
conflicts.push({ path, reason: result.reason });
197203
throw new Error(`Merge failed at ${path}: ${result.reason}`);
198204
}

lib/src/types.ts

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { Matcher } from "./matcher";
2+
import {
3+
StrategyStatus_OK,
4+
StrategyStatus_CONTINUE,
5+
StrategyStatus_FAIL,
6+
StrategyStatus_SKIP,
7+
} from "./utils";
28

39
/**
410
* Built-in merge strategies.
@@ -29,31 +35,20 @@ export type InbuiltMergeStrategies =
2935
| "concat"
3036
| "unique";
3137

32-
/**
33-
* Status codes returned by strategy functions.
34-
*/
35-
export enum StrategyStatus {
36-
/** Strategy successfully produced a value. */
37-
OK = 0,
38-
39-
/** Strategy deferred — let other strategies continue. */
40-
CONTINUE = 1,
41-
42-
/** Strategy failed — unrecoverable error, stop processing. */
43-
FAIL = 2,
44-
45-
/** Strategy explicitly skipped this field. */
46-
SKIP = 3,
47-
}
38+
export type StrategyStatus =
39+
| typeof StrategyStatus_OK
40+
| typeof StrategyStatus_CONTINUE
41+
| typeof StrategyStatus_FAIL
42+
| typeof StrategyStatus_SKIP;
4843

4944
/**
5045
* Union type representing the outcome of a strategy function.
5146
*/
5247
export type StrategyResult =
53-
| { status: StrategyStatus.OK; value: unknown }
54-
| { status: StrategyStatus.CONTINUE; reason?: string }
55-
| { status: StrategyStatus.SKIP; reason: string }
56-
| { status: StrategyStatus.FAIL; reason: string };
48+
| { status: typeof StrategyStatus_OK; value: unknown }
49+
| { status: typeof StrategyStatus_CONTINUE; reason?: string }
50+
| { status: typeof StrategyStatus_SKIP; reason: string }
51+
| { status: typeof StrategyStatus_FAIL; reason: string };
5752

5853
/**
5954
* Strategy function signature.
@@ -107,6 +102,29 @@ export interface PluginStrategies {}
107102
*/
108103
export type AllStrategies = InbuiltMergeStrategies | keyof PluginStrategies;
109104

105+
/**
106+
* Interface for plugin-specific configuration.
107+
* Plugins augment this interface with their own config types.
108+
*
109+
* Example plugin declaration:
110+
* ```ts
111+
* declare module "git-json-resolver" {
112+
* interface PluginConfigs {
113+
* "git-json-resolver-semver": {
114+
* strict?: boolean;
115+
* fallback?: "ours" | "theirs" | "continue" | "error";
116+
* preferValid?: boolean;
117+
* preferRange?: boolean;
118+
* workspacePattern?: string;
119+
* };
120+
* }
121+
* }
122+
* ```
123+
*/
124+
export interface PluginConfigs {
125+
[pluginName: string]: Record<string, unknown>;
126+
}
127+
110128
/**
111129
* Rule tree: maps field glob patterns → strategy names or nested rule trees.
112130
*
@@ -160,15 +178,7 @@ export interface StrategyPlugin<TContext = unknown> {
160178
strategies: Record<string, StrategyFn<TContext>>;
161179

162180
/** Optional plugin initialization with config. */
163-
init?(config: PluginConfig): void | Promise<void>;
164-
}
165-
166-
/**
167-
* Plugin configuration passed to plugin.init().
168-
*/
169-
export interface PluginConfig {
170-
/** Custom plugin-specific configuration. */
171-
[key: string]: unknown;
181+
init?(config: PluginConfigs[keyof PluginConfigs]): void | Promise<void>;
172182
}
173183

174184
/**
@@ -240,7 +250,7 @@ export interface Config<T extends string = AllStrategies, TContext = unknown> {
240250
plugins?: string[];
241251

242252
/** Plugin-specific configuration passed to plugin.init(). */
243-
pluginConfig?: Record<string, PluginConfig>;
253+
pluginConfig?: Partial<PluginConfigs>;
244254
}
245255

246256
export type { Matcher };

0 commit comments

Comments
 (0)