Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ venv.bak/
Thumbs.db

*.txt
.chronotab/
todo/
.coverage
gitgo-simulation/
simulate_gitgo.py
src/pygitgo/commands/tag.py
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **Project Scaffolding:** Added `gitgo init` to handle local project setup, gitignore fetching, and GitHub template downloads.
- **Remote Repo Creation:** Added `gitgo new` to create remote GitHub repositories without leaving the terminal.

### Changed
- **Documentation:** Added `docs/login-guide.md`, a step-by-step login guide with screenshots covering the full `gitgo user login` flow for starters.
- **Internal Utils:** Centralized browser opening logic and updated the auth manager to support repo creation tokens.

---

Expand Down
2 changes: 0 additions & 2 deletions src/pygitgo/auth/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def get_user():

return name, email


def set_user(name, email):
run_command(["git", "config", "--global", "user.name", name])
run_command(["git", "config", "--global", "user.email", email])
Expand Down Expand Up @@ -66,4 +65,3 @@ def ensure_user_configure(default_email=None, default_username=None):
error("Invalid configuration. Name and Email are required.")
return False


4 changes: 3 additions & 1 deletion src/pygitgo/auth/manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pygitgo.utils.colors import info, success, warning, error
from pygitgo.utils.executor import run_command
from pygitgo.exceptions import GitCommandError
from pygitgo.utils.platform import open_url
from . import ssh_utils
import os

Expand Down Expand Up @@ -39,7 +40,7 @@ def login():
info(" 2. Once as 'Signing Key' (so your commits show as Verified)")
info("Both entries use the exact same key text.")

ssh_utils.open_github_settings()
open_url("https://github.com/settings/ssh/new")

input(
"After adding both keys on GitHub,\n"
Expand All @@ -65,6 +66,7 @@ def login():
info(" 1. The key was not pasted on GitHub")
info(" 2. SSH agent is not running (try: eval $(ssh-agent) && ssh-add)")
info(" 3. Network or firewall is blocking SSH connections")
info("Need help? Full guide: https://github.com/Huerte/GitGo/blob/main/docs/login-guide.md")
return False

def logout():
Expand Down
24 changes: 0 additions & 24 deletions src/pygitgo/auth/ssh_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from pygitgo.exceptions import GitCommandError, GitGoError
from pygitgo.utils.colors import info, success, warning
from pygitgo.utils.executor import run_command
from pygitgo.utils import platform as platform_utils
from pathlib import Path
import webbrowser
import subprocess
import shutil
import os
import re

Expand Down Expand Up @@ -103,27 +100,6 @@ def generate_ssh_key(email):
return key_path


def open_github_settings():
url = "https://github.com/settings/ssh/new"
opened = False

try:
if platform_utils.is_termux():
if shutil.which("termux-open"):
subprocess.run(["termux-open", url], check=False)
opened = True
else:
opened = webbrowser.open(url)
except Exception:
opened = False

if not opened:
warning("Could not open browser automatically.")

info("\nIf the browser did not open, visit this URL manually:")
print(f"\n {url}\n")


def convert_https_to_ssh(url):
pattern = r'^https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$'
match = re.match(pattern, url.strip())
Expand Down
Empty file added src/pygitgo/commands/init.py
Empty file.
104 changes: 104 additions & 0 deletions src/pygitgo/commands/new.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from pygitgo.utils.colors import info, success
from pygitgo.utils.platform import open_url
from pygitgo.exceptions import GitGoError
import subprocess
import urllib
import json
import os

GITHUB_API = "https://api.github.com"

def _get_github_token():

token = os.environ.get("GITHUB_TOKEN", "").strip()
if token:
return token

try:
result = subprocess.run(
["gh", "auth", "token"],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
token = result.stdout.strip()
if token:
return token
except (FileNotFoundError, subprocess.TimeoutExpired):
pass

info("GitGo needs a GitHub token to create repositories.")
info("Required scope: repo")
open_url("https://github.com/settings/tokens/new?scopes=repo")

token = input(
"After creating the token on GitHub,\n"
"come back here and paste it: "
).strip()

if not token:
raise GitGoError("Cancelled. No repository was created.")

return token


def create_github_repo(name, private=False, description="", token=None):

if not token:
token = _get_github_token()

payload = json.dumps({
"name": name,
"private": private,
"description": description,
}).encode("utf-8")

req = urllib.request.Request(
f"{GITHUB_API}/user/repos",
data=payload,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"Content-Type": "application/json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "GitGo-CLI"
}, method="POST",
)

try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
if e.code == 422:
raise GitGoError(f"Repository '{name}' already exists on GitHub.")
elif e.code == 401:
raise GitGoError("Unauthorized. Your token might be expired. Try logging in again.")

body = e.read().decode("utf-8", errors="replace")
try:
msg = json.loads(body).get("message", body)
except Exception:
msg = body
raise GitGoError(f"GitHub API error {e.code}: {msg}")
except urllib.error.URLError as e:
raise GitGoError(f"Network error creating repo: {e.reason}")


def new_operation(args):

if args.name:
repo_name = args.name
else:
repo_name = os.path.basename(os.path.abspath("."))
info(f"No name given: using current directory name: '{repo_name}'")

token = _get_github_token()
info(f"Creating GitHub repository '{repo_name}'...")
repo_ = create_github_repo(
name=repo_name,
private=args.private,
description=args.description or "",
token=token
)
repo_url = repo_.get("clone_url")
success(f"Successfully created remote repository: {repo_url}")
info(f"\nTo connect and push your local code, run:\n gitgo link {repo_url}")
34 changes: 34 additions & 0 deletions src/pygitgo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pygitgo.commands.link import link_operation
from pygitgo.commands.push import push_operation
from pygitgo.commands.user import user_operation
from pygitgo.commands.new import new_operation
from pygitgo.exceptions import GitGoError
import argparse
import sys
Expand Down Expand Up @@ -146,6 +147,37 @@ def main():
)
pull_parser.add_argument("branch", nargs="?", default=None, help="The branch to pull from (default is your current branch)")

