limn-review 0.1.123
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.
- package/LICENSE +110 -0
- package/README.md +133 -0
- package/dist/ai/runtime.js +535 -0
- package/dist/api/plugins/auth.js +169 -0
- package/dist/api/plugins/error-handler.js +114 -0
- package/dist/api/plugins/rate-limit.js +855 -0
- package/dist/api/plugins/security.js +321 -0
- package/dist/api/routes/activity.js +103 -0
- package/dist/api/routes/ai-runtime.js +67 -0
- package/dist/api/routes/auth.js +194 -0
- package/dist/api/routes/billing.js +209 -0
- package/dist/api/routes/bot-triage.js +1289 -0
- package/dist/api/routes/conflicts.js +730 -0
- package/dist/api/routes/contact.js +318 -0
- package/dist/api/routes/daily-brief.js +58 -0
- package/dist/api/routes/flow.js +71 -0
- package/dist/api/routes/health.js +7 -0
- package/dist/api/routes/insights.js +200 -0
- package/dist/api/routes/me.js +458 -0
- package/dist/api/routes/mentions.js +22 -0
- package/dist/api/routes/mergers.js +8 -0
- package/dist/api/routes/ml-labels.js +109 -0
- package/dist/api/routes/open-prs.js +35 -0
- package/dist/api/routes/prs.js +1633 -0
- package/dist/api/routes/reactions.js +217 -0
- package/dist/api/routes/repos.js +563 -0
- package/dist/api/routes/search.js +41 -0
- package/dist/api/routes/threads.js +118 -0
- package/dist/api/routes/timeline.js +150 -0
- package/dist/api/routes/users.js +67 -0
- package/dist/api/routes/webhooks.js +181 -0
- package/dist/api/routes/workspaces.js +322 -0
- package/dist/app.js +274 -0
- package/dist/ascii.js +19 -0
- package/dist/auth/account.js +482 -0
- package/dist/auth/crypto.js +46 -0
- package/dist/bot-triage/resolve.js +59 -0
- package/dist/cli.js +388 -0
- package/dist/coding/activity.js +80 -0
- package/dist/coding/agent.js +288 -0
- package/dist/coding/ai-fix/auto-fix.js +212 -0
- package/dist/coding/ai-fix/manager.js +370 -0
- package/dist/coding/ai-fix/persist.js +213 -0
- package/dist/coding/ai-fix/pr-context.js +58 -0
- package/dist/coding/ai-fix/prompts.js +138 -0
- package/dist/coding/ai-fix/review-seed.js +357 -0
- package/dist/coding/ai-fix/routes.js +425 -0
- package/dist/coding/git-ops.js +321 -0
- package/dist/coding/git.js +184 -0
- package/dist/coding/merge.js +262 -0
- package/dist/coding/safe-path.js +75 -0
- package/dist/coding/schema.js +43 -0
- package/dist/config.js +648 -0
- package/dist/conflict/diff.js +304 -0
- package/dist/conflict/git.js +118 -0
- package/dist/conflict/hash.js +73 -0
- package/dist/conflict/janitor.js +218 -0
- package/dist/conflict/land.js +675 -0
- package/dist/conflict/model-types.js +2 -0
- package/dist/conflict/model.js +1081 -0
- package/dist/conflict/parse.js +142 -0
- package/dist/conflict/seam.js +136 -0
- package/dist/conflict/session.js +532 -0
- package/dist/conflict/suggestion.js +398 -0
- package/dist/conflict/tokens.js +72 -0
- package/dist/data-dir.js +112 -0
- package/dist/db/actor-lanes.js +213 -0
- package/dist/db/automation-ids.js +97 -0
- package/dist/db/automation-output.js +349 -0
- package/dist/db/blast-radius-query.js +36 -0
- package/dist/db/blast-radius.js +226 -0
- package/dist/db/bot-dormancy.js +115 -0
- package/dist/db/bot-overlap.js +579 -0
- package/dist/db/bot-volume.js +722 -0
- package/dist/db/bot-window.js +35 -0
- package/dist/db/branch-queries.js +310 -0
- package/dist/db/change-shape.js +243 -0
- package/dist/db/changepoint.js +75 -0
- package/dist/db/cleanup.js +36 -0
- package/dist/db/client.js +102 -0
- package/dist/db/code-loc.js +388 -0
- package/dist/db/credits.js +79 -0
- package/dist/db/daily-brief.js +406 -0
- package/dist/db/dependency-cards.js +145 -0
- package/dist/db/erase-account.js +255 -0
- package/dist/db/export-account.js +189 -0
- package/dist/db/failing-checks.js +124 -0
- package/dist/db/file-coupling.js +197 -0
- package/dist/db/flow-detail.js +661 -0
- package/dist/db/flow-pointers.js +197 -0
- package/dist/db/flow-settings.js +67 -0
- package/dist/db/forecast.js +114 -0
- package/dist/db/line-overlap.js +81 -0
- package/dist/db/merge-queue.js +189 -0
- package/dist/db/migrate.js +17 -0
- package/dist/db/migrations/0000_purple_ben_grimm.sql +155 -0
- package/dist/db/migrations/0001_colorful_ozymandias.sql +31 -0
- package/dist/db/migrations/0002_famous_sersi.sql +9 -0
- package/dist/db/migrations/0003_clever_shinobi_shaw.sql +3 -0
- package/dist/db/migrations/0004_pale_scalphunter.sql +1 -0
- package/dist/db/migrations/0005_daffy_guardian.sql +2 -0
- package/dist/db/migrations/0006_cute_violations.sql +45 -0
- package/dist/db/migrations/0007_dark_anthem.sql +2 -0
- package/dist/db/migrations/0008_multitenant_accounts.sql +44 -0
- package/dist/db/migrations/0009_lean_bodies.sql +13 -0
- package/dist/db/migrations/0010_lean_bodies_nullable.sql +56 -0
- package/dist/db/migrations/0011_pr_diff_size.sql +10 -0
- package/dist/db/migrations/0012_findings_included_default.sql +7 -0
- package/dist/db/migrations/0013_review_routing.sql +9 -0
- package/dist/db/migrations/0014_account_last_active.sql +6 -0
- package/dist/db/migrations/0015_repos_inbox_watch.sql +9 -0
- package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
- package/dist/db/migrations/0017_account_display_name.sql +7 -0
- package/dist/db/migrations/0018_finding_comment_kind.sql +7 -0
- package/dist/db/migrations/0019_finding_file_in_diff.sql +7 -0
- package/dist/db/migrations/0020_claude_review_cost_telemetry.sql +11 -0
- package/dist/db/migrations/0021_account_feed_last_seen.sql +7 -0
- package/dist/db/migrations/0022_ci_status_events.sql +21 -0
- package/dist/db/migrations/0023_ai_usage.sql +21 -0
- package/dist/db/migrations/0024_pr_head_ref_name.sql +6 -0
- package/dist/db/migrations/0025_account_billing.sql +7 -0
- package/dist/db/migrations/0026_ai_credit_allowance.sql +5 -0
- package/dist/db/migrations/0027_user_github_type.sql +6 -0
- package/dist/db/migrations/0028_bot_review_classification.sql +21 -0
- package/dist/db/migrations/0029_bot_mute_rules.sql +19 -0
- package/dist/db/migrations/0030_teams.sql +32 -0
- package/dist/db/migrations/0031_events_pr_idx.sql +7 -0
- package/dist/db/migrations/0032_benchmark_contributions.sql +29 -0
- package/dist/db/migrations/0033_thread_addressed_confidence.sql +9 -0
- package/dist/db/migrations/0034_thread_resolved_at.sql +6 -0
- package/dist/db/migrations/0035_pr_first_review_requested_at.sql +5 -0
- package/dist/db/migrations/0036_search_index.sql +25 -0
- package/dist/db/migrations/0037_author_indexes.sql +15 -0
- package/dist/db/migrations/0038_auto_merge_requests.sql +37 -0
- package/dist/db/migrations/0039_branch_status.sql +44 -0
- package/dist/db/migrations/0040_pr_review_decision.sql +10 -0
- package/dist/db/migrations/0041_branch_commit_detail.sql +38 -0
- package/dist/db/migrations/0042_repo_reviewers.sql +176 -0
- package/dist/db/migrations/0043_account_reviewers.sql +131 -0
- package/dist/db/migrations/0044_workspaces.sql +138 -0
- package/dist/db/migrations/0045_workspace_reviewers.sql +180 -0
- package/dist/db/migrations/0046_drop_inbox_watch.sql +25 -0
- package/dist/db/migrations/0047_ml_comment_labels.sql +40 -0
- package/dist/db/migrations/0048_ml_vendor_severity.sql +31 -0
- package/dist/db/migrations/0049_seat_pricing_repo_description.sql +21 -0
- package/dist/db/migrations/0050_users_app_slug.sql +12 -0
- package/dist/db/migrations/0051_auto_merge_queue.sql +17 -0
- package/dist/db/migrations/0052_trunk_ci_status_events.sql +43 -0
- package/dist/db/migrations/0053_automation_roles.sql +79 -0
- package/dist/db/migrations/0054_automation_vendor_kinds.sql +514 -0
- package/dist/db/migrations/0055_auto_merge_phase.sql +12 -0
- package/dist/db/migrations/0056_pr_mentions.sql +22 -0
- package/dist/db/migrations/0057_large_pr_threshold.sql +14 -0
- package/dist/db/migrations/0058_pending_mute.sql +53 -0
- package/dist/db/migrations/0059_merge_queue_state.sql +33 -0
- package/dist/db/migrations/0060_drop_my_turn_dismissals.sql +33 -0
- package/dist/db/migrations/0061_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations/0062_blast_radius_config.sql +21 -0
- package/dist/db/migrations/0063_repo_file_coupling.sql +33 -0
- package/dist/db/migrations/0064_pr_content_kind.sql +21 -0
- package/dist/db/migrations/0065_workspace_flow_settings.sql +13 -0
- package/dist/db/migrations/0066_review_request_events.sql +31 -0
- package/dist/db/migrations/0067_pr_security_signals.sql +86 -0
- package/dist/db/migrations/0068_my_turn_settings.sql +18 -0
- package/dist/db/migrations/0069_my_turn_dismissals.sql +41 -0
- package/dist/db/migrations/0070_claude_review_follow_up_ticket.sql +22 -0
- package/dist/db/migrations/0071_claude_review_trigger.sql +11 -0
- package/dist/db/migrations/0072_commit_message_headline.sql +13 -0
- package/dist/db/migrations/0073_claude_review_chat.sql +31 -0
- package/dist/db/migrations/0074_adopt_agentic_tables.sql +98 -0
- package/dist/db/migrations/0075_review_lens_drop_learnings.sql +15 -0
- package/dist/db/migrations/0076_claude_review_threads.sql +17 -0
- package/dist/db/migrations/0077_claude_review_ci_failures.sql +10 -0
- package/dist/db/migrations/0078_ai_fix_change_report.sql +17 -0
- package/dist/db/migrations/0079_claude_finding_story.sql +16 -0
- package/dist/db/migrations/meta/0000_snapshot.json +1116 -0
- package/dist/db/migrations/meta/0001_snapshot.json +1321 -0
- package/dist/db/migrations/meta/0002_snapshot.json +1375 -0
- package/dist/db/migrations/meta/0003_snapshot.json +1396 -0
- package/dist/db/migrations/meta/0004_snapshot.json +1416 -0
- package/dist/db/migrations/meta/0005_snapshot.json +1430 -0
- package/dist/db/migrations/meta/0006_snapshot.json +1745 -0
- package/dist/db/migrations/meta/0007_snapshot.json +1759 -0
- package/dist/db/migrations/meta/_journal.json +566 -0
- package/dist/db/migrations-pg/0000_tired_alex_power.sql +268 -0
- package/dist/db/migrations-pg/0001_dizzy_mockingbird.sql +3 -0
- package/dist/db/migrations-pg/0002_glorious_ikaris.sql +4 -0
- package/dist/db/migrations-pg/0003_melted_secret_warriors.sql +2 -0
- package/dist/db/migrations-pg/0004_numerous_triton.sql +1 -0
- package/dist/db/migrations-pg/0005_thick_blockbuster.sql +2 -0
- package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
- package/dist/db/migrations-pg/0007_same_human_torch.sql +2 -0
- package/dist/db/migrations-pg/0008_slimy_toxin.sql +1 -0
- package/dist/db/migrations-pg/0009_fuzzy_jubilee.sql +4 -0
- package/dist/db/migrations-pg/0010_late_hammerhead.sql +1 -0
- package/dist/db/migrations-pg/0011_happy_leech.sql +16 -0
- package/dist/db/migrations-pg/0012_woozy_captain_midlands.sql +16 -0
- package/dist/db/migrations-pg/0013_yielding_nicolaos.sql +1 -0
- package/dist/db/migrations-pg/0014_wakeful_bill_hollister.sql +2 -0
- package/dist/db/migrations-pg/0015_aspiring_black_tom.sql +1 -0
- package/dist/db/migrations-pg/0016_cute_ghost_rider.sql +30 -0
- package/dist/db/migrations-pg/0017_far_changeling.sql +24 -0
- package/dist/db/migrations-pg/0018_fancy_miss_america.sql +1 -0
- package/dist/db/migrations-pg/0019_benchmark_contributions.sql +22 -0
- package/dist/db/migrations-pg/0020_majestic_edwin_jarvis.sql +3 -0
- package/dist/db/migrations-pg/0021_green_salo.sql +1 -0
- package/dist/db/migrations-pg/0022_wise_baron_strucker.sql +1 -0
- package/dist/db/migrations-pg/0023_search_index.sql +19 -0
- package/dist/db/migrations-pg/0024_author_indexes.sql +9 -0
- package/dist/db/migrations-pg/0025_auto_merge_requests.sql +26 -0
- package/dist/db/migrations-pg/0026_branch_status.sql +32 -0
- package/dist/db/migrations-pg/0027_pr_review_decision.sql +5 -0
- package/dist/db/migrations-pg/0028_branch_commit_detail.sql +9 -0
- package/dist/db/migrations-pg/0029_repo_reviewers.sql +127 -0
- package/dist/db/migrations-pg/0030_account_reviewers.sql +77 -0
- package/dist/db/migrations-pg/0031_workspaces.sql +131 -0
- package/dist/db/migrations-pg/0032_workspace_reviewers.sql +165 -0
- package/dist/db/migrations-pg/0033_drop_inbox_watch.sql +14 -0
- package/dist/db/migrations-pg/0034_ml_comment_labels.sql +33 -0
- package/dist/db/migrations-pg/0035_ml_vendor_severity.sql +16 -0
- package/dist/db/migrations-pg/0036_seat_pricing_repo_description.sql +12 -0
- package/dist/db/migrations-pg/0037_users_app_slug.sql +9 -0
- package/dist/db/migrations-pg/0038_auto_merge_queue.sql +10 -0
- package/dist/db/migrations-pg/0039_trunk_ci_status_events.sql +27 -0
- package/dist/db/migrations-pg/0040_automation_roles.sql +63 -0
- package/dist/db/migrations-pg/0041_automation_vendor_kinds.sql +488 -0
- package/dist/db/migrations-pg/0042_auto_merge_phase.sql +8 -0
- package/dist/db/migrations-pg/0043_pr_mentions.sql +19 -0
- package/dist/db/migrations-pg/0044_large_pr_threshold.sql +6 -0
- package/dist/db/migrations-pg/0045_pending_mute.sql +26 -0
- package/dist/db/migrations-pg/0046_merge_queue_state.sql +18 -0
- package/dist/db/migrations-pg/0047_drop_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0048_auto_merge_durable_anchors.sql +27 -0
- package/dist/db/migrations-pg/0049_blast_radius_config.sql +8 -0
- package/dist/db/migrations-pg/0050_repo_file_coupling.sql +13 -0
- package/dist/db/migrations-pg/0051_pr_content_kind.sql +6 -0
- package/dist/db/migrations-pg/0052_workspace_flow_settings.sql +4 -0
- package/dist/db/migrations-pg/0053_review_request_events.sql +18 -0
- package/dist/db/migrations-pg/0054_pr_security_signals.sql +59 -0
- package/dist/db/migrations-pg/0055_my_turn_settings.sql +6 -0
- package/dist/db/migrations-pg/0056_my_turn_dismissals.sql +21 -0
- package/dist/db/migrations-pg/0057_claude_review_follow_up_ticket.sql +8 -0
- package/dist/db/migrations-pg/0058_claude_review_trigger.sql +4 -0
- package/dist/db/migrations-pg/0059_commit_message_headline.sql +3 -0
- package/dist/db/migrations-pg/0060_claude_review_chat.sql +24 -0
- package/dist/db/migrations-pg/0061_adopt_agentic_tables.sql +79 -0
- package/dist/db/migrations-pg/0062_review_lens_drop_learnings.sql +5 -0
- package/dist/db/migrations-pg/0063_claude_review_threads.sql +7 -0
- package/dist/db/migrations-pg/0064_claude_review_ci_failures.sql +4 -0
- package/dist/db/migrations-pg/0065_ai_fix_change_report.sql +9 -0
- package/dist/db/migrations-pg/0066_claude_finding_story.sql +7 -0
- package/dist/db/migrations-pg/meta/0000_snapshot.json +2137 -0
- package/dist/db/migrations-pg/meta/0001_snapshot.json +2143 -0
- package/dist/db/migrations-pg/meta/0002_snapshot.json +2170 -0
- package/dist/db/migrations-pg/meta/0003_snapshot.json +2182 -0
- package/dist/db/migrations-pg/meta/0004_snapshot.json +2188 -0
- package/dist/db/migrations-pg/meta/0005_snapshot.json +2201 -0
- package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
- package/dist/db/migrations-pg/meta/0007_snapshot.json +2219 -0
- package/dist/db/migrations-pg/meta/0008_snapshot.json +2226 -0
- package/dist/db/migrations-pg/meta/0009_snapshot.json +2250 -0
- package/dist/db/migrations-pg/meta/0010_snapshot.json +2256 -0
- package/dist/db/migrations-pg/meta/0011_snapshot.json +2412 -0
- package/dist/db/migrations-pg/meta/0012_snapshot.json +2528 -0
- package/dist/db/migrations-pg/meta/0013_snapshot.json +2534 -0
- package/dist/db/migrations-pg/meta/0014_snapshot.json +2547 -0
- package/dist/db/migrations-pg/meta/0015_snapshot.json +2553 -0
- package/dist/db/migrations-pg/meta/0016_snapshot.json +2774 -0
- package/dist/db/migrations-pg/meta/0017_snapshot.json +2999 -0
- package/dist/db/migrations-pg/meta/0018_snapshot.json +3020 -0
- package/dist/db/migrations-pg/meta/0020_snapshot.json +3228 -0
- package/dist/db/migrations-pg/meta/0021_snapshot.json +3234 -0
- package/dist/db/migrations-pg/meta/0022_snapshot.json +3240 -0
- package/dist/db/migrations-pg/meta/_journal.json +475 -0
- package/dist/db/ml-labels.js +1666 -0
- package/dist/db/my-turn-dismissals.js +343 -0
- package/dist/db/my-turn-settings.js +34 -0
- package/dist/db/pending-classify.js +46 -0
- package/dist/db/pending-mute.js +171 -0
- package/dist/db/pending-tabs.js +238 -0
- package/dist/db/period-metrics.js +820 -0
- package/dist/db/person-period.js +763 -0
- package/dist/db/pr-intervals.js +930 -0
- package/dist/db/pr-liveness.js +240 -0
- package/dist/db/pr-mentions.js +279 -0
- package/dist/db/pr-merge-queue-stamp.js +91 -0
- package/dist/db/queries.js +14631 -0
- package/dist/db/reaction-targets.js +94 -0
- package/dist/db/repo-activity.js +171 -0
- package/dist/db/retention.js +138 -0
- package/dist/db/review-threads-for-review.js +240 -0
- package/dist/db/run-migrations.js +24 -0
- package/dist/db/schema.pg.js +1220 -0
- package/dist/db/schema.sqlite.js +1801 -0
- package/dist/db/search.js +317 -0
- package/dist/db/security-alerts.js +218 -0
- package/dist/db/synthesis-input.js +565 -0
- package/dist/db/triage.js +320 -0
- package/dist/db/usage.js +63 -0
- package/dist/db/work-plan.js +1086 -0
- package/dist/db/working-hours.js +161 -0
- package/dist/feed/my-turn.js +122 -0
- package/dist/github/actions-logs.js +397 -0
- package/dist/github/auth.js +86 -0
- package/dist/github/branch-queries.js +229 -0
- package/dist/github/client.js +454 -0
- package/dist/github/codeowners.js +221 -0
- package/dist/github/commit-checks.js +105 -0
- package/dist/github/compare.js +110 -0
- package/dist/github/diff-anchor.js +227 -0
- package/dist/github/mutations.js +706 -0
- package/dist/github/pr-file-content.js +254 -0
- package/dist/github/pr-liveness.js +161 -0
- package/dist/github/queries.js +875 -0
- package/dist/github/rate-budget.js +129 -0
- package/dist/github/reactions.js +161 -0
- package/dist/github/reviewer-suggest.js +104 -0
- package/dist/github/team-reviewers.js +96 -0
- package/dist/index.js +184 -0
- package/dist/merge/auto-merge-runner.js +960 -0
- package/dist/ml/severity-client.js +218 -0
- package/dist/pr/detail-enricher.js +26 -0
- package/dist/pro/bind.js +302 -0
- package/dist/pro/contract.js +37 -0
- package/dist/pro/migrate.js +64 -0
- package/dist/review/agent-context.js +127 -0
- package/dist/review/agent.js +385 -0
- package/dist/review/agentic.js +59 -0
- package/dist/review/auth.js +94 -0
- package/dist/review/chat-agent.js +179 -0
- package/dist/review/claude-review/ai-ready.js +16 -0
- package/dist/review/claude-review/auto-settings.js +139 -0
- package/dist/review/claude-review/auto.js +244 -0
- package/dist/review/claude-review/chat.js +489 -0
- package/dist/review/claude-review/ci-failures.js +350 -0
- package/dist/review/claude-review/follow-up.js +310 -0
- package/dist/review/claude-review/manager.js +686 -0
- package/dist/review/claude-review/persist.js +996 -0
- package/dist/review/claude-review/prompts.js +357 -0
- package/dist/review/claude-review/routes.js +518 -0
- package/dist/review/claude-review/routing.js +61 -0
- package/dist/review/claude-review/specialists.js +190 -0
- package/dist/review/claude-review/threads.js +265 -0
- package/dist/review/claude-review/ticket.js +282 -0
- package/dist/review/clone-hygiene.js +380 -0
- package/dist/review/clone-manager.js +367 -0
- package/dist/review/llm.js +113 -0
- package/dist/review/model-options.js +48 -0
- package/dist/review/plugin-providers.js +12 -0
- package/dist/review/post-review.js +216 -0
- package/dist/review/post-seam.js +161 -0
- package/dist/review/prepare.js +168 -0
- package/dist/review/pricing.js +17 -0
- package/dist/review/schema.js +156 -0
- package/dist/review/submit-map.js +41 -0
- package/dist/review/usage.js +90 -0
- package/dist/shared/claude-review.js +335 -0
- package/dist/shared/conflict-fold.js +151 -0
- package/dist/shared/conflicts.js +30 -0
- package/dist/shared/flow-settings.js +191 -0
- package/dist/shared/index.js +8 -0
- package/dist/shared/my-turn-settings.js +238 -0
- package/dist/shared/pending-rules.js +281 -0
- package/dist/shared/product.js +54 -0
- package/dist/shared/types.js +896 -0
- package/dist/status.js +386 -0
- package/dist/sync/adaptive.js +185 -0
- package/dist/sync/app-attribution.js +66 -0
- package/dist/sync/auth-notices.js +36 -0
- package/dist/sync/backfill-ci-history.js +315 -0
- package/dist/sync/backfill-commit-headlines.js +136 -0
- package/dist/sync/backfill-pr-security.js +175 -0
- package/dist/sync/backfill-review-requests.js +97 -0
- package/dist/sync/benchmark-rollup.js +50 -0
- package/dist/sync/bot-detection.js +309 -0
- package/dist/sync/bot-resolution-markers.js +30 -0
- package/dist/sync/branch-status.js +634 -0
- package/dist/sync/classify-change-shape.js +150 -0
- package/dist/sync/commit-files.js +74 -0
- package/dist/sync/derive-thread-state.js +99 -0
- package/dist/sync/drain-review-threads.js +212 -0
- package/dist/sync/hydrate-detail.js +482 -0
- package/dist/sync/mention-scan.js +89 -0
- package/dist/sync/ml-enrichment.js +478 -0
- package/dist/sync/pr-change-signal.js +96 -0
- package/dist/sync/pr-liveness-sweep.js +139 -0
- package/dist/sync/pr-settle.js +381 -0
- package/dist/sync/refresh-pr.js +163 -0
- package/dist/sync/reparse-vendor-badges.js +495 -0
- package/dist/sync/resync-after-write.js +280 -0
- package/dist/sync/review-fingerprint.js +117 -0
- package/dist/sync/reviewer-behavior.js +103 -0
- package/dist/sync/reviewer-classify.js +503 -0
- package/dist/sync/routing-files.js +97 -0
- package/dist/sync/scheduled-jobs.js +12 -0
- package/dist/sync/scheduler.js +159 -0
- package/dist/sync/security-detect.js +521 -0
- package/dist/sync/sync-manager.js +750 -0
- package/dist/sync/sync-one-pr.js +205 -0
- package/dist/sync/sync-repo.js +400 -0
- package/dist/sync/unsettled-prs.js +506 -0
- package/dist/sync/upsert.js +1190 -0
- package/package.json +56 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/AiFixTab-CQP6PT5u.js +2 -0
- package/public/assets/ClaudeReviewTab-DQJ-58ZA.js +8 -0
- package/public/assets/PrBotBehaviourTab-CCnFrWbC.js +1 -0
- package/public/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public/assets/highlight-BEHUn5zE.css +10 -0
- package/public/assets/highlight-CIIZLa6Z.js +12 -0
- package/public/assets/index-BDnYs6zg.js +73 -0
- package/public/assets/index-mCuI--RC.css +1 -0
- package/public/assets/markdown-I_JLLVAZ.js +32 -0
- package/public/assets/perfProbe-Da13EhTE.js +1 -0
- package/public/assets/tanstack-5qsxxaLe.js +17 -0
- package/public/assets/vis-C3BCkP5U.js +1280 -0
- package/public/assets/vis-ziCVehMy.css +1 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon-48.png +0 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +41 -0
- package/public-landing/apple-touch-icon.png +0 -0
- package/public-landing/assets/archivo-latin-500-BWmvx2Mg.woff2 +0 -0
- package/public-landing/assets/archivo-latin-600-BN-JkNrl.woff2 +0 -0
- package/public-landing/assets/index-CA3eEGXO.js +40 -0
- package/public-landing/assets/index-naQrQ6Fe.css +1 -0
- package/public-landing/assets/jetbrains-mono-latin-400-RruCX7RW.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-DExrRRrN.woff2 +0 -0
- package/public-landing/assets/newsreader-latin-400-italic-b0I-AwbV.woff2 +0 -0
- package/public-landing/bots/index.html +156 -0
- package/public-landing/contact/index.html +156 -0
- package/public-landing/cookies/index.html +156 -0
- package/public-landing/demo/limn-walkthrough-poster.jpg +0 -0
- package/public-landing/demo/limn-walkthrough.cues.json +44 -0
- package/public-landing/demo/limn-walkthrough.mp4 +0 -0
- package/public-landing/favicon-16.png +0 -0
- package/public-landing/favicon-32.png +0 -0
- package/public-landing/favicon-48.png +0 -0
- package/public-landing/favicon.svg +5 -0
- package/public-landing/features/index.html +156 -0
- package/public-landing/for-developers/index.html +156 -0
- package/public-landing/for-managers/index.html +156 -0
- package/public-landing/how-it-works/index.html +156 -0
- package/public-landing/how-we-measure/index.html +156 -0
- package/public-landing/icon-192.png +0 -0
- package/public-landing/icon-512.png +0 -0
- package/public-landing/index.html +156 -0
- package/public-landing/insights/index.html +156 -0
- package/public-landing/og-image.png +0 -0
- package/public-landing/pricing/index.html +156 -0
- package/public-landing/privacy/index.html +156 -0
- package/public-landing/pro/index.html +156 -0
- package/public-landing/reviews/index.html +156 -0
- package/public-landing/robots.txt +8 -0
- package/public-landing/shots/benchmark.png +0 -0
- package/public-landing/shots/bot-roi.png +0 -0
- package/public-landing/shots/bot-settings.png +0 -0
- package/public-landing/shots/chronology.png +0 -0
- package/public-landing/shots/feed.png +0 -0
- package/public-landing/shots/flow-metrics.png +0 -0
- package/public-landing/shots/free-reports.png +0 -0
- package/public-landing/shots/pending-board.png +0 -0
- package/public-landing/shots/pending-card.png +0 -0
- package/public-landing/shots/period-report.png +0 -0
- package/public-landing/shots/pr-detail.png +0 -0
- package/public-landing/shots/pr-threads.png +0 -0
- package/public-landing/shots/reach.png +0 -0
- package/public-landing/shots/repo-rows.png +0 -0
- package/public-landing/shots/severity-findings.png +0 -0
- package/public-landing/shots/severity-strip.png +0 -0
- package/public-landing/site.webmanifest +14 -0
- package/public-landing/sitemap.xml +51 -0
- package/public-landing/terms/index.html +156 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { foldFile, foldToText, } from '../shared/index.js';
|
|
5
|
+
import { getAccessToken, getAccountById } from '../auth/account.js';
|
|
6
|
+
import { pushForceWithLease, pushRef } from '../coding/git.js';
|
|
7
|
+
import { protectedRefsFor } from '../coding/git-ops.js';
|
|
8
|
+
import { disarmAutoMerge, getPrWriteContext, WRITE_PERMISSIONS } from '../db/queries.js';
|
|
9
|
+
import { ghRestGetText } from '../github/client.js';
|
|
10
|
+
import { createPullRequest, fetchPrHeadInfo } from '../github/mutations.js';
|
|
11
|
+
import { cleanupCloneCache, ensureClone, withRepoLock } from '../review/clone-manager.js';
|
|
12
|
+
import { asSyncLogger, settlePrAfterWrite, syncNewPrBesideAfterWrite, } from '../sync/resync-after-write.js';
|
|
13
|
+
import { git, gitTry } from './git.js';
|
|
14
|
+
import { conflictModelHash } from './hash.js';
|
|
15
|
+
import { allowedDecisions, buildConflictModel } from './model.js';
|
|
16
|
+
/** The land path's own coded error. `coding/git.ts`'s `codedError` carries `CodingErrorCode`,
|
|
17
|
+
* a different vocabulary; this one is the wire's `ConflictLandErrorCode` so the route can
|
|
18
|
+
* pass the code straight through to `ConflictCommitState.error`. */
|
|
19
|
+
export function landError(code, message) {
|
|
20
|
+
const err = new Error(message);
|
|
21
|
+
err.code = code;
|
|
22
|
+
return err;
|
|
23
|
+
}
|
|
24
|
+
export async function landConflictResolution(args) {
|
|
25
|
+
const { model, body, signal } = args;
|
|
26
|
+
const repoKey = `${model.owner}/${model.name}`;
|
|
27
|
+
const ns = `refs/pierre/conflict/${args.sessionId}`;
|
|
28
|
+
args.onPhase('preparing');
|
|
29
|
+
// ---- the free pins, before any I/O ----
|
|
30
|
+
// A decision made against different bytes is not a decision, so the session's own model
|
|
31
|
+
// must be the one the client folded. This is cheap and catches a client bug without
|
|
32
|
+
// spending a clone, a fetch and a merge on it.
|
|
33
|
+
if (body.expectedHeadSha !== model.headSha || body.expectedBaseSha !== model.baseSha) {
|
|
34
|
+
throw landError('ModelStale', 'This session was built against different commits.');
|
|
35
|
+
}
|
|
36
|
+
if (conflictModelHash(model) !== body.modelHash) {
|
|
37
|
+
throw landError('ModelStale', 'This merge changed while it was open. Reopen it.');
|
|
38
|
+
}
|
|
39
|
+
if (model.files.length === 0) {
|
|
40
|
+
throw landError('NoConflicts', 'This merge has no conflicts.');
|
|
41
|
+
}
|
|
42
|
+
if (body.strategy === 'rebase' && !model.strategies.includes('rebase')) {
|
|
43
|
+
throw landError('RebaseNotOffered', model.rebaseUnavailableReason ?? 'Rebase is not available for this pull request.');
|
|
44
|
+
}
|
|
45
|
+
// ---- the model is the allow-list, and the fold is the same one the pane rendered ----
|
|
46
|
+
// Pure CPU over strings already in memory: no side effect can survive a refusal here, which
|
|
47
|
+
// is why it runs before the rebuild rather than after it. `IncompleteDecisions` is worth
|
|
48
|
+
// getting for free.
|
|
49
|
+
const plan = planResolution(model, body.files, args.suggestions, args.edits);
|
|
50
|
+
// ⚠ REBASE IS ALL-OR-NOTHING. The model only OFFERS rebase when every conflicted file is
|
|
51
|
+
// resolvable, but the request can still leave one out — and a rebased commit that reparents
|
|
52
|
+
// onto the base while a conflicted file keeps head's blob has silently dropped the base
|
|
53
|
+
// branch's version of it, with no marker and no second chance to notice.
|
|
54
|
+
if (body.strategy === 'rebase' && !plan.full) {
|
|
55
|
+
throw landError('RebaseNotOffered', 'Rebase needs every conflicted file resolved here.');
|
|
56
|
+
}
|
|
57
|
+
if (body.target.kind === 'new_branch') {
|
|
58
|
+
await assertNewBranchName(model, body.target.branch);
|
|
59
|
+
}
|
|
60
|
+
assertNotCancelled(signal);
|
|
61
|
+
let tmpDir = null;
|
|
62
|
+
let cloneDir = null;
|
|
63
|
+
try {
|
|
64
|
+
// ⚠ Everything ABOVE this line is pure — no token, no clone, no ref. A refusal up there
|
|
65
|
+
// acquired nothing, and the session's own fetch refs are the SESSION's to drop; the
|
|
66
|
+
// teardown below can only reach them once we hold a clone to delete them in.
|
|
67
|
+
const token = await getAccessToken(args.accountId);
|
|
68
|
+
// The clone is resolved BEFORE the rebuild so that teardown can always reach the session
|
|
69
|
+
// refs — including on the bails (`HeadMoved`, `ModelStale`) where the rebuild is the thing
|
|
70
|
+
// that created them. `withRepoLock` is a queue, not a reentrant lock, so this acquisition
|
|
71
|
+
// must complete before `buildConflictModel` takes its own.
|
|
72
|
+
cloneDir = await withRepoLock(repoKey, () => ensureClone(model.owner, model.name, token));
|
|
73
|
+
// ---- re-derive through the ONE builder ----
|
|
74
|
+
// Not a second derivation path: the same function, so `conflictModelHash` keeps exactly
|
|
75
|
+
// one producer. Only the HEAD is pinned — the base branch is allowed to move under us,
|
|
76
|
+
// and rule 3 below decides whether that matters.
|
|
77
|
+
args.onPhase('fetching');
|
|
78
|
+
const rebuilt = await buildConflictModel({
|
|
79
|
+
accountId: args.accountId,
|
|
80
|
+
prId: args.prId,
|
|
81
|
+
expectHeadSha: body.expectedHeadSha,
|
|
82
|
+
sessionId: args.sessionId,
|
|
83
|
+
onPhase: (p) => args.onPhase(p === 'cloning' || p === 'fetching' ? 'fetching' : 'merging'),
|
|
84
|
+
});
|
|
85
|
+
if (!rebuilt) {
|
|
86
|
+
throw landError('NotPermitted', 'This pull request is no longer available.');
|
|
87
|
+
}
|
|
88
|
+
if (rebuilt.status === 'moved') {
|
|
89
|
+
throw landError('HeadMoved', `The pull request moved to ${rebuilt.headSha.slice(0, 7)} while this was open.`);
|
|
90
|
+
}
|
|
91
|
+
if (rebuilt.status === 'clean') {
|
|
92
|
+
throw landError('NoConflicts', 'This merge no longer conflicts.');
|
|
93
|
+
}
|
|
94
|
+
if (rebuilt.status === 'failed') {
|
|
95
|
+
throw landError('GitFailed', rebuilt.message);
|
|
96
|
+
}
|
|
97
|
+
const fresh = rebuilt.model;
|
|
98
|
+
assertNotCancelled(signal);
|
|
99
|
+
// Rule 3: THE BASE MOVED. Reported, never silent, and never a reason to refuse on its own
|
|
100
|
+
// — every contested region the user decided is byte-identical, and the merge picking up
|
|
101
|
+
// the base branch's other commits is what merging the base MEANS.
|
|
102
|
+
//
|
|
103
|
+
// ⚠ The comparison is the CONFLICT CONTENT, not the model hash. `conflictModelHash` folds
|
|
104
|
+
// in `baseSha` and the merged tree oid, both of which move on any base commit at all —
|
|
105
|
+
// so testing the hash here would make this branch unreachable and turn every base advance
|
|
106
|
+
// into `ModelStale`. What has to be unchanged is the material the decisions were made
|
|
107
|
+
// against, which is exactly what `conflictContentMatches` compares.
|
|
108
|
+
const baseAdvanced = fresh.baseSha !== model.baseSha;
|
|
109
|
+
const same = baseAdvanced
|
|
110
|
+
? conflictContentMatches(model, fresh)
|
|
111
|
+
: conflictModelHash(fresh) === body.modelHash;
|
|
112
|
+
if (!same) {
|
|
113
|
+
throw landError('ModelStale', 'This merge changed while it was open. Reopen it.');
|
|
114
|
+
}
|
|
115
|
+
// ⚠ RE-CHECKED AGAINST THE REBUILT MODEL, not just the session's. A base advance moves
|
|
116
|
+
// the MERGE BASE, and with it the commit count above it — so a PR that was a single
|
|
117
|
+
// commit when the session opened can be two by the time it lands, and reparenting that
|
|
118
|
+
// onto the base tip would silently squash the second commit away, keeping only the
|
|
119
|
+
// first's message.
|
|
120
|
+
if (body.strategy === 'rebase' && !fresh.strategies.includes('rebase')) {
|
|
121
|
+
throw landError('RebaseNotOffered', fresh.rebaseUnavailableReason ?? 'Rebase is not available for this pull request.');
|
|
122
|
+
}
|
|
123
|
+
// ---- build the tree ----
|
|
124
|
+
args.onPhase('committing');
|
|
125
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'pierre-conflict-land-'));
|
|
126
|
+
const indexEnv = { GIT_INDEX_FILE: join(tmpDir, 'index') };
|
|
127
|
+
const built = await withRepoLock(repoKey, async () => {
|
|
128
|
+
const dir = cloneDir;
|
|
129
|
+
// FULL seeds from merge-tree's own tree, so every file NOBODY conflicted over already
|
|
130
|
+
// carries the merge's answer. PARTIAL seeds from HEAD, so an unresolved file keeps
|
|
131
|
+
// head's blob oid EXACTLY and the base branch's version of it is not silently dropped.
|
|
132
|
+
const seed = plan.full ? fresh.mergedTreeSha : fresh.headSha;
|
|
133
|
+
await git(['read-tree', seed], dir, undefined, indexEnv);
|
|
134
|
+
const entries = [];
|
|
135
|
+
for (const file of plan.resolved) {
|
|
136
|
+
const hashed = await git(['hash-object', '-w', '--stdin'], dir, file.content);
|
|
137
|
+
const oid = hashed.stdout.toString('utf8').trim();
|
|
138
|
+
if (!/^[0-9a-f]{40,64}$/.test(oid)) {
|
|
139
|
+
throw landError('GitFailed', `git could not store the resolved ${file.path}.`);
|
|
140
|
+
}
|
|
141
|
+
entries.push(`${file.mode} ${oid}\t${file.path}`);
|
|
142
|
+
}
|
|
143
|
+
if (entries.length > 0) {
|
|
144
|
+
await git(['update-index', '-z', '--index-info'], dir, Buffer.from(`${entries.join('\0')}\0`, 'utf8'), indexEnv);
|
|
145
|
+
}
|
|
146
|
+
// ⚠ FULL means the merge commit claims every conflict was settled. merge-tree's tree
|
|
147
|
+
// stores CONFLICT-MARKER CONTENT at each conflicted path (MEASURED via
|
|
148
|
+
// `cat-file -p $TREE:f.txt`), so a path we seeded from it and then failed to overwrite
|
|
149
|
+
// would commit `<<<<<<<` under a two-parent commit. A miss is a refusal, never a commit.
|
|
150
|
+
if (plan.full) {
|
|
151
|
+
const written = new Set(plan.resolved.map((f) => f.path));
|
|
152
|
+
for (const f of fresh.files) {
|
|
153
|
+
if (!written.has(f.path)) {
|
|
154
|
+
throw landError('GitFailed', `The merged tree still holds a conflict in ${f.path}.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const newTree = (await git(['write-tree'], dir, undefined, indexEnv)).stdout
|
|
159
|
+
.toString('utf8')
|
|
160
|
+
.trim();
|
|
161
|
+
// A PARTIAL whose every decision was "ours" produces head's own tree — a commit that
|
|
162
|
+
// says nothing and pushes nothing. The FULL path is allowed an identical tree: that is
|
|
163
|
+
// `-s ours`, and the merge commit itself is the point.
|
|
164
|
+
if (!plan.full) {
|
|
165
|
+
const headTree = (await git(['rev-parse', `${fresh.headSha}^{tree}`], dir)).stdout
|
|
166
|
+
.toString('utf8')
|
|
167
|
+
.trim();
|
|
168
|
+
if (newTree === headTree) {
|
|
169
|
+
throw landError('NothingToCommit', 'Every file already reads exactly as it does on this branch.');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const ident = await identFor(args.accountId);
|
|
173
|
+
const commitEnv = {
|
|
174
|
+
GIT_COMMITTER_NAME: ident.name,
|
|
175
|
+
GIT_COMMITTER_EMAIL: ident.email,
|
|
176
|
+
GIT_AUTHOR_NAME: ident.name,
|
|
177
|
+
GIT_AUTHOR_EMAIL: ident.email,
|
|
178
|
+
};
|
|
179
|
+
let message;
|
|
180
|
+
const parents = [];
|
|
181
|
+
if (body.strategy === 'rebase') {
|
|
182
|
+
// The rebase equivalence: for a SINGLE commit C on merge base M rebased onto base tip
|
|
183
|
+
// B, the three-way is (M, B, C) — the same three inputs as the merge with the sides
|
|
184
|
+
// swapped — so the resolved tree IS the merge path's tree and the rebased commit is
|
|
185
|
+
// just that tree reparented onto B. No `git rebase` process runs, so there is no
|
|
186
|
+
// stall, no `--continue` and no abort path to get wrong.
|
|
187
|
+
const original = await readCommitIdent(cloneDir, fresh.headSha);
|
|
188
|
+
commitEnv.GIT_AUTHOR_NAME = original.name;
|
|
189
|
+
commitEnv.GIT_AUTHOR_EMAIL = original.email;
|
|
190
|
+
commitEnv.GIT_AUTHOR_DATE = original.date;
|
|
191
|
+
message = original.message;
|
|
192
|
+
parents.push('-p', fresh.baseSha);
|
|
193
|
+
}
|
|
194
|
+
else if (plan.full) {
|
|
195
|
+
message = fullMergeMessage(fresh, plan);
|
|
196
|
+
parents.push('-p', fresh.headSha, '-p', fresh.baseSha);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
message = partialMessage(fresh, plan);
|
|
200
|
+
parents.push('-p', fresh.headSha);
|
|
201
|
+
}
|
|
202
|
+
const commit = await git(['commit-tree', newTree, ...parents, '-F', '-'], dir, Buffer.from(capMessage(message), 'utf8'), commitEnv);
|
|
203
|
+
return { newTree, commitSha: commit.stdout.toString('utf8').trim() };
|
|
204
|
+
});
|
|
205
|
+
assertNotCancelled(signal);
|
|
206
|
+
/* ───────────────────────── push safety ─────────────────────────
|
|
207
|
+
Immediately before the irreversible half, and after the tree and the commit already
|
|
208
|
+
exist, so the window between the checks and the push is milliseconds — the
|
|
209
|
+
`auto-merge-runner.ts` land-time re-check pattern. */
|
|
210
|
+
args.onPhase('pushing');
|
|
211
|
+
const ctx = await getPrWriteContext(args.prId, args.accountId);
|
|
212
|
+
if (!ctx || ctx.state !== 'open') {
|
|
213
|
+
throw landError('NotPermitted', 'This pull request is no longer open.');
|
|
214
|
+
}
|
|
215
|
+
if (!WRITE_PERMISSIONS.has(ctx.viewerPermission ?? '')) {
|
|
216
|
+
throw landError('NotPermitted', 'You do not have write access to this repository.');
|
|
217
|
+
}
|
|
218
|
+
const info = await fetchPrHeadInfo(token, model.owner, model.name, model.number);
|
|
219
|
+
if (info.headSha !== body.expectedHeadSha) {
|
|
220
|
+
throw landError('HeadMoved', `The pull request moved to ${info.headSha.slice(0, 7)} while this was open.`);
|
|
221
|
+
}
|
|
222
|
+
// The PR head branch lives in the HEAD repo, which for a fork PR is not the watched one.
|
|
223
|
+
// A NEW branch always lands in the watched repo instead, which is why the fork check is
|
|
224
|
+
// the pr_branch path's alone.
|
|
225
|
+
const target = body.target;
|
|
226
|
+
const toPrBranch = target.kind === 'pr_branch';
|
|
227
|
+
let pushOwner = model.owner;
|
|
228
|
+
let pushName = model.name;
|
|
229
|
+
if (target.kind === 'pr_branch') {
|
|
230
|
+
if (info.isFork && !info.maintainerCanModify) {
|
|
231
|
+
throw landError('PushDenied', 'This pull request comes from a fork that does not allow maintainer edits. Push to a new branch instead.');
|
|
232
|
+
}
|
|
233
|
+
const slash = info.headRepoFullName.indexOf('/');
|
|
234
|
+
if (slash > 0) {
|
|
235
|
+
pushOwner = info.headRepoFullName.slice(0, slash);
|
|
236
|
+
pushName = info.headRepoFullName.slice(slash + 1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// ⚠ DISARM FIRST, and not for tidiness. A merge-strategy resolution commit has exactly
|
|
240
|
+
// the two-parent shape `isOurUpdateMerge` proves against, so an intent still holding a
|
|
241
|
+
// live `updateIssuedAgainstOid` would ADOPT this commit, re-pin to it and land code the
|
|
242
|
+
// reader never consented to merge. Disarming makes that unreachable instead of relying on
|
|
243
|
+
// the window being narrow.
|
|
244
|
+
const autoMergeDisarmed = await disarmAutoMerge(args.accountId, args.prId);
|
|
245
|
+
const protect = await protectedRefsFor(args.accountId, model.owner, model.name, model.number, pushOwner, pushName);
|
|
246
|
+
const push = {
|
|
247
|
+
worktree: cloneDir,
|
|
248
|
+
owner: pushOwner,
|
|
249
|
+
name: pushName,
|
|
250
|
+
token,
|
|
251
|
+
committish: built.commitSha,
|
|
252
|
+
protect,
|
|
253
|
+
};
|
|
254
|
+
let branch;
|
|
255
|
+
let compareUrl = null;
|
|
256
|
+
let openedNumber = null;
|
|
257
|
+
if (target.kind === 'pr_branch') {
|
|
258
|
+
branch = info.headRef;
|
|
259
|
+
if (body.strategy === 'rebase') {
|
|
260
|
+
// ⚠ THE VALUE FORM OF THE LEASE. A bare `--force-with-lease` leases against the
|
|
261
|
+
// remote-tracking ref, which our own fetch just updated — it would pass every time.
|
|
262
|
+
await pushForceWithLease({
|
|
263
|
+
...push,
|
|
264
|
+
remoteBranch: branch,
|
|
265
|
+
leaseSha: body.expectedHeadSha,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
await pushRef({ ...push, remoteBranch: branch });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
branch = await pickFreeBranch(token, pushOwner, pushName, target.branch);
|
|
274
|
+
await pushRef({ ...push, remoteBranch: branch });
|
|
275
|
+
compareUrl = `https://github.com/${pushOwner}/${pushName}/compare/${encodeURIComponent(model.baseRef)}...${encodeURIComponent(branch)}`;
|
|
276
|
+
if (target.openPr) {
|
|
277
|
+
// ⚠ ONCE GITHUB 201s, NOTHING BELOW MAY THROW. A retry double-posts, and the copy
|
|
278
|
+
// contract for an unconfirmed write is "it'll show up here shortly", never "it failed".
|
|
279
|
+
const opened = await createPullRequest(token, {
|
|
280
|
+
owner: pushOwner,
|
|
281
|
+
name: pushName,
|
|
282
|
+
head: branch,
|
|
283
|
+
base: model.baseRef,
|
|
284
|
+
title: `Resolve conflicts with ${model.baseRef} for #${model.number}`,
|
|
285
|
+
body: `Conflict resolution for #${model.number}.`,
|
|
286
|
+
}).catch(() => null);
|
|
287
|
+
if (opened) {
|
|
288
|
+
compareUrl = opened.url;
|
|
289
|
+
openedNumber = opened.number;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// ⚠ CONFIRMING VERIFIES THE HEAD, and says what the push implies about the conflict.
|
|
294
|
+
// A bare resync here reported `visible: true` on ANY successful persist — including a read
|
|
295
|
+
// taken before GitHub attached the push, or one carrying GitHub's still-stale CONFLICTING /
|
|
296
|
+
// DIRTY for the new head — and the Pending board then re-served the "Conflicts" card this
|
|
297
|
+
// resolution had just cleared. Now `visible` means the pushed commit IS the stored head, and
|
|
298
|
+
// anything unsettled (the head, a stale conflict verdict, an unknown merge state) goes to the
|
|
299
|
+
// background settle ladder. The inline wait is deadline-bounded, so the SIGTERM drain that
|
|
300
|
+
// waits on this job is never held by it.
|
|
301
|
+
//
|
|
302
|
+
// `notConflicting` ONLY for a FULL resolution pushed to the PR's OWN branch (the rebase path
|
|
303
|
+
// is full by construction). A partial resolution, or a push to a new branch, legitimately
|
|
304
|
+
// leaves the original PR conflicting — expecting otherwise would run the ladder for nothing.
|
|
305
|
+
args.onPhase('confirming');
|
|
306
|
+
const slog = asSyncLogger(args.log);
|
|
307
|
+
const { visible } = await settlePrAfterWrite({
|
|
308
|
+
prId: args.prId,
|
|
309
|
+
accountId: args.accountId,
|
|
310
|
+
log: slog,
|
|
311
|
+
expect: toPrBranch
|
|
312
|
+
? { headSha: built.commitSha, ...(plan.full ? { notConflicting: true } : {}) }
|
|
313
|
+
: {},
|
|
314
|
+
});
|
|
315
|
+
// A NEW branch that opened a PR: sync THAT PR too, so it exists locally before the reader is
|
|
316
|
+
// told about it. Its number resolves only inside the original PR's (account, repo).
|
|
317
|
+
if (target.kind === 'new_branch' && openedNumber != null) {
|
|
318
|
+
await syncNewPrBesideAfterWrite({
|
|
319
|
+
accountId: args.accountId,
|
|
320
|
+
prId: args.prId,
|
|
321
|
+
number: openedNumber,
|
|
322
|
+
log: slog,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
strategy: body.strategy,
|
|
327
|
+
branch,
|
|
328
|
+
pushedToPrBranch: toPrBranch,
|
|
329
|
+
commitSha: built.commitSha,
|
|
330
|
+
resolvedPaths: plan.resolved.map((f) => f.path),
|
|
331
|
+
skipped: plan.skipped,
|
|
332
|
+
stillConflicting: !plan.full,
|
|
333
|
+
baseAdvanced,
|
|
334
|
+
baseShaUsed: fresh.baseSha,
|
|
335
|
+
autoMergeDisarmed,
|
|
336
|
+
compareUrl,
|
|
337
|
+
visible,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
// Every step independently swallowed: a teardown failure must never be what the reader
|
|
342
|
+
// sees instead of the result they got.
|
|
343
|
+
if (tmpDir) {
|
|
344
|
+
try {
|
|
345
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
/* best effort */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (cloneDir) {
|
|
352
|
+
for (const ref of [`${ns}/head`, `${ns}/base`]) {
|
|
353
|
+
await gitTry(['update-ref', '-d', ref], cloneDir).catch(() => { });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
setImmediate(() => {
|
|
357
|
+
try {
|
|
358
|
+
cleanupCloneCache();
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
/* advisory */
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Turn the request's per-region decisions into per-file bytes, or refuse.
|
|
368
|
+
*
|
|
369
|
+
* THE MODEL IS THE ALLOW-LIST. Every index, every region id and every decision is checked
|
|
370
|
+
* against it, and a `suggestionId` / `editId` is checked against the session's own store — so
|
|
371
|
+
* the only text that can reach a blob is text the server holds: either produced here, or typed
|
|
372
|
+
* by the reader and validated on the edit route before its id existed.
|
|
373
|
+
*/
|
|
374
|
+
function planResolution(model, files, suggestions, edits) {
|
|
375
|
+
const byIndex = new Map(model.files.map((f) => [f.index, f]));
|
|
376
|
+
const resolved = [];
|
|
377
|
+
const seenFiles = new Set();
|
|
378
|
+
for (const req of files) {
|
|
379
|
+
const file = byIndex.get(req.index);
|
|
380
|
+
if (!file) {
|
|
381
|
+
throw landError('UnknownFileIndex', `This merge has no file ${req.index}.`);
|
|
382
|
+
}
|
|
383
|
+
if (seenFiles.has(req.index)) {
|
|
384
|
+
throw landError('UnknownFileIndex', `File ${file.path} was sent twice.`);
|
|
385
|
+
}
|
|
386
|
+
seenFiles.add(req.index);
|
|
387
|
+
// An unsupported file has no blob triple to resolve, and a resolvable one with no stage-2
|
|
388
|
+
// mode has nothing to write the result back AS. Either way it is not on offer, and
|
|
389
|
+
// pretending otherwise would put an unreviewed blob in the tree.
|
|
390
|
+
if (file.unsupported !== null || file.stage2Mode === null) {
|
|
391
|
+
throw landError('UnknownFileIndex', `${file.path} cannot be resolved here${file.unsupportedLabel ? ` (${file.unsupportedLabel.toLowerCase()})` : ''}.`);
|
|
392
|
+
}
|
|
393
|
+
const decisions = new Map();
|
|
394
|
+
const regionsById = new Map(file.regions.map((r) => [r.id, r]));
|
|
395
|
+
for (const d of req.decisions) {
|
|
396
|
+
const region = regionsById.get(d.id);
|
|
397
|
+
if (!region) {
|
|
398
|
+
throw landError('IncompleteDecisions', `${file.path} has no region ${d.id}.`);
|
|
399
|
+
}
|
|
400
|
+
if (decisions.has(d.id)) {
|
|
401
|
+
throw landError('IncompleteDecisions', `${file.path} carries two decisions for the same region.`);
|
|
402
|
+
}
|
|
403
|
+
decisions.set(d.id, resolveDecision(file, region, d, suggestions, edits));
|
|
404
|
+
}
|
|
405
|
+
// Rule 2 of the fold, enforced here so the refusal names the file: EXHAUSTIVE over every
|
|
406
|
+
// non-`unchanged` region. A missing one is a line of code nobody chose.
|
|
407
|
+
for (const region of file.regions) {
|
|
408
|
+
if (region.kind === 'unchanged')
|
|
409
|
+
continue;
|
|
410
|
+
if (!decisions.has(region.id)) {
|
|
411
|
+
throw landError('IncompleteDecisions', `${file.path} still has an undecided change.`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const folded = foldFile({ regions: file.regions, terminators: file.terminators }, decisions);
|
|
415
|
+
if (!folded.ok) {
|
|
416
|
+
throw landError('IncompleteDecisions', `${file.path} could not be assembled (${folded.reason}).`);
|
|
417
|
+
}
|
|
418
|
+
resolved.push({
|
|
419
|
+
index: file.index,
|
|
420
|
+
path: file.path,
|
|
421
|
+
mode: file.stage2Mode,
|
|
422
|
+
// Every side decoded STRICTLY as UTF-8 at model build, so this re-encode round-trips
|
|
423
|
+
// byte for byte. That is the whole reason `not_text` is a refusal rather than a
|
|
424
|
+
// lossy decode.
|
|
425
|
+
content: Buffer.from(foldToText(folded), 'utf8'),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const unresolved = model.files.filter((f) => !seenFiles.has(f.index));
|
|
429
|
+
const skipped = [];
|
|
430
|
+
for (const f of unresolved) {
|
|
431
|
+
if (f.unsupported === null)
|
|
432
|
+
continue;
|
|
433
|
+
skipped.push({
|
|
434
|
+
index: f.index,
|
|
435
|
+
path: f.path,
|
|
436
|
+
reason: f.unsupported,
|
|
437
|
+
label: f.unsupportedLabel ?? '',
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
return { resolved, skipped, full: unresolved.length === 0, unresolved };
|
|
441
|
+
}
|
|
442
|
+
/** One region's decision, with the three payload-bearing members resolved from the places their
|
|
443
|
+
* lines actually live — never recomputed, so the fold that lands is the fold that was
|
|
444
|
+
* reviewed. */
|
|
445
|
+
function resolveDecision(file, region, sent, suggestions, edits) {
|
|
446
|
+
const decision = sent.decision;
|
|
447
|
+
if (decision === 'suggestion') {
|
|
448
|
+
if (!sent.suggestionId) {
|
|
449
|
+
throw landError('UnknownSuggestion', `${file.path} accepted a suggestion with no id.`);
|
|
450
|
+
}
|
|
451
|
+
const stored = suggestions.get(sent.suggestionId);
|
|
452
|
+
// The id must address THIS region: a suggestion is a handle on one hunk's lines, and a
|
|
453
|
+
// handle that travels to another hunk is text nobody read in the place it lands.
|
|
454
|
+
if (!stored || stored.fileIndex !== file.index || stored.regionId !== region.id) {
|
|
455
|
+
throw landError('UnknownSuggestion', 'That suggestion is no longer available.');
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
decision: 'suggestion',
|
|
459
|
+
lines: stored.lines,
|
|
460
|
+
endsWithNewline: stored.endsWithNewline,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (decision === 'edited') {
|
|
464
|
+
if (!sent.editId) {
|
|
465
|
+
throw landError('UnknownEdit', `${file.path} carries an edit with no id.`);
|
|
466
|
+
}
|
|
467
|
+
const stored = edits.get(sent.editId);
|
|
468
|
+
// ⚠ THE SAME GUARD, FOR THE SAME REASON, AND IT IS NOT REDUNDANT WITH THE FINGERPRINT CHECK
|
|
469
|
+
// THE EDIT ROUTE ALREADY MADE. That one proved the text was written against THIS region's
|
|
470
|
+
// bytes; this one proves the handle has not since been moved onto a different region by the
|
|
471
|
+
// commit body. Both are "text nobody read, in the place it lands" — one across time, one
|
|
472
|
+
// across regions.
|
|
473
|
+
if (!stored || stored.fileIndex !== file.index || stored.regionId !== region.id) {
|
|
474
|
+
throw landError('UnknownEdit', 'That edit is no longer available.');
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
decision: 'edited',
|
|
478
|
+
lines: stored.lines,
|
|
479
|
+
endsWithNewline: stored.endsWithNewline,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (!allowedDecisions(region).includes(decision)) {
|
|
483
|
+
throw landError('IncompleteDecisions', `${file.path} cannot take '${decision}' on this change.`);
|
|
484
|
+
}
|
|
485
|
+
if (decision === 'disjoint_merge') {
|
|
486
|
+
// Non-null iff the wand called it `disjoint_words`, which `allowedDecisions` already
|
|
487
|
+
// gates on — this is the belt to that braces, because a null here would fold to nothing.
|
|
488
|
+
if (region.mergedLines === null) {
|
|
489
|
+
throw landError('IncompleteDecisions', `${file.path} has no word-level merge for this change.`);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
decision: 'disjoint_merge',
|
|
493
|
+
lines: region.mergedLines,
|
|
494
|
+
// Fold rule 4: the word merge is built on the ours side, so it inherits its terminator.
|
|
495
|
+
endsWithNewline: file.terminators.ours,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return { decision };
|
|
499
|
+
}
|
|
500
|
+
/* ═════════════════════════════════ staleness ═════════════════════════════════ */
|
|
501
|
+
/**
|
|
502
|
+
* Did the material the decisions were made against change?
|
|
503
|
+
*
|
|
504
|
+
* ⚠ THIS IS NOT `conflictModelHash`, and it must not become it. The hash pins the two
|
|
505
|
+
* COMMITS and the merged tree, all of which move when the base branch gains any commit at
|
|
506
|
+
* all — so it answers "is this the same merge", while the question here is "are these the
|
|
507
|
+
* same conflicts". A base advance that leaves every contested region byte-identical is
|
|
508
|
+
* reported as `baseAdvanced`, not refused.
|
|
509
|
+
*/
|
|
510
|
+
function conflictContentMatches(a, b) {
|
|
511
|
+
if (a.files.length !== b.files.length)
|
|
512
|
+
return false;
|
|
513
|
+
if (a.renameDetection !== b.renameDetection || a.truncated !== b.truncated)
|
|
514
|
+
return false;
|
|
515
|
+
for (let i = 0; i < a.files.length; i++) {
|
|
516
|
+
const x = a.files[i];
|
|
517
|
+
const y = b.files[i];
|
|
518
|
+
if (!x || !y)
|
|
519
|
+
return false;
|
|
520
|
+
if (x.index !== y.index || x.path !== y.path)
|
|
521
|
+
return false;
|
|
522
|
+
if (x.unsupported !== y.unsupported || x.stage2Mode !== y.stage2Mode)
|
|
523
|
+
return false;
|
|
524
|
+
if (x.terminators.base !== y.terminators.base ||
|
|
525
|
+
x.terminators.ours !== y.terminators.ours ||
|
|
526
|
+
x.terminators.theirs !== y.terminators.theirs) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
if (x.regions.length !== y.regions.length)
|
|
530
|
+
return false;
|
|
531
|
+
for (let r = 0; r < x.regions.length; r++) {
|
|
532
|
+
const p = x.regions[r];
|
|
533
|
+
const q = y.regions[r];
|
|
534
|
+
if (!p || !q)
|
|
535
|
+
return false;
|
|
536
|
+
if (p.id !== q.id || p.kind !== q.kind || p.fingerprint !== q.fingerprint)
|
|
537
|
+
return false;
|
|
538
|
+
if (p.wand?.decision !== q.wand?.decision || p.wand?.reason !== q.wand?.reason)
|
|
539
|
+
return false;
|
|
540
|
+
if ((p.mergedLines === null) !== (q.mergedLines === null))
|
|
541
|
+
return false;
|
|
542
|
+
if (p.mergedLines && q.mergedLines) {
|
|
543
|
+
if (p.mergedLines.length !== q.mergedLines.length)
|
|
544
|
+
return false;
|
|
545
|
+
for (let l = 0; l < p.mergedLines.length; l++) {
|
|
546
|
+
if (p.mergedLines[l] !== q.mergedLines[l])
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
/* ═════════════════════════════════ branch names ═════════════════════════════════ */
|
|
555
|
+
/** Reserved BY NAME — the repo default branch and the PR base ref. Case-insensitive, because
|
|
556
|
+
* on a case-folding filesystem `Main` and `main` are the same branch and the refusal has to
|
|
557
|
+
* hold for the spelling the user typed. */
|
|
558
|
+
async function assertNewBranchName(model, branch) {
|
|
559
|
+
const wanted = branch.trim();
|
|
560
|
+
if (!wanted)
|
|
561
|
+
throw landError('InvalidBranch', 'Give the new branch a name.');
|
|
562
|
+
const lowered = wanted.toLowerCase();
|
|
563
|
+
if (model.reservedBranchNames.some((r) => r.toLowerCase() === lowered)) {
|
|
564
|
+
throw landError('ReservedBranch', `${wanted} is this repository's own branch.`);
|
|
565
|
+
}
|
|
566
|
+
if (wanted.startsWith('-') ||
|
|
567
|
+
wanted.startsWith('/') ||
|
|
568
|
+
wanted.endsWith('/') ||
|
|
569
|
+
wanted.includes('//')) {
|
|
570
|
+
throw landError('InvalidBranch', `git does not accept ${wanted} as a branch name.`);
|
|
571
|
+
}
|
|
572
|
+
// git's own format check is the authority; a regex here would refuse real branch names.
|
|
573
|
+
const res = await gitTry(['check-ref-format', `refs/heads/${wanted}`], tmpdir());
|
|
574
|
+
if (res.code !== 0) {
|
|
575
|
+
throw landError('InvalidBranch', `git does not accept ${wanted} as a branch name.`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* The first free name at or after `wanted`, `-2` … `-9`, then a refusal.
|
|
580
|
+
*
|
|
581
|
+
* A live ref under this name belongs to someone — an earlier attempt, a person — so it is
|
|
582
|
+
* never overwritten. The ladder is what makes a second attempt after a failed push work
|
|
583
|
+
* without the reader having to invent a name; the result names the branch that was actually
|
|
584
|
+
* created, so nothing about it is silent.
|
|
585
|
+
*/
|
|
586
|
+
async function pickFreeBranch(token, owner, name, wanted) {
|
|
587
|
+
for (let n = 1; n <= 9; n++) {
|
|
588
|
+
const candidate = n === 1 ? wanted : `${wanted}-${n}`;
|
|
589
|
+
const existing = await ghRestGetText(token, `/repos/${owner}/${name}/git/ref/heads/${candidate}`);
|
|
590
|
+
if (!existing.ok)
|
|
591
|
+
return candidate;
|
|
592
|
+
}
|
|
593
|
+
throw landError('BranchExists', `${wanted} and its numbered variants all exist already.`);
|
|
594
|
+
}
|
|
595
|
+
/* ═════════════════════════════════ commit messages ═════════════════════════════════ */
|
|
596
|
+
const MESSAGE_MAX_BYTES = 8 * 1024;
|
|
597
|
+
const PATHS_LISTED = 20;
|
|
598
|
+
function capMessage(message) {
|
|
599
|
+
const buf = Buffer.from(message, 'utf8');
|
|
600
|
+
if (buf.length <= MESSAGE_MAX_BYTES)
|
|
601
|
+
return message;
|
|
602
|
+
// Slice on a character boundary, not a byte one: a half-encoded sequence in a commit
|
|
603
|
+
// message is a mojibake bug that outlives the session by a long way.
|
|
604
|
+
let cut = message;
|
|
605
|
+
while (Buffer.byteLength(cut, 'utf8') > MESSAGE_MAX_BYTES - 1) {
|
|
606
|
+
cut = cut.slice(0, Math.floor(cut.length * 0.9) || 0);
|
|
607
|
+
}
|
|
608
|
+
return `${cut}\n`;
|
|
609
|
+
}
|
|
610
|
+
function pathList(paths) {
|
|
611
|
+
const shown = paths.slice(0, PATHS_LISTED).map((p) => ` ${p}`);
|
|
612
|
+
if (paths.length > PATHS_LISTED)
|
|
613
|
+
shown.push(` …and ${paths.length - PATHS_LISTED} more`);
|
|
614
|
+
return shown;
|
|
615
|
+
}
|
|
616
|
+
function fullMergeMessage(model, plan) {
|
|
617
|
+
const paths = plan.resolved.map((f) => f.path);
|
|
618
|
+
return [
|
|
619
|
+
`Merge branch '${model.baseRef}' into ${model.headRef || 'this branch'}`,
|
|
620
|
+
'',
|
|
621
|
+
`Resolved conflicts in ${paths.length} ${paths.length === 1 ? 'file' : 'files'}:`,
|
|
622
|
+
...pathList(paths),
|
|
623
|
+
'',
|
|
624
|
+
].join('\n');
|
|
625
|
+
}
|
|
626
|
+
/** ⚠ THE WORD "MERGE" MUST NOT APPEAR. A partial resolution is a plain one-parent commit on
|
|
627
|
+
* the PR branch, and calling it a merge in the log is a claim that the base branch landed. */
|
|
628
|
+
function partialMessage(model, plan) {
|
|
629
|
+
const paths = plan.resolved.map((f) => f.path);
|
|
630
|
+
const lines = [
|
|
631
|
+
`Resolve conflicts in ${paths.length} of ${model.files.length} files`,
|
|
632
|
+
'',
|
|
633
|
+
...pathList(paths),
|
|
634
|
+
];
|
|
635
|
+
if (plan.unresolved.length > 0) {
|
|
636
|
+
lines.push('');
|
|
637
|
+
for (const f of plan.unresolved.slice(0, PATHS_LISTED)) {
|
|
638
|
+
lines.push(`Still conflicting: ${f.path}${f.unsupportedLabel ? ` (${f.unsupportedLabel})` : ''}`);
|
|
639
|
+
}
|
|
640
|
+
if (plan.unresolved.length > PATHS_LISTED) {
|
|
641
|
+
lines.push(`…and ${plan.unresolved.length - PATHS_LISTED} more`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
lines.push('');
|
|
645
|
+
return lines.join('\n');
|
|
646
|
+
}
|
|
647
|
+
/* ═════════════════════════════════ small helpers ═════════════════════════════════ */
|
|
648
|
+
function assertNotCancelled(signal) {
|
|
649
|
+
if (signal.aborted)
|
|
650
|
+
throw landError('Cancelled', 'Cancelled.');
|
|
651
|
+
}
|
|
652
|
+
/** The original commit's author and message, for the rebase path. The committer is the
|
|
653
|
+
* account doing the resolving — the author wrote the code, we only moved it. */
|
|
654
|
+
async function readCommitIdent(cloneDir, sha) {
|
|
655
|
+
const out = await git(['show', '-s', '--format=%an%x00%ae%x00%aI%x00%B', sha], cloneDir);
|
|
656
|
+
const parts = out.stdout.toString('utf8').split('\0');
|
|
657
|
+
return {
|
|
658
|
+
name: parts[0] ?? 'pierre-review',
|
|
659
|
+
email: parts[1] ?? 'pierre-review@users.noreply.github.com',
|
|
660
|
+
date: parts[2] ?? new Date().toISOString(),
|
|
661
|
+
message: parts[3] ?? '',
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
/** The `coding/merge.ts` ident, which is module-private there. Same fallbacks on purpose:
|
|
665
|
+
* two spellings of "who committed this" would show up in the git log as two people. */
|
|
666
|
+
async function identFor(accountId) {
|
|
667
|
+
const account = await getAccountById(accountId);
|
|
668
|
+
return {
|
|
669
|
+
name: account?.displayName || account?.githubLogin || 'pierre-review',
|
|
670
|
+
email: account?.githubLogin
|
|
671
|
+
? `${account.githubLogin}@users.noreply.github.com`
|
|
672
|
+
: 'pierre-review@users.noreply.github.com',
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=land.js.map
|