git-commit-guard 0.10.0__tar.gz → 0.11.0__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.
@@ -6,6 +6,7 @@ on: # yamllint disable-line rule:truthy
6
6
  - 'v*'
7
7
  permissions:
8
8
  contents: write
9
+ id-token: write
9
10
  jobs:
10
11
  release:
11
12
  runs-on: ubuntu-latest
@@ -16,6 +17,19 @@ jobs:
16
17
  with:
17
18
  persist-credentials: false
18
19
  fetch-depth: 0
20
+ - name: Set up Python
21
+ # yamllint disable-line rule:line-length
22
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
23
+ - name: Set up uv
24
+ # yamllint disable-line rule:line-length
25
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
26
+ with:
27
+ enable-cache: false
28
+ - name: Build package
29
+ run: uv build
30
+ - name: Publish to PyPI
31
+ # yamllint disable-line rule:line-length
32
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
19
33
  - name: Generate release notes
20
34
  id: git-cliff
21
35
  # yamllint disable-line rule:line-length
@@ -31,3 +45,8 @@ jobs:
31
45
  gh release create "$TAG" \
32
46
  --title "$TAG" \
33
47
  --notes-file "$NOTES_FILE"
48
+ - name: Upload dist to GitHub Release
49
+ env:
50
+ GH_TOKEN: ${{ github.token }}
51
+ TAG: ${{ github.ref_name }}
52
+ run: gh release upload "$TAG" dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-commit-guard
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: Opinionated conventional commit message linter with imperative mood detection
5
5
  Project-URL: Homepage, https://github.com/benner/commit-guard
6
6
  Project-URL: Repository, https://github.com/benner/commit-guard
@@ -110,6 +110,13 @@ The default maximum subject line length is 72 characters. Override with
110
110
  commit-guard --max-subject-length 100
111
111
  ```
112
112
 
113
+ By default there is no minimum description length. Enforce one with
114
+ `--min-description-length`:
115
+
116
+ ```bash
117
+ commit-guard --min-description-length 10
118
+ ```
119
+
113
120
  ### Type validation
114
121
 
115
122
  By default the standard conventional commit types are accepted. Use `--types`
@@ -143,9 +150,9 @@ commit-guard --scopes auth,api --require-scope
143
150
  ### Configuration file
144
151
 
145
152
  Place `.commit-guard.toml` in your project root (or any parent directory) to
146
- set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`, and
147
- `max-subject-length`. commit-guard searches upward from the working directory
148
- and uses the first file found.
153
+ set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`,
154
+ `max-subject-length`, and `min-description-length`. commit-guard searches
155
+ upward from the working directory and uses the first file found.
149
156
 
150
157
  ```toml
151
158
  # .commit-guard.toml
@@ -154,6 +161,7 @@ scopes = ["auth", "api", "db"]
154
161
  require-scope = true
155
162
  types = ["feat", "fix", "chore", "wip"]
156
163
  max-subject-length = 100
164
+ min-description-length = 10
157
165
  ```
158
166
 
159
167
  ```toml
@@ -162,8 +170,8 @@ enable = ["subject", "imperative"]
162
170
  ```
163
171
 
164
172
  CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
165
- `--max-subject-length`) take full precedence and ignore config file values when
166
- provided.
173
+ `--max-subject-length`, `--min-description-length`) take full precedence and
174
+ ignore config file values when provided.
167
175
 
168
176
  ### Checking a range of commits
169
177
 
@@ -89,6 +89,13 @@ The default maximum subject line length is 72 characters. Override with
89
89
  commit-guard --max-subject-length 100
90
90
  ```
91
91
 
92
+ By default there is no minimum description length. Enforce one with
93
+ `--min-description-length`:
94
+
95
+ ```bash
96
+ commit-guard --min-description-length 10
97
+ ```
98
+
92
99
  ### Type validation
93
100
 
94
101
  By default the standard conventional commit types are accepted. Use `--types`
@@ -122,9 +129,9 @@ commit-guard --scopes auth,api --require-scope
122
129
  ### Configuration file
123
130
 
124
131
  Place `.commit-guard.toml` in your project root (or any parent directory) to
125
- set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`, and
126
- `max-subject-length`. commit-guard searches upward from the working directory
127
- and uses the first file found.
132
+ set defaults for `enable`, `disable`, `scopes`, `require-scope`, `types`,
133
+ `max-subject-length`, and `min-description-length`. commit-guard searches
134
+ upward from the working directory and uses the first file found.
128
135
 
129
136
  ```toml
130
137
  # .commit-guard.toml
@@ -133,6 +140,7 @@ scopes = ["auth", "api", "db"]
133
140
  require-scope = true
134
141
  types = ["feat", "fix", "chore", "wip"]
135
142
  max-subject-length = 100
143
+ min-description-length = 10
136
144
  ```
137
145
 
138
146
  ```toml
