Compare commits

...

6 Commits

Author SHA1 Message Date
Sosuke Suzuki
a40e2b857e fix(git): fix resource leak in countCommits on exception
Replace RETURN_IF_EXCEPTION with explicit cleanup that frees the
git_revwalk before returning when toWTFString throws an exception.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:54:24 +09:00
Sosuke Suzuki
48a6082167 fix(git): fix resource leaks and add edge case tests
Fix memory leaks when JS exceptions occur during array construction
in getStatus, listFiles, and diff functions. Add comprehensive tests
for empty repositories, invalid arguments, and error messages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:41:34 +09:00
Sosuke Suzuki
0b6d896adf test(git): add comprehensive tests for bun:git APIs
Add tests for error handling, edge cases, and temporary repository
scenarios including status detection, diff operations, and detached
HEAD state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:29:45 +09:00
Sosuke Suzuki
c9dc5dd381 feat(git): add status, diff, log, and rev-parse APIs to bun:git
Add read-only Git operations: getStatus, revParse, getCurrentBranch,
aheadBehind, listFiles, diff, countCommits, and log. Includes Status
and DeltaType constants for nodegit compatibility.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 13:02:28 +09:00
Sosuke Suzuki
d33550ddba docs: add type definitions for bun:git
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:53:39 +09:00
Sosuke Suzuki
f02511d2f8 feat: add bun:git module with libgit2 integration
Adds a new built-in module for Git operations using libgit2 (v1.9.0) statically linked.
Initial implementation includes Repository.open(), head(), and Commit properties (id, message, summary, author, committer, time).
Designed for local-only operations with lazy initialization to avoid overhead for users who don't need Git functionality.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:50:25 +09:00
14 changed files with 3567 additions and 1 deletions

View File

@@ -46,6 +46,7 @@
"src/io/*.cpp",
"src/bun.js/modules/*.cpp",
"src/bun.js/bindings/*.cpp",
"src/bun.js/bindings/git/*.cpp",
"src/bun.js/bindings/webcore/*.cpp",
"src/bun.js/bindings/sqlite/*.cpp",
"src/bun.js/bindings/webcrypto/*.cpp",

View File

