diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ac1e5d11..42dc1903 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -42,7 +42,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: - path: "/home/runner/work/herolib/docs" + path: "/home/runner/work/herolib/herolib/docs" - name: Deploy to GitHub Pages id: deployment diff --git a/cli/compile_upload.sh b/cli/compile_upload.sh new file mode 100644 index 00000000..f95ee466 --- /dev/null +++ b/cli/compile_upload.sh @@ -0,0 +1,47 @@ + + + +function s3_configure { + +SECRET_FILE="/${HOME}/code/git.ourworld.tf/despiegk/hero_secrets/mysecrets.sh" +if [ -f "$SECRET_FILE" ]; then + echo get secrets + source "$SECRET_FILE" +fi + +# Check if environment variables are set +if [ -z "$S3KEYID" ] || [ -z "$S3APPID" ]; then + echo "Error: S3KEYID or S3APPID is not set" + exit 1 +fi + +# Create rclone config file +mkdir -p "${HOME}/.config/rclone" +cat > "${HOME}/.config/rclone/rclone.conf" </dev/null) + if [ -z "$hero_path" ]; then + echo "Error: 'hero' command not found in PATH" >&2 + exit 1 + fi + set -x + s3_configure + # rclone_config=$(get_rclone_config) || { echo "$rclone_config"; exit 1; } + rclone --config="${HOME}/.config/rclone/rclone.conf" lsl b2:threefold/$MYPLATFORMID/ + rclone --config="${HOME}/.config/rclone/rclone.conf" copy "$hero_path" b2:threefold/$MYPLATFORMID/ +} + \ No newline at end of file diff --git a/cli/compile_upload.vsh b/cli/compile_upload.vsh new file mode 100755 index 00000000..07c0841a --- /dev/null +++ b/cli/compile_upload.vsh @@ -0,0 +1,93 @@ +#!/usr/bin/env -S v run + +import os + +fn get_platform_id() string { + os_name := os.user_os() + arch := os.uname().machine + + return match os_name { + 'linux' { + match arch { + 'aarch64', 'arm64' { 'linux-arm64' } + 'x86_64' { 'linux-i64' } + else { 'unknown' } + } + } + 'macos' { + match arch { + 'arm64' { 'macos-arm64' } + 'x86_64' { 'macos-i64' } + else { 'unknown' } + } + } + else { + 'unknown' + } + } +} + +fn read_secrets() ! { + secret_file := os.join_path(os.home_dir(), 'code/git.ourworld.tf/despiegk/hero_secrets/mysecrets.sh') + if os.exists(secret_file) { + println('Reading secrets from ${secret_file}') + content := os.read_file(secret_file)! + lines := content.split('\n') + for line in lines { + if line.contains('export') { + parts := line.replace('export ', '').split('=') + if parts.len == 2 { + key := parts[0].trim_space() + value := parts[1].trim_space().trim('"').trim("'") + os.setenv(key, value, true) + } + } + } + } +} + +fn s3_configure() ! { + read_secrets()! + + // Check if environment variables are set + s3keyid := os.getenv_opt('S3KEYID') or { return error('S3KEYID is not set') } + s3appid := os.getenv_opt('S3APPID') or { return error('S3APPID is not set') } + + // Create rclone config file + rclone_dir := os.join_path(os.home_dir(), '.config/rclone') + os.mkdir_all(rclone_dir) or { return error('Failed to create rclone directory: ${err}') } + + rclone_conf := os.join_path(rclone_dir, 'rclone.conf') + config_content := '[b2] +type = b2 +account = ${s3keyid} +key = ${s3appid} +hard_delete = true' + + os.write_file(rclone_conf, config_content) or { return error('Failed to write rclone config: ${err}') } + + println('made S3 config on: ${rclone_conf}') + content := os.read_file(rclone_conf) or { return error('Failed to read rclone config: ${err}') } + println(content) +} + +fn hero_upload() ! { + hero_path := os.find_abs_path_of_executable('hero') or { return error("Error: 'hero' command not found in PATH") } + + s3_configure()! + + platform_id := get_platform_id() + rclone_conf := os.join_path(os.home_dir(), '.config/rclone/rclone.conf') + + println('Uploading hero binary for platform: ${platform_id}') + + // List contents + os.execute_or_panic('rclone --config="${rclone_conf}" lsl b2:threefold/${platform_id}/') + + // Copy hero binary + os.execute_or_panic('rclone --config="${rclone_conf}" copy "${hero_path}" b2:threefold/${platform_id}/') +} + +fn main() { + hero_upload() or { eprintln(err) exit(1) } +} diff --git a/lib/develop/gittools/README.md b/lib/develop/gittools/README.md new file mode 100644 index 00000000..8aa7efe8 --- /dev/null +++ b/lib/develop/gittools/README.md @@ -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 diff --git a/lib/develop/gittools/factory.v b/lib/develop/gittools/factory.v new file mode 100644 index 00000000..1f951213 --- /dev/null +++ b/lib/develop/gittools/factory.v @@ -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()! +} diff --git a/lib/develop/gittools/gitlocation.v b/lib/develop/gittools/gitlocation.v new file mode 100644 index 00000000..60ffb272 --- /dev/null +++ b/lib/develop/gittools/gitlocation.v @@ -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('/') +} diff --git a/lib/develop/gittools/gitstructure.v b/lib/develop/gittools/gitstructure.v new file mode 100644 index 00000000..4408ff8f --- /dev/null +++ b/lib/develop/gittools/gitstructure.v @@ -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 } +} diff --git a/lib/develop/gittools/gittools_do.v b/lib/develop/gittools/gittools_do.v new file mode 100644 index 00000000..f172726a --- /dev/null +++ b/lib/develop/gittools/gittools_do.v @@ -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}') +} diff --git a/lib/develop/gittools/repos_get.v b/lib/develop/gittools/repos_get.v new file mode 100644 index 00000000..8e7f4638 --- /dev/null +++ b/lib/develop/gittools/repos_get.v @@ -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) +} diff --git a/lib/develop/gittools/repos_print.v b/lib/develop/gittools/repos_print.v new file mode 100644 index 00000000..aef85494 --- /dev/null +++ b/lib/develop/gittools/repos_print.v @@ -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) +} diff --git a/lib/develop/gittools/repository.v b/lib/develop/gittools/repository.v new file mode 100644 index 00000000..691ffeac --- /dev/null +++ b/lib/develop/gittools/repository.v @@ -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 +} diff --git a/lib/develop/gittools/repository_cache.v b/lib/develop/gittools/repository_cache.v new file mode 100644 index 00000000..49c3daf0 --- /dev/null +++ b/lib/develop/gittools/repository_cache.v @@ -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 +} diff --git a/lib/develop/gittools/repository_clone.v b/lib/develop/gittools/repository_clone.v new file mode 100644 index 00000000..084d1870 --- /dev/null +++ b/lib/develop/gittools/repository_clone.v @@ -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 +} diff --git a/lib/develop/gittools/repository_info.v b/lib/develop/gittools/repository_info.v new file mode 100644 index 00000000..caef93f5 --- /dev/null +++ b/lib/develop/gittools/repository_info.v @@ -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}") +} diff --git a/lib/develop/gittools/repository_load.v b/lib/develop/gittools/repository_load.v new file mode 100644 index 00000000..013fe4b8 --- /dev/null +++ b/lib/develop/gittools/repository_load.v @@ -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 + } + } + } +} diff --git a/lib/develop/gittools/repository_utils.v b/lib/develop/gittools/repository_utils.v new file mode 100644 index 00000000..c2f34301 --- /dev/null +++ b/lib/develop/gittools/repository_utils.v @@ -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 /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()! +} diff --git a/lib/develop/gittools/tests/gittools_test.v b/lib/develop/gittools/tests/gittools_test.v new file mode 100644 index 00000000..575fcc84 --- /dev/null +++ b/lib/develop/gittools/tests/gittools_test.v @@ -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 +} diff --git a/lib/develop/gittools/tests/setup.v b/lib/develop/gittools/tests/setup.v new file mode 100644 index 00000000..59bf5853 --- /dev/null +++ b/lib/develop/gittools/tests/setup.v @@ -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)! +} diff --git a/lib/develop/performance/README.md b/lib/develop/performance/README.md new file mode 100644 index 00000000..36d16330 --- /dev/null +++ b/lib/develop/performance/README.md @@ -0,0 +1,64 @@ +# Performance Module + +A simple V module for measuring and visualizing process performance using Redis for data storage. + +## Features + +- **Timestamp Management**: Record timestamps for specific events during a process. +- **Epoch Handling**: Start and end measurement phases using epochs. +- **Timeline Visualization**: Display detailed timelines with duration bars and color-coded performance indicators. + +## Installation + +Install the repository and import the module: + +`import performance` + +## Usage + +### Create a Timer + +`mut timer := performance.new('my_process')` + +### Add Timestamps + +Record a timestamp for an event: + +`timer.new_timestamp('event_name')` + +### Manage Epochs + +Start or end a measurement phase: + +``` +timer.epoch() // Start a new epoch +timer.epoch_end() // End the current epoch +``` + +### Visualize Timelines + +Display the recorded timeline: + +`timer.timeline()` + +## Dependencies + + • Redis: Requires a Redis server for data storage. + • Redis Client: Uses freeflowuniverse.herolib.clients.redisclient. + +## Example +``` +mut timer := performance.new('example_process') + +timer.epoch() +timer.new_timestamp('start') +time.sleep(1 * time.second) +timer.new_timestamp('middle') +time.sleep(2 * time.second) +timer.new_timestamp('end') +timer.epoch_end() + +timer.timeline() +``` + +This will output a detailed timeline with duration bars for each event. \ No newline at end of file diff --git a/lib/develop/performance/performance.v b/lib/develop/performance/performance.v new file mode 100644 index 00000000..52710e29 --- /dev/null +++ b/lib/develop/performance/performance.v @@ -0,0 +1,130 @@ +module performance + +import arrays +import time +import sync +import term // For color coding +import freeflowuniverse.herolib.clients.redisclient + +// Struct to represent a timer for measuring process performance +@[noinit] +pub struct ProcessTimer { +pub: + name string // Name of the timer instance +} + +// Create a new ProcessTimer instance with a unique name including thread ID +pub fn new(name string) ProcessTimer { + return ProcessTimer{ + name: '${name}_${sync.thread_id()}' + } +} + +// Add a new timestamp to the current epoch for a specific name or event +pub fn (p ProcessTimer) new_timestamp(name_ string) { + mut name := name_ + mut redis := redisclient.core_get() or { panic(err) } // Get a Redis client + epoch := redis.get('${p.name}_epoch') or { '0' } // Fetch the current epoch value, default to '0' + all := redis.hgetall('${p.name}_${epoch}') or { + map[string]string{} + } // Get all timestamps for the current epoch + + // If a timestamp with the same name exists, make it unique + if name in all.keys() { + i := all.keys().filter(it.starts_with(name)).len + name = '${name}_${i}' + } + // Store the new timestamp in Redis + redis.hset('${p.name}_${epoch}', name, time.now().unix_micro().str()) or { panic(err) } +} + +// Increment the epoch value, effectively starting a new measurement phase +pub fn (p ProcessTimer) epoch() { + mut redis := redisclient.core_get() or { panic(err) } + redis.incr('${p.name}_epoch') or { panic(err) } +} + +// Increment the epoch value to signify the end of a measurement phase +pub fn (p ProcessTimer) epoch_end() { + mut redis := redisclient.core_get() or { panic(err) } + redis.incr('${p.name}_epoch') or { panic(err) } +} + +// Generate and display a timeline of events and their durations for each epoch +pub fn (p ProcessTimer) timeline() { + mut redis := redisclient.core_get() or { panic(err) } // Get a Redis client + epoch := redis.get('${p.name}_epoch') or { '0' } // Fetch the current epoch value + println(term.cyan('\nTimelines:\n')) // Print header + + // Loop through all epochs + for e in 0 .. epoch.int() { + result := redis.hgetall('${p.name}_${e}') or { continue } // Get all timestamps for the epoch + + if result.len == 0 { + // No data for this epoch + println(term.yellow('No timeline data found for process: ${p.name}_${e}')) + continue + } + + // Parse the results into a map of event names to timestamps + mut timestamps := map[string]i64{} + for key, value in result { + timestamps[key] = value.i64() + } + + // Calculate the durations between consecutive timestamps + mut durations := []i64{} + for i, timestamp in timestamps.values() { + prev_timestamp := if i == 0 { + timestamp + } else { + timestamps.values()[i - 1] + } + durations << timestamp - prev_timestamp + } + + // Find the maximum duration for normalization + max_duration := arrays.max(durations) or { 1 } + scale := 40.0 / f64(max_duration) // Scale for the timeline bar + + // Print the timeline for the epoch + println(term.cyan('\nProcess Timeline:\n')) + mut i := 0 + for key, timestamp in timestamps { + // Print event name and formatted timestamp + println('${key}: ${time.unix_micro(timestamp).format_rfc3339_micro()[10..]}') + + // Calculate and display the duration bar + prev_timestamp := if i == 0 { + 0 + } else { + timestamps.values()[i - 1] + } + if i == timestamps.len - 1 { + continue + } + duration := durations[i + 1] + + // Determine bar length and color based on duration + bar_length := int(duration * scale) + color := if duration < max_duration / 3 { + term.green + } else if duration < 2 * max_duration / 3 { + term.yellow + } else { + term.red + } + + // Create the bar visualization + bar := if duration == 0 { + '' + } else { + color('|') + color('-'.repeat(bar_length)) + color(' '.repeat(40 - bar_length)) + + color('|') + } + println('${bar} (${duration}μs)') + i++ + } + } + println('\n') // End with a newline +} diff --git a/lib/develop/sourcetree/readme.md b/lib/develop/sourcetree/readme.md new file mode 100644 index 00000000..29808e1c --- /dev/null +++ b/lib/develop/sourcetree/readme.md @@ -0,0 +1,11 @@ +## sourcetree + +```v +import freeflowuniverse.herolib.develop.sourcetree + +//will look for git in location if not found will give error +sourcetree.open(path:"/tmp/something")! + +``` + +- if path not specified will chose current path \ No newline at end of file diff --git a/lib/develop/sourcetree/sourcetree.v b/lib/develop/sourcetree/sourcetree.v new file mode 100644 index 00000000..4c332e28 --- /dev/null +++ b/lib/develop/sourcetree/sourcetree.v @@ -0,0 +1,22 @@ +module sourcetree + +import freeflowuniverse.herolib.osal +import os +// import freeflowuniverse.herolib.ui.console + +@[params] +pub struct OpenArgs { +pub mut: + path string +} + +// will look for git in location if not found will give error +// if not specified will use current dir +pub fn open(args OpenArgs) ! { + if !os.exists(args.path) { + return error('Cannot open SourceTree: could not find path ${args.path}') + } + cmd4 := 'open -a SourceTree ${args.path}' + // console.print_debug(cmd4) + osal.execute_interactive(cmd4)! +} diff --git a/lib/develop/vscode/readme.md b/lib/develop/vscode/readme.md new file mode 100644 index 00000000..80680bb5 --- /dev/null +++ b/lib/develop/vscode/readme.md @@ -0,0 +1,10 @@ +## visual studio code + +```v +import freeflowuniverse.herolib.develop.vscode + +vscode.open(path:"/tmp/something")! + +``` + +- if path not specified will chose current path \ No newline at end of file diff --git a/lib/develop/vscode/vscode.v b/lib/develop/vscode/vscode.v new file mode 100644 index 00000000..33b6e33d --- /dev/null +++ b/lib/develop/vscode/vscode.v @@ -0,0 +1,63 @@ +module vscode + +import freeflowuniverse.herolib.osal +import os + +pub struct VSCodeHelper { +pub mut: + install_if_not_exists bool + path string +} + +pub fn new(path string) VSCodeHelper { + return VSCodeHelper{ + path: if path == '' { + os.getwd() + } else { + path + } + } +} + +// Open Visual Studio Code at the specified path. +// If the path is not provided, it defaults to the current working directory. +pub fn (self VSCodeHelper) open() ! { + self.check_installation()! + + if !os.exists(self.path) { + return error('Cannot open Visual Studio Code: path not found: ${self.path}') + } + + cmd := '${self.get_executable_binary()} ${self.path}' + osal.execute_interactive(cmd)! +} + +// Get the executable binary for Visual Studio Code. +fn (self VSCodeHelper) get_executable_binary() string { + if self.is_installed() { + if osal.cmd_exists('vscode') { + return 'vscode' + } + return 'code' + } + return '' +} + +// Check if Visual Studio Code is installed. +pub fn (self VSCodeHelper) is_installed() bool { + return osal.cmd_exists('vscode') || osal.cmd_exists('code') +} + +// Check the installation status of Visual Studio Code. +// If not installed and the flag is set, attempt to install it. +pub fn (self VSCodeHelper) check_installation() ! { + if !self.is_installed() { + if self.install_if_not_exists { + // Uncomment and implement the installation logic if needed + // vscodeinstaller.install()! + // return check_installation() + return error('Visual Studio Code is not installed.\nPlease see https://code.visualstudio.com/download') + } + return error('Visual Studio Code is not installed.\nPlease see https://code.visualstudio.com/download') + } +} diff --git a/lib/readme.md b/lib/readme.md index 4e9b0aa1..bf357eb2 100644 --- a/lib/readme.md +++ b/lib/readme.md @@ -1,22 +1,22 @@ -# Crystallib +# herolib Is an opinionated library as used by threefold mainly to automate cloud environments, its still very much work in progress and we welcome any contribution. -Please check also our [cookbook](https://github.com/freeflowuniverse/crystallib/tree/development/cookbook) which might give some ideas how to use it. +Please check also our [cookbook](https://github.com/freeflowuniverse/herolib/tree/development/cookbook) which might give some ideas how to use it. ## Get started with hero ```bash -curl -sL https://raw.githubusercontent.com/freeflowuniverse/crystallib/development/scripts/install_hero.sh | bash +curl -sL https://raw.githubusercontent.com/freeflowuniverse/herolib/development/scripts/install_hero.sh | bash ``` -## Get started with crystallib +## Get started with herolib -the following script will install vlang and crystallib (report bugs please) +the following script will install vlang and herolib (report bugs please) ```bash -curl https://raw.githubusercontent.com/freeflowuniverse/crystallib/development/scripts/installer.sh > /tmp/install.sh +curl https://raw.githubusercontent.com/freeflowuniverse/herolib/development/scripts/installer.sh > /tmp/install.sh bash /tmp/install.sh ``` @@ -34,8 +34,8 @@ requirements ```bash mkdir -p ~/code/github/freeflowuniverse cd ~/code/github/freeflowuniverse -git clone git@github.com:freeflowuniverse/crystallib.git -cd crystallib +git clone git@github.com:freeflowuniverse/herolib.git +cd herolib # checkout a branch with most recent changes # git checkout development bash install.sh @@ -52,7 +52,7 @@ hero will be installed in - ~/hero/bin for osx ```bash -curl https://raw.githubusercontent.com/freeflowuniverse/crystallib/development/scripts/install_hero.sh > /tmp/hero_install.sh +curl https://raw.githubusercontent.com/freeflowuniverse/herolib/development/scripts/install_hero.sh > /tmp/hero_install.sh bash /tmp/hero_install.sh #to debug bash -x /tmp/hero_install.sh @@ -72,7 +72,7 @@ requirements ```bash #cd in this directory -cd ~/code/github/freeflowuniverse/crystallib +cd ~/code/github/freeflowuniverse/herolib bash doc.sh ``` diff --git a/lib/v.mod b/lib/v.mod index 7efea51c..631381cb 100644 --- a/lib/v.mod +++ b/lib/v.mod @@ -1,9 +1,9 @@ Module { - name: 'crystallib' + name: 'herolib' author: 'freeflowuniverse' description: 'Set of various libraries' version: '0.1.0' - repo_url: 'https://github.com/freeflowuniverse/crystallib/crystallib' + repo_url: 'https://github.com/freeflowuniverse/herolib/herolib' deps: [] vcs: 'git' license: 'apache2' diff --git a/lib/vpkg.json b/lib/vpkg.json index 2a9e070e..5f7b2f81 100644 --- a/lib/vpkg.json +++ b/lib/vpkg.json @@ -1,10 +1,10 @@ { - "name": "crystallib", + "name": "herolib", "version": "0.1.0", "author": [ "despiegk " ], - "repo": "https://github.com/crystaluniverse/crystallib/crystallib", + "repo": "https://github.com/herouniverse/herolib/herolib", "sources": [ "https://vpkg-project.github.io/registry/src/" ],