Update from Git Manager GUI
This commit is contained in:
@@ -305,11 +305,366 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
COMMIT HISTORY FUNCTIONS (GITEA)
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit history from Gitea repository
|
||||||
|
*/
|
||||||
|
async function getGiteaCommits({ token, url, owner, repo, branch = 'main', page = 1, limit = 50 }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
params: {
|
||||||
|
sha: branch,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaCommits error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific commit with diff
|
||||||
|
*/
|
||||||
|
async function getGiteaCommit({ token, url, owner, repo, sha }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaCommit error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit diff/patch
|
||||||
|
*/
|
||||||
|
async function getGiteaCommitDiff({ token, url, owner, repo, sha }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
// Gitea returns diff in the commit endpoint with .diff extension
|
||||||
|
const endpoint = `${base}/${owner}/${repo}/commit/${sha}.diff`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaCommitDiff error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit file changes/stats
|
||||||
|
*/
|
||||||
|
async function getGiteaCommitFiles({ token, url, owner, repo, sha }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/commits/${sha}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gitea commit response includes stats
|
||||||
|
const commit = response.data;
|
||||||
|
|
||||||
|
// Normalize files to match local git format
|
||||||
|
const files = (commit.files || []).map(f => ({
|
||||||
|
file: f.filename || f.file || '',
|
||||||
|
changes: (f.additions || 0) + (f.deletions || 0),
|
||||||
|
insertions: f.additions || 0,
|
||||||
|
deletions: f.deletions || 0,
|
||||||
|
binary: f.binary || false
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
stats: commit.stats || { additions: 0, deletions: 0, total: 0 }
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaCommitFiles error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search commits in Gitea repository
|
||||||
|
*/
|
||||||
|
async function searchGiteaCommits({ token, url, owner, repo, query, branch = 'main' }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
// Gitea doesn't have direct commit search, so we get commits and filter
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
params: {
|
||||||
|
sha: branch,
|
||||||
|
limit: 100 // Get more for searching
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter commits by query
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const filtered = response.data.filter(commit => {
|
||||||
|
const message = (commit.commit?.message || '').toLowerCase();
|
||||||
|
const author = (commit.commit?.author?.name || '').toLowerCase();
|
||||||
|
return message.includes(lowerQuery) || author.includes(lowerQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('searchGiteaCommits error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get branches for branch graph visualization
|
||||||
|
*/
|
||||||
|
async function getGiteaBranches({ token, url, owner, repo }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaBranches error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
RELEASE MANAGEMENT FUNCTIONS
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all releases for a repository
|
||||||
|
*/
|
||||||
|
async function listGiteaReleases({ token, url, owner, repo }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('listGiteaReleases error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific release by tag
|
||||||
|
*/
|
||||||
|
async function getGiteaRelease({ token, url, owner, repo, tag }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getGiteaRelease error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new release
|
||||||
|
*/
|
||||||
|
async function createGiteaRelease({ token, url, owner, repo, data }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
tag_name: data.tag_name,
|
||||||
|
name: data.name || data.tag_name,
|
||||||
|
body: data.body || '',
|
||||||
|
draft: data.draft || false,
|
||||||
|
prerelease: data.prerelease || false,
|
||||||
|
target_commitish: data.target_commitish || 'main'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('createGiteaRelease error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit/update an existing release
|
||||||
|
*/
|
||||||
|
async function editGiteaRelease({ token, url, owner, repo, releaseId, data }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
|
||||||
|
|
||||||
|
const body = {};
|
||||||
|
if (data.name !== undefined) body.name = data.name;
|
||||||
|
if (data.body !== undefined) body.body = data.body;
|
||||||
|
if (data.draft !== undefined) body.draft = data.draft;
|
||||||
|
if (data.prerelease !== undefined) body.prerelease = data.prerelease;
|
||||||
|
if (data.tag_name !== undefined) body.tag_name = data.tag_name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.patch(endpoint, body, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('editGiteaRelease error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a release
|
||||||
|
*/
|
||||||
|
async function deleteGiteaRelease({ token, url, owner, repo, releaseId }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('deleteGiteaRelease error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a release asset (attachment)
|
||||||
|
* Note: Gitea uses multipart/form-data for asset uploads
|
||||||
|
*/
|
||||||
|
async function uploadReleaseAsset({ token, url, owner, repo, releaseId, filePath, fileName }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const FormData = require('form-data');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/${releaseId}/assets`;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachment', fs.createReadStream(filePath), {
|
||||||
|
filename: fileName || require('path').basename(filePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
...formData.getHeaders()
|
||||||
|
},
|
||||||
|
maxContentLength: Infinity,
|
||||||
|
maxBodyLength: Infinity
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('uploadReleaseAsset error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a release asset
|
||||||
|
*/
|
||||||
|
async function deleteReleaseAsset({ token, url, owner, repo, assetId }) {
|
||||||
|
const base = normalizeBase(url);
|
||||||
|
if (!base) throw new Error('Invalid Gitea base URL');
|
||||||
|
|
||||||
|
const endpoint = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/assets/${assetId}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete(endpoint, {
|
||||||
|
headers: { Authorization: `token ${token}` }
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
console.error('deleteReleaseAsset error:', err.response?.data || err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createRepoGitHub,
|
createRepoGitHub,
|
||||||
createRepoGitea,
|
createRepoGitea,
|
||||||
listGiteaRepos,
|
listGiteaRepos,
|
||||||
getGiteaRepoContents,
|
getGiteaRepoContents,
|
||||||
getGiteaFileContent,
|
getGiteaFileContent,
|
||||||
uploadGiteaFile
|
uploadGiteaFile,
|
||||||
|
// Commit History
|
||||||
|
getGiteaCommits,
|
||||||
|
getGiteaCommit,
|
||||||
|
getGiteaCommitDiff,
|
||||||
|
getGiteaCommitFiles,
|
||||||
|
searchGiteaCommits,
|
||||||
|
getGiteaBranches,
|
||||||
|
// Release Management
|
||||||
|
listGiteaReleases,
|
||||||
|
getGiteaRelease,
|
||||||
|
createGiteaRelease,
|
||||||
|
editGiteaRelease,
|
||||||
|
deleteGiteaRelease,
|
||||||
|
uploadReleaseAsset,
|
||||||
|
deleteReleaseAsset
|
||||||
};
|
};
|
||||||
@@ -54,4 +54,282 @@ async function getCommitLogs(folderPath, count = 50) {
|
|||||||
return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`);
|
return log.all.map(c => `${c.hash.substring(0,7)} - ${c.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { initRepo, commitAndPush, getBranches, getCommitLogs };
|
/* ================================
|
||||||
|
EXTENDED COMMIT HISTORY FUNCTIONS
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed commit history with full information
|
||||||
|
*/
|
||||||
|
async function getDetailedCommitHistory(folderPath, options = {}) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
const logOptions = {
|
||||||
|
maxCount: options.limit || 100,
|
||||||
|
file: options.file || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.author) {
|
||||||
|
logOptions['--author'] = options.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.since) {
|
||||||
|
logOptions['--since'] = options.since;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.until) {
|
||||||
|
logOptions['--until'] = options.until;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.grep) {
|
||||||
|
logOptions['--grep'] = options.grep;
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = await git.log(logOptions);
|
||||||
|
|
||||||
|
return log.all.map(commit => ({
|
||||||
|
hash: commit.hash,
|
||||||
|
shortHash: commit.hash.substring(0, 7),
|
||||||
|
author: commit.author_name,
|
||||||
|
authorEmail: commit.author_email,
|
||||||
|
date: commit.date,
|
||||||
|
message: commit.message,
|
||||||
|
body: commit.body,
|
||||||
|
refs: commit.refs || '',
|
||||||
|
parentHashes: commit.parent || []
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit diff for a specific commit
|
||||||
|
*/
|
||||||
|
async function getCommitDiff(folderPath, commitHash) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get diff from parent to this commit
|
||||||
|
const diff = await git.diff([`${commitHash}~1`, commitHash]);
|
||||||
|
return diff;
|
||||||
|
} catch (error) {
|
||||||
|
// If no parent (first commit), show diff from empty tree
|
||||||
|
try {
|
||||||
|
const diff = await git.diff(['--root', commitHash]);
|
||||||
|
return diff;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error getting diff:', err);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file changes for a specific commit
|
||||||
|
*/
|
||||||
|
async function getCommitFileChanges(folderPath, commitHash) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const diff = await git.diffSummary([`${commitHash}~1`, commitHash]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: diff.files.map(file => ({
|
||||||
|
file: file.file,
|
||||||
|
changes: file.changes,
|
||||||
|
insertions: file.insertions,
|
||||||
|
deletions: file.deletions,
|
||||||
|
binary: file.binary || false
|
||||||
|
})),
|
||||||
|
insertions: diff.insertions,
|
||||||
|
deletions: diff.deletions,
|
||||||
|
changed: diff.changed
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// First commit case
|
||||||
|
try {
|
||||||
|
const diff = await git.diffSummary(['--root', commitHash]);
|
||||||
|
return {
|
||||||
|
files: diff.files.map(file => ({
|
||||||
|
file: file.file,
|
||||||
|
changes: file.changes,
|
||||||
|
insertions: file.insertions,
|
||||||
|
deletions: file.deletions,
|
||||||
|
binary: file.binary || false
|
||||||
|
})),
|
||||||
|
insertions: diff.insertions,
|
||||||
|
deletions: diff.deletions,
|
||||||
|
changed: diff.changed
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error getting file changes:', err);
|
||||||
|
return { files: [], insertions: 0, deletions: 0, changed: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed commit info including stats
|
||||||
|
*/
|
||||||
|
async function getCommitDetails(folderPath, commitHash) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const log = await git.log({ from: commitHash, to: commitHash, maxCount: 1 });
|
||||||
|
|
||||||
|
if (log.all.length === 0) {
|
||||||
|
throw new Error('Commit not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const commit = log.all[0];
|
||||||
|
const fileChanges = await getCommitFileChanges(folderPath, commitHash);
|
||||||
|
const diff = await getCommitDiff(folderPath, commitHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: commit.hash,
|
||||||
|
shortHash: commit.hash.substring(0, 7),
|
||||||
|
author: commit.author_name,
|
||||||
|
authorEmail: commit.author_email,
|
||||||
|
date: commit.date,
|
||||||
|
message: commit.message,
|
||||||
|
body: commit.body,
|
||||||
|
refs: commit.refs || '',
|
||||||
|
parentHashes: commit.parent || [],
|
||||||
|
fileChanges,
|
||||||
|
diff
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting commit details:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get branch graph data for visualization
|
||||||
|
*/
|
||||||
|
async function getBranchGraph(folderPath, limit = 50) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all branches
|
||||||
|
const branches = await git.branchLocal();
|
||||||
|
const allBranches = branches.all;
|
||||||
|
|
||||||
|
// Get graph structure
|
||||||
|
const log = await git.log({
|
||||||
|
maxCount: limit,
|
||||||
|
'--all': null,
|
||||||
|
'--graph': null,
|
||||||
|
'--decorate': null,
|
||||||
|
'--oneline': null
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
branches: allBranches,
|
||||||
|
current: branches.current,
|
||||||
|
commits: log.all
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting branch graph:', error);
|
||||||
|
return { branches: [], current: '', commits: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search commits by message, author, or content
|
||||||
|
*/
|
||||||
|
async function searchCommits(folderPath, query, options = {}) {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
const logOptions = {
|
||||||
|
maxCount: options.limit || 100,
|
||||||
|
'--all': null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search in commit message
|
||||||
|
if (options.searchMessage !== false) {
|
||||||
|
logOptions['--grep'] = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by author
|
||||||
|
if (options.author) {
|
||||||
|
logOptions['--author'] = options.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case insensitive
|
||||||
|
if (options.caseInsensitive !== false) {
|
||||||
|
logOptions['--regexp-ignore-case'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const log = await git.log(logOptions);
|
||||||
|
|
||||||
|
return log.all.map(commit => ({
|
||||||
|
hash: commit.hash,
|
||||||
|
shortHash: commit.hash.substring(0, 7),
|
||||||
|
author: commit.author_name,
|
||||||
|
authorEmail: commit.author_email,
|
||||||
|
date: commit.date,
|
||||||
|
message: commit.message,
|
||||||
|
body: commit.body,
|
||||||
|
refs: commit.refs || ''
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching commits:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get commit statistics
|
||||||
|
*/
|
||||||
|
async function getCommitStats(folderPath, since = '1 month ago') {
|
||||||
|
const git = gitFor(folderPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const log = await git.log({
|
||||||
|
'--since': since,
|
||||||
|
'--all': null
|
||||||
|
});
|
||||||
|
|
||||||
|
const authorStats = {};
|
||||||
|
const dailyStats = {};
|
||||||
|
|
||||||
|
log.all.forEach(commit => {
|
||||||
|
// Author stats
|
||||||
|
const author = commit.author_name;
|
||||||
|
if (!authorStats[author]) {
|
||||||
|
authorStats[author] = { commits: 0, email: commit.author_email };
|
||||||
|
}
|
||||||
|
authorStats[author].commits++;
|
||||||
|
|
||||||
|
// Daily stats
|
||||||
|
const date = new Date(commit.date).toISOString().split('T')[0];
|
||||||
|
if (!dailyStats[date]) {
|
||||||
|
dailyStats[date] = 0;
|
||||||
|
}
|
||||||
|
dailyStats[date]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCommits: log.all.length,
|
||||||
|
authors: authorStats,
|
||||||
|
daily: dailyStats
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting commit stats:', error);
|
||||||
|
return { totalCommits: 0, authors: {}, daily: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initRepo,
|
||||||
|
commitAndPush,
|
||||||
|
getBranches,
|
||||||
|
getCommitLogs,
|
||||||
|
getDetailedCommitHistory,
|
||||||
|
getCommitDiff,
|
||||||
|
getCommitFileChanges,
|
||||||
|
getCommitDetails,
|
||||||
|
getBranchGraph,
|
||||||
|
searchCommits,
|
||||||
|
getCommitStats
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user