@nozich/git-mood 0.1.2

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 ADDED
@@ -0,0 +1,88 @@
1
+ # 🎨 git-mood
2
+
3
+ > Color your GitHub contribution graph with the mood of your git commits.
4
+
5
+ `git-mood` is a blazingly fast, zero-dependency local CLI tool written in Rust. It hooks into your git workflow, analyzes the sentiment of your commit messages and diff stats in real time, and logs your daily coding "mood".
6
+
7
+ Using a local database, it generates a beautiful, custom-colored contribution graph SVG (shining with **Turquoise**, **Red**, **Yellow**, **Purple**, and **Green**) that you can embed directly in your GitHub Profile README.
8
+
9
+ ---
10
+
11
+ ## 🎭 The Mood Map
12
+
13
+ | Color | Mood | Trigger Rule |
14
+ | :--- | :--- | :--- |
15
+ | **🔵 Turquoise** | **Productive & Happy** | Commits with positive keywords (`feat`, `add`, `awesome`, `super`, `love`, `success`) |
16
+ | **🔴 Red** | **Bug-Fixing / Stressed** | Commits containing bugs, fails, crashes, errors, or swear words |
17
+ | **🟡 Yellow** | **Cleanups & Refactoring** | Commits with refactor keywords OR where deletions represent $\ge 70\%$ of the total diff |
18
+ | **🟣 Purple** | **Weekend Warrior** | Commits made on Saturday or Sunday |
19
+ | **🟢 Green** | **Steady Progress** | Default / Neutral commits (chores, fixes, docs) |
20
+
21
+ ---
22
+
23
+ ## 🚀 Key Features
24
+
25
+ - **Blazingly Fast**: Compiled binary in Rust. Starts and finishes in less than 2 milliseconds.
26
+ - **Privacy First**: Fully local. No external AI APIs, no trackers, no network requests during commit.
27
+ - **Smart Diff Parsing**: Dynamically calculates refactoring mood when you delete more code than you write.
28
+ - **Sleek Dark Mode SVG**: Generates a premium, responsive widget that matches GitHub's dark theme perfectly.
29
+
30
+ ---
31
+
32
+ ## 📦 Installation
33
+
34
+ Download the precompiled binary for your system from the [Latest Releases](https://github.com/anilcan-kara/git-mood/releases):
35
+
36
+ - **macOS (Apple Silicon)**: [git-mood-darwin-arm64](https://github.com/anilcan-kara/git-mood/releases/latest/download/git-mood-darwin-arm64)
37
+ - **macOS (Intel)**: [git-mood-darwin-x64](https://github.com/anilcan-kara/git-mood/releases/latest/download/git-mood-darwin-x64)
38
+ - **Linux (x64)**: [git-mood-linux-x64](https://github.com/anilcan-kara/git-mood/releases/latest/download/git-mood-linux-x64)
39
+ - **Linux (ARM64)**: [git-mood-linux-arm64](https://github.com/anilcan-kara/git-mood/releases/latest/download/git-mood-linux-arm64)
40
+ - **Windows (x64)**: [git-mood-win32-x64.exe](https://github.com/anilcan-kara/git-mood/releases/latest/download/git-mood-win32-x64.exe)
41
+
42
+ Move the downloaded binary to a folder in your system `$PATH` (e.g. `/usr/local/bin` or `%USERPROFILE%\AppData\Local\Microsoft\WindowsApps`) and rename it to `git-mood`.
43
+
44
+ ---
45
+
46
+ ## 🛠️ Usage
47
+
48
+ ### 1. Initialize hook in your project
49
+ Navigate to any git repository and initialize the commit-msg hook:
50
+ ```bash
51
+ git-mood init
52
+ ```
53
+
54
+ ### 2. Write code & commit
55
+ Work on your code as usual. When you commit, `git-mood` automatically analyzes the message and prints live feedback:
56
+ ```bash
57
+ git commit -m "feat: add beautiful dashboard ui"
58
+
59
+ # Output:
60
+ # [git-mood] Sentiment Analyzed: Positive (Turquoise) 🚀
61
+ # Diff: +142 / -12 lines
62
+ ```
63
+
64
+ ### 3. Check your mood dashboard
65
+ See a weekly breakdown and stats directly in your terminal:
66
+ ```bash
67
+ git-mood status
68
+ ```
69
+
70
+ ### 4. Sync to your GitHub Profile
71
+ To show your custom mood contribution graph on your GitHub Profile:
72
+
73
+ 1. Clone your special **GitHub Profile Repository** (the repo with the same name as your username, e.g., `github.com/username/username`).
74
+ 2. Run `git-mood sync` inside that repository:
75
+ ```bash
76
+ git-mood sync
77
+ ```
78
+ This will generate a `git-mood.svg` file, automatically commit it, and push it to your remote repo!
79
+ 3. Add the following line to your Profile `README.md`:
80
+ ```markdown
81
+ ![git-mood](https://raw.githubusercontent.com/<your-username>/<your-username>/main/git-mood.svg)
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 🛡️ License
87
+
88
+ MIT License. See [LICENSE](LICENSE) for details.
package/bin/cli.js ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+ const { spawn } = require('child_process');
7
+
8
+ const VERSION = '0.1.2';
9
+ const REPO = 'anilcan-kara/git-mood';
10
+
11
+ const platform = process.platform;
12
+ const arch = process.arch;
13
+
14
+ let osKey = '';
15
+ let ext = '';
16
+
17
+ if (platform === 'win32') {
18
+ osKey = 'win32';
19
+ ext = '.exe';
20
+ } else if (platform === 'darwin') {
21
+ osKey = 'darwin';
22
+ } else if (platform === 'linux') {
23
+ osKey = 'linux';
24
+ } else {
25
+ console.error(`[git-mood] Unsupported platform: ${platform}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ let archKey = '';
30
+ if (arch === 'x64') {
31
+ archKey = 'x64';
32
+ } else if (arch === 'arm64') {
33
+ archKey = 'arm64';
34
+ } else {
35
+ console.error(`[git-mood] Unsupported architecture: ${arch}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const binaryName = `git-mood-${osKey}-${archKey}${ext}`;
40
+ const targetDir = path.join(__dirname, '..', 'dist');
41
+ const binaryPath = path.join(targetDir, `git-mood${ext}`);
42
+ const downloadUrl = `https://github.com/${REPO}/releases/download/v${VERSION}/${binaryName}`;
43
+
44
+ function downloadFile(url, dest, callback) {
45
+ https.get(url, (res) => {
46
+ if (res.statusCode === 302 || res.statusCode === 301) {
47
+ downloadFile(res.headers.location, dest, callback);
48
+ return;
49
+ }
50
+ if (res.statusCode !== 200) {
51
+ callback(new Error(`Failed to download binary: HTTP ${res.statusCode}`));
52
+ return;
53
+ }
54
+ const file = fs.createWriteStream(dest);
55
+ res.pipe(file);
56
+ file.on('finish', () => {
57
+ file.close(callback);
58
+ });
59
+ }).on('error', (err) => {
60
+ fs.unlink(dest, () => {});
61
+ callback(err);
62
+ });
63
+ }
64
+
65
+ fnRunBinary = () => {
66
+ const args = process.argv.slice(2);
67
+ const child = spawn(binaryPath, args, { stdio: 'inherit' });
68
+
69
+ child.on('close', (code) => {
70
+ process.exit(code === null ? 1 : code);
71
+ });
72
+
73
+ child.on('error', (err) => {
74
+ console.error(`[git-mood] Failed to start binary:`, err);
75
+ process.exit(1);
76
+ });
77
+ };
78
+
79
+ if (fs.existsSync(binaryPath)) {
80
+ fnRunBinary();
81
+ } else {
82
+ console.log(`\x1b[36m[git-mood]\x1b[0m Downloading platform binary v${VERSION} (${osKey}-${archKey})...`);
83
+
84
+ if (!fs.existsSync(targetDir)) {
85
+ fs.mkdirSync(targetDir, { recursive: true });
86
+ }
87
+
88
+ downloadFile(downloadUrl, binaryPath, (err) => {
89
+ if (err) {
90
+ console.error(`\x1b[31m[git-mood] Download failed:\x1b[0m`, err.message);
91
+ console.error(`Please download git-mood manually from https://github.com/${REPO}/releases`);
92
+ process.exit(1);
93
+ }
94
+
95
+ if (platform !== 'win32') {
96
+ try {
97
+ fs.chmodSync(binaryPath, 0755);
98
+ } catch (chmodErr) {
99
+ console.warn(`[git-mood] Failed to set permissions:`, chmodErr);
100
+ }
101
+ }
102
+
103
+ console.log(`\x1b[32m[git-mood] Binary downloaded successfully!\x1b[0m\n`);
104
+ fnRunBinary();
105
+ });
106
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@nozich/git-mood",
3
+ "version": "0.1.2",
4
+ "description": "Color your GitHub contribution graph with the mood of your git commits",
5
+ "main": "bin/cli.js",
6
+ "bin": {
7
+ "git-mood": "./bin/cli.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/anilcan-kara/git-mood.git"
12
+ },
13
+ "keywords": [
14
+ "git",
15
+ "mood",
16
+ "contribution",
17
+ "github",
18
+ "profile",
19
+ "visualization",
20
+ "sentiment",
21
+ "rust"
22
+ ],
23
+ "author": "Anilcan Kara",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/anilcan-kara/git-mood/issues"
27
+ },
28
+ "homepage": "https://github.com/anilcan-kara/git-mood#readme",
29
+ "engines": {
30
+ "node": ">=14"
31
+ }
32
+ }
package/src/hook.rs ADDED
@@ -0,0 +1,70 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ #[cfg(unix)]
6
+ use std::os::unix::fs::PermissionsExt;
7
+
8
+ pub fn install_hook() -> Result<(), String> {
9
+ // Find git repo root using git rev-parse
10
+ let output = Command::new("git")
11
+ .args(["rev-parse", "--show-toplevel"])
12
+ .output()
13
+ .map_err(|e| format!("Failed to run git: {}", e))?;
14
+
15
+ if !output.status.success() {
16
+ return Err("Not inside a git repository".to_string());
17
+ }
18
+
19
+ let repo_root_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
20
+ let repo_root = Path::new(&repo_root_str);
21
+ let hook_dir = repo_root.join(".git").join("hooks");
22
+
23
+ if !hook_dir.exists() {
24
+ fs::create_dir_all(&hook_dir)
25
+ .map_err(|e| format!("Failed to create hooks directory: {}", e))?;
26
+ }
27
+
28
+ let hook_path = hook_dir.join("commit-msg");
29
+
30
+ // Get current executable path
31
+ let current_exe = std::env::current_exe()
32
+ .map_err(|e| format!("Failed to get current executable path: {}", e))?;
33
+ let exe_path_str = current_exe.to_string_lossy().replace('\\', "/");
34
+
35
+ // Check if there is already a hook and back it up
36
+ if hook_path.exists() {
37
+ let existing_content = fs::read_to_string(&hook_path).unwrap_or_default();
38
+ if existing_content.contains("git-mood") {
39
+ println!("git-mood hook already installed in this repository.");
40
+ return Ok(());
41
+ }
42
+ let backup_path = hook_path.with_extension("backup");
43
+ fs::copy(&hook_path, &backup_path)
44
+ .map_err(|e| format!("Failed to back up existing hook: {}", e))?;
45
+ println!("Existing commit-msg hook backed up to {:?}", backup_path);
46
+ }
47
+
48
+ // Shell script template (works on Windows via Git Bash and Unix natively)
49
+ let hook_content = format!(
50
+ "#!/bin/sh\n\n# git-mood commit-msg hook\n\"{}\" commit-hook \"$1\"\n",
51
+ exe_path_str
52
+ );
53
+
54
+ fs::write(&hook_path, hook_content)
55
+ .map_err(|e| format!("Failed to write hook file: {}", e))?;
56
+
57
+ // Make the hook executable on Unix
58
+ #[cfg(unix)]
59
+ {
60
+ let mut perms = fs::metadata(&hook_path)
61
+ .map_err(|e| format!("Failed to get hook metadata: {}", e))?
62
+ .permissions();
63
+ perms.set_mode(0o755);
64
+ fs::set_permissions(&hook_path, perms)
65
+ .map_err(|e| format!("Failed to set hook permissions: {}", e))?;
66
+ }
67
+
68
+ println!("git-mood hook successfully installed at {:?}", hook_path);
69
+ Ok(())
70
+ }
package/src/main.rs ADDED
@@ -0,0 +1,264 @@
1
+ mod sentiment;
2
+ mod hook;
3
+ mod svg;
4
+
5
+ use std::collections::HashMap;
6
+ use std::env;
7
+ use std::fs;
8
+ use std::path::{Path, PathBuf};
9
+ use std::process::Command;
10
+ use chrono::{Local, Datelike};
11
+ use colored::Colorize;
12
+ use sentiment::Mood;
13
+ use svg::DayStats;
14
+
15
+ fn get_log_path() -> PathBuf {
16
+ let home = env::var("USERPROFILE")
17
+ .or_else(|_| env::var("HOME"))
18
+ .unwrap_or_else(|_| ".".to_string());
19
+ Path::new(&home).join(".git-mood.json")
20
+ }
21
+
22
+ fn load_db(path: &Path) -> HashMap<String, DayStats> {
23
+ if !path.exists() {
24
+ return HashMap::new();
25
+ }
26
+ let content = fs::read_to_string(path).unwrap_or_default();
27
+ serde_json::from_str(&content).unwrap_or_default()
28
+ }
29
+
30
+ fn save_db(path: &Path, db: &HashMap<String, DayStats>) -> Result<(), String> {
31
+ let content = serde_json::to_string_pretty(db).map_err(|e| e.to_string())?;
32
+ fs::write(path, content).map_err(|e| e.to_string())
33
+ }
34
+
35
+ fn get_diff_stats() -> (usize, usize) {
36
+ let output = Command::new("git")
37
+ .args(["diff", "--cached", "--numstat"])
38
+ .output();
39
+
40
+ let mut insertions = 0;
41
+ let mut deletions = 0;
42
+
43
+ if let Ok(out) = output {
44
+ if out.status.success() {
45
+ let stdout_str = String::from_utf8_lossy(&out.stdout);
46
+ for line in stdout_str.lines() {
47
+ let parts: Vec<&str> = line.split_whitespace().collect();
48
+ if parts.len() >= 2 {
49
+ let ins: usize = parts[0].parse().unwrap_or(0);
50
+ let del: usize = parts[1].parse().unwrap_or(0);
51
+ insertions += ins;
52
+ deletions += del;
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ (insertions, deletions)
59
+ }
60
+
61
+ fn print_help() {
62
+ println!("{}", "==============================================".cyan());
63
+ println!(" {} - GitHub Graph Colorizer", "git-mood".green().bold());
64
+ println!("{}", "==============================================".cyan());
65
+ println!("Usage:");
66
+ println!(" git-mood init Install git hook in the current repository");
67
+ println!(" git-mood status Display terminal dashboard of your moods");
68
+ println!(" git-mood sync Generate git-mood.svg and push it to profile");
69
+ println!();
70
+ println!("Hook Command (Internal):");
71
+ println!(" git-mood commit-hook <file> Analyze commit message file");
72
+ println!("{}", "==============================================".cyan());
73
+ }
74
+
75
+ fn main() {
76
+ let args: Vec<String> = env::args().collect();
77
+ if args.len() < 2 {
78
+ print_help();
79
+ return;
80
+ }
81
+
82
+ match args[1].as_str() {
83
+ "init" => {
84
+ println!("Initializing git-mood hook...");
85
+ if let Err(e) = hook::install_hook() {
86
+ eprintln!("{}: {}", "Error".red().bold(), e);
87
+ }
88
+ }
89
+ "commit-hook" => {
90
+ if args.len() < 3 {
91
+ eprintln!("{}: Missing commit message file path", "Error".red().bold());
92
+ return;
93
+ }
94
+ let msg_file = &args[2];
95
+ let commit_msg = match fs::read_to_string(msg_file) {
96
+ Ok(content) => content,
97
+ Err(e) => {
98
+ eprintln!("{}: Failed to read commit msg: {}", "Error".red().bold(), e);
99
+ return;
100
+ }
101
+ };
102
+
103
+ // Analyze
104
+ let (insertions, deletions) = get_diff_stats();
105
+ let today = Local::now().date_naive();
106
+ let weekday = today.weekday();
107
+ let is_weekend = weekday == chrono::Weekday::Sat || weekday == chrono::Weekday::Sun;
108
+
109
+ let mood = sentiment::analyze_commit(&commit_msg, insertions, deletions, is_weekend);
110
+
111
+ // Update database
112
+ let db_path = get_log_path();
113
+ let mut db = load_db(&db_path);
114
+ let date_key = today.format("%Y-%m-%d").to_string();
115
+
116
+ let mut stats = db.entry(date_key).or_insert_with(DayStats::default);
117
+ stats.commits_count += 1;
118
+ match mood {
119
+ Mood::Positive => stats.positive += 1,
120
+ Mood::Negative => stats.negative += 1,
121
+ Mood::Neutral => stats.neutral += 1,
122
+ Mood::Refactor => stats.refactor += 1,
123
+ Mood::Weekend => stats.weekend += 1,
124
+ }
125
+
126
+ if let Err(e) = save_db(&db_path, &db) {
127
+ eprintln!("{}: Failed to save mood: {}", "Error".red().bold(), e);
128
+ return;
129
+ }
130
+
131
+ // Print feedback to the developer during their git workflow!
132
+ let colored_mood = match mood {
133
+ Mood::Positive => "Positive (Turquoise) 🚀".cyan().bold(),
134
+ Mood::Negative => "Stressed / Negative (Red) ⚠️".red().bold(),
135
+ Mood::Neutral => "Steady / Neutral (Green) 🟩".green().bold(),
136
+ Mood::Refactor => "Refactor / Cleanup (Yellow) 🟨".yellow().bold(),
137
+ Mood::Weekend => "Weekend Coding (Purple) 🟪".magenta().bold(),
138
+ };
139
+
140
+ println!();
141
+ println!(" [git-mood] Sentiment Analyzed: {}", colored_mood);
142
+ println!(" Diff: +{} / -{} lines", insertions, deletions);
143
+ println!();
144
+ }
145
+ "status" => {
146
+ let db_path = get_log_path();
147
+ let db = load_db(&db_path);
148
+
149
+ println!("{}", "\n--- Your git-mood Dashboard ---".cyan().bold());
150
+ if db.is_empty() {
151
+ println!("No commits tracked yet. Run `git-mood init` in a repo to start logging!");
152
+ return;
153
+ }
154
+
155
+ // Print summary of last 7 days
156
+ let today = Local::now().date_naive();
157
+ println!("\nRecent Days:");
158
+ for i in (0..7).rev() {
159
+ let day = today - chrono::Duration::days(i);
160
+ let date_str = day.format("%Y-%m-%d").to_string();
161
+ let day_name = day.format("%A").to_string();
162
+
163
+ if let Some(stats) = db.get(&date_str) {
164
+ if let Some(m) = stats.determine_mood() {
165
+ let block = match m {
166
+ Mood::Positive => "■".cyan(),
167
+ Mood::Negative => "■".red(),
168
+ Mood::Neutral => "■".green(),
169
+ Mood::Refactor => "■".yellow(),
170
+ Mood::Weekend => "■".magenta(),
171
+ };
172
+ println!(" {} ({}): {} [{} commits]", day_name, date_str, block, stats.commits_count);
173
+ continue;
174
+ }
175
+ }
176
+ // If no commits
177
+ println!(" {} ({}): {} [0 commits]", day_name, date_str, "■".truecolor(80, 80, 80));
178
+ }
179
+
180
+ // Dominant overall mood breakdown
181
+ let mut mood_counts = HashMap::new();
182
+ let mut total_commits = 0;
183
+ for stats in db.values() {
184
+ total_commits += stats.commits_count;
185
+ if let Some(m) = stats.determine_mood() {
186
+ *mood_counts.entry(m).or_insert(0) += 1;
187
+ }
188
+ }
189
+
190
+ println!("\nOverall Stats:");
191
+ println!(" Total Commits: {}", total_commits);
192
+ for (m, count) in &mood_counts {
193
+ let name = match m {
194
+ Mood::Positive => "Turquoise (Positive)".cyan(),
195
+ Mood::Negative => "Red (Negative)".red(),
196
+ Mood::Neutral => "Green (Neutral)".green(),
197
+ Mood::Refactor => "Yellow (Refactoring)".yellow(),
198
+ Mood::Weekend => "Purple (Weekend)".magenta(),
199
+ };
200
+ println!(" {}: {} days", name, count);
201
+ }
202
+ println!();
203
+ }
204
+ "sync" => {
205
+ println!("{} Generating git-mood.svg...", "[git-mood]".green());
206
+ let db_path = get_log_path();
207
+ let db = load_db(&db_path);
208
+
209
+ let svg_content = svg::generate_svg(&db);
210
+ let svg_path = Path::new("git-mood.svg");
211
+
212
+ if let Err(e) = fs::write(svg_path, svg_content) {
213
+ eprintln!("{}: Failed to write SVG file: {}", "Error".red().bold(), e);
214
+ return;
215
+ }
216
+ println!("{} Saved SVG to {:?}", "[git-mood]".green(), svg_path);
217
+
218
+ // Check if git repository to perform auto-commit/push
219
+ if Path::new(".git").exists() {
220
+ println!("{} Detected local git repository. Committing and pushing SVG...", "[git-mood]".green());
221
+
222
+ // git add
223
+ let add_status = Command::new("git")
224
+ .args(["add", "git-mood.svg"])
225
+ .status();
226
+
227
+ if let Ok(status) = add_status {
228
+ if status.success() {
229
+ // git commit
230
+ let commit_status = Command::new("git")
231
+ .args(["commit", "-m", "docs: update git-mood contribution graph [skip ci]", "--no-verify"])
232
+ .status();
233
+
234
+ if let Ok(c_status) = commit_status {
235
+ if c_status.success() {
236
+ println!("{} Committed changes.", "[git-mood]".green());
237
+
238
+ // git push
239
+ let push_status = Command::new("git")
240
+ .args(["push"])
241
+ .status();
242
+
243
+ if let Ok(p_status) = push_status {
244
+ if p_status.success() {
245
+ println!("{} Successfully pushed to remote!", "[git-mood]".green());
246
+ } else {
247
+ println!("{} Push failed. You may need to run 'git push' manually.", "[git-mood]".yellow());
248
+ }
249
+ }
250
+ } else {
251
+ println!("{} No new changes to commit or commit failed.", "[git-mood]".yellow());
252
+ }
253
+ }
254
+ }
255
+ }
256
+ } else {
257
+ println!("{} Not a git repository or no .git folder found locally. Skipping git push.", "[git-mood]".yellow());
258
+ }
259
+ }
260
+ _ => {
261
+ print_help();
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,120 @@
1
+ use std::fmt;
2
+ use serde::{Deserialize, Serialize};
3
+
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5
+ pub enum Mood {
6
+ Positive, // Turquoise
7
+ Negative, // Red
8
+ Neutral, // Green
9
+ Refactor, // Yellow
10
+ Weekend, // Purple
11
+ }
12
+
13
+ impl Mood {
14
+ pub fn as_str(&self) -> &'static str {
15
+ match self {
16
+ Mood::Positive => "positive",
17
+ Mood::Negative => "negative",
18
+ Mood::Neutral => "neutral",
19
+ Mood::Refactor => "refactor",
20
+ Mood::Weekend => "weekend",
21
+ }
22
+ }
23
+
24
+ pub fn from_str(s: &str) -> Option<Self> {
25
+ match s.to_lowercase().as_str() {
26
+ "positive" => Some(Mood::Positive),
27
+ "negative" => Some(Mood::Negative),
28
+ "neutral" => Some(Mood::Neutral),
29
+ "refactor" => Some(Mood::Refactor),
30
+ "weekend" => Some(Mood::Weekend),
31
+ _ => None,
32
+ }
33
+ }
34
+
35
+ pub fn to_color_code(&self) -> &'static str {
36
+ match self {
37
+ Mood::Positive => "#00f2fe", // Turquoise
38
+ Mood::Negative => "#ff0844", // Red
39
+ Mood::Refactor => "#f6d365", // Yellow
40
+ Mood::Weekend => "#7f00ff", // Purple
41
+ Mood::Neutral => "#00cdac", // Green
42
+ }
43
+ }
44
+ }
45
+
46
+ impl fmt::Display for Mood {
47
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48
+ write!(f, "{}", self.as_str())
49
+ }
50
+ }
51
+
52
+ pub fn analyze_commit(
53
+ message: &str,
54
+ insertions: usize,
55
+ deletions: usize,
56
+ is_weekend: bool,
57
+ ) -> Mood {
58
+ let lower_msg = message.to_lowercase();
59
+
60
+ // 1. Check for Negative / Stress Keywords (High Priority)
61
+ let negative_keywords = [
62
+ "fuck", "shit", "damn", "hate", "hell", "stuck", "broken", "fail", "error",
63
+ "wrong", "panic", "crash", "issue", "ugly", "mess", "bug", "wtf", "stupid",
64
+ "annoying", "rage", "die", "nonsense", "garbage", "trash", "crap",
65
+ ];
66
+ for kw in &negative_keywords {
67
+ if lower_msg.contains(kw) {
68
+ return Mood::Negative;
69
+ }
70
+ }
71
+
72
+ // 2. Check for Refactor / Cleanup (Yellow)
73
+ let total_changes = insertions + deletions;
74
+ let is_refactor_keyword = lower_msg.contains("refactor")
75
+ || lower_msg.contains("cleanup")
76
+ || lower_msg.contains("simplify")
77
+ || lower_msg.contains("remove")
78
+ || lower_msg.contains("delete");
79
+
80
+ let is_heavy_deletions = total_changes > 10 && (deletions as f64 / total_changes as f64) >= 0.70;
81
+
82
+ if is_refactor_keyword || is_heavy_deletions {
83
+ return Mood::Refactor;
84
+ }
85
+
86
+ // 3. Check for Weekend Warrior (Purple)
87
+ if is_weekend && total_changes > 0 {
88
+ return Mood::Weekend;
89
+ }
90
+
91
+ // 4. Check for Positive (Turquoise)
92
+ let positive_keywords = [
93
+ "feat", "add", "improve", "perf", "docs", "love", "great", "awesome",
94
+ "easy", "solved", "fixed", "clean", "nice", "super", "happy", "resolve",
95
+ "success", "optimize", "cool", "proud", "perfect", "magic", "beautiful",
96
+ ];
97
+ for kw in &positive_keywords {
98
+ if lower_msg.contains(kw) {
99
+ return Mood::Positive;
100
+ }
101
+ }
102
+
103
+ // 5. Default/Neutral (Green)
104
+ Mood::Neutral
105
+ }
106
+
107
+ #[cfg(test)]
108
+ mod tests {
109
+ use super::*;
110
+
111
+ #[test]
112
+ fn test_sentiment_analysis() {
113
+ assert_eq!(analyze_commit("feat: add login page", 100, 0, false), Mood::Positive);
114
+ assert_eq!(analyze_commit("fix: stupid crash on login", 2, 2, false), Mood::Negative);
115
+ assert_eq!(analyze_commit("refactor auth flows", 10, 50, false), Mood::Refactor);
116
+ assert_eq!(analyze_commit("clean up helper functions", 5, 20, false), Mood::Refactor);
117
+ assert_eq!(analyze_commit("change some config", 5, 0, true), Mood::Weekend);
118
+ assert_eq!(analyze_commit("chore: update readme", 1, 1, false), Mood::Neutral);
119
+ }
120
+ }