carson 2.19.0 → 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/MANUAL.md +2 -2
- data/RELEASE.md +14 -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/MANUAL.md
CHANGED
|
@@ -101,7 +101,7 @@ Notes:
|
|
|
101
101
|
|
|
102
102
|
### Canonical Templates
|
|
103
103
|
|
|
104
|
-
Carson manages 5 governance files (carson.md, CLAUDE.md, AGENTS.md, copilot-instructions.md, pull_request_template.md). Beyond those, you can tell Carson about your own canonical `.github/` files — CI workflows, linter configs,
|
|
104
|
+
Carson manages 5 governance files (carson.md, CLAUDE.md, AGENTS.md, copilot-instructions.md, pull_request_template.md). Beyond those, you can tell Carson about your own canonical `.github/` files — CI workflows, linter configs, labeller rules, anything that belongs in `.github/`.
|
|
105
105
|
|
|
106
106
|
Set `template.canonical` in `~/.carson/config.json`:
|
|
107
107
|
|
|
@@ -120,7 +120,7 @@ That directory mirrors the `.github/` structure:
|
|
|
120
120
|
├── workflows/
|
|
121
121
|
│ └── lint.yml → deployed to .github/workflows/lint.yml
|
|
122
122
|
├── .mega-linter.yml → deployed to .github/.mega-linter.yml
|
|
123
|
-
└──
|
|
123
|
+
└── labeler.yml → deployed to .github/labeler.yml
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
Carson discovers files in this directory and syncs them to governed repos alongside its own governance files. `carson template check` detects drift, `carson template apply` writes them, and `carson refresh` propagates them to the remote.
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
|
|
16
|
+
## 2.19.1 — Remove Dependabot References
|
|
17
|
+
|
|
18
|
+
### What changed
|
|
19
|
+
|
|
20
|
+
- Replaced all Dependabot example references in documentation and tests with `labeler.yml`. Carson never had a Dependabot feature — these were illustrative filenames for the canonical template system.
|
|
21
|
+
|
|
8
22
|
## 2.19.0 — Canonical Templates, Lint Removed
|
|
9
23
|
|
|
10
24
|
### 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" )
|