diff --git a/.editorconfig b/.editorconfig index bc1a4e7..b715678 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,3 +20,11 @@ trim_trailing_whitespace = false [*.py] indent_size = 4 + +[*.bats] +indent_style = tab +indent_size = 4 + +[tests/*.sh] +indent_style = tab +indent_size = 4 diff --git a/.github/.ignore_words b/.github/.ignore_words index 8aad33d..35d9297 100644 --- a/.github/.ignore_words +++ b/.github/.ignore_words @@ -1 +1,2 @@ gool +Cant diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b409721..9e66f95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,53 +6,109 @@ on: branches: [main] jobs: lint: - runs-on: 'ubuntu-latest' + runs-on: ubuntu-latest steps: + - name: Check out code. + uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: 'Get Changed Files' id: 'files' - uses: 'masesgroup/retrieve-changed-files@v3' - with: - format: 'json' - # a force push will cause the action above to fail. Don't do force push when people are - # reviewing! - - name: Check out code. - uses: actions/checkout@v4 - - uses: 'actions/setup-go@v5' + run: | + CHANGED_FILES=$(git diff --name-only "${BASE_SHA:-$BEFORE_SHA}...${HEAD_SHA:-$GITHUB_REF}") + echo "::notice::Changed files: $CHANGED_FILES" + { + echo "added_modified<> "$GITHUB_OUTPUT" + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BEFORE_SHA: "${{ github.event.before }}" + - uses: 'actions/setup-go@v6' with: go-version: '1.20' + - name: 'Install EditorConfig Lint' + run: go install 'github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@latest' - name: 'Check EditorConfig Lint' - run: | - sudo apt install -y jq - go install 'github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@latest' - - readarray -t changed_files <<<"$(jq -r '.[]' <<<'${{ steps.files.outputs.added_modified }}')" - ~/go/bin/editorconfig-checker ${changed_files[@]} + run: echo "$FILES" | xargs ~/go/bin/editorconfig-checker + env: + # NOTE: use env to pass the output in order to avoid possible injection attacks + FILES: "${{ steps.files.outputs.added_modified }}" + - name: checkstyle + run: ./scripts/checkstyle.py + - name: Shellcheck + run: shellcheck --severity=error bin/* ./*.sh + - name: Lint and format Python with Ruff + uses: astral-sh/ruff-action@v3 typo: runs-on: ubuntu-latest steps: - name: Check out code. - uses: actions/checkout@v4 - - name: spell check - run: | - pip install codespell==2.2 - git grep --cached -l '' | grep -v -e 'History\.md' -e 'AUTHORS' -e 'man/.*\.1' -e 'man/.*\.html' | xargs codespell --ignore-words=.github/.ignore_words - - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + uses: actions/checkout@v6 + - name: Install poetry + run: pip install poetry + - name: Set up Python + uses: actions/setup-python@v6 with: - python-version: '3.12' + cache: 'poetry' + cache-dependency-path: "tests/pyproject.toml" + python-version-file: "tests/pyproject.toml" - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install pytest==8.1.2 GitPython==3.1.43 testpath==0.6.0 - - name: Unit test - run: make test + cd tests || exit + poetry install --only dev + - name: spell check + run: | + cd tests + git grep --cached -l '' .. | \ + grep -v -e 'History\.md' -e 'AUTHORS' -e 'man/.*\.1' -e 'man/.*\.html' | \ + xargs poetry run codespell --ignore-words=../.github/.ignore_words + + test-pytest: + name: 'Test with Pytest' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - name: Install poetry + run: pip install poetry + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'poetry' + cache-dependency-path: "tests/pyproject.toml" + python-version-file: "tests/pyproject.toml" + - name: Install Python Dependencies + run: | + cd tests || exit + poetry install --only test + - name: Test with Pytest + run: | + cd tests + poetry run pytest + + test-bats: + name: 'Test with Bats' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - name: Setup Bats + id: setup-bats + uses: bats-core/bats-action@4.0.0 + with: + bats-version: 'v1.8.1' + - name: Test with Bats + env: + BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} + TERM: xterm + run: bats ./tests build: strategy: @@ -64,14 +120,12 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Check out code. - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Linux Install if: matrix.platform == 'ubuntu-latest' - run: | - sudo apt-get install -y bsdmainutils + run: sudo apt-get install -y bsdmainutils - name: Script - run: | - ./check_integrity.sh $(find bin | cut -b 5- | xargs) + run: ./check_integrity.sh - name: Brew release if: matrix.platform == 'macos-latest' run: | diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..ff2fc42 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,18 @@ +name: "Maintenance: Close Stale PRs" +on: + schedule: + - cron: "30 1 * * *" + +permissions: + pull-requests: "write" + +jobs: + stale: + runs-on: "ubuntu-latest" + if: github.repository_owner == 'tj' + steps: + - uses: "actions/stale@v10" + with: + close-pr-message: "This PR was closed because it has been stalled for 365 days with no activity. Feel free to make a new PR if you wish to continue" + days-before-pr-stale: 350 + days-before-pr-close: 15 diff --git a/.gitignore b/.gitignore index a230a78..294ece4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv/ __pycache__/ +coverage/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..33dec90 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/bats-all"] + path = vendor/bats-all + url = https://github.com/hyperupcall/bats-all diff --git a/AUTHORS b/AUTHORS index a008703..06598c6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,12 +1,8 @@ -git-extras is written and maintained by TJ Holowaychuk and -various contributors: - -Development Lead -```````````````` -- TJ Holowaychuk +git-extras is written and maintained by various contributors: Maintainers ``````````` +- TJ Holowaychuk - Hemanth.HM - Nimit Kalra - Nicolai Skogheim @@ -43,12 +39,15 @@ Patches and Suggestions - Luke Childs - Sasha Khamkov - equt +- oikarinen - vyas - Don Harper +- Pierre Ayoub - Robin Winslow - Ross Smith II - Yi EungJun - grindhold +- wyattscarpenter - Aggelos Orfanakos - Camille Reynders - Carlos Prado @@ -65,11 +64,13 @@ Patches and Suggestions - Dominik Gedon - Hogan Long - Ivan Malopinsky +- John Bachir - Jonathan "Duke" Leto - Julio Napurí - Justin Dugger - Nils Winkler - Philipp Klose +- Raphael Boidol - Richard Russon - Sam Bostock - Vladimir Jimenez @@ -85,6 +86,7 @@ Patches and Suggestions - Beth Skurrie - Brice Dutheil - Curtis McEnroe +- Daniele Paolella - David Rogers - Devin Withers - Domenico Rotiroti @@ -129,6 +131,7 @@ Patches and Suggestions - Andrew Griffiths - Andrew Marcinkevičius - Andrew Starr-Bochicchio +- Andrew Sullivan Cant - Andrey Elizarov - Angel Aguilera - Antoine Beaupré @@ -146,6 +149,7 @@ Patches and Suggestions - Christophe Badoit - Ciro Nunes - CleanMachine1 +- CodeByZach - Craig MacGregor - Dan Goodliffe - Dan Jackson @@ -160,20 +164,24 @@ Patches and Suggestions - Edward Betts - Eli Schwartz - Emil Kjelsrud +- Erik Skultety - François M - George Crabtree - Gerrit-K - Greg Allen +- Guilhem Saurel - Guillermo Rauch - Gunnlaugur Thor Briem - Hasse Ramlev Hansen - Heiko Becker +- Hugo Ruiz-Mireles - Isaac Mungai - J.C. Yamokoski - Jack O. Wasey - James Manning - James Zhu - Jan Krueger +- Jared Baur - Jarod Stewart - Jason Young - Jens K. Mueller @@ -201,7 +209,9 @@ Patches and Suggestions - Mathieu D. (MatToufoutu) - Matt - Matt Colyer +- Matt Headley - Matthew Avant +- Mattias Andersson - Michael Komitee - Michael Matuzak - Michele Bologna @@ -224,9 +234,12 @@ Patches and Suggestions - Raphael Fleischlin - Rasmus Wriedt Larsen - René +- Revisor +- Ricardo Fernández Serrata - Riceball LEE - Rob Kennedy - Robin von Bülow +- Russ - Ryan Bohn - Sachin Gupta - Sam Thursfield @@ -254,6 +267,8 @@ Patches and Suggestions - Vitaly Chikunov - Wei Lee - Wei Wu +- Wen Sun +- Wiktor Żurawik - Xavier Krantz - Xiaopei Li - Zeeshan Ahmed @@ -262,15 +277,16 @@ Patches and Suggestions - dead-horse - eszabpt - fengkx +- johnpyp - jykntr - kang - meza - neydroid - nulltask +- phanium - sgleizes - tfendin - tiemonl -- Zachary Miller - zentarul - zeroDivisible - zhiyanfoo diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e7e7a3..7336c6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Let's say you wish to add a new command. Assuming your new command is named `foo 5. (Optional) Update `./etc/bash_completion.sh`. 6. (Optional) Update `./etc/git-extras.fish`. 7. (Optional) Add a test under `./tests`. -8. Run `./check_integrity.sh foo` to check if all done. +8. Run `./check_integrity.sh foo` to check if all done. (You may also run `./check_integrity.sh` to check all commands.) You are welcome to open up an issue to discuss new commands or features before opening a pull request. diff --git a/Commands.md b/Commands.md index af72af0..295f71e 100644 --- a/Commands.md +++ b/Commands.md @@ -12,6 +12,7 @@ - [`git clear-soft`](#git-clear-soft) - [`git coauthor`](#git-coauthor) - [`git commits-since`](#git-commits-since) + - [`git continue`](#git-continue) - [`git contrib`](#git-contrib) - [`git count`](#git-count) - [`git cp`](#git-cp) @@ -74,6 +75,8 @@ - [`git undo`](#git-undo) - [`git unlock`](#git-unlock) - [`git utimes`](#git-utimes) + - [`git unwip`](#git-unwip) + - [`git wip`](#git-wip) ## git extras @@ -279,11 +282,20 @@ usage: git bulk [-g] ([-a]|[-w ]) git bulk --listall ``` - Register a workspace so that `git bulk` knows about it (notice that must be absolute path): + Register a workspace so that `git bulk` knows about it (it will be registered in your `.gitconfig`): ```bash $ git bulk --addworkspace personal ~/workspaces/personal ``` + + Notice that `` must be an absolute path (or an environment variable pointing to an absolute path). + In the case of a **single quoted environment variable**, it will be dereferenced at `git-bulk` runtime, suitable for dynamic workspaces (*e.g.*, defined in your `.bashrc`). + As an illustration: + +```bash +$ git bulk --addworkspace personal '$PERSONAL_WORKSPACE' +``` + With option `--from` the URL to a single repository or a file containing multiple URLs can be added and they will be cloned directly into the workspace. Suitable for the initial setup of a multi-repo project. ```bash @@ -375,14 +387,53 @@ Git read-eval-print-loop. Lets you run `git` commands without typing 'git'. Commands can be prefixed with an exclamation mark (!) to be interpreted as a regular command. -Type `exit` or `quit` to end the repl session. +Type `exit`, `quit`, or `q` to end the repl session. + +Any arguments to git repl will be taken as the first command to execute in +the repl. + +### CONFIGURATION + +Commands entered in a repl session will be saved to a history file and be available in +future sessions, similar to a shell or programming language repl. By default, +there is one global history file, ~/.git_repl_history. You can specify that your projects +each have their own independent history file. This file will be saved in .git_repl_history +at the top level of the repo, and will need to be added to the repo or global .gitignore. + +```bash +# remove the --global flag to configure only an individual project to have its own history file +git config --global git-extras.repl.use-local-history "true" +``` + +You can specify a default command to run when hitting enter: + +```bash +git config --global git-extras.repl.on-enter-command "git status -sb" +``` + +You can configure which character is used at the end of the prompt: (default `>`): + +```bash +git config --global git-extras.repl.prompt-character "±" +``` + +You can specify the prefix for the prompt, or remove it (default `git`): + +```bash +git config --global git-extras.repl.prefix "" +``` + +You can have the name of the current git repo shown in the prompt (default `false`): + +```bash +git config --global git-extras.repl.show-project-name "true" +``` ```bash $ git repl -git version 2.9.2 -git-extras version 3.0.0 -type 'ls' to ls files below current directory, -'!command' to execute any command or just 'subcommand' to execute any git subcommand +git version 2.34.1 +git-extras version 7.3.0 +Type 'ls' to ls files below current directory; '!command' to execute any command or just 'subcommand' to execute any git subcommand; 'quit', 'exit', 'q', ^D, or ^C to exit the git repl. git (master)> ls-files History.md @@ -413,9 +464,9 @@ $ git coauthor user user@email.com 2 files changed, 145 insertions(+), 0 deletions(-) create mode 100644 README.md create mode 100644 CONTRIBUTING.md - + $ git log -1 - + commit b62ceae2685e6ece071f3c3754e9b77fd0a35c88 (HEAD -> master) Author: user person Date: Sat Aug 17 17:33:53 2019 -0500 @@ -1296,7 +1347,7 @@ Creates a zip archive of the current git repository. The name of the archive wil ## git missing -Print out which commits are on one branch or the other but not both. +Print out which commits are on one branch or the other but not both. Optionally, you can specify a path to limit the comparison to a specific directory or file. ```bash $ git missing master @@ -1304,6 +1355,12 @@ $ git missing master > 97ef387 only on master ``` +```bash +$ git missing master -- src/ +< ed52989 only on current branch, in src/ directory +> 7988c4b only on master, in src/ directory +``` + ## git lock Lock a local file `filename`: @@ -1360,7 +1417,7 @@ Switched to branch 'mr/51' With full URL, the head is fetched from a temporary remote pointing to the base URL. ``` bash -$ git mr https://gitlab.com/owner/repository/merge_requests/51 +$ git mr https://gitlab.com/owner/repository/merge_requests/51 From gitlab.com:owner/repository * [new ref] refs/merge-requests/51/head -> mr/51 Switched to branch 'mr/51' @@ -1615,3 +1672,25 @@ Abort current revert, rebase, merge or cherry-pick, without the need to find exa ## git magic Commits changes with a generated message. + +## git continue + +Continue current revert, rebase, merge or cherry-pick, without the need to find exact command in history. + +## git wip + +Create a Work In Progress(WIP) commit, which will include all changes in the +working directory. (i.e., changes to existing files, new files, removed files) + +```bash +$ git wip +``` + +## git unwip + +Undo a Work In Progress(WIP) commit and put all of those changes back into the +working directory. + +```bash +$ git unwip +``` diff --git a/History.md b/History.md index 20fbcbe..fc9617e 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,83 @@ +7.5.0 / 2026-05-09 +================== + + * Use standard prefix for home dir installs (#1248) + * refactor(abort): common `gitdir` (#1244) + * Fix man page usage docs (#1241) + * Fix `git-changelog` to handle whitespace in command arguments (#1232) + * chore(deps): bump bats-core/bats-action from 3.0.1 to 4.0.0 (#1230) + * add details when multiple active operations found (#1229) + * Add persistent history to `git-repl` (#1226) + * Support passing ref to `git-commits-since` (#1228) + * ability to specify command when hitting enter (#1223) + * Improve `git-repl` prompt (#1224) + * feat: `delete-branch` multiple unique branch names completions (#1221) + * chore(deps): bump actions/checkout from 5 to 6 (#1219) + * Made Git Magic use `--force-with-lease` (#1218) + * chore(deps): bump actions/stale from 9 to 10 (#1214) + * chore(deps): bump actions/setup-go from 5 to 6 (#1215) + * chore(deps): bump actions/setup-python from 5 to 6 (#1216) + * Fix minor typo in `git rename-branch` man page (#1211) + * Improvements to Bash completion (#1210) + * Fix use of `git-whatchanged` to `git-log` (#1212) + * chore(deps): bump actions/checkout from 4 to 5 (#1209) + * Bump version to 7.5.0-dev (#1207) + +7.4.0 / 2025-06-19 +================== + + * Format comparisons, functions, and redirections to be consistent (#1201) + * add git-wip and git-unwip (#669) + * chore(deps): bump bats-core/bats-action from 3.0.0 to 3.0.1 + * Mostly finish pytest to Bats conversion (#1200) + * Implement half of tests in Bats (#1187) + * Add stale bot for old PRs (#1186) + * feat(git-bulk): add new option to not follow hidden directories (#1195) + * Feat: allow git-summary showing full path of repository (#1193) + * feat(git-bulk): add new option to no follow symlinks (#1194) + * docs(git-bulk): Add zsh completion (#1190) + * fix(git-bulk): fix workspace selection when cd fails (#1197) + * fix(git-bulk): quiet find errors by default (#1196) + * fix(git-bulk): fix a bad integer expression (#1198) + * chore(deps): bump astral-sh/ruff-action from 2 to 3 (#1189) + * Fix all ShellCheck errors and add to CI (#1179) + * chore(deps): bump astral-sh/ruff-action from 1 to 2 (#1188) + * fix(ci): use poetry (#1183) + * Delete etc/test.fish (#1185) + * feat: add git-continue (#1176) + * feat: add ruff linter with ci check (#1182) + * fix(ci): missing dollar sign (#1184) + * fix(github-actions): changed files output for editorcondig-checker (#1180) + * Revert "feat: add ruff linter with ci check (#1178)" (#1181) + * feat: add ruff linter with ci check (#1178) + * Support `GITHUB_TOKEN` var for `git-fork` and `git-pull-request` (#1177) + * Bump version to 7.4.0-dev (#1175) + +7.3.0 / 2024-10-20 +================== + + * Fix stripping trailing forward slash for git-get (#1172) + * Change git-cp to use cleaner branch approach (#1169) + * Improve warning for `git clear` (#1168) + * Enhance `git-repl` (#1160) + * Update some documentation that was out of sync (#1164) + * Use filetimes in check_integrity (#1162) + * Update git-alias.md: add brs to prevent incorrect line behavior (#1161) + * Update git-bulk.md: use correct stylization in synopsis (#1163) + * Update git-repl.md: typo: "let's" for "lets" (#1158) + * Update instructions for the OpenSUSE installation (#1157) + * Add pathspec support in `git-missing` (#1156) + * feat: add rename-file command (#1149) + * Update Commands.md (#1148) + * fix: git-summary commit count (#1147) + * tests: update dependencies (#1142) + * chore(deps): bump masesgroup/retrieve-changed-files from 2 to 3 (#1144) + * chore(deps): bump actions/setup-go from 4 to 5 (#1143) + * ci: add dependabot update for GitHub actions (#1141) + * ci: update actions (#1140) + * Bump version to 7.3.0-dev (#1139) + 7.2.0 / 2024-04-21 ================== diff --git a/Installation.md b/Installation.md index 2e82a69..dfe2ccc 100644 --- a/Installation.md +++ b/Installation.md @@ -41,10 +41,10 @@ $ sudo dnf install git-extras ### openSUSE -Substitute your openSUSE version in the command below (in this case we are considering openSUSE Leap 15.2): +Substitute your openSUSE version in the command below (in this case we are considering openSUSE Leap 15.6): ```bash -$ sudo zypper ar https://download.opensuse.org/repositories/devel:/tools:/scm/openSUSE_Leap_15.2/devel:tools:scm.repo +$ sudo zypper ar https://download.opensuse.org/repositories/devel:/tools:/scm/15.6/devel:tools:scm.repo ``` and install it: @@ -144,7 +144,7 @@ alternate location, specify a `PREFIX` when calling `make`. ```bash # Non-root users can install under their home directory -make install PREFIX=$HOME/software +make install PREFIX=$HOME/.local # For third-party software kept under /opt make install PREFIX=/opt diff --git a/bin/git-abort b/bin/git-abort index 9c17380..19b544a 100755 --- a/bin/git-abort +++ b/bin/git-abort @@ -1,17 +1,45 @@ #!/usr/bin/env bash +set -euo pipefail + +# git rev-parse emits an error if not in a git repo so only need to bail out gitdir="$(git rev-parse --git-dir)" || exit -opfound= -fcnt= -for i in cherry-pick merge rebase revert; do - f=${i^^} - f=${f/-/_} - test -f "${gitdir}/${f}_HEAD" && fcnt=1$fcnt && opfound=$i -done -if [ "${fcnt}" != 1 ]; then - echo "I don't know what to abort" >&2 - exit 1 -fi +discover_op() { + local op + for op in cherry_pick merge rebase revert ; do + if [ -f "${gitdir}/${op^^}_HEAD" ]; then + echo "${op/_/-}" + fi + done +} -git "${opfound}" --abort +validate_op() { + local op="$1" + if [ -z "$op" ]; then + echo "No active operation found" >&2 + exit 1 + fi + if [[ "$(echo "$op" | wc -l)" -gt 1 ]]; then + echo "Multiple active operations found:" >&2 + for o in $op; do + echo " - $o: HEAD: $(cat "${gitdir}/${o}_HEAD")" >&2 + done + exit 1 + fi +} + +discover_action() { + local action=${1/git-/} + if [ "$action" != "abort" ] && [ "$action" != "continue" ]; then + echo "Invalid action: $1" >&2 + exit 1 + fi + echo "$action" +} + +action=$(discover_action "$(basename "$0")") +op=$(discover_op) +validate_op "$op" + +git "$op" "--$action" diff --git a/bin/git-bulk b/bin/git-bulk index e3bdd66..06eecd2 100755 --- a/bin/git-bulk +++ b/bin/git-bulk @@ -9,12 +9,14 @@ guardedmode=false singlemode=false allwsmode=false quiet=false +no_follow_symlinks=false +no_follow_hidden=false # # print usage message # usage() { - echo 1>&2 "usage: git bulk [-q|--quiet] [-g] ([-a]|[-w ]) " + echo 1>&2 "usage: git bulk [--no-follow-symlinks] [--no-follow-hidden] [-q|--quiet] [-g] ([-a]|[-w ]) " echo 1>&2 " git bulk --addworkspace (--from )" echo 1>&2 " git bulk --removeworkspace " echo 1>&2 " git bulk --addcurrent " @@ -28,7 +30,7 @@ cdfail() { } # add another workspace to global git config -function addworkspace { +addworkspace() { git config --global bulkworkspaces."$wsname" "$wsdir"; if [ -n "$source" ]; then if [ ! -d "$wsdir" ]; then echo 1>&2 "Path of workspace doesn't exist, make it first."; exit 1; fi @@ -57,19 +59,19 @@ function addworkspace { } # add current directory -function addcurrent { git config --global bulkworkspaces."$wsname" "$PWD"; } +addcurrent() { git config --global bulkworkspaces."$wsname" "$PWD"; } # remove workspace from global git config -function removeworkspace { checkWSName && git config --global --unset bulkworkspaces."$wsname"; } +removeworkspace() { checkWSName && git config --global --unset bulkworkspaces."$wsname"; } # remove workspace from global git config -function purge { git config --global --remove-section bulkworkspaces; } +purge() { git config --global --remove-section bulkworkspaces; } # list all current workspace locations defined -function listall { git config --global --get-regexp bulkworkspaces; } +listall() { git config --global --get-regexp bulkworkspaces; } # guarded execution of a git command in one specific repository -function guardedExecution () { +guardedExecution () { if [ "${quiet?}" != "true" ] || $guardedmode; then echo 1>&2 "${bldred}->${reset} executing ${inverse}git $gitcommand${reset} in repository ${leadingpath%/*}/${bldred}${curdir##*/}${reset}" fi @@ -86,7 +88,7 @@ function guardedExecution () { } # check if the passed command is known as a core git command -function checkGitCommand () { +checkGitCommand () { if git help -a | grep -o -q "\b${corecommand}\b"; then echo 1>&2 "Core command \"$corecommand\" accepted." else @@ -99,7 +101,7 @@ function checkGitCommand () { } # check if workspace name is registered -function checkWSName () { +checkWSName () { while read -r workspace; do parseWsName "$workspace" if [[ $rwsname == "$wsname" ]]; then return; fi @@ -109,14 +111,24 @@ function checkWSName () { } # parse out wsname from workspacespec -function parseWsName () { +parseWsName () { local wsspec="$1" + # Get the workspace value from its specification in the `.gitconfig`. + # May be an absolute path or a variable name of the form: `$VARNAME` rwsdir=${wsspec#* } + if [[ ${rwsdir:0:1} == '$' ]]; then + # Dereference the `rwsdir` value which is a variable name. + rwsdir_varname=${rwsdir:1} + rwsdir=${!rwsdir_varname} + if [[ -z "${rwsdir}" ]]; then + echo 1>&2 "error: bad environment variable: $rwsdir_varname" && exit 1 + fi + fi rwsname=${wsspec#*.} && rwsname=${rwsname%% *} } # detects the wsname of the current directory -function wsnameToCurrent () { +wsnameToCurrent () { while read -r workspace; do if [ -z "$workspace" ]; then continue; fi parseWsName "$workspace" @@ -128,33 +140,44 @@ function wsnameToCurrent () { } # helper to check number of arguments. -function allowedargcount () { - if [ "$paramcount" -ne "$1" ] && [ "$paramcount" -ne "$2" ]; then +allowedargcount () { + if [ "$paramcount" -ne "${1:-0}" ] && [ "$paramcount" -ne "${2:-0}" ]; then echo 1>&2 "error: wrong number of arguments" && usage; exit 1; fi } # execute the bulk operation -function executBulkOp () { +executBulkOp () { checkGitCommand if ! $allwsmode && ! $singlemode; then wsnameToCurrent; fi # by default git bulk works within the 'current' workspace listall | while read -r workspacespec; do parseWsName "$workspacespec" if [[ -n $wsname ]] && [[ $rwsname != "$wsname" ]]; then continue; fi - eval cd "\"$rwsdir\"" + cd "$rwsdir" || exit 1 local actual=$PWD [ "${quiet?}" != "true" ] && echo 1>&2 "Executing bulk operation in workspace ${inverse}$actual${reset}" - allGitFolders=( $(eval find -L . -name ".git") ) + # build `find` flags depending on command-line options + local find_flags=() + if [[ "$no_follow_symlinks" == true ]]; then + find_flags+=(-P) + else + find_flags+=(-L) + fi + # find all git repositories under the workspace on which we want to operate + readarray allGitFolders < <(find "${find_flags[@]}" . -name ".git" 2>/dev/null) for line in "${allGitFolders[@]}"; do local gitrepodir=${line::${#line}-5} # cut the .git part of find results to have the root git directory of that repository - eval cd "\"$gitrepodir\"" # into git repo location + cd "$gitrepodir" || exit 1 # into git repo location local curdir=$PWD local leadingpath=${curdir#"${actual}"} - guardedExecution "$@" - eval cd "\"$rwsdir\"" # back to origin location of last find command + # do not execute if we do not want to consider a ".git" directory under a hidden directory + if [ $no_follow_hidden = false ] || ! [[ "$leadingpath" =~ "/." ]]; then + guardedExecution "$@" + fi + cd "$rwsdir" || exit 1 # back to origin location of last find command done done } @@ -171,7 +194,11 @@ while [ "${#}" -ge 1 ] ; do --listall|--purge) butilcommand="${1:2}" && break ;; --removeworkspace|--addcurrent|--addworkspace) - butilcommand="${1:2}" && wsname="$2" && wsdir="$3" && if [ "$4" == "--from" ]; then source="$5"; fi && break ;; + butilcommand="${1:2}" && wsname="$2" && wsdir="$3" && if [ "$4" = "--from" ]; then source="$5"; fi && break ;; + --no-follow-symlinks) + no_follow_symlinks=true ;; + --no-follow-hidden) + no_follow_hidden=true ;; -a) allwsmode=true ;; -g) diff --git a/bin/git-changelog b/bin/git-changelog index ad5c2bb..ab4367f 100755 --- a/bin/git-changelog +++ b/bin/git-changelog @@ -9,6 +9,15 @@ GIT_MERGELOG_FORMAT="$(git config changelog.mergeformat)" GIT_EDITOR="$(git var GIT_EDITOR)" PROGNAME="git-changelog" +git_editor() { + if test -z "${GIT_EDITOR:+set}" + then + GIT_EDITOR="$(git var GIT_EDITOR)" || return $? + fi + + eval "$GIT_EDITOR" '"$@"' +} + _usage() { cat << EOF usage: $PROGNAME options [file] @@ -574,7 +583,7 @@ main() { rm -f "$tmpfile" if [[ -n "$GIT_EDITOR" ]]; then - $GIT_EDITOR "$changelog" || _exit + git_editor "$changelog" || _exit else less "$changelog" || _exit fi diff --git a/bin/git-clear b/bin/git-clear index e443290..6fd3ae8 100755 --- a/bin/git-clear +++ b/bin/git-clear @@ -2,7 +2,7 @@ PROGNAME="git-clear" FORCE=0 -function _usage() { +_usage() { cat << EOF usage: $PROGNAME options usage: $PROGNAME -h|help|? @@ -32,12 +32,12 @@ done # Only wait for answer if not forced by user if [[ $FORCE == 0 ]]; then - echo -n "Sure? - This command may delete files that cannot be recovered, including those in .gitignore [y/N]: " + echo -n "Sure? - THIS COMMAND MAY DELETE FILES THAT CANNOT BE RECOVERED, including those in .gitignore [y/N]: " read -r clean else clean=y fi -if [ "$clean" == "y" ]; then +if [ "$clean" = "y" ]; then git clean -d -f -x && git reset --hard fi diff --git a/bin/git-clear-soft b/bin/git-clear-soft index 0473b57..1274d21 100755 --- a/bin/git-clear-soft +++ b/bin/git-clear-soft @@ -2,6 +2,6 @@ echo -n "Sure? - This command may delete files that cannot be recovered. Files and directories in .gitignore will be preserved [y/N]: " read -r answer -if [ "$answer" == "y" ] +if [ "$answer" = "y" ] then git clean -d -f && git reset --hard fi diff --git a/bin/git-commits-since b/bin/git-commits-since index c07c530..5cd53e7 100755 --- a/bin/git-commits-since +++ b/bin/git-commits-since @@ -1,6 +1,36 @@ #!/usr/bin/env bash -SINCE="last week" -test $# -ne 0 && SINCE=$* -echo "... commits since $SINCE" >&2 -git log --pretty='%an - %s' --after="@{$SINCE}" \ No newline at end of file +REF="" +SINCE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -r|--ref) + if [[ -z "$2" ]]; then + echo "error: option $1 requires an argument" >&2 + exit 1 + fi + REF="$2" + shift + shift + ;; + *) + # non-flag args accumulation, e.g. "last " + "week" -> "last week" + SINCE="${SINCE:+$SINCE }$1" + shift + ;; + esac +done + +if [[ -n "$REF" && -n "$SINCE" ]]; then + echo "error: '--ref ' and '' are mutually exclusive" >&2 + exit 1 +fi + +SINCE="${SINCE:-last week}" + +if [[ -n "$REF" ]]; then + git log --pretty='%h %an - %s' "$REF..HEAD" +else + git log --pretty='%h %an - %s' --after="@{$SINCE}" +fi diff --git a/bin/git-continue b/bin/git-continue new file mode 120000 index 0000000..31ceda5 --- /dev/null +++ b/bin/git-continue @@ -0,0 +1 @@ +git-abort \ No newline at end of file diff --git a/bin/git-cp b/bin/git-cp index 0911824..cabb564 100755 --- a/bin/git-cp +++ b/bin/git-cp @@ -50,37 +50,29 @@ else fi fi - MERGE_OPT= - ff=$(git config --get merge.ff || true) - if [[ "$ff" == "only" ]]; then - MERGE_OPT="--ff" - fi echo "Copying $CURRENT_FILENAME into $DESTINATION_FILENAME" - INTERMEDIATE_FILENAME="${CURRENT_FILENAME//\//__}-move-to-${DESTINATION_FILENAME//\//__}" + # Pre-check that the source and destination will work + git mv --dry-run "${CURRENT_FILENAME}" "${DESTINATION_FILENAME}" - # We keep the existing file on the side in a commit - git mv "${CURRENT_FILENAME}" "${INTERMEDIATE_FILENAME}" - git commit -nm "Keep $CURRENT_FILENAME" + # Make a new branch and switch to it + BRANCH_NAME="git-cp-$(date +%s)" + git checkout -b "$BRANCH_NAME" - # We come back to the previous state and revert that change - INTERMEDIATE_SAVED=$(git rev-parse HEAD) - git reset --hard HEAD^ - - # We move the file to its new destination + # Move the original file to the new destination, in this branch git mv "${CURRENT_FILENAME}" "${DESTINATION_FILENAME}" - git commit -nm "Copy $CURRENT_FILENAME into $DESTINATION_FILENAME" + git commit -nm "--Duplicate $CURRENT_FILENAME history into $DESTINATION_FILENAME" - # We come back to the previous state and revert that change again - DESTINATION_SAVED=$(git rev-parse HEAD) - git reset --hard HEAD^ + # Restore the original file to keep it in the history + git checkout HEAD~ "${CURRENT_FILENAME}" + git commit -nm "--Restore $CURRENT_FILENAME" - # We keep both files - # shellcheck disable=SC2086 - git merge $MERGE_OPT "${DESTINATION_SAVED}" "${INTERMEDIATE_SAVED}" -m "Duplicate ${CURRENT_FILENAME} history." + # Switch to the original branch and merge this back in. + git checkout - + git merge --no-ff "$BRANCH_NAME" -m "Copy $CURRENT_FILENAME into $DESTINATION_FILENAME" - # We get back our original name - git mv "${INTERMEDIATE_FILENAME}" "${CURRENT_FILENAME}" - git commit -nm "Set back ${CURRENT_FILENAME} file" + # We're now done with the branch, so delete it. + # We shouldn't need -D here, as we've already merged it back in. + git branch -d "$BRANCH_NAME" fi diff --git a/bin/git-create-branch b/bin/git-create-branch index c11c4ea..369a5f8 100755 --- a/bin/git-create-branch +++ b/bin/git-create-branch @@ -39,7 +39,7 @@ then REMOTE=origin fi -test -z $BRANCH && echo "branch argument required." 1>&2 && exit 1 +test -z "$BRANCH" && echo "branch argument required." 1>&2 && exit 1 if [[ -n $REMOTE ]] then diff --git a/bin/git-extras b/bin/git-extras index b9d6b42..d57dd7e 100755 --- a/bin/git-extras +++ b/bin/git-extras @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION="7.3.0-dev" +VERSION="7.6.0-dev" INSTALL_SCRIPT="https://raw.githubusercontent.com/tj/git-extras/main/install.sh" update() { diff --git a/bin/git-fork b/bin/git-fork index ba85283..2242c4a 100755 --- a/bin/git-fork +++ b/bin/git-fork @@ -17,9 +17,9 @@ read -r user # personal access token # config name is github-personal-access-token '_' is not allowed in git config -github_personal_access_token=$(git config git-extras.github-personal-access-token) +github_personal_access_token=$(git config --default "$GITHUB_TOKEN" git-extras.github-personal-access-token) -test -z "$github_personal_access_token" && abort "git config git-extras.github-personal-access-token required" +test -z "$github_personal_access_token" && abort "GITHUB_TOKEN, or git config git-extras.github-personal-access-token required" # extract owner + project from repo url project=${url##*/} @@ -61,7 +61,7 @@ else # clone forked repo into current dir git clone "${remote_prefix}${user}/${project}.git" "$project" # add reference to origin fork so can merge in upstream changes - cd "$project" + cd "$project" || exit git remote add upstream "${remote_prefix}${owner}/${project}.git" git fetch upstream fi diff --git a/bin/git-get b/bin/git-get index 978badd..af43e42 100755 --- a/bin/git-get +++ b/bin/git-get @@ -33,7 +33,7 @@ if [ -z "$clone_path" ]; then fi dirname=${url%/} -dirname=${url%.git} +dirname=${dirname%.git} dirname=${dirname##*/} mkdir -p "$clone_path" diff --git a/bin/git-guilt b/bin/git-guilt index 3fe644a..30e2e0a 100755 --- a/bin/git-guilt +++ b/bin/git-guilt @@ -31,7 +31,7 @@ do esac done -cd "$(git-root)" # cd for git blame +cd "$(git-root)" || exit # cd for git blame MERGED_LOG=$(git_extra_mktemp) if [[ $EMAIL == '-e' ]] then @@ -44,9 +44,11 @@ for file in $(git diff --name-only "$@") do test -n "$DEBUG" && echo "git blame $file" # $1 - since $2 - until + # shellcheck disable=SC2086 git blame $NOT_WHITESPACE --line-porcelain "$1" -- "$file" 2> /dev/null | LC_ALL=C sed -n "$PATTERN" | sort | uniq -c | LC_ALL=C sed 's/^\(.\)/- \1/' >> "$MERGED_LOG" # if $2 not given, use current commit as "until" + # shellcheck disable=SC2086 git blame $NOT_WHITESPACE --line-porcelain "${2-@}" -- "$file" 2> /dev/null | LC_ALL=C sed -n "$PATTERN" | sort | uniq -c | LC_ALL=C sed 's/^\(.\)/+ \1/' >> "$MERGED_LOG" done @@ -71,7 +73,7 @@ END { printf("%d %s\n", contributors[people], people) } } -}' $MERGED_LOG | sort -nr | # only gawk supports built-in sort function +}' "$MERGED_LOG" | sort -nr | # only gawk supports built-in sort function while read -r line do people=${line#* } @@ -103,7 +105,7 @@ do do printf "-" done - printf "(%s)" $num + printf "(%s)" "$num" else for (( i = 0; i > num; i-- )) do diff --git a/bin/git-ignore b/bin/git-ignore index e380a2e..34a1791 100755 --- a/bin/git-ignore +++ b/bin/git-ignore @@ -2,7 +2,7 @@ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) -function show_contents { +show_contents() { local file="${2/#~/$HOME}" if [ -f "$file" ]; then echo "$1 gitignore: $2" && cat "$file" @@ -11,7 +11,7 @@ function show_contents { fi } -function cd_to_git_root { +cd_to_git_root() { local error_level="$1" if ! git rev-parse --git-dir &>/dev/null; then @@ -29,7 +29,7 @@ function cd_to_git_root { fi } -function global_ignore() { +global_ignore() { if ! git config --global core.excludesFile 2>/dev/null; then if [ -f "$HOME/.gitignore" ]; then echo "$HOME/.gitignore" @@ -39,11 +39,11 @@ function global_ignore() { fi } -function show_global { +show_global() { show_contents Global "$(global_ignore)" } -function add_global { +add_global() { local global_gitignore global_gitignore="$(global_ignore)" if [ -z "$global_gitignore" ]; then @@ -56,28 +56,28 @@ function add_global { fi } -function show_local { +show_local() { cd_to_git_root --warn show_contents Local .gitignore } -function add_local { +add_local() { cd_to_git_root --warn add_patterns .gitignore "$@" } -function show_private { +show_private() { cd_to_git_root --error show_contents Private "${GIT_DIR}/info/exclude" } -function add_private { +add_private() { cd_to_git_root --error test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info" add_patterns "${GIT_DIR}/info/exclude" "$@" } -function add_patterns { +add_patterns() { echo "Adding pattern(s) to: $1" local file="${1/#~/$HOME}" dir_name=$(dirname "$file") diff --git a/bin/git-magic b/bin/git-magic index 8bc159a..de1dbaf 100755 --- a/bin/git-magic +++ b/bin/git-magic @@ -30,7 +30,7 @@ while getopts "m:eapfh" arg; do PUSH=true ;; f) - FORCE='-f' + FORCE='--force-with-lease' ;; h) echo "$USAGE" diff --git a/bin/git-merge-into b/bin/git-merge-into index 0561a8d..e547555 100755 --- a/bin/git-merge-into +++ b/bin/git-merge-into @@ -18,7 +18,7 @@ then git stash fi -if [ "${!#}" == '--ff-only' ]; then +if [ "${!#}" = '--ff-only' ]; then case $# in 2 ) # dest --ff git push "$(git rev-parse --show-toplevel)" "$cur_branch":"$1";; diff --git a/bin/git-missing b/bin/git-missing index 9569468..32fbdab 100755 --- a/bin/git-missing +++ b/bin/git-missing @@ -1,7 +1,7 @@ #!/usr/bin/env bash usage() { - echo 1>&2 "usage: git missing [] []" + echo 1>&2 "usage: git missing [] [] [[--] ...]" } if [ "${#}" -lt 1 ] @@ -12,9 +12,20 @@ fi declare -a git_log_args=() declare -a branches=() +declare -a pathspec=() +declare parse_path=false + for arg in "$@" ; do + if [[ $parse_path == true ]]; then + pathspec+=("$@") + break + fi + case "$arg" in + --) + parse_path=true + ;; --*) git_log_args+=( "$arg" ) ;; @@ -38,4 +49,4 @@ else exit 1 fi -git log "${git_log_args[@]}" "$firstbranch"..."$secondbranch" --format="%m %h %s" --left-right +git log "${git_log_args[@]}" "$firstbranch"..."$secondbranch" --format="%m %h %s" --left-right -- "${pathspec[@]}" diff --git a/bin/git-obliterate b/bin/git-obliterate index 992e0eb..83e4ca1 100755 --- a/bin/git-obliterate +++ b/bin/git-obliterate @@ -9,15 +9,15 @@ do file="$file"' '"$i" shift done -test -n "$*" && range="$*" +test -n "$*" && range=("$@") test -z "$file" && echo "file required." 1>&2 && exit 1 -if [ -z "$range" ] +if [ -z "${range[*]}" ] then git filter-branch -f --index-filter "git rm -r --cached ""$file"" --ignore-unmatch" \ --prune-empty --tag-name-filter cat -- --all else - # don't quote $range so that we can forward multiple rev-list arguments + # $range is an array so that we can forward multiple rev-list arguments git filter-branch -f --index-filter "git rm -r --cached ""$file"" --ignore-unmatch" \ - --prune-empty --tag-name-filter cat -- $range + --prune-empty --tag-name-filter cat -- "${range[@]}" fi diff --git a/bin/git-paste b/bin/git-paste index 33643dd..173367c 100755 --- a/bin/git-paste +++ b/bin/git-paste @@ -2,7 +2,7 @@ set -e set -o pipefail -if ! command -v pastebinit >/dev/null 2>&1; then +if ! command -v pastebinit &>/dev/null; then echo >&2 "To run 'git paste', you need to install pastebinit in your system" exit 1 fi diff --git a/bin/git-psykorebase b/bin/git-psykorebase index b3e1a7c..a5e7577 100755 --- a/bin/git-psykorebase +++ b/bin/git-psykorebase @@ -5,7 +5,7 @@ SECONDARY_BRANCH="" FF="--ff" CONTINUE="no" -function current_branch() { +current_branch() { git rev-parse --abbrev-ref HEAD } diff --git a/bin/git-pull-request b/bin/git-pull-request index 5787da1..e31de4f 100755 --- a/bin/git-pull-request +++ b/bin/git-pull-request @@ -31,9 +31,9 @@ EOF # personal access token # config name is github-personal-access-token '_' is not allowed in git config -github_personal_access_token=$(git config git-extras.github-personal-access-token) +github_personal_access_token=$(git config --default "$GITHUB_TOKEN" git-extras.github-personal-access-token) -test -z "$github_personal_access_token" && abort "git config git-extras.github-personal-access-token required" +test -z "$github_personal_access_token" && abort "GITHUB_TOKEN or git config git-extras.github-personal-access-token required" # branch @@ -43,7 +43,7 @@ if [ -z "$remote" ]; then echo 'no upstream found, push to origin as default' remote="origin" fi -[ "$remote" == "." ] && abort "the upstream should be a remote branch." +[ "$remote" = "." ] && abort "the upstream should be a remote branch." # make sure it's pushed diff --git a/bin/git-rebase-patch b/bin/git-rebase-patch index 395304e..521d5fd 100755 --- a/bin/git-rebase-patch +++ b/bin/git-rebase-patch @@ -30,7 +30,7 @@ do GIT_INDEX_FILE=$index git read-tree "$rev" # Try to apply the patch. - GIT_INDEX_FILE=$index git apply --cached "$1" >/dev/null 2>&1 + GIT_INDEX_FILE=$index git apply --cached "$1" &>/dev/null patch_failed=$? # Do it again, but show the error, if the problem is the patch itself. diff --git a/bin/git-release b/bin/git-release index 676495d..8df102b 100755 --- a/bin/git-release +++ b/bin/git-release @@ -119,7 +119,7 @@ if test $# -gt 0; then fi declare -a sign_args - if [ "$sign" == true ]; then + if [ "$sign" = true ]; then sign_args=("-s") fi diff --git a/bin/git-repl b/bin/git-repl index f9996f6..5390f8d 100755 --- a/bin/git-repl +++ b/bin/git-repl @@ -1,8 +1,25 @@ #!/usr/bin/env bash +shopt -s extglob git version echo "git-extras version ""$(git-extras -v)" -echo "type 'ls' to ls files below current directory, '!command' to execute any command or just 'subcommand' to execute any git subcommand" +echo "Type 'ls' to ls files below current directory; '!command' to execute any command or just 'subcommand' to execute any git subcommand; 'quit', 'exit', 'q', ^D, or ^C to exit the git repl." + +HISTIGNORE=${HISTIGNORE:-+([[:space:]])} +HISTCONTROL=${HISTCONTROL:-ignoredups} +use_local_history=$(git config --get --default 'false' git-extras.repl.use-local-history) +if [[ "$use_local_history" == "true" ]]; then + HISTFILE="$(git rev-parse --show-toplevel)/.git_extras_repl_history" +else + HISTFILE=${XDG_STATE_HOME:-$HOME/.local/state}/git_extras_repl_history +fi + +# file doesn't exist, is empty, or contains only whitespace +if [[ ! -f "$HISTFILE" ]] || [[ ! -s "$HISTFILE" ]] || ! grep -q '[^[:space:]]' "$HISTFILE"; then + # `history -r` .... `history -a` are not happy with an empty initial file in bash 3.2.57, which is what MacOS 26 ships with + echo '!echo welcome to git-repl!' >> "$HISTFILE" +fi +history -r while true; do # Current branch @@ -10,34 +27,73 @@ while true; do # Prompt if test -n "$cur"; then - prompt="git ($cur)> " + cur_string=" ($cur)" else - prompt="git> " + cur_string="" + fi + if test -n "$exit_status" && test "$exit_status" -ne 0; then + es_string=$' \e[31m['"$exit_status"$']\e[0m' + else + es_string="" + fi + prompt_character=$(git config --get --default '>' git-extras.repl.prompt-character) + + prefix=$(git config --get --default 'git' git-extras.repl.prefix) + + show_project_name=$(git config --get --default 'false' git-extras.repl.show-project-name) + if [[ "$show_project_name" == "true" ]]; then + project_name=" $(basename "$(git rev-parse --show-toplevel)" .git)" + else + project_name="" fi - # Readline - read -e -r -p "$prompt" cmd + prompt_base="$prefix$project_name$cur_string$es_string$prompt_character" + prompt_base_stripped=$(echo "$prompt_base" | awk '{$1=$1};1') + prompt="$prompt_base_stripped " - # EOF - test $? -ne 0 && break + # Use arguments as a command if any are provided. + if [ $# -ne 0 ]; then + cmd=$* + set -- + echo "$prompt" "$cmd" # It is as though you had entered a command. + else + # Readline + read -e -r -p "$prompt" cmd + # Check for EOF, and end the program if so (handles ^D). + test $? -ne 0 && break + fi - # History - history -s "$cmd" + # Add command to history if it is not all whitespace + if [[ ! "$cmd" =~ ^[[:space:]]*$ ]]; then + history -s "$cmd" + fi # Built-in commands case $cmd in ls) cmd=ls-files;; - "") continue;; - quit|exit) break;; + "") + on_enter_cmd=$(git config --get --default '' git-extras.repl.on-enter-command) + if test -n "$on_enter_cmd"; then + cmd="$on_enter_cmd" + else + continue + fi + ;; + quit|exit|q) break;; esac + history -a + if [[ $cmd == !* ]]; then - eval ${cmd:1} + # shellcheck disable=SC2086 + eval ${cmd:1} elif [[ $cmd == git* ]]; then + # shellcheck disable=SC2086 eval $cmd else eval git "$cmd" fi + exit_status=$? done echo diff --git a/bin/git-scp b/bin/git-scp index 52d4d14..7a10a1f 100755 --- a/bin/git-scp +++ b/bin/git-scp @@ -59,7 +59,7 @@ function php_lint() function _dos2unix() { - command -v dos2unix > /dev/null && dos2unix $@ + command -v dos2unix > /dev/null && dos2unix "$@" return 0 } @@ -68,8 +68,8 @@ function _sanitize() git config --get-all extras.scp.sanitize | while read -r i do case $i in - php_lint) php_lint $@;; # git config --global --add extras.scp.sanitize php_lint - dos2unix) _dos2unix $@;; # git config --global --add extras.scp.sanitize dos2unix + php_lint) php_lint "$@";; # git config --global --add extras.scp.sanitize php_lint + dos2unix) _dos2unix "$@";; # git config --global --add extras.scp.sanitize dos2unix esac done return $? @@ -107,6 +107,7 @@ function scp_and_stage if [ -n "$list" ] then local _TMP=${0///} + # shellcheck disable=SC2086 echo "$list" > "$_TMP" && _sanitize $list && _info "Pushing to $remote ($(git config "remote.$remote.url"))" && @@ -131,7 +132,7 @@ function reverse_scp() shift local _TMP=${0///} - echo $@ > "$_TMP" && + echo "$@" > "$_TMP" && rsync -rlDv --files-from="$_TMP" "$(git config "remote.$remote.url")/" ./ && rm "$_TMP" } @@ -173,8 +174,8 @@ case $(basename "$0") in git-scp) case $1 in ''|-h|'?'|help|--help) shift; _test_git_scp; _usage "$@";; - *) scp_and_stage $@;; + *) scp_and_stage "$@";; esac ;; - git-rscp) reverse_scp $@;; + git-rscp) reverse_scp "$@";; esac diff --git a/bin/git-setup b/bin/git-setup index d1319ec..2a8c82d 100755 --- a/bin/git-setup +++ b/bin/git-setup @@ -2,7 +2,7 @@ COMMIT_MESSAGE='Initial commit' -if [ "$1" == "-m" ]; then +if [ "$1" = "-m" ]; then COMMIT_MESSAGE=$2 shift; shift fi diff --git a/bin/git-standup b/bin/git-standup index c0787f5..89f8afe 100755 --- a/bin/git-standup +++ b/bin/git-standup @@ -35,7 +35,7 @@ in_git_repo=$? # Use colors, but only if connected to a terminal, and that terminal # supports them. -if command -v tput >/dev/null 2>&1; then +if command -v tput &>/dev/null; then ncolors=$(tput colors) fi if [[ -t 1 ]] && [[ -n "$ncolors" ]] && [[ "$ncolors" -ge 8 ]] ; then diff --git a/bin/git-summary b/bin/git-summary index f90adf1..3dbe1b6 100755 --- a/bin/git-summary +++ b/bin/git-summary @@ -3,12 +3,16 @@ cd "$(git root)" || { echo "Can't cd to top level directory";exit 1; } +PROJECT_FULL_PATH= SUMMARY_BY_LINE= DEDUP_BY_EMAIL= MERGES_ARG= OUTPUT_STYLE= for arg in "$@"; do case "$arg" in + --full-path) + PROJECT_FULL_PATH=1 + ;; --line) SUMMARY_BY_LINE=1 ;; @@ -51,7 +55,12 @@ if [ -n "$SUMMARY_BY_LINE" ]; then else [ $# -ne 0 ] && commit=$* fi -project=${PWD##*/} + +if [[ -n "$PROJECT_FULL_PATH" ]]; then + project=${PWD/${HOME}/\~} +else + project=${PWD##*/} +fi # # get date for the given @@ -197,10 +206,10 @@ COLUMN_CMD_DELIMTER="¬" # Hopefully, this symbol is not used in branch names... SP="$COLUMN_CMD_DELIMTER|" print_summary_by_line() { - if [ "$OUTPUT_STYLE" == "tabular" ]; then + if [ "$OUTPUT_STYLE" = "tabular" ]; then tabular_headers="# Repo $SP Lines" echo -e "$tabular_headers\n$project $SP $(line_count "${paths[@]}")" | column -t -s "$COLUMN_CMD_DELIMTER" - elif [ "$OUTPUT_STYLE" == "oneline" ]; then + elif [ "$OUTPUT_STYLE" = "oneline" ]; then echo "$project / lines: $(line_count "${paths[@]}")" elif [ -n "$SUMMARY_BY_LINE" ]; then echo @@ -212,10 +221,10 @@ print_summary_by_line() { } print_summary() { - if [ "$OUTPUT_STYLE" == "tabular" ]; then + if [ "$OUTPUT_STYLE" = "tabular" ]; then tabular_headers="# Repo $SP Age $SP Last active $SP Active on $SP Commits $SP Uncommitted $SP Branch" echo -e "$tabular_headers\n$project $SP $(repository_age) $SP $(last_active) $SP $(active_days "$commit") days $SP $(commit_count "$commit") $SP $(uncommitted_changes_count) $SP $(current_branch_name)" | column -t -s "$COLUMN_CMD_DELIMTER" - elif [ "$OUTPUT_STYLE" == "oneline" ]; then + elif [ "$OUTPUT_STYLE" = "oneline" ]; then echo "$project / age: $(repository_age) / last active: $(last_active) / active on $(active_days "$commit") days / commits: $(commit_count "$commit") / uncommitted: $(uncommitted_changes_count) / branch: $(current_branch_name)" else echo @@ -227,7 +236,7 @@ print_summary() { echo " commits : $(commit_count "$commit")" # The file count doesn't support passing a git ref so ignore it if a ref is given - if [ "$commit" == "HEAD" ]; then + if [ "$commit" = "HEAD" ]; then echo " files : $(file_count)" fi echo " uncommitted : $(uncommitted_changes_count)" diff --git a/bin/git-unwip b/bin/git-unwip new file mode 100755 index 0000000..4e772b0 --- /dev/null +++ b/bin/git-unwip @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Based on scripts from git-utils: +# * https://github.com/ddollar/git-utils/blob/master/git-unwip + +# Check if the last commit is a 'WIP' commit +LAST_COMMIT=`git log -1 --pretty=%B | tr -d '[:space:]'` + +if [ 'WIP' != $LAST_COMMIT ]; then + echo 'Last commit is not a WIP commit, so it will not be unWIP-ed.' + exit 1 +fi + +git undo --soft diff --git a/bin/git-utimes b/bin/git-utimes index 9955a9d..f0dc1a4 100755 --- a/bin/git-utimes +++ b/bin/git-utimes @@ -34,10 +34,10 @@ fi status_opts=(--porcelain --short) # %ct: committer date, UNIX timestamp / %at: author date, UNIX timestamp -whatchanged_opts=(--format='%ct') +log_opts=(--format='%ct') if git status --help 2>&1 | grep -q -- "--no-renames"; then status_opts+=(--no-renames) - whatchanged_opts+=(--no-renames) + log_opts+=(--no-renames) fi if git status --help 2>&1 | grep -q -- "--untracked-files"; then status_opts+=(--untracked-files=no) @@ -126,6 +126,6 @@ git --no-pager status "${status_opts[@]}" . \ | cut -c 4- >"${tmpfile}" # prefix is not stripped: -git --no-pager whatchanged "${whatchanged_opts[@]}" . \ +git --no-pager log --raw --no-merges "${log_opts[@]}" . \ | awk "${awk_flags[@]}" "${awk_script}" "${tmpfile}" - \ | BASH_ENV='' bash "${bash_opts[@]}" - diff --git a/bin/git-wip b/bin/git-wip new file mode 100755 index 0000000..a25d68f --- /dev/null +++ b/bin/git-wip @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Based on scripts from git-utils: +# * https://github.com/ddollar/git-utils/blob/master/git-wip +# * https://github.com/ddollar/git-utils/blob/master/git-addremove + +git add --all +git commit --all --message="WIP" diff --git a/check_integrity.sh b/check_integrity.sh index 16c2583..28bb276 100755 --- a/check_integrity.sh +++ b/check_integrity.sh @@ -6,7 +6,7 @@ err() { } make_doc() { - echo "'touch man/git-$1.md && make man/git-$1.{1,html}'" + echo "touch man/git-$1.md && make man/git-$1.{1,html}" } check_bash_script() { @@ -47,6 +47,11 @@ check_documentation() { err "Create man/$cmd.1 and man/$cmd.html via $(make_doc "$1")" fi + if [ "man/$cmd.md" -nt "man/$cmd.1" ] || [ "man/$cmd.md" -nt "man/$cmd.html" ] + then + err "man/$cmd.md, man/$cmd.1, and man/$cmd.html all exist, but man/$cmd.md is newer. You should rm man/$cmd.{1,html} && $(make_doc "$1")" + fi + check_git_extras_cmd_list "$@" check_man_page_index "$@" } @@ -77,12 +82,9 @@ check() { check_completion "$1" } -usage() { - echo >&2 "Usage: ./check_integrity.sh [ ...]" - exit 0 -} +test $# == 0 && set -- $(find bin | cut -b 5- | xargs) -test $# == 0 && usage +./bin/git-utimes --newer for name in "$@"; do name=${name#git-} diff --git a/etc/bash_completion.sh b/etc/bash_completion.sh index 7a6360a..d48cdb3 100644 --- a/etc/bash_completion.sh +++ b/etc/bash_completion.sh @@ -1,6 +1,29 @@ # shellcheck shell=bash # bash completion support for git-extras. +__gitex_heads_unique() { + local branch specified=("${COMP_WORDS[@]:2}") + for branch in $(__git_heads); do + [[ " ${specified[*]} " == *" $branch "* ]] || printf '%s\n' "$branch" + done +} + +_git_authors(){ + __gitcomp "-l --list --no-email" +} + +_git_browse(){ + __git_complete_remote_or_refspec +} + +_git_browse_ci(){ + __git_complete_remote_or_refspec +} + +_git_brv(){ + __gitcomp "-r --reverse" +} + _git_changelog(){ local s_opts=( '-a' '-l' '-t' '-f' '-s' '-n' '-p' '-x' '-h' '?' ) local l_opts=( @@ -22,14 +45,6 @@ _git_changelog(){ __gitcomp "$merged_opts_str" } -_git_authors(){ - __gitcomp "-l --list --no-email" -} - -_git_brv(){ - __gitcomp "-r --reverse" -} - _git_coauthor(){ local oldIfs=$IFS IFS=$'\n' @@ -67,6 +82,10 @@ _git_contrib(){ COMPREPLY=($(compgen -W "$all" -- "$cur")) } +_git_commits_since(){ + __gitcomp "-r --ref" +} + _git_count(){ __gitcomp "--all" } @@ -76,7 +95,7 @@ __git_cp(){ } _git_delete_branch(){ - __gitcomp "$(__git_heads)" + __gitcomp "$(__gitex_heads_unique)" } _git_delete_squashed_branches(){ @@ -136,6 +155,10 @@ _git_ignore(){ esac } +_git_info(){ + __gitcomp "--color -c --no-config" +} + _git_missing(){ # Suggest all known refs __gitcomp "$(git for-each-ref --format='%(refname:short)')" @@ -158,38 +181,40 @@ _git_reauthor(){ __gitcomp "${comp}" } -_git_scp(){ - __git_complete_remote_or_refspec +__git_extras_rename(){ + if ((COMP_CWORD == 2 || COMP_CWORD == 3)); then + __gitcomp "$1" + fi } -_git_stamp(){ - __gitcomp '--replace -r' +_git_rename_branch(){ + __git_extras_rename "$(__git_heads)" +} + +_git_rename_remote(){ + __git_extras_rename "$(__git_remotes)" +} + +_git_rename_tag(){ + __git_extras_rename "$(__git_tags)" } _git_rscp(){ __git_complete_remote_or_refspec } +_git_scp(){ + __git_complete_remote_or_refspec +} + _git_squash(){ __gitcomp "$(__git_heads)" } +_git_stamp(){ + __gitcomp '--replace -r' +} + _git_undo(){ __gitcomp "--hard --soft -h -s" } - -_git_info(){ - __gitcomp "--color -c --no-config" -} - -_git_browse(){ - __git_complete_remote_or_refspec -} - -_git_browse_ci(){ - __git_complete_remote_or_refspec -} - -_git_rename_file() { - __gitcomp "-h --help" -} diff --git a/etc/git-extras-completion.zsh b/etc/git-extras-completion.zsh index 3a44d6b..128d269 100644 --- a/etc/git-extras-completion.zsh +++ b/etc/git-extras-completion.zsh @@ -72,6 +72,15 @@ __gitex_branch_names() { _wanted branch-names expl branch-name compadd $* - $branch_names } +__gitex_branch_names_unique() { + local expl + declare -a branch_names already_specified + branch_names=(${${(f)"$(_call_program branchrefs git for-each-ref --format='"%(refname)"' refs/heads 2>/dev/null)"}#refs/heads/}) + __gitex_command_successful || return + already_specified=(${words[2,-1]}) + _wanted branch-names expl branch-name compadd -F already_specified $* - $branch_names +} + __gitex_specific_branch_names() { local expl declare -a branch_names @@ -92,6 +101,13 @@ __gitex_submodule_names() { _wanted submodule-names expl submodule-name compadd $* - $submodule_names } +__gitex_workspace_names() { + local expl + declare -a workspace_names + workspace_names=($(git bulk --listall | awk '{print $1}' | cut -d "." -f 2)) + __gitex_command_successful || return + _wanted workspace-names expl workspace-names compadd $* - $workspace_names +} __gitex_author_names() { local expl @@ -122,6 +138,22 @@ _git-brv() { '(-r --reverse)'{-r,--reverse}'[reverse order]' } +_git-bulk() { + _arguments \ + '-a[Run a git command on all workspaces and their repositories.]' \ + '-g[Ask the user for confirmation on every execution (guarded mode).]' \ + '-w[Run the git command on the specified workspace.]:workspace-name:__gitex_workspace_names' \ + '-q[Suppress bulk output about current execution (quiet mode).]' \ + '--no-follow-symlinks[Do not traverse symbolic links when searching for git repositories.]' \ + '--no-follow-hidden[Do not traverse hidden directories when searching for git repositories.]' \ + '--addworkspace[Register a workspace for bulk operations.]' \ + '--removeworkspace[Remove the specified workspace.]:workspace-name:__gitex_workspace_names' \ + '--addcurrent[Adds the current directory as workspace to git bulk operations]' \ + '--purge[Removes all defined repository locations.]' \ + '--listall[List all registered repositories.]' \ + '--help[Show the help.]' +} + _git-changelog() { _arguments \ '(-l --list)'{-l,--list}'[list commits]' \ @@ -139,6 +171,11 @@ _git-coauthor() { ':co-author-email[email address of co-author to add]:__gitex_author_emails' } +_git-commits-since() { + _arguments \ + '(-r --ref)'{-r,--ref}'[show commits since ref]' +} + _git-contrib() { _arguments \ ':author:__gitex_author_names' @@ -173,8 +210,7 @@ _git-create-branch() { } _git-delete-branch() { - _arguments \ - ':branch-name:__gitex_branch_names' + __gitex_branch_names_unique } _git-delete-squashed-branches() { @@ -349,6 +385,7 @@ _git-standup() { } _git-summary() { + _arguments '--full-path[show repository full path]' _arguments '--line[summarize with lines rather than commits]' _arguments '--dedup-by-email[remove duplicate users by the email address]' _arguments '--no-merges[exclude merge commits]' @@ -379,6 +416,7 @@ zstyle ':completion:*:*:git:*' user-commands $existing_user_commands \ clear:'rigorously clean up a repository' \ coauthor:'add a co-author to the last commit' \ commits-since:'show commit logs since some date' \ + continue:'continue current revert, merge, rebase, or cherry-pick process' \ contrib:'show user contributions' \ count:'show commit count' \ create-branch:'create branches' \ @@ -439,4 +477,6 @@ zstyle ':completion:*:*:git:*' user-commands $existing_user_commands \ touch:'touch and add file to the index' \ undo:'remove latest commits' \ unlock:'unlock a file excluded from version control' \ - utimes:'change files modification time to their last commit date' + utimes:'change files modification time to their last commit date' \ + unwip:'undo a WIP commit' \ + wip:'create a WIP commit' diff --git a/etc/git-extras.fish b/etc/git-extras.fish index 0de358f..976cfca 100644 --- a/etc/git-extras.fish +++ b/etc/git-extras.fish @@ -131,6 +131,8 @@ function __fish_git_extra_coauthor_email end complete -c git -f -n '__fish_git_using_command coauthor; and __fish_git_arg_number 2' -a '(__fish_git_extra_coauthor_name)' complete -c git -f -n '__fish_git_using_command coauthor; and __fish_git_arg_number 3' -a '(__fish_git_extra_coauthor_email)' +# commits-since +complete -c git -f -n '__fish_git_using_command commits-since' -s r -l ref -d 'show commits since ref' # count complete -c git -f -n '__fish_git_using_command count' -l all -d 'detailed commit count' # create-branch diff --git a/etc/test.fish b/etc/test.fish deleted file mode 100644 index 159e97d..0000000 --- a/etc/test.fish +++ /dev/null @@ -1,20 +0,0 @@ -complete -e blah - -function __fish_git_arg_number -a number - set -l cmd (commandline -opc) - test (count $cmd) -eq $number -end - -function __fish_git_extra_coauthor_name - printf '%s\n' 'a' 'apple' 'ann' -end - -function __fish_git_extra_coauthor_email - set -l cmd (commandline -opc) - - set -l value $cmd[3] - printf '%s\n' 'n' 'n1' 'n2' "$value" -end - -complete -c blah -f -n '__fish_git_using_command coauthor; and __fish_git_arg_number 2' -a '(__fish_git_extra_coauthor_name)' -complete -c blah -f -n '__fish_git_using_command coauthor; and __fish_git_arg_number 3' -a '(__fish_git_extra_coauthor_email)' diff --git a/man/git-alias.1 b/man/git-alias.1 index b0bcd9b..0a54415 100644 --- a/man/git-alias.1 +++ b/man/git-alias.1 @@ -1,10 +1,27 @@ .\" generated with Ronn-NG/v0.9.1 .\" http://github.com/apjanke/ronn-ng/tree/0.9.1 -.TH "GIT\-ALIAS" "1" "September 2023" "" "Git Extras" +.TH "GIT\-ALIAS" "1" "September 2024" "" "Git Extras" .SH "NAME" \fBgit\-alias\fR \- Define, search and show aliases .SH "SYNOPSIS" -\fBgit\-alias\fR \fBgit\-alias\fR \fBgit\-alias\fR \fBgit\-alias\fR [\-\-global] \fBgit\-alias\fR [\-\-local] \fBgit\-alias\fR [\-\-global] \fBgit\-alias\fR [\-\-local] \fBgit\-alias\fR [\-\-global] \fBgit\-alias\fR [\-\-local] +\fBgit\-alias\fR +.br +\fBgit\-alias\fR +.br +\fBgit\-alias\fR +.br +\fBgit\-alias\fR [\-\-global] +.br +\fBgit\-alias\fR [\-\-local] +.br +\fBgit\-alias\fR [\-\-global] +.br +\fBgit\-alias\fR [\-\-local] +.br +\fBgit\-alias\fR [\-\-global] +.br +\fBgit\-alias\fR [\-\-local] +.br .SH "DESCRIPTION" List all aliases, show one alias, or set one (global or local) alias\. .SH "OPTIONS" @@ -50,8 +67,8 @@ $ git alias s = status amend = commit \-\-amend rank = shortlog \-sn \-\-no\-merges -whatis = show \-s \-\-pretty=\'tformat:%h (%s, %ad)\' \-\-date=short -whois = !sh \-c \'git log \-i \-1 \-\-pretty="format:%an <%ae> +whatis = show \-s \-\-pretty='tformat:%h (%s, %ad)' \-\-date=short +whois = !sh \-c 'git log \-i \-1 \-\-pretty="format:%an <%ae> .fi .IP "" 0 .SH "AUTHOR" diff --git a/man/git-alias.html b/man/git-alias.html index 2089ade..7a477e1 100644 --- a/man/git-alias.html +++ b/man/git-alias.html @@ -77,15 +77,15 @@

