carson 2.19.1 → 2.20.0
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.
- checksums.yaml +4 -4
- data/RELEASE.md +8 -0
- data/VERSION +1 -1
- data/lib/carson/runtime/local.rb +83 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1baa45e25838165768104a260d5641a4f3cfe25fbfe3f7c1d430a9d62dc53cf1
|
|
4
|
+
data.tar.gz: 99ea0d1c0ba2b332cd10c599ea472f8b2d0b1ca016bfc73ec879f421b32dffae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f746f1fb2d74940e0e5b21d6ab0a9d5857aa78972453202016345214b5b77504792ab490ac5afda8028b1b2b07618d0451c4a996a004ca2e1908c29a3de82c78
|
|
7
|
+
data.tar.gz: 95ad47ab82a4a617d6fe590c2ead6e15c8b11e36695e0039447158f35c07790fa001997bfc180f56cc441387a32e259af19d0ef02b3d01580d9a449d1ff337ee
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,14 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
+
## 2.20.0 — Prune Orphan Branches
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson prune` now detects orphan branches.** Local branches with no upstream tracking are pruned when GitHub confirms a merged PR matching the exact branch name and tip SHA. Previously, only branches with `[gone]` upstream tracking were detected — branches that were never pushed with `-u` or lost tracking would linger indefinitely.
|
|
13
|
+
- Orphan deletions count towards the same "Pruned N stale branches." total in concise output. Verbose output uses distinct `deleted_orphan_branch:` / `skip_orphan_branch:` log lines.
|
|
14
|
+
- Carson's own `carson/template-sync` branch and protected branches are excluded from orphan detection.
|
|
15
|
+
|
|
8
16
|
## 2.19.1 — Remove Dependabot References
|
|
9
17
|
|
|
10
18
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.20.0
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -34,35 +34,45 @@ module Carson
|
|
|
34
34
|
git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
# Removes stale local branches
|
|
37
|
+
# Removes stale local branches (gone upstream) and orphan branches (no tracking) with merged PR evidence.
|
|
38
38
|
def prune!
|
|
39
39
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
40
40
|
return fingerprint_status unless fingerprint_status.nil?
|
|
41
41
|
|
|
42
42
|
git_system!( "fetch", config.git_remote, "--prune" )
|
|
43
43
|
active_branch = current_branch
|
|
44
|
+
counters = { deleted: 0, skipped: 0 }
|
|
45
|
+
|
|
44
46
|
stale_branches = stale_local_branches
|
|
45
|
-
|
|
47
|
+
prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, counters: counters )
|
|
48
|
+
|
|
49
|
+
orphan_branches = orphan_local_branches( active_branch: active_branch )
|
|
50
|
+
prune_orphan_branch_entries( orphan_branches: orphan_branches, counters: counters )
|
|
51
|
+
|
|
52
|
+
return prune_no_stale_branches if counters.fetch( :deleted ).zero? && counters.fetch( :skipped ).zero?
|
|
46
53
|
|
|
47
|
-
counters = prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch )
|
|
48
54
|
puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
|
|
49
55
|
unless verbose?
|
|
50
|
-
|
|
56
|
+
deleted_count = counters.fetch( :deleted )
|
|
57
|
+
if deleted_count.zero?
|
|
58
|
+
puts_line "No stale branches."
|
|
59
|
+
else
|
|
60
|
+
puts_line "Pruned #{deleted_count} stale branch#{plural_suffix( count: deleted_count )}."
|
|
61
|
+
end
|
|
51
62
|
end
|
|
52
63
|
EXIT_OK
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
def prune_no_stale_branches
|
|
56
67
|
if verbose?
|
|
57
|
-
puts_line "OK: no stale
|
|
68
|
+
puts_line "OK: no stale or orphan branches to prune."
|
|
58
69
|
else
|
|
59
70
|
puts_line "No stale branches."
|
|
60
71
|
end
|
|
61
72
|
EXIT_OK
|
|
62
73
|
end
|
|
63
74
|
|
|
64
|
-
def prune_stale_branch_entries( stale_branches:, active_branch: )
|
|
65
|
-
counters = { deleted: 0, skipped: 0 }
|
|
75
|
+
def prune_stale_branch_entries( stale_branches:, active_branch:, counters: { deleted: 0, skipped: 0 } )
|
|
66
76
|
stale_branches.each do |entry|
|
|
67
77
|
outcome = prune_stale_branch_entry( entry: entry, active_branch: active_branch )
|
|
68
78
|
counters[ outcome ] += 1
|
|
@@ -851,10 +861,8 @@ module Carson
|
|
|
851
861
|
violations
|
|
852
862
|
end
|
|
853
863
|
|
|
854
|
-
#
|
|
855
|
-
#
|
|
856
|
-
# 2) upstream tracking state is marked as gone after fetch --prune.
|
|
857
|
-
# Branches without upstream tracking are intentionally excluded.
|
|
864
|
+
# Detects local branches whose upstream tracking is marked [gone] after fetch --prune.
|
|
865
|
+
# Branches without upstream tracking are handled separately by orphan_local_branches.
|
|
858
866
|
def stale_local_branches
|
|
859
867
|
git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)\t%(upstream:track)", "refs/heads" ).lines.map do |line|
|
|
860
868
|
branch, upstream, track = line.strip.split( "\t", 3 )
|
|
@@ -867,6 +875,70 @@ module Carson
|
|
|
867
875
|
end.compact
|
|
868
876
|
end
|
|
869
877
|
|
|
878
|
+
# Detects local branches with no upstream tracking ref — candidates for orphan pruning.
|
|
879
|
+
# Filters out protected branches, the active branch, and Carson's own sync branch.
|
|
880
|
+
def orphan_local_branches( active_branch: )
|
|
881
|
+
git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads" ).lines.filter_map do |line|
|
|
882
|
+
branch, upstream = line.strip.split( "\t", 2 )
|
|
883
|
+
branch = branch.to_s.strip
|
|
884
|
+
upstream = upstream.to_s.strip
|
|
885
|
+
next if branch.empty?
|
|
886
|
+
next unless upstream.empty?
|
|
887
|
+
next if config.protected_branches.include?( branch )
|
|
888
|
+
next if branch == active_branch
|
|
889
|
+
next if branch == TEMPLATE_SYNC_BRANCH
|
|
890
|
+
|
|
891
|
+
branch
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# Processes orphan branches: verifies merged PR evidence via GitHub API before deleting.
|
|
896
|
+
def prune_orphan_branch_entries( orphan_branches:, counters: )
|
|
897
|
+
return counters if orphan_branches.empty?
|
|
898
|
+
return counters unless gh_available?
|
|
899
|
+
|
|
900
|
+
orphan_branches.each do |branch|
|
|
901
|
+
outcome = prune_orphan_branch_entry( branch: branch )
|
|
902
|
+
counters[ outcome ] += 1
|
|
903
|
+
end
|
|
904
|
+
counters
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# Checks a single orphan branch for merged PR evidence and force-deletes if confirmed.
|
|
908
|
+
def prune_orphan_branch_entry( branch: )
|
|
909
|
+
tip_sha_text, tip_sha_error, tip_sha_success, = git_run( "rev-parse", "--verify", branch.to_s )
|
|
910
|
+
unless tip_sha_success
|
|
911
|
+
error_text = tip_sha_error.to_s.strip
|
|
912
|
+
error_text = "unable to read local branch tip sha" if error_text.empty?
|
|
913
|
+
puts_verbose "skip_orphan_branch: #{branch} reason=#{error_text}"
|
|
914
|
+
return :skipped
|
|
915
|
+
end
|
|
916
|
+
branch_tip_sha = tip_sha_text.to_s.strip
|
|
917
|
+
if branch_tip_sha.empty?
|
|
918
|
+
puts_verbose "skip_orphan_branch: #{branch} reason=unable to read local branch tip sha"
|
|
919
|
+
return :skipped
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
merged_pr, error = merged_pr_for_branch( branch: branch, branch_tip_sha: branch_tip_sha )
|
|
923
|
+
if merged_pr.nil?
|
|
924
|
+
reason = error.to_s.strip
|
|
925
|
+
reason = "no merged PR evidence for branch tip into #{config.main_branch}" if reason.empty?
|
|
926
|
+
puts_verbose "skip_orphan_branch: #{branch} reason=#{reason}"
|
|
927
|
+
return :skipped
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
force_stdout, force_stderr, force_success, = git_run( "branch", "-D", branch )
|
|
931
|
+
if force_success
|
|
932
|
+
out.print force_stdout if verbose? && !force_stdout.empty?
|
|
933
|
+
puts_verbose "deleted_orphan_branch: #{branch} merged_pr=#{merged_pr.fetch( :url )}"
|
|
934
|
+
return :deleted
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
force_error_text = normalise_branch_delete_error( error_text: force_stderr )
|
|
938
|
+
puts_verbose "fail_delete_orphan_branch: #{branch} reason=#{force_error_text}"
|
|
939
|
+
:skipped
|
|
940
|
+
end
|
|
941
|
+
|
|
870
942
|
# Safe delete can fail after squash merges because branch tip is no longer an ancestor.
|
|
871
943
|
def non_merged_delete_error?( error_text: )
|
|
872
944
|
error_text.to_s.downcase.include?( "not fully merged" )
|