implemented user defaults function

This commit is contained in:
Kevin Veen-Birkenbach
2025-03-04 14:51:31 +01:00
parent 47ddf75148
commit 9f8c5c4cc1
2 changed files with 73 additions and 58 deletions

View File

@@ -19,3 +19,4 @@ repos:
setup: '' setup: ''
teardown: '' teardown: ''
verified: '' verified: ''
alias: 'fh'

130
main.py
View File

@@ -7,11 +7,11 @@ This script provides the following commands:
- Creates an executable bash wrapper in the bin folder that calls the command for the repository. - Creates an executable bash wrapper in the bin folder that calls the command for the repository.
- Executes the repositorys "setup" command if specified. - Executes the repositorys "setup" command if specified.
pull {identifier(s)|--all} [<git-args>...] pull {identifier(s)|--all} [<git-args>...]
- Executes 'git pull' in the repository directory with any extra arguments passed. - Executes 'git pull' with any extra arguments passed.
clone {identifier(s)|--all} clone {identifier(s)|--all}
- Clones the repository from a remote. - Clones the repository from a remote.
push {identifier(s)|--all} [<git-args>...] push {identifier(s)|--all} [<git-args>...]
- Executes 'git push' in the repository directory with any extra arguments passed. - Executes 'git push' with any extra arguments passed.
deinstall {identifier(s)|--all} deinstall {identifier(s)|--all}
- Removes the executable alias. - Removes the executable alias.
- Executes the repositorys "teardown" command if specified. - Executes the repositorys "teardown" command if specified.
@@ -20,9 +20,7 @@ This script provides the following commands:
update {identifier(s)|--all|--system} update {identifier(s)|--all|--system}
- Combines pull and install; if --system is specified also runs system update commands. - Combines pull and install; if --system is specified also runs system update commands.
status {identifier(s)|--all} [<git-args>...] [--system] [--list] status {identifier(s)|--all} [<git-args>...] [--system] [--list]
- Executes 'git status' in the repository directory with any extra arguments passed. - Executes 'git status' with any extra arguments passed.
- With --system, shows system update information.
- With --list, only the identifiers are printed.
diff {identifier(s)|--all} [<git-args>...] diff {identifier(s)|--all} [<git-args>...]
- Executes 'git diff' with any extra arguments passed. - Executes 'git diff' with any extra arguments passed.
add {identifier(s)|--all} [<git-args>...] add {identifier(s)|--all} [<git-args>...]
@@ -34,11 +32,11 @@ This script provides the following commands:
Additionally, there is a **config** command with subcommands: Additionally, there is a **config** command with subcommands:
config show {identifier(s)|--all} config show {identifier(s)|--all}
- Displays configuration for one or more repositories (or the entire config if no identifier is given). - Displays the merged configuration (defaults plus user additions).
config add config add
- Starts an interactive dialog to add a new repository configuration entry. - Interactively adds a new repository entry to the user configuration.
config edit config edit
- Opens the configuration file (config.yaml) in nano. - Opens the user configuration file (config/config.yaml) in nano.
Additional flags: Additional flags:
--preview Only show the changes without executing commands. --preview Only show the changes without executing commands.
@@ -48,19 +46,11 @@ Identifiers:
- If a repositorys name is unique then you can just use the repository name. - If a repositorys name is unique then you can just use the repository name.
- Otherwise use "provider/account/repository". - Otherwise use "provider/account/repository".
Configuration is read from a YAML file (default: config.yaml) with the following structure: Configuration is merged from two files:
- **config/defaults.yaml** (system defaults)
- **config/config.yaml** (user-specific configuration)
base: "/path/to/repositories" The user config supplements the default repositories and can override the base path.
repos:
- provider: "github.com"
account: "youraccount"
repository: "mytool"
verified: "commit-id"
command: "bash main.sh" # optional; if not set, the script checks for main.sh/main.py in the repository
description: "My tool description" # optional
replacement: "" # optional; alternative provider/account/repository string to use instead
setup: "echo Setting up..." # optional setup command executed during install
teardown: "echo Tearing down..." # optional command executed during deinstall
""" """
@@ -71,27 +61,46 @@ import shutil
import sys import sys
import yaml import yaml
# Define default paths # Define configuration file paths.
CONFIG_PATH = "config.yaml" DEFAULT_CONFIG_PATH = os.path.join("config", "defaults.yaml")
USER_CONFIG_PATH = os.path.join("config", "config.yaml")
BIN_DIR = os.path.expanduser("~/.local/bin") BIN_DIR = os.path.expanduser("~/.local/bin")
def load_config(config_path): def load_config():
"""Load YAML configuration file.""" """Load configuration from defaults and merge in user config if present.
if not os.path.exists(config_path):
print(f"Configuration file '{config_path}' not found.") If the user config defines a 'base', it overrides the default.
The user config's 'repos' are appended to the default repos.
"""
if not os.path.exists(DEFAULT_CONFIG_PATH):
print(f"Default configuration file '{DEFAULT_CONFIG_PATH}' not found.")
sys.exit(1) sys.exit(1)
with open(config_path, 'r') as f: with open(DEFAULT_CONFIG_PATH, 'r') as f:
config = yaml.safe_load(f) config = yaml.safe_load(f)
# Verify defaults have required keys.
if "base" not in config or "repos" not in config: if "base" not in config or "repos" not in config:
print("Config file must contain 'base' and 'repos' keys.") print("Default config file must contain 'base' and 'repos' keys.")
sys.exit(1) sys.exit(1)
# If user config exists, merge it.
if os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, 'r') as f:
user_config = yaml.safe_load(f)
if user_config:
# Override base if defined.
if "base" in user_config:
config["base"] = user_config["base"]
# Append any user-defined repos.
if "repos" in user_config:
config["repos"].extend(user_config["repos"])
return config return config
def save_config(config, config_path): def save_user_config(user_config):
"""Save the config dictionary back to the YAML file.""" """Save the user configuration to USER_CONFIG_PATH."""
with open(config_path, 'w') as f: os.makedirs(os.path.dirname(USER_CONFIG_PATH), exist_ok=True)
yaml.dump(config, f) with open(USER_CONFIG_PATH, 'w') as f:
print(f"Configuration updated in {config_path}.") yaml.dump(user_config, f)
print(f"User configuration updated in {USER_CONFIG_PATH}.")
def run_command(command, cwd=None, preview=False): def run_command(command, cwd=None, preview=False):
"""Run a shell command in a given directory, or print it in preview mode.""" """Run a shell command in a given directory, or print it in preview mode."""
@@ -140,8 +149,7 @@ def create_executable(repo, base_dir, bin_dir, all_repos, preview=False):
If 'verified' is set, the wrapper will checkout that commit and warn in orange if it does not match. If 'verified' is set, the wrapper will checkout that commit and warn in orange if it does not match.
If no verified commit is set, a warning in orange is printed. If no verified commit is set, a warning in orange is printed.
If an 'alias' field is provided, a symlink is created in the bin directory with that name.
If an 'alias' field is provided in the configuration, a symlink with that alias is created in the bin directory.
""" """
repo_identifier = get_repo_identifier(repo, all_repos) repo_identifier = get_repo_identifier(repo, all_repos)
repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository")) repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
@@ -157,7 +165,7 @@ def create_executable(repo, base_dir, bin_dir, all_repos, preview=False):
print(f"No command defined and no main.sh/main.py found in {repo_dir}. Skipping alias creation.") print(f"No command defined and no main.sh/main.py found in {repo_dir}. Skipping alias creation.")
return return
# ANSI escape codes for orange (color code 208) and reset # ANSI escape codes for orange (color code 208) and reset.
ORANGE = r"\033[38;5;208m" ORANGE = r"\033[38;5;208m"
RESET = r"\033[0m" RESET = r"\033[0m"
@@ -188,7 +196,7 @@ cd "{repo_dir}"
os.chmod(alias_path, 0o755) os.chmod(alias_path, 0o755)
print(f"Installed executable for {repo_identifier} at {alias_path}") print(f"Installed executable for {repo_identifier} at {alias_path}")
# Create alias if the 'alias' field is provided in the config. # Create alias if provided in the config.
alias_name = repo.get("alias") alias_name = repo.get("alias")
if alias_name: if alias_name:
alias_link_path = os.path.join(bin_dir, alias_name) alias_link_path = os.path.join(bin_dir, alias_name)
@@ -199,7 +207,7 @@ cd "{repo_dir}"
print(f"Created alias '{alias_name}' pointing to {repo_identifier}") print(f"Created alias '{alias_name}' pointing to {repo_identifier}")
except Exception as e: except Exception as e:
print(f"Error creating alias '{alias_name}': {e}") print(f"Error creating alias '{alias_name}': {e}")
def install_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False): def install_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False):
"""Install repositories by creating executable wrappers and running setup.""" """Install repositories by creating executable wrappers and running setup."""
for repo in selected_repos: for repo in selected_repos:
@@ -225,7 +233,7 @@ def exec_git_command(selected_repos, base_dir, all_repos, git_cmd, extra_args, p
else: else:
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.") print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
# Refactored functions for pull, push, and status. # Refactored git command functions.
def pull_repos(selected_repos, base_dir, all_repos, extra_args, preview=False): def pull_repos(selected_repos, base_dir, all_repos, extra_args, preview=False):
exec_git_command(selected_repos, base_dir, all_repos, "pull", extra_args, preview) exec_git_command(selected_repos, base_dir, all_repos, "pull", extra_args, preview)
@@ -243,7 +251,6 @@ def status_repos(selected_repos, base_dir, all_repos, extra_args, list_only=Fals
exec_git_command(selected_repos, base_dir, all_repos, "status", extra_args, preview) exec_git_command(selected_repos, base_dir, all_repos, "status", extra_args, preview)
def clone_repos(selected_repos, base_dir, all_repos, preview=False): def clone_repos(selected_repos, base_dir, all_repos, preview=False):
"""Clone repositories based on the config."""
for repo in selected_repos: for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos) repo_identifier = get_repo_identifier(repo, all_repos)
repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository")) repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
@@ -257,7 +264,6 @@ def clone_repos(selected_repos, base_dir, all_repos, preview=False):
run_command(f"git clone {clone_url} {repo_dir}", cwd=parent_dir, preview=preview) run_command(f"git clone {clone_url} {repo_dir}", cwd=parent_dir, preview=preview)
def deinstall_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False): def deinstall_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False):
"""Remove the executable wrapper and run teardown if defined."""
for repo in selected_repos: for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos) repo_identifier = get_repo_identifier(repo, all_repos)
alias_path = os.path.join(bin_dir, repo_identifier) alias_path = os.path.join(bin_dir, repo_identifier)
@@ -275,7 +281,6 @@ def deinstall_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False)
run_command(teardown_cmd, cwd=repo_dir, preview=preview) run_command(teardown_cmd, cwd=repo_dir, preview=preview)
def delete_repos(selected_repos, base_dir, all_repos, preview=False): def delete_repos(selected_repos, base_dir, all_repos, preview=False):
"""Delete the repository directory."""
for repo in selected_repos: for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos) repo_identifier = get_repo_identifier(repo, all_repos)
repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository")) repo_dir = os.path.join(base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
@@ -289,14 +294,13 @@ def delete_repos(selected_repos, base_dir, all_repos, preview=False):
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.") print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
def update_repos(selected_repos, base_dir, bin_dir, all_repos, system_update=False, preview=False): def update_repos(selected_repos, base_dir, bin_dir, all_repos, system_update=False, preview=False):
"""Combine pull and install. If system_update is True, run system update commands."""
pull_repos(selected_repos, base_dir, all_repos, extra_args=[], preview=preview) pull_repos(selected_repos, base_dir, all_repos, extra_args=[], preview=preview)
install_repos(selected_repos, base_dir, BIN_DIR, all_repos, preview=preview) install_repos(selected_repos, base_dir, bin_dir, all_repos, preview=preview)
if system_update: if system_update:
run_command("yay -S", preview=preview) run_command("yay -S", preview=preview)
run_command("sudo pacman -Syyu", preview=preview) run_command("sudo pacman -Syyu", preview=preview)
# New functions for additional git commands. # Additional git commands.
def diff_repos(selected_repos, base_dir, all_repos, extra_args, preview=False): def diff_repos(selected_repos, base_dir, all_repos, extra_args, preview=False):
exec_git_command(selected_repos, base_dir, all_repos, "diff", extra_args, preview) exec_git_command(selected_repos, base_dir, all_repos, "diff", extra_args, preview)
@@ -310,10 +314,11 @@ def checkout_repos(selected_repos, base_dir, all_repos, extra_args, preview=Fals
exec_git_command(selected_repos, base_dir, all_repos, "checkout", extra_args, preview) exec_git_command(selected_repos, base_dir, all_repos, "checkout", extra_args, preview)
def show_config(selected_repos, full_config=False): def show_config(selected_repos, full_config=False):
"""Display configuration for one or more repositories, or entire config.""" """Display configuration for one or more repositories, or entire merged config."""
if full_config: if full_config:
with open(CONFIG_PATH, 'r') as f: # Print the merged config.
print(f.read()) merged = load_config()
print(yaml.dump(merged, default_flow_style=False))
else: else:
for repo in selected_repos: for repo in selected_repos:
identifier = f'{repo.get("provider")}/{repo.get("account")}/{repo.get("repository")}' identifier = f'{repo.get("provider")}/{repo.get("account")}/{repo.get("repository")}'
@@ -323,7 +328,10 @@ def show_config(selected_repos, full_config=False):
print("-" * 40) print("-" * 40)
def interactive_add(config): def interactive_add(config):
"""Interactively prompt the user to add a new repository entry to the config.""" """Interactively prompt the user to add a new repository entry to the user config.
The new entry is saved into the user config file (config/config.yaml).
"""
print("Adding a new repository configuration entry.") print("Adding a new repository configuration entry.")
new_entry = {} new_entry = {}
new_entry["provider"] = input("Provider (e.g., github.com): ").strip() new_entry["provider"] = input("Provider (e.g., github.com): ").strip()
@@ -335,25 +343,32 @@ def interactive_add(config):
new_entry["replacement"] = input("Replacement (optional): ").strip() new_entry["replacement"] = input("Replacement (optional): ").strip()
new_entry["setup"] = input("Setup command (optional): ").strip() new_entry["setup"] = input("Setup command (optional): ").strip()
new_entry["teardown"] = input("Teardown command (optional): ").strip() new_entry["teardown"] = input("Teardown command (optional): ").strip()
new_entry["alias"] = input("Alias (optional): ").strip()
print("\nNew entry:") print("\nNew entry:")
for key, value in new_entry.items(): for key, value in new_entry.items():
if value: if value:
print(f"{key}: {value}") print(f"{key}: {value}")
confirm = input("Add this entry to config? (y/N): ").strip().lower() confirm = input("Add this entry to user config? (y/N): ").strip().lower()
if confirm == "y": if confirm == "y":
config["repos"].append(new_entry) # Load existing user config or initialize.
save_config(config, CONFIG_PATH) if os.path.exists(USER_CONFIG_PATH):
with open(USER_CONFIG_PATH, 'r') as f:
user_config = yaml.safe_load(f) or {}
else:
user_config = {}
user_config.setdefault("repos", [])
user_config["repos"].append(new_entry)
save_user_config(user_config)
else: else:
print("Entry not added.") print("Entry not added.")
def edit_config(): def edit_config():
"""Open the configuration file in nano.""" """Open the user configuration file in nano."""
run_command(f"nano {CONFIG_PATH}") run_command(f"nano {USER_CONFIG_PATH}")
if __name__ == "__main__": if __name__ == "__main__":
# Load configuration config = load_config()
config = load_config(CONFIG_PATH)
base_dir = os.path.expanduser(config["base"]) base_dir = os.path.expanduser(config["base"])
all_repos_list = config["repos"] all_repos_list = config["repos"]
@@ -418,7 +433,6 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
# Dispatch top-level commands
if args.command == "install": if args.command == "install":
selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list) selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list)
install_repos(selected, base_dir, BIN_DIR, all_repos_list, preview=args.preview) install_repos(selected, base_dir, BIN_DIR, all_repos_list, preview=args.preview)
@@ -447,7 +461,7 @@ if __name__ == "__main__":
selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list) selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list)
diff_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview) diff_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview)
elif args.command == "add": elif args.command == "add":
# Top-level git add command. # Top-level 'add' is used for git add.
selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list) selected = all_repos_list if args.all or (not args.identifiers) else resolve_repos(args.identifiers, all_repos_list)
gitadd_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview) gitadd_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview)
elif args.command == "show": elif args.command == "show":