github2gerrit 0.1.15__tar.gz → 0.1.17__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 (98) hide show
  1. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/PKG-INFO +1 -1
  2. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/action.yaml +2 -2
  3. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/cli.py +50 -8
  4. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/core.py +331 -21
  5. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.editorconfig +0 -0
  6. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.gitignore +0 -0
  7. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.gitlint +0 -0
  8. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.markdownlint.yaml +0 -0
  9. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.pre-commit-config.yaml +0 -0
  10. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.readthedocs.yml +0 -0
  11. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/.yamllint +0 -0
  12. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/LICENSE +0 -0
  13. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/LICENSES/Apache-2.0.txt +0 -0
  14. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/README.md +0 -0
  15. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/REUSE.toml +0 -0
  16. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/REVISION_PLAN.md +0 -0
  17. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  18. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/docs/github2gerrit_token_permissions_classic.png +0 -0
  19. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/pyproject.toml +0 -0
  20. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/sitecustomize.py +0 -0
  21. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/__init__.py +0 -0
  22. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/commit_normalization.py +0 -0
  23. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/config.py +0 -0
  24. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/duplicate_detection.py +0 -0
  25. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/external_api.py +0 -0
  26. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/gerrit_query.py +0 -0
  27. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/gerrit_rest.py +0 -0
  28. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/gerrit_urls.py +0 -0
  29. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/github_api.py +0 -0
  30. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/gitutils.py +0 -0
  31. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/mapping_comment.py +0 -0
  32. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/models.py +0 -0
  33. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/orchestrator/__init__.py +0 -0
  34. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  35. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/pr_content_filter.py +0 -0
  36. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/reconcile_matcher.py +0 -0
  37. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/rich_display.py +0 -0
  38. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/rich_logging.py +0 -0
  39. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/similarity.py +0 -0
  40. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/ssh_agent_setup.py +0 -0
  41. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/ssh_common.py +0 -0
  42. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/ssh_discovery.py +0 -0
  43. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/trailers.py +0 -0
  44. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/src/github2gerrit/utils.py +0 -0
  45. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/conftest.py +0 -0
  46. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/fixtures/__init__.py +0 -0
  47. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/fixtures/make_repo.py +0 -0
  48. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_action_environment_mapping.py +0 -0
  49. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_action_outputs.py +0 -0
  50. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_action_pr_number_handling.py +0 -0
  51. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_action_step_validation.py +0 -0
  52. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_change_id_deduplication.py +0 -0
  53. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_cli.py +0 -0
  54. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_cli_helpers.py +0 -0
  55. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_cli_outputs_file.py +0 -0
  56. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_cli_url_and_dryrun.py +0 -0
  57. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_commit_normalization.py +0 -0
  58. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_composite_action_coverage.py +0 -0
  59. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_config_and_reviewers.py +0 -0
  60. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_config_helpers.py +0 -0
  61. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_close_pr_policy.py +0 -0
  62. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_config_and_errors.py +0 -0
  63. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_gerrit_backref_comment.py +0 -0
  64. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_gerrit_push_errors.py +0 -0
  65. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_gerrit_rest_results.py +0 -0
  66. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_integration_fixture_repo.py +0 -0
  67. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_prepare_commits.py +0 -0
  68. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_core_ssh_setup.py +0 -0
  69. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_duplicate_detection.py +0 -0
  70. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_email_case_normalization.py +0 -0
  71. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_external_api_framework.py +0 -0
  72. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_gerrit_change_id_footer.py +0 -0
  73. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_gerrit_rest_client.py +0 -0
  74. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_gerrit_urls.py +0 -0
  75. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_gerrit_urls_more.py +0 -0
  76. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_ghe_and_gitreview_args.py +0 -0
  77. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_github_api_helpers.py +0 -0
  78. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_github_api_retry_and_helpers.py +0 -0
  79. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_gitutils_helpers.py +0 -0
  80. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_mapping_comment_additional.py +0 -0
  81. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  82. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_metadata_and_reconciliation.py +0 -0
  83. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_metadata_trailer_separation_bug.py +0 -0
  84. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_misc_small_coverage.py +0 -0
  85. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_orphan_rest_side_effects.py +0 -0
  86. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_pr_content_filter.py +0 -0
  87. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_pr_content_filter_integration.py +0 -0
  88. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_reconciliation_extracted_module.py +0 -0
  89. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  90. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_reconciliation_scenarios.py +0 -0
  91. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_ssh_agent.py +0 -0
  92. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_ssh_artifact_prevention.py +0 -0
  93. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_ssh_common.py +0 -0
  94. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_ssh_discovery.py +0 -0
  95. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_trailers_additional.py +0 -0
  96. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_url_parser.py +0 -0
  97. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/tests/test_utils.py +0 -0
  98. {github2gerrit-0.1.15 → github2gerrit-0.1.17}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit
@@ -134,13 +134,13 @@ runs:
134
134
  steps:
135
135
  - name: Setup Python
136
136
  # yamllint disable-line rule:line-length
137
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
137
+ uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
138
138
  with:
139
139
  python-version-file: '${{ github.action_path }}/pyproject.toml'
140
140
 
141
141
  - name: Setup uv
142
142
  # yamllint disable-line rule:line-length
143
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
143
+ uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
144
144
 
145
145
  - name: Checkout repository
146
146
  # yamllint disable-line rule:line-length
@@ -36,6 +36,7 @@ from .github_api import get_pr_title_body
36
36
  from .github_api import get_pull
37
37
  from .github_api import get_repo_from_env
38
38
  from .github_api import iter_open_pulls
39
+ from .gitutils import CommandError
39
40
  from .gitutils import run_cmd
40
41
  from .models import GitHubContext
41
42
  from .models import Inputs
@@ -246,7 +247,7 @@ if "--help" in sys.argv or _is_github_actions_context():
246
247
 
247
248
  app: typer.Typer = typer.Typer(
248
249
  add_completion=False,
249
- no_args_is_help=False,
250
+ no_args_is_help=True,
250
251
  cls=cast(Any, _SingleUsageGroup),
251
252
  rich_markup_mode="rich",
252
253
  help="Tool to convert GitHub pull requests into Gerrit changes",
@@ -990,15 +991,56 @@ def _process_single(
990
991
  else:
991
992
  progress_tracker.change_updated()
992
993
  except Exception as exc:
993
- error_msg = str(exc)
994
- log.debug("Execution failed; continuing to write outputs: %s", exc)
994
+ # Enhanced error handling for CommandError to show git command
995
+ # details
996
+ if isinstance(exc, CommandError):
997
+ # Always show the basic error message
998
+ cmd_str = " ".join(exc.cmd) if exc.cmd else "unknown command"
999
+ basic_error = f"Git command failed: {cmd_str}"
1000
+ if exc.returncode is not None:
1001
+ basic_error += f" (exit code: {exc.returncode})"
1002
+
1003
+ # In verbose mode, show detailed stdout/stderr
1004
+ if is_verbose_mode():
1005
+ detailed_msg = basic_error
1006
+ if exc.stdout and exc.stdout.strip():
1007
+ detailed_msg += f"\nGit stdout: {exc.stdout.strip()}"
1008
+ if exc.stderr and exc.stderr.strip():
1009
+ detailed_msg += f"\nGit stderr: {exc.stderr.strip()}"
1010
+
1011
+ # Show debugging suggestion for merge failures
1012
+ if "merge --squash" in " ".join(exc.cmd or []):
1013
+ detailed_msg += (
1014
+ "\n💡 For local debugging: python "
1015
+ "test_merge_failure.py --verbose"
1016
+ )
995
1017
 
996
- # Always show the actual error to the user, not just in debug mode
997
- if progress_tracker:
998
- progress_tracker.add_error(f"Execution failed: {error_msg}")
1018
+ safe_console_print(f"❌ {detailed_msg}", style="red")
1019
+ if progress_tracker:
1020
+ progress_tracker.add_error(basic_error)
1021
+ if exc.stderr and exc.stderr.strip():
1022
+ progress_tracker.add_error(
1023
+ f"Details: {exc.stderr.strip()}"
1024
+ )
1025
+ else:
1026
+ # In non-verbose mode, show basic error with hint to enable
1027
+ # verbose
1028
+ hint_msg = (
1029
+ basic_error
1030
+ + "\n💡 Run with VERBOSE=true for detailed git output"
1031
+ )
1032
+ safe_console_print(f"❌ {hint_msg}", style="red")
1033
+ if progress_tracker:
1034
+ progress_tracker.add_error(basic_error)
999
1035
  else:
1000
- # If no progress tracker, show error directly
1001
- safe_console_print(f"❌ Error: {error_msg}", style="red")
1036
+ # For other exceptions, use original handling
1037
+ error_msg = str(exc)
1038
+ if progress_tracker:
1039
+ progress_tracker.add_error(f"Execution failed: {error_msg}")
1040
+ else:
1041
+ safe_console_print(f"❌ Error: {error_msg}", style="red")
1042
+
1043
+ log.debug("Execution failed; continuing to write outputs: %s", exc)
1002
1044
 
1003
1045
  # In verbose mode, also log the full exception with traceback
1004
1046
  if is_verbose_mode():
@@ -1749,6 +1749,10 @@ class Orchestrator:
1749
1749
  ) -> None:
1750
1750
  """Set git global config and initialize git-review if needed."""
1751
1751
  log.debug("Configuring git and git-review for %s", gerrit.host)
1752
+
1753
+ # Configure git user identity (required for merge operations)
1754
+ self._ensure_git_user_identity(inputs)
1755
+
1752
1756
  # Prefer repo-local config; fallback to global if needed
1753
1757
  try:
1754
1758
  git_config(
@@ -1761,26 +1765,7 @@ class Orchestrator:
1761
1765
  git_config(
1762
1766
  "gitreview.username", inputs.gerrit_ssh_user_g2g, global_=True
1763
1767
  )
1764
- try:
1765
- git_config(
1766
- "user.name",
1767
- inputs.gerrit_ssh_user_g2g,
1768
- global_=False,
1769
- cwd=self.workspace,
1770
- )
1771
- except GitError:
1772
- git_config("user.name", inputs.gerrit_ssh_user_g2g, global_=True)
1773
- try:
1774
- git_config(
1775
- "user.email",
1776
- inputs.gerrit_ssh_user_g2g_email,
1777
- global_=False,
1778
- cwd=self.workspace,
1779
- )
1780
- except GitError:
1781
- git_config(
1782
- "user.email", inputs.gerrit_ssh_user_g2g_email, global_=True
1783
- )
1768
+ # Git user identity is configured by _ensure_git_user_identity
1784
1769
  # Disable GPG signing to avoid interactive prompts for signing keys
1785
1770
  try:
1786
1771
  git_config(
@@ -2028,10 +2013,99 @@ class Orchestrator:
2028
2013
  # Create temp branch from base and merge-squash PR head
2029
2014
  tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
2030
2015
  os.environ["G2G_TMP_BRANCH"] = tmp_branch
2016
+
2017
+ log.debug(
2018
+ "Git merge preparation: base_sha=%s, head_sha=%s, tmp_branch=%s",
2019
+ base_sha,
2020
+ head_sha,
2021
+ tmp_branch,
2022
+ )
2023
+
2024
+ # Check if we have any commits to merge
2025
+ try:
2026
+ merge_base = run_cmd(
2027
+ ["git", "merge-base", base_sha, head_sha], cwd=self.workspace
2028
+ ).stdout.strip()
2029
+ log.debug("Merge base: %s", merge_base)
2030
+
2031
+ # Check if there are any commits between base and head
2032
+ commits_to_merge = run_cmd(
2033
+ ["git", "rev-list", f"{base_sha}..{head_sha}"],
2034
+ cwd=self.workspace,
2035
+ ).stdout.strip()
2036
+ if not commits_to_merge:
2037
+ log.warning(
2038
+ "No commits found between base (%s) and head (%s)",
2039
+ base_sha,
2040
+ head_sha,
2041
+ )
2042
+ else:
2043
+ commit_count = len(commits_to_merge.splitlines())
2044
+ log.debug("Found %d commits to merge", commit_count)
2045
+
2046
+ except Exception as debug_exc:
2047
+ log.warning("Failed to analyze merge situation: %s", debug_exc)
2048
+
2031
2049
  run_cmd(
2032
2050
  ["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
2033
2051
  )
2034
- run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
2052
+
2053
+ # Show git status before attempting merge
2054
+ try:
2055
+ status_output = run_cmd(
2056
+ ["git", "status", "--porcelain"], cwd=self.workspace
2057
+ ).stdout
2058
+ if status_output.strip():
2059
+ log.debug(
2060
+ "Git status before merge (modified files detected):\n%s",
2061
+ status_output,
2062
+ )
2063
+ else:
2064
+ log.debug("Git status before merge: working directory clean")
2065
+
2066
+ # Show current branch
2067
+ current_branch = run_cmd(
2068
+ ["git", "branch", "--show-current"], cwd=self.workspace
2069
+ ).stdout.strip()
2070
+ log.debug("Current branch before merge: %s", current_branch)
2071
+
2072
+ except Exception as status_exc:
2073
+ log.warning("Failed to get git status before merge: %s", status_exc)
2074
+
2075
+ log.debug("About to run: git merge --squash %s", head_sha)
2076
+ try:
2077
+ run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
2078
+ except CommandError as merge_exc:
2079
+ # Enhanced error handling for git merge failures
2080
+ error_details = self._analyze_merge_failure(
2081
+ merge_exc, base_sha, head_sha
2082
+ )
2083
+
2084
+ # Try to provide recovery suggestions
2085
+ recovery_msg = self._suggest_merge_recovery(
2086
+ merge_exc, base_sha, head_sha
2087
+ )
2088
+
2089
+ # Log detailed error information
2090
+ log.exception("Git merge --squash failed: %s", error_details)
2091
+ if recovery_msg:
2092
+ log.exception("Suggested recovery: %s", recovery_msg)
2093
+
2094
+ # Enhanced debugging if verbose mode is enabled
2095
+ from .utils import is_verbose_mode
2096
+
2097
+ if is_verbose_mode():
2098
+ self._debug_merge_failure_context(base_sha, head_sha)
2099
+
2100
+ # Re-raise with enhanced context
2101
+ raise OrchestratorError(
2102
+ f"Failed to merge PR commits: {error_details}"
2103
+ + (
2104
+ f"\nSuggested recovery: {recovery_msg}"
2105
+ if recovery_msg
2106
+ else ""
2107
+ )
2108
+ ) from merge_exc
2035
2109
 
2036
2110
  def _collect_log_lines() -> list[str]:
2037
2111
  body = run_cmd(
@@ -4260,6 +4334,242 @@ class Orchestrator:
4260
4334
  except Exception as exc:
4261
4335
  log.debug("File validation failed (non-critical): %s", exc)
4262
4336
 
4337
+ def _analyze_merge_failure(
4338
+ self, merge_exc: CommandError, base_sha: str, head_sha: str
4339
+ ) -> str:
4340
+ """Analyze git merge failure and provide detailed error information."""
4341
+ error_parts = []
4342
+
4343
+ # Include basic command info
4344
+ if merge_exc.cmd:
4345
+ error_parts.append(f"Command: {' '.join(merge_exc.cmd)}")
4346
+ if merge_exc.returncode is not None:
4347
+ error_parts.append(f"Exit code: {merge_exc.returncode}")
4348
+
4349
+ # Analyze stderr for common patterns
4350
+ stderr = merge_exc.stderr or ""
4351
+ if "conflict" in stderr.lower():
4352
+ error_parts.append("Merge conflicts detected")
4353
+ if "abort" in stderr.lower():
4354
+ error_parts.append("Merge was aborted")
4355
+ if "fatal" in stderr.lower():
4356
+ error_parts.append("Fatal git error occurred")
4357
+
4358
+ # Include actual git output
4359
+ if merge_exc.stdout and merge_exc.stdout.strip():
4360
+ error_parts.append(f"Git output: {merge_exc.stdout.strip()}")
4361
+ if stderr and stderr.strip():
4362
+ error_parts.append(f"Git error: {stderr.strip()}")
4363
+
4364
+ return (
4365
+ "; ".join(error_parts) if error_parts else "Unknown merge failure"
4366
+ )
4367
+
4368
+ def _suggest_merge_recovery(
4369
+ self, merge_exc: CommandError, base_sha: str, head_sha: str
4370
+ ) -> str:
4371
+ """Suggest recovery actions based on merge failure analysis."""
4372
+ stderr = (merge_exc.stderr or "").lower()
4373
+
4374
+ if (
4375
+ "committer identity unknown" in stderr
4376
+ or "empty ident name" in stderr
4377
+ ):
4378
+ return (
4379
+ "Git user identity not configured - this should be handled "
4380
+ "automatically by the tool. Please report this as a bug."
4381
+ )
4382
+ elif "conflict" in stderr:
4383
+ return "Check for merge conflicts in the PR files and resolve them"
4384
+ elif "fatal: refusing to merge unrelated histories" in stderr:
4385
+ return (
4386
+ "The branches have unrelated histories - check if the PR "
4387
+ "branch is based on the correct target"
4388
+ )
4389
+ elif "nothing to commit" in stderr:
4390
+ return (
4391
+ "No changes to merge - the PR may already be merged or have "
4392
+ "no differences"
4393
+ )
4394
+ elif "abort" in stderr:
4395
+ return (
4396
+ "Previous merge operation may have been interrupted - check "
4397
+ "repository state"
4398
+ )
4399
+
4400
+ # Try to provide generic guidance
4401
+ try:
4402
+ # Check if commits exist between base and head
4403
+ commits_cmd = ["git", "rev-list", f"{base_sha}..{head_sha}"]
4404
+ commits_result = run_cmd(
4405
+ commits_cmd, cwd=self.workspace, check=False
4406
+ )
4407
+ if (
4408
+ commits_result.returncode == 0
4409
+ and not commits_result.stdout.strip()
4410
+ ):
4411
+ return (
4412
+ "No commits found between base and head - PR may be empty "
4413
+ "or already merged"
4414
+ )
4415
+ except Exception as e:
4416
+ log.debug(
4417
+ "Failed to check commit range for recovery suggestion: %s", e
4418
+ )
4419
+
4420
+ return (
4421
+ "Review git repository state and ensure PR branch is properly "
4422
+ "synchronized with target"
4423
+ )
4424
+
4425
+ def _debug_merge_failure_context(
4426
+ self, base_sha: str, head_sha: str
4427
+ ) -> None:
4428
+ """Provide extensive debugging context for merge failures when verbose mode is enabled.""" # noqa: E501
4429
+ log.error("=== VERBOSE MODE: Extended merge failure analysis ===")
4430
+
4431
+ try:
4432
+ # Show detailed git log between base and head
4433
+ log_result = run_cmd(
4434
+ [
4435
+ "git",
4436
+ "log",
4437
+ "--oneline",
4438
+ "--graph",
4439
+ f"{base_sha}..{head_sha}",
4440
+ ],
4441
+ cwd=self.workspace,
4442
+ check=False,
4443
+ )
4444
+ if log_result.returncode == 0:
4445
+ log.error("Commits to be merged:\n%s", log_result.stdout)
4446
+ else:
4447
+ log.error("Failed to get commit log: %s", log_result.stderr)
4448
+
4449
+ # Show file differences
4450
+ diff_result = run_cmd(
4451
+ ["git", "diff", "--name-status", base_sha, head_sha],
4452
+ cwd=self.workspace,
4453
+ check=False,
4454
+ )
4455
+ if diff_result.returncode == 0:
4456
+ log.error(
4457
+ "Files changed between base and head:\n%s",
4458
+ diff_result.stdout,
4459
+ )
4460
+
4461
+ # Show merge-base information
4462
+ merge_base_result = run_cmd(
4463
+ ["git", "merge-base", "--is-ancestor", base_sha, head_sha],
4464
+ cwd=self.workspace,
4465
+ check=False,
4466
+ )
4467
+ if merge_base_result.returncode == 0:
4468
+ log.error(
4469
+ "Base SHA %s is an ancestor of head SHA %s",
4470
+ base_sha[:8],
4471
+ head_sha[:8],
4472
+ )
4473
+ else:
4474
+ log.error(
4475
+ "Base SHA %s is NOT an ancestor of head SHA %s",
4476
+ base_sha[:8],
4477
+ head_sha[:8],
4478
+ )
4479
+
4480
+ # Show current repository state
4481
+ status_result = run_cmd(
4482
+ ["git", "status", "--porcelain"],
4483
+ cwd=self.workspace,
4484
+ check=False,
4485
+ )
4486
+ if status_result.stdout.strip():
4487
+ log.error(
4488
+ "Repository has uncommitted changes:\n%s",
4489
+ status_result.stdout,
4490
+ )
4491
+
4492
+ # Show current branch and HEAD
4493
+ branch_result = run_cmd(
4494
+ ["git", "branch", "--show-current"],
4495
+ cwd=self.workspace,
4496
+ check=False,
4497
+ )
4498
+ if branch_result.returncode == 0:
4499
+ log.error("Current branch: %s", branch_result.stdout.strip())
4500
+
4501
+ head_result = run_cmd(
4502
+ ["git", "rev-parse", "HEAD"], cwd=self.workspace, check=False
4503
+ )
4504
+ if head_result.returncode == 0:
4505
+ log.error("Current HEAD: %s", head_result.stdout.strip())
4506
+
4507
+ except Exception:
4508
+ log.exception("Failed to gather debug context")
4509
+
4510
+ log.error("=== End verbose merge failure analysis ===")
4511
+
4512
+ def _ensure_git_user_identity(self, inputs: Inputs) -> None:
4513
+ """Ensure git user identity is configured for merge operations."""
4514
+ log.debug("Ensuring git user identity is configured")
4515
+
4516
+ # Check if user.name and user.email are already configured
4517
+ try:
4518
+ name_result = run_cmd(
4519
+ ["git", "config", "user.name"], cwd=self.workspace, check=False
4520
+ )
4521
+ email_result = run_cmd(
4522
+ ["git", "config", "user.email"], cwd=self.workspace, check=False
4523
+ )
4524
+
4525
+ if (
4526
+ name_result.returncode == 0
4527
+ and name_result.stdout.strip()
4528
+ and email_result.returncode == 0
4529
+ and email_result.stdout.strip()
4530
+ ):
4531
+ log.debug(
4532
+ "Git user identity already configured: %s <%s>",
4533
+ name_result.stdout.strip(),
4534
+ email_result.stdout.strip(),
4535
+ )
4536
+ return
4537
+
4538
+ except Exception as e:
4539
+ log.debug("Failed to check existing git identity: %s", e)
4540
+
4541
+ # Configure git identity using Gerrit credentials
4542
+ user_name = inputs.gerrit_ssh_user_g2g or "github2gerrit-bot"
4543
+ user_email = (
4544
+ inputs.gerrit_ssh_user_g2g_email or "github2gerrit@example.com"
4545
+ )
4546
+
4547
+ log.debug("Configuring git identity: %s <%s>", user_name, user_email)
4548
+
4549
+ try:
4550
+ # Set local repository identity
4551
+ run_cmd(
4552
+ ["git", "config", "user.name", user_name], cwd=self.workspace
4553
+ )
4554
+ run_cmd(
4555
+ ["git", "config", "user.email", user_email], cwd=self.workspace
4556
+ )
4557
+ log.debug("Successfully configured git user identity")
4558
+
4559
+ except CommandError as e:
4560
+ # Fallback to global config if local fails
4561
+ log.warning(
4562
+ "Failed to set local git identity, trying global: %s", e
4563
+ )
4564
+ try:
4565
+ run_cmd(["git", "config", "--global", "user.name", user_name])
4566
+ run_cmd(["git", "config", "--global", "user.email", user_email])
4567
+ log.debug("Successfully configured global git user identity")
4568
+ except CommandError as global_e:
4569
+ log.exception("Failed to configure git user identity")
4570
+ msg = "Cannot configure git user identity"
4571
+ raise OrchestratorError(msg) from global_e
4572
+
4263
4573
 
4264
4574
  # ---------------------
4265
4575
  # Utility functions
File without changes
File without changes
File without changes
File without changes
File without changes