Repository: payu-org/payu

Issue:
cloning from a tag
Sometimes, we want to be able to clone model configurations from a github tag, instead of a branch.

For example, during alpha / early release of access-om3, we are using 'tags' to provide a configuration to alpha testers. e.g. https://github.com/ACCESS-NRI/access-om3-configs/releases/tag/dev-MC_025deg_jra_ryf-0.4

 For example, 

`payu clone -B dev-025deg_jra55do_ryf-0.4 https://github.com/ACCESS-NRI/access-om3-configs dev-025deg_jra55do_ryf-0.4`

Fails with : 
```
Traceback (most recent call last):
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/bin/payu", line 8, in <module>
    sys.exit(parse())
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/cli.py", line 49, in parse
    run_cmd(**args)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/subcommands/clone_cmd.py", line 37, in runcmd
    clone(repository=repository,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 296, in clone
    repo = git_clone(repository, control_path, branch)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/git_utils.py", line 202, in git_clone
    repo = git.Repo.clone_from(repository,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/git/repo/base.py", line 1541, in clone_from
    return cls._clone(
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/git/repo/base.py", line 1412, in _clone
    finalize_process(proc, stderr=stderr)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/git/util.py", line 504, in finalize_process
    proc.wait(**kwargs)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/git/cmd.py", line 834, in wait
    raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
  cmdline: git clone -v --branch=dev-025deg_jra55do_ryf-0.4 -- https://github.com/ACCESS-NRI/access-om3-configs /g/data/tm70/as2285/payu/dev-025deg_jra55do_ryf-0.4
  stderr: 'Cloning into '/g/data/tm70/as2285/payu/dev-025deg_jra55do_ryf-0.4'...
POST git-upload-pack (490 bytes)
fatal: Remote branch dev-025deg_jra55do_ryf-0.4 not found in upstream origin
'
```

`payu clone -s dev-025deg_jra55do_ryf-0.4 https://github.com/ACCESS-NRI/access-om3-configs dev-025deg_jra55do_ryf-0.4`

also fails

However, cloning from the tag works ok when a new branch name is specified: 

`payu clone -b expt -B dev-025deg_jra55do_ryf-0.4 https://github.com/ACCESS-NRI/access-om3-configs dev-025deg_jra55do_ryf-0.4`

I realise that `payu clone` was written around branches, so this behaviour is not necessarily essential. As a minimum, I think we should give a better error when failing (e.g. please provide a new branch name).

Discussion:
@anton-seaice: Ping @jo-basevi @jo-basevi: > fatal: Remote branch dev-025deg_jra55do_ryf-0.4 not found in upstream origin

That failed at the git clone stage as `dev-025deg_jra55do_ryf-0.4` isn't a tag in the repository.  Though the error for `payu clone -B dev-MC_025deg_jra_ryf-0.4 https://github.com/ACCESS-NRI/access-om3-configs dev-MC_025deg_jra_ryf-0.4` is still a bit confusing as payu suggests to add a `-b` to create a branch..

```
Cloned repository from https://github.com/ACCESS-NRI/access-om3-configs to directory: <home>/test_clone/dev-MC_025deg_jra_ryf-0.4
Traceback (most recent call last):
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 325, in clone
    checkout_branch(branch_name=branch,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 177, in checkout_branch
    repo.checkout_branch(branch_name, is_new_branch, start_point)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/git_utils.py", line 186, in checkout_branch
    raise PayuBranchError(
payu.git_utils.PayuBranchError: There is no existing branch called dev-MC_025deg_jra_ryf-0.4. To create this branch, add the new branch flag '-b' to the checkout command.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/bin/payu", line 8, in <module>
    sys.exit(parse())
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/cli.py", line 49, in parse
    run_cmd(**args)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/subcommands/clone_cmd.py", line 37, in runcmd
    clone(repository=repository,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 343, in clone
    raise PayuBranchError(msg)
payu.git_utils.PayuBranchError: Incomplete checkout. To run payu clone again, modify/remove the checkout new branch flag: --new-branch/-b, or checkout existing branch flag: --branch/-B
  Checkout error: There is no existing branch called dev-MC_025deg_jra_ryf-0.4. To create this branch, add the new branch flag '-b' to the checkout command.
```

I think creating a new branch is probably best when cloning/checking out starting from a commit or tag, otherwise git will be in a 'detached HEAD' state and it would be very easy to lose any subsequent git commits without creating a new branch. I would also use the added `-s/--start-point` flag for commits or tags in `payu clone` as it won't do any checks to see if it's a branch or not. E.g.  `payu clone -b expt -s dev-MC_025deg_jra_ryf-0.4 https://github.com/ACCESS-NRI/access-om3-configs dev-MC_025deg_jra_ryf-0.4`. It should then be straight-forward to add a check in clone that if `-s/--start-point` is defined, then `-b` for a new branch should also be defined?

