mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
6 Commits
claude/fix
...
add-bun-gi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a40e2b857e | ||
|
|
48a6082167 | ||
|
|
0b6d896adf | ||
|
|
c9dc5dd381 | ||
|
|
d33550ddba | ||
|
|
f02511d2f8 |
@@ -46,6 +46,7 @@
|
||||
"src/io/*.cpp",
|
||||
"src/bun.js/modules/*.cpp",
|
||||
"src/bun.js/bindings/*.cpp",
|
||||
"src/bun.js/bindings/git/*.cpp",
|
||||
"src/bun.js/bindings/webcore/*.cpp",
|
||||
"src/bun.js/bindings/sqlite/*.cpp",
|
||||
"src/bun.js/bindings/webcrypto/*.cpp",
|
||||
|
||||
@@ -54,6 +54,7 @@ set(BUN_DEPENDENCIES
|
||||
Cares
|
||||
Highway
|
||||
LibDeflate
|
||||
Libgit2
|
||||
LolHtml
|
||||
Lshpack
|
||||
Mimalloc
|
||||
@@ -1322,7 +1323,7 @@ list(TRANSFORM BUN_DEPENDENCIES TOLOWER OUTPUT_VARIABLE BUN_TARGETS)
|
||||
add_custom_target(dependencies DEPENDS ${BUN_TARGETS})
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(${bun} PRIVATE icucore resolv)
|
||||
target_link_libraries(${bun} PRIVATE icucore resolv iconv)
|
||||
target_compile_definitions(${bun} PRIVATE U_DISABLE_RENAMING=1)
|
||||
endif()
|
||||
|
||||
|
||||
40
cmake/targets/BuildLibgit2.cmake
Normal file
40
cmake/targets/BuildLibgit2.cmake
Normal file
@@ -0,0 +1,40 @@
|
||||
register_repository(
|
||||
NAME
|
||||
libgit2
|
||||
REPOSITORY
|
||||
libgit2/libgit2
|
||||
TAG
|
||||
v1.9.0
|
||||
)
|
||||
|
||||
register_cmake_command(
|
||||
TARGET
|
||||
libgit2
|
||||
TARGETS
|
||||
libgit2package
|
||||
ARGS
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DBUILD_TESTS=OFF
|
||||
-DBUILD_CLI=OFF
|
||||
-DBUILD_EXAMPLES=OFF
|
||||
-DBUILD_FUZZERS=OFF
|
||||
# Network disabled - local operations only
|
||||
-DUSE_HTTPS=OFF
|
||||
-DUSE_SSH=OFF
|
||||
# Use bundled dependencies to avoid symbol conflicts with Bun's libraries
|
||||
-DUSE_BUNDLED_ZLIB=ON
|
||||
-DUSE_HTTP_PARSER=builtin
|
||||
-DREGEX_BACKEND=builtin
|
||||
-DUSE_SHA1=CollisionDetection
|
||||
# Enable threading
|
||||
-DUSE_THREADS=ON
|
||||
# Disable authentication features (not needed for local operations)
|
||||
-DUSE_GSSAPI=OFF
|
||||
LIB_PATH
|
||||
.
|
||||
LIBRARIES
|
||||
git2
|
||||
INCLUDES
|
||||
include
|
||||
)
|
||||
642
packages/bun-types/git.d.ts
vendored
Normal file
642
packages/bun-types/git.d.ts
vendored
Normal file
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* Fast Git operations for Bun.js powered by libgit2.
|
||||
*
|
||||
* This module provides read-only Git repository operations.
|
||||
* Network operations (HTTPS/SSH) are not supported - local operations only.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Repository } from 'bun:git';
|
||||
*
|
||||
* const repo = Repository.open('.');
|
||||
* const head = repo.head();
|
||||
* console.log(`HEAD: ${head.id} - ${head.summary}`);
|
||||
* console.log(`Author: ${head.author.name} <${head.author.email}>`);
|
||||
* ```
|
||||
*
|
||||
* @module bun:git
|
||||
*/
|
||||
declare module "bun:git" {
|
||||
/**
|
||||
* Represents a Git signature (author or committer information).
|
||||
*/
|
||||
export interface Signature {
|
||||
/**
|
||||
* The name of the person.
|
||||
* @example "John Doe"
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The email address of the person.
|
||||
* @example "john@example.com"
|
||||
*/
|
||||
readonly email: string;
|
||||
|
||||
/**
|
||||
* Unix timestamp of when the signature was created.
|
||||
* @example 1704067200
|
||||
*/
|
||||
readonly time: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status flags for working directory entries.
|
||||
* These are bit flags that can be combined with bitwise OR.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Status } from 'bun:git';
|
||||
*
|
||||
* const entries = repo.getStatus();
|
||||
* for (const entry of entries) {
|
||||
* if (entry.status & Status.WT_MODIFIED) {
|
||||
* console.log('Modified in workdir:', entry.path);
|
||||
* }
|
||||
* if (entry.status & Status.INDEX_NEW) {
|
||||
* console.log('New in index:', entry.path);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const Status: {
|
||||
/** Entry is current and unchanged */
|
||||
readonly CURRENT: 0;
|
||||
/** Entry is new in the index */
|
||||
readonly INDEX_NEW: 1;
|
||||
/** Entry is modified in the index */
|
||||
readonly INDEX_MODIFIED: 2;
|
||||
/** Entry is deleted in the index */
|
||||
readonly INDEX_DELETED: 4;
|
||||
/** Entry is renamed in the index */
|
||||
readonly INDEX_RENAMED: 8;
|
||||
/** Entry type changed in the index */
|
||||
readonly INDEX_TYPECHANGE: 16;
|
||||
/** Entry is new in the working tree */
|
||||
readonly WT_NEW: 128;
|
||||
/** Entry is modified in the working tree */
|
||||
readonly WT_MODIFIED: 256;
|
||||
/** Entry is deleted in the working tree */
|
||||
readonly WT_DELETED: 512;
|
||||
/** Entry type changed in the working tree */
|
||||
readonly WT_TYPECHANGE: 1024;
|
||||
/** Entry is renamed in the working tree */
|
||||
readonly WT_RENAMED: 2048;
|
||||
/** Entry is ignored */
|
||||
readonly IGNORED: 16384;
|
||||
/** Entry is conflicted */
|
||||
readonly CONFLICTED: 32768;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delta types for diff entries.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { DeltaType } from 'bun:git';
|
||||
*
|
||||
* const diff = repo.diff();
|
||||
* for (const file of diff.files) {
|
||||
* if (file.status === DeltaType.ADDED) {
|
||||
* console.log('Added:', file.newPath);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const DeltaType: {
|
||||
/** No changes */
|
||||
readonly UNMODIFIED: 0;
|
||||
/** Entry does not exist in old version */
|
||||
readonly ADDED: 1;
|
||||
/** Entry does not exist in new version */
|
||||
readonly DELETED: 2;
|
||||
/** Entry content changed between old and new */
|
||||
readonly MODIFIED: 3;
|
||||
/** Entry was renamed between old and new */
|
||||
readonly RENAMED: 4;
|
||||
/** Entry was copied from another old entry */
|
||||
readonly COPIED: 5;
|
||||
/** Entry is ignored item in workdir */
|
||||
readonly IGNORED: 6;
|
||||
/** Entry is untracked item in workdir */
|
||||
readonly UNTRACKED: 7;
|
||||
/** Entry type changed between old and new */
|
||||
readonly TYPECHANGE: 8;
|
||||
/** Entry is unreadable */
|
||||
readonly CONFLICTED: 10;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for getting repository status.
|
||||
*/
|
||||
export interface StatusOptions {
|
||||
/**
|
||||
* Include untracked files in the status.
|
||||
* @default true
|
||||
*/
|
||||
includeUntracked?: boolean;
|
||||
|
||||
/**
|
||||
* Include ignored files in the status.
|
||||
* @default false
|
||||
*/
|
||||
includeIgnored?: boolean;
|
||||
|
||||
/**
|
||||
* Recurse into untracked directories.
|
||||
* @default true
|
||||
*/
|
||||
recurseUntrackedDirs?: boolean;
|
||||
|
||||
/**
|
||||
* Detect renamed files.
|
||||
* @default false
|
||||
*/
|
||||
detectRenames?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a status entry for a file in the working directory.
|
||||
*/
|
||||
export class StatusEntry {
|
||||
/**
|
||||
* The path of the file relative to the repository root.
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* Status flags (combination of Status values).
|
||||
*/
|
||||
readonly status: number;
|
||||
|
||||
/**
|
||||
* Check if the entry is new (untracked or staged as new).
|
||||
*/
|
||||
isNew(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry is modified.
|
||||
*/
|
||||
isModified(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry is deleted.
|
||||
*/
|
||||
isDeleted(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry is renamed.
|
||||
*/
|
||||
isRenamed(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry is ignored.
|
||||
*/
|
||||
isIgnored(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry has changes staged in the index.
|
||||
*/
|
||||
inIndex(): boolean;
|
||||
|
||||
/**
|
||||
* Check if the entry has changes in the working tree.
|
||||
*/
|
||||
inWorkingTree(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an entry in the Git index.
|
||||
*/
|
||||
export interface IndexEntry {
|
||||
/**
|
||||
* The path of the file relative to the repository root.
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* The file mode (e.g., 0o100644 for regular files).
|
||||
*/
|
||||
readonly mode: number;
|
||||
|
||||
/**
|
||||
* The blob OID (SHA-1 hash) of the file content.
|
||||
*/
|
||||
readonly oid: string;
|
||||
|
||||
/**
|
||||
* The stage number (0 for normal, 1-3 for conflict stages).
|
||||
*/
|
||||
readonly stage: number;
|
||||
|
||||
/**
|
||||
* The file size in bytes.
|
||||
*/
|
||||
readonly size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for getting diff information.
|
||||
*/
|
||||
export interface DiffOptions {
|
||||
/**
|
||||
* If true, compare HEAD to index (staged changes).
|
||||
* If false, compare HEAD to working directory.
|
||||
* @default false
|
||||
*/
|
||||
cached?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a changed file in a diff.
|
||||
*/
|
||||
export interface DiffFile {
|
||||
/**
|
||||
* The type of change (see DeltaType).
|
||||
*/
|
||||
readonly status: number;
|
||||
|
||||
/**
|
||||
* The old path (null for added files).
|
||||
*/
|
||||
readonly oldPath: string | null;
|
||||
|
||||
/**
|
||||
* The new path.
|
||||
*/
|
||||
readonly newPath: string;
|
||||
|
||||
/**
|
||||
* Similarity percentage for renamed/copied files (0-100).
|
||||
*/
|
||||
readonly similarity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a diff operation.
|
||||
*/
|
||||
export interface DiffResult {
|
||||
/**
|
||||
* List of changed files.
|
||||
*/
|
||||
readonly files: DiffFile[];
|
||||
|
||||
/**
|
||||
* Statistics about the diff.
|
||||
*/
|
||||
readonly stats: {
|
||||
/** Number of files changed */
|
||||
readonly filesChanged: number;
|
||||
/** Total lines inserted */
|
||||
readonly insertions: number;
|
||||
/** Total lines deleted */
|
||||
readonly deletions: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for getting commit history.
|
||||
*/
|
||||
export interface LogOptions {
|
||||
/**
|
||||
* Starting point for history traversal.
|
||||
* @default "HEAD"
|
||||
*/
|
||||
from?: string;
|
||||
|
||||
/**
|
||||
* Range specification (e.g., "origin/main..HEAD").
|
||||
* If provided, `from` is ignored.
|
||||
*/
|
||||
range?: string;
|
||||
|
||||
/**
|
||||
* Maximum number of commits to return.
|
||||
* @default unlimited
|
||||
*/
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Git commit object.
|
||||
*
|
||||
* A commit contains information about a snapshot of the repository,
|
||||
* including the author, committer, message, and parent commits.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const head = repo.head();
|
||||
* console.log(head.id); // "abc123..."
|
||||
* console.log(head.message); // "feat: add new feature\n\nDetailed description..."
|
||||
* console.log(head.summary); // "feat: add new feature"
|
||||
* ```
|
||||
*/
|
||||
export class Commit {
|
||||
/**
|
||||
* The full 40-character hexadecimal SHA-1 hash of the commit.
|
||||
* @example "a1b2c3d4e5f6..."
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The full commit message, including the body.
|
||||
* @example "feat: add new feature\n\nThis commit adds..."
|
||||
*/
|
||||
readonly message: string;
|
||||
|
||||
/**
|
||||
* The first line of the commit message (the summary/title).
|
||||
* Does not include any trailing newline.
|
||||
* @example "feat: add new feature"
|
||||
*/
|
||||
readonly summary: string;
|
||||
|
||||
/**
|
||||
* The author of the commit (who wrote the changes).
|
||||
*/
|
||||
readonly author: Signature;
|
||||
|
||||
/**
|
||||
* The committer of the commit (who committed the changes).
|
||||
* This may differ from the author in cases like cherry-picks or rebases.
|
||||
*/
|
||||
readonly committer: Signature;
|
||||
|
||||
/**
|
||||
* Unix timestamp of when the commit was created.
|
||||
* This is the committer's timestamp.
|
||||
* @example 1704067200
|
||||
*/
|
||||
readonly time: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Git repository.
|
||||
*
|
||||
* Use {@link Repository.open} to open an existing repository.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Repository } from 'bun:git';
|
||||
*
|
||||
* // Open the repository at the current directory
|
||||
* const repo = Repository.open('.');
|
||||
*
|
||||
* // Get repository info
|
||||
* console.log('Path:', repo.path); // "/path/to/repo/.git/"
|
||||
* console.log('Workdir:', repo.workdir); // "/path/to/repo/"
|
||||
* console.log('Is bare:', repo.isBare); // false
|
||||
*
|
||||
* // Get the HEAD commit
|
||||
* const head = repo.head();
|
||||
* console.log('HEAD:', head.id.slice(0, 7), head.summary);
|
||||
* ```
|
||||
*/
|
||||
export class Repository {
|
||||
/**
|
||||
* Opens an existing Git repository.
|
||||
*
|
||||
* The path can point to either a working directory or a bare repository.
|
||||
* If the path points to a working directory, the `.git` directory will be located automatically.
|
||||
*
|
||||
* @param path Path to the repository (working directory or .git directory)
|
||||
* @returns A Repository instance
|
||||
* @throws Error if the path is not a valid Git repository
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Open by working directory
|
||||
* const repo = Repository.open('/path/to/project');
|
||||
*
|
||||
* // Open by .git directory
|
||||
* const repo2 = Repository.open('/path/to/project/.git');
|
||||
*
|
||||
* // Open current directory
|
||||
* const repo3 = Repository.open('.');
|
||||
* ```
|
||||
*/
|
||||
static open(path: string): Repository;
|
||||
|
||||
/**
|
||||
* Gets the commit that HEAD currently points to.
|
||||
*
|
||||
* @returns The commit that HEAD references
|
||||
* @throws Error if HEAD is unborn (new repository with no commits)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const head = repo.head();
|
||||
* console.log(`Current commit: ${head.summary}`);
|
||||
* console.log(`Author: ${head.author.name}`);
|
||||
* ```
|
||||
*/
|
||||
head(): Commit;
|
||||
|
||||
/**
|
||||
* The path to the `.git` directory.
|
||||
* Always ends with a trailing slash.
|
||||
*
|
||||
* @example "/Users/me/project/.git/"
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* The path to the working directory.
|
||||
* Returns `null` for bare repositories.
|
||||
* When present, always ends with a trailing slash.
|
||||
*
|
||||
* @example "/Users/me/project/"
|
||||
*/
|
||||
readonly workdir: string | null;
|
||||
|
||||
/**
|
||||
* Whether this is a bare repository.
|
||||
* Bare repositories have no working directory.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (repo.isBare) {
|
||||
* console.log('This is a bare repository');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
readonly isBare: boolean;
|
||||
|
||||
/**
|
||||
* Gets the working directory status.
|
||||
*
|
||||
* Returns an array of status entries for all changed files in the
|
||||
* working directory and index.
|
||||
*
|
||||
* @param options Options to control which files are included
|
||||
* @returns Array of status entries
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Repository, Status } from 'bun:git';
|
||||
*
|
||||
* const repo = Repository.open('.');
|
||||
* const status = repo.getStatus();
|
||||
*
|
||||
* for (const entry of status) {
|
||||
* if (entry.isModified()) {
|
||||
* console.log('Modified:', entry.path);
|
||||
* }
|
||||
* if (entry.isNew()) {
|
||||
* console.log('New:', entry.path);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getStatus(options?: StatusOptions): StatusEntry[];
|
||||
|
||||
/**
|
||||
* Resolves a revision specification to a commit OID.
|
||||
*
|
||||
* Supports standard Git revision syntax including:
|
||||
* - Branch names: "main", "feature/foo"
|
||||
* - Tag names: "v1.0.0"
|
||||
* - SHA prefixes: "abc123"
|
||||
* - Special refs: "HEAD", "HEAD~1", "HEAD^2"
|
||||
* - Upstream: "@{u}", "main@{u}"
|
||||
*
|
||||
* @param spec The revision specification to resolve
|
||||
* @returns The 40-character hex OID
|
||||
* @throws Error if the spec cannot be resolved
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const headOid = repo.revParse('HEAD');
|
||||
* const parentOid = repo.revParse('HEAD~1');
|
||||
* const branchOid = repo.revParse('main');
|
||||
* ```
|
||||
*/
|
||||
revParse(spec: string): string;
|
||||
|
||||
/**
|
||||
* Gets the name of the current branch.
|
||||
*
|
||||
* @returns The branch name, or null if HEAD is detached or unborn
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const branch = repo.getCurrentBranch();
|
||||
* if (branch) {
|
||||
* console.log('On branch:', branch);
|
||||
* } else {
|
||||
* console.log('HEAD is detached');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getCurrentBranch(): string | null;
|
||||
|
||||
/**
|
||||
* Gets the ahead/behind counts between two commits.
|
||||
*
|
||||
* This is useful for comparing a local branch to its upstream.
|
||||
*
|
||||
* @param local The local ref (default: "HEAD")
|
||||
* @param upstream The upstream ref (default: "@{u}")
|
||||
* @returns Object with ahead and behind counts
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { ahead, behind } = repo.aheadBehind();
|
||||
* console.log(`${ahead} ahead, ${behind} behind`);
|
||||
*
|
||||
* // Compare specific refs
|
||||
* const { ahead, behind } = repo.aheadBehind('feature', 'origin/main');
|
||||
* ```
|
||||
*/
|
||||
aheadBehind(local?: string, upstream?: string): { ahead: number; behind: number };
|
||||
|
||||
/**
|
||||
* Gets the list of files tracked in the index.
|
||||
*
|
||||
* @returns Array of index entries
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const files = repo.listFiles();
|
||||
* console.log(`Tracking ${files.length} files`);
|
||||
*
|
||||
* for (const file of files) {
|
||||
* console.log(`${file.path} (mode: ${file.mode.toString(8)})`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
listFiles(): IndexEntry[];
|
||||
|
||||
/**
|
||||
* Gets diff information between HEAD and working directory or index.
|
||||
*
|
||||
* @param options Options to control the diff behavior
|
||||
* @returns Diff result with file list and statistics
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { Repository, DeltaType } from 'bun:git';
|
||||
*
|
||||
* const repo = Repository.open('.');
|
||||
*
|
||||
* // Unstaged changes (HEAD vs workdir)
|
||||
* const diff = repo.diff();
|
||||
* console.log(`${diff.stats.filesChanged} files changed`);
|
||||
* console.log(`+${diff.stats.insertions} -${diff.stats.deletions}`);
|
||||
*
|
||||
* // Staged changes (HEAD vs index)
|
||||
* const staged = repo.diff({ cached: true });
|
||||
*
|
||||
* for (const file of diff.files) {
|
||||
* if (file.status === DeltaType.MODIFIED) {
|
||||
* console.log('Modified:', file.newPath);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
diff(options?: DiffOptions): DiffResult;
|
||||
|
||||
/**
|
||||
* Counts the number of commits in a range.
|
||||
*
|
||||
* @param range Optional range specification (e.g., "origin/main..HEAD")
|
||||
* @returns Number of commits
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Total commits
|
||||
* const total = repo.countCommits();
|
||||
*
|
||||
* // Commits since origin/main
|
||||
* const since = repo.countCommits('origin/main..HEAD');
|
||||
* ```
|
||||
*/
|
||||
countCommits(range?: string): number;
|
||||
|
||||
/**
|
||||
* Gets the commit history.
|
||||
*
|
||||
* @param options Options to control the log behavior
|
||||
* @returns Array of commits
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Last 10 commits
|
||||
* const commits = repo.log({ limit: 10 });
|
||||
*
|
||||
* for (const commit of commits) {
|
||||
* console.log(`${commit.id.slice(0, 7)} ${commit.summary}`);
|
||||
* }
|
||||
*
|
||||
* // Commits in a range
|
||||
* const range = repo.log({ range: 'origin/main..HEAD' });
|
||||
*
|
||||
* // Commits from a specific ref
|
||||
* const fromTag = repo.log({ from: 'v1.0.0', limit: 5 });
|
||||
* ```
|
||||
*/
|
||||
log(options?: LogOptions): Commit[];
|
||||
}
|
||||
|
||||
export default Repository;
|
||||
}
|
||||
1
packages/bun-types/index.d.ts
vendored
1
packages/bun-types/index.d.ts
vendored
@@ -14,6 +14,7 @@
|
||||
/// <reference path="./html-rewriter.d.ts" />
|
||||
/// <reference path="./jsc.d.ts" />
|
||||
/// <reference path="./sqlite.d.ts" />
|
||||
/// <reference path="./git.d.ts" />
|
||||
/// <reference path="./test.d.ts" />
|
||||
/// <reference path="./wasm.d.ts" />
|
||||
/// <reference path="./overrides.d.ts" />
|
||||
|
||||
@@ -10,6 +10,7 @@ pub const HardcodedModule = enum {
|
||||
@"bun:test",
|
||||
@"bun:wrap",
|
||||
@"bun:sqlite",
|
||||
@"bun:git",
|
||||
@"node:assert",
|
||||
@"node:assert/strict",
|
||||
@"node:async_hooks",
|
||||
@@ -98,6 +99,7 @@ pub const HardcodedModule = enum {
|
||||
.{ "bun:main", .@"bun:main" },
|
||||
.{ "bun:test", .@"bun:test" },
|
||||
.{ "bun:sqlite", .@"bun:sqlite" },
|
||||
.{ "bun:git", .@"bun:git" },
|
||||
.{ "bun:wrap", .@"bun:wrap" },
|
||||
.{ "bun:internal-for-testing", .@"bun:internal-for-testing" },
|
||||
// Node.js
|
||||
@@ -366,6 +368,7 @@ pub const HardcodedModule = enum {
|
||||
.{ "bun:ffi", .{ .path = "bun:ffi" } },
|
||||
.{ "bun:jsc", .{ .path = "bun:jsc" } },
|
||||
.{ "bun:sqlite", .{ .path = "bun:sqlite" } },
|
||||
.{ "bun:git", .{ .path = "bun:git" } },
|
||||
.{ "bun:wrap", .{ .path = "bun:wrap" } },
|
||||
.{ "bun:internal-for-testing", .{ .path = "bun:internal-for-testing" } },
|
||||
.{ "ffi", .{ .path = "bun:ffi" } },
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
#include "JSSocketAddressDTO.h"
|
||||
#include "JSReactElement.h"
|
||||
#include "JSSQLStatement.h"
|
||||
#include "git/JSGit.h"
|
||||
#include "JSStringDecoder.h"
|
||||
#include "JSTextEncoder.h"
|
||||
#include "JSTextEncoderStream.h"
|
||||
@@ -1868,6 +1869,16 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(WebCore::createJSSQLStatementStructure(init.owner));
|
||||
});
|
||||
|
||||
m_JSGitRepositoryStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(WebCore::createJSGitRepositoryStructure(init.owner));
|
||||
});
|
||||
|
||||
m_JSGitCommitStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(WebCore::createJSGitCommitStructure(init.owner));
|
||||
});
|
||||
|
||||
m_V8GlobalInternals.initLater(
|
||||
[](const JSC::LazyProperty<JSC::JSGlobalObject, v8::shim::GlobalInternals>::Initializer& init) {
|
||||
init.set(
|
||||
|
||||
@@ -316,6 +316,9 @@ public:
|
||||
|
||||
Structure* JSSQLStatementStructure() const { return m_JSSQLStatementStructure.getInitializedOnMainThread(this); }
|
||||
|
||||
Structure* JSGitRepositoryStructure() const { return m_JSGitRepositoryStructure.getInitializedOnMainThread(this); }
|
||||
Structure* JSGitCommitStructure() const { return m_JSGitCommitStructure.getInitializedOnMainThread(this); }
|
||||
|
||||
v8::shim::GlobalInternals* V8GlobalInternals() const { return m_V8GlobalInternals.getInitializedOnMainThread(this); }
|
||||
|
||||
Bun::BakeAdditionsToGlobalObject& bakeAdditions() { return m_bakeAdditions; }
|
||||
@@ -620,6 +623,8 @@ public:
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_NapiTypeTagStructure) \
|
||||
\
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSQLStatementStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSGitRepositoryStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSGitCommitStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<v8::shim::GlobalInternals>, m_V8GlobalInternals) \
|
||||
\
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunObject) \
|
||||
|
||||
1305
src/bun.js/bindings/git/JSGit.cpp
Normal file
1305
src/bun.js/bindings/git/JSGit.cpp
Normal file
File diff suppressed because it is too large
Load Diff
140
src/bun.js/bindings/git/JSGit.h
Normal file
140
src/bun.js/bindings/git/JSGit.h
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Oven-sh
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "root.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
|
||||
#include <JavaScriptCore/JSFunction.h>
|
||||
#include <JavaScriptCore/JSDestructibleObject.h>
|
||||
#include <JavaScriptCore/VM.h>
|
||||
|
||||
#include "headers-handwritten.h"
|
||||
#include "BunClientData.h"
|
||||
#include <JavaScriptCore/CallFrame.h>
|
||||
|
||||
// Forward declarations for libgit2 types
|
||||
typedef struct git_repository git_repository;
|
||||
typedef struct git_commit git_commit;
|
||||
typedef struct git_oid git_oid;
|
||||
|
||||
namespace WebCore {
|
||||
|
||||
// Forward declarations
|
||||
class JSGitRepository;
|
||||
class JSGitCommit;
|
||||
class JSGitOid;
|
||||
|
||||
// JSGitRepository - Wraps git_repository*
|
||||
class JSGitRepository final : public JSC::JSDestructibleObject {
|
||||
public:
|
||||
using Base = JSC::JSDestructibleObject;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static JSGitRepository* create(JSC::VM& vm, JSC::Structure* structure, git_repository* repo);
|
||||
static void destroy(JSC::JSCell* cell);
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
template<typename CellType, JSC::SubspaceAccess mode>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
return WebCore::subspaceForImpl<JSGitRepository, WebCore::UseCustomHeapCellType::No>(
|
||||
vm,
|
||||
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitRepository.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitRepository = std::forward<decltype(space)>(space); },
|
||||
[](auto& spaces) { return spaces.m_subspaceForJSGitRepository.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitRepository = std::forward<decltype(space)>(space); });
|
||||
}
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
}
|
||||
|
||||
git_repository* repository() const { return m_repo; }
|
||||
|
||||
private:
|
||||
JSGitRepository(JSC::VM& vm, JSC::Structure* structure, git_repository* repo)
|
||||
: Base(vm, structure)
|
||||
, m_repo(repo)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm);
|
||||
|
||||
git_repository* m_repo { nullptr };
|
||||
};
|
||||
|
||||
// JSGitCommit - Wraps git_commit*
|
||||
class JSGitCommit final : public JSC::JSDestructibleObject {
|
||||
public:
|
||||
using Base = JSC::JSDestructibleObject;
|
||||
static constexpr unsigned StructureFlags = Base::StructureFlags;
|
||||
|
||||
static JSGitCommit* create(JSC::VM& vm, JSC::Structure* structure, git_commit* commit);
|
||||
static void destroy(JSC::JSCell* cell);
|
||||
|
||||
DECLARE_INFO;
|
||||
|
||||
template<typename CellType, JSC::SubspaceAccess mode>
|
||||
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
|
||||
{
|
||||
return WebCore::subspaceForImpl<JSGitCommit, WebCore::UseCustomHeapCellType::No>(
|
||||
vm,
|
||||
[](auto& spaces) { return spaces.m_clientSubspaceForJSGitCommit.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSGitCommit = std::forward<decltype(space)>(space); },
|
||||
[](auto& spaces) { return spaces.m_subspaceForJSGitCommit.get(); },
|
||||
[](auto& spaces, auto&& space) { spaces.m_subspaceForJSGitCommit = std::forward<decltype(space)>(space); });
|
||||
}
|
||||
|
||||
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
|
||||
{
|
||||
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
|
||||
}
|
||||
|
||||
git_commit* commit() const { return m_commit; }
|
||||
|
||||
private:
|
||||
JSGitCommit(JSC::VM& vm, JSC::Structure* structure, git_commit* commit)
|
||||
: Base(vm, structure)
|
||||
, m_commit(commit)
|
||||
{
|
||||
}
|
||||
|
||||
void finishCreation(JSC::VM& vm);
|
||||
|
||||
git_commit* m_commit { nullptr };
|
||||
};
|
||||
|
||||
// Structure creation functions
|
||||
JSC::Structure* createJSGitRepositoryStructure(JSC::JSGlobalObject* globalObject);
|
||||
JSC::Structure* createJSGitCommitStructure(JSC::JSGlobalObject* globalObject);
|
||||
|
||||
// Module creation function (called from $cpp)
|
||||
JSC::JSValue createJSGitModule(Zig::GlobalObject* globalObject);
|
||||
|
||||
} // namespace WebCore
|
||||
@@ -24,6 +24,8 @@ public:
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForNapiPrototype;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSQLStatement;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSQLStatementConstructor;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitRepository;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSGitCommit;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSinkConstructor;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSinkController;
|
||||
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSSink;
|
||||
|
||||
@@ -24,6 +24,8 @@ public:
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForNapiPrototype;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSSQLStatement;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSSQLStatementConstructor;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSGitRepository;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSGitCommit;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSSinkConstructor;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSSinkController;
|
||||
std::unique_ptr<IsoSubspace> m_subspaceForJSSink;
|
||||
|
||||
306
src/js/bun/git.ts
Normal file
306
src/js/bun/git.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
// Hardcoded module "bun:git"
|
||||
|
||||
let Git: any;
|
||||
|
||||
function initializeGit() {
|
||||
Git = $cpp("git/JSGit.cpp", "createJSGitModule");
|
||||
}
|
||||
|
||||
interface Signature {
|
||||
name: string;
|
||||
email: string;
|
||||
time: number; // Unix timestamp in milliseconds
|
||||
}
|
||||
|
||||
interface StatusOptions {
|
||||
includeUntracked?: boolean;
|
||||
includeIgnored?: boolean;
|
||||
recurseUntrackedDirs?: boolean;
|
||||
detectRenames?: boolean;
|
||||
}
|
||||
|
||||
interface InternalStatusEntry {
|
||||
path: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface IndexEntry {
|
||||
path: string;
|
||||
mode: number;
|
||||
oid: string;
|
||||
stage: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface DiffOptions {
|
||||
cached?: boolean;
|
||||
}
|
||||
|
||||
interface DiffFile {
|
||||
status: number;
|
||||
oldPath: string | null;
|
||||
newPath: string;
|
||||
similarity?: number;
|
||||
}
|
||||
|
||||
interface DiffResult {
|
||||
files: DiffFile[];
|
||||
stats: {
|
||||
filesChanged: number;
|
||||
insertions: number;
|
||||
deletions: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface LogOptions {
|
||||
from?: string;
|
||||
range?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// Status constants (nodegit compatible)
|
||||
const Status = {
|
||||
CURRENT: 0,
|
||||
INDEX_NEW: 1,
|
||||
INDEX_MODIFIED: 2,
|
||||
INDEX_DELETED: 4,
|
||||
INDEX_RENAMED: 8,
|
||||
INDEX_TYPECHANGE: 16,
|
||||
WT_NEW: 128,
|
||||
WT_MODIFIED: 256,
|
||||
WT_DELETED: 512,
|
||||
WT_TYPECHANGE: 1024,
|
||||
WT_RENAMED: 2048,
|
||||
IGNORED: 16384,
|
||||
CONFLICTED: 32768,
|
||||
};
|
||||
|
||||
// DeltaType constants (nodegit compatible)
|
||||
const DeltaType = {
|
||||
UNMODIFIED: 0,
|
||||
ADDED: 1,
|
||||
DELETED: 2,
|
||||
MODIFIED: 3,
|
||||
RENAMED: 4,
|
||||
COPIED: 5,
|
||||
IGNORED: 6,
|
||||
UNTRACKED: 7,
|
||||
TYPECHANGE: 8,
|
||||
CONFLICTED: 10,
|
||||
};
|
||||
|
||||
class StatusEntry {
|
||||
path: string;
|
||||
status: number;
|
||||
|
||||
constructor(entry: InternalStatusEntry) {
|
||||
this.path = entry.path;
|
||||
this.status = entry.status;
|
||||
}
|
||||
|
||||
isNew(): boolean {
|
||||
return (this.status & (Status.INDEX_NEW | Status.WT_NEW)) !== 0;
|
||||
}
|
||||
|
||||
isModified(): boolean {
|
||||
return (this.status & (Status.INDEX_MODIFIED | Status.WT_MODIFIED)) !== 0;
|
||||
}
|
||||
|
||||
isDeleted(): boolean {
|
||||
return (this.status & (Status.INDEX_DELETED | Status.WT_DELETED)) !== 0;
|
||||
}
|
||||
|
||||
isRenamed(): boolean {
|
||||
return (this.status & (Status.INDEX_RENAMED | Status.WT_RENAMED)) !== 0;
|
||||
}
|
||||
|
||||
isIgnored(): boolean {
|
||||
return (this.status & Status.IGNORED) !== 0;
|
||||
}
|
||||
|
||||
inIndex(): boolean {
|
||||
return (
|
||||
(this.status &
|
||||
(Status.INDEX_NEW |
|
||||
Status.INDEX_MODIFIED |
|
||||
Status.INDEX_DELETED |
|
||||
Status.INDEX_RENAMED |
|
||||
Status.INDEX_TYPECHANGE)) !==
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
inWorkingTree(): boolean {
|
||||
return (
|
||||
(this.status &
|
||||
(Status.WT_NEW | Status.WT_MODIFIED | Status.WT_DELETED | Status.WT_TYPECHANGE | Status.WT_RENAMED)) !==
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Repository {
|
||||
#repo: any;
|
||||
|
||||
constructor(repo: any) {
|
||||
this.#repo = repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an existing Git repository
|
||||
*/
|
||||
static open(path: string): Repository {
|
||||
if (!Git) {
|
||||
initializeGit();
|
||||
}
|
||||
const repo = Git.Repository.open(path);
|
||||
return new Repository(repo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HEAD commit
|
||||
*/
|
||||
head(): Commit {
|
||||
const commit = this.#repo.head();
|
||||
return new Commit(commit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the .git directory path
|
||||
*/
|
||||
get path(): string {
|
||||
return this.#repo.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the working directory path (null for bare repositories)
|
||||
*/
|
||||
get workdir(): string | null {
|
||||
return this.#repo.workdir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a bare repository
|
||||
*/
|
||||
get isBare(): boolean {
|
||||
return this.#repo.isBare;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the working directory status (nodegit compatible)
|
||||
*/
|
||||
getStatus(options?: StatusOptions): StatusEntry[] {
|
||||
const entries = this.#repo.getStatus(options);
|
||||
return entries.map((e: InternalStatusEntry) => new StatusEntry(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a revision spec to an OID
|
||||
*/
|
||||
revParse(spec: string): string {
|
||||
return this.#repo.revParse(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the current branch (null if detached HEAD or no commits)
|
||||
*/
|
||||
getCurrentBranch(): string | null {
|
||||
return this.#repo.getCurrentBranch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ahead/behind counts between two commits
|
||||
*/
|
||||
aheadBehind(local?: string, upstream?: string): { ahead: number; behind: number } {
|
||||
return this.#repo.aheadBehind(local, upstream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files in the index
|
||||
*/
|
||||
listFiles(): IndexEntry[] {
|
||||
return this.#repo.listFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diff information
|
||||
*/
|
||||
diff(options?: DiffOptions): DiffResult {
|
||||
return this.#repo.diff(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count commits in a range
|
||||
*/
|
||||
countCommits(range?: string): number {
|
||||
return this.#repo.countCommits(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit history
|
||||
*/
|
||||
log(options?: LogOptions): Commit[] {
|
||||
const commits = this.#repo.log(options);
|
||||
return commits.map((c: any) => new Commit(c));
|
||||
}
|
||||
}
|
||||
|
||||
class Commit {
|
||||
#commit: any;
|
||||
|
||||
constructor(commit: any) {
|
||||
this.#commit = commit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the commit OID (SHA-1 hash)
|
||||
*/
|
||||
get id(): string {
|
||||
return this.#commit.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full commit message
|
||||
*/
|
||||
get message(): string {
|
||||
return this.#commit.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first line of the commit message
|
||||
*/
|
||||
get summary(): string {
|
||||
return this.#commit.summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the author signature
|
||||
*/
|
||||
get author(): Signature {
|
||||
return this.#commit.author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the committer signature
|
||||
*/
|
||||
get committer(): Signature {
|
||||
return this.#commit.committer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the commit time as Unix timestamp (seconds since epoch)
|
||||
*/
|
||||
get time(): number {
|
||||
return this.#commit.time;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
__esModule: true,
|
||||
Repository,
|
||||
Commit,
|
||||
StatusEntry,
|
||||
Status,
|
||||
DeltaType,
|
||||
default: Repository,
|
||||
};
|
||||
1107
test/js/bun/git/repository.test.ts
Normal file
1107
test/js/bun/git/repository.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user