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: ''
teardown: ''
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.
- Executes the repositorys "setup" command if specified.
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}
- Clones the repository from a remote.
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}
- Removes the executable alias.
- Executes the repositorys "teardown" command if specified.
@@ -20,9 +20,7 @@ This script provides the following commands:
update {identifier(s)|--all|--system}
- Combines pull and install; if --system is specified also runs system update commands.
status {identifier(s)|--all} [<git-args>...] [--system] [--list]
- Executes 'git status' in the repository directory with any extra arguments passed.
- With --system, shows system update information.
- With --list, only the identifiers are printed.
- Executes 'git status' with any extra arguments passed.
diff {identifier(s)|--all} [<git-args>...]
- Executes 'git diff' with any extra arguments passed.
add {identifier(s)|--all} [<git-args>...]
@@ -34,11 +32,11 @@ This script provides the following commands:
Additionally, there is a **config** command with subcommands:
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
- Starts an interactive dialog to add a new repository configuration entry.
- Interactively adds a new repository entry to the user configuration.
config edit
- Opens the configuration file (config.yaml) in nano.
- Opens the user configuration file (config/config.yaml) in nano.
Additional flags:
--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.
- 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"
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
The user config supplements the default repositories and can override the base path.
"""
@@ -71,27 +61,46 @@ import shutil
import sys
import yaml
# Define default paths
CONFIG_PATH = "config.yaml"
# Define configuration file paths.
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")
def load_config(config_path):
"""Load YAML configuration file."""
if not os.path.exists(config_path):
print(f"Configuration file '{config_path}' not found.")
def load_config():
"""Load configuration from defaults and merge in user config if present.
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)
with open(config_path, 'r') as f:
with open(DEFAULT_CONFIG_PATH, 'r') as f:
config = yaml.safe_load(f)
# Verify defaults have required keys.
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)
# 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
def save_config(config, config_path):
"""Save the config dictionary back to the YAML file."""
with open(config_path, 'w') as f:
yaml.dump(config, f)
print(f"Configuration updated in {config_path}.")
def save_user_config(user_config):
"""Save the user configuration to USER_CONFIG_PATH."""
os.makedirs(os.path.dirname(USER_CONFIG_PATH), exist_ok=True)
with open(USER_CONFIG_PATH, 'w') as f:
yaml.dump(user_config, f)
print(f"User configuration updated in {USER_CONFIG_PATH}.")
def run_command(command, cwd=None, preview=False):
"""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 no verified commit is set, a warning in orange is printed.
If an 'alias' field is provided in the configuration, a symlink with that alias is created in the bin directory.
If an 'alias' field is provided, a symlink is created in the bin directory with that name.
"""
repo_identifier = get_repo_identifier(repo, all_repos)
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.")
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"
RESET = r"\033[0m"
@@ -188,7 +196,7 @@ cd "{repo_dir}"
os.chmod(alias_path, 0o755)
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")
if 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}")
except Exception as e:
print(f"Error creating alias '{alias_name}': {e}")
def install_repos(selected_repos, base_dir, bin_dir, all_repos, preview=False):
"""Install repositories by creating executable wrappers and running setup."""
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:
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):
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)
def clone_repos(selected_repos, base_dir, all_repos, preview=False):
"""Clone repositories based on the config."""
for repo in selected_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"))
@@ -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)
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:
repo_identifier = get_repo_identifier(repo, all_repos)
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)
def delete_repos(selected_repos, base_dir, all_repos, preview=False):
"""Delete the repository directory."""
for repo in selected_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"))
@@ -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}.")
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)
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:
run_command("yay -S", 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):
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)
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:
with open(CONFIG_PATH, 'r') as f:
print(f.read())
# Print the merged config.
merged = load_config()
print(yaml.dump(merged, default_flow_style=False))
else:
for repo in selected_repos:
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)
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.")
new_entry = {}
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["setup"] = input("Setup command (optional): ").strip()
new_entry["teardown"] = input("Teardown command (optional): ").strip()
new_entry["alias"] = input("Alias (optional): ").strip()
print("\nNew entry:")
for key, value in new_entry.items():
if 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":
config["repos"].append(new_entry)
save_config(config, CONFIG_PATH)
# Load existing user config or initialize.
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:
print("Entry not added.")
def edit_config():
"""Open the configuration file in nano."""
run_command(f"nano {CONFIG_PATH}")
"""Open the user configuration file in nano."""
run_command(f"nano {USER_CONFIG_PATH}")
if __name__ == "__main__":
# Load configuration
config = load_config(CONFIG_PATH)
config = load_config()
base_dir = os.path.expanduser(config["base"])
all_repos_list = config["repos"]
@@ -418,7 +433,6 @@ if __name__ == "__main__":
args = parser.parse_args()
# Dispatch top-level commands
if args.command == "install":
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)
@@ -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)
diff_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview)
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)
gitadd_repos(selected, base_dir, all_repos_list, args.extra_args, preview=args.preview)
elif args.command == "show":