SYNOPSIS

-

git-alias -git-alias <search-pattern> -git-alias <alias-name> <command> -git-alias [--global] -git-alias [--local] -git-alias [--global] <search-pattern> -git-alias [--local] <search-pattern> -git-alias [--global] <alias-name> <command> -git-alias [--local] <alias-name> <command>

+

git-alias
+git-alias <search-pattern>
+git-alias <alias-name> <command>
+git-alias [--global]
+git-alias [--local]
+git-alias [--global] <search-pattern>
+git-alias [--local] <search-pattern>
+git-alias [--global] <alias-name> <command>
+git-alias [--local] <alias-name> <command>

DESCRIPTION

@@ -150,7 +150,7 @@ whois = !sh -c 'git log -i -1 --pretty="format:%an <%ae>
  1. -
  2. September 2023
  3. +
  4. September 2024
  5. git-alias(1)
diff --git a/man/git-alias.md b/man/git-alias.md index 81d7873..f32e893 100644 --- a/man/git-alias.md +++ b/man/git-alias.md @@ -3,15 +3,15 @@ git-alias(1) -- Define, search and show aliases ## SYNOPSIS -`git-alias` -`git-alias` <search-pattern> -`git-alias` <alias-name> <command> -`git-alias` [--global] -`git-alias` [--local] -`git-alias` [--global] <search-pattern> -`git-alias` [--local] <search-pattern> -`git-alias` [--global] <alias-name> <command> -`git-alias` [--local] <alias-name> <command> +`git-alias`
+`git-alias` <search-pattern>
+`git-alias` <alias-name> <command>
+`git-alias` [--global]
+`git-alias` [--local]
+`git-alias` [--global] <search-pattern>
+`git-alias` [--local] <search-pattern>
+`git-alias` [--global] <alias-name> <command>
+`git-alias` [--local] <alias-name> <command>
## DESCRIPTION diff --git a/man/git-archive-file.1 b/man/git-archive-file.1 index 4610b48..bb28065 100644 --- a/man/git-archive-file.1 +++ b/man/git-archive-file.1 @@ -1,6 +1,6 @@ -.\" generated with Ronn-NG/v0.9.1 -.\" http://github.com/apjanke/ronn-ng/tree/0.9.1 -.TH "GIT\-ARCHIVE\-FILE" "1" "May 2020" "" "Git Extras" +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "GIT\-ARCHIVE\-FILE" "1" "March 2026" "" "Git Extras" .SH "NAME" \fBgit\-archive\-file\fR \- Export the current HEAD of the git repository to an archive .SH "SYNOPSIS" @@ -14,11 +14,11 @@ Archive naming conventions: .P On any branch: \fBgit\-extras\.1\.7\.0\-110\-gafefba7\.branch\-name\.zip\fR .P -On master branch: \fBgit\-extras\.1\.7\.0\-110\-gafefba7\.zip\fR +On the default branch: \fBgit\-extras\.1\.7\.0\-110\-gafefba7\.zip\fR .P On a detached HEAD (e\.g\. a tag): \fBgit\-extras\.1\.7\.0\.zip\fR .P -The \'/\' and \'\e\' in the branch name will be converted into \'\-\'\. +The '/' and '\e' in the branch name will be converted into '\-'\. .SH "AUTHOR" Written by Philipp Klose <\fIme@thehippo\.de\fR> .SH "REPORTING BUGS" diff --git a/man/git-archive-file.html b/man/git-archive-file.html index 1a53ed8..27c9761 100644 --- a/man/git-archive-file.html +++ b/man/git-archive-file.html @@ -1,8 +1,8 @@ - - + + git-archive-file(1) - Export the current HEAD of the git repository to an archive + + + +
+ + + +
    +
  1. git-continue(1)
  2. +
  3. Git Extras
  4. +
  5. git-continue(1)
  6. +
