@coldge.com/gitbase 1.0.2 → 1.0.3
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 +65 -15
- package/dist/api/supabase.js +139 -32
- package/dist/commands/branch.js +86 -14
- package/dist/commands/diff.js +180 -94
- package/dist/commands/init.js +8 -3
- package/dist/commands/log.js +85 -11
- package/dist/commands/merge.js +226 -15
- package/dist/commands/pull.js +171 -0
- package/dist/commands/push.js +324 -71
- package/dist/commands/revert.js +247 -95
- package/dist/commands/snapshot.js +49 -0
- package/dist/commands/stash.js +211 -0
- package/dist/commands/status.js +46 -38
- package/dist/commands/verify.js +120 -0
- package/dist/index.js +90 -10
- package/dist/schema/extractor.js +183 -26
- package/dist/schema/queries.js +160 -8
- package/dist/utils/hashing.js +52 -3
- package/dist/utils/sqlDiff.js +245 -0
- package/package.json +2 -1
package/dist/commands/diff.js
CHANGED
|
@@ -7,8 +7,125 @@ import { extractSchema } from '../schema/extractor.js';
|
|
|
7
7
|
import { canonicalize } from '../utils/hashing.js';
|
|
8
8
|
const GITBASE_DIR = '.gitbase';
|
|
9
9
|
const CONFIG_FILE = path.join(GITBASE_DIR, 'config');
|
|
10
|
+
const HEAD_FILE = path.join(GITBASE_DIR, 'HEAD');
|
|
11
|
+
const OBJECTS_DIR = path.join(GITBASE_DIR, 'objects');
|
|
12
|
+
const ALL_TYPES = [
|
|
13
|
+
'extensions', 'types', 'sequences', 'tables', 'matviews',
|
|
14
|
+
'views', 'functions', 'triggers', 'policies', 'grants', 'publications'
|
|
15
|
+
];
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// HEAD~N resolution
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
async function resolveRef(ref) {
|
|
20
|
+
// HEAD~N syntax
|
|
21
|
+
const tildeMatch = ref.match(/^HEAD~(\d+)$/i);
|
|
22
|
+
if (tildeMatch || ref.toUpperCase() === 'HEAD') {
|
|
23
|
+
const steps = tildeMatch ? parseInt(tildeMatch[1], 10) : 0;
|
|
24
|
+
let hash = await fs.readFile(HEAD_FILE, 'utf-8').catch(() => null);
|
|
25
|
+
if (!hash)
|
|
26
|
+
throw new Error('No HEAD commit found.');
|
|
27
|
+
for (let i = 0; i < steps; i++) {
|
|
28
|
+
const commit = await readCommit(hash);
|
|
29
|
+
if (!commit.parent)
|
|
30
|
+
throw new Error(`HEAD~${steps} does not exist (only ${i} ancestors).`);
|
|
31
|
+
hash = commit.parent;
|
|
32
|
+
}
|
|
33
|
+
return hash;
|
|
34
|
+
}
|
|
35
|
+
// Otherwise treat as a raw commit hash (prefix search)
|
|
36
|
+
if (ref.length < 40) {
|
|
37
|
+
const objects = await fs.readdir(OBJECTS_DIR).catch(() => []);
|
|
38
|
+
const match = objects.find(h => h.startsWith(ref));
|
|
39
|
+
if (!match)
|
|
40
|
+
throw new Error(`Commit '${ref}' not found.`);
|
|
41
|
+
return match;
|
|
42
|
+
}
|
|
43
|
+
return ref;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Tree loader — commit hash → { type: { name: sql } }
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
async function loadCommitSchema(commitHash) {
|
|
49
|
+
const schema = {};
|
|
50
|
+
for (const t of ALL_TYPES)
|
|
51
|
+
schema[t] = {};
|
|
52
|
+
const commit = await readCommit(commitHash);
|
|
53
|
+
const tree = await readTree(commit.tree);
|
|
54
|
+
for (const [relPath, hash] of Object.entries(tree)) {
|
|
55
|
+
const parts = relPath.split('/');
|
|
56
|
+
if (parts.length < 2)
|
|
57
|
+
continue;
|
|
58
|
+
const type = parts[0];
|
|
59
|
+
const name = path.basename(relPath, '.sql');
|
|
60
|
+
if (schema[type] !== undefined) {
|
|
61
|
+
schema[type][name] = await readObject(hash);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return schema;
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Local files → schema map
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
async function loadLocalSchema() {
|
|
70
|
+
const schema = {};
|
|
71
|
+
for (const type of ALL_TYPES) {
|
|
72
|
+
schema[type] = {};
|
|
73
|
+
const dir = path.join('supabase', type);
|
|
74
|
+
try {
|
|
75
|
+
const files = await fs.readdir(dir);
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (!file.endsWith('.sql'))
|
|
78
|
+
continue;
|
|
79
|
+
schema[type][path.basename(file, '.sql')] = await fs.readFile(path.join(dir, file), 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
return schema;
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Diff renderer
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function renderDiff(aSchema, bSchema, aLabel, bLabel, filterFiles) {
|
|
90
|
+
let hasDiff = false;
|
|
91
|
+
for (const type of ALL_TYPES) {
|
|
92
|
+
const keys = Array.from(new Set([
|
|
93
|
+
...Object.keys(aSchema[type] ?? {}),
|
|
94
|
+
...Object.keys(bSchema[type] ?? {})
|
|
95
|
+
])).sort();
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const filePath = `${type}/${key}.sql`;
|
|
98
|
+
if (filterFiles.length > 0 && !filterFiles.includes(filePath))
|
|
99
|
+
continue;
|
|
100
|
+
const aSql = aSchema[type]?.[key] ?? '';
|
|
101
|
+
const bSql = bSchema[type]?.[key] ?? '';
|
|
102
|
+
if (canonicalize(aSql) === canonicalize(bSql))
|
|
103
|
+
continue;
|
|
104
|
+
hasDiff = true;
|
|
105
|
+
console.log(chalk.bold(`\ndiff --git a/${filePath} b/${filePath}`));
|
|
106
|
+
const patch = diffLib.createTwoFilesPatch(`a/${filePath}`, `b/${filePath}`, aSql, bSql, aLabel, bLabel);
|
|
107
|
+
for (const line of patch.split('\n')) {
|
|
108
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
109
|
+
process.stdout.write(chalk.green(line) + '\n');
|
|
110
|
+
}
|
|
111
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
112
|
+
process.stdout.write(chalk.red(line) + '\n');
|
|
113
|
+
}
|
|
114
|
+
else if (line.startsWith('@@')) {
|
|
115
|
+
process.stdout.write(chalk.cyan(line) + '\n');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
process.stdout.write(line + '\n');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return hasDiff;
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Main command
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
10
128
|
export async function diff(argv) {
|
|
11
|
-
// 1. Check Config
|
|
12
129
|
try {
|
|
13
130
|
await fs.access(CONFIG_FILE);
|
|
14
131
|
}
|
|
@@ -17,109 +134,78 @@ export async function diff(argv) {
|
|
|
17
134
|
return;
|
|
18
135
|
}
|
|
19
136
|
const config = JSON.parse(await fs.readFile(CONFIG_FILE, 'utf-8'));
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const projectRef = currentBranch ? currentBranch.projectRef : config.projectRef;
|
|
137
|
+
const branchData = config.branches?.[config.currentBranch];
|
|
138
|
+
const projectRef = branchData?.projectRef ?? config.projectRef;
|
|
23
139
|
if (!projectRef) {
|
|
24
|
-
console.error(chalk.red('Project
|
|
140
|
+
console.error(chalk.red('Project ref not found in config.'));
|
|
25
141
|
return;
|
|
26
142
|
}
|
|
27
|
-
console.log(chalk.blue(`Fetching current database state...`));
|
|
28
|
-
const liveSchema = await extractSchema(projectRef);
|
|
29
|
-
let targetSchema = { tables: {}, functions: {}, views: {}, triggers: {}, policies: {}, types: {} };
|
|
30
|
-
let targetLabel = '';
|
|
31
|
-
// Filter files if provided
|
|
32
143
|
const filterFiles = argv.files || [];
|
|
33
|
-
//
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
144
|
+
// Positional args: could be one commit (vs live/local) or two commits
|
|
145
|
+
const args = (argv._ ?? []).filter((a) => typeof a === 'string' && a !== 'diff');
|
|
146
|
+
const commitArg1 = argv.commit ?? args[0];
|
|
147
|
+
const commitArg2 = args[1];
|
|
148
|
+
// ── Case 1: two commit hashes ──────────────────────────────────────────
|
|
149
|
+
if (commitArg1 && commitArg2) {
|
|
150
|
+
let hash1, hash2;
|
|
38
151
|
try {
|
|
39
|
-
|
|
40
|
-
const tree = await readTree(commit.tree);
|
|
41
|
-
for (const [relPath, hash] of Object.entries(tree)) {
|
|
42
|
-
const parts = relPath.split('/');
|
|
43
|
-
const type = parts[0];
|
|
44
|
-
const name = path.basename(relPath, '.sql');
|
|
45
|
-
const content = await readObject(hash);
|
|
46
|
-
if (targetSchema[type])
|
|
47
|
-
targetSchema[type][name] = content;
|
|
48
|
-
}
|
|
152
|
+
[hash1, hash2] = await Promise.all([resolveRef(commitArg1), resolveRef(commitArg2)]);
|
|
49
153
|
}
|
|
50
|
-
catch {
|
|
51
|
-
console.error(chalk.red(
|
|
154
|
+
catch (e) {
|
|
155
|
+
console.error(chalk.red(e.message));
|
|
52
156
|
return;
|
|
53
157
|
}
|
|
158
|
+
const [schemaA, schemaB] = await Promise.all([
|
|
159
|
+
loadCommitSchema(hash1),
|
|
160
|
+
loadCommitSchema(hash2)
|
|
161
|
+
]);
|
|
162
|
+
const label1 = `${commitArg1} (${hash1.substring(0, 7)})`;
|
|
163
|
+
const label2 = `${commitArg2} (${hash2.substring(0, 7)})`;
|
|
164
|
+
console.log(chalk.blue(`Comparing ${label1} → ${label2}`));
|
|
165
|
+
const hasDiff = renderDiff(schemaA, schemaB, label1, label2, filterFiles);
|
|
166
|
+
if (!hasDiff)
|
|
167
|
+
console.log(chalk.green('\nNo differences between the two commits.'));
|
|
168
|
+
return;
|
|
54
169
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const dir = path.join('supabase', type);
|
|
61
|
-
try {
|
|
62
|
-
const files = await fs.readdir(dir);
|
|
63
|
-
for (const file of files) {
|
|
64
|
-
if (!file.endsWith('.sql'))
|
|
65
|
-
continue;
|
|
66
|
-
const name = path.basename(file, '.sql');
|
|
67
|
-
// Un-sanitize name if needed?
|
|
68
|
-
// In our extractor, we replace '/' with '_' for local files.
|
|
69
|
-
// But for functions and tables they don't usually have '/'.
|
|
70
|
-
const content = await fs.readFile(path.join(dir, file), 'utf-8');
|
|
71
|
-
targetSchema[type][name] = content;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (e) {
|
|
75
|
-
// Directory doesn't exist, ignore
|
|
76
|
-
}
|
|
170
|
+
// ── Case 2: one commit (or HEAD~N) vs live DB / local files ───────────
|
|
171
|
+
if (commitArg1) {
|
|
172
|
+
let resolvedHash;
|
|
173
|
+
try {
|
|
174
|
+
resolvedHash = await resolveRef(commitArg1);
|
|
77
175
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const types = ['tables', 'functions', 'views', 'triggers', 'policies', 'types'];
|
|
82
|
-
for (const type of types) {
|
|
83
|
-
const keys = Array.from(new Set([
|
|
84
|
-
...Object.keys(liveSchema[type]),
|
|
85
|
-
...Object.keys(targetSchema[type])
|
|
86
|
-
])).sort();
|
|
87
|
-
for (const key of keys) {
|
|
88
|
-
const filePath = `${type}/${key}.sql`;
|
|
89
|
-
if (filterFiles.length > 0 && !filterFiles.includes(filePath))
|
|
90
|
-
continue;
|
|
91
|
-
const liveSql = liveSchema[type][key] || '';
|
|
92
|
-
const targetSql = targetSchema[type][key] || '';
|
|
93
|
-
if (canonicalize(liveSql) !== canonicalize(targetSql)) {
|
|
94
|
-
hasDifferences = true;
|
|
95
|
-
console.log(chalk.bold(`\ndiff --git a/${type}/${key}.sql b/${type}/${key}.sql`));
|
|
96
|
-
const patch = diffLib.createTwoFilesPatch(`a/${type}/${key}.sql`, `b/${type}/${key}.sql`, targetSql, liveSql, targetLabel, 'Live Database');
|
|
97
|
-
// Colorize patch output, skipping the first 4 header lines usually
|
|
98
|
-
const lines = patch.split('\n');
|
|
99
|
-
for (let i = 0; i < lines.length; i++) {
|
|
100
|
-
const line = lines[i];
|
|
101
|
-
if (i < 4) {
|
|
102
|
-
// Print headers in default or bold
|
|
103
|
-
console.log(chalk.white(line));
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
107
|
-
console.log(chalk.green(line));
|
|
108
|
-
}
|
|
109
|
-
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
110
|
-
console.log(chalk.red(line));
|
|
111
|
-
}
|
|
112
|
-
else if (line.startsWith('@@')) {
|
|
113
|
-
console.log(chalk.cyan(line));
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
console.log(line);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
console.error(chalk.red(e.message));
|
|
178
|
+
return;
|
|
120
179
|
}
|
|
180
|
+
const label = `${commitArg1} (${resolvedHash.substring(0, 7)})`;
|
|
181
|
+
if (argv.live) {
|
|
182
|
+
// vs live DB
|
|
183
|
+
console.log(chalk.blue(`Fetching live database state...`));
|
|
184
|
+
const liveSchema = await extractSchema(projectRef);
|
|
185
|
+
const commitSchema = await loadCommitSchema(resolvedHash);
|
|
186
|
+
const hasDiff = renderDiff(commitSchema, liveSchema, label, 'Live Database', filterFiles);
|
|
187
|
+
if (!hasDiff)
|
|
188
|
+
console.log(chalk.green(`\nNo differences. Live DB matches ${label}.`));
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// vs local files (default for one-commit comparison)
|
|
192
|
+
const [commitSchema, localSchema] = await Promise.all([
|
|
193
|
+
loadCommitSchema(resolvedHash),
|
|
194
|
+
loadLocalSchema()
|
|
195
|
+
]);
|
|
196
|
+
const hasDiff = renderDiff(commitSchema, localSchema, label, 'Local Files', filterFiles);
|
|
197
|
+
if (!hasDiff)
|
|
198
|
+
console.log(chalk.green(`\nNo differences. Local files match ${label}.`));
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
121
201
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
202
|
+
// ── Case 3: no commits — compare live DB vs local files (original behavior) ──
|
|
203
|
+
console.log(chalk.blue('Fetching current database state...'));
|
|
204
|
+
const [liveSchema, localSchema] = await Promise.all([
|
|
205
|
+
extractSchema(projectRef),
|
|
206
|
+
loadLocalSchema()
|
|
207
|
+
]);
|
|
208
|
+
const hasDiff = renderDiff(localSchema, liveSchema, 'Local Files', 'Live Database', filterFiles);
|
|
209
|
+
if (!hasDiff)
|
|
210
|
+
console.log(chalk.green('\nNo differences found. Live database matches local files exactly.'));
|
|
125
211
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -64,13 +64,18 @@ export async function init(argv = {}) {
|
|
|
64
64
|
};
|
|
65
65
|
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
66
66
|
console.log(chalk.green(`\nLinked to project: ${ref.trim()} (Branch: production)`));
|
|
67
|
-
// Create supabase directory structure
|
|
67
|
+
// Create supabase directory structure (all tracked schema types)
|
|
68
|
+
await fs.mkdir('supabase/extensions', { recursive: true });
|
|
69
|
+
await fs.mkdir('supabase/types', { recursive: true });
|
|
70
|
+
await fs.mkdir('supabase/sequences', { recursive: true });
|
|
68
71
|
await fs.mkdir('supabase/tables', { recursive: true });
|
|
69
|
-
await fs.mkdir('supabase/
|
|
72
|
+
await fs.mkdir('supabase/matviews', { recursive: true });
|
|
70
73
|
await fs.mkdir('supabase/views', { recursive: true });
|
|
71
|
-
await fs.mkdir('supabase/
|
|
74
|
+
await fs.mkdir('supabase/functions', { recursive: true });
|
|
72
75
|
await fs.mkdir('supabase/triggers', { recursive: true });
|
|
73
76
|
await fs.mkdir('supabase/policies', { recursive: true });
|
|
77
|
+
await fs.mkdir('supabase/grants', { recursive: true });
|
|
78
|
+
await fs.mkdir('supabase/publications', { recursive: true });
|
|
74
79
|
// Auto-ignore .gitbase
|
|
75
80
|
try {
|
|
76
81
|
const gitignorePath = '.gitignore';
|
package/dist/commands/log.js
CHANGED
|
@@ -1,30 +1,104 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { readCommit } from '../storage/git.js';
|
|
4
|
+
import { readCommit, readTree } from '../storage/git.js';
|
|
5
5
|
const GITBASE_DIR = '.gitbase';
|
|
6
6
|
const HEAD_FILE = path.join(GITBASE_DIR, 'HEAD');
|
|
7
|
-
export async function log() {
|
|
8
|
-
let
|
|
7
|
+
export async function log(argv = {}) {
|
|
8
|
+
let startHash = null;
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
startHash = await fs.readFile(HEAD_FILE, 'utf-8');
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
13
|
console.log(chalk.red('No commits yet.'));
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
if (!startHash) {
|
|
17
|
+
console.log(chalk.red('No commits yet.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Parse flags
|
|
21
|
+
const oneline = argv.oneline ?? argv['one-line'] ?? false;
|
|
22
|
+
const sinceStr = argv.since;
|
|
23
|
+
const filterFile = argv.file ?? argv._?.[0]; // e.g. tables/users.sql
|
|
24
|
+
const maxCount = parseInt(argv.n ?? argv['max-count'] ?? '0', 10) || 0;
|
|
25
|
+
// Parse --since into a Date threshold
|
|
26
|
+
let sinceDate = null;
|
|
27
|
+
if (sinceStr) {
|
|
28
|
+
sinceDate = new Date(sinceStr);
|
|
29
|
+
if (isNaN(sinceDate.getTime())) {
|
|
30
|
+
console.error(chalk.red(`Invalid --since value: "${sinceStr}". Use ISO date format, e.g. 2026-01-01`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!oneline) {
|
|
35
|
+
console.log(chalk.cyan('Commit History:'));
|
|
36
|
+
}
|
|
37
|
+
let currentHash = startHash;
|
|
38
|
+
let count = 0;
|
|
17
39
|
while (currentHash) {
|
|
40
|
+
let commit;
|
|
18
41
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
commit = await readCommit(currentHash);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const commitDate = new Date(commit.timestamp);
|
|
48
|
+
// --since filter
|
|
49
|
+
if (sinceDate && commitDate < sinceDate) {
|
|
24
50
|
currentHash = commit.parent;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// File filter: only include commits that touched the given file
|
|
54
|
+
if (filterFile) {
|
|
55
|
+
let touched = false;
|
|
56
|
+
try {
|
|
57
|
+
const tree = await readTree(commit.tree);
|
|
58
|
+
touched = Object.keys(tree).some(p => p === filterFile || p.endsWith('/' + filterFile));
|
|
59
|
+
// Also check parent tree to detect if file actually changed
|
|
60
|
+
if (touched && commit.parent) {
|
|
61
|
+
const parentCommit = await readCommit(commit.parent).catch(() => null);
|
|
62
|
+
if (parentCommit) {
|
|
63
|
+
const parentTree = (await readTree(parentCommit.tree).catch(() => ({})));
|
|
64
|
+
const fileHash = tree[filterFile] ?? Object.entries(tree).find(([p]) => p.endsWith('/' + filterFile))?.[1];
|
|
65
|
+
const parentHash = parentTree[filterFile] ?? Object.entries(parentTree).find(([p]) => p.endsWith('/' + filterFile))?.[1];
|
|
66
|
+
if (fileHash === parentHash)
|
|
67
|
+
touched = false; // file didn't actually change
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
if (!touched) {
|
|
73
|
+
currentHash = commit.parent;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Render
|
|
78
|
+
const shortHash = chalk.yellow(currentHash.substring(0, 7));
|
|
79
|
+
if (oneline) {
|
|
80
|
+
console.log(`${shortHash} ${commit.message}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk.yellow(`\ncommit ${currentHash}`));
|
|
84
|
+
console.log(chalk.gray(`Author: ${commit.author}`));
|
|
85
|
+
console.log(chalk.gray(`Date: ${commitDate.toLocaleString()}`));
|
|
86
|
+
console.log(`\n ${commit.message}`);
|
|
25
87
|
}
|
|
26
|
-
|
|
88
|
+
count++;
|
|
89
|
+
if (maxCount > 0 && count >= maxCount)
|
|
27
90
|
break;
|
|
91
|
+
currentHash = commit.parent;
|
|
92
|
+
}
|
|
93
|
+
if (count === 0) {
|
|
94
|
+
if (filterFile) {
|
|
95
|
+
console.log(chalk.yellow(`No commits found that touched '${filterFile}'.`));
|
|
96
|
+
}
|
|
97
|
+
else if (sinceDate) {
|
|
98
|
+
console.log(chalk.yellow(`No commits found since ${sinceStr}.`));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(chalk.yellow('No commits found.'));
|
|
28
102
|
}
|
|
29
103
|
}
|
|
30
104
|
}
|