diffsage 1.2.1__tar.gz → 1.3.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.
Files changed (86) hide show
  1. {diffsage-1.2.1/src/diffsage.egg-info → diffsage-1.3.0}/PKG-INFO +105 -10
  2. {diffsage-1.2.1 → diffsage-1.3.0}/README.md +104 -9
  3. {diffsage-1.2.1 → diffsage-1.3.0}/pyproject.toml +1 -1
  4. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/cli.py +7 -1
  5. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/commit.py +23 -6
  6. diffsage-1.3.0/src/diffsage/commands/pr.py +220 -0
  7. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/exceptions/__init__.py +31 -10
  8. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/exceptions/base.py +12 -0
  9. diffsage-1.3.0/src/diffsage/exceptions/git.py +21 -0
  10. diffsage-1.3.0/src/diffsage/exceptions/github.py +13 -0
  11. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/git/client.py +102 -0
  12. diffsage-1.3.0/src/diffsage/github/client.py +65 -0
  13. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/commit_message.py +2 -0
  14. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/git.py +13 -1
  15. diffsage-1.3.0/src/diffsage/models/pull_request.py +27 -0
  16. diffsage-1.3.0/src/diffsage/models/pull_request_analysis.py +29 -0
  17. diffsage-1.3.0/src/diffsage/parsers/pull_request_parser.py +68 -0
  18. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/ai_service.py +11 -1
  19. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/commit_service.py +10 -2
  20. diffsage-1.3.0/src/diffsage/services/git_service.py +116 -0
  21. diffsage-1.3.0/src/diffsage/services/github_service.py +75 -0
  22. diffsage-1.3.0/src/diffsage/services/prompt_service.py +182 -0
  23. diffsage-1.3.0/src/diffsage/services/pull_request_analysis_service.py +281 -0
  24. diffsage-1.3.0/src/diffsage/services/pull_request_service.py +42 -0
  25. diffsage-1.3.0/src/diffsage/ui/__init__.py +0 -0
  26. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/commit_view.py +2 -2
  27. diffsage-1.3.0/src/diffsage/ui/pull_request_view.py +122 -0
  28. {diffsage-1.2.1 → diffsage-1.3.0/src/diffsage.egg-info}/PKG-INFO +105 -10
  29. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage.egg-info/SOURCES.txt +12 -1
  30. diffsage-1.2.1/src/diffsage/exceptions/git.py +0 -9
  31. diffsage-1.2.1/src/diffsage/services/git_service.py +0 -20
  32. diffsage-1.2.1/src/diffsage/services/prompt_service.py +0 -60
  33. {diffsage-1.2.1 → diffsage-1.3.0}/LICENSE +0 -0
  34. {diffsage-1.2.1 → diffsage-1.3.0}/setup.cfg +0 -0
  35. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/__init__.py +0 -0
  36. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/__main__.py +0 -0
  37. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/__init__.py +0 -0
  38. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/ask.py +0 -0
  39. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/auth.py +0 -0
  40. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/config.py +0 -0
  41. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/commands/doctor.py +0 -0
  42. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/__init__.py +0 -0
  43. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/defaults.py +0 -0
  44. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/file_loader.py +0 -0
  45. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/loader.py +0 -0
  46. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/paths.py +0 -0
  47. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/resolver.py +0 -0
  48. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/schema.py +0 -0
  49. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/scope.py +0 -0
  50. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/config/settings.py +0 -0
  51. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/exceptions/credentials.py +0 -0
  52. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/exceptions/provider.py +0 -0
  53. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/git/__init__.py +0 -0
  54. {diffsage-1.2.1/src/diffsage/logging → diffsage-1.3.0/src/diffsage/github}/__init__.py +0 -0
  55. {diffsage-1.2.1/src/diffsage/models → diffsage-1.3.0/src/diffsage/logging}/__init__.py +0 -0
  56. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/logging/formatter.py +0 -0
  57. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/logging/logger.py +0 -0
  58. {diffsage-1.2.1/src/diffsage/prompts → diffsage-1.3.0/src/diffsage/models}/__init__.py +0 -0
  59. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/config.py +0 -0
  60. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/credentials.py +0 -0
  61. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/doctor.py +0 -0
  62. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/models/provider.py +0 -0
  63. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/parsers/commit_message_parser.py +0 -0
  64. {diffsage-1.2.1/src/diffsage/providers → diffsage-1.3.0/src/diffsage/prompts}/__init__.py +0 -0
  65. {diffsage-1.2.1/src/diffsage/services → diffsage-1.3.0/src/diffsage/providers}/__init__.py +0 -0
  66. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/providers/base.py +0 -0
  67. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/providers/factory.py +0 -0
  68. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/providers/gemini_provider.py +0 -0
  69. {diffsage-1.2.1/src/diffsage/storage → diffsage-1.3.0/src/diffsage/services}/__init__.py +0 -0
  70. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/config_service.py +0 -0
  71. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/credentials_service.py +0 -0
  72. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/doctor_service.py +0 -0
  73. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/services/editor_service.py +0 -0
  74. {diffsage-1.2.1/src/diffsage/ui → diffsage-1.3.0/src/diffsage/storage}/__init__.py +0 -0
  75. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/storage/base.py +0 -0
  76. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/storage/config_repository.py +0 -0
  77. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/storage/credentials_repository.py +0 -0
  78. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/ask_view.py +0 -0
  79. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/auth_view.py +0 -0
  80. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/base.py +0 -0
  81. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/config_view.py +0 -0
  82. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage/ui/doctor_view.py +0 -0
  83. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage.egg-info/dependency_links.txt +0 -0
  84. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage.egg-info/entry_points.txt +0 -0
  85. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage.egg-info/requires.txt +0 -0
  86. {diffsage-1.2.1 → diffsage-1.3.0}/src/diffsage.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diffsage
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: AI-powered Git workflow assistant
5
5
  Author: Gunitha Rathnayake