I wonder if it'll be worth adding some usage examples to `payu clone --help`
@anton-seaice: Sorry - I have the brain fog today!

This is the message I was intending:


```
 $ payu clone -s dev-MC_025deg_jra_ryf-0.4 https://github.com/access-nri/access-om3-configs my_folder
```

```
Cloned repository from https://github.com/access-nri/access-om3-configs to directory: /g/data/tm70/as2285/my_folder
Checked out branch: main
No configuration file found on this branch.
Skipping adding new metadata file and creating archive/work symlinks.

To find a branch that has a config file, you can:
    - Display local branches by running:
        payu branch
    - Or display remote branches by running:
        payu branch --remote

To checkout an existing branch, run:
    payu checkout BRANCH_NAME
Where BRANCH_NAME is the name of the branch
Traceback (most recent call last):
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/bin/payu", line 8, in <module>
    sys.exit(parse())
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/cli.py", line 49, in parse
    run_cmd(**args)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/subcommands/clone_cmd.py", line 37, in runcmd
    clone(repository=repository,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 325, in clone
    checkout_branch(branch_name=branch,
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 180, in checkout_branch
    config_path = check_config_path(config_path)
  File "/g/data/vk83/prerelease/apps/base_conda/envs/payu-dev-20250403T221819Z-39e4b9b/lib/python3.10/site-packages/payu/branch.py", line 128, in check_config_path
    raise FileNotFoundError(f"Configuration file {config_path} not found")
FileNotFoundError: Configuration file config.yaml not found
```

Which works if you add `-b expt` 

Solution patch:
diff --git a/payu/branch.py b/payu/branch.py
index 8d94413..17a60c4 100644
--- a/payu/branch.py
+++ b/payu/branch.py
@@ -12,6 +12,7 @@ import warnings
 from pathlib import Path
 from typing import Optional
 import shutil
+import sys
 
 from ruamel.yaml import YAML, CommentedMap, constructor
 import git
@@ -44,6 +45,17 @@ To checkout an existing branch, run:
 Where BRANCH_NAME is the name of the branch"""
 
 
+def remove_traceback_hook(kind, message, traceback):
+    """Remove traceback for only PayuBranchError"""
+    if kind is PayuBranchError:
+        print(f'{kind.__name__}: {message}', file=sys.stderr)
+    else:
+        sys.__excepthook__(kind, message, traceback)
+
+# Override the default exception hook to remove traceback
+sys.excepthook = remove_traceback_hook
+
+
 def check_restart(restart_path: Path,
                   archive_path: Optional[Path] = None) -> Optional[Path]:
     """Checks if restart path exists and whether the archive already
@@ -292,6 +304,15 @@ def clone(repository: str,
             "and use `payu checkout` if it is the same git repository"
         )
 
+    # Check -b is set when -s/--start-point is set
+    if start_point is not None and new_branch_name is None:
+        raise PayuBranchError(
+            "Starting from a specific commit or tag requires a new branch "
+            "name to be specified. Use the --new-branch/-b flag in payu clone "
+            "to create a new git branch.\n"
+            "For more infomation on payu clone, run `payu clone --help`"
+        )
+
     # git clone the repository
     repo = git_clone(repository, control_path, branch)
 
