This commit is contained in:
relikd
2019-08-13 17:28:34 +02:00
commit 5474d67d19
6 changed files with 537 additions and 0 deletions

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2019 Oleg Geier
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
git-alias
=========
A command line tool that will display all registered git aliases.
`git-alias` runs with python 2 & 3 and should be compatible with macOS [tested], Linux, and Windows.
![screenshot](screenshot.png)
Example
-------
Try yourself:
```
python3 git-alias.py example.config
```
Or with the `gitconfig` from [GitAlias.com][1] (options used: `-vvq`)
![screenshot](screenshot_vvq.png)
Install
-------
Copy the `git-alias.py` to any path of your choosing, e.g., `/usr/local/bin`.
Extend your global `~/.gitconfig` file `alias` section:
```
[alias]
# Show this message (help: -h)
# !: ignore, inline
alias = !python3 /usr/local/bin/git-alias.py
```
Python version and paths may differ!
Done.
Usage
-----
Run `git alias [options] [FILE]` or simply `git alias`.
### Output Options
- `-a` Print all aliases, including hidden ones
- `--color` Force colorful output
- `--no-color` Disable colorful output
### Verbosity Options
- `-q` Don't print alias descriptions
- `-qq` Also don't print section titles
- `-qqq` Also don't print (inline) commands
- `-qqqq` Also don't print usage help
- `-v` Print all multiline commands (unmodified)
- `-vv` Print multiline commands using pretty print
- `-vvqq` Print just the alias and command in pretty format
### Config file
- If `[FILE]` not provided, will default to `~/.gitconfig`
[1]: https://github.com/gitalias/gitalias/blob/master/gitalias.txt

76
examle.config Normal file
View File

@@ -0,0 +1,76 @@
[alias]
### Basic usage
# This is the alias description
# Descriptions can be multiline
# use: example "these" "are the parameters"
# see: this-is-the-alias-explanatory-url
# !# this an ignored comment line
; this line is not ignored (but could be in 'isComment()')
# !: here are linting options
# !# lint options are comma separated: show cmd, inline, pretty
example = !echo "this is the alias command"
### git-alias, directives
# use: usage param1 param2
# !# or 'usage:'
usage = !echo "show expected alias parameters in red"
# https://example.com
# !# or prefix with: 'see:', 'link:', 'url:', 'web:'
link = !echo "print url with light gray color"
### git-alias, lint options
# !: ignore
# this description is also ignored
ignored = !echo "this alias is completly ignored unless --all"
# !: hide, hide cmd, not cmd, no cmd, hide command, not command, no command
# this description is still printed
hide_cmd = !echo "hide command unless --all"
# !: show cmd, show command
show_cmd = !echo "always show command even if not inline"
# !: inline, singleline, single line
single_line = !echo "forces command inline regardless of how long the line is"
# !: newline, new line, multiline, multi line, not inline, no inline, not single line, not singleline, no single line, no singleline
multi_line = !echo "force line break before cmd"
# !: pretty, prettify
prettify = "!f() { echo \"force pretty print command\"; }; f"
## Simple aliases
fpull = pull --rebase
fpush = push --force --all
fpushtags = push --force --tags
# Replace an existing tag with new commit
# use: ftag v1.0 c9a...d87
ftag = tag -a -f
### Advanced ###
# List all contributers with email
authors = !"echo; git log --format='%aN <%aE>' | sort -u; echo;"
# Replace email information
# see: https://help.github.com/articles/changing-author-info/
# use: new-email "old email" "new name" "new email"
new-email = !"f() { git filter-branch -f --env-filter ' \
if [ \"$GIT_COMMITTER_EMAIL\" = \"'\"$1\"'\" ]; then \
export GIT_COMMITTER_NAME=\"'\"$2\"'\"; \
export GIT_COMMITTER_EMAIL=\"'\"$3\"'\"; \
fi;\
if [ \"$GIT_AUTHOR_EMAIL\" = \"'\"$1\"'\" ]; then \
export GIT_AUTHOR_NAME=\"'\"$2\"'\"; \
export GIT_AUTHOR_EMAIL=\"'\"$3\"'\"; \
fi' --tag-name-filter cat -- --branches --tags; }; f"
# Show this message (help: -h)
# !: ignore, inline
alias = !python3 /usr/local/bin/git-alias.py

390
git-alias.py Executable file
View File

