Skip to content

Commit 81d71ee

Browse files
committed
feat(mcl): Implement dev_commit command
1 parent a26a713 commit 81d71ee

File tree

7 files changed

+311
-7
lines changed

7 files changed

+311
-7
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- uses: actions/checkout@v4
3838

3939
- name: Build and test the `mcl` command
40-
run: nix develop -c sh -c "dub test --root packages/mcl -- -e 'fetchJson|(coda\.)|nix.run|nix.build'"
40+
run: nix develop -c sh -c "dub test --root packages/mcl --compiler ldc2 -- -e 'fetchJson|(coda\.)|nix.run|nix.build'"
4141

4242
ci:
4343
uses: ./.github/workflows/reusable-flake-checks-ci-matrix.yml

packages/mcl/dub.sdl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ buildType "unittest-debug" {
1515
}
1616

1717
dflags "-preview=in"
18-
dflags "-preview=shortenedMethods"
19-
dflags "-defaultlib=libphobos2.so" platform="dmd"
2018
lflags "-fuse-ld=gold" platform="dmd"
2119
dflags "-mcpu=generic" platform="ldc"
2220
dflags "-mcpu=baseline" platform="dmd"

packages/mcl/src/main.d

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ alias supportedCommands = imported!`std.traits`.AliasSeq!(
1616
cmds.shard_matrix,
1717
cmds.host_info,
1818
cmds.ci,
19-
cmds.machine_create
19+
cmds.machine_create,
20+
cmds.dev_commit
2021
);
2122

