Compare commits

..

13 Commits

Author SHA1 Message Date
jkunz 66cf31d50d v3.3.2 2026-04-30 18:19:27 +00:00
jkunz 8ed7413e62 fix(build): modernize project configuration and tighten Node.js typing support 2026-04-30 18:19:27 +00:00
jkunz ff0bc72408 3.3.1 2025-11-04 03:40:49 +00:00
jkunz 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
jkunz 238dd152ba 3.3.0 2025-11-04 01:26:54 +00:00
jkunz 9bacb2e548 feat(GitRepo): Add glob-pattern exclusions for getUncommittedDiff and add minimatch; bump dependencies 2025-11-04 01:26:54 +00:00
jkunz 5ba6c5370e 3.2.1 2025-08-04 15:16:42 +00:00
jkunz 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
jkunz a92275088b update 2025-08-04 14:32:04 +00:00
jkunz 2b246502f5 update deps 2025-08-04 14:01:52 +00:00
philkunz 81b2b225a3 fix(changelog): Update to neweset format. 2024-06-23 23:42:47 +02:00
philkunz 9a2d5afc04 3.1.1 2024-06-23 23:37:30 +02:00
philkunz 6404c8342a fix(documentation): Remove outdated changelog entries 2024-06-23 23:37:29 +02:00
16 changed files with 6489 additions and 4332 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartgit",
"description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.",
"npmPackagename": "@push.rocks/smartgit",
"license": "MIT",
"projectDomain": "push.rocks"
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy 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.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
}
}
+94 -17
View File
@@ -1,25 +1,102 @@
# Changelog # Changelog
## 2024-06-23 - 3.1.0 - feat(gitrepo) ## 2026-04-30 - 3.3.2 - fix(build)
Enhance GitRepo to include commit date and version information modernize project configuration and tighten Node.js typing support
- Enhanced `getAllCommitMessages` to include commit date and package.json version.
- Improved `getUncommittedDiff` by providing detailed diff information for each file.
- migrate npmextra configuration to namespaced tool settings and add .smartconfig.json
- update build and test scripts alongside TypeScript and dependency versions
- refactor Smartgit to expose validated fs/http accessors and improve return type annotations
- adjust tests and repository URLs to use code.foss.global
```markdown ## 2025-11-04 - 3.3.1 - fix(getUncommittedDiff)
## 2023-10-01 - 3.0.0 - core Avoid false-positive diffs in getUncommittedDiff by detecting symlinked directories and skipping identical files
BREAKING CHANGE: switch to esm
## 2023-09-15 - 2.0.0 - dependencies - 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.
BREAKING CHANGE: switch to isomorphic git - 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.
## 2023-08-01 - 1.0.0 - general ## 2025-11-04 - 3.3.0 - feat(GitRepo)
Implement new class approach Add glob-pattern exclusions for getUncommittedDiff and add minimatch; bump dependencies
- Update README
## 2023-07-15 - 0.1.0 - general - getUncommittedDiff now supports glob patterns for excluded files via minimatch (skip files when filepath matches exact or glob pattern).
Initial release - Expose minimatch through plugins (ts/smartgit.plugins.ts) so plugin code can use glob matching consistently.
- Now works with SSH keys - Add minimatch to dependencies (minimatch ^10.1.1).
- Better sshKey understanding - 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
- Enhance GitRepo to include commit date and version information
## 2024-06-22 - 3.0.3 to 3.0.4 - core & GitRepo
General updates and new feature addition
- Fixed core functionality
- Added diff function in GitRepo
## 2023-11-15 - 3.0.1 to 3.0.2 - core, tsconfig, npmextra
Minor updates and fixes
- Fixed core functionality
- Updated tsconfig and npmextra.json
## 2023-07-10 - 3.0.0 - core
Structural changes to organization scheme
- Switched to a new organizational scheme
## 2023-07-27 - 3.0.0 to 3.0.1 - core
Structural changes and updates
- Fixed core functionality
- Switched to new organizational scheme
## 2022-07-31 - 2.0.2 to 3.0.0 - core
Breaking changes and updates
- Switching to ESM for core
- Fixed core functionality
## 2021-10-22 - 1.0.18 to 2.0.1 - dependencies, core
Breaking changes and updates
- Switched to isomorphic git dependencies
- Fixed core functionality
## 2020-08-15 - 1.0.14 to 1.0.18 - core
Fixes
- Fixed core functionality in multiple patches
## 2019-06-18 - 1.0.5 to 1.0.10 - core
Fixes
- Fixed core functionality in multiple patches
## 2016-06-23 - 0.0.10 to 0.1.11 - gitlab & other fixes
Initial implementations and setup
- Fixed README and merge issues
- Updated gitlab.yml and CI settings
- Implemented new class approach and other updates
- Removed unnecessary imports, postinstall
- Added npmextra.json, CI tests, SSH key support
+1 -2
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2015 Lossless GmbH Copyright (c) 2026 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2026 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14 -6
View File
@@ -1,9 +1,5 @@
{ {
"npmci": { "@git.zone/cli": {
"npmGlobalTools": [],
"npmAccessLevel": "public"
},
"gitzone": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "code.foss.global", "githost": "code.foss.global",
@@ -12,6 +8,7 @@
"description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.", "description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.",
"npmPackagename": "@push.rocks/smartgit", "npmPackagename": "@push.rocks/smartgit",
"license": "MIT", "license": "MIT",
"projectDomain": "push.rocks",
"keywords": [ "keywords": [
"git", "git",
"nodegit", "nodegit",
@@ -23,9 +20,20 @@
"repository management", "repository management",
"git operations" "git operations"
] ]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
} }
}, },
"tsdoc": { "@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy 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.\n" "legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy 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.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmRegistryUrl": "registry.npmjs.org"
} }
} }
+22 -20
View File
@@ -1,13 +1,13 @@
{ {
"name": "@push.rocks/smartgit", "name": "@push.rocks/smartgit",
"version": "3.1.0", "version": "3.3.2",
"description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.", "description": "A smart wrapper for nodegit that simplifies Git operations in Node.js.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "tstest test/ --verbose",
"build": "(tsbuild --web --allowimplicitany)", "build": "tsbuild --web",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"repository": { "repository": {
@@ -28,27 +28,26 @@
"author": "Smart Coordination GmbH <office@push.rocks> (https://push.rocks)", "author": "Smart Coordination GmbH <office@push.rocks> (https://push.rocks)",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://gitlab.com/pushrocks/smartgit/issues" "url": "https://code.foss.global/push.rocks/smartgit/issues"
}, },
"homepage": "https://code.foss.global/push.rocks/smartgit", "homepage": "https://code.foss.global/push.rocks/smartgit",
"dependencies": { "dependencies": {
"@push.rocks/smartenv": "^5.0.12", "@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartfile": "^11.0.20", "@push.rocks/smartfile": "^13.1.3",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.2", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartshell": "^3.0.5", "@push.rocks/smartshell": "^3.3.8",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smarttime": "^4.0.6", "@push.rocks/smarttime": "^4.2.3",
"@types/diff": "^5.2.1", "diff": "^9.0.0",
"@types/minimatch": "^5.1.2", "isomorphic-git": "^1.37.6",
"diff": "^5.2.0", "minimatch": "^10.2.5"
"isomorphic-git": "^1.25.10"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.80", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^1.2.44", "@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^3.6.3",
"@push.rocks/tapbundle": "^5.0.23" "@types/node": "^25.6.0"
}, },
"private": false, "private": false,
"files": [ "files": [
@@ -60,10 +59,13 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
".smartconfig.json",
"license",
"npmextra.json", "npmextra.json",
"readme.md" "readme.md"
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
] ],
"packageManager": "pnpm@10.28.2"
} }
+5931 -4148
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer
+31
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()`
+175 -74
View File
@@ -1,121 +1,222 @@
# @push.rocks/smartgit # @push.rocks/smartgit
smart wrapper for nodegit
## Install > 🚀 **Modern Git operations for Node.js** - A powerful TypeScript wrapper around isomorphic-git that makes repository management and analysis a breeze
To install @push.rocks/smartgit, use the following command with npm:
[![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 ```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 yarn add @push.rocks/smartgit
``` ```
Make sure you have `node` installed on your system to use this package. ## 🚀 Quick Start
## 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.
```typescript ```typescript
import { Smartgit } from '@push.rocks/smartgit'; import { Smartgit } from '@push.rocks/smartgit';
// Initialize the Smartgit instance // Initialize SmartGit
const smartgit = new Smartgit(); const smartgit = new Smartgit();
await smartgit.init(); 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 #### Creating a New Repository
```typescript ```typescript
const pathToNewRepo = '/path/to/your/new/repo'; const repo = await smartgit.createRepoByInit('./my-new-repo');
const newRepo = await smartgit.createRepoByInit(pathToNewRepo);
``` ```
#### Cloning a Repository #### Cloning from Remote
```typescript ```typescript
const cloneUrl = 'https://github.com/yourusername/your-repo.git'; const repo = await smartgit.createRepoByClone(
const pathToClone = '/path/to/clone/repo'; 'https://github.com/octocat/Hello-World.git',
const clonedRepo = await smartgit.createRepoByClone(cloneUrl, pathToClone); './hello-world'
);
``` ```
#### Opening an Existing Repository #### Opening Existing Repository
```typescript ```typescript
const pathToExistingRepo = '/path/to/your/existing/repo'; const repo = await smartgit.createRepoByOpen('./existing-repo');
const existingRepo = await smartgit.createRepoByOpen(pathToExistingRepo);
``` ```
### 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. #### List All Remotes
#### Adding Changes
To stage changes, you can add all changes or specify particular files.
```typescript ```typescript
// Add all changes to staging const remotes = await repo.listRemotes();
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();
console.log(remotes); 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. ```typescript
- For complex Git workflows, consider combining `Smartgit`'s capabilities with other Node.js modules or scripts to automate and streamline your processes. 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 ## License and Legal Information
+6 -4
View File
@@ -1,8 +1,9 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartgit from '../ts/index.js'; import * as smartgit from '../ts/index.js';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
import * as path from 'path'; import * as fs from 'node:fs/promises';
import * as path from 'node:path';
let testSmartgitInstance: smartgit.Smartgit; let testSmartgitInstance: smartgit.Smartgit;
const packageDir = path.join(smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../'); const packageDir = path.join(smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');
@@ -20,8 +21,9 @@ tap.test('should create a new repo at .nogit', async () => {
}); });
tap.test('should clone a repo', async () => { tap.test('should clone a repo', async () => {
await fs.rm(testRepoDirSmartfile, { recursive: true, force: true });
const gitRepo = await testSmartgitInstance.createRepoByClone( const gitRepo = await testSmartgitInstance.createRepoByClone(
'https://gitlab.com/push.rocks/smartfile.git', 'https://code.foss.global/push.rocks/smartfile.git',
testRepoDirSmartfile testRepoDirSmartfile
); );
}); });
@@ -48,4 +50,4 @@ tap.test('should print all commit messages', async () => {
console.log(commitMessages); console.log(commitMessages);
}); });
await tap.start(); export default tap.start();
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartgit', name: '@push.rocks/smartgit',
version: '3.1.0', version: '3.3.2',
description: 'A smart wrapper for nodegit that simplifies Git operations in Node.js.' description: 'A smart wrapper for nodegit that simplifies Git operations in Node.js.'
} }
+57 -25
View File
@@ -17,8 +17,8 @@ export class GitRepo {
const dirArg = plugins.path.resolve(toArg); const dirArg = plugins.path.resolve(toArg);
await plugins.isomorphicGit.clone({ await plugins.isomorphicGit.clone({
dir: toArg, dir: toArg,
fs: smartgitRefArg.envDeps.fs, fs: smartgitRefArg.fs,
http: smartgitRefArg.envDeps.http, http: smartgitRefArg.http,
url: fromArg, url: fromArg,
}); });
return new GitRepo(smartgitRefArg, toArg); return new GitRepo(smartgitRefArg, toArg);
@@ -31,12 +31,12 @@ export class GitRepo {
dirArg = plugins.path.resolve(dirArg); dirArg = plugins.path.resolve(dirArg);
await plugins.isomorphicGit.init({ await plugins.isomorphicGit.init({
dir: dirArg, dir: dirArg,
fs: smartgitRefArg.envDeps.fs, fs: smartgitRefArg.fs,
}); });
return new GitRepo(smartgitRefArg, dirArg); return new GitRepo(smartgitRefArg, dirArg);
} }
public static async fromOpeningRepoDir(smartgitRefArg: Smartgit, dirArg: string) { public static async fromOpeningRepoDir(smartgitRefArg: Smartgit, dirArg: string): Promise<GitRepo> {
dirArg = plugins.path.resolve(dirArg); dirArg = plugins.path.resolve(dirArg);
return new GitRepo(smartgitRefArg, dirArg); return new GitRepo(smartgitRefArg, dirArg);
} }
@@ -60,7 +60,7 @@ export class GitRepo {
}[] }[]
> { > {
const remotes = await plugins.isomorphicGit.listRemotes({ const remotes = await plugins.isomorphicGit.listRemotes({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
}); });
return remotes; return remotes;
@@ -78,7 +78,7 @@ export class GitRepo {
if (existingRemote.url !== remoteUrlArg) { if (existingRemote.url !== remoteUrlArg) {
await plugins.isomorphicGit.deleteRemote({ await plugins.isomorphicGit.deleteRemote({
remote: remoteNameArg, remote: remoteNameArg,
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
}); });
} else { } else {
@@ -87,7 +87,7 @@ export class GitRepo {
} }
await plugins.isomorphicGit.addRemote({ await plugins.isomorphicGit.addRemote({
remote: remoteNameArg, remote: remoteNameArg,
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
url: remoteUrlArg, url: remoteUrlArg,
}); });
} }
@@ -95,16 +95,16 @@ export class GitRepo {
/** /**
* gets the url for a specific remote * gets the url for a specific remote
*/ */
public async getUrlForRemote(remoteName: string): Promise<string> { public async getUrlForRemote(remoteName: string): Promise<string | undefined> {
const remotes = await this.listRemotes(); const remotes = await this.listRemotes();
const existingRemote = remotes.find((remoteArg) => remoteArg.remote === remoteName); const existingRemote = remotes.find((remoteArg) => remoteArg.remote === remoteName);
return existingRemote?.url; return existingRemote?.url;
} }
public async pushBranchToRemote(branchName: string, remoteName: string) { public async pushBranchToRemote(branchName: string, remoteName: string): Promise<void> {
await plugins.isomorphicGit.push({ await plugins.isomorphicGit.push({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
http: this.smartgitRef.envDeps.http, http: this.smartgitRef.http,
ref: branchName, ref: branchName,
remote: remoteName, remote: remoteName,
}); });
@@ -115,15 +115,15 @@ export class GitRepo {
*/ */
public async getUncommittedDiff(excludeFiles: string[] = []): Promise<string[]> { public async getUncommittedDiff(excludeFiles: string[] = []): Promise<string[]> {
const statusMatrix = await plugins.isomorphicGit.statusMatrix({ const statusMatrix = await plugins.isomorphicGit.statusMatrix({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
}); });
const diffs: string[] = []; const diffs: string[] = [];
for (const row of statusMatrix) { for (const row of statusMatrix) {
const [filepath, head, workdir] = row; const [filepath, head, workdir] = row;
if (excludeFiles.includes(filepath)) { if (excludeFiles.some(pattern => filepath === pattern || plugins.minimatch(filepath, pattern))) {
continue; // Skip excluded files continue; // Skip excluded files (supports exact matches and glob patterns)
} }
let headContent = ''; let headContent = '';
@@ -133,10 +133,10 @@ export class GitRepo {
if (head !== 0 && workdir !== 0 && head !== workdir) { if (head !== 0 && workdir !== 0 && head !== workdir) {
headContent = await plugins.isomorphicGit headContent = await plugins.isomorphicGit
.readBlob({ .readBlob({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
oid: await plugins.isomorphicGit.resolveRef({ oid: await plugins.isomorphicGit.resolveRef({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
ref: 'HEAD', ref: 'HEAD',
}), }),
@@ -144,7 +144,7 @@ export class GitRepo {
}) })
.then((result) => new TextDecoder().decode(result.blob)); .then((result) => new TextDecoder().decode(result.blob));
workdirContent = await this.smartgitRef.envDeps.fs.promises.readFile( workdirContent = await this.smartgitRef.fs.promises.readFile(
plugins.path.join(this.repoDir, filepath), plugins.path.join(this.repoDir, filepath),
'utf8' 'utf8'
); );
@@ -152,20 +152,45 @@ export class GitRepo {
// Handle added files // Handle added files
if (head === 0 && workdir !== 0) { if (head === 0 && workdir !== 0) {
workdirContent = await this.smartgitRef.envDeps.fs.promises.readFile( workdirContent = await this.smartgitRef.fs.promises.readFile(
plugins.path.join(this.repoDir, filepath), plugins.path.join(this.repoDir, filepath),
'utf8' '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.fs,
dir: this.repoDir,
oid: await plugins.isomorphicGit.resolveRef({
fs: this.smartgitRef.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 instanceof Error && 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 // Handle deleted files
if (head !== 0 && workdir === 0) { if (head !== 0 && workdir === 0) {
headContent = await plugins.isomorphicGit headContent = await plugins.isomorphicGit
.readBlob({ .readBlob({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
oid: await plugins.isomorphicGit.resolveRef({ oid: await plugins.isomorphicGit.resolveRef({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
ref: 'HEAD', ref: 'HEAD',
}), }),
@@ -175,6 +200,11 @@ export class GitRepo {
} }
if (headContent || workdirContent) { if (headContent || workdirContent) {
// Skip files with identical content (filters false positives from statusMatrix)
if (headContent === workdirContent) {
continue;
}
const diff = plugins.diff.createTwoFilesPatch( const diff = plugins.diff.createTwoFilesPatch(
filepath, filepath,
filepath, filepath,
@@ -195,23 +225,25 @@ export class GitRepo {
{ date: string; version: string; message: string }[] { date: string; version: string; message: string }[]
> { > {
const commits = await plugins.isomorphicGit.log({ const commits = await plugins.isomorphicGit.log({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
}); });
const results = []; const results: { date: string; version: string; message: string }[] = [];
for (const commit of commits) { for (const commit of commits) {
let version = 'unknown'; let version = 'unknown';
try { try {
const packageJsonBlob = await plugins.isomorphicGit.readBlob({ const packageJsonBlob = await plugins.isomorphicGit.readBlob({
fs: this.smartgitRef.envDeps.fs, fs: this.smartgitRef.fs,
dir: this.repoDir, dir: this.repoDir,
oid: commit.oid, oid: commit.oid,
filepath: 'package.json', filepath: 'package.json',
}); });
const packageJson = JSON.parse(new TextDecoder().decode(packageJsonBlob.blob)); const packageJson = JSON.parse(new TextDecoder().decode(packageJsonBlob.blob)) as {
version = packageJson.version; version?: string;
};
version = packageJson.version ?? 'unknown';
} catch (error) { } catch (error) {
// If package.json does not exist or any error occurs, leave version as 'unknown' // If package.json does not exist or any error occurs, leave version as 'unknown'
} }
+83 -15
View File
@@ -1,40 +1,108 @@
import * as plugins from './smartgit.plugins.js'; import * as plugins from './smartgit.plugins.js';
import { GitRepo } from './smartgit.classes.gitrepo.js'; import { GitRepo } from './smartgit.classes.gitrepo.js';
type TNodeFs = typeof import('node:fs');
interface IEnvDeps {
fs?: TNodeFs;
http?: plugins.HttpClient;
}
/**
* class Smartgit provides a high-level interface for git operations
* Must be initialized before use by calling init()
*/
export class Smartgit { export class Smartgit {
public smartenvInstance = new plugins.smartenv.Smartenv(); public smartenvInstance = new plugins.smartenv.Smartenv();
public envDeps: { public envDeps: IEnvDeps = {
fs: any; fs: undefined,
http: any; http: undefined,
} = {
fs: null,
http: null,
}; };
constructor() {} /**
* initializes the Smartgit instance with required environment dependencies
public async init() { * Must be called before using any repository methods
*/
public async init(): Promise<void> {
try {
if (this.smartenvInstance.isNode) { if (this.smartenvInstance.isNode) {
this.envDeps.fs = await this.smartenvInstance.getSafeNodeModule('fs'); this.envDeps.fs = await this.smartenvInstance.getSafeNodeModule<TNodeFs>('fs');
this.envDeps.http = await this.smartenvInstance.getSafeNodeModule( this.envDeps.http = await this.smartenvInstance.getSafeNodeModule<plugins.HttpClient>(
'isomorphic-git/http/node/index.js' 'isomorphic-git/http/node'
); );
} else { } else {
throw new Error('currently only node.js is supported.'); 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) { private ensureInitialized(): void {
if (!this.envDeps.fs || !this.envDeps.http) {
throw new Error('Smartgit must be initialized before use. Call init() first.');
}
}
public get fs(): TNodeFs {
const fs = this.envDeps.fs;
if (!fs) {
throw new Error('Smartgit must be initialized before use. Call init() first.');
}
return fs;
}
public get http(): plugins.HttpClient {
const http = this.envDeps.http;
if (!http) {
throw new Error('Smartgit must be initialized before use. Call init() first.');
}
return http;
}
/**
* 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); 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 createRepoByInit(dirArg: string) { /**
* 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); const repo = await GitRepo.fromCreatingRepoInDir(this, dirArg);
return repo; return repo;
} catch (error) {
throw new Error(`Failed to initialize repository: ${error instanceof Error ? error.message : String(error)}`);
}
} }
public async createRepoByOpen(dirArg: string) { /**
* 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); const repo = await GitRepo.fromOpeningRepoDir(this, dirArg);
return repo; return repo;
} catch (error) {
throw new Error(`Failed to open repository: ${error instanceof Error ? error.message : String(error)}`);
}
} }
} }
+4 -3
View File
@@ -1,5 +1,5 @@
// node native // node native
import * as path from 'path'; import * as path from 'node:path';
export { path }; export { path };
@@ -14,6 +14,7 @@ export { smartenv, smartfile, smartpath, smartpromise, smartstring, smarttime };
// third party // third party
import * as diff from 'diff'; import * as diff from 'diff';
import isomorphicGit from 'isomorphic-git'; import isomorphicGit, { type HttpClient } from 'isomorphic-git';
import { minimatch } from 'minimatch';
export { diff, isomorphicGit }; export { diff, isomorphicGit, minimatch, type HttpClient };
+4 -4
View File
@@ -5,10 +5,10 @@
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"noImplicitAny": true,
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"types": ["node"]
}, },
"exclude": [ "exclude": ["dist_*/**/*.d.ts"]
"dist_*/**/*.d.ts"
]
} }