@@ -33,7 +33,7 @@ log_header() {
3333}
3434
3535log_info () {
36- echo -e " $* $ "
36+ echo -e " $* "
3737}
3838
3939# Helper: get ISO8601 date N days ago (cross-platform)
@@ -83,6 +83,40 @@ get_days_diff_from_now() {
8383 echo $(( (now_epoch - date_epoch) / 86400 ))
8484}
8585
86+ # Get stale branches with age filtering (atomic git operation)
87+ get_stale_branches () {
88+ local days_threshold=" $1 "
89+ local temp_file=$( mktemp)
90+
91+ # Ensure temp file cleanup on function exit/interruption
92+ trap ' rm -f "$temp_file"' RETURN
93+
94+ # Single atomic git command gets all branch info at once, write to file to avoid SIGPIPE
95+ # Reference: TabrisJS uses git for-each-ref -> file -> process for automation across repos
96+ # https://tabris.com/iterate-over-branches-in-your-git-repository/
97+ git for-each-ref --format=' %(refname:short)|%(objectname)|%(committerdate:iso8601-strict)|%(authorname)' refs/remotes/origin/ > " $temp_file "
98+
99+ local stale_info=" "
100+ while IFS=' |' read -r refname sha date author; do
101+ local branch=${refname# origin/ }
102+ [[ " $branch " == " HEAD" ]] && continue
103+
104+ # Skip ignored branches
105+ [[ " $branch " =~ $IGNORE_BRANCHES_REGEX ]] && continue
106+
107+ # Calculate age and filter immediately
108+ # Convert git date format "2025-05-20 09:48:29 +0200" to ISO8601 "2025-05-20T09:48:29+0200"
109+ local iso_date=" ${date/ / T} " # Replace first space with T
110+ iso_date=" ${iso_date/ / } " # Remove space before timezone
111+ local age_days=$( get_days_diff_from_now " $iso_date " )
112+ if [[ " $age_days " -ge " $days_threshold " ]]; then
113+ stale_info=" $stale_info$age_days " $' \t ' " $sha " $' \t ' " $date " $' \t ' " $branch " $' \t ' " $author " $' \n '
114+ fi
115+ done < " $temp_file "
116+
117+ echo " ${stale_info% $' \n ' } "
118+ }
119+
86120validate_requirements () {
87121 local missing_deps=()
88122
@@ -134,10 +168,6 @@ parse_repo_info() {
134168}
135169
136170main () {
137- # Initialize temporary file
138- branch_info_file=$( mktemp)
139- trap ' rm -f "$branch_info_file"' EXIT
140-
141171 # Validate script requirements
142172 validate_requirements
143173
@@ -158,47 +188,24 @@ main() {
158188 log_info " Max branches per run: $MAX_BRANCHES_PER_RUN "
159189 log_info " Dry run: $DRY_RUN "
160190
161- # Get all branches
162- local branches_all branches_all_count branches_ignored branches_candidates
163- branches_all=$( git branch -r | grep -v ' \->' | sed ' s/origin\///' | xargs -I{} echo " {}" )
164- branches_all_count=$( echo " $branches_all " | wc -l)
165- branches_ignored=$( echo " $branches_all " | grep -E " $IGNORE_BRANCHES_REGEX " || true)
166-
167- log_header " Branch Filtering:"
168- log_info " IGNORE_BRANCHES_REGEX: ${Y}${IGNORE_BRANCHES_REGEX}${R} "
169- log_info " Ignored branches:"
170- echo " $branches_ignored "
171-
172- branches_candidates=$( echo " $branches_all " | grep -Ev " $IGNORE_BRANCHES_REGEX " || true)
173-
174- # Collect branch info
175- for branch in $branches_candidates ; do
176- local last_commit_date last_commit_author last_commit_sha age_days
177- last_commit_date=$( git log -1 --format=%aI " origin/$branch " )
178- last_commit_author=$( git log -1 --format=%an " origin/$branch " )
179- last_commit_sha=$( git log -1 --format=%H " origin/$branch " )
180- age_days=$( get_days_diff_from_now " $last_commit_date " )
181-
182- if [[ " $age_days " -lt " $DAYS_BEFORE_STALE " ]]; then
183- continue
184- fi
185- printf " %d\t%s\t%s\t%s\t%s\n" " $age_days " " $last_commit_sha " " $last_commit_date " " $branch " " $last_commit_author " >> " $branch_info_file "
186- done
191+ local stale_branches
192+ stale_branches=$( get_stale_branches " $DAYS_BEFORE_STALE " )
193+
194+ if [[ -z " $stale_branches " ]]; then
195+ log_header " No stale branches found."
196+ exit 0
197+ fi
187198
188- # Sort and get oldest branches
199+ # Sort and show stale branches
189200 if [[ " $VERBOSE " == " true" ]]; then
190201 log_header " Stale branches by age:"
191- sort -nr " $branch_info_file " | while IFS=$' \t ' read -r age_days sha date branch author; do
192- printf " ${G} %-4d days${R} %s ${G} %s${R} by %s\n" " $age_days " " $branch " " $date " " $author "
202+ echo " $stale_branches " | sort -nr | while IFS=$' \t ' read -r age_days sha date branch author; do
203+ printf " ${G} %-4d days${R} %s ${G} %s${R} by %s\n" " $age_days " " $branch " " ${ date%% T * } " " $author "
193204 done
194205 fi
195206
196207 local oldest_branches
197- oldest_branches=$( sort -nr " $branch_info_file " | head -n " $MAX_BRANCHES_PER_RUN " )
198- if [[ -z " $oldest_branches " ]]; then
199- log_header " No branches to delete."
200- exit 0
201- fi
208+ oldest_branches=$( echo " $stale_branches " | sort -nr | head -n " $MAX_BRANCHES_PER_RUN " )
202209
203210 log_header " Deleting oldest branches (max $MAX_BRANCHES_PER_RUN ):"
204211 while IFS=$' \t ' read -r age_days sha date branch author; do
@@ -209,18 +216,16 @@ main() {
209216 done <<< " $oldest_branches"
210217
211218 # Print summary
212- local branches_remaining branches_remaining_count deleted_count
213- branches_remaining=$( git branch -r | grep -v ' \->' | sed ' s/origin\///' | xargs -I{} echo " {}" )
214- branches_remaining_count=$( echo " $branches_remaining " | wc -l)
215- deleted_count=$(( branches_all_count - branches_remaining_count))
219+ local stale_count deleted_count
220+ stale_count=$( printf " %s\n" " $stale_branches " | wc -l)
221+ deleted_count=$( printf " %s\n" " $oldest_branches " | wc -l)
216222
217223 log_header " Summary:"
218224 if [[ " $DRY_RUN " != " false" ]]; then
219225 log_info " DRY_RUN: No branches deleted"
220226 fi
221- log_info " Branches before: $branches_all_count "
222- log_info " Branches after: $branches_remaining_count "
223- log_info " Deleted: $deleted_count "
227+ log_info " Stale branches found: $stale_count "
228+ log_info " Branches deleted: $deleted_count "
224229 echo
225230}
226231
0 commit comments