6
6
  License: MIT
@@ -116,6 +116,10 @@ Current capabilities include:
116
116
  - AI provider authentication and error handling
117
117
  - Layered architecture
118
118
  - Comprehensive automated test suite
119
+ - Add AI-assisted PR generation
120
+ - Interactive PR draft review/edit/regeneration
121
+ - GitHub PR creation
122
+ - Branch synchronization validation
119
123
 
120
124
  ---
121
125
 
@@ -313,6 +317,35 @@ Display AI suggestion
313
317
  [N] Cancel
314
318
  ```
315
319
 
320
+ ```bash
321
+ diffsage pr
322
+ ```
323
+
324
+ PR workflow
325
+
326
+ ```text
327
+ Generate PR draft
328
+ │
329
+ ▼
330
+ Show base/head branches
331
+ │
332
+ ▼
333
+ Display PR draft
334
+ │
335
+ ┌─────┼───────────┐
336
+ ▼ ▼ ▼
337
+ Edit Regenerate Cancel
338
+ │
339
+ ▼
340
+ Accept
341
+ │
342
+ ▼
343
+ Create GitHub PR
344
+ │
345
+ ▼
346
+ Display PR URL
347
+ ```
348
+
316
349
  Named credential profiles are also supported:
317
350
 
318
351
  ```bash
@@ -340,6 +373,8 @@ diffsage auth unset gemini
340
373
  |----------|-------------|
341
374
  | `diffsage doctor` | Validate the local DiffSage environment |
342
375
  | `diffsage commit` | Generate and manage AI-assisted commit messages |
376
+ | `diffsage pr` | Generate, review, and create a GitHub pull request |
377
+ | `diffsage pr <base-branch>` | Generate and create a pull request against a specific base branch |
343
378
  | `diffsage config list` | Display the current DiffSage configuration |
344
379
  | `diffsage config get` | Display the value of a configuration setting |
345
380
  | `diffsage config set` | Update a configuration setting |
@@ -350,7 +385,46 @@ diffsage auth unset gemini
350
385
  | `diffsage auth unset` | Remove a provider credential |
351
386
  | `diffsage ask` | Ask the configured AI provider a question |
352
387
 