@@ -141,8 +149,8 @@ enable = ["subject", "imperative"]
141
149
  ```
142
150
 
143
151
  CLI flags (`--enable`, `--disable`, `--scopes`, `--require-scope`, `--types`,
144
- `--max-subject-length`) take full precedence and ignore config file values when
145
- provided.
152
+ `--max-subject-length`, `--min-description-length`) take full precedence and
153
+ ignore config file values when provided.
146
154
 
147
155
  ### Checking a range of commits
148
156
 
@@ -120,12 +120,13 @@ def _strip_comments(message):
120
120
  )
121
121
 
122
122
 
123
- def check_subject( # noqa: PLR0913 Too many arguments in function definition (6 > 5)
123
+ def check_subject( # noqa: PLR0913 Too many arguments in function definition (7 > 5)
124
124
  line,
125
125
  result,
126
126
  allowed_scopes=frozenset(),
127
127
  allowed_types=TYPES,
128
128
  max_subject_length=MAX_SUBJECT_LEN,
129
+ min_description_length=0,
129
130
  *,
130
131
  require_scope=False,
131
132
  ):
@@ -150,6 +151,8 @@ def check_subject( # noqa: PLR0913 Too many arguments in function definition (6
150
151
  result.error("description must not end with period")
151
152
  if len(line) > max_subject_length:
152
153
  result.error(f"subject too long: {len(line)} > {max_subject_length}")
154
+ if min_description_length > 0 and len(desc) < min_description_length:
155
+ result.error(f"description too short: {len(desc)} < {min_description_length}")
153
156
  return desc
154
157
 
155
158
 
@@ -250,6 +253,7 @@ class Args:
250
253
  require_scope: bool
251
254
  allowed_types: frozenset
252
255
  max_subject_length: int
256
+ min_description_length: int
253
257
  rev_range: str | None
254
258
  allow_empty: bool
255
259
  include_merges: bool
@@ -279,6 +283,14 @@ def _resolve_max_subject_length(args, config):
279
283
  return MAX_SUBJECT_LEN
280
284
 
281
285
 
286
+ def _resolve_min_description_length(args, config):
287
+ if args.min_description_length is not None:
288
+ return args.min_description_length
289
+ if "min-description-length" in config:
290
+ return config["min-description-length"]
291
+ return 0
292
+
293
+
282
294
  def _resolve_types(args, config):
283
295
  if args.types:
284
296
  return frozenset(t.strip() for t in args.types.split(","))
@@ -350,6 +362,13 @@ def _parse_args():
350
362
  metavar="N",
351
363
  help=f"maximum subject line length (default: {MAX_SUBJECT_LEN})",
352
364
  )
365
+ parser.add_argument(
366
+ "--min-description-length",
367
+ type=int,
368
+ default=None,
369
+ metavar="N",
370
+ help="minimum description length in characters (default: 0, off)",
371
+ )
353
372
  parser.add_argument(
354
373
  "--range",
355
374
  dest="rev_range",
@@ -374,6 +393,7 @@ def _parse_args():
374
393
  allowed_scopes, require_scope = _resolve_scopes(args, config)
375
394
  allowed_types = _resolve_types(args, config)
376
395
  max_subject_length = _resolve_max_subject_length(args, config)
396
+ min_description_length = _resolve_min_description_length(args, config)
377
397
 
378
398
  if args.allow_empty and not args.rev_range:
379
399
  parser.error("--allow-empty requires --range")
@@ -406,6 +426,7 @@ def _parse_args():
406
426
  require_scope=require_scope,
407
427
  allowed_types=allowed_types,
408
428
  max_subject_length=max_subject_length,
429
+ min_description_length=min_description_length,
409
430
  rev_range=args.rev_range,
410
431
  allow_empty=args.allow_empty,
411
432
  include_merges=args.include_merges,
@@ -432,6 +453,7 @@ def _run_checks(args, rev, message, result):
432
453
  args.allowed_scopes,
433
454
  args.allowed_types,
434
455
  args.max_subject_length,
456
+ args.min_description_length,
435
457
  require_scope=args.require_scope,
436
458
  )
437
459
  if Check.IMPERATIVE in args.enabled:
@@ -17,6 +17,7 @@ from git_commit_guard import (
17
17
  _parse_config_checks,
18
18
  _report,
19
19
  _resolve_max_subject_length,
20
+ _resolve_min_description_length,
20
21
  _resolve_types,
21
22
  _strip_comments,
22
23
  check_body,
@@ -161,6 +162,22 @@ class TestCheckSubject:
161
162
  check_subject("fix: ok", r, max_subject_length=10)
162
163
  assert r.ok
163
164
 
165
+ def test_min_description_length_zero_disables_check(self):
166
+ r = Result()
167
+ check_subject("fix: add x", r, min_description_length=0)
168
+ assert r.ok
169
+
170
+ def test_min_description_length_enforced(self):
171
+ r = Result()
172
+ check_subject("fix: add x", r, min_description_length=6)
173
+ assert not r.ok
174
+ assert any("description too short" in m for _, m in r.errors)
175
+
176
+ def test_min_description_length_exact_passes(self):
177
+ r = Result()
178
+ check_subject("fix: hello", r, min_description_length=5)
179
+ assert r.ok
180
+
164
181
  def test_custom_type_passes(self):
165
182
  r = Result()
166
183
  check_subject("wip: add thing", r, allowed_types=frozenset(["wip"]))
@@ -455,6 +472,32 @@ class TestResolveMaxSubjectLength:
455
472
  assert result == 50 # noqa: PLR2004 Magic value used in comparison, consider replacing 50 with a constant variable
456
473
 
457
474
 
475
+ class TestResolveMinDescriptionLength:
476
+ def test_defaults_to_zero(self):
477
+ result = _resolve_min_description_length(
478
+ Namespace(min_description_length=None), {}
479
+ )
480
+ assert result == 0
481
+
482
+ def test_cli_flag_overrides_default(self):
483
+ result = _resolve_min_description_length(
484
+ Namespace(min_description_length=10), {}
485
+ )
486
+ assert result == 10 # noqa: PLR2004 Magic value used in comparison, consider replacing 10 with a constant variable
487
+
488
+ def test_config_overrides_default(self):
489
+ result = _resolve_min_description_length(
490
+ Namespace(min_description_length=None), {"min-description-length": 8}
491
+ )
492
+ assert result == 8 # noqa: PLR2004 Magic value used in comparison, consider replacing 8 with a constant variable
493
+
494
+ def test_cli_overrides_config(self):
495
+ result = _resolve_min_description_length(
496
+ Namespace(min_description_length=10), {"min-description-length": 8}
497
+ )
498
+ assert result == 10 # noqa: PLR2004 Magic value used in comparison, consider replacing 10 with a constant variable
499
+
500
+
458
501
  class TestResolveTypes:
459
502
  def test_defaults_when_no_config_or_flag(self):
460
503
  assert _resolve_types(Namespace(types=None), {}) == TYPES
@@ -812,6 +855,70 @@ class TestMain:
812
855
  ):
813
856
  assert main() == 0
814
857
 
858
+ def test_min_description_length_flag_passes(self, tmp_path):
859
+ f = tmp_path / "msg"
860
+ f.write_text("fix: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")
861
+ argv = [
862
+ "cg",
863
+ "--message-file",
864
+ str(f),
865
+ "--disable",
866
+ "signature",
867
+ "--min-description-length",
868
+ "5",
869
+ ]
870
+ with patch("sys.argv", argv):
871
+ assert main() == 0
872
+
873
+ def test_min_description_length_flag_fails(self, tmp_path):
874
+ f = tmp_path / "msg"
875
+ f.write_text("fix: add x\n\nbody\n\nSigned-off-by: A User <a@b.com>")
876
+ argv = [
877
+ "cg",
878
+ "--message-file",
879
+ str(f),
880
+ "--disable",
881
+ "signature,imperative",
882
+ "--min-description-length",
883
+ "6",
884
+ ]
885
+ with patch("sys.argv", argv):
886
+ assert main() == 1
887
+
888
+ def test_min_description_length_from_config(self, tmp_path):
889
+ f = tmp_path / "msg"
890
+ f.write_text("fix: add x\n\nbody\n\nSigned-off-by: A User <a@b.com>")
891
+ argv = ["cg", "--message-file", str(f), "--disable", "signature,imperative"]
892
+ with (
893
+ patch("sys.argv", argv),
894
+ patch(
895
+ "git_commit_guard._load_config",
896
+ return_value={"min-description-length": 6},
897
+ ),
898
+ ):
899
+ assert main() == 1
900
+
901
+ def test_min_description_length_cli_overrides_config(self, tmp_path):
902
+ f = tmp_path / "msg"
903
+ f.write_text("fix: add x\n\nbody\n\nSigned-off-by: A User <a@b.com>")
904
+ argv = [
905
+ "cg",
906
+ "--message-file",
907
+ str(f),
908
+ "--disable",
909
+ "signature,imperative",
910
+ "--min-description-length",
911
+ "3",
912
+ ]
913
+ with (
914
+ patch("sys.argv", argv),
915
+ patch(
916
+ "git_commit_guard._load_config",
917
+ return_value={"min-description-length": 6},
918
+ ),
919
+ ):
920
+ assert main() == 0
921
+
815
922
  def test_types_cli_overrides_config(self, tmp_path):
816
923
  f = tmp_path / "msg"
817
924
  f.write_text("wip: add thing\n\nbody\n\nSigned-off-by: A User <a@b.com>")