refactor(server): consolidate git utilities into dedicated module with documentation (#435)
* refactor:move_git_service_module_to_lib_git * refactor:move_git_credentials_module_to_lib_git * refactor:move_git_identity_storage_module_to_lib_git * refactor:add_git_domain_entrypoint_reexports * refactor:update_server_git_imports_to_domain_entrypoint * refactor:update_github_repo_git_import_to_domain_entrypoint * chore:remove_legacy_git_service_module_path * chore:remove_legacy_git_credentials_module_path * chore:remove_legacy_git_identity_storage_module_path * docs:add_git_module_documentation_in_domain_folder * docs:add_git_module_to_agents_documentation_map
This commit is contained in:
@@ -37,6 +37,10 @@ Server-side integration modules used by API routes and runtime services.
|
|||||||
Quota provider registry, dispatch, and provider integrations for usage endpoints.
|
Quota provider registry, dispatch, and provider integrations for usage endpoints.
|
||||||
- Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md`
|
- Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md`
|
||||||
|
|
||||||
|
##### git
|
||||||
|
Git repository operations for the web server runtime.
|
||||||
|
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
|
||||||
|
|
||||||
## Build / dev commands (verified)
|
## Build / dev commands (verified)
|
||||||
All scripts are in `package.json`.
|
All scripts are in `package.json`.
|
||||||
- Validate: `bun run type-check`, `bun run lint`
|
- Validate: `bun run type-check`, `bun run lint`
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ const resolveWorkspacePathFromWorktrees = async (targetPath, baseDirectory) => {
|
|||||||
const resolvedBase = path.resolve(baseDirectory || os.homedir());
|
const resolvedBase = path.resolve(baseDirectory || os.homedir());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { getWorktrees } = await import('./lib/git-service.js');
|
const { getWorktrees } = await import('./lib/git/index.js');
|
||||||
const worktrees = await getWorktrees(resolvedBase);
|
const worktrees = await getWorktrees(resolvedBase);
|
||||||
|
|
||||||
for (const worktree of worktrees) {
|
for (const worktree of worktrees) {
|
||||||
@@ -6712,7 +6712,7 @@ async function main(options = {}) {
|
|||||||
const { scanSkillsRepository } = await import('./lib/skills-catalog/scan.js');
|
const { scanSkillsRepository } = await import('./lib/skills-catalog/scan.js');
|
||||||
const { installSkillsFromRepository } = await import('./lib/skills-catalog/install.js');
|
const { installSkillsFromRepository } = await import('./lib/skills-catalog/install.js');
|
||||||
const { scanClawdHubPage, installSkillsFromClawdHub, isClawdHubSource } = await import('./lib/skills-catalog/clawdhub/index.js');
|
const { scanClawdHubPage, installSkillsFromClawdHub, isClawdHubSource } = await import('./lib/skills-catalog/clawdhub/index.js');
|
||||||
const { getProfiles, getProfile } = await import('./lib/git-identity-storage.js');
|
const { getProfiles, getProfile } = await import('./lib/git/index.js');
|
||||||
|
|
||||||
const listGitIdentitiesForResponse = () => {
|
const listGitIdentitiesForResponse = () => {
|
||||||
try {
|
try {
|
||||||
@@ -7491,7 +7491,7 @@ async function main(options = {}) {
|
|||||||
let headOwnerForSearch = null;
|
let headOwnerForSearch = null;
|
||||||
|
|
||||||
// First, check the branch's tracking info to see which remote it's on
|
// First, check the branch's tracking info to see which remote it's on
|
||||||
const { getStatus } = await import('./lib/git-service.js');
|
const { getStatus } = await import('./lib/git/index.js');
|
||||||
const status = await getStatus(directory).catch(() => null);
|
const status = await getStatus(directory).catch(() => null);
|
||||||
if (status?.tracking) {
|
if (status?.tracking) {
|
||||||
const trackingRemote = status.tracking.split('/')[0];
|
const trackingRemote = status.tracking.split('/')[0];
|
||||||
@@ -7744,7 +7744,7 @@ async function main(options = {}) {
|
|||||||
// Determine the source remote for the head branch
|
// Determine the source remote for the head branch
|
||||||
// Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
|
// Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
|
||||||
let sourceRemote = headRemote;
|
let sourceRemote = headRemote;
|
||||||
const { getStatus, getRemotes } = await import('./lib/git-service.js');
|
const { getStatus, getRemotes } = await import('./lib/git/index.js');
|
||||||
|
|
||||||
// If no explicit headRemote, check the branch's tracking info
|
// If no explicit headRemote, check the branch's tracking info
|
||||||
if (!sourceRemote) {
|
if (!sourceRemote) {
|
||||||
@@ -8744,11 +8744,7 @@ async function main(options = {}) {
|
|||||||
let gitLibraries = null;
|
let gitLibraries = null;
|
||||||
const getGitLibraries = async () => {
|
const getGitLibraries = async () => {
|
||||||
if (!gitLibraries) {
|
if (!gitLibraries) {
|
||||||
const [storage, service] = await Promise.all([
|
gitLibraries = await import('./lib/git/index.js');
|
||||||
import('./lib/git-identity-storage.js'),
|
|
||||||
import('./lib/git-service.js')
|
|
||||||
]);
|
|
||||||
gitLibraries = { ...storage, ...service };
|
|
||||||
}
|
}
|
||||||
return gitLibraries;
|
return gitLibraries;
|
||||||
};
|
};
|
||||||
@@ -8813,7 +8809,7 @@ async function main(options = {}) {
|
|||||||
|
|
||||||
app.get('/api/git/discover-credentials', async (req, res) => {
|
app.get('/api/git/discover-credentials', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { discoverGitCredentials } = await import('./lib/git-credentials.js');
|
const { discoverGitCredentials } = await import('./lib/git/index.js');
|
||||||
const credentials = discoverGitCredentials();
|
const credentials = discoverGitCredentials();
|
||||||
res.json(credentials);
|
res.json(credentials);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Git Module Documentation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This module provides Git repository operations for the web server runtime, including repository management, branch/worktree operations, status/diff queries, commit handling, and merge/rebase workflows.
|
||||||
|
|
||||||
|
## Entrypoints and structure
|
||||||
|
- `packages/web/server/lib/git/`: Git module directory containing all Git-related functionality.
|
||||||
|
- `index.js`: Public API entry point imported by `packages/web/server/index.js`.
|
||||||
|
- `service.js`: Core Git operations (repository, branch, worktree, commit, merge/rebase, status/diff, log).
|
||||||
|
- `credentials.js`: Git credentials management.
|
||||||
|
- `identity-storage.js`: Git identity (user.name, user.email) storage.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
The following functions are exported and used by the web server:
|
||||||
|
|
||||||
|
### Repository Operations
|
||||||
|
- `isGitRepository(directory)`: Check if a directory is a Git repository.
|
||||||
|
- `getGlobalIdentity()`: Get global Git user.name, user.email, and core.sshCommand.
|
||||||
|
- `getCurrentIdentity(directory)`: Get local Git identity (fallback to global if not set locally).
|
||||||
|
- `hasLocalIdentity(directory)`: Check if local Git identity is configured.
|
||||||
|
- `setLocalIdentity(directory, profile)`: Set local Git identity (userName, userEmail, authType, sshKey/host).
|
||||||
|
- `getRemoteUrl(directory, remoteName)`: Get URL for a specific remote.
|
||||||
|
|
||||||
|
### Status and Diff Operations
|
||||||
|
- `getStatus(directory)`: Get comprehensive Git status including current branch, tracking, ahead/behind, file changes, diff stats, merge/rebase state.
|
||||||
|
- `getDiff(directory, { path, staged, contextLines })`: Get diff output for files or entire working tree.
|
||||||
|
- `getRangeDiff(directory, { base, head, path, contextLines })`: Get diff between two refs.
|
||||||
|
- `getRangeFiles(directory, { base, head })`: Get list of changed files between two refs.
|
||||||
|
- `getFileDiff(directory, { path, staged })`: Get original and modified file contents for a single file (handles images as data URLs).
|
||||||
|
- `collectDiffs(directory, files)`: Collect diff output for multiple files.
|
||||||
|
- `revertFile(directory, filePath)`: Revert a file to HEAD state.
|
||||||
|
|
||||||
|
### Branch Operations
|
||||||
|
- `getBranches(directory)`: Get list of local and remote branches (filtered to active remote branches).
|
||||||
|
- `createBranch(directory, branchName, options)`: Create and checkout a new branch.
|
||||||
|
- `checkoutBranch(directory, branchName)`: Checkout an existing branch.
|
||||||
|
- `deleteBranch(directory, branch, options)`: Delete a branch (supports force flag).
|
||||||
|
- `renameBranch(directory, oldName, newName)`: Rename a branch and preserve upstream tracking.
|
||||||
|
- `getRemotes(directory)`: Get list of configured remotes.
|
||||||
|
|
||||||
|
### Worktree Operations
|
||||||
|
- `getWorktrees(directory)`: List all git worktrees for a repository.
|
||||||
|
- `validateWorktreeCreate(directory, input)`: Validate worktree creation parameters (mode, branchName, startRef, upstream config).
|
||||||
|
- `createWorktree(directory, input)`: Create a new worktree (supports 'new' and 'existing' modes, upstream setup).
|
||||||
|
- `removeWorktree(directory, input)`: Remove a worktree (optionally delete local branch).
|
||||||
|
- `isLinkedWorktree(directory)`: Check if directory is a linked worktree (not primary).
|
||||||
|
|
||||||
|
### Commit and Remote Operations
|
||||||
|
- `commit(directory, message, options)`: Create a commit (supports addAll or specific files).
|
||||||
|
- `pull(directory, options)`: Pull changes from remote.
|
||||||
|
- `push(directory, options)`: Push changes to remote (auto-sets upstream if needed).
|
||||||
|
- `fetch(directory, options)`: Fetch changes from remote.
|
||||||
|
- `deleteRemoteBranch(directory, options)`: Delete a remote branch.
|
||||||
|
|
||||||
|
### Log Operations
|
||||||
|
- `getLog(directory, options)`: Get commit history with stats (supports maxCount, from, to, file filters).
|
||||||
|
- `getCommitFiles(directory, commitHash)`: Get file changes for a specific commit.
|
||||||
|
|
||||||
|
### Merge and Rebase Operations
|
||||||
|
- `rebase(directory, options)`: Start a rebase onto a target branch.
|
||||||
|
- `abortRebase(directory)`: Abort an in-progress rebase.
|
||||||
|
- `continueRebase(directory)`: Continue a rebase after conflict resolution.
|
||||||
|
- `merge(directory, options)`: Merge a branch into current branch.
|
||||||
|
- `abortMerge(directory)`: Abort an in-progress merge.
|
||||||
|
- `continueMerge(directory)`: Continue a merge after conflict resolution.
|
||||||
|
- `getConflictDetails(directory)`: Get detailed conflict information including operation type, unmerged files, and diff.
|
||||||
|
|
||||||
|
### Stash Operations
|
||||||
|
- `stash(directory, options)`: Stash changes (supports message and includeUntracked options).
|
||||||
|
- `stashPop(directory)`: Pop and apply the most recent stash.
|
||||||
|
|
||||||
|
## Internal Helpers
|
||||||
|
|
||||||
|
The following functions are internal helpers used by exported functions:
|
||||||
|
- `buildSshCommand(sshKeyPath)`: Build SSH command string for git config.
|
||||||
|
- `buildGitEnv()`: Build Git environment with SSH_AUTH_SOCK resolution.
|
||||||
|
- `createGit(directory)`: Create simple-git instance with environment.
|
||||||
|
- `normalizeDirectoryPath(value)`: Normalize directory paths (supports ~ expansion).
|
||||||
|
- `cleanBranchName(branch)`: Remove refs/heads/ or refs/ prefixes.
|
||||||
|
- `parseWorktreePorcelain(raw)`: Parse `git worktree list --porcelain` output.
|
||||||
|
- `resolveWorktreeProjectContext(directory)`: Resolve project context (projectID, primaryWorktree, worktreeRoot).
|
||||||
|
- `resolveCandidateDirectory(...)`: Generate unique worktree directory candidates.
|
||||||
|
- `resolveBranchForExistingMode(...)`: Resolve branch for existing-mode worktree creation.
|
||||||
|
- `applyUpstreamConfiguration(...)`: Set upstream tracking for new branches.
|
||||||
|
- And various other internal helpers for Git command execution and parsing.
|
||||||
|
|
||||||
|
## Response Contracts
|
||||||
|
|
||||||
|
### Status Response
|
||||||
|
- `current`: Current branch name.
|
||||||
|
- `tracking`: Upstream branch (e.g., 'origin/main').
|
||||||
|
- `ahead`: Number of commits ahead of upstream.
|
||||||
|
- `behind`: Number of commits behind upstream.
|
||||||
|
- `files`: Array of file objects with `path`, `index`, `working_dir` status codes.
|
||||||
|
- `isClean`: Boolean indicating if working tree is clean.
|
||||||
|
- `diffStats`: Object mapping file paths to `{ insertions, deletions }`.
|
||||||
|
- `mergeInProgress`: Object with `{ head, message }` if merge in progress.
|
||||||
|
- `rebaseInProgress`: Object with `{ headName, onto }` if rebase in progress.
|
||||||
|
|
||||||
|
### Worktree Create/Remove Response
|
||||||
|
- `head`: HEAD commit SHA.
|
||||||
|
- `name`: Worktree name.
|
||||||
|
- `branch`: Local branch name.
|
||||||
|
- `path`: Absolute path to worktree directory.
|
||||||
|
|
||||||
|
### Log Response
|
||||||
|
- `all`: Array of commit objects with hash, date, message, author info, stats.
|
||||||
|
- `latest`: Latest commit object or null.
|
||||||
|
- `total`: Total number of commits.
|
||||||
|
|
||||||
|
## Notes for Contributors
|
||||||
|
|
||||||
|
### Adding a New Git Operation
|
||||||
|
1. Add the function to `packages/web/server/lib/git/service.js`.
|
||||||
|
2. Export the function if it's part of the public API.
|
||||||
|
3. Use `createGit(directory)` to get a simple-git instance with the correct environment.
|
||||||
|
4. Use `runGitCommand(cwd, args)` for direct git command execution with better error handling.
|
||||||
|
5. Use `runGitCommandOrThrow(cwd, args, fallbackMessage)` for commands that must succeed.
|
||||||
|
6. Return consistent error messages; use `parseGitErrorText(error)` to extract meaningful git errors.
|
||||||
|
7. Update this file with the new function in the appropriate API section.
|
||||||
|
|
||||||
|
### SSH Key Handling
|
||||||
|
- SSH keys are escaped and validated via `escapeSshKeyPath` to prevent command injection.
|
||||||
|
- On Windows, paths are converted to MSYS format (`C:/path` → `/c/path`).
|
||||||
|
- SSH_AUTH_SOCK is automatically resolved via `resolveSshAuthSock` (checks GPG agent, gpgconf).
|
||||||
|
|
||||||
|
### Worktree Naming
|
||||||
|
- Worktree names are slugified via `slugWorktreeName`.
|
||||||
|
- Random names use adjectives/nouns from `OPENCODE_ADJECTIVES` and `OPENCODE_NOUNS` lists.
|
||||||
|
- Branches created for new worktrees use `openchamber/<worktree-name>` pattern.
|
||||||
|
|
||||||
|
### Cross-Platform Considerations
|
||||||
|
- Use `normalizeDirectoryPath` for all directory inputs to handle `~` and path separators.
|
||||||
|
- Use `canonicalPath` for path comparisons to handle case-insensitive filesystems (Windows).
|
||||||
|
- Windows Git commands use MSYS/MinGW paths; avoid direct Windows paths in git commands.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- All exported functions should throw errors with descriptive messages.
|
||||||
|
- Use `console.error` for logging Git operation failures.
|
||||||
|
- Return structured objects for operations that need partial success reporting (e.g., merge/rebase conflicts).
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing changes.
|
||||||
|
- Consider edge cases: non-Git directories, missing remotes, conflict states, concurrent worktree operations.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Git library public entrypoint
|
||||||
|
// Re-exports all Git operations, credentials, and identity storage functions
|
||||||
|
|
||||||
|
export * from './service.js';
|
||||||
|
export * from './credentials.js';
|
||||||
|
export * from './identity-storage.js';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getRemoteUrl } from './git-service.js';
|
import { getRemoteUrl } from './git/index.js';
|
||||||
|
|
||||||
export const parseGitHubRemoteUrl = (raw) => {
|
export const parseGitHubRemoteUrl = (raw) => {
|
||||||
if (typeof raw !== 'string') {
|
if (typeof raw !== 'string') {
|
||||||
|
|||||||
Reference in New Issue
Block a user