353
- More commands will be introduced as the project evolves.
388
+ ---
389
+
390
+ ## Pull Request Workflow
391
+
392
+ The `diffsage pr` command analyzes the current Git branch against a resolved base branch and generates a structured pull request draft.
393
+
394
+ The workflow is interactive:
395
+
396
+ ```text
397
+ Analyze Git repository
398
+ │
399
+ ▼
400
+ Resolve base branch
401
+ │
402
+ ▼
403
+ Generate PR draft
404
+ │
405
+ ▼
406
+ Display base/head branches
407
+ │
408
+ ▼
409
+ Review PR draft
410
+ │
411
+ ┌──────┼──────────────┐
412
+ ▼ ▼ ▼
413
+ Edit Regenerate Cancel
414
+ │ │
415
+ └──────┴───────┐
416
+ ▼
417
+ Accept
418
+ │
419
+ ▼
420
+ Validate GitHub access
421
+ │
422
+ ▼
423
+ Create GitHub pull request
424
+ │
425
+ ▼
426
+ Display pull request URL
427
+ ```
354
428
 
355
429
  ---
356
430
 
@@ -368,15 +442,22 @@ Commands
368
442
  │
369
443
  ├── Authentication
370
444
  │
371
- ▼
372
- Services
445
+ ├── Commit Workflow
373
446
  │
374
- ├── Git
375
- ├── AI
376
- ├── Prompt Generation
377
- └── Credentials
378
- │
379
- ▼
447
+ └── Pull Request Workflow
448
+ │
449
+ ▼
450
+ Services
451
+ │
452
+ ├── Git
453
+ ├── GitHub
454
+ ├── AI
455
+ ├── Prompt Generation
456
+ ├── Pull Request Analysis
457
+ ├── Pull Request Parsing
458
+ └── Credentials
459
+ │
460
+ ▼
380
461
  Providers / Storage
381
462
  ```
382
463
 
@@ -402,6 +483,7 @@ src/
402
483
  ├── config/
403
484
  ├── exceptions/
404
485
  ├── git/
486
+ ├── github/
405
487
  ├── logging/
406
488
  ├── models/
407
489
  ├── parsers/
@@ -412,6 +494,18 @@ src/
412
494
  └── ui/
413
495
 
414
496
  tests/
497
+ ├── commands/
498
+ ├── config/
499
+ ├── git/
500
+ ├── github/
501
+ ├── integration/
502
+ ├── models/
503
+ ├── parsers/
504
+ ├── prompts/
505
+ ├── providers/
506
+ ├── services/
507
+ ├── storage/
508
+ └── ui/
415
509
 
416
510
  docs/
417
511
  ```
@@ -460,6 +554,7 @@ Instead of requiring every automation tool or AI agent to implement Git workflow
460
554
 
461
555
  - Repository validation
462
556
  - Git operations
557
+ - GitHub integration
463
558
  - AI integration
464
559
  - Workflow execution
465
560
  - Error handling
@@ -77,6 +77,10 @@ Current capabilities include:
77
77
  - AI provider authentication and error handling
78
78
  - Layered architecture
79
79
  - Comprehensive automated test suite
80
+ - Add AI-assisted PR generation
81
+ - Interactive PR draft review/edit/regeneration
82
+ - GitHub PR creation
83
+ - Branch synchronization validation
80
84
 
81
85
  ---
82
86
 
@@ -274,6 +278,35 @@ Display AI suggestion
274
278
  [N] Cancel
275
279
  ```
276
280
 
281
+ ```bash
282
+ diffsage pr
283
+ ```
284
+
285
+ PR workflow
286
+
287
+ ```text
288
+ Generate PR draft
289
+ │
290
+ ▼
291
+ Show base/head branches
292
+ │
293
+ ▼
294
+ Display PR draft
295
+ │
296
+ ┌─────┼───────────┐
297
+ ▼ ▼ ▼
298
+ Edit Regenerate Cancel
299
+ │
300
+ ▼
301
+ Accept
302
+ │
303
+ ▼
304
+ Create GitHub PR
305
+ │
306
+ ▼
307
+ Display PR URL
308
+ ```
309
+
277
310
  Named credential profiles are also supported:
278
311
 
279
312
  ```bash
@@ -301,6 +334,8 @@ diffsage auth unset gemini
301
334
  |----------|-------------|
302
335
  | `diffsage doctor` | Validate the local DiffSage environment |
303
336
  | `diffsage commit` | Generate and manage AI-assisted commit messages |
