diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..abd7452a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[{vcsh.in,completions/vcsh.*}] +indent_style = tab +trim_trailing_whitespace = true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..749157df --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint +on: [push, pull_request] +jobs: + editor-config: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Lint code style + uses: editorconfig-checker/action-editorconfig-checker@v1.0.0 + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Configure + run: | + ./bootstrap.sh + ./configure --without-man-page --disable-tests + - name: Run shellcheck + uses: reviewdog/action-shellcheck@v1.0.0 + with: + pattern: vcsh + reporter: github-pr-review + github_token: ${{ github.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c3bdfa1b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +on: + push: + tags: + - v*.*.* + +jobs: + + ghrelase: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Configure + run: | + echo "VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV + echo "${GITHUB_REF#refs/*/v}" > .tarball-version + ./bootstrap.sh + ./configure --disable-dependency-checks + - name: Build source package + run: | + make dist + - name: Check source package behaviour + run: | + make distcheck + - name: Make sure changelog was updated + run: | + make changelog-HEAD + grep -F "* Release ${{ env.VERSION }}" changelog-HEAD + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + body_path: changelog-HEAD + files: | + vcsh-${{ env.VERSION }}.zip + vcsh-${{ env.VERSION }}.tar.xz + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..c1808e01 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Test +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Fetch tags + run: | + git fetch --prune --tags ||: + - name: Install dependencies + run: | + sudo apt install ronn + - name: Install perl test dependencies + uses: perl-actions/install-with-cpanm@v1.1 + with: + install: | + Shell::Command + Test::Most + - name: Configure + run: | + ./bootstrap.sh + ./configure + - name: Run tests + run: | + make check + - name: Build source package + run: | + make dist + echo VERSION=$(cat .version) >> $GITHUB_ENV + - name: Post build artifacts + uses: actions/upload-artifact@v2 + with: + name: vcsh-${{ env.VERSION }} + path: vcsh-${{ env.VERSION }}.zip diff --git a/.gitignore b/.gitignore index 4dec9bf3..2562f6ab 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,18 @@ vcsh.1 *.swp .swp *.bak +.version +.version-prev +Makefile +Makefile.in +aclocal.m4 +autom4te.cache/ +build-aux/install-sh +build-aux/missing +completions/_vcsh +completions/vcsh +config.log +config.status +configure +/vcsh +vcsh-* diff --git a/.mailmap b/.mailmap index 65c64b49..5c8bf56a 100644 --- a/.mailmap +++ b/.mailmap @@ -1,4 +1,6 @@ Richard Hartmann +Richard Hartmann Richard Hartmann Richard Hartmann +Kevin Lyda Alexander Skurikhin diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index be5a1d79..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: perl -before_install: - - cpanm Shell::Command - - cpanm Test::Most - - apt-get moo -install: - - sudo apt-get update - - sudo apt-get install cowsay git ruby-ronn -script: - - make test -after_script: - - make moo -notifications: - email: - on_success: change - on_failure: always diff --git a/CONTRIBUTORS b/CONTRIBUTORS index cf286198..8cd4d5f3 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,41 +1,66 @@ -Alphabetical list of surnames of everyone who ever committed to this repository. -Auto-generated from tools/list_CONTRIBUTORS. +Alphabetical list of names of everyone who ever committed to this repository. +Auto-generated using `make -B CONTRIBUTORS` -Skurikhin Alexander -Eric Bouchut -Dridi Boukelmoune -Rob Cornish -Vincent Demeester -Mert Dirik -Jeff Fein-Worton -Thomas Ferris Nicolaisen -martin f. krafft +Aaron Schumacher +Aaron VonderHaar Alessandro Ghedini -Dennis Gilmore -Thorsten Glaser -G.raud -Mikhail Gusarov -Valentin Haenel -Richard Hartmann -Gregor Jasny -Errietta Kostala -Yuval Langer +Alexander Skurikhin +Andrew Schwartzmeyer +arndtc +Aryel Mota Góis Caleb Maclennan -Markus Martin -mek-apelsin -Evan Pitstick -Dieter Plaetinck Corey Quinn -Pavlos Ratis +Daniel Shahaf +Dato Simó +Debian Janitor +Dennis Gilmore +Devin J. Pohly Dewey Sasser +Dieter Plaetinck +Don +Don March +Dridi Boukelmoune +Edward Betts +Eli Young +Eric Bouchut +Errietta Kostala +Evan Pitstick +Fedora Release Engineering +Felix Eckhofer +Florian Engel +Frank Terbeck Gernot Schulz -Aaron Schumacher -Andrew Schwartzmeyer -Dato Simó -Alexander Skurikhin +G.raud +Gregor Jasny +guy hughes +Harendra Kumar +James Davidson +Jeff Fein-Worton +Jochen Keil Jonathan Sternberg +Julien Lecomte +Kevin Lyda +leycec +Markus Martin +martin f. krafft Mathias Svensson -Frank Terbeck +mek-apelsin +Mert Dirik +Mikhail Gusarov mirabilos -Aaron VonderHaar +miramir +Noah Birnel +Pavlos Ratis +Richard Hartmann +Rob Cornish +Roland Hopferwieser +Skurikhin Alexander +soulofmischief <30357883+soulofmischief@users.noreply.github.com> +Thomas Ferris Nicolaisen +Thomas Tuegel +Thorsten Glaser +tikki Tony +Valentin Haenel +Vincent Demeester +Yuval Langer diff --git a/Makefile b/Makefile deleted file mode 100644 index b9154e7f..00000000 --- a/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -PREFIX?=/usr -DOCDIR_PREFIX=$(PREFIX)/share/doc -DOCDIR=$(DOCDIR_PREFIX)/$(self) -ZSHDIR=$(PREFIX)/share/zsh/vendor-completions -RONN ?= ronn - -self=vcsh -manpages=$(self).1 -all=test manpages - -all: $(all) - -install: all - install -d $(DESTDIR)$(PREFIX)/bin - install -m 0755 $(self) $(DESTDIR)$(PREFIX)/bin - install -d $(DESTDIR)$(PREFIX)/share/man/man1 - install -m 0644 $(manpages) $(DESTDIR)$(PREFIX)/share/man/man1 - install -d $(DESTDIR)$(DOCDIR) - install -m 0644 README.md $(DESTDIR)$(DOCDIR) - install -m 0644 doc/hooks $(DESTDIR)$(DOCDIR) - install -d $(DESTDIR)$(ZSHDIR) - install -m 0644 _$(self) $(DESTDIR)$(ZSHDIR) - -manpages: $(manpages) - -$(self).1: doc/$(self).1.ronn - $(RONN) < doc/$(self).1.ronn > $(self).1 || rm $(self).1 - -clean: - rm -rf $(self).1 - -uninstall: - rm -rf $(DESTDIR)$(PREFIX)/bin/$(self) - rm -rf $(DESTDIR)$(PREFIX)/share/man/man1/$(self).1 - rm -rf $(DESTDIR)$(DOCDIR) - rm -rf $(DESTDIR)$(ZSHDIR)/_$(self) - -# Potentially harmful, used a non-standard option on purpose. -# If PREFIX=/usr/local and that's empty... -purge: uninstall - rmdir -p --ignore-fail-on-non-empty $(DESTDIR)$(PREFIX)/bin/ - rmdir -p --ignore-fail-on-non-empty $(DESTDIR)$(PREFIX)/share/man/man1/ - rmdir -p --ignore-fail-on-non-empty $(DESTDIR)$(DOCDIR) - rmdir -p --ignore-fail-on-non-empty $(DESTDIR)$(ZSHDIR) - -test: - @if which git > /dev/null; then : ; else echo "'git' not found, exiting..." ; exit 1; fi - @if which prove > /dev/null; then prove; else echo "'prove' not found; not running tests"; fi - -moo: - @which cowsay >/dev/null 2>&1 && cowsay "I hope you're happy now..." diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 00000000..ca452654 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,99 @@ +ACLOCAL_AMFLAGS = -I build-aux + +.ONESHELL: +.SECONDARY: +.SECONDEXPANSION: +.DELETE_ON_ERROR: + +docdir = $(datarootdir)/doc/$(TRANSFORMED_PACKAGE_NAME) +licensedir = $(datarootdir)/licenses/$(TRANSFORMED_PACKAGE_NAME) + +dist_doc_DATA = changelog doc/INSTALL.md doc/README.md doc/error_codes.md +samplehooksdir = $(docdir)/sample_hooks +dist_samplehooks_DATA = doc/sample_hooks/post-init-add-origin doc/sample_hooks/post-init-setup-mr doc/sample_hooks/post-merge-unclobber doc/sample_hooks/pre-merge-unclobber +dist_license_DATA = LICENSE CONTRIBUTORS +if ENABLE_MAN_PAGE +dist_man_MANS = vcsh.1 +endif +bin_SCRIPTS = vcsh + +EXTRA_DIST = completions/vcsh.bash completions/vcsh.zsh build-aux/git-version-gen build-aux/ax_prog_perl_modules.m4 + +BUILT_SOURCES = .version +CLEANFILES = $(BUILT_SOURCES) .version-prev $(dist_man_MANS) $(bin_SCRIPTS) + +if ENABLE_BASH_COMPLETION +bashcompletiondir = $(BASH_COMPLETION_DIR) +nodist_bashcompletion_DATA = completions/$(TRANSFORMED_PACKAGE_NAME) +CLEANFILES += $(nodist_bashcompletion_DATA) +endif + +if ENABLE_ZSH_COMPLETION +zshcompletiondir = $(ZSH_COMPLETION_DIR) +nodist_zshcompletion_DATA = completions/_$(TRANSFORMED_PACKAGE_NAME) +CLEANFILES += $(nodist_zshcompletion_DATA) +endif + +vcsh.1: doc/vcsh.1.ronn + $(RONN) < $< > $@ + +completions/$(TRANSFORMED_PACKAGE_NAME): completions/vcsh.bash + mkdir -p $(dir $@) + cp -bf $< $@ + +completions/_$(TRANSFORMED_PACKAGE_NAME): completions/vcsh.zsh + mkdir -p $(dir $@) + cp -bf $< $@ + +.version: $(shell $(AWK) '{print ".git/" $$2}' .git/HEAD 2>/dev/null ||:) + [ -e "$@" ] && mv "$@" "$@-prev" || $(if $<,touch,cp "$(srcdir)/.tarball-version") "$@-prev" + $(if $<,./build-aux/git-version-gen "$(srcdir)/.tarball-version",printf "$(VERSION)") > "$@" + $(CMP) -s "$@" "$@-prev" || autoreconf configure.ac --force + +_CHECKDEPS = check-version + +if ENABLE_TESTS +_CHECKDEPS += prove +endif + +check-local: $(_CHECKDEPS) + +installcheck-local: + ./$(TRANSFORMED_PACKAGE_NAME) version + +.PHONY: check-version +check-version: vcsh | .version + $(GREP) -Fx '$(VERSION)' $| + ./$< version | $(GREP) -Ff $| + ./$< version | $(GREP) -Ff <($(GIT) version) + +.PHONY: prove +prove: + prove + +.PHONY: test +test: prove + +.PHONY: lint +lint: lint-editor-config lint-shellcheck + +.PHONY: lint-editor-config +lint-editor-config: + ec + +.PHONY: lint-shellheck +lint-shellcheck: vcsh + shellcheck $< + +CONTRIBUTORS: + exec > $@ + echo 'Alphabetical list of names of everyone who ever committed to this repository.' + echo 'Auto-generated using `make -B CONTRIBUTORS`' + echo + $(GIT) shortlog -se --all | cut -f1 --complement | sort -u + +changelog-HEAD: changelog + sed -nEe '2d;s/^\t//p;/^$$/q;' $< > $@ + +dist-hook: + printf "$(VERSION)" > "$(distdir)/.tarball-version" diff --git a/README.md b/README.md index f885b4a3..77ce7cd5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ vcsh - Version Control System for $HOME - multiple Git repositories in $HOME -[![Build Status](https://travis-ci.org/RichiH/vcsh.svg?branch=master)](https://travis-ci.org/RichiH/vcsh) - +[![Test Status](https://github.com/RichiH/vcsh/actions/workflows/test.yml/badge.svg)](https://github.com/RichiH/vcsh/actions/workflows/test.yml) # Index 1. [30 Second How-to](#30-second-how-to) 2. [Introduction](#introduction) -3. [Contact](#contact) +3. [Installation](#installation) +4. [Contact](#contact) # 30 Second How-to @@ -47,19 +47,19 @@ For example, you may not need to have your `mplayer` configuration on a server or available to root and you may want to maintain different configuration for `ssh` on your personal and your work machines. -A lot of modern UNIX-based systems offer packages for `vcsh`. In case yours -does not, read [INSTALL.md](doc/INSTALL.md) for install instructions or -[PACKAGING.md](doc/PACKAGING.md) to create a package yourself. If you do end -up packaging `vcsh` please let us know so we can give you your own packaging -branch in the upstream repository. - ## Talks -Some people found it useful to look at slides and videos explaining how `vcsh` +Some people found it useful to look at [slides](https://github.com/RichiH/talks/blob/main/2013/10-linuxcon-eu/linuxcon_eu-2013-10-gitify_your_life.pdf) and videos explaining how `vcsh` works instead of working through the docs. All slides, videos, and further information can be found [on the author's talk page][talks]. +# Installation + +A lot of modern UNIX-based systems offer packages for `vcsh`. In case yours +does not, read [INSTALL.md](doc/INSTALL.md) for instructions on installing from +sources or even create a package for your system. If you do end up packaging +`vcsh` please let us know so we can document package availability. # Contact @@ -74,6 +74,6 @@ community around the general idea of version controlling your (digital) life. [myrepos]: http://myrepos.branchable.com/ -[talks]: http://richardhartmann.de/talks/ +[talks]: https://github.com/RichiH/talks [vcsh]: https://github.com/RichiH/vcsh [vcs-home-list]: http://lists.madduck.net/listinfo/vcs-home diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 00000000..0d8d2841 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +set -e + +incomplete_source () { + printf '%s\n' \ + "$1. Please either:" \ + "* $2," \ + "* or use the source packages instead of a repo archive" \ + "* or use a full Git clone." >&2 + exit 1 +} + +# This enables easy building from Github's snapshot archives +if [ ! -e ".git" ]; then + if [ ! -f ".tarball-version" ]; then + incomplete_source "No version information found" \ + "identify the correct version with \`echo \$version > .tarball-version\`" + fi +else + # Just a head start to save a ./configure cycle + ./build-aux/git-version-gen .tarball-version > .version +fi + +autoreconf --install diff --git a/build-aux/ax_prog_perl_modules.m4 b/build-aux/ax_prog_perl_modules.m4 new file mode 100644 index 00000000..70b3230e --- /dev/null +++ b/build-aux/ax_prog_perl_modules.m4 @@ -0,0 +1,77 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_prog_perl_modules.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_PROG_PERL_MODULES([MODULES], [ACTION-IF-TRUE], [ACTION-IF-FALSE]) +# +# DESCRIPTION +# +# Checks to see if the given perl modules are available. If true the shell +# commands in ACTION-IF-TRUE are executed. If not the shell commands in +# ACTION-IF-FALSE are run. Note if $PERL is not set (for example by +# calling AC_CHECK_PROG, or AC_PATH_PROG), AC_CHECK_PROG(PERL, perl, perl) +# will be run. +# +# MODULES is a space separated list of module names. To check for a +# minimum version of a module, append the version number to the module +# name, separated by an equals sign. +# +# Example: +# +# AX_PROG_PERL_MODULES( Text::Wrap Net::LDAP=1.0.3, , +# AC_MSG_WARN(Need some Perl modules) +# +# LICENSE +# +# Copyright (c) 2009 Dean Povey +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AU_ALIAS([AC_PROG_PERL_MODULES], [AX_PROG_PERL_MODULES]) +AC_DEFUN([AX_PROG_PERL_MODULES],[dnl + +m4_define([ax_perl_modules]) +m4_foreach([ax_perl_module], m4_split(m4_normalize([$1])), + [ + m4_append([ax_perl_modules], + [']m4_bpatsubst(ax_perl_module,=,[ ])[' ]) + ]) + +# Make sure we have perl +if test -z "$PERL"; then +AC_CHECK_PROG(PERL,perl,perl) +fi + +if test "x$PERL" != x; then + ax_perl_modules_failed=0 + for ax_perl_module in ax_perl_modules; do + AC_MSG_CHECKING(for perl module $ax_perl_module) + + # Would be nice to log result here, but can't rely on autoconf internals + $PERL -e "use $ax_perl_module; exit" > /dev/null 2>&1 + if test $? -ne 0; then + AC_MSG_RESULT(no); + ax_perl_modules_failed=1 + else + AC_MSG_RESULT(ok); + fi + done + + # Run optional shell commands + if test "$ax_perl_modules_failed" = 0; then + : + $2 + else + : + $3 + fi +else + AC_MSG_WARN(could not find perl) +fi])dnl diff --git a/build-aux/git-version-gen b/build-aux/git-version-gen new file mode 100755 index 00000000..e80e810e --- /dev/null +++ b/build-aux/git-version-gen @@ -0,0 +1,220 @@ +#!/usr/bin/env sh + +# Print a version string. +scriptversion=2012-03-18.17; # UTC + +# Copyright (C) 2007-2012 Free Software Foundation, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/. +# It may be run two ways: +# - from a git repository in which the "git describe" command below +# produces useful output (thus requiring at least one signed tag) +# - from a non-git-repo directory containing a .tarball-version file, which +# presumes this script is invoked like "./git-version-gen .tarball-version". + +# In order to use intra-version strings in your project, you will need two +# separate generated version string files: +# +# .tarball-version - present only in a distribution tarball, and not in +# a checked-out repository. Created with contents that were learned at +# the last time autoconf was run, and used by git-version-gen. Must not +# be present in either $(srcdir) or $(builddir) for git-version-gen to +# give accurate answers during normal development with a checked out tree, +# but must be present in a tarball when there is no version control system. +# Therefore, it cannot be used in any dependencies. GNUmakefile has +# hooks to force a reconfigure at distribution time to get the value +# correct, without penalizing normal development with extra reconfigures. +# +# .version - present in a checked-out repository and in a distribution +# tarball. Usable in dependencies, particularly for files that don't +# want to depend on config.h but do want to track version changes. +# Delete this file prior to any autoconf run where you want to rebuild +# files to pick up a version string change; and leave it stale to +# minimize rebuild time after unrelated changes to configure sources. +# +# As with any generated file in a VC'd directory, you should add +# /.version to .gitignore, so that you don't accidentally commit it. +# .tarball-version is never generated in a VC'd directory, so needn't +# be listed there. +# +# Use the following line in your configure.ac, so that $(VERSION) will +# automatically be up-to-date each time configure is run (and note that +# since configure.ac no longer includes a version string, Makefile rules +# should not depend on configure.ac for version updates). +# +# AC_INIT([GNU project], +# m4_esyscmd([build-aux/git-version-gen .tarball-version]), +# [bug-project@example]) +# +# Then use the following lines in your Makefile.am, so that .version +# will be present for dependencies, and so that .version and +# .tarball-version will exist in distribution tarballs. +# +# EXTRA_DIST = $(top_srcdir)/.version +# BUILT_SOURCES = $(top_srcdir)/.version +# $(top_srcdir)/.version: +# echo $(VERSION) > $@-t && mv $@-t $@ +# dist-hook: +# echo $(VERSION) > $(distdir)/.tarball-version + + +me=$0 + +version="git-version-gen $scriptversion + +Copyright 2011 Free Software Foundation, Inc. +There is NO warranty. You may redistribute this software +under the terms of the GNU General Public License. +For more information about these matters, see the files named COPYING." + +usage="\ +Usage: $me [OPTION]... \$srcdir/.tarball-version [TAG-NORMALIZATION-SED-SCRIPT] +Print a version string. + +Options: + + --prefix prefix of git tags (default 'v') + + --help display this help and exit + --version output version information and exit + +Running without arguments will suffice in most cases." + +prefix=v + +while test $# -gt 0; do + case $1 in + --help) echo "$usage"; exit 0;; + --version) echo "$version"; exit 0;; + --prefix) shift; prefix="$1";; + -*) + echo "$0: Unknown option '$1'." >&2 + echo "$0: Try '--help' for more information." >&2 + exit 1;; + *) + if test -z "$tarball_version_file"; then + tarball_version_file="$1" + elif test -z "$tag_sed_script"; then + tag_sed_script="$1" + else + echo "$0: extra non-option argument '$1'." >&2 + exit 1 + fi;; + esac + shift +done + +if test -z "$tarball_version_file"; then + echo "$usage" + exit 1 +fi + +tag_sed_script="${tag_sed_script:-s/x/x/}" + +nl=' +' + +# Avoid meddling by environment variable of the same name. +v= +v_from_git= + +# First see if there is a tarball-only version file. +# then try "git describe", then default. +if test -f $tarball_version_file +then + v=`cat $tarball_version_file` || v= + case $v in + *$nl*) v= ;; # reject multi-line output + [0-9]*) ;; + *) v= ;; + esac + test -z "$v" \ + && echo "$0: WARNING: $tarball_version_file is missing or damaged" 1>&2 +fi + +if test -n "$v" +then + : # use $v +# Otherwise, if there is at least one git commit involving the working +# directory, and "git describe" output looks sensible, use that to +# derive a version string. +elif test "`git log -1 --pretty=format:x . 2>/dev/null`" = x \ + && v=`git describe --tags --abbrev=7 --match="$prefix*" HEAD 2>/dev/null \ + || git describe --tags --abbrev=7 HEAD 2>/dev/null \ + || git log -1 --pretty=format:'v0-HEAD-%h' 2>/dev/null` \ + && v=`printf '%s\n' "$v" | sed "$tag_sed_script"` \ + && case $v in + $prefix[0-9]*) ;; + *) (exit 1) ;; + esac +then + # Is this a new git that lists number of commits since the last + # tag or the previous older version that did not? + # Newer: v6.10-77-g0f8faeb + # Older: v6.10-g0f8faeb + case $v in + *-*-*) : git describe is okay three part flavor ;; + *-*) + : git describe is older two part flavor + # Recreate the number of commits and rewrite such that the + # result is the same as if we were using the newer version + # of git describe. + vtag=`echo "$v" | sed 's/-.*//'` + commit_list=`git rev-list "$vtag"..HEAD 2>/dev/null` \ + || { commit_list=failed; + echo "$0: WARNING: git rev-list failed" 1>&2; } + numcommits=`echo "$commit_list" | wc -l` + v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`; + test "$commit_list" = failed && v=UNKNOWN + ;; + esac + + v=`echo "$v" | sed 's/-/.r/'`; + v_from_git=1 +else + v=UNKNOWN +fi + +v=`echo "$v" |sed "s/^$prefix//"` + +# Test whether to append the "-dirty" suffix only if the version +# string we're using came from git. I.e., skip the test if it's "UNKNOWN" +# or if it came from .tarball-version. +if test -n "$v_from_git"; then + # Don't declare a version "dirty" merely because a time stamp has changed. + git update-index --refresh > /dev/null 2>&1 + + dirty=`exec 2>/dev/null;git diff-index --name-only HEAD` || dirty= + case "$dirty" in + '') ;; + *) # Append the suffix only if there isn't one already. + case $v in + *-dirty) ;; + *) v="$v-dirty" ;; + esac ;; + esac +fi + +# Omit the trailing newline, so that m4_esyscmd can use the result directly. +echo "$v" | tr -d "$nl" + +# Local variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "scriptversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC" +# time-stamp-end: "; # UTC" +# End: diff --git a/changelog b/changelog index 9cc6c16f..a63821b0 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,66 @@ +unreleased + + * Replace homegrown Makefile with Autotools for configure and build + * Post source builds as part of automated release process + * Switch to versioning scheme to semver + * Replace Travis test runner with GitHub Actions + * Make VCSH_* variables available to hooks + * More carefully handle shell quoting + * Improve handling of non-default remote names and branches + * Fix per-repo pre/post hook handling + * Output path relative to $HOME in ‘vcsh status’ + * Add flag to prefix output of ‘vcsh foreach -p’ with repo name + * Fix debugging on Windows 10 / Cygwin + * Improve option flag handling to allow multiple flags, squash bugs + * Don't require Ruby or Perl tooling to build and install + * Avoid false-positive conflicts on checkout + * Make arg optional for write-ignore subcommand + * Fix ZSH completions, improve Bash completions + * Allow use of specific path when running Git or any dependency + * Fail if hook scripts return failure codes + * Check GIT_REMOTE early on clone() + +2021-04-05 Richard Hartmann + + * Release 1.20190621 + * Make version reflected in `vcsh version` correct... + +2021-03-29 Richard Hartmann + + * Release 1.20190620 + * This is a safe harbour release. + * `release` branch should ensure downstream picks it up + +2021-03-29 Richard Hartmann + + * Release 1.20190619 + * Various bugfixes + * This is a safe harbour release. + * @alerque is now a co-maintainer + +2015-12-29 Richard Hartmann + + * Fix tests to always use C locale, this makes vcsh reproducible + * Implement `vcsh foreach` + * Implement `vcsh list-untracked -a` + * Handle Git older than 2.x gracefully + * Fix broken list-tracked-by + * Fix regression in `vcsh which` + * Skip ignored files in the output of list-untracked + * Improve cloning + * Clean up docs + * Implement `vcsh status --terse` + * Improve tests + +2015-05-02 Richard Hartmann + + * Release 1.20150502 + * Add tests + * Integrate tests with Travis CI + * Make `vcsh --list_untracked` GNU-independent + * Improve vcsh's code quality + * Improve Makefile, documentation, etc + 2014-10-26 Richard Hartmann * Release 1.20141026 @@ -201,7 +264,7 @@ 2011-11-19 Richard Hartmann * Bugfixes - * Improve XDG compability + * Improve XDG compatibility 2011-11-18 Richard Hartmann diff --git a/_vcsh_bash b/completions/vcsh.bash similarity index 64% rename from _vcsh_bash rename to completions/vcsh.bash index 3cfe88a6..47367a54 100644 --- a/_vcsh_bash +++ b/completions/vcsh.bash @@ -1,10 +1,8 @@ -# bash completion for vcsh. - # run git command # based on bash_completion:_command_offset() _vcsh_git_command () { local word_offset=$1 - for (( i=0; i < $word_offset; i++ )); do + for (( i=0; i < word_offset; i++ )); do for (( j=0; j <= ${#COMP_LINE}; j++ )); do [[ "$COMP_LINE" == "${COMP_WORDS[i]}"* ]] && break COMP_LINE=${COMP_LINE:1} @@ -17,16 +15,23 @@ _vcsh_git_command () { ((COMP_POINT+=4)) # shift COMP_WORDS elements and adjust COMP_CWORD - for (( i=1; i <= COMP_CWORD - $word_offset + 1; i++ )); do + for (( i=1; i <= COMP_CWORD - word_offset + 1; i++ )); do COMP_WORDS[i]=${COMP_WORDS[i+$word_offset-1]} done for (( i; i <= COMP_CWORD; i++ )); do unset 'COMP_WORDS[i]' done COMP_WORDS[0]=git - ((COMP_CWORD -= $word_offset - 1)) - - local cspec=$( complete -p git 2>/dev/null ) + ((COMP_CWORD -= word_offset - 1)) + + local cspec + cspec=$( complete -p git 2>/dev/null ) + if [[ -z $cspec ]]; then + if declare -F __load_completion &> /dev/null; then + __load_completion git + cspec=$( complete -p git 2>/dev/null ) + fi + fi if [[ -n $cspec ]]; then if [[ ${cspec#* -F } != $cspec ]]; then local func=${cspec#*-F } @@ -52,13 +57,16 @@ _vcsh_git_command () { } _vcsh () { - local cur prev words cword OPTS + local cur prev words OPTS _init_completion -n = || return - local repos cmds - repos=( $(command vcsh list) ) - cmds="clone delete enter foreach help init list list-tracked list-untracked - pull push rename run status upgrade version which write-gitignore" + local r reponames + local -A repos + mapfile -t reponames < <(command vcsh list) + for r in "${reponames[@]}"; do repos["$r"]="$r"; done + unset r reponames + local cmds + cmds="clone delete enter foreach help init list list-tracked list-untracked pull push rename run status upgrade version which write-gitignore" local subcword cmd subcmd for (( subcword=1; subcword < ${#words[@]}-1; subcword++ )); do @@ -69,7 +77,7 @@ _vcsh () { if [[ -z $cmd ]]; then case $prev in -c) - COMPREPLY=( $(compgen -f -- $cur) ) + mapfile -t COMPREPLY < <(compgen -f -- "$cur") return ;; esac @@ -77,11 +85,11 @@ _vcsh () { case $cur in -*) OPTS='-c -d -h -v' - COMPREPLY=( $(compgen -W "${OPTS[*]}" -- $cur) ) + mapfile -t COMPREPLY < <(compgen -W "$OPTS" -- "$cur") return ;; esac - COMPREPLY=( $(compgen -W "${repos[*]} ${cmds[*]}" -- $cur) ) + mapfile -t COMPREPLY < <(compgen -W "${repos[*]} ${cmds}" -- "$cur") return 0 fi @@ -92,12 +100,12 @@ _vcsh () { list-untracked) [[ $cur == -* ]] && \ - COMPREPLY=( $(compgen -W '-a -r' -- $cur) ) && return + mapfile -t COMPREPLY < <(compgen -W '-a -r' -- "$cur") && return ;;& run) if [[ -n $subcmd && -n "${repos[$subcmd]}" ]]; then - _command_offset $(( $subcword+1 )) + _command_offset $(( subcword+1 )) return fi ;;& @@ -105,7 +113,7 @@ _vcsh () { delete|enter|list-tracked|list-untracked|rename|run|status|upgrade|write-gitignore) # return repos if [[ -z $subcmd ]]; then - COMPREPLY=( $(compgen -W "${repos[*]}" -- $cur) ) + mapfile -t COMPREPLY < <(compgen -W "${repos[*]}" -- "$cur") return fi return @@ -113,13 +121,13 @@ _vcsh () { clone) [[ $cur == -* ]] && \ - COMPREPLY=( $(compgen -W '-b' -- $cur) ) + mapfile -t COMPREPLY < <(compgen -W '-b' -- "$cur") return ;; foreach) [[ $cur == -* ]] \ - && COMPREPLY=( $(compgen -W "-g" -- $cur) ) && return + && mapfile -t COMPREPLY < <(compgen -W "-g" -- "$cur") && return _vcsh_git_command $subcword return ;; @@ -128,11 +136,10 @@ _vcsh () { # git command on repository if [[ -n "${repos[$cmd]}" ]]; then - _vcsh_git_command $subcword + : "${VCSH_REPO_D:=${XDG_CONFIG_HOME:-$HOME/.config}/vcsh/repo.d}" + GIT_DIR="${VCSH_REPO_D}/${cmd}.git" _vcsh_git_command "$subcword" fi return 0 } complete -F _vcsh vcsh - -# vim: ft=sh: diff --git a/_vcsh b/completions/vcsh.zsh similarity index 84% rename from _vcsh rename to completions/vcsh.zsh index 9aca0f97..a3c964db 100644 --- a/_vcsh +++ b/completions/vcsh.zsh @@ -1,10 +1,9 @@ #compdef vcsh function __vcsh_repositories () { - local expl local -a repos - repos=( ${(f)"$(command vcsh list)"} ) - _describe -t repos 'repositories' repos + repos=( ${(f)"$(_call_program repositories vcsh list)"} ) + _describe -t repositories 'repository' repos } function __vcsh_not_implemented_yet () { @@ -56,9 +55,11 @@ function _vcsh-push () { } function _vcsh-rename () { - (( CURRENT == 2 )) && __vcsh_repositories - (( CURRENT == 3 )) && _message "new repository name" - (( CURRENT > 3 )) && _nothing + case $CURRENT in + 2) __vcsh_repositories ;; + 3) _message "new repository name" ;; + *) _nothing ;; + esac } function _vcsh-run () { @@ -93,12 +94,12 @@ function _vcsh-write-gitignore () { } function _vcsh () { - local curcontext="${curcontext}" + local curcontext="${curcontext}" ret=1 local state vcshcommand local -a args subcommands local VCSH_REPO_D - : ${VCSH_REPO_D:="${XDG_CONFIG_HOME:-"$HOME/.config"}/vcsh/repo.d"} + : ${VCSH_REPO_D:="${XDG_CONFIG_HOME:-"$HOME/.config"}/vcsh/repo.d"} subcommands=( "clone:clone an existing repository" @@ -129,25 +130,26 @@ function _vcsh () { '*:: :->subcommand_or_options_or_repo' ) - _arguments -C ${args} && return + _arguments -C ${args} && ret=0 if [[ ${state} == "subcommand_or_options_or_repo" ]]; then if (( CURRENT == 1 )); then - _describe -t subcommands 'vcsh sub-commands' subcommands - __vcsh_repositories + _describe -t subcommands 'vcsh sub-commands' subcommands && ret=0 + __vcsh_repositories && ret=0 else vcshcommand="${words[1]}" if ! (( ${+functions[_vcsh-$vcshcommand]} )); then # There is no handler function, so this is probably the name # of a repository. Act accordingly. # FIXME: this may want to use '_dispatch vcsh git' - GIT_DIR=$VCSH_REPO_D/$words[1].git _dispatch git git + GIT_DIR=$VCSH_REPO_D/$words[1].git _dispatch git git && ret=0 else curcontext="${curcontext%:*:*}:vcsh-${vcshcommand}:" _call_function ret _vcsh-${vcshcommand} && (( ret )) fi fi fi + return ret } _vcsh "$@" diff --git a/configure.ac b/configure.ac new file mode 100644 index 00000000..19b0747d --- /dev/null +++ b/configure.ac @@ -0,0 +1,87 @@ +AC_PREREQ([2.69]) +AC_INIT([vcsh], [m4_esyscmd(build-aux/git-version-gen .tarball-version)]) +AC_CONFIG_AUX_DIR([build-aux]) +AM_INIT_AUTOMAKE([foreign tar-pax dist-xz dist-zip no-dist-gzip color-tests]) +AM_SILENT_RULES([yes]) +AC_CONFIG_MACRO_DIR([build-aux]) + +AC_DEFUN([AX_PROGVAR], [ + test -n "$m4_toupper($1)" || AC_PATH_PROG(m4_toupper($1), m4_default($2,$1)) + test -n "$m4_toupper($1)" || AC_MSG_ERROR([m4_default($2,$1) is required]) + ]) + +AC_PROG_AWK +AC_PROG_GREP +AC_PROG_SED + +AX_PROGVAR([comm]) +AX_PROGVAR([cmp]) +AX_PROGVAR([git]) + +AC_ARG_WITH([man-page], + AS_HELP_STRING([--with-man-page], + [Generate man page @<:@default=yes@:>@]), + [], + [with_man_page=yes]) +if test x"$with_man_page" = x"yes"; then + AX_PROGVAR([ronn]) +fi +AM_CONDITIONAL([ENABLE_MAN_PAGE], + [test x"$with_man_page" != x"no"]) + +AS_IF([test -e .tarball-version], + m4_define([TESTDEF], [yes]), + m4_define([TESTDEF], [no])) +AC_ARG_ENABLE([tests], + AS_HELP_STRING([--disable-tests], + [Configure tooling to run tests @<:@default=TESTDEF@:>@]), + [], + [enable_tests=TESTDEF]) +AM_CONDITIONAL([ENABLE_TESTS],[test x"$enable_tests" != x"no"]) + +AS_IF([test x"$enable_tests" != x"no"], [ + AX_PROGVAR([prove]) + AX_PROG_PERL_MODULES(Shell::Command, [], + AC_MSG_ERROR(Perl module required for testing not found)) + AX_PROG_PERL_MODULES(Test::Most, [], + AC_MSG_ERROR(Perl module required for testing not found)) +]) + +AC_ARG_WITH([bash-completion-dir], + AS_HELP_STRING([--with-bash-completion-dir[=PATH]], + [Install bash auto-completion definitions to a directory. @<:@default=yes@:>@]), + [], + [with_bash_completion_dir=yes]) +AS_IF([test x"$with_bash_completion_dir" = x"yes"], + [PKG_CHECK_MODULES([BASH_COMPLETION], [bash-completion >= 2.0], + [BASH_COMPLETION_DIR="$(pkg-config --define-variable=datadir=$datadir --variable=completionsdir bash-completion)"], + [BASH_COMPLETION_DIR="$datadir/bash-completion/completions"])], + [BASH_COMPLETION_DIR="$with_bash_completion_dir"]) +AC_SUBST([BASH_COMPLETION_DIR]) +AM_CONDITIONAL([ENABLE_BASH_COMPLETION], + [test x"$with_bash_completion_dir" != x"no"]) + +AC_ARG_WITH([zsh-completion-dir], + AS_HELP_STRING([--with-zsh-completion-dir[=PATH]], + [Install zsh auto-completion definitions to a directory. @<:@default=yes@:>@]), + [], + [with_zsh_completion_dir=yes]) +if test x"$with_zsh_completion_dir" = x"yes"; then + ZSH_COMPLETION_DIR="$datadir/zsh/site-functions" +else + ZSH_COMPLETION_DIR="$with_zsh_completion_dir" +fi +AC_SUBST([ZSH_COMPLETION_DIR]) +AM_CONDITIONAL([ENABLE_ZSH_COMPLETION], + [test x"$with_zsh_completion_dir" != x"no"]) + +TRANSFORMED_PACKAGE_NAME="$(printf "$PACKAGE_NAME" | $SED -e "${program_transform_name//\$\$/\$}")" +AC_SUBST([TRANSFORMED_PACKAGE_NAME]) + +AC_CONFIG_FILES([Makefile]) +AC_CONFIG_FILES([vcsh], [chmod +x vcsh]) +AC_CONFIG_FILES([doc/vcsh.1.ronn]) + +AC_ARG_PROGRAM + +AC_OUTPUT diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 65fc5f3b..c2965e63 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -1,51 +1,127 @@ -# Pre-requisites # +# Distro Packages -If you want to build the manpage, you will need [ronn] [1]. -Debian 7.0 and above come with a package, so do most Debian clones. +Many distributions have packages ready to go. +If yours doesn't, you can install [from source](#installing-from-source). +If you package VCSH for a distro please let us know. -To install ronn on your Debian-based system, simply run +## Arch Linux - apt-get install ruby-ronn +Use your favorite AUR helper to build and install the [vcsh](https://aur.archlinux.org/packages/vcsh) package: -There are no other dependencies other than `git`, `ronn` and a POSIX shell. +```console +$ paru -S aur +``` +## CentOS / Fedora / RedHat -# Installing # +```console +$ yum install vcsh +``` - sudo make install +## Debian / Deepin / Kali Linux / Parrot / PureOS / Raspbian / Trisquel / Ubuntu -## Installing without root privileges ## +```console +$ apt install vcsh +``` - make install DESTDIR=/home/myuser/local +## Gentoo / Funtoo / LiGurOS -or simply copy the shell script into any place you like, e.g. `~/bin` +```console +$ emerge --ask dev-vcs/vcsh +``` +## GNU Guix -# Uninstalling # +```console +$ guix install vcsh +``` - sudo make uninstall +## Homebrew (macOS) / Linuxbrew -There is another, more thorough, version. Just make sure you are not running -this when you have installed to an important directory which is empty, -otherwise. +```console +$ brew install vcsh +``` -**THIS WILL DELETE /usr/local IF YOU INSTALLED THERE AND IT BECOMES EMPTY** +## KISS Linux - sudo make purge +```console +$ kiss install vcsh +``` -**THIS WILL DELETE /usr/local IF YOU INSTALLED THERE AND IT BECOMES EMPTY** +## MacPorts (macOS) -This is not in the default behaviour of `make uninstall` for obvious reasons. +```console +$ port install vcsh +``` +## NIX -# Other stuff # +```console +$ nix-env -i vcsh +``` -To clean up the generated manpage, run +## openSUSE - make clean +```console +$ zypper install vcsh +``` -and if you are bored, I suggest +## Pardus - make moo +```console +$ pisi install vcsh +``` + +## Termux + +```console +$ pkg install vcsh +``` + +# Installing from Source + +First you'll want a copy of the source code. +The easiest to use place to get this is the [latest release](https://github.com/RichiH/vcsh/releases/latest) posted on GitHub. +The souree distribution will have a name such as `vcsh-2.0.0.tar.xz`. +Note under each release GitHub also shows a "Source code" link that will download a snapshot of the repository; this is **not** the file you want (unless you want to jump through extra hoops). +The official source release packages are the ones you want. + +Alternatively you may `git clone` the source repository. +Note than some extra tooling will be required over using the regular source releases. +Building from a clone will require a system with GNU Autotools installed; something not needed if using a source package. +Also source releases have prebuilt man pages; to (optionally) build them from a Git clone you will need `ronn`. +Finally building from Git clones will check for extra dependencies needed for testing, although tests can be disabled. +If starting from a clone, run `./bootstrap.sh` once before doing anything below. + +Once you have the source, it's time to let it get aquainted with your system: + +```console +$ ./configure +``` + +This command has *lots* of possible options, but the defaults should suite most use cases. +See `./configure --help` for details if you have special needs. + +Once configured, you can build: + +```console +$ make +``` + +Lastly you'll want to install it somewhere. + +```console +$ make install +``` + +If you need elevated system permissions you may need to use `sudo make install` for this step. +If you don't have such permissions and wish to install to your home directory, something like this might work: + +```console +$ ./configure --prefix=/ +$ make DESTDIR="$HOME" install-exec +``` + +This will install to `~/bin/vcsh`; add `~/bin` to your path to use. [1]: http://rtomayko.github.io/ronn/ diff --git a/doc/PACKAGING.md b/doc/PACKAGING.md deleted file mode 100644 index ecbfa551..00000000 --- a/doc/PACKAGING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Distributions with readily available packages - -## Archlinux - -AUR does not require any packaging information within this repository. - -## Debian - -Debian packages are provided by the author in separate branches, maintained in -the upstream repository - -### Ubuntu - -Ubuntu imports Debian's package automagically. - - -## Mac OS X / Homebrew - -Homebrew does not require any packaging information within this repository. -A separate branch with a statically compiled manpage and release tags is -provided to ease the work of Homebrew packagers: - -* The static manpage because Homebrew lacks ronn -* The tag so GitHub generates tarballs Homebrew can be pointed at - - -# Supporting new distributions - -## Your own work - -If you are maintaining a package for a different distribution, please get -in touch so your work can be included in a packaging branch in the upstream -repository. -This allows others to adapt your work for their own distributions or -packaging needs. - -## Static manpage - -The "debian-squeeze" branch carries a quilt patchset with a pre-compiled -manpage and the "homebrew" one carries a static manpage. - -In case you can not build the manpage because you are missing ronn or you -prefer a precompiled manpage for another reason, please contact us; we will -gladly provide up-to-date packages with every release. diff --git a/doc/README.md b/doc/README.md index b14d782e..8b9b2446 100644 --- a/doc/README.md +++ b/doc/README.md @@ -293,7 +293,7 @@ Note the portage package for myrepos still has the old project name: vcsh is available via this [AUR](https://aur.archlinux.org/packages/vcsh/) package. Likewise myrepos is available [here](https://aur.archlinux.org/packages/myrepos/). -You may install both useing your favorite AUR helper. e.g. with yaourt: +You may install both using your favorite AUR helper. e.g. with yaourt: yaourt -Sya myrepos vcsh diff --git a/doc/vcsh.1.ronn b/doc/vcsh.1.ronn.in similarity index 73% rename from doc/vcsh.1.ronn rename to doc/vcsh.1.ronn.in index 719736b4..cbd4e8db 100644 --- a/doc/vcsh.1.ronn +++ b/doc/vcsh.1.ronn.in @@ -3,68 +3,68 @@ vcsh(1) - Version Control System for $HOME - multiple Git repositories in $HOME ## SYNOPSIS -`vcsh` [] +`@TRANSFORMED_PACKAGE_NAME@` [] -`vcsh` alias [-d] [[=]] +`@TRANSFORMED_PACKAGE_NAME@` alias [-d] [[=]] -`vcsh` clone [-b ] [] +`@TRANSFORMED_PACKAGE_NAME@` clone [-b ] [] -`vcsh` delete +`@TRANSFORMED_PACKAGE_NAME@` delete -`vcsh` enter +`@TRANSFORMED_PACKAGE_NAME@` enter -`vcsh` foreach [-g] +`@TRANSFORMED_PACKAGE_NAME@` foreach [-g] -`vcsh` help +`@TRANSFORMED_PACKAGE_NAME@` help -`vcsh` init +`@TRANSFORMED_PACKAGE_NAME@` init -`vcsh` list +`@TRANSFORMED_PACKAGE_NAME@` list -`vcsh` list-tracked [] +`@TRANSFORMED_PACKAGE_NAME@` list-tracked [] -`vcsh` list-untracked [<-a>] [<-r>] [] +`@TRANSFORMED_PACKAGE_NAME@` list-untracked [<-a>] [<-r>] [] -`vcsh` pull +`@TRANSFORMED_PACKAGE_NAME@` pull -`vcsh` push +`@TRANSFORMED_PACKAGE_NAME@` push -`vcsh` rename +`@TRANSFORMED_PACKAGE_NAME@` rename -`vcsh` run +`@TRANSFORMED_PACKAGE_NAME@` run -`vcsh` status [] +`@TRANSFORMED_PACKAGE_NAME@` status [] -`vcsh` upgrade +`@TRANSFORMED_PACKAGE_NAME@` upgrade -`vcsh` version +`@TRANSFORMED_PACKAGE_NAME@` version -`vcsh` which +`@TRANSFORMED_PACKAGE_NAME@` which -`vcsh` write-gitignore +`@TRANSFORMED_PACKAGE_NAME@` write-gitignore [] -`vcsh` +`@TRANSFORMED_PACKAGE_NAME@` -`vcsh` +`@TRANSFORMED_PACKAGE_NAME@` ## DESCRIPTION -`vcsh` allows you to have several `git`(1) repositories, all maintaining their +`@TRANSFORMED_PACKAGE_NAME@` allows you to have several `git`(1) repositories, all maintaining their working trees in $HOME without clobbering each other. That, in turn, means you can have one repository per config set (zsh, vim, ssh, etc), picking and choosing which configs you want to use on which machine. -`vcsh` is using a technique called fake bare Git repositories, keeping <$GIT_DIR> +`@TRANSFORMED_PACKAGE_NAME@` is using a technique called fake bare Git repositories, keeping <$GIT_DIR> in a different directory from <$GIT_WORK_TREE> which is pointed to <$HOME>. The use of symlinks is not needed in this setup, making for a cleaner setup. -`vcsh` was designed with `mr`(1) in mind so you might want to install it alongside -vcsh. That being said, you can easily use `vcsh` without `mr` if you prefer. +`@TRANSFORMED_PACKAGE_NAME@` was designed with `mr`(1) in mind so you might want to install it alongside +vcsh. That being said, you can easily use `@TRANSFORMED_PACKAGE_NAME@` without `mr` if you prefer. -A sample configuration for `vcsh` and `mr` can be found at -*https://github.com/RichiH/vcsh_mr_template* and used with `vcsh clone +A sample configuration for `@TRANSFORMED_PACKAGE_NAME@` and `mr` can be found at +*https://github.com/RichiH/vcsh_mr_template* and used with `@TRANSFORMED_PACKAGE_NAME@ clone https://github.com/RichiH/vcsh_mr_template mr`. Please note that you can always use a path instead of a name for . @@ -163,7 +163,7 @@ an interactive user. Please note that there is a somewhat magic feature for run. Instead of it accepts , as well. Anything that has a slash in it will be assumed to - be a path. `vcsh run` will then operate on this directory instead of the one + be a path. `@TRANSFORMED_PACKAGE_NAME@ run` will then operate on this directory instead of the one normally generated from the repository's name. This is needed to support mr and other scripts properly and of no concern to an interactive user. @@ -181,25 +181,26 @@ an interactive user. Find in name of any tracked file. * write-gitignore: - Write .gitignore.d/ via `git ls-files`. + Write .gitignore.d/ via `git ls-files`. If is not specified but + we're withing a vcsh repository, use that. * : Shortcut to run `git` commands on a repo. Will prepend `git` to . * : - Shortcut to run `vcsh enter `. + Shortcut to run `@TRANSFORMED_PACKAGE_NAME@ enter `. ## ENVIRONMENT -As noted earlier, `vcsh` will set <$GIT_DIR> and <$GIT_WORK_TREE> to the +As noted earlier, `@TRANSFORMED_PACKAGE_NAME@` will set <$GIT_DIR> and <$GIT_WORK_TREE> to the appropriate values for fake bare Git repositories. ## ALIASES -`vcsh' allows to define aliases. The first existing file +`@TRANSFORMED_PACKAGE_NAME@` allows to define aliases. The first existing file -* $XDG_CONFIG_HOME/vcsh/aliases -* /etc/vcsh/aliases +* <$XDG_CONFIG_HOME/vcsh/aliases> +* will be read. An alias definition has the format `alias = command`. Empty lines and lines starting with '#' are ignored. @@ -207,13 +208,13 @@ Empty lines and lines starting with '#' are ignored. ## CONFIG -There are several ways to turn the various knobs on `vcsh`. In order of +There are several ways to turn the various knobs on `@TRANSFORMED_PACKAGE_NAME@`. In order of ascending precedence, they are: -* `VARIABLE=foo vcsh` +* `VARIABLE=foo @TRANSFORMED_PACKAGE_NAME@` * * <$XDG_CONFIG_HOME/vcsh/config> -* `vcsh -c ` +* `@TRANSFORMED_PACKAGE_NAME@ -c ` Please note that those files are sourced. Any and all commands will be executed in the context of your shell. @@ -242,7 +243,7 @@ Interesting knobs you can turn: Defaults to . -* <$VCSH_VCSH_WORKTREE>: +* <$VCSH_WORKTREE>: Can be , or . will set an absolute path; defaulting to <$HOME>. @@ -280,9 +281,9 @@ Less interesting knobs you could turn: ## HOOK SYSTEM -`vcsh` provides a hook system. Hook scripts must be executable and should be +`@TRANSFORMED_PACKAGE_NAME@` provides a hook system. Hook scripts must be executable and should be placed in <$XDG_CONFIG_HOME/vcsh/hooks-available>. From there, they can be -soft-linked into <$XDG_CONFIG_HOME/vcsh/hooks-enabled>; `vcsh` will only +soft-linked into <$XDG_CONFIG_HOME/vcsh/hooks-enabled>; `@TRANSFORMED_PACKAGE_NAME@` will only execute hooks that are in this directory. Hooks follow a simple format. will be run before anything is run. @@ -305,7 +306,7 @@ we can ship them by default. ## OVERLAY SYSTEM -`vcsh` also provides an overlay system. Similar to hooks, the recommended +`@TRANSFORMED_PACKAGE_NAME@` also provides an overlay system. Similar to hooks, the recommended locations are <$XDG_CONFIG_HOME/vcsh/overlays-available> and <$XDG_CONFIG_HOME/vcsh/overlays-enabled>. @@ -335,8 +336,8 @@ On Debian-based systems, this file can be found in . ## SECURITY CONSIDERATIONS -`vcsh` allows you to execute arbitrary commands via `vcsh run`. For example, -adding a `sudo`(8) rule for `vcsh` would be pretty stupid. +`@TRANSFORMED_PACKAGE_NAME@` allows you to execute arbitrary commands via `@TRANSFORMED_PACKAGE_NAME@ run`. For example, +adding a `sudo`(8) rule for `@TRANSFORMED_PACKAGE_NAME@` would be pretty stupid. Additionally, vcsh will source, i.e. execute, all files listed in . You can put any and all commands into these config files and they will be @@ -360,13 +361,13 @@ config files, all of which were soft-linked into <$HOME>. Martin F. Krafft aka madduck came up with the concept of fake bare Git repositories. -vcsh was initally written by madduck. This version is a re-implementation from +vcsh was initially written by madduck. This version is a re-implementation from scratch with a lot more features. madduck graciously agreed to let the author take over the name. ## AUTHOR -This manpage and `vcsh` itself were written by Richard "RichiH" Hartmann. +This manpage and `@TRANSFORMED_PACKAGE_NAME@` itself were written by Richard "RichiH" Hartmann. ## COPYRIGHT diff --git a/t/001-setup-env.t b/t/001-setup-env.t index fb59f058..b50313b9 100644 --- a/t/001-setup-env.t +++ b/t/001-setup-env.t @@ -3,6 +3,7 @@ use strict; use warnings; +use Cwd 'abs_path'; use Test::Most; system ("mkdir -p t/etc"); @@ -16,4 +17,10 @@ chdir 't/etc/' or die $!; system ("ln -s '../../vcsh'"); ok !$?; +$ENV{'HOME'} = abs_path ('.vcsh_home'); +$ENV{'XDG_CONFIG_HOME'} = $ENV{'HOME'}.'/.config'; + +system ("git config --global init.defaultBranch test"); +ok !$?; + done_testing; diff --git a/t/100-init.t b/t/100-init.t index 15ce922f..c8984281 100644 --- a/t/100-init.t +++ b/t/100-init.t @@ -11,6 +11,7 @@ use Test::Most; chdir 't/etc/' or die $!; $ENV{'HOME'} = abs_path ('.vcsh_home'); +$ENV{'XDG_CONFIG_HOME'} = $ENV{'HOME'}.'/.config'; my $output = `./vcsh status`; diff --git a/t/300-add.t b/t/300-add.t index 4acd0a2c..c7e9d2f4 100644 --- a/t/300-add.t +++ b/t/300-add.t @@ -11,6 +11,7 @@ use Test::Most; chdir 't/etc/' or die $!; $ENV{'HOME'} = abs_path ('.vcsh_home'); +$ENV{'XDG_CONFIG_HOME'} = $ENV{'HOME'}.'/.config'; chdir '.vcsh_home' or die $!; @@ -25,14 +26,14 @@ system (".././vcsh test1 add 'a'"); my $output = `.././vcsh status`; ok $output eq "test1: -A a +A ~/a ", 'Adding a file works'; $output = `.././vcsh status --terse`; ok $output eq "test1: -A a +A ~/a ", 'Terse output works'; done_testing; diff --git a/t/950-delete.t b/t/950-delete.t index cd078714..012586b7 100644 --- a/t/950-delete.t +++ b/t/950-delete.t @@ -9,6 +9,7 @@ use Test::Most; chdir 't/etc/' or die $!; $ENV{'HOME'} = abs_path ('.vcsh_home'); +$ENV{'XDG_CONFIG_HOME'} = $ENV{'HOME'}.'/.config'; system ("echo 'Yes, do as I say' | ./vcsh delete test1"); diff --git a/tools/hooks/pre-commit b/tools/hooks/pre-commit deleted file mode 100755 index ddb0550d..00000000 --- a/tools/hooks/pre-commit +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# Unfortunately, Git decided to set those two during pre-commit -unset GIT_DIR -unset GIT_INDEX_FILE - -prove diff --git a/tools/list_CONTRIBUTORS b/tools/list_CONTRIBUTORS deleted file mode 100755 index 1b88451d..00000000 --- a/tools/list_CONTRIBUTORS +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# This program is licensed under the GNU GPL version 2 or later. -# (c) Richard "RichiH" Hartmann , 2012-2014 -# For details, see LICENSE. To submit patches, you have to agree to -# license your code under the GNU GPL version 2 or later. - - -echo 'Alphabetical list of surnames of everyone who ever committed to this repository. -Auto-generated from tools/list_CONTRIBUTORS. -' -git shortlog -se --all | cut -f1 --complement | sort -u -k2 diff --git a/vcsh b/vcsh.in similarity index 62% rename from vcsh rename to vcsh.in index 9d6dd879..da923db7 100755 --- a/vcsh +++ b/vcsh.in @@ -1,4 +1,4 @@ -#!/bin/sh +#!@SHELL@ # This program is licensed under the GNU GPL version 2 or later. # (c) Richard "RichiH" Hartmann , 2011-2015 @@ -16,43 +16,52 @@ [ -n "$VCSH_DEBUG" ] && set -vx -# If '.git-HEAD' is appended to the version, you are seeing an unreleased -# version of vcsh; the master branch is supposed to be clean at all times +# If '.r-g' is appended to the version, you are seeing an unreleased +# version of vcsh; the main branch is supposed to be clean at all times # so you can most likely just use it nonetheless -VERSION='1.20141026' -SELF=$(basename $0) +VCSH_VERSION='@VERSION@'; export VCSH_VERSION +VCSH_SELF="@TRANSFORMED_PACKAGE_NAME@"; export VCSH_SELF # Ensure all files created are accessible only to the current user. umask 0077 fatal() { - echo "$SELF: fatal: $1" >&2 - [ -z $2 ] && exit 1 - exit $2 + echo "$VCSH_SELF: fatal: $1" >&2 + [ -z "$2" ] && exit 1 + exit "$2" } # We need to run getops as soon as possible so we catch -d and other # options that will modify our behaviour. # Commands are handled at the end of this script. -while getopts "c:dv" flag; do - if [ x"$1" = x'-d' ] || [ x"$1" = x'--debug' ]; then - set -vx - VCSH_DEBUG=1 - echo "debug mode on" - echo "$SELF $VERSION" - elif [ x"$1" = x'-v' ]; then - VCSH_VERBOSE=1 - echo "verbose mode on" - echo "$SELF $VERSION" - elif [ x"$1" = x'-c' ]; then - VCSH_OPTION_CONFIG=$OPTARG - fi - shift 1 +# shellcheck disable=SC2220 +while getopts c:dv flag; do + case "$flag" in + d) + set -vx + VCSH_DEBUG=1 + export VCSH_DEBUG + echo 'debug mode on' + echo "$VCSH_SELF $VCSH_VERSION" + ;; + v) + VCSH_VERBOSE=1 + export VCSH_VERBOSE + echo 'verbose mode on' + echo "$VCSH_SELF $VCSH_VERSION" + ;; + c) + VCSH_OPTION_CONFIG="$OPTARG" + export VCSH_OPTION_CONFIG + ;; + esac done +shift $((OPTIND-1)) source_all() { # Source file even if it's in $PWD and does not have any slashes in it - case $1 in + # shellcheck source=/dev/null + case "$1" in */*) . "$1";; *) . "$PWD/$1";; esac; @@ -61,10 +70,12 @@ source_all() { # Read configuration and set defaults if anything's not set [ -n "$VCSH_DEBUG" ] && set -vx -: ${XDG_CONFIG_HOME:="$HOME/.config"} +: "${XDG_CONFIG_HOME:="$HOME/.config"}" # Read configuration files if there are any +# shellcheck source=/dev/null [ -r "/etc/vcsh/config" ] && . "/etc/vcsh/config" +# shellcheck source=/dev/null [ -r "$XDG_CONFIG_HOME/vcsh/config" ] && . "$XDG_CONFIG_HOME/vcsh/config" if [ -n "$VCSH_OPTION_CONFIG" ]; then # Source $VCSH_OPTION_CONFIG if it can be read and is in $PWD of $PATH @@ -77,13 +88,13 @@ fi [ -n "$VCSH_DEBUG" ] && set -vx # Read defaults -: ${VCSH_REPO_D:="$XDG_CONFIG_HOME/vcsh/repo.d"} -: ${VCSH_HOOK_D:="$XDG_CONFIG_HOME/vcsh/hooks-enabled"} -: ${VCSH_OVERLAY_D:="$XDG_CONFIG_HOME/vcsh/overlays-enabled"} -: ${VCSH_BASE:="$HOME"} -: ${VCSH_GITIGNORE:=exact} -: ${VCSH_GITATTRIBUTES:=none} -: ${VCSH_WORKTREE:=absolute} +: "${VCSH_REPO_D:="$XDG_CONFIG_HOME/vcsh/repo.d"}"; export VCSH_REPO_D +: "${VCSH_HOOK_D:="$XDG_CONFIG_HOME/vcsh/hooks-enabled"}"; export VCSH_HOOK_D +: "${VCSH_OVERLAY_D:="$XDG_CONFIG_HOME/vcsh/overlays-enabled"}"; export VCSH_OVERLAY_D +: "${VCSH_BASE:="$HOME"}"; export VCSH_BASE +: "${VCSH_GITIGNORE:=exact}"; export VCSH_GITIGNORE +: "${VCSH_GITATTRIBUTES:=none}"; export VCSH_GITATTRIBUTES +: "${VCSH_WORKTREE:=absolute}"; export VCSH_WORKTREE if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1 @@ -94,8 +105,9 @@ if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ] && [ ! "x$VCSH_WORKTREE" = 'xrelative' fi +# editorconfig-checker-disable help() { - echo "usage: $SELF + echo "usage: $VCSH_SELF options: -c Source file @@ -111,7 +123,8 @@ help() { delete Delete an existing repository enter Enter repository; spawn new instance of \$SHELL with \$GIT_DIR set. - foreach [<-g>] + foreach \\ + [<-g>] [<-p>] \\ Execute a command for every repository help Display this help text init Initialize a new repository @@ -133,39 +146,41 @@ help() { version Print version information which Find substring in name of any tracked file write-gitignore \\ - Write .gitignore.d/ via git ls-files + [] Write .gitignore.d/ via git ls-files Shortcut to run git commands directly Shortcut to enter repository" >&2 } +# editorconfig-checker-enable debug() { - [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@" + if [ -n "$VCSH_DEBUG" ]; then echo "$VCSH_SELF: debug: $*"; fi } verbose() { - if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi + if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then + echo "$VCSH_SELF: verbose: $*" + fi } error() { - echo "$SELF: error: $1" >&2 + echo "$VCSH_SELF: error: $1" >&2 } info() { - echo "$SELF: info: $1" + echo "$VCSH_SELF: info: $1" } alias_read() { - local aliases - if [ -r "$XDG_CONFIG_HOME/vcsh/aliases" ]; then - aliases="$XDG_CONFIG_HOME/vcsh/aliases" - elif [ -r /etc/vcsh/aliases ]; then - aliases=/etc/vcsh/aliases + local aliases + if [ -r "$XDG_CONFIG_HOME/vcsh/aliases" ]; then + aliases="$XDG_CONFIG_HOME/vcsh/aliases" + elif [ -r /etc/vcsh/aliases ]; then + aliases=/etc/vcsh/aliases else return - fi - - sed -r -e 's/#.*//' -ne 's/(\w+)\s*=\s*(.+)/\1 \2/p' "$aliases" + fi + sed -r -e 's/#.*//' -ne 's/(\w+)\s*=\s*(.+)/\1 \2/p' "$aliases" } alias_get() { @@ -230,32 +245,29 @@ aliases() { clone() { hook pre-clone + # Check if remote is reachable. Abort early if there's a typo, TLS certificate problem, etc + @GIT@ ls-remote "$GIT_REMOTE" 2> /dev/null || fatal "Can not reach '$GIT_REMOTE'" init - git remote add origin "$GIT_REMOTE" - git checkout -b "$VCSH_BRANCH" || return $? - git config branch."$VCSH_BRANCH".remote origin - git config branch."$VCSH_BRANCH".merge refs/heads/"$VCSH_BRANCH" - if [ $(git ls-remote origin "$VCSH_BRANCH" 2> /dev/null | wc -l ) -lt 1 ]; then + @GIT@ remote add origin "$GIT_REMOTE" + @GIT@ checkout -b "$VCSH_BRANCH" || return $? + @GIT@ config branch."$VCSH_BRANCH".remote origin + @GIT@ config branch."$VCSH_BRANCH".merge refs/heads/"$VCSH_BRANCH" + if [ "$(@GIT@ ls-remote origin "$VCSH_BRANCH" 2> /dev/null | wc -l )" -lt 1 ]; then info "remote is empty, not merging anything. - You should add files to your new repository." + You should add files to your new repository." # editorconfig-checker-disable-line exit fi - GIT_VERSION_MAJOR=$(git --version | sed -E -n 's/.* ([0-9]+)\..*/\1/p' ) + GIT_VERSION_MAJOR=$(@GIT@ --version | @SED@ -E -n 's/.* ([0-9]+)\..*/\1/p' ) if [ 1 -lt "$GIT_VERSION_MAJOR" ];then - git fetch origin "$VCSH_BRANCH" + @GIT@ fetch origin "$VCSH_BRANCH" else - git fetch origin + @GIT@ fetch origin fi hook pre-merge - git ls-tree -r --name-only origin/"$VCSH_BRANCH" | (while read object; do - [ -e "$object" ] && - error "'$object' exists." && - VCSH_CONFLICT=1 - done - [ x"$VCSH_CONFLICT" = x'1' ]) && - fatal "will stop after fetching and not try to merge! - Once this situation has been resolved, run 'vcsh $VCSH_REPO_NAME pull' to finish cloning." 17 - git -c merge.ff=true merge origin/"$VCSH_BRANCH" + @GIT@ read-tree -n -mu origin/"$VCSH_BRANCH" \ + || fatal "will stop after fetching and not try to merge! + Once this situation has been resolved, run 'vcsh $VCSH_REPO_NAME pull' to finish cloning." 17 # editorconfig-checker-disable-line + @GIT@ -c merge.ff=true merge origin/"$VCSH_BRANCH" hook post-merge hook post-clone retire @@ -263,34 +275,37 @@ clone() { } commit() { - hook pre-commit - shift # remove the "commit" command. + hook_global pre-commit + shift # remove the "commit" command. for VCSH_REPO_NAME in $(list); do + export VCSH_REPO_NAME echo "$VCSH_REPO_NAME: " GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR use - git commit --untracked-files=no --quiet "$@" + hook_repo pre-commit + @GIT@ commit --untracked-files=no --quiet "$@" + hook_repo post-commit VCSH_COMMAND_RETURN_CODE=$? echo done - hook post-commit + hook_global post-commit } delete() { cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11 use info "This operation WILL DESTROY DATA!" - files=$(git ls-files) + files=$(@GIT@ ls-files) echo "These files will be deleted: $files AGAIN, THIS WILL DELETE YOUR DATA! To continue, type 'Yes, do as I say'" - read answer + read -r answer [ "x$answer" = 'xYes, do as I say' ] || exit 16 for file in $files; do - rm -f $file || info "could not delete '$file', continuing with deletion" + rm -f "$file" || info "could not delete '$file', continuing with deletion" done rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'" } @@ -303,57 +318,87 @@ enter() { } foreach() { - hook pre-foreach + hook_global pre-foreach # We default to prefixing `git` to all commands passed to foreach, but # allow running in general context with -g - command_prefix=git - while getopts "g" flag; do - if [ x"$1" = x'-g' ]; then - unset command_prefix - fi - shift 1 + command_prefix=@GIT@ + # shellcheck disable=SC2220 + while getopts gp flag; do + case "$flag" in + g) unset command_prefix ;; + p) VCSH_PRINT_REPO_PREFIX=1 ;; + esac done + export VCSH_PRINT_REPO_PREFIX + shift $((OPTIND-1)) for VCSH_REPO_NAME in $(list); do - echo "$VCSH_REPO_NAME:" + export VCSH_REPO_NAME GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR use - $command_prefix "$@" + hook_repo pre-foreach + if [ -n "${VCSH_PRINT_REPO_PREFIX+x}" ]; then + $command_prefix "$@" | @SED@ "s/^/$VCSH_REPO_NAME: /" + else + echo "$VCSH_REPO_NAME:" + $command_prefix "$@" + fi + hook_repo post-foreach done - hook post-foreach + hook_global post-foreach } git_dir_exists() { [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12 } -hook() { - for hook in "$VCSH_HOOK_D/$1"* "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do +hook_global() { + for hook in "$VCSH_HOOK_D/$1"*; do [ -x "$hook" ] || continue verbose "executing '$hook'" "$hook" done } +hook_repo() { + for hook in "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do + [ -x "$hook" ] || continue + verbose "executing '$hook'" + "$hook" || fatal "hook [$hook] failed" 1 + done +} + +hook() { + hook_global "$1" + hook_repo "$1" +} + init() { hook pre-init [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10 mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50 cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11 - git init --shared=false + @GIT@ init --shared=false upgrade hook post-init } list() { for repo in "$VCSH_REPO_D"/*.git; do - [ -d "$repo" ] && [ -r "$repo" ] && echo "$(basename "$repo" .git)" + [ -d "$repo" ] && [ -r "$repo" ] && basename "$repo" .git + done +} + +list_has_remote() { + for VCSH_REPO_NAME in $(list); do + GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR + @GIT@ config branch."$VCSH_BRANCH".remote > /dev/null && echo "$VCSH_REPO_NAME" done } get_files() { GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR - git ls-files --full-name + @GIT@ ls-files --full-name } list_tracked() { @@ -368,16 +413,14 @@ list_tracked() { } list_tracked_helper() { - sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | sed 's/[,\&]/\\&/g')," | sort -u + @SED@ "s,^,$(printf '%s\n' "$VCSH_BASE/" | @SED@ 's/[,\&]/\\&/g')," | sort -u } list_tracked_by() { - list_tracked '' $2 + list_tracked '' "$2" } list_untracked() { - command -v 'comm' >/dev/null 2>&1 || fatal "Could not find 'comm'" - temp_file_others=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file' temp_file_untracked=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file' temp_file_untracked_copy=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file' @@ -398,59 +441,65 @@ list_untracked() { VCSH_REPO_NAME=$1; export VCSH_REPO_NAME if [ -n "$VCSH_REPO_NAME" ]; then - list_untracked_helper $VCSH_REPO_NAME + list_untracked_helper "$VCSH_REPO_NAME" else for VCSH_REPO_NAME in $(list); do - list_untracked_helper $VCSH_REPO_NAME + list_untracked_helper "$VCSH_REPO_NAME" done fi - cat $temp_file_untracked + cat "$temp_file_untracked" unset directory_opt directory_component - rm -f $temp_file_others $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not delete temp files' + rm -f "$temp_file_others" "$temp_file_untracked" "$temp_file_untracked_copy" || fatal 'Could not delete temp files' } list_untracked_helper() { export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git" - git ls-files --others $exclude_standard_opt "$directory_opt" | ( - while read line; do + @GIT@ ls-files --others $exclude_standard_opt "$directory_opt" | ( + while read -r line; do echo "$line" directory_component=${line%%/*} [ -d "$directory_component" ] && printf '%s/\n' "$directory_component" done - ) | sort -u > $temp_file_others + ) | sort -u > "$temp_file_others" if [ -z "$ran_once" ]; then ran_once=1 - cp $temp_file_others $temp_file_untracked || fatal 'Could not copy temp file' + cp "$temp_file_others" "$temp_file_untracked" || fatal 'Could not copy temp file' fi - cp $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not copy temp file' - comm -12 $temp_file_others $temp_file_untracked_copy > $temp_file_untracked + cp "$temp_file_untracked" "$temp_file_untracked_copy" || fatal 'Could not copy temp file' + @COMM@ -12 "$temp_file_others" "$temp_file_untracked_copy" > "$temp_file_untracked" } pull() { - hook pre-pull - for VCSH_REPO_NAME in $(list); do + hook_global pre-pull + for VCSH_REPO_NAME in $(list_has_remote); do + export VCSH_REPO_NAME printf '%s: ' "$VCSH_REPO_NAME" GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR use - git pull + hook_repo pre-pull + @GIT@ pull + hook_repo post-pull VCSH_COMMAND_RETURN_CODE=$? echo done - hook post-pull + hook_global post-pull } push() { - hook pre-push - for VCSH_REPO_NAME in $(list); do + hook_global pre-push + for VCSH_REPO_NAME in $(list_has_remote); do + export VCSH_REPO_NAME printf '%s: ' "$VCSH_REPO_NAME" GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR use - git push + hook_repo pre-push + @GIT@ push + hook_repo post-push VCSH_COMMAND_RETURN_CODE=$? echo done - hook post-push + hook_global post-push } @@ -484,28 +533,33 @@ status() { COLORING="-c color.status=always" fi if [ -n "$VCSH_REPO_NAME" ]; then - status_helper $VCSH_REPO_NAME + status_helper "$VCSH_REPO_NAME" else for VCSH_REPO_NAME in $(list); do - STATUS=$(status_helper $VCSH_REPO_NAME "$COLORING") - [ -n "$STATUS" -o -z "$VCSH_STATUS_TERSE" ] && echo "$VCSH_REPO_NAME:" + STATUS="$(status_helper "$VCSH_REPO_NAME" "$COLORING")" + [ -n "$STATUS" ] || [ -z "$VCSH_STATUS_TERSE" ] && echo "$VCSH_REPO_NAME:" [ -n "$STATUS" ] && echo "$STATUS" [ -z "$VCSH_STATUS_TERSE" ] && echo done fi } +# Warning: disabled quoting check here due to all of them relating to +# wc -l output and a deliberate lack of quoting for git args. +# shellcheck disable=SC2086 status_helper() { GIT_DIR=$VCSH_REPO_D/$1.git; export GIT_DIR - VCSH_GIT_OPTIONS=$2 + VCSH_GIT_OPTIONS="-c status.relativePaths=false $2" use - remote_tracking_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2> /dev/null) && { - commits_behind=$(git log ..${remote_tracking_branch} --oneline | wc -l) - commits_ahead=$(git log ${remote_tracking_branch}.. --oneline | wc -l) + # Shellcheck isn't understanding a complex block. + # shellcheck disable=SC1083 + remote_tracking_branch=$(@GIT@ rev-parse --abbrev-ref --symbolic-full-name @{u} 2> /dev/null) && { + commits_behind=$(@GIT@ log ..${remote_tracking_branch} --oneline | wc -l) + commits_ahead=$(@GIT@ log ${remote_tracking_branch}.. --oneline | wc -l) [ ${commits_behind} -ne 0 ] && echo "Behind $remote_tracking_branch by $commits_behind commits" [ ${commits_ahead} -ne 0 ] && echo "Ahead of $remote_tracking_branch by $commits_ahead commits" } - git ${VCSH_GIT_OPTIONS} status --short --untracked-files='no' + @GIT@ ${VCSH_GIT_OPTIONS} status --short --untracked-files='no' | @SED@ -E 's@([^ ] +)@\1~/@' VCSH_COMMAND_RETURN_CODE=$? } @@ -514,20 +568,20 @@ upgrade() { # fake-bare repositories are not bare, actually. Set this to false # because otherwise Git complains "fatal: core.bare and core.worktree # do not make sense" - git config core.bare false + @GIT@ config core.bare false # core.worktree may be absolute or relative to $GIT_DIR, depending on # user preference if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then - git config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE git rev-parse --show-cdup)" + @GIT@ config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE @GIT@ rev-parse --show-cdup)" elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then - git config core.worktree "$VCSH_BASE" + @GIT@ config core.worktree "$VCSH_BASE" fi - [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME" - [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && git config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME" - git config vcsh.vcsh 'true' + [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && @GIT@ config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME" + [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && @GIT@ config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME" + @GIT@ config vcsh.vcsh 'true' use - [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" - [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" + [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && @GIT@ add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" + [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && @GIT@ add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" hook post-upgrade } @@ -537,8 +591,10 @@ use() { } which() { + # It's ok to modify VCSH_REPO_NAME in a subshell. + # shellcheck disable=SC2030 output=$(for VCSH_REPO_NAME in $(list); do - get_files | grep -- "$VCSH_COMMAND_PARAMETER" | sed "s/^/$VCSH_REPO_NAME: /" + get_files | @GREP@ -- "$VCSH_COMMAND_PARAMETER" | @SED@ "s/^/$VCSH_REPO_NAME: /" done | sort -u) if [ -z "$output" ]; then fatal "'$VCSH_COMMAND_PARAMETER' does not exist" 1 @@ -547,6 +603,8 @@ which() { fi } +# Ignore warnings about VCSH_REPO_NAME being changed in a subshell. +# shellcheck disable=SC2031 write_gitignore() { # Don't do anything if the user does not want to write gitignore if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then @@ -556,13 +614,17 @@ write_gitignore() { use cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11 - local GIT_VERSION="$(git --version)" - local GIT_VERSION_MAJOR=$(echo $GIT_VERSION | sed -E -n 's/.* ([0-9]+)\..*/\1/p') - local GIT_VERSION_MINOR=$(echo $GIT_VERSION | sed -E -n 's/.* ([0-9]+)\.([0-9]+)\..*/\2/p') + # Works in all shells we care about. + # shellcheck disable=SC2039,SC3043 + local GIT_VERSION GIT_VERSION_MAJOR GIT_VERSION_MINOR + GIT_VERSION="$(@GIT@ --version)" + GIT_VERSION_MAJOR="$(echo "$GIT_VERSION" | @SED@ -E -n 's/.* ([0-9]+)\..*/\1/p')" + GIT_VERSION_MINOR="$(echo "$GIT_VERSION" | @SED@ -E -n 's/.* ([0-9]+)\.([0-9]+)\..*/\2/p')" OLDIFS=$IFS IFS=$(printf '\n\t') - gitignores=$(for file in $(git ls-files); do - if [ $GIT_VERSION_MAJOR -ge 2 -a $GIT_VERSION_MINOR -ge 7 ]; then + gitignores=$(for file in $(@GIT@ ls-files); do + if [ "$GIT_VERSION_MAJOR" -ge 2 ] \ + && [ "$GIT_VERSION_MINOR" -ge 7 ]; then echo "$file"; else while true; do @@ -580,9 +642,9 @@ write_gitignore() { echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57 for gitignore in $gitignores; do - echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57 + echo "$gitignore" | @SED@ 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then - { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; } + { echo "$gitignore/*" | @SED@ 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; } fi done IFS=$OLDIFS @@ -599,7 +661,7 @@ write_gitignore() { fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53 } -debug $(git version) +debug "$(@GIT@ version)" if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1 @@ -607,26 +669,26 @@ fi VCSH_COMMAND=$1; export VCSH_COMMAND -alias=$(alias_get $VCSH_COMMAND) +alias="$(alias_get $VCSH_COMMAND)" if [ -n "$alias" ]; then VCSH_COMMAND="$alias" else case $VCSH_COMMAND in - clon|clo|cl) old_alias=1 ;; - commi|comm|com|co) old_alias=1 ;; - delet|dele|del|de) old_alias=1 ;; - ente|ent|en) old_alias=1 ;; - hel|he) old_alias=1 ;; - ini|in) old_alias=1 ;; - pul) old_alias=1 ;; - pus) old_alias=1 ;; - renam|rena|ren|re) old_alias=1 ;; - ru) old_alias=1 ;; - statu|stat|sta|st) old_alias=1 ;; - upgrad|upgra|upgr|up) old_alias=1 ;; - versio|versi|vers|ver|ve) old_alias=1 ;; - which|whi|wh) old_alias=1 ;; - write|writ|wri|wr) old_alias=1 ;; + clon|clo|cl) old_alias=1;; + commi|comm|com|co|ci) old_alias=1;; + delet|dele|del|de) old_alias=1;; + ente|ent|en) old_alias=1;; + hel|he) old_alias=1;; + ini|in) old_alias=1;; + pul) old_alias=1;; + pus) old_alias=1;; + renam|rena|ren|re) old_alias=1;; + ru) old_alias=1;; + statu|stat|sta|st) old_alias=1;; + upgrad|upgra|upgr|up) old_alias=1;; + versio|versi|vers|ver|ve) old_alias=1;; + which|whi|wh) old_alias=1;; + write|writ|wri|wr) old_alias=1;; esac if [ -n "$old_alias" ]; then echo "Aliases now dynamically defined. See vcsh(1) for details." @@ -658,39 +720,44 @@ if [ x"$VCSH_COMMAND" = x'clone' ]; then export VCSH_REPO_NAME [ -n "$VCSH_BRANCH" ] || VCSH_BRANCH=master GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR +elif [ "$VCSH_COMMAND" = 'help' ]; then + help && exit elif [ "$VCSH_COMMAND" = 'version' ]; then - echo "$SELF $VERSION" - git version + echo "$VCSH_SELF $VCSH_VERSION" + @GIT@ version exit elif [ x"$VCSH_COMMAND" = x'which' ]; then [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1 [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1 VCSH_COMMAND_PARAMETER=$2; export VCSH_COMMAND_PARAMETER elif [ x"$VCSH_COMMAND" = x'delete' ] || - [ x"$VCSH_COMMAND" = x'enter' ] || - [ x"$VCSH_COMMAND" = x'init' ] || - [ x"$VCSH_COMMAND" = x'list-tracked-by' ] || - [ x"$VCSH_COMMAND" = x'rename' ] || - [ x"$VCSH_COMMAND" = x'run' ] || - [ x"$VCSH_COMMAND" = x'upgrade' ] || - [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then - [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify repository to work on" 1 + [ x"$VCSH_COMMAND" = x'enter' ] || + [ x"$VCSH_COMMAND" = x'init' ] || + [ x"$VCSH_COMMAND" = x'list-tracked-by' ] || + [ x"$VCSH_COMMAND" = x'rename' ] || + [ x"$VCSH_COMMAND" = x'run' ] || + [ x"$VCSH_COMMAND" = x'upgrade' ] || + [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then + if [ -z "$2" ]; then + [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: please specify repository to work on" 1 + else + VCSH_REPO_NAME=$2; export VCSH_REPO_NAME + fi + GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR [ x"$VCSH_COMMAND" = x'rename' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1 [ x"$VCSH_COMMAND" = x'run' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1 - VCSH_REPO_NAME=$2; export VCSH_REPO_NAME - GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR [ x"$VCSH_COMMAND" = x'rename' ] && { VCSH_REPO_NAME_NEW=$3; export VCSH_REPO_NAME_NEW; - GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; } + GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; } [ x"$VCSH_COMMAND" = x'run' ] && shift 2 elif [ x"$VCSH_COMMAND" = x'foreach' ]; then [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a command" 1 shift 1 elif [ x"$VCSH_COMMAND" = x'commit' ] || - [ x"$VCSH_COMMAND" = x'list' ] || - [ x"$VCSH_COMMAND" = x'list-tracked' ] || - [ x"$VCSH_COMMAND" = x'list-untracked' ] || - [ x"$VCSH_COMMAND" = x'pull' ] || - [ x"$VCSH_COMMAND" = x'push' ]; then + [ x"$VCSH_COMMAND" = x'list' ] || + [ x"$VCSH_COMMAND" = x'list-tracked' ] || + [ x"$VCSH_COMMAND" = x'list-untracked' ] || + [ x"$VCSH_COMMAND" = x'pull' ] || + [ x"$VCSH_COMMAND" = x'push' ]; then : elif [ x"$VCSH_COMMAND" = x'status' ]; then if [ x"$2" = x'--terse' ]; then @@ -708,20 +775,20 @@ elif [ -n "$2" ]; then GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR [ -d "$GIT_DIR" ] || { help; exit 1; } shift 1 - set -- "git" "$@" + set -- "@GIT@" "$@" elif [ -n "$VCSH_COMMAND" ]; then VCSH_COMMAND='enter'; export VCSH_COMMAND VCSH_REPO_NAME=$1; export VCSH_REPO_NAME GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR [ -d "$GIT_DIR" ] || { help; exit 1; } else - # $1 is empty, or 'help' - help && exit + # $1 is empty + help && exit 1 fi # Did we receive a directory instead of a name? # Mangle the input to fit normal operation. -if echo "$VCSH_REPO_NAME" | grep -q '/'; then +if echo "$VCSH_REPO_NAME" | @GREP@ -q '/'; then GIT_DIR=$VCSH_REPO_NAME; export GIT_DIR VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git); export VCSH_REPO_NAME fi @@ -743,15 +810,18 @@ check_dir "$VCSH_REPO_D" [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && check_dir "$VCSH_BASE/.gitattributes.d" verbose "$VCSH_COMMAND begin" -VCSH_COMMAND=$(echo "$VCSH_COMMAND" | sed 's/-/_/g'); export VCSH_COMMAND +VCSH_COMMAND=$(echo "$VCSH_COMMAND" | @SED@ 's/-/_/g'); export VCSH_COMMAND # Source repo-specific configuration file -[ -r "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" ] && . "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" +# shellcheck source=/dev/null +[ -r "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" ] \ + && . "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" # source overlay functions for overlay in "$VCSH_OVERLAY_D/$VCSH_COMMAND"* "$VCSH_OVERLAY_D/$VCSH_REPO_NAME.$VCSH_COMMAND"*; do [ -r "$overlay" ] || continue info "sourcing '$overlay'" + # shellcheck source=/dev/null . "$overlay" done @@ -760,3 +830,9 @@ $VCSH_COMMAND "$@" hook post-command verbose "$VCSH_COMMAND end, exiting" exit $VCSH_COMMAND_RETURN_CODE + +# Local Variables: +# mode:shell-script +# sh-shell:sh +# End: +# vim: ft=sh: