gac 2.1.0__tar.gz → 2.2.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.

Potentially problematic release.


This version of gac might be problematic. Click here for more details.

Files changed (44) hide show
  1. {gac-2.1.0 → gac-2.2.0}/PKG-INFO +17 -4
  2. {gac-2.1.0 → gac-2.2.0}/README.md +15 -3
  3. {gac-2.1.0 → gac-2.2.0}/pyproject.toml +1 -0
  4. {gac-2.1.0 → gac-2.2.0}/src/gac/__version__.py +1 -1
  5. {gac-2.1.0 → gac-2.2.0}/src/gac/constants.py +1 -1
  6. {gac-2.1.0 → gac-2.2.0}/src/gac/main.py +14 -1
  7. gac-2.2.0/src/gac/utils.py +270 -0
  8. gac-2.1.0/src/gac/utils.py +0 -132
  9. {gac-2.1.0 → gac-2.2.0}/.gitignore +0 -0
  10. {gac-2.1.0 → gac-2.2.0}/LICENSE +0 -0
  11. {gac-2.1.0 → gac-2.2.0}/src/gac/__init__.py +0 -0
  12. {gac-2.1.0 → gac-2.2.0}/src/gac/ai.py +0 -0
  13. {gac-2.1.0 → gac-2.2.0}/src/gac/ai_utils.py +0 -0
  14. {gac-2.1.0 → gac-2.2.0}/src/gac/cli.py +0 -0
  15. {gac-2.1.0 → gac-2.2.0}/src/gac/config.py +0 -0
  16. {gac-2.1.0 → gac-2.2.0}/src/gac/config_cli.py +0 -0
  17. {gac-2.1.0 → gac-2.2.0}/src/gac/diff_cli.py +0 -0
  18. {gac-2.1.0 → gac-2.2.0}/src/gac/errors.py +0 -0
  19. {gac-2.1.0 → gac-2.2.0}/src/gac/git.py +0 -0
  20. {gac-2.1.0 → gac-2.2.0}/src/gac/init_cli.py +0 -0
  21. {gac-2.1.0 → gac-2.2.0}/src/gac/language_cli.py +0 -0
  22. {gac-2.1.0 → gac-2.2.0}/src/gac/preprocess.py +0 -0
  23. {gac-2.1.0 → gac-2.2.0}/src/gac/prompt.py +0 -0
  24. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/__init__.py +0 -0
  25. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/anthropic.py +0 -0
  26. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/cerebras.py +0 -0
  27. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/chutes.py +0 -0
  28. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/custom_anthropic.py +0 -0
  29. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/custom_openai.py +0 -0
  30. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/deepseek.py +0 -0
  31. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/fireworks.py +0 -0
  32. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/gemini.py +0 -0
  33. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/groq.py +0 -0
  34. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/lmstudio.py +0 -0
  35. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/minimax.py +0 -0
  36. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/mistral.py +0 -0
  37. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/ollama.py +0 -0
  38. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/openai.py +0 -0
  39. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/openrouter.py +0 -0
  40. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/streamlake.py +0 -0
  41. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/synthetic.py +0 -0
  42. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/together.py +0 -0
  43. {gac-2.1.0 → gac-2.2.0}/src/gac/providers/zai.py +0 -0
  44. {gac-2.1.0 → gac-2.2.0}/src/gac/security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gac
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: LLM-powered Git commit message generator with multi-provider support
5
5
  Project-URL: Homepage, https://github.com/cellwebb/gac
6
6
  Project-URL: Documentation, https://github.com/cellwebb/gac#readme
@@ -25,6 +25,7 @@ Requires-Dist: click>=8.3.0
25
25
  Requires-Dist: halo
26
26
  Requires-Dist: httpcore>=1.0.9
27
27
  Requires-Dist: httpx>=0.28.0
28
+ Requires-Dist: prompt-toolkit>=3.0.36
28
29
  Requires-Dist: pydantic>=2.12.0
29
30
  Requires-Dist: python-dotenv>=1.1.1
30
31
  Requires-Dist: questionary
