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 CHANGED
@@ -1,3 +1,38 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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);
@@ -0,0 +1,13 @@
1
+ {
2
+ "versions": [
3
+ {
4
+ "version": "*",
5
+ "target": {
6
+ "node": "supported"
7
+ },
8
+ "response": {
9
+ "type": "time-permitting"
10
+ }
11
+ }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,27 @@
1
1
  {
2
2
  "name": "smart-merge-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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,7 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const err = chalk.bold.red;
4
+ export const warn = chalk.yellow;
5
+ export const info = chalk.blue;
6
+ export const success = chalk.green;
7
+ export const debug = chalk.magenta;
@@ -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
+ }
@@ -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
+ }