+ + + +

NAME

+

+ git-continue - Continue current git operation +

+

SYNOPSIS

+ +

git-continue

+ +

DESCRIPTION

+ +

Continue current git revert, rebase, merge or cherry-pick process.

+ +

OPTIONS

+ +

There are no options, it just continues current operation.

+ +

EXAMPLES

+ +

git-continue

+ +

AUTHOR

+ +

Written by oikarinen

+ +

REPORTING BUGS

+ +

<https://github.com/tj/git-extras/issues>

+ +

SEE ALSO

+ +

<https://github.com/tj/git-extras>

+ +
    +
  1. +
  2. November 2024
  3. +
  4. git-continue(1)
  5. +
+ +
+ + diff --git a/man/git-continue.md b/man/git-continue.md new file mode 100644 index 0000000..439a95c --- /dev/null +++ b/man/git-continue.md @@ -0,0 +1,30 @@ +git-continue(1) -- Continue current git operation +================================ + +## SYNOPSIS + +`git-continue` + +## DESCRIPTION + + Continue current git revert, rebase, merge or cherry-pick process. + +## OPTIONS + + There are no options, it just continues current operation. + +## EXAMPLES + + `git-continue` + +## AUTHOR + +Written by oikarinen + +## REPORTING BUGS + +<> + +## SEE ALSO + +<> diff --git a/man/git-delete-merged-branches.1 b/man/git-delete-merged-branches.1 index 590879f..4d03841 100644 --- a/man/git-delete-merged-branches.1 +++ b/man/git-delete-merged-branches.1 @@ -1,30 +1,19 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "GIT\-DELETE\-MERGED\-BRANCHES" "1" "October 2017" "" "Git Extras" -. +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "GIT\-DELETE\-MERGED\-BRANCHES" "1" "March 2026" "" "Git Extras" .SH "NAME" \fBgit\-delete\-merged\-branches\fR \- Delete merged branches -. .SH "SYNOPSIS" \fBgit\-delete\-merged\-branches\fR -. .SH "DESCRIPTION" -Deletes all branches merged in to current HEAD\. Does not delete \fImaster\fR, even if run from a branch that is a descendant of \fImaster\fR\. -. +Deletes all branches merged in to current HEAD\. Does not delete the default branch, even if run from a branch that is a descendant of it\. .SH "EXAMPLES" -. .nf - $ git delete\-merged\-branches -. .fi -. .SH "AUTHOR" Written by Jesús Espino <\fIjespinog@gmail\.com\fR> -. .SH "REPORTING BUGS" <\fIhttps://github\.com/tj/git\-extras/issues\fR> -. .SH "SEE ALSO" <\fIhttps://github\.com/tj/git\-extras\fR> diff --git a/man/git-delete-merged-branches.html b/man/git-delete-merged-branches.html index c1a2a8f..0330f72 100644 --- a/man/git-delete-merged-branches.html +++ b/man/git-delete-merged-branches.html @@ -1,8 +1,8 @@ - - + + git-delete-merged-branches(1) - Delete merged branches - - - -
- - - -
    -
  1. git-magic(1)
  2. -
  3. Git Extras
  4. -
  5. git-magic(1)
  6. -