@@ -123,7 +124,7 @@ uv tool upgrade gac
123
124
 
124
125
  ### 💻 **Developer Experience**
125
126
 
126
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
127
+ - **Interactive feedback**: Type `r` to reroll, `e` to edit in-place with vi/emacs keybindings, or directly type your feedback like `make it shorter` or `focus on the bug fix`
127
128
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
128
129
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
129
130
 
@@ -146,7 +147,7 @@ git add .
146
147
  # Generate and commit with LLM
147
148
  gac
148
149
 
149
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
150
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
150
151
  ```
151
152
 
152
153
  ### Common Commands
@@ -183,12 +184,17 @@ gac --skip-secret-scan
183
184
 
184
185
  ### Interactive Feedback System
185
186
 
186
- Not happy with the result? You have two options:
187
+ Not happy with the result? You have several options:
187
188
 
188
189
  ```bash
189
190
  # Simple reroll (no feedback)
190
191
  r
191
192
 
193
+ # Edit in-place with rich terminal editing
194
+ e
195
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
196
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
197
+
192
198
  # Or just type your feedback directly!
193
199
  make it shorter and focus on the performance improvement
194
200
  use conventional commit format with scope
@@ -197,6 +203,13 @@ explain the security implications
197
203
  # Press Enter on empty input to see the prompt again
198
204
  ```
199
205
 
206
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
207
+
208
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
209
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
210
+ - **Add details**: Include information the LLM might have missed
211
+ - **Restructure**: Reorganize bullet points or change the message structure
212
+
200
213
  ---
201
214
 
202
215
  ## Configuration
@@ -82,7 +82,7 @@ uv tool upgrade gac
82
82
 
83
83
  ### 💻 **Developer Experience**
84
84
 
85
- - **Interactive feedback**: Type `r` to reroll, or directly type your feedback like `make it shorter` or `focus on the bug fix`
85
+ - **Interactive feedback**: Type `r` to reroll, `e` to edit in-place with vi/emacs keybindings, or directly type your feedback like `make it shorter` or `focus on the bug fix`
86
86
  - **One-command workflows**: Complete workflows with flags like `gac -ayp` (stage all, auto-confirm, push)
87
87
  - **Git integration**: Respects pre-commit and lefthook hooks, running them before expensive LLM operations
88
88
 
@@ -105,7 +105,7 @@ git add .
105
105
  # Generate and commit with LLM
106
106
  gac
107
107
 
108
- # Review → y (commit) | n (cancel) | r (reroll) | or type feedback
108
+ # Review → y (commit) | n (cancel) | r (reroll) | e (edit) | or type feedback
109
109
  ```
110
110
 
111
111
  ### Common Commands
@@ -142,12 +142,17 @@ gac --skip-secret-scan
142
142
 
143
143
  ### Interactive Feedback System
144
144
 
145
- Not happy with the result? You have two options:
145
+ Not happy with the result? You have several options:
146
146
 
147
147
  ```bash
148
148
  # Simple reroll (no feedback)
149
149
  r
150
150
 
151
+ # Edit in-place with rich terminal editing
152
+ e
153
+ # Uses prompt_toolkit for multi-line editing with vi/emacs keybindings
154
+ # Press Esc+Enter or Ctrl+S to submit, Ctrl+C to cancel
155
+
151
156
  # Or just type your feedback directly!
152
157
  make it shorter and focus on the performance improvement
153
158
  use conventional commit format with scope
@@ -156,6 +161,13 @@ explain the security implications
156
161
  # Press Enter on empty input to see the prompt again
157
162
  ```
158
163
 
164
+ The edit feature (`e`) provides rich in-place terminal editing, allowing you to:
165
+
166
+ - **Edit naturally**: Multi-line editing with familiar vi/emacs key bindings
167
+ - **Make quick fixes**: Correct typos, adjust wording, or refine formatting
168
+ - **Add details**: Include information the LLM might have missed
169
+ - **Restructure**: Reorganize bullet points or change the message structure
170
+
159
171
  ---
160
172
 
161
173
  ## Configuration