337
+ | `diffsage pr` | Generate, review, and create a GitHub pull request |
338
+ | `diffsage pr <base-branch>` | Generate and create a pull request against a specific base branch |
304
339
  | `diffsage config list` | Display the current DiffSage configuration |
305
340
  | `diffsage config get` | Display the value of a configuration setting |
306
341
  | `diffsage config set` | Update a configuration setting |
@@ -311,7 +346,46 @@ diffsage auth unset gemini
311
346
  | `diffsage auth unset` | Remove a provider credential |
312
347
  | `diffsage ask` | Ask the configured AI provider a question |
313
348
 
314
- More commands will be introduced as the project evolves.
349
+ ---
350
+
351
+ ## Pull Request Workflow
352
+
353
+ The `diffsage pr` command analyzes the current Git branch against a resolved base branch and generates a structured pull request draft.
354
+
355
+ The workflow is interactive:
356
+
357
+ ```text
358
+ Analyze Git repository
359
+ │
360
+ ▼
361
+ Resolve base branch
362
+ │
363
+ ▼
364
+ Generate PR draft
365
+ │
366
+ ▼
367
+ Display base/head branches
368
+ │
369
+ ▼
370
+ Review PR draft
371
+ │
372
+ ┌──────┼──────────────┐
373
+ ▼ ▼ ▼
374
+ Edit Regenerate Cancel
375
+ │ │
376
+ └──────┴───────┐
377
+ ▼
378
+ Accept
379
+ │
380
+ ▼
381
+ Validate GitHub access
382
+ │
383
+ ▼
384
+ Create GitHub pull request
385
+ │
386
+ ▼
387
+ Display pull request URL
388
+ ```
315
389
 
316
390
  ---
317
391
 
@@ -329,15 +403,22 @@ Commands
329
403
  │
330
404
  ├── Authentication
331
405
  │
332
- ▼
333
- Services
406
+ ├── Commit Workflow
334
407
  │
335
- ├── Git
336
- ├── AI
337
- ├── Prompt Generation
338
- └── Credentials
339
- │
340
- ▼
408
+ └── Pull Request Workflow
409
+ │
410
+ ▼
411
+ Services
412
+ │
413
+ ├── Git
414
+ ├── GitHub
415
+ ├── AI
416
+ ├── Prompt Generation
417
+ ├── Pull Request Analysis
418
+ ├── Pull Request Parsing
419
+ └── Credentials
420
+ │
421
+ ▼
341
422
  Providers / Storage
342
423
  ```
343
424
 
@@ -363,6 +444,7 @@ src/
363
444
  ├── config/
364
445
  ├── exceptions/
365
446
  ├── git/
447
+ ├── github/
366
448
  ├── logging/
367
449
  ├── models/
368
450
  ├── parsers/
@@ -373,6 +455,18 @@ src/
373
455
  └── ui/
374
456
 
375
457
  tests/
458
+ ├── commands/
459
+ ├── config/
460
+ ├── git/
461
+ ├── github/
462
+ ├── integration/
463
+ ├── models/
464
+ ├── parsers/
465
+ ├── prompts/
466
+ ├── providers/
467
+ ├── services/
468
+ ├── storage/
469
+ └── ui/
376
470
 
377
471
  docs/
378
472
  ```
@@ -421,6 +515,7 @@ Instead of requiring every automation tool or AI agent to implement Git workflow
421
515
 
422
516
  - Repository validation
423
517
  - Git operations
518
+ - GitHub integration
424
519
  - AI integration
425
520
  - Workflow execution
426
521
  - Error handling
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "diffsage"
7
- version = "1.2.1"
7
+ version = "1.3.0"
8
8
  description = "AI-powered Git workflow assistant"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -7,6 +7,7 @@ from diffsage.commands.auth import app as auth_app
7
7
  from diffsage.commands.commit import commit
8
8
  from diffsage.commands.config import app as config_app
9
9
  from diffsage.commands.doctor import doctor
10
+ from diffsage.commands.pr import pr
10
11
  from diffsage.config.loader import load_settings
11
12
  from diffsage.logging.logger import configure_logging
12
13
 
@@ -31,8 +32,13 @@ def main() -> None:
31
32
  app.command()(doctor)
32
33
  app.command()(ask)
