1
0
Fork 0

Adding upstream version 0.16.7.2.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-11 18:51:56 +01:00
parent 50453fb690
commit e4b3680961
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
15 changed files with 737 additions and 81 deletions

View file

@ -4,13 +4,14 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
timeout-minutes: 10
strategy: strategy:
matrix: matrix:
os: [ubuntu-20.04, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:

View file

@ -1,4 +1,4 @@
.PHONY: dist test install clean twine .PHONY: dist test install clean twine auto-completion
install: install:
pip3 install -e . pip3 install -e .
@ -10,3 +10,10 @@ twine:
twine upload dist/* twine upload dist/*
clean: clean:
git clean -fdx 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

View file

@ -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: The branch color distinguishes 5 situations between local and remote branches:
color | meaning | color | meaning |
---|--- | ------ | ---------------------------------------- |
white| local has no remote | white | local has no remote |
green| local is the same as remote | green | local is the same as remote |
red| local has diverged from remote | red | local has diverged from remote |
purple| local is ahead of remote (good for push) | purple | local is ahead of remote (good for push) |
yellow| local is behind remote (good for merge) | yellow | local is behind remote (good for merge) |
The choice of purple for ahead and yellow for behind is motivated by 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), [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 The additional status symbols denote
symbol | meaning | symbol | meaning |
---|--- | ------ | ----------------------- |
`+`| staged changes | `+` | staged changes |
`*`| unstaged changes | `*` | unstaged changes |
`?`| untracked files/folders | `?` | untracked files/folders |
| `$` | stashed contents |
The bookkeeping sub-commands are The bookkeeping sub-commands are
@ -158,11 +159,21 @@ See [this stackoverflow post](https://stackoverflow.com/questions/51680709/color
## Auto-completion ## Auto-completion
Download 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.
[.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash)
or ### Bash
[.gita-completion.zsh](https://github.com/nosarthur/gita/blob/master/.gita-completion.zsh) Download [.gita-completion.bash](https://github.com/nosarthur/gita/blob/master/.gita-completion.bash) and source it in shell.
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 ## <a name='superman'></a> Superman mode
@ -380,10 +391,10 @@ their results agree.
## Tips ## Tips
effect | shell command | effect | shell command |
---|--- | ------------------------- | ---------------------------------------- |
enter `<repo>` directory|`` cd `gita ls <repo>` `` | enter `<repo>` directory | `` cd `gita ls <repo>` `` |
delete repos in `<group>` | `gita group ll <group> \| xargs gita rm` | delete repos in `<group>` | `gita group ll <group> \| xargs gita rm` |
## Contributing ## Contributing
@ -411,4 +422,4 @@ I haven't tried them but I heard good things about them.
- [myrepos](https://myrepos.branchable.com/) - [myrepos](https://myrepos.branchable.com/)
- [repo](https://source.android.com/setup/develop/repo) - [repo](https://source.android.com/setup/develop/repo)
- [mu-repo](https://github.com/fabioz/mu-repo)

View file

@ -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)'

473
auto-completion/zsh/_gita Normal file
View file

@ -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 "$@"

View file

@ -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()

View file

@ -18,14 +18,15 @@ import os
import sys import sys
import csv import csv
import argparse import argparse
import argcomplete
import subprocess import subprocess
from functools import partial from functools import partial
import pkg_resources
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
import glob 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: 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: with open(csv_config, "w", newline="") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(to_display) 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): def f_clone(args: argparse.Namespace):
if args.dry_run: if args.dry_run:
if args.from_file: 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}") print(f"git clone {url} {abs_path}")
else: else:
print(f"git clone {args.clonee}") print(f"git clone {args.clonee}")
@ -172,28 +188,35 @@ def f_clone(args: argparse.Namespace):
f_add(args) f_add(args)
return return
# TODO: add repos to group too
repos, groups = io.parse_clone_config(args.clonee)
if args.preserve_path: if args.preserve_path:
utils.exec_async_tasks( utils.exec_async_tasks(
utils.run_async(repo_name, path, ["git", "clone", url, abs_path]) utils.run_async(repo_name, path, ["git", "clone", r["url"], r["path"]])
for url, repo_name, abs_path in utils.parse_clone_config(args.clonee) for repo_name, r in repos.items()
) )
else: else:
utils.exec_async_tasks( utils.exec_async_tasks(
utils.run_async(repo_name, path, ["git", "clone", url]) utils.run_async(repo_name, path, ["git", "clone", r["url"]])
for url, repo_name, _ in utils.parse_clone_config(args.clonee) for repo_name, r in repos.items()
) )
def f_freeze(args): def f_freeze(args):
repos = utils.get_repos() """
print repo and group information for future cloning
"""
ctx = utils.get_context() ctx = utils.get_context()
if args.group is None and ctx: if args.group is None and ctx:
args.group = ctx.stem args.group = ctx.stem
repos = utils.get_repos()
group_name = args.group
group_repos = None group_repos = None
if args.group: # only display repos in this group if group_name: # only display repos in this group
group_repos = utils.get_groups()[args.group]["repos"] group_repos = utils.get_groups()[group_name]["repos"]
repos = {k: repos[k] for k in group_repos if k in repos} repos = {k: repos[k] for k in group_repos if k in repos}
seen = {""} seen = {""}
# print(repos)
for name, prop in repos.items(): for name, prop in repos.items():
path = prop["path"] path = prop["path"]
url = "" url = ""
@ -211,7 +234,16 @@ def f_freeze(args):
url = parts[1] url = parts[1]
if url not in seen: if url not in seen:
seen.add(url) 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): def f_ll(args: argparse.Namespace):
@ -435,8 +467,9 @@ def main(argv=None):
title="sub-commands", help="additional help with sub-command -h" title="sub-commands", help="additional help with sub-command -h"
) )
version = pkg_resources.require("gita")[0].version p.add_argument(
p.add_argument("-v", "--version", action="version", version=f"%(prog)s {version}") "-v", "--version", action="version", version=f"%(prog)s {get_version()}"
)
# bookkeeping sub-commands # bookkeeping sub-commands
p_add = subparsers.add_parser("add", description="add repo(s)", help="add repo(s)") 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_cmds.add_parser("rm", description="Disable information item.").add_argument(
"info_item", choices=info.ALL_INFO_ITEMS, help="information item to delete" "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: ll_doc = f""" status symbols:
+: staged changes +: staged changes
*: unstaged changes *: unstaged changes
_: untracked files/folders ?: untracked files/folders
$: stashed changes
branch colors: branch colors:
{info.Color.white}white{info.Color.end}: local has no remote {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() cmds = utils.get_cmds_from_files()
for name, data in cmds.items(): for name, data in cmds.items():
help = data.get("help") help = data.get("help")
repo_help = help
cmd = data["cmd"] cmd = data["cmd"]
if data.get("allow_all"): if data.get("allow_all"):
choices = utils.get_choices() choices = utils.get_choices()
nargs = "*" nargs = "*"
help += " for all repos or" repo_help += " for all repos or"
else: else:
choices = utils.get_repos().keys() | utils.get_groups().keys() choices = utils.get_repos().keys() | utils.get_groups().keys()
nargs = "+" nargs = "+"
help += " for the chosen repo(s) or group(s)" repo_help += " for the chosen repo(s) or group(s)"
sp = subparsers.add_parser(name, description=help) sp = subparsers.add_parser(name, description=help, help=help)
sp.add_argument("repo", nargs=nargs, choices=choices, help=help) sp.add_argument("repo", nargs=nargs, choices=choices, help=repo_help)
is_shell = bool(data.get("shell")) is_shell = bool(data.get("shell"))
sp.add_argument( sp.add_argument(
"-s", "-s",
@ -805,6 +845,7 @@ def main(argv=None):
cmd = cmd.split() cmd = cmd.split()
sp.set_defaults(func=f_git_cmd, cmd=cmd) sp.set_defaults(func=f_git_cmd, cmd=cmd)
argcomplete.autocomplete(p)
args = p.parse_args(argv) args = p.parse_args(argv)
args.async_blacklist = { args.async_blacklist = {

View file

@ -22,8 +22,13 @@
"cmd": "git log -1 HEAD", "cmd": "git log -1 HEAD",
"help": "show log information of HEAD" "help": "show log information of HEAD"
}, },
"log": "lo":{
{"cmd": "git log", "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, "disable_async": true,
"help": "show logs" "help": "show logs"
}, },
@ -77,10 +82,12 @@
}, },
"stat":{ "stat":{
"cmd": "git diff --stat", "cmd": "git diff --stat",
"allow_all": true,
"help": "show edit statistics" "help": "show edit statistics"
}, },
"st":{ "st":{
"cmd": "git status", "cmd": "git status",
"allow_all": true,
"help": "show status" "help": "show status"
}, },
"tag":{ "tag":{

View file

@ -9,6 +9,40 @@ from typing import Tuple, List, Callable, Dict
from . import common 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): class Color(Enum):
""" """
Terminal color Terminal color
@ -113,8 +147,8 @@ def get_info_items() -> List[str]:
return display_items return display_items
def get_path(prop: Dict[str, str]) -> str: def get_path(prop: Dict[str, str], truncator: Truncate) -> str:
return f'{Color.cyan}{prop["path"]}{Color.end}' return f'{Color.cyan}{truncator.truncate("path", prop["path"])}{Color.end}'
# TODO: do we need to add the flags here too? # 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) 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. Return the last commit message.
""" """
@ -177,10 +225,10 @@ def get_commit_msg(prop: Dict[str, str]) -> str:
universal_newlines=True, universal_newlines=True,
cwd=prop["path"], 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. Return the last commit time in parenthesis.
""" """
@ -192,13 +240,14 @@ def get_commit_time(prop: Dict[str, str]) -> str:
universal_newlines=True, universal_newlines=True,
cwd=prop["path"], cwd=prop["path"],
) )
return f"({result.stdout.strip()})" return truncator.truncate("commit_time", f"({result.stdout.strip()})")
default_symbols = { default_symbols = {
"dirty": "*", "dirty": "*",
"staged": "+", "staged": "+",
"untracked": "?", "untracked": "?",
"stashed": "$",
"local_ahead": "", "local_ahead": "",
"remote_ahead": "", "remote_ahead": "",
"diverged": "", "diverged": "",
@ -223,11 +272,11 @@ def get_symbols() -> Dict[str, str]:
return default_symbols return default_symbols
def get_repo_status(prop: Dict[str, str], no_colors=False) -> str: def get_repo_status(prop: Dict[str, str], truncator: Truncate, no_colors=False) -> str:
branch = get_head(prop["path"]) branch = truncator.truncate("branch", get_head(prop["path"]))
dirty, staged, untracked, situ = _get_repo_status(prop) dirty, staged, untracked, stashed, situ = _get_repo_status(prop)
symbols = get_symbols() 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: if no_colors:
return f"{info:<18}" 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}" return f"{color}{info:<18}{Color.end}"
def get_repo_branch(prop: Dict[str, str]) -> str: def get_repo_branch(prop: Dict[str, str], truncator: Truncate) -> str:
return get_head(prop["path"]) 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 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 "" dirty = "dirty" if run_quiet_diff(flags, [], path) else ""
staged = "staged" if run_quiet_diff(flags, ["--cached"], path) else "" staged = "staged" if run_quiet_diff(flags, ["--cached"], path) else ""
untracked = "untracked" if has_untracked(flags, 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) diff_returncode = run_quiet_diff(flags, ["@{u}", "@{0}"], path)
if diff_returncode == 128: 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" situ = "diverged" if diverged else "remote_ahead"
else: # local is ahead of remote else: # local is ahead of remote
situ = "local_ahead" situ = "local_ahead"
return dirty, staged, untracked, situ return dirty, staged, untracked, stashed, situ
ALL_INFO_ITEMS = { ALL_INFO_ITEMS = {

33
gita/io.py Normal file
View file

@ -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

View file

@ -7,7 +7,7 @@ import platform
import subprocess import subprocess
from functools import lru_cache from functools import lru_cache
from pathlib import Path 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 collections import Counter, defaultdict
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import multiprocessing import multiprocessing
@ -367,15 +367,6 @@ def auto_group(repos: Dict[str, Dict[str, str]], paths: List[str]) -> Dict[str,
return new_groups 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]: 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 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 Return the status of all repos
""" """
if repos: if repos:
truncator = info.Truncate()
name_width = len(max(repos, key=len)) + 1 name_width = len(max(repos, key=len)) + 1
funcs = info.get_info_funcs(no_colors=no_colors) funcs = info.get_info_funcs(no_colors=no_colors)
num_threads = min(multiprocessing.cpu_count(), len(repos)) num_threads = min(multiprocessing.cpu_count(), len(repos))
with ThreadPoolExecutor(max_workers=num_threads) as executor: with ThreadPoolExecutor(max_workers=num_threads) as executor:
for line in executor.map( 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), sorted(repos),
): ):
yield line yield line

View file

@ -7,7 +7,7 @@ with open("README.md", encoding="utf-8") as f:
setup( setup(
name="gita", name="gita",
packages=["gita"], packages=["gita"],
version="0.16.6", version="0.16.7.2",
license="MIT", license="MIT",
description="Manage multiple git repos with sanity", description="Manage multiple git repos with sanity",
long_description=long_description, long_description=long_description,
@ -19,6 +19,7 @@ setup(
author_email="zhou.dong@gmail.com", author_email="zhou.dong@gmail.com",
entry_points={"console_scripts": ["gita = gita.__main__:main"]}, entry_points={"console_scripts": ["gita = gita.__main__:main"]},
python_requires="~=3.6", python_requires="~=3.6",
install_requires=["argcomplete"],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -29,10 +30,12 @@ setup(
"Topic :: Software Development :: Version Control :: Git", "Topic :: Software Development :: Version Control :: Git",
"Topic :: Terminals", "Topic :: Terminals",
"Topic :: Utilities", "Topic :: Utilities",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
], ],
include_package_data=True, include_package_data=True,
) )

View file

@ -143,7 +143,7 @@ class TestLsLl:
@patch("gita.info.get_head", return_value="master") @patch("gita.info.get_head", return_value="master")
@patch( @patch(
"gita.info._get_repo_status", "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_msg", return_value="msg")
@patch("gita.info.get_commit_time", return_value="") @patch("gita.info.get_commit_time", return_value="")
@ -196,8 +196,11 @@ def test_clone_with_url(mock_run):
@patch( @patch(
"gita.utils.parse_clone_config", "gita.io.parse_clone_config",
return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]], return_value=(
{"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}},
{},
),
) )
@patch("gita.utils.run_async", new=async_mock()) @patch("gita.utils.run_async", new=async_mock())
@patch("subprocess.run") @patch("subprocess.run")
@ -217,8 +220,11 @@ def test_clone_with_config_file(*_):
@patch( @patch(
"gita.utils.parse_clone_config", "gita.io.parse_clone_config",
return_value=[["git@github.com:user/repo.git", "repo", "/a/repo"]], return_value=(
{"repo": {"url": "git@github.com:user/repo.git", "path": "/a/repo"}},
{},
),
) )
@patch("gita.utils.run_async", new=async_mock()) @patch("gita.utils.run_async", new=async_mock())
@patch("subprocess.run") @patch("subprocess.run")