smart-merge-cli 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/eslint.config.js +20 -0
- package/index.js +72 -0
- package/package-support.json +13 -0
- package/package.json +25 -4
- package/src/commands/check.js +20 -0
- package/src/commands/explain.js +125 -0
- package/src/commands/history.js +43 -0
- package/src/commands/status.js +85 -0
- package/src/core/colors.js +7 -0
- package/src/core/git-commands.js +51 -0
- package/src/core/json.js +17 -0
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# SmartMerge
|
|
2
|
+
Your git cli's best friend.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## About
|
|
6
|
+
SmartMerge was made from the utter frustration of git merge errors. It is designed to help tracking down those pesky merge errors. It also has a `--json` flag that sends the output to machine readable output. And, it even comes with the `--no-color` flag for no reason at all.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
To use it, Well you will have to really fuck up your local git repository so much that rebase says `CONFLICT: ` or something. The SmartMerger can help you in this situation by telling you allmost all the info that git dosen't tell you. Because git is a tortuous and insuferable, No. I didn't say that. But anyway, SmartMerger allows you to see commits that are related to the file on your current branch, see an explaination for the curse git gave you and the repo status. Batterys included
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
`
|
|
13
|
+
smartmerge check .
|
|
14
|
+
`
|
|
15
|
+
This runs the checker in the local repo to find any conflicts and tell you the files they are in.
|
|
16
|
+
|
|
17
|
+
`
|
|
18
|
+
smartmerge status .
|
|
19
|
+
`
|
|
20
|
+
This command shows you the glorified git status.
|
|
21
|
+
|
|
22
|
+
`
|
|
23
|
+
smartmerge explain .
|
|
24
|
+
`
|
|
25
|
+
Tells you what is wrong and how to fix it
|
|
26
|
+
|
|
27
|
+
`
|
|
28
|
+
smartmerge history .
|
|
29
|
+
`
|
|
30
|
+
Shows you where it all went wrong.
|
|
31
|
+
|
|
32
|
+
## Keynote
|
|
33
|
+
|
|
34
|
+
Git created the problem.
|
|
35
|
+
SmartMerge helps explain it.
|
|
36
|
+
|
|
37
|
+
Built by one f***ed off developer
|
|
38
|
+
For everyone who's ever stared at CONFLICT (content) and wondered what on earth Git was on about.
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import { defineConfig } from "eslint/config";
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
{
|
|
7
|
+
files: ["**/*.{js,mjs,cjs}"],
|
|
8
|
+
plugins: { js },
|
|
9
|
+
extends: ["js/recommended"],
|
|
10
|
+
languageOptions: {
|
|
11
|
+
globals: globals.node,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
files: ["**/*.js"],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
sourceType: "module",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
]);
|
package/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { checkConflicts } from "./src/commands/check.js";
|
|
3
|
+
import { explainConflicts, renderExplain } from "./src/commands/explain.js";
|
|
4
|
+
import { repoStatus, renderStatus } from "./src/commands/status.js";
|
|
5
|
+
import { exportJson } from "./src/core/json.js";
|
|
6
|
+
import { repoHistory, renderRepoHistory } from "./src/commands/history.js";
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name("SmartMerger")
|
|
12
|
+
.description("A tool to help with merging of git conflicts")
|
|
13
|
+
.version("1.0.0")
|
|
14
|
+
.option("--no-color", "Output with no color")
|
|
15
|
+
.option("--json", "Output in machine readable json format");
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("check")
|
|
19
|
+
.description("Check for git conflicts")
|
|
20
|
+
.argument("<path>", "Path to the git repository")
|
|
21
|
+
.action((path) => {
|
|
22
|
+
checkConflicts(path);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command("explain")
|
|
27
|
+
.description("Explain git conflicts")
|
|
28
|
+
.argument("<path>", "Path to the git repository")
|
|
29
|
+
.action((path) => {
|
|
30
|
+
const options = program.opts();
|
|
31
|
+
const data = explainConflicts(path);
|
|
32
|
+
|
|
33
|
+
if (options.json) {
|
|
34
|
+
exportJson(data);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
renderExplain(data);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command("status")
|
|
43
|
+
.description("Show current git repository status")
|
|
44
|
+
.argument("<path>", "Path to the git repository")
|
|
45
|
+
.action((path) => {
|
|
46
|
+
const options = program.opts();
|
|
47
|
+
const data = repoStatus(path);
|
|
48
|
+
|
|
49
|
+
if (options.json) {
|
|
50
|
+
exportJson(data);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
renderStatus(data);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command("history")
|
|
58
|
+
.description("Show commit history for conflicting files")
|
|
59
|
+
.argument("<path>", "Path to the git repository")
|
|
60
|
+
.action((path) => {
|
|
61
|
+
const options = program.opts();
|
|
62
|
+
const data = repoHistory(path);
|
|
63
|
+
|
|
64
|
+
if (options.json) {
|
|
65
|
+
exportJson(data);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
renderRepoHistory(data);
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-merge-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A tool that helps with git merge conflicts",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git",
|
|
7
|
+
"merge"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "gtref",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "index.js",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"No Tests\"",
|
|
15
|
+
"lint": "eslint ."
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^6.0.1",
|
|
19
|
+
"commander": "^15.0.0",
|
|
20
|
+
"glob": "^13.0.6"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^10.0.1",
|
|
24
|
+
"eslint": "^10.13.0",
|
|
25
|
+
"globals": "^17.13.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getConflictFiles } from "../core/git-commands.js";
|
|
2
|
+
import { info } from "../core/colors.js";
|
|
3
|
+
|
|
4
|
+
export function checkConflicts(path) {
|
|
5
|
+
try {
|
|
6
|
+
const files = getConflictFiles(path);
|
|
7
|
+
|
|
8
|
+
if (files.length === 0) {
|
|
9
|
+
console.log(info("No merge conflicts found."));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
console.log(file);
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error(error.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { getConflictFiles } from "../core/git-commands.js";
|
|
3
|
+
import { info } from "../core/colors.js"
|
|
4
|
+
|
|
5
|
+
export function explainConflicts(path = ".") {
|
|
6
|
+
const files = getConflictFiles(path);
|
|
7
|
+
|
|
8
|
+
const results = [];
|
|
9
|
+
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const contents = fs.readFileSync(
|
|
12
|
+
`${path}/${file}`,
|
|
13
|
+
"utf8",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const lines = contents.split("\n");
|
|
17
|
+
|
|
18
|
+
let ours = [];
|
|
19
|
+
let theirs = [];
|
|
20
|
+
let state = "normal";
|
|
21
|
+
let conflictNumber = 0;
|
|
22
|
+
|
|
23
|
+
const conflicts = [];
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (line.startsWith("<<<<<<<")) {
|
|
27
|
+
conflictNumber++;
|
|
28
|
+
ours = [];
|
|
29
|
+
theirs = [];
|
|
30
|
+
state = "ours";
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (line.startsWith("=======")) {
|
|
35
|
+
state = "theirs";
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (line.startsWith(">>>>>>>")) {
|
|
40
|
+
conflicts.push({
|
|
41
|
+
number: conflictNumber,
|
|
42
|
+
identical:
|
|
43
|
+
ours.join("\n") === theirs.join("\n"),
|
|
44
|
+
current: {
|
|
45
|
+
lineCount: ours.length,
|
|
46
|
+
content: ours,
|
|
47
|
+
},
|
|
48
|
+
incoming: {
|
|
49
|
+
lineCount: theirs.length,
|
|
50
|
+
content: theirs,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
state = "normal";
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (state === "ours") {
|
|
59
|
+
ours.push(line);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (state === "theirs") {
|
|
63
|
+
theirs.push(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
results.push({
|
|
68
|
+
file,
|
|
69
|
+
conflicts,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
totalFiles: results.length,
|
|
75
|
+
files: results,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderExplain(data) {
|
|
80
|
+
if (!data.files.length) {
|
|
81
|
+
console.log(info("No merge conflicts found."));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const file of data.files) {
|
|
86
|
+
console.log(info(`\nFile ${file.file}`));
|
|
87
|
+
|
|
88
|
+
for (const conflict of file.conflicts) {
|
|
89
|
+
console.log(info(`\nConflict: #${conflict.number}`));
|
|
90
|
+
console.log(info("\nSummary:"));
|
|
91
|
+
|
|
92
|
+
if (conflict.identical) {
|
|
93
|
+
console.log(info("Both sides are identical"));
|
|
94
|
+
} else {
|
|
95
|
+
console.log(
|
|
96
|
+
info(
|
|
97
|
+
`- Current branch has ${conflict.current.lineCount} line(s).`,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
info(
|
|
103
|
+
`- Incoming branch has ${conflict.incoming.lineCount} line(s).`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
console.log(info("- The content differs."));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("\nCurrent:");
|
|
111
|
+
console.log(
|
|
112
|
+
conflict.current.content
|
|
113
|
+
.map((line) => ` ${line}`)
|
|
114
|
+
.join("\n"),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
console.log("\nIncoming:");
|
|
118
|
+
console.log(
|
|
119
|
+
conflict.incoming.content
|
|
120
|
+
.map((line) => ` ${line}`)
|
|
121
|
+
.join("\n"),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getHistory, getConflictFiles } from "../core/git-commands.js";
|
|
2
|
+
|
|
3
|
+
export function repoHistory(path) {
|
|
4
|
+
const files = getConflictFiles(path);
|
|
5
|
+
|
|
6
|
+
const data = [];
|
|
7
|
+
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const history = getHistory(file, path)
|
|
10
|
+
.split("\n")
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.map((entry) => {
|
|
13
|
+
const [hash, date, message] = entry.split("|");
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
hash,
|
|
17
|
+
date,
|
|
18
|
+
message,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
data.push({
|
|
23
|
+
file,
|
|
24
|
+
history,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderRepoHistory(data) {
|
|
32
|
+
for (const item of data) {
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(`Conflicting file: ${item.file}`);
|
|
35
|
+
console.log();
|
|
36
|
+
|
|
37
|
+
for (const commit of item.history) {
|
|
38
|
+
console.log(
|
|
39
|
+
` ${commit.hash.slice(0, 7)} ${commit.date} ${commit.message}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getStatus, getBranches } from "../core/git-commands.js";
|
|
2
|
+
import { info } from "../core/colors.js";
|
|
3
|
+
|
|
4
|
+
export function repoStatus(path = ".") {
|
|
5
|
+
const status = getStatus(path);
|
|
6
|
+
const branches = getBranches(path);
|
|
7
|
+
|
|
8
|
+
const files = status
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.map((line) => ({
|
|
12
|
+
index: line[0],
|
|
13
|
+
workingTree: line[1],
|
|
14
|
+
file: line.slice(3),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const rBranches = branches
|
|
18
|
+
.split("\n")
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map((line) => ({
|
|
21
|
+
name: line.replace("*", " ").trim(),
|
|
22
|
+
current: line.startsWith("*"),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
totalFiles: files.length,
|
|
27
|
+
files,
|
|
28
|
+
branches: rBranches
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getStatusText(index, workingTree) {
|
|
33
|
+
if (
|
|
34
|
+
index === "U" ||
|
|
35
|
+
workingTree === "U" ||
|
|
36
|
+
(index === "A" && workingTree === "A") ||
|
|
37
|
+
(index === "D" && workingTree === "D")
|
|
38
|
+
) {
|
|
39
|
+
return "! CONFLICT";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (index === "?" && workingTree === "?") {
|
|
43
|
+
return "Untracked";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (index === "A") {
|
|
47
|
+
return "+ Added";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (index === "M" || workingTree === "M") {
|
|
51
|
+
return "Modified";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (index === "D" || workingTree === "D") {
|
|
55
|
+
return "- Deleted";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (index === "R") {
|
|
59
|
+
return "Renamed";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return "Changed";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderStatus(data) {
|
|
66
|
+
if (data.totalFiles === 0) {
|
|
67
|
+
console.log(info("Working tree clean."));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(info(`Found ${data.totalFiles} changed file(s):\n`));
|
|
72
|
+
|
|
73
|
+
for (const file of data.files) {
|
|
74
|
+
console.log(
|
|
75
|
+
`${getStatusText(file.index, file.workingTree)}: ${file.file}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(info("\nBranches:"));
|
|
80
|
+
for (const branch of data.branches) {
|
|
81
|
+
console.log(
|
|
82
|
+
`${branch.current ? "*" : " "} ${branch.name}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {err} from "./colors.js";
|
|
3
|
+
|
|
4
|
+
function run(command, cwd = process.cwd()) {
|
|
5
|
+
try {
|
|
6
|
+
return execSync(command, {
|
|
7
|
+
cwd,
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
}).trim();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
const message = error.stderr?.toString().trim();
|
|
13
|
+
|
|
14
|
+
if (message.includes("Not a git repository")) {
|
|
15
|
+
console.log(err("Not a Git repository."));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(err(message || error.message));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getConflictFiles(path = process.cwd()) {
|
|
25
|
+
return run(
|
|
26
|
+
"git diff --name-only --diff-filter=U",
|
|
27
|
+
path,
|
|
28
|
+
)
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((file) => file.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getDiffCheck(path = process.cwd()) {
|
|
35
|
+
return run("git diff --check", path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getStatus(path = process.cwd()) {
|
|
39
|
+
return run("git status --short", path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getBranches(path = process.cwd()) {
|
|
43
|
+
return run("git branch --list", path);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getHistory(file, path = process.cwd()) {
|
|
47
|
+
return run(
|
|
48
|
+
`git log --format="%H|%ad|%s" --date=short -- "${file}"`,
|
|
49
|
+
path,
|
|
50
|
+
);
|
|
51
|
+
}
|
package/src/core/json.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export function exportJson(data) {
|
|
4
|
+
if (data === undefined) {
|
|
5
|
+
console.error(
|
|
6
|
+
chalk.red("Error: Command did not return any data to export."),
|
|
7
|
+
);
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(
|
|
13
|
+
chalk.yellow(
|
|
14
|
+
JSON.stringify(data, null, 2),
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
}
|