UNPKG

@thinkeloquent/github-sdk-repos

Version:

GitHub Repository API client and CLI - Full-featured repository management

473 lines (398 loc) 14.2 kB
/** * @fileoverview Repository API endpoints * @module api/repositories */ import { validateRepositoryName, validateUsername, validateRepository, validatePagination, validateSort } from '../utils/validation.mjs'; import { NotFoundError, ValidationError } from '../utils/errors.mjs'; /** * Get a repository */ export async function get(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); return await httpClient.get(`/repos/${owner}/${repo}`); } /** * List repositories for a user */ export async function listForUser(httpClient, username, options = {}) { validateUsername(username); validatePagination(options); validateSort(options, ['created', 'updated', 'pushed', 'full_name']); const params = new URLSearchParams({ type: options.type || 'all', sort: options.sort || 'updated', direction: options.direction || 'desc', page: options.page || 1, per_page: Math.min(options.per_page || 30, 100) }); return await httpClient.get(`/users/${username}/repos?${params.toString()}`); } /** * List repositories for the authenticated user */ export async function listForAuthenticatedUser(httpClient, options = {}) { validatePagination(options); validateSort(options, ['created', 'updated', 'pushed', 'full_name']); const params = new URLSearchParams({ visibility: options.visibility || 'all', affiliation: options.affiliation || 'owner,collaborator,organization_member', type: options.type || 'all', sort: options.sort || 'updated', direction: options.direction || 'desc', page: options.page || 1, per_page: Math.min(options.per_page || 30, 100) }); return await httpClient.get(`/user/repos?${params.toString()}`); } /** * List organization repositories */ export async function listForOrg(httpClient, org, options = {}) { validateUsername(org); validatePagination(options); validateSort(options, ['created', 'updated', 'pushed', 'full_name']); const params = new URLSearchParams({ type: options.type || 'all', sort: options.sort || 'updated', direction: options.direction || 'desc', page: options.page || 1, per_page: Math.min(options.per_page || 30, 100) }); return await httpClient.get(`/orgs/${org}/repos?${params.toString()}`); } /** * Create a repository for the authenticated user */ export async function create(httpClient, repoData) { validateRepository(repoData); const payload = { name: repoData.name, description: repoData.description || null, homepage: repoData.homepage || null, private: repoData.private || false, has_issues: repoData.has_issues !== undefined ? repoData.has_issues : true, has_projects: repoData.has_projects !== undefined ? repoData.has_projects : true, has_wiki: repoData.has_wiki !== undefined ? repoData.has_wiki : true, has_discussions: repoData.has_discussions || false, is_template: repoData.is_template || false, auto_init: repoData.auto_init || false, gitignore_template: repoData.gitignore_template || null, license_template: repoData.license_template || null, allow_squash_merge: repoData.allow_squash_merge !== undefined ? repoData.allow_squash_merge : true, allow_merge_commit: repoData.allow_merge_commit !== undefined ? repoData.allow_merge_commit : true, allow_rebase_merge: repoData.allow_rebase_merge !== undefined ? repoData.allow_rebase_merge : true, allow_auto_merge: repoData.allow_auto_merge || false, delete_branch_on_merge: repoData.delete_branch_on_merge || false, allow_update_branch: repoData.allow_update_branch || false, squash_merge_commit_title: repoData.squash_merge_commit_title || null, squash_merge_commit_message: repoData.squash_merge_commit_message || null, merge_commit_title: repoData.merge_commit_title || null, merge_commit_message: repoData.merge_commit_message || null }; // Add topics if provided if (repoData.topics && Array.isArray(repoData.topics)) { // Topics are set separately after creation const repository = await httpClient.post('/user/repos', payload); await replaceAllTopics(httpClient, repository.owner.login, repository.name, repoData.topics); return repository; } return await httpClient.post('/user/repos', payload); } /** * Create a repository in an organization */ export async function createInOrg(httpClient, org, repoData) { validateUsername(org); validateRepository(repoData); const payload = { name: repoData.name, description: repoData.description || null, homepage: repoData.homepage || null, private: repoData.private || false, visibility: repoData.visibility || (repoData.private ? 'private' : 'public'), has_issues: repoData.has_issues !== undefined ? repoData.has_issues : true, has_projects: repoData.has_projects !== undefined ? repoData.has_projects : true, has_wiki: repoData.has_wiki !== undefined ? repoData.has_wiki : true, has_discussions: repoData.has_discussions || false, is_template: repoData.is_template || false, team_id: repoData.team_id || null, auto_init: repoData.auto_init || false, gitignore_template: repoData.gitignore_template || null, license_template: repoData.license_template || null, allow_squash_merge: repoData.allow_squash_merge !== undefined ? repoData.allow_squash_merge : true, allow_merge_commit: repoData.allow_merge_commit !== undefined ? repoData.allow_merge_commit : true, allow_rebase_merge: repoData.allow_rebase_merge !== undefined ? repoData.allow_rebase_merge : true, allow_auto_merge: repoData.allow_auto_merge || false, delete_branch_on_merge: repoData.delete_branch_on_merge || false, allow_update_branch: repoData.allow_update_branch || false }; const repository = await httpClient.post(`/orgs/${org}/repos`, payload); // Add topics if provided if (repoData.topics && Array.isArray(repoData.topics)) { await replaceAllTopics(httpClient, org, repository.name, repoData.topics); } return repository; } /** * Update a repository */ export async function update(httpClient, owner, repo, updates) { validateUsername(owner); validateRepositoryName(repo); // Validate the updates if (updates.name) { validateRepositoryName(updates.name); } const payload = { ...updates }; // Remove topics from payload as they need special handling if (payload.topics) { const topics = payload.topics; delete payload.topics; const repository = await httpClient.patch(`/repos/${owner}/${repo}`, payload); await replaceAllTopics(httpClient, owner, repo, topics); return repository; } return await httpClient.patch(`/repos/${owner}/${repo}`, payload); } /** * Delete a repository */ export async function deleteRepo(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); await httpClient.delete(`/repos/${owner}/${repo}`); return { message: 'Repository deleted successfully' }; } /** * Get repository topics */ export async function getAllTopics(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); return await httpClient.get(`/repos/${owner}/${repo}/topics`); } /** * Replace all repository topics */ export async function replaceAllTopics(httpClient, owner, repo, topics) { validateUsername(owner); validateRepositoryName(repo); if (!Array.isArray(topics)) { throw new ValidationError('Topics must be an array', 'topics', topics); } if (topics.length > 20) { throw new ValidationError('Cannot have more than 20 topics', 'topics', topics.length); } // Validate each topic for (const topic of topics) { if (typeof topic !== 'string') { throw new ValidationError('All topics must be strings', 'topics', topic); } if (topic.length > 50) { throw new ValidationError(`Topic "${topic}" must be 50 characters or less`, 'topics', topic); } if (!/^[a-z0-9-]+$/.test(topic)) { throw new ValidationError(`Topic "${topic}" can only contain lowercase letters, numbers, and hyphens`, 'topics', topic); } } return await httpClient.put(`/repos/${owner}/${repo}/topics`, { names: topics }); } /** * Get repository languages */ export async function getLanguages(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); return await httpClient.get(`/repos/${owner}/${repo}/languages`); } /** * Get repository contributors */ export async function getContributors(httpClient, owner, repo, options = {}) { validateUsername(owner); validateRepositoryName(repo); validatePagination(options); const params = new URLSearchParams({ anon: options.anon ? '1' : '0', page: options.page || 1, per_page: Math.min(options.per_page || 30, 100) }); return await httpClient.get(`/repos/${owner}/${repo}/contributors?${params.toString()}`); } /** * Get repository statistics */ export async function getStats(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); const [ languages, contributors, commits, participation ] = await Promise.allSettled([ getLanguages(httpClient, owner, repo), getContributors(httpClient, owner, repo), httpClient.get(`/repos/${owner}/${repo}/stats/commit_activity`), httpClient.get(`/repos/${owner}/${repo}/stats/participation`) ]); return { languages: languages.status === 'fulfilled' ? languages.value : null, contributors: contributors.status === 'fulfilled' ? contributors.value : null, commits: commits.status === 'fulfilled' ? commits.value : null, participation: participation.status === 'fulfilled' ? participation.value : null }; } /** * Fork a repository */ export async function fork(httpClient, owner, repo, options = {}) { validateUsername(owner); validateRepositoryName(repo); const payload = {}; if (options.organization) { validateUsername(options.organization); payload.organization = options.organization; } if (options.name) { validateRepositoryName(options.name); payload.name = options.name; } if (options.default_branch_only !== undefined) { payload.default_branch_only = options.default_branch_only; } return await httpClient.post(`/repos/${owner}/${repo}/forks`, payload); } /** * List forks of a repository */ export async function listForks(httpClient, owner, repo, options = {}) { validateUsername(owner); validateRepositoryName(repo); validatePagination(options); validateSort(options, ['newest', 'oldest', 'stargazers', 'watchers']); const params = new URLSearchParams({ sort: options.sort || 'newest', page: options.page || 1, per_page: Math.min(options.per_page || 30, 100) }); return await httpClient.get(`/repos/${owner}/${repo}/forks?${params.toString()}`); } /** * Transfer repository ownership */ export async function transfer(httpClient, owner, repo, newOwner, teamIds = []) { validateUsername(owner); validateRepositoryName(repo); validateUsername(newOwner); const payload = { new_owner: newOwner }; if (Array.isArray(teamIds) && teamIds.length > 0) { payload.team_ids = teamIds; } return await httpClient.post(`/repos/${owner}/${repo}/transfer`, payload); } /** * Check if repository is starred by authenticated user */ export async function checkIfStarred(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); try { await httpClient.get(`/user/starred/${owner}/${repo}`); return true; } catch (error) { if (error.statusCode === 404) { return false; } throw error; } } /** * Star a repository */ export async function star(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); await httpClient.put(`/user/starred/${owner}/${repo}`); return { message: 'Repository starred successfully' }; } /** * Unstar a repository */ export async function unstar(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); await httpClient.delete(`/user/starred/${owner}/${repo}`); return { message: 'Repository unstarred successfully' }; } /** * Check if repository is watched by authenticated user */ export async function checkIfWatched(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); try { const response = await httpClient.get(`/repos/${owner}/${repo}/subscription`); return response.subscribed === true; } catch (error) { if (error.statusCode === 404) { return false; } throw error; } } /** * Watch a repository */ export async function watch(httpClient, owner, repo, subscribed = true, ignored = false) { validateUsername(owner); validateRepositoryName(repo); const payload = { subscribed, ignored }; return await httpClient.put(`/repos/${owner}/${repo}/subscription`, payload); } /** * Unwatch a repository */ export async function unwatch(httpClient, owner, repo) { validateUsername(owner); validateRepositoryName(repo); await httpClient.delete(`/repos/${owner}/${repo}/subscription`); return { message: 'Repository unwatched successfully' }; } /** * Generate repository name suggestions */ export function generateNameSuggestions(baseName) { const suggestions = []; const timestamp = Date.now().toString(36); // Basic variations suggestions.push(baseName); suggestions.push(`${baseName}-project`); suggestions.push(`${baseName}-app`); suggestions.push(`${baseName}-api`); suggestions.push(`${baseName}-cli`); // With timestamp/random suggestions.push(`${baseName}-${timestamp}`); suggestions.push(`${baseName}-${Math.random().toString(36).substr(2, 4)}`); // Common patterns suggestions.push(`my-${baseName}`); suggestions.push(`awesome-${baseName}`); suggestions.push(`${baseName}-toolkit`); suggestions.push(`${baseName}-utils`); return suggestions.filter(name => { try { validateRepositoryName(name); return true; } catch { return false; } }); }