npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

git-chopstick-core

v0.1.15

Published

Git backend library extracted from GitHub Desktop

Readme

git-chopstick-core

A standalone Git backend library extracted from GitHub Desktop. Provides TypeScript-first wrappers around the Git CLI for repository operations — no native bindings, no Electron dependencies, just child_process.spawn('git', ...).

Features

  • 45+ Git command wrappers — every major operation: status, diff, log, branch (CRUD), commit, merge, rebase, stash, worktree, fetch, push, pull, cherry-pick, revert, tag, clone, init, clean, config, submodule, LFS, and more
  • Full dugite replacement — direct child_process.spawn('git', ...) with 50+ typed error codes
  • Pure CLI-based — works with the user's installed Git, no native bindings
  • TypeScript-first — complete type definitions for all models and operations (strict: true)
  • Zero Electron dependencies — works in Node.js ≥18 or Bun
  • Barrel exports — import everything from a single entry point

Installation

npm install git-chopstick-core
# or via file path for local development:
npm install file:../path/to/git-chopstick-core

Note: Published on npm as git-chopstick-core. Use a file: dependency for local development.

Quick Start

import { Repository, getStatus, GitError } from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')

try {
  const status = await getStatus(repo)
  console.log(`🌿 ${status.currentBranch}`)
  console.log(`📝 ${status.workingDirectory.files.length} changed files`)
} catch (e) {
  if (e instanceof GitError) {
    console.error(`Git error: ${e.message}`)
  }
}

Usage Examples

Repository Status

import { Repository, getStatus } from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')
const status = await getStatus(repo)

if (!status) {
  console.log('Not a git repository')
  process.exit(1)
}

console.log(`Branch: ${status.currentBranch ?? '(detached)'}`)
console.log(`Upstream: ${status.currentUpstreamBranch ?? 'none'}`)

if (status.branchAheadBehind) {
  console.log(`Ahead: ${status.branchAheadBehind.ahead}`)
  console.log(`Behind: ${status.branchAheadBehind.behind}`)
}

// Working directory changes
for (const file of status.workingDirectory.files) {
  console.log(`${file.status.kind}: ${file.path}`)
}

// Conflict detection
if (status.doConflictedFilesExist) {
  console.log('⚠️  Merge conflicts detected')
}
if (status.rebaseInternalState) {
  console.log(`🔄 Rebase onto ${status.rebaseInternalState.targetBranch}`)
}

Create a Commit

import { Repository, getStatus, createCommit } from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')
const status = await getStatus(repo)

// Stage and commit all tracked files
const files = status!.workingDirectory.files
const sha = await createCommit(repo, 'feat: add new feature', files)
console.log(`Committed: ${sha}`)

Branch Operations

import {
  Repository, Branch, BranchType, createBranch,
  deleteLocalBranch, renameBranch, getBranches
} from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')

// Create a branch
await createBranch(repo, 'feature/new-feature', 'main')

// List all branches
const branches = await getBranches(repo)
for (const branch of branches) {
  const icon = branch.type === BranchType.Local ? '🌿' : '🌐'
  console.log(`${icon} ${branch.name}`)
}

// Rename
await renameBranch(repo, branches[0], 'feature/renamed')

// Delete
await deleteLocalBranch(repo, 'feature/old-branch')

Merge

import {
  Repository, merge, MergeResult,
  abortMerge, getMergeBase, isMergeHeadSet
} from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')

const result = await merge(repo, 'feature/new-feature')

switch (result) {
  case MergeResult.Success:
    console.log('✓ Merge successful')
    break
  case MergeResult.AlreadyUpToDate:
    console.log('✓ Already up to date')
    break
  case MergeResult.Failed:
    console.log('✗ Merge failed — resolve conflicts')
    break
}

Push & Pull

import {
  Repository, push, pull, getRemotes, getStatus
} from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')
const [remote] = await getRemotes(repo)
const status = await getStatus(repo)