@@ -54,6 +54,7 @@ set(BUN_DEPENDENCIES
Cares
Highway
LibDeflate
Libgit2
LolHtml
Lshpack
Mimalloc
@@ -1322,7 +1323,7 @@ list(TRANSFORM BUN_DEPENDENCIES TOLOWER OUTPUT_VARIABLE BUN_TARGETS)
add_custom_target(dependencies DEPENDS ${BUN_TARGETS})
if(APPLE)
target_link_libraries(${bun} PRIVATE icucore resolv)
target_link_libraries(${bun} PRIVATE icucore resolv iconv)
target_compile_definitions(${bun} PRIVATE U_DISABLE_RENAMING=1)
endif()

View File

@@ -0,0 +1,40 @@
register_repository(
NAME
libgit2
REPOSITORY
libgit2/libgit2
TAG
v1.9.0
)
register_cmake_command(
TARGET
libgit2
TARGETS
libgit2package
ARGS
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
-DBUILD_SHARED_LIBS=OFF
-DBUILD_TESTS=OFF
-DBUILD_CLI=OFF
-DBUILD_EXAMPLES=OFF
-DBUILD_FUZZERS=OFF
# Network disabled - local operations only
-DUSE_HTTPS=OFF
-DUSE_SSH=OFF
# Use bundled dependencies to avoid symbol conflicts with Bun's libraries
-DUSE_BUNDLED_ZLIB=ON
-DUSE_HTTP_PARSER=builtin
-DREGEX_BACKEND=builtin
-DUSE_SHA1=CollisionDetection
# Enable threading
-DUSE_THREADS=ON
# Disable authentication features (not needed for local operations)
-DUSE_GSSAPI=OFF
LIB_PATH
.
LIBRARIES
git2
INCLUDES
include
)

642
packages/bun-types/git.d.ts vendored Normal file
View File

@@ -0,0 +1,642 @@
/**
* Fast Git operations for Bun.js powered by libgit2.
*
* This module provides read-only Git repository operations.
* Network operations (HTTPS/SSH) are not supported - local operations only.
*
* @example
* ```ts
* import { Repository } from 'bun:git';
*
* const repo = Repository.open('.');
* const head = repo.head();
* console.log(`HEAD: ${head.id} - ${head.summary}`);
* console.log(`Author: ${head.author.name} <${head.author.email}>`);
* ```
*
* @module bun:git
*/
declare module "bun:git" {
/**
* Represents a Git signature (author or committer information).
*/
export interface Signature {
/**
* The name of the person.
* @example "John Doe"
*/
readonly name: string;
/**
* The email address of the person.
* @example "john@example.com"
*/
readonly email: string;
/**
* Unix timestamp of when the signature was created.
* @example 1704067200
*/
readonly time: number;
}
/**
* Status flags for working directory entries.
* These are bit flags that can be combined with bitwise OR.
*
* @example
* ```ts
* import { Status } from 'bun:git';
*
* const entries = repo.getStatus();
* for (const entry of entries) {
* if (entry.status & Status.WT_MODIFIED) {
* console.log('Modified in workdir:', entry.path);
* }
* if (entry.status & Status.INDEX_NEW) {
* console.log('New in index:', entry.path);
* }
* }
* ```
*/
export const Status: {
/** Entry is current and unchanged */
readonly CURRENT: 0;
/** Entry is new in the index */
readonly INDEX_NEW: 1;
/** Entry is modified in the index */
readonly INDEX_MODIFIED: 2;
/** Entry is deleted in the index */
readonly INDEX_DELETED: 4;
/** Entry is renamed in the index */
readonly INDEX_RENAMED: 8;
/** Entry type changed in the index */
readonly INDEX_TYPECHANGE: 16;
/** Entry is new in the working tree */
readonly WT_NEW: 128;
/** Entry is modified in the working tree */
readonly WT_MODIFIED: 256;
/** Entry is deleted in the working tree */
readonly WT_DELETED: 512;
/** Entry type changed in the working tree */
readonly WT_TYPECHANGE: 1024;
/** Entry is renamed in the working tree */
readonly WT_RENAMED: 2048;
/** Entry is ignored */
readonly IGNORED: 16384;
/** Entry is conflicted */
readonly CONFLICTED: 32768;
};
/**
* Delta types for diff entries.
*
* @example
* ```ts
* import { DeltaType } from 'bun:git';
*
* const diff = repo.diff();
* for (const file of diff.files) {
* if (file.status === DeltaType.ADDED) {
* console.log('Added:', file.newPath);
* }
* }
* ```
*/
export const DeltaType: {
/** No changes */
readonly UNMODIFIED: 0;
/** Entry does not exist in old version */
readonly ADDED: 1;
/** Entry does not exist in new version */
readonly DELETED: 2;
/** Entry content changed between old and new */
readonly MODIFIED: 3;
/** Entry was renamed between old and new */
readonly RENAMED: 4;
/** Entry was copied from another old entry */
readonly COPIED: 5;
/** Entry is ignored item in workdir */
readonly IGNORED: 6;
/** Entry is untracked item in workdir */
readonly UNTRACKED: 7;
/** Entry type changed between old and new */
readonly TYPECHANGE: 8;
/** Entry is unreadable */
readonly CONFLICTED: 10;
};
/**
* Options for getting repository status.
*/
export interface StatusOptions {
/**
* Include untracked files in the status.
* @default true
*/
includeUntracked?: boolean;
/**
* Include ignored files in the status.
* @default false
*/
includeIgnored?: boolean;
/**
* Recurse into untracked directories.
* @default true
*/
recurseUntrackedDirs?: boolean;
/**
* Detect renamed files.
* @default false
*/
detectRenames?: boolean;
}
/**
* Represents a status entry for a file in the working directory.
*/
export class StatusEntry {
/**
* The path of the file relative to the repository root.
*/
readonly path: string;
/**
* Status flags (combination of Status values).
*/
readonly status: number;
/**
* Check if the entry is new (untracked or staged as new).
*/
isNew(): boolean;
/**
* Check if the entry is modified.
*/
isModified(): boolean;
/**
* Check if the entry is deleted.
*/
isDeleted(): boolean;
/**
* Check if the entry is renamed.
*/
isRenamed(): boolean;
/**
* Check if the entry is ignored.
*/
isIgnored(): boolean;
/**
* Check if the entry has changes staged in the index.
*/
inIndex(): boolean;
/**
* Check if the entry has changes in the working tree.
*/
inWorkingTree(): boolean;
}
/**
* Represents an entry in the Git index.
*/
export interface IndexEntry {
/**
* The path of the file relative to the repository root.
*/
readonly path: string;
/**
* The file mode (e.g., 0o100644 for regular files).
*/
readonly mode: number;
/**
* The blob OID (SHA-1 hash) of the file content.
*/
readonly oid: string;
/**
* The stage number (0 for normal, 1-3 for conflict stages).
*/
readonly stage: number;
/**
* The file size in bytes.
*/
readonly size: number;
}
/**
* Options for getting diff information.
*/
export interface DiffOptions {
/**
* If true, compare HEAD to index (staged changes).
* If false, compare HEAD to working directory.
* @default false
*/
cached?: boolean;
}
/**
* Represents a changed file in a diff.
*/
export interface DiffFile {
/**
* The type of change (see DeltaType).
*/
readonly status: number;
/**
* The old path (null for added files).
*/
readonly oldPath: string | null;
/**
* The new path.
*/
readonly newPath: string;
/**
* Similarity percentage for renamed/copied files (0-100).
*/
readonly similarity?: number;
}
/**
* Result of a diff operation.
*/
export interface DiffResult {
/**
* List of changed files.
*/
readonly files: DiffFile[];
/**
* Statistics about the diff.
*/
readonly stats: {
/** Number of files changed */
readonly filesChanged: number;
/** Total lines inserted */
readonly insertions: number;
/** Total lines deleted */
readonly deletions: number;
};
}
/**
* Options for getting commit history.
*/
export interface LogOptions {
/**
* Starting point for history traversal.
* @default "HEAD"
*/
from?: string;
/**
* Range specification (e.g., "origin/main..HEAD").
* If provided, `from` is ignored.
*/
range?: string;
/**
* Maximum number of commits to return.
* @default unlimited
*/
limit?: number;
}
/**
* Represents a Git commit object.
*
* A commit contains information about a snapshot of the repository,
* including the author, committer, message, and parent commits.
*
* @example
* ```ts
* const head = repo.head();
* console.log(head.id); // "abc123..."
* console.log(head.message); // "feat: add new feature\n\nDetailed description..."
* console.log(head.summary); // "feat: add new feature"
* ```
*/
export class Commit {
/**
* The full 40-character hexadecimal SHA-1 hash of the commit.
* @example "a1b2c3d4e5f6..."
*/
readonly id: string;
/**
* The full commit message, including the body.
* @example "feat: add new feature\n\nThis commit adds..."
*/
readonly message: string;
/**
* The first line of the commit message (the summary/title).
* Does not include any trailing newline.
* @example "feat: add new feature"
*/
readonly summary: string;
/**
* The author of the commit (who wrote the changes).
*/
readonly author: Signature;
/**
* The committer of the commit (who committed the changes).
* This may differ from the author in cases like cherry-picks or rebases.
*/
readonly committer: Signature;
/**
* Unix timestamp of when the commit was created.
* This is the committer's timestamp.
* @example 1704067200
*/
readonly time: number;
}
/**
* Represents a Git repository.
*
* Use {@link Repository.open} to open an existing repository.
*
* @example
* ```ts
* import { Repository } from 'bun:git';
*
* // Open the repository at the current directory
* const repo = Repository.open('.');
*
* // Get repository info
* console.log('Path:', repo.path); // "/path/to/repo/.git/"
* console.log('Workdir:', repo.workdir); // "/path/to/repo/"
* console.log('Is bare:', repo.isBare); // false
*
* // Get the HEAD commit
* const head = repo.head();
* console.log('HEAD:', head.id.slice(0, 7), head.summary);
* ```
*/
export class Repository {
/**
* Opens an existing Git repository.
*
* The path can point to either a working directory or a bare repository.
* If the path points to a working directory, the `.git` directory will be located automatically.
*
* @param path Path to the repository (working directory or .git directory)
* @returns A Repository instance
* @throws Error if the path is not a valid Git repository
*
* @example
* ```ts
* // Open by working directory
* const repo = Repository.open('/path/to/project');
*
* // Open by .git directory
* const repo2 = Repository.open('/path/to/project/.git');
*
* // Open current directory
* const repo3 = Repository.open('.');
* ```
*/
static open(path: string): Repository;
/**
* Gets the commit that HEAD currently points to.
*
* @returns The commit that HEAD references
* @throws Error if HEAD is unborn (new repository with no commits)
*
* @example
* ```ts
* const head = repo.head();
* console.log(`Current commit: ${head.summary}`);
* console.log(`Author: ${head.author.name}`);
* ```
*/
head(): Commit;
/**
* The path to the `.git` directory.
* Always ends with a trailing slash.
*
* @example "/Users/me/project/.git/"
*/
readonly path: string;
/**
* The path to the working directory.
* Returns `null` for bare repositories.
* When present, always ends with a trailing slash.
*
* @example "/Users/me/project/"
*/
readonly workdir: string | null;
/**
* Whether this is a bare repository.
* Bare repositories have no working directory.
*
* @example
* ```ts
* if (repo.isBare) {
* console.log('This is a bare repository');
* }
* ```
*/
readonly isBare: boolean;
/**
* Gets the working directory status.
*
* Returns an array of status entries for all changed files in the
* working directory and index.
*
* @param options Options to control which files are included
* @returns Array of status entries
*
* @example
* ```ts
* import { Repository, Status } from 'bun:git';
*
* const repo = Repository.open('.');
* const status = repo.getStatus();
*
* for (const entry of status) {
* if (entry.isModified()) {
* console.log('Modified:', entry.path);
* }
* if (entry.isNew()) {
* console.log('New:', entry.path);
* }
* }
* ```
*/
getStatus(options?: StatusOptions): StatusEntry[];
/**
* Resolves a revision specification to a commit OID.
*
* Supports standard Git revision syntax including:
* - Branch names: "main", "feature/foo"
* - Tag names: "v1.0.0"
* - SHA prefixes: "abc123"
* - Special refs: "HEAD", "HEAD~1", "HEAD^2"
* - Upstream: "@{u}", "main@{u}"
*
* @param spec The revision specification to resolve
* @returns The 40-character hex OID
* @throws Error if the spec cannot be resolved
*
* @example
* ```ts
* const headOid = repo.revParse('HEAD');
* const parentOid = repo.revParse('HEAD~1');
* const branchOid = repo.revParse('main');
* ```
*/
revParse(spec: string): string;
/**
* Gets the name of the current branch.
*
* @returns The branch name, or null if HEAD is detached or unborn
*
* @example
* ```ts
* const branch = repo.getCurrentBranch();
* if (branch) {
* console.log('On branch:', branch);
* } else {
* console.log('HEAD is detached');
* }
* ```
*/
getCurrentBranch(): string | null;
/**
* Gets the ahead/behind counts between two commits.
*
* This is useful for comparing a local branch to its upstream.
*
* @param local The local ref (default: "HEAD")
* @param upstream The upstream ref (default: "@{u}")
* @returns Object with ahead and behind counts
*
* @example
* ```ts
* const { ahead, behind } = repo.aheadBehind();
* console.log(`${ahead} ahead, ${behind} behind`);
*
* // Compare specific refs
* const { ahead, behind } = repo.aheadBehind('feature', 'origin/main');
* ```
*/
aheadBehind(local?: string, upstream?: string): { ahead: number; behind: number };
/**
* Gets the list of files tracked in the index.
*
* @returns Array of index entries
*
* @example
* ```ts
* const files = repo.listFiles();
* console.log(`Tracking ${files.length} files`);
*
* for (const file of files) {
* console.log(`${file.path} (mode: ${file.mode.toString(8)})`);
* }
* ```
*/
listFiles(): IndexEntry[];
/**
* Gets diff information between HEAD and working directory or index.
*
* @param options Options to control the diff behavior
* @returns Diff result with file list and statistics
*
* @example
* ```ts
* import { Repository, DeltaType } from 'bun:git';
*
* const repo = Repository.open('.');
*
* // Unstaged changes (HEAD vs workdir)
* const diff = repo.diff();
* console.log(`${diff.stats.filesChanged} files changed`);
* console.log(`+${diff.stats.insertions} -${diff.stats.deletions}`);
*
* // Staged changes (HEAD vs index)
* const staged = repo.diff({ cached: true });
*
* for (const file of diff.files) {
* if (file.status === DeltaType.MODIFIED) {
* console.log('Modified:', file.newPath);
* }
* }
* ```
*/
diff(options?: DiffOptions): DiffResult;
/**
* Counts the number of commits in a range.
*
* @param range Optional range specification (e.g., "origin/main..HEAD")
* @returns Number of commits
*
* @example
* ```ts
* // Total commits
* const total = repo.countCommits();
*
* // Commits since origin/main
* const since = repo.countCommits('origin/main..HEAD');
* ```
*/
countCommits(range?: string): number;
/**
* Gets the commit history.
*
* @param options Options to control the log behavior
* @returns Array of commits
*
* @example
* ```ts
* // Last 10 commits
* const commits = repo.log({ limit: 10 });
*
* for (const commit of commits) {
* console.log(`${commit.id.slice(0, 7)} ${commit.summary}`);
* }
*
* // Commits in a range
* const range = repo.log({ range: 'origin/main..HEAD' });
*
* // Commits from a specific ref
* const fromTag = repo.log({ from: 'v1.0.0', limit: 5 });
* ```
*/
log(options?: LogOptions): Commit[];
}
export default Repository;
}

View File

@@ -14,6 +14,7 @@
/// <reference path="./html-rewriter.d.ts" />
/// <reference path="./jsc.d.ts" />
/// <reference path="./sqlite.d.ts" />
/// <reference path="./git.d.ts" />
/// <reference path="./test.d.ts" />
/// <reference path="./wasm.d.ts" />
/// <reference path="./overrides.d.ts" />

View File

@@ -10,6 +10,7 @@ pub const HardcodedModule = enum {
@"bun:test",
@"bun:wrap",
@"bun:sqlite",
@"bun:git",
@"node:assert",
@"node:assert/strict",
@"node:async_hooks",
@@ -98,6 +99,7 @@ pub const HardcodedModule = enum {
.{ "bun:main", .@"bun:main" },
.{ "bun:test", .@"bun:test" },
.{ "bun:sqlite", .@"bun:sqlite" },
.{ "bun:git", .@"bun:git" },
.{ "bun:wrap", .@"bun:wrap" },
.{ "bun:internal-for-testing", .@"bun:internal-for-testing" },
// Node.js
@@ -366,6 +368,7 @@ pub const HardcodedModule = enum {
.{ "bun:ffi", .{ .path = "bun:ffi" } },
.{ "bun:jsc", .{ .path = "bun:jsc" } },
.{ "bun:sqlite", .{ .path = "bun:sqlite" } },
.{ "bun:git", .{ .path = "bun:git" } },
.{ "bun:wrap", .{ .path = "bun:wrap" } },
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
.{ "ffi", .{ .path = "bun:ffi" } },

View File

@@ -125,6 +125,7 @@
#include "JSSocketAddressDTO.h"
#include "JSReactElement.h"
#include "JSSQLStatement.h"
#include "git/JSGit.h"
#include "JSStringDecoder.h"
#include "JSTextEncoder.h"
#include "JSTextEncoderStream.h"
@@ -1868,6 +1869,16 @@ void GlobalObject::finishCreation(VM& vm)
init.set(WebCore::createJSSQLStatementStructure(init.owner));
});
m_JSGitRepositoryStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(WebCore::createJSGitRepositoryStructure(init.owner));
});
m_JSGitCommitStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(WebCore::createJSGitCommitStructure(init.owner));
});
m_V8GlobalInternals.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, v8::shim::GlobalInternals>::Initializer& init) {
init.set(

View File

@@ -316,6 +316,9 @@ public:
Structure* JSSQLStatementStructure() const { return m_JSSQLStatementStructure.getInitializedOnMainThread(this); }
Structure* JSGitRepositoryStructure() const { return m_JSGitRepositoryStructure.getInitializedOnMainThread(this); }
Structure* JSGitCommitStructure() const { return m_JSGitCommitStructure.getInitializedOnMainThread(this); }
v8::shim::GlobalInternals* V8GlobalInternals() const { return m_V8GlobalInternals.getInitializedOnMainThread(this); }
Bun::BakeAdditionsToGlobalObject& bakeAdditions() { return m_bakeAdditions; }
@@ -620,6 +623,8 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_NapiTypeTagStructure) \
\
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSQLStatementStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSGitRepositoryStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSGitCommitStructure) \
V(private, LazyPropertyOfGlobalObject<v8::shim::GlobalInternals>, m_V8GlobalInternals) \
\
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunObject) \

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) 2024 Oven-sh
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "root.h"
#include "ZigGlobalObject.h"
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/JSDestructibleObject.h>
#include <JavaScriptCore/VM.h>
#include "headers-handwritten.h"
#include "BunClientData.h"
#include <JavaScriptCore/CallFrame.h>
// Forward declarations for libgit2 types
typedef struct git_repository git_repository;
typedef struct git_commit git_commit;
typedef struct git_oid git_oid;
namespace WebCore {
// Forward declarations
class JSGitRepository;
class JSGitCommit;
class JSGitOid;
// JSGitRepository - Wraps git_repository*
class JSGitRepository final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSGitRepository* create(JSC::VM& vm, JSC::Structure* structure, git_repository* repo);
static void destroy(JSC::JSCell* cell);
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::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); });
}
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());
}
git_repository* repository() const { return m_repo; }
private:
JSGitRepository(JSC::VM& vm, JSC::Structure* structure, git_repository* repo)
: Base(vm, structure)
, m_repo(repo)
{
}
void finishCreation(JSC::VM& vm);
git_repository* m_repo { nullptr };
};
// JSGitCommit - Wraps git_commit*
class JSGitCommit final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static JSGitCommit* create(JSC::VM& vm, JSC::Structure* structure, git_commit* commit);
static void destroy(JSC::JSCell* cell);
DECLARE_INFO;
template<typename CellType, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::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); });
}
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());
}
git_commit* commit() const { return m_commit; }
private:
JSGitCommit(JSC::VM& vm, JSC::Structure* structure, git_commit* commit)
: Base(vm, structure)
, m_commit(commit)
{
}
void finishCreation(JSC::VM& vm);
git_commit* m_commit { nullptr };
};
// Structure creation functions
JSC::Structure* createJSGitRepositoryStructure(JSC::JSGlobalObject* globalObject);
JSC::Structure* createJSGitCommitStructure(JSC::JSGlobalObject* globalObject);
// Module creation function (called from $cpp)
JSC::JSValue createJSGitModule(Zig::GlobalObject* globalObject);
} // namespace WebCore