2223
int main(string[] args)
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
module mcl.commands.dev_commit;
2+
3+
import std.algorithm : any, cache, canFind, filter, find, map, sort, startsWith, uniq;
4+
import std.array : appender, array, assocArray, front, join, replace, split;
5+
import std.conv : to;
6+
import std.file : dirEntries, exists, readText, SpanMode;
7+
import std.json : JSONOptions, parseJSON;
8+
import std.parallelism : parallel, taskPool;
9+
import std.path : globMatch, stripExtension;
10+
import std.process : ProcessPipes, wait;
11+
import std.regex : ctRegex, match, Regex, regex, replaceAll, replaceFirst;
12+
import std.stdio : writeln;
13+
import std.string : indexOf, startsWith, strip;
14+
import std.typecons : tuple;
15+
import mcl.utils.env : parseEnv, optional;
16+
import mcl.utils.process : execute;
17+
import mcl.utils.path : rootDir;
18+
import mcl.utils.log : prompt;
19+
import mcl.utils.json : fromJSON;
20+
21+
string[] modifiedFiles = [];
22+
static const enum CommitType
23+
{
24+
feat,
25+
fix,
26+
refactor,
27+
ci,
28+
docs,
29+
style,
30+
config,
31+
build,
32+
chore,
33+
perf,
34+
test
35+
}
36+
37+
struct Config
38+
{
39+
struct Exclude
40+
{
41+
string[] startsWith = [];
42+
string[] contains = [".gitkeep"];
43+
string[] equals = [
44+
"src", "packages", "pkg", "pkgs", "apps", "libs", "modules",
45+
"services", ".git"
46+
];
47+
}
48+
49+
struct Scope
50+
{
51+
string[string] replaceAll = [
52+
"(src|packages|pkg|pkgs|apps|libs|modules|services)/": "",
53+
"mcl/mcl/": "mcl/",
54+
"mcl/commands/": "mcl/",
55+
"(docs.*/)?(pages/)?docs/": "docs/"
56+
];
57+
string[string] replaceFirst = [
58+
"^docs/": "",
59+
"^nix/": "",
60+
"/(default|main|index|start|app|init|__init__|entry|package)$": ""
61+
];
62+
}
63+
64+
struct Type
65+
{
66+
CommitType[string] equals = [
67+
".gitignore": CommitType.config,
68+
];
69+
CommitType[string] contains;
70+
CommitType[string] startsWith = [
71+
"docs": CommitType.docs,
72+
".github/": CommitType.ci,
73+
".gitlab/": CommitType.ci,
74+
75+
];
76+
}
77+
78+
Exclude exclude;
79+
Scope _scope;
80+
Type type;
81+
}
82+
83+
static Config config;
84+
85+
void initGitDiff()
86+
{
87+
auto status = execute("git diff --name-only --cached", false).split("\n")
88+
.map!(a => a.strip)
89+
.cache
90+
.filter!((a) {
91+
if (config.exclude.equals.canFind(a))
92+
return false;
93+
else if (config.exclude.contains.any!(c => a.indexOf(c) != -1))
94+
return false;
95+
else if (config.exclude.startsWith.any!(c => a.startsWith(c)))
96+
return false;
97+
else
98+
return true;
99+
})
100+
.array;
101+
if (status.length)
102+
{
103+
modifiedFiles = status
104+
.map!(a => stripExtension(a.strip)).array;
105+
writeln("Modified files (staged): ");
106+
writeln(status.map!(f => "> " ~ f).array.join("\n"));
107+
writeln("\n");
108+
}
109+
}
110+
111+
CommitType guessType()
112+
{
113+
if (modifiedFiles.length)
114+
{
115+
foreach (string file; modifiedFiles)
116+
{
117+
auto contains = config.type.contains.keys.find!(k => file.indexOf(k) != -1);
118+
auto startsWith = config.type.startsWith.keys.find!(k => file.startsWith(k));
119+
120+
if (config.type.equals.keys.canFind(file))
121+
{
122+
return config.type.equals[file];
123+
}
124+
else if (contains.length)
125+
{
126+
return config.type.contains[contains.front];
127+
}
128+
else if (startsWith.length)
129+
{
130+
return config.type.startsWith[startsWith.front];
131+
}
132+
}
133+
}
134+
return CommitType.feat;
135+
}
136+
137+
string[] guessScope()
138+
{
139+
140+
Regex!char[string] replaceAllRegexes = config._scope.replaceAll.keys.map!(
141+
key => tuple(key, regex(key, "g"))).assocArray;
142+
Regex!char[string] replaceFirstRegexes = config._scope.replaceFirst.keys.map!(
143+
key => tuple(key, regex(key, "g"))).assocArray;
144+
145+
auto files = modifiedFiles
146+
.map!((a) {
147+
foreach (i, value; config._scope.replaceAll)
148+
{
149+
a = a.replaceAll(replaceAllRegexes[i], value);
150+
}
151+
foreach (i, value; config._scope.replaceFirst)
152+
{
153+
a = a.replaceFirst(replaceFirstRegexes[i], value);
154+
}
155+
return a;
156+
}
157+
)
158+
.array
159+
.sort
160+
.uniq
161+
.array;
162+
return files;
163+
}
164+
165+
static immutable auto botRegex = ctRegex!(`(\[bot\]|dependabot|actions-bot)`);
166+
167+
string[] getAuthors()
168+
{
169+
auto authors = execute("git log --format='%aN' | sort -u", false).split("\n");
170+
return authors
171+
.filter!(a => !match(a, botRegex))
172+
.map!(a => a.strip)
173+
.array ~ [""];
174+
}
175+
176+
struct CommitParams
177+
{
178+
CommitType type;
179+
string _scope;
180+
string shortDescription;
181+
string description;
182+
bool isBreaking;
183+
string breaking;
184+
bool isIssue;
185+
int issue;
186+
string[] coAuthors;
187+
}
188+
189+
string createCommitMessage(CommitParams params)
190+
{
191+
auto strBuilder = appender!string;
192+
strBuilder.put(params.type.to!string);
193+
strBuilder.put("(");
194+
strBuilder.put(params._scope);
195+
strBuilder.put("): ");
196+
strBuilder.put(params.shortDescription);
197+
if (params.description.length)
198+
{
199+
strBuilder.put("\n\n");
200+
strBuilder.put(params.description);
201+
}
202+
if (params.isBreaking)
203+
{
204+
strBuilder.put("\n\nBREAKING CHANGE:");
205+
strBuilder.put(params.breaking);
206+
207+
}
208+
if (params.isIssue)
209+
{
210+
strBuilder.put("\n\nCloses #");
211+
strBuilder.put(params.issue.to!string);
212+
}
213+
if (params.coAuthors.length)
214+
{
215+
strBuilder.put("\n\nCo-authored-by: ");
216+
strBuilder.put(params.coAuthors.join(", "));
217+
}
218+
return strBuilder.toString();
219+
}
220+
221+
CommitParams promptCommitParams(bool automatic)
222+
{
223+
CommitParams commitParams;
224+
commitParams.type = automatic ? guessType
225+
: prompt!CommitType("Commit type (suggestion: " ~ guessType.to!string ~ ")");
226+
auto scopeSuggestion = guessScope;
227+
commitParams._scope = automatic ? scopeSuggestion.front
228+
: prompt!string(
229+
"Scope (suggestion: " ~ scopeSuggestion.join(", ") ~ ")");
230+
commitParams.shortDescription = automatic ? "" : prompt!string("Short Description");
231+
commitParams.description = automatic ? "" : prompt!string("Description");
232+
commitParams.isBreaking = automatic ? false : prompt!bool("Breaking change");
233+
commitParams.breaking = commitParams.isBreaking ? prompt!string("Breaking change description")
234+
: "";
235+
commitParams.isIssue = automatic ? false : prompt!bool(
236+
"Does this commit relate to an existing issue");
237+
if (commitParams.isIssue)
238+
{
239+
commitParams.issue = prompt!int("Issue number");
240+
}
241+
commitParams.coAuthors = automatic ? [] : prompt!string(
242+
"Co-authors (comma separated)", getAuthors).split(",").map!(a => a.strip)
243+
.cache
244+
.filter!(a => a != "")
245+
.array;
246+
return commitParams;
247+
}
248+
249+
export void dev_commit()
250+
{
251+
Params params = parseEnv!Params;
252+
253+
string mclFile = rootDir ~ "/.mcl.json";
254+
if (mclFile.exists)
255+
config = parseJSON(readText(mclFile), JSONOptions.none).fromJSON!Config;
256+
257+
initGitDiff();
258+
259+
CommitParams commitParams = promptCommitParams(params.automatic);
260+
261+
writeln();
262+
string commitMessage = createCommitMessage(commitParams);
263+
writeln(commitMessage);
264+
writeln();
265+
266+
bool commit = prompt!bool("Commit?");
267+
if (commit)
268+
{
269+
auto pipes = execute!ProcessPipes("git commit -F -", false);
270+
pipes.stdin.writeln(commitMessage);
271+
pipes.stdin.flush();
272+
pipes.stdin.close();
273+
writeln(pipes.stdout.byLineCopy.array.join("\n"));
274+
writeln(pipes.stderr.byLineCopy.array.join("\n"));
275+
wait(pipes.pid);
276+
}
277+
}
278+
279+
struct Params
280+
{
281+
@optional() bool automatic = false;
282+
void setup()
283+
{
284+
}
285+
}

