gitnc 0.1.5__tar.gz → 0.1.6__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.5 → gitnc-0.1.6}/CLAUDE.md +1 -1
- {gitnc-0.1.5 → gitnc-0.1.6}/PKG-INFO +1 -1
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/app.py +40 -11
- {gitnc-0.1.5 → gitnc-0.1.6}/pyproject.toml +1 -1
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/test_app.py +48 -7
- {gitnc-0.1.5 → gitnc-0.1.6}/.devcontainer/Dockerfile +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/.devcontainer/devcontainer.json +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/.gitignore +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/LICENSE +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/README.md +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/__init__.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/__main__.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/git.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/terminal.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/__init__.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/atom-one-dark.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/atom-one-light.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/catppuccin-frappe.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/catppuccin-latte.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/catppuccin-macchiato.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/catppuccin-mocha.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/dracula.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/flexoki.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/gruvbox.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/midnight-commander.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/monokai.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/nord.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/rose-pine-dawn.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/rose-pine-moon.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/rose-pine.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/solarized-dark.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/solarized-light.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/textual-ansi.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/textual-dark.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/textual-light.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/themes/tokyo-night.yaml +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/gitnc/ui_theme.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/__init__.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/gitprocess.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/test_git.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/test_terminal.py +0 -0
- {gitnc-0.1.5 → gitnc-0.1.6}/tests/test_themes.py +0 -0
|
@@ -62,7 +62,7 @@ Menu chrome is composed once and stays mounted, so `action_set_theme` / `_load_s
|
|
|
62
62
|
|
|
63
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
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.
|
|
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. It says so *beside* the help rather than in place of it: a pattern is invalid for most of the time it takes to type one (`[` already is), which is when the example is being read, and the help is two fixed lines so the dialog doesn't change height as the pattern passes in and out of compiling. 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.
|
|
66
66
|
|
|
67
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.
|
|
68
68
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: gitnc
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -728,14 +728,29 @@ def _strip_markup(text: str) -> str:
|
|
|
728
728
|
return re.sub(r"\[/?[^\[\]]*\]", "", text)
|
|
729
729
|
|
|
730
730
|
|
|
731
|
+
# A zero-width space: takes no cell on screen, and is enough to stop a
|
|
732
|
+
# backslash from escaping the tag that follows it. See `_escape_markup()`.
|
|
733
|
+
MARKUP_BACKSLASH_GUARD = "\u200b"
|
|
734
|
+
|
|
735
|
+
|
|
731
736
|
def _escape_markup(text: str) -> str:
|
|
732
737
|
"""`text` rendered literally, i.e. with its markup tags neutralised.
|
|
733
738
|
|
|
734
739
|
Settings values are typed by the user and drawn into a markup-rendering
|
|
735
740
|
Static, so a prompt template mentioning "[files]" would otherwise be read
|
|
736
741
|
as a style tag and swallowed.
|
|
742
|
+
|
|
743
|
+
A backslash at the end gets `MARKUP_BACKSLASH_GUARD` after it: whatever
|
|
744
|
+
the caller puts next is usually a tag — the caret's, a closing one — and
|
|
745
|
+
a "[" behind a backslash is never read as a tag, so the value's last
|
|
746
|
+
character would otherwise escape the caller's markup. Textual has no
|
|
747
|
+
escape for the backslash itself, hence a separator rather than a second
|
|
748
|
+
backslash.
|
|
737
749
|
"""
|
|
738
|
-
|
|
750
|
+
escaped = text.replace("[", "\\[")
|
|
751
|
+
if escaped.endswith("\\"):
|
|
752
|
+
escaped += MARKUP_BACKSLASH_GUARD
|
|
753
|
+
return escaped
|
|
739
754
|
|
|
740
755
|
|
|
741
756
|
# One word of a settings field plus the spaces that follow it, or a run of
|
|
@@ -978,6 +993,8 @@ SETTINGS_FIELD_MAX_LINES = 8
|
|
|
978
993
|
# field, which is the widest thing the dialog draws (its four-cell indent
|
|
979
994
|
# plus the frame's two borders).
|
|
980
995
|
SETTINGS_SECTION_WIDTH = 4 + SETTINGS_FIELD_WIDTH + 2
|
|
996
|
+
# Shown beside the required-prefix help while the pattern doesn't compile.
|
|
997
|
+
PREFIX_PATTERN_INVALID = "not a valid regular expression"
|
|
981
998
|
|
|
982
999
|
|
|
983
1000
|
def _dialog_theme(screen: ModalScreen) -> UITheme:
|
|
@@ -1576,9 +1593,7 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1576
1593
|
lines.append(
|
|
1577
1594
|
self._text_row(SETTINGS_ROW_PREFIX_PATTERN, " Warn on prefix not matching: ")
|
|
1578
1595
|
)
|
|
1579
|
-
lines.
|
|
1580
|
-
f"[{theme.dialog_hint}] ({self._prefix_pattern_hint()})[/{theme.dialog_hint}]"
|
|
1581
|
-
)
|
|
1596
|
+
lines.extend(self._prefix_pattern_hint())
|
|
1582
1597
|
lines.append("")
|
|
1583
1598
|
lines.append(self._section_header("Commit message editor"))
|
|
1584
1599
|
lines.append("")
|
|
@@ -1625,14 +1640,28 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
|
|
|
1625
1640
|
# values are already recorded, so there is simply nothing to paint.
|
|
1626
1641
|
pass
|
|
1627
1642
|
|
|
1628
|
-
def _prefix_pattern_hint(self) -> str:
|
|
1629
|
-
"""The
|
|
1630
|
-
|
|
1631
|
-
rather than first at the
|
|
1632
|
-
|
|
1633
|
-
|
|
1643
|
+
def _prefix_pattern_hint(self) -> list[str]:
|
|
1644
|
+
"""The lines under the required-prefix row: what goes in it, and —
|
|
1645
|
+
added to that rather than put in its place — that what is in it won't
|
|
1646
|
+
compile, said here, while it is being typed, rather than first at the
|
|
1647
|
+
next commit.
|
|
1648
|
+
|
|
1649
|
+
The help stays because a pattern is invalid for most of the time it
|
|
1650
|
+
takes to type one ("[" already is), which is exactly when the example
|
|
1651
|
+
is being read. It is two fixed lines for the same reason: left to
|
|
1652
|
+
wrap, the dialog grew and shrank a row as the pattern passed in and
|
|
1653
|
+
out of compiling.
|
|
1654
|
+
"""
|
|
1655
|
+
theme = _dialog_theme(self)
|
|
1656
|
+
hint = theme.dialog_hint
|
|
1634
1657
|
example = _escape_markup(r"[A-Z]+-\d+:")
|
|
1635
|
-
|
|
1658
|
+
invalid = ""
|
|
1659
|
+
if not _is_valid_pattern(self._prefix_require_pattern):
|
|
1660
|
+
invalid = f" [{theme.warning_text}]{PREFIX_PATTERN_INVALID}[/{theme.warning_text}]"
|
|
1661
|
+
return [
|
|
1662
|
+
f"[{hint}] (optional regular expression for the start of the message,[/{hint}]",
|
|
1663
|
+
f"[{hint}] e.g. {example})[/{hint}]{invalid}",
|
|
1664
|
+
]
|
|
1636
1665
|
|
|
1637
1666
|
def _key_hint(self) -> str:
|
|
1638
1667
|
"""The key line under the dialog, which is not the same in a framed
|
|
@@ -3442,14 +3442,31 @@ class TestSettingsDialog(unittest.TestCase):
|
|
|
3442
3442
|
self.assertEqual(dialog.get_values().prefix_warn_branches, "")
|
|
3443
3443
|
|
|
3444
3444
|
def test_the_required_prefix_pattern_hint_says_when_it_does_not_compile(self):
|
|
3445
|
-
|
|
3445
|
+
invalid = _app_module.PREFIX_PATTERN_INVALID
|
|
3446
|
+
for pattern in ("", "[A-Z]+:"):
|
|
3447
|
+
hint = "\n".join(SettingsDialog(prefix_require_pattern=pattern)._prefix_pattern_hint())
|
|
3448
|
+
self.assertIn("optional", hint)
|
|
3449
|
+
self.assertNotIn(invalid, hint)
|
|
3446
3450
|
self.assertIn(
|
|
3447
|
-
"
|
|
3448
|
-
)
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3451
|
+
invalid, "\n".join(SettingsDialog(prefix_require_pattern="[A-Z")._prefix_pattern_hint())
|
|
3452
|
+
)
|
|
3453
|
+
|
|
3454
|
+
def test_the_required_prefix_pattern_help_stays_while_the_pattern_is_invalid(self):
|
|
3455
|
+
"""A pattern starting with "[" doesn't compile until its "]" is typed,
|
|
3456
|
+
and the example used to be replaced by the complaint for all of that
|
|
3457
|
+
time — the help went away just as it was being followed."""
|
|
3458
|
+
valid = SettingsDialog()._prefix_pattern_hint()
|
|
3459
|
+
invalid = SettingsDialog(prefix_require_pattern="[")._prefix_pattern_hint()
|
|
3460
|
+
|
|
3461
|
+
# Same number of rows either way, so the dialog doesn't change height.
|
|
3462
|
+
self.assertEqual(len(invalid), len(valid))
|
|
3463
|
+
self.assertEqual(invalid[:-1], valid[:-1])
|
|
3464
|
+
self.assertTrue(invalid[-1].startswith(valid[-1]))
|
|
3465
|
+
for line in invalid:
|
|
3466
|
+
self.assertLessEqual(
|
|
3467
|
+
len(_app_module._strip_markup(line.replace("\\[", ""))),
|
|
3468
|
+
_app_module.SETTINGS_SECTION_WIDTH,
|
|
3469
|
+
)
|
|
3453
3470
|
|
|
3454
3471
|
def test_the_required_prefix_pattern_round_trips_through_the_settings_file(self):
|
|
3455
3472
|
app = _mock_app()
|
|
@@ -3710,6 +3727,30 @@ class TestSettingsDialog(unittest.TestCase):
|
|
|
3710
3727
|
dialog.on_key(_key_event("d", character="d"))
|
|
3711
3728
|
self.assertEqual(dialog.get_values().draft_prompt, "abdc")
|
|
3712
3729
|
|
|
3730
|
+
def test_a_backslash_does_not_escape_the_tag_that_follows_it(self):
|
|
3731
|
+
"""A regex ends in a backslash halfway through typing `\\d`, and the
|
|
3732
|
+
caret's tag comes right after it: unguarded, the backslash escaped
|
|
3733
|
+
that tag and its closing half had nothing to close — a MarkupError
|
|
3734
|
+
that took the app down on the keypress."""
|
|
3735
|
+
escape = _app_module._escape_markup
|
|
3736
|
+
guard = _app_module.MARKUP_BACKSLASH_GUARD
|
|
3737
|
+
|
|
3738
|
+
self.assertEqual(escape("[A-Z]+-\\"), f"\\[A-Z]+-\\{guard}")
|
|
3739
|
+
# Only the end needs it: inside the value the next character is text.
|
|
3740
|
+
self.assertEqual(escape("\\d+:"), "\\d+:")
|
|
3741
|
+
for value, column in (("\\", 1), ("\\", 0), ("a\\b", 1), ("a\\b\\", 4)):
|
|
3742
|
+
markup = _app_module._caret_markup(value, column=column, style="reverse")
|
|
3743
|
+
self.assertNotIn("\\[", markup)
|
|
3744
|
+
try:
|
|
3745
|
+
from textual.content import Content
|
|
3746
|
+
except ImportError:
|
|
3747
|
+
continue
|
|
3748
|
+
# The real parser, when it is installed: the tags are read as
|
|
3749
|
+
# tags, and the guard costs no cell.
|
|
3750
|
+
content = Content.from_markup(markup)
|
|
3751
|
+
self.assertEqual(content.plain.replace(guard, ""), value.ljust(column + 1))
|
|
3752
|
+
self.assertEqual(content.cell_length, max(len(value), column + 1))
|
|
3753
|
+
|
|
3713
3754
|
def test_backspace_takes_the_character_in_front_of_the_caret(self):
|
|
3714
3755
|
dialog = SettingsDialog(draft_prompt="abc")
|
|
3715
3756
|
self._focus_prompt(dialog)
|
|
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
|
|
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
|
|
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
|