View File

@@ -24,6 +24,8 @@ public:
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNapiPrototype;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSQLStatement;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSQLStatementConstructor;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitRepository;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitCommit;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSinkConstructor;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSinkController;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSink;

View File

@@ -24,6 +24,8 @@ public:
std::unique_ptr<IsoSubspace> m_subspaceForNapiPrototype;
std::unique_ptr<IsoSubspace> m_subspaceForJSSQLStatement;
std::unique_ptr<IsoSubspace> m_subspaceForJSSQLStatementConstructor;
std::unique_ptr<IsoSubspace> m_subspaceForJSGitRepository;
std::unique_ptr<IsoSubspace> m_subspaceForJSGitCommit;
std::unique_ptr<IsoSubspace> m_subspaceForJSSinkConstructor;
std::unique_ptr<IsoSubspace> m_subspaceForJSSinkController;
std::unique_ptr<IsoSubspace> m_subspaceForJSSink;

306
src/js/bun/git.ts Normal file
View File

@@ -0,0 +1,306 @@
// Hardcoded module "bun:git"
let Git: any;
function initializeGit() {
Git = $cpp("git/JSGit.cpp", "createJSGitModule");
}
interface Signature {
name: string;
email: string;
time: number; // Unix timestamp in milliseconds
}
interface StatusOptions {
includeUntracked?: boolean;
includeIgnored?: boolean;
recurseUntrackedDirs?: boolean;
detectRenames?: boolean;
}
interface InternalStatusEntry {
path: string;
status: number;
}
interface IndexEntry {
path: string;
mode: number;
oid: string;
stage: number;
size: number;
}
interface DiffOptions {
cached?: boolean;
}
interface DiffFile {
status: number;
oldPath: string | null;
newPath: string;
similarity?: number;
}
interface DiffResult {
files: DiffFile[];
stats: {
filesChanged: number;
insertions: number;
deletions: number;
};
}
interface LogOptions {
from?: string;
range?: string;
limit?: number;
}
// Status constants (nodegit compatible)
const Status = {
CURRENT: 0,
INDEX_NEW: 1,
INDEX_MODIFIED: 2,
INDEX_DELETED: 4,
INDEX_RENAMED: 8,
INDEX_TYPECHANGE: 16,
WT_NEW: 128,
WT_MODIFIED: 256,
WT_DELETED: 512,
WT_TYPECHANGE: 1024,
WT_RENAMED: 2048,
IGNORED: 16384,
CONFLICTED: 32768,
};
// DeltaType constants (nodegit compatible)
const DeltaType = {
UNMODIFIED: 0,
ADDED: 1,
DELETED: 2,
MODIFIED: 3,
RENAMED: 4,
COPIED: 5,
IGNORED: 6,
UNTRACKED: 7,
TYPECHANGE: 8,
CONFLICTED: 10,
};
class StatusEntry {
path: string;
status: number;
constructor(entry: InternalStatusEntry) {
this.path = entry.path;
this.status = entry.status;
}
isNew(): boolean {
return (this.status & (Status.INDEX_NEW | Status.WT_NEW)) !== 0;
}
isModified(): boolean {
return (this.status & (Status.INDEX_MODIFIED | Status.WT_MODIFIED)) !== 0;
}
isDeleted(): boolean {
return (this.status & (Status.INDEX_DELETED | Status.WT_DELETED)) !== 0;
}
isRenamed(): boolean {
return (this.status & (Status.INDEX_RENAMED | Status.WT_RENAMED)) !== 0;
}
isIgnored(): boolean {
return (this.status & Status.IGNORED) !== 0;
}
inIndex(): boolean {
return (
(this.status &
(Status.INDEX_NEW |
Status.INDEX_MODIFIED |
Status.INDEX_DELETED |
Status.INDEX_RENAMED |
Status.INDEX_TYPECHANGE)) !==
0
);
}
inWorkingTree(): boolean {
return (
(this.status &
(Status.WT_NEW | Status.WT_MODIFIED | Status.WT_DELETED | Status.WT_TYPECHANGE | Status.WT_RENAMED)) !==
0
);
}
}
class Repository {
#repo: any;
constructor(repo: any) {
this.#repo = repo;
}
/**
* Open an existing Git repository
*/
static open(path: string): Repository {
if (!Git) {
initializeGit();
}
const repo = Git.Repository.open(path);
return new Repository(repo);
}
/**
* Get the HEAD commit
*/
head(): Commit {
const commit = this.#repo.head();
return new Commit(commit);
}
/**
* Get the .git directory path
*/
get path(): string {
return this.#repo.path;
}
/**
* Get the working directory path (null for bare repositories)
*/
get workdir(): string | null {
return this.#repo.workdir;
}
/**
* Check if this is a bare repository
*/
get isBare(): boolean {
return this.#repo.isBare;
}
/**
* Get the working directory status (nodegit compatible)
*/
getStatus(options?: StatusOptions): StatusEntry[] {
const entries = this.#repo.getStatus(options);
return entries.map((e: InternalStatusEntry) => new StatusEntry(e));
}
/**
* Resolve a revision spec to an OID
*/
revParse(spec: string): string {
return this.#repo.revParse(spec);
}
/**
* Get the name of the current branch (null if detached HEAD or no commits)
*/
getCurrentBranch(): string | null {
return this.#repo.getCurrentBranch();
}
/**
* Get ahead/behind counts between two commits
*/
aheadBehind(local?: string, upstream?: string): { ahead: number; behind: number } {
return this.#repo.aheadBehind(local, upstream);
}
/**
* Get list of files in the index
*/
listFiles(): IndexEntry[] {
return this.#repo.listFiles();
}
/**
* Get diff information
*/
diff(options?: DiffOptions): DiffResult {
return this.#repo.diff(options);
}
/**
* Count commits in a range
*/
countCommits(range?: string): number {
return this.#repo.countCommits(range);
}
/**
* Get commit history
*/
log(options?: LogOptions): Commit[] {
const commits = this.#repo.log(options);
return commits.map((c: any) => new Commit(c));
}
}
class Commit {
#commit: any;
constructor(commit: any) {
this.#commit = commit;
}
/**
* Get the commit OID (SHA-1 hash)
*/
get id(): string {
return this.#commit.id;
}
/**
* Get the full commit message
*/
get message(): string {
return this.#commit.message;
}
/**
* Get the first line of the commit message
*/
get summary(): string {
return this.#commit.summary;
}
/**
* Get the author signature
*/
get author(): Signature {
return this.#commit.author;
}
/**
* Get the committer signature
*/
get committer(): Signature {
return this.#commit.committer;
}
/**
* Get the commit time as Unix timestamp (seconds since epoch)
*/
get time(): number {
return this.#commit.time;
}
}
export default {
__esModule: true,
Repository,
Commit,
StatusEntry,
Status,
DeltaType,
default: Repository,
};

File diff suppressed because it is too large Load Diff