diff --git a/README.md b/README.md index dabc28aa..97b29312 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,17 @@ Herolib provides a wide range of functionality: - Cloud automation tools - 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 - Hero AI integration - System management utilities diff --git a/lib/core/texttools/namefix.v b/lib/core/texttools/namefix.v index c64ef4ef..043bc94a 100644 --- a/lib/core/texttools/namefix.v +++ b/lib/core/texttools/namefix.v @@ -192,17 +192,17 @@ pub fn path_fix(path_ string) string { if starts_with_dot_slash { result_components << '.' // Skip the first component which is '.' - components = components[1..] + components = components[1..].clone() } else if starts_with_dot_dot_slash { result_components << '..' // Skip the first component which is '..' - components = components[1..] + components = components[1..].clone() } else if is_absolute { // Keep the empty component for absolute paths result_components << '' // Skip the first empty component if components.len > 0 && components[0] == '' { - components = components[1..] + components = components[1..].clone() } } diff --git a/lib/develop/gittools/factory.v b/lib/develop/gittools/factory.v index 52d97b2b..4c4be444 100644 --- a/lib/develop/gittools/factory.v +++ b/lib/develop/gittools/factory.v @@ -22,6 +22,7 @@ pub mut: ssh_key_name string // name of ssh key to be used when loading the gitstructure ssh_key_path string reload bool + offline bool = false } // Retrieve or create a new GitStructure instance with the given configuration. @@ -37,6 +38,7 @@ pub fn new(args_ GitStructureArgsNew) !&GitStructure { debug: args.debug ssh_key_name: args.ssh_key_name ssh_key_path: args.ssh_key_path + offline: args.offline } return get(coderoot: args.coderoot, reload: args.reload, cfg: cfg) diff --git a/lib/develop/gittools/gitstructure.v b/lib/develop/gittools/gitstructure.v index 24025463..50412630 100644 --- a/lib/develop/gittools/gitstructure.v +++ b/lib/develop/gittools/gitstructure.v @@ -15,6 +15,7 @@ pub mut: debug bool = true ssh_key_name string ssh_key_path string + offline bool = false } // GitStructure holds information about repositories within a specific code root. @@ -53,13 +54,13 @@ pub fn (mut gitstructure GitStructure) load(reload bool) ! { redisclient.checkempty() for _, mut repo in gitstructure.repos { - // mut myfunction := fn (mut repo GitRepo) ! { - // } - // ths << spawn myfunction(mut repo_) repo.status_update(reload: reload) or { - msg := 'Error in git repo: ${repo.path()}\n${err}' - console.print_stderr(msg) - return error(msg) + // If status_update fails, the error is already captured within the repo object. + // We log it here and continue to process 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 } } } diff --git a/lib/develop/gittools/repos_get.v b/lib/develop/gittools/repos_get.v index 1a0b31a7..4ee095ad 100644 --- a/lib/develop/gittools/repos_get.v +++ b/lib/develop/gittools/repos_get.v @@ -1,6 +1,7 @@ module gittools import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.ui.console import time // 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()! } 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 { repo.reset()! diff --git a/lib/develop/gittools/repos_print.v b/lib/develop/gittools/repos_print.v index 4e84136e..230bbdd4 100644 --- a/lib/develop/gittools/repos_print.v +++ b/lib/develop/gittools/repos_print.v @@ -7,6 +7,21 @@ fn get_repo_status(gr GitRepo) !string { mut repo := gr 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 { statuses << 'COMMIT' } @@ -38,32 +53,22 @@ fn format_repo_info(repo GitRepo) ![]string { // 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.status_update()! repo_data << format_repo_info(repo)! } // Clear the console and start printing the formatted repository information 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}' console.print_header(header) + console.print_lf(1) // Keep one newline after 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) + // console.print_lf(5) // Removed to reduce newlines } diff --git a/lib/develop/gittools/repository.v b/lib/develop/gittools/repository.v index dda7f298..365999d7 100644 --- a/lib/develop/gittools/repository.v +++ b/lib/develop/gittools/repository.v @@ -36,6 +36,7 @@ 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 + error string // Error message if remote status update fails } // GitRepoStatusLocal holds local status information for a repository. @@ -44,6 +45,7 @@ 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 + error string // Error message if local status update fails } // GitRepoConfig holds repository-specific configuration options. diff --git a/lib/develop/gittools/repository_load.v b/lib/develop/gittools/repository_load.v index ce692ff0..bd8ea320 100644 --- a/lib/develop/gittools/repository_load.v +++ b/lib/develop/gittools/repository_load.v @@ -7,13 +7,33 @@ import os @[params] pub struct StatusUpdateArgs { reload bool + force bool // Add force flag to bypass cache when callers need it. } 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 - repo.cache_get() or { return error('Failed to get cache for repo ${repo.name}: ${err}') } // Ensure we have the situation from redis - repo.init() or { return error('Failed to initialize repo ${repo.name}: ${err}') } - if 'OFFLINE' !in os.environ() { + repo.cache_get() or { + repo.status_local.error = 'Failed to get cache: ${err}' + 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 } 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 { // 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)} - 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. fn (mut repo GitRepo) load() ! { 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' 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)') } 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.') } - 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.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}') } repo.cache_set() or { + repo.status_local.error = 'Failed to update cache: ${err}' return error('Failed to update cache for repository ${repo.name}: ${err}') } }