├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── git-recent └── misc ├── IDE files └── git-recent.wpr ├── generate_authors.py └── git-recent.gif /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | __pycache__/ 3 | 4 | .tox/ 5 | .pytest_cache/ 6 | .mypy_cache/ 7 | 8 | dist/ 9 | build/ 10 | *.egg-info/ 11 | 12 | *.bak 13 | 14 | *.wpu 15 | 16 | .coverage 17 | htmlcov 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ram Rachum 2 | Chris Friedman 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ram Rachum and collaborators 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-recent - Quickly check out your favorite branches # 2 | 3 | Do you often find yourself switching between a handful of branches? If it's 4 | more than 2 branches, `git checkout -` won't cut it. Give these typing fingers 5 | a rest with `git-recent`. 6 | 7 | ![git-recent usage](https://i.imgur.com/HFQfJm9.gif) 8 | 9 | Running `git recent` shows a nice menu with the 5 recently checked-out 10 | branches, and lets you check them out in 2 keystrokes. 11 | 12 | 13 | # Installation # 14 | 15 | Copy-paste this line to Bash and run: 16 | 17 | ```console 18 | curl https://raw.githubusercontent.com/cool-RR/git-recent/master/git-recent -o ~/bin/git-recent && chmod +x ~/bin/git-recent 19 | ``` 20 | 21 | Give `git recent` a test run to make sure you did it right. 22 | 23 | # Manual installation # 24 | 25 | Copy the git-recent file above to `~/bin/` or anywhere that's on your path, 26 | chmod it to be executable, and give `git recent` a test run to make sure you 27 | did it right. 28 | 29 | # Alias # 30 | 31 | I like to use `git rc` as a shorter alias to `git recent`. This command would 32 | add that alias: 33 | 34 | ```console 35 | $ git config --global alias.rc recent 36 | ``` 37 | 38 | # Shameless plug # 39 | 40 | I've quit my job recently and I'm looking for my next home! If you're hiring 41 | and you're looking for a gray-bearded Pythonista, [shoot me an 42 | email](mailto:ram@rachum.com) and I'll send you my CV. 43 | 44 | I'm thinking mostly of Berlin or Munich, but anywhere in the EU could work. 45 | 46 | 47 | # License # 48 | 49 | Copyright (c) 2020 Ram Rachum and collaborators, released under the MIT license. 50 | -------------------------------------------------------------------------------- /git-recent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2020 Ram Rachum and collaborators. 3 | # This program is distributed under the MIT license. 4 | 5 | import subprocess 6 | import re 7 | import sys 8 | import textwrap 9 | 10 | __version__ = '0.2.0' 11 | 12 | 13 | if sys.version_info.major == 2: 14 | input = raw_input 15 | 16 | class BadUsage(Exception): 17 | pass 18 | 19 | class BadUsageShowHelp(BadUsage): 20 | pass 21 | 22 | 23 | class ShellColor: 24 | HEADER = '\033[95m' 25 | OKBLUE = '\033[94m' 26 | OKGREEN = '\033[92m' 27 | WARNING = '\033[93m' 28 | FAIL = '\033[91m' 29 | ENDC = '\033[0m' 30 | BOLD = '\033[1m' 31 | UNDERLINE = '\033[4m' 32 | 33 | 34 | def git_output(*args): 35 | '''Launch a Git command, output would be returned rather than printed.''' 36 | return subprocess.check_output(('git',) + args).decode() 37 | 38 | def git(*args): 39 | '''Launch a Git command, with the output written to console normally.''' 40 | return subprocess.check_call(('git',) + args) 41 | 42 | 43 | def remove_duplicates(iterable): 44 | '''Iterate over an iterable with duplicates removed''' 45 | visited = set() 46 | for item in iterable: 47 | if item in visited: 48 | continue 49 | else: 50 | visited.add(item) 51 | yield item 52 | 53 | 54 | def get_recent_branches(): 55 | '''Get a tuple of recently checked-out branches.''' 56 | text = git_output('reflog') 57 | matches = re.finditer(r'checkout: moving from .* to (.*)', text) 58 | return tuple(remove_duplicates(match.group(1) for match in matches)) 59 | 60 | 61 | def git_recent(n_branches=5): 62 | ''' 63 | Show a menu of recently checked-out branches and offer to check them out. 64 | ''' 65 | recent_branches = get_recent_branches()[:n_branches] 66 | if not recent_branches: 67 | print('No branches.') 68 | return 69 | for i, branch in enumerate(recent_branches, start=1): 70 | def format_number(i): 71 | if len(recent_branches) <= 9: 72 | return '[%s]' % (i,) 73 | else: 74 | return '[%2s]' % (i,) 75 | 76 | print( 77 | '%s%s%s git checkout %s' % ( 78 | ShellColor.WARNING, format_number(i), ShellColor.ENDC, branch 79 | ) 80 | ) 81 | input_text = input( 82 | '%sChoose by number (1-%s): %s' % ( 83 | ShellColor.OKBLUE, len(recent_branches), ShellColor.ENDC 84 | ) 85 | ).strip() 86 | if input_text in ('', 'q', 'quit', 'exit'): 87 | return 88 | try: 89 | input_number = int(input_text) 90 | except ValueError: 91 | raise BadUsage("That's not a number.") 92 | 93 | if not (1 <= input_number <= len(recent_branches)): 94 | raise BadUsage( 95 | 'That number is not in the range 1-%s.' % (len(recent_branches),) 96 | ) 97 | 98 | branch = recent_branches[input_number - 1] 99 | print('git checkout %s' % (branch,)) 100 | git('checkout', branch) 101 | 102 | 103 | def show_help(): 104 | print(textwrap.dedent('''\ 105 | git recent - Quickly check out your favorite branches 106 | 107 | This command shows a nice menu letting you check out one of your 108 | recent branches with just two keystrokes. 109 | 110 | Simple usage: 111 | 112 | git recent 113 | 114 | 115 | Advanced usage: 116 | 117 | git recent 10 # Show the last 10 branches instead of 5 118 | 119 | 120 | Copyright 2020 Ram Rachum and collaborators. MIT license. 121 | ''')) 122 | 123 | 124 | def git_recent_cli(): 125 | '''Process command line argument and run git-recent.''' 126 | try: 127 | arguments = sys.argv[1:] 128 | if len(arguments) >= 2: 129 | raise BadUsageShowHelp( 130 | 'Error: Too many arguments' 131 | ) 132 | elif len(arguments) == 1: 133 | (argument,) = arguments 134 | if argument in ('-h', '--help'): 135 | show_help() 136 | sys.exit() 137 | try: 138 | n_branches = int(argument) 139 | except ValueError: 140 | raise BadUsageShowHelp( 141 | textwrap.dedent('''\ 142 | Error: You passed an argument %s which is not a number. 143 | The only argument we accept should be the number of 144 | branches to show. 145 | ''' % (argument,)) 146 | ) 147 | git_recent(n_branches) 148 | else: 149 | # No arguments 150 | git_recent() 151 | except BadUsageShowHelp as bad_usage_show_help: 152 | print(bad_usage_show_help.args[0]) 153 | show_help() 154 | sys.exit(129) 155 | except BadUsage as bad_usage: 156 | print(bad_usage.args[0]) 157 | sys.exit(129) 158 | 159 | 160 | 161 | 162 | 163 | 164 | if __name__ == '__main__': 165 | git_recent_cli() 166 | 167 | 168 | -------------------------------------------------------------------------------- /misc/IDE files/git-recent.wpr: -------------------------------------------------------------------------------- 1 | #!wing 2 | #!version=7.0 3 | ################################################################## 4 | # Wing project file # 5 | ################################################################## 6 | [project attributes] 7 | proj.directory-list = [{'dirloc': loc('../..'), 8 | 'excludes': [u'git-recent.egg-info', 9 | u'dist', 10 | u'build'], 11 | 'filter': '*', 12 | 'include_hidden': False, 13 | 'recursive': True, 14 | 'watch_for_changes': True}] 15 | proj.file-type = 'shared' 16 | proj.home-dir = loc('../..') 17 | proj.launch-config = {} 18 | testing.auto-test-file-specs = (('regex', 19 | 'git-recent/tests.*/test[^./]*.py.?$'),) 20 | testing.test-framework = {None: ':internal pytest'} 21 | -------------------------------------------------------------------------------- /misc/generate_authors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2019 Ram Rachum and collaborators. 3 | # This program is distributed under the MIT license. 4 | 5 | 6 | ''' 7 | Generate an AUTHORS file for your Git repo. 8 | 9 | This will list the authors by chronological order, from their first 10 | contribution. 11 | 12 | You probably want to run it this way: 13 | 14 | ./generate_authors > AUTHORS 15 | 16 | ''' 17 | 18 | 19 | import subprocess 20 | import sys 21 | 22 | 23 | def drop_recurrences(iterable): 24 | s = set() 25 | for item in iterable: 26 | if item not in s: 27 | s.add(item) 28 | yield item 29 | 30 | 31 | def iterate_authors_by_chronological_order(branch): 32 | log_call = subprocess.run( 33 | ( 34 | 'git', 'log', branch, '--encoding=utf-8', '--full-history', 35 | '--reverse', '--format=format:%at;%an;%ae' 36 | ), 37 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 38 | ) 39 | log_lines = log_call.stdout.decode('utf-8').split('\n') 40 | 41 | return drop_recurrences( 42 | (line.strip().split(";")[1] for line in log_lines) 43 | ) 44 | 45 | 46 | def print_authors(branch): 47 | for author in iterate_authors_by_chronological_order(branch): 48 | sys.stdout.buffer.write(author.encode()) 49 | sys.stdout.buffer.write(b'\n') 50 | 51 | 52 | if __name__ == '__main__': 53 | try: 54 | branch = sys.argv[1] 55 | except IndexError: 56 | branch = 'master' 57 | print_authors(branch) 58 | -------------------------------------------------------------------------------- /misc/git-recent.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cool-RR/git-recent/f9a8c9dbf1aa749ebc164cbc9cafcb848358a8fc/misc/git-recent.gif --------------------------------------------------------------------------------