summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/nos.yml9
-rw-r--r--Makefile9
-rw-r--r--README.md55
-rw-r--r--auto-completion/bash/.gita-completion.bash (renamed from .gita-completion.bash)0
-rw-r--r--auto-completion/fish/gita.fish17
-rw-r--r--auto-completion/zsh/.gita-completion.zsh (renamed from .gita-completion.zsh)0
-rw-r--r--auto-completion/zsh/_gita473
-rw-r--r--gita/__init__.py19
-rw-r--r--gita/__main__.py77
-rw-r--r--gita/cmds.json11
-rw-r--r--gita/info.py78
-rw-r--r--gita/io.py33
-rw-r--r--gita/utils.py14
-rw-r--r--setup.py7
-rw-r--r--tests/test_main.py16
15 files changed, 737 insertions, 81 deletions
diff --git a/.github/workflows/nos.yml b/.github/workflows/nos.yml
index 52cf572..c82d49d 100644
--- a/.github/workflows/nos.yml
+++ b/.github/workflows/nos.yml
@@ -4,13 +4,14 @@ on: [push, pull_request]
jobs:
build:
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
strategy:
matrix:
- os: [ubuntu-20.04, macos-latest, windows-latest]
- python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
diff --git a/Makefile b/Makefile
index 1c0698f..c744814 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: dist test install clean twine
+.PHONY: dist test install clean twine auto-completion
install:
pip3 install -e .
@@ -10,3 +10,10 @@ twine:
twine upload dist/*
clean:
git clean -fdx
+auto-completion:
+ @ mkdir -p auto-completion/bash
+ @ mkdir -p auto-completion/zsh
+ @ mkdir -p auto-completion/fish
+ register-python-argcomplete gita -s bash > auto-completion/bash/.gita-completion.bash
+ register-python-argcomplete gita -s zsh > auto-completion/zsh/_gita
+ register-python-argcomplete gita -s fish > auto-completion/fish/gita.fish
diff --git a/README.md b/README.md
index 79c6009..dbd950c 100644
--- a/README.md
+++ b/README.md
@@ -41,13 +41,13 @@ I also made a youtube video to demonstrate the common usages
The branch color distinguishes 5 situations between local and remote branches:
-color | meaning
----|---
- white| local has no remote
- green| local is the same as remote
- red| local has diverged from remote
- purple| local is ahead of remote (good for push)
- yellow| local is behind remote (good for merge)
+| color | meaning |
+| ------ | ---------------------------------------- |
+| white | local has no remote |
+| green | local is the same as remote |
+| red | local has diverged from remote |
+| purple | local is ahead of remote (good for push) |
+| yellow | local is behind remote (good for merge) |
The choice of purple for ahead and yellow for behind is motivated by
[blueshift](https://en.wikipedia.org/wiki/Blueshift) and [redshift](https://en.wikipedia.org/wiki/Redshift),
@@ -57,11 +57,12 @@ See the [customization section](#custom).
The additional status symbols denote
-symbol | meaning
----|---
- `+`| staged changes
- `*`| unstaged changes
- `?`| untracked files/folders
+| symbol | meaning |
+| ------ | ----------------------- |
+| `+` | staged changes |
+| `*` | unstaged changes |
+| `?` | untracked files/folders |
+| `$` | stashed contents |
The bookkeeping sub-commands are
@@ -158,11 +159,21 @@ See [this stackoverflow post](https://stackoverflow.com/questions/51680709/color
## Auto-completion
-Download
-[.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash)
-or
-[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh)
-and source it in shell.
+You can download the generated auto-completion file in the following locations for your specific shell. Alternatively, if you have installed `argcomplete` on your system, you can also directly run `eval "$(register-python-argcomplete gita -s SHELL)"` (e.g. `SHELL` as `bash`/`zsh`) in your dotfile.
+
+### Bash
+Download [.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash) and source it in shell.
+
+### Zsh
+There are 2 options :
+- [.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/contrib.completion/zsh/.gita-completion.zsh). Use the help of gita command to display options. It uses the bash completion system for zsh.
+Add `autoload -U +X bashcompinit && bashcompinit` in .zshrc and source the zsh file
+- [_gita](https://github.com/nosarthur/gita/blob/master/contrib.completion/zsh/_gita_).
+Completion more Zsh style. Copy it in a folder and add this folder path in `FPATH` variable. This completion file doesn't take account to command from cmds.json
+
+### Fish
+Download [gita.fish](https://github.com/nosarthur/gita/tree/master/auto-completion/fish/gita.fish) and place it in `~/.config/fish/completions/`
+
## <a name='superman'></a> Superman mode
@@ -380,10 +391,10 @@ their results agree.
## Tips
-effect | shell command
----|---
-enter `<repo>` directory|`` cd `gita ls <repo>` ``
-delete repos in `<group>` | `gita group ll <group> \| xargs gita rm`
+| effect | shell command |
+| ------------------------- | ---------------------------------------- |
+| enter `<repo>` directory | `` cd `gita ls <repo>` `` |
+| delete repos in `<group>` | `gita group ll <group> \| xargs gita rm` |
## Contributing
@@ -411,4 +422,4 @@ I haven't tried them but I heard good things about them.
- [myrepos](https://myrepos.branchable.com/)
- [repo](https://source.android.com/setup/develop/repo)
-
+- [mu-repo](https://github.com/fabioz/mu-repo)
diff --git a/.gita-completion.bash b/auto-completion/bash/.gita-completion.bash
index cad120c..cad120c 100644
--- a/.gita-completion.bash
+++ b/auto-completion/bash/.gita-completion.bash
diff --git a/auto-completion/fish/gita.fish b/auto-completion/fish/gita.fish
new file mode 100644
index 0000000..91580db
--- /dev/null
+++ b/auto-completion/fish/gita.fish
@@ -0,0 +1,17 @@
+
+function __fish_gita_complete
+ set -x _ARGCOMPLETE 1
+ set -x _ARGCOMPLETE_DFS \t
+ set -x _ARGCOMPLETE_IFS \n
+ set -x _ARGCOMPLETE_SUPPRESS_SPACE 1
+ set -x _ARGCOMPLETE_SHELL fish
+ set -x COMP_LINE (commandline -p)
+ set -x COMP_POINT (string length (commandline -cp))
+ set -x COMP_TYPE
+ if set -q _ARC_DEBUG
+ gita 8>&1 9>&2 1>&9 2>&1
+ else
+ gita 8>&1 9>&2 1>/dev/null 2>&1
+ end
+end
+complete --command gita -f -a '(__fish_gita_complete)'
diff --git a/.gita-completion.zsh b/auto-completion/zsh/.gita-completion.zsh
index d1fe952..d1fe952 100644
--- a/.gita-completion.zsh
+++ b/auto-completion/zsh/.gita-completion.zsh
diff --git a/auto-completion/zsh/_gita b/auto-completion/zsh/_gita
new file mode 100644
index 0000000..b133972
--- /dev/null
+++ b/auto-completion/zsh/_gita
@@ -0,0 +1,473 @@
+#compdef gita
+
+__gita_get_repos() {
+ local -a repositories
+ repositories=($(_call_program commands gita ls))
+ _describe -t repositories 'gita repositories' repositories
+}
+
+__gita_get_context() {
+ local -a context
+ context=(
+ "auto"
+ "none"
+ )
+ _describe -t context 'gita context' context
+ __gita_get_groups
+}
+
+__gita_get_infos() {
+ local -a all_infos infos_in_used infos_unused
+ all_infos=($(_call_program commands gita info ll | cut -d ":" -f2))
+ infos_in_used=($(echo ${all_infos[1]} | tr ',' ' '))
+ infos_unused=($(echo ${all_infos[2]} | tr ',' ' '))
+ _describe -t infos_used 'gita infos in used' infos_in_used
+ _describe -t infos_unused 'gita infos unused' infos_unused
+}
+
+__gita_get_groups() {
+ local -a groups
+
+ groups=($(_call_program commands gita group ls))
+ _describe -t groups 'gita groups' groups
+}
+
+__gita_commands() {
+ local -a commands
+ commands=(
+ 'add:Add repo(s)'
+ 'rm:remove repo(s)'
+ 'freeze:Print all repo information'
+ 'clone:Clone repos'
+ 'rename:Rename a repo'
+ 'flags:Git flags configuration'
+ 'color:Color configuration'
+ 'info:Information setting'
+ 'll:Display summary of all repos'
+ 'context:Set context'
+ 'ls:Show repo(s) or repo path'
+ 'group:Group repos'
+ 'super:Run any git command/alias'
+ 'shell:Run any shell command'
+ 'clear:Removes all groups and repositories'
+ )
+ _describe -t commands 'gita sub-commands' commands
+}
+
+# FUNCTION: _gita_add [[[
+_gita_add() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-n --dry-run)'{-n,--dry-run}'[dry run]' \
+ '(-g --group)'{-g=,--group=}'[add repo(s) to the specified group]:Gita groups:__gita_get_groups' \
+ '(-s --skip-modules)'{-s,--skip-modules}'[skip submodule repo(s)]' \
+ '(-r --recursive)'{-r,--recursive}'[recursively add repo(s) in the given path(s)]' \
+ '(-a --auto-group)'{-a,--auto-group}'[recursively add repo(s) in the given path(s) and create hierarchical groups based on folder structure]' \
+ '(-b --bare)'{-b,--bare}'[add bare repo(s)]' \
+ "(-h --help -)*:Directories:_directories"
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_rm [[[
+_gita_rm() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -)*:gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_freeze [[[
+_gita_freeze() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-g --group)'{-g=,--group=}'[freeze repos in the specified group]:Gita groups:__gita_get_groups' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_clone [[[
+_gita_clone() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-C --directory)'{-C=,--directory=}'[ Change to DIRECTORY before doing anything]:Directories:_directories' \
+ '(-p --preserve-path)'{-p,--preserve-path}'[clone repo(s) in their original paths]' \
+ '(-n --dry-run)'{-n,--dry-run}'[dry run]' \
+ '(-g --group)'{-g=,--group=}'[If set, add repo to the specified group after cloning, otherwise add to gita without group]:Gita groups:__gita_get_groups' \
+ '(-f --from-file)'{-f=,--from-file=}'[ If set, clone repos in a config file rendered from `gita freeze`]:File:_path_files' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_rename [[[
+_gita_rename() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_flags_commands[[[
+_gita_flags_commands() {
+
+ local -a subcommands
+ subcommands=(
+ 'll:Display repos with custom flags'
+ 'set:Set flags for repo'
+ )
+ _describe -t subcommands 'gita flag sub-commands' subcommands
+}
+#]]]
+
+# FUNCTION: _gita_flags_ll [[[
+_gita_flags_ll() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_flags_set [[[
+_gita_flags_set() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_flags[[[
+_gita_flags() {
+ local curcontext="$curcontext" state state_descr line expl
+ local tmp ret=1
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]'
+
+ _arguments -C \
+ "1: :->cmds" \
+ "*::arg:->args"
+ case "$state" in
+ cmds)
+ _gita_flags_commands && return 0
+ ;;
+ args)
+ local cmd="${line[1]}"
+ curcontext="${curcontext%:*}-${cmd}:${curcontext##*:}"
+ local completion_func="_gita_flags_${cmd//-/_}"
+ _call_function ret "${completion_func}" && return ret
+ _message "a completion function is not defined for command or alias: ${cmd}"
+ return 1
+ ;;
+ esac
+}
+#]]]
+
+# FUNCTION: _gita_color_commands[[[
+_gita_color_commands() {
+
+ local -a subcommands
+ subcommands=(
+ 'll:Display available colors and the current branch coloring in the ll sub-command'
+ 'set:Set color for local/remote situation'
+ 'reset:Reset color scheme'
+ )
+ _describe -t subcommands 'gita color sub-commands' subcommands
+}
+#]]]
+
+# FUNCTION: _gita_color_ll [[[
+_gita_color_ll() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_color_set [[[
+_gita_color_set() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_color_reset [[[
+_gita_color_reset() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_color[[[
+_gita_color() {
+ local curcontext="$curcontext" state state_descr line expl
+ local tmp ret=1
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]'
+
+ _arguments -C \
+ "1: :->cmds" \
+ "*::arg:->args"
+ case "$state" in
+ cmds)
+ _gita_color_commands && return 0
+ ;;
+ args)
+ local cmd="${line[1]}"
+ curcontext="${curcontext%:*}-${cmd}:${curcontext##*:}"
+ local completion_func="_gita_color_${cmd//-/_}"
+ _call_function ret "${completion_func}" && return ret
+ _message "a completion function is not defined for command or alias: ${cmd}"
+ return 1
+ ;;
+ esac
+}
+#]]]
+
+# FUNCTION: _gita_info_commands[[[
+_gita_info_commands() {
+
+ local -a subcommands
+ subcommands=(
+ 'll:Show used and unused information items of the ll sub-command'
+ 'add:Enable information item'
+ 'rm:Disable information item'
+ )
+ _describe -t subcommands 'gita info sub-commands' subcommands
+}
+#]]]
+
+# FUNCTION: _gita_info_ll [[[
+_gita_info_ll() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_info_add [[[
+_gita_info_add() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita infos:__gita_get_infos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_info_rm [[[
+_gita_info_rm() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita infos:__gita_get_infos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_info[[[
+_gita_info() {
+ local curcontext="$curcontext" state state_descr line expl
+ local tmp ret=1
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]'
+
+ _arguments -C \
+ "1: :->cmds" \
+ "*::arg:->args"
+ case "$state" in
+ cmds)
+ _gita_info_commands && return 0
+ ;;
+ args)
+ local cmd="${line[1]}"
+ curcontext="${curcontext%:*}-${cmd}:${curcontext##*:}"
+ local completion_func="_gita_info_${cmd//-/_}"
+ _call_function ret "${completion_func}" && return ret
+ _message "a completion function is not defined for command or alias: ${cmd}"
+ return 1
+ ;;
+ esac
+}
+#]]]
+
+# FUNCTION: _gita_ll [[[
+_gita_ll() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-C --no-colors)'{-C,--no-colors}'[Disable coloring on the branch names]' \
+ '(-g)'-g'[Show repo summaries by group]' \
+ "(-h --help -):Groups name:__gita_get_groups" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_context [[[
+_gita_context() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita context:__gita_get_context" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_ls [[[
+_gita_ls() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_commands[[[
+_gita_group_commands() {
+
+ local -a subcommands
+ subcommands=(
+ 'll:List all groups with repos.'
+ 'ls:List all group names'
+ 'add:Add repo(s) to a group'
+ 'rmrepo:remove repo(s) from a group'
+ 'rename:Change group name'
+ 'rm:Remove group(s)'
+ )
+ _describe -t subcommands 'gita group sub-commands' subcommands
+}
+#]]]
+
+# FUNCTION: _gita_group_ll [[[
+_gita_group_ll() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -):Groups name:__gita_get_groups" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_ls [[[
+_gita_group_ls() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_add [[[
+_gita_group_add() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-n --name)'{-n=,--name=}'[group-name,]:Groups name:__gita_get_groups' \
+ '(-p --path)'{-p=,--path=}'[group-path]:Group path:_directories' \
+ "(-h --help -)*:Gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_rmrepo [[[
+_gita_group_rmrepo() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-n --name)'{-n=,--name=}'[group-name,]:Groups name:__gita_get_groups' \
+ "(-h --help -)*:Gita repositories:__gita_get_repos" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_rename [[[
+_gita_group_rename() {
+ _arguments -A \
+ '(-h --help -)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group_rm [[[
+_gita_group_rm() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ "(-h --help -)*:Groups name:__gita_get_groups" &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_group[[[
+_gita_group() {
+ local curcontext="$curcontext" state state_descr line expl
+ local tmp ret=1
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]'
+
+ _arguments -C \
+ "1: :->cmds" \
+ "*::arg:->args"
+ case "$state" in
+ cmds)
+ _gita_group_commands && return 0
+ ;;
+ args)
+ local cmd="${line[1]}"
+ curcontext="${curcontext%:*}-${cmd}:${curcontext##*:}"
+ local completion_func="_gita_group_${cmd//-/_}"
+ _call_function ret "${completion_func}" && return ret
+ _message "a completion function is not defined for command or alias: ${cmd}"
+ return 1
+ ;;
+ esac
+}
+#]]]
+
+# FUNCTION: _gita_super [[[
+_gita_super() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-q --quote-mode)'{-q,--quote-mode}'[use quote mode]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_shell [[[
+_gita_shell() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-q --quote-mode)'{-q,--quote-mode}'[use quote mode]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita_clear [[[
+_gita_clear() {
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' &&
+ ret=0
+}
+#]]]
+
+# FUNCTION: _gita [[[
+_gita() {
+ local curcontext="$curcontext" state state_descr line expl
+ local tmp ret=1
+ _arguments -A \
+ '(-h --help)'{-h,--help}'[show this help message and exit]' \
+ '(-v --version)'{-v,--version}'[show program'\''s version number and exit]'
+
+ _arguments -C \
+ "1: :->cmds" \
+ "*::arg:->args"
+ case "$state" in
+ cmds)
+ __gita_commands && return 0
+ ;;
+ args)
+ local cmd="${line[1]}"
+ curcontext="${curcontext%:*}-${cmd}:${curcontext##*:}"
+ local completion_func="_gita_${cmd//-/_}"
+ _call_function ret "${completion_func}" && return ret
+ _message "a completion function is not defined for command or alias: ${cmd}"
+ return 1
+ ;;
+ esac
+} # ]]]
+
+_gita "$@"
diff --git a/gita/__init__.py b/gita/__init__.py
index 33c0f41..de4873d 100644
--- a/gita/__init__.py
+++ b/gita/__init__.py
@@ -1,3 +1,18 @@
-import pkg_resources
+import sys
-__version__ = pkg_resources.get_distribution("gita").version
+
+def get_version() -> str:
+ try:
+ import pkg_resources
+ except ImportError:
+ try:
+ from importlib.metadata import version
+ except ImportError:
+ print("cannot determine version", sys.version_info)
+ else:
+ return version("gita")
+ else:
+ return pkg_resources.get_distribution("gita").version
+
+
+__version__ = get_version()
diff --git a/gita/__main__.py b/gita/__main__.py
index b2bc32b..6809f15 100644
--- a/gita/__main__.py
+++ b/gita/__main__.py
@@ -18,14 +18,15 @@ import os
import sys
import csv
import argparse
+import argcomplete
import subprocess
from functools import partial
-import pkg_resources
from itertools import chain
from pathlib import Path
import glob
+from typing import Dict, Optional
-from . import utils, info, common
+from . import utils, info, common, io, get_version
def _group_name(name: str, exclude_old_names=True) -> str:
@@ -146,13 +147,28 @@ def f_info(args: argparse.Namespace):
with open(csv_config, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(to_display)
+ elif cmd == "set-length":
+ csv_config = common.get_config_fname("layout.csv")
+ print(f"Settings are in {csv_config}")
+ defaults = {
+ "branch": 19,
+ "symbols": 5,
+ "branch_name": 27,
+ "commit_msg": 0,
+ "commit_time": 0, # 0 means no limit
+ "path": 30,
+ }
+ with open(csv_config, "w", newline="") as f:
+ writer = csv.DictWriter(f, fieldnames=defaults)
+ writer.writeheader()
+ writer.writerow(defaults)
def f_clone(args: argparse.Namespace):
if args.dry_run:
if args.from_file:
- for url, repo_name, abs_path in utils.parse_clone_config(args.clonee):
+ for url, repo_name, abs_path in io.parse_clone_config(args.clonee):
print(f"git clone {url} {abs_path}")
else:
print(f"git clone {args.clonee}")
@@ -172,28 +188,35 @@ def f_clone(args: argparse.Namespace):
f_add(args)
return
+ # TODO: add repos to group too
+ repos, groups = io.parse_clone_config(args.clonee)
if args.preserve_path:
utils.exec_async_tasks(
- utils.run_async(repo_name, path, ["git", "clone", url, abs_path])
- for url, repo_name, abs_path in utils.parse_clone_config(args.clonee)
+ utils.run_async(repo_name, path, ["git", "clone", r["url"], r["path"]])
+ for repo_name, r in repos.items()
)
else:
utils.exec_async_tasks(
- utils.run_async(repo_name, path, ["git", "clone", url])
- for url, repo_name, _ in utils.parse_clone_config(args.clonee)
+ utils.run_async(repo_name, path, ["git", "clone", r["url"]])
+ for repo_name, r in repos.items()
)
def f_freeze(args):
- repos = utils.get_repos()
+ """
+ print repo and group information for future cloning
+ """
ctx = utils.get_context()
if args.group is None and ctx:
args.group = ctx.stem
+ repos = utils.get_repos()
+ group_name = args.group
group_repos = None
- if args.group: # only display repos in this group
- group_repos = utils.get_groups()[args.group]["repos"]
+ if group_name: # only display repos in this group
+ group_repos = utils.get_groups()[group_name]["repos"]
repos = {k: repos[k] for k in group_repos if k in repos}
seen = {""}
+ # print(repos)
for name, prop in repos.items():
path = prop["path"]
url = ""
@@ -211,7 +234,16 @@ def f_freeze(args):
url = parts[1]
if url not in seen:
seen.add(url)
- print(f"{url},{name},{path}")
+ # TODO: add another field to distinguish regular repo or worktree or submodule
+ print(f"{url},{name},{path},")
+ # group information: these lines don't have URL
+ if group_name:
+ group_path = utils.get_groups()[group_name]["path"]
+ print(f",{group_name},{group_path},{'|'.join(group_repos)}")
+ else: # show all groups
+ for gname, g in utils.get_groups().items():
+ group_repos = "|".join(g["repos"])
+ print(f",{gname},{g['path']},{group_repos}")
def f_ll(args: argparse.Namespace):
@@ -435,8 +467,9 @@ def main(argv=None):
title="sub-commands", help="additional help with sub-command -h"
)
- version = pkg_resources.require("gita")[0].version
- p.add_argument("-v", "--version", action="version", version=f"%(prog)s {version}")
+ p.add_argument(
+ "-v", "--version", action="version", version=f"%(prog)s {get_version()}"
+ )
# bookkeeping sub-commands
p_add = subparsers.add_parser("add", description="add repo(s)", help="add repo(s)")
@@ -598,11 +631,17 @@ def main(argv=None):
info_cmds.add_parser("rm", description="Disable information item.").add_argument(
"info_item", choices=info.ALL_INFO_ITEMS, help="information item to delete"
)
+ info_cmds.add_parser(
+ "set-length",
+ description="Set default column widths for information items. "
+ "The settings are in layout.csv",
+ )
ll_doc = f""" status symbols:
+: staged changes
*: unstaged changes
- _: untracked files/folders
+ ?: untracked files/folders
+ $: stashed changes
branch colors:
{info.Color.white}white{info.Color.end}: local has no remote
@@ -780,17 +819,18 @@ def main(argv=None):
cmds = utils.get_cmds_from_files()
for name, data in cmds.items():
help = data.get("help")
+ repo_help = help
cmd = data["cmd"]
if data.get("allow_all"):
choices = utils.get_choices()
nargs = "*"
- help += " for all repos or"
+ repo_help += " for all repos or"
else:
choices = utils.get_repos().keys() | utils.get_groups().keys()
nargs = "+"
- help += " for the chosen repo(s) or group(s)"
- sp = subparsers.add_parser(name, description=help)
- sp.add_argument("repo", nargs=nargs, choices=choices, help=help)
+ repo_help += " for the chosen repo(s) or group(s)"
+ sp = subparsers.add_parser(name, description=help, help=help)
+ sp.add_argument("repo", nargs=nargs, choices=choices, help=repo_help)
is_shell = bool(data.get("shell"))
sp.add_argument(
"-s",
@@ -805,6 +845,7 @@ def main(argv=None):
cmd = cmd.split()
sp.set_defaults(func=f_git_cmd, cmd=cmd)
+ argcomplete.autocomplete(p)
args = p.parse_args(argv)
args.async_blacklist = {
diff --git a/gita/cmds.json b/gita/cmds.json
index eadda81..c1890c5 100644
--- a/gita/cmds.json
+++ b/gita/cmds.json
@@ -22,8 +22,13 @@
"cmd": "git log -1 HEAD",
"help": "show log information of HEAD"
},
-"log":
- {"cmd": "git log",
+"lo":{
+ "cmd": "git log --oneline -7",
+ "allow_all": true,
+ "help": "show one-line log for the latest 7 commits"
+ },
+"log":{
+ "cmd": "git log",
"disable_async": true,
"help": "show logs"
},
@@ -77,10 +82,12 @@
},
"stat":{
"cmd": "git diff --stat",
+ "allow_all": true,
"help": "show edit statistics"
},
"st":{
"cmd": "git status",
+ "allow_all": true,
"help": "show status"
},
"tag":{
diff --git a/gita/info.py b/gita/info.py
index 10d8bea..0a253a0 100644
--- a/gita/info.py
+++ b/gita/info.py
@@ -9,6 +9,40 @@ from typing import Tuple, List, Callable, Dict
from . import common
+class Truncate:
+ """
+ Reads in user layout.csv file and uses the values there
+ to truncate the string passed in. If the file doesn't
+ exist or the requested field doesn't exist then don't
+ truncate
+ """
+
+ widths = {}
+
+ def __init__(self):
+ csv_config = Path(common.get_config_fname("layout.csv"))
+ if csv_config.is_file():
+ with open(csv_config, "r") as f:
+ reader = csv.DictReader(f)
+ self.widths = next(reader)
+
+ # Ensure the Dict type is Dict[str, int] to reduce casting elsewhere
+ for e, width in self.widths.items():
+ self.widths[e] = int(width)
+
+ def truncate(self, field: str, message: str):
+ # 0 means no width limit applied
+ if not self.widths.get(field):
+ return message
+
+ length = 3 if self.widths[field] < 3 else self.widths[field]
+ return (
+ message[: length - 3] + "..."
+ if len(message) > length
+ else message.ljust(length)
+ )
+
+
class Color(Enum):
"""
Terminal color
@@ -113,8 +147,8 @@ def get_info_items() -> List[str]:
return display_items
-def get_path(prop: Dict[str, str]) -> str:
- return f'{Color.cyan}{prop["path"]}{Color.end}'
+def get_path(prop: Dict[str, str], truncator: Truncate) -> str:
+ return f'{Color.cyan}{truncator.truncate("path", prop["path"])}{Color.end}'
# TODO: do we need to add the flags here too?
@@ -164,7 +198,21 @@ def has_untracked(flags: List[str], path) -> bool:
return bool(result.stdout)
-def get_commit_msg(prop: Dict[str, str]) -> str:
+def has_stashed(flags: List[str], path) -> bool:
+ """
+ Return True if stashed content exists
+ """
+ # FIXME: this doesn't work for repos like worktrees, bare, etc
+ p = Path(path) / ".git" / "logs" / "refs" / "stash"
+ got = False
+ try:
+ got = p.is_file()
+ except Exception:
+ pass
+ return got
+
+
+def get_commit_msg(prop: Dict[str, str], truncator: Truncate) -> str:
"""
Return the last commit message.
"""
@@ -177,10 +225,10 @@ def get_commit_msg(prop: Dict[str, str]) -> str:
universal_newlines=True,
cwd=prop["path"],
)
- return result.stdout.strip()
+ return truncator.truncate("commit_msg", result.stdout.strip())
-def get_commit_time(prop: Dict[str, str]) -> str:
+def get_commit_time(prop: Dict[str, str], truncator: Truncate) -> str:
"""
Return the last commit time in parenthesis.
"""
@@ -192,13 +240,14 @@ def get_commit_time(prop: Dict[str, str]) -> str:
universal_newlines=True,
cwd=prop["path"],
)
- return f"({result.stdout.strip()})"
+ return truncator.truncate("commit_time", f"({result.stdout.strip()})")
default_symbols = {
"dirty": "*",
"staged": "+",
"untracked": "?",
+ "stashed": "$",
"local_ahead": "↑",
"remote_ahead": "↓",
"diverged": "⇕",
@@ -223,11 +272,11 @@ def get_symbols() -> Dict[str, str]:
return default_symbols
-def get_repo_status(prop: Dict[str, str], no_colors=False) -> str:
- branch = get_head(prop["path"])
- dirty, staged, untracked, situ = _get_repo_status(prop)
+def get_repo_status(prop: Dict[str, str], truncator: Truncate, no_colors=False) -> str:
+ branch = truncator.truncate("branch", get_head(prop["path"]))
+ dirty, staged, untracked, stashed, situ = _get_repo_status(prop)
symbols = get_symbols()
- info = f"{branch:<10} [{symbols[dirty]+symbols[staged]+symbols[untracked]+symbols[situ]}]"
+ info = f"{branch:<10} {truncator.truncate('symbols', f'[{symbols[dirty]}{symbols[staged]}{symbols[stashed]}{symbols[untracked]}{symbols[situ]}]')}"
if no_colors:
return f"{info:<18}"
@@ -236,11 +285,11 @@ def get_repo_status(prop: Dict[str, str], no_colors=False) -> str:
return f"{color}{info:<18}{Color.end}"
-def get_repo_branch(prop: Dict[str, str]) -> str:
- return get_head(prop["path"])
+def get_repo_branch(prop: Dict[str, str], truncator: Truncate) -> str:
+ return truncator.truncate("branch_name", get_head(prop["path"]))
-def _get_repo_status(prop: Dict[str, str]) -> Tuple[str, str, str, str]:
+def _get_repo_status(prop: Dict[str, str]) -> Tuple[str, str, str, str, str]:
"""
Return the status of one repo
"""
@@ -249,6 +298,7 @@ def _get_repo_status(prop: Dict[str, str]) -> Tuple[str, str, str, str]:
dirty = "dirty" if run_quiet_diff(flags, [], path) else ""
staged = "staged" if run_quiet_diff(flags, ["--cached"], path) else ""
untracked = "untracked" if has_untracked(flags, path) else ""
+ stashed = "stashed" if has_stashed(flags, path) else ""
diff_returncode = run_quiet_diff(flags, ["@{u}", "@{0}"], path)
if diff_returncode == 128:
@@ -263,7 +313,7 @@ def _get_repo_status(prop: Dict[str, str]) -> Tuple[str, str, str, str]:
situ = "diverged" if diverged else "remote_ahead"
else: # local is ahead of remote
situ = "local_ahead"
- return dirty, staged, untracked, situ
+ return dirty, staged, untracked, stashed, situ
ALL_INFO_ITEMS = {
diff --git a/gita/io.py b/gita/io.py
new file mode 100644
index 0000000..78665b7
--- /dev/null
+++ b/gita/io.py
@@ -0,0 +1,33 @@
+import os
+import csv
+from typing import Tuple
+
+
+def parse_clone_config(fname: str) -> Tuple:
+ """
+ Return the repo information (url, name, path, type) and group information
+ (, name, path, repos) saved in `fname`.
+ """
+ repos = {}
+ groups = {}
+ if os.path.isfile(fname) and os.stat(fname).st_size > 0:
+ with open(fname) as f:
+ rows = csv.DictReader(
+ f, ["url", "name", "path", "type", "flags"], restval=""
+ ) # it's actually a reader
+ for r in rows:
+ if r["url"]:
+ repos[r["name"]] = {
+ "path": r["path"],
+ "type": r["type"],
+ "flags": r["flags"].split(),
+ "url": r["url"],
+ }
+ else:
+ groups[r["name"]] = {
+ "path": r["path"],
+ "repos": [
+ repo for repo in r["type"].split("|") if repo in repos
+ ],
+ }
+ return repos, groups
diff --git a/gita/utils.py b/gita/utils.py
index 6746d7f..f7717e0 100644
--- a/gita/utils.py
+++ b/gita/utils.py
@@ -7,7 +7,7 @@ import platform
import subprocess
from functools import lru_cache
from pathlib import Path
-from typing import List, Dict, Coroutine, Union, Iterator, Tuple
+from typing import List, Dict, Coroutine, Union, Tuple
from collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor
import multiprocessing
@@ -367,15 +367,6 @@ def auto_group(repos: Dict[str, Dict[str, str]], paths: List[str]) -> Dict[str,
return new_groups
-def parse_clone_config(fname: str) -> Iterator[List[str]]:
- """
- Return the url, name, and path of all repos in `fname`.
- """
- with open(fname) as f:
- for line in f:
- yield line.strip().split(",")
-
-
async def run_async(repo_name: str, path: str, cmds: List[str]) -> Union[None, str]:
"""
Run `cmds` asynchronously in `path` directory. Return the `path` if
@@ -430,13 +421,14 @@ def describe(repos: Dict[str, Dict[str, str]], no_colors: bool = False) -> str:
Return the status of all repos
"""
if repos:
+ truncator = info.Truncate()
name_width = len(max(repos, key=len)) + 1
funcs = info.get_info_funcs(no_colors=no_colors)
num_threads = min(multiprocessing.cpu_count(), len(repos))
with ThreadPoolExecutor(max_workers=num_threads) as executor:
for line in executor.map(
- lambda name: f'{name:<{name_width}}{" ".join(f(repos[name]) for f in funcs)}',
+ lambda name: f'{name:<{name_width}}{" ".join(f(repos[name], truncator) for f in funcs)}',
sorted(repos),
):
yield line
diff --git a/setup.py b/setup.py
index 4950b49..394d0c2 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ with open("README.md", encoding="utf-8") as f:
setup(
name="gita",
packages=["gita"],
- version="0.16.6",
+ version="0.16.7.2",
license="MIT",
description="Manage multiple git repos with sanity",
long_description=long_description,
@@ -19,6 +19,7 @@ setup(
author_email="zhou.dong@gmail.com",
entry_points={"console_scripts": ["gita = gita.__main__:main"]},
python_requires="~=3.6",
+ install_requires=["argcomplete"],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
@@ -29,10 +30,12 @@ setup(
"Topic :: Software Development :: Version Control :: Git",
"Topic :: Terminals",
"Topic :: Utilities",
- "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
],
include_package_data=True,
)
diff --git a/tests/test_main.py b/tests/test_main.py
index a877160..03b15d4 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -143,7 +143,7 @@ class TestLsLl:
@patch("gita.info.get_head", return_value="master")
@patch(
"gita.info._get_repo_status",
- return_value=("dirty", "staged", "untracked", "diverged"),
+ return_value=("dirty", "staged", "untracked", "", "diverged"),
)
@patch("gita.info.get_commit_msg", return_value="msg")
@patch("gita.info.get_commit_time", return_value="")
@@ -196,8 +196,11 @@ def test_clone_with_url(mock_run):
@patch(
- "gita.utils.parse_clone_config",
- return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]],
+ "gita.io.parse_clone_config",
+ return_value=(
+ {"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}},
+ {},
+ ),
)
@patch("gita.utils.run_async", new=async_mock())
@patch("subprocess.run")
@@ -217,8 +220,11 @@ def test_clone_with_config_file(*_):
@patch(
- "gita.utils.parse_clone_config",
- return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]],
+ "gita.io.parse_clone_config",
+ return_value=(
+ {"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}},
+ {},
+ ),
)
@patch("gita.utils.run_async", new=async_mock())
@patch("subprocess.run")