33
34
  app.command()(commit)
35
+ app.command()(pr)
36
+
34
37
  app.add_typer(
35
38
  config_app,
36
39
  name="config",
37
40
  )
38
- app.add_typer(auth_app, name="auth")
41
+ app.add_typer(
42
+ auth_app,
43
+ name="auth",
44
+ )
@@ -1,3 +1,5 @@
1
+ from collections.abc import Callable
2
+
1
3
  from diffsage.config.loader import load_settings
2
4
  from diffsage.config.paths import get_credentials_path
3
5
  from diffsage.exceptions import (
@@ -22,8 +24,13 @@ from diffsage.ui.commit_view import CommitView
22
24
  logger = get_logger(__name__)
23
25
 
24
26
 
25
- def generate_message(commit_service: CommitService) -> str:
26
- return commit_service.generate_commit_message()
27
+ def generate_message(
28
+ commit_service: CommitService,
29
+ on_attempt: Callable[[int, int], None] | None = None,
30
+ ) -> str:
31
+ return commit_service.generate_commit_message(
32
+ on_attempt=on_attempt,
33
+ )
27
34
 
28
35
 
29
36
  def commit() -> None:
@@ -54,8 +61,13 @@ def commit() -> None:
54
61
  ai_service,
55
62
  )
56
63
 
57
- with view.generating():
58
- raw_message = generate_message(commit_service)
64
+ with view.generating() as status:
65
+ raw_message = generate_message(
66
+ commit_service,
67
+ on_attempt=lambda attempt, total: status.update(
68
+ f"Generating commit message... Attempt {attempt}/{total}"
69
+ ),
70
+ )
59
71
  commit_message = CommitMessageParser.parse(raw_message)
60
72
 
61
73
  view.show_generated()
@@ -79,8 +91,13 @@ def commit() -> None:
79
91
 
80
92
  if choice == "r":
81
93
  logger.info("User selected regenerate.")
82
- with view.generating():
83
- raw_message = generate_message(commit_service)
94
+ with view.generating() as status:
95
+ raw_message = generate_message(
96
+ commit_service,
97
+ on_attempt=lambda attempt, total: status.update(
98
+ f"Generating commit message... Attempt {attempt}/{total}"
99
+ ),
100
+ )
84
101
  commit_message = CommitMessageParser.parse(raw_message)
85
102
 
86
103
  view.show_generated()
