mirror of
https://github.com/oven-sh/bun
synced 2026-02-06 00:48:55 +00:00
Compare commits
8 Commits
dylan/pyth
...
claude/bun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6653c96f99 | ||
|
|
38d399861f | ||
|
|
25ad2a0b8a | ||
|
|
efeadc42b1 | ||
|
|
c9c2a560b0 | ||
|
|
29dc5af8c9 | ||
|
|
0ed687fdaf | ||
|
|
3422bd6411 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "vendor/libgit2"]
|
||||
path = vendor/libgit2
|
||||
url = https://github.com/libgit2/libgit2.git
|
||||
@@ -54,6 +54,7 @@ set(BUN_DEPENDENCIES
|
||||
Cares
|
||||
Highway
|
||||
LibDeflate
|
||||
Libgit2
|
||||
LolHtml
|
||||
Lshpack
|
||||
Mimalloc
|
||||
|
||||
23
cmake/targets/BuildLibgit2.cmake
Normal file
23
cmake/targets/BuildLibgit2.cmake
Normal 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
513
docs/git-api-reference.md
Normal 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
|
||||
@@ -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
518
src/bun.js/bindings/JSGit.h
Normal 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
|
||||
368
src/bun.js/bindings/JSGitBranch.cpp
Normal file
368
src/bun.js/bindings/JSGitBranch.cpp
Normal 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
|
||||
363
src/bun.js/bindings/JSGitCommit.cpp
Normal file
363
src/bun.js/bindings/JSGitCommit.cpp
Normal 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
|
||||
712
src/bun.js/bindings/JSGitRepository.cpp
Normal file
712
src/bun.js/bindings/JSGitRepository.cpp
Normal 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
|
||||
188
src/bun.js/bindings/JSGitSignature.cpp
Normal file
188
src/bun.js/bindings/JSGitSignature.cpp
Normal 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
|
||||
@@ -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()));
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
37
src/bun.js/modules/BunGitModule.h
Normal file
37
src/bun.js/modules/BunGitModule.h
Normal 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
|
||||
@@ -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
551
test/js/bun/git/git.test.ts
Normal 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
1
vendor/libgit2
vendored
Submodule
Submodule vendor/libgit2 added at d908000464
Reference in New Issue
Block a user