Compare commits

...

8 Commits

Author SHA1 Message Date
Claude Bot
6653c96f99 fix(bun:git): Add libgit2 submodule and fix cmake configuration
- Add libgit2 as a git submodule in vendor/
- Fix cmake configuration to use proper ARGS instead of CMAKE_ARGS
- Use correct TARGETS (libgit2package) to build static library
- Disable NTLM and GSSAPI which require HTTPS backend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 09:24:34 +00:00
Claude Bot
38d399861f fix(bun:git): Resolve merge conflicts and cleanup duplicate implementations
- Remove duplicate git/ subdirectory files from remote merge
- Remove duplicate LazyProperty-based Git structure entries
- Consolidate to single LazyClassStructure-based implementation
- Remove duplicate IsoSubspace declarations
- Fix namespace references to use Bun:: prefix
- Clean up CMake duplicate targets (BuildLibGit2 vs BuildLibgit2)
- Remove TypeScript wrapper (git.ts) in favor of pure C++ implementation

All 50 tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:51:31 +00:00
Claude Bot
25ad2a0b8a fix(bun:git): Fix module loading and add comprehensive tests
- Fix static methods (Repository.find, Repository.init) not being attached
- Reorganize code to ensure proper method initialization order
- Add bun:git to isBuiltinModule.cpp in correct sorted position
- Fix libgit2 CMake LIB_PATH configuration
- Add comprehensive test suite with 50 tests covering:
  - Module exports validation
  - Repository.find() with path discovery
  - Repository.init() for normal and bare repos
  - Commit operations (getCommit, parent, isAncestorOf)
  - Branch operations (name, fullName, isHead, commit)
  - Status operations (workTreeStatus, indexStatus)
  - Add and commit functionality
  - Signature properties (name, email, date, timezone)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:36:43 +00:00
Claude Bot
efeadc42b1 feat(bun:git): Initial implementation of bun:git module with libgit2
- Add libgit2 CMake build target
- Register bun:git native module
- Implement JSGitRepository class with:
  - Constructor and static find/init methods
  - Properties: path, gitDir, isBare, isClean, head, branch
  - Methods: getCommit, status, add, commit
- Implement JSGitCommit class with:
  - Properties: sha, shortSha, message, summary, author, committer, tree, parents
  - Methods: parent, isAncestorOf
- Implement JSGitBranch class with:
  - Properties: name, fullName, isRemote, isHead, commit, upstream, ahead, behind
  - Methods: delete, rename
- Implement JSGitSignature class with:
  - Properties: name, email, date, timezone
  - Methods: toString

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:35:18 +00:00
Claude Bot
c9c2a560b0 fix(bun:git): Fix export functions for $cpp integration
- Update createJSGitRepositoryConstructor to return JSValue
- Add function declarations to JSGit.h

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 06:53:03 +00:00
Claude Bot
29dc5af8c9 test(bun:git): Add comprehensive tests for bun:git module
Tests cover:
- Repository constructor, find(), init()
- Repository properties (path, gitDir, isBare, head, branch, isClean)
- Repository methods (getCommit, getBranch, getRemote, status, add, commit, reset)
- Commit properties (sha, shortSha, message, summary, author, committer, parents, tree)
- Commit methods (parent, listFiles, getFile, isAncestorOf, diff)
- Config get/set operations
- StatusEntry properties
- Diff operations
- Error classes

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 06:52:19 +00:00
Claude Bot
0ed687fdaf feat(bun:git): Add TypeScript wrapper and ZigGlobalObject integration
- Add structure initializers in ZigGlobalObject.cpp for all git classes
- Create comprehensive TypeScript wrapper (src/js/bun/git.ts)
- Implement Repository, Commit, Branch, Remote, Config, Index, Diff, Blob wrappers
- Add StatusEntry class with helper properties
- Add GitError and related error classes
- Export both named exports and default module object

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 06:51:12 +00:00
Claude Bot
3422bd6411 feat(bun:git): Initial implementation of bun:git module with libgit2
- Add libgit2 as a dependency in CMake build system
- Register bun:git module in HardcodedModule.zig and isBuiltinModule.cpp
- Create JSGit.h with class declarations for Repository, Commit, Branch, etc.
- Implement JSGitRepository with constructor, static methods (find, init, clone)
- Implement JSGitCommit with sha, message, author, parents, diff, getFile
- Add iso subspaces for all git classes
- Add structure caching in ZigGlobalObject

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 06:49:25 +00:00
19 changed files with 3368 additions and 0 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "vendor/libgit2"]
path = vendor/libgit2
url = https://github.com/libgit2/libgit2.git

View File