- -

NAME

-

- git-magic - Automate add/commit/push routines -

- -

SYNOPSIS

- -

git-magic [-a] [-m msg] [-e] [-p] [-f]

- -

DESCRIPTION

- -

Produces summary of changes for commit message from git status --porcelain output. -Commits staged changes with the generated commit message and -opens editor to modify generated commit message optionally. -Also staging and pushing can be automated optionally.

- -

OPTIONS

- -

-a

- -

Adds everything including untracked files.

- -

-m msg

- -

Use the given msg as the commit message. If multiple -m options are given, their values are concatenated as separate paragraphs. -Passed to git commit command. The generated is appended to user-given messages.

- -

-e

- -

This option lets you further edit the generated message. -Passed to git commit command.

- -

-p

- -

Runs git push after commit.

- -

-f

- -

Adds -f option to git push command.

- -

-h

- -

Prints synopsis.

- -

EXAMPLES

- -

This example stages all changes then commits with automatic commit message.

- -
$ git magic -a
-[feature/magic dc2a11e] A  man/git-magic.md
- 1 file changed, 37 insertions(+)
- create mode 100644 man/git-auto.md
-# git log
-Author: overengineer <54alpersaid@gmail.com>
-Date:   Thu Sep 30 20:14:22 2021 +0300
-
-    M  man/git-magic.md
-
- -

