├── .gitignore ├── LICENSE.txt ├── README.rst └── git-blast /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.o 3 | *.pyc 4 | *.sqlite3 5 | .*.sw[a-z] 6 | .sw[a-z] 7 | *~ 8 | .DS_Store 9 | bin-debug/* 10 | bin-release/* 11 | bin/* 12 | tags 13 | *.beam 14 | *.dump 15 | env/ 16 | .env/ 17 | *egg-info* 18 | misc/ 19 | dist/ 20 | Icon? 21 | node_modules/ 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | tl;dr: all code code is licensed under simplified BSD, unless stated otherwise. 2 | 3 | Unless stated otherwise in the source files, all code is copyright 2017 David 4 | Wolever . All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY DAVID WOLEVER ``AS IS'' AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 | EVENT SHALL DAVID WOLEVER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 20 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 23 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 24 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | The views and conclusions contained in the software and documentation are those 28 | of the authors and should not be interpreted as representing official policies, 29 | either expressed or implied, of David Wolever. 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | git-blast: show git branches sorted by last commit date 2 | ======================================================= 3 | 4 | ``git-blast`` (Branch LAST) shows git branches ordered by the date of the last commit, including the committer and commit message: 5 | 6 | .. image:: https://i.imgur.com/2VhqNWT.png 7 | 8 | 9 | Installation 10 | ============ 11 | 12 | With `Homebrew`__:: 13 | 14 | $ brew install wolever/git-blast/git-blast 15 | 16 | __ https://brew.sh/ 17 | 18 | Manually:: 19 | 20 | $ curl https://raw.githubusercontent.com/wolever/git-blast/master/git-blast -o /usr/local/bin/git-blast 21 | $ chmod +x /usr/local/bin/git-blast 22 | -------------------------------------------------------------------------------- /git-blast: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Shows git branches sorted by last commit date, noting when branch has been 4 | merged: 5 | 6 | $ git blast 7 | * master 33 minutes ago 8 | some-feature 4 days ago [M] 9 | dev-branch 4 days ago 10 | legacy-branch 5 days ago [M] 11 | another-feature 4 months ago 12 | """ 13 | 14 | from __future__ import print_function 15 | 16 | import os 17 | import argparse 18 | import subprocess as sp 19 | 20 | def get_terminal_size(): 21 | # copy-paste from StackOverflow: https://stackoverflow.com/a/3010495/71522 22 | # No idea if this will work on Windows and friends; PRs very appreciated 23 | import fcntl, termios, struct 24 | th, tw, hp, wp = struct.unpack('HHHH', 25 | fcntl.ioctl(0, termios.TIOCGWINSZ, 26 | struct.pack('HHHH', 0, 0, 0, 0))) 27 | return tw, th 28 | 29 | def xcall(cmd): 30 | return sp.check_output(cmd).decode("utf-8") 31 | 32 | C_GREEN = '\033[0;32m' 33 | C_BLUE = '\033[0;34m' 34 | C_RESET = '\033[0;0m' 35 | C_BLACK = '\033[30;1m' 36 | 37 | parser = argparse.ArgumentParser(description="Show git branches, sorted by last commit date") 38 | parser.add_argument( 39 | "-a", "--all", action="store_true", 40 | help="Show all refs, not just local heads (equivilent to `git blast /`)", 41 | ) 42 | parser.add_argument( 43 | "-n", type=int, default=None, help="Number of branches to show (use 0 to disable)", 44 | ) 45 | parser.add_argument("PATTERN", nargs="?", default="heads/", help=( 46 | "Show refs matching PATTERN. Use `/` for all refs, tags/` for tags, " 47 | "`remotes/` for all remote branches, and `origin` for branches on the " 48 | "remote named `origin`. This is similar to the `` option " 49 | "of `git for-each-ref`, except it will automatically prefix remotes " 50 | "with `remotes/`, and prefix everything with `refs/`." 51 | )) 52 | 53 | def main(args): 54 | if args.n is None and not args.all: 55 | args.n = 30 56 | 57 | cur_branch = xcall("git rev-parse --abbrev-ref HEAD".split()).strip() 58 | merged_branches = set([ 59 | x.split()[-1] for x 60 | in xcall("git branch -a --merged".split()).splitlines() 61 | ]) 62 | 63 | pattern = ("/" if args.all else args.PATTERN).lstrip("/") 64 | if pattern and "/" not in pattern: 65 | remotes = set([x.strip() for x in xcall("git remote".split()).splitlines()]) 66 | if pattern in remotes: 67 | pattern = "remotes/%s" %(pattern, ) 68 | 69 | by_date = xcall([ 70 | "git", "for-each-ref", "--sort=-committerdate", 71 | "refs/%s" %(pattern, ), 72 | "--format=%(refname)%09%(committerdate:relative)%09%(authorname) %(authoremail)%09%(contents:subject)", 73 | ]) 74 | lines = by_date.splitlines() 75 | term_width, _ = get_terminal_size() 76 | for idx, line in enumerate(lines): 77 | if args.n and idx >= args.n: 78 | break 79 | 80 | branch, _, rest = line.partition("\t") 81 | 82 | # Take out the initial refs/ and possibly refs/heads/ from the branch 83 | # name 84 | _, _, branch = branch.partition("/") 85 | if branch.startswith("heads/"): 86 | _, _, branch = branch.partition("/") 87 | 88 | output = "" 89 | if branch == cur_branch: 90 | output += "* %s%s" %(C_GREEN, branch) 91 | else: 92 | output += " %s" %(branch, ) 93 | (date, author, subject) = rest.split("\t") 94 | output += " %s%s%s" %(C_BLUE, date, C_RESET) 95 | if branch in merged_branches and branch != cur_branch: 96 | output += " [%sM%s]" %(C_GREEN, C_RESET) 97 | output += " %s%s: %s" %(C_BLACK, author, subject) 98 | if term_width and term_width < len(output): 99 | output = output[:term_width - 3] + "..." 100 | output += C_RESET 101 | print(output) 102 | 103 | if args.n and len(lines) >= args.n: 104 | print("(... and %s more ...)" %(len(lines) - args.n)) 105 | 106 | 107 | if __name__ == "__main__": 108 | args = parser.parse_args() 109 | main(args) 110 | --------------------------------------------------------------------------------