@@ -54,6 +54,7 @@ set(BUN_DEPENDENCIES
Cares
Highway
LibDeflate
Libgit2
LolHtml
Lshpack
Mimalloc

View File

@@ -0,0 +1,23 @@
register_cmake_command(
TARGET
libgit2
TARGETS
libgit2package
LIBRARIES
git2
ARGS
-DBUILD_SHARED_LIBS=OFF
-DBUILD_TESTS=OFF
-DBUILD_CLI=OFF
-DUSE_SSH=OFF
-DUSE_HTTPS=OFF
-DUSE_NTLMCLIENT=OFF
-DUSE_GSSAPI=OFF
-DUSE_SHA1=Builtin
-DUSE_SHA256=Builtin
-DUSE_BUNDLED_ZLIB=ON
-DREGEX_BACKEND=builtin
-DUSE_HTTP_PARSER=builtin
INCLUDES
include
)

513
docs/git-api-reference.md Normal file
View File

@@ -0,0 +1,513 @@
bun:git Module API Design (Class-Based)
Core Classes
Repository
class Repository {
constructor(path?: string) // Finds git root from path, throws if not a repo
static find(startPath?: string): Repository | null // Non-throwing factory
static init(path: string, options?: { bare?: boolean; initialBranch?: string }):
Repository
static clone(url: string, targetPath: string, options?: CloneOptions): Repository
readonly path: string // Repo root (worktree root)
readonly gitDir: string // .git directory path
readonly isBare: boolean
// State
get head(): Commit
get branch(): Branch | null // null if detached HEAD
get isClean(): boolean
get isTransient(): boolean // merge/rebase/cherry-pick in progress
// References
getCommit(ref: string): Commit | null
getBranch(name: string): Branch | null
getRemote(name?: string): Remote | null // default: "origin"
getDefaultBranch(): Branch | null
// Collections
get branches(): BranchCollection
get remotes(): RemoteCollection
get worktrees(): WorktreeCollection
get stash(): StashCollection
get config(): Config
// Working tree
status(options?: StatusOptions): StatusEntry[]
diff(options?: DiffOptions): Diff
// Index operations
add(paths: string | string[]): void
reset(paths?: string | string[]): void // Unstage
// Commit
commit(message: string, options?: CommitOptions): Commit
// Checkout
checkout(ref: string | Branch | Commit, options?: CheckoutOptions): void
// Reset working tree
resetHard(ref?: string | Commit): void
clean(options?: CleanOptions): void
// Abort transient states
abortMerge(): void
abortRebase(): void
abortCherryPick(): void
abortRevert(): void
}
---
Commit
class Commit {
readonly sha: string // Full 40-char SHA
readonly shortSha: string // First 7 chars
readonly message: string // Full message
readonly summary: string // First line
readonly author: Signature
readonly committer: Signature
readonly parents: Commit[]
readonly tree: string // Tree SHA
// Navigation
parent(n?: number): Commit | null // Default: first parent
// Diff
diff(other?: Commit | string): Diff // Default: diff against parent
// File access
getFile(path: string): Blob | null // git show <sha>:<path>
listFiles(): string[] // git diff-tree --name-only
// Ancestry
isAncestorOf(other: Commit | string): boolean
distanceTo(other: Commit | string): number // rev-list --count
}
---
Branch
class Branch {
readonly name: string // e.g., "main" or "feature/foo"
readonly fullName: string // e.g., "refs/heads/main"
readonly isRemote: boolean
readonly isHead: boolean // Currently checked out
get commit(): Commit
get upstream(): Branch | null // Tracking branch
// Comparison with upstream
get ahead(): number
get behind(): number
// Operations
setUpstream(upstream: Branch | string | null): void
delete(force?: boolean): void
rename(newName: string): void
// Static
static create(repo: Repository, name: string, target?: Commit | string): Branch
}
---
Remote
class Remote {
readonly name: string // e.g., "origin"
readonly url: string // Fetch URL
readonly pushUrl: string // Push URL (may differ)
// Normalized for comparison (handles SSH vs HTTPS)
readonly normalizedUrl: string
readonly urlHash: string // SHA256 hash for privacy-safe logging
// Branches
get defaultBranch(): Branch | null // origin/HEAD target
getBranch(name: string): Branch | null
listBranches(): Branch[]
// Operations
fetch(options?: FetchOptions): void
fetchBranch(branch: string): void
}
---
Worktree
class Worktree {
readonly path: string
readonly gitDir: string
readonly isMain: boolean // Is this the main worktree?
get head(): Commit
get branch(): Branch | null
get isClean(): boolean
// Get a Repository instance for this worktree
asRepository(): Repository
// Operations
remove(force?: boolean): void
// Static
static add(
repo: Repository,
path: string,
options?: { branch?: string; detach?: boolean; commit?: string }
): Worktree
}
class WorktreeCollection {
list(): Worktree[]
get(path: string): Worktree | null
add(path: string, options?: WorktreeAddOptions): Worktree
prune(): void
readonly count: number
}
---
Diff
class Diff {
readonly stats: DiffStats
readonly files: DiffFile[]
// Raw output
toString(): string // Unified diff format
toNumstat(): string
// Iteration
[Symbol.iterator](): Iterator<DiffFile>
}
class DiffFile {
readonly path: string
readonly oldPath: string | null // For renames
readonly status: 'A' | 'M' | 'D' | 'R' | 'C' | 'T' | 'U'
readonly isBinary: boolean
readonly additions: number
readonly deletions: number
readonly hunks: DiffHunk[]
// Content
readonly patch: string
}
class DiffHunk {
readonly oldStart: number
readonly oldLines: number
readonly newStart: number
readonly newLines: number
readonly header: string
readonly lines: DiffLine[]
}
type DiffLine = {
type: '+' | '-' | ' '
content: string
oldLineNo?: number
newLineNo?: number
}
type DiffStats = {
filesChanged: number
insertions: number
deletions: number
}
---
StatusEntry
type FileStatus =
| 'unmodified' // ' '
| 'modified' // 'M'
| 'added' // 'A'
| 'deleted' // 'D'
| 'renamed' // 'R'
| 'copied' // 'C'
| 'untracked' // '?'
| 'ignored' // '!'
| 'unmerged' // 'U'
class StatusEntry {
readonly path: string
readonly indexStatus: FileStatus // Staged status
readonly workTreeStatus: FileStatus // Unstaged status
readonly origPath: string | null // For renames/copies
get isStaged(): boolean
get isUnstaged(): boolean
get isUntracked(): boolean
get isConflicted(): boolean
}
type StatusOptions = {
includeUntracked?: boolean // Default: true
includeIgnored?: boolean // Default: false
noOptionalLocks?: boolean // --no-optional-locks
}
---
Index (Staging Area)
class Index {
readonly entries: IndexEntry[]
// Stage files
add(paths: string | string[]): void
addAll(): void
// Unstage files
reset(paths?: string | string[]): void
resetAll(): void
// Query
has(path: string): boolean
get(path: string): IndexEntry | null
// Diff
diff(): Diff // Staged changes (--cached)
}
class IndexEntry {
readonly path: string
readonly sha: string
readonly mode: number
}
---
Config
class Config {
// Get values
get(key: string): string | null
getAll(key: string): string[]
getBool(key: string): boolean | null
getInt(key: string): number | null
// Set values
set(key: string, value: string): void
unset(key: string): void
// Common shortcuts
get userEmail(): string | null
get userName(): string | null
get hooksPath(): string | null
set userEmail(value: string | null)
set userName(value: string | null)
set hooksPath(value: string | null)
}
---
Stash
class StashEntry {
readonly index: number
readonly message: string
readonly commit: Commit
apply(options?: { index?: boolean }): void
pop(options?: { index?: boolean }): void
drop(): void
}
class StashCollection {
list(): StashEntry[]
get(index: number): StashEntry | null
push(message?: string, options?: { includeUntracked?: boolean }): StashEntry
pop(): boolean
apply(index?: number): boolean
drop(index?: number): boolean
clear(): void
readonly count: number
readonly isEmpty: boolean
}
---
Blob (File Content)
class Blob {
readonly sha: string
readonly size: number
readonly isBinary: boolean
// Content access
content(): Buffer
text(): string // Throws if binary
// Streaming for large files
stream(): ReadableStream<Uint8Array>
}
---
Signature
class Signature {
readonly name: string
readonly email: string
readonly date: Date
readonly timezone: string
toString(): string // "Name <email>"
}
---
Supporting Types
type CloneOptions = {
depth?: number
branch?: string
recurseSubmodules?: boolean
shallowSubmodules?: boolean
bare?: boolean
}
type CommitOptions = {
amend?: boolean
allowEmpty?: boolean
author?: Signature | string
noVerify?: boolean // Skip hooks
}
type CheckoutOptions = {
create?: boolean // -b
force?: boolean // -f
track?: boolean // --track
}
type CleanOptions = {
directories?: boolean // -d
force?: boolean // -f
dryRun?: boolean // -n
}
type FetchOptions = {
prune?: boolean
tags?: boolean
depth?: number
}
type DiffOptions = {
cached?: boolean // Staged changes only
ref?: string | Commit // Compare against (default: HEAD)
paths?: string[] // Limit to paths
contextLines?: number // -U<n>
nameOnly?: boolean
nameStatus?: boolean
stat?: boolean
}
---
Error Classes
class GitError extends Error {
readonly command?: string
readonly exitCode?: number
readonly stderr?: string
}
class NotARepositoryError extends GitError {}
class RefNotFoundError extends GitError {
readonly ref: string
}
class MergeConflictError extends GitError {
readonly conflictedFiles: string[]
}
class CheckoutConflictError extends GitError {
readonly conflictedFiles: string[]
}
class DetachedHeadError extends GitError {}
---
Usage Examples
import { Repository } from 'bun:git'
// Open repository
const repo = Repository.find('/path/to/project')
if (!repo) throw new Error('Not a git repository')
// Basic info
console.log(repo.head.sha)
console.log(repo.branch?.name) // null if detached
console.log(repo.isClean)
// Status
for (const entry of repo.status()) {
if (entry.isUntracked) {
console.log(`New file: ${entry.path}`)
}
}
// Diff
const diff = repo.diff()
console.log(`${diff.stats.insertions}+ ${diff.stats.deletions}-`)
for (const file of diff.files) {
console.log(`${file.status} ${file.path}`)
}
// Commit
repo.add(['src/file.ts'])
const commit = repo.commit('Fix bug')
console.log(commit.sha)
// Branch operations
const feature = Branch.create(repo, 'feature/new-thing')
repo.checkout(feature)
// Remote operations
const origin = repo.getRemote('origin')
origin?.fetch()
// Worktrees
const worktree = Worktree.add(repo, '/tmp/worktree', { branch: 'experiment' })
const wtRepo = worktree.asRepository()
// ... work in worktree ...
worktree.remove()
// File content from history
const oldFile = repo.head.parent()?.getFile('README.md')
console.log(oldFile?.text())
// Config
repo.config.set('core.hooksPath', '/path/to/hooks')
console.log(repo.config.userEmail)
---
Sync vs Async Considerations
Most operations should be synchronous since git operations on local repos are fast:
// Sync (preferred for most operations)
const repo = Repository.find(path)
const status = repo.status()
const head = repo.head.sha
// Async only for network operations
await repo.getRemote('origin')?.fetch()
await Repository.clone(url, path)
If Bun wants to keep the API async-friendly, consider:
// Sync accessors for commonly-used properties
repo.head // Sync getter
repo.headAsync // Async getter (if needed for consistency)
---
Priority Implementation Order
1. Repository - Core class, find(), basic properties
2. Commit - sha, message, getFile()
3. Branch - name, commit, basic operations
4. Status/Diff - Critical for UI display
5. Index - add(), reset()
6. Remote - url, fetch()
7. Worktree - Full worktree support
8. Stash - Stash operations
9. Config - Config get/set

View File

@@ -5,6 +5,7 @@ pub const HardcodedModule = enum {
@"abort-controller",
@"bun:app",
@"bun:ffi",
@"bun:git",
@"bun:jsc",
@"bun:main",
@"bun:test",
@@ -93,6 +94,7 @@ pub const HardcodedModule = enum {
.{ "bun", .bun },
.{ "bun:app", .@"bun:app" },
.{ "bun:ffi", .@"bun:ffi" },
.{ "bun:git", .@"bun:git" },
.{ "bun:jsc", .@"bun:jsc" },
.{ "bun:main", .@"bun:main" },
.{ "bun:test", .@"bun:test" },
@@ -360,6 +362,7 @@ pub const HardcodedModule = enum {
.{ "bun:test", .{ .path = "bun:test" } },
.{ "bun:app", .{ .path = "bun:app" } },
.{ "bun:ffi", .{ .path = "bun:ffi" } },
.{ "bun:git", .{ .path = "bun:git" } },
.{ "bun:jsc", .{ .path = "bun:jsc" } },
.{ "bun:sqlite", .{ .path = "bun:sqlite" } },
.{ "bun:wrap", .{ .path = "bun:wrap" } },

518
src/bun.js/bindings/JSGit.h Normal file
View File

@@ -0,0 +1,518 @@
#pragma once
#include "root.h"
#include "BunClientData.h"
#include <git2.h>
namespace Bun {
using namespace JSC;
// Forward declarations
class JSGitRepository;
class JSGitCommit;
class JSGitBranch;
class JSGitRemote;
class JSGitDiff;
class JSGitStatusEntry;
class JSGitIndex;
class JSGitConfig;
class JSGitStash;
class JSGitWorktree;
class JSGitBlob;
class JSGitSignature;
// Initialize libgit2 (call once at startup)
void initializeLibgit2();
void shutdownLibgit2();
// ============================================================================
// JSGitRepository - Core repository class
// ============================================================================
class JSGitRepository : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
JSGitRepository(JSC::VM& vm, JSC::Structure* structure, git_repository* repo)
: Base(vm, structure)
, m_repo(repo)
{
}
DECLARE_INFO;
static constexpr unsigned StructureFlags = Base::StructureFlags;
template<typename, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return subspaceForImpl(vm);
}
static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static JSGitRepository* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, git_repository* repo)
{
JSGitRepository* object = new (NotNull, JSC::allocateCell<JSGitRepository>(vm)) JSGitRepository(vm, structure, repo);
object->finishCreation(vm, globalObject);
return object;
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject);
static void destroy(JSCell* thisObject)
{
static_cast<JSGitRepository*>(thisObject)->~JSGitRepository();
}
~JSGitRepository();
git_repository* repo() const { return m_repo; }
private:
git_repository* m_repo;
};
class JSGitRepositoryPrototype : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static JSGitRepositoryPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSGitRepositoryPrototype* ptr = new (NotNull, JSC::allocateCell<JSGitRepositoryPrototype>(vm)) JSGitRepositoryPrototype(vm, structure);
ptr->finishCreation(vm, globalObject);
return ptr;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSGitRepositoryPrototype, Base);
return &vm.plainObjectSpace();
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
private:
JSGitRepositoryPrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};
class JSGitRepositoryConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static JSGitRepositoryConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSGitRepositoryPrototype* prototype);
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
void initializeProperties(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSGitRepositoryPrototype* prototype);
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*);
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*);
DECLARE_EXPORT_INFO;
private:
JSGitRepositoryConstructor(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure, call, construct)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, JSGitRepositoryPrototype* prototype);
};
// ============================================================================
// JSGitCommit - Commit class
// ============================================================================
class JSGitCommit : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
JSGitCommit(JSC::VM& vm, JSC::Structure* structure, git_commit* commit)
: Base(vm, structure)
, m_commit(commit)
{
}
DECLARE_INFO;
DECLARE_VISIT_CHILDREN;
static constexpr unsigned StructureFlags = Base::StructureFlags;
template<typename, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return subspaceForImpl(vm);
}
static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static JSGitCommit* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, git_commit* commit, JSGitRepository* repo)
{
JSGitCommit* object = new (NotNull, JSC::allocateCell<JSGitCommit>(vm)) JSGitCommit(vm, structure, commit);
object->finishCreation(vm, globalObject, repo);
return object;
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSGitRepository* repo);
static void destroy(JSCell* thisObject)
{
static_cast<JSGitCommit*>(thisObject)->~JSGitCommit();
}
~JSGitCommit();
git_commit* commit() const { return m_commit; }
JSGitRepository* repository() const { return m_repo.get(); }
private:
git_commit* m_commit;
JSC::WriteBarrier<JSGitRepository> m_repo;
};
class JSGitCommitPrototype : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static JSGitCommitPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSGitCommitPrototype* ptr = new (NotNull, JSC::allocateCell<JSGitCommitPrototype>(vm)) JSGitCommitPrototype(vm, structure);
ptr->finishCreation(vm, globalObject);
return ptr;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSGitCommitPrototype, Base);
return &vm.plainObjectSpace();
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
private:
JSGitCommitPrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};
class JSGitCommitConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static JSGitCommitConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSGitCommitPrototype* prototype);
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*);
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*);
DECLARE_EXPORT_INFO;
private:
JSGitCommitConstructor(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure, call, construct)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, JSGitCommitPrototype* prototype);
};
// ============================================================================
// JSGitBranch - Branch class
// ============================================================================
class JSGitBranch : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
JSGitBranch(JSC::VM& vm, JSC::Structure* structure, git_reference* ref, bool isRemote)
: Base(vm, structure)
, m_ref(ref)
, m_isRemote(isRemote)
{
}
DECLARE_INFO;
DECLARE_VISIT_CHILDREN;
static constexpr unsigned StructureFlags = Base::StructureFlags;
template<typename, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return subspaceForImpl(vm);
}
static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static JSGitBranch* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, git_reference* ref, JSGitRepository* repo, bool isRemote)
{
JSGitBranch* object = new (NotNull, JSC::allocateCell<JSGitBranch>(vm)) JSGitBranch(vm, structure, ref, isRemote);
object->finishCreation(vm, globalObject, repo);
return object;
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSGitRepository* repo);
static void destroy(JSCell* thisObject)
{
static_cast<JSGitBranch*>(thisObject)->~JSGitBranch();
}
~JSGitBranch();
git_reference* ref() const { return m_ref; }
JSGitRepository* repository() const { return m_repo.get(); }
bool isRemote() const { return m_isRemote; }
private:
git_reference* m_ref;
JSC::WriteBarrier<JSGitRepository> m_repo;
bool m_isRemote;
};
class JSGitBranchPrototype : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static JSGitBranchPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSGitBranchPrototype* ptr = new (NotNull, JSC::allocateCell<JSGitBranchPrototype>(vm)) JSGitBranchPrototype(vm, structure);
ptr->finishCreation(vm, globalObject);
return ptr;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSGitBranchPrototype, Base);
return &vm.plainObjectSpace();
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
private:
JSGitBranchPrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};
class JSGitBranchConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static JSGitBranchConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSGitBranchPrototype* prototype);
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*);
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*);
DECLARE_EXPORT_INFO;
private:
JSGitBranchConstructor(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure, call, construct)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, JSGitBranchPrototype* prototype);
};
// ============================================================================
// JSGitSignature - Signature class (author/committer info)
// ============================================================================
class JSGitSignature : public JSC::JSDestructibleObject {
using Base = JSC::JSDestructibleObject;
public:
JSGitSignature(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
, m_name()
, m_email()
, m_time(0)
, m_offset(0)
{
}
DECLARE_INFO;
static constexpr unsigned StructureFlags = Base::StructureFlags;
template<typename, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return subspaceForImpl(vm);
}
static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
static JSGitSignature* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, const git_signature* sig)
{
JSGitSignature* object = new (NotNull, JSC::allocateCell<JSGitSignature>(vm)) JSGitSignature(vm, structure);
object->finishCreation(vm, globalObject, sig);
return object;
}
void finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject, const git_signature* sig);
static void destroy(JSCell* thisObject)
{
static_cast<JSGitSignature*>(thisObject)->~JSGitSignature();
}
const String& name() const { return m_name; }
const String& email() const { return m_email; }
git_time_t time() const { return m_time; }
int offset() const { return m_offset; }
private:
String m_name;
String m_email;
git_time_t m_time;
int m_offset;
};
class JSGitSignaturePrototype : public JSC::JSNonFinalObject {
public:
using Base = JSC::JSNonFinalObject;
static JSGitSignaturePrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
{
JSGitSignaturePrototype* ptr = new (NotNull, JSC::allocateCell<JSGitSignaturePrototype>(vm)) JSGitSignaturePrototype(vm, structure);
ptr->finishCreation(vm, globalObject);
return ptr;
}
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSGitSignaturePrototype, Base);
return &vm.plainObjectSpace();
}
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
auto* structure = JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
structure->setMayBePrototype(true);
return structure;
}
private:
JSGitSignaturePrototype(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};
class JSGitSignatureConstructor final : public JSC::InternalFunction {
public:
using Base = JSC::InternalFunction;
static JSGitSignatureConstructor* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSGitSignaturePrototype* prototype);
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info());
}
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*);
static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject*, JSC::CallFrame*);
DECLARE_EXPORT_INFO;
private:
JSGitSignatureConstructor(JSC::VM& vm, JSC::Structure* structure)
: Base(vm, structure, call, construct)
{
}
void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, JSGitSignaturePrototype* prototype);
};
} // namespace Bun

