|
44 | 44 | "fix": (0, 0), |
45 | 45 | "check-version": (0, 0), |
46 | 46 | "fragment": (1, 3), |
47 | | - "dep": (3, 3), |
| 47 | + "dep": (1, 3), |
48 | 48 | "badge": (0, 0), |
| 49 | + "init": (0, 0), |
49 | 50 | } |
50 | 51 |
|
51 | 52 | #: commands that accept an optional trailing file argument |
@@ -153,6 +154,17 @@ def main(argv=None) -> int: |
153 | 154 | help="Fragments directory (default: changelog.d/ next to the " |
154 | 155 | "changelog)", |
155 | 156 | ) |
| 157 | + parser.add_argument( |
| 158 | + "--requirements", |
| 159 | + action="store_true", |
| 160 | + help="With 'dep': compare two requirements files instead of one " |
| 161 | + "package (dep --requirements old.txt new.txt)", |
| 162 | + ) |
| 163 | + parser.add_argument( |
| 164 | + "--workflow", |
| 165 | + action="store_true", |
| 166 | + help="With 'init': also scaffold .github/workflows/changelog.yml", |
| 167 | + ) |
156 | 168 | parser.add_argument( |
157 | 169 | "--all", |
158 | 170 | action="store_true", |
@@ -204,6 +216,8 @@ def main(argv=None) -> int: |
204 | 216 | if args.format == "sarif" and args.command != "validate": |
205 | 217 | parser.error("--format sarif is only supported with 'validate'") |
206 | 218 |
|
| 219 | + if args.command == "init": |
| 220 | + return _cmd_init(args) |
207 | 221 | if args.command == "dep": |
208 | 222 | return _cmd_dep(args) |
209 | 223 | if args.command == "fragment": |
@@ -590,7 +604,40 @@ def _cmd_fragment(args) -> int: |
590 | 604 | return EXIT_USAGE |
591 | 605 |
|
592 | 606 |
|
| 607 | +def _cmd_init(args) -> int: |
| 608 | + from ._scaffold import write_changelog, write_workflow |
| 609 | + |
| 610 | + try: |
| 611 | + write_changelog(args.file, repo_url=args.repo_url) |
| 612 | + except FileExistsError as e: |
| 613 | + print(f"Error: {e}", file=sys.stderr) |
| 614 | + return EXIT_FAIL |
| 615 | + created = [args.file] |
| 616 | + if args.workflow: |
| 617 | + try: |
| 618 | + created.append(write_workflow(args.file)) |
| 619 | + except FileExistsError as e: |
| 620 | + print(f"Warning: {e}", file=sys.stderr) |
| 621 | + if not args.quiet: |
| 622 | + for path in created: |
| 623 | + print(f"Created {path}") |
| 624 | + print("Next: add entries under [Unreleased], then " |
| 625 | + f"`patchnotes {args.file} bump X.Y.Z` on release day.") |
| 626 | + return EXIT_OK |
| 627 | + |
| 628 | + |
593 | 629 | def _cmd_dep(args) -> int: |
| 630 | + if args.requirements: |
| 631 | + if len(args.params) != 2: |
| 632 | + print("Usage: patchnotes dep --requirements OLD.txt NEW.txt", |
| 633 | + file=sys.stderr) |
| 634 | + return EXIT_USAGE |
| 635 | + return _cmd_dep_requirements(args) |
| 636 | + if len(args.params) != 3: |
| 637 | + print("Usage: patchnotes dep PACKAGE FROM_VERSION TO_VERSION", |
| 638 | + file=sys.stderr) |
| 639 | + return EXIT_USAGE |
| 640 | + |
594 | 641 | from ._depdiff import fetch_dep_changelog, find_github_repo, render_dep_diff |
595 | 642 |
|
596 | 643 | package, from_v, to_v = args.params |
@@ -622,6 +669,80 @@ def _cmd_dep(args) -> int: |
622 | 669 | )) |
623 | 670 | else: |
624 | 671 | print(text) |
| 672 | + if args.strict and flagged: |
| 673 | + return EXIT_FAIL |
| 674 | + return EXIT_OK |
| 675 | + |
| 676 | + |
| 677 | +def _cmd_dep_requirements(args) -> int: |
| 678 | + from ._depdiff import ( |
| 679 | + diff_requirements, |
| 680 | + fetch_dep_changelog, |
| 681 | + find_github_repo, |
| 682 | + render_dep_diff, |
| 683 | + ) |
| 684 | + |
| 685 | + old_path, new_path = args.params |
| 686 | + try: |
| 687 | + with open(old_path, "r", encoding="utf-8") as fh: |
| 688 | + old_text = fh.read() |
| 689 | + with open(new_path, "r", encoding="utf-8") as fh: |
| 690 | + new_text = fh.read() |
| 691 | + except OSError as e: |
| 692 | + print(f"Error: {e}", file=sys.stderr) |
| 693 | + return EXIT_USAGE |
| 694 | + |
| 695 | + changed, added, removed = diff_requirements(old_text, new_text) |
| 696 | + total_flagged = 0 |
| 697 | + results = [] |
| 698 | + |
| 699 | + for name, old_v, new_v in changed: |
| 700 | + try: |
| 701 | + owner, repo = find_github_repo(name) |
| 702 | + cl = fetch_dep_changelog(owner, repo) |
| 703 | + text, flagged = render_dep_diff( |
| 704 | + cl, name, old_v, new_v, show_all=args.show_all |
| 705 | + ) |
| 706 | + results.append({"package": name, "from": old_v, "to": new_v, |
| 707 | + "flagged": flagged, "text": text, "error": None}) |
| 708 | + total_flagged += flagged |
| 709 | + except (ValueError, OSError) as e: |
| 710 | + results.append({"package": name, "from": old_v, "to": new_v, |
| 711 | + "flagged": 0, "text": None, "error": str(e)}) |
| 712 | + |
| 713 | + if args.format == "json": |
| 714 | + print(json.dumps( |
| 715 | + { |
| 716 | + "changed": [ |
| 717 | + {k: v for k, v in r.items() if k != "text"} |
| 718 | + for r in results |
| 719 | + ], |
| 720 | + "added": added, |
| 721 | + "removed": removed, |
| 722 | + "flagged": total_flagged, |
| 723 | + }, |
| 724 | + indent=2, |
| 725 | + )) |
| 726 | + return EXIT_FAIL if (args.strict and total_flagged) else EXIT_OK |
| 727 | + |
| 728 | + if not changed and not added and not removed: |
| 729 | + print("No pinned dependency changes found.") |
| 730 | + return EXIT_OK |
| 731 | + for r in results: |
| 732 | + print() |
| 733 | + if r["error"]: |
| 734 | + print(f"{r['package']}: {r['from']} -> {r['to']}") |
| 735 | + print(f" (couldn't analyze: {r['error']})") |
| 736 | + else: |
| 737 | + print(r["text"]) |
| 738 | + if added: |
| 739 | + print(f"\nNew dependencies: {', '.join(added)}") |
| 740 | + if removed: |
| 741 | + print(f"Removed dependencies: {', '.join(removed)}") |
| 742 | + print(f"\nTotal: {len(changed)} version change(s), " |
| 743 | + f"{total_flagged} breaking/security change(s) flagged.") |
| 744 | + if args.strict and total_flagged: |
| 745 | + return EXIT_FAIL |
625 | 746 | return EXIT_OK |
626 | 747 |
|
627 | 748 |
|
|
0 commit comments