diff --git a/CHANGELOG.md b/CHANGELOG.md index c6aebef..37707bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - **Auto SSH Commit Signing:** GitGo now uses the SSH key generated during `gitgo user login` to automatically sign all commits using temporary `-c` flags, giving you the Verified badge on GitHub without modifying global git configs. ### Changed +- Refactored CLI messaging to use a consistent tone, removing robotic phrases and filler words. +- Standardized terminal output spacing and colors across all commands. +- Updated success banners to a concise, military-style format for major operations. +- Improved error messages to provide actionable next steps instead of generic advice. - Refactored codebase to standardize internal API returns, remove redundant checks, and optimize stash operations. ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 5734f4e..299fa11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygitgo" -version = "1.7.0b3" +version = "1.7.0b4" description = "GitGo CLI - Your Fast Git Companion. Simplifies git push, link, stash, and user management." readme = "README.md" license = {text = "GPL-3.0-or-later"} diff --git a/src/pygitgo/commands/git_operations.py b/src/pygitgo/commands/git_operations.py index 5a141da..723f7c1 100644 --- a/src/pygitgo/commands/git_operations.py +++ b/src/pygitgo/commands/git_operations.py @@ -11,7 +11,7 @@ def get_status_content(): status = run_command(["git", "status", "--porcelain"], allow_fail=True) if command_failed(status) or not status.strip(): - raise GitGoError("\nWorking tree is clean. Nothing to commit.\n") + raise GitGoError("Working tree is clean. Nothing to commit.") return status def get_current_branch(): @@ -25,7 +25,7 @@ def get_main_branch(): default_main_branch = get_config("default-branch", "main") if command_failed(main_branch): return default_main_branch - + return main_branch.split("HEAD branch:")[-1].strip().splitlines()[0].strip() if "HEAD branch:" in main_branch else default_main_branch def is_branch_exist(branch): @@ -41,9 +41,10 @@ def git_new_branch(branch): from pygitgo.commands.jump import jump_operation jump_operation(Namespace(branch=branch)) else: - raise GitGoError(f"\nOperation canceled. Branch '{branch}' already exists.\n") + raise GitGoError(f"Operation canceled. Branch '{branch}' already exists.") else: - success(f"\nBranch '{branch}' created.\n") + print() + success(f"Branch '{branch}' created.") return branch @@ -64,7 +65,7 @@ def git_commit(commit_message, loading_msg="Commiting changes...", skip_staging= if not skip_staging: run_command(["git", "add", "."], loading_msg="Staging files...") - + clean_message = commit_message.strip('"\'') signing_flags = _get_signing_flags() @@ -78,7 +79,7 @@ def git_init(): if os.path.isdir(".git"): warning("Already a git repository! Skipping init...") return False - + default_main_branch = get_config("default-branch", "main") result = run_command(["git", "init", "-b", default_main_branch], allow_fail=True, loading_msg="Initializing git repository...") @@ -92,32 +93,32 @@ def git_init(): def add_remote_origin(repo_url): clean_url = repo_url.strip('"\'') - + existing_remote = run_command(["git", "remote", "get-url", "origin"], allow_fail=True) if not command_failed(existing_remote): warning(f"Remote origin already exists: {existing_remote}") run_command(["git", "remote", "set-url", "origin", clean_url], loading_msg="Updating remote URL...") else: run_command(["git", "remote", "add", "origin", clean_url], loading_msg="Adding remote origin...") - + success(f"Remote origin set to: {clean_url}") def confirm_remote_link(): test_result = run_command(["git", "ls-remote", "origin"], allow_fail=True, loading_msg="Testing connection to remote...") - + if command_failed(test_result): - error("Failed to connect to remote repository!") - warning("Please check your repository URL and network connection.") + error("Connection failed — verify the URL and your SSH key.") + info("Run: git remote -v to inspect your current remote.") return False - - success("Successfully connected to remote repository.") + + success("Remote is reachable.") return True def create_main_branch(): current_branch = run_command(["git", "branch", "--show-current"], allow_fail=True) - + if command_failed(current_branch) or not current_branch.strip(): run_command(["git", "checkout", "-b", "main"], loading_msg="Setting default branch to 'main'...") elif current_branch.strip() != "main": @@ -143,9 +144,9 @@ def check_and_sync_branch(branch): output = run_command(["git", "pull", "--rebase", "origin", branch], loading_msg="Pulling changes from remote...") if output: print(output) - success("Successfully synced with remote!") + success("Synced with remote.") else: - success("Branch is up to date or ahead of remote.") + success("Branch is up to date.") else: success("Branch is already up to date.") except (GitCommandError, ValueError): @@ -156,25 +157,25 @@ def check_and_sync_branch(branch): def git_push(branch): remote_url = run_command(["git", "remote", "get-url", "origin"], allow_fail=True) - + if not command_failed(remote_url) and remote_url: remote_url = remote_url.strip() - + if not is_ssh_url(remote_url) and check_connection(): ssh_url = convert_https_to_ssh(remote_url) if ssh_url: run_command(["git", "remote", "set-url", "origin", ssh_url], loading_msg="Converting remote from HTTPS to SSH for secure push...") success(f"Remote updated to: {ssh_url}") - try: + try: run_command(["git", "push", "-u", "origin", branch], loading_msg=f"Pushing to remote branch '{branch}'...") except (GitCommandError, OSError) as e: - error("Failed to push to remote repository!") - warning("Please check your network connection, remote URL, and authentication.") + error("Push failed — verify your remote URL and SSH key, then try again.") + info("Run: git remote -v to inspect your current remote.") if "rebase in progress" in str(e): handle_rebase() else: - raise GitGoError() + raise GitGoError("Push failed — see above.") def handle_rebase(): @@ -183,11 +184,11 @@ def handle_rebase(): return False if "rebase in progress" in status or "rebase" in status.lower(): - warning("\nConflict detected!") - warning("Please resolve conflicts manually, then run:") + warning("Conflict detected during rebase.") + info("Resolve conflicts manually, then run:") info(" git add ") info(" git rebase --continue") - warning("When finished, run 'gitgo push ' again.\n") - raise GitGoError() + info("When finished, run 'gitgo push ' again.") + raise GitGoError("Push aborted — rebase conflict in progress.") return True diff --git a/src/pygitgo/commands/jump.py b/src/pygitgo/commands/jump.py index bfe03b5..9f59d74 100644 --- a/src/pygitgo/commands/jump.py +++ b/src/pygitgo/commands/jump.py @@ -21,54 +21,59 @@ def undo_jump_operation(original_branch, stashed_code, created_branch=None): if stashed_code: pop_result = git_stash_pop() if not pop_result: - warning("\nCould not restore your unsaved changes automatically. Run 'gitgo state list' to recover them.\n") + warning("Could not restore your unsaved changes automatically. Run 'gitgo state list' to recover them.") - success(f"\nCanceled safely!") - success(f"You are back on your original branch '{original_branch}', and your code is totally safe.\n") + print() + success(f"Canceled safely.") + success(f"Back on '{original_branch}'. Your code is safe.") -def jump_operation(args): +def jump_operation(args): target_branch = args.branch original_branch = get_current_branch() - + if original_branch == target_branch: - warning(f"\nYou are already on branch '{target_branch}'.\n") + warning(f"Already on branch '{target_branch}'.") return has_changes = run_command(['git', 'status', '--porcelain'], allow_fail=True, loading_msg="Checking for uncommitted changes...") if command_failed(has_changes): - raise GitGoError("\nUnable to check for uncommitted changes. Please ensure you're in a valid git repository.\n") - + raise GitGoError("Unable to check for uncommitted changes — make sure you're in a valid git repository.") + stashed_code = False if has_changes.strip(): - info("\nYou have unsaved changes here.") + print() + info("You have unsaved changes here.") user_input = input("Do you want to move these changes to your new branch? (y/n): ").strip().lower() if user_input != 'y': - warning("\nYou cannot switch branches with unsaved changes. Jump canceled.\n") + print() + warning("You cannot switch branches with unsaved changes. Jump canceled.") return else: stash_result = git_stash_push(label="GitGo Jump Auto-Stash", loading_msg="Saving your changes before jumping...") if not stash_result: - warning("\nFailed to save your changes. Please resolve any issues and try again.") - raise GitGoError() - info("\nYour changes have been saved. Jumping to the new branch...") + warning("Stash failed. Your working tree may have untracked files.") + info("Run: git status to see what's blocking the stash.") + raise GitGoError("Jump aborted — could not save working changes.") + info("Changes saved. Jumping to the new branch...") stashed_code = True - + created_branch = None if not is_branch_exist(target_branch): - warning(f"\nBranch '{target_branch}' does not exist.\n") + print() + warning(f"Branch '{target_branch}' does not exist.") user_input = input("Do you want to create it and jump to it? (y/n): ").strip().lower() if user_input != 'y': - info("Exiting without jumping...\n") + info("Exiting without jumping...") if stashed_code: pop_result = git_stash_pop(loading_msg="Putting your unsaved changes back...") if not pop_result: - warning("\nCould not restore your unsaved changes automatically. Run 'gitgo state list' to recover them.") + warning("Could not restore your unsaved changes automatically. Run 'gitgo state list' to recover them.") return - + git_new_branch(target_branch) created_branch = target_branch else: @@ -76,42 +81,44 @@ def jump_operation(args): main_branch = get_main_branch() get_origin_updates = run_command(['git', 'pull', 'origin', main_branch], allow_fail=True, loading_msg=f"Downloading the latest updates from '{main_branch}'...") - + if command_failed(get_origin_updates): - warning(f"\nFailed to pull updates from '{main_branch}'. Make sure you have internet or the remote branch exists.") - user_input = input("Do you want to stay on the new branch without the latest updates? (y/n): ").strip().lower() + warning(f"Failed to pull updates from '{main_branch}'. No internet, or the remote branch doesn't exist yet.") + user_input = input("Stay on the new branch without the latest updates? (y/n): ").strip().lower() if user_input != 'y': undo_jump_operation(original_branch, stashed_code, created_branch) - raise GitGoError() + raise GitGoError("Jump aborted — could not sync with remote.") else: - success(f"\nOkay! You are on the new branch, but without the latest updates from '{main_branch}'.") - + info(f"On '{target_branch}', but without the latest updates from '{main_branch}'.") if stashed_code: apply_result = git_stash_apply(loading_msg="Unpacking your unsaved changes...") if not apply_result: - error("\nSTOP! There is a 'Merge Conflict'.") - warning("Your unsaved code clashes with the new code from 'main'.\n") - info("Option [Y]: Stay here and fix the red conflict lines yourself.") - info("Option [N]: Cancel everything and go back to normal.\n") + print() + error("MERGE CONFLICT — your changes clash with the target branch.") + print() + info("Option [Y]: Stay here and fix the conflict lines yourself.") + info("Option [N]: Cancel everything and go back to where you started.") + print() conflict_choice = input("Do you want to fix it yourself? (y/n): ").strip().lower() if conflict_choice != 'y': undo_jump_operation(original_branch, stashed_code, created_branch) return else: - success("\nOkay! You are on the new branch with your code.") - warning("Please open your code editor RIGHT NOW to fix the conflicts!") - info("Your stash backup is still saved. Run 'gitgo state list' to see it.\n") + print() + success(f"On '{target_branch}'. Conflict markers are in your files.") + warning("Open your editor and fix the conflict lines.") + info("Your stash backup is still saved. Run 'gitgo state list' to see it.") return else: drop_result = git_stash_drop(loading_msg="Cleaning up the temporary stash...") if not drop_result: - warning("\nCould not clean up the temporary stash. Run 'gitgo state list' to remove it manually.") - success(f"\nSuccess! You are now on '{target_branch}'.") - success("Your unsaved code was moved here safely!\n") + warning("Could not clean up the temporary stash. Run 'gitgo state list' to remove it manually.") + print() + success(f"On '{target_branch}'. Your changes came with you.") return else: - success(f"\nSuccess! You are now on '{target_branch}'.\n") - return - \ No newline at end of file + print() + success(f"On '{target_branch}'.") + return \ No newline at end of file diff --git a/src/pygitgo/commands/pull.py b/src/pygitgo/commands/pull.py index d2c5203..d1cccf2 100644 --- a/src/pygitgo/commands/pull.py +++ b/src/pygitgo/commands/pull.py @@ -9,35 +9,32 @@ def pull_operation(args): if not branch: branch = get_current_branch() - info(f"No branch provided. Pulling latest updates for '{branch}'...\n") + info(f"No branch provided. Pulling latest updates for '{branch}'...") try: remote_refs = run_command(["git", "ls-remote", "--heads", "origin", branch], allow_fail=True) if command_failed(remote_refs) or not remote_refs.strip(): - error(f"\nFailed! The branch '{branch}' does not exist on the remote server.") - warning("You might need to push your local branch first.\n") - raise GitGoError() + error(f"Branch '{branch}' does not exist on the remote.") + info("Push your local branch first, or verify the branch name.") + raise GitGoError("Pull aborted — branch not found on remote.") run_command( - ["git", "pull", "--rebase", "--autostash", "origin", branch], + ["git", "pull", "--rebase", "--autostash", "origin", branch], loading_msg=f"Downloading latest updates for '{branch}' (auto-saving your code)..." ) - - success(f"\nSuccess! Your project is now up to date with '{branch}'.\n") + + success(f"Project is up to date with '{branch}'.") except GitCommandError as e: error_msg = str(e).lower() if "conflict" in error_msg or "rebase in progress" in error_msg: - error("\nMERGE CONFLICT DETECTED!") - warning("Your local code clashes with the new code downloaded from the server.\n") - info("1. Open your code editor down below.") - info("2. Fix the red conflict lines in your files.") + error("MERGE CONFLICT DETECTED!") + print() + info("1. Open your code editor.") + info("2. Fix the conflict lines in your files.") info("3. Save the files.") - info("4. Run this command to finish: git rebase --continue\n") - raise GitGoError() + info("4. Run: git rebase --continue") + print() + raise GitGoError("Pull failed — resolve conflicts to continue.") else: - raise GitGoError( - "\nFailed to pull updates from the server!" - "Please check your internet connection and try again." - f"Details: {e}\n" - ) + raise GitGoError(f"Failed to pull from remote: {e}") diff --git a/src/pygitgo/commands/state.py b/src/pygitgo/commands/state.py index cca8ffb..8701c48 100644 --- a/src/pygitgo/commands/state.py +++ b/src/pygitgo/commands/state.py @@ -1,4 +1,4 @@ -from pygitgo.utils.colors import info, highlight, error, warning, success +from pygitgo.utils.colors import info, success, warning, error, highlight from pygitgo.commands.stash_operation import ( git_stash_apply, git_stash_clear, git_stash_drop, git_stash_list, git_stash_push @@ -24,8 +24,8 @@ def all_save_state(): stash_index = total - 1 - i save_states.append({ - "id": i + 1, - "ref": f"stash@{{{stash_index}}}", + "id": i + 1, + "ref": f"stash@{{{stash_index}}}", "date": date, "message": message, "stash_index": stash_index @@ -41,7 +41,7 @@ def display_save_states(save_state=None): save_states = save_state if save_state is not None else all_save_state() if not save_states: - info("\nNo saved states found.\n") + info("No saved states found.") return print("\nID | Date | Saved State") @@ -52,7 +52,8 @@ def display_save_states(save_state=None): f"{state['id']:>2} | {state['date']} | {state['message']}" ) - print("-" * 60 + "\n") + print("-" * 60) + print() def is_number(value): @@ -60,16 +61,17 @@ def is_number(value): if val.startswith('-'): return val[1:].isdigit() return val.isdigit() - + + def validate_state_id(state_id, save_states): if not is_number(state_id): - error("\nInvalid input. Please enter a valid state ID.\n") + error(f"Invalid ID. Expected a number between 1 and {len(save_states)}.") return False elif (int(state_id) - 1) < 0: - error("\nState ID cannot be '0' or negative. Please enter a valid state ID.\n") + error(f"Invalid ID. Range is 1 to {len(save_states)}.") return False elif (int(state_id) - 1) >= len(save_states): - error("\nState ID out of range. Please enter a valid state ID.\n") + error(f"ID out of range. Range is 1 to {len(save_states)}.") return False return True @@ -79,15 +81,15 @@ def ask_state_id(save_states): state_id = None display_save_states(save_states) - info("\nEnter the ID (or 'q' to cancel): ") + print("Enter the ID (or 'q' to cancel):") while not proceed: state_id = input(">> ").strip().lower() if state_id == 'q': - warning("\nLoad operation cancelled by user.\n") + info("Load canceled.") return proceed = validate_state_id(state_id, save_states) - + return state_id @@ -97,9 +99,9 @@ def state_list(): def load_state(state_id=None): save_states = all_save_state() - + if not save_states: - warning("\nNo saved states to load.\n") + info("No saved states to load.") return proceed = False @@ -108,44 +110,44 @@ def load_state(state_id=None): if state_id.isdigit(): proceed = validate_state_id(state_id, save_states) if not proceed: - raise GitGoError() + raise GitGoError("Load aborted — invalid state ID.") else: - raise GitGoError(f"\nInvalid argument '{state_id}' for load operation. Expected a state ID.\n") - + raise GitGoError(f"Invalid argument '{state_id}' for load. Expected a state ID number.") + if not proceed: state_id = ask_state_id(save_states) if not state_id: return - + selected_state = save_states[int(state_id) - 1] - + apply_result = git_stash_apply(stash_id=str(selected_state["stash_index"])) if not apply_result: - error(f"\nFailed to load state. There may be a conflict with your current changes.\n") - raise GitGoError() - success(f"\nState '{selected_state['message']}' loaded successfully.\n") + error(f"Failed to load state. There may be a conflict with your current changes.") + raise GitGoError("Load failed — resolve conflicts first.") + success(f"State '{selected_state['message']}' restored.") def save_state(state_name=None): if not state_name: state_name = "Auto-Save" - + has_changes = run_command(['git', 'status', '--porcelain'], allow_fail=True) if not command_failed(has_changes) and not has_changes.strip(): - warning("\nNo local changes to save.\n") + info("No local changes to save.") return - + output = git_stash_push(label=state_name) if not output: - error(f"\nFailed to save state '{state_name}'.\n") + error(f"Failed to save state '{state_name}'.") else: - success(f"\nState '{state_name}' saved successfully.\n") + success(f"State '{state_name}' saved.") def delete_state(identifier=None): save_states = all_save_state() if not save_states: - warning("\nNo saved states to delete.\n") + info("No saved states to delete.") return if not identifier: @@ -158,26 +160,26 @@ def delete_state(identifier=None): if confirm == 'y': clear_result = git_stash_clear() if not clear_result: - error("\nFailed to delete all saved states.\n") + error("Failed to delete all saved states.") else: - success("\nAll saved states deleted successfully.\n") + success("All saved states deleted.") else: - warning("\nDelete operation cancelled by user.\n") + info("Delete canceled.") return elif not identifier.isdigit(): - raise GitGoError("\nInvalid input. Please enter a valid state ID.\n") + raise GitGoError(f"Invalid input '{identifier}'. Expected a state ID number.") state_id = identifier if not validate_state_id(state_id, save_states): - raise GitGoError() - + raise GitGoError("Delete aborted — invalid state ID.") + selected_state = save_states[int(state_id) - 1] - + drop_result = git_stash_drop(stash_id=str(selected_state["stash_index"])) if not drop_result: - error(f"\nFailed to delete state with ID '{state_id}'.\n") - raise GitGoError() - success(f"\nState with ID '{state_id}' deleted successfully.\n") + error(f"Failed to delete state {state_id}.") + raise GitGoError("Delete failed — see above.") + success(f"State {state_id} deleted.") def state_operations(args): @@ -186,21 +188,21 @@ def state_operations(args): if alias and positional and alias != positional: raise GitGoError( - f"\nConflicting actions: '{positional}' and '{alias}'. Use one or the other.\n" + f"Conflicting actions: '{positional}' and '{alias}'. Use one or the other." ) action = alias or positional identifier = getattr(args, "identifier", None) - + if getattr(args, "all", False): if action != "delete": raise GitGoError( - "\nThe -a/--all flag is only valid with the delete action.\n" + "The -a/--all flag is only valid with the delete action." ) identifier = "-a" - + if not action: - raise GitGoError("\nMissing action. Please specify an action (list, save, load, delete) or use a flag (-l, -s, -o, -d).\n") + raise GitGoError("Missing action. Use: list, save, load, delete (or -l, -s, -o, -d).") if action == "list": state_list() @@ -211,5 +213,4 @@ def state_operations(args): elif action == "delete": delete_state(identifier) else: - raise GitGoError(f"\nUnknown state operation: {action}\n") - \ No newline at end of file + raise GitGoError(f"Unknown state operation: {action}") \ No newline at end of file diff --git a/src/pygitgo/commands/undo.py b/src/pygitgo/commands/undo.py index 1c98914..87dd434 100644 --- a/src/pygitgo/commands/undo.py +++ b/src/pygitgo/commands/undo.py @@ -1,35 +1,35 @@ from pygitgo.exceptions import GitCommandError, GitGoError -from pygitgo.utils.colors import success, warning, info +from pygitgo.utils.colors import success, warning, info, error from pygitgo.utils.executor import run_command def undo_commit(): try: run_command(["git", "reset", "--soft", "HEAD~"]) - success("\nSuccess! Your last commit is undone. Your files are safe.\n") + success("Last commit undone. Files are untouched.") except GitCommandError as e: raise GitGoError( - "\nFailed! You might not have any previous commits to undo yet." - f"Details: {e}\n" + f"Undo failed — no previous commit to revert. Details: {e}" ) - + def undo_add(): run_command(["git", "reset", "HEAD"]) - success("\nSuccess! Files are no longer ready to commit.\n") + success("Staging cleared. Files are back to unstaged.") def undo_changes(): - warning("\nDANGER: This will permanently delete all your new edits and new files!") + error("DANGER: This will permanently delete all your new edits and new files!") + warning("This action cannot be undone.") confirm = input("Are you sure you want to throw away all changes? (y/n): ") if confirm.lower() != "y": - info("\nCanceled. Your files are safe.\n") + info("Canceled. Your files are safe.") return - + run_command(["git", "reset", "--hard", "HEAD"], loading_msg="Throwing away edits...") run_command(["git", "clean", "-fd"], loading_msg="Removing new files...") - - success("\nSuccess! All changes are completely gone. You have a clean start.\n") + + success("Working tree reset. All changes discarded.") def undo_operations(args): @@ -42,5 +42,4 @@ def undo_operations(args): elif action == "changes": undo_changes() else: - raise GitGoError(f"\nUnknown undo operation: {action}\n") - \ No newline at end of file + raise GitGoError(f"Unknown undo operation: {action}") \ No newline at end of file diff --git a/src/pygitgo/main.py b/src/pygitgo/main.py index 4ce3bdb..f747005 100644 --- a/src/pygitgo/main.py +++ b/src/pygitgo/main.py @@ -5,7 +5,7 @@ from pygitgo.commands.staging import get_changed_files, display_file_picker, selective_stage from pygitgo.utils.update_checker import check_for_updates_background from pygitgo.utils.executor import run_command, command_failed -from pygitgo.utils.colors import info, success, warning, error +from pygitgo.utils.colors import info, success, warning, error, highlight, print_banner from pygitgo.utils.config import get_config, config_operation from pygitgo.exceptions import GitCommandError, GitGoError from pygitgo.utils.setup import ensure_first_run_setup @@ -45,8 +45,9 @@ def link_operation(args): commit_message = args.message - info("\nINITIATING LINK OPERATION...") - info(f"Target: {repo_url}\n") + highlight("INITIATING LINK OPERATION...") + highlight(f"Target: {repo_url}") + print() is_new_repo = git_init() @@ -85,21 +86,17 @@ def link_operation(args): warning(f"Run: git pull origin {main_branch} --allow-unrelated-histories") warning(f"Then: gitgo push {main_branch} 'your message'\n") return - success("Remote content merged successfully.") + success("Remote content merged.") - print("\n" + ("=" * 90)) - success("LINK OPERATION COMPLETE! REPOSITORY LOCKED AND LOADED!") - success(f"Ready to push with: gitgo push {main_branch} 'your message'") - info("AWAITING FURTHER ORDERS...\n") + print_banner("REPOSITORY INITIALIZED AND LINKED.") - user_choice = input(f"\nDo you want to push now? (y/n): ").lower() + user_choice = input(f"Do you want to push now? (y/n): ").lower() if user_choice != 'y': return - + git_push(current_branch) - - print("\n" + ("=" * 90)) - success("MISSION COMPLETE: REPOSITORY INITIALIZED AND PUSHED!\nAWAITING FOR YOUR NEXT ORDERS.\n\n") + + print_banner("REPOSITORY INITIALIZED AND DEPLOYED.") def push_operation(args): @@ -171,20 +168,23 @@ def push_operation(args): warning("Make some changes first or check your git remote configuration.") return - print("\n" + ("=" * 90)) - success("MISSION COMPLETE: NO CASUALTIES. ALL TARGETS NEUTRALIZED.\nAWAITING FOR YOUR NEXT ORDERS.\n\n") + print_banner("MISSION COMPLETE. ALL TARGETS COMMITTED AND PUSHED.") def display_current_user(): + import shutil username, email = get_user() if username and email: - print("\n" + "="*40) + width = min(shutil.get_terminal_size().columns, 40) + print() + print("=" * width) info(f"Git User: {username}") info(f"Git Email: {email}") - print("="*40 + "\n") + print("=" * width) + print() else: - warning("\nNo Git user identity configured.") - info("Run 'gitgo user login'\n") + warning("No Git user identity configured.") + info("Run 'gitgo user login'") def user_management(args): @@ -339,7 +339,7 @@ def main(): return if args.ready: - info("\nALL UNITS ONLINE. GitGo STANDING BY. AWAITING COMMANDS...\n") + info("ALL UNITS ONLINE. GitGo STANDING BY. AWAITING COMMANDS...") return if not args.command: diff --git a/src/pygitgo/utils/colors.py b/src/pygitgo/utils/colors.py index 3953c33..d67160e 100644 --- a/src/pygitgo/utils/colors.py +++ b/src/pygitgo/utils/colors.py @@ -1,3 +1,5 @@ +import shutil + RED = "\033[31m" GREEN = "\033[32m" YELLOW = "\033[33m" @@ -6,17 +8,27 @@ RESET = "\033[0m" -def info(msg): - print(f"{BLUE}{msg}{RESET}") +def info(msg): + print(f"{BLUE}{msg.strip()}{RESET}") + +def success(msg): + print(f"{GREEN}{msg.strip()}{RESET}") -def success(msg): - print(f"{GREEN}{msg}{RESET}") +def warning(msg): + print(f"{YELLOW}{msg.strip()}{RESET}") -def warning(msg): - print(f"{YELLOW}{msg}{RESET}") +def error(msg): + print(f"{RED}{msg.strip()}{RESET}") -def error(msg): - print(f"{RED}{msg}{RESET}") +def highlight(msg): + print(f"{CYAN}{msg.strip()}{RESET}") -def highlight(msg): - print(f"{CYAN}{msg}{RESET}") +def print_banner(title, subtitle="AWAITING NEXT ORDERS."): + width = min(shutil.get_terminal_size().columns, 72) + print() + print(GREEN + ("=" * width) + RESET) + print(GREEN + title.center(width) + RESET) + if subtitle: + print(CYAN + subtitle.center(width) + RESET) + print(GREEN + ("=" * width) + RESET) + print() diff --git a/src/pygitgo/utils/executor.py b/src/pygitgo/utils/executor.py index 6fc73a7..95082bd 100644 --- a/src/pygitgo/utils/executor.py +++ b/src/pygitgo/utils/executor.py @@ -9,7 +9,7 @@ def run_command(command, allow_fail=False, return_complete=False, loading_msg=None): """ Runs a shell command safely. - + :param command: list of command + args :param allow_fail: if True, do not raise on error :param return_complete: if True, return subprocess.CompletedProcess instead of stdout @@ -46,34 +46,33 @@ def run_command(command, allow_fail=False, return_complete=False, loading_msg=No returncode = e.returncode else: stderr = f"Command not found or execution failed: {str(e)}" - + if "detected dubious ownership" in stderr: - error(f"\nSECURITY ALERT: {stderr}") - warning("\nThis is a known Git security feature in shared environments (like Termux).") - info("GitGo can fix this for you by adding this directory to your safe list.") - + error(f"SECURITY ALERT: {stderr}") + warning("This is a known Git security feature in shared environments (like Termux).") + info("GitGo can fix this by adding this directory to your safe list.") + path_match = re.search(r"repository at '(.+)'", stderr) repo_path = path_match.group(1) if path_match else os.getcwd() info(f"Directory to trust: {repo_path}") confirm = input("\nDo you want GitGo to add this directory as an exception? (y/n): ").strip().lower() - + if confirm == 'y': info("Running security fix...") fix_command = ["git", "config", "--global", "--add", "safe.directory", repo_path] try: subprocess.run(fix_command, check=True) - success("Success! Directory added to safe list.") - info("Retrying your original command...\n") + success("Directory trusted. Retrying command...") return run_command(command, allow_fail, return_complete, loading_msg=loading_msg) except OSError as fix_err: error(f"Failed to apply fix: {fix_err}") else: - warning("\nFix declined. Operations in this directory will continue to fail.") + warning("Fix declined. Operations in this directory will continue to fail.") if allow_fail: return e - + raise GitCommandError(command, stderr=stderr, returncode=returncode) def command_failed(result): diff --git a/tests/test_git_operations.py b/tests/test_git_operations.py index f88b180..70d5317 100644 --- a/tests/test_git_operations.py +++ b/tests/test_git_operations.py @@ -399,7 +399,7 @@ def test_check_and_sync_branch_remote_ahead(mocker): remote_branch = 'master' check_and_sync_branch(branch) - fake_success.assert_called_once_with("Branch is up to date or ahead of remote.") + fake_success.assert_called_once_with("Branch is up to date.") fake_run.call_args_list == [ call(["git", "fetch", "origin"], loading_msg="Checking if branch is up to date..."), @@ -422,7 +422,7 @@ def test_check_and_sync_branch_need_sync(mocker): remote_branch = 'master' check_and_sync_branch(branch) - fake_success.assert_called_once_with("Successfully synced with remote!") + fake_success.assert_called_once_with("Synced with remote.") fake_run.call_args_list == [ call(["git", "fetch", "origin"], loading_msg="Checking if branch is up to date..."), diff --git a/tests/test_jump.py b/tests/test_jump.py index eb1e13d..0e19d94 100644 --- a/tests/test_jump.py +++ b/tests/test_jump.py @@ -65,7 +65,7 @@ def test_jump_operation_same_branch(mocker): fake_warning = mocker.patch('pygitgo.commands.jump.warning') assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 0 - fake_warning.assert_called_with("\nYou are already on branch 'main'.\n") + fake_warning.assert_called_with("Already on branch 'main'.") def test_jump_operation_not_valid_repo(mocker): @@ -86,7 +86,7 @@ def test_jump_operation_has_changes_exit(mocker): mocker.patch('pygitgo.commands.jump.run_command', return_value='M file.txt') assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 0 - fake_warning.assert_any_call("\nYou cannot switch branches with unsaved changes. Jump canceled.\n") + fake_warning.assert_any_call("You cannot switch branches with unsaved changes. Jump canceled.") def test_jump_operation_no_changes(mocker): @@ -98,7 +98,7 @@ def test_jump_operation_no_changes(mocker): assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_success.assert_called_with("\nSuccess! You are now on 'feature'.\n") + fake_success.assert_called_with("On 'feature'.") def test_jump_operation_save_changes_error(mocker): @@ -113,9 +113,7 @@ def test_jump_operation_save_changes_error(mocker): ) assert capture_system_exit_code(lambda: jump_operation(make_args('main'))) == 1 - fake_warning.assert_called_with( - "\nFailed to save your changes. Please resolve any issues and try again." - ) + fake_warning.assert_called_with("Stash failed. Your working tree may have untracked files.") @@ -132,8 +130,8 @@ def test_jump_operation_branch_not_exist_cancel_operation(mocker): assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_info.assert_any_call('\nYour changes have been saved. Jumping to the new branch...') - fake_info.assert_any_call("Exiting without jumping...\n") + fake_info.assert_any_call('Changes saved. Jumping to the new branch...') + fake_info.assert_any_call("Exiting without jumping...") fake_pop.assert_called_once() @@ -157,9 +155,8 @@ def test_jump_operation_branch_not_exist_create_branch(mocker): assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_info.assert_called_with('\nYour changes have been saved. Jumping to the new branch...') - fake_success.assert_any_call("\nSuccess! You are now on 'feature'.") - fake_success.assert_any_call("Your unsaved code was moved here safely!\n") + fake_info.assert_called_with('Changes saved. Jumping to the new branch...') + fake_success.assert_any_call("On 'feature'. Your changes came with you.") fake_apply.assert_called_once() fake_drop.assert_called_once() @@ -185,7 +182,7 @@ def _run(*args, **kwargs): assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 1 fake_warning.assert_any_call( - "\nFailed to pull updates from 'main'. Make sure you have internet or the remote branch exists." + "Failed to pull updates from 'main'. No internet, or the remote branch doesn't exist yet." ) @@ -195,7 +192,7 @@ def test_jump_operation_sync_fail_stay(mocker): mocker.patch('pygitgo.commands.jump.is_branch_exist', return_value=True) mocker.patch('builtins.input', side_effect=['y']) # "stay?" → yes - fake_success = mocker.patch('pygitgo.commands.jump.success') + fake_info = mocker.patch('pygitgo.commands.jump.info') def _run(*args, **kwargs): cmd = args[0] @@ -208,8 +205,8 @@ def _run(*args, **kwargs): mocker.patch('pygitgo.commands.jump.run_command', side_effect=_run) assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_success.assert_any_call( - "\nOkay! You are on the new branch, but without the latest updates from 'main'." + fake_info.assert_any_call( + "On 'feature', but without the latest updates from 'main'." ) @@ -234,7 +231,7 @@ def test_jump_operation_merge_conflict_cancel(mocker): ) assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_error.assert_any_call("\nSTOP! There is a 'Merge Conflict'.") + fake_error.assert_any_call("MERGE CONFLICT — your changes clash with the target branch.") def test_jump_operation_merge_conflict_stay(mocker): @@ -260,7 +257,7 @@ def test_jump_operation_merge_conflict_stay(mocker): assert capture_system_exit_code(lambda: jump_operation(make_args('feature'))) == 0 - fake_success.assert_any_call("\nOkay! You are on the new branch with your code.") - fake_warning.assert_any_call("Please open your code editor RIGHT NOW to fix the conflicts!") - fake_info.assert_any_call("Your stash backup is still saved. Run 'gitgo state list' to see it.\n") + fake_success.assert_any_call("On 'feature'. Conflict markers are in your files.") + fake_warning.assert_any_call("Open your editor and fix the conflict lines.") + fake_info.assert_any_call("Your stash backup is still saved. Run 'gitgo state list' to see it.") fake_drop.assert_not_called() diff --git a/tests/test_state.py b/tests/test_state.py index c2d1d37..17fa4d3 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -18,7 +18,7 @@ def test_validate_state_id_negative(state_id, mocker): fake_error = mocker.patch('pygitgo.commands.state.error') result = validate_state_id(state_id, [1] * 12) assert result is False - fake_error.assert_called_with("\nState ID cannot be '0' or negative. Please enter a valid state ID.\n") + fake_error.assert_called_with("Invalid ID. Range is 1 to 12.") @pytest.mark.parametrize('state_id', ['4', '10', '15', '0000020']) @@ -26,7 +26,7 @@ def test_validate_state_id_out_scope(state_id, mocker): fake_error = mocker.patch('pygitgo.commands.state.error') result = validate_state_id(state_id, [1] * 3) assert result is False - fake_error.assert_called_with("\nState ID out of range. Please enter a valid state ID.\n") + fake_error.assert_called_with("ID out of range. Range is 1 to 3.") def test_all_save_state_no_output(mocker): @@ -82,7 +82,7 @@ def test_load_state_specific_id(mocker): load_state("1") fake_apply.assert_called_once_with(stash_id="0") - fake_success.assert_called_once_with("\nState 'msg' loaded successfully.\n") + fake_success.assert_called_once_with("State 'msg' restored.") def test_load_state_invalid_id(mocker): @@ -117,7 +117,7 @@ def test_load_state_no_args(mocker): load_state() fake_apply.assert_called_once_with(stash_id="0") - fake_success.assert_called_once_with("\nState 'msg2' loaded successfully.\n") + fake_success.assert_called_once_with("State 'msg2' restored.") def test_save_state_no_args(mocker): @@ -131,7 +131,7 @@ def test_save_state_no_args(mocker): save_state() fake_push.assert_called_once_with(label="Auto-Save") - fake_success.assert_called_once_with("\nState 'Auto-Save' saved successfully.\n") + fake_success.assert_called_once_with("State 'Auto-Save' saved.") def test_save_state_with_name(mocker): @@ -145,7 +145,7 @@ def test_save_state_with_name(mocker): save_state("My-State") fake_push.assert_called_once_with(label="My-State") - fake_success.assert_called_once_with("\nState 'My-State' saved successfully.\n") + fake_success.assert_called_once_with("State 'My-State' saved.") def test_delete_state_all_confirm(mocker): @@ -157,17 +157,17 @@ def test_delete_state_all_confirm(mocker): delete_state("-a") fake_clear.assert_called_once() - fake_success.assert_called_once_with("\nAll saved states deleted successfully.\n") + fake_success.assert_called_once_with("All saved states deleted.") def test_delete_state_all_cancel(mocker): mocker.patch("builtins.input", return_value="n") mocker.patch("pygitgo.commands.state.all_save_state", return_value=[{"id": 1}]) - fake_warning = mocker.patch("pygitgo.commands.state.warning") + fake_info = mocker.patch("pygitgo.commands.state.info") delete_state("-a") - fake_warning.assert_called_once_with("\nDelete operation cancelled by user.\n") + fake_info.assert_called_once_with("Delete canceled.") def test_delete_state_invalid_id(mocker): @@ -188,7 +188,7 @@ def test_delete_state_specific_id(mocker): delete_state("1") fake_drop.assert_called_once_with(stash_id="0") - fake_success.assert_called_once_with("\nState with ID '1' deleted successfully.\n") + fake_success.assert_called_once_with("State 1 deleted.") def test_delete_state_no_args(mocker): @@ -204,4 +204,4 @@ def test_delete_state_no_args(mocker): delete_state() fake_drop.assert_called_once_with(stash_id="0") - fake_success.assert_called_once_with("\nState with ID '2' deleted successfully.\n") + fake_success.assert_called_once_with("State 2 deleted.")