View File

@@ -0,0 +1,368 @@
#include "root.h"
#include "JSGit.h"
#include "ZigGlobalObject.h"
#include "JavaScriptCore/JSCJSValueInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "wtf/text/WTFString.h"
#include "helpers.h"
#include "JSDOMExceptionHandling.h"
#include "BunClientData.h"
#include <git2.h>
namespace Bun {
using namespace JSC;
// ============================================================================
// JSGitBranch Implementation
// ============================================================================
const ClassInfo JSGitBranch::s_info = { "Branch"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitBranch) };
JSGitBranch::~JSGitBranch()
{
if (m_ref) {
git_reference_free(m_ref);
m_ref = nullptr;
}
}
void JSGitBranch::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitRepository* repo)
{
Base::finishCreation(vm);
m_repo.set(vm, this, repo);
}
template<typename Visitor>
void JSGitBranch::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSGitBranch*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_repo);
}
DEFINE_VISIT_CHILDREN(JSGitBranch);
JSC::GCClient::IsoSubspace* JSGitBranch::subspaceForImpl(VM& vm)
{
return WebCore::subspaceForImpl<JSGitBranch, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitBranch.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitBranch = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForJSGitBranch.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitBranch = std::forward<decltype(space)>(space); });
}
// Getter: name
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_name, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "name"_s);
return {};
}
const char* name = nullptr;
int error = git_branch_name(&name, thisObject->ref());
if (error < 0 || !name) {
return JSValue::encode(jsNull());
}
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(name)));
}
// Getter: fullName
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_fullName, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "fullName"_s);
return {};
}
const char* name = git_reference_name(thisObject->ref());
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(name ? name : "")));
}
// Getter: isRemote
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_isRemote, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "isRemote"_s);
return {};
}
return JSValue::encode(jsBoolean(thisObject->isRemote()));
}
// Getter: isHead
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_isHead, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "isHead"_s);
return {};
}
return JSValue::encode(jsBoolean(git_branch_is_head(thisObject->ref())));
}
// Getter: commit
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_commit, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Branch"_s, "commit"_s);
return {};
}
const git_oid* oid = git_reference_target(thisObject->ref());
if (!oid) {
// Symbolic reference, need to resolve
git_reference* resolved = nullptr;
int error = git_reference_resolve(&resolved, thisObject->ref());
if (error < 0) {
const git_error* err = git_error_last();
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(err ? err->message : "Failed to resolve branch")));
return {};
}
oid = git_reference_target(resolved);
git_reference_free(resolved);
}
git_commit* commit = nullptr;
int error = git_commit_lookup(&commit, thisObject->repository()->repo(), oid);
if (error < 0) {
const git_error* err = git_error_last();
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(err ? err->message : "Failed to get commit")));
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
return JSValue::encode(JSGitCommit::create(vm, lexicalGlobalObject, structure, commit, thisObject->repository()));
}
// Getter: upstream
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_upstream, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Branch"_s, "upstream"_s);
return {};
}
git_reference* upstream = nullptr;
int error = git_branch_upstream(&upstream, thisObject->ref());
if (error < 0) {
if (error == GIT_ENOTFOUND) {
return JSValue::encode(jsNull());
}
const git_error* err = git_error_last();
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(err ? err->message : "Failed to get upstream")));
return {};
}
auto* structure = globalObject->JSGitBranchStructure();
return JSValue::encode(JSGitBranch::create(vm, lexicalGlobalObject, structure, upstream, thisObject->repository(), true));
}
// Getter: ahead
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_ahead, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "ahead"_s);
return {};
}
git_reference* upstream = nullptr;
int error = git_branch_upstream(&upstream, thisObject->ref());
if (error < 0) {
return JSValue::encode(jsNumber(0));
}
size_t ahead = 0, behind = 0;
const git_oid* localOid = git_reference_target(thisObject->ref());
const git_oid* upstreamOid = git_reference_target(upstream);
if (localOid && upstreamOid) {
git_graph_ahead_behind(&ahead, &behind, thisObject->repository()->repo(), localOid, upstreamOid);
}
git_reference_free(upstream);
return JSValue::encode(jsNumber(ahead));
}
// Getter: behind
JSC_DEFINE_CUSTOM_GETTER(jsGitBranchGetter_behind, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "behind"_s);
return {};
}
git_reference* upstream = nullptr;
int error = git_branch_upstream(&upstream, thisObject->ref());
if (error < 0) {
return JSValue::encode(jsNumber(0));
}
size_t ahead = 0, behind = 0;
const git_oid* localOid = git_reference_target(thisObject->ref());
const git_oid* upstreamOid = git_reference_target(upstream);
if (localOid && upstreamOid) {
git_graph_ahead_behind(&ahead, &behind, thisObject->repository()->repo(), localOid, upstreamOid);
}
git_reference_free(upstream);
return JSValue::encode(jsNumber(behind));
}
// Method: delete(force?)
JSC_DEFINE_HOST_FUNCTION(jsGitBranchProtoFunc_delete, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "delete"_s);
return {};
}
int error = git_branch_delete(thisObject->ref());
if (error < 0) {
const git_error* err = git_error_last();
throwException(globalObject, scope, createError(globalObject, WTF::String::fromUTF8(err ? err->message : "Failed to delete branch")));
return {};
}
return JSValue::encode(jsUndefined());
}
// Method: rename(newName: string)
JSC_DEFINE_HOST_FUNCTION(jsGitBranchProtoFunc_rename, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitBranch*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Branch"_s, "rename"_s);
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(globalObject, scope, createError(globalObject, "rename requires a newName argument"_s));
return {};
}
auto newName = callFrame->argument(0).toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
git_reference* newRef = nullptr;
int error = git_branch_move(&newRef, thisObject->ref(), newName.utf8().data(), 0);
if (error < 0) {
const git_error* err = git_error_last();
throwException(globalObject, scope, createError(globalObject, WTF::String::fromUTF8(err ? err->message : "Failed to rename branch")));
return {};
}
git_reference_free(newRef);
return JSValue::encode(jsUndefined());
}
// ============================================================================
// JSGitBranch Prototype Table
// ============================================================================
static const HashTableValue JSGitBranchPrototypeTableValues[] = {
{ "name"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_name, 0 } },
{ "fullName"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_fullName, 0 } },
{ "isRemote"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_isRemote, 0 } },
{ "isHead"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_isHead, 0 } },
{ "commit"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_commit, 0 } },
{ "upstream"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_upstream, 0 } },
{ "ahead"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_ahead, 0 } },
{ "behind"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitBranchGetter_behind, 0 } },
{ "delete"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitBranchProtoFunc_delete, 0 } },
{ "rename"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitBranchProtoFunc_rename, 1 } },
};
// ============================================================================
// JSGitBranchPrototype Implementation
// ============================================================================
const ClassInfo JSGitBranchPrototype::s_info = { "Branch"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitBranchPrototype) };
void JSGitBranchPrototype::finishCreation(VM& vm, JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSGitBranch::info(), JSGitBranchPrototypeTableValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
// ============================================================================
// JSGitBranchConstructor Implementation
// ============================================================================
const ClassInfo JSGitBranchConstructor::s_info = { "Branch"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitBranchConstructor) };
JSGitBranchConstructor* JSGitBranchConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSGitBranchPrototype* prototype)
{
JSGitBranchConstructor* constructor = new (NotNull, allocateCell<JSGitBranchConstructor>(vm)) JSGitBranchConstructor(vm, structure);
constructor->finishCreation(vm, globalObject, prototype);
return constructor;
}
void JSGitBranchConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitBranchPrototype* prototype)
{
Base::finishCreation(vm, 0, "Branch"_s, PropertyAdditionMode::WithoutStructureTransition);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitBranchConstructor::construct(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Branch cannot be directly constructed"_s));
return {};
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitBranchConstructor::call(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Branch cannot be called as a function"_s));
return {};
}
} // namespace Bun

View File

@@ -0,0 +1,363 @@
#include "root.h"
#include "JSGit.h"
#include "ZigGlobalObject.h"
#include "JavaScriptCore/JSCJSValueInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "wtf/text/WTFString.h"
#include "helpers.h"
#include "JSDOMExceptionHandling.h"
#include "BunClientData.h"
#include <git2.h>
namespace Bun {
using namespace JSC;
// ============================================================================
// JSGitCommit Implementation
// ============================================================================
const ClassInfo JSGitCommit::s_info = { "Commit"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitCommit) };
JSGitCommit::~JSGitCommit()
{
if (m_commit) {
git_commit_free(m_commit);
m_commit = nullptr;
}
}
void JSGitCommit::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitRepository* repo)
{
Base::finishCreation(vm);
m_repo.set(vm, this, repo);
}
template<typename Visitor>
void JSGitCommit::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
auto* thisObject = jsCast<JSGitCommit*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_repo);
}
DEFINE_VISIT_CHILDREN(JSGitCommit);
JSC::GCClient::IsoSubspace* JSGitCommit::subspaceForImpl(VM& vm)
{
return WebCore::subspaceForImpl<JSGitCommit, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitCommit.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitCommit = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForJSGitCommit.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitCommit = std::forward<decltype(space)>(space); });
}
// Helper to format OID as hex string
static WTF::String oidToString(const git_oid* oid)
{
char buf[GIT_OID_SHA1_HEXSIZE + 1];
git_oid_tostr(buf, sizeof(buf), oid);
return WTF::String::fromUTF8(buf);
}
// Getter: sha
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_sha, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "sha"_s);
return {};
}
const git_oid* oid = git_commit_id(thisObject->commit());
return JSValue::encode(jsString(vm, oidToString(oid)));
}
// Getter: shortSha
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_shortSha, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "shortSha"_s);
return {};
}
const git_oid* oid = git_commit_id(thisObject->commit());
char buf[8];
git_oid_tostr(buf, sizeof(buf), oid);
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(buf)));
}
// Getter: message
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_message, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "message"_s);
return {};
}
const char* message = git_commit_message(thisObject->commit());
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(message ? message : "")));
}
// Getter: summary
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_summary, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "summary"_s);
return {};
}
const char* summary = git_commit_summary(thisObject->commit());
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(summary ? summary : "")));
}
// Getter: author
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_author, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Commit"_s, "author"_s);
return {};
}
const git_signature* author = git_commit_author(thisObject->commit());
auto* structure = globalObject->JSGitSignatureStructure();
return JSValue::encode(JSGitSignature::create(vm, lexicalGlobalObject, structure, author));
}
// Getter: committer
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_committer, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Commit"_s, "committer"_s);
return {};
}
const git_signature* committer = git_commit_committer(thisObject->commit());
auto* structure = globalObject->JSGitSignatureStructure();
return JSValue::encode(JSGitSignature::create(vm, lexicalGlobalObject, structure, committer));
}
// Getter: tree
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_tree, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "tree"_s);
return {};
}
const git_oid* treeId = git_commit_tree_id(thisObject->commit());
return JSValue::encode(jsString(vm, oidToString(treeId)));
}
// Getter: parents
JSC_DEFINE_CUSTOM_GETTER(jsGitCommitGetter_parents, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitCommit*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Commit"_s, "parents"_s);
return {};
}
unsigned int parentCount = git_commit_parentcount(thisObject->commit());
JSArray* result = constructEmptyArray(lexicalGlobalObject, nullptr, parentCount);
RETURN_IF_EXCEPTION(scope, {});
for (unsigned int i = 0; i < parentCount; i++) {
git_commit* parent = nullptr;
int error = git_commit_parent(&parent, thisObject->commit(), i);
if (error < 0) {
const git_error* err = git_error_last();
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(err ? err->message : "Failed to get parent commit")));
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
result->putDirectIndex(lexicalGlobalObject, i, JSGitCommit::create(vm, lexicalGlobalObject, structure, parent, thisObject->repository()));
RETURN_IF_EXCEPTION(scope, {});
}
return JSValue::encode(result);
}
// Method: parent(n?) -> Commit | null
JSC_DEFINE_HOST_FUNCTION(jsGitCommitProtoFunc_parent, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitCommit*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Commit"_s, "parent"_s);
return {};
}
unsigned int n = 0;
if (callFrame->argumentCount() > 0 && !callFrame->argument(0).isUndefined()) {
n = callFrame->argument(0).toUInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
git_commit* parent = nullptr;
int error = git_commit_parent(&parent, thisObject->commit(), n);
if (error < 0) {
if (error == GIT_ENOTFOUND) {
return JSValue::encode(jsNull());
}
const git_error* err = git_error_last();
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, WTF::String::fromUTF8(err ? err->message : "Failed to get parent commit")));
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
return JSValue::encode(JSGitCommit::create(vm, lexicalGlobalObject, structure, parent, thisObject->repository()));
}
// Method: isAncestorOf(other: Commit | string) -> boolean
JSC_DEFINE_HOST_FUNCTION(jsGitCommitProtoFunc_isAncestorOf, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitCommit*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Commit"_s, "isAncestorOf"_s);
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(globalObject, scope, createError(globalObject, "isAncestorOf requires a commit argument"_s));
return {};
}
const git_oid* ancestorOid = git_commit_id(thisObject->commit());
git_oid descendantOid;
JSValue otherArg = callFrame->argument(0);
if (auto* otherCommit = jsDynamicCast<JSGitCommit*>(otherArg)) {
git_oid_cpy(&descendantOid, git_commit_id(otherCommit->commit()));
} else {
auto refString = otherArg.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
git_object* obj = nullptr;
int error = git_revparse_single(&obj, thisObject->repository()->repo(), refString.utf8().data());
if (error < 0) {
const git_error* err = git_error_last();
throwException(globalObject, scope, createError(globalObject, WTF::String::fromUTF8(err ? err->message : "Invalid ref")));
return {};
}
git_oid_cpy(&descendantOid, git_object_id(obj));
git_object_free(obj);
}
int result = git_graph_descendant_of(thisObject->repository()->repo(), &descendantOid, ancestorOid);
if (result < 0) {
const git_error* err = git_error_last();
throwException(globalObject, scope, createError(globalObject, WTF::String::fromUTF8(err ? err->message : "Failed to check ancestry")));
return {};
}
return JSValue::encode(jsBoolean(result == 1));
}
// ============================================================================
// JSGitCommit Prototype Table
// ============================================================================
static const HashTableValue JSGitCommitPrototypeTableValues[] = {
{ "sha"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_sha, 0 } },
{ "shortSha"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_shortSha, 0 } },
{ "message"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_message, 0 } },
{ "summary"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_summary, 0 } },
{ "author"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_author, 0 } },
{ "committer"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_committer, 0 } },
{ "tree"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_tree, 0 } },
{ "parents"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitCommitGetter_parents, 0 } },
{ "parent"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitCommitProtoFunc_parent, 0 } },
{ "isAncestorOf"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitCommitProtoFunc_isAncestorOf, 1 } },
};
// ============================================================================
// JSGitCommitPrototype Implementation
// ============================================================================
const ClassInfo JSGitCommitPrototype::s_info = { "Commit"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitCommitPrototype) };
void JSGitCommitPrototype::finishCreation(VM& vm, JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSGitCommit::info(), JSGitCommitPrototypeTableValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
// ============================================================================
// JSGitCommitConstructor Implementation
// ============================================================================
const ClassInfo JSGitCommitConstructor::s_info = { "Commit"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitCommitConstructor) };
JSGitCommitConstructor* JSGitCommitConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSGitCommitPrototype* prototype)
{
JSGitCommitConstructor* constructor = new (NotNull, allocateCell<JSGitCommitConstructor>(vm)) JSGitCommitConstructor(vm, structure);
constructor->finishCreation(vm, globalObject, prototype);
return constructor;
}
void JSGitCommitConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitCommitPrototype* prototype)
{
Base::finishCreation(vm, 0, "Commit"_s, PropertyAdditionMode::WithoutStructureTransition);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitCommitConstructor::construct(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Commit cannot be directly constructed"_s));
return {};
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitCommitConstructor::call(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Commit cannot be called as a function"_s));
return {};
}
} // namespace Bun

View File

@@ -0,0 +1,712 @@
#include "root.h"
#include "JSGit.h"
#include "ZigGlobalObject.h"
#include "JavaScriptCore/JSCJSValueInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "wtf/text/WTFString.h"
#include "helpers.h"
#include "JSDOMExceptionHandling.h"
#include "BunClientData.h"
#include <git2.h>
namespace Bun {
using namespace JSC;
// libgit2 initialization
static std::once_flag s_libgit2InitFlag;
void initializeLibgit2()
{
std::call_once(s_libgit2InitFlag, []() {
git_libgit2_init();
});
}
void shutdownLibgit2()
{
git_libgit2_shutdown();
}
// Helper to throw git errors
static void throwGitError(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, int errorCode)
{
const git_error* err = git_error_last();
WTF::String message;
if (err && err->message) {
message = WTF::String::fromUTF8(err->message);
} else {
message = makeString("Git error: "_s, errorCode);
}
throwException(globalObject, scope, createError(globalObject, message));
}
// ============================================================================
// JSGitRepository Implementation
// ============================================================================
const ClassInfo JSGitRepository::s_info = { "Repository"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitRepository) };
JSGitRepository::~JSGitRepository()
{
if (m_repo) {
git_repository_free(m_repo);
m_repo = nullptr;
}
}
void JSGitRepository::finishCreation(VM& vm, JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
}
JSC::GCClient::IsoSubspace* JSGitRepository::subspaceForImpl(VM& vm)
{
return WebCore::subspaceForImpl<JSGitRepository, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitRepository.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitRepository = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForJSGitRepository.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitRepository = std::forward<decltype(space)>(space); });
}
// ============================================================================
// JSGitRepository Prototype Methods and Getters
// ============================================================================
// Getter: path
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_path, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "path"_s);
return {};
}
const char* path = git_repository_workdir(thisObject->repo());
if (!path) {
path = git_repository_path(thisObject->repo());
}
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(path)));
}
// Getter: gitDir
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_gitDir, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "gitDir"_s);
return {};
}
const char* path = git_repository_path(thisObject->repo());
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(path)));
}
// Getter: isBare
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_isBare, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "isBare"_s);
return {};
}
return JSValue::encode(jsBoolean(git_repository_is_bare(thisObject->repo())));
}
// Getter: isClean
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_isClean, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "isClean"_s);
return {};
}
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED;
git_status_list* statusList = nullptr;
int error = git_status_list_new(&statusList, thisObject->repo(), &opts);
if (error < 0) {
throwGitError(globalObject, scope, error);
return {};
}
size_t count = git_status_list_entrycount(statusList);
git_status_list_free(statusList);
return JSValue::encode(jsBoolean(count == 0));
}
// Getter: head (returns the HEAD commit)
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_head, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Repository"_s, "head"_s);
return {};
}
git_reference* headRef = nullptr;
int error = git_repository_head(&headRef, thisObject->repo());
if (error < 0) {
if (error == GIT_EUNBORNBRANCH || error == GIT_ENOTFOUND) {
return JSValue::encode(jsNull());
}
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
const git_oid* oid = git_reference_target(headRef);
git_commit* commit = nullptr;
error = git_commit_lookup(&commit, thisObject->repo(), oid);
git_reference_free(headRef);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
return JSValue::encode(JSGitCommit::create(vm, lexicalGlobalObject, structure, commit, thisObject));
}
// Getter: branch (returns the current branch or null if detached)
JSC_DEFINE_CUSTOM_GETTER(jsGitRepositoryGetter_branch, (JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitRepository*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Repository"_s, "branch"_s);
return {};
}
git_reference* headRef = nullptr;
int error = git_repository_head(&headRef, thisObject->repo());
if (error < 0) {
if (error == GIT_EUNBORNBRANCH || error == GIT_ENOTFOUND) {
return JSValue::encode(jsNull());
}
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
if (git_reference_is_branch(headRef)) {
auto* structure = globalObject->JSGitBranchStructure();
return JSValue::encode(JSGitBranch::create(vm, lexicalGlobalObject, structure, headRef, thisObject, false));
}
git_reference_free(headRef);
return JSValue::encode(jsNull());
}
// Method: getCommit(ref: string) -> Commit | null
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryProtoFunc_getCommit, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitRepository*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Repository"_s, "getCommit"_s);
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "getCommit requires a ref argument"_s));
return {};
}
auto refString = callFrame->argument(0).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
git_object* obj = nullptr;
int error = git_revparse_single(&obj, thisObject->repo(), refString.utf8().data());
if (error < 0) {
if (error == GIT_ENOTFOUND) {
return JSValue::encode(jsNull());
}
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
git_commit* commit = nullptr;
error = git_commit_lookup(&commit, thisObject->repo(), git_object_id(obj));
git_object_free(obj);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
return JSValue::encode(JSGitCommit::create(vm, lexicalGlobalObject, structure, commit, thisObject));
}
// Method: status(options?) -> StatusEntry[]
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryProtoFunc_status, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "status"_s);
return {};
}
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
// Parse options if provided
if (callFrame->argumentCount() > 0 && !callFrame->argument(0).isUndefinedOrNull()) {
JSObject* options = callFrame->argument(0).toObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});
JSValue includeUntracked = options->get(globalObject, Identifier::fromString(vm, "includeUntracked"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!includeUntracked.isUndefined() && !includeUntracked.toBoolean(globalObject)) {
opts.flags &= ~(GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS);
}
JSValue includeIgnored = options->get(globalObject, Identifier::fromString(vm, "includeIgnored"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!includeIgnored.isUndefined() && includeIgnored.toBoolean(globalObject)) {
opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED | GIT_STATUS_OPT_RECURSE_IGNORED_DIRS;
}
}
git_status_list* statusList = nullptr;
int error = git_status_list_new(&statusList, thisObject->repo(), &opts);
if (error < 0) {
throwGitError(globalObject, scope, error);
return {};
}
size_t count = git_status_list_entrycount(statusList);
JSArray* result = constructEmptyArray(globalObject, nullptr, count);
RETURN_IF_EXCEPTION(scope, {});
for (size_t i = 0; i < count; i++) {
const git_status_entry* entry = git_status_byindex(statusList, i);
JSObject* entryObj = constructEmptyObject(globalObject);
const char* path = entry->head_to_index ? entry->head_to_index->new_file.path
: entry->index_to_workdir ? entry->index_to_workdir->new_file.path
: nullptr;
if (path) {
entryObj->putDirect(vm, Identifier::fromString(vm, "path"_s), jsString(vm, WTF::String::fromUTF8(path)));
}
// Index status
WTF::String indexStatus = "unmodified"_s;
if (entry->status & GIT_STATUS_INDEX_NEW) indexStatus = "added"_s;
else if (entry->status & GIT_STATUS_INDEX_MODIFIED) indexStatus = "modified"_s;
else if (entry->status & GIT_STATUS_INDEX_DELETED) indexStatus = "deleted"_s;
else if (entry->status & GIT_STATUS_INDEX_RENAMED) indexStatus = "renamed"_s;
else if (entry->status & GIT_STATUS_INDEX_TYPECHANGE) indexStatus = "typechange"_s;
entryObj->putDirect(vm, Identifier::fromString(vm, "indexStatus"_s), jsString(vm, indexStatus));
// Worktree status
WTF::String wtStatus = "unmodified"_s;
if (entry->status & GIT_STATUS_WT_NEW) wtStatus = "untracked"_s;
else if (entry->status & GIT_STATUS_WT_MODIFIED) wtStatus = "modified"_s;
else if (entry->status & GIT_STATUS_WT_DELETED) wtStatus = "deleted"_s;
else if (entry->status & GIT_STATUS_WT_RENAMED) wtStatus = "renamed"_s;
else if (entry->status & GIT_STATUS_WT_TYPECHANGE) wtStatus = "typechange"_s;
else if (entry->status & GIT_STATUS_IGNORED) wtStatus = "ignored"_s;
else if (entry->status & GIT_STATUS_CONFLICTED) wtStatus = "unmerged"_s;
entryObj->putDirect(vm, Identifier::fromString(vm, "workTreeStatus"_s), jsString(vm, wtStatus));
// Original path for renames
const char* origPath = entry->head_to_index ? entry->head_to_index->old_file.path
: entry->index_to_workdir ? entry->index_to_workdir->old_file.path
: nullptr;
if (origPath && path && strcmp(origPath, path) != 0) {
entryObj->putDirect(vm, Identifier::fromString(vm, "origPath"_s), jsString(vm, WTF::String::fromUTF8(origPath)));
} else {
entryObj->putDirect(vm, Identifier::fromString(vm, "origPath"_s), jsNull());
}
result->putDirectIndex(globalObject, i, entryObj);
RETURN_IF_EXCEPTION(scope, {});
}
git_status_list_free(statusList);
return JSValue::encode(result);
}
// Method: add(paths: string | string[])
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryProtoFunc_add, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitRepository*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Repository"_s, "add"_s);
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(globalObject, scope, createError(globalObject, "add requires a path argument"_s));
return {};
}
git_index* index = nullptr;
int error = git_repository_index(&index, thisObject->repo());
if (error < 0) {
throwGitError(globalObject, scope, error);
return {};
}
JSValue pathsArg = callFrame->argument(0);
if (pathsArg.isString()) {
auto pathStr = pathsArg.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
error = git_index_add_bypath(index, pathStr.utf8().data());
} else if (isArray(globalObject, pathsArg)) {
JSArray* paths = jsCast<JSArray*>(pathsArg);
uint32_t length = paths->length();
for (uint32_t i = 0; i < length; i++) {
JSValue pathValue = paths->get(globalObject, i);
RETURN_IF_EXCEPTION(scope, {});
auto pathStr = pathValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
error = git_index_add_bypath(index, pathStr.utf8().data());
if (error < 0) break;
}
} else {
git_index_free(index);
throwException(globalObject, scope, createTypeError(globalObject, "paths must be a string or array of strings"_s));
return {};
}
if (error < 0) {
git_index_free(index);
throwGitError(globalObject, scope, error);
return {};
}
error = git_index_write(index);
git_index_free(index);
if (error < 0) {
throwGitError(globalObject, scope, error);
return {};
}
return JSValue::encode(jsUndefined());
}
// Method: commit(message: string, options?) -> Commit
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryProtoFunc_commit, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
auto* thisObject = jsDynamicCast<JSGitRepository*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*lexicalGlobalObject, scope, "Repository"_s, "commit"_s);
return {};
}
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "commit requires a message argument"_s));
return {};
}
auto message = callFrame->argument(0).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
// Get the index
git_index* index = nullptr;
int error = git_repository_index(&index, thisObject->repo());
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Write the index as a tree
git_oid treeId;
error = git_index_write_tree(&treeId, index);
git_index_free(index);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Lookup the tree
git_tree* tree = nullptr;
error = git_tree_lookup(&tree, thisObject->repo(), &treeId);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Get the default signature
git_signature* sig = nullptr;
error = git_signature_default(&sig, thisObject->repo());
if (error < 0) {
git_tree_free(tree);
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Get the parent commit (HEAD)
git_commit* parent = nullptr;
git_reference* headRef = nullptr;
error = git_repository_head(&headRef, thisObject->repo());
if (error == 0) {
const git_oid* parentId = git_reference_target(headRef);
error = git_commit_lookup(&parent, thisObject->repo(), parentId);
git_reference_free(headRef);
if (error < 0) {
git_signature_free(sig);
git_tree_free(tree);
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
} else if (error != GIT_EUNBORNBRANCH && error != GIT_ENOTFOUND) {
git_signature_free(sig);
git_tree_free(tree);
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Create the commit
git_oid commitId;
const git_commit* parents[] = { parent };
size_t parentCount = parent ? 1 : 0;
error = git_commit_create(
&commitId,
thisObject->repo(),
"HEAD",
sig,
sig,
nullptr,
message.utf8().data(),
tree,
parentCount,
parents
);
git_signature_free(sig);
git_tree_free(tree);
if (parent) git_commit_free(parent);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
// Return the new commit
git_commit* newCommit = nullptr;
error = git_commit_lookup(&newCommit, thisObject->repo(), &commitId);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
auto* structure = globalObject->JSGitCommitStructure();
return JSValue::encode(JSGitCommit::create(vm, lexicalGlobalObject, structure, newCommit, thisObject));
}
// ============================================================================
// JSGitRepository Prototype Table
// ============================================================================
static const HashTableValue JSGitRepositoryPrototypeTableValues[] = {
{ "path"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_path, 0 } },
{ "gitDir"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_gitDir, 0 } },
{ "isBare"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_isBare, 0 } },
{ "isClean"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_isClean, 0 } },
{ "head"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_head, 0 } },
{ "branch"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitRepositoryGetter_branch, 0 } },
{ "getCommit"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryProtoFunc_getCommit, 1 } },
{ "status"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryProtoFunc_status, 0 } },
{ "add"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryProtoFunc_add, 1 } },
{ "commit"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryProtoFunc_commit, 1 } },
};
// ============================================================================
// JSGitRepositoryPrototype Implementation
// ============================================================================
const ClassInfo JSGitRepositoryPrototype::s_info = { "Repository"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitRepositoryPrototype) };
void JSGitRepositoryPrototype::finishCreation(VM& vm, JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSGitRepository::info(), JSGitRepositoryPrototypeTableValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
// ============================================================================
// JSGitRepositoryConstructor Implementation
// ============================================================================
const ClassInfo JSGitRepositoryConstructor::s_info = { "Repository"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitRepositoryConstructor) };
// Static method: Repository.find(startPath?) -> Repository | null
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryConstructorFunc_find, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
initializeLibgit2();
WTF::String pathStr = "."_s;
if (callFrame->argumentCount() > 0 && !callFrame->argument(0).isUndefinedOrNull()) {
pathStr = callFrame->argument(0).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
git_buf repoPath = GIT_BUF_INIT;
int error = git_repository_discover(&repoPath, pathStr.utf8().data(), 0, nullptr);
if (error < 0) {
git_buf_dispose(&repoPath);
return JSValue::encode(jsNull());
}
git_repository* repo = nullptr;
error = git_repository_open(&repo, repoPath.ptr);
git_buf_dispose(&repoPath);
if (error < 0) {
return JSValue::encode(jsNull());
}
auto* structure = globalObject->JSGitRepositoryStructure();
return JSValue::encode(JSGitRepository::create(vm, lexicalGlobalObject, structure, repo));
}
// Static method: Repository.init(path, options?) -> Repository
JSC_DEFINE_HOST_FUNCTION(jsGitRepositoryConstructorFunc_init, (JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame))
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
initializeLibgit2();
if (callFrame->argumentCount() < 1) {
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "init requires a path argument"_s));
return {};
}
auto pathStr = callFrame->argument(0).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
bool isBare = false;
if (callFrame->argumentCount() > 1 && !callFrame->argument(1).isUndefinedOrNull()) {
JSObject* options = callFrame->argument(1).toObject(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
JSValue bareValue = options->get(lexicalGlobalObject, Identifier::fromString(vm, "bare"_s));
RETURN_IF_EXCEPTION(scope, {});
isBare = bareValue.toBoolean(lexicalGlobalObject);
}
git_repository* repo = nullptr;
int error = git_repository_init(&repo, pathStr.utf8().data(), isBare ? 1 : 0);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
auto* structure = globalObject->JSGitRepositoryStructure();
return JSValue::encode(JSGitRepository::create(vm, lexicalGlobalObject, structure, repo));
}
static const HashTableValue JSGitRepositoryConstructorTableValues[] = {
{ "find"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryConstructorFunc_find, 0 } },
{ "init"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitRepositoryConstructorFunc_init, 1 } },
};
JSGitRepositoryConstructor* JSGitRepositoryConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSGitRepositoryPrototype* prototype)
{
JSGitRepositoryConstructor* constructor = new (NotNull, allocateCell<JSGitRepositoryConstructor>(vm)) JSGitRepositoryConstructor(vm, structure);
constructor->finishCreation(vm, globalObject, prototype);
return constructor;
}
void JSGitRepositoryConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitRepositoryPrototype* prototype)
{
Base::finishCreation(vm, 1, "Repository"_s, PropertyAdditionMode::WithoutStructureTransition);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
reifyStaticProperties(vm, info(), JSGitRepositoryConstructorTableValues, *this);
}
// Constructor: new Repository(path?)
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitRepositoryConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame)
{
VM& vm = lexicalGlobalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = jsCast<Zig::GlobalObject*>(lexicalGlobalObject);
initializeLibgit2();
WTF::String pathStr = "."_s;
if (callFrame->argumentCount() > 0 && !callFrame->argument(0).isUndefinedOrNull()) {
pathStr = callFrame->argument(0).toWTFString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, {});
}
// Discover the repository
git_buf repoPath = GIT_BUF_INIT;
int error = git_repository_discover(&repoPath, pathStr.utf8().data(), 0, nullptr);
if (error < 0) {
git_buf_dispose(&repoPath);
throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, "Not a git repository"_s));
return {};
}
git_repository* repo = nullptr;
error = git_repository_open(&repo, repoPath.ptr);
git_buf_dispose(&repoPath);
if (error < 0) {
throwGitError(lexicalGlobalObject, scope, error);
return {};
}
auto* structure = globalObject->JSGitRepositoryStructure();
return JSValue::encode(JSGitRepository::create(vm, lexicalGlobalObject, structure, repo));
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitRepositoryConstructor::call(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Repository constructor cannot be called as a function"_s));
return {};
}
} // namespace Bun

