This commit is contained in:
2025-07-21 13:35:30 +02:00
parent 02c4229116
commit bb3dd2dbf9
8 changed files with 94 additions and 30 deletions

View File

@@ -72,6 +72,17 @@ Herolib provides a wide range of functionality:
- Cloud automation tools - Cloud automation tools
- Git operations and management - Git operations and management
### Offline Mode for Git Operations
Herolib now supports an `offline` mode for Git operations, which prevents automatic fetching from remote repositories. This can be useful in environments with limited or no internet connectivity, or when you want to avoid network calls during development or testing.
To enable offline mode:
- **Via `GitStructureConfig`**: Set the `offline` field to `true` in the `GitStructureConfig` struct.
- **Via `GitStructureArgsNew`**: When creating a new `GitStructure` instance using `gittools.new()`, set the `offline` parameter to `true`.
- **Via Environment Variable**: Set the `OFFLINE` environment variable to any value (e.g., `export OFFLINE=true`).
When offline mode is active, `git fetch --all` operations will be skipped, and a debug message "fetch skipped (offline)" will be printed.
- Documentation building - Documentation building
- Hero AI integration - Hero AI integration
- System management utilities - System management utilities

View File

@@ -192,17 +192,17 @@ pub fn path_fix(path_ string) string {
if starts_with_dot_slash { if starts_with_dot_slash {
result_components << '.' result_components << '.'
// Skip the first component which is '.' // Skip the first component which is '.'
components = components[1..] components = components[1..].clone()
} else if starts_with_dot_dot_slash { } else if starts_with_dot_dot_slash {
result_components << '..' result_components << '..'
// Skip the first component which is '..' // Skip the first component which is '..'
components = components[1..] components = components[1..].clone()
} else if is_absolute { } else if is_absolute {
// Keep the empty component for absolute paths // Keep the empty component for absolute paths
result_components << '' result_components << ''
// Skip the first empty component // Skip the first empty component
if components.len > 0 && components[0] == '' { if components.len > 0 && components[0] == '' {
components = components[1..] components = components[1..].clone()
} }
} }

View File

@@ -22,6 +22,7 @@ pub mut:
ssh_key_name string // name of ssh key to be used when loading the gitstructure ssh_key_name string // name of ssh key to be used when loading the gitstructure
ssh_key_path string ssh_key_path string
reload bool reload bool
offline bool = false
} }
// Retrieve or create a new GitStructure instance with the given configuration. // Retrieve or create a new GitStructure instance with the given configuration.
@@ -37,6 +38,7 @@ pub fn new(args_ GitStructureArgsNew) !&GitStructure {
debug: args.debug debug: args.debug
ssh_key_name: args.ssh_key_name ssh_key_name: args.ssh_key_name
ssh_key_path: args.ssh_key_path ssh_key_path: args.ssh_key_path
offline: args.offline
} }
return get(coderoot: args.coderoot, reload: args.reload, cfg: cfg) return get(coderoot: args.coderoot, reload: args.reload, cfg: cfg)

View File

@@ -15,6 +15,7 @@ pub mut:
debug bool = true debug bool = true
ssh_key_name string ssh_key_name string
ssh_key_path string ssh_key_path string
offline bool = false
} }
// GitStructure holds information about repositories within a specific code root. // GitStructure holds information about repositories within a specific code root.
@@ -53,13 +54,13 @@ pub fn (mut gitstructure GitStructure) load(reload bool) ! {
redisclient.checkempty() redisclient.checkempty()
for _, mut repo in gitstructure.repos { for _, mut repo in gitstructure.repos {
// mut myfunction := fn (mut repo GitRepo) ! {
// }
// ths << spawn myfunction(mut repo_)
repo.status_update(reload: reload) or { repo.status_update(reload: reload) or {
msg := 'Error in git repo: ${repo.path()}\n${err}' // If status_update fails, the error is already captured within the repo object.
console.print_stderr(msg) // We log it here and continue to process other repositories.
return error(msg) console.print_stderr('Error updating status for repo ${repo.path()}: ${err}')
// Ensure last_load is reset to 0 if there was an error, so it's re-checked next time.
repo.last_load = 0
repo.cache_set()! // Persist the updated last_load and error state
} }
} }
} }

View File