new_parser = subparsers.add_parser(
"new",
help="Create a GitHub repo and link the current project to it",
epilog=(
"Examples:\n"
" gitgo new Use current directory name as repo name\n"
" gitgo new my-app Create repo 'my-app' and push\n"
" gitgo new my-app --private Create a private repo\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter
)
new_parser.add_argument(
"name",
default=None,
nargs="?",
metavar="NAME",
help="Repository name. Defaults to current directory name."
)
new_parser.add_argument(
"-p", "--private",
default=False,
action="store_true",
help="Create a private repository."
)
new_parser.add_argument(
"-d", "--description",
default="",
metavar="TEXT",
help="Short repository description shown on GitHub."
)

args = parser.parse_args()

if getattr(args, 'version', False):
Expand Down Expand Up @@ -184,6 +216,8 @@ def main():
undo_operation(args)
elif args.command == "pull":
pull_operation(args)
elif args.command == "new":
new_operation(args)
except GitGoError as e:
error(f"{e}")
sys.exit(1)
Expand Down
23 changes: 23 additions & 0 deletions src/pygitgo/utils/platform.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from pygitgo.utils.colors import warning, info
import webbrowser
import subprocess
import platform
import shutil
import os


Expand All @@ -25,3 +29,22 @@ def is_termux():
return True

return False


def open_url(url: str):
opened = False
try:
if is_termux():
if shutil.which("termux-open"):
subprocess.run(["termux-open", url], check=False)
opened = True
else:
opened = webbrowser.open(url)
except Exception:
opened = False

if not opened:
warning("Could not open browser automatically.")
info("\nIf the browser did not open, visit this URL manually:")
print(f"\n {url}\n")

10 changes: 5 additions & 5 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import pytest
from pathlib import Path
from pygitgo.exceptions import GitCommandError
from pygitgo.auth.manager import login, logout
from pathlib import Path


def test_login_already_logged_in(mocker):
fake_check = mocker.patch("pygitgo.auth.manager.ssh_utils.check_connection", return_value=True)
fake_success = mocker.patch("pygitgo.auth.manager.success")
mocker.patch("pygitgo.auth.manager.open_url")

result = login()

Expand All @@ -19,13 +18,13 @@ def test_login_new_user_success(mocker):
# check_connection returns False first, then True on verification
fake_check = mocker.patch("pygitgo.auth.manager.ssh_utils.check_connection", side_effect=[False, True])
mocker.patch("pygitgo.auth.manager.info")
mocker.patch("pygitgo.auth.manager.open_url")
mocker.patch("pygitgo.auth.manager.success")
mocker.patch("builtins.input", side_effect=["test@example.com", ""])
mocker.patch("pygitgo.auth.manager.ssh_utils.generate_ssh_key", return_value=Path("mock_key"))

# Mock reading public key
mocker.patch("builtins.open", mocker.mock_open(read_data="ssh-rsa AAAAB3NzaC1yc2E..."))
mocker.patch("pygitgo.auth.manager.ssh_utils.open_github_settings")
mocker.patch("pygitgo.auth.manager.ssh_utils.get_github_username", return_value="GithubUser")
fake_ensure = mocker.patch("pygitgo.auth.account.ensure_user_configure", return_value=True)

Expand All @@ -42,8 +41,9 @@ def test_login_new_user_verification_fails(mocker):
mocker.patch("pygitgo.auth.manager.error")
mocker.patch("builtins.input", side_effect=["test@example.com", ""])
mocker.patch("pygitgo.auth.manager.ssh_utils.generate_ssh_key", return_value=Path("mock_key"))
mocker.patch("pygitgo.auth.manager.open_url")
mocker.patch("builtins.open", mocker.mock_open(read_data="ssh-rsa AAAAB3NzaC1yc2E..."))
mocker.patch("pygitgo.auth.manager.ssh_utils.open_github_settings")


result = login()

Expand Down
Loading
Loading