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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ gimmegit = "gimmegit._cli:main"
dev = [
"ruff",
"pytest",
"ty>=0.0.1a27",
"ty>=0.0.1a33",
]

[build-system]
Expand Down
24 changes: 24 additions & 0 deletions src/gimmegit/_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def parse_args(args_to_parse) -> ArgsWithUsage:
parser.add_argument("-u", "--upstream-owner", nargs="?")
parser.add_argument("repo", nargs="?")
parser.add_argument("new_branch", nargs="?")
parser.add_argument("-c", "--compare", action="store_const")
parser.add_argument("-h", "--help", action="store_const")
parser.add_argument("--version", action="store_const")
Comment thread
dwilding marked this conversation as resolved.
parser.add_argument("--parse-url", nargs="?")
Expand All @@ -50,6 +51,8 @@ def parse_args(args_to_parse) -> ArgsWithUsage:
# Handle usages of the gimmegit command.
if hasattr(args, "repo"):
return parse_as_primary(args, unknown_args)
if hasattr(args, "compare"):
return parse_as_compare(args, unknown_args)
if hasattr(args, "help"):
return parse_as_help(args, unknown_args)
if hasattr(args, "version"):
Expand Down Expand Up @@ -93,6 +96,27 @@ def done(error: str | None) -> ArgsWithUsage:
if not hasattr(args, "new_branch"):
args.new_branch = None
# Handle unknown args.
if hasattr(args, "compare"):
unknown_args.append("-c/--compare")
if hasattr(args, "help"):
unknown_args.append("-h/--help")
if hasattr(args, "version"):
unknown_args.append("--version")
if hasattr(args, "parse_url"):
unknown_args.append("--parse-url")
if unknown_args:
return done(f"Unexpected options: {', '.join(unknown_args)}.")
return done(None)


def parse_as_compare(args: argparse.Namespace, unknown_args: list[str]) -> ArgsWithUsage:
def done(error: str | None) -> ArgsWithUsage:
return ArgsWithUsage(args=args, error=error, usage="compare")

# Handle unknown args.
unknown_args = add_non_primary_unknown_args(args, unknown_args)
if hasattr(args, "ssh"):
unknown_args.append("--ssh")
if hasattr(args, "help"):
unknown_args.append("-h/--help")
if hasattr(args, "version"):
Expand Down
79 changes: 59 additions & 20 deletions src/gimmegit/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import sys
import tempfile
import urllib.parse
import webbrowser

import git
import github
Expand Down Expand Up @@ -108,38 +109,44 @@ def main() -> None:
if not status:
logger.error("The working directory is inside a repo.")
sys.exit(1)
else:
status_usage(status)
logger.warning(
"Skipped cloning because the working directory is inside a gimmegit clone."
)
return
assert status # For the type checker.
status_usage(status)
logger.warning(
"Skipped cloning because the working directory is inside a gimmegit clone."
)
return
primary_usage(args, cloning_args)
elif args_with_usage.usage == "compare":
working = _inspect.get_outer_repo()
status = _status.get_status(working) if working else None
if not status:
logger.error("The working directory is not inside a gimmegit clone.")
sys.exit(1)
assert status # For the type checker.
compare_usage(status)
elif args_with_usage.usage == "help":
logger.info(_help.help)
elif args_with_usage.usage == "version":
logger.log(DATA_LEVEL, f"gimmegit {_version.__version__}")
elif args_with_usage.usage == "tool":
parsed_url = parse_github_url(args.parse_url)
if parsed_url:
logger.log(DATA_LEVEL, json.dumps(asdict(parsed_url)))
else:
if not parsed_url:
logger.error(f"'{args.parse_url}' is not a supported GitHub URL.")
sys.exit(1)
assert parsed_url # For the type checker.
logger.log(DATA_LEVEL, json.dumps(asdict(parsed_url)))
elif args_with_usage.usage == "bare":
working = _inspect.get_outer_repo()
if working:
status = _status.get_status(working)
if not status:
logger.error(
"The working directory is inside a repo that is not supported by gimmegit."
)
sys.exit(1)
else:
status_usage(status)
else:
if not working:
logger.error("No repo specified. Run 'gimmegit -h' for help.")
sys.exit(2)
assert working # For the type checker.
status = _status.get_status(working)
if not status:
logger.error("The working directory is not inside a gimmegit clone.")
sys.exit(1)
assert status # For the type checker.
status_usage(status)