@@ -1,6 +1,7 @@
module gittools module gittools
import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.ui.console
import time import time
// ReposGetArgs defines arguments to retrieve repositories from the git structure. // ReposGetArgs defines arguments to retrieve repositories from the git structure.
@@ -61,7 +62,13 @@ pub fn (mut gitstructure GitStructure) get_repos(args_ ReposGetArgs) ![]&GitRepo
repo.cache_last_load_clear()! repo.cache_last_load_clear()!
} }
if args.status_update { if args.status_update {
repo.status_update()! repo.status_update() or {
// Log the error but continue processing other repositories
console.print_stderr('Error updating status for repo ${repo.path()}: ${err}')
// Ensure last_load is reset to 0 if there was an error, so it's re-checked next time.
repo.last_load = 0
repo.cache_set()! // Persist the updated last_load and error state
}
} }
if args.reset { if args.reset {
repo.reset()! repo.reset()!

View File

@@ -7,6 +7,21 @@ fn get_repo_status(gr GitRepo) !string {
mut repo := gr mut repo := gr
mut statuses := []string{} mut statuses := []string{}
if repo.status_local.error.len > 0 {
mut err_msg := repo.status_local.error
if err_msg.len > 40 {
err_msg = err_msg[0..40] + '...'
}
statuses << 'ERROR (Local): ${err_msg}'
}
if repo.status_remote.error.len > 0 {
mut err_msg := repo.status_remote.error
if err_msg.len > 40 {
err_msg = err_msg[0..40] + '...'
}
statuses << 'ERROR (Remote): ${err_msg}'
}
if repo.has_changes { if repo.has_changes {
statuses << 'COMMIT' statuses << 'COMMIT'
} }
@@ -38,32 +53,22 @@ fn format_repo_info(repo GitRepo) ![]string {
// Print repositories based on the provided criteria, showing their statuses // Print repositories based on the provided criteria, showing their statuses
pub fn (mut gitstructure GitStructure) repos_print(args ReposGetArgs) ! { pub fn (mut gitstructure GitStructure) repos_print(args ReposGetArgs) ! {
// console.print_debug('#### Overview of repositories:')
// console.print_debug('')
mut repo_data := [][]string{} mut repo_data := [][]string{}
// Collect repository information based on the provided criteria // Collect repository information based on the provided criteria
for _, repo in gitstructure.get_repos(args)! { for _, repo in gitstructure.get_repos(args)! {
// repo.status_update()!
repo_data << format_repo_info(repo)! repo_data << format_repo_info(repo)!
} }
// Clear the console and start printing the formatted repository information // Clear the console and start printing the formatted repository information
console.clear() console.clear()
console.print_lf(1) // console.print_lf(1) // Removed to reduce newlines
// Display header with optional argument filtering information
// header := if args.str().len > 0 {
// 'Repositories: ${gitstructure.config()!.coderoot} [${args.str()}]'
// } else {
// 'Repositories: ${gitstructure.config()!.coderoot}'
// }
header := 'Repositories: ${gitstructure.config()!.coderoot}' header := 'Repositories: ${gitstructure.config()!.coderoot}'
console.print_header(header) console.print_header(header)
console.print_lf(1) // Keep one newline after header
// Print the repository information in a formatted array // 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_array(repo_data, ' ', true) // true -> aligned for better readability
console.print_lf(5) // console.print_lf(5) // Removed to reduce newlines
} }

View File

@@ -36,6 +36,7 @@ pub mut:
ref_default string // is the default branch hash ref_default string // is the default branch hash
branches map[string]string // Branch name -> commit hash branches map[string]string // Branch name -> commit hash
tags map[string]string // Tag name -> commit hash tags map[string]string // Tag name -> commit hash
error string // Error message if remote status update fails
} }
// GitRepoStatusLocal holds local status information for a repository. // GitRepoStatusLocal holds local status information for a repository.
@@ -44,6 +45,7 @@ pub mut:
branches map[string]string // Branch name -> commit hash branches map[string]string // Branch name -> commit hash
branch string // the current branch branch string // the current branch
tag string // If the local branch is not set, the tag may be set tag string // If the local branch is not set, the tag may be set
error string // Error message if local status update fails
} }
// GitRepoConfig holds repository-specific configuration options. // GitRepoConfig holds repository-specific configuration options.

View File

