git-copilot-commit 0.5.5__py3-none-any.whl → 0.5.7__py3-none-any.whl

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.
git_copilot_commit/cli.py CHANGED
@@ -95,6 +95,14 @@ class PreparedSplitCommit:
95
95
  patch_units: tuple[PatchUnit, ...]
96
96
 
97
97
 
98
+ @dataclass(frozen=True, slots=True)
99
+ class SplitCommitExecutionState:
100
+ """Original HEAD state used to roll back partial split-commit execution."""
101
+
102
+ original_head_sha: str | None
103
+ original_head_ref: str | None
104
+
105
+
98
106
  CORE_CHANGE_COMMIT_TYPES = frozenset({"feat", "fix", "perf", "refactor", "revert"})
99
107
  FOLLOW_UP_COMMIT_TYPE_PRIORITY = {
100
108
  "test": 2,
@@ -757,39 +765,62 @@ def execute_split_commit_plan(
757
765
  console.print("Invalid choice. Commit cancelled.")
758
766
  raise typer.Exit()
759
767
 
768
+ execution_state = SplitCommitExecutionState(
769
+ original_head_sha=repo.get_head_sha() if repo.has_commit("HEAD") else None,
770
+ original_head_ref=repo.get_symbolic_head_ref(),
771
+ )
760
772
  commit_shas: list[str] = []
761
773
  total_commits = len(prepared_commits)
762
774
 
763
- for index, prepared_commit in enumerate(prepared_commits, start=1):
764
- console.print(
765
- f"[cyan]Creating commit {index}/{total_commits}:[/cyan] {prepared_commit.message}"
766
- )
775
+ try:
776
+ for index, prepared_commit in enumerate(prepared_commits, start=1):
777
+ console.print(
778
+ f"[cyan]Creating commit {index}/{total_commits}:[/cyan] {prepared_commit.message}"
779
+ )
767
780
 
768
- with repo.temporary_alternate_index() as alternate_index:
769
- try:
770
- for patch_unit in prepared_commit.patch_units:
771
- repo.check_patch_for_alternate_index(
772
- patch_unit.patch,
773
- index=alternate_index,
781
+ with repo.temporary_alternate_index() as alternate_index:
782
+ try:
783
+ for patch_unit in prepared_commit.patch_units:
784
+ repo.check_patch_for_alternate_index(
785
+ patch_unit.patch,
786
+ index=alternate_index,
787
+ )
788
+ repo.apply_patch_to_alternate_index(
789
+ patch_unit.patch,
790
+ index=alternate_index,
791
+ )
792
+ except GitError as exc:
793
+ console.print(
794
+ f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
774
795
  )
775
- repo.apply_patch_to_alternate_index(
776
- patch_unit.patch,
777
- index=alternate_index,
796
+ raise typer.Exit(1)
797
+
798
+ commit_shas.append(
799
+ commit_with_retry_no_verify(
800
+ repo,
801
+ prepared_commit.message,
802
+ use_editor=use_editor,
803
+ env=alternate_index.env,
778
804
  )
779
- except GitError as exc:
780
- console.print(
781
- f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
782
- )
783
- raise typer.Exit(1)
784
-
785
- commit_shas.append(
786
- commit_with_retry_no_verify(
787
- repo,
788
- prepared_commit.message,
789
- use_editor=use_editor,
790
- env=alternate_index.env,
791
805
  )
806
+ except BaseException:
807
+ try:
808
+ if execution_state.original_head_sha is not None:
809
+ repo.soft_reset(execution_state.original_head_sha)
810
+ elif execution_state.original_head_ref is not None and repo.has_commit(
811
+ "HEAD"
812
+ ):
813
+ repo.delete_ref(execution_state.original_head_ref)
814
+ except GitError as exc:
815
+ console.print(
816
+ "[red]Failed to restore the original staged changes after split commit creation stopped early: "
817
+ f"{exc}[/red]"
818
+ )
819
+ else:
820
+ console.print(
821
+ "[yellow]Split commit creation did not complete; restored the original staged changes.[/yellow]"
792
822
  )
823
+ raise
793
824
 
794
825
  return commit_shas
795
826
 
git_copilot_commit/git.py CHANGED
@@ -139,7 +139,9 @@ class GitRepository:
139
139
  except subprocess.CalledProcessError:
140
140
  raise NotAGitRepositoryError(f"{self.cwd} is not a git repository")
141
141
  except subprocess.TimeoutExpired:
142
- raise GitCommandError("Git command timed out: git rev-parse --show-toplevel")
142
+ raise GitCommandError(
143
+ "Git command timed out: git rev-parse --show-toplevel"
144
+ )
143
145
 
144
146
  repo_root = result.stdout.strip()
145
147
  if not repo_root:
@@ -264,10 +266,27 @@ class GitRepository:
264
266
  result = self._run_git_command(["rev-parse", ref])
265
267
  return result.stdout.strip()
266
268
 
269
+ def has_commit(self, ref: str = "HEAD") -> bool:
270
+ """Return whether the provided ref resolves to a commit."""
271
+ result = self._run_git_command(
272
+ ["rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"],
273
+ check=False,
274
+ )
275
+ return result.returncode == 0
276
+
277
+ def get_symbolic_head_ref(self) -> str | None:
278
+ """Return the symbolic ref for HEAD when attached to a branch."""
279
+ result = self._run_git_command(["symbolic-ref", "-q", "HEAD"], check=False)
280
+ if result.returncode != 0:
281
+ return None
282
+
283
+ ref = result.stdout.strip()
284
+ return ref or None
285
+
267
286
  def _parse_status_output(self, status_output: str) -> list[GitFile]:
268
287
  """Parse git status --porcelain output into GitFile objects."""
269
288
  files = []
270
- for line in status_output.strip().split("\n"):
289
+ for line in status_output.splitlines():
271
290
  if not line:
272
291
  continue
273
292
 
@@ -316,14 +335,32 @@ class GitRepository:
316
335
  else:
317
336
  self._run_git_command(["reset", "HEAD"] + self._normalize_paths(paths))
318
337
 
338
+ def soft_reset(self, ref: str) -> None:
339
+ """Move HEAD to ref while preserving the working tree and index."""
340
+ self._run_git_command(["reset", "--soft", ref])
341
+
342
+ def delete_ref(self, ref: str, *, missing_ok: bool = False) -> None:
343
+ """Delete a ref, optionally ignoring missing refs."""
344
+ result = self._run_git_command(["update-ref", "-d", ref], check=False)
345
+ if result.returncode == 0 or missing_ok:
346
+ return
347
+
348
+ error_output = result.stderr or result.stdout or ""
349
+ if error_output:
350
+ raise GitCommandError(
351
+ f"Git command failed: git update-ref -d {ref}\n{error_output}"
352
+ )
353
+ raise GitCommandError(f"Git command failed: git update-ref -d {ref}")
354
+
319
355
  def create_alternate_index(self, from_ref: str = "HEAD") -> AlternateGitIndex:
320
356
  """Create a temporary git index initialized from the provided ref."""
321
- fd, index_path = tempfile.mkstemp(
322
- prefix="git-copilot-commit-", suffix=".index"
323
- )
357
+ fd, index_path = tempfile.mkstemp(prefix="git-copilot-commit-", suffix=".index")
324
358
  os.close(fd)
325
359
  alternate_index = AlternateGitIndex(Path(index_path))
326
- self.read_tree(from_ref, index=alternate_index)
360
+ if from_ref == "HEAD" and not self.has_commit(from_ref):
361
+ self.read_empty_tree(index=alternate_index)
362
+ else:
363
+ self.read_tree(from_ref, index=alternate_index)
327
364
  return alternate_index
328
365
 
329
366
  @contextmanager
@@ -341,6 +378,10 @@ class GitRepository:
341
378
  """Populate an alternate index from the provided ref."""
342
379
  self._run_git_command(["read-tree", ref], env=index.env)
343
380
 
381
+ def read_empty_tree(self, *, index: AlternateGitIndex) -> None:
382
+ """Initialize an alternate index with an empty tree."""
383
+ self._run_git_command(["read-tree", "--empty"], env=index.env)
384
+
344
385
  def apply_patch(
345
386
  self,
346
387
  patch: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-copilot-commit
3
- Version: 0.5.5
3
+ Version: 0.5.7
4
4
  Summary: Automatically generate and commit changes using GitHub Copilot
5
5
  Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  git_copilot_commit/__init__.py,sha256=v3x5oBkxwKJEZLv62QqSmP3iqNKLtZgrWZfH8eFzlQg,60
2
- git_copilot_commit/cli.py,sha256=yN7fq43i3TFerhEOMm4ZHUy2rbiH0n70tIcwrxdWCdc,33929
3
- git_copilot_commit/git.py,sha256=vNuh2j8TGmocLio4XgPpbXIktlgczNdEt2Fg1c40wuk,14962
2
+ git_copilot_commit/cli.py,sha256=FynjjH0cnMdyjsvNayNhwmDfMaoJyxu8OD5O7pv_Kvg,35221
3
+ git_copilot_commit/git.py,sha256=dEsyazWfD2TIVCpDObwu6TDWRBkzxEjdIKHrQ59h7I0,16697
4
4
  git_copilot_commit/github_copilot.py,sha256=2SKVVFmQ2yETAySjSRYKQTujW-1vZEW4rFKSr--dtLs,50427
5
5
  git_copilot_commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  git_copilot_commit/settings.py,sha256=WrM10_J3F7QBfOVmPDWpNZrNHhmZSeN-9FqQZxgdWvQ,3730
@@ -8,8 +8,8 @@ git_copilot_commit/split_commits.py,sha256=rHyuVJggjmYjbva7BVqsM3aZRxUgOKkuZtxxv
8
8
  git_copilot_commit/version.py,sha256=AieHOUX52g6N67HL0iLWtDKrgOYyulxwHWViu26Jrd4,105
9
9
  git_copilot_commit/prompts/commit-message-generator-prompt.md,sha256=EHAS6w15vLQ-kgT1N7nPG2nWqdeTmlHje_kN9yZIoZQ,2378
10
10
  git_copilot_commit/prompts/split-commit-planner-prompt.md,sha256=JActWTKMbdR9eaJyCbQ6UwzyRu5jJi8Iec7H7qWAmAA,1418
11
- git_copilot_commit-0.5.5.dist-info/METADATA,sha256=8lx9m_ZB9r3MOmWAz-pb8XLHZH1cp2PC_IvWlqRp9yw,6649
12
- git_copilot_commit-0.5.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
- git_copilot_commit-0.5.5.dist-info/entry_points.txt,sha256=-D4bQqiuSPwQJG2zx--vJbZD1iqB5coUfoJ_gmC3rSg,66
14
- git_copilot_commit-0.5.5.dist-info/licenses/LICENSE,sha256=14lNZAoKJPI1U7eGpletjN_PFm1JwP1vT_0jFKY6eWg,1065
15
- git_copilot_commit-0.5.5.dist-info/RECORD,,
11
+ git_copilot_commit-0.5.7.dist-info/METADATA,sha256=8Dw3F46Ybp1PmkPQZmAbCmSrlJNBg_puowP3v2xxMVQ,6649
12
+ git_copilot_commit-0.5.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
13
+ git_copilot_commit-0.5.7.dist-info/entry_points.txt,sha256=-D4bQqiuSPwQJG2zx--vJbZD1iqB5coUfoJ_gmC3rSg,66
14
+ git_copilot_commit-0.5.7.dist-info/licenses/LICENSE,sha256=14lNZAoKJPI1U7eGpletjN_PFm1JwP1vT_0jFKY6eWg,1065
15
+ git_copilot_commit-0.5.7.dist-info/RECORD,,