gitnc 0.1.3__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gitnc-0.1.3 → gitnc-0.1.5}/CLAUDE.md +7 -1
- {gitnc-0.1.3 → gitnc-0.1.5}/PKG-INFO +1 -1
- {gitnc-0.1.3 → gitnc-0.1.5}/README.md +14 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/app.py +152 -9
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/atom-one-dark.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/atom-one-light.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/catppuccin-frappe.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/catppuccin-latte.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/catppuccin-macchiato.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/catppuccin-mocha.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/dracula.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/flexoki.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/gruvbox.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/midnight-commander.yaml +7 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/monokai.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/nord.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/rose-pine-dawn.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/rose-pine-moon.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/rose-pine.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/solarized-dark.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/solarized-light.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/textual-ansi.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/textual-dark.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/textual-light.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/tokyo-night.yaml +5 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/ui_theme.py +8 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/pyproject.toml +2 -1
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/test_app.py +302 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/.devcontainer/Dockerfile +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/.devcontainer/devcontainer.json +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/.gitignore +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/LICENSE +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/__init__.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/__main__.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/git.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/terminal.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/gitnc/themes/__init__.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/__init__.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/gitprocess.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/test_git.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/test_terminal.py +0 -0
- {gitnc-0.1.3 → gitnc-0.1.5}/tests/test_themes.py +0 -0
|
@@ -56,7 +56,13 @@ Menu chrome is composed once and stays mounted, so `action_set_theme` / `_load_s
|
|
|
56
56
|
|
|
57
57
|
**Text selection colors:** selected text in the commit message box (Shift+arrows in the `TextArea`) is painted from `UITheme.text_selection_background` / `text_selection_foreground`, which `css_variables()` emits as Textual's own `$input-selection-background` / `$input-selection-foreground` — the variables `TextArea` and `Input` already read, so there is no selection CSS in the app. Unlike the chrome fields these are optional: an empty value emits nothing and leaves Textual's derived default (`$primary` lightened, at 40% over the background), because that default is fine in most themes and can't be written as a `$ref` `_resolve_refs()` understands. It is not fine in midnight-commander, where primary and background are both blue and a selection all but vanished; that theme sets `$warning` / `$background`, mcedit's dark-blue-on-cyan marked block. Set the selection through these fields, not through `variables`: `get_theme()` lets a `variables` entry win, so a second copy there would silently override the field.
|
|
58
58
|
|
|
59
|
-
**
|
|
59
|
+
**Warning notification color:** a `notify(..., severity="warning")` toast — today only the up-front branch-prefix warning (see *Committing*) — takes its text and left bar from `UITheme.warning_notification`, emitted as `$warning-notification` and applied by the `Toast.-warning` rules in the app `CSS`. It is a field of its own for the same reason the chrome colors are: Textual paints that bar in `$warning`, which is midnight-commander's cyan menu bar color and doesn't read as a warning there, so that theme sets a red (`#FF5555`) while every other theme keeps the default `$warning`.
|
|
60
|
+
|
|
61
|
+
**Committing:** `action_commit_files()` asks twice, and the two questions are different. The first — only when a target still has unstaged changes — is `Stage N file(s) for commit?`, listing the `git add` commands that would run. The second always comes, after the message dialog: `_confirm_commit()` shows `Commit N file(s)?` over the commands from `GIT.commit_with_selection_commands()`, which is the last point at which the files going into the commit are visible before `commit_with_selection` walks the submodules and commits. The command list comes from that method rather than being rebuilt in `app.py` so the popup and the executor can't drift apart, and the message inside it is cut to `_commit_message_subject()` — its first line, marked with `TRUNCATION_MARKER` when there is more — because the popup renders one command per row and a multi-line `-m` argument would break that. N on it doesn't cancel: `_show_confirm()`'s `on_cancel` reopens the message editor — `CommitDialog`, or the external editor the settings name — on the message just written, so backing out of the confirmation costs nothing. That, the prefix warning below, which reopens the message the same way, and the startup submodule question (see *Starting inside a submodule*), whose N still has to start the app, are the only callers of `on_cancel`; every other caller wants the default, which hands focus back to the active view.
|
|
62
|
+
|
|
63
|
+
A message prefixed with a branch the settings warn against gets a question of its own first: `_commit_result()` goes through `_confirm_commit_prefix()`, which — when the message starts with `<name>:` for a name in the `commit_prefix_warn_branches` setting — asks `Warning: commit message is prefixed with branch '…'. Continue?` over the message's subject line, and only on Y moves on to `Commit N file(s)?`. N reopens the message, the same way back the commit confirmation offers. It is a separate dialog rather than a row under the commands because a row only has to be overlooked. The setting is the *Warn on prefix* text row under the branch-prefix checkbox, a space- or comma-separated list read by `_branch_names()`. The check is on the message rather than the checked-out branch, because the prefill is editable and the prefix can have been typed by hand, and it comes after the editor because that is where the message is final whichever editor wrote it. `_begin_commit_message()` raises the same text — both come from `_commit_prefix_warning()` — as a warning notification when the prefill it is about to open already carries a listed prefix, so the user hears about it before writing the message around it.
|
|
64
|
+
|
|
65
|
+
The row under it, *Warn on prefix not matching* (`commit_prefix_require_pattern`), is the opposite check through the same two places: an optional regular expression the message has to *start with*. `_starts_with_pattern()` is `re.match`, which anchors at the start and stops where the pattern does, so the setting describes the prefix alone — no `^`, no trailing `.*`. `_commit_prefix_warning()` collects both checks into one sentence (`… is prefixed with branch 'x' and does not start with a prefix matching '…'`), so a message that trips both is still asked about once. The pattern is quoted through `_escape_markup()`, because the confirm prompt and the toast both render markup and a character class would be read as a tag. A pattern that doesn't compile is reported as a warning of its own rather than skipped — a check that silently stopped checking looks exactly like a message that passed — and `SettingsDialog._prefix_pattern_hint()` says so under the row while it is being typed. Up front the pattern is only applied to a non-empty prefill: with the branch-prefix setting off nothing has been written yet, and the warning would fire on every commit.
|
|
60
66
|
|
|
61
67
|
Cancelling the message dialog rolls the index back to where the flow found it. Both halves of the mutation are undone: `_restore_externals()` re-stages the files it unstaged, and `_unstage_pending()` unstages what the stage confirmation added, from the `_pending_commit_staged` list `_begin_commit_message()` was handed. Without the second half the files stayed staged, so a second `C` on the same selection found nothing left to stage and skipped straight past `Stage N file(s) for commit?` — the question never came back. Only entries that had *nothing* staged before go on that list (`not code.has_staged_changes`): an entry that was already partly staged had its index state built by the user, and `git restore --staged` would throw that away instead of putting it back. `_commit_submit()` clears the list without running it — the files it staged are the files it just committed.
|
|
62
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitnc
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: GitNightCommander — a TUI git log browser built with Textual
|
|
5
5
|
Project-URL: Homepage, https://github.com/thomashyldgaard/GitNightCommander
|
|
6
6
|
Project-URL: Source, https://github.com/thomashyldgaard/GitNightCommander
|
|
@@ -92,6 +92,7 @@ first use, so no manual venv activation is needed for the commands below.
|
|
|
92
92
|
|
|
93
93
|
```sh
|
|
94
94
|
hatch build # sdist + wheel into dist/
|
|
95
|
+
hatch run rebuild # same, removing the old artifacts from dist/ first
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
The artifacts land in `dist/` as `gitnc-<version>-py3-none-any.whl` and
|
|
@@ -272,6 +273,19 @@ Options -> Settings configures the things it uses:
|
|
|
272
273
|
- **Use branch name as prefix in commit messages** — prefills the message with
|
|
273
274
|
`<branch>: `, and puts that prefix in front of a drafted message that does not
|
|
274
275
|
already start with the branch name.
|
|
276
|
+
- **Warn on prefix** — branch names, separated by spaces or commas, that should
|
|
277
|
+
not end up in front of a commit message (e.g. `master main`). A message that
|
|
278
|
+
starts with `<name>:` is asked about in a confirmation of its own before the
|
|
279
|
+
`Commit N file(s)?` one: `Y` continues, `N` puts you back in the message to
|
|
280
|
+
change it. When the branch prefix setting prefills such a name, the same
|
|
281
|
+
warning also pops up as the message editor opens.
|
|
282
|
+
- **Warn on prefix not matching** — optional; a regular expression the commit
|
|
283
|
+
message must start with (e.g. `[A-Z]+-\d+:` for a ticket number). It describes
|
|
284
|
+
the prefix only — it is matched at the start of the message, so no `^` and no
|
|
285
|
+
trailing `.*` are needed. A message that does not start with a match gets the
|
|
286
|
+
same confirmation and the same up-front warning as **Warn on prefix**; when
|
|
287
|
+
both settings object, the one question names both. A pattern that does not
|
|
288
|
+
compile is flagged in the settings dialog and reported in that question.
|
|
275
289
|
- **Commit message editor** — where the message is written:
|
|
276
290
|
- **Built-in editor** (the default) is the commit dialog described above.
|
|
277
291
|
- **$EDITOR** runs the editor named by the `EDITOR` environment variable;
|
|
@@ -10,7 +10,7 @@ import subprocess
|
|
|
10
10
|
import tempfile
|
|
11
11
|
import textwrap
|
|
12
12
|
import threading
|
|
13
|
-
from collections.abc import Callable, Mapping
|
|
13
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
14
14
|
from contextlib import ExitStack
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
|
@@ -815,6 +815,41 @@ def _caret_markup(text: str, *, column: int, style: str) -> str:
|
|
|
815
815
|
return f"{_escape_markup(head)}[{style}]{_escape_markup(cell)}[/{style}]{_escape_markup(tail)}"
|
|
816
816
|
|
|
817
817
|
|
|
818
|
+
def _branch_names(text: str) -> list[str]:
|
|
819
|
+
"""The branch names in a settings value that lists several, separated by
|
|
820
|
+
spaces or commas — neither of which a branch name can contain."""
|
|
821
|
+
return [name for name in re.split(r"[\s,]+", text) if name]
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _warned_commit_prefix(message: str, branches: Iterable[str]) -> str:
|
|
825
|
+
"""Whichever of `branches` the message is prefixed with, or "".
|
|
826
|
+
|
|
827
|
+
The message is what is checked, not the branch that is checked out: the
|
|
828
|
+
prefill is editable, so the prefix can have been removed since — or typed
|
|
829
|
+
in by hand with the prefix setting off.
|
|
830
|
+
"""
|
|
831
|
+
return next((branch for branch in branches if message.startswith(f"{branch}:")), "")
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _is_valid_pattern(pattern: str) -> bool:
|
|
835
|
+
"""Whether `pattern` compiles as a regular expression."""
|
|
836
|
+
try:
|
|
837
|
+
re.compile(pattern)
|
|
838
|
+
except re.error:
|
|
839
|
+
return False
|
|
840
|
+
return True
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _starts_with_pattern(message: str, pattern: str) -> bool:
|
|
844
|
+
"""Whether `message` starts with something `pattern` matches.
|
|
845
|
+
|
|
846
|
+
`re.match` anchors at the start and stops where the pattern does, so the
|
|
847
|
+
setting describes the prefix alone — no `^` in front, and no `.*` behind
|
|
848
|
+
it for the rest of the message.
|
|
849
|
+
"""
|
|
850
|
+
return re.match(pattern, message) is not None
|
|
851
|
+
|
|
852
|
+
|
|
818
853
|
def _setting_str(settings: Mapping[str, object], key: str, default: str) -> str:
|
|
819
854
|
"""`settings[key]` when it holds a string, else `default`.
|
|
820
855
|
|
|
@@ -897,6 +932,8 @@ class SettingsValues(NamedTuple):
|
|
|
897
932
|
parse_suggestions: bool = False
|
|
898
933
|
editor: str = COMMIT_EDITOR_BUILTIN
|
|
899
934
|
editor_command: str = ""
|
|
935
|
+
prefix_warn_branches: str = ""
|
|
936
|
+
prefix_require_pattern: str = ""
|
|
900
937
|
|
|
901
938
|
|
|
902
939
|
# Rows of the settings dialog, in the order they are drawn. The checkbox rows
|
|
@@ -906,14 +943,16 @@ class SettingsValues(NamedTuple):
|
|
|
906
943
|
# Experimental heading.
|
|
907
944
|
SETTINGS_ROW_BRANCH = 0
|
|
908
945
|
SETTINGS_ROW_PREFIX = 1
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
946
|
+
SETTINGS_ROW_PREFIX_WARN = 2
|
|
947
|
+
SETTINGS_ROW_PREFIX_PATTERN = 3
|
|
948
|
+
SETTINGS_ROW_EDITOR_BUILTIN = 4
|
|
949
|
+
SETTINGS_ROW_EDITOR_ENVIRONMENT = 5
|
|
950
|
+
SETTINGS_ROW_EDITOR_COMMAND = 6
|
|
951
|
+
SETTINGS_ROW_EDITOR_COMMAND_LINE = 7
|
|
952
|
+
SETTINGS_ROW_COMMAND = 8
|
|
953
|
+
SETTINGS_ROW_PROMPT = 9
|
|
954
|
+
SETTINGS_ROW_PARSE = 10
|
|
955
|
+
SETTINGS_ROW_COUNT = 11
|
|
917
956
|
|
|
918
957
|
# Which editor each radio row selects.
|
|
919
958
|
SETTINGS_EDITOR_ROWS: dict[int, str] = {
|
|
@@ -1284,6 +1323,8 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1284
1323
|
# rows differ only by their label and the value they carry.
|
|
1285
1324
|
TEXT_ROWS: ClassVar[dict[int, str]] = {
|
|
1286
1325
|
SETTINGS_ROW_BRANCH: "_default_branch",
|
|
1326
|
+
SETTINGS_ROW_PREFIX_WARN: "_prefix_warn_branches",
|
|
1327
|
+
SETTINGS_ROW_PREFIX_PATTERN: "_prefix_require_pattern",
|
|
1287
1328
|
SETTINGS_ROW_EDITOR_COMMAND_LINE: "_editor_command",
|
|
1288
1329
|
SETTINGS_ROW_COMMAND: "_draft_command",
|
|
1289
1330
|
SETTINGS_ROW_PROMPT: "_draft_prompt",
|
|
@@ -1303,11 +1344,15 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1303
1344
|
parse_suggestions: bool = False,
|
|
1304
1345
|
editor: str = COMMIT_EDITOR_BUILTIN,
|
|
1305
1346
|
editor_command: str = "",
|
|
1347
|
+
prefix_warn_branches: str = "",
|
|
1348
|
+
prefix_require_pattern: str = "",
|
|
1306
1349
|
) -> None:
|
|
1307
1350
|
super().__init__()
|
|
1308
1351
|
self._cursor: int = 0
|
|
1309
1352
|
self._default_branch: str = default_branch
|
|
1310
1353
|
self._branch_prefix: bool = branch_prefix
|
|
1354
|
+
self._prefix_warn_branches: str = prefix_warn_branches
|
|
1355
|
+
self._prefix_require_pattern: str = prefix_require_pattern
|
|
1311
1356
|
self._draft_command: str = draft_command
|
|
1312
1357
|
self._draft_prompt: str = draft_prompt
|
|
1313
1358
|
self._parse_suggestions: bool = parse_suggestions
|
|
@@ -1522,6 +1567,18 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1522
1567
|
"Use branch name as prefix in commit messages",
|
|
1523
1568
|
)
|
|
1524
1569
|
)
|
|
1570
|
+
lines.append(self._text_row(SETTINGS_ROW_PREFIX_WARN, " Warn on prefix: "))
|
|
1571
|
+
lines.append(
|
|
1572
|
+
f"[{theme.dialog_hint}]"
|
|
1573
|
+
" (branch names, separated by spaces, e.g. master main)"
|
|
1574
|
+
f"[/{theme.dialog_hint}]"
|
|
1575
|
+
)
|
|
1576
|
+
lines.append(
|
|
1577
|
+
self._text_row(SETTINGS_ROW_PREFIX_PATTERN, " Warn on prefix not matching: ")
|
|
1578
|
+
)
|
|
1579
|
+
lines.append(
|
|
1580
|
+
f"[{theme.dialog_hint}] ({self._prefix_pattern_hint()})[/{theme.dialog_hint}]"
|
|
1581
|
+
)
|
|
1525
1582
|
lines.append("")
|
|
1526
1583
|
lines.append(self._section_header("Commit message editor"))
|
|
1527
1584
|
lines.append("")
|
|
@@ -1568,6 +1625,15 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1568
1625
|
# values are already recorded, so there is simply nothing to paint.
|
|
1569
1626
|
pass
|
|
1570
1627
|
|
|
1628
|
+
def _prefix_pattern_hint(self) -> str:
|
|
1629
|
+
"""The line under the required-prefix row: what goes in it, or that
|
|
1630
|
+
what is in it won't compile — said here, while it is being typed,
|
|
1631
|
+
rather than first at the next commit."""
|
|
1632
|
+
if not _is_valid_pattern(self._prefix_require_pattern):
|
|
1633
|
+
return "not a valid regular expression"
|
|
1634
|
+
example = _escape_markup(r"[A-Z]+-\d+:")
|
|
1635
|
+
return f"optional regular expression for the start of the message, e.g. {example}"
|
|
1636
|
+
|
|
1571
1637
|
def _key_hint(self) -> str:
|
|
1572
1638
|
"""The key line under the dialog, which is not the same in a framed
|
|
1573
1639
|
field: the arrows belong to the caret there, and Tab is what leaves."""
|
|
@@ -1600,6 +1666,8 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1600
1666
|
parse_suggestions=self._parse_suggestions,
|
|
1601
1667
|
editor=self._editor,
|
|
1602
1668
|
editor_command=self._editor_command,
|
|
1669
|
+
prefix_warn_branches=self._prefix_warn_branches,
|
|
1670
|
+
prefix_require_pattern=self._prefix_require_pattern,
|
|
1603
1671
|
)
|
|
1604
1672
|
|
|
1605
1673
|
|
|
@@ -2641,6 +2709,15 @@ class GitNightCommanderApp(App):
|
|
|
2641
2709
|
]
|
|
2642
2710
|
|
|
2643
2711
|
CSS = """
|
|
2712
|
+
/* Warning notifications take the theme's own color for them instead of
|
|
2713
|
+
$warning, which a theme may have spent on its chrome. */
|
|
2714
|
+
Toast.-warning {
|
|
2715
|
+
border-left: outer $warning-notification;
|
|
2716
|
+
color: $warning-notification;
|
|
2717
|
+
}
|
|
2718
|
+
Toast.-warning .toast--title {
|
|
2719
|
+
color: $warning-notification;
|
|
2720
|
+
}
|
|
2644
2721
|
Screen {
|
|
2645
2722
|
layout: horizontal;
|
|
2646
2723
|
layers: default overlay;
|
|
@@ -4014,6 +4091,14 @@ class GitNightCommanderApp(App):
|
|
|
4014
4091
|
self._pending_commit_targets = list(targets)
|
|
4015
4092
|
branch = self._commit_branch_prefix()
|
|
4016
4093
|
prefill = f"{branch}: " if branch else ""
|
|
4094
|
+
# Only a prefill is checked: with nothing written yet there is no
|
|
4095
|
+
# prefix to judge, and the required pattern would fail every time.
|
|
4096
|
+
warning = self._commit_prefix_warning(prefill) if prefill else ""
|
|
4097
|
+
if warning:
|
|
4098
|
+
# Up front as well as in the question asked after the message:
|
|
4099
|
+
# the prefix is already known here, and this is before the
|
|
4100
|
+
# message has been written around it.
|
|
4101
|
+
self.notify(warning, severity="warning")
|
|
4017
4102
|
self._edit_commit_message(prefill)
|
|
4018
4103
|
|
|
4019
4104
|
def _edit_commit_message(self, message: str) -> None:
|
|
@@ -4287,7 +4372,30 @@ class GitNightCommanderApp(App):
|
|
|
4287
4372
|
if message is None:
|
|
4288
4373
|
self._commit_cancel()
|
|
4289
4374
|
else:
|
|
4375
|
+
self._confirm_commit_prefix(message)
|
|
4376
|
+
|
|
4377
|
+
def _confirm_commit_prefix(self, message: str) -> None:
|
|
4378
|
+
"""Ask about a prefix the settings warn against — a listed branch,
|
|
4379
|
+
or one the required pattern doesn't match — before the commit
|
|
4380
|
+
confirmation rather than as a row inside it.
|
|
4381
|
+
|
|
4382
|
+
A question of its own has to be answered; a row under a list of
|
|
4383
|
+
commands only has to be overlooked. It comes here rather than on the
|
|
4384
|
+
message dialog because this is where the message is final, whichever
|
|
4385
|
+
editor wrote it. Y goes on to the commit confirmation, N reopens the
|
|
4386
|
+
message — the same way back that confirmation offers — and a message
|
|
4387
|
+
without such a prefix goes straight through.
|
|
4388
|
+
"""
|
|
4389
|
+
warning = self._commit_prefix_warning(message)
|
|
4390
|
+
if not warning:
|
|
4290
4391
|
self._confirm_commit(message)
|
|
4392
|
+
return
|
|
4393
|
+
self._show_confirm(
|
|
4394
|
+
f"{warning}. Continue?",
|
|
4395
|
+
[_commit_message_subject(message)],
|
|
4396
|
+
lambda: self._confirm_commit(message),
|
|
4397
|
+
lambda: self._edit_commit_message(message),
|
|
4398
|
+
)
|
|
4291
4399
|
|
|
4292
4400
|
def _confirm_commit(self, message: str) -> None:
|
|
4293
4401
|
"""Show what the commit will run and ask before running it.
|
|
@@ -4315,6 +4423,37 @@ class GitNightCommanderApp(App):
|
|
|
4315
4423
|
fill_height=True,
|
|
4316
4424
|
)
|
|
4317
4425
|
|
|
4426
|
+
def _commit_prefix_warning(self, message: str) -> str:
|
|
4427
|
+
"""The warning for a message whose prefix the settings object to, or
|
|
4428
|
+
"" when they don't: it starts with a branch named in
|
|
4429
|
+
`commit_prefix_warn_branches`, or it doesn't start with what the
|
|
4430
|
+
`commit_prefix_require_pattern` regular expression asks for. Both
|
|
4431
|
+
are reported when both apply.
|
|
4432
|
+
|
|
4433
|
+
One text for the notification the message editor opens with and the
|
|
4434
|
+
question asked once the message is written, so the two can't drift.
|
|
4435
|
+
The pattern is quoted with its brackets escaped, because both of
|
|
4436
|
+
those render markup and a character class would be read as a tag.
|
|
4437
|
+
"""
|
|
4438
|
+
settings = self._load_settings()
|
|
4439
|
+
problems: list[str] = []
|
|
4440
|
+
branches = _branch_names(_setting_str(settings, "commit_prefix_warn_branches", ""))
|
|
4441
|
+
branch = _warned_commit_prefix(message, branches)
|
|
4442
|
+
if branch:
|
|
4443
|
+
problems.append(f"is prefixed with branch '{branch}'")
|
|
4444
|
+
pattern = _setting_str(settings, "commit_prefix_require_pattern", "")
|
|
4445
|
+
if pattern.strip():
|
|
4446
|
+
quoted = _escape_markup(pattern)
|
|
4447
|
+
if not _is_valid_pattern(pattern):
|
|
4448
|
+
# Said rather than skipped: a check that silently stopped
|
|
4449
|
+
# checking would look exactly like a message that passed.
|
|
4450
|
+
problems.append(f"can't be checked against invalid pattern '{quoted}'")
|
|
4451
|
+
elif not _starts_with_pattern(message, pattern):
|
|
4452
|
+
problems.append(f"does not start with a prefix matching '{quoted}'")
|
|
4453
|
+
if not problems:
|
|
4454
|
+
return ""
|
|
4455
|
+
return f"Warning: commit message {' and '.join(problems)}"
|
|
4456
|
+
|
|
4318
4457
|
def _commit_submit(self, message: str) -> None:
|
|
4319
4458
|
result = self.git.commit_with_selection(self._pending_commit_targets, message)
|
|
4320
4459
|
self._pending_commit_targets = []
|
|
@@ -4575,6 +4714,8 @@ class GitNightCommanderApp(App):
|
|
|
4575
4714
|
parse_suggestions=bool(settings.get("commit_draft_parse_suggestions", False)),
|
|
4576
4715
|
editor=_setting_str(settings, "commit_editor", COMMIT_EDITOR_BUILTIN),
|
|
4577
4716
|
editor_command=_setting_str(settings, "commit_editor_command", ""),
|
|
4717
|
+
prefix_warn_branches=_setting_str(settings, "commit_prefix_warn_branches", ""),
|
|
4718
|
+
prefix_require_pattern=_setting_str(settings, "commit_prefix_require_pattern", ""),
|
|
4578
4719
|
),
|
|
4579
4720
|
callback=self._settings_result,
|
|
4580
4721
|
)
|
|
@@ -4592,6 +4733,8 @@ class GitNightCommanderApp(App):
|
|
|
4592
4733
|
settings["commit_draft_parse_suggestions"] = values.parse_suggestions
|
|
4593
4734
|
settings["commit_editor"] = values.editor
|
|
4594
4735
|
settings["commit_editor_command"] = values.editor_command
|
|
4736
|
+
settings["commit_prefix_warn_branches"] = values.prefix_warn_branches
|
|
4737
|
+
settings["commit_prefix_require_pattern"] = values.prefix_require_pattern
|
|
4595
4738
|
if not self._save_settings(settings):
|
|
4596
4739
|
self.notify("Settings saved (could not write to disk)")
|
|
4597
4740
|
else:
|
|
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
|
|
|
89
89
|
footer_background: $panel
|
|
90
90
|
footer_foreground: $foreground
|
|
91
91
|
|
|
92
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
93
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
94
|
+
# its left edge.
|
|
95
|
+
warning_notification: $warning
|
|
96
|
+
|
|
92
97
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
93
98
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
94
99
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
|
|
|
89
89
|
footer_background: $panel
|
|
90
90
|
footer_foreground: $foreground
|
|
91
91
|
|
|
92
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
93
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
94
|
+
# its left edge.
|
|
95
|
+
warning_notification: $warning
|
|
96
|
+
|
|
92
97
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
93
98
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
94
99
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
|
|
|
97
97
|
footer_background: $panel
|
|
98
98
|
footer_foreground: $foreground
|
|
99
99
|
|
|
100
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
101
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
102
|
+
# its left edge.
|
|
103
|
+
warning_notification: $warning
|
|
104
|
+
|
|
100
105
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
101
106
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
102
107
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
|
|
|
90
90
|
footer_background: $panel
|
|
91
91
|
footer_foreground: $foreground
|
|
92
92
|
|
|
93
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
94
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
95
|
+
# its left edge.
|
|
96
|
+
warning_notification: $warning
|
|
97
|
+
|
|
93
98
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
94
99
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
95
100
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
|
|
|
97
97
|
footer_background: $panel
|
|
98
98
|
footer_foreground: $foreground
|
|
99
99
|
|
|
100
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
101
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
102
|
+
# its left edge.
|
|
103
|
+
warning_notification: $warning
|
|
104
|
+
|
|
100
105
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
101
106
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
102
107
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
|
|
|
97
97
|
footer_background: $panel
|
|
98
98
|
footer_foreground: $foreground
|
|
99
99
|
|
|
100
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
101
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
102
|
+
# its left edge.
|
|
103
|
+
warning_notification: $warning
|
|
104
|
+
|
|
100
105
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
101
106
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
102
107
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
|
|
|
90
90
|
footer_background: $panel
|
|
91
91
|
footer_foreground: $foreground
|
|
92
92
|
|
|
93
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
94
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
95
|
+
# its left edge.
|
|
96
|
+
warning_notification: $warning
|
|
97
|
+
|
|
93
98
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
94
99
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
95
100
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -92,6 +92,11 @@ menu_bar_foreground: $foreground
|
|
|
92
92
|
footer_background: $panel
|
|
93
93
|
footer_foreground: $foreground
|
|
94
94
|
|
|
95
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
96
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
97
|
+
# its left edge.
|
|
98
|
+
warning_notification: $warning
|
|
99
|
+
|
|
95
100
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
96
101
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
97
102
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -91,6 +91,11 @@ menu_bar_foreground: $foreground
|
|
|
91
91
|
footer_background: $panel
|
|
92
92
|
footer_foreground: $foreground
|
|
93
93
|
|
|
94
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
95
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
96
|
+
# its left edge.
|
|
97
|
+
warning_notification: $warning
|
|
98
|
+
|
|
94
99
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
95
100
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
96
101
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -93,6 +93,13 @@ menu_bar_foreground: $background
|
|
|
93
93
|
footer_background: $warning
|
|
94
94
|
footer_foreground: $background
|
|
95
95
|
|
|
96
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
97
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
98
|
+
# its left edge. $warning is this theme's cyan — the color of the menu bar,
|
|
99
|
+
# which reads as chrome rather than as a warning — so the toast gets a red
|
|
100
|
+
# of its own.
|
|
101
|
+
warning_notification: '#FF5555'
|
|
102
|
+
|
|
96
103
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
97
104
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
98
105
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -91,6 +91,11 @@ menu_bar_foreground: $foreground
|
|
|
91
91
|
footer_background: $panel
|
|
92
92
|
footer_foreground: $foreground
|
|
93
93
|
|
|
94
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
95
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
96
|
+
# its left edge.
|
|
97
|
+
warning_notification: $warning
|
|
98
|
+
|
|
94
99
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
95
100
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
96
101
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -95,6 +95,11 @@ menu_bar_foreground: $foreground
|
|
|
95
95
|
footer_background: $panel
|
|
96
96
|
footer_foreground: $foreground
|
|
97
97
|
|
|
98
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
99
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
100
|
+
# its left edge.
|
|
101
|
+
warning_notification: $warning
|
|
102
|
+
|
|
98
103
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
99
104
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
100
105
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
|
|
|
96
96
|
footer_background: $panel
|
|
97
97
|
footer_foreground: $foreground
|
|
98
98
|
|
|
99
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
100
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
101
|
+
# its left edge.
|
|
102
|
+
warning_notification: $warning
|
|
103
|
+
|
|
99
104
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
100
105
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
101
106
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
|
|
|
96
96
|
footer_background: $panel
|
|
97
97
|
footer_foreground: $foreground
|
|
98
98
|
|
|
99
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
100
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
101
|
+
# its left edge.
|
|
102
|
+
warning_notification: $warning
|
|
103
|
+
|
|
99
104
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
100
105
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
101
106
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
|
|
|
96
96
|
footer_background: $panel
|
|
97
97
|
footer_foreground: $foreground
|
|
98
98
|
|
|
99
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
100
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
101
|
+
# its left edge.
|
|
102
|
+
warning_notification: $warning
|
|
103
|
+
|
|
99
104
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
100
105
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
101
106
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -93,6 +93,11 @@ menu_bar_foreground: $foreground
|
|
|
93
93
|
footer_background: $panel
|
|
94
94
|
footer_foreground: $foreground
|
|
95
95
|
|
|
96
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
97
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
98
|
+
# its left edge.
|
|
99
|
+
warning_notification: $warning
|
|
100
|
+
|
|
96
101
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
97
102
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
98
103
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -93,6 +93,11 @@ menu_bar_foreground: $foreground
|
|
|
93
93
|
footer_background: $panel
|
|
94
94
|
footer_foreground: $foreground
|
|
95
95
|
|
|
96
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
97
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
98
|
+
# its left edge.
|
|
99
|
+
warning_notification: $warning
|
|
100
|
+
|
|
96
101
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
97
102
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
98
103
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -99,6 +99,11 @@ menu_bar_foreground: $foreground
|
|
|
99
99
|
footer_background: $panel
|
|
100
100
|
footer_foreground: $foreground
|
|
101
101
|
|
|
102
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
103
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
104
|
+
# its left edge.
|
|
105
|
+
warning_notification: $warning
|
|
106
|
+
|
|
102
107
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
103
108
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
104
109
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
|
|
|
89
89
|
footer_background: $panel
|
|
90
90
|
footer_foreground: $foreground
|
|
91
91
|
|
|
92
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
93
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
94
|
+
# its left edge.
|
|
95
|
+
warning_notification: $warning
|
|
96
|
+
|
|
92
97
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
93
98
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
94
99
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
|
|
|
90
90
|
footer_background: $panel
|
|
91
91
|
footer_foreground: $foreground
|
|
92
92
|
|
|
93
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
94
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
95
|
+
# its left edge.
|
|
96
|
+
warning_notification: $warning
|
|
97
|
+
|
|
93
98
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
94
99
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
95
100
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
|
|
|
90
90
|
footer_background: $panel
|
|
91
91
|
footer_foreground: $foreground
|
|
92
92
|
|
|
93
|
+
# Warning notifications (the toast that says a commit message is about to be
|
|
94
|
+
# prefixed with a branch the settings warn against): its text and the bar on
|
|
95
|
+
# its left edge.
|
|
96
|
+
warning_notification: $warning
|
|
97
|
+
|
|
93
98
|
# CSS variables emitted for the file-list cursor and multi-selection.
|
|
94
99
|
# Values may reference Textual palette variables with `$name` (e.g. `$primary`).
|
|
95
100
|
# References are resolved against this theme's own fields when the CSS is generated.
|
|
@@ -117,6 +117,13 @@ class UITheme:
|
|
|
117
117
|
footer_background: str = "$panel"
|
|
118
118
|
footer_foreground: str = "$foreground"
|
|
119
119
|
|
|
120
|
+
# CSS-side color of a warning notification: the toast's text and the bar
|
|
121
|
+
# on its left edge. The default is the palette's own warning role, which
|
|
122
|
+
# is what Textual paints that bar in anyway; a theme that has spent
|
|
123
|
+
# $warning on something else (midnight-commander's cyan chrome) names a
|
|
124
|
+
# color that still reads as a warning.
|
|
125
|
+
warning_notification: str = "$warning"
|
|
126
|
+
|
|
120
127
|
# CSS-side palette for the file list cursor + multi-selection.
|
|
121
128
|
cursor_row_background: str = "$primary"
|
|
122
129
|
cursor_row_foreground: str = "$foreground"
|
|
@@ -184,6 +191,7 @@ class UITheme:
|
|
|
184
191
|
"menu-bar-foreground": self._resolve_refs(self.menu_bar_foreground),
|
|
185
192
|
"footer-background": self._resolve_refs(self.footer_background),
|
|
186
193
|
"footer-foreground": self._resolve_refs(self.footer_foreground),
|
|
194
|
+
"warning-notification": self._resolve_refs(self.warning_notification),
|
|
187
195
|
"cursor-row-background": self._resolve_refs(self.cursor_row_background),
|
|
188
196
|
"cursor-row-foreground": self._resolve_refs(self.cursor_row_foreground),
|
|
189
197
|
"selected-file-background": self._resolve_refs(self.selected_file_background),
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitnc"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "GitNightCommander — a TUI git log browser built with Textual"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
dependencies = [
|
|
@@ -44,6 +44,7 @@ fmt = [
|
|
|
44
44
|
"ruff format gitnc tests",
|
|
45
45
|
]
|
|
46
46
|
start = "python -m gitnc {args}"
|
|
47
|
+
rebuild = "hatch build --clean {args}"
|
|
47
48
|
|
|
48
49
|
[tool.ruff]
|
|
49
50
|
line-length = 100
|
|
@@ -346,6 +346,8 @@ try:
|
|
|
346
346
|
SETTINGS_ROW_EDITOR_COMMAND_LINE,
|
|
347
347
|
SETTINGS_ROW_EDITOR_ENVIRONMENT,
|
|
348
348
|
SETTINGS_ROW_PARSE,
|
|
349
|
+
SETTINGS_ROW_PREFIX_PATTERN,
|
|
350
|
+
SETTINGS_ROW_PREFIX_WARN,
|
|
349
351
|
SETTINGS_ROW_PROMPT,
|
|
350
352
|
SETTINGS_SECTION_WIDTH,
|
|
351
353
|
SHORTCUT_ALIASES,
|
|
@@ -441,6 +443,8 @@ except ModuleNotFoundError as exc:
|
|
|
441
443
|
SETTINGS_ROW_EDITOR_COMMAND_LINE,
|
|
442
444
|
SETTINGS_ROW_EDITOR_ENVIRONMENT,
|
|
443
445
|
SETTINGS_ROW_PARSE,
|
|
446
|
+
SETTINGS_ROW_PREFIX_PATTERN,
|
|
447
|
+
SETTINGS_ROW_PREFIX_WARN,
|
|
444
448
|
SETTINGS_ROW_PROMPT,
|
|
445
449
|
SETTINGS_SECTION_WIDTH,
|
|
446
450
|
SHORTCUT_ALIASES,
|
|
@@ -3394,6 +3398,77 @@ class TestSettingsDialog(unittest.TestCase):
|
|
|
3394
3398
|
self.assertEqual(dialog.get_values().draft_prompt, "a ")
|
|
3395
3399
|
self.assertFalse(dialog.get_values().branch_prefix)
|
|
3396
3400
|
|
|
3401
|
+
def test_the_warned_prefixes_are_typed_under_the_prefix_checkbox(self):
|
|
3402
|
+
dialog = SettingsDialog(prefix_warn_branches="master")
|
|
3403
|
+
for _ in range(SETTINGS_ROW_PREFIX_WARN):
|
|
3404
|
+
dialog.on_key(_key_event("down"))
|
|
3405
|
+
self.assertEqual(dialog._cursor, SETTINGS_ROW_PREFIX_WARN)
|
|
3406
|
+
|
|
3407
|
+
for key, character in (("space", None), ("m", "m")):
|
|
3408
|
+
dialog.on_key(_key_event(key, character=character))
|
|
3409
|
+
|
|
3410
|
+
self.assertEqual(dialog.get_values().prefix_warn_branches, "master m")
|
|
3411
|
+
self.assertFalse(dialog.get_values().branch_prefix)
|
|
3412
|
+
|
|
3413
|
+
def test_the_warned_prefixes_round_trip_through_the_settings_file(self):
|
|
3414
|
+
app = _mock_app()
|
|
3415
|
+
with patch.object(
|
|
3416
|
+
app, "_load_settings", return_value={"commit_prefix_warn_branches": "master main"}
|
|
3417
|
+
):
|
|
3418
|
+
app.action_settings()
|
|
3419
|
+
values = _pushed_screen(app).get_values()
|
|
3420
|
+
self.assertEqual(values.prefix_warn_branches, "master main")
|
|
3421
|
+
|
|
3422
|
+
saved: dict[str, object] = {}
|
|
3423
|
+
with (
|
|
3424
|
+
patch.object(app, "_load_settings", return_value={}),
|
|
3425
|
+
patch.object(app, "_save_settings", side_effect=lambda s: saved.update(s) or True),
|
|
3426
|
+
):
|
|
3427
|
+
app._settings_result(values)
|
|
3428
|
+
|
|
3429
|
+
self.assertEqual(saved["commit_prefix_warn_branches"], "master main")
|
|
3430
|
+
|
|
3431
|
+
def test_the_required_prefix_pattern_is_typed_under_the_warned_prefixes(self):
|
|
3432
|
+
dialog = SettingsDialog(prefix_require_pattern="[A-Z]+")
|
|
3433
|
+
for _ in range(SETTINGS_ROW_PREFIX_PATTERN):
|
|
3434
|
+
dialog.on_key(_key_event("down"))
|
|
3435
|
+
self.assertEqual(dialog._cursor, SETTINGS_ROW_PREFIX_PATTERN)
|
|
3436
|
+
self.assertEqual(SETTINGS_ROW_PREFIX_PATTERN, SETTINGS_ROW_PREFIX_WARN + 1)
|
|
3437
|
+
|
|
3438
|
+
for key, character in (("minus", "-"), ("1", "1")):
|
|
3439
|
+
dialog.on_key(_key_event(key, character=character))
|
|
3440
|
+
|
|
3441
|
+
self.assertEqual(dialog.get_values().prefix_require_pattern, "[A-Z]+-1")
|
|
3442
|
+
self.assertEqual(dialog.get_values().prefix_warn_branches, "")
|
|
3443
|
+
|
|
3444
|
+
def test_the_required_prefix_pattern_hint_says_when_it_does_not_compile(self):
|
|
3445
|
+
self.assertIn("optional", SettingsDialog()._prefix_pattern_hint())
|
|
3446
|
+
self.assertIn(
|
|
3447
|
+
"optional", SettingsDialog(prefix_require_pattern="[A-Z]+:")._prefix_pattern_hint()
|
|
3448
|
+
)
|
|
3449
|
+
self.assertEqual(
|
|
3450
|
+
SettingsDialog(prefix_require_pattern="[A-Z")._prefix_pattern_hint(),
|
|
3451
|
+
"not a valid regular expression",
|
|
3452
|
+
)
|
|
3453
|
+
|
|
3454
|
+
def test_the_required_prefix_pattern_round_trips_through_the_settings_file(self):
|
|
3455
|
+
app = _mock_app()
|
|
3456
|
+
with patch.object(
|
|
3457
|
+
app, "_load_settings", return_value={"commit_prefix_require_pattern": r"[A-Z]+-\d+:"}
|
|
3458
|
+
):
|
|
3459
|
+
app.action_settings()
|
|
3460
|
+
values = _pushed_screen(app).get_values()
|
|
3461
|
+
self.assertEqual(values.prefix_require_pattern, r"[A-Z]+-\d+:")
|
|
3462
|
+
|
|
3463
|
+
saved: dict[str, object] = {}
|
|
3464
|
+
with (
|
|
3465
|
+
patch.object(app, "_load_settings", return_value={}),
|
|
3466
|
+
patch.object(app, "_save_settings", side_effect=lambda s: saved.update(s) or True),
|
|
3467
|
+
):
|
|
3468
|
+
app._settings_result(values)
|
|
3469
|
+
|
|
3470
|
+
self.assertEqual(saved["commit_prefix_require_pattern"], r"[A-Z]+-\d+:")
|
|
3471
|
+
|
|
3397
3472
|
def test_cursor_wraps_over_every_row(self):
|
|
3398
3473
|
dialog = SettingsDialog()
|
|
3399
3474
|
|
|
@@ -4681,6 +4756,7 @@ class TestUIThemeCssVariables(unittest.TestCase):
|
|
|
4681
4756
|
"menu-bar-foreground",
|
|
4682
4757
|
"footer-background",
|
|
4683
4758
|
"footer-foreground",
|
|
4759
|
+
"warning-notification",
|
|
4684
4760
|
"cursor-row-background",
|
|
4685
4761
|
"cursor-row-foreground",
|
|
4686
4762
|
"selected-file-background",
|
|
@@ -4722,6 +4798,28 @@ class TestUIThemeCssVariables(unittest.TestCase):
|
|
|
4722
4798
|
self.assertNotIn("$", variables[field_name])
|
|
4723
4799
|
|
|
4724
4800
|
|
|
4801
|
+
class TestWarningNotificationThemeColor(unittest.TestCase):
|
|
4802
|
+
"""A warning toast is painted from the theme's `warning_notification`
|
|
4803
|
+
field rather than from `$warning`, which a theme may use for chrome."""
|
|
4804
|
+
|
|
4805
|
+
def test_the_default_is_the_palette_warning_role(self):
|
|
4806
|
+
from gitnc.ui_theme import UITheme
|
|
4807
|
+
|
|
4808
|
+
theme = UITheme(name="plain", primary="#123456", warning="#abcdef")
|
|
4809
|
+
|
|
4810
|
+
self.assertEqual(theme.css_variables()["warning-notification"], "#abcdef")
|
|
4811
|
+
|
|
4812
|
+
def test_midnight_commander_warns_in_red_not_in_its_chrome_cyan(self):
|
|
4813
|
+
css_vars = MIDNIGHT_COMMANDER_THEME.css_variables()
|
|
4814
|
+
|
|
4815
|
+
self.assertEqual(css_vars["warning-notification"], "#FF5555")
|
|
4816
|
+
self.assertNotEqual(css_vars["warning-notification"], MIDNIGHT_COMMANDER_THEME.warning)
|
|
4817
|
+
|
|
4818
|
+
def test_the_app_css_paints_warning_toasts_from_the_variable(self):
|
|
4819
|
+
self.assertIn("border-left: outer $warning-notification;", GitNightCommanderApp.CSS)
|
|
4820
|
+
self.assertIn("color: $warning-notification;", GitNightCommanderApp.CSS)
|
|
4821
|
+
|
|
4822
|
+
|
|
4725
4823
|
class TestTextSelectionThemeColors(unittest.TestCase):
|
|
4726
4824
|
"""Selected text in the commit message box takes its colors from the
|
|
4727
4825
|
theme's text selection fields, emitted as Textual's own
|
|
@@ -5195,6 +5293,210 @@ class TestCommitFlow(unittest.TestCase):
|
|
|
5195
5293
|
self.assertTrue(dialogs.pushed[-1].fill_height)
|
|
5196
5294
|
app.git.commit.assert_not_called()
|
|
5197
5295
|
|
|
5296
|
+
def _warned_app(self):
|
|
5297
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5298
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
|
|
5299
|
+
app._load_settings = MagicMock(return_value={"commit_prefix_warn_branches": "main, master"})
|
|
5300
|
+
app.action_commit_files()
|
|
5301
|
+
return app, dialogs
|
|
5302
|
+
|
|
5303
|
+
def test_a_warned_prefix_is_asked_about_before_the_commit_confirmation(self):
|
|
5304
|
+
app, dialogs = self._warned_app()
|
|
5305
|
+
|
|
5306
|
+
dialogs._resolve("master: fix x")
|
|
5307
|
+
|
|
5308
|
+
self.assertEqual(
|
|
5309
|
+
dialogs.prompt(),
|
|
5310
|
+
"Warning: commit message is prefixed with branch 'master'. Continue?",
|
|
5311
|
+
)
|
|
5312
|
+
self.assertEqual(dialogs.commands(), ["master: fix x"])
|
|
5313
|
+
app.git.commit.assert_not_called()
|
|
5314
|
+
|
|
5315
|
+
def test_accepting_the_prefix_warning_goes_on_to_the_commit_confirmation(self):
|
|
5316
|
+
app, dialogs = self._warned_app()
|
|
5317
|
+
dialogs._resolve("master: fix x")
|
|
5318
|
+
|
|
5319
|
+
dialogs.confirm()
|
|
5320
|
+
|
|
5321
|
+
self.assertEqual(dialogs.prompt(), "Commit 1 file(s)?")
|
|
5322
|
+
self.assertEqual(dialogs.commands(), ["git commit -m master: fix x"])
|
|
5323
|
+
app.git.commit.assert_not_called()
|
|
5324
|
+
|
|
5325
|
+
def test_declining_the_prefix_warning_reopens_the_message(self):
|
|
5326
|
+
app, dialogs = self._warned_app()
|
|
5327
|
+
dialogs._resolve("master: fix x")
|
|
5328
|
+
|
|
5329
|
+
dialogs.cancel()
|
|
5330
|
+
|
|
5331
|
+
reopened = dialogs.pushed[-1]
|
|
5332
|
+
self.assertIsInstance(reopened, CommitDialog)
|
|
5333
|
+
self.assertEqual(reopened.prefill, "master: fix x")
|
|
5334
|
+
app.git.commit.assert_not_called()
|
|
5335
|
+
|
|
5336
|
+
def test_a_warned_prefix_is_announced_when_the_message_editor_opens(self):
|
|
5337
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5338
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"}, branch="master")
|
|
5339
|
+
app._load_settings = MagicMock(
|
|
5340
|
+
return_value={
|
|
5341
|
+
"branch_prefix_in_commits": True,
|
|
5342
|
+
"commit_prefix_warn_branches": "master",
|
|
5343
|
+
}
|
|
5344
|
+
)
|
|
5345
|
+
|
|
5346
|
+
with patch.object(app, "notify") as notify:
|
|
5347
|
+
app.action_commit_files()
|
|
5348
|
+
|
|
5349
|
+
self.assertEqual(self._commit_screen(dialogs).prefill, "master: ")
|
|
5350
|
+
notify.assert_called_once_with(
|
|
5351
|
+
"Warning: commit message is prefixed with branch 'master'", severity="warning"
|
|
5352
|
+
)
|
|
5353
|
+
|
|
5354
|
+
def test_an_unlisted_prefix_opens_the_message_editor_quietly(self):
|
|
5355
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5356
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"}, branch="feature")
|
|
5357
|
+
app._load_settings = MagicMock(
|
|
5358
|
+
return_value={
|
|
5359
|
+
"branch_prefix_in_commits": True,
|
|
5360
|
+
"commit_prefix_warn_branches": "master",
|
|
5361
|
+
}
|
|
5362
|
+
)
|
|
5363
|
+
|
|
5364
|
+
with patch.object(app, "notify") as notify:
|
|
5365
|
+
app.action_commit_files()
|
|
5366
|
+
|
|
5367
|
+
self.assertEqual(self._commit_screen(dialogs).prefill, "feature: ")
|
|
5368
|
+
notify.assert_not_called()
|
|
5369
|
+
|
|
5370
|
+
def _pattern_app(self, pattern: str, **settings: Any):
|
|
5371
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5372
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
|
|
5373
|
+
app._load_settings = MagicMock(
|
|
5374
|
+
return_value={"commit_prefix_require_pattern": pattern, **settings}
|
|
5375
|
+
)
|
|
5376
|
+
app.action_commit_files()
|
|
5377
|
+
return app, dialogs
|
|
5378
|
+
|
|
5379
|
+
def test_a_message_missing_the_required_prefix_is_asked_about(self):
|
|
5380
|
+
app, dialogs = self._pattern_app(r"[A-Z]+-\d+:")
|
|
5381
|
+
|
|
5382
|
+
dialogs._resolve("fix x")
|
|
5383
|
+
|
|
5384
|
+
self.assertEqual(
|
|
5385
|
+
dialogs.prompt(),
|
|
5386
|
+
"Warning: commit message does not start with a prefix matching"
|
|
5387
|
+
r" '\[A-Z]+-\d+:'. Continue?",
|
|
5388
|
+
)
|
|
5389
|
+
self.assertEqual(dialogs.commands(), ["fix x"])
|
|
5390
|
+
app.git.commit.assert_not_called()
|
|
5391
|
+
|
|
5392
|
+
def test_declining_the_required_prefix_warning_reopens_the_message(self):
|
|
5393
|
+
_, dialogs = self._pattern_app(r"\w+-\d+:")
|
|
5394
|
+
dialogs._resolve("fix x")
|
|
5395
|
+
|
|
5396
|
+
dialogs.cancel()
|
|
5397
|
+
|
|
5398
|
+
reopened = dialogs.pushed[-1]
|
|
5399
|
+
self.assertIsInstance(reopened, CommitDialog)
|
|
5400
|
+
self.assertEqual(reopened.prefill, "fix x")
|
|
5401
|
+
|
|
5402
|
+
def test_the_required_pattern_covers_only_the_start_of_the_message(self):
|
|
5403
|
+
"""No `.*` needed for the rest of the message, and a match further
|
|
5404
|
+
in doesn't count as a prefix."""
|
|
5405
|
+
for message, accepted in (
|
|
5406
|
+
("ABC-12: fix x\n\nbody", True),
|
|
5407
|
+
("ABC-12:", True),
|
|
5408
|
+
("fix ABC-12: x", False),
|
|
5409
|
+
("\nABC-12: x", False),
|
|
5410
|
+
):
|
|
5411
|
+
with self.subTest(message=message):
|
|
5412
|
+
_, dialogs = self._pattern_app(r"[A-Z]+-\d+:")
|
|
5413
|
+
|
|
5414
|
+
dialogs._resolve(message)
|
|
5415
|
+
|
|
5416
|
+
self.assertEqual(dialogs.prompt() == "Commit 1 file(s)?", accepted)
|
|
5417
|
+
|
|
5418
|
+
def test_a_blank_required_pattern_checks_nothing(self):
|
|
5419
|
+
_, dialogs = self._pattern_app(" ")
|
|
5420
|
+
|
|
5421
|
+
dialogs._resolve("fix x")
|
|
5422
|
+
|
|
5423
|
+
self.assertEqual(dialogs.prompt(), "Commit 1 file(s)?")
|
|
5424
|
+
|
|
5425
|
+
def test_an_invalid_required_pattern_is_reported_instead_of_skipped(self):
|
|
5426
|
+
_, dialogs = self._pattern_app("[A-Z")
|
|
5427
|
+
|
|
5428
|
+
dialogs._resolve("fix x")
|
|
5429
|
+
|
|
5430
|
+
self.assertEqual(
|
|
5431
|
+
dialogs.prompt(),
|
|
5432
|
+
r"Warning: commit message can't be checked against invalid pattern '\[A-Z'."
|
|
5433
|
+
" Continue?",
|
|
5434
|
+
)
|
|
5435
|
+
|
|
5436
|
+
def test_a_warned_branch_and_a_missing_prefix_are_reported_together(self):
|
|
5437
|
+
_, dialogs = self._pattern_app(r"\d+:", commit_prefix_warn_branches="master")
|
|
5438
|
+
|
|
5439
|
+
dialogs._resolve("master: fix x")
|
|
5440
|
+
|
|
5441
|
+
self.assertEqual(
|
|
5442
|
+
dialogs.prompt(),
|
|
5443
|
+
"Warning: commit message is prefixed with branch 'master'"
|
|
5444
|
+
r" and does not start with a prefix matching '\d+:'. Continue?",
|
|
5445
|
+
)
|
|
5446
|
+
|
|
5447
|
+
def test_a_prefill_missing_the_required_prefix_is_announced_up_front(self):
|
|
5448
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5449
|
+
for branch, warned in (("feature", True), ("ABC-12", False)):
|
|
5450
|
+
with self.subTest(branch=branch):
|
|
5451
|
+
app, _, _ = self._app([entry], staged={"pkg/a.py"}, branch=branch)
|
|
5452
|
+
app._load_settings = MagicMock(
|
|
5453
|
+
return_value={
|
|
5454
|
+
"branch_prefix_in_commits": True,
|
|
5455
|
+
"commit_prefix_require_pattern": r"[A-Z]+-\d+:",
|
|
5456
|
+
}
|
|
5457
|
+
)
|
|
5458
|
+
|
|
5459
|
+
with patch.object(app, "notify") as notify:
|
|
5460
|
+
app.action_commit_files()
|
|
5461
|
+
|
|
5462
|
+
if warned:
|
|
5463
|
+
notify.assert_called_once_with(
|
|
5464
|
+
"Warning: commit message does not start with a prefix matching"
|
|
5465
|
+
r" '\[A-Z]+-\d+:'",
|
|
5466
|
+
severity="warning",
|
|
5467
|
+
)
|
|
5468
|
+
else:
|
|
5469
|
+
notify.assert_not_called()
|
|
5470
|
+
|
|
5471
|
+
def test_an_empty_prefill_is_not_checked_against_the_required_pattern(self):
|
|
5472
|
+
"""Nothing has been written yet, so there is no prefix to judge."""
|
|
5473
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5474
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
|
|
5475
|
+
app._load_settings = MagicMock(return_value={"commit_prefix_require_pattern": r"\d+:"})
|
|
5476
|
+
|
|
5477
|
+
with patch.object(app, "notify") as notify:
|
|
5478
|
+
app.action_commit_files()
|
|
5479
|
+
|
|
5480
|
+
self.assertEqual(self._commit_screen(dialogs).prefill, "")
|
|
5481
|
+
notify.assert_not_called()
|
|
5482
|
+
|
|
5483
|
+
def test_other_prefixes_go_straight_to_the_commit_confirmation(self):
|
|
5484
|
+
"""Only a whole listed name followed by the colon counts — not a
|
|
5485
|
+
branch that merely starts with one, and not the name mid-message."""
|
|
5486
|
+
entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
|
|
5487
|
+
for message in ("master-fix: x", "feature: merge master: x", "masterful change"):
|
|
5488
|
+
with self.subTest(message=message):
|
|
5489
|
+
app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
|
|
5490
|
+
app._load_settings = MagicMock(
|
|
5491
|
+
return_value={"commit_prefix_warn_branches": "master"}
|
|
5492
|
+
)
|
|
5493
|
+
app.action_commit_files()
|
|
5494
|
+
|
|
5495
|
+
dialogs._resolve(message)
|
|
5496
|
+
|
|
5497
|
+
self.assertEqual(dialogs.prompt(), "Commit 1 file(s)?")
|
|
5498
|
+
self.assertEqual(dialogs.commands(), [f"git commit -m {message}"])
|
|
5499
|
+
|
|
5198
5500
|
def test_the_confirmation_shows_only_the_subject_of_a_long_message(self):
|
|
5199
5501
|
"""The popup renders one command per row, so a multi-line `-m` has to
|
|
5200
5502
|
be cut down to its first line — marked as cut."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|