packages/mcl/src/src/mcl/commands/package.d

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ public import mcl.commands.shard_matrix : shard_matrix;
77
public import mcl.commands.ci : ci;
88
public import mcl.commands.host_info : host_info;
99
public import mcl.commands.machine_create : machine_create;
10+
public import mcl.commands.dev_commit : dev_commit;

packages/mcl/src/src/mcl/utils/log.d

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
module mcl.utils.log;
22

3-
T prompt(T)(string message, T[] options = [], string input = "unfilled")
3+
template MaybeArray(T)
44
{
5-
import std.stdio : write, writeln, readln;
5+
static if (is(immutable(T) == immutable(char[])))
6+
{
7+
alias MaybeArray = T[];
8+
}
9+
else static if (is(T == U[], U))
10+
{
11+
alias MaybeArray = T;
12+
}
13+
else
14+
{
15+
alias MaybeArray = T[];
16+
}
17+
18+
}
19+
20+
T prompt(T)(string message, MaybeArray!T options = [], string input = "unfilled")
21+
{
22+
import std.stdio : write, writeln, readln, stdin;
623
import std.string : strip;
724
import std.algorithm : canFind, map;
825
import std.conv : to;
@@ -30,7 +47,7 @@ T prompt(T)(string message, T[] options = [], string input = "unfilled")
3047
{
3148
input = readln().strip();
3249
}
33-
if (options.length && !options.canFind(input.to!T))
50+
if (options.length && !(options.to!(string[])).canFind(input))
3451
{
3552
writeln("Invalid input.");
3653
return prompt!T(message, options);

shells/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@
4141
]
4242
++ pkgs.lib.optionals (pkgs.stdenv.system == "x86_64-linux") [
4343
inputs'.dlang-nix.packages.dmd
44+
inputs'.dlang-nix.packages."ldc-binary-1_38_0"
4445
];
4546

4647
shellHook =
4748
''
4849
export REPO_ROOT="$PWD"
50+
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.curl.out}/lib"
4951
figlet -t "Metacraft Nixos Modules"
5052
''
5153
+ config.pre-commit.installationScript;

0 commit comments

Comments
 (0)