def clone(context: Context, cloning_args: list[str]) -> None:
Expand Down Expand Up @@ -217,6 +224,33 @@ def clone(context: Context, cloning_args: list[str]) -> None:
)


def compare_usage(status: _status.Status) -> None:
if not status.has_remote:
logger.error("The review branch has not been created.")
sys.exit(1)
if not os.isatty(sys.stdout.fileno()):
logger.log(DATA_LEVEL, status.compare_url)
return
# Try xdg-open first, to suppress a Linux/snap/Firefox error message:
# Gtk-Message: ... Not loading module "atk-bridge"...
if shutil.which("xdg-open"):
result = subprocess.run(
["xdg-open", status.compare_url],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if result.returncode:
logger.log(DATA_LEVEL, status.compare_url)
return
try:
opened = webbrowser.open(status.compare_url, new=2)
except webbrowser.Error:
logger.log(DATA_LEVEL, status.compare_url)
else:
if not opened:
logger.log(DATA_LEVEL, status.compare_url)


def configure_logger_data() -> None:
retval = logging.StreamHandler(sys.stdout)
retval.setFormatter(logging.Formatter("%(message)s"))
Expand Down Expand Up @@ -430,7 +464,12 @@ def is_valid_branch_name(branch: str) -> bool:
# When run in a repo, 'git check-ref-format --branch' expands "previous checkout" references.
# Such references should be flagged as invalid, so we run the Git command in an empty dir.
with tempfile.TemporaryDirectory() as empty_dir:
command = ["git", "check-ref-format", "--branch", branch]
command = [
"git",
"check-ref-format",
"--branch",
branch,
]
result = subprocess.run(
command,
cwd=empty_dir,
Expand Down
3 changes: 3 additions & 0 deletions src/gimmegit/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
gimmegit [--color {auto,always,never}]

Additional commands:
gimmegit -c
gimmegit --compare

gimmegit -h
gimmegit --help

Expand Down
21 changes: 21 additions & 0 deletions tests/functional/test_arg_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,27 @@ def test_invalid_ssh(uv_run, test_dir):
assert result.stderr == expected_stderr


def test_repo_with_compare(uv_run, test_dir):
command = [
*uv_run,
"gimmegit",
"-c",
"dwilding/jubilant",
]
result = subprocess.run(
command,
cwd=test_dir,
capture_output=True,
text=True,
)
assert result.returncode == 2
assert not result.stdout
expected_stderr = """\
Error: Unexpected options: -c/--compare. Run 'gimmegit -h' for help.
"""
assert result.stderr == expected_stderr


def test_repo_with_help(uv_run, test_dir):
command = [
*uv_run,
Expand Down
21 changes: 21 additions & 0 deletions tests/functional/test_bare_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import subprocess


def test_compre_no_outer(uv_run, test_dir):
command = [
*uv_run,
"gimmegit",
"-c",
]
result = subprocess.run(
command,
cwd=test_dir,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert not result.stdout
expected_stderr = """\
Error: The working directory is not inside a gimmegit clone.
"""
assert result.stderr == expected_stderr
58 changes: 55 additions & 3 deletions tests/functional/test_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ def test_existing_clone(uv_run, test_dir):

def test_dashboard(uv_run, test_dir):
working_dir = Path(test_dir) / "operator/canonical-2.23-maintenance"
command = [*uv_run, "gimmegit"]
command = [
*uv_run,
"gimmegit",
]
result = subprocess.run(
command,
cwd=working_dir,
Expand All @@ -113,7 +116,10 @@ def test_dashboard(uv_run, test_dir):

def test_dashboard_no_remote(uv_run, test_dir):
working_dir = Path(test_dir) / "jubilant/dwilding-my-feature/docs"
command = [*uv_run, "gimmegit"]
command = [
*uv_run,
"gimmegit",
]
result = subprocess.run(
command,
cwd=working_dir,
Expand All @@ -130,7 +136,11 @@ def test_dashboard_no_remote(uv_run, test_dir):

def test_dashboard_warning(uv_run, test_dir):
working_dir = Path(test_dir) / "jubilant/dwilding-my-feature/docs"
command = [*uv_run, "gimmegit", "some-project"]
command = [
*uv_run,
"gimmegit",
"some-project",
]
result = subprocess.run(
command,
cwd=working_dir,
Expand All @@ -149,6 +159,48 @@ def test_dashboard_warning(uv_run, test_dir):
assert result.stderr == expected_stderr


def test_compare(uv_run, test_dir):
working_dir = Path(test_dir) / "operator/canonical-2.23-maintenance"
command = [
*uv_run,
"gimmegit",
"-c",
]
result = subprocess.run(
command,
cwd=working_dir,
capture_output=True,
text=True,
check=True,
)
# Normally 'gimmegit -c' opens the URL, but if the output isn't going to a terminal
# then gimmegit outputs the URL instead.
expected_stdout = """\
https://github.com/canonical/operator/compare/main...canonical:operator:2.23-maintenance?expand=1
"""
assert result.stdout == expected_stdout


def test_compare_no_remote(uv_run, test_dir):
working_dir = Path(test_dir) / "jubilant/dwilding-my-feature/docs"
command = [
*uv_run,
"gimmegit",
"-c",
]
result = subprocess.run(
command,
cwd=working_dir,
capture_output=True,
text=True,
)
assert not result.stdout
expected_stderr = """\
Error: The review branch has not been created.
"""
assert result.stderr == expected_stderr


def test_in_project_dir(uv_run, test_dir):
# .
# └── jubilant Try running 'gimmegit dwilding/jubilant my-feature'
Expand Down
37 changes: 34 additions & 3 deletions tests/functional/test_outer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,34 @@ def test_working_repo_no_dashboard(uv_run, test_dir):
)
working_dir = repo_dir / "foo"
working_dir.mkdir()
command = [*uv_run, "gimmegit"]
command = [
*uv_run,
"gimmegit",
]
result = subprocess.run(
command,
cwd=working_dir,
capture_output=True,
text=True,
)
assert result.returncode == 1
assert not result.stdout
expected_stderr = """\
Error: The working directory is not inside a gimmegit clone.
"""
assert result.stderr == expected_stderr


def test_working_repo_no_compare(uv_run, test_dir):
# .
# └── frogtab Suppose that this dir is a repo
# └── foo Try running 'gimmegit -c'
working_dir = Path(test_dir) / "frogtab/foo"
command = [
*uv_run,
"gimmegit",
"-c",
]
result = subprocess.run(
command,
cwd=working_dir,
Expand All @@ -26,7 +53,7 @@ def test_working_repo_no_dashboard(uv_run, test_dir):
assert result.returncode == 1
assert not result.stdout
expected_stderr = """\
Error: The working directory is inside a repo that is not supported by gimmegit.
Error: The working directory is not inside a gimmegit clone.
"""
assert result.stderr == expected_stderr

Expand All @@ -36,7 +63,11 @@ def test_working_repo_no_clone(uv_run, test_dir):
# └── frogtab Suppose that this dir is a repo
# └── foo Try running 'gimmegit some-project'
working_dir = Path(test_dir) / "frogtab/foo"
command = [*uv_run, "gimmegit", "some-project"]
command = [
*uv_run,
"gimmegit",
"some-project",
]
result = subprocess.run(
command,
cwd=working_dir,
Expand Down
Loading