View File

@@ -0,0 +1,188 @@
#include "root.h"
#include "JSGit.h"
#include "ZigGlobalObject.h"
#include "JavaScriptCore/JSCJSValueInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "JavaScriptCore/DateInstance.h"
#include "wtf/text/WTFString.h"
#include "helpers.h"
#include "JSDOMExceptionHandling.h"
#include "BunClientData.h"
#include <git2.h>
namespace Bun {
using namespace JSC;
// ============================================================================
// JSGitSignature Implementation
// ============================================================================
const ClassInfo JSGitSignature::s_info = { "Signature"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitSignature) };
JSC::GCClient::IsoSubspace* JSGitSignature::subspaceForImpl(VM& vm)
{
return WebCore::subspaceForImpl<JSGitSignature, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitSignature.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitSignature = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForJSGitSignature.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitSignature = std::forward<decltype(space)>(space); });
}
void JSGitSignature::finishCreation(VM& vm, JSGlobalObject* globalObject, const git_signature* sig)
{
Base::finishCreation(vm);
if (sig) {
m_name = WTF::String::fromUTF8(sig->name ? sig->name : "");
m_email = WTF::String::fromUTF8(sig->email ? sig->email : "");
m_time = sig->when.time;
m_offset = sig->when.offset;
}
}
// Getter: name
JSC_DEFINE_CUSTOM_GETTER(jsGitSignatureGetter_name, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitSignature*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Signature"_s, "name"_s);
return {};
}
return JSValue::encode(jsString(vm, thisObject->name()));
}
// Getter: email
JSC_DEFINE_CUSTOM_GETTER(jsGitSignatureGetter_email, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitSignature*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Signature"_s, "email"_s);
return {};
}
return JSValue::encode(jsString(vm, thisObject->email()));
}
// Getter: date
JSC_DEFINE_CUSTOM_GETTER(jsGitSignatureGetter_date, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitSignature*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Signature"_s, "date"_s);
return {};
}
// Convert git_time_t (seconds since epoch) to JavaScript Date (milliseconds)
double ms = static_cast<double>(thisObject->time()) * 1000.0;
return JSValue::encode(DateInstance::create(vm, globalObject->dateStructure(), ms));
}
// Getter: timezone
JSC_DEFINE_CUSTOM_GETTER(jsGitSignatureGetter_timezone, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitSignature*>(JSValue::decode(thisValue));
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Signature"_s, "timezone"_s);
return {};
}
int offset = thisObject->offset();
int hours = offset / 60;
int minutes = offset % 60;
if (minutes < 0) minutes = -minutes;
char buf[16];
snprintf(buf, sizeof(buf), "%+03d:%02d", hours, minutes);
return JSValue::encode(jsString(vm, WTF::String::fromUTF8(buf)));
}
// Method: toString() -> "Name <email>"
JSC_DEFINE_HOST_FUNCTION(jsGitSignatureProtoFunc_toString, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* thisObject = jsDynamicCast<JSGitSignature*>(callFrame->thisValue());
if (!thisObject) {
throwThisTypeError(*globalObject, scope, "Signature"_s, "toString"_s);
return {};
}
WTF::String result = makeString(thisObject->name(), " <"_s, thisObject->email(), ">"_s);
return JSValue::encode(jsString(vm, result));
}
// ============================================================================
// JSGitSignature Prototype Table
// ============================================================================
static const HashTableValue JSGitSignaturePrototypeTableValues[] = {
{ "name"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitSignatureGetter_name, 0 } },
{ "email"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitSignatureGetter_email, 0 } },
{ "date"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitSignatureGetter_date, 0 } },
{ "timezone"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly | PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsGitSignatureGetter_timezone, 0 } },
{ "toString"_s, static_cast<unsigned>(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsGitSignatureProtoFunc_toString, 0 } },
};
// ============================================================================
// JSGitSignaturePrototype Implementation
// ============================================================================
const ClassInfo JSGitSignaturePrototype::s_info = { "Signature"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitSignaturePrototype) };
void JSGitSignaturePrototype::finishCreation(VM& vm, JSGlobalObject* globalObject)
{
Base::finishCreation(vm);
reifyStaticProperties(vm, JSGitSignature::info(), JSGitSignaturePrototypeTableValues, *this);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
}
// ============================================================================
// JSGitSignatureConstructor Implementation
// ============================================================================
const ClassInfo JSGitSignatureConstructor::s_info = { "Signature"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSGitSignatureConstructor) };
JSGitSignatureConstructor* JSGitSignatureConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSGitSignaturePrototype* prototype)
{
JSGitSignatureConstructor* constructor = new (NotNull, allocateCell<JSGitSignatureConstructor>(vm)) JSGitSignatureConstructor(vm, structure);
constructor->finishCreation(vm, globalObject, prototype);
return constructor;
}
void JSGitSignatureConstructor::finishCreation(VM& vm, JSGlobalObject* globalObject, JSGitSignaturePrototype* prototype)
{
Base::finishCreation(vm, 0, "Signature"_s, PropertyAdditionMode::WithoutStructureTransition);
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitSignatureConstructor::construct(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Signature cannot be directly constructed"_s));
return {};
}
JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSGitSignatureConstructor::call(JSGlobalObject* globalObject, CallFrame*)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, createTypeError(globalObject, "Signature cannot be called as a function"_s));
return {};
}
} // namespace Bun

