the base
This commit is contained in:
214
lib/develop/gittools/README.md
Normal file
214
lib/develop/gittools/README.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Git Tools Module
|
||||
|
||||
A comprehensive Git management module for V that provides high-level abstractions for Git operations, repository management, and automation of common Git workflows.
|
||||
|
||||
## Features
|
||||
|
||||
- Repository management (clone, load, delete)
|
||||
- Branch operations (create, switch, checkout)
|
||||
- Tag management (create, switch, verify)
|
||||
- Change tracking and commits
|
||||
- Remote operations (push, pull)
|
||||
- SSH key integration
|
||||
- Submodule support
|
||||
- Repository status tracking
|
||||
- Light cloning option for large repositories
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Repository Management
|
||||
|
||||
```v
|
||||
import freeflowuniverse.herolib.develop.gittools
|
||||
|
||||
// Initialize with code root directory
|
||||
mut gs := gittools.new(coderoot: '~/code')!
|
||||
|
||||
// Clone a repository
|
||||
mut repo := gs.clone(GitCloneArgs{
|
||||
url: 'git@github.com:username/repo.git'
|
||||
sshkey: 'deploy_key' // Optional SSH key name
|
||||
})!
|
||||
|
||||
// Or get existing repository
|
||||
mut repo := gs.get_repo(name: 'existing_repo')!
|
||||
|
||||
// Delete repository
|
||||
repo.delete()!
|
||||
```
|
||||
|
||||
### Branch Operations
|
||||
|
||||
```v
|
||||
// Create and switch to new branch
|
||||
repo.branch_create('feature-branch')!
|
||||
repo.branch_switch('feature-branch')!
|
||||
|
||||
// Check status and commit changes
|
||||
if repo.has_changes() {
|
||||
repo.commit('feat: Add new feature')!
|
||||
repo.push()!
|
||||
}
|
||||
|
||||
// Pull latest changes
|
||||
repo.pull()!
|
||||
|
||||
// Pull with submodules
|
||||
repo.pull(submodules: true)!
|
||||
```
|
||||
|
||||
### Tag Management
|
||||
|
||||
```v
|
||||
// Create a new tag
|
||||
repo.tag_create('v1.0.0')!
|
||||
|
||||
// Switch to tag
|
||||
repo.tag_switch('v1.0.0')!
|
||||
|
||||
// Check if tag exists
|
||||
exists := repo.tag_exists('v1.0.0')!
|
||||
|
||||
// Get tag information
|
||||
if repo.status_local.tag == 'v1.0.0' {
|
||||
// Currently on tag v1.0.0
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### SSH Key Integration
|
||||
|
||||
```v
|
||||
// Clone with SSH key
|
||||
mut repo := gs.clone(GitCloneArgs{
|
||||
url: 'git@github.com:username/repo.git'
|
||||
sshkey: 'deploy_key'
|
||||
})!
|
||||
|
||||
// Set SSH key for existing repository
|
||||
repo.set_sshkey('deploy_key')!
|
||||
```
|
||||
|
||||
### Repository Status
|
||||
|
||||
```v
|
||||
// Update repository status
|
||||
repo.status_update()!
|
||||
|
||||
// Check various status conditions
|
||||
if repo.need_commit() {
|
||||
// Has uncommitted changes
|
||||
}
|
||||
|
||||
if repo.need_push_or_pull() {
|
||||
// Has unpushed/unpulled changes
|
||||
}
|
||||
|
||||
if repo.need_checkout() {
|
||||
// Needs to checkout different branch/tag
|
||||
}
|
||||
```
|
||||
|
||||
### Change Management
|
||||
|
||||
```v
|
||||
// Check for changes
|
||||
if repo.has_changes() {
|
||||
// Handle changes
|
||||
}
|
||||
|
||||
// Reset all changes
|
||||
repo.reset()!
|
||||
// or
|
||||
repo.remove_changes()!
|
||||
|
||||
// Update submodules
|
||||
repo.update_submodules()!
|
||||
```
|
||||
|
||||
## Repository Configuration
|
||||
|
||||
### GitRepo Structure
|
||||
|
||||
```v
|
||||
pub struct GitRepo {
|
||||
pub mut:
|
||||
provider string // e.g., github.com
|
||||
account string // Git account name
|
||||
name string // Repository name
|
||||
status_remote GitRepoStatusRemote // Remote repository status
|
||||
status_local GitRepoStatusLocal // Local repository status
|
||||
status_wanted GitRepoStatusWanted // Desired status
|
||||
config GitRepoConfig // Repository configuration
|
||||
deploysshkey string // SSH key for git operations
|
||||
}
|
||||
```
|
||||
|
||||
### Status Tracking
|
||||
|
||||
```v
|
||||
// Remote Status
|
||||
pub struct GitRepoStatusRemote {
|
||||
pub mut:
|
||||
ref_default string // Default branch hash
|
||||
branches map[string]string // Branch name -> commit hash
|
||||
tags map[string]string // Tag name -> commit hash
|
||||
}
|
||||
|
||||
// Local Status
|
||||
pub struct GitRepoStatusLocal {
|
||||
pub mut:
|
||||
branches map[string]string // Branch name -> commit hash
|
||||
branch string // Current branch
|
||||
tag string // Current tag
|
||||
}
|
||||
|
||||
// Desired Status
|
||||
pub struct GitRepoStatusWanted {
|
||||
pub mut:
|
||||
branch string
|
||||
tag string
|
||||
url string // Remote repository URL
|
||||
readonly bool // Prevent push/commit operations
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The module provides comprehensive error handling:
|
||||
|
||||
```v
|
||||
// Clone with error handling
|
||||
mut repo := gs.clone(url: 'invalid_url') or {
|
||||
println('Clone failed: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
// Commit with error handling
|
||||
repo.commit('feat: New feature') or {
|
||||
if err.msg().contains('nothing to commit') {
|
||||
println('No changes to commit')
|
||||
} else {
|
||||
println('Commit failed: ${err}')
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
v -enable-globals test herolib/develop/gittools/tests/
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- SSH keys should be properly configured in `~/.ssh/`
|
||||
- For readonly repositories, all local changes will be reset on pull
|
||||
- Light cloning option (`config.light: true`) creates shallow clones
|
||||
- Repository status is automatically cached and updated
|
||||
- Submodules are handled recursively when specified
|
||||
- All operations maintain repository consistency
|
||||
129
lib/develop/gittools/factory.v
Normal file
129
lib/develop/gittools/factory.v
Normal file
@@ -0,0 +1,129 @@
|
||||
module gittools
|
||||
|
||||
import os
|
||||
import json
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
__global (
|
||||
gsinstances map[string]&GitStructure
|
||||
)
|
||||
|
||||
pub fn reset() {
|
||||
gsinstances = map[string]&GitStructure{} // they key is the redis_key (hash of coderoot)
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GitStructureArgsNew {
|
||||
pub mut:
|
||||
coderoot string
|
||||
light bool = true // If true, clones only the last history for all branches (clone with only 1 level deep)
|
||||
log bool = true // If true, logs git commands/statements
|
||||
debug bool = true
|
||||
ssh_key_name string // name of ssh key to be used when loading the gitstructure
|
||||
reload bool
|
||||
}
|
||||
|
||||
// Retrieve or create a new GitStructure instance with the given configuration.
|
||||
pub fn new(args_ GitStructureArgsNew) !&GitStructure {
|
||||
mut args := args_
|
||||
if args.coderoot == '' {
|
||||
args.coderoot = '${os.home_dir()}/code'
|
||||
}
|
||||
mut cfg := GitStructureConfig{
|
||||
coderoot: args.coderoot
|
||||
light: args.light
|
||||
log: args.log
|
||||
debug: args.debug
|
||||
ssh_key_name: args.ssh_key_name
|
||||
}
|
||||
// Retrieve the configuration from Redis.
|
||||
rediskey_ := rediskey(args.coderoot)
|
||||
mut redis := redis_get()
|
||||
datajson := json.encode(cfg)
|
||||
redis.set(rediskey_, datajson)!
|
||||
|
||||
return get(coderoot: args.coderoot, reload: args.reload)
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GitStructureArgGet {
|
||||
pub mut:
|
||||
coderoot string
|
||||
reload bool
|
||||
}
|
||||
|
||||
// Retrieve a GitStructure instance based on the given arguments.
|
||||
pub fn get(args_ GitStructureArgGet) !&GitStructure {
|
||||
mut args := args_
|
||||
if args.coderoot == '' {
|
||||
args.coderoot = '${os.home_dir()}/code'
|
||||
}
|
||||
if args.reload {
|
||||
cachereset()!
|
||||
}
|
||||
rediskey_ := rediskey(args.coderoot)
|
||||
// println(rediskey_)
|
||||
|
||||
// Return existing instance if already created.
|
||||
if rediskey_ in gsinstances {
|
||||
mut gs := gsinstances[rediskey_] or {
|
||||
panic('Unexpected error: key not found in gsinstances')
|
||||
}
|
||||
if args.reload {
|
||||
gs.load()!
|
||||
}
|
||||
return gs
|
||||
}
|
||||
|
||||
mut redis := redis_get()
|
||||
mut datajson := redis.get(rediskey_) or { '' }
|
||||
|
||||
if datajson == '' {
|
||||
if args_.coderoot == '' {
|
||||
return new()!
|
||||
}
|
||||
return error("can't find repostructure for coderoot: ${args.coderoot}")
|
||||
}
|
||||
|
||||
mut config := json.decode(GitStructureConfig, datajson) or { GitStructureConfig{} }
|
||||
|
||||
// Create and load the GitStructure instance.
|
||||
mut gs := GitStructure{
|
||||
key: rediskey_
|
||||
config: config
|
||||
coderoot: pathlib.get_dir(path: args.coderoot, create: true)!
|
||||
}
|
||||
|
||||
if args.reload {
|
||||
gs.load()!
|
||||
} else {
|
||||
gs.init()!
|
||||
}
|
||||
|
||||
gsinstances[rediskey_] = &gs
|
||||
|
||||
return gsinstances[rediskey_] or { panic('bug') }
|
||||
}
|
||||
|
||||
// Reset the configuration cache for Git structures.
|
||||
pub fn configreset() ! {
|
||||
mut redis := redis_get()
|
||||
key_check := 'git:config:*'
|
||||
keys := redis.keys(key_check)!
|
||||
|
||||
for key in keys {
|
||||
redis.del(key)!
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all caches and configurations for all Git repositories.
|
||||
pub fn cachereset() ! {
|
||||
key_check := 'git:repos:**'
|
||||
mut redis := redis_get()
|
||||
keys := redis.keys(key_check)!
|
||||
|
||||
for key in keys {
|
||||
redis.del(key)!
|
||||
}
|
||||
configreset()!
|
||||
}
|
||||
129
lib/develop/gittools/gitlocation.v
Normal file
129
lib/develop/gittools/gitlocation.v
Normal file
@@ -0,0 +1,129 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
// GitLocation uniquely identifies a Git repository, its online URL, and its location in the filesystem.
|
||||
@[heap]
|
||||
pub struct GitLocation {
|
||||
pub mut:
|
||||
provider string // Git provider (e.g., GitHub)
|
||||
account string // Account name
|
||||
name string // Repository name
|
||||
branch_or_tag string // Branch name
|
||||
path string // Path in the repository (not the filesystem)
|
||||
anker string // Position in a file
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Get GitLocation from a path within the Git repository
|
||||
pub fn (mut gs GitStructure) gitlocation_from_path(path string) !GitLocation {
|
||||
mut full_path := pathlib.get(path)
|
||||
rel_path := full_path.path_relative(gs.coderoot.path)!
|
||||
|
||||
// Validate the relative path
|
||||
mut parts := rel_path.split('/')
|
||||
if parts.len < 3 {
|
||||
return error("git: path is not valid, should contain provider/account/repository: '${rel_path}'")
|
||||
}
|
||||
|
||||
// Extract provider, account, and repository name
|
||||
provider := parts[0]
|
||||
account := parts[1]
|
||||
name := parts[2]
|
||||
mut repo_path := if parts.len > 3 { parts[3..].join('/') } else { '' }
|
||||
|
||||
return GitLocation{
|
||||
provider: provider
|
||||
account: account
|
||||
name: name
|
||||
path: repo_path
|
||||
}
|
||||
}
|
||||
|
||||
// Get GitLocation from a URL
|
||||
pub fn (mut gs GitStructure) gitlocation_from_url(url string) !GitLocation {
|
||||
mut urllower := url.trim_space()
|
||||
if urllower == '' {
|
||||
return error('url cannot be empty')
|
||||
}
|
||||
|
||||
// Normalize URL
|
||||
urllower = normalize_url(urllower)
|
||||
|
||||
// Split URL into parts
|
||||
mut parts := urllower.split('/')
|
||||
mut anchor := ''
|
||||
mut path := ''
|
||||
mut branch_or_tag := ''
|
||||
|
||||
// Deal with path and anchor
|
||||
if parts.len > 4 {
|
||||
path = parts[4..].join('/')
|
||||
if path.contains('#') {
|
||||
parts2 := path.split('#')
|
||||
if parts2.len == 2 {
|
||||
path = parts2[0]
|
||||
anchor = parts2[1]
|
||||
} else {
|
||||
return error("git: url badly formatted, more than 1 '#' in ${url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract branch if available
|
||||
if parts.len > 3 {
|
||||
branch_or_tag = parts[3]
|
||||
parts[2] = parts[2].replace('.git', '')
|
||||
}
|
||||
|
||||
// Validate parts
|
||||
if parts.len < 3 {
|
||||
return error("git: url badly formatted, not enough parts in '${urllower}' \nparts:\n${parts}")
|
||||
}
|
||||
|
||||
// Extract provider, account, and name
|
||||
provider := if parts[0] == 'github.com' { 'github' } else { parts[0] }
|
||||
account := parts[1]
|
||||
name := parts[2].replace('.git', '')
|
||||
|
||||
return GitLocation{
|
||||
provider: provider
|
||||
account: account
|
||||
name: name
|
||||
branch_or_tag: branch_or_tag
|
||||
path: path
|
||||
anker: anchor
|
||||
}
|
||||
}
|
||||
|
||||
// Return a herolib path object on the filesystem pointing to the locator
|
||||
pub fn (mut l GitLocation) patho() !pathlib.Path {
|
||||
mut addrpath := pathlib.get_dir(path: '${l.provider}/${l.account}/${l.name}', create: false)!
|
||||
if l.path.len > 0 {
|
||||
return pathlib.get('${addrpath.path}/${l.path}')
|
||||
}
|
||||
return addrpath
|
||||
}
|
||||
|
||||
// Normalize the URL for consistent parsing
|
||||
fn normalize_url(url string) string {
|
||||
// Remove common URL prefixes
|
||||
if url.starts_with('ssh://') {
|
||||
return url[6..].replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
if url.starts_with('git@') {
|
||||
return url[4..].replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
if url.starts_with('http:/') {
|
||||
return url[6..].replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
if url.starts_with('https:/') {
|
||||
return url[7..].replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
if url.ends_with('.git') {
|
||||
return url[0..url.len - 4].replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
return url.replace(':', '/').replace('//', '/').trim('/')
|
||||
}
|
||||
196
lib/develop/gittools/gitstructure.v
Normal file
196
lib/develop/gittools/gitstructure.v
Normal file
@@ -0,0 +1,196 @@
|
||||
module gittools
|
||||
|
||||
import crypto.md5
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.clients.redisclient
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
pub struct GitStructureConfig {
|
||||
pub mut:
|
||||
coderoot string
|
||||
light bool = true // If true, clones only the last history for all branches (clone with only 1 level deep)
|
||||
log bool = true // If true, logs git commands/statements
|
||||
debug bool = true
|
||||
ssh_key_name string
|
||||
}
|
||||
|
||||
fn rediskey(coderoot string) string {
|
||||
key := md5.hexhash(coderoot)
|
||||
return 'git:config:${key}'
|
||||
}
|
||||
|
||||
// GitStructure holds information about repositories within a specific code root.
|
||||
// This structure keeps track of loaded repositories, their configurations, and their status.
|
||||
@[heap]
|
||||
pub struct GitStructure {
|
||||
pub mut:
|
||||
key string // Unique key representing the git structure (default is hash of $home/code).
|
||||
config GitStructureConfig // Configuration settings for the git structure.
|
||||
coderoot pathlib.Path // Root directory where repositories are located.
|
||||
repos map[string]&GitRepo // Map of repositories, keyed by their unique names.
|
||||
loaded bool // Indicates if the repositories have been loaded into memory.
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Loads all repository information from the filesystem and updates from remote if necessary.
|
||||
// Use the reload argument to force reloading from the disk.
|
||||
//
|
||||
// Args:
|
||||
// - args (StatusUpdateArgs): Arguments controlling the reload behavior.
|
||||
pub fn (mut gitstructure GitStructure) load(args StatusUpdateArgs) ! {
|
||||
mut processed_paths := []string{}
|
||||
// println("1")
|
||||
gitstructure.load_recursive(gitstructure.coderoot.path, mut processed_paths)!
|
||||
// println("2")
|
||||
|
||||
if args.reload {
|
||||
mut ths := []thread !{}
|
||||
redisclient.reset()! // make sure redis is empty, we don't want to reuse
|
||||
for _, mut repo_ in gitstructure.repos {
|
||||
mut myfunction := fn (mut repo GitRepo) ! {
|
||||
// println("reload repo ${repo.name} on ${repo.get_path()!}")
|
||||
redisclient.reset()!
|
||||
redisclient.checkempty()
|
||||
repo.status_update(reload: true)!
|
||||
}
|
||||
|
||||
ths << spawn myfunction(mut repo_)
|
||||
}
|
||||
console.print_debug('loaded all threads for git on ${gitstructure.coderoot.path}')
|
||||
|
||||
for th in ths {
|
||||
th.wait()!
|
||||
}
|
||||
// console.print_debug("threads finished")
|
||||
// exit(0)
|
||||
}
|
||||
|
||||
gitstructure.init()!
|
||||
}
|
||||
|
||||
// just some initialization mechanism
|
||||
pub fn (mut gitstructure GitStructure) init() ! {
|
||||
if gitstructure.config.debug {
|
||||
gitstructure.config.log = true
|
||||
}
|
||||
if gitstructure.repos.keys().len == 0 {
|
||||
gitstructure.load()!
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively loads repositories from the provided path, updating their statuses.
|
||||
//
|
||||
// Args:
|
||||
// - path (string): The path to search for repositories.
|
||||
// - processed_paths ([]string): List of already processed paths to avoid duplication.
|
||||
fn (mut gitstructure GitStructure) load_recursive(path string, mut processed_paths []string) ! {
|
||||
path_object := pathlib.get(path)
|
||||
relpath := path_object.path_relative(gitstructure.coderoot.path)!
|
||||
|
||||
// Limit the recursion depth to avoid deep directory traversal.
|
||||
if relpath.count('/') > 4 {
|
||||
return
|
||||
}
|
||||
|
||||
items := os.ls(path) or {
|
||||
return error('Cannot load gitstructure because directory not found: ${path}')
|
||||
}
|
||||
|
||||
for item in items {
|
||||
current_path := os.join_path(path, item)
|
||||
|
||||
if os.is_dir(current_path) {
|
||||
if os.exists(os.join_path(current_path, '.git')) {
|
||||
// Initialize the repository from the current path.
|
||||
mut repo := gitstructure.repo_init_from_path_(current_path)!
|
||||
// repo.status_update()!
|
||||
|
||||
key_ := repo.get_key()
|
||||
path_ := repo.get_path()!
|
||||
|
||||
if processed_paths.contains(key_) || processed_paths.contains(path_) {
|
||||
return error('Duplicate repository detected.\nPath: ${path_}\nKey: ${key_}')
|
||||
}
|
||||
|
||||
processed_paths << path_
|
||||
processed_paths << key_
|
||||
gitstructure.repos[key_] = &repo
|
||||
continue
|
||||
}
|
||||
|
||||
if item.starts_with('.') || item.starts_with('_') {
|
||||
continue
|
||||
}
|
||||
// Recursively search in subdirectories.
|
||||
gitstructure.load_recursive(current_path, mut processed_paths)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resets the cache for the current Git structure, removing cached data from Redis.
|
||||
pub fn (mut gitstructure GitStructure) cachereset() ! {
|
||||
mut redis := redis_get()
|
||||
keys := redis.keys('git:repos:${gitstructure.key}:**')!
|
||||
|
||||
for key in keys {
|
||||
redis.del(key)!
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RepoInitParams {
|
||||
ssh_key_name string // name of ssh key to be used in repo
|
||||
}
|
||||
|
||||
// Initializes a Git repository from a given path by locating the parent directory with `.git`.
|
||||
//
|
||||
// Args:
|
||||
// - path (string): Path to initialize the repository from.
|
||||
//
|
||||
// Returns:
|
||||
// - GitRepo: Reference to the initialized repository.
|
||||
//
|
||||
// Raises:
|
||||
// - Error: If `.git` is not found in the parent directories.
|
||||
fn (mut gitstructure GitStructure) repo_init_from_path_(path string, params RepoInitParams) !GitRepo {
|
||||
mypath := pathlib.get_dir(path: path, create: false)!
|
||||
mut parent_path := mypath.parent_find('.git') or {
|
||||
return error('Cannot find .git in parent directories starting from: ${path}')
|
||||
}
|
||||
|
||||
if parent_path.path == '' {
|
||||
return error('Cannot find .git in parent directories starting from: ${path}')
|
||||
}
|
||||
|
||||
// Retrieve GitLocation from the path.
|
||||
gl := gitstructure.gitlocation_from_path(mypath.path)!
|
||||
|
||||
// Initialize and return a GitRepo struct.
|
||||
mut r := GitRepo{
|
||||
gs: &gitstructure
|
||||
status_remote: GitRepoStatusRemote{}
|
||||
status_local: GitRepoStatusLocal{}
|
||||
config: GitRepoConfig{}
|
||||
provider: gl.provider
|
||||
account: gl.account
|
||||
name: gl.name
|
||||
deploysshkey: params.ssh_key_name
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// returns the git repository of the working directory by locating the parent directory with `.git`.
|
||||
//
|
||||
// Returns:
|
||||
// - GitRepo: Reference to the initialized repository.
|
||||
//
|
||||
// Raises:
|
||||
// - None: If `.git` is not found in the parent directories.
|
||||
pub fn (mut gitstructure GitStructure) get_working_repo() ?GitRepo {
|
||||
curdir := pathlib.get_wd()
|
||||
return gitstructure.repo_init_from_path_(curdir.path) or { return none }
|
||||
}
|
||||
274
lib/develop/gittools/gittools_do.v
Normal file
274
lib/develop/gittools/gittools_do.v
Normal file
@@ -0,0 +1,274 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.ui as gui
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.ui.generic
|
||||
import freeflowuniverse.herolib.clients.redisclient
|
||||
import os
|
||||
|
||||
pub const gitcmds = 'clone,commit,pull,push,delete,reload,list,edit,sourcetree,cd'
|
||||
|
||||
@[params]
|
||||
pub struct ReposActionsArgs {
|
||||
pub mut:
|
||||
cmd string // clone,commit,pull,push,delete,reload,list,edit,sourcetree
|
||||
filter string // if used will only show the repo's which have the filter string inside
|
||||
repo string
|
||||
account string
|
||||
provider string
|
||||
msg string
|
||||
url string
|
||||
branch string
|
||||
recursive bool
|
||||
pull bool
|
||||
script bool = true // run non interactive
|
||||
reset bool = true // means we will lose changes (only relevant for clone, pull)
|
||||
}
|
||||
|
||||
// do group actions on repo
|
||||
// args
|
||||
//```
|
||||
// cmd string // clone,commit,pull,push,delete,reload,list,edit,sourcetree,cd
|
||||
// filter string // if used will only show the repo's which have the filter string inside
|
||||
// repo string
|
||||
// account string
|
||||
// provider string
|
||||
// msg string
|
||||
// url string
|
||||
// pull bool
|
||||
// script bool = true // run non interactive
|
||||
// reset bool = true // means we will lose changes (only relevant for clone, pull)
|
||||
//```
|
||||
pub fn (mut gs GitStructure) do(args_ ReposActionsArgs) !string {
|
||||
mut args := args_
|
||||
console.print_debug('git do ${args.cmd}')
|
||||
|
||||
if args.repo == '' && args.account == '' && args.provider == '' && args.filter == '' {
|
||||
curdir := os.getwd()
|
||||
mut curdiro := pathlib.get_dir(path: curdir, create: false)!
|
||||
mut parentpath := curdiro.parent_find('.git') or { pathlib.Path{} }
|
||||
if parentpath.path != '' {
|
||||
r0 := gs.repo_init_from_path_(parentpath.path)!
|
||||
args.repo = r0.name
|
||||
args.account = r0.account
|
||||
args.provider = r0.provider
|
||||
}
|
||||
}
|
||||
|
||||
args.cmd = args.cmd.trim_space().to_lower()
|
||||
|
||||
mut ui := gui.new()!
|
||||
|
||||
if args.cmd == 'reload' {
|
||||
console.print_header(' - reload gitstructure ${gs.config.coderoot}')
|
||||
gs.load(reload: true)!
|
||||
return ''
|
||||
}
|
||||
|
||||
if args.cmd == 'list' {
|
||||
gs.repos_print(
|
||||
filter: args.filter
|
||||
name: args.repo
|
||||
account: args.account
|
||||
provider: args.provider
|
||||
)!
|
||||
return ''
|
||||
}
|
||||
|
||||
mut repos := gs.get_repos(
|
||||
filter: args.filter
|
||||
name: args.repo
|
||||
account: args.account
|
||||
provider: args.provider
|
||||
)!
|
||||
|
||||
if args.url.len > 0 {
|
||||
mut g := gs.get_repo(url: args.url)!
|
||||
g.load()!
|
||||
if args.cmd == 'cd' {
|
||||
return g.get_path()!
|
||||
}
|
||||
if args.reset {
|
||||
g.remove_changes()!
|
||||
}
|
||||
if args.cmd == 'pull' || args.pull {
|
||||
g.pull()!
|
||||
}
|
||||
if args.cmd == 'push' {
|
||||
if g.need_commit()! {
|
||||
if args.msg.len == 0 {
|
||||
return error('please specify message with -m ...')
|
||||
}
|
||||
g.commit(args.msg)!
|
||||
}
|
||||
g.push()!
|
||||
}
|
||||
if args.cmd == 'pull' || args.cmd == 'clone' || args.cmd == 'push' {
|
||||
gpath := g.get_path()!
|
||||
console.print_debug('git do ok, on path ${gpath}')
|
||||
return gpath
|
||||
}
|
||||
repos = [g]
|
||||
}
|
||||
|
||||
if args.cmd in 'sourcetree,edit'.split(',') {
|
||||
if repos.len == 0 {
|
||||
return error('please specify at least 1 repo for cmd:${args.cmd}')
|
||||
}
|
||||
if repos.len > 4 {
|
||||
return error('more than 4 repo found for cmd:${args.cmd}')
|
||||
}
|
||||
for r in repos {
|
||||
if args.cmd == 'edit' {
|
||||
r.open_vscode()!
|
||||
}
|
||||
if args.cmd == 'sourcetree' {
|
||||
r.sourcetree()!
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
if args.cmd in 'pull,push,commit,delete'.split(',') {
|
||||
gs.repos_print(
|
||||
filter: args.filter
|
||||
name: args.repo
|
||||
account: args.account
|
||||
provider: args.provider
|
||||
)!
|
||||
|
||||
mut need_commit := false
|
||||
mut need_pull := false
|
||||
mut need_push := false
|
||||
|
||||
if repos.len == 0 {
|
||||
console.print_header(' - nothing to do.')
|
||||
return ''
|
||||
}
|
||||
|
||||
// check on repos who needs what
|
||||
for mut g in repos {
|
||||
g.load()!
|
||||
// console.print_debug(st)
|
||||
need_commit = g.need_commit()! || need_commit
|
||||
if args.cmd == 'push' && need_commit {
|
||||
need_push = true
|
||||
}
|
||||
need_pull = args.cmd in 'pull,push'.split(',') // always do pull when push and pull
|
||||
need_push = args.cmd == 'push' && (g.need_push_or_pull()! || need_push)
|
||||
}
|
||||
|
||||
mut ok := false
|
||||
if need_commit || need_pull || need_push {
|
||||
mut out := '\n ** NEED TO '
|
||||
if need_commit {
|
||||
out += 'COMMIT '
|
||||
}
|
||||
if need_pull {
|
||||
out += 'PULL '
|
||||
}
|
||||
if need_push {
|
||||
out += 'PUSH '
|
||||
}
|
||||
if args.reset {
|
||||
out += ' (changes will be lost!)'
|
||||
}
|
||||
console.print_debug(out + ' ** \n')
|
||||
if args.script {
|
||||
ok = true
|
||||
} else {
|
||||
ok = ui.ask_yesno(question: 'Is above ok?')!
|
||||
}
|
||||
}
|
||||
if args.cmd == 'delete' {
|
||||
if args.script {
|
||||
ok = true
|
||||
} else {
|
||||
ok = ui.ask_yesno(question: 'Is it ok to delete above repos? (DANGEROUS)')!
|
||||
}
|
||||
}
|
||||
|
||||
if ok == false {
|
||||
return error('cannot continue with action, you asked me to stop.\n${args}')
|
||||
}
|
||||
|
||||
// mut changed := false
|
||||
|
||||
mut ths := []thread !bool{}
|
||||
for mut g in repos {
|
||||
ths << spawn fn (mut g GitRepo, args ReposActionsArgs, need_commit bool, need_push bool, shared ui generic.UserInterface) !bool {
|
||||
redisclient.reset()!
|
||||
redisclient.checkempty()
|
||||
mut has_changed := false
|
||||
need_commit_repo := (g.need_commit()! || need_commit)
|
||||
&& args.cmd in 'commit,pull,push'.split(',')
|
||||
need_pull_repo := args.cmd in 'pull,push'.split(',') // always do pull when push and pull
|
||||
need_push_repo := args.cmd in 'push'.split(',')
|
||||
&& (g.need_push_or_pull()! || need_push)
|
||||
// console.print_debug(" --- git_do ${g.addr.name} ${st.need_commit} ${st.need_pull} ${st.need_push}")
|
||||
|
||||
if need_commit_repo {
|
||||
mut msg := args.msg
|
||||
if msg.len == 0 {
|
||||
if args.script {
|
||||
return error('message needs to be specified for commit.')
|
||||
}
|
||||
|
||||
lock ui {
|
||||
msg = ui.ask_question(
|
||||
question: 'commit message for repo: ${g.account}/${g.name} '
|
||||
)!
|
||||
}
|
||||
}
|
||||
console.print_header(' - commit ${g.account}/${g.name}')
|
||||
g.commit(msg)!
|
||||
has_changed = true
|
||||
}
|
||||
if need_pull_repo {
|
||||
if args.reset {
|
||||
console.print_header(' - remove changes ${g.account}/${g.name}')
|
||||
g.remove_changes()!
|
||||
}
|
||||
console.print_header(' - pull ${g.account}/${g.name}')
|
||||
g.pull()!
|
||||
has_changed = true
|
||||
}
|
||||
if need_push_repo {
|
||||
console.print_header(' - push ${g.account}/${g.name}')
|
||||
g.push()!
|
||||
has_changed = true
|
||||
}
|
||||
if args.cmd == 'delete' {
|
||||
g.delete()!
|
||||
has_changed = true
|
||||
}
|
||||
|
||||
return has_changed
|
||||
}(mut g, args, need_commit, need_push, shared &ui)
|
||||
}
|
||||
|
||||
for th in ths {
|
||||
has_changed := th.wait()!
|
||||
if has_changed {
|
||||
// console.clear()
|
||||
console.print_header('\nCompleted required actions.\n')
|
||||
|
||||
gs.repos_print(
|
||||
filter: args.filter
|
||||
name: args.repo
|
||||
account: args.account
|
||||
provider: args.provider
|
||||
)!
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
// end for the commit, pull, push, delete
|
||||
|
||||
$if debug {
|
||||
print_backtrace()
|
||||
}
|
||||
return error('did not find cmd: ${args.cmd}')
|
||||
}
|
||||
145
lib/develop/gittools/repos_get.v
Normal file
145
lib/develop/gittools/repos_get.v
Normal file
@@ -0,0 +1,145 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.clients.redisclient
|
||||
import time
|
||||
|
||||
// ReposGetArgs defines arguments to retrieve repositories from the git structure.
|
||||
// It includes filters by name, account, provider, and an option to clone a missing repo.
|
||||
@[params]
|
||||
pub struct ReposGetArgs {
|
||||
pub mut:
|
||||
filter string // Optional filter for repository names
|
||||
name string // Specific repository name to retrieve.
|
||||
account string // Git account associated with the repository.
|
||||
provider string // Git provider (e.g., GitHub).
|
||||
pull bool // Pull the last changes.
|
||||
reset bool // Reset the changes.
|
||||
reload bool // Reload the repo into redis cache
|
||||
url string // Repository URL
|
||||
}
|
||||
|
||||
// Retrieves a list of repositories from the git structure that match the provided arguments.
|
||||
// if pull will force a pull, if it can't will be error, if reset will remove the changes
|
||||
//
|
||||
// Args:
|
||||
//```
|
||||
// ReposGetArgs {
|
||||
// filter string // Optional filter for repository names
|
||||
// name string // Specific repository name to retrieve.
|
||||
// account string // Git account associated with the repository.
|
||||
// provider string // Git provider (e.g., GitHub).
|
||||
// pull bool // Pull the last changes.
|
||||
// reset bool // Reset the changes.
|
||||
// reload bool // Reload the repo into redis cache
|
||||
// url string // Repository URL, used if cloning is needed.
|
||||
//```
|
||||
// Returns:
|
||||
// - []&GitRepo: A list of repository references that match the criteria.
|
||||
pub fn (mut gitstructure GitStructure) get_repos(args_ ReposGetArgs) ![]&GitRepo {
|
||||
mut args := args_
|
||||
|
||||
mut res := []&GitRepo{}
|
||||
|
||||
for _, repo in gitstructure.repos {
|
||||
relpath := repo.get_relative_path()!
|
||||
|
||||
if args.filter != '' && relpath.contains(args.filter) {
|
||||
res << repo
|
||||
continue
|
||||
}
|
||||
|
||||
if args.url.len > 0 {
|
||||
// if being mathed from url load repo info
|
||||
git_location := gitstructure.gitlocation_from_url(args.url)!
|
||||
args.account = git_location.account
|
||||
args.provider = git_location.provider
|
||||
args.name = git_location.name
|
||||
}
|
||||
if repo_match_check(repo, args) {
|
||||
res << repo
|
||||
}
|
||||
}
|
||||
|
||||
// operate per repo on thread based on args
|
||||
mut ths := []thread !{}
|
||||
for mut repo in res {
|
||||
// check redis cache outside, in threads is problematic
|
||||
repo.cache_get() or { return error('failed to get repo cache ${err}') }
|
||||
if time.since(time.unix(repo.last_load)) > 24 * time.hour {
|
||||
args.reload = true
|
||||
}
|
||||
ths << spawn fn (mut repo GitRepo, args ReposGetArgs) ! {
|
||||
redisclient.reset()!
|
||||
redisclient.checkempty()
|
||||
if args.pull {
|
||||
repo.pull()!
|
||||
} else if args.reset {
|
||||
repo.reset()!
|
||||
} else if args.reload {
|
||||
repo.load()!
|
||||
}
|
||||
}(mut repo, args)
|
||||
}
|
||||
|
||||
for th in ths {
|
||||
th.wait()!
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Retrieves a single repository based on the provided arguments.
|
||||
// if pull will force a pull, if it can't will be error, if reset will remove the changes
|
||||
// If the repository does not exist, it will clone it
|
||||
//
|
||||
// Args:
|
||||
//```
|
||||
// ReposGetArgs {
|
||||
// filter string // Optional filter for repository names
|
||||
// name string // Specific repository name to retrieve.
|
||||
// account string // Git account associated with the repository.
|
||||
// provider string // Git provider (e.g., GitHub).
|
||||
// pull bool // Pull the last changes.
|
||||
// reset bool // Reset the changes.
|
||||
// reload bool // Reload the repo into redis cache
|
||||
// url string // Repository URL, used if cloning is needed.
|
||||
//```
|
||||
//
|
||||
// Returns:
|
||||
// - &GitRepo: Reference to the retrieved or cloned repository.
|
||||
//
|
||||
// Raises:
|
||||
// - Error: If multiple repositories are found with similar names or if cloning fails.
|
||||
pub fn (mut gitstructure GitStructure) get_repo(args_ ReposGetArgs) !&GitRepo {
|
||||
mut args := args_
|
||||
|
||||
repositories := gitstructure.get_repos(args)!
|
||||
|
||||
if repositories.len == 0 {
|
||||
if args.url.len == 0 {
|
||||
return error('Cannot clone the repository, no URL provided: ${args.url}')
|
||||
}
|
||||
return gitstructure.clone(url: args.url)!
|
||||
}
|
||||
|
||||
if repositories.len > 1 {
|
||||
repos := repositories.map('- ${it} ${it.account}.${it.name}').join_lines()
|
||||
return error('Found more than one repository for \n${args}\n${repos}')
|
||||
}
|
||||
|
||||
return repositories[0]
|
||||
}
|
||||
|
||||
// Helper function to check if a repository matches the criteria (name, account, provider).
|
||||
//
|
||||
// Args:
|
||||
// - repo (GitRepo): The repository to check.
|
||||
// - args (ReposGetArgs): The criteria to match against.
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the repository matches, false otherwise.
|
||||
fn repo_match_check(repo GitRepo, args ReposGetArgs) bool {
|
||||
return (args.name.len == 0 || repo.name == args.name)
|
||||
&& (args.account.len == 0 || repo.account == args.account)
|
||||
&& (args.provider.len == 0 || repo.provider == args.provider)
|
||||
}
|
||||
62
lib/develop/gittools/repos_print.v
Normal file
62
lib/develop/gittools/repos_print.v
Normal file
@@ -0,0 +1,62 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
// Check and return the status of a repository (whether it needs a commit, pull, or push)
|
||||
fn get_repo_status(gr GitRepo) !string {
|
||||
mut repo := gr
|
||||
mut statuses := []string{}
|
||||
|
||||
if repo.need_commit()! {
|
||||
statuses << 'COMMIT'
|
||||
}
|
||||
if repo.need_push_or_pull()! {
|
||||
statuses << 'PULL'
|
||||
statuses << 'PUSH'
|
||||
}
|
||||
return statuses.join(', ')
|
||||
}
|
||||
|
||||
// Format repository information for display, including path, tag/branch, and status
|
||||
fn format_repo_info(repo GitRepo) ![]string {
|
||||
status := get_repo_status(repo)!
|
||||
|
||||
tag_or_branch := if repo.status_local.tag.len > 0 {
|
||||
'[[${repo.status_local.tag}]]' // Display tag if it exists
|
||||
} else {
|
||||
'[${repo.status_local.branch}]' // Otherwise, display branch
|
||||
}
|
||||
|
||||
relative_path := repo.get_relative_path()!
|
||||
return [' - ${relative_path}', tag_or_branch, status]
|
||||
}
|
||||
|
||||
// Print repositories based on the provided criteria, showing their statuses
|
||||
pub fn (mut gitstructure GitStructure) repos_print(args ReposGetArgs) ! {
|
||||
console.print_debug('#### Overview of repositories:')
|
||||
console.print_debug('')
|
||||
|
||||
mut repo_data := [][]string{}
|
||||
|
||||
// Collect repository information based on the provided criteria
|
||||
for _, repo in gitstructure.get_repos(args)! {
|
||||
repo_data << format_repo_info(repo)!
|
||||
}
|
||||
|
||||
// Clear the console and start printing the formatted repository information
|
||||
console.clear()
|
||||
console.print_lf(1)
|
||||
|
||||
// Display header with optional argument filtering information
|
||||
header := if args.str().len > 0 {
|
||||
'Repositories: ${gitstructure.config.coderoot} [${args.str()}]'
|
||||
} else {
|
||||
'Repositories: ${gitstructure.config.coderoot}'
|
||||
}
|
||||
console.print_header(header)
|
||||
|
||||
// Print the repository information in a formatted array
|
||||
console.print_lf(1)
|
||||
console.print_array(repo_data, ' ', true) // true -> aligned for better readability
|
||||
console.print_lf(5)
|
||||
}
|
||||
313
lib/develop/gittools/repository.v
Normal file
313
lib/develop/gittools/repository.v
Normal file
@@ -0,0 +1,313 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.osal
|
||||
import os
|
||||
import time
|
||||
|
||||
// GitRepo holds information about a single Git repository.
|
||||
@[heap]
|
||||
pub struct GitRepo {
|
||||
pub mut:
|
||||
gs &GitStructure @[skip; str: skip] // Reference to the parent GitStructure
|
||||
provider string // e.g., github.com, shortened to 'github'
|
||||
account string // Git account name
|
||||
name string // Repository name
|
||||
status_remote GitRepoStatusRemote // Remote repository status
|
||||
status_local GitRepoStatusLocal // Local repository status
|
||||
status_wanted GitRepoStatusWanted // what is the status we want?
|
||||
config GitRepoConfig // Repository-specific configuration
|
||||
last_load int // Epoch timestamp of the last load from reality
|
||||
deploysshkey string // to use with git
|
||||
}
|
||||
|
||||
// this is the status we want, we need to work towards off
|
||||
pub struct GitRepoStatusWanted {
|
||||
pub mut:
|
||||
branch string
|
||||
tag string
|
||||
url string // Remote repository URL, is basically the one we want
|
||||
readonly bool // if read only then we cannot push or commit, all changes will be reset when doing pull
|
||||
}
|
||||
|
||||
// GitRepoStatusRemote holds remote status information for a repository.
|
||||
pub struct GitRepoStatusRemote {
|
||||
pub mut:
|
||||
ref_default string // is the default branch hash
|
||||
branches map[string]string // Branch name -> commit hash
|
||||
tags map[string]string // Tag name -> commit hash
|
||||
}
|
||||
|
||||
// GitRepoStatusLocal holds local status information for a repository.
|
||||
pub struct GitRepoStatusLocal {
|
||||
pub mut:
|
||||
branches map[string]string // Branch name -> commit hash
|
||||
branch string // the current branch
|
||||
tag string // If the local branch is not set, the tag may be set
|
||||
}
|
||||
|
||||
// GitRepoConfig holds repository-specific configuration options.
|
||||
pub struct GitRepoConfig {
|
||||
pub mut:
|
||||
remote_check_period int = 3600 * 24 * 3 // Seconds to wait between remote checks (0 = check every time), default 3 days
|
||||
}
|
||||
|
||||
// just some initialization mechanism
|
||||
pub fn (mut gitstructure GitStructure) repo_new_from_gitlocation(git_location GitLocation) !&GitRepo {
|
||||
mut repo := GitRepo{
|
||||
provider: git_location.provider
|
||||
name: git_location.name
|
||||
account: git_location.account
|
||||
gs: &gitstructure
|
||||
status_remote: GitRepoStatusRemote{}
|
||||
status_local: GitRepoStatusLocal{}
|
||||
status_wanted: GitRepoStatusWanted{}
|
||||
}
|
||||
gitstructure.repos[repo.name] = &repo
|
||||
|
||||
return &repo
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Commit the staged changes with the provided commit message.
|
||||
pub fn (mut repo GitRepo) commit(msg string) ! {
|
||||
repo.status_update()!
|
||||
if repo.need_commit()! {
|
||||
if msg == '' {
|
||||
return error('Commit message is empty.')
|
||||
}
|
||||
repo_path := repo.get_path()!
|
||||
repo.exec('git add . -A') or {
|
||||
return error('Cannot add to repo: ${repo_path}. Error: ${err}')
|
||||
}
|
||||
repo.exec('git commit -m "${msg}"') or {
|
||||
return error('Cannot commit repo: ${repo_path}. Error: ${err}')
|
||||
}
|
||||
console.print_green('Changes committed successfully.')
|
||||
} else {
|
||||
console.print_debug('No changes to commit.')
|
||||
}
|
||||
repo.load()!
|
||||
}
|
||||
|
||||
// Push local changes to the remote repository.
|
||||
pub fn (mut repo GitRepo) push() ! {
|
||||
repo.status_update()!
|
||||
if repo.need_push_or_pull()! {
|
||||
url := repo.get_repo_url()!
|
||||
console.print_header('Pushing changes to ${url}')
|
||||
// We may need to push the locally created branches
|
||||
repo.exec('git push --set-upstream origin ${repo.status_local.branch}')!
|
||||
console.print_green('Changes pushed successfully.')
|
||||
repo.load()!
|
||||
} else {
|
||||
console.print_header('Everything is up to date.')
|
||||
repo.load()!
|
||||
}
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PullCheckoutArgs {
|
||||
pub mut:
|
||||
submodules bool // if we want to pull for submodules
|
||||
}
|
||||
|
||||
// Pull remote content into the repository.
|
||||
pub fn (mut repo GitRepo) pull(args_ PullCheckoutArgs) ! {
|
||||
repo.status_update()!
|
||||
if repo.need_checkout() {
|
||||
repo.checkout()!
|
||||
}
|
||||
|
||||
repo.exec('git pull') or {
|
||||
return error('Cannot pull repo: ${repo.get_path()!}. Error: ${err}')
|
||||
}
|
||||
|
||||
if args_.submodules {
|
||||
repo.update_submodules()!
|
||||
}
|
||||
|
||||
repo.load()!
|
||||
console.print_green('Changes pulled successfully.')
|
||||
}
|
||||
|
||||
// Checkout a branch in the repository.
|
||||
pub fn (mut repo GitRepo) checkout() ! {
|
||||
repo.status_update()!
|
||||
if repo.status_wanted.readonly {
|
||||
repo.reset()!
|
||||
}
|
||||
if repo.need_commit()! {
|
||||
return error('Cannot checkout branch due to uncommitted changes in ${repo.get_path()!}.')
|
||||
}
|
||||
if repo.status_wanted.tag.len > 0 {
|
||||
repo.exec('git checkout tags/${repo.status_wanted.tag}')!
|
||||
}
|
||||
if repo.status_wanted.branch.len > 0 {
|
||||
repo.exec('git checkout ${repo.status_wanted.tag}')!
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new branch in the repository.
|
||||
pub fn (mut repo GitRepo) branch_create(branchname string) ! {
|
||||
repo.exec('git branch -c ${branchname}') or {
|
||||
return error('Cannot Create branch: ${repo.get_path()!} to ${branchname}\nError: ${err}')
|
||||
}
|
||||
console.print_green('Branch ${branchname} created successfully.')
|
||||
}
|
||||
|
||||
pub fn (mut repo GitRepo) branch_switch(branchname string) ! {
|
||||
repo.exec('git switch ${branchname}') or {
|
||||
return error('Cannot switch branch: ${repo.get_path()!} to ${branchname}\nError: ${err}')
|
||||
}
|
||||
console.print_green('Branch ${branchname} switched successfully.')
|
||||
repo.status_local.branch = branchname
|
||||
repo.status_local.tag = ''
|
||||
repo.pull()!
|
||||
}
|
||||
|
||||
// Create a new branch in the repository.
|
||||
pub fn (mut repo GitRepo) tag_create(tagname string) ! {
|
||||
repo_path := repo.get_path()!
|
||||
repo.exec('git tag ${tagname}') or {
|
||||
return error('Cannot create tag: ${repo_path}. Error: ${err}')
|
||||
}
|
||||
console.print_green('Tag ${tagname} created successfully.')
|
||||
}
|
||||
|
||||
pub fn (mut repo GitRepo) tag_switch(tagname string) ! {
|
||||
repo.exec('git checkout ${tagname}') or {
|
||||
return error('Cannot switch to tag: ${tagname}. Error: ${err}')
|
||||
}
|
||||
console.print_green('Tag ${tagname} activated.')
|
||||
repo.status_local.branch = ''
|
||||
repo.status_local.tag = tagname
|
||||
repo.pull()!
|
||||
}
|
||||
|
||||
// Create a new branch in the repository.
|
||||
pub fn (mut repo GitRepo) tag_exists(tag string) !bool {
|
||||
repo.exec('git show ${tag}') or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
// Deletes the Git repository
|
||||
pub fn (mut repo GitRepo) delete() ! {
|
||||
repo_path := repo.get_path()!
|
||||
repo.cache_delete()!
|
||||
osal.rm(repo_path)!
|
||||
repo.load()!
|
||||
}
|
||||
|
||||
// Create GitLocation from the path within the Git repository
|
||||
pub fn (mut gs GitRepo) gitlocation_from_path(path string) !GitLocation {
|
||||
if path.starts_with('/') || path.starts_with('~') {
|
||||
return error('Path must be relative, cannot start with / or ~')
|
||||
}
|
||||
|
||||
// TODO: check that path is inside gitrepo
|
||||
// TODO: get relative path in relation to root of gitrepo
|
||||
|
||||
mut git_path := gs.patho()!
|
||||
if !os.exists(git_path.path) {
|
||||
return error('Path does not exist inside the repository: ${git_path.path}')
|
||||
}
|
||||
|
||||
mut branch_or_tag := gs.status_wanted.branch
|
||||
if gs.status_wanted.tag.len > 0 {
|
||||
branch_or_tag = gs.status_wanted.tag
|
||||
}
|
||||
|
||||
return GitLocation{
|
||||
provider: gs.provider
|
||||
account: gs.account
|
||||
name: gs.name
|
||||
branch_or_tag: branch_or_tag
|
||||
path: path // relative path in relation to git repo
|
||||
}
|
||||
}
|
||||
|
||||
// Check if repo path exists and validate fields
|
||||
pub fn (mut repo GitRepo) init() ! {
|
||||
path_string := repo.get_path()!
|
||||
if repo.gs.coderoot.path == '' {
|
||||
return error('Coderoot cannot be empty')
|
||||
}
|
||||
if repo.provider == '' {
|
||||
return error('Provider cannot be empty')
|
||||
}
|
||||
if repo.account == '' {
|
||||
return error('Account cannot be empty')
|
||||
}
|
||||
if repo.name == '' {
|
||||
return error('Name cannot be empty')
|
||||
}
|
||||
|
||||
if !os.exists(path_string) {
|
||||
return error('Path does not exist: ${path_string}')
|
||||
}
|
||||
|
||||
// TODO: check deploy key has been set in repo
|
||||
// if not do git config core.sshCommand "ssh -i /path/to/deploy_key"
|
||||
if repo.deploysshkey.len > 0 {
|
||||
repo.set_sshkey(repo.deploysshkey)!
|
||||
}
|
||||
// TODO: check tag or branch set on wanted, and not both
|
||||
}
|
||||
|
||||
// Set the ssh key on the repo
|
||||
fn (mut repo GitRepo) set_sshkey(key_name string) ! {
|
||||
// will use this dir to find and set key from
|
||||
ssh_dir := os.join_path(os.home_dir(), '.ssh')
|
||||
key := osal.get_ssh_key(key_name, directory: ssh_dir) or {
|
||||
return error('SSH Key with name ${key_name} not found.')
|
||||
}
|
||||
|
||||
private_key := key.private_key_path()!
|
||||
_ := 'git config core.sshcommand "ssh -i ~/.ssh/${private_key.path}"'
|
||||
repo.deploysshkey = key_name
|
||||
}
|
||||
|
||||
// Removes all changes from the repo; be cautious
|
||||
pub fn (mut repo GitRepo) remove_changes() ! {
|
||||
repo.status_update()!
|
||||
if repo.has_changes()! {
|
||||
console.print_header('Removing changes in ${repo.get_path()!}')
|
||||
repo.exec('git reset HEAD --hard && git clean -xfd') or {
|
||||
return error("can't remove changes on repo: ${repo.get_path()!}.\n${err}")
|
||||
// TODO: we can do this fall back later
|
||||
// console.print_header('Could not remove changes; will re-clone ${repo.get_path()!}')
|
||||
// mut p := repo.patho()!
|
||||
// p.delete()! // remove path, this will re-clone the full thing
|
||||
// repo.load_from_url()!
|
||||
}
|
||||
repo.load()!
|
||||
}
|
||||
}
|
||||
|
||||
// alias for remove changes
|
||||
pub fn (mut repo GitRepo) reset() ! {
|
||||
return repo.remove_changes()
|
||||
}
|
||||
|
||||
// Update submodules
|
||||
fn (mut repo GitRepo) update_submodules() ! {
|
||||
repo.exec('git submodule update --init --recursive') or {
|
||||
return error('Cannot update submodules for repo: ${repo.get_path()!}. Error: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
fn (repo GitRepo) exec(cmd_ string) !string {
|
||||
repo_path := repo.get_path()!
|
||||
cmd := 'cd ${repo_path} && ${cmd_}'
|
||||
if repo.gs.config.log {
|
||||
console.print_green(cmd)
|
||||
}
|
||||
r := os.execute(cmd)
|
||||
if r.exit_code != 0 {
|
||||
return error('Repo failed to exec cmd: ${cmd}\n${r.output})')
|
||||
}
|
||||
return r.output
|
||||
}
|
||||
42
lib/develop/gittools/repository_cache.v
Normal file
42
lib/develop/gittools/repository_cache.v
Normal file
@@ -0,0 +1,42 @@
|
||||
module gittools
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.clients.redisclient
|
||||
|
||||
fn redis_get() redisclient.Redis {
|
||||
mut redis_client := redisclient.core_get() or { panic(err) }
|
||||
return redis_client
|
||||
}
|
||||
|
||||
// Save repo to redis cache
|
||||
fn (mut repo GitRepo) cache_set() ! {
|
||||
mut redis_client := redis_get()
|
||||
repo_json := json.encode(repo)
|
||||
cache_key := repo.get_cache_key()
|
||||
redis_client.set(cache_key, repo_json)!
|
||||
}
|
||||
|
||||
// Get repo from redis cache
|
||||
fn (mut repo GitRepo) cache_get() ! {
|
||||
mut repo_json := ''
|
||||
mut redis_client := redis_get()
|
||||
cache_key := repo.get_cache_key()
|
||||
repo_json = redis_client.get(cache_key) or {
|
||||
return error('Failed to get redis key ${cache_key}\n${err}')
|
||||
}
|
||||
|
||||
if repo_json.len > 0 {
|
||||
mut cached := json.decode(GitRepo, repo_json)!
|
||||
cached.gs = repo.gs
|
||||
repo = cached
|
||||
}
|
||||
}
|
||||
|
||||
// Remove cache
|
||||
fn (repo GitRepo) cache_delete() ! {
|
||||
mut redis_client := redis_get()
|
||||
cache_key := repo.get_cache_key()
|
||||
redis_client.del(cache_key) or { return error('Cannot delete the repo cache due to: ${err}') }
|
||||
// TODO: report v bug, function should work without return as well
|
||||
return
|
||||
}
|
||||
47
lib/develop/gittools/repository_clone.v
Normal file
47
lib/develop/gittools/repository_clone.v
Normal file
@@ -0,0 +1,47 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.osal
|
||||
import os
|
||||
|
||||
@[params]
|
||||
pub struct GitCloneArgs {
|
||||
pub mut:
|
||||
url string
|
||||
sshkey string
|
||||
}
|
||||
|
||||
// Clones a new repository into the git structure based on the provided arguments.
|
||||
pub fn (mut gitstructure GitStructure) clone(args GitCloneArgs) !&GitRepo {
|
||||
if args.url.len == 0 {
|
||||
return error('url needs to be specified when doing a clone.')
|
||||
}
|
||||
console.print_header('Git clone from the URL: ${args.url}.')
|
||||
git_location := gitstructure.gitlocation_from_url(args.url)!
|
||||
|
||||
mut repo := gitstructure.repo_new_from_gitlocation(git_location)!
|
||||
repo.status_wanted.url = args.url
|
||||
|
||||
if args.sshkey.len > 0 {
|
||||
repo.set_sshkey(args.sshkey)!
|
||||
}
|
||||
|
||||
parent_dir := repo.get_parent_dir(create: true)!
|
||||
|
||||
mut extra := ''
|
||||
if gitstructure.config.light {
|
||||
extra = '--depth 1 --no-single-branch '
|
||||
}
|
||||
|
||||
cmd := 'cd ${parent_dir} && git clone ${extra} ${repo.get_repo_url()!} ${repo.name}'
|
||||
result := os.execute(cmd)
|
||||
if result.exit_code != 0 {
|
||||
return error('Cannot clone the repository due to: \n${result.output}')
|
||||
}
|
||||
|
||||
repo.load()!
|
||||
|
||||
console.print_green("The repository '${repo.name}' cloned into ${parent_dir}.")
|
||||
|
||||
return repo
|
||||
}
|
||||
110
lib/develop/gittools/repository_info.v
Normal file
110
lib/develop/gittools/repository_info.v
Normal file
@@ -0,0 +1,110 @@
|
||||
module gittools
|
||||
|
||||
// FUNCITONS TO GET INFO FROM REALITY
|
||||
|
||||
// Retrieves a list of unstaged changes in the repository.
|
||||
//
|
||||
// This function returns a list of files that are modified or untracked.
|
||||
//
|
||||
// Returns:
|
||||
// - An array of strings representing file paths of unstaged changes.
|
||||
// - Throws an error if the command execution fails.
|
||||
pub fn (repo GitRepo) get_changes_unstaged() ![]string {
|
||||
unstaged_result := repo.exec('git ls-files --other --modified --exclude-standard') or {
|
||||
return error('Failed to check for unstaged changes: ${err}')
|
||||
}
|
||||
|
||||
// Filter out any empty lines from the result.
|
||||
return unstaged_result.split('\n').filter(it.len > 0)
|
||||
}
|
||||
|
||||
// Retrieves a list of staged changes in the repository.
|
||||
//
|
||||
// This function returns a list of files that are staged and ready to be committed.
|
||||
//
|
||||
// Returns:
|
||||
// - An array of strings representing file paths of staged changes.
|
||||
// - Throws an error if the command execution fails.
|
||||
pub fn (repo GitRepo) get_changes_staged() ![]string {
|
||||
staged_result := repo.exec('git diff --name-only --staged') or {
|
||||
return error('Failed to check for staged changes: ${err}')
|
||||
}
|
||||
// Filter out any empty lines from the result.
|
||||
return staged_result.split('\n').filter(it.len > 0)
|
||||
}
|
||||
|
||||
// Check if there are any unstaged or untracked changes in the repository.
|
||||
pub fn (mut repo GitRepo) has_changes() !bool {
|
||||
repo.status_update()!
|
||||
r0 := repo.get_changes_unstaged()!
|
||||
r1 := repo.get_changes_staged()!
|
||||
if r0.len + r1.len > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if there are staged changes to commit.
|
||||
pub fn (mut repo GitRepo) need_commit() !bool {
|
||||
return repo.has_changes()!
|
||||
}
|
||||
|
||||
// Check if the repository has changes that need to be pushed.
|
||||
pub fn (mut repo GitRepo) need_push_or_pull() !bool {
|
||||
repo.status_update()!
|
||||
last_remote_commit := repo.get_last_remote_commit() or {
|
||||
return error('Failed to get last remote commit: ${err}')
|
||||
}
|
||||
last_local_commit := repo.get_last_local_commit() or {
|
||||
return error('Failed to get last local commit: ${err}')
|
||||
}
|
||||
println('last_local_commit: ${last_local_commit}')
|
||||
println('last_remote_commit: ${last_remote_commit}')
|
||||
return last_local_commit != last_remote_commit
|
||||
}
|
||||
|
||||
// Determine if the repository needs to checkout to a different branch or tag
|
||||
fn (mut repo GitRepo) need_checkout() bool {
|
||||
if repo.status_wanted.branch.len > 0 {
|
||||
if repo.status_wanted.branch != repo.status_local.branch {
|
||||
return true
|
||||
}
|
||||
} else if repo.status_wanted.tag.len > 0 {
|
||||
if repo.status_wanted.tag != repo.status_local.tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// it could be empty since the status_wanted are optional.
|
||||
// else{
|
||||
// panic("bug, should never be empty ${repo.status_wanted.branch}, ${repo.status_local.branch}")
|
||||
// }
|
||||
return false
|
||||
}
|
||||
|
||||
fn (mut repo GitRepo) get_remote_default_branchname() !string {
|
||||
if repo.status_remote.ref_default.len == 0 {
|
||||
return error('ref_default cannot be empty for ${repo}')
|
||||
}
|
||||
|
||||
return repo.status_remote.branches[repo.status_remote.ref_default] or {
|
||||
return error("can't find ref_default in branches for ${repo}")
|
||||
}
|
||||
}
|
||||
|
||||
// is always the commit for the branch as known remotely, if not known will return ""
|
||||
pub fn (repo GitRepo) get_last_remote_commit() !string {
|
||||
if repo.status_local.branch in repo.status_remote.branches {
|
||||
return repo.status_local.branches[repo.status_local.branch]
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// get commit for branch, will return '' if local branch doesn't exist remotely
|
||||
pub fn (repo GitRepo) get_last_local_commit() !string {
|
||||
if repo.status_local.branch in repo.status_local.branches {
|
||||
return repo.status_local.branches[repo.status_local.branch]
|
||||
}
|
||||
|
||||
return error("can't find branch: ${repo.status_local.branch} in local branches:\n${repo.status_local.branches}")
|
||||
}
|
||||
91
lib/develop/gittools/repository_load.v
Normal file
91
lib/develop/gittools/repository_load.v
Normal file
@@ -0,0 +1,91 @@
|
||||
module gittools
|
||||
|
||||
import time
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
@[params]
|
||||
pub struct StatusUpdateArgs {
|
||||
reload bool
|
||||
ssh_key_name string // name of ssh key to be used when loading
|
||||
}
|
||||
|
||||
pub fn (mut repo GitRepo) status_update(args StatusUpdateArgs) ! {
|
||||
// Check current time vs last check, if needed (check period) then load
|
||||
// println("${repo.name} ++")
|
||||
repo.cache_get()! // Ensure we have the situation from redis
|
||||
repo.init()!
|
||||
current_time := int(time.now().unix())
|
||||
if args.reload || repo.last_load == 0
|
||||
|| current_time - repo.last_load >= repo.config.remote_check_period {
|
||||
console.print_debug('${repo.name} ${current_time}-${repo.last_load}: ${repo.config.remote_check_period} +++')
|
||||
// if true{exit(0)}
|
||||
repo.load()!
|
||||
// println("${repo.name} ++++")
|
||||
}
|
||||
}
|
||||
|
||||
// Load repo information
|
||||
// Does not check cache, it is the callers responsibility to check cache and load accordingly.
|
||||
fn (mut repo GitRepo) load() ! {
|
||||
console.print_debug('load ${repo.get_key()}')
|
||||
repo.init()!
|
||||
repo.exec('git fetch --all') or {
|
||||
return error('Cannot fetch repo: ${repo.get_path()!}. Error: ${err}')
|
||||
}
|
||||
repo.load_branches()!
|
||||
repo.load_tags()!
|
||||
repo.last_load = int(time.now().unix())
|
||||
repo.cache_set()!
|
||||
}
|
||||
|
||||
// Helper to load remote tags
|
||||
fn (mut repo GitRepo) load_branches() ! {
|
||||
tags_result := repo.exec("git for-each-ref --format='%(objectname) %(refname:short)' refs/heads refs/remotes/origin")!
|
||||
for line in tags_result.split('\n') {
|
||||
if line.trim_space() != '' {
|
||||
parts := line.split(' ')
|
||||
if parts.len == 2 {
|
||||
commit_hash := parts[0].trim_space()
|
||||
mut name := parts[1].trim_space()
|
||||
if name.contains('_archive') {
|
||||
continue
|
||||
} else if name == 'origin' {
|
||||
repo.status_remote.ref_default = commit_hash
|
||||
} else if name.starts_with('origin') {
|
||||
name = name.all_after('origin/').trim_space()
|
||||
// Update remote tags info
|
||||
repo.status_remote.branches[name] = commit_hash
|
||||
} else {
|
||||
repo.status_local.branches[name] = commit_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mybranch := repo.exec('git branch --show-current')!.split_into_lines().filter(it.trim_space() != '')
|
||||
if mybranch.len == 1 {
|
||||
repo.status_local.branch = mybranch[0].trim_space()
|
||||
}
|
||||
// Could be a tag.
|
||||
// else{
|
||||
// panic("bug: git branch does not give branchname")
|
||||
// }
|
||||
}
|
||||
|
||||
// Helper to load remote tags
|
||||
fn (mut repo GitRepo) load_tags() ! {
|
||||
tags_result := repo.exec('git tag --list')!
|
||||
|
||||
for line in tags_result.split('\n') {
|
||||
if line.trim_space() != '' {
|
||||
parts := line.split(' ')
|
||||
if parts.len == 2 {
|
||||
commit_hash := parts[0].trim_space()
|
||||
tag_name := parts[1].all_after('refs/tags/').trim_space()
|
||||
|
||||
// Update remote tags info
|
||||
repo.status_remote.tags[tag_name] = commit_hash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
lib/develop/gittools/repository_utils.v
Normal file
151
lib/develop/gittools/repository_utils.v
Normal file
@@ -0,0 +1,151 @@
|
||||
module gittools
|
||||
|
||||
import freeflowuniverse.herolib.osal
|
||||
import freeflowuniverse.herolib.osal.sshagent
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.develop.vscode
|
||||
import freeflowuniverse.herolib.develop.sourcetree
|
||||
import os
|
||||
|
||||
@[params]
|
||||
struct GetParentDir {
|
||||
pub mut:
|
||||
create bool
|
||||
}
|
||||
|
||||
fn (repo GitRepo) get_key() string {
|
||||
return '${repo.gs.key}:${repo.provider}:${repo.account}:${repo.name}'
|
||||
}
|
||||
|
||||
fn (repo GitRepo) get_cache_key() string {
|
||||
return 'git:repos:${repo.gs.key}:${repo.provider}:${repo.account}:${repo.name}'
|
||||
}
|
||||
|
||||
pub fn (repo GitRepo) get_path() !string {
|
||||
return '${repo.gs.coderoot.path}/${repo.provider}/${repo.account}/${repo.name}'
|
||||
}
|
||||
|
||||
// gets the path of a given url within a repo
|
||||
// ex: 'https://git.ourworld.tf/ourworld_holding/info_ourworld/src/branch/main/books/cocreation/SUMMARY.md'
|
||||
// returns <repo_path>/books/cocreation/SUMMARY.md
|
||||
pub fn (repo GitRepo) get_path_of_url(url string) !string {
|
||||
// Split the URL into components
|
||||
url_parts := url.split('/')
|
||||
|
||||
// Find the index of "src" (Gitea) or "blob/tree" (GitHub)
|
||||
mut repo_root_idx := url_parts.index('src')
|
||||
if repo_root_idx == -1 {
|
||||
repo_root_idx = url_parts.index('blob')
|
||||
}
|
||||
|
||||
if repo_root_idx == -1 {
|
||||
// maybe default repo url (without src and blob)
|
||||
return repo.get_path() or {
|
||||
return error('Invalid URL format: Cannot find repository path')
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the repository path starts after the branch
|
||||
if url_parts.len < repo_root_idx + 2 {
|
||||
return error('Invalid URL format: Missing branch or file path')
|
||||
}
|
||||
|
||||
// Extract the path inside the repository
|
||||
path_in_repo := url_parts[repo_root_idx + 3..].join('/')
|
||||
|
||||
// Construct the full path
|
||||
return '${repo.get_path()!}/${path_in_repo}'
|
||||
}
|
||||
|
||||
// Relative path inside the gitstructure, pointing to the repo
|
||||
pub fn (repo GitRepo) get_relative_path() !string {
|
||||
mut mypath := repo.patho()!
|
||||
return mypath.path_relative(repo.gs.coderoot.path) or { panic("couldn't get relative path") }
|
||||
}
|
||||
|
||||
pub fn (repo GitRepo) get_parent_dir(args GetParentDir) !string {
|
||||
repo_path := repo.get_path()!
|
||||
parent_dir := os.dir(repo_path)
|
||||
if !os.exists(parent_dir) && !args.create {
|
||||
return error('Parent directory does not exist: ${parent_dir}')
|
||||
}
|
||||
os.mkdir_all(parent_dir)!
|
||||
return parent_dir
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct GetRepoUrlArgs {
|
||||
pub mut:
|
||||
with_branch bool // // If true, return the repo URL for an exact branch.
|
||||
}
|
||||
|
||||
// url_get returns the URL of a git address
|
||||
fn (self GitRepo) get_repo_url(args GetRepoUrlArgs) !string {
|
||||
url := self.status_wanted.url
|
||||
if url.len != 0 {
|
||||
if args.with_branch {
|
||||
return '${url}/tree/${self.status_local.branch}'
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
if sshagent.loaded() {
|
||||
return self.get_ssh_url()!
|
||||
} else {
|
||||
return self.get_http_url()!
|
||||
}
|
||||
}
|
||||
|
||||
fn (self GitRepo) get_ssh_url() !string {
|
||||
mut provider := self.provider
|
||||
if provider == 'github' {
|
||||
provider = 'github.com'
|
||||
}
|
||||
return 'git@${provider}:${self.account}/${self.name}.git'
|
||||
}
|
||||
|
||||
fn (self GitRepo) get_http_url() !string {
|
||||
mut provider := self.provider
|
||||
if provider == 'github' {
|
||||
provider = 'github.com'
|
||||
}
|
||||
return 'https://${provider}/${self.account}/${self.name}'
|
||||
}
|
||||
|
||||
// Return rich path object from our library hero lib
|
||||
pub fn (repo GitRepo) patho() !pathlib.Path {
|
||||
return pathlib.get_dir(path: repo.get_path()!, create: false)!
|
||||
}
|
||||
|
||||
pub fn (mut repo GitRepo) display_current_status() ! {
|
||||
staged_changes := repo.get_changes_staged()!
|
||||
unstaged_changes := repo.get_changes_unstaged()!
|
||||
|
||||
console.print_header('Staged changes:')
|
||||
for f in staged_changes {
|
||||
console.print_green('\t- ${f}')
|
||||
}
|
||||
|
||||
console.print_header('Unstaged changes:')
|
||||
if unstaged_changes.len == 0 {
|
||||
console.print_stderr('No unstaged changes; the changes need to be committed.')
|
||||
return
|
||||
}
|
||||
|
||||
for f in unstaged_changes {
|
||||
console.print_stderr('\t- ${f}')
|
||||
}
|
||||
}
|
||||
|
||||
// Opens SourceTree for the Git repo
|
||||
pub fn (repo GitRepo) sourcetree() ! {
|
||||
sourcetree.open(path: repo.get_path()!)!
|
||||
}
|
||||
|
||||
// Opens Visual Studio Code for the repo
|
||||
pub fn (repo GitRepo) open_vscode() ! {
|
||||
path := repo.get_path()!
|
||||
mut vs_code := vscode.new(path)
|
||||
vs_code.open()!
|
||||
}
|
||||
241
lib/develop/gittools/tests/gittools_test.v
Normal file
241
lib/develop/gittools/tests/gittools_test.v
Normal file
@@ -0,0 +1,241 @@
|
||||
module tests
|
||||
|
||||
import freeflowuniverse.herolib.develop.gittools
|
||||
import freeflowuniverse.herolib.osal
|
||||
import os
|
||||
import time
|
||||
|
||||
__global (
|
||||
branch_name_tests string
|
||||
tag_name_tests string
|
||||
repo_path_tests string
|
||||
repo_tests gittools.GitRepo
|
||||
repo_setup_tests GittoolsTests
|
||||
)
|
||||
|
||||
// Setup function that initializes global variables for use across tests.
|
||||
// This simulates lifecycle methods like `before_all` and runs before the tests.
|
||||
fn setup_generate_globals() {
|
||||
runtime := time.now().unix()
|
||||
branch_name_tests = 'branch_${runtime}'
|
||||
tag_name_tests = 'tag_${runtime}'
|
||||
}
|
||||
|
||||
// Function that runs at the start of the testsuite, ensuring that the repository
|
||||
// is set up and global variables are initialized for the tests.
|
||||
fn testsuite_begin() {
|
||||
setup_generate_globals()
|
||||
}
|
||||
|
||||
fn testsuite_end() {
|
||||
repo_setup_tests.clean()!
|
||||
}
|
||||
|
||||
// Test cloning a Git repository and verifying that it exists locally.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Sets up the repository directory and global variables.
|
||||
// - Clones the repository using a provided URL.
|
||||
// - Verifies that the repository's path exists in the local filesystem.
|
||||
@[test]
|
||||
fn test_clone_repo() {
|
||||
repo_setup_tests = setup_repo()!
|
||||
mut gs := gittools.new(coderoot: repo_setup_tests.coderoot)!
|
||||
repo_tests = gs.get_repo(url: repo_setup_tests.repo_url)!
|
||||
repo_path_tests = repo_tests.get_path()!
|
||||
assert os.exists(repo_path_tests) == true
|
||||
}
|
||||
|
||||
// Test creating a new branch in the Git repository.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Clones the repository.
|
||||
// - Creates a new branch using the global `branch_name_tests` variable.
|
||||
// - Verifies that the new branch was created but not checked out.
|
||||
@[test]
|
||||
fn test_branch_create() {
|
||||
repo_tests.branch_create(branch_name_tests)!
|
||||
assert repo_tests.status_local.branch != branch_name_tests
|
||||
}
|
||||
|
||||
// Test switching to an existing branch in the repository.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Ensures the repository is set up.
|
||||
// - Switches to the branch created in the `test_branch_create` test.
|
||||
// - Verifies that the branch was successfully switched.
|
||||
@[test]
|
||||
fn test_switch() {
|
||||
repo_tests.branch_switch(branch_name_tests)!
|
||||
assert repo_tests.status_local.branch == branch_name_tests
|
||||
}
|
||||
|
||||
// Test creating a tag in the Git repository.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Ensures the repository is set up.
|
||||
// - Creates a tag using the global `tag_name_tests` variable.
|
||||
// - Verifies that the tag was successfully created in the repository.
|
||||
@[test]
|
||||
fn test_tag_create() {
|
||||
repo_tests.tag_create(tag_name_tests)!
|
||||
assert repo_tests.tag_exists(tag_name_tests)! == true
|
||||
}
|
||||
|
||||
// Test detecting changes in the repository, adding changes, and committing them.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Clones the repository and creates a new file.
|
||||
// - Verifies that the repository has unstaged changes after creating the file.
|
||||
// - Stages and commits the changes, then verifies that there are no more unstaged changes.
|
||||
@[test]
|
||||
fn test_has_changes_add_changes_commit_changes() {
|
||||
file_name := create_new_file(repo_path_tests)!
|
||||
assert repo_tests.has_changes()! == true
|
||||
mut unstaged_changes := repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 1
|
||||
mut staged_changes := repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
commit_msg := 'feat: Added ${file_name} file.'
|
||||
repo_tests.commit(commit_msg)!
|
||||
staged_changes = repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
unstaged_changes = repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 0
|
||||
}
|
||||
|
||||
// Test pushing changes to the repository.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Clones the repository and creates a new branch.
|
||||
// - Creates a new file and commits the changes.
|
||||
// - Verifies that the changes have been successfully pushed to the remote repository.
|
||||
@[test]
|
||||
fn test_push_changes() {
|
||||
file_name := create_new_file(repo_path_tests)!
|
||||
assert repo_tests.has_changes()! == true
|
||||
mut unstaged_changes := repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 1
|
||||
mut staged_changes := repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
commit_msg := 'feat: Added ${file_name} file.'
|
||||
repo_tests.commit(commit_msg)!
|
||||
repo_tests.push()!
|
||||
staged_changes = repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
unstaged_changes = repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 0
|
||||
}
|
||||
|
||||
// Test performing multiple commits and pushing them.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Clones the repository.
|
||||
// - Creates multiple files.
|
||||
// - Commits each change and pushes them to the remote repository.
|
||||
@[test]
|
||||
fn test_multiple_commits_and_push() {
|
||||
file_name_1 := create_new_file(repo_path_tests)!
|
||||
repo_tests.commit('feat: Added ${file_name_1} file.')!
|
||||
|
||||
file_name_2 := create_new_file(repo_path_tests)!
|
||||
repo_tests.commit('feat: Added ${file_name_2} file.')!
|
||||
|
||||
repo_tests.push()!
|
||||
assert repo_tests.has_changes()! == false
|
||||
}
|
||||
|
||||
// Test committing with valid changes.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Creates a new file in the repository.
|
||||
// - Verifies that there are uncommitted changes.
|
||||
// - Commits the changes.
|
||||
@[test]
|
||||
fn test_commit_with_valid_changes() {
|
||||
file_name_1 := create_new_file(repo_path_tests)!
|
||||
assert repo_tests.need_commit()! == true
|
||||
repo_tests.commit('Initial commit')!
|
||||
}
|
||||
|
||||
// Test committing without changes.
|
||||
//
|
||||
// This test performs the following steps:
|
||||
// - Verifies that there are no uncommitted changes.
|
||||
// - Attempts to commit and expects a failure.
|
||||
@[test]
|
||||
fn test_commit_without_changes() {
|
||||
assert repo_tests.has_changes()! == false
|
||||
assert repo_tests.need_commit()! == false
|
||||
repo_tests.commit('Initial commit') or {
|
||||
assert false, 'Commit should be done with some changes'
|
||||
}
|
||||
}
|
||||
|
||||
// Test pushing with no changes.
|
||||
//
|
||||
// This test verifies that pushing with no changes results in no action.
|
||||
@[test]
|
||||
fn test_push_with_no_changes() {
|
||||
assert repo_tests.need_push_or_pull()! == false
|
||||
repo_tests.push() or {
|
||||
assert false, 'Push should not perform any action when no changes exist'
|
||||
}
|
||||
}
|
||||
|
||||
// Test pulling remote changes.
|
||||
//
|
||||
// This test ensures that the pull operation succeeds without errors.
|
||||
@[test]
|
||||
fn test_pull_remote_changes() {
|
||||
repo_tests.pull() or { assert false, 'Pull should succeed' }
|
||||
}
|
||||
|
||||
// Test creating and switching to a new branch.
|
||||
//
|
||||
// This test creates a new branch and then switches to it.
|
||||
@[test]
|
||||
fn test_create_and_switch_new_branch() {
|
||||
repo_tests.branch_create('testing-branch') or { assert false, 'Branch creation should succeed' }
|
||||
|
||||
repo_tests.branch_switch('testing-branch') or { assert false, 'Branch switch should succeed' }
|
||||
}
|
||||
|
||||
// Test creating and switching to a tag.
|
||||
//
|
||||
// This test creates a new tag and then switches to it.
|
||||
@[test]
|
||||
fn test_create_and_check_tag() {
|
||||
repo_tests.tag_create('v1.0.0') or { assert false, 'Tag creation should succeed' }
|
||||
|
||||
repo_tests.tag_exists('v1.0.0') or { assert false, 'Tag switch should succeed' }
|
||||
}
|
||||
|
||||
// Test removing changes.
|
||||
//
|
||||
// This test verifies that changes are successfully removed after they are made.
|
||||
@[test]
|
||||
fn test_remove_changes() {
|
||||
mut unstaged_changes := repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 0
|
||||
|
||||
mut staged_changes := repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
|
||||
file_name := create_new_file(repo_path_tests)!
|
||||
assert repo_tests.has_changes()! == true
|
||||
|
||||
unstaged_changes = repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 1
|
||||
|
||||
staged_changes = repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
|
||||
repo_tests.remove_changes() or { assert false, 'Changes should be removed successfully' }
|
||||
|
||||
unstaged_changes = repo_tests.get_changes_unstaged()!
|
||||
assert unstaged_changes.len == 0
|
||||
|
||||
staged_changes = repo_tests.get_changes_staged()!
|
||||
assert staged_changes.len == 0
|
||||
}
|
||||
54
lib/develop/gittools/tests/setup.v
Normal file
54
lib/develop/gittools/tests/setup.v
Normal file
@@ -0,0 +1,54 @@
|
||||
module tests
|
||||
|
||||
import freeflowuniverse.herolib.osal
|
||||
import os
|
||||
import time
|
||||
|
||||
struct GittoolsTests {
|
||||
coderoot string
|
||||
repo_dir string
|
||||
repo_url string
|
||||
repo_name string
|
||||
}
|
||||
|
||||
// Creates a new Python file with 'Hello, World!' content in the specified repository path.
|
||||
// The file name includes a timestamp to ensure uniqueness.
|
||||
//
|
||||
// Args:
|
||||
// - repo_path (string): Path to the repository where the new file will be created.
|
||||
// - runtime (i64): Unix timestamp used to generate a unique file name.
|
||||
//
|
||||
// Returns:
|
||||
// - string: Name of the newly created file.
|
||||
fn create_new_file(repo_path string) !string {
|
||||
coded_now := time.now().unix()
|
||||
file_name := 'hello_world_${coded_now}.py'
|
||||
osal.execute_silent("echo \"print('Hello, World!')\" > ${repo_path}/${file_name}")!
|
||||
return file_name
|
||||
}
|
||||
|
||||
// Sets up a GittoolsTests instance with predefined values for repository setup.
|
||||
//
|
||||
// Returns:
|
||||
// - GittoolsTests: Struct containing information about the repo setup.
|
||||
fn setup_repo() !GittoolsTests {
|
||||
ts := GittoolsTests{
|
||||
coderoot: '/tmp/code'
|
||||
repo_url: 'https://github.com/freeflowuniverse/test_repo.git'
|
||||
}
|
||||
|
||||
if os.exists(ts.coderoot) {
|
||||
ts.clean()!
|
||||
}
|
||||
|
||||
os.mkdir_all(ts.coderoot)!
|
||||
return ts
|
||||
}
|
||||
|
||||
// Removes the directory structure created during repository setup.
|
||||
//
|
||||
// Raises:
|
||||
// - Error: If the directory cannot be removed.
|
||||
fn (ts GittoolsTests) clean() ! {
|
||||
os.rmdir_all(ts.coderoot)!
|
||||
}
|
||||
Reference in New Issue
Block a user