Compare commits

...

9 Commits

Author SHA1 Message Date
ff0bc72408 3.3.1 2025-11-04 03:40:49 +00:00
734137e7b5 fix(getUncommittedDiff): Avoid false-positive diffs in getUncommittedDiff by detecting symlinked directories and skipping identical files 2025-11-04 03:40:49 +00:00
238dd152ba 3.3.0 2025-11-04 01:26:54 +00:00
9bacb2e548 feat(GitRepo): Add glob-pattern exclusions for getUncommittedDiff and add minimatch; bump dependencies 2025-11-04 01:26:54 +00:00
5ba6c5370e 3.2.1 2025-08-04 15:16:42 +00:00
c6e3a1caa3 feat(core): enhance error handling, type safety, and documentation
- Add comprehensive error handling with try/catch blocks and meaningful error messages
- Improve type safety with proper IEnvDeps interface replacing 'any' types
- Add complete JSDoc documentation for all classes and methods
- Add return type annotations for better TypeScript support
- Add ensureInitialized() validation method
- Fix missing return statement in createRepoByClone() method
- Remove deprecated @types/minimatch dependency
- Complete readme rewrite with modern styling, accurate documentation, and proper API examples
- Update license from LICENSE to license.md following project guidelines
2025-08-04 15:12:23 +00:00
a92275088b update 2025-08-04 14:32:04 +00:00
2b246502f5 update deps 2025-08-04 14:01:52 +00:00
81b2b225a3 fix(changelog): Update to neweset format. 2024-06-23 23:42:47 +02:00
12 changed files with 5914 additions and 3366 deletions

View File

@@ -1,12 +1,39 @@
# Changelog
## 2025-11-04 - 3.3.1 - fix(getUncommittedDiff)
Avoid false-positive diffs in getUncommittedDiff by detecting symlinked directories and skipping identical files
- Detect files reported as "added" that are actually inside symlinked directories (catch isomorphic-git error: "anticipated to be a tree but it is a blob") and skip them to avoid huge false-positive lists.
- Compare HEAD and workdir file contents and skip entries where contents are identical to filter out permission/timestamp/line-ending false positives.
- Add glob support for excludeFiles via minimatch and skip exact or glob-matching paths during diff collection.
- Files changed: ts/smartgit.classes.gitrepo.ts (symlink detection, content comparison, diff filtering), ts/smartgit.plugins.ts (export minimatch), readme.hints.md (notes).
- Observed impact: false positives reduced dramatically in reported case (1,883 → 2 files); output size reduced from ~59 MB → ~2 KB.
## 2025-11-04 - 3.3.0 - feat(GitRepo)
Add glob-pattern exclusions for getUncommittedDiff and add minimatch; bump dependencies
- getUncommittedDiff now supports glob patterns for excluded files via minimatch (skip files when filepath matches exact or glob pattern).
- Expose minimatch through plugins (ts/smartgit.plugins.ts) so plugin code can use glob matching consistently.
- Add minimatch to dependencies (minimatch ^10.1.1).
- Bump several dependencies: @push.rocks/smartenv to ^6.0.0, @push.rocks/smartfile to ^11.2.7, @push.rocks/smartshell to ^3.3.0, @push.rocks/smartstring to ^4.1.0, isomorphic-git to ^1.34.2.
- Bump devDependencies for build/test tooling: @git.zone/tsbuild ^2.7.1, @git.zone/tsrun ^1.6.2, @git.zone/tstest ^2.7.0.
## 2025-01-04 - 3.2.0 - feat(core)
Enhanced error handling, type safety, and documentation
- Add comprehensive error handling with try/catch blocks and meaningful error messages
- Improve type safety with proper IEnvDeps interface replacing 'any' types
- Add complete JSDoc documentation for all classes and methods
- Add return type annotations for better TypeScript support
- Add ensureInitialized() validation method
- Fix missing return statement in createRepoByClone() method
- Remove deprecated @types/minimatch dependency
- Complete readme rewrite with modern styling, accurate documentation, and proper API examples
- Update license from LICENSE to license.md following project guidelines
## 2024-06-23 - 3.1.1 - fix(documentation)
Remove outdated changelog entries
-
## 2024-06-23 - 3.1.0 - gitrepo
Enhancements and fixes to GitRepo

View File

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartgit",
"version": "3.1.1",
"version": "3.3.1",
"description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
@@ -32,23 +32,22 @@
},
"homepage": "https://code.foss.global/push.rocks/smartgit",
"dependencies": {
"@push.rocks/smartenv": "^5.0.12",
"@push.rocks/smartfile": "^11.0.20",
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpromise": "^4.0.2",
"@push.rocks/smartshell": "^3.0.5",
"@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smarttime": "^4.0.6",
"@types/diff": "^5.2.1",
"@types/minimatch": "^5.1.2",
"diff": "^5.2.0",
"isomorphic-git": "^1.25.10"
"@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartshell": "^3.3.0",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smarttime": "^4.1.1",
"@types/diff": "^8.0.0",
"diff": "^8.0.2",
"isomorphic-git": "^1.34.2",
"minimatch": "^10.1.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.80",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.90",
"@push.rocks/tapbundle": "^5.0.23"
"@git.zone/tsbuild": "^2.7.1",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.7.0"
},
"private": false,
"files": [
@@ -65,5 +64,6 @@
],
"browserslist": [
"last 1 chrome versions"
]
],
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}

8788
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer

View File

@@ -1 +1,32 @@
# smartgit Project Hints
## Recent Fixes
### getUncommittedDiff() False Positives Fix (2025-11-04)
**Problem**:
- Method was reporting 1,883 diffs when only 1-2 files were actually modified
- Root cause: isomorphic-git's `statusMatrix()` reports files inside symlinked directories as "added" files
- Example: `ghost_local/current` → symlink to `ghost_local/versions/5.129.1` causes all 1,880+ files inside to be reported as changes
**Solution Implemented**:
1. **Symlink detection** (lines 160-184): For files reported as "added" (head=0, workdir≠0), try to read from HEAD anyway. If we get error "anticipated to be a tree but it is a blob", the parent path is a symlink - skip the file entirely.
2. **Content comparison** (lines 196-200): Before creating any diff, check if `headContent === workdirContent`. If identical, skip (catches permission/timestamp/line-ending false positives).
**Results**:
- Reduced false positives from 1,883 → 2 files (99.89% reduction)
- Output size: 59 MB → 2 KB (29,500x reduction)
- Only reports actual content changes
**Files Modified**:
- `ts/smartgit.classes.gitrepo.ts` lines 153-209
**Dependencies Added**:
- `minimatch` for glob pattern support in excludeFiles parameter
## Architecture Notes
- Main class: `Smartgit` (not `SmartGit` - lowercase 'g')
- Must call `await smartgit.init()` before use
- Repository methods: `createRepoByOpen()`, `createRepoByClone()`, `createRepoByInit()`

251
readme.md
View File

@@ -1,121 +1,222 @@
# @push.rocks/smartgit
smart wrapper for nodegit
## Install
To install @push.rocks/smartgit, use the following command with npm:
> 🚀 **Modern Git operations for Node.js** - A powerful TypeScript wrapper around isomorphic-git that makes repository management and analysis a breeze
[![npm version](https://badge.fury.io/js/@push.rocks%2Fsmartgit.svg)](https://badge.fury.io/js/@push.rocks%2Fsmartgit)
[![TypeScript](https://badgen.net/badge/built%20with/TypeScript/blue)](https://www.typescriptlang.org/)
## ✨ What is SmartGit?
SmartGit is a sophisticated, promise-based Git toolkit designed for Node.js applications. Built on top of `isomorphic-git`, it provides a clean, intuitive API for repository management, diff analysis, and commit history exploration. Perfect for automation tools, CI/CD pipelines, and any application that needs to interact with Git repositories programmatically.
## 🎯 Key Features
- **🔧 Repository Management** - Create, clone, and open repositories with ease
- **🌐 Remote Operations** - Manage remotes and push changes effortlessly
- **📊 Diff Analysis** - Get detailed diffs of uncommitted changes
- **📚 Commit History** - Extract commit messages with metadata and version tracking
- **⚡ Async/Await** - Modern promise-based API throughout
- **🛡️ TypeScript First** - Full type safety and IntelliSense support
- **🎯 Production Ready** - Battle-tested and actively maintained
## 📦 Installation
```bash
npm install @push.rocks/smartgit --save
```
# Using pnpm (recommended)
pnpm install @push.rocks/smartgit
Or if you prefer using yarn:
# Using npm
npm install @push.rocks/smartgit
```bash
# Using yarn
yarn add @push.rocks/smartgit
```
Make sure you have `node` installed on your system to use this package.
## Usage
This guide assumes familiarity with TypeScript and basic Git operations. The `@push.rocks/smartgit` module offers a sophisticated, promise-based interface to interact with Git repositories in a Node.js environment, abstracting over the `isomorphic-git` library and adding additional functionality.
### Setting Up
First, you need to import `Smartgit` and other necessary classes from the package. This is also when you'd typically configure any additional settings or dependencies required by your project.
## 🚀 Quick Start
```typescript
import { Smartgit } from '@push.rocks/smartgit';
// Initialize the Smartgit instance
// Initialize SmartGit
const smartgit = new Smartgit();
await smartgit.init();
// Clone a repository
const repo = await smartgit.createRepoByClone(
'https://github.com/username/repo.git',
'./local-repo'
);
console.log('🎉 Repository cloned successfully!');
```
### Creating, Cloning, and Opening Repositories
## 📖 Usage Guide
With `Smartgit`, you can easily manage local repositories by creating new ones, cloning existing repositories, or opening an already existing local repository.
### 🏗️ Repository Operations
#### Creating a New Repository
```typescript
const pathToNewRepo = '/path/to/your/new/repo';
const newRepo = await smartgit.createRepoByInit(pathToNewRepo);
const repo = await smartgit.createRepoByInit('./my-new-repo');
```
#### Cloning a Repository
#### Cloning from Remote
```typescript
const cloneUrl = 'https://github.com/yourusername/your-repo.git';
const pathToClone = '/path/to/clone/repo';
const clonedRepo = await smartgit.createRepoByClone(cloneUrl, pathToClone);
const repo = await smartgit.createRepoByClone(
'https://github.com/octocat/Hello-World.git',
'./hello-world'
);
```
#### Opening an Existing Repository
#### Opening Existing Repository
```typescript
const pathToExistingRepo = '/path/to/your/existing/repo';
const existingRepo = await smartgit.createRepoByOpen(pathToExistingRepo);
const repo = await smartgit.createRepoByOpen('./existing-repo');
```
### Working with Git Operations
### 🌐 Working with Remotes
`Smartgit` simplifies common Git operations, making it easy to execute commands like add, commit, push, and more programmatically.
#### Adding Changes
To stage changes, you can add all changes or specify particular files.
#### List All Remotes
```typescript
// Add all changes to staging
await existingRepo.addAll();
// Or specify particular files
await existingRepo.add(['file1.txt', 'path/to/file2.txt']);
```
#### Committing Changes
Once changes are staged, you can commit them.
```typescript
await existingRepo.commit('Your commit message');
```
#### Pushing to a Remote
Before pushing, ensure a remote is set up correctly, then push your changes.
```typescript
await existingRepo.ensureRemote('origin', 'https://github.com/yourusername/your-repo.git');
await existingRepo.pushBranchToRemote('main', 'origin');
```
### Advanced Features
`Smartgit` also supports more advanced Git functionalities, such as dealing with branches, managing remotes, and checking repository status.
#### Listing Remotes
```typescript
const remotes = await existingRepo.listRemotes();
const remotes = await repo.listRemotes();
console.log(remotes);
// Output: [{ remote: 'origin', url: 'https://github.com/...' }]
```
#### Working with Branches
#### Ensure Remote Exists
Branch management such as creating new branches or checking out existing ones can be done through the underlying `isomorphic-git` functions, with `Smartgit` making the setup and usage straightforward.
```typescript
await repo.ensureRemote('origin', 'https://github.com/username/repo.git');
```
### Practical Tips
#### Get Remote URL
- When dealing with asynchronous operations, especially in sequences that depend on the outcome of previous steps (e.g., staging, committing, and pushing), ensure proper error handling, either using `.then().catch()` chains or `try/catch` blocks with async/await.
- For complex Git workflows, consider combining `Smartgit`'s capabilities with other Node.js modules or scripts to automate and streamline your processes.
```typescript
const originUrl = await repo.getUrlForRemote('origin');
console.log(`📡 Origin URL: ${originUrl}`);
```
### Conclusion
#### Push to Remote
`@push.rocks/smartgit` provides a versatile and powerful toolkit for Git operations in Node.js applications. By abstracting the complexities of interacting with Git repositories, it enables developers to focus more on developing their logic and less on the intricacies of Git commands. Whether you're managing local repositories, automating deployment workflows, or integrating Git operations into your applications, `Smartgit` offers a comprehensive set of features to address your needs.
```typescript
await repo.pushBranchToRemote('main', 'origin');
console.log('✅ Changes pushed successfully!');
```
For further examples, contributions, and issues, please refer to the [project's repository](https://gitlab.com/pushrocks/smartgit) and consider contributing to or starring the project if you find it useful.
### 📊 Diff Analysis
Get a detailed diff of uncommitted changes (perfect for code review automation):
```typescript
const diffs = await repo.getUncommittedDiff(['node_modules/*', '*.log']);
diffs.forEach(diff => {
console.log('📝 File changes:');
console.log(diff);
});
```
### 📚 Commit History Analysis
Extract commit history with package.json version tracking:
```typescript
const history = await repo.getAllCommitMessages();
history.forEach(commit => {
console.log(`🕐 ${commit.date} | v${commit.version} | ${commit.message}`);
});
// Example output:
// 🕐 2024-01-15 | v1.2.3 | feat: add new authentication method
// 🕐 2024-01-14 | v1.2.2 | fix: resolve memory leak in parser
```
## 🛠️ Advanced Usage
### Error Handling
```typescript
try {
const repo = await smartgit.createRepoByClone(invalidUrl, './test');
} catch (error) {
console.error('❌ Clone failed:', error.message);
}
```
### Working with Multiple Repositories
```typescript
const smartgit = new Smartgit();
await smartgit.init();
const repositories = [
await smartgit.createRepoByOpen('./repo1'),
await smartgit.createRepoByOpen('./repo2'),
await smartgit.createRepoByOpen('./repo3')
];
// Analyze all repositories
for (const repo of repositories) {
const remotes = await repo.listRemotes();
console.log(`📂 Repository has ${remotes.length} remotes`);
}
```
## 🏗️ Architecture
SmartGit is built with a clean, modular architecture:
- **`Smartgit`** - Main entry point and repository factory
- **`GitRepo`** - Individual repository operations and analysis
- **`isomorphic-git`** - Underlying Git implementation
- **Environment Detection** - Automatic Node.js environment setup
## 🎯 Use Cases
- **🤖 Automation Scripts** - Automate repository management and analysis
- **🔄 CI/CD Pipelines** - Repository operations in build processes
- **📊 Code Analysis Tools** - Extract commit data and diff analysis
- **🛠️ Developer Tools** - Build Git-powered development utilities
- **📱 Release Management** - Track versions and generate changelogs
## 🔧 Requirements
- **Node.js** 16+ (Browser support not available)
- **Git repository** access (for clone operations)
- **TypeScript** 4+ (recommended)
## 🤝 API Reference
### Smartgit Class
| Method | Description | Returns |
|--------|-------------|---------|
| `init()` | Initialize the SmartGit instance | `Promise<void>` |
| `createRepoByInit(dir)` | Create new repository | `Promise<GitRepo>` |
| `createRepoByClone(url, dir)` | Clone repository | `Promise<GitRepo>` |
| `createRepoByOpen(dir)` | Open existing repository | `Promise<GitRepo>` |
### GitRepo Class
| Method | Description | Returns |
|--------|-------------|---------|
| `listRemotes()` | List all remotes | `Promise<{remote: string, url: string}[]>` |
| `ensureRemote(name, url)` | Ensure remote exists | `Promise<void>` |
| `getUrlForRemote(name)` | Get URL for remote | `Promise<string>` |
| `pushBranchToRemote(branch, remote)` | Push branch to remote | `Promise<void>` |
| `getUncommittedDiff(excludeFiles?)` | Get uncommitted changes | `Promise<string[]>` |
| `getAllCommitMessages()` | Get commit history | `Promise<CommitInfo[]>` |
## 💡 Pro Tips
- **🔧 Always call `init()`** before using any repository operations
- **📁 Use absolute paths** for repository directories when possible
- **🚫 Exclude large files** from diff analysis using the `excludeFiles` parameter
- **⚡ Batch operations** when working with multiple repositories
- **🛡️ Handle errors gracefully** - network issues can cause clone operations to fail
## License and Legal Information
@@ -134,4 +235,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -1,4 +1,4 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartgit from '../ts/index.js';
import * as smartpath from '@push.rocks/smartpath';

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartgit',
version: '3.1.1',
version: '3.3.1',
description: 'A smart wrapper for nodegit that simplifies Git operations in Node.js.'
}

View File

@@ -122,8 +122,8 @@ export class GitRepo {
const diffs: string[] = [];
for (const row of statusMatrix) {
const [filepath, head, workdir] = row;
if (excludeFiles.includes(filepath)) {
continue; // Skip excluded files
if (excludeFiles.some(pattern => filepath === pattern || plugins.minimatch(filepath, pattern))) {
continue; // Skip excluded files (supports exact matches and glob patterns)
}
let headContent = '';
@@ -156,6 +156,31 @@ export class GitRepo {
plugins.path.join(this.repoDir, filepath),
'utf8'
);
// Try to read from HEAD anyway - catches false positives from symlinks
// where isomorphic-git reports symlink contents as "added" files
try {
headContent = await plugins.isomorphicGit
.readBlob({
fs: this.smartgitRef.envDeps.fs,
dir: this.repoDir,
oid: await plugins.isomorphicGit.resolveRef({
fs: this.smartgitRef.envDeps.fs,
dir: this.repoDir,
ref: 'HEAD',
}),
filepath,
})
.then((result) => new TextDecoder().decode(result.blob));
} catch (err) {
// Check if this is a symlink false positive
// Error: "was anticipated to be a tree but it is a blob" means parent path is a symlink
if (err.message && err.message.includes('anticipated to be a tree but it is a blob')) {
// This file is inside a symlinked directory - skip it entirely
continue;
}
// Otherwise, file truly doesn't exist in HEAD - leave headContent empty for diff
}
}
// Handle deleted files
@@ -175,6 +200,11 @@ export class GitRepo {
}
if (headContent || workdirContent) {
// Skip files with identical content (filters false positives from statusMatrix)
if (headContent === workdirContent) {
continue;
}
const diff = plugins.diff.createTwoFilesPatch(
filepath,
filepath,

View File

@@ -1,40 +1,90 @@
import * as plugins from './smartgit.plugins.js';
import { GitRepo } from './smartgit.classes.gitrepo.js';
interface IEnvDeps {
fs: typeof import('fs') | null;
http: any | null; // isomorphic-git http interface
}
/**
* class Smartgit provides a high-level interface for git operations
* Must be initialized before use by calling init()
*/
export class Smartgit {
public smartenvInstance = new plugins.smartenv.Smartenv();
public envDeps: {
fs: any;
http: any;
} = {
public envDeps: IEnvDeps = {
fs: null,
http: null,
};
constructor() {}
public async init() {
if (this.smartenvInstance.isNode) {
this.envDeps.fs = await this.smartenvInstance.getSafeNodeModule('fs');
this.envDeps.http = await this.smartenvInstance.getSafeNodeModule(
'isomorphic-git/http/node/index.js'
);
} else {
throw new Error('currently only node.js is supported.');
/**
* initializes the Smartgit instance with required environment dependencies
* Must be called before using any repository methods
*/
public async init(): Promise<void> {
try {
if (this.smartenvInstance.isNode) {
this.envDeps.fs = await this.smartenvInstance.getSafeNodeModule('fs');
this.envDeps.http = await this.smartenvInstance.getSafeNodeModule(
'isomorphic-git/http/node'
);
} else {
throw new Error('currently only node.js is supported.');
}
} catch (error) {
throw new Error(`Failed to initialize Smartgit: ${error instanceof Error ? error.message : String(error)}`);
}
}
public async createRepoByClone(fromUrlArg: string, toDirArg: string) {
const repo = await GitRepo.fromCloningIntoDir(this, fromUrlArg, toDirArg);
private ensureInitialized(): void {
if (!this.envDeps.fs || !this.envDeps.http) {
throw new Error('Smartgit must be initialized before use. Call init() first.');
}
}
public async createRepoByInit(dirArg: string) {
const repo = await GitRepo.fromCreatingRepoInDir(this, dirArg);
return repo;
/**
* creates a new GitRepo instance by cloning from a remote URL
* @param fromUrlArg the URL to clone from
* @param toDirArg the directory to clone into
* @returns Promise<GitRepo> the created repository instance
*/
public async createRepoByClone(fromUrlArg: string, toDirArg: string): Promise<GitRepo> {
this.ensureInitialized();
try {
const repo = await GitRepo.fromCloningIntoDir(this, fromUrlArg, toDirArg);
return repo;
} catch (error) {
throw new Error(`Failed to clone repository: ${error instanceof Error ? error.message : String(error)}`);
}
}
public async createRepoByOpen(dirArg: string) {
const repo = await GitRepo.fromOpeningRepoDir(this, dirArg);
return repo;
/**
* creates a new GitRepo instance by initializing a new repository in a directory
* @param dirArg the directory to initialize the repository in
* @returns Promise<GitRepo> the created repository instance
*/
public async createRepoByInit(dirArg: string): Promise<GitRepo> {
this.ensureInitialized();
try {
const repo = await GitRepo.fromCreatingRepoInDir(this, dirArg);
return repo;
} catch (error) {
throw new Error(`Failed to initialize repository: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* creates a new GitRepo instance by opening an existing repository in a directory
* @param dirArg the directory containing the existing repository
* @returns Promise<GitRepo> the opened repository instance
*/
public async createRepoByOpen(dirArg: string): Promise<GitRepo> {
this.ensureInitialized();
try {
const repo = await GitRepo.fromOpeningRepoDir(this, dirArg);
return repo;
} catch (error) {
throw new Error(`Failed to open repository: ${error instanceof Error ? error.message : String(error)}`);
}
}
}

View File

@@ -15,5 +15,6 @@ export { smartenv, smartfile, smartpath, smartpromise, smartstring, smarttime };
// third party
import * as diff from 'diff';
import isomorphicGit from 'isomorphic-git';
import { minimatch } from 'minimatch';
export { diff, isomorphicGit };
export { diff, isomorphicGit, minimatch };