resulgit 1.0.3 → 1.0.5
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 +4 -0
- package/lib/blame.js +79 -0
- package/lib/errors.js +213 -0
- package/lib/hooks.js +251 -0
- package/lib/log-viz.js +188 -0
- package/lib/validation.js +260 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ resulgit <command> [options]
|
|
|
39
39
|
|
|
40
40
|
### Clone & Workspace
|
|
41
41
|
|
|
42
|
+
- `resulgit init [--dir <path>] [--repo <id>] [--server <url>] [--branch <name>] [--token <token>]` - Initialize a new repository
|
|
42
43
|
- `resulgit clone --repo <id> --branch <name> [--dest <dir>]` - Clone a repository
|
|
43
44
|
- `resulgit workspace set-root --path <dir>` - Set workspace root directory
|
|
44
45
|
|
|
@@ -130,6 +131,9 @@ resulgit auth login --email user@example.com --password mypassword
|
|
|
130
131
|
# List repositories
|
|
131
132
|
resulgit repo list
|
|
132
133
|
|
|
134
|
+
# Initialize a new repository
|
|
135
|
+
resulgit init --repo 123 --branch main
|
|
136
|
+
|
|
133
137
|
# Clone a repository
|
|
134
138
|
resulgit clone --repo 123 --branch main
|
|
135
139
|
|
package/lib/blame.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Blame Implementation
|
|
3
|
+
* Shows line-by-line authorship information for files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse blame data from commit history
|
|
8
|
+
*/
|
|
9
|
+
function parseBlame(fileContent, commits, filePath) {
|
|
10
|
+
const lines = fileContent.split(/\r?\n/)
|
|
11
|
+
const blameData = []
|
|
12
|
+
|
|
13
|
+
// For simplicity, we'll attribute all lines to the most recent commit
|
|
14
|
+
// In a full implementation, we'd track line-by-line changes through history
|
|
15
|
+
const latestCommit = commits.length > 0 ? commits[0] : null
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
blameData.push({
|
|
19
|
+
lineNumber: i + 1,
|
|
20
|
+
commitId: latestCommit?.id || latestCommit?._id || 'uncommitted',
|
|
21
|
+
author: latestCommit?.author?.name || 'Unknown',
|
|
22
|
+
authorEmail: latestCommit?.author?.email || '',
|
|
23
|
+
date: latestCommit?.createdAt || latestCommit?.committer?.date || new Date().toISOString(),
|
|
24
|
+
content: lines[i]
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return blameData
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format blame output for terminal
|
|
33
|
+
*/
|
|
34
|
+
function formatBlameOutput(blameData, options = {}) {
|
|
35
|
+
const { colors = true } = options
|
|
36
|
+
const COLORS = {
|
|
37
|
+
reset: '\x1b[0m',
|
|
38
|
+
dim: '\x1b[2m',
|
|
39
|
+
cyan: '\x1b[36m',
|
|
40
|
+
yellow: '\x1b[33m',
|
|
41
|
+
green: '\x1b[32m'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lines = []
|
|
45
|
+
const maxLineNum = String(blameData.length).length
|
|
46
|
+
|
|
47
|
+
for (const line of blameData) {
|
|
48
|
+
const lineNum = String(line.lineNumber).padStart(maxLineNum, ' ')
|
|
49
|
+
const commitShort = line.commitId.slice(0, 8)
|
|
50
|
+
const author = line.author.slice(0, 20).padEnd(20, ' ')
|
|
51
|
+
const date = new Date(line.date).toISOString().split('T')[0]
|
|
52
|
+
|
|
53
|
+
if (colors) {
|
|
54
|
+
const formatted = `${COLORS.dim}${lineNum}${COLORS.reset} ` +
|
|
55
|
+
`${COLORS.yellow}${commitShort}${COLORS.reset} ` +
|
|
56
|
+
`${COLORS.cyan}${author}${COLORS.reset} ` +
|
|
57
|
+
`${COLORS.green}${date}${COLORS.reset} ` +
|
|
58
|
+
`${line.content}`
|
|
59
|
+
lines.push(formatted)
|
|
60
|
+
} else {
|
|
61
|
+
lines.push(`${lineNum} ${commitShort} ${author} ${date} ${line.content}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join('\n')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Format blame output as JSON
|
|
70
|
+
*/
|
|
71
|
+
function formatBlameJson(blameData) {
|
|
72
|
+
return JSON.stringify(blameData, null, 2)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
parseBlame,
|
|
77
|
+
formatBlameOutput,
|
|
78
|
+
formatBlameJson
|
|
79
|
+
}
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Classes for ResulGit
|
|
3
|
+
* Provides specific error types for better error handling and user feedback
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for ResulGit
|
|
8
|
+
*/
|
|
9
|
+
class ResulGitError extends Error {
|
|
10
|
+
constructor(message, code, details = {}) {
|
|
11
|
+
super(message)
|
|
12
|
+
this.name = 'ResulGitError'
|
|
13
|
+
this.code = code
|
|
14
|
+
this.details = details
|
|
15
|
+
Error.captureStackTrace(this, this.constructor)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toJSON() {
|
|
19
|
+
return {
|
|
20
|
+
error: this.name,
|
|
21
|
+
code: this.code,
|
|
22
|
+
message: this.message,
|
|
23
|
+
details: this.details
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validation error
|
|
30
|
+
*/
|
|
31
|
+
class ValidationError extends ResulGitError {
|
|
32
|
+
constructor(message, field) {
|
|
33
|
+
super(message, 'VALIDATION_ERROR', { field })
|
|
34
|
+
this.name = 'ValidationError'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Authentication error
|
|
40
|
+
*/
|
|
41
|
+
class AuthenticationError extends ResulGitError {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message, 'AUTH_ERROR')
|
|
44
|
+
this.name = 'AuthenticationError'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Repository not found error
|
|
50
|
+
*/
|
|
51
|
+
class RepositoryNotFoundError extends ResulGitError {
|
|
52
|
+
constructor(repoId) {
|
|
53
|
+
super(`Repository not found: ${repoId}`, 'REPO_NOT_FOUND', { repoId })
|
|
54
|
+
this.name = 'RepositoryNotFoundError'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Branch not found error
|
|
60
|
+
*/
|
|
61
|
+
class BranchNotFoundError extends ResulGitError {
|
|
62
|
+
constructor(branchName, repoId) {
|
|
63
|
+
super(`Branch not found: ${branchName}`, 'BRANCH_NOT_FOUND', { branchName, repoId })
|
|
64
|
+
this.name = 'BranchNotFoundError'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Commit not found error
|
|
70
|
+
*/
|
|
71
|
+
class CommitNotFoundError extends ResulGitError {
|
|
72
|
+
constructor(commitId) {
|
|
73
|
+
super(`Commit not found: ${commitId}`, 'COMMIT_NOT_FOUND', { commitId })
|
|
74
|
+
this.name = 'CommitNotFoundError'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Merge conflict error
|
|
80
|
+
*/
|
|
81
|
+
class ConflictError extends ResulGitError {
|
|
82
|
+
constructor(conflicts) {
|
|
83
|
+
const fileList = conflicts.map(c => c.path).join(', ')
|
|
84
|
+
super(`Merge conflicts in files: ${fileList}`, 'MERGE_CONFLICT', { conflicts })
|
|
85
|
+
this.name = 'ConflictError'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Network error
|
|
91
|
+
*/
|
|
92
|
+
class NetworkError extends ResulGitError {
|
|
93
|
+
constructor(message, statusCode, url) {
|
|
94
|
+
super(message, 'NETWORK_ERROR', { statusCode, url })
|
|
95
|
+
this.name = 'NetworkError'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* File system error
|
|
101
|
+
*/
|
|
102
|
+
class FileSystemError extends ResulGitError {
|
|
103
|
+
constructor(message, path, operation) {
|
|
104
|
+
super(message, 'FS_ERROR', { path, operation })
|
|
105
|
+
this.name = 'FileSystemError'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Configuration error
|
|
111
|
+
*/
|
|
112
|
+
class ConfigurationError extends ResulGitError {
|
|
113
|
+
constructor(message, configKey) {
|
|
114
|
+
super(message, 'CONFIG_ERROR', { configKey })
|
|
115
|
+
this.name = 'ConfigurationError'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remote repository error
|
|
121
|
+
*/
|
|
122
|
+
class RemoteError extends ResulGitError {
|
|
123
|
+
constructor(message) {
|
|
124
|
+
super(message, 'REMOTE_ERROR')
|
|
125
|
+
this.name = 'RemoteError'
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format error for display
|
|
131
|
+
*/
|
|
132
|
+
function formatError(err, jsonMode = false) {
|
|
133
|
+
if (jsonMode) {
|
|
134
|
+
if (err instanceof ResulGitError) {
|
|
135
|
+
return JSON.stringify(err.toJSON(), null, 2)
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify({
|
|
138
|
+
error: 'Error',
|
|
139
|
+
message: err.message,
|
|
140
|
+
code: err.code || 'UNKNOWN_ERROR'
|
|
141
|
+
}, null, 2)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Colorized output for terminal
|
|
145
|
+
const COLORS = {
|
|
146
|
+
red: '\x1b[31m',
|
|
147
|
+
yellow: '\x1b[33m',
|
|
148
|
+
reset: '\x1b[0m',
|
|
149
|
+
bold: '\x1b[1m'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let output = `${COLORS.bold}${COLORS.red}Error:${COLORS.reset} ${err.message}\n`
|
|
153
|
+
|
|
154
|
+
if (err instanceof ResulGitError && err.code) {
|
|
155
|
+
output += `${COLORS.yellow}Code:${COLORS.reset} ${err.code}\n`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (err instanceof ConflictError && err.details.conflicts) {
|
|
159
|
+
output += `${COLORS.yellow}Conflicts:${COLORS.reset}\n`
|
|
160
|
+
for (const conflict of err.details.conflicts) {
|
|
161
|
+
output += ` - ${conflict.path}${conflict.line ? `:${conflict.line}` : ''}\n`
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return output
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wrap async function with error handling
|
|
170
|
+
*/
|
|
171
|
+
function wrapAsync(fn) {
|
|
172
|
+
return async function (...args) {
|
|
173
|
+
try {
|
|
174
|
+
return await fn(...args)
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof ResulGitError) {
|
|
177
|
+
throw err
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Convert common errors to ResulGit errors
|
|
181
|
+
if (err.code === 'ENOENT') {
|
|
182
|
+
throw new FileSystemError(`File or directory not found: ${err.path}`, err.path, 'read')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (err.code === 'EACCES') {
|
|
186
|
+
throw new FileSystemError(`Permission denied: ${err.path}`, err.path, 'access')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (err.message && err.message.includes('fetch failed')) {
|
|
190
|
+
throw new NetworkError(err.message, null, null)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Re-throw unknown errors
|
|
194
|
+
throw err
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
ResulGitError,
|
|
201
|
+
ValidationError,
|
|
202
|
+
AuthenticationError,
|
|
203
|
+
RepositoryNotFoundError,
|
|
204
|
+
BranchNotFoundError,
|
|
205
|
+
CommitNotFoundError,
|
|
206
|
+
ConflictError,
|
|
207
|
+
NetworkError,
|
|
208
|
+
FileSystemError,
|
|
209
|
+
ConfigurationError,
|
|
210
|
+
RemoteError,
|
|
211
|
+
formatError,
|
|
212
|
+
wrapAsync
|
|
213
|
+
}
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Hooks System
|
|
3
|
+
* Allows execution of custom scripts at specific points in the Git workflow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const { exec } = require('child_process')
|
|
9
|
+
const util = require('util')
|
|
10
|
+
const execPromise = util.promisify(exec)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Available hook types
|
|
14
|
+
*/
|
|
15
|
+
const HOOK_TYPES = [
|
|
16
|
+
'pre-commit',
|
|
17
|
+
'post-commit',
|
|
18
|
+
'pre-push',
|
|
19
|
+
'post-push',
|
|
20
|
+
'pre-merge',
|
|
21
|
+
'post-merge',
|
|
22
|
+
'pre-checkout',
|
|
23
|
+
'post-checkout'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get hooks directory path
|
|
28
|
+
*/
|
|
29
|
+
function getHooksDir(repoDir) {
|
|
30
|
+
return path.join(repoDir, '.git', 'hooks')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a hook exists
|
|
35
|
+
*/
|
|
36
|
+
async function hookExists(repoDir, hookName) {
|
|
37
|
+
if (!HOOK_TYPES.includes(hookName)) {
|
|
38
|
+
throw new Error(`Invalid hook type: ${hookName}. Valid types: ${HOOK_TYPES.join(', ')}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hooksDir = getHooksDir(repoDir)
|
|
42
|
+
const hookPath = path.join(hooksDir, hookName)
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const stat = await fs.stat(hookPath)
|
|
46
|
+
return stat.isFile()
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute a hook
|
|
54
|
+
*/
|
|
55
|
+
async function executeHook(repoDir, hookName, context = {}) {
|
|
56
|
+
if (!await hookExists(repoDir, hookName)) {
|
|
57
|
+
return { executed: false, output: null, error: null }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hookPath = path.join(getHooksDir(repoDir), hookName)
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Check if file is executable
|
|
64
|
+
const stat = await fs.stat(hookPath)
|
|
65
|
+
const isExecutable = (stat.mode & 0o111) !== 0
|
|
66
|
+
|
|
67
|
+
if (!isExecutable) {
|
|
68
|
+
return { executed: false, output: null, error: 'Hook is not executable' }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Pass context as environment variables
|
|
72
|
+
const env = {
|
|
73
|
+
...process.env,
|
|
74
|
+
RESULGIT_HOOK: hookName,
|
|
75
|
+
RESULGIT_REPO_DIR: repoDir,
|
|
76
|
+
...Object.fromEntries(
|
|
77
|
+
Object.entries(context).map(([k, v]) => [`RESULGIT_${k.toUpperCase()}`, String(v)])
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Execute the hook
|
|
82
|
+
const { stdout, stderr } = await execPromise(hookPath, {
|
|
83
|
+
cwd: repoDir,
|
|
84
|
+
env,
|
|
85
|
+
timeout: 60000 // 60 second timeout
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
executed: true,
|
|
90
|
+
output: stdout,
|
|
91
|
+
error: stderr || null,
|
|
92
|
+
exitCode: 0
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
executed: true,
|
|
97
|
+
output: err.stdout || '',
|
|
98
|
+
error: err.stderr || err.message,
|
|
99
|
+
exitCode: err.code || 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Install a hook
|
|
106
|
+
*/
|
|
107
|
+
async function installHook(repoDir, hookName, script) {
|
|
108
|
+
if (!HOOK_TYPES.includes(hookName)) {
|
|
109
|
+
throw new Error(`Invalid hook type: ${hookName}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hooksDir = getHooksDir(repoDir)
|
|
113
|
+
await fs.mkdir(hooksDir, { recursive: true })
|
|
114
|
+
|
|
115
|
+
const hookPath = path.join(hooksDir, hookName)
|
|
116
|
+
|
|
117
|
+
// Ensure script has shebang
|
|
118
|
+
let finalScript = script
|
|
119
|
+
if (!script.startsWith('#!')) {
|
|
120
|
+
finalScript = '#!/bin/sh\n' + script
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await fs.writeFile(hookPath, finalScript, { mode: 0o755 })
|
|
124
|
+
|
|
125
|
+
return { installed: true, path: hookPath }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove a hook
|
|
130
|
+
*/
|
|
131
|
+
async function removeHook(repoDir, hookName) {
|
|
132
|
+
if (!HOOK_TYPES.includes(hookName)) {
|
|
133
|
+
throw new Error(`Invalid hook type: ${hookName}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const hookPath = path.join(getHooksDir(repoDir), hookName)
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await fs.unlink(hookPath)
|
|
140
|
+
return { removed: true }
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err.code === 'ENOENT') {
|
|
143
|
+
return { removed: false, error: 'Hook does not exist' }
|
|
144
|
+
}
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* ListAll hooks
|
|
151
|
+
*/
|
|
152
|
+
async function listHooks(repoDir) {
|
|
153
|
+
const hooksDir = getHooksDir(repoDir)
|
|
154
|
+
const hooks = []
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const files = await fs.readdir(hooksDir)
|
|
158
|
+
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
if (HOOK_TYPES.includes(file)) {
|
|
161
|
+
const hookPath = path.join(hooksDir, file)
|
|
162
|
+
const stat = await fs.stat(hookPath)
|
|
163
|
+
const isExecutable = (stat.mode & 0o111) !== 0
|
|
164
|
+
|
|
165
|
+
hooks.push({
|
|
166
|
+
name: file,
|
|
167
|
+
path: hookPath,
|
|
168
|
+
executable: isExecutable,
|
|
169
|
+
size: stat.size
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (err.code !== 'ENOENT') {
|
|
175
|
+
throw err
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return hooks
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Read hook content
|
|
184
|
+
*/
|
|
185
|
+
async function readHook(repoDir, hookName) {
|
|
186
|
+
if (!HOOK_TYPES.includes(hookName)) {
|
|
187
|
+
throw new Error(`Invalid hook type: ${hookName}`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const hookPath = path.join(getHooksDir(repoDir), hookName)
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const content = await fs.readFile(hookPath, 'utf8')
|
|
194
|
+
return { content, path: hookPath }
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err.code === 'ENOENT') {
|
|
197
|
+
return { content: null, error: 'Hook does not exist' }
|
|
198
|
+
}
|
|
199
|
+
throw err
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Sample hooks for common use cases
|
|
205
|
+
*/
|
|
206
|
+
const SAMPLE_HOOKS = {
|
|
207
|
+
'pre-commit': `#!/bin/sh
|
|
208
|
+
# Pre-commit hook: Run linting before commit
|
|
209
|
+
echo "Running pre-commit checks..."
|
|
210
|
+
|
|
211
|
+
# Example: Run linter
|
|
212
|
+
# npm run lint
|
|
213
|
+
# if [ $? -ne 0 ]; then
|
|
214
|
+
# echo "Linting failed. Commit aborted."
|
|
215
|
+
# exit 1
|
|
216
|
+
# fi
|
|
217
|
+
|
|
218
|
+
echo "Pre-commit checks passed!"
|
|
219
|
+
exit 0
|
|
220
|
+
`,
|
|
221
|
+
'post-commit': `#!/bin/sh
|
|
222
|
+
# Post-commit hook: Log commit info
|
|
223
|
+
echo "Commit completed: $(git log -1 --pretty=%B)"
|
|
224
|
+
exit 0
|
|
225
|
+
`,
|
|
226
|
+
'pre-push': `#!/bin/sh
|
|
227
|
+
# Pre-push hook: Run tests before push
|
|
228
|
+
echo "Running tests before push..."
|
|
229
|
+
|
|
230
|
+
# Example: Run tests
|
|
231
|
+
# npm test
|
|
232
|
+
# if [ $? -ne 0 ]; then
|
|
233
|
+
# echo "Tests failed. Push aborted."
|
|
234
|
+
# exit 1
|
|
235
|
+
# fi
|
|
236
|
+
|
|
237
|
+
echo "Tests passed!"
|
|
238
|
+
exit 0
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
HOOK_TYPES,
|
|
244
|
+
hookExists,
|
|
245
|
+
executeHook,
|
|
246
|
+
installHook,
|
|
247
|
+
removeHook,
|
|
248
|
+
listHooks,
|
|
249
|
+
readHook,
|
|
250
|
+
SAMPLE_HOOKS
|
|
251
|
+
}
|
package/lib/log-viz.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Log Visualization
|
|
3
|
+
* Creates ASCII graph visualization of commit history
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate ASCII graph for commit history
|
|
8
|
+
*/
|
|
9
|
+
function generateLogGraph(commits, options = {}) {
|
|
10
|
+
const { includeGraph = true, colors = true, maxCommits = 50 } = options
|
|
11
|
+
|
|
12
|
+
const COLORS = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
yellow: '\x1b[33m',
|
|
15
|
+
cyan: '\x1b[36m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
blue: '\x1b[34m',
|
|
18
|
+
red: '\x1b[31m',
|
|
19
|
+
dim: '\x1b[2m',
|
|
20
|
+
bold: '\x1b[1m'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const limitedCommits = commits.slice(0, maxCommits)
|
|
24
|
+
const lines = []
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < limitedCommits.length; i++) {
|
|
27
|
+
const commit = limitedCommits[i]
|
|
28
|
+
const isFirst = i === 0
|
|
29
|
+
const isLast = i === limitedCommits.length - 1
|
|
30
|
+
|
|
31
|
+
// Graph symbols
|
|
32
|
+
let graph = ''
|
|
33
|
+
if (includeGraph) {
|
|
34
|
+
if (isFirst) {
|
|
35
|
+
graph = '* '
|
|
36
|
+
} else if (isLast) {
|
|
37
|
+
graph = '* '
|
|
38
|
+
} else {
|
|
39
|
+
graph = '* '
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add branch lines
|
|
43
|
+
if (!isLast) {
|
|
44
|
+
graph = `${graph}`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Commit ID
|
|
49
|
+
const commitId = (commit.id || commit._id || '').slice(0, 7)
|
|
50
|
+
const coloredId = colors ? `${COLORS.yellow}${commitId}${COLORS.reset}` : commitId
|
|
51
|
+
|
|
52
|
+
// Author
|
|
53
|
+
const author = commit.author?.name || 'Unknown'
|
|
54
|
+
const authorShort = author.slice(0, 20)
|
|
55
|
+
const coloredAuthor = colors ? `${COLORS.cyan}${authorShort}${COLORS.reset}` : authorShort
|
|
56
|
+
|
|
57
|
+
// Date
|
|
58
|
+
const date = commit.createdAt || commit.committer?.date || ''
|
|
59
|
+
const dateObj = new Date(date)
|
|
60
|
+
const relativeDate = getRelativeDate(dateObj)
|
|
61
|
+
const coloredDate = colors ? `${COLORS.green}${relativeDate}${COLORS.reset}` : relativeDate
|
|
62
|
+
|
|
63
|
+
// Message
|
|
64
|
+
const message = (commit.message || 'No message').split('\n')[0].slice(0, 80)
|
|
65
|
+
const coloredMessage = colors ? `${COLORS.bold}${message}${COLORS.reset}` : message
|
|
66
|
+
|
|
67
|
+
// Branch tags (if any)
|
|
68
|
+
const tags = []
|
|
69
|
+
if (commit.branches && commit.branches.length > 0) {
|
|
70
|
+
tags.push(...commit.branches.map(b => `branch: ${b}`))
|
|
71
|
+
}
|
|
72
|
+
if (commit.tags && commit.tags.length > 0) {
|
|
73
|
+
tags.push(...commit.tags.map(t => `tag: ${t}`))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let tagStr = ''
|
|
77
|
+
if (tags.length > 0) {
|
|
78
|
+
const tagText = tags.join(', ')
|
|
79
|
+
tagStr = colors ? ` ${COLORS.red}(${tagText})${COLORS.reset}` : ` (${tagText})`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Construct line
|
|
83
|
+
const line = `${graph}${coloredId} - ${coloredMessage}${tagStr} ${COLORS.dim}${coloredDate} ${coloredAuthor}${COLORS.reset}`
|
|
84
|
+
lines.push(line)
|
|
85
|
+
|
|
86
|
+
// Add connecting line if not last
|
|
87
|
+
if (!isLast && includeGraph) {
|
|
88
|
+
lines.push('|')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (commits.length > maxCommits) {
|
|
93
|
+
lines.push('')
|
|
94
|
+
lines.push(`... ${commits.length - maxCommits} more commits`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return lines.join('\n')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get relative date string
|
|
102
|
+
*/
|
|
103
|
+
function getRelativeDate(date) {
|
|
104
|
+
const now = new Date()
|
|
105
|
+
const diffMs = now - date
|
|
106
|
+
const diffSecs = Math.floor(diffMs / 1000)
|
|
107
|
+
const diffMins = Math.floor(diffSecs / 60)
|
|
108
|
+
const diffHours = Math.floor(diffMins / 60)
|
|
109
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
110
|
+
const diffWeeks = Math.floor(diffDays / 7)
|
|
111
|
+
const diffMonths = Math.floor(diffDays / 30)
|
|
112
|
+
const diffYears = Math.floor(diffDays / 365)
|
|
113
|
+
|
|
114
|
+
if (diffYears > 0) return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`
|
|
115
|
+
if (diffMonths > 0) return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`
|
|
116
|
+
if (diffWeeks > 0) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`
|
|
117
|
+
if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`
|
|
118
|
+
if (diffHours > 0) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`
|
|
119
|
+
if (diffMins > 0) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`
|
|
120
|
+
return 'just now'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format compact log (one line per commit)
|
|
125
|
+
*/
|
|
126
|
+
function formatCompactLog(commits, colors = true) {
|
|
127
|
+
const COLORS = {
|
|
128
|
+
reset: '\x1b[0m',
|
|
129
|
+
yellow: '\x1b[33m',
|
|
130
|
+
dim: '\x1b[2m'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return commits.map(commit => {
|
|
134
|
+
const id = (commit.id || commit._id || '').slice(0, 7)
|
|
135
|
+
const message = (commit.message || 'No message').split('\n')[0].slice(0, 60)
|
|
136
|
+
|
|
137
|
+
if (colors) {
|
|
138
|
+
return `${COLORS.yellow}${id}${COLORS.reset} ${message}`
|
|
139
|
+
}
|
|
140
|
+
return `${id} ${message}`
|
|
141
|
+
}).join('\n')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate commit statistics
|
|
146
|
+
*/
|
|
147
|
+
function generateCommitStats(commits) {
|
|
148
|
+
const stats = {
|
|
149
|
+
totalCommits: commits.length,
|
|
150
|
+
authors: {},
|
|
151
|
+
datesRange: { earliest: null, latest: null },
|
|
152
|
+
commitsPerDay: {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const commit of commits) {
|
|
156
|
+
// Author stats
|
|
157
|
+
const authorName = commit.author?.name || 'Unknown'
|
|
158
|
+
stats.authors[authorName] = (stats.authors[authorName] || 0) + 1
|
|
159
|
+
|
|
160
|
+
// Date stats
|
|
161
|
+
const date = new Date(commit.createdAt || commit.committer?.date || new Date())
|
|
162
|
+
if (!stats.datesRange.earliest || date < stats.datesRange.earliest) {
|
|
163
|
+
stats.datesRange.earliest = date
|
|
164
|
+
}
|
|
165
|
+
if (!stats.datesRange.latest || date > stats.datesRange.latest) {
|
|
166
|
+
stats.datesRange.latest = date
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Commits per day
|
|
170
|
+
const dayKey = date.toISOString().split('T')[0]
|
|
171
|
+
stats.commitsPerDay[dayKey] = (stats.commitsPerDay[dayKey] || 0) + 1
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Sort authors by commit count
|
|
175
|
+
stats.topAuthors = Object.entries(stats.authors)
|
|
176
|
+
.sort(([, a], [, b]) => b - a)
|
|
177
|
+
.slice(0, 5)
|
|
178
|
+
.map(([name, count]) => ({ name, commits: count }))
|
|
179
|
+
|
|
180
|
+
return stats
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
generateLogGraph,
|
|
185
|
+
formatCompactLog,
|
|
186
|
+
generateCommitStats,
|
|
187
|
+
getRelativeDate
|
|
188
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validation Module
|
|
3
|
+
* Provides validation functions for user inputs to prevent security issues
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates repository ID format
|
|
10
|
+
*/
|
|
11
|
+
function validateRepoId(repoId) {
|
|
12
|
+
if (!repoId || typeof repoId !== 'string') {
|
|
13
|
+
throw new Error('Invalid repository ID: must be a non-empty string')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Allow alphanumeric, hyphens, underscores, and UUIDs
|
|
17
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/
|
|
18
|
+
if (!validPattern.test(repoId)) {
|
|
19
|
+
throw new Error('Invalid repository ID: contains invalid characters')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (repoId.length > 100) {
|
|
23
|
+
throw new Error('Invalid repository ID: too long (max 100 characters)')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return repoId.trim()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validates branch name format
|
|
31
|
+
*/
|
|
32
|
+
function validateBranchName(branchName) {
|
|
33
|
+
if (!branchName || typeof branchName !== 'string') {
|
|
34
|
+
throw new Error('Invalid branch name: must be a non-empty string')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const trimmed = branchName.trim()
|
|
38
|
+
|
|
39
|
+
// Git branch naming rules
|
|
40
|
+
const invalidPatterns = [
|
|
41
|
+
/\.\./, // No double dots
|
|
42
|
+
/\/$/, // Cannot end with /
|
|
43
|
+
/^\//, // Cannot start with /
|
|
44
|
+
/\@\{/, // No @{
|
|
45
|
+
/\\/, // No backslash
|
|
46
|
+
/[\x00-\x1f\x7f]/, // No control characters
|
|
47
|
+
/[\s~^:?*\[]/, // No whitespace or special chars
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
for (const pattern of invalidPatterns) {
|
|
51
|
+
if (pattern.test(trimmed)) {
|
|
52
|
+
throw new Error(`Invalid branch name: "${branchName}" violates Git naming rules`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (trimmed.length > 255) {
|
|
57
|
+
throw new Error('Invalid branch name: too long (max 255 characters)')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return trimmed
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates file path to prevent path traversal attacks
|
|
65
|
+
*/
|
|
66
|
+
function validateFilePath(filePath, allowAbsolute = false) {
|
|
67
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
68
|
+
throw new Error('Invalid file path: must be a non-empty string')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normalized = path.normalize(filePath)
|
|
72
|
+
|
|
73
|
+
// Prevent path traversal
|
|
74
|
+
if (normalized.includes('..')) {
|
|
75
|
+
throw new Error('Invalid file path: path traversal detected')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for absolute paths if not allowed
|
|
79
|
+
if (!allowAbsolute && path.isAbsolute(normalized)) {
|
|
80
|
+
throw new Error('Invalid file path: absolute paths not allowed')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Prevent null bytes
|
|
84
|
+
if (normalized.includes('\0')) {
|
|
85
|
+
throw new Error('Invalid file path: null byte detected')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check length
|
|
89
|
+
if (normalized.length > 4096) {
|
|
90
|
+
throw new Error('Invalid file path: too long (max 4096 characters)')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return normalized
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validates commit message
|
|
98
|
+
*/
|
|
99
|
+
function validateCommitMessage(message) {
|
|
100
|
+
if (!message || typeof message !== 'string') {
|
|
101
|
+
throw new Error('Invalid commit message: must be a non-empty string')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const trimmed = message.trim()
|
|
105
|
+
|
|
106
|
+
if (trimmed.length === 0) {
|
|
107
|
+
throw new Error('Invalid commit message: cannot be empty or whitespace only')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (trimmed.length > 10000) {
|
|
111
|
+
throw new Error('Invalid commit message: too long (max 10000 characters)')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return trimmed
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validates email address format
|
|
119
|
+
*/
|
|
120
|
+
function validateEmail(email) {
|
|
121
|
+
if (!email || typeof email !== 'string') {
|
|
122
|
+
throw new Error('Invalid email: must be a non-empty string')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trimmed = email.trim()
|
|
126
|
+
|
|
127
|
+
// Basic email validation
|
|
128
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
129
|
+
if (!emailPattern.test(trimmed)) {
|
|
130
|
+
throw new Error('Invalid email format')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (trimmed.length > 254) {
|
|
134
|
+
throw new Error('Invalid email: too long (max 254 characters)')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return trimmed
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates URL format
|
|
142
|
+
*/
|
|
143
|
+
function validateUrl(url) {
|
|
144
|
+
if (!url || typeof url !== 'string') {
|
|
145
|
+
throw new Error('Invalid URL: must be a non-empty string')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const parsed = new URL(url)
|
|
150
|
+
|
|
151
|
+
// Only allow http and https
|
|
152
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
153
|
+
throw new Error('Invalid URL: only HTTP and HTTPS protocols are allowed')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return url
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw new Error(`Invalid URL format: ${err.message}`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates username format
|
|
164
|
+
*/
|
|
165
|
+
function validateUsername(username) {
|
|
166
|
+
if (!username || typeof username !== 'string') {
|
|
167
|
+
throw new Error('Invalid username: must be a non-empty string')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const trimmed = username.trim()
|
|
171
|
+
|
|
172
|
+
// Alphanumeric, hyphens, underscores only
|
|
173
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/
|
|
174
|
+
if (!validPattern.test(trimmed)) {
|
|
175
|
+
throw new Error('Invalid username: only alphanumeric characters, hyphens, and underscores allowed')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (trimmed.length < 3) {
|
|
179
|
+
throw new Error('Invalid username: too short (min 3 characters)')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (trimmed.length > 39) {
|
|
183
|
+
throw new Error('Invalid username: too long (max 39 characters)')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return trimmed
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validates commit ID (hash) format
|
|
191
|
+
*/
|
|
192
|
+
function validateCommitId(commitId) {
|
|
193
|
+
if (!commitId || typeof commitId !== 'string') {
|
|
194
|
+
throw new Error('Invalid commit ID: must be a non-empty string')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const trimmed = commitId.trim()
|
|
198
|
+
|
|
199
|
+
// Allow HEAD or valid hex hash
|
|
200
|
+
if (trimmed.toLowerCase() === 'head') {
|
|
201
|
+
return trimmed.toUpperCase()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// SHA-1 hash (40 chars) or short hash (7-40 chars)
|
|
205
|
+
const hashPattern = /^[a-f0-9]{7,40}$/i
|
|
206
|
+
if (!hashPattern.test(trimmed)) {
|
|
207
|
+
throw new Error('Invalid commit ID: must be "HEAD" or a valid commit hash')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return trimmed
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Validates repository name
|
|
215
|
+
*/
|
|
216
|
+
function validateRepoName(name) {
|
|
217
|
+
if (!name || typeof name !== 'string') {
|
|
218
|
+
throw new Error('Invalid repository name: must be a non-empty string')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const trimmed = name.trim()
|
|
222
|
+
|
|
223
|
+
if (trimmed.length < 1) {
|
|
224
|
+
throw new Error('Invalid repository name: cannot be empty')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (trimmed.length > 100) {
|
|
228
|
+
throw new Error('Invalid repository name: too long (max 100 characters)')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Prevent path traversal in repo names
|
|
232
|
+
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
|
|
233
|
+
throw new Error('Invalid repository name: cannot contain path separators or ".."')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return trimmed
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Sanitizes text input for display
|
|
241
|
+
*/
|
|
242
|
+
function sanitizeText(text) {
|
|
243
|
+
if (typeof text !== 'string') return String(text)
|
|
244
|
+
|
|
245
|
+
// Remove control characters except newline and tab
|
|
246
|
+
return text.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
validateRepoId,
|
|
251
|
+
validateBranchName,
|
|
252
|
+
validateFilePath,
|
|
253
|
+
validateCommitMessage,
|
|
254
|
+
validateEmail,
|
|
255
|
+
validateUrl,
|
|
256
|
+
validateUsername,
|
|
257
|
+
validateCommitId,
|
|
258
|
+
validateRepoName,
|
|
259
|
+
sanitizeText
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resulgit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A powerful CLI tool for version control system operations - clone, commit, push, pull, merge, branch management, and more",
|
|
5
5
|
"main": "resulgit.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"resulgit.js",
|
|
37
|
-
"README.md"
|
|
37
|
+
"README.md",
|
|
38
|
+
"lib"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"ora": "^5.4.1"
|
|
@@ -42,4 +43,4 @@
|
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"jest": "^29.7.0"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|