View File

@@ -125,6 +125,7 @@
#include "JSSocketAddressDTO.h"
#include "JSSQLStatement.h"
#include "JSStringDecoder.h"
#include "JSGit.h"
#include "JSTextEncoder.h"
#include "JSTextEncoderStream.h"
#include "JSTextDecoderStream.h"
@@ -2411,6 +2412,55 @@ void GlobalObject::finishCreation(VM& vm)
init.setConstructor(constructor);
});
// Git class structures
m_JSGitRepositoryClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSGitRepositoryPrototype::create(
init.vm, init.global, Bun::JSGitRepositoryPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSGitRepository::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSGitRepositoryConstructor::create(
init.vm, init.global, Bun::JSGitRepositoryConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});
m_JSGitCommitClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSGitCommitPrototype::create(
init.vm, init.global, Bun::JSGitCommitPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSGitCommit::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSGitCommitConstructor::create(
init.vm, init.global, Bun::JSGitCommitConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});
m_JSGitBranchClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSGitBranchPrototype::create(
init.vm, init.global, Bun::JSGitBranchPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSGitBranch::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSGitBranchConstructor::create(
init.vm, init.global, Bun::JSGitBranchConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});
m_JSGitSignatureClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSGitSignaturePrototype::create(
init.vm, init.global, Bun::JSGitSignaturePrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSGitSignature::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSGitSignatureConstructor::create(
init.vm, init.global, Bun::JSGitSignatureConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});
m_JSFFIFunctionStructure.initLater(
[](LazyClassStructure::Initializer& init) {
init.setStructure(Zig::JSFFIFunction::createStructure(init.vm, init.global, init.global->functionPrototype()));

View File

@@ -263,6 +263,22 @@ public:
JSC::JSObject* NodeVMSyntheticModule() const { return m_NodeVMSyntheticModuleClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue NodeVMSyntheticModulePrototype() const { return m_NodeVMSyntheticModuleClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSGitRepositoryStructure() const { return m_JSGitRepositoryClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSGitRepositoryConstructor() const { return m_JSGitRepositoryClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSGitRepositoryPrototype() const { return m_JSGitRepositoryClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSGitCommitStructure() const { return m_JSGitCommitClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSGitCommitConstructor() const { return m_JSGitCommitClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSGitCommitPrototype() const { return m_JSGitCommitClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSGitBranchStructure() const { return m_JSGitBranchClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSGitBranchConstructor() const { return m_JSGitBranchClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSGitBranchPrototype() const { return m_JSGitBranchClassStructure.prototypeInitializedOnMainThread(this); }
JSC::Structure* JSGitSignatureStructure() const { return m_JSGitSignatureClassStructure.getInitializedOnMainThread(this); }
JSC::JSObject* JSGitSignatureConstructor() const { return m_JSGitSignatureClassStructure.constructorInitializedOnMainThread(this); }
JSC::JSValue JSGitSignaturePrototype() const { return m_JSGitSignatureClassStructure.prototypeInitializedOnMainThread(this); }
JSC::JSMap* readableStreamNativeMap() const { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); }
JSC::JSMap* requireMap() const { return m_requireMap.getInitializedOnMainThread(this); }
JSC::JSMap* esmRegistryMap() const { return m_esmRegistryMap.getInitializedOnMainThread(this); }
@@ -563,6 +579,11 @@ public:
V(public, LazyClassStructure, m_JSConnectionsListClassStructure) \
V(public, LazyClassStructure, m_JSHTTPParserClassStructure) \
\
V(public, LazyClassStructure, m_JSGitRepositoryClassStructure) \
V(public, LazyClassStructure, m_JSGitCommitClassStructure) \
V(public, LazyClassStructure, m_JSGitBranchClassStructure) \
V(public, LazyClassStructure, m_JSGitSignatureClassStructure) \
\
V(private, LazyPropertyOfGlobalObject<Structure>, m_pendingVirtualModuleResultStructure) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskFunction) \
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
@@ -615,6 +636,7 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_NapiTypeTagStructure) \
\
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSQLStatementStructure) \
\
V(private, LazyPropertyOfGlobalObject<v8::shim::GlobalInternals>, m_V8GlobalInternals) \
\
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunObject) \

View File

@@ -32,6 +32,7 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
"timers"_s,
"undici"_s,
"bun:ffi"_s,
"bun:git"_s,
"bun:jsc"_s,
"cluster"_s,
"console"_s,

View File

@@ -18,6 +18,12 @@ public:
/* --- bun --- */
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBunClassConstructor;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForBufferList;
/* --- git --- */
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitRepository;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitCommit;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitBranch;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitSignature;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForFFIFunction;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForWrappingFunction;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNapiClass;

View File

@@ -18,6 +18,13 @@ public:
/*-- BUN --*/
std::unique_ptr<IsoSubspace> m_subspaceForBunClassConstructor;
std::unique_ptr<IsoSubspace> m_subspaceForBufferList;
/*-- GIT --*/
std::unique_ptr<IsoSubspace> m_subspaceForJSGitRepository;
std::unique_ptr<IsoSubspace> m_subspaceForJSGitCommit;
std::unique_ptr<IsoSubspace> m_subspaceForJSGitBranch;
std::unique_ptr<IsoSubspace> m_subspaceForJSGitSignature;
std::unique_ptr<IsoSubspace> m_subspaceForFFIFunction;
std::unique_ptr<IsoSubspace> m_subspaceForWrappingFunction;
std::unique_ptr<IsoSubspace> m_subspaceForNapiClass;

View File

@@ -0,0 +1,37 @@
#pragma once
#include "root.h"
#include "_NativeModule.h"
namespace Zig {
using namespace WebCore;
using namespace JSC;
DEFINE_NATIVE_MODULE(BunGit)
{
// Currently we export 4 classes: Repository, Commit, Branch, Signature
INIT_NATIVE_MODULE(4);
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(globalObject);
// Main classes (implemented so far)
put(JSC::Identifier::fromString(vm, "Repository"_s), zigGlobalObject->JSGitRepositoryConstructor());
put(JSC::Identifier::fromString(vm, "Commit"_s), zigGlobalObject->JSGitCommitConstructor());
put(JSC::Identifier::fromString(vm, "Branch"_s), zigGlobalObject->JSGitBranchConstructor());
put(JSC::Identifier::fromString(vm, "Signature"_s), zigGlobalObject->JSGitSignatureConstructor());
// TODO: Implement the remaining classes:
// - Remote
// - Diff
// - StatusEntry
// - Index
// - Config
// - Stash
// - Worktree
// - Blob
// - GitError
RETURN_NATIVE_MODULE();
}
} // namespace Zig

View File

@@ -28,6 +28,7 @@
macro("bun:test"_s, BunTest) \
macro("bun:jsc"_s, BunJSC) \
macro("bun:app"_s, BunApp) \
macro("bun:git"_s, BunGit) \
macro("node:buffer"_s, NodeBuffer) \
macro("node:constants"_s, NodeConstants) \
macro("node:string_decoder"_s, NodeStringDecoder) \

551
test/js/bun/git/git.test.ts Normal file
View File

@@ -0,0 +1,551 @@
import * as git from "bun:git";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, rm, stat, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
const { Repository, Commit, Branch, Signature } = git;
describe("bun:git module exports", () => {
test("exports Repository constructor", () => {
expect(Repository).toBeDefined();
expect(typeof Repository).toBe("function");
});
test("exports Commit constructor", () => {
expect(Commit).toBeDefined();
expect(typeof Commit).toBe("function");
});
test("exports Branch constructor", () => {
expect(Branch).toBeDefined();
expect(typeof Branch).toBe("function");
});
test("exports Signature constructor", () => {
expect(Signature).toBeDefined();
expect(typeof Signature).toBe("function");
});
test("new Repository() finds repo from current directory", () => {
// new Repository() is supported and finds the repo from current directory
// This tests from the bun workspace which is a git repo
const repo = new (Repository as any)();
expect(repo).toBeDefined();
expect(repo.path).toBeDefined();
});
test("Commit cannot be directly constructed", () => {
expect(() => new (Commit as any)()).toThrow();
});
test("Branch cannot be directly constructed", () => {
expect(() => new (Branch as any)()).toThrow();
});
test("Signature cannot be directly constructed", () => {
expect(() => new (Signature as any)()).toThrow();
});
});
describe("Repository.find()", () => {
let repoDir: string;
beforeAll(async () => {
// Create a temp directory and initialize a git repo
repoDir = await mkdtemp(join(tmpdir(), "bun-git-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("finds repository from exact path", () => {
const repo = Repository.find(repoDir);
expect(repo).toBeDefined();
expect(repo.path).toBe(repoDir + "/");
});
test("finds repository from subdirectory", async () => {
const subDir = join(repoDir, "subdir");
await mkdir(subDir, { recursive: true });
const repo = Repository.find(subDir);
expect(repo).toBeDefined();
expect(repo.path).toBe(repoDir + "/");
});
test("finds repository with default path (current directory)", () => {
const originalCwd = process.cwd();
try {
process.chdir(repoDir);
const repo = Repository.find();
expect(repo).toBeDefined();
expect(repo.path).toBe(repoDir + "/");
} finally {
process.chdir(originalCwd);
}
});
test("returns null when no repository found", async () => {
const noRepoDir = await mkdtemp(join(tmpdir(), "bun-git-no-repo-"));
try {
const repo = Repository.find(noRepoDir);
expect(repo).toBeNull();
} finally {
await rm(noRepoDir, { recursive: true, force: true });
}
});
test("repository has correct properties", () => {
const repo = Repository.find(repoDir);
expect(repo).toBeDefined();
expect(repo!.path).toContain(repoDir);
expect(repo!.gitDir).toContain(".git");
expect(repo!.isBare).toBe(false);
});
});
describe("Repository.init()", () => {
let testDir: string;
beforeAll(async () => {
testDir = await mkdtemp(join(tmpdir(), "bun-git-init-test-"));
});
afterAll(async () => {
await rm(testDir, { recursive: true, force: true });
});
test("initializes a new repository", async () => {
const newRepoPath = join(testDir, "new-repo");
await mkdir(newRepoPath);
const repo = Repository.init(newRepoPath);
expect(repo).toBeDefined();
expect(repo.path).toBe(newRepoPath + "/");
expect(repo.isBare).toBe(false);
// Verify .git directory was created
const gitDir = await stat(join(newRepoPath, ".git"));
expect(gitDir.isDirectory()).toBe(true);
});
test("initializes a bare repository", async () => {
const bareRepoPath = join(testDir, "bare-repo");
await mkdir(bareRepoPath);
const repo = Repository.init(bareRepoPath, { bare: true });
expect(repo).toBeDefined();
expect(repo.isBare).toBe(true);
});
test("init creates directory if it doesn't exist", async () => {
// libgit2 creates the directory structure if needed
const newPath = join(testDir, "auto-created-repo");
const repo = Repository.init(newPath);
expect(repo).toBeDefined();
expect(repo.path).toBe(newPath + "/");
});
});
describe("Repository with commits", () => {
let repoDir: string;
let repo: InstanceType<typeof Repository>;
beforeAll(async () => {
repoDir = await mkdtemp(join(tmpdir(), "bun-git-commit-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
// Create initial commit
await writeFile(join(repoDir, "README.md"), "# Test Repo\n");
await Bun.$`git -C ${repoDir} add README.md`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Initial commit"`.quiet();
// Create second commit
await writeFile(join(repoDir, "file1.txt"), "content1\n");
await Bun.$`git -C ${repoDir} add file1.txt`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Add file1"`.quiet();
repo = Repository.find(repoDir)!;
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("repository has head as Commit object", () => {
expect(repo.head).toBeDefined();
expect(typeof repo.head).toBe("object");
expect(repo.head.sha).toBeDefined();
expect(repo.head.sha.length).toBe(40); // SHA-1 hash length
});
test("repository has branch", () => {
const branch = repo.branch;
expect(branch).toBeDefined();
expect(branch!.name).toMatch(/^(main|master)$/);
expect(branch!.isHead).toBe(true);
expect(branch!.isRemote).toBe(false);
});
test("getCommit returns commit object", () => {
const commit = repo.getCommit(repo.head.sha);
expect(commit).toBeDefined();
expect(commit!.sha).toBe(repo.head.sha);
expect(commit!.shortSha.length).toBe(7);
expect(commit!.message).toBe("Add file1\n");
expect(commit!.summary).toBe("Add file1");
});
test("getCommit returns null for invalid SHA", () => {
const commit = repo.getCommit("0000000000000000000000000000000000000000");
expect(commit).toBeNull();
});
test("commit has author signature", () => {
const commit = repo.getCommit(repo.head.sha);
expect(commit!.author).toBeDefined();
expect(commit!.author.name).toBe("Test User");
expect(commit!.author.email).toBe("test@example.com");
expect(commit!.author.date).toBeInstanceOf(Date);
});
test("commit has committer signature", () => {
const commit = repo.getCommit(repo.head.sha);
expect(commit!.committer).toBeDefined();
expect(commit!.committer.name).toBe("Test User");
expect(commit!.committer.email).toBe("test@example.com");
});
test("commit has parent", () => {
const commit = repo.getCommit(repo.head.sha);
expect(commit!.parents).toBeDefined();
expect(commit!.parents.length).toBe(1);
const parent = commit!.parent(0);
expect(parent).toBeDefined();
expect(parent!.message).toBe("Initial commit\n");
});
test("commit.isAncestorOf works", () => {
const headCommit = repo.getCommit(repo.head.sha)!;
const parentCommit = headCommit.parent(0)!;
expect(parentCommit.isAncestorOf(headCommit)).toBe(true);
expect(headCommit.isAncestorOf(parentCommit)).toBe(false);
});
});
describe("Branch operations", () => {
let repoDir: string;
let repo: InstanceType<typeof Repository>;
beforeAll(async () => {
repoDir = await mkdtemp(join(tmpdir(), "bun-git-branch-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
await writeFile(join(repoDir, "README.md"), "# Test\n");
await Bun.$`git -C ${repoDir} add README.md`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Initial commit"`.quiet();
// Create a feature branch
await Bun.$`git -C ${repoDir} branch feature-branch`.quiet();
repo = Repository.find(repoDir)!;
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("branch has name property", () => {
const branch = repo.branch;
expect(branch).toBeDefined();
expect(branch!.name).toMatch(/^(main|master)$/);
});
test("branch has fullName property", () => {
const branch = repo.branch;
expect(branch!.fullName).toMatch(/^refs\/heads\/(main|master)$/);
});
test("branch has isHead property", () => {
const branch = repo.branch;
expect(branch!.isHead).toBe(true);
});
test("branch has isRemote property", () => {
const branch = repo.branch;
expect(branch!.isRemote).toBe(false);
});
test("branch has commit property", () => {
const branch = repo.branch;
const commit = branch!.commit;
expect(commit).toBeDefined();
// head is a Commit object, so compare SHAs
expect(commit.sha).toBe(repo.head.sha);
});
test("branch upstream is null for local branch", () => {
const branch = repo.branch;
expect(branch!.upstream).toBeNull();
});
test("branch ahead/behind are 0 without upstream", () => {
const branch = repo.branch;
expect(branch!.ahead).toBe(0);
expect(branch!.behind).toBe(0);
});
});
describe("Repository.status()", () => {
let repoDir: string;
let repo: InstanceType<typeof Repository>;
beforeAll(async () => {
repoDir = await mkdtemp(join(tmpdir(), "bun-git-status-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
await writeFile(join(repoDir, "README.md"), "# Test\n");
await Bun.$`git -C ${repoDir} add README.md`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Initial commit"`.quiet();
repo = Repository.find(repoDir)!;
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("status returns empty array for clean repo", () => {
const status = repo.status();
expect(status).toEqual([]);
});
test("status shows new file", async () => {
await writeFile(join(repoDir, "new-file.txt"), "new content\n");
const status = repo.status();
expect(status.length).toBe(1);
expect(status[0].path).toBe("new-file.txt");
expect(status[0].workTreeStatus).toBe("untracked");
expect(status[0].indexStatus).toBe("unmodified");
// Cleanup
await rm(join(repoDir, "new-file.txt"));
});
test("status shows modified file", async () => {
await writeFile(join(repoDir, "README.md"), "# Modified Test\n");
const status = repo.status();
expect(status.length).toBe(1);
expect(status[0].path).toBe("README.md");
expect(status[0].workTreeStatus).toBe("modified");
// Restore
await Bun.$`git -C ${repoDir} checkout README.md`.quiet();
});
test("isClean returns true for clean repo", async () => {
// Make sure repo is clean first
await Bun.$`git -C ${repoDir} checkout .`.quiet();
const freshRepo = Repository.find(repoDir)!;
expect(freshRepo.isClean).toBe(true);
});
test("isClean returns false for dirty repo", async () => {
await writeFile(join(repoDir, "dirty.txt"), "dirty\n");
// Need to refresh the repo
const freshRepo = Repository.find(repoDir)!;
expect(freshRepo.isClean).toBe(false);
// Cleanup
await rm(join(repoDir, "dirty.txt"));
});
});
describe("Repository.add() and commit()", () => {
let repoDir: string;
let repo: InstanceType<typeof Repository>;
beforeAll(async () => {
repoDir = await mkdtemp(join(tmpdir(), "bun-git-add-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
await writeFile(join(repoDir, "README.md"), "# Test\n");
await Bun.$`git -C ${repoDir} add README.md`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Initial commit"`.quiet();
repo = Repository.find(repoDir)!;
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("add stages a file", async () => {
await writeFile(join(repoDir, "staged.txt"), "staged content\n");
repo.add("staged.txt");
const status = repo.status();
const staged = status.find(s => s.path === "staged.txt");
expect(staged).toBeDefined();
expect(staged!.indexStatus).toBe("added");
// Unstage for cleanup
await Bun.$`git -C ${repoDir} reset HEAD staged.txt`.quiet();
await rm(join(repoDir, "staged.txt"));
});
test("add stages multiple files", async () => {
await writeFile(join(repoDir, "file1.txt"), "content1\n");
await writeFile(join(repoDir, "file2.txt"), "content2\n");
repo.add(["file1.txt", "file2.txt"]);
const status = repo.status();
const addedFiles = status.filter(s => s.indexStatus === "added");
expect(addedFiles.some(s => s.path === "file1.txt")).toBe(true);
expect(addedFiles.some(s => s.path === "file2.txt")).toBe(true);
// Cleanup
await Bun.$`git -C ${repoDir} reset HEAD file1.txt file2.txt`.quiet();
await rm(join(repoDir, "file1.txt"));
await rm(join(repoDir, "file2.txt"));
});
test("commit creates new commit", async () => {
await writeFile(join(repoDir, "committed.txt"), "committed content\n");
repo.add("committed.txt");
const oldHeadSha = repo.head.sha;
const newCommit = repo.commit("Test commit message");
expect(newCommit).toBeDefined();
expect(newCommit.sha).toBeDefined();
expect(newCommit.sha.length).toBe(40);
expect(newCommit.sha).not.toBe(oldHeadSha);
const commit = repo.getCommit(newCommit.sha);
expect(commit!.message).toContain("Test commit message");
});
});
describe("Signature", () => {
let repoDir: string;
let repo: InstanceType<typeof Repository>;
beforeAll(async () => {
repoDir = await mkdtemp(join(tmpdir(), "bun-git-sig-test-"));
await Bun.$`git init ${repoDir}`.quiet();
await Bun.$`git -C ${repoDir} config user.email "test@example.com"`.quiet();
await Bun.$`git -C ${repoDir} config user.name "Test User"`.quiet();
await writeFile(join(repoDir, "README.md"), "# Test\n");
await Bun.$`git -C ${repoDir} add README.md`.quiet();
await Bun.$`git -C ${repoDir} commit -m "Initial commit"`.quiet();
repo = Repository.find(repoDir)!;
});
afterAll(async () => {
await rm(repoDir, { recursive: true, force: true });
});
test("signature has name property", () => {
const commit = repo.getCommit(repo.head.sha);
const sig = commit!.author;
expect(sig.name).toBe("Test User");
});
test("signature has email property", () => {
const commit = repo.getCommit(repo.head.sha);
const sig = commit!.author;
expect(sig.email).toBe("test@example.com");
});
test("signature has date property", () => {
const commit = repo.getCommit(repo.head.sha);
const sig = commit!.author;
expect(sig.date).toBeInstanceOf(Date);
expect(sig.date.getTime()).toBeLessThanOrEqual(Date.now());
expect(sig.date.getTime()).toBeGreaterThan(Date.now() - 60000); // Within last minute
});
test("signature has timezone property", () => {
const commit = repo.getCommit(repo.head.sha);
const sig = commit!.author;
expect(sig.timezone).toMatch(/^[+-]\d{2}:\d{2}$/);
});
test("signature toString() returns formatted string", () => {
const commit = repo.getCommit(repo.head.sha);
const sig = commit!.author;
expect(sig.toString()).toBe("Test User <test@example.com>");
});
});
describe("Error handling", () => {
test("Repository.find returns null for invalid argument type", () => {
// find() coerces arguments to string, so 123 becomes "123" which is not a valid repo
const result = (Repository as any).find(123);
expect(result).toBeNull();
});
test("Repository.init throws on missing path", () => {
expect(() => (Repository as any).init()).toThrow();
});
test("getCommit handles invalid sha gracefully", () => {
const repoDir = process.cwd(); // Use current bun repo
const repo = Repository.find(repoDir);
if (repo) {
const result = repo.getCommit("invalid-sha");
expect(result).toBeNull();
}
});
});
describe("Using Bun repository", () => {
test("can find Bun repository", () => {
const repo = Repository.find(process.cwd());
expect(repo).toBeDefined();
});
test("Bun repo has commits", () => {
const repo = Repository.find(process.cwd());
if (repo) {
expect(repo.head).toBeDefined();
expect(repo.head.sha.length).toBe(40);
const commit = repo.getCommit(repo.head.sha);
expect(commit).toBeDefined();
expect(commit!.message.length).toBeGreaterThan(0);
}
});
test("Bun repo has branch", () => {
const repo = Repository.find(process.cwd());
if (repo) {
const branch = repo.branch;
expect(branch).toBeDefined();
expect(branch!.name.length).toBeGreaterThan(0);
}
});
});

1
vendor/libgit2 vendored Submodule

Submodule vendor/libgit2 added at d908000464