tgit 0.13.2__tar.gz → 0.16.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.
@@ -1,6 +1,90 @@
1
- ## HEAD
1
+ ## v0.16.0
2
2
 
3
- [v0.13.0...HEAD](https://github.com/Jannchie/tgit/compare/v0.13.0...HEAD)
3
+ [v0.15.1...v0.16.0](https://github.com/Jannchie/tgit/compare/v0.15.1...v0.16.0)
4
+
5
+ ### :sparkles: Features
6
+
7
+ - **version-changelog**: add interactive changelog generation on version bump - By [Jianqi Pan](mailto:jannchie@gmail.com) in [2c6c3b3](https://github.com/Jannchie/tgit/commit/2c6c3b3)
8
+
9
+ ### :adhesive_bandage: Fixes
10
+
11
+ - **changelog**: report no changes when changelog empty - By [Jianqi Pan](mailto:jannchie@gmail.com) in [02d94ff](https://github.com/Jannchie/tgit/commit/02d94ff)
12
+
13
+ ### :art: Refactors
14
+
15
+ - **changelog**: simplify current tag handling in changelog - By [Jianqi Pan](mailto:jannchie@gmail.com) in [6ac7903](https://github.com/Jannchie/tgit/commit/6ac7903)
16
+
17
+ ## v0.15.1
18
+
19
+ [v0.15.0...v0.15.1](https://github.com/Jannchie/tgit/compare/v0.15.0...v0.15.1)
20
+
21
+ ### :wrench: Chores
22
+
23
+ - **deps**: update lock file - By [Jianqi Pan](mailto:jannchie@gmail.com) in [4ab3b1e](https://github.com/Jannchie/tgit/commit/4ab3b1e)
24
+
25
+ ## v0.15.0
26
+
27
+ [v0.14.2...v0.15.0](https://github.com/Jannchie/tgit/compare/v0.14.2...v0.15.0)
28
+
29
+ ### :sparkles: Features
30
+
31
+ - **prompts**: revise commit message instructions for clarity - By [Jianqi Pan](mailto:jannchie@gmail.com) in [b58f9ec](https://github.com/Jannchie/tgit/commit/b58f9ec)
32
+
33
+ ## v0.14.2
34
+
35
+ [v0.14.1...v0.14.2](https://github.com/Jannchie/tgit/compare/v0.14.1...v0.14.2)
36
+
37
+ ### :adhesive_bandage: Fixes
38
+
39
+ - **version**: fix bump logic for v0 breaking changes to minor - By [Jianqi Pan](mailto:jannchie@gmail.com) in [0f14045](https://github.com/Jannchie/tgit/commit/0f14045)
40
+
41
+ ### :art: Refactors
42
+
43
+ - **changelog**: extract changelog segment, write helpers && simplify handle logic - By [Jianqi Pan](mailto:jannchie@gmail.com) in [ebc1e17](https://github.com/Jannchie/tgit/commit/ebc1e17)
44
+
45
+ ### :lipstick: Styles
46
+
47
+ - **changelog**: add extra newline before old changelog content && remove noqa for rich print import - By [Jianqi Pan](mailto:jannchie@gmail.com) in [ca93aaf](https://github.com/Jannchie/tgit/commit/ca93aaf)
48
+
49
+ ### :memo: Documentation
50
+
51
+ - **changelog**: update changelog for v0.14.0 and v0.14.1 - By [Jianqi Pan](mailto:jannchie@gmail.com) in [3d26e26](https://github.com/Jannchie/tgit/commit/3d26e26)
52
+
53
+ ## v0.14.1
54
+
55
+ [v0.14.0...v0.14.1](https://github.com/Jannchie/tgit/compare/v0.14.0...v0.14.1)
56
+
57
+ ### :adhesive_bandage: Fixes
58
+
59
+ - **changelog**: prepend new changelog entries if file exists - By [Jianqi Pan](mailto:jannchie@gmail.com) in [c4b3dff](https://github.com/Jannchie/tgit/commit/c4b3dff)
60
+
61
+ ## v0.14.0
62
+
63
+ [v0.13.2...v0.14.0](https://github.com/Jannchie/tgit/compare/v0.13.2...v0.14.0)
64
+
65
+ ### :sparkles: Features
66
+
67
+ - **changelog**: add incremental changelog generation && support appending to existing file - By [Jianqi Pan](mailto:jannchie@gmail.com) in [2d193a5](https://github.com/Jannchie/tgit/commit/2d193a5)
68
+
69
+ ### :adhesive_bandage: Fixes
70
+
71
+ - **changelog**: remove head from default changelog range && update changelog entries - By [Jianqi Pan](mailto:jannchie@gmail.com) in [f6509e3](https://github.com/Jannchie/tgit/commit/f6509e3)
72
+
73
+ ## v0.13.2
74
+
75
+ [v0.13.1...v0.13.2](https://github.com/Jannchie/tgit/compare/v0.13.1...v0.13.2)
76
+
77
+ ### :adhesive_bandage: Fixes
78
+
79
+ - **cli**: suppress import error for openai in thread - By [Jianqi Pan](mailto:jannchie@gmail.com) in [4973127](https://github.com/Jannchie/tgit/commit/4973127)
80
+
81
+ ## v0.13.1
82
+
83
+ [v0.13.0...v0.13.1](https://github.com/Jannchie/tgit/compare/v0.13.0...v0.13.1)
84
+
85
+ ### :adhesive_bandage: Fixes
86
+
87
+ - **commit**: update no-files message for ai command - By [Jianqi Pan](mailto:jannchie@gmail.com) in [cda3740](https://github.com/Jannchie/tgit/commit/cda3740)
4
88
 
5
89
  ### :wrench: Chores
6
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tgit
3
- Version: 0.13.2
3
+ Version: 0.16.0
4
4
  Summary: Tool for Git Interaction Temptation (tgit): An elegant CLI tool that simplifies and streamlines your Git workflow, making version control a breeze.
5
5
  Author-email: Jannchie <jannchie@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tgit"
3
- version = "0.13.2"
3
+ version = "0.16.0"
4
4
  description = "Tool for Git Interaction Temptation (tgit): An elegant CLI tool that simplifies and streamlines your Git workflow, making version control a breeze."
5
5
  authors = [{ name = "Jannchie", email = "jannchie@gmail.com" }]
6
6
  dependencies = [
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import logging
2
3
  import re
3
4
  import warnings
@@ -231,27 +232,88 @@ def generate_changelog(commits_by_type: dict[str, list[TGITCommit]], from_ref: s
231
232
  return out_str
232
233
 
233
234
 
234
- def handle_changelog(args: ChangelogArgs) -> None:
235
- repo = git.Repo(args.path)
235
+ def extract_latest_tag_from_changelog(filepath: str) -> str | None:
236
+ filepath = Path(filepath)
237
+ with contextlib.suppress(FileNotFoundError), filepath.open(encoding="utf-8") as f:
238
+ for line in f:
239
+ if line.startswith("## "):
240
+ return line.strip().removeprefix("## ").strip()
241
+ return None
236
242
 
243
+
244
+ def prepare_changelog_segments(
245
+ repo: git.Repo,
246
+ latest_tag_in_file: str | None = None,
247
+ current_tag: str | None = None,
248
+ ) -> list[tuple[str, str, str, str]]:
249
+ tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)
250
+ if not tags:
251
+ print("[yellow]No tags found in the repository.[/yellow]")
252
+ return []
253
+ first_commit = get_first_commit_hash(repo)
254
+
255
+ points = [first_commit] + [tag.commit.hexsha for tag in tags]
256
+ point_names = [first_commit] + [tag.name for tag in tags]
257
+ if current_tag is not None:
258
+ point_names += ["HEAD"]
259
+ points += ["HEAD"]
260
+ start_idx = 1
261
+ if latest_tag_in_file and latest_tag_in_file in point_names:
262
+ idx = point_names.index(latest_tag_in_file)
263
+ start_idx = idx + 1
264
+ segments: list[tuple[str, str, str, str]] = [
265
+ (points[i - 1], points[i], point_names[i - 1], point_names[i]) for i in reversed(range(start_idx, len(points)))
266
+ ]
267
+ if current_tag is not None:
268
+ segments[0] = (segments[0][0], "HEAD", segments[0][2], current_tag)
269
+ return segments
270
+
271
+
272
+ def write_changelog_prepend(filepath: str, new_content: str) -> None:
273
+ path = Path(filepath)
274
+ if path.exists():
275
+ with path.open("r", encoding="utf-8") as f:
276
+ old_content = f.read()
277
+ with path.open("w", encoding="utf-8") as f:
278
+ f.write(new_content.strip("\n") + "\n\n" + old_content)
279
+ else:
280
+ with path.open("w", encoding="utf-8") as f:
281
+ f.write(new_content.strip("\n") + "\n")
282
+
283
+
284
+ def print_and_write_changelog(
285
+ changelog: str,
286
+ output_path: str | None = None,
287
+ *,
288
+ prepend: bool = False,
289
+ ) -> None:
290
+ if not changelog or not changelog.strip():
291
+ print("[yellow]No changes found, nothing to output.[/yellow]")
292
+ return
293
+ print()
294
+ print(changelog.strip("\n"))
295
+ if output_path:
296
+ if prepend:
297
+ write_changelog_prepend(output_path, changelog)
298
+ else:
299
+ with Path(output_path).open("w", encoding="utf-8") as output_file:
300
+ output_file.write(changelog.strip("\n") + "\n")
301
+
302
+
303
+ def handle_changelog(args: ChangelogArgs, current_tag: str | None = None) -> None:
304
+ repo = git.Repo(args.path)
237
305
  from_raw = args.from_raw
238
306
  to_raw = args.to_raw
239
-
240
- # 如果未指定 from/to,聚合所有 tag 版本的 changelog,优化性能
307
+ latest_tag_in_file = None
308
+ if args.output and Path(args.output).exists() and from_raw is None and to_raw is None:
309
+ latest_tag_in_file = extract_latest_tag_from_changelog(args.output)
310
+ # 默认:统计所有 tag 之间的差分(不包含最新 tag 到 HEAD)
241
311
  if from_raw is None and to_raw is None:
242
- tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime)
243
- first_commit = get_first_commit_hash(repo)
244
- points = [first_commit] + [tag.commit.hexsha for tag in tags] + [repo.head.commit.hexsha]
245
- point_names = [first_commit] + [tag.name for tag in tags] + ["HEAD"]
312
+ segments = prepare_changelog_segments(repo, latest_tag_in_file, current_tag)
246
313
  changelogs = ""
247
314
  with Progress() as progress:
248
- task = progress.add_task("Generating changelog...", total=len(points) - 1)
249
- # 反向遍历区间,实现反向输出
250
- for i in reversed(range(len(points) - 1)):
251
- from_hash = points[i]
252
- to_hash = points[i + 1]
253
- from_name = point_names[i]
254
- to_name = point_names[i + 1]
315
+ task = progress.add_task("Generating changelog...", total=len(segments))
316
+ for from_hash, to_hash, from_name, to_name in segments:
255
317
  raw_commits = list(repo.iter_commits(f"{from_hash}...{to_hash}"))
256
318
  tgit_commits = []
257
319
  for commit in raw_commits:
@@ -267,23 +329,9 @@ def handle_changelog(args: ChangelogArgs) -> None:
267
329
  changelog = generate_changelog(commits_by_type, from_name, to_name, remote_uri)
268
330
  changelogs += changelog
269
331
  progress.update(task, advance=1)
270
- if args.output:
271
- with Path(args.output).open("w") as output_file:
272
- output_file.write(changelogs.strip("\n") + "\n")
273
- print()
274
- print(changelogs.strip("\n"))
332
+ print_and_write_changelog(changelogs, args.output, prepend=bool(latest_tag_in_file))
275
333
  return
276
334
 
277
- # 否则输出指定范围的 changelog
278
- from_ref, to_ref = get_git_commits_range(repo, from_raw, to_raw)
279
- changelog = get_changelog_by_range(repo, from_ref, to_ref)
280
- if args.output:
281
- with Path(args.output).open("w") as output_file:
282
- output_file.write(changelog.strip("\n") + "\n")
283
- else:
284
- print()
285
- print(changelog)
286
-
287
335
 
288
336
  def get_changelog_by_range(repo: git.Repo, from_ref: str, to_ref: str) -> str:
289
337
  try:
@@ -1,6 +1,6 @@
1
1
  import argparse
2
2
 
3
- from rich import print # noqa: A004
3
+ from rich import print
4
4
 
5
5
  from .settings import set_global_settings
6
6
 
@@ -0,0 +1,62 @@
1
+ # Git Commit Message Generator
2
+
3
+ You are a git commit message generator. Analyze the provided diff and generate an appropriate commit message following the Conventional Commits specification.
4
+
5
+ ## Current Context
6
+ - **Branch**: {{ branch }}
7
+ - **Commit Type**: {% if specified_type is defined %}"{{ specified_type }}" (user-specified - MUST be used){% else %}Choose from: {{ types | join(', ') }}{% endif %}
8
+
9
+ ## Commit Message Requirements
10
+
11
+ ### Type
12
+ {% if specified_type is defined %}
13
+ **MANDATORY**: Use "{{ specified_type }}" as specified by the user.
14
+ {% else %}
15
+ Select the most appropriate type from the available options based on the nature of the changes.
16
+ {% endif %}
17
+
18
+ ### Scope
19
+ - Use a single word when possible
20
+ - If multiple words needed, separate with hyphens (e.g., `user-auth`)
21
+ - Keep it concise and descriptive
22
+ - Optional if changes are global or unclear in scope
23
+
24
+ ### Message
25
+ - Write in lowercase
26
+ - Use present tense (e.g., "add feature" not "added feature")
27
+ - Be concise but descriptive (aim for 3-7 words)
28
+ - Cover the primary change(s) in the diff
29
+ - If multiple distinct changes, separate with " && " (e.g., "update api && fix validation")
30
+
31
+ ### Breaking Changes
32
+ Mark `is_breaking: true` only if changes:
33
+ - Break existing API contracts
34
+ - Require user action for compatibility
35
+ - Remove or significantly change existing functionality
36
+
37
+ ## Analysis Process
38
+ 1. Review the diff comprehensively
39
+ 2. Identify the primary type of change
40
+ 3. Determine appropriate scope from modified files/areas
41
+ 4. Craft a concise message covering main changes
42
+ 5. Assess backward compatibility impact
43
+
44
+ ## Output Format
45
+ Return valid JSON matching this structure:
46
+ ```json
47
+ {
48
+ "type": "string",
49
+ "scope": "string|null",
50
+ "msg": "string",
51
+ "is_breaking": "boolean"
52
+ }
53
+ ```
54
+
55
+ ## Examples
56
+ ```json
57
+ {"type": "feat", "scope": "auth", "msg": "add oauth2 login", "is_breaking": false}
58
+ {"type": "fix", "scope": "api", "msg": "handle null user responses", "is_breaking": false}
59
+ {"type": "refactor", "scope": null, "msg": "restructure project layout", "is_breaking": true}
60
+ ```
61
+
62
+ Now analyze the provided diff and generate the commit message.
@@ -273,9 +273,12 @@ def get_version_from_git(path: Path) -> Version | None:
273
273
  return None
274
274
 
275
275
 
276
- def get_default_bump_by_commits_dict(commits_by_type: dict[str, list[git.Commit]]) -> str:
277
- # sourcery skip: assign-if-exp, reintroduce-else
278
- if commits_by_type.get("breaking"):
276
+ def get_default_bump_by_commits_dict(commits_by_type: dict[str, list[git.Commit]], prev_version: Version | None = None) -> str:
277
+ # v0.x.x breaking change 只 bump minor,v1+ 才 bump major
278
+ if prev_version and prev_version.major == 0:
279
+ if commits_by_type.get("breaking"):
280
+ return "minor"
281
+ elif commits_by_type.get("breaking"):
279
282
  return "major"
280
283
  if commits_by_type.get("feat"):
281
284
  return "minor"
@@ -289,6 +292,32 @@ def handle_version(args: VersionArgs) -> None:
289
292
  reclusive = args.recursive
290
293
 
291
294
  if next_version := get_next_version(args, prev_version, verbose):
295
+ # 获取目标 tag 名
296
+ target_tag = f"v{next_version}"
297
+ # 询问是否生成 changelog
298
+ ans = inquirer.prompt(
299
+ [
300
+ inquirer.Confirm(
301
+ "gen_changelog",
302
+ message=f"should generate changelog for {target_tag}?",
303
+ default=True,
304
+ ),
305
+ ],
306
+ )
307
+ if ans and ans.get("gen_changelog"):
308
+ # 构造 changelog 参数对象
309
+ from argparse import Namespace
310
+
311
+ changelog_args = Namespace(
312
+ path=path,
313
+ from_raw=None,
314
+ to_raw=None,
315
+ output="CHANGELOG.md",
316
+ verbose=verbose,
317
+ )
318
+ from tgit.changelog import handle_changelog
319
+
320
+ handle_changelog(changelog_args, current_tag=target_tag)
292
321
  update_version_files(args, next_version, verbose, reclusive=reclusive)
293
322
  execute_git_commands(args, next_version, verbose)
294
323
 
@@ -311,7 +340,7 @@ def get_next_version(args: VersionArgs, prev_version: Version, verbose: int) ->
311
340
  from_ref, to_ref = get_git_commits_range(repo, None, None)
312
341
  tgit_commits = get_commits(repo, from_ref, to_ref)
313
342
  commits_by_type = group_commits_by_type(tgit_commits)
314
- default_bump = get_default_bump_by_commits_dict(commits_by_type)
343
+ default_bump = get_default_bump_by_commits_dict(commits_by_type, prev_version)
315
344
 
316
345
  choices = [
317
346
  VersionChoice(prev_version, bump) for bump in ["patch", "minor", "major", "prepatch", "preminor", "premajor", "previous", "custom"]