This commit is contained in:
2025-08-15 06:20:00 +02:00
parent b0ff9e3fbf
commit bd86f2c4f7
7 changed files with 197 additions and 232 deletions

View File

@@ -34,34 +34,23 @@ pub mut:
//////////////////////////////////////////////////////////////////////////////////////
// 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. (is just a reload:bool)
pub fn (mut gitstructure GitStructure) load(reload bool) ! {
mut processed_paths := []string{}
// Use the reset argument to force reloading from the disk.
pub fn (mut gitstructure GitStructure) load(reset bool) ! {
mut processed_paths := []string{} // Re-added initialization
if reload {
if reset {
gitstructure.repos = map[string]&GitRepo{}
}
gitstructure.load_recursive(gitstructure.coderoot.path, mut processed_paths)!
if reload {
if reset {
gitstructure.cache_reset()!
$dbg;
}
redisclient.reset()!
redisclient.checkempty()
for _, mut repo in gitstructure.repos {
repo.status_update(reload: reload) or {
// 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
}
repo.status_update(reset: reset)!
}
}

View File

@@ -72,12 +72,19 @@ pub fn (mut gs GitStructure) do(args_ ReposActionsArgs) !string {
provider: args.provider
)!
// reset the status for the repo
if args.reload || args.cmd == 'reload' {
for mut repo in repos {
repo.cache_last_load_clear()!
}
gs.load(true)!
// MODIFIED: Remove the global reload.
// The reload flag will now be handled inside the loop.
// if args.reload || args.cmd == 'reload' {
// for mut repo in repos {
// repo.cache_last_load_clear()!
// }
// gs.load(true)! // <-- REMOVED
// }
// NEW: Update status only for the relevant repos.
console.print_header('Updating status for selected repos...')
for mut repo in repos {
repo.status_update(reload: args.reload || args.cmd == 'reload')!
}
if args.cmd == 'list' {

View File

@@ -4,75 +4,6 @@ import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.core as osal
import os
// 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
has_changes bool
}
// 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
error string // Error message if remote status update fails
}
// 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
error string // Error message if local status update fails
}
// GitRepoConfig holds repository-specific configuration options.
pub struct GitRepoConfig {
pub mut:
remote_check_period int = 3600 * 24 * 7 // Seconds to wait between remote checks (0 = check every time), default 7 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.cache_key()] = &repo
return &repo
}
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
// Commit the staged changes with the provided commit message.
pub fn (mut repo GitRepo) commit(msg string) ! {
repo.status_update()!
@@ -90,7 +21,7 @@ pub fn (mut repo GitRepo) commit(msg string) ! {
return error('Cannot commit repo: ${repo_path}. Error: ${err}')
}
console.print_green('Changes committed successfully.')
repo.load()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
}
// Push local changes to the remote repository.
@@ -102,7 +33,7 @@ pub fn (mut repo GitRepo) push() ! {
// 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()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
} else {
console.print_header('Everything is up to date.')
}
@@ -127,7 +58,7 @@ pub fn (mut repo GitRepo) pull(args_ PullCheckoutArgs) ! {
repo.update_submodules()!
}
repo.load()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
console.print_green('Changes pulled successfully.')
}
@@ -165,7 +96,7 @@ pub fn (mut repo GitRepo) branch_switch(branchname string) ! {
console.print_green('Branch ${branchname} switched successfully.')
repo.status_local.branch = branchname
repo.status_local.tag = ''
repo.pull()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
}
// Create a new branch in the repository.
@@ -175,6 +106,7 @@ pub fn (mut repo GitRepo) tag_create(tagname string) ! {
return error('Cannot create tag: ${repo_path}. Error: ${err}')
}
console.print_green('Tag ${tagname} created successfully.')
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
}
pub fn (mut repo GitRepo) tag_switch(tagname string) ! {
@@ -184,7 +116,7 @@ pub fn (mut repo GitRepo) tag_switch(tagname string) ! {
console.print_green('Tag ${tagname} activated.')
repo.status_local.branch = ''
repo.status_local.tag = tagname
repo.pull()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
}
// Create a new branch in the repository.
@@ -294,7 +226,7 @@ pub fn (mut repo GitRepo) remove_changes() ! {
// p.delete()! // remove path, this will re-clone the full thing
// repo.load_from_url()!
}
repo.load()!
repo.cache_last_load_clear()! // MODIFIED: Invalidate cache instead of full reload.
}
}

View File

@@ -35,8 +35,6 @@ fn (mut repo GitRepo) cache_delete() ! {
mut redis_client := redis_get()
cache_key := repo.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
}
// put the data of last load on 0, means first time a git status check will be done it will update its info

View File

