├── LICENSE ├── README.md └── git-cleanup-branches.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 weirdgiraffe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git cleanup-branches 2 | 3 | When you're working on some project for a while the amount of git branches is 4 | getting up, so it's getting hard to understand which branches are still useful 5 | and which were already merged and deleted from the remote. This script tries to 6 | address this problem by providing a simple, interactive experience to remove branches 7 | which were deleted at remote and those which were already merged. 8 | 9 | This simple script will delete local branches which are: 10 | 11 | - deleted in remote (remote name could be set via `--remote`, by default `origin`) 12 | - merged into the current branch 13 | 14 | The script is interactive and will ask before pruning the remote or deleting 15 | branches. 16 | 17 | **Note:** you need python3 to be installed to run this script 18 | 19 | Installation: 20 | 21 | ``` 22 | curl https://raw.githubusercontent.com/weirdgiraffe/git-cleanup-branches/master/git-cleanup-branches.py --output /usr/local/bin/git-cleanup-branches 23 | chmod +x /usr/local/bin/git-cleanup-branches 24 | ``` 25 | Then open a new shell, go to a git repository and use it with `git cleanup-branches`. 26 | 27 | # examples 28 | 29 | The script gives you a list of branches to be deleted and waits for confirmation: 30 | 31 | ``` 32 | $ git cleanup-branches 33 | prune: feature/a was deleted in origin. delete? (y/N): n 34 | prune: feature/b was deleted in origin. delete? (y/N): y 35 | merged: feature/c was already merged to master. delete? (y/N): n 36 | 37 | local branches to keep: 38 | - feature/a 39 | - feature/c 40 | 41 | local branches to delete: 42 | - prune feature/b (prune origin) 43 | 44 | if you will delete those branches you will 45 | not be able to restore them from remote. 46 | continue? (y/N): n 47 | ``` 48 | 49 | Script will warn you in case you want to keep local branches which were deleted 50 | from remote: 51 | 52 | ``` 53 | $ git cleanup-branches 54 | prune: feature/a was deleted in origin. delete? (y/N): n 55 | prune: feature/b was deleted in origin. delete? (y/N): y 56 | merged: feature/c was already merged to master. delete? (y/N): y 57 | 58 | local branches to keep: 59 | - feature/a 60 | 61 | local branches to delete: 62 | - prune feature/b (prune origin) 63 | - prune feature/c (merged into master) 64 | 65 | if you will delete those branches you will 66 | not be able to restore them from remote. 67 | continue? (y/N): y 68 | Deleted branch feature/b (was 031b5b312). 69 | Deleted branch feature/c (was ab120153e). 70 | 71 | stale branches in origin are about to be pruned. 72 | if you will press y now it would be impossible 73 | to link your stale local branches to stale 74 | remote branches in origin. 75 | 76 | prune remote origin? (y/N): y 77 | Pruning origin 78 | URL: git@github.com:xxx.git 79 | * [pruned] origin/feature/a 80 | * [pruned] origin/feature/b 81 | ``` 82 | 83 | You can skip interactive selection by running with `--all`: 84 | 85 | ``` 86 | $ git cleanup-branches --all 87 | 88 | local branches to delete: 89 | - prune feature/b (prune origin) 90 | - prune feature/b (prune origin) 91 | - prune feature/c (merged into master) 92 | 93 | if you will delete those branches you will 94 | not be able to restore them from remote. 95 | continue? (y/N): y 96 | Deleted branch feature/a (was ce195b1ef). 97 | Deleted branch feature/b (was 031b5b312). 98 | Deleted branch feature/c (was ab120153e). 99 | Pruning origin 100 | URL: git@github.com:xxx.git 101 | * [pruned] origin/feature/a 102 | * [pruned] origin/feature/b 103 | ``` 104 | 105 | You can specify remote name (`origin` by default): 106 | 107 | ``` 108 | $ git cleanup-branches --remote foo --all 109 | 110 | local branches to delete: 111 | - prune feature/b (prune foo) 112 | - prune feature/b (prune foo) 113 | - prune feature/c (merged into master) 114 | 115 | if you will delete those branches you will 116 | not be able to restore them from remote. 117 | continue? (y/N): y 118 | Deleted branch feature/a (was ce195b1ef). 119 | Deleted branch feature/b (was 031b5b312). 120 | Deleted branch feature/c (was ab120153e). 121 | Pruning foo 122 | URL: git@github.com:xxx.git 123 | * [pruned] foo/feature/a 124 | * [pruned] foo/feature/b 125 | ``` 126 | -------------------------------------------------------------------------------- /git-cleanup-branches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import subprocess 4 | import sys 5 | import argparse 6 | 7 | 8 | 9 | parser = argparse.ArgumentParser(description='delete stale branches') 10 | parser.add_argument('--remote', type=str, default='origin', 11 | help='name of the remote to sync with') 12 | parser.add_argument('--all', dest='delete_all', action='store_const', 13 | const=True, default=False, 14 | help='cleanup all stalled branches') 15 | 16 | args = parser.parse_args() 17 | remote_name = args.remote 18 | current_branch = subprocess.check_output( 19 | ['git','rev-parse','--abbrev-ref', 'HEAD'] 20 | ).decode(encoding="utf-8").strip() 21 | 22 | 23 | def parse_vv(): 24 | r = re.compile(r'^\s*\*?\s*(\S+)[^[]+\[([^]]+)\]') 25 | b = subprocess.check_output(['git','branch','-vv']) 26 | s = b.decode(encoding="utf-8") 27 | for line in s.splitlines(): 28 | m = r.match(line) 29 | if m is not None: 30 | yield {'local': m[1], 'remote':m[2]} 31 | 32 | def parse_prune(): 33 | r = re.compile(r'^\s*\*?\s*\[would prune\] (\S+)') 34 | b = subprocess.check_output(['git','remote', 'prune', remote_name, '--dry-run']) 35 | s = b.decode(encoding="utf-8") 36 | for line in s.splitlines(): 37 | m = r.match(line) 38 | if m is not None: 39 | yield m[1] 40 | 41 | def parse_merged(): 42 | r = re.compile(r'^\s*([^*]+)') 43 | b = subprocess.check_output(['git','branch', '--merged']) 44 | s = b.decode(encoding="utf-8") 45 | for line in s.splitlines(): 46 | m = r.match(line) 47 | if m is not None: 48 | yield m[1] 49 | 50 | def yes_or_no(question): 51 | reply = str(input(question+' (y/N): ')).lower().strip() 52 | if len(reply) == 1 and reply[0] == 'y': 53 | return True 54 | if len(reply) == 1 and reply[0] == 'n': 55 | return False 56 | if len(reply) == 0: 57 | return False 58 | else: 59 | return yes_or_no("please enter y or n") 60 | 61 | 62 | pb = [x for x in parse_prune()] 63 | lb = [x['local'] for x in parse_vv() if x['remote'] in pb] 64 | mb = [x for x in parse_merged() ] 65 | 66 | 67 | bd = [] 68 | bk = [] 69 | for item in lb: 70 | to_delete = args.delete_all 71 | if to_delete is False: 72 | to_delete = yes_or_no( 73 | ' prune: {} was deleted in {}. delete?'.format( 74 | item, 75 | remote_name, 76 | ) 77 | ) 78 | if to_delete: 79 | bd.append({'branch':item, 'reason':'prune '+remote_name}) 80 | else: 81 | bk.append(item) 82 | 83 | 84 | for item in [x for x in mb if x not in lb]: 85 | to_delete = args.delete_all 86 | if to_delete is False: 87 | to_delete = yes_or_no( 88 | 'merged: {} was already merged to {}. delete?'.format( 89 | item, 90 | current_branch, 91 | ) 92 | ) 93 | if to_delete: 94 | bd.append({'branch':item, 'reason':'merged into '+current_branch}) 95 | else: 96 | bk.append(item) 97 | 98 | 99 | if len(bk) != 0: 100 | s = '\n- '.join(bk) 101 | print('\nlocal branches to keep:\n-', s) 102 | 103 | if len(bd) == 0: 104 | sys.exit(0) 105 | 106 | if len(bd) != 0: 107 | s = '\n- '.join('{} ({})'.format(x['branch'], x['reason']) 108 | for x in bd) 109 | print('\nlocal branches to delete:\n-', s) 110 | 111 | if yes_or_no( 112 | '\nif you will delete those branches you will' 113 | '\nnot be able to restore them from remote.' 114 | '\ncontinue?') is False: 115 | sys.exit(0) 116 | 117 | for item in bd: 118 | subprocess.call(["git","branch","-D", item['branch']]) 119 | 120 | if len([x for x in bk if x in lb]) > 0: 121 | if not yes_or_no( 122 | '\nstale branches in {0} are about to be pruned.' 123 | '\nif you will press y now it would be impossible' 124 | '\nto link your stale local branches to stale' 125 | '\nremote branches in {0}.' 126 | '\n\nprune remote {0}?'.format(remote_name)): 127 | sys.exit(0) 128 | 129 | subprocess.call(["git","remote", "prune", remote_name]) 130 | --------------------------------------------------------------------------------