@@ -335,10 +356,11 @@ def clone(repository: str,
         # Remove directory if incomplete checkout
         shutil.rmtree(control_path)
         msg = (
-            "Incomplete checkout. To run payu clone again, modify/remove " +
-            "the checkout new branch flag: --new-branch/-b, or " +
-            "checkout existing branch flag: --branch/-B " +
-            f"\n  Checkout error: {e}"
+            "Incomplete checkout. To run payu clone again, modify/remove "
+            "the checkout new branch flag: --new-branch/-b, or "
+            "checkout existing branch flag: --branch/-B "
+            f"\n  Checkout error: {e}\n"
+            "For more infomation on payu clone, run `payu clone --help`"
         )
         raise PayuBranchError(msg)
     finally:
diff --git a/payu/subcommands/checkout_cmd.py b/payu/subcommands/checkout_cmd.py
index 8ec2610..6c61b30 100644
--- a/payu/subcommands/checkout_cmd.py
+++ b/payu/subcommands/checkout_cmd.py
@@ -3,16 +3,32 @@
 :copyright: Copyright 2018 Marshall Ward, see AUTHORS for details.
 :license: Apache License, Version 2.0, see LICENSE for details.
 """
+from argparse import RawDescriptionHelpFormatter
 from pathlib import Path
 
 from payu.branch import checkout_branch
 import payu.subcommands.args as args
 
 title = 'checkout'
-parameters = {'description': ('A wrapper around git checkout. '
-                              'Create a new branch (if specified), '
-                              'checkout branch, setup experiment metadata '
-                              'and create/switch archive/work symlinks')}
+parameters = {
+    "description": (
+        "A wrapper around git checkout. Create a new branch (if specified), "
+        "checkout branch, setup experiment metadata and "
+        "create/switch archive/work symlinks"
+    ),
+    "epilog": (
+        "Example usage:\n"
+        "\n  To checkout an existing git branch:\n"
+        "    payu checkout <branch_name>\n"
+        "\n  To create a new branch from an existing branch:\n"
+        "    payu checkout -b <new_branch_name> <existing_branch_name>\n"
+        "\n  To create a new branch from an existing commit or tag:\n"
+        "    payu checkout -b <new_branch_name> <commit_or_tag>\n"
+        "\n  To checkout a new branch, and specify a restart path to start from:\n"
+        "    payu checkout -r <path_to_restart_dir> -b <new_branch_name>"
+    ),
+    "formatter_class": RawDescriptionHelpFormatter,
+}
 
 arguments = [args.model, args.config, args.laboratory, args.new_branch,
              args.branch_name, args.start_point, args.restart_path,
diff --git a/payu/subcommands/clone_cmd.py b/payu/subcommands/clone_cmd.py
index 1616593..44ad127 100644
--- a/payu/subcommands/clone_cmd.py
+++ b/payu/subcommands/clone_cmd.py
@@ -4,15 +4,31 @@
 :license: Apache License, Version 2.0, see LICENSE for details.
 """
 
+from argparse import RawDescriptionHelpFormatter
 from pathlib import Path
 
 from payu.branch import clone
 import payu.subcommands.args as args
 
 title = 'clone'
-parameters = {'description': ('A wrapper around git clone. Clones a '
-                              'control repository and setup new experiment '
-                              'metadata')}
+parameters = {
+    'description': (
+        'A wrapper around git clone. Clones a control repository and setup '
+        'new experiment metadata'
+    ),
+    'epilog': (
+        'Example usage:\n'
+        '\n  To clone repository and checkout an existing git branch:\n'
+        '    payu clone -B <branch_name> <repository> <local_directory>\n'
+        '\n  To clone and create a new branch from an existing branch:\n'
+        '    payu clone -B <branch_name> -b <new_branch_name> <repository> <local_directory>\n'
+        '\n  To clone and create a new branch from an existing commit or tag:\n'
+        '    payu clone -s <commit_or_tag> -b <new_branch_name> <repository> <local_directory>\n'
+        '\n  To clone and checkout a new branch, and specify a restart path to start from:\n'
+        '    payu clone -b <new_branch_name> -r <path_to_restart_dir> <repository> <local_directory>\n'
+    ),
+    'formatter_class': RawDescriptionHelpFormatter,
+}
 
 arguments = [args.model, args.config, args.laboratory,
              args.keep_uuid, args.clone_branch,


Test patch:
diff --git a/test/test_branch.py b/test/test_branch.py
index 9277b7a..1ebc28b 100644
--- a/test/test_branch.py
+++ b/test/test_branch.py
@@ -8,7 +8,7 @@ from ruamel.yaml import YAML
 from unittest.mock import patch
 
 from payu.branch import add_restart_to_config, check_restart, switch_symlink
-from payu.branch import checkout_branch, clone, list_branches
+from payu.branch import checkout_branch, clone, list_branches, PayuBranchError
 from payu.metadata import MetadataWarning
 from payu.fsops import read_config
 
@@ -654,6 +654,38 @@ def test_clone_startpoint(start_point_type):
     assert source_repo_commit != cloned_repo.active_branch.object.hexsha
 
 
+def test_clone_startpoint_with_no_new_branch_error():
+    """Test clone when -s/--start-point is used without -b/--new-branch"""
+    # Create a repo to clone
+    source_repo_path = tmpdir / "sourceRepo"
+    source_repo_path.mkdir()
+    source_repo = setup_control_repository(path=source_repo_path)
+
+    # Create branch1
+    branch1 = source_repo.create_head("Branch1")
+    branch1_commit = branch1.object.hexsha
+
+    expected_msg = (
+        "Starting from a specific commit or tag requires a new branch "
+        "name to be specified. Use the --new-branch/-b flag in payu clone "
+        "to create a new git branch."
+    )
+
+    # Run Clone
+    cloned_repo_path = tmpdir / "clonedRepo"
+    with cd(tmpdir):
+        with pytest.raises(PayuBranchError, match=expected_msg):
+            clone(
+                repository=str(source_repo_path),
+                directory=cloned_repo_path,
+                lab_path=labdir,
+                start_point=branch1_commit,
+            )
+
+    # Check cloned repo is not created
+    assert not cloned_repo_path.exists()
+
+
 def test_clone_with_relative_restart_path():
     """Test clone with a restart path that is relative with respect to
     the directory in which the clone command is run from"""