@@ -0,0 +1,390 @@
#!/usr/bin/env python3
# coding=utf8
# Iterate over git config file and extract alias help
from argparse import ArgumentParser, FileType
import re
from os import path, linesep as CRLF
from sys import stdout
ARGS = None
def main():
global ARGS
parser = ArgumentParser()
parser.add_argument('FILE', type=FileType('r'),
default=path.expanduser('~/.gitconfig'), nargs='?',
help='git config file (default: %(default)s)')
parser.add_argument('-a', '--all', action='store_true',
help='include hidden aliases')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='print complex commands (-vv: pretty print)')
parser.add_argument('-q', '--quiet', action='count', default=0,
help='print less information (up to -qqqq | -qqvv)')
parser.add_argument('--color', action='store_const', const='+',
help='force color output', dest='color')
parser.add_argument('--no-color', action='store_const', const='-',
help='disable color output', dest='color')
ARGS = parser.parse_args()
colorMap = {'+': True, '-': False, None: stdout.isatty()}
ARGS.color = colorMap[ARGS.color]
AliasConfig(ARGS.FILE).parse()
ARGS.FILE.close()
class AliasConfig(object):
''' Parser for git-config files '''
def __init__(self, file):
super(AliasConfig, self).__init__()
self.file = file
self.dirty = False
self.state = None
self.prevLine = None
self.comments = []
def parse(self):
for line in self.file.readlines():
# A command can be multiline if the line ends with \
if self.state == 'alias' and self.prevLine:
line = line.rstrip()
if line.endswith('\\'):
self.prevLine += '\n' + line[:-1]
else:
self.prevLine += '\n' + line
self.parseAlias(self.prevLine)
self.prevLine = None
continue
else:
line = line.strip()
# Ignore empty lines
# But empty comment will insert a blank line
if len(line) == 0:
continue
if self.parseState(line):
continue
# Check if we need to include another alias file
if self.state == 'include':
self.parseIncludePath(line)
continue
# Ignore parsing for groups other than 'alias'
if self.state != 'alias':
continue
# Add comments to internal array
if self.isComment(line):
continue
# not a config group, not a comment ... must be the alias
if line.endswith('\\'):
self.prevLine = line[:-1]
else:
self.parseAlias(line)
def parseState(self, line):
config = re.search(r'^\[(.+)\]$', line)
if config:
self.state = config.group(1).strip().lower()
return config
def printSection(self, title):
if ARGS.quiet <= 1:
print(Format().section(title.strip(';# \t'), self.dirty))
self.dirty = True
def isComment(self, line):
if line[0] not in ';#':
return False
# If line starts with ## asume this to be a section title
noBlanks = line.replace(' ', '')
if len(noBlanks) > 1 and noBlanks[1] in ';#':
self.printSection(line)
else:
self.comments.append(line[1:].lstrip())
return True
def parseAlias(self, line):
cmd = re.search(r'^(.+?)\s*=\s*([\s\S]+)$', line)
alias = Alias(cmd.group(1).strip(), cmd.group(2).strip())
if alias.parse(self.comments):
print(alias)
self.comments = []
self.dirty = True
def parseIncludePath(self, line):
param = re.search(r'^(.+?)\s*=\s*([\s\S]+)$', line)
if param and param.group(1).strip() == 'path':
path = param.group(2).strip()
if ARGS.quiet <= 1: # if 'quiet == 0', copy self.dirty
print(CRLF + '@include: %s' % path)
with open(path, 'r') as f:
AliasConfig(f).parse()
if ARGS.quiet <= 1:
print('@end: %s' % path + CRLF)
class Alias(object):
''' Object for alias name, command, and helping hints '''
def __init__(self, name, command):
super(Alias, self).__init__()
self.name = name
self.command = AliasCommand(command)
self.usage = None
self.hints = ''
self.ignore = False
def __str__(self):
str = Format().alias(self.name)
if self.usage and ARGS.quiet <= 3:
str += ' ' + Format().usage(self.usage)
if self.command.shouldPrint(self.hints):
str += self.command.__str__()
if ARGS.quiet == 0:
str += self.hints
if ARGS.verbose >= 1 and ARGS.quiet <= 2:
str += CRLF
return str
def addDescription(self, line):
self.hints += Format().indent(line)
def parse(self, commentsList):
''' Process list of comments (all comments above alias) '''
for comment in commentsList:
x = comment.split(':')
ctrl = x[0].strip().lower()
if len(x) < 2 or self.ctrlSequence(ctrl, ':'.join(x[1:]).strip()):
# Allow user to hide individual comments with '#!#'
if not comment.startswith('!#'):
self.addDescription(comment)
if self.ignore and not ARGS.all:
return False
return True
def ctrlSequence(self, instruction, tail):
''' Parse lines with format `%s: %s` '''
# Auto-detect urls
if instruction in ['http', 'https']:
tail = instruction + ':' + tail
instruction = 'link'
# Append usage (in red) to command (ignoring first word)
if instruction in ['usage', 'use']:
self.usage = re.sub(r'^%s\s*' % self.name, '', tail, flags=re.I)
# Print url (in light gray)
elif instruction in ['see', 'link', 'url', 'web']:
self.addDescription(Format().link(tail))
# Some program specific controls
elif instruction == '!':
self.lintInstruction(tail.lower())
else:
return True
return False
def lintInstruction(self, instruction):
''' Parse lines with format `!: %s` '''
for _cmd_ in [x.strip() for x in instruction.split(',')]:
if _cmd_ == 'ignore':
self.ignore = True
elif re.match('^show( command| cmd)?$', _cmd_):
self.command.show = True
elif re.match('^hide|(hide|not?) (command|cmd)$', _cmd_):
self.command.show = False
elif re.match('^(single ?|in)line$', _cmd_):
self.command.inline = True
elif re.match('^(new ?|multi ?|not? (single ?|in))line$', _cmd_):
self.command.inline = False
elif re.match('^prett(if)?y', _cmd_):
self.command.prettify = True
class AliasCommand(object):
'''
Very basic bash parser.
Inserts new line for: ';', '{', '}', ' && ' and ' | '
Escapes: \"
'''
def __init__(self, txt):
super(AliasCommand, self).__init__()
self.input = txt
# linting
self.inline = len(txt) < 42 # not self.isComplex()
self.show = None
self.prettify = False
# result parsing
self.skip = 0
self.indentation = 0
self.tempIndent = False
self.newline = False
self.escapeQuotes = False
def __str__(self):
cmd = self.input
if ARGS.verbose >= 2:
self.inline = False
if self.isComplex() and (self.prettify or ARGS.verbose >= 2):
cmd = self.parse()
elif ARGS.verbose == 1 and not self.inline:
cmd = Format().fx([RED, FAINT], cmd)
return Format().command(cmd, self.inline)
def shouldPrint(self, hasHints=False):
if (self.show is False and not ARGS.all) or ARGS.quiet >= 3:
return False
# Print complex command only if user added no comment to describe it
# Otherwise, its most likely too complex to display
if not self.inline:
if hasHints and not self.show and ARGS.verbose == 0:
return False
return True
def isComplex(self):
return self.input.lstrip('"').startswith('!')
def preprocessInput(self, text):
if text.startswith('!'):
text = text[1:].lstrip()
if text.startswith('"'):
text = text[1:].lstrip('! \t')
self.escapeQuotes = True
return text
def append(self, char):
if self.newline and char in ' \t\n\r':
return
if self.newline:
self.newline = False
self.result += Format().indent(' ' * 4 * self.indentation)
if self.tempIndent:
self.result += Format().fx([BLACK, FAINT], '')
self.tempIndent = False
self.result += char
def parse(self):
raw = self.preprocessInput(self.input)
self.result = ''
for i, char in enumerate(raw):
if self.skip > 0:
self.skip -= 1
continue
if char == '\\':
if self.parseCharEscape(raw[i + 1]):
continue
elif char == '$':
if self.parseBashVariable(raw[i + 1:]):
continue
elif self.escapeQuotes and char == '"':
self.escapeQuotes = False
continue
# Insert new lines after '{', '}', and ';'
elif char == '}' and raw[i - 1] != '{':
self.indentation -= 1
self.newline = True
self.append(char)
if char == '{' and raw[i + 1] != '}':
self.indentation += 1
self.newline = True
elif char == ';':
self.newline = True
# Insert new line for ' && ' and ' | '
elif char in '&|':
self.parseBashPipes(raw[i - 2:i + 2])
return self.result
def parseBashPipes(self, text):
if text == ' && ' or text[1:] == ' | ':
self.tempIndent = True
self.newline = True
def parseBashVariable(self, text):
c = text[0]
var = None
if c in '0123456789!$?#*-@':
var = c
elif c in '{':
match = re.search(r'^.*?[}]', text)
var = match.group(0)
else:
match = re.search(r'^[a-zA-Z_][a-zA-Z_0-9]*', text)
if match:
var = match.group(0)
if var:
# Highlight input variables
self.result += Format().inlineVariable('$' + var)
self.skip = len(var)
return self.skip > 0
def parseCharEscape(self, following):
if self.escapeQuotes and following == '"':
self.result += following
self.skip = 1
return self.skip > 0
class Format(object):
''' Abstraction for structuring output '''
def __init__(self):
super(Format, self).__init__()
def section(self, text, newline=True):
head = self.fx([BLUE, BOLD, UNDERLINE], text, alt='=== %s ===')
return (CRLF if newline else '') + head
def alias(self, text):
if ARGS.quiet >= 2:
return self.fx([RED, BOLD], text)
return ' ' + self.fx([RED, BOLD], text, alt='+ %s')
def usage(self, text):
return self.fx([RED], text)
def command(self, text, inline=True):
return ('' if inline else self.indent()) + text
def link(self, text):
return '@: ' + self.fx([BLACK, FAINT], text)
def indent(self, text=''):
return CRLF + ' ' + text
def inlineVariable(self, text):
return self.fx([YELLOW, BOLD], text)
def fx(self, params, text, alt='%s'):
''' if --no-color use alternative format '''
if ARGS.color:
left = '\x1b[' + ';'.join(params) + 'm'
right = '\x1b[0m'
return left + text.replace(right, right + left) + right
else:
return alt % text
BLACK = '30'
RED = '31'
GREEN = '32'
YELLOW = '33'
BLUE = '34'
MAGENTA = '35'
CYAN = '36'
WHITE = '37'
BOLD = '01'
FAINT = '02'
ITALIC = '03'
UNDERLINE = '04'
BLINKSLOW = '05'
BLINK = '06'
NEGATIVE = '07'
CONCEALED = '08'
if __name__ == '__main__':
main()

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
screenshot_vvq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB