gent-cli 2.0.0 → 5.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.
@@ -1,18 +1,17 @@
1
1
  /**
2
2
  * Commit Command - Record changes to the repository
3
- * Creates a new commit with staged files
3
+ * Creates a new commit with tree object referencing staged blobs
4
4
  */
5
5
 
6
6
  const path = require('path');
7
- const fs = require('fs').promises;
8
7
  const chalk = require('chalk');
9
8
  const inquirer = require('inquirer');
10
9
  const ora = require('ora');
11
- const { getGentPath, readJSON, writeJSON, saveBlob, getBlob } = require('../utils/fileSystem');
10
+ const { getGentPath, readJSON, writeJSON } = require('../utils/fileSystem');
12
11
  const authStorage = require('../utils/auth-storage');
13
12
  const { STAGING_FILE, COMMITS_FILE, CONFIG_FILE } = require('../utils/constants');
14
- const { generateCommitHash, getFileHash } = require('../utils/helpers');
15
- const { computeDiff } = require('../utils/diff');
13
+ const { generateCommitHash } = require('../utils/helpers');
14
+ const { storeTree, snapshotFile } = require('../utils/hash-engine');
16
15
 
17
16
  /**
18
17
  * Create a new commit
@@ -25,9 +24,10 @@ async function commit(options) {
25
24
 
26
25
  // Read staging area
27
26
  const staging = await readJSON(path.join(gentPath, STAGING_FILE));
27
+ const stagedEntries = staging.entries || [];
28
28
  const stagedFiles = staging.files || [];
29
29
 
30
- if (stagedFiles.length === 0) {
30
+ if (stagedEntries.length === 0 && stagedFiles.length === 0) {
31
31
  console.log(chalk.yellow('No changes added to commit'));
32
32
  console.log(chalk.gray('Use "gent add <file>..." to stage files'));
33
33
  return;
@@ -59,20 +59,14 @@ async function commit(options) {
59
59
  let authorName = config.user.name;
60
60
  let authorEmail = config.user.email;
61
61
 
62
- // Fallback to global auth if local config is empty
63
62
  if (!authorName || !authorEmail) {
64
63
  const globalUser = await authStorage.getUser();
65
64
  if (globalUser) {
66
- if (!authorName) {
67
- authorName = [globalUser.first_name, globalUser.last_name].filter(Boolean).join(' ');
68
- }
69
- if (!authorEmail) {
70
- authorEmail = globalUser.email;
71
- }
65
+ if (!authorName) authorName = [globalUser.first_name, globalUser.last_name].filter(Boolean).join(' ');
66
+ if (!authorEmail) authorEmail = globalUser.email;
72
67
  }
73
68
  }
74
69
 
75
- // Fail if still no identity
76
70
  if (!authorName || !authorEmail) {
77
71
  spinner.stop();
78
72
  console.error(chalk.red('Author identity unknown'));
@@ -80,92 +74,106 @@ async function commit(options) {
80
74
  return;
81
75
  }
82
76
 
83
- // Create commit object
84
- const commit = {
85
- sha: generateCommitHash(),
86
- message: message,
87
- author: {
88
- name: authorName,
89
- email: authorEmail
90
- },
91
- timestamp: new Date().toISOString(),
92
- parent: repository.branches[repository.currentBranch] ? [repository.branches[repository.currentBranch]] : [],
93
- files: []
94
- };
77
+ // Build tree entries from staged data
78
+ let treeEntries = [];
79
+
80
+ if (stagedEntries.length > 0) {
81
+ // New format: entries already have blob hashes from gent add
82
+ // Carry forward unchanged files from parent commit
83
+ const parentHash = repository.branches[repository.currentBranch] || null;
84
+ const parentCommit = parentHash
85
+ ? (repository.commits || []).find(c => c.hash === parentHash)
86
+ : null;
87
+ const parentTree = parentCommit && parentCommit.tree
88
+ ? parentCommit.tree
89
+ : (parentCommit ? parentCommit.files.map(f => ({ mode: '100644', name: f.path, hash: f.hash, type: 'blob' })) : []);
90
+
91
+ // Start from parent tree, overlay staged changes
92
+ const treeMap = new Map(parentTree.map(e => [e.name, e]));
93
+
94
+ for (const entry of stagedEntries) {
95
+ if (entry.status === 'deleted') {
96
+ treeMap.delete(entry.path);
97
+ } else {
98
+ treeMap.set(entry.path, {
99
+ mode: '100644',
100
+ name: entry.path,
101
+ hash: entry.hash,
102
+ type: 'blob'
103
+ });
104
+ }
105
+ }
95
106
 
96
- // Create a map of parent files for quick lookup
97
- const parentFilesMap = new Map();
98
- if (commit.parent && commit.parent.length > 0) {
99
- const parentSha = commit.parent[0];
100
- const parentCommit = repository.commits.find(c => c.sha === parentSha);
101
- if (parentCommit) {
102
- parentCommit.files.forEach(f => parentFilesMap.set(f.path, f.hash));
107
+ treeEntries = Array.from(treeMap.values());
108
+ } else {
109
+ // Legacy fallback: files array without blob hashes
110
+ for (const file of stagedFiles) {
111
+ const entry = await snapshotFile(gentPath, cwd, file);
112
+ treeEntries.push(entry);
103
113
  }
104
114
  }
105
115
 
106
- // Hash staged files and compute diffs
107
- for (const file of stagedFiles) {
108
- const filePath = path.join(cwd, file);
109
- let content;
110
-
111
- try {
112
- content = await fs.readFile(filePath, 'utf-8');
113
- } catch (err) {
114
- // If file is deleted or unreadable, we handle it as empty content or handle deletion if we supported it
115
- // For now assuming staged files exist
116
- content = '';
116
+ // Store tree object
117
+ const treeHash = await storeTree(gentPath, treeEntries);
118
+
119
+ // Compute diff stats
120
+ let totalInsertions = 0;
121
+ let totalDeletions = 0;
122
+ if (stagedEntries.length > 0) {
123
+ for (const e of stagedEntries) {
124
+ if (e.stats) {
125
+ totalInsertions += e.stats.insertions || 0;
126
+ totalDeletions += e.stats.deletions || 0;
127
+ }
117
128
  }
129
+ }
118
130
 
119
- const hash = await getFileHash(filePath);
120
-
121
- // Save the blob for future diffs
122
- await saveBlob(content, hash);
123
-
124
- // Get previous content
125
- let oldContent = '';
126
- if (parentFilesMap.has(file)) {
127
- const oldHash = parentFilesMap.get(file);
128
- oldContent = await getBlob(oldHash);
131
+ // Create commit object
132
+ const commitObj = {
133
+ hash: generateCommitHash(),
134
+ message,
135
+ author: {
136
+ name: authorName,
137
+ email: authorEmail
138
+ },
139
+ timestamp: new Date().toISOString(),
140
+ parent: repository.branches[repository.currentBranch] || null,
141
+ treeHash,
142
+ tree: treeEntries,
143
+ files: treeEntries.map(e => ({ path: e.name, hash: e.hash })), // backward compat
144
+ stats: {
145
+ filesChanged: stagedEntries.length || stagedFiles.length,
146
+ insertions: totalInsertions,
147
+ deletions: totalDeletions
129
148
  }
149
+ };
130
150
 
131
- // Compute Myers diff
132
- const changes = computeDiff(oldContent, content);
133
-
134
- commit.files.push({
135
- path: file,
136
- hash: hash,
137
- diff: changes
138
- });
139
- }
140
-
141
- // Add commit to repository
151
+ // Save commit
142
152
  repository.commits = repository.commits || [];
143
- repository.commits.push(commit);
144
- repository.branches[repository.currentBranch] = commit.sha;
153
+ repository.commits.push(commitObj);
154
+ repository.branches[repository.currentBranch] = commitObj.hash;
145
155
 
146
- // Save repository and clear staging
147
156
  await writeJSON(path.join(gentPath, COMMITS_FILE), repository);
157
+
158
+ // Clear staging
159
+ staging.entries = [];
148
160
  staging.files = [];
149
161
  await writeJSON(path.join(gentPath, STAGING_FILE), staging);
150
162
 
151
- spinner.succeed(chalk.green('Changes committed successfully!'));
152
-
153
- // Display commit info
154
- console.log(chalk.cyan(`\n[${repository.currentBranch} ${commit.sha.substring(0, 7)}] ${message}`));
155
- console.log(chalk.gray(`Author: ${commit.author.name} <${commit.author.email}>`));
156
- console.log(chalk.gray(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
157
- console.log(chalk.gray(`\n${commit.files.length} file(s) changed`));
163
+ spinner.succeed(chalk.green('Changes committed successfully!'));
158
164
 
159
- // Handle --push option
160
- if (options.push) {
161
- const push = require('./push');
162
- await push('origin', repository.currentBranch, {});
163
- }
165
+ console.log(chalk.cyan(`\n[${repository.currentBranch} ${commitObj.hash.substring(0, 7)}] ${message}`));
166
+ console.log(chalk.gray(`Author: ${commitObj.author.name} <${commitObj.author.email}>`));
167
+ console.log(chalk.gray(`Date: ${new Date(commitObj.timestamp).toLocaleString()}`));
168
+ console.log(chalk.gray(`Tree: ${treeHash.substring(0, 7)}`));
169
+ console.log(chalk.gray(`\n${commitObj.stats.filesChanged} file(s) changed, `) +
170
+ chalk.green(`+${totalInsertions} insertions`) + ', ' +
171
+ chalk.red(`-${totalDeletions} deletions`));
164
172
 
165
173
  } catch (error) {
166
174
  if (error.code === 'ENOENT' && error.message.includes('.gent')) {
167
175
  console.error(chalk.red('Error: Not a gent repository'));
168
- console.log(chalk.yellow('\nℹ Run "gent init" to initialize a repository'));
176
+ console.log(chalk.yellow('\nRun "gent init" to initialize a repository'));
169
177
  } else {
170
178
  console.error(chalk.red('Error:'), error.message);
171
179
  }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * ============================================================================
3
+ * Diff Command - Show changes between commits, staging, and working tree
4
+ * ============================================================================
5
+ *
6
+ * PURPOSE:
7
+ * Display line-by-line differences between file versions, similar to
8
+ * `git diff`. Shows what changed, where, and how much.
9
+ *
10
+ * USAGE:
11
+ * gent diff → Working tree vs staging area
12
+ * gent diff --staged → Staging area vs last commit
13
+ * gent diff <file> → Diff specific file(s)
14
+ * gent diff --stat → Summary only (no patch)
15
+ *
16
+ * ALGORITHM:
17
+ * Uses LCS (Longest Common Subsequence) line-level diff from diff-engine.js.
18
+ * Outputs unified diff format with context lines (3 by default).
19
+ * Time complexity: O(m*n) where m,n = line counts of old/new files.
20
+ *
21
+ * BACKEND EXPECTATIONS:
22
+ * None (local only). Backend receives final blob hashes, not diffs.
23
+ *
24
+ * ============================================================================
25
+ */
26
+
27
+ const fs = require('fs').promises;
28
+ const path = require('path');
29
+ const chalk = require('chalk');
30
+ const { getGentPath, readJSON, pathExists, getAllFiles, getIgnorePatterns } = require('../utils/fileSystem');
31
+ const { STAGING_FILE, COMMITS_FILE } = require('../utils/constants');
32
+ const { readBlobAsString, hashBlob } = require('../utils/hash-engine');
33
+ const { formatUnifiedDiff, diffText } = require('../utils/diff-engine');
34
+
35
+ /**
36
+ * Show differences
37
+ * @param {Array} files - Optional specific files to diff
38
+ * @param {Object} options - Command options
39
+ */
40
+ async function diff(files, options) {
41
+ try {
42
+ const gentPath = await getGentPath();
43
+ const cwd = process.cwd();
44
+
45
+ const staging = await readJSON(path.join(gentPath, STAGING_FILE));
46
+ const repository = await readJSON(path.join(gentPath, COMMITS_FILE));
47
+
48
+ const currentBranch = repository.currentBranch || 'main';
49
+ const headHash = repository.branches[currentBranch] || null;
50
+ const headCommit = headHash
51
+ ? (repository.commits || []).find(c => c.hash === headHash)
52
+ : null;
53
+
54
+ // Build HEAD tree map: path → blobHash
55
+ const headTree = buildTreeMap(headCommit);
56
+
57
+ // Build staging map: path → blobHash
58
+ const stagingEntries = staging.entries || [];
59
+ const stagingMap = new Map(stagingEntries.map(e => [e.path, e.hash]));
60
+
61
+ if (options.staged) {
62
+ // Staged vs HEAD
63
+ await diffStagedVsHead(gentPath, cwd, stagingEntries, headTree, files, options);
64
+ } else {
65
+ // Working tree vs staged (or HEAD if not staged)
66
+ await diffWorkingTree(gentPath, cwd, stagingMap, headTree, files, options);
67
+ }
68
+
69
+ } catch (error) {
70
+ if (error.code === 'ENOENT' && error.message.includes('.gent')) {
71
+ console.error(chalk.red('Error: Not a gent repository'));
72
+ console.log(chalk.yellow('Run "gent init" to initialize a repository'));
73
+ } else {
74
+ console.error(chalk.red('Error:'), error.message);
75
+ }
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Diff working tree vs staging/HEAD
82
+ */
83
+ async function diffWorkingTree(gentPath, cwd, stagingMap, headTree, filterFiles, options) {
84
+ const ignorePatterns = await getIgnorePatterns(cwd);
85
+ const allFiles = await getAllFiles(cwd, ignorePatterns);
86
+ let hasDiffs = false;
87
+
88
+ let totalInsertions = 0;
89
+ let totalDeletions = 0;
90
+ const fileSummaries = [];
91
+
92
+ for (const absPath of allFiles) {
93
+ const relPath = path.relative(cwd, absPath);
94
+
95
+ // Filter if specific files provided
96
+ if (filterFiles && filterFiles.length > 0 && !filterFiles.includes(relPath)) continue;
97
+
98
+ // Determine base hash (staging → HEAD fallback)
99
+ const baseHash = stagingMap.get(relPath) || headTree.get(relPath);
100
+ if (!baseHash) continue; // untracked
101
+
102
+ const currentContent = await fs.readFile(absPath, 'utf-8');
103
+ const currentHash = hashBlob(currentContent);
104
+
105
+ if (currentHash === baseHash) continue; // unchanged
106
+
107
+ let oldContent = '';
108
+ try {
109
+ oldContent = await readBlobAsString(gentPath, baseHash);
110
+ } catch {
111
+ // No blob = new file
112
+ }
113
+
114
+ const d = diffText(oldContent, currentContent);
115
+ totalInsertions += d.stats.insertions;
116
+ totalDeletions += d.stats.deletions;
117
+ fileSummaries.push({ file: relPath, stats: d.stats });
118
+
119
+ if (!options.stat) {
120
+ const unified = formatUnifiedDiff(relPath, oldContent, currentContent);
121
+ if (unified) {
122
+ hasDiffs = true;
123
+ printColorizedDiff(unified);
124
+ }
125
+ } else {
126
+ hasDiffs = true;
127
+ }
128
+ }
129
+
130
+ if (options.stat || hasDiffs) {
131
+ printDiffStat(fileSummaries, totalInsertions, totalDeletions);
132
+ }
133
+
134
+ if (!hasDiffs) {
135
+ console.log(chalk.gray('No changes'));
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Diff staged files vs HEAD commit
141
+ */
142
+ async function diffStagedVsHead(gentPath, cwd, stagedEntries, headTree, filterFiles, options) {
143
+ let hasDiffs = false;
144
+ let totalInsertions = 0;
145
+ let totalDeletions = 0;
146
+ const fileSummaries = [];
147
+
148
+ for (const entry of stagedEntries) {
149
+ if (filterFiles && filterFiles.length > 0 && !filterFiles.includes(entry.path)) continue;
150
+
151
+ const headBlobHash = headTree.get(entry.path);
152
+
153
+ if (entry.status === 'deleted') {
154
+ if (headBlobHash) {
155
+ const oldContent = await readBlobAsString(gentPath, headBlobHash);
156
+ const d = diffText(oldContent, '');
157
+ totalDeletions += d.stats.deletions;
158
+ fileSummaries.push({ file: entry.path, stats: d.stats });
159
+ if (!options.stat) {
160
+ printColorizedDiff(formatUnifiedDiff(entry.path, oldContent, ''));
161
+ }
162
+ hasDiffs = true;
163
+ }
164
+ continue;
165
+ }
166
+
167
+ if (!entry.hash) continue;
168
+
169
+ if (entry.hash === headBlobHash) continue; // unchanged
170
+
171
+ let oldContent = '';
172
+ try {
173
+ if (headBlobHash) oldContent = await readBlobAsString(gentPath, headBlobHash);
174
+ } catch { /* new file */ }
175
+
176
+ const newContent = await readBlobAsString(gentPath, entry.hash);
177
+ const d = diffText(oldContent, newContent);
178
+ totalInsertions += d.stats.insertions;
179
+ totalDeletions += d.stats.deletions;
180
+ fileSummaries.push({ file: entry.path, stats: d.stats });
181
+
182
+ if (!options.stat) {
183
+ const unified = formatUnifiedDiff(entry.path, oldContent, newContent);
184
+ if (unified) {
185
+ hasDiffs = true;
186
+ printColorizedDiff(unified);
187
+ }
188
+ } else {
189
+ hasDiffs = true;
190
+ }
191
+ }
192
+
193
+ if (options.stat || hasDiffs) {
194
+ printDiffStat(fileSummaries, totalInsertions, totalDeletions);
195
+ }
196
+
197
+ if (!hasDiffs) {
198
+ console.log(chalk.gray('No staged changes'));
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Build path → hash map from commit object
204
+ */
205
+ function buildTreeMap(commit) {
206
+ const map = new Map();
207
+ if (!commit) return map;
208
+ const tree = commit.tree || commit.files || [];
209
+ for (const f of tree) {
210
+ map.set(f.path || f.name, f.hash);
211
+ }
212
+ return map;
213
+ }
214
+
215
+ /**
216
+ * Print colorized unified diff
217
+ */
218
+ function printColorizedDiff(unifiedDiff) {
219
+ const lines = unifiedDiff.split('\n');
220
+ for (const line of lines) {
221
+ if (line.startsWith('---') || line.startsWith('+++')) {
222
+ console.log(chalk.bold(line));
223
+ } else if (line.startsWith('@@')) {
224
+ console.log(chalk.cyan(line));
225
+ } else if (line.startsWith('+')) {
226
+ console.log(chalk.green(line));
227
+ } else if (line.startsWith('-')) {
228
+ console.log(chalk.red(line));
229
+ } else {
230
+ console.log(line);
231
+ }
232
+ }
233
+ console.log('');
234
+ }
235
+
236
+ /**
237
+ * Print diff stat summary
238
+ */
239
+ function printDiffStat(fileSummaries, totalIns, totalDel) {
240
+ if (fileSummaries.length === 0) return;
241
+
242
+ console.log('');
243
+ const maxLen = Math.max(...fileSummaries.map(f => f.file.length));
244
+
245
+ for (const { file, stats } of fileSummaries) {
246
+ const total = stats.insertions + stats.deletions;
247
+ const bar = chalk.green('+'.repeat(Math.min(stats.insertions, 30))) +
248
+ chalk.red('-'.repeat(Math.min(stats.deletions, 30)));
249
+ console.log(` ${file.padEnd(maxLen)} | ${String(total).padStart(4)} ${bar}`);
250
+ }
251
+
252
+ console.log(chalk.gray(` ${fileSummaries.length} file(s) changed, `) +
253
+ chalk.green(`${totalIns} insertion(s)`) + ', ' +
254
+ chalk.red(`${totalDel} deletion(s)`));
255
+ }
256
+
257
+ module.exports = diff;
@@ -28,10 +28,9 @@ async function init(options) {
28
28
  // Get authenticated user profile if available
29
29
  let defaultName = '';
30
30
  let defaultEmail = '';
31
- let user = null;
32
31
 
33
32
  try {
34
- user = await authStorage.getUser();
33
+ const user = await authStorage.getUser();
35
34
  if (user) {
36
35
  if (user.first_name || user.last_name) {
37
36
  defaultName = [user.first_name, user.last_name].filter(Boolean).join(' ');
@@ -63,8 +62,12 @@ async function init(options) {
63
62
  await ensureDir(path.join(gentPath, 'refs', 'tags'));
64
63
 
65
64
  // Create/Update configuration
65
+ // Only write config if it doesn't exist OR if we have valid user info to update
66
66
  const configPath = path.join(gentPath, CONFIG_FILE);
67
67
  if (!(await pathExists(configPath)) || (defaultName && defaultEmail)) {
68
+ // If re-init, we might want to preserve existing config unless we have better info?
69
+ // Git re-init doesn't overwrite config usually.
70
+ // But for now, let's write ensuring we have a config file.
68
71
  if (!isReinit || !(await pathExists(configPath))) {
69
72
  await writeJSON(configPath, config);
70
73
  }
@@ -106,40 +109,6 @@ node_modules/
106
109
  console.log(chalk.gray(`Initialized empty Gent repository in ${gentPath}`));
107
110
  }
108
111
 
109
- // Handle --cloud option
110
- if (options.cloud) {
111
- if (!user) {
112
- console.log(chalk.yellow('\nWarning: Skipping cloud repository creation (not logged in)'));
113
- console.log(chalk.gray('Run "gent login" then "gent create <name> --init-local"'));
114
- } else {
115
- try {
116
- const repoService = require('../services/repo-service');
117
- const { addRemote } = require('../utils/cloud-sync');
118
- const ora = require('ora');
119
-
120
- const spinner = ora('Creating cloud repository...').start();
121
- const repoName = path.basename(cwd);
122
-
123
- const repository = await repoService.createRepository(
124
- repoName,
125
- options.description || 'A gent repository',
126
- options.private || false,
127
- 'main'
128
- );
129
-
130
- const ownerId = repository.owner_id || repository.owner?.id || repository.owner;
131
- const name = repository.name || repository.project_name || repoName;
132
-
133
- await addRemote('origin', ownerId, name, cwd);
134
- spinner.succeed(chalk.green(`Created cloud repository: ${ownerId}/${name}`));
135
- console.log(chalk.gray(`Remote 'origin' added`));
136
- } catch (error) {
137
- console.log(chalk.red(`\nFailed to create cloud repository: ${error.message}`));
138
- console.log(chalk.gray('You can create it later with "gent create <name>"'));
139
- }
140
- }
141
- }
142
-
143
112
  } catch (error) {
144
113
  console.error(chalk.red('Failed to initialize repository'));
145
114
  console.error(chalk.red('Error:'), error.message);
@@ -1,6 +1,25 @@
1
1
  /**
2
- * Log Command - Show commit logs
3
- * Displays commit history with details
2
+ * ============================================================================
3
+ * Log Command - Show commit history
4
+ * ============================================================================
5
+ *
6
+ * PURPOSE:
7
+ * Display commit history with details, stats, and merge info. Like `git log`.
8
+ *
9
+ * USAGE:
10
+ * gent log → Show last 10 commits (detailed)
11
+ * gent log -n 20 → Show last 20 commits
12
+ * gent log --oneline → Condensed one-line-per-commit view
13
+ * gent log --stat → Include diffstat per commit
14
+ *
15
+ * ALGORITHM:
16
+ * Reads commits.json, filters by branch HEAD → parent chain, displays
17
+ * in reverse chronological order.
18
+ *
19
+ * BACKEND EXPECTATIONS:
20
+ * GET /api/repos/:id/commits/?branch=main&limit=10
21
+ *
22
+ * ============================================================================
4
23
  */
5
24
 
6
25
  const path = require('path');
@@ -27,14 +46,24 @@ async function log(options) {
27
46
  return;
28
47
  }
29
48
 
30
- // Limit number of commits to show
31
49
  const limit = parseInt(options.number) || 10;
32
- const commitsToShow = commits.slice(-limit).reverse();
50
+
51
+ // Walk branch chain for ordered display
52
+ const headHash = repository.branches[currentBranch];
53
+ const commitMap = new Map(commits.map(c => [c.hash, c]));
54
+ const ordered = [];
55
+ let cur = headHash;
56
+ while (cur && ordered.length < limit) {
57
+ const c = commitMap.get(cur);
58
+ if (!c) break;
59
+ ordered.push(c);
60
+ cur = c.parent;
61
+ }
33
62
 
34
63
  if (options.oneline) {
35
- displayOnelineLog(commitsToShow, repository.branches[currentBranch]);
64
+ displayOnelineLog(ordered, headHash);
36
65
  } else {
37
- displayDetailedLog(commitsToShow, repository.branches[currentBranch], currentBranch);
66
+ displayDetailedLog(ordered, headHash, currentBranch, options);
38
67
  }
39
68
 
40
69
  } catch (error) {
@@ -51,21 +80,36 @@ async function log(options) {
51
80
  /**
52
81
  * Display detailed commit log
53
82
  */
54
- function displayDetailedLog(commits, currentCommitHash, currentBranch) {
55
- console.log(chalk.bold.cyan(`\nCommit History (${currentBranch} branch):\n`));
83
+ function displayDetailedLog(commits, currentCommitHash, currentBranch, options) {
84
+ console.log(chalk.bold.cyan(`\nCommit History (${currentBranch}):\n`));
56
85
 
57
86
  commits.forEach((commit, index) => {
58
- const isHead = commit.sha === currentCommitHash;
87
+ const isHead = commit.hash === currentCommitHash;
59
88
  const headLabel = isHead ? chalk.yellow.bold(' (HEAD)') : '';
60
89
 
61
- console.log(chalk.yellow(`commit ${commit.sha}`) + headLabel);
90
+ console.log(chalk.yellow(`commit ${commit.hash}`) + headLabel);
91
+ if (commit.mergeParent) {
92
+ console.log(chalk.gray(`Merge: ${commit.parent?.substring(0, 7)} ${commit.mergeParent.substring(0, 7)}`));
93
+ }
62
94
  console.log(chalk.white(`Author: ${commit.author.name} <${commit.author.email}>`));
63
95
  console.log(chalk.white(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
64
96
  console.log(chalk.gray(` (${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`));
97
+ if (commit.treeHash) {
98
+ console.log(chalk.gray(`Tree: ${commit.treeHash.substring(0, 7)}`));
99
+ }
65
100
  console.log();
66
101
  console.log(chalk.white(` ${commit.message}`));
67
102
  console.log();
68
- console.log(chalk.gray(` ${commit.files.length} file(s) changed`));
103
+
104
+ // Show stats if --stat flag or if commit has stats
105
+ if (options && options.stat && commit.stats) {
106
+ console.log(chalk.gray(` ${commit.stats.filesChanged} file(s), `) +
107
+ chalk.green(`+${commit.stats.insertions}`) + ' ' +
108
+ chalk.red(`-${commit.stats.deletions}`));
109
+ } else {
110
+ const fileCount = commit.files ? commit.files.length : (commit.tree ? commit.tree.length : 0);
111
+ console.log(chalk.gray(` ${fileCount} file(s) in tree`));
112
+ }
69
113
 
70
114
  if (index < commits.length - 1) {
71
115
  console.log(chalk.gray(' │'));
@@ -79,9 +123,9 @@ function displayDetailedLog(commits, currentCommitHash, currentBranch) {
79
123
  */
80
124
  function displayOnelineLog(commits, currentCommitHash) {
81
125
  commits.forEach(commit => {
82
- const isHead = commit.sha === currentCommitHash;
126
+ const isHead = commit.hash === currentCommitHash;
83
127
  const headLabel = isHead ? chalk.yellow(' (HEAD)') : '';
84
- const shortHash = chalk.yellow(commit.sha.substring(0, 7));
128
+ const shortHash = chalk.yellow(commit.hash.substring(0, 7));
85
129
  const message = chalk.white(commit.message);
86
130
  const timeAgo = chalk.gray(`(${formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true })})`);
87
131