@@ -7,13 +7,33 @@ import os
@[params] @[params]
pub struct StatusUpdateArgs { pub struct StatusUpdateArgs {
reload bool reload bool
force bool // Add force flag to bypass cache when callers need it.
} }
pub fn (mut repo GitRepo) status_update(args StatusUpdateArgs) ! { pub fn (mut repo GitRepo) status_update(args StatusUpdateArgs) ! {
// Clear previous errors
repo.status_local.error = ''
repo.status_remote.error = ''
// Check current time vs last check, if needed (check period) then load // Check current time vs last check, if needed (check period) then load
repo.cache_get() or { return error('Failed to get cache for repo ${repo.name}: ${err}') } // Ensure we have the situation from redis repo.cache_get() or {
repo.init() or { return error('Failed to initialize repo ${repo.name}: ${err}') } repo.status_local.error = 'Failed to get cache: ${err}'
if 'OFFLINE' !in os.environ() { return error('Failed to get cache for repo ${repo.name}: ${err}')
} // Ensure we have the situation from redis
repo.init() or {
repo.status_local.error = 'Failed to initialize: ${err}'
return error('Failed to initialize repo ${repo.name}: ${err}')
}
// If there's an existing error, skip loading and just return.
// This prevents repeated attempts to load a problematic repo.
if repo.status_local.error.len > 0 || repo.status_remote.error.len > 0 {
console.print_debug('Skipping load for ${repo.name} due to existing error.')
return
}
if 'OFFLINE' in os.environ() || (repo.gs.config()!.offline) {
console.print_debug('fetch skipped (offline)')
return return
} }
current_time := int(time.now().unix()) current_time := int(time.now().unix())
@@ -21,7 +41,10 @@ pub fn (mut repo GitRepo) status_update(args StatusUpdateArgs) ! {
|| current_time - repo.last_load >= repo.config.remote_check_period { || current_time - repo.last_load >= repo.config.remote_check_period {
// console.print_debug('${repo.name} ${current_time}-${repo.last_load} (${current_time - repo.last_load >= repo.config.remote_check_period}): ${repo.config.remote_check_period} +++') // console.print_debug('${repo.name} ${current_time}-${repo.last_load} (${current_time - repo.last_load >= repo.config.remote_check_period}): ${repo.config.remote_check_period} +++')
// if true{exit(0)} // if true{exit(0)}
repo.load() or { return error('Failed to load repository ${repo.name}: ${err}') } repo.load() or {
repo.status_remote.error = 'Failed to load repository: ${err}'
return error('Failed to load repository ${repo.name}: ${err}')
}
} }
} }
@@ -29,27 +52,40 @@ pub fn (mut repo GitRepo) status_update(args StatusUpdateArgs) ! {
// Does not check cache, it is the callers responsibility to check cache and load accordingly. // Does not check cache, it is the callers responsibility to check cache and load accordingly.
fn (mut repo GitRepo) load() ! { fn (mut repo GitRepo) load() ! {
console.print_header('load ${repo.print_key()}') console.print_header('load ${repo.print_key()}')
repo.init() or { return error('Failed to initialize repo during load operation: ${err}') } repo.init() or {
repo.status_local.error = 'Failed to initialize repo during load operation: ${err}'
return error('Failed to initialize repo during load operation: ${err}')
}
git_path := '${repo.path()}/.git' git_path := '${repo.path()}/.git'
if os.exists(git_path) == false { if os.exists(git_path) == false {
repo.status_local.error = 'Repository not found: missing .git directory'
return error('Repository not found: ${repo.path()} is not a valid git repository (missing .git directory)') return error('Repository not found: ${repo.path()} is not a valid git repository (missing .git directory)')
} }
repo.exec('git fetch --all') or { repo.exec('git fetch --all') or {
repo.status_remote.error = 'Failed to fetch updates: ${err}'
return error('Failed to fetch updates for ${repo.name} at ${repo.path()}: ${err}. Please check network connection and repository access.') return error('Failed to fetch updates for ${repo.name} at ${repo.path()}: ${err}. Please check network connection and repository access.')
} }
repo.load_branches() or { return error('Failed to load branches for ${repo.name}: ${err}') } repo.load_branches() or {
repo.status_remote.error = 'Failed to load branches: ${err}'
return error('Failed to load branches for ${repo.name}: ${err}')
}
repo.load_tags() or { return error('Failed to load tags for ${repo.name}: ${err}') } repo.load_tags() or {
repo.status_remote.error = 'Failed to load tags: ${err}'
return error('Failed to load tags for ${repo.name}: ${err}')
}
repo.last_load = int(time.now().unix()) repo.last_load = int(time.now().unix())
repo.has_changes = repo.detect_changes() or { repo.has_changes = repo.detect_changes() or {
repo.status_local.error = 'Failed to detect changes: ${err}'
return error('Failed to detect changes in repository ${repo.name}: ${err}') return error('Failed to detect changes in repository ${repo.name}: ${err}')
} }
repo.cache_set() or { repo.cache_set() or {
repo.status_local.error = 'Failed to update cache: ${err}'
return error('Failed to update cache for repository ${repo.name}: ${err}') return error('Failed to update cache for repository ${repo.name}: ${err}')
} }
} }