-m option PREPENDS generated message.

- -
$ git magic -am "Added documentation for git magic"
-[feature/magic dc2a11e] Added documentation for git magic
- 1 file changed, 42 insertions(+), 0 deletions(-)
- create mode 100644 A man/git-auto.md
-$ git log
-Author: overengineer <54alpersaid@gmail.com>
-Date:   Thu Sep 30 20:14:22 2021 +0300
-
-    Added documentation for git magic
-
-    M  man/git-magic.md
-
- -

AUTHOR

- -

Written by Alper S. Soylu <54alpersaid@gmail.com>

- -

REPORTING BUGS

- -

<https://github.com/tj/git-extras/issues>

- -

SEE ALSO

- -

<https://github.com/tj/git-extras>

- - -
    -
  1. -
  2. May 2023
  3. -
  4. git-magic(1)
  5. -
- -
- - + + + + + + git-magic(1) - Automate add/commit/push routines + + + + +
+ + + +
    +
  1. git-magic(1)
  2. +
  3. Git Extras
  4. +
  5. git-magic(1)
  6. +
+ + + +

NAME

+

+ git-magic - Automate add/commit/push routines +

+

SYNOPSIS

+ +

git-magic [-a] [-m msg] [-e] [-p] [-f]

+ +

DESCRIPTION

+ +

Produces summary of changes for commit message from git status --porcelain output. +Commits staged changes with the generated commit message and +opens editor to modify generated commit message optionally. +Also staging and pushing can be automated optionally.

+ +

OPTIONS

+ +

-a

+ +

Adds everything including untracked files.

+ +

-m msg

+ +

Use the given msg as the commit message. If multiple -m options are given, their values are concatenated as separate paragraphs. +Passed to git commit command. The generated is appended to user-given messages.

+ +

-e

+ +

This option lets you further edit the generated message. +Passed to git commit command.

+ +

-p

+ +

Runs git push after commit.

+ +

-f

+ +

Adds --force-with-lease option to git push command for safer force pushing.

+ +

-h

+ +

Prints synopsis.

+ +

EXAMPLES

+ +

This example stages all changes then commits with automatic commit message.

+ +
$ git magic -a
+[feature/magic dc2a11e] A  man/git-magic.md
+ 1 file changed, 37 insertions(+)
+ create mode 100644 man/git-auto.md
+# git log
+Author: overengineer <54alpersaid@gmail.com>
+Date:   Thu Sep 30 20:14:22 2021 +0300
+
+    M  man/git-magic.md
+
+ +

-m option PREPENDS generated message.

+ +
$ git magic -am "Added documentation for git magic"
+[feature/magic dc2a11e] Added documentation for git magic
+ 1 file changed, 42 insertions(+), 0 deletions(-)
+ create mode 100644 A man/git-auto.md
+$ git log
+Author: overengineer <54alpersaid@gmail.com>
+Date:   Thu Sep 30 20:14:22 2021 +0300
+
+    Added documentation for git magic
+    
+    M  man/git-magic.md
+
+ +

AUTHOR

+ +

Written by Alper S. Soylu 54alpersaid@gmail.com

+ +

REPORTING BUGS

+ +

<https://github.com/tj/git-extras/issues>

+ +

SEE ALSO

+ +

<https://github.com/tj/git-extras>

+ +
    +
  1. +
  2. September 2025
  3. +
  4. git-magic(1)
  5. +
+ +
+ + diff --git a/man/git-magic.md b/man/git-magic.md index 8da5878..d640d66 100644 --- a/man/git-magic.md +++ b/man/git-magic.md @@ -34,7 +34,7 @@ Runs `git push` after commit. -f -Adds `-f` option to `git push` command. +Adds `--force-with-lease` option to `git push` command for safer force pushing. -h diff --git a/man/git-missing.1 b/man/git-missing.1 index 2e82e7a..413880d 100644 --- a/man/git-missing.1 +++ b/man/git-missing.1 @@ -1,16 +1,16 @@ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . -.TH "GIT\-MISSING" "1" "April 2018" "" "Git Extras" +.TH "GIT\-MISSING" "1" "August 2024" "" "Git Extras" . .SH "NAME" \fBgit\-missing\fR \- Show commits missing from another branch . .SH "SYNOPSIS" -\fBgit\-missing\fR [] [] +\fBgit\-missing\fR [] [] [[\-\-] \.\.\.] . .SH "DESCRIPTION" -Shows commits that are in either of two branches but not both\. Useful for seeing what would come across in a merge or push\. +Shows commits that are in either of two branches but not both\. Useful for seeing what would come across in a merge or push\. Optionally, the comparison can be limited to specific paths\. . .SH "OPTIONS" [] @@ -30,6 +30,12 @@ Second branch to compare\. .P Any flags that should be passed to \'git log\', such as \-\-no\-merges\. . +.P +[[\-\-] \.\.\.] +. +.P +Optional path specifications (pathspec) to limit the comparison to specific files or directories\. For more details about the pathspec syntax, see the pathspec entry in gitglossary[7] \fIhttps://git\-scm\.com/docs/gitglossary#Documentation/gitglossary\.txt\-aiddefpathspecapathspec\fR\. +. .SH "EXAMPLES" Show commits on either my current branch or master but not both: . @@ -60,11 +66,26 @@ $ git missing foo bar . .IP "" 0 . +.P +Show commits on either my current branch or master but not both, limited to the src/ directory: +. +.IP "" 4 +. +.nf + +$ git missing master \-\- src/ +< ed52989 only on current checked out branch, in src/ directory +> 7988c4b only on master, in src/ directory +. +.fi +. +.IP "" 0 +. .SH "AUTHOR" Written by Nate Jones <\fInate@endot\.org\fR> . .SH "REPORTING BUGS" -<\fIhttp://github\.com/tj/git\-extras/issues\fR> +<\fIhttps://github\.com/tj/git\-extras/issues\fR> . .SH "SEE ALSO" -<\fIhttp://github\.com/tj/git\-extras\fR> +<\fIhttps://github\.com/tj/git\-extras\fR> diff --git a/man/git-missing.html b/man/git-missing.html index 427d924..d82a07c 100644 --- a/man/git-missing.html +++ b/man/git-missing.html @@ -76,12 +76,13 @@

SYNOPSIS

-

git-missing [<first branch>] <second branch> [<git log options>]

+

git-missing [<first branch>] <second branch> [<git log options>] [[--] <path>...]

DESCRIPTION

-

Shows commits that are in either of two branches but not both. Useful for - seeing what would come across in a merge or push.

+

Shows commits that are in either of two branches but not both. Useful for + seeing what would come across in a merge or push. Optionally, the comparison + can be limited to specific paths.

OPTIONS

@@ -97,6 +98,12 @@

Any flags that should be passed to 'git log', such as --no-merges.

+

[[--] <path>...]

+ +

Optional path specifications (pathspec) to limit the comparison to specific + files or directories. For more details about the pathspec syntax, see the + pathspec entry in gitglossary[7].

+

EXAMPLES

Show commits on either my current branch or master but not both:

@@ -113,22 +120,30 @@ > f38797e only on bar +

Show commits on either my current branch or master but not both, limited to the + src/ directory:

+ +
$ git missing master -- src/
+< ed52989 only on current checked out branch, in src/ directory
+> 7988c4b only on master, in src/ directory
+
+

AUTHOR

-

Written by Nate Jones <nate@endot.org>

+

Written by Nate Jones <nate@endot.org>

REPORTING BUGS

-

<http://github.com/tj/git-extras/issues>

+

<https://github.com/tj/git-extras/issues>

SEE ALSO

-

<http://github.com/tj/git-extras>

+

<https://github.com/tj/git-extras>

  1. -
  2. April 2018
  3. +
  4. August 2024
  5. git-missing(1)
diff --git a/man/git-missing.md b/man/git-missing.md index 6f4ff65..8b49430 100644 --- a/man/git-missing.md +++ b/man/git-missing.md @@ -3,12 +3,13 @@ git-missing(1) -- Show commits missing from another branch ## SYNOPSIS -`git-missing` [<first branch>] <second branch> [<git log options>] +`git-missing` [<first branch>] <second branch> [<git log options>] [[--] <path>...] ## DESCRIPTION - Shows commits that are in either of two branches but not both. Useful for - seeing what would come across in a merge or push. + Shows commits that are in either of two branches but not both. Useful for + seeing what would come across in a merge or push. Optionally, the comparison + can be limited to specific paths. ## OPTIONS @@ -24,6 +25,12 @@ git-missing(1) -- Show commits missing from another branch Any flags that should be passed to 'git log', such as --no-merges. + [[--] <path>...] + + Optional path specifications (pathspec) to limit the comparison to specific + files or directories. For more details about the pathspec syntax, see the + pathspec entry in [gitglossary[7]](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec). + ## EXAMPLES Show commits on either my current branch or master but not both: @@ -38,6 +45,13 @@ git-missing(1) -- Show commits missing from another branch < b8f0d14 only on foo > f38797e only on bar + Show commits on either my current branch or master but not both, limited to the + src/ directory: + + $ git missing master -- src/ + < ed52989 only on current checked out branch, in src/ directory + > 7988c4b only on master, in src/ directory + ## AUTHOR Written by Nate Jones <> diff --git a/man/git-pull-request.1 b/man/git-pull-request.1 index b44859c..2311773 100644 --- a/man/git-pull-request.1 +++ b/man/git-pull-request.1 @@ -1,58 +1,41 @@ -.\" generated with Ronn/v0.7.3 -.\" http://github.com/rtomayko/ronn/tree/0.7.3 -. -.TH "GIT\-PULL\-REQUEST" "1" "August 2020" "" "Git Extras" -. +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "GIT\-PULL\-REQUEST" "1" "March 2026" "" "Git Extras" .SH "NAME" \fBgit\-pull\-request\fR \- Create pull request for GitHub project -. .SH "SYNOPSIS" -\fBgit\-pull\-request\fR [] -. +\fBgit\-pull\-request\fR [] .SH "DESCRIPTION" Create pull request for a project on GitHub via command line\. -. .P A personal access token is required for making the API call to open the pull request(s) in GitHub\. API Documentation here \fIhttps://docs\.github\.com/en/rest/reference/pulls#create\-a\-pull\-request\fR -. .P Make sure the personal access token has the right \fBOAuth\fR scopes for the repo(s) -. .P -Use \fBgit config \-\-global \-\-add git\-extras\.github\-personal\-access\-token \fR -. +Use \fBGITHUB_TOKEN\fR environment variable, or \fBgit config \-\-global \-\-add git\-extras\.github\-personal\-access\-token \fR .P If using multiple accounts, override the global value in the specific repo using \fBgit config git\-extras\.github\-personal\-access\-token \fR -. .SH "OPTIONS" - -. + .P -The target branch you want to send pull request to\. -. +The local branch to push and use as the pull request head\. The base branch is prompted separately\. .SH "EXAMPLES" -. .nf - $ git pull\-request master Everything up\-to\-date - create pull\-request for spacewander/spacewander\-toolbox \'master\' + create pull\-request for spacewander/spacewander\-toolbox 'master' title: test body: base [master]: GitHub two\-factor authentication code (leave blank if not set up): -\.\.\. -. +\|\.\|\.\|\. .fi -. .SH "AUTHOR" Written by Tj Holowaychuk <\fItj@vision\-media\.ca\fR> -. .SH "REPORTING BUGS" <\fIhttps://github\.com/tj/git\-extras/issues\fR> -. .SH "SEE ALSO" <\fIhttps://github\.com/tj/git\-extras\fR> diff --git a/man/git-pull-request.html b/man/git-pull-request.html index fa5796e..05169ac 100644 --- a/man/git-pull-request.html +++ b/man/git-pull-request.html @@ -1,8 +1,8 @@ - - + + git-pull-request(1) - Create pull request for GitHub project + + + +
+ + + +
    +
  1. git-unwip(1)
  2. +
  3. Git Extras
  4. +
  5. git-unwip(1)
  6. +