@@ -41,6 +41,7 @@ dependencies = [
41
41
  "halo",
42
42
  "questionary",
43
43
  "rich>=14.1.0",
44
+ "prompt_toolkit>=3.0.36",
44
45
 
45
46
  ]
46
47
 
@@ -1,3 +1,3 @@
1
1
  """Version information for gac package."""
2
2
 
3
- __version__ = "2.1.0"
3
+ __version__ = "2.2.0"
@@ -21,7 +21,7 @@ class EnvDefaults:
21
21
  MAX_RETRIES: int = 3
22
22
  TEMPERATURE: float = 1
23
23
  MAX_OUTPUT_TOKENS: int = 1024 # includes reasoning tokens
24
- WARNING_LIMIT_TOKENS: int = 16384
24
+ WARNING_LIMIT_TOKENS: int = 32768
25
25
  ALWAYS_INCLUDE_SCOPE: bool = False
26
26
  SKIP_SECRET_SCAN: bool = False
27
27
  VERBOSE: bool = False
@@ -25,6 +25,7 @@ from gac.git import (
25
25
  from gac.preprocess import preprocess_diff
26
26
  from gac.prompt import build_prompt, clean_commit_message
27
27
  from gac.security import get_affected_files, scan_staged_diff
28
+ from gac.utils import edit_commit_message_inplace
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
31
 
@@ -277,7 +278,7 @@ def main(
277
278
  if require_confirmation:
278
279
  while True:
279
280
  response = click.prompt(
280
- "Proceed with commit above? [y/n/r/<feedback>]",
281
+ "Proceed with commit above? [y/n/r/e/<feedback>]",
281
282
  type=str,
282
283
  show_default=False,
283
284
  ).strip()
@@ -290,6 +291,18 @@ def main(
290
291
  sys.exit(0)
291
292
  if response == "":
292
293
  continue
294
+ if response_lower in ["e", "edit"]:
295
+ edited_message = edit_commit_message_inplace(commit_message)
296
+ if edited_message:
297
+ commit_message = edited_message
298
+ conversation_messages[-1] = {"role": "assistant", "content": commit_message}
299
+ logger.info("Commit message edited by user")
300
+ console.print("\n[bold green]Edited commit message:[/bold green]")
301
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
302
+ else:
303
+ console.print("[yellow]Using previous message.[/yellow]")
304
+ console.print(Panel(commit_message, title="Commit Message", border_style="cyan"))
305
+ continue
293
306
  if response_lower in ["r", "reroll"]:
294
307
  feedback_message = (
295
308
  "Please provide an alternative commit message using the same repository context."
@@ -0,0 +1,270 @@
1
+ """Utility functions for gac."""
2
+
3
+ import logging
4
+ import subprocess
5
+
6
+ from rich.console import Console
7
+ from rich.theme import Theme
8
+
9
+ from gac.constants import Logging
10
+ from gac.errors import GacError
11
+
12
+
13
+ def setup_logging(
14
+ log_level: int | str = Logging.DEFAULT_LEVEL,
15
+ quiet: bool = False,
16
+ force: bool = False,
17
+ suppress_noisy: bool = False,
18
+ ) -> None:
19
+ """Configure logging for the application.
20
+
21
+ Args:
22
+ log_level: Log level to use (DEBUG, INFO, WARNING, ERROR)
23
+ quiet: If True, suppress all output except errors
24
+ force: If True, force reconfiguration of logging
25
+ suppress_noisy: If True, suppress noisy third-party loggers
26
+ """
27
+ if isinstance(log_level, str):
28
+ log_level = getattr(logging, log_level.upper(), logging.WARNING)
29
+
30
+ if quiet:
31
+ log_level = logging.ERROR
32
+
33
+ kwargs = {"force": force} if force else {}
34
+
35
+ logging.basicConfig(
36
+ level=log_level,
37
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
+ datefmt="%Y-%m-%d %H:%M:%S",
39
+ **kwargs, # type: ignore[arg-type]
40
+ )
41
+
42
+ if suppress_noisy:
43
+ for noisy_logger in ["requests", "urllib3"]:
44
+ logging.getLogger(noisy_logger).setLevel(logging.WARNING)
45
+
46
+ logger.info(f"Logging initialized with level: {logging.getLevelName(log_level)}")
47
+
48
+
49
+ theme = Theme(
50
+ {
51
+ "success": "green bold",
52
+ "info": "blue",
53
+ "warning": "yellow",
54
+ "error": "red bold",
55
+ "header": "magenta",
56
+ "notification": "bright_cyan bold",
57
+ }
58
+ )
59
+ console = Console(theme=theme)
60
+ logger = logging.getLogger(__name__)
61
+
62
+
63
+ def print_message(message: str, level: str = "info") -> None:
64
+ """Print a styled message with the specified level."""
65
+ console.print(message, style=level)
66
+
67
+
68
+ def run_subprocess(
69
+ command: list[str],
70
+ silent: bool = False,
71
+ timeout: int = 60,
72
+ check: bool = True,
73
+ strip_output: bool = True,
74
+ raise_on_error: bool = True,
75
+ ) -> str:
76
+ """Run a subprocess command safely and return the output.
77
+
78
+ Args:
79
+ command: List of command arguments
80
+ silent: If True, suppress debug logging
81
+ timeout: Command timeout in seconds
82
+ check: Whether to check return code (for compatibility)
83
+ strip_output: Whether to strip whitespace from output
84
+ raise_on_error: Whether to raise an exception on error
85
+
86
+ Returns:
87
+ Command output as string
88
+
89
+ Raises:
90
+ GacError: If the command times out
91
+ subprocess.CalledProcessError: If the command fails and raise_on_error is True
92
+ """
93
+ if not silent:
94
+ logger.debug(f"Running command: {' '.join(command)}")
95
+
96
+ try:
97
+ result = subprocess.run(
98
+ command,
99
+ capture_output=True,
100
+ text=True,
101
+ check=False,
102
+ timeout=timeout,
103
+ )
104
+
105
+ should_raise = result.returncode != 0 and (check or raise_on_error)
106
+
107
+ if should_raise:
108
+ if not silent:
109
+ logger.debug(f"Command stderr: {result.stderr}")
110
+ raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
111
+
112
+ output = result.stdout
113
+ if strip_output:
114
+ output = output.strip()
115
+
116
+ return output
117
+ except subprocess.TimeoutExpired as e:
118
+ logger.error(f"Command timed out after {timeout} seconds: {' '.join(command)}")
119
+ raise GacError(f"Command timed out: {' '.join(command)}") from e
120
+ except subprocess.CalledProcessError as e:
121
+ if not silent:
122
+ logger.error(f"Command failed: {e.stderr.strip() if e.stderr else str(e)}")
123
+ if raise_on_error:
124
+ raise
125
+ return ""
126
+ except Exception as e:
127
+ if not silent:
128
+ logger.debug(f"Command error: {e}")
129
+ if raise_on_error:
130
+ # Convert generic exceptions to CalledProcessError for consistency
131
+ raise subprocess.CalledProcessError(1, command, "", str(e)) from e
132
+ return ""
133
+
134
+
135
+ def edit_commit_message_inplace(message: str) -> str | None:
136
+ """Edit commit message in-place using rich terminal editing.
137
+
138
+ Uses prompt_toolkit to provide a rich editing experience with:
139
+ - Multi-line editing
140
+ - Vi/Emacs key bindings
141
+ - Line editing capabilities
142
+ - Esc+Enter or Ctrl+S to submit
143
+ - Ctrl+C to cancel
144
+
145
+ Args:
146
+ message: The initial commit message
147
+
148
+ Returns:
149
+ The edited commit message, or None if editing was cancelled
150
+
151
+ Example:
152
+ >>> edited = edit_commit_message_inplace("feat: add feature")
153
+ >>> # User can edit the message using vi/emacs key bindings
154
+ >>> # Press Esc+Enter or Ctrl+S to submit
155
+ """
156
+ from prompt_toolkit import Application
157
+ from prompt_toolkit.buffer import Buffer
158
+ from prompt_toolkit.document import Document
159
+ from prompt_toolkit.enums import EditingMode
160
+ from prompt_toolkit.key_binding import KeyBindings
161
+ from prompt_toolkit.layout import HSplit, Layout, Window
162
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
163
+ from prompt_toolkit.layout.margins import ScrollbarMargin
164
+ from prompt_toolkit.styles import Style
165
+
166
+ try:
167
+ console.print("\n[info]Edit commit message:[/info]")
168
+ console.print()
169
+
170
+ # Create buffer for text editing
171
+ text_buffer = Buffer(
172
+ document=Document(text=message, cursor_position=0),
173
+ multiline=True,
174
+ enable_history_search=False,
175
+ )
176
+
177
+ # Track submission state
178
+ cancelled = {"value": False}
179
+ submitted = {"value": False}
180
+
181
+ # Create text editor window
182
+ text_window = Window(
183
+ content=BufferControl(
184
+ buffer=text_buffer,
185
+ focus_on_click=True,
186
+ ),
187
+ height=lambda: max(5, message.count("\n") + 3),
188
+ wrap_lines=True,
189
+ right_margins=[ScrollbarMargin()],
190
+ )
191
+
192
+ # Create hint window
193
+ hint_window = Window(
194
+ content=FormattedTextControl(
195
+ text=[("class:hint", " Esc+Enter or Ctrl+S to submit | Ctrl+C to cancel ")],
196
+ ),
197
+ height=1,
198
+ dont_extend_height=True,
199
+ )
200
+
201
+ # Create layout
202
+ root_container = HSplit(
203
+ [
204
+ text_window,
205
+ hint_window,
206
+ ]
207
+ )
208
+
209
+ layout = Layout(root_container, focused_element=text_window)
210
+
211
+ # Create key bindings
212
+ kb = KeyBindings()
213
+
214
+ @kb.add("c-s")
215
+ def _(event):
216
+ """Submit with Ctrl+S."""
217
+ submitted["value"] = True
218
+ event.app.exit()
219
+
220
+ @kb.add("c-c")
221
+ def _(event):
222
+ """Cancel editing."""
223
+ cancelled["value"] = True
224
+ event.app.exit()
225
+
226
+ @kb.add("escape", "enter")
227
+ def _(event):
228
+ """Submit with Esc+Enter."""
229
+ submitted["value"] = True
230
+ event.app.exit()
231
+
232
+ # Create and run application
233
+ custom_style = Style.from_dict(
234
+ {
235
+ "hint": "#888888",
236
+ }
237
+ )
238
+
239
+ app: Application[None] = Application(
240
+ layout=layout,
241
+ key_bindings=kb,
242
+ full_screen=False,
243
+ mouse_support=False,
244
+ editing_mode=EditingMode.VI, # Enable vi key bindings
245
+ style=custom_style,
246
+ )
247
+
248
+ app.run()
249
+
250
+ # Handle result
251
+ if cancelled["value"]:
252
+ console.print("\n[yellow]Edit cancelled.[/yellow]")
253
+ return None
254
+
255
+ if submitted["value"]:
256
+ edited_message = text_buffer.text.strip()
257
+ if not edited_message:
258
+ console.print("[yellow]Commit message cannot be empty. Edit cancelled.[/yellow]")
259
+ return None
260
+ return edited_message
261
+
262
+ return None
263
+
264
+ except (EOFError, KeyboardInterrupt):
265
+ console.print("\n[yellow]Edit cancelled.[/yellow]")
266
+ return None
267
+ except Exception as e:
268
+ logger.error(f"Error during in-place editing: {e}")
269
+ console.print(f"[error]Failed to edit commit message: {e}[/error]")
270
+ return None
@@ -1,132 +0,0 @@
1
- """Utility functions for gac."""
2
-
3
- import logging
4
- import subprocess
5
-
6
- from rich.console import Console
7
- from rich.theme import Theme
8
-
9
- from gac.constants import Logging
10
- from gac.errors import GacError
11
-
12
-
13
- def setup_logging(
14
- log_level: int | str = Logging.DEFAULT_LEVEL,
15
- quiet: bool = False,
16
- force: bool = False,
17
- suppress_noisy: bool = False,
18
- ) -> None:
19
- """Configure logging for the application.
20
-
21
- Args:
22
- log_level: Log level to use (DEBUG, INFO, WARNING, ERROR)
23
- quiet: If True, suppress all output except errors
24
- force: If True, force reconfiguration of logging
25
- suppress_noisy: If True, suppress noisy third-party loggers
26
- """
27
- if isinstance(log_level, str):
28
- log_level = getattr(logging, log_level.upper(), logging.WARNING)
29
-
30
- if quiet:
31
- log_level = logging.ERROR
32
-
33
- kwargs = {"force": force} if force else {}
34
-
35
- logging.basicConfig(
36
- level=log_level,
37
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
- datefmt="%Y-%m-%d %H:%M:%S",
39
- **kwargs, # type: ignore[arg-type]
40
- )
41
-
42
- if suppress_noisy:
43
- for noisy_logger in ["requests", "urllib3"]:
44
- logging.getLogger(noisy_logger).setLevel(logging.WARNING)
45
-
46
- logger.info(f"Logging initialized with level: {logging.getLevelName(log_level)}")
47
-
48
-
49
- theme = Theme(
50
- {
51
- "success": "green bold",
52
- "info": "blue",
53
- "warning": "yellow",
54
- "error": "red bold",
55
- "header": "magenta",
56
- "notification": "bright_cyan bold",
57
- }
58
- )
59
- console = Console(theme=theme)
60
- logger = logging.getLogger(__name__)
61
-
62
-
63
- def print_message(message: str, level: str = "info") -> None:
64
- """Print a styled message with the specified level."""
65
- console.print(message, style=level)
66
-
67
-
68
- def run_subprocess(
69
- command: list[str],
70
- silent: bool = False,
71
- timeout: int = 60,
72
- check: bool = True,
73
- strip_output: bool = True,
74
- raise_on_error: bool = True,
75
- ) -> str:
76
- """Run a subprocess command safely and return the output.
77
-
78
- Args:
79
- command: List of command arguments
80
- silent: If True, suppress debug logging
81
- timeout: Command timeout in seconds
82
- check: Whether to check return code (for compatibility)
83
- strip_output: Whether to strip whitespace from output
84
- raise_on_error: Whether to raise an exception on error
85
-
86
- Returns:
87
- Command output as string
88
-
89
- Raises:
90
- GacError: If the command times out
91
- subprocess.CalledProcessError: If the command fails and raise_on_error is True
92
- """
93
- if not silent:
94
- logger.debug(f"Running command: {' '.join(command)}")
95
-
96
- try:
97
- result = subprocess.run(
98
- command,
99
- capture_output=True,
100
- text=True,
101
- check=False,
102
- timeout=timeout,
103
- )
104
-
105
- should_raise = result.returncode != 0 and (check or raise_on_error)
106
-
107
- if should_raise:
108
- if not silent:
109
- logger.debug(f"Command stderr: {result.stderr}")
110
- raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
111
-
112
- output = result.stdout
113
- if strip_output:
114
- output = output.strip()
115
-
116
- return output
117
- except subprocess.TimeoutExpired as e:
118
- logger.error(f"Command timed out after {timeout} seconds: {' '.join(command)}")
119
- raise GacError(f"Command timed out: {' '.join(command)}") from e
120
- except subprocess.CalledProcessError as e:
121
- if not silent:
122
- logger.error(f"Command failed: {e.stderr.strip() if e.stderr else str(e)}")
123
- if raise_on_error:
124
- raise
125
- return ""
126
- except Exception as e:
127
- if not silent:
128
- logger.debug(f"Command error: {e}")
129
- if raise_on_error:
130
- # Convert generic exceptions to CalledProcessError for consistency
131
- raise subprocess.CalledProcessError(1, command, "", str(e)) from e
132
- return ""
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