@@ -1,96 +1,34 @@
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: ${repo.path()}\n${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: ${repo.path()}\n${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) detect_changes() !bool {
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 {
// This function assumes `status_update` has already been called.
return repo.has_changes
}
// Check if the repository has local changes that need to be pushed to remote
pub fn (mut repo GitRepo) need_push() !bool {
repo.status_update()!
last_remote_commit := repo.get_last_remote_commit() or {
return error('Failed to get last remote commit: ${repo.path()}\n${err}')
}
last_local_commit := repo.get_last_local_commit()!
// If remote commit is empty, it means the branch doesn't exist remotely yet
if last_remote_commit.len == 0 {
// This function assumes `status_update` has already run to populate the status.
// A new local branch that doesn't exist on the remote needs to be pushed.
if repo.status_local.branch != '' && repo.get_last_remote_commit()! == '' {
return true
}
// If local commit is different from remote and exists, we need to push
return last_local_commit != last_remote_commit
// If the local branch is ahead of its remote counterpart, it needs to be pushed.
return repo.status_local.ahead > 0
}
// Check if the repository needs to pull changes from remote
pub fn (mut repo GitRepo) need_pull() !bool {
repo.status_update()!
last_remote_commit := repo.get_last_remote_commit() or {
return error('Failed to get last remote commit: ${repo.path()}\n${err}')
}
// If remote doesn't exist, no need to pull
if last_remote_commit.len == 0 {
return false
}
// Check if the remote commit exists in our local history
// If it doesn't exist, we need to pull
result := repo.exec('git merge-base --is-ancestor ${last_remote_commit} HEAD') or {
// if err.msg().contains('exit code: 1') {
// // Exit code 1 means the remote commit is not in our history
// // Therefore we need to pull
// return true
// }
return true
// return error('Failed to check merge-base: ${err}')
}
// If we get here, the remote commit is in our history
// Therefore we don't need to pull
return false
// This function assumes `status_update` has already run to populate the status.
// If the local branch is behind its remote counterpart, it needs to be pulled.
return repo.status_local.behind > 0
}
// Legacy function for backward compatibility
pub fn (mut repo GitRepo) need_push_or_pull() !bool {
// This function relies on the simplified need_push() and need_pull() checks.
return repo.need_push()! || repo.need_pull()!
}

View File

@@ -6,76 +6,67 @@ import os
@[params]
pub struct StatusUpdateArgs {
reload bool
force bool // Add force flag to bypass cache when callers need it.
reset bool
}
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 {
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
}
repo.init()!
// Skip remote checks if offline.
if 'OFFLINE' in os.environ() || (repo.gs.config()!.offline) {
console.print_debug('fetch skipped (offline)')
console.print_debug('status update skipped (offline) for ${repo.path()}')
return
}
current_time := int(time.now().unix())
if args.reload || repo.last_load == 0
// Decide if a full load is needed.
if args.reset || repo.last_load == 0
|| 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 {
repo.status_remote.error = 'Failed to load repository: ${err}'
repo.load_internal() or {
// Persist the error state to the cache
if repo.status_remote.error == '' {
repo.status_remote.error = 'Failed to load repository: ${err}'
}
repo.cache_set()!
return error('Failed to load repository ${repo.name}: ${err}')
}
}
}
// Load repo information
// Does not check cache, it is the callers responsibility to check cache and load accordingly.
fn (mut repo GitRepo) load() ! {
// load_internal performs the expensive git operations to refresh the repository state.
// It should only be called by status_update().
fn (mut repo GitRepo) load_internal() ! {
console.print_header('load ${repo.print_key()}')
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.init()!
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()!
repo.load_tags()!
repo.load_branches() or {
repo.status_remote.error = 'Failed to load branches: ${err}'
return error('Failed to load branches for ${repo.name}: ${err}')
}
// Reset ahead/behind counts before recalculating
repo.status_local.ahead = 0
repo.status_local.behind = 0
repo.load_tags() or {
repo.status_remote.error = 'Failed to load tags: ${err}'
return error('Failed to load tags for ${repo.name}: ${err}')
// Get ahead/behind information for the current branch
status_res := repo.exec('git status --porcelain=v2 --branch')!
for line in status_res.split_into_lines() {
if line.starts_with('# branch.ab') {
parts := line.split(' ')
if parts.len > 3 {
ahead_str := parts[2]
behind_str := parts[3]
if ahead_str.starts_with('+') {
repo.status_local.ahead = ahead_str[1..].int()
}
if behind_str.starts_with('-') {
repo.status_local.behind = behind_str[1..].int()
}
}
break // We only need this one line
}
}
repo.last_load = int(time.now().unix())
@@ -84,10 +75,9 @@ fn (mut repo GitRepo) load() ! {
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}')
}
// Persist the newly loaded state to the cache.
repo.cache_set()!
}
// Helper to load remote tags
@@ -132,23 +122,65 @@ fn (mut repo GitRepo) load_branches() ! {
// Helper to load remote tags
fn (mut repo GitRepo) load_tags() ! {
tags_result := repo.exec('git tag --list') or {
// CORRECTED: Use for-each-ref to get commit hashes for tags.
tags_result := repo.exec("git for-each-ref --format='%(objectname) %(refname:short)' refs/tags") or {
return error('Failed to list tags: ${err}. Please ensure git is installed and repository is accessible.')
}
// println(tags_result)
for line in tags_result.split('\n') {
line_trimmed := line.trim_space()
if line_trimmed != '' {
parts := line_trimmed.split(' ')
if parts.len < 2 {
// console.print_debug('Skipping malformed tag line: ${line_trimmed}')
continue
continue // Skip malformed lines
}
commit_hash := parts[0].trim_space()
tag_name := parts[1].all_after('refs/tags/').trim_space()
// refname:short for tags is just the tag name itself.
tag_name := parts[1].trim_space()
// Update remote tags info
repo.status_remote.tags[tag_name] = commit_hash
}
}
}
// 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: ${repo.path()}\n${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: ${repo.path()}\n${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) detect_changes() !bool {
r0 := repo.get_changes_unstaged()!
r1 := repo.get_changes_staged()!
if r0.len + r1.len > 0 {
return true
}
return false
}

View File

@@ -0,0 +1,69 @@
module gittools
// 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
has_changes bool
}
// 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
error string // Error message if remote status update fails
}
// 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
ahead int // Commits ahead of remote
behind int // Commits behind remote
error string // Error message if local status update fails
}
// GitRepoConfig holds repository-specific configuration options.
pub struct GitRepoConfig {
pub mut:
remote_check_period int = 3600 * 24 * 7 // Seconds to wait between remote checks (0 = check every time), default 7 days
}
// just some initialization mechanism
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.cache_key()] = &repo
return &repo
}