+ + + +

NAME

+

+ git-unwip - Undo a Work In Progress commit +

+

SYNOPSIS

+ +

git-unwip

+ +

DESCRIPTION

+ +

Undo a Work In Progress commit.

+ +

OPTIONS

+ +

None

+ +

EXAMPLES

+ +

Create a WIP commit which stores all changes in the working directory.

+ +
$ git wip
+
+ +

Later on, undo the commit and continue making changes.

+ +
$ git unwip
+
+ +

AUTHOR

+ +

Written by Andrew Sullivan Cant <mail@andrewsullivancant.ca>

+ +

REPORTING BUGS

+ +

<https://github.com/tj/git-extras/issues>

+ +

SEE ALSO

+ +

<https://github.com/tj/git-extras>

+ +
    +
  1. +
  2. May 2025
  3. +
  4. git-unwip(1)
  5. +
+ +
+ + diff --git a/man/git-unwip.md b/man/git-unwip.md new file mode 100644 index 0000000..6d4d961 --- /dev/null +++ b/man/git-unwip.md @@ -0,0 +1,36 @@ +git-unwip(1) -- Undo a Work In Progress commit +================================ + +## SYNOPSIS + +`git-unwip` + +## DESCRIPTION + + Undo a Work In Progress commit. + +## OPTIONS + + None + +## EXAMPLES + + Create a WIP commit which stores all changes in the working directory. + + $ git wip + + Later on, undo the commit and continue making changes. + + $ git unwip + +## AUTHOR + +Written by Andrew Sullivan Cant <> + +## REPORTING BUGS + +<> + +## SEE ALSO + +<> diff --git a/man/git-wip.1 b/man/git-wip.1 new file mode 100644 index 0000000..3d1e326 --- /dev/null +++ b/man/git-wip.1 @@ -0,0 +1,31 @@ +.\" generated with Ronn-NG/v0.10.1 +.\" http://github.com/apjanke/ronn-ng/tree/0.10.1 +.TH "GIT\-WIP" "1" "May 2025" "" "Git Extras" +.SH "NAME" +\fBgit\-wip\fR \- Create a Work In Progress commit +.SH "SYNOPSIS" +\fBgit\-wip\fR +.SH "DESCRIPTION" +Create a Work In Progress commit, include all files in the working directory\. +.SH "OPTIONS" +None +.SH "EXAMPLES" +Create a WIP commit which stores all changes in the working directory\. +.IP "" 4 +.nf +$ git wip +.fi +.IP "" 0 +.P +Later on, undo the commit and continue making changes\. +.IP "" 4 +.nf +$ git unwip +.fi +.IP "" 0 +.SH "AUTHOR" +Written by Andrew Sullivan Cant <\fImail@andrewsullivancant\.ca\fR> +.SH "REPORTING BUGS" +<\fIhttps://github\.com/tj/git\-extras/issues\fR> +.SH "SEE ALSO" +<\fIhttps://github\.com/tj/git\-extras\fR> diff --git a/man/git-wip.html b/man/git-wip.html new file mode 100644 index 0000000..b640255 --- /dev/null +++ b/man/git-wip.html @@ -0,0 +1,122 @@ + + + + + + git-wip(1) - Create a Work In Progress commit + + + + +
+ + + +
    +
  1. git-wip(1)
  2. +
  3. Git Extras
  4. +
  5. git-wip(1)
  6. +
+ + + +

NAME

+

+ git-wip - Create a Work In Progress commit +

+

SYNOPSIS

+ +

git-wip

+ +

DESCRIPTION

+ +

Create a Work In Progress commit, include all files in the working directory.

+ +

OPTIONS

+ +

None

+ +

EXAMPLES

+ +

Create a WIP commit which stores all changes in the working directory.

+ +
$ git wip
+
+ +

Later on, undo the commit and continue making changes.

+ +
$ git unwip
+
+ +

AUTHOR

+ +

Written by Andrew Sullivan Cant <mail@andrewsullivancant.ca>

+ +

REPORTING BUGS

+ +

<https://github.com/tj/git-extras/issues>

+ +

SEE ALSO

+ +

<https://github.com/tj/git-extras>

+ +
    +
  1. +
  2. May 2025
  3. +
  4. git-wip(1)
  5. +