@@ -0,0 +1,220 @@
1
+ import json
2
+
3
+ import typer
4
+
5
+ from diffsage.config.loader import load_settings
6
+ from diffsage.config.paths import get_credentials_path
7
+ from diffsage.exceptions import (
8
+ BaseBranchNotFoundError,
9
+ ConfigError,
10
+ CredentialNotFoundError,
11
+ DetachedHeadError,
12
+ GitHubAuthenticationError,
13
+ GitHubCLIUnavailableError,
14
+ InvalidPullRequestDraftError,
15
+ NotGitRepositoryError,
16
+ ProviderError,
17
+ RemoteBranchNotFoundError,
18
+ SameBranchError,
19
+ UnpushedChangesError,
20
+ )
21
+ from diffsage.git.client import GitClient
22
+ from diffsage.github.client import GitHubClient
23
+ from diffsage.logging.logger import get_logger
24
+ from diffsage.parsers.pull_request_parser import PullRequestParser
25
+ from diffsage.services.ai_service import AIService
26
+ from diffsage.services.credentials_service import CredentialService
27
+ from diffsage.services.editor_service import EditorService
28
+ from diffsage.services.git_service import GitService
29
+ from diffsage.services.github_service import GitHubService
30
+ from diffsage.services.prompt_service import PromptService
31
+ from diffsage.services.pull_request_analysis_service import (
32
+ PullRequestAnalysisService,
33
+ )
34
+ from diffsage.services.pull_request_service import PullRequestService
35
+ from diffsage.storage.credentials_repository import CredentialsRepository
36
+ from diffsage.ui.pull_request_view import PullRequestView
37
+
38
+ logger = get_logger(__name__)
39
+
40
+
41
+ def pr(
42
+ base_branch: str | None = typer.Argument(
43
+ None,
44
+ help="Base branch to compare against. If omitted, DiffSage resolves it automatically.",
45
+ ),
46
+ ) -> None:
47
+ """Generate a pull request draft"""
48
+
49
+ view = PullRequestView()
50
+
51
+ try:
52
+ git_client = GitClient()
53
+ github_client = GitHubClient()
54
+ settings = load_settings()
55
+
56
+ credential_path = get_credentials_path()
57
+ credential_repository = CredentialsRepository(credential_path)
58
+ credential_service = CredentialService(credential_repository)
59
+
60
+ git_service = GitService(git_client)
61
+ github_service = GitHubService(github_client)
62
+ analysis_service = PullRequestAnalysisService()
63
+ prompt_service = PromptService()
64
+ editor_service = EditorService()
65
+
66
+ ai_service = AIService(
67
+ settings,
68
+ credential_service,
69
+ )
70
+
71
+ parser = PullRequestParser()
72
+
73
+ pull_request_service = PullRequestService(
74
+ git_service=git_service,
75
+ analysis_service=analysis_service,
76
+ prompt_service=prompt_service,
77
+ ai_service=ai_service,
78
+ parser=parser,
79
+ )
80
+
81
+ resolved_base_branch = git_service.resolve_base_branch(base_branch)
82
+ head_branch = git_service.current_branch()
83
+
84
+ git_service.validate_remote_head(head_branch)
85
+ github_service.validate()
86
+
87
+ view.show_branches(
88
+ base_branch=resolved_base_branch,
89
+ head_branch=head_branch,
90
+ )
91
+
92
+ with view.generating() as status:
93
+ draft = pull_request_service.generate_draft(
94
+ resolved_base_branch,
95
+ on_attempt=lambda attempt, total: status.update(
96
+ f"Generating pull request draft... Attempt {attempt}/{total}"
97
+ ),
98
+ )
99
+
100
+ view.show_generated(draft)
101
+
102
+ while True:
103
+ choice = view.prompt_action()
104
+
105
+ if choice in ("", "y"):
106
+ logger.info("User accepted pull request draft.")
107
+
108
+ with view.creating():
109
+ pull_request_url = github_service.create_pull_request(
110
+ draft=draft,
111
+ base_branch=resolved_base_branch,
112
+ head_branch=head_branch,
113
+ )
114
+
115
+ view.show_created(pull_request_url)
116
+ break
117
+
118
+ if choice == "e":
119
+ logger.info("User selected edit.")
120
+
121
+ edited_content = editor_service.edit(
122
+ json.dumps(
123
+ draft.to_dict(),
124
+ indent=2,
125
+ )
126
+ )
127
+ try:
128
+ draft = parser.parse(edited_content)
129
+ except InvalidPullRequestDraftError as e:
130
+ view.show_error(str(e))
131
+ continue
132
+
133
+ view.show_generated(draft)
134
+ continue
135
+
136
+ if choice == "r":
137
+ logger.info("User selected regenerate.")
138
+
139
+ with view.generating():
140
+ draft = pull_request_service.generate_draft(
141
+ resolved_base_branch,
142
+ on_attempt=lambda attempt, total: status.update(
143
+ f"Generating pull request draft... Attempt {attempt}/{total}"
144
+ ),
145
+ )
146
+
147
+ view.show_generated(draft)
148
+ continue
149
+
150
+ if choice == "n":
151
+ logger.info("User cancelled pull request generation.")
152
+ view.show_cancelled()
153
+ break
154
+
155
+ view.show_invalid_option()
156
+
157
+ except NotGitRepositoryError:
158
+ logger.info("Command aborted: not a Git repository.")
159
+ view.show_not_git_repository()
160
+ raise SystemExit(1) from None
161
+
162
+ except DetachedHeadError:
163
+ logger.info("Command aborted: detached HEAD.")
164
+ view.show_detached_head()
165
+ raise SystemExit(1) from None
166
+
167
+ except BaseBranchNotFoundError:
168
+ logger.info("Command aborted: base branch could not be determined.")
169
+ view.show_base_branch_not_found()
170
+ raise SystemExit(1) from None
171
+
172
+ except SameBranchError:
173
+ logger.info("Current branch and base branch are the same.")
174
+ view.show_same_branch()
175
+ raise SystemExit(1) from None
176
+
177
+ except RemoteBranchNotFoundError:
178
+ logger.info("Command aborted: remote branch does not exist.")
179
+ view.show_error("Remote branch does not exist on origin.")
180
+ raise SystemExit(1) from None
181
+
182
+ except UnpushedChangesError:
183
+ logger.info("Command aborted: branch contains unpushed commits.")
184
+ view.show_error("Your branch contains commits that have not been pushed to origin.")
185
+ raise SystemExit(1) from None
186
+
187
+ except ProviderError as e:
188
+ logger.warning(str(e))
189
+ view.show_error(str(e))
190
+ raise SystemExit(1) from None
191
+
192
+ except CredentialNotFoundError as e:
193
+ logger.warning(str(e))
194
+ view.show_error(str(e))
195
+ raise SystemExit(1) from None
196
+
197
+ except ConfigError as e:
198
+ logger.warning(str(e))
199
+ view.show_error(str(e))
200
+ raise SystemExit(1) from None
201
+
202
+ except InvalidPullRequestDraftError as e:
203
+ logger.warning(str(e))
204
+ view.show_error(str(e))
205
+ raise SystemExit(1) from None
206
+
207
+ except GitHubCLIUnavailableError as e:
208
+ logger.warning(str(e))
209
+ view.show_error(str(e))
210
+ raise SystemExit(1) from None
211
+
212
+ except GitHubAuthenticationError as e:
213
+ logger.warning(str(e))
214
+ view.show_error(str(e))
215
+ raise SystemExit(1) from None
216
+
217
+ except Exception:
218
+ logger.exception("Unexpected error while executing PR command.")
219
+ view.show_error("An unexpected error occurred. Please check the log file for more details.")
220
+ raise SystemExit(1) from None
@@ -1,14 +1,27 @@
1
1
  from .base import (
2
2
  ConfigError,
3
+ DetachedHeadError,
3
4
  DiffSageError,
4
5
  GitError,
5
6
  InvalidCommitMessageError,
6
7
  InvalidConfigurationValueError,
8
+ InvalidPullRequestDraftError,
7
9
  ProviderError,
10
+ SameBranchError,
8
11
  UnknownConfigurationKeyError,
9
12
  )