// Push current branch
await push(repo, remote, status!.currentBranch!, null, null)

// Pull from remote
await pull(repo, remote)

Error Handling

import {
  Repository, merge, GitError, GitErrorCodes
} from 'git-chopstick-core'

const repo = new Repository('/path/to/repo')

try {
  await merge(repo, 'other-branch')
} catch (e) {
  if (e instanceof GitError) {
    // e.result.gitError is the error code enum
    switch (e.result.gitError) {
      case GitErrorCodes.MergeConflicts:
        console.error('Conflicts need resolution')
        break
      case GitErrorCodes.RebaseConflicts:
        console.error('Rebase conflicts')
        break
      case GitErrorCodes.PushNotFastForward:
        console.error('Push rejected — pull first')
        break
      case GitErrorCodes.HTTPSAuthenticationFailed:
      case GitErrorCodes.SSHAuthenticationFailed:
        console.error('Authentication failed')
        break
      default:
        console.error(`Git error: ${e.result.gitErrorDescription}`)
    }
  }
}

API Reference

Git Operations (src/git/)

| Module | Key Exports | Description | |--------|-------------|-------------| | add | addConflictedFile | Stage a resolved conflicted file | | apply | applyPatchToIndex, checkPatch, discardChangesFromSelection | Apply patches to the index | | branch | createBranch, renameBranch, deleteLocalBranch, deleteRemoteBranch, getBranchNames, getBranchesPointedAt, getMergedBranches | Branch CRUD | | checkout | checkoutBranch, checkoutCommit, checkoutPaths, checkoutConflictedFile | Checkout branches/commits/files | | checkout-index | checkoutIndex | Checkout files from the index | | cherry-pick | cherryPick, continueCherryPick, abortCherryPick, getCherryPickSnapshot | Cherry-pick commits | | clean | cleanUntrackedFiles | Remove untracked files | | clone | clone | Clone a repository | | commit | createCommit, createMergeCommit | Create commits | | config | getConfigValue, getGlobalConfigValue, getBooleanConfigValue | Read git config | | diff | getWorkingDirectoryDiff, getCommitDiff, getBranchMergeBaseDiff, getCommitRangeDiff, getBinaryPaths | Diff rendering | | diff-index | getIndexChanges | Compare index with tree | | discover | getRepositories | Discover git repos in a directory tree (monorepo support) | | fetch | fetch, fetchRefspec, fastForwardBranches | Fetch from remotes | | for-each-ref | getBranches, getBranchesDifferingFromUpstream | List refs | | format-patch | formatPatch | Generate patch files | | gitignore | readGitIgnoreAtRoot, saveGitIgnore, appendIgnoreRule, escapeGitSpecialCharacters | Manage .gitignore | | init | initGitRepository | Initialize a repo | | interpret-trailers | parseRawUnfoldedTrailers, isCoAuthoredByTrailer, getTrailerSeparatorCharacters | Git trailer parsing | | lfs | installGlobalLFSFilters, isUsingLFS, isTrackedByLFS, filesNotTrackedByLFS | Git LFS support | | log | getCommits, getCommit, getChangedFiles, getChangedFilesFlat, getAuthors | Commit history | | merge | merge, getMergeBase, abortMerge | Merge branches | | merge-tree | determineMergeability | Test mergeability without merging | | pull | pull | Pull from remote | | push | push | Push to remote | | rebase | rebase, continueRebase, abortRebase, rebaseInteractive, getRebaseInternalState, getRebaseSnapshot | Rebase operations | | reflog | getRecentBranches, getBranchCheckouts | Reflog inspection | | refs | formatAsLocalRef, getSymbolicRef | Ref manipulation | | remote | getRemotes, addRemote, removeRemote, getRemoteURL, getRemoteUrl, getRemotesFromPath, setRemoteURL | Remote management | | reorder | reorder | Interactive rebase reordering | | reset | reset, resetPaths, unstageAll | Reset operations | | revert | revertCommit | Revert a commit | | rev-list | getAheadBehind, getBranchAheadBehind, revRange, revSymmetricDifference | Commit range queries | | rev-parse | getRepositoryType, getCurrentBranch, getRepositorySummary, getUpstreamRefForRef, getCurrentUpstreamRef | Rev parsing / branch detection | | rm | removeConflictedFile | Remove files | | squash | squash | Interactive rebase squashing | | stage | stageManualConflictResolution, stageResolvedConflictFiles | Stage conflict resolutions | | stash | getStashes, getStashesByPath, createStashEntry, popStashEntry, getStashedFiles, dropStashEntry | Stash management | | status | getStatus, getWorkingDirectoryChanges, getWorkingDirectoryChangesDetailed, fileChangeSummaryToWorkingDirectoryFile | Repository status + flat summaries + selection-aware variant with roundtrip | | submodule | updateSubmodulesAfterOperation, listSubmodules, resetSubmodulePaths | Submodule operations | | tag | createTag, deleteTag, getTags, getAllTags | Tag management | | update-index | stageFiles | Stage files | | update-ref | updateRef, deleteRef | Ref updates | | var | getAuthorIdentity | Git var queries | | worktree | listWorktrees, addWorktree, removeWorktree, pruneWorktrees, getWorktreeCheckedOutBranches | Worktree management | | worktree-include | addWorktreeWithIncludes, readWorktreeIncludePatterns | Worktree with file includes |

