├── .gitignore ├── AUTHORS ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO ├── bin └── git-goggles ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── overview.rst ├── gitgoggles ├── __init__.py ├── asciitable.py ├── codereview.py ├── git.py ├── progress.py └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | \#*# 4 | *.pyc 5 | doc/_build/ 6 | MANIFEST 7 | dist/ 8 | build/ -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nowell Strite 2 | Thibaud Morel l'Horset 3 | Shawn Rider 4 | Keith Rarick 5 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install ``git-goggles`` either via the Python Package Index (PyPI) 5 | or from source. 6 | 7 | To install using ``pip``,:: 8 | 9 | $ pip install git-goggles 10 | 11 | 12 | To install using ``easy_install``,:: 13 | 14 | $ easy_install git-goggles 15 | 16 | 17 | If you have downloaded a source tarball you can install it 18 | by doing the following,:: 19 | 20 | $ python setup.py build 21 | # python setup.py install # as root 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Nowell Strite 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include AUTHORS 4 | include MANIFEST.in 5 | include INSTALL 6 | 7 | include docs/Makefile 8 | include docs/conf.py 9 | recursive-include docs *.rst 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | git-goggles Readme 3 | ####################### 4 | 5 | git-goggles is a git management utilities that allows you to manage your source code as 6 | it evolves through its development lifecycle. 7 | 8 | Overview 9 | ======== 10 | 11 | This project accomplishes two things: 12 | 13 | * Manage the code review state of your branches 14 | * Gives a snapshot of the where your local branches are vs origin in terms of being ahead / behind on commits 15 | 16 | There is a nice blog post describing the features along with screenshots at http://bit.ly/git-goggles 17 | 18 | Field Reference 19 | =============== 20 | 21 | In the table outputted by git-goggles, each row corresponds to a branch, with the following fields: 22 | 23 | * Status: the current status of your branch 24 | 25 | * new: this is a branch that has never been through the review process 26 | * review: this branch has code that needs to be reviewed 27 | * merge: everything has been reviewed, but needs to be merged into parent (same as done for being ahead) 28 | * done: reviewed and merged (doens't matter if you're behind but you can't be ahead) 29 | 30 | * Branch: the branch name 31 | 32 | * Review: how many commits have taken place since the last review 33 | 34 | * Ahead: how many commits are in your local branch that are not in origin 35 | 36 | * Behind: how many commits are in origin that are not in your local branch 37 | 38 | * Pull & Push: whether your branches need to be pushed or pulled to track origin 39 | 40 | * green checkbox: you don't need to pull 41 | * red cross: you need to pull 42 | * question mark: you either don't have a checked out copy of this branch or you need to prune your local tree 43 | 44 | * Modified: the last time that HEAD was modified (NOT the last time the review happened) 45 | 46 | Installation 47 | ============ 48 | 49 | To install from PyPi you should run one of the following commands. (If you use pip for your package installation, you should take a look!) 50 | 51 | :: 52 | 53 | pip install git-goggles 54 | 55 | or 56 | 57 | :: 58 | 59 | easy_install git-goggles 60 | 61 | Checkout the project from github http://github.com/nowells/git-goggles 62 | 63 | :: 64 | 65 | git clone git://github.com/nowells/git-goggles.git 66 | 67 | Run setup.py as root 68 | 69 | :: 70 | 71 | cd git-goggles 72 | sudo python setup.py install 73 | 74 | **Documentation**: 75 | With `Sphinx `_ docs deployment: in the docs/ directory, type: 76 | 77 | :: 78 | 79 | make html 80 | 81 | Then open ``docs/_build/index.html`` 82 | 83 | Usage 84 | ===== 85 | 86 | Viewing the status of your branches: 87 | 88 | :: 89 | 90 | git goggles 91 | 92 | Starting your review process (shows an origin diff): 93 | 94 | :: 95 | 96 | git goggles codereview 97 | 98 | Complete your review process (automatically pushes up): 99 | 100 | :: 101 | 102 | git goggles codereview complete 103 | 104 | Configuration 105 | ============= 106 | 107 | You can set a few configuration variables to alter to way git-goggles works out of the box. 108 | 109 | Disable automatic fetching from all remote servers. 110 | 111 | :: 112 | 113 | git config --global gitgoggles.fetch false 114 | 115 | Disable colorized output 116 | 117 | :: 118 | 119 | git config --global gitgoggles.colors false 120 | 121 | Alter the symbols used to display success, failure, unknown states 122 | 123 | :: 124 | 125 | git config --global gitgoggles.icons.success "OK" 126 | git config --global gitgoggles.icons.failure "FAIL" 127 | git config --global gitgoggles.icons.unknown "N/A" 128 | 129 | Alter the colors of branch states. The available colors are [grey, red, green, yellow, blue, magenta, cyan, white] 130 | 131 | :: 132 | 133 | git config --global gitgoggles.colors.local cyan 134 | git config --global gitgoggles.colors.new red 135 | git config --global gitgoggles.colors.review red 136 | git config --global gitgoggles.colors.merge yellow 137 | git config --global gitgoggles.colors.done green 138 | 139 | Alter the width of branch column to turn on wordwrap. 140 | 141 | :: 142 | 143 | git config --global gitgoggles.table.branch-width 15 144 | 145 | Alter the table cell padding (defaults to 0) 146 | 147 | :: 148 | 149 | git config --global gitgoggles.table.left-padding 1 150 | git config --global gitgoggles.table.right-padding 1 151 | 152 | Alter the display of horizontal rule between rows of table (default false) 153 | 154 | :: 155 | 156 | git config --global gitgoggles.table.horizontal-rule true 157 | 158 | Internals 159 | ========= 160 | 161 | git-goggles works by creating and managing special tags called 162 | 'codereview-' and tracking them against HEAD. 163 | 164 | The first time a codereview is completed, the tag is created. Subsequent 165 | reviews delete and re-create the tag so that it awlays accurately tracks HEAD. -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | # "git remote prune " suggestion. Handle the case where a remote branch no longer exists, but git does not know about it yet. 2 | # Make current "master" branch name configurable to allow for better calculation of ahead/behind 3 | -------------------------------------------------------------------------------- /bin/git-goggles: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | 6 | from gitgoggles.codereview import get_status, update_branches 7 | from gitgoggles.progress import enable_progress 8 | 9 | def handle_command(): 10 | if not os.path.exists('.git'): 11 | print 'Not within a git repository.' 12 | sys.exit(1) 13 | 14 | if len(sys.argv) == 1: 15 | enable_progress() 16 | get_status() 17 | elif len(sys.argv) >= 2: 18 | if sys.argv[1] == 'update': 19 | update_branches() 20 | else: 21 | print 'usage: git goggles [update]' 22 | 23 | if __name__ == "__main__": 24 | try: 25 | handle_command() 26 | except KeyboardInterrupt: 27 | print 'Exiting.' 28 | sys.exit(1) 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " devhelp to make HTML files and a Devhelp project" 26 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 27 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 28 | @echo " changes to make an overview of all changed/added/deprecated items" 29 | @echo " linkcheck to check all external links for integrity" 30 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 31 | 32 | clean: 33 | -rm -rf $(BUILDDIR)/* 34 | 35 | html: 36 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 37 | @echo 38 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 39 | 40 | dirhtml: 41 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 42 | @echo 43 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 44 | 45 | pickle: 46 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 47 | @echo 48 | @echo "Build finished; now you can process the pickle files." 49 | 50 | json: 51 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 52 | @echo 53 | @echo "Build finished; now you can process the JSON files." 54 | 55 | htmlhelp: 56 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 57 | @echo 58 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 59 | ".hhp project file in $(BUILDDIR)/htmlhelp." 60 | 61 | qthelp: 62 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 63 | @echo 64 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 65 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 66 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/git-codereview.qhcp" 67 | @echo "To view the help file:" 68 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/git-codereview.qhc" 69 | 70 | devhelp: 71 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) _build/devhelp 72 | @echo 73 | @echo "Build finished." 74 | @echo "To view the help file:" 75 | @echo "# mkdir -p $$HOME/.local/share/devhelp/git-codereview" 76 | @echo "# ln -s _build/devhelp $$HOME/.local/share/devhelp/git-codereview" 77 | @echo "# devhelp" 78 | 79 | latex: 80 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 81 | @echo 82 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 83 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 84 | "run these through (pdf)latex." 85 | 86 | latexpdf: latex 87 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 88 | @echo "Running LaTeX files through pdflatex..." 89 | make -C _build/latex all-pdf 90 | @echo "pdflatex finished; the PDF files are in _build/latex." 91 | 92 | changes: 93 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 94 | @echo 95 | @echo "The overview file is in $(BUILDDIR)/changes." 96 | 97 | linkcheck: 98 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 99 | @echo 100 | @echo "Link check complete; look for any errors in the above output " \ 101 | "or in $(BUILDDIR)/linkcheck/output.txt." 102 | 103 | doctest: 104 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 105 | @echo "Testing of doctests in the sources finished, look at the " \ 106 | "results in $(BUILDDIR)/doctest/output.txt." 107 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # git-codereview documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Dec 31 10:38:41 2009. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.append(os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = [] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ['_templates'] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = '.rst' 32 | 33 | # The encoding of source files. 34 | #source_encoding = 'utf-8-sig' 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # General information about the project. 40 | project = u'git-codereview' 41 | copyright = u'2009, Nowell Strike, Thibaud Morel l\'Horset' 42 | 43 | # The version info for the project you're documenting, acts as replacement for 44 | # |version| and |release|, also used in various other places throughout the 45 | # built documents. 46 | # 47 | # The short X.Y version. 48 | version = '0.1' 49 | # The full version, including alpha/beta/rc tags. 50 | release = '0.1' 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | #language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | #today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | #today_fmt = '%B %d, %Y' 61 | 62 | # List of documents that shouldn't be included in the build. 63 | #unused_docs = [] 64 | 65 | # List of directories, relative to source directory, that shouldn't be searched 66 | # for source files. 67 | exclude_trees = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. Major themes that come with 93 | # Sphinx are currently 'default' and 'sphinxdoc'. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_use_modindex = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = '' 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'git-codereviewdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'git-codereview.tex', u'git-codereview Documentation', 182 | u'Nowell Strike, Thibaud Morel l\'Horset', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #latex_preamble = '' 195 | 196 | # Documents to append as an appendix to all manuals. 197 | #latex_appendices = [] 198 | 199 | # If false, no module index is generated. 200 | #latex_use_modindex = True 201 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. git-goggles documentation master file, created by 2 | sphinx-quickstart on Thu Dec 31 10:38:41 2009. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to git-goggles's documentation! 7 | ========================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | overview 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set SPHINXBUILD=sphinx-build 6 | set BUILDDIR=_build 7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 8 | if NOT "%PAPER%" == "" ( 9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 10 | ) 11 | 12 | if "%1" == "" goto help 13 | 14 | if "%1" == "help" ( 15 | :help 16 | echo.Please use `make ^` where ^ is one of 17 | echo. html to make standalone HTML files 18 | echo. dirhtml to make HTML files named index.html in directories 19 | echo. pickle to make pickle files 20 | echo. json to make JSON files 21 | echo. htmlhelp to make HTML files and a HTML help project 22 | echo. qthelp to make HTML files and a qthelp project 23 | echo. devhelp to make HTML files and a Devhelp project 24 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 25 | echo. changes to make an overview over all changed/added/deprecated items 26 | echo. linkcheck to check all external links for integrity 27 | echo. doctest to run all doctests embedded in the documentation if enabled 28 | goto end 29 | ) 30 | 31 | if "%1" == "clean" ( 32 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 33 | del /q /s %BUILDDIR%\* 34 | goto end 35 | ) 36 | 37 | if "%1" == "html" ( 38 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 39 | echo. 40 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 41 | goto end 42 | ) 43 | 44 | if "%1" == "dirhtml" ( 45 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 48 | goto end 49 | ) 50 | 51 | if "%1" == "pickle" ( 52 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 53 | echo. 54 | echo.Build finished; now you can process the pickle files. 55 | goto end 56 | ) 57 | 58 | if "%1" == "json" ( 59 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 60 | echo. 61 | echo.Build finished; now you can process the JSON files. 62 | goto end 63 | ) 64 | 65 | if "%1" == "htmlhelp" ( 66 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 67 | echo. 68 | echo.Build finished; now you can run HTML Help Workshop with the ^ 69 | .hhp project file in %BUILDDIR%/htmlhelp. 70 | goto end 71 | ) 72 | 73 | if "%1" == "qthelp" ( 74 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 75 | echo. 76 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 77 | .qhcp project file in %BUILDDIR%/qthelp, like this: 78 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\git-codereview.qhcp 79 | echo.To view the help file: 80 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\git-codereview.ghc 81 | goto end 82 | ) 83 | 84 | if "%1" == "devhelp" ( 85 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% _build/devhelp 86 | echo. 87 | echo.Build finished. 88 | goto end 89 | ) 90 | 91 | if "%1" == "latex" ( 92 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 93 | echo. 94 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 95 | goto end 96 | ) 97 | 98 | if "%1" == "changes" ( 99 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 100 | echo. 101 | echo.The overview file is in %BUILDDIR%/changes. 102 | goto end 103 | ) 104 | 105 | if "%1" == "linkcheck" ( 106 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 107 | echo. 108 | echo.Link check complete; look for any errors in the above output ^ 109 | or in %BUILDDIR%/linkcheck/output.txt. 110 | goto end 111 | ) 112 | 113 | if "%1" == "doctest" ( 114 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 115 | echo. 116 | echo.Testing of doctests in the sources finished, look at the ^ 117 | results in %BUILDDIR%/doctest/output.txt. 118 | goto end 119 | ) 120 | 121 | :end 122 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst -------------------------------------------------------------------------------- /gitgoggles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nowells/git-goggles/022dc0cd6dfe8f1641ccb33e85ab05309dba7dbf/gitgoggles/__init__.py -------------------------------------------------------------------------------- /gitgoggles/asciitable.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from gitgoggles.utils import force_unicode, force_str, console, colored 4 | 5 | class AsciiCell(object): 6 | def __init__(self, value, color=None, background=None, reverse=False, width=None, align='left', resizable=False): 7 | self.value = force_unicode(value) 8 | self.color = color 9 | self.align = align 10 | self.resizable = resizable 11 | self.background = background 12 | self.attrs = reverse and ['reverse'] or [] 13 | self.width = width and int(width) or len(self.value) 14 | 15 | def lines(self): 16 | if self.width: 17 | return int(math.ceil(len(self.value) / float(self.width))) 18 | return self.width 19 | lines = property(lines) 20 | 21 | def line(self, num): 22 | return self.value[num * self.width:(1 + num) * self.width] 23 | 24 | def pad(self, text, width): 25 | if self.align == 'right': 26 | return text.rjust(width) 27 | elif self.align == 'center': 28 | return text.center(width) 29 | else: 30 | return text.ljust(width) 31 | 32 | class AsciiRow(object): 33 | def __init__(self, *cells): 34 | super(AsciiRow, self).__init__() 35 | self.cells = [ isinstance(x, AsciiCell) and x or AsciiCell(x) for x in cells ] 36 | 37 | def lines(self): 38 | return max([ x.lines for x in self.cells ]) 39 | lines = property(lines) 40 | 41 | def __getitem__(self, index): 42 | return self.cells[index] 43 | 44 | def __iter__(self): 45 | for cell in self.cells: 46 | yield cell 47 | 48 | def __len__(self): 49 | return len(self.cells) 50 | 51 | class AsciiTable(object): 52 | def __init__(self, headers, left_padding=None, right_padding=None, horizontal_rule=True, max_width=None, border_characters=[u'+', u'|', u'-']): 53 | self.headers = AsciiRow(*headers) 54 | self.rows = [] 55 | self._widths = [ x.width for x in self.headers ] 56 | self.left_padding = left_padding and int(left_padding) or 0 57 | self.right_padding = right_padding and int(right_padding) or 0 58 | self.horizontal_rule = horizontal_rule 59 | self.max_width = max_width 60 | self.border_corner = border_characters[0] 61 | self.border_vertical = border_characters[1] 62 | self.border_horizontal = border_characters[2] 63 | 64 | def add_row(self, data): 65 | if len(data) != len(self.headers): 66 | raise Exception('The number of columns in a row must be equal to the header column count.') 67 | self.rows.append(AsciiRow(*data)) 68 | 69 | def __str__(self): 70 | self.__unicode__() 71 | 72 | def __unicode__(self): 73 | self._print() 74 | 75 | def _print_horizontal_rule(self): 76 | if self.border_corner or self.border_horizontal: 77 | console(self.border_corner) 78 | for column, width in zip(self.headers, self._widths): 79 | console(self.border_horizontal * (self.right_padding + self.left_padding + width)) 80 | console(self.border_corner) 81 | console(u'\n') 82 | 83 | def _print_headers(self): 84 | self._print_horizontal_rule() 85 | self._print_row(self.headers) 86 | self._print_horizontal_rule() 87 | 88 | def _print_rows(self): 89 | for row in self.rows: 90 | self._print_row(row) 91 | if self.horizontal_rule: 92 | self._print_horizontal_rule() 93 | 94 | def _print_row(self, row): 95 | for line in xrange(row.lines): 96 | console(self.border_vertical) 97 | for cell, width in zip(row, self._widths): 98 | console(colored(u' ' * self.left_padding + cell.pad(cell.line(line), width) + u' ' * self.right_padding, cell.color, cell.background, attrs=cell.attrs)) 99 | console(self.border_vertical) 100 | console(u'\n') 101 | 102 | def render(self): 103 | self._calculate_widths() 104 | 105 | self._print_headers() 106 | self._print_rows() 107 | if not self.horizontal_rule: 108 | self._print_horizontal_rule() 109 | 110 | def _calculate_widths(self): 111 | for row in self.rows: 112 | for column, cell in enumerate(row): 113 | self._widths[column] = max(self._widths[column], cell.width) 114 | 115 | width = sum([ x for x in self._widths ]) + ((self.left_padding + self.right_padding) * len(self._widths)) + len(self._widths) + 1 116 | if self.max_width and width > self.max_width: 117 | difference = width - self.max_width 118 | # TODO: being lazy right now, but should recalculate resizable columns widths based on percentage of current length (longer columns give up more) 119 | self._widths[1] = max(self._widths[1] - difference, 5) 120 | for row in self.rows + [ self.headers ]: 121 | row[1].width = self._widths[1] 122 | -------------------------------------------------------------------------------- /gitgoggles/codereview.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import subprocess 3 | import itertools 4 | import sys 5 | 6 | from gitgoggles.asciitable import AsciiTable, AsciiCell 7 | from gitgoggles.git import Repository, TrackingBranch, LocalBranch, PublishedBranch, TrackedBranch 8 | from gitgoggles.utils import colored, terminal_dimensions, console 9 | from gitgoggles.progress import handler 10 | 11 | def get_status(): 12 | repo = Repository() 13 | 14 | console(colored('# Working Tree: ', 'magenta')) 15 | console(colored(repo.branch(), 'cyan')) 16 | console(u'\n') 17 | 18 | uncommitted, changed, untracked, stashed = repo.status() 19 | if uncommitted or changed or untracked or stashed: 20 | table = AsciiTable(['', ''], 2, 0, False, border_characters=[u'', u'', u'']) 21 | 22 | if uncommitted: 23 | table.add_row([ 24 | AsciiCell('Uncommitted', 'green'), 25 | AsciiCell(str(len(uncommitted)), align='right'), 26 | ]) 27 | 28 | if changed: 29 | table.add_row([ 30 | AsciiCell('Changed', 'red'), 31 | AsciiCell(str(len(changed)), align='right'), 32 | ]) 33 | 34 | if untracked: 35 | table.add_row([ 36 | AsciiCell('Untracked', 'yellow'), 37 | AsciiCell(str(len(untracked)), align='right'), 38 | ]) 39 | 40 | if stashed: 41 | table.add_row([ 42 | AsciiCell('Stashed', 'cyan'), 43 | AsciiCell(str(len(stashed)), align='right'), 44 | ]) 45 | 46 | table.render() 47 | 48 | console('\n') 49 | 50 | handler.uncapture_stdout() 51 | handler.capture_stdout() 52 | 53 | if repo.configs.get('gitgoggles.fetch', 'true') != 'false': 54 | repo.fetch() 55 | 56 | git_refs = repo.branches(LocalBranch, TrackedBranch, TrackingBranch, PublishedBranch) 57 | tags = repo.tags() 58 | 59 | BRANCH_WIDTH = repo.configs.get('gitgoggles.table.branch-width') 60 | LEFT_PADDING = repo.configs.get('gitgoggles.table.left-padding', 0) 61 | RIGHT_PADDING = repo.configs.get('gitgoggles.table.right-padding', 0) 62 | HORIZONTAL_RULE = repo.configs.get('gitgoggles.table.horizontal-rule', 'false') != 'false' 63 | 64 | TERMINAL_ROWS, TERMINAL_COLUMNS = terminal_dimensions() 65 | 66 | table = AsciiTable([ 67 | AsciiCell('Branch', width=BRANCH_WIDTH, resizable=True), 68 | AsciiCell('Ahead', align='right'), 69 | AsciiCell('Behind', align='right'), 70 | AsciiCell('Pull'), 71 | AsciiCell('Push'), 72 | AsciiCell('Mod', align='right'), 73 | ], LEFT_PADDING, RIGHT_PADDING, HORIZONTAL_RULE, TERMINAL_COLUMNS) 74 | 75 | if repo.configs.get('gitgoggles.colors', 'true') == 'false': 76 | colored.disabled = True 77 | 78 | icons = { 79 | 'unknown': repo.configs.get('gitgoggles.icons.unknown', u'\u203D'), 80 | 'success': repo.configs.get('gitgoggles.icons.success', u'\u2714'), 81 | 'failure': repo.configs.get('gitgoggles.icons.failure', u'\u2718'), 82 | } 83 | colors = { 84 | 'local': repo.configs.get('gitgoggles.colors.local', 'cyan'), 85 | 'new': repo.configs.get('gitgoggles.colors.new', 'red'), 86 | 'review': repo.configs.get('gitgoggles.colors.review', 'red'), 87 | 'merge': repo.configs.get('gitgoggles.colors.merge', 'yellow'), 88 | 'done': repo.configs.get('gitgoggles.colors.done', 'green'), 89 | } 90 | 91 | key_func = lambda x: x.shortname 92 | git_refs = sorted(git_refs, key=key_func) 93 | groups = [ [x, list(y)] for x, y in itertools.groupby(git_refs, key_func) ] 94 | 95 | if repo.configs.get('gitgoggles.sorted', 'age') == 'age': 96 | groups = reversed(sorted(groups, key=lambda refs: min([ x.modified.date() for x in refs[1] ]))) 97 | else: 98 | groups = sorted(groups, key=lambda refs: refs[0]) 99 | 100 | for name, git_refs in groups: 101 | for ref in git_refs: 102 | if repo.configs.get('gitgoggles.ignore.%s' % ref.shortname, 'false') == 'true': 103 | continue 104 | 105 | color = 'red' 106 | ahead_commits = ref.ahead 107 | behind_commits = ref.behind 108 | pull = ref.pull 109 | push = ref.push 110 | 111 | if ref.__class__ == LocalBranch: 112 | color = colors['local'] 113 | else: 114 | if ahead_commits: 115 | color = colors['merge'] 116 | else: 117 | color = colors['done'] 118 | 119 | ahead = bool(ahead_commits) or None 120 | behind = bool(behind_commits) or None 121 | tracked = ref.__class__ in (TrackingBranch, LocalBranch, TrackedBranch) 122 | 123 | ahead_text, ahead_color = ahead_commits is not None and (u'%s ahead' % ahead_commits, ahead and color) or (icons['unknown'], 'yellow',) 124 | behind_text, behind_color = behind_commits is not None and (u'%s behind' % behind_commits, behind and color) or (icons['unknown'], 'yellow',) 125 | 126 | pull_text, pull_color = not tracked and (icons['unknown'], 'yellow',) or (pull and (icons['failure'], 'red',) or (icons['success'], 'green',)) 127 | push_text, push_color = not tracked and (icons['unknown'], 'yellow',) or (push and (icons['failure'], 'red',) or (icons['success'], 'green',)) 128 | 129 | delta = datetime.date.today() - ref.modified.date() 130 | if delta <= datetime.timedelta(days=1): 131 | modified_color = 'cyan' 132 | elif delta <= datetime.timedelta(days=7): 133 | modified_color = 'green' 134 | elif delta < datetime.timedelta(days=31): 135 | modified_color = 'yellow' 136 | else: 137 | modified_color = 'red' 138 | 139 | ahead_color = behind_color = modified_color 140 | 141 | table.add_row([ 142 | AsciiCell(ref.name, width=BRANCH_WIDTH, resizable=True), 143 | AsciiCell(ahead_text, ahead_color, reverse=ahead, align='right'), 144 | AsciiCell(behind_text, behind_color, reverse=behind, align='right'), 145 | AsciiCell(pull_text, pull_color, align='center'), 146 | AsciiCell(push_text, push_color, align='center'), 147 | AsciiCell(ref.timedelta, modified_color, align='right'), 148 | ]) 149 | 150 | table.render() 151 | 152 | def update_branches(): 153 | repo = Repository() 154 | 155 | if repo.configs.get('gitgoggles.fetch', 'true') != 'false': 156 | repo.fetch() 157 | 158 | branch = repo.branch() 159 | refs = repo.branches(TrackingBranch) 160 | for branch in refs: 161 | if branch.pull: 162 | repo.shell('git', 'checkout', branch.name) 163 | repo.shell('git', 'pull') 164 | -------------------------------------------------------------------------------- /gitgoggles/git.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import re 4 | import subprocess 5 | 6 | from gitgoggles.progress import log 7 | from gitgoggles.utils import AccumulatorDict, memoize, force_unicode, force_str, console 8 | 9 | def log_activity(func): 10 | def _(self, *args, **kwargs): 11 | log.info('Processing %s' % self.name) 12 | return func(self, *args, **kwargs) 13 | return _ 14 | 15 | class Ref(object): 16 | def __new__(cls, repo, sha, refspec): 17 | if cls != Ref: 18 | return object.__new__(cls) 19 | 20 | ref_type, name = refspec[5:].partition("/")[0::2] 21 | if ref_type in ('heads', 'remotes',): 22 | return Branch(repo, sha, refspec) 23 | elif ref_type in ('tags',): 24 | return Tag(repo, sha, refspec) 25 | return object.__new__(cls) 26 | 27 | def __init__(self, repo, sha, refspec): 28 | self.repo = repo 29 | self.refspec = refspec 30 | self.sha = sha 31 | 32 | self.ref_type, self.name = refspec[5:].partition("/")[0::2] 33 | self.shortname = re.match('^origin\/', self.name) and self.name.partition("/")[2] or self.name 34 | 35 | def modified(self): 36 | try: 37 | timestamp = float(self.repo.shell('git', 'log', '--no-merges', '-1', '--pretty=format:%at', '%s..%s' % (self.repo.master, self.refspec)).split[0].strip()) 38 | except IndexError: 39 | timestamp = float(self.repo.shell('git', 'log', '--no-merges', '-1', '--pretty=format:%at', self.refspec).split[0].strip()) 40 | return datetime.datetime.fromtimestamp(timestamp) 41 | modified = property(log_activity(memoize(modified))) 42 | 43 | def timedelta(self): 44 | delta = datetime.datetime.now() - self.modified 45 | if delta.days > 365: 46 | return '-%sy' % round(delta.days / 365.0, 1) 47 | elif delta.days >= 1: 48 | return '-%sd' % delta.days 49 | elif delta.seconds >= 3600: 50 | return '-%sh' % (delta.seconds / 3600) 51 | else: 52 | return '-%sm' % (delta.seconds / 60) 53 | timedelta = property(timedelta) 54 | 55 | def __unicode__(self): 56 | return '<%s %s>' % (self.__class__.__name__, self.name) 57 | 58 | def __str__(self): 59 | return self.__unicode__() 60 | 61 | def __repr__(self): 62 | return self.__unicode__() 63 | 64 | class Branch(Ref): 65 | display_name = 'Branch' 66 | 67 | def __new__(cls, repo, sha, refspec): 68 | if cls != Branch: 69 | return object.__new__(cls) 70 | 71 | # This is a local branch, see if it is a tracking branch or a local branch 72 | if refspec.startswith('refs/heads/'): 73 | remote = repo.configs.get('branch.%s.remote' % '/'.join(refspec.split('/')[2:]), '.') 74 | remote = remote != '.' and remote or None 75 | if remote is None: 76 | cls = LocalBranch 77 | else: 78 | cls = TrackingBranch 79 | # This is a remote branch, see if it is a tracked branch or a published branch 80 | else: 81 | if refspec in repo.branch_parents.keys(): 82 | cls = TrackedBranch 83 | else: 84 | cls = PublishedBranch 85 | return object.__new__(cls) 86 | 87 | def __init__(self, *args, **kwargs): 88 | super(Branch, self).__init__(*args, **kwargs) 89 | self.parent_refspec = self.repo.branch_parents.get(self.refspec, self.refspec) 90 | log.info('Processing %s' % self.name) 91 | # TODO: find a better way to determine parent refspec 92 | # Find the common merge ancestor to show ahead/behind statistics. 93 | self.merge_refspec = None 94 | if self.repo.master_sha: 95 | merge_refspec = self.repo.shell('git', 'merge-base', self.repo.master_sha, self.sha).split 96 | if merge_refspec: 97 | self.merge_refspec = merge_refspec[0].strip() 98 | 99 | def pull(self): 100 | return bool(self.repo.shell('git', 'log', '--pretty=format:%H', '%s..%s' % (self.refspec, self.parent_refspec)).split) 101 | pull = property(log_activity(memoize(pull))) 102 | 103 | def push(self): 104 | return bool(self.repo.shell('git', 'log', '--pretty=format:%H', '%s..%s' % (self.parent_refspec, self.refspec)).split) 105 | push = property(log_activity(memoize(push))) 106 | 107 | def ahead(self): 108 | if self.merge_refspec: 109 | return len(self.repo.shell('git', 'log', '--no-merges', '--pretty=format:%H', '%s..%s' % (self.repo.master, self.refspec)).split) 110 | return None 111 | ahead = property(log_activity(memoize(ahead))) 112 | 113 | def behind(self): 114 | if self.merge_refspec: 115 | # TODO: find a better way to determine how fare behind we are from our branch "parent" 116 | return len(self.repo.shell('git', 'log', '--no-merges', '--pretty=format:%H', '%s..%s' % (self.refspec, self.repo.master)).split) 117 | return None 118 | behind = property(log_activity(memoize(behind))) 119 | 120 | @property 121 | def tracking(self): 122 | return None 123 | 124 | class LocalBranch(Branch): 125 | """ 126 | A local branch that is not tracking a published branch. 127 | """ 128 | display_name = 'Local' 129 | 130 | def push(self): 131 | return False 132 | push = property(push) 133 | 134 | def pull(self): 135 | return False 136 | pull = property(pull) 137 | 138 | class PublishedBranch(Branch): 139 | """ 140 | A branch on a remote server that is not being tracked locally. 141 | """ 142 | display_name = 'Remote' 143 | 144 | class TrackingBranch(PublishedBranch): 145 | """ 146 | A local branch that is tracking a published branch. 147 | """ 148 | display_name = 'Tracking' 149 | 150 | @property 151 | def tracking(self): 152 | return True 153 | 154 | class TrackedBranch(PublishedBranch): 155 | """ 156 | A branch on a remote server that is being tracked locally. 157 | """ 158 | display_name = 'Tracked' 159 | 160 | @property 161 | def tracking(self): 162 | return True 163 | 164 | class Tag(Ref): 165 | pass 166 | 167 | class Repository(object): 168 | def __init__(self, path=None): 169 | self.path = os.path.abspath(path or os.path.curdir) 170 | # Hack, make configurable 171 | try: 172 | self.master = 'origin/master' 173 | master_sha = self.shell('git', 'log', '-1', '--pretty=format:%H', self.master, exceptions=True).split 174 | except: 175 | self.master = 'master' 176 | master_sha = self.shell('git', 'log', '-1', '--pretty=format:%H', self.master).split 177 | self.master_sha = master_sha and master_sha[0].strip() or '' 178 | 179 | def shell(self, *args, **kwargs): 180 | exceptions = kwargs.pop('exceptions', False) 181 | join = kwargs.pop('join', False) 182 | 183 | if kwargs: 184 | raise Exception('Unsupported kwargs provided to function.') 185 | 186 | log.debug(' '.join(args)) 187 | 188 | if join: 189 | process = subprocess.Popen(args, cwd=self.path) 190 | process.communicate() 191 | else: 192 | process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.path) 193 | stdout, stderr = process.communicate() 194 | process.stdout = stdout and force_unicode(stdout.strip()) or u'' 195 | process.stderr = stderr and force_unicode(stderr.strip()) or u'' 196 | process.split = filter(lambda x: x, map(lambda x: x.strip(), process.stdout.split(u'\n'))) 197 | 198 | if process.returncode != 0: 199 | message = '%s: %s: %s' % (process.returncode, process.stderr, process.stdout) 200 | log.debug(message) 201 | if exceptions: 202 | raise Exception(message) 203 | 204 | return process 205 | 206 | def status(self): 207 | UNCOMMITTED_RE = re.compile(r'^[MDA]') 208 | CHANGED_RE = re.compile(r'^ [MDA]') 209 | UNTRACKED_RE = re.compile(r'^\?\?') 210 | uncommitted, changed, untracked, stashed = [], [], [], [] 211 | for entry in self.shell('git', 'status', '--porcelain').split: 212 | filename = entry[3:] 213 | if UNCOMMITTED_RE.match(entry): 214 | uncommitted.append(filename) 215 | elif CHANGED_RE.match(entry): 216 | changed.append(filename) 217 | elif UNTRACKED_RE.match(entry): 218 | untracked.append(filename) 219 | 220 | stashed = [ x for x in self.shell('git', 'stash', 'list').split if x ] 221 | 222 | return uncommitted, changed, untracked, stashed 223 | 224 | def configs(self): 225 | return dict([ x.partition('=')[0::2] for x in self.shell('git', 'config', '--list').split ]) 226 | configs = property(memoize(configs)) 227 | 228 | def fetch(self): 229 | log.info('Fetching updates.') 230 | self.shell('git', 'remote', 'update', '--prune') 231 | 232 | def remotes(self): 233 | log.info('Retreiving list of remotes') 234 | return self.shell('git', 'remote').split 235 | remotes = memoize(remotes) 236 | 237 | def refs(self): 238 | return [ Ref(self, *x.split()) for x in self.shell('git', 'show-ref').split ] 239 | refs = memoize(refs) 240 | 241 | def branches(self, *types): 242 | if types: 243 | return [ x for x in self.refs() if x.__class__ in types ] 244 | else: 245 | return [ x for x in self.refs() if isinstance(x, Branch) ] 246 | branches = memoize(branches) 247 | 248 | def tags(self): 249 | return [ x for x in self.refs() if isinstance(x, Tag) ] 250 | tags = memoize(tags) 251 | 252 | def branch(self): 253 | try: 254 | return '/'.join(self.shell('git', 'symbolic-ref', 'HEAD').stdout.split('/')[2:]) 255 | except: 256 | return self.shell('git', 'rev-parse', '--short', 'HEAD').stdout 257 | 258 | def branch_parents(self): 259 | parents = {} 260 | for branch_refspec in [ x.split()[1] for x in self.shell('git', 'show-ref', '--heads').split ]: 261 | branch_name = '/'.join(branch_refspec.split('/')[2:]) 262 | remote = self.configs.get('branch.%s.remote' % branch_name, '.') 263 | parent = self.configs.get('branch.%s.merge' % branch_name, '') 264 | if remote != '.' and parent: 265 | parent = 'refs/remotes/%s/%s' % (remote, '/'.join(parent.split('/')[2:])) 266 | parents[branch_refspec] = parent 267 | parents[parent] = branch_refspec 268 | else: 269 | parents[branch_refspec] = parent 270 | return parents 271 | branch_parents = property(memoize(branch_parents)) 272 | -------------------------------------------------------------------------------- /gitgoggles/progress.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import logging 3 | import StringIO 4 | import sys 5 | from gitgoggles.utils import console, force_unicode, force_str 6 | 7 | class ProgressStreamHandler(logging.StreamHandler): 8 | def __init__(self, *args, **kwargs): 9 | self._stdout = sys.stdout 10 | self._capture_stdout = StringIO.StringIO() 11 | self.spinner = '-\\|/' 12 | self.msg = '' 13 | self.max_length = 0 14 | logging.StreamHandler.__init__(self, *args, **kwargs) 15 | 16 | def capture_stdout(self): 17 | self._stdout = sys.stdout 18 | sys.stdout = self._capture_stdout 19 | 20 | def uncapture_stdout(self): 21 | sys.__stdout__.write(''.ljust(self.max_length)) 22 | sys.__stdout__.write('\r') 23 | sys.stdout = self._stdout 24 | console(force_unicode(self._capture_stdout.getvalue())) 25 | self._capture_stdout = StringIO.StringIO() 26 | 27 | def emit(self, record): 28 | if self.msg != record.msg: 29 | self.msg = record.msg 30 | msg = ' %s %s' % (self.spinner[0], self.msg) 31 | self.max_length = max(len(msg), self.max_length) 32 | self.spinner = self.spinner[1:] + self.spinner[:1] 33 | sys.__stdout__.write(force_str(msg.ljust(self.max_length))) 34 | sys.__stdout__.write('\r') 35 | 36 | handler = ProgressStreamHandler() 37 | 38 | def enable_progress(): 39 | log.addHandler(handler) 40 | log.setLevel(logging.INFO) 41 | handler.capture_stdout() 42 | atexit.register(handler.uncapture_stdout) 43 | 44 | log = logging.getLogger('progress') 45 | -------------------------------------------------------------------------------- /gitgoggles/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import subprocess 3 | import sys 4 | import unicodedata 5 | 6 | def disable_colored_func(text, *args, **kwargs): 7 | return text 8 | 9 | try: 10 | from termcolor import colored as colored_func 11 | except ImportError: 12 | print 'You should run "pip install termcolor" to fully utilize these utilities.' 13 | colored_func = disable_colored_func 14 | 15 | def supports_color(): 16 | """ 17 | Returns True if the running system's terminal supports color, and False 18 | otherwise. 19 | """ 20 | unsupported_platform = (sys.platform in ('win32', 'Pocket PC')) 21 | # isatty is not always implemented, #6223. 22 | is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() 23 | if unsupported_platform or not is_a_tty: 24 | return False 25 | return True 26 | 27 | if not supports_color(): 28 | colored_func = disable_colored_func 29 | 30 | class Colored(object): 31 | disabled = False 32 | def __call__(self, *args, **kwargs): 33 | if self.disabled: 34 | return disable_colored_func(*args, **kwargs) 35 | return colored_func(*args, **kwargs) 36 | 37 | colored = Colored() 38 | 39 | def force_unicode(obj, encoding='utf-8'): 40 | if isinstance(obj, basestring): 41 | if not isinstance(obj, unicode): 42 | obj = unicode(obj, encoding) 43 | # Normalize the unicode data to have characters that in NFKD format would be represented by 2 characters, instead of 1. 44 | obj = unicodedata.normalize('NFKC', obj) 45 | return obj 46 | 47 | def force_str(obj, encoding='utf-8'): 48 | if isinstance(obj, basestring): 49 | if not isinstance(obj, str): 50 | obj = obj.encode(encoding) 51 | return obj 52 | 53 | def console(obj): 54 | sys.stdout.write(force_str(obj)) 55 | 56 | class AccumulatorDict(dict): 57 | def __init__(self, default, *args, **kwargs): 58 | self.__default = default 59 | 60 | def __getitem__(self, key): 61 | if key not in self: 62 | self[key] = copy.copy(self.__default) 63 | return super(AccumulatorDict, self).__getitem__(key) 64 | 65 | def memoize(func): 66 | def _(self, *args, **kwargs): 67 | if not hasattr(self, '__memoize_cache'): 68 | self.__memoize_cache = AccumulatorDict(AccumulatorDict({})) 69 | key = tuple([ tuple(args), tuple([ tuple([x, y]) for x, y in kwargs.items() ]) ]) 70 | if key not in self.__memoize_cache[func]: 71 | self.__memoize_cache[func][key] = func(self, *args, **kwargs) 72 | return self.__memoize_cache[func][key] 73 | return _ 74 | 75 | def terminal_dimensions(): 76 | try: 77 | # This probably does not work on windows, but it should work just about 78 | # everywhere else. 79 | p = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE) 80 | (stdout, stderr) = p.communicate(None) 81 | stdout = force_unicode(stdout) 82 | stderr = force_unicode(stderr) 83 | rows, columns = [ int(x) for x in stdout.split() ] 84 | except: 85 | rows, columns = 40, 79 86 | return rows, columns 87 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from distutils.core import setup 5 | import os 6 | 7 | version = '0.2.12' 8 | 9 | setup( 10 | name='git-goggles', 11 | version=version, 12 | description="A series of GIT utilities to streamline working with remote branches and reviewing code. You can think of git-goggles as 'git branch -a' on steroids. Just install and run 'git goggles'", 13 | long_description=open('README.rst', 'r').read(), 14 | author='Nowell Strite', 15 | author_email='nowell@strite.org', 16 | url='http://github.com/nowells/git-goggles/', 17 | packages=['gitgoggles'], 18 | license='MIT', 19 | classifiers=[ 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python', 24 | 'Topic :: Utilities', 25 | 'Environment :: Console', 26 | 'Natural Language :: English', 27 | 'Operating System :: MacOS :: MacOS X', 28 | 'Operating System :: POSIX :: Linux', 29 | 'Operating System :: Unix', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.4', 32 | 'Programming Language :: Python :: 2.5', 33 | 'Programming Language :: Python :: 2.6', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Topic :: Software Development :: Version Control', 36 | 'Topic :: Utilities', 37 | ], 38 | scripts=['bin/git-goggles'], 39 | ) 40 | --------------------------------------------------------------------------------