+ +
+ + diff --git a/man/git-wip.md b/man/git-wip.md new file mode 100644 index 0000000..58bc19b --- /dev/null +++ b/man/git-wip.md @@ -0,0 +1,36 @@ +git-wip(1) -- Create a Work In Progress commit +================================ + +## SYNOPSIS + +`git-wip` + +## DESCRIPTION + + Create a Work In Progress commit, include all files in the working directory. + +## OPTIONS + + None + +## EXAMPLES + + Create a WIP commit which stores all changes in the working directory. + + $ git wip + + Later on, undo the commit and continue making changes. + + $ git unwip + +## AUTHOR + +Written by Andrew Sullivan Cant <> + +## REPORTING BUGS + +<> + +## SEE ALSO + +<> diff --git a/man/index.txt b/man/index.txt index 21c6503..4f24ad0 100644 --- a/man/index.txt +++ b/man/index.txt @@ -12,6 +12,7 @@ git-clear-soft(1) git-clear-soft git-clear(1) git-clear git-coauthor(1) git-coauthor git-commits-since(1) git-commits-since +git-continue(1) git-continue git-contrib(1) git-contrib git-count(1) git-count git-cp(1) git-cp @@ -72,4 +73,6 @@ git-sync(1) git-sync git-touch(1) git-touch git-undo(1) git-undo git-unlock(1) git-unlock +git-unwip(1) git-unwip git-utimes(1) git-utimes +git-wip(1) git-wip diff --git a/scripts/checkstyle.py b/scripts/checkstyle.py new file mode 100755 index 0000000..6e2d3dd --- /dev/null +++ b/scripts/checkstyle.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +import re +import os +import argparse +from pathlib import Path +from typing import Callable, List, Dict, Any # compat + +# This file checks Bash and Shell scripts for violations not found with +# shellcheck or existing methods. You can use it in several ways: +# +# Lint all .bash, .sh, .bats files along with 'bin/asdf' and print out violations: +# $ ./scripts/checkstyle.py +# +# The former, but also fix all violations. This must be ran until there +# are zero violations since any line can have more than one violation: +# $ ./scripts/checkstyle.py --fix +# +# Lint a particular file: +# $ ./scripts/checkstyle.py ./lib/functions/installs.bash +# +# Check to ensure all regular expressions are working as intended: +# $ ./scripts/checkstyle.py --internal-test-regex + +Rule = Dict[str, Any] + +class c: + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + MAGENTA = '\033[95m' + CYAN = '\033[96m' + RESET = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + LINK: Callable[[str, str], str] = lambda href, text: f'\033]8;;{href}\a{text}\033]8;;\a' + +def utilGetStrs(line: Any, m: Any): + return ( + line[0:m.start('match')], + line[m.start('match'):m.end('match')], + line[m.end('match'):] + ) + +# Before: printf '%s\\n' '^w^' +# After: printf '%s\n' '^w^' +def noDoubleBackslashFixer(line: str, m: Any) -> str: + prestr, midstr, poststr = utilGetStrs(line, m) + + return f'{prestr}{midstr[1:]}{poststr}' + +# Before: $(pwd) +# After: $PWD +def noPwdCaptureFixer(line: str, m: Any) -> str: + prestr, _, poststr = utilGetStrs(line, m) + + return f'{prestr}$PWD{poststr}' + +# Before: [ a == b ] +# After: [ a = b ] +def noTestDoubleEqualsFixer(line: str, m: Any) -> str: + prestr, _, poststr = utilGetStrs(line, m) + + return f'{prestr}={poststr}' + +# Before: function fn() { ... +# After: fn() { ... +# --- +# Before: function fn { ... +# After fn() { ... +def noFunctionKeywordFixer(line: str, m: Any) -> str: + prestr, midstr, poststr = utilGetStrs(line, m) + + midstr = midstr.strip() + midstr = midstr[len('function'):] + midstr = midstr.strip() + + parenIdx = midstr.find('(') + if parenIdx != -1: + midstr = midstr[:parenIdx] + + return f'{prestr}{midstr}() {poststr}' + +# Before: >/dev/null 2>&1 +# After: &>/dev/null +# --- +# Before: 2>/dev/null 1>&2 +# After: &>/dev/null +def noVerboseRedirectionFixer(line: str, m: Any) -> str: + prestr, _, poststr = utilGetStrs(line, m) + + return f'{prestr}&>/dev/null{poststr}' + +def lintfile(file: Path, rules: List[Rule], options: Dict[str, Any]): + content_arr = file.read_text().split('\n') + + for line_i, line in enumerate(content_arr): + if 'checkstyle-ignore' in line: + continue + + for rule in rules: + should_run = False + if 'sh' in rule['fileTypes']: + if file.name.endswith('.sh'): + should_run = True + if 'bash' in rule['fileTypes']: + if file.name.endswith('.bash') or file.name.endswith('.bats') or file.name.startswith('git-'): + should_run = True + + if options['verbose']: + print(f'{str(file)}: {should_run}') + + if not should_run: + continue + + m = re.search(rule['regex'], line) + if m is not None and m.group('match') is not None: + dir = os.path.relpath(file.resolve(), Path.cwd()) + prestr = line[0:m.start('match')] + midstr = line[m.start('match'):m.end('match')] + poststr = line[m.end('match'):] + + print(f'{c.CYAN}{dir}{c.RESET}:{line_i + 1}') + print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: {rule["reason"]}') + print(f'{prestr}{c.RED}{midstr}{c.RESET}{poststr}') + print() + + if options['fix']: + content_arr[line_i] = rule['fixerFn'](line, m) + + rule['found'] += 1 + + if options['fix']: + file.write_text('\n'.join(content_arr)) + +def main(): + rules: List[Rule] = [ + { + 'name': 'no-pwd-capture', + 'regex': '(?P\\$\\(pwd\\))', + 'reason': '$PWD is essentially equivalent to $(pwd) without the overhead of a subshell', + 'fileTypes': ['bash', 'sh'], + 'fixerFn': noPwdCaptureFixer, + 'testPositiveMatches': [ + '$(pwd)' + ], + 'testNegativeMatches': [ + '$PWD' + ], + }, + { + 'name': 'no-test-double-equals', + 'regex': '(?==).*?]', + 'reason': 'Disallow double equals in places where they are not necessary for consistency', + 'fileTypes': ['bash', 'sh'], + 'fixerFn': noTestDoubleEqualsFixer, + 'testPositiveMatches': [ + '[ a == b ]', + '[ "${lines[0]}" == blah ]', + ], + 'testNegativeMatches': [ + '[ a = b ]', + '[[ a = b ]]', + '[[ a == b ]]', + '[ a = b ] || [[ a == b ]]', + '[[ a = b ]] || [[ a == b ]]', + '[[ "${lines[0]}" == \'usage: \'* ]]', + '[ "${lines[0]}" = blah ]', + ], + }, + { + 'name': 'no-function-keyword', + 'regex': '^[ \\t]*(?Pfunction .*?(?:\\([ \\t]*\\))?[ \\t]*){', + 'reason': 'Only allow functions declared like `fn_name() {{ :; }}` for consistency (see ' + c.LINK('https://www.shellcheck.net/wiki/SC2113', 'ShellCheck SC2113') + ')', + 'fileTypes': ['bash', 'sh'], + 'fixerFn': noFunctionKeywordFixer, + 'testPositiveMatches': [ + 'function fn() { :; }', + 'function fn { :; }', + ], + 'testNegativeMatches': [ + 'fn() { :; }', + ], + }, + { + 'name': 'no-verbose-redirection', + 'regex': '(?P(>/dev/null 2>&1|2>/dev/null 1>&2))', + 'reason': 'Use `&>/dev/null` instead of `>/dev/null 2>&1` or `2>/dev/null 1>&2` for consistency', + 'fileTypes': ['bash'], + 'fixerFn': noVerboseRedirectionFixer, + 'testPositiveMatches': [ + 'echo woof >/dev/null 2>&1', + 'echo woof 2>/dev/null 1>&2', + ], + 'testNegativeMatches': [ + 'echo woof &>/dev/null', + 'echo woof >&/dev/null', + ], + }, + ] + [rule.update({ 'found': 0 }) for rule in rules] + + parser = argparse.ArgumentParser() + parser.add_argument('files', metavar='FILES', nargs='*') + parser.add_argument('--fix', action='store_true') + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--internal-test-regex', action='store_true') + args = parser.parse_args() + + if args.internal_test_regex: + for rule in rules: + for positiveMatch in rule['testPositiveMatches']: + m: Any = re.search(rule['regex'], positiveMatch) + if m is None or m.group('match') is None: + print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: Failed {c.CYAN}positive{c.RESET} test:') + print(f'=> {positiveMatch}') + print() + + for negativeMatch in rule['testNegativeMatches']: + m: Any = re.search(rule['regex'], negativeMatch) + if m is not None and m.group('match') is not None: + print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: Failed {c.YELLOW}negative{c.RESET} test:') + print(f'=> {negativeMatch}') + print() + print('Done.') + return + + options = { + 'fix': args.fix, + 'verbose': args.verbose, + } + + # parse files and print matched lints + if len(args.files) > 0: + for file in args.files: + p = Path(file) + if p.is_file(): + lintfile(p, rules, options) + else: + for file in Path.cwd().glob('**/*'): + if '.git' in str(file.absolute()): + continue + + if file.is_file(): + lintfile(file, rules, options) + + # print final results + print(f'{c.UNDERLINE}TOTAL ISSUES{c.RESET}') + for rule in rules: + print(f'{c.MAGENTA}{rule["name"]}{c.RESET}: {rule["found"]}') + + grand_total = sum([rule['found'] for rule in rules]) + print(f'GRAND TOTAL: {grand_total}') + print(f'{c.BOLD}{c.YELLOW}NOTE:{c.RESET} Run "./scripts/checkstyle.py --fix" to automatically fix all issues (may need to run multiple times)') + + # exit + if grand_total == 0: + exit(0) + else: + exit(2) + +main() diff --git a/tests/README.md b/tests/README.md index d97c077..836021d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,28 @@ -# Test for git-extras -The git-extras has its own testcases now, and the more is on the way! So let's introduce it. +# Testing -We choose python to help us to reach to other shore cause **python is life saver**. +Originally, the tests were written in pytest. However, tests are in the process of being converted to Bats so coverage can be calculated. + +## Bats Testing + +We require a somewhat recent version of Bats. Version v1.8.1 is tested in CI. Once it is installed, the tests can be executed like so: + +```sh +bats ./tests +``` + +We highly recommend adding tests for new features and fixes. + +### Code Coverage + +Coverage can be calculated with [bashcov](https://github.com/infertux/bashcov) like so: + +```sh +bashcov -- bats ./tests +``` + +By default, the report will be generated in `./coverage/index.html`. + +## Python Testing The test part depends on: @@ -12,7 +33,8 @@ The test part depends on: So the versions are higher than above is recommended. -# How to run the tests +### How to run the tests + 1. Install `poetry` 2. Install the dependencies via `poetry install` 3. Run `poetry run pytest` @@ -22,11 +44,13 @@ It is done or go without `poetry`, 1. Install python >= 3.11 2. Install pytest >= 8.1.2 3. Install gitpython >= 3.1.43 -4. Run `pytest` +4. Install testpath >= 0.6.0 +5. Run `pytest` The second way maybe blocked the some missing dependencies at someday, so the first one is recommended. -# What and how to create a unit test +### What and how to create a unit test + One command has a unit test, because one `git-*` command is just do one thing, so we can eat a piece of `git-*` command in one time. For example, @@ -38,7 +62,8 @@ For example, * `named_temp_repo` is just same as `temp_repo` except the custom directory renaming. 4. Loop the third step until the 100% coverage of the function of the `git-alias` -# References +### References + * [poetry](https://github.com/python-poetry/poetry) * [pytest](https://github.com/pytest-dev/pytest/) * [git python](https://github.com/gitpython-developers/GitPython) diff --git a/tests/bin/open b/tests/bin/open new file mode 100755 index 0000000..2be3eaf --- /dev/null +++ b/tests/bin/open @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +printf '%s\n' "open $*" diff --git a/tests/bin/powershell.exe b/tests/bin/powershell.exe new file mode 100755 index 0000000..aee36a1 --- /dev/null +++ b/tests/bin/powershell.exe @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +printf '%s\n' "powershell.exe $*" diff --git a/tests/bin/start b/tests/bin/start new file mode 100755 index 0000000..89eeebd --- /dev/null +++ b/tests/bin/start @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +printf '%s\n' "start $*" diff --git a/tests/bin/xdg-open b/tests/bin/xdg-open new file mode 100755 index 0000000..951b999 --- /dev/null +++ b/tests/bin/xdg-open @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +printf '%s\n' "xdg-open $*" diff --git a/tests/conftest.py b/tests/conftest.py index 74c91c8..3b31908 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,13 +4,15 @@ import pytest from helper import TempRepository -def create_repo(dirname = None): + +def create_repo(dirname=None): repo = TempRepository(dirname) - tmp_file_a = repo.create_tmp_file() - tmp_file_b = repo.create_tmp_file() + repo.create_tmp_file() # tmp_file_a + repo.create_tmp_file() # tmp_file_b repo.switch_cwd_under_repo() return repo + def init_repo_git_status(repo): git = repo.get_repo_git() git.add(".") @@ -18,12 +20,14 @@ def init_repo_git_status(repo): git.config("--local", "user.email", "test@git-extras.com") git.commit("-m", "chore: initial commit") + @pytest.fixture(scope="module") def temp_repo(): repo = create_repo() init_repo_git_status(repo) return repo + @pytest.fixture(scope="module") def named_temp_repo(request): dirname = request.param @@ -31,3 +35,11 @@ def named_temp_repo(request): init_repo_git_status(repo) yield repo repo.teardown() + + +@pytest.fixture(scope="function") +def temp_repo_clean(): + """Create a temporary repository that is reset for each function call.""" + repo = create_repo() + init_repo_git_status(repo) + return repo diff --git a/tests/git-abort.bats b/tests/git-abort.bats new file mode 100644 index 0000000..73e8302 --- /dev/null +++ b/tests/git-abort.bats @@ -0,0 +1,93 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + git commit --allow-empty -m "Initial commit" + git branch A + git branch B + git checkout A + printf '%s\n' 'a' > tmpfile + git add . + git commit -m A + git checkout B + printf '%s\n' 'b' > tmpfile + git add . + git commit -m B + git status +} + +@test "works with cherry pick" { + run git cherry-pick A + assert_failure + + run git status + assert_line -p 'You are currently cherry-picking commit' + assert_line -p 'Unmerged paths:' + assert_success + + run git abort + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with merge" { + run git merge A + assert_failure + + run git status + assert_line -p 'You have unmerged paths' + assert_line -p 'Unmerged paths:' + assert_success + + run git abort + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with rebase" { + run git rebase A + assert_failure + + run git status + assert_line -p 'You are currently rebasing branch' + assert_line -p 'Unmerged paths:' + assert_success + + run git abort + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with revert" { + run git revert A + assert_failure + + run git status + assert_line -p 'You are currently reverting commit' + assert_line -p 'Unmerged paths:' + assert_success + + run git abort + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} diff --git a/tests/git-alias.bats b/tests/git-alias.bats new file mode 100644 index 0000000..bb15410 --- /dev/null +++ b/tests/git-alias.bats @@ -0,0 +1,106 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + git config --global alias.globalalias status + git config --global alias.x status + git config --local alias.localalias status + git config --local alias.y status +} + +@test "list all works" { + run git alias + assert_output - <<-'EOF' + globalalias = status + localalias = status + x = status + y = status +EOF + assert_success +} + +@test "list all globally works" { + run git alias --global + assert_output - <<-'EOF' + globalalias = status + x = status +EOF + assert_success +} + +@test "list all locally works" { + run git alias --local + assert_output - <<-'EOF' + localalias = status + y = status +EOF + assert_success +} + +@test "search globally works" { + run git alias --global global + assert_output - <<-'EOF' + globalalias = status +EOF + assert_success + + run git alias --global local + assert_output '' + assert_failure +} + +@test "search locally works" { + run git alias --local local + assert_output - <<-'EOF' + localalias = status +EOF + assert_success + + run git alias --local global + assert_output '' + assert_failure +} + +@test "get alias globally and defaultly" { + run git alias globalalias + assert_output - <<-'EOF' + globalalias = status +EOF + assert_success +} + +@test "set alias globally and defaultly" { + git alias globalalias diff + run git alias diff + assert_output - <<-'EOF' + globalalias = diff +EOF + assert_success +} + +@test "get alias locally" { + run git alias --local localalias + assert_output - <<-'EOF' + localalias = status +EOF + assert_success +} + +@test "set alias locally" { + git alias --local localalias diff + run git alias + assert_output - <<-'EOF' + globalalias = status + localalias = diff + x = status + y = status +EOF +} diff --git a/tests/git-archive-file.bats b/tests/git-archive-file.bats new file mode 100644 index 0000000..b7f1849 --- /dev/null +++ b/tests/git-archive-file.bats @@ -0,0 +1,66 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + printf '%s\n' 'data' > tmpfile + git add . + git commit -m 'test: add data' + git tag 0.1.0 -m 'bump: 0.1.0' +} + +@test "archive file on tags branch" { + git checkout -b tags0.1.0 + run git archive-file + assert_success + + local describe_output= + describe_output=$(git describe) + assert_file_exists "${PWD##*/}.$describe_output.zip" +} + +@test "archive file on any not tags branch without default branch" { + git checkout -b not-tags-branch + run git archive-file + assert_success + + local describe_output= + describe_output=$(git describe --always --long) + assert_file_exists "${PWD##*/}.$describe_output.not-tags-branch.zip" +} + +@test "archive file on any not tags branch with default branch" { + skip "Not working as expected" + + run git archive-file + assert_success + + local describe_output= + describe_output=$(git describe --always --long) + assert_file_exists "${PWD##*/}.$describe_output.zip" +} + +@test "archive file on branch name has slash" { + git checkout -b feature/slash + run git archive-file + assert_success + + local describe_output= + describe_output=$(git describe --always --long) + assert_file_exists "${PWD##*/}.$describe_output.feature-slash.zip" +} + +@test "archive file on dirname has backslash" { + skip +} + +@test "archive file on tag name has slash" { + skip +} diff --git a/tests/git-authors.bats b/tests/git-authors.bats new file mode 100644 index 0000000..2fafffa --- /dev/null +++ b/tests/git-authors.bats @@ -0,0 +1,57 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + + git config user.name 'test' + git config user.email 'test@example.com' + printf '%s\n' 'A' > tmpfile + git add . + git commit -m 'test: add data A' + + git config user.name 'testagain' + git config user.email 'testagain@example.com' + printf '%s\n' 'B' > tmpfile + git add . + git commit -m 'test: add data B' + + # git config unset user.name + # git config unset user.email +} + +@test "output authors has email without any parameter" { + run git authors + assert_success + + local content=$(\ntestagain ' +} + +@test "list authors has email defaultly" { + run git authors --list + assert_output $'test \ntestagain ' + assert_success + + run git authors -l + assert_output $'test \ntestagain ' + assert_success +} + +@test "list authors has no email" { + run git authors --list --no-email + assert_output $'test\ntestagain' + assert_success + + run git authors -l --no-email + assert_output $'test\ntestagain' + assert_success +} diff --git a/tests/git-browse-ci.bats b/tests/git-browse-ci.bats new file mode 100644 index 0000000..fee89fc --- /dev/null +++ b/tests/git-browse-ci.bats @@ -0,0 +1,117 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file + + PATH="$BATS_TEST_DIRNAME/bin:$PATH" +} + +setup() { + test_util.cd_test + + test_util.git_init +} + +get_ci_uri() { + local mode=$1 + + if [ "$mode" = 'github' ]; then + REPLY="https://github.com/tj/git-extras/actions" + elif [ "$mode" = 'gitlab' ]; then + REPLY="https://gitlab.com/tj/git-extras/-/pipelines" + elif [ "$mode" = 'bitbucket' ]; then + REPLY="https://bitbucket.org/tj/git-extras/addon/pipelines/home" + fi +} + +@test "works with mac and github" { + get_ci_uri 'github' + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=darwin run git browse-ci upstream + assert_output "open $expected_url" + assert_success +} + +@test "works with mac and gitlab" { + get_ci_uri 'gitlab' + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=darwin run git browse-ci upstream + assert_output "open $expected_url" + assert_success +} + +@test "works with mac and bitbucket" { + get_ci_uri 'bitbucket' + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=darwin run git browse-ci upstream + assert_output "open $expected_url" + assert_success +} + +@test "works with windows and github" { + get_ci_uri 'github' + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=msys run git browse-ci upstream + assert_output "start $expected_url" + assert_success +} + +@test "works with windows and gitlab" { + get_ci_uri 'gitlab' + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=msys run git browse-ci upstream + assert_output "start $expected_url" + assert_success +} + +@test "works with windows and bitbucket" { + get_ci_uri 'bitbucket' + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=msys run git browse-ci upstream + assert_output "start $expected_url" + assert_success +} + +@test "works with linux and github" { + get_ci_uri 'github' + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=linux-gnu run git browse-ci upstream + assert_output "xdg-open $expected_url" + assert_success +} + +@test "works with linux and gitlab" { + get_ci_uri 'gitlab' + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=linux-gnu run git browse-ci upstream + assert_output "xdg-open $expected_url" + assert_success +} + +@test "works with linux and bitbucket" { + get_ci_uri 'bitbucket' + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=linux-gnu run git browse-ci upstream + assert_output "xdg-open $expected_url" + assert_success +} diff --git a/tests/git-browse.bats b/tests/git-browse.bats new file mode 100644 index 0000000..190ef00 --- /dev/null +++ b/tests/git-browse.bats @@ -0,0 +1,123 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file + + PATH="$BATS_TEST_DIRNAME/bin:$PATH" +} + +setup() { + test_util.cd_test + + test_util.git_init + touch ./browse_this + git add ./browse_this + git commit -m 'Add test file' +} + +get_file_uri() { + local mode=$1 + local filename=$2 + + local commit_hash= + commit_hash=$(git rev-parse HEAD) + if [ "$mode" = 'github' ]; then + REPLY="https://github.com/tj/git-extras/blob/$commit_hash/${filename}" + elif [ "$mode" = 'gitlab' ]; then + REPLY="https://gitlab.com/tj/git-extras/-/blob/${commit_hash}/${filename}" + elif [ "$mode" = 'bitbucket' ]; then + REPLY="https://bitbucket.org/tj/git-extras/src/${commit_hash}/${filename}" + fi +} + +@test "works with mac and github" { + get_file_uri github ./browse_this + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=darwin run git browse upstream ./browse_this + assert_output "open $expected_url" + assert_success +} + +@test "works with mac and gitlab" { + get_file_uri gitlab ./browse_this + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=darwin run git browse upstream ./browse_this + assert_output "open $expected_url" + assert_success +} + +@test "works with mac and bitbucket" { + get_file_uri bitbucket ./browse_this + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=darwin run git browse upstream ./browse_this + assert_output "open $expected_url" + assert_success +} + +@test "works with windows and github" { + get_file_uri github ./browse_this + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=msys run git browse upstream ./browse_this + assert_output "start $expected_url" + assert_success +} + +@test "works with windows and gitlab" { + get_file_uri gitlab ./browse_this + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=msys run git browse upstream ./browse_this + assert_output "start $expected_url" + assert_success +} + +@test "works with windows and bitbucket" { + get_file_uri bitbucket ./browse_this + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=msys run git browse upstream ./browse_this + assert_output "start $expected_url" + assert_success +} + +@test "works with linux and github" { + get_file_uri github ./browse_this + local expected_url=$REPLY + + git remote add upstream https://github.com/tj/git-extras + OSTYPE=linux-gnu run git browse upstream ./browse_this + assert_output "xdg-open $expected_url" + assert_success +} + +@test "works with linux and gitlab" { + get_file_uri gitlab ./browse_this + local expected_url=$REPLY + + git remote add upstream https://gitlab.com/tj/git-extras + OSTYPE=linux-gnu run git browse upstream ./browse_this + assert_output "xdg-open $expected_url" + assert_success +} + +@test "works with linux and bitbucket" { + get_file_uri bitbucket ./browse_this + local expected_url=$REPLY + + git remote add upstream https://bitbucket.org/tj/git-extras + OSTYPE=linux-gnu run git browse upstream ./browse_this + assert_output "xdg-open $expected_url" + assert_success +} diff --git a/tests/git-continue.bats b/tests/git-continue.bats new file mode 100755 index 0000000..da916a8 --- /dev/null +++ b/tests/git-continue.bats @@ -0,0 +1,92 @@ +#!/usr/bin/env bats + +source "$BATS_TEST_DIRNAME/test_util.sh" + +setup_file() { + test_util.setup_file +} + +setup() { + test_util.cd_test + + test_util.git_init + git commit -m 'Initial commit' --allow-empty + + git switch -c A main + printf '%s\n' 'a' >> ./tmp_file + git add ./tmp_file + git commit -m 'A' + + git switch -c B main + printf '%s\n' 'b' >> ./tmp_file + git add ./tmp_file + git commit -m 'B' +} + +@test "works with cherry pick" { + run git cherry-pick A + assert_failure + + run git status + assert_line -p 'Unmerged paths:' + assert_success + + git add . + GIT_EDITOR=cat run git continue + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with merge" { + run git merge A + assert_failure + + run git status + assert_line -p 'Unmerged paths:' + assert_success + + git add . + GIT_EDITOR=cat run git continue + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with rebase" { + run git rebase A + assert_failure + + run git status + assert_line -p 'Unmerged paths:' + assert_success + + git add . + GIT_EDITOR=cat run git continue + assert_success + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} + +@test "works with revert" { + run git revert A + assert_failure + + run git status + assert_line -p 'Unmerged paths:' + assert_success + + git add . + GIT_EDITOR=cat run git continue + assert_failure # TODO: Git seems to do nothing and error out? + + run git status + assert_line -p 'nothing to commit, working tree clean' + assert_success +} diff --git a/tests/helper.py b/tests/helper.py index 1c9349f..e4e931d 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,6 +1,8 @@ -import os, subprocess, shutil, tempfile +import os +import subprocess +import shutil +import tempfile from git import Repo, GitCommandError -from testpath import MockCommand, modified_env CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) GIT_EXTRAS_BIN = os.path.abspath(os.path.join(CURRENT_DIR, "..", "bin")) @@ -10,15 +12,16 @@ GITHUB_ORIGIN = "https://github.com/tj/git-extras.git" GITLAB_ORIGIN = "https://gitlab.com/tj/git-extras.git" BITBUCKET_ORIGIN = "https://bitbucket.org/tj/git-extras.git" + class TempRepository: - def __init__(self, repo_work_dir = None): + def __init__(self, repo_work_dir=None): self._system_tmpdir = tempfile.gettempdir() - if repo_work_dir == None: + if repo_work_dir is None: repo_work_dir = tempfile.mkdtemp() else: repo_work_dir = os.path.join(self._system_tmpdir, repo_work_dir) self._cwd = repo_work_dir - self._tempdirname = self._cwd[len(self._system_tmpdir) + 1:] + self._tempdirname = self._cwd[len(self._system_tmpdir) + 1 :] self._git_repo = Repo.init(repo_work_dir, b="default") self._files = [] self.change_origin_to_github() @@ -50,8 +53,8 @@ class TempRepository: tmp_dir = tempfile.mkdtemp() return tmp_dir - def create_tmp_file(self, temp_dir = None): - if temp_dir == None: + def create_tmp_file(self, temp_dir=None): + if temp_dir is None: temp_dir = self._cwd tmp_file = tempfile.mkstemp(dir=temp_dir) @@ -63,7 +66,7 @@ class TempRepository: print(f"File {file_path} has been removed") def writefile(self, temp_file, data): - if data == None: + if data is None: return with open(temp_file, "w", encoding="utf-8") as f: @@ -86,8 +89,9 @@ class TempRepository: origin_extras_command = os.path.join(GIT_EXTRAS_BIN, command_name) temp_extras_command = os.path.join(self._cwd, command_name) helpers = [ - os.path.join(GIT_EXTRAS_HELPER, "git-extra-utility"), - os.path.join(GIT_EXTRAS_HELPER, "is-git-repo")] + os.path.join(GIT_EXTRAS_HELPER, "git-extra-utility"), + os.path.join(GIT_EXTRAS_HELPER, "is-git-repo"), + ] if not os.path.exists(temp_extras_command): whole = [] @@ -106,7 +110,7 @@ class TempRepository: os.chmod(temp_extras_command, 0o775) script = [temp_extras_command, *params] - print(f"Run the script \"{script}\"") + print(f'Run the script "{script}"') return subprocess.run(script, capture_output=True) def change_origin(self, origin_url): diff --git a/tests/poetry.lock b/tests/poetry.lock index 5fa5e29..cef29c8 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -1,4 +1,21 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "codespell" +version = "2.4.0" +description = "Fix common misspellings in text files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "codespell-2.4.0-py3-none-any.whl", hash = "sha256:b4c5b779f747dd481587aeecb5773301183f52b94b96ed51a28126d0482eec1d"}, + {file = "codespell-2.4.0.tar.gz", hash = "sha256:587d45b14707fb8ce51339ba4cce50ae0e98ce228ef61f3c5e160e34f681be58"}, +] + +[package.extras] +dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] +hard-encoding-detection = ["chardet"] +toml = ["tomli"] +types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] [[package]] name = "colorama" @@ -56,13 +73,13 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -127,5 +144,5 @@ test = ["pytest"] [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "e73f9840c5f034acbd9b1ebc082e7ce4600ac3235991e57cd825c261a5f2e14e" +python-versions = "^3.12" +content-hash = "fcf0ba3dd6655735da5c1e21ba32b94011d32bc5a30fbdaf31146d3820c1442a" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 4326657..3d017cb 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -8,11 +8,16 @@ license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.11" +python = "^3.12" + +[tool.poetry.group.test.dependencies] pytest = "8.1.2" gitpython = "3.1.43" testpath = "0.6.0" +[tool.poetry.group.dev.dependencies] +codespell = "2.4" + [tool.pytest.ini_options] minversion = "7.4" addopts = "-ra -q" @@ -21,3 +26,66 @@ testpaths = ["."] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.9 +target-version = "py39" + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/tests/test_git_abort.py b/tests/test_git_abort.py index 38e52c5..1e3785f 100644 --- a/tests/test_git_abort.py +++ b/tests/test_git_abort.py @@ -1,5 +1,6 @@ from git import GitCommandError + class TestGitAbort: def test_init(self, temp_repo): git = temp_repo.get_repo_git() diff --git a/tests/test_git_archive_file.py b/tests/test_git_archive_file.py index 6874322..ddf117e 100644 --- a/tests/test_git_archive_file.py +++ b/tests/test_git_archive_file.py @@ -1,4 +1,6 @@ -import os, pytest +import os +import pytest + class TestGitArchiveFile: def test_init(self, temp_repo): @@ -16,14 +18,17 @@ class TestGitArchiveFile: filename = "{0}.{1}.zip".format(temp_repo.get_repo_dirname(), git.describe()) assert filename in os.listdir() - def test_archive_file_on_any_not_tags_branch_without_default_branch(self, temp_repo): + def test_archive_file_on_any_not_tags_branch_without_default_branch( + self, temp_repo + ): git = temp_repo.get_repo_git() git.checkout("-b", "not-tags-branch") temp_repo.invoke_installed_extras_command("archive-file") filename = "{0}.{1}.{2}.zip".format( - temp_repo.get_repo_dirname(), - git.describe("--always", "--long"), - "not-tags-branch") + temp_repo.get_repo_dirname(), + git.describe("--always", "--long"), + "not-tags-branch", + ) assert filename in os.listdir() def test_archive_file_on_any_not_tags_branch_with_default_branch(self, temp_repo): @@ -32,8 +37,8 @@ class TestGitArchiveFile: git.config("git-extras.default-branch", "default") temp_repo.invoke_installed_extras_command("archive-file") filename = "{0}.{1}.zip".format( - temp_repo.get_repo_dirname(), - git.describe("--always", "--long")) + temp_repo.get_repo_dirname(), git.describe("--always", "--long") + ) assert filename in os.listdir() def test_archive_file_on_branch_name_has_slash(self, temp_repo): @@ -41,9 +46,10 @@ class TestGitArchiveFile: git.checkout("-b", "feature/slash") temp_repo.invoke_installed_extras_command("archive-file") filename = "{0}.{1}.{2}.zip".format( - temp_repo.get_repo_dirname(), - git.describe("--always", "--long"), - "feature-slash") + temp_repo.get_repo_dirname(), + git.describe("--always", "--long"), + "feature-slash", + ) assert filename in os.listdir() @pytest.mark.parametrize("named_temp_repo", ["backslash\\dir"], indirect=True) @@ -51,9 +57,8 @@ class TestGitArchiveFile: named_temp_repo.invoke_installed_extras_command("archive-file") git = named_temp_repo.get_repo_git() filename = "{0}.{1}.{2}.zip".format( - "backslash-dir", - git.describe("--always", "--long"), - "default") + "backslash-dir", git.describe("--always", "--long"), "default" + ) assert filename in os.listdir() def test_archive_file_on_tag_name_has_slash(self, temp_repo): @@ -65,6 +70,6 @@ class TestGitArchiveFile: temp_repo.invoke_installed_extras_command("archive-file") description_include_version = git.describe("--always", "--long") filename = "{0}.{1}.zip".format( - temp_repo.get_repo_dirname(), - description_include_version.replace("/", "-")) + temp_repo.get_repo_dirname(), description_include_version.replace("/", "-") + ) assert filename in os.listdir() diff --git a/tests/test_git_authors.py b/tests/test_git_authors.py index 8eb3cf9..f342433 100644 --- a/tests/test_git_authors.py +++ b/tests/test_git_authors.py @@ -1,9 +1,10 @@ -import os, subprocess - -expected_authors_list = "test \ntestagain \n" +expected_authors_list = ( + "test \ntestagain \n" +) expected_authors_list_without_email = "test\ntestagain\n" authors_file = "AUTHORS" + class TestGitAuthors: def test_init(self, temp_repo): git = temp_repo.get_repo_git() @@ -18,8 +19,8 @@ class TestGitAuthors: git.commit("-m", "test: add data B") def test_output_authors_has_email_without_any_parameter(self, temp_repo): - git = temp_repo.get_repo_git() - rs = temp_repo.invoke_extras_command("authors") + temp_repo.get_repo_git() + temp_repo.invoke_extras_command("authors") with open(authors_file) as f: content = f.read() print(content) @@ -27,7 +28,7 @@ class TestGitAuthors: assert content == expected_authors_list def test_list_authors_has_email_defaultly(self, temp_repo): - git = temp_repo.get_repo_git() + temp_repo.get_repo_git() actual = temp_repo.invoke_extras_command("authors", "--list") actual = actual.stdout.decode() assert actual == expected_authors_list @@ -36,7 +37,7 @@ class TestGitAuthors: assert actual == expected_authors_list def test_list_authors_has_not_email(self, temp_repo): - git = temp_repo.get_repo_git() + temp_repo.get_repo_git() actual = temp_repo.invoke_extras_command("authors", "--list", "--no-email") actual = actual.stdout.decode() assert actual == expected_authors_list_without_email diff --git a/tests/test_git_browse.py b/tests/test_git_browse.py index ff4b4db..39822bf 100644 --- a/tests/test_git_browse.py +++ b/tests/test_git_browse.py @@ -1,22 +1,23 @@ -import os, subprocess from testpath import MockCommand, modified_env UNKNOWN_SITE_ORIGIN = "https://unknown-site.com/tj/git-extras.git" + def get_file_uri(mode, filename, git): commit_hash = git.rev_parse("HEAD") if mode == "github": - return "https://github.com/tj/git-extras/blob/" + commit_hash + '/' + filename + return "https://github.com/tj/git-extras/blob/" + commit_hash + "/" + filename if mode == "gitlab": - return "https://gitlab.com/tj/git-extras/-/blob/" + commit_hash + '/' + filename + return "https://gitlab.com/tj/git-extras/-/blob/" + commit_hash + "/" + filename if mode == "bitbucket": - return "https://bitbucket.org/tj/git-extras/src/" + commit_hash + '/' + filename + return "https://bitbucket.org/tj/git-extras/src/" + commit_hash + "/" + filename + class TestGitBrowse: def test_browse_github_file_on_mac(self, temp_repo): git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("github", filename, git) openCommand.assert_called([expected_url]) @@ -25,7 +26,7 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called([expected_url]) @@ -34,7 +35,7 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called([expected_url]) @@ -43,7 +44,7 @@ class TestGitBrowse: temp_repo.change_origin_to_github() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("github", filename, git) openCommand.assert_called([expected_url]) @@ -52,7 +53,7 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called([expected_url]) @@ -61,7 +62,7 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called([expected_url]) @@ -70,8 +71,12 @@ class TestGitBrowse: temp_repo.change_origin_to_github() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("github", filename, git) openCommand.assert_called(["-NoProfile", "start", expected_url]) @@ -80,8 +85,12 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called(["-NoProfile", "start", expected_url]) @@ -90,8 +99,12 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called(["-NoProfile", "start", expected_url]) @@ -100,8 +113,12 @@ class TestGitBrowse: temp_repo.change_origin_to_github() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("github", filename, git) openCommand.assert_called([expected_url]) @@ -110,8 +127,12 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called([expected_url]) @@ -120,8 +141,12 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called([expected_url]) @@ -130,7 +155,10 @@ class TestGitBrowse: temp_repo.change_origin_to_github() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("github", filename, git) openCommand.assert_called([expected_url]) @@ -139,7 +167,10 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called([expected_url]) @@ -148,7 +179,10 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename) expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called([expected_url]) @@ -157,7 +191,10 @@ class TestGitBrowse: temp_repo.change_origin_to_github() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename, "10", "20") expected_url = get_file_uri("github", filename, git) openCommand.assert_called([expected_url + "#L10-L20"]) @@ -166,7 +203,10 @@ class TestGitBrowse: temp_repo.change_origin_to_gitlab() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename, "10", "20") expected_url = get_file_uri("gitlab", filename, git) openCommand.assert_called([expected_url + "#L10-20"]) @@ -175,21 +215,30 @@ class TestGitBrowse: temp_repo.change_origin_to_bitbucket() git = temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename, "10", "20") expected_url = get_file_uri("bitbucket", filename, git) openCommand.assert_called([expected_url + "#lines-10:20"]) def test_browse_unknown_site_file(self, temp_repo): temp_repo.change_origin(UNKNOWN_SITE_ORIGIN) - git = temp_repo.get_repo_git() - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + temp_repo.get_repo_git() + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin") openCommand.assert_called([UNKNOWN_SITE_ORIGIN[0:-4]]) def test_browse_unknown_site_file_with_line_number(self, temp_repo): - git = temp_repo.get_repo_git() + temp_repo.get_repo_git() filename = temp_repo.get_filename(0) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse", "origin", filename, "10", "20") openCommand.assert_called([UNKNOWN_SITE_ORIGIN[0:-4]]) diff --git a/tests/test_git_browse_ci.py b/tests/test_git_browse_ci.py index 761d86d..b69e554 100644 --- a/tests/test_git_browse_ci.py +++ b/tests/test_git_browse_ci.py @@ -1,8 +1,8 @@ -import os, subprocess from testpath import MockCommand, modified_env UNKNOWN_SITE_ORIGIN = "https://unknown-site.com/tj/git-extras.git" + def get_ci_uri_by_domain(mode): if mode == "github": return "https://github.com/tj/git-extras/actions" @@ -11,119 +11,156 @@ def get_ci_uri_by_domain(mode): if mode == "bitbucket": return "https://bitbucket.org/tj/git-extras/addon/pipelines/home" + class TestGitBrowse: def test_browse_github_ci_on_mac(self, temp_repo): - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("github") openCommand.assert_called([expected_url]) def test_browse_gitlab_ci_on_mac(self, temp_repo): temp_repo.change_origin_to_gitlab() - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("gitlab") openCommand.assert_called([expected_url]) def test_browse_bitbucket_ci_on_mac(self, temp_repo): temp_repo.change_origin_to_bitbucket() - with modified_env({ "OSTYPE": "darwin" }), MockCommand("open") as openCommand: + with modified_env({"OSTYPE": "darwin"}), MockCommand("open") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("bitbucket") openCommand.assert_called([expected_url]) def test_browse_github_ci_on_git_bash_on_window(self, temp_repo): temp_repo.change_origin_to_github() - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("github") openCommand.assert_called([expected_url]) def test_browse_gitlab_ci_on_git_bash_on_window(self, temp_repo): temp_repo.change_origin_to_gitlab() - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("gitlab") openCommand.assert_called([expected_url]) def test_browse_bitbucket_ci_on_git_bash_on_window(self, temp_repo): temp_repo.change_origin_to_bitbucket() - with modified_env({ "OSTYPE": "msys" }), MockCommand("start") as openCommand: + with modified_env({"OSTYPE": "msys"}), MockCommand("start") as openCommand: temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("bitbucket") openCommand.assert_called([expected_url]) def test_browse_github_ci_on_WSL_with_microsoft_key(self, temp_repo): temp_repo.change_origin_to_github() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("github") openCommand.assert_called(["-NoProfile", "start", expected_url]) def test_browse_gitlab_ci_on_WSL_with_microsoft_key(self, temp_repo): temp_repo.change_origin_to_gitlab() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("gitlab") openCommand.assert_called(["-NoProfile", "start", expected_url]) def test_browse_bitbucket_ci_on_WSL_with_microsoft_key(self, temp_repo): temp_repo.change_origin_to_bitbucket() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "microsoft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("powershell.exe") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "microsoft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("powershell.exe") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("bitbucket") openCommand.assert_called(["-NoProfile", "start", expected_url]) def test_browse_github_ci_on_WSL_without_microsoft_key(self, temp_repo): temp_repo.change_origin_to_github() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("github") openCommand.assert_called([expected_url]) def test_browse_gitlab_ci_on_WSL_without_microsoft_key(self, temp_repo): temp_repo.change_origin_to_gitlab() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("gitlab") openCommand.assert_called([expected_url]) def test_browse_bitbucket_ci_on_WSL_without_microsoft_key(self, temp_repo): temp_repo.change_origin_to_bitbucket() - with modified_env({ "OSTYPE": "linux" }), MockCommand.fixed_output("uname", "no-micro-soft"), \ - MockCommand.fixed_output("command", "/powershell.exe"), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "linux"}), + MockCommand.fixed_output("uname", "no-micro-soft"), + MockCommand.fixed_output("command", "/powershell.exe"), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("bitbucket") openCommand.assert_called([expected_url]) def test_browse_github_ci_not_mac_or_msys_or_linux(self, temp_repo): temp_repo.change_origin_to_github() - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("github") openCommand.assert_called([expected_url]) def test_browse_gitlab_ci_not_mac_or_msys_or_linux(self, temp_repo): temp_repo.change_origin_to_gitlab() - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("gitlab") openCommand.assert_called([expected_url]) def test_browse_bitbucket_ci_not_mac_or_msys_or_linux(self, temp_repo): temp_repo.change_origin_to_bitbucket() - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") expected_url = get_ci_uri_by_domain("bitbucket") openCommand.assert_called([expected_url]) def test_browse_unknown_site_file(self, temp_repo): temp_repo.change_origin(UNKNOWN_SITE_ORIGIN) - with modified_env({ "OSTYPE": "unique-system" }), MockCommand("xdg-open") as openCommand: + with ( + modified_env({"OSTYPE": "unique-system"}), + MockCommand("xdg-open") as openCommand, + ): temp_repo.invoke_extras_command("browse-ci", "origin") - openCommand.assert_called(['']) + openCommand.assert_called([""]) diff --git a/tests/test_git_continue.py b/tests/test_git_continue.py new file mode 100644 index 0000000..8fd7d5e --- /dev/null +++ b/tests/test_git_continue.py @@ -0,0 +1,84 @@ +from git import GitCommandError + + +class TestGitContinue: + + @classmethod + def _init_repo(cls, repo): + git = repo.get_repo_git() + tmp_file = repo.get_file(0) + git.branch("A") + git.branch("B") + git.branch("C") + git.checkout("A") + repo.writefile(tmp_file, "a") + git.add(".") + git.commit("-m", "A") + git.checkout("B") + repo.writefile(tmp_file, "b") + git.add(".") + git.commit("-m", "B") + + def test_init(self, temp_repo_clean): + TestGitContinue._init_repo(temp_repo_clean) + git = temp_repo_clean.get_repo_git() + git.status() + + def test_cherry_pick(self, temp_repo_clean): + TestGitContinue._init_repo(temp_repo_clean) + git = temp_repo_clean.get_repo_git() + try: + git.cherry_pick("A") + except GitCommandError as err: + print(err) + result = git.status() + assert "Unmerged path" in result + git.add(".") + temp_repo_clean.invoke_extras_command("continue") + result = git.status() + assert "nothing to commit, working tree clean" in result + + def test_merge(self, temp_repo_clean): + TestGitContinue._init_repo(temp_repo_clean) + git = temp_repo_clean.get_repo_git() + try: + git.merge("A") + except GitCommandError as err: + print(err) + result = git.status() + assert "Unmerged path" in result + git.add(".") + git.commit("-m", "resolve conflict") + temp_repo_clean.invoke_extras_command("continue") + result = git.status() + assert "nothing to commit, working tree clean" in result + + def test_rebase(self, temp_repo_clean): + TestGitContinue._init_repo(temp_repo_clean) + git = temp_repo_clean.get_repo_git() + try: + git.rebase("A") + except GitCommandError as err: + print(err) + result = git.status() + assert "Unmerged path" in result + git.add(".") + git.commit("-m", "resolve conflict") + temp_repo_clean.invoke_extras_command("continue") + result = git.status() + assert "nothing to commit, working tree clean" in result + + def test_revert(self, temp_repo_clean): + TestGitContinue._init_repo(temp_repo_clean) + git = temp_repo_clean.get_repo_git() + try: + git.revert("A") + except GitCommandError as err: + print(err) + result = git.status() + assert "Unmerged path" in result + git.add(".") + git.commit("-m", "resolve conflict") + temp_repo_clean.invoke_extras_command("continue") + result = git.status() + assert "nothing to commit, working tree clean" in result diff --git a/tests/test_util.sh b/tests/test_util.sh new file mode 100644 index 0000000..4c85773 --- /dev/null +++ b/tests/test_util.sh @@ -0,0 +1,24 @@ +# shellcheck shell=bash + +source "$BATS_TEST_DIRNAME/../vendor/bats-all/load.bash" + +test_util.setup_file() { + cd "$BATS_FILE_TMPDIR" + + export GIT_CONFIG_NOSYSTEM=1 + export GIT_CONFIG_GLOBAL="$PWD/git_config" + + # Append to path so that we can access all commands included from git-extras + # TODO: This currently breaks with commands that are included in "not_needed_git_repo" etc. + PATH="$BATS_TEST_DIRNAME/../bin:$PATH" +} + +test_util.cd_test() { + cd "$BATS_TEST_TMPDIR" +} + +test_util.git_init() { + git init --initial-branch main + git config user.name 'Name' + git config user.email 'name@example.com' +} diff --git a/vendor/bats-all b/vendor/bats-all new file mode 160000 index 0000000..a16a4a2 --- /dev/null +++ b/vendor/bats-all @@ -0,0 +1 @@ +Subproject commit a16a4a2cfa744878992a5f5d3f206319dba54158