Domain Models (src/models/)

| Export | Type | Description | |--------|------|-------------| | Repository | class | Represents a git repository | | Commit | class | A single commit (sha, summary, body, author, etc.) | | CommitIdentity | class | Author/committer identity | | Branch | class | Branch with upstream tracking info | | BranchType | enum | Local or Remote | | AppFileStatusKind | enum | New, Modified, Deleted, Renamed, Copied, Conflicted, Untracked | | appFileStatusToString | fn | Converts AppFileStatus to human-readable string | | FlatFileChange | type | { path: string; statusKind: AppFileStatusKind; oldPath?: string } | | FileChange | class | Base file change model | | WorkingDirectoryFileChange | class | Uncommitted file change with diff selection | | CommittedFileChange | class | File change from a commit | | WorkingDirectoryStatus | class | Container for working directory changes | | WorkingDirectoryChangeSummary | type | { path: string; status: string; oldPath?: string } — flattened working directory change | | WorkingDirectoryFileChangeSummary | type | Like WorkingDirectoryChangeSummary but also includes selectionType + selection for staging decisions | | DiffSelection / DiffSelectionType | class/enum | Partial file staging | | ComputedAction | enum | Clean, Conflicts, Invalid, Loading | | ManualConflictResolution | enum | theirs / ours | | MergeResult / RebaseResult / CherryPickResult | enum | Operation outcomes |

Core Types

| Export | Description | |--------|-------------| | GitError (class) | Thrown when a git command fails — catch this | | GitErrorCodes (enum) | Error code constants — MergeConflicts, PushNotFastForward, SSHAuthenticationFailed, etc. (50+ codes) | | IGitResult | Full command result with gitError, gitErrorDescription, path | | IGitExecutionOptions | Options for git() function |

Known Limitations

This library is extracted directly from GitHub Desktop, a mature Electron application. Some subsystems that depend on GitHub Desktop's specific runtime environment have been extracted as stubs — they compile and type-check correctly but don't provide real functionality:

🟢 Progress Reporting (src/lib/progress/)

The progress parsers (CheckoutProgressParser, FetchProgressParser, PullProgressParser, PushProgressParser, CloneProgressParser, RevertProgressParser) now have real parse() methods that extract percentage and description from git's stderr output. Progress callbacks receive updates for clone, fetch, push, pull, and checkout operations.

Note: git revert does not produce progress output, so RevertProgressParser still returns null from its parse() method — this is correct behavior.

🟡 Git Hook Interception (src/lib/hooks/)

withHooksEnv is a stub that calls through without configuring any hook environment. The interceptHooks option for git() and operation functions (e.g., createCommit, push, pull) will not fire onHookProgress or onHookFailure callbacks. Git hooks will still run normally on their own — the library just can't intercept them.

🟡 Git LFS Trampoline (src/lib/trampoline/)

withTrampolineEnv is a stub that calls through without setting up the Git LFS trampoline environment. Git LFS operations may not work correctly as a result.


Architecture

src/
├── index.ts           ← Public API entry (barrel)
├── git/               ← 46 files: one per git subcommand
│   ├── exec.ts        ← Core dugite replacement: spawns git, parses errors
│   ├── core.ts        ← Higher-level git() wrapper with hooks, progress, env
│   └── index.ts       ← Git barrel (re-exports all git modules)
├── models/            ← 17+ files: domain types
│   ├── repository.ts
│   ├── commit.ts
│   ├── status.ts
│   ├── diff/          ← Diff types (DiffLine, DiffHunk, IDiff, IRawDiff, etc.)
│   └── index.ts       ← Models barrel
└── lib/               ← Utilities: parsers, progress reporting, hooks, fs helpers
    ├── diff-parser.ts  ← Full diff parser (text/binary/image)
    ├── status-parser.ts ← Porcelain v2 status parser
    ├── progress/       ← 🟢 Progress parsers (real — see Known Limitations)
    ├── hooks/          ← 🟡 Git hook env (stubbed — see Known Limitations)
    └── trampoline/     ← 🟡 Git LFS trampoline (stubbed — see Known Limitations)

Development

# Type-check
npm run typecheck

# Run integration tests (uses vitest, creates temp git repos)
npm test

# Build (compile TypeScript → dist/)
npm run build

# Package contents dry-run (verify files before publish)
npm pack --dry-run

# Run examples (uses tsx for source-level execution)
npx tsx examples/get-status.ts /path/to/repo
npx tsx examples/branch-operations.ts /path/to/repo
npx tsx examples/create-commit.ts /path/to/repo
npx tsx examples/spawn-git-progress.ts /path/to/repo

Status

Pre-1.0 — The public API (exports map, barrel exports, error types) is stabilizing but may see minor breaking changes before 1.0. See CHANGELOG.md for version history. Tested with TypeScript 5.7+.

Consumption Patterns

Upgrading from v0.1.4? See the exec → git() migration guide if you were using exec() from the public API.

Root barrel (recommended for most use cases)

// All 45+ git operations, domain models, error types
import { Repository, getStatus, createCommit, getBranches, merge, GitError, GitErrorCodes } from 'git-chopstick-core'

Git subpath (low-level access)

// Low-level helpers (parse git errors, spawn streaming git processes)
import { parseError, spawnGit } from 'git-chopstick-core/git'

// Use git() from the root barrel for most operations
import { git } from 'git-chopstick-core'

const result = await git(['rev-parse', '--show-toplevel'], '/path/to/repo', 'my-op')
console.log(result.stdout)

Models only

import { Commit, Branch, BranchType, AppFileStatusKind } from 'git-chopstick-core'

Granular (for bundle size)

import { getCommits } from 'git-chopstick-core/git/log'
import { getBranches } from 'git-chopstick-core/git/for-each-ref'

Consumption Notes

  • Compiled consumption (recommended): Import from 'git-chopstick-core' — the exports field in package.json points to the compiled output in dist/. Works with Node.js ESM and bundlers alike.
  • Run examples: After building, use npx tsx to execute the examples: npx tsx examples/get-status.ts /path/to/repo.

Dependencies

| Package | Purpose | |---------|---------| | byline | Line-by-line stream reading | | ignore | .gitignore pattern matching | | memoize-one | Memoization for remote fetching |

License

MIT — extracted from GitHub Desktop (MIT licensed).