10
13
  from .credentials import CredentialNotFoundError
11
- from .git import NoStagedChangesError, NotGitRepositoryError
14
+ from .git import (
15
+ BaseBranchNotFoundError,
16
+ NoStagedChangesError,
17
+ NotGitRepositoryError,
18
+ RemoteBranchNotFoundError,
19
+ UnpushedChangesError,
20
+ )
21
+ from .github import (
22
+ GitHubAuthenticationError,
23
+ GitHubCLIUnavailableError,
24
+ )
12
25
  from .provider import (
13
26
  AuthenticationError,
14
27
  ModelNotFoundError,
@@ -17,18 +30,26 @@ from .provider import (
17
30
  )
18
31
 
19
32
  __all__ = [
20
- "DiffSageError",
21
- "ProviderError",
22
- "AuthenticationError",
23
- "RateLimitError",
24
- "ModelNotFoundError",
25
- "ProviderUnavailableError",
26
33
  "ConfigError",
34
+ "DetachedHeadError",
35
+ "DiffSageError",
27
36
  "GitError",
28
- "UnknownConfigurationKeyError",
29
- "InvalidConfigurationValueError",
30
37
  "InvalidCommitMessageError",
38
+ "InvalidConfigurationValueError",
39
+ "InvalidPullRequestDraftError",
40
+ "ProviderError",
41
+ "SameBranchError",
42
+ "UnknownConfigurationKeyError",
43
+ "CredentialNotFoundError",
44
+ "BaseBranchNotFoundError",
31
45
  "NoStagedChangesError",
32
46
  "NotGitRepositoryError",
33
- "CredentialNotFoundError",
47
+ "RemoteBranchNotFoundError",
48
+ "UnpushedChangesError",
49
+ "GitHubAuthenticationError",
50
+ "GitHubCLIUnavailableError",
51
+ "AuthenticationError",
52
+ "ModelNotFoundError",
53
+ "ProviderUnavailableError",
54
+ "RateLimitError",
34
55
  ]