Using !! to access bash history inside a Git alias - bash

I want to create an alias that runs git stash and then executes the command that was before it.
This would be useful when git doesn't allow to run a command with unsaved changes, such as checkout, rebase, etc.
What I've tried:
s = "!git stash && fc -s
s = "!bash -c \"git stash && !!\""
None of the above work. It looks like in the first one git creates a subshell to run this command, as fc outputs no command found.
The second one is similar, but here I explicitly create a subshell and it obviously doesn't work, with no access to the history.
Is there a way around this? It's likely that this could be accomplished with a bash alias, but I'd prefer to do it through a git alias.

It's likely that this could be accomplished with a bash alias,
Yes, this would be simple and robust.
but I'd prefer to do it through a git alias.
Given how ugly and fragile this would be, I doubt it. However, here you go:
First, you need to relay the history. Make your shell write it out to the history file after every command by adding this to your .bashrc:
PROMPT_COMMAND='history -a'
You can then add your git alias. It needs to
run with Bash, since the system sh may not support history expansion
enable history and history expansion
read the history file
use history expansion in a separate parsing unit (e.g. after a linefeed, outside a compound command):
So all in all:
s = "!bash -c 'set -Ho history; history -r ~/.bash_history\ngit stash && !-1'"
Note that it'll run the last command executed regardless of bash instance, so if you use multiple tmux/screen/terminal windows, it won't necessarily run the last command in your current shell.

You can't use a git alias to access the Bash history (or any other shell data for that matter), since the command after the exclamation mark is not run as a subshell. Git is an external command, so all its child processes are also not subshells.
More details
You can confirm this by setting a git alias foo = !echo $$ $BASHPID and comparing it with the current shell:
$ echo $$ $BASHPID
11461 11461
$ git foo
25437
In my case it's not even running Bash. After some testing I think it's running /bin/sh, which is Dash for me since I'm using Ubuntu.

Related

Invoke "git bisect run <script>" from a shell script containing the bisect script, too

I working on Git now and having a big problem.
The command git bisect run needs to be like that:
$ git bisect run <my_script> $arguments
But I need to work with git bisect run only in one script.
I know that my solution with 2 scripts works but I can't find a way to combine them.
What can I do that will resolve the problem?
I tried to use:
git bisect run sh -c
As example: In working on git and writing: bash ../bisecter.sh 102. When bisecter.sh is the name of the script and 102 is something we need to search in the commits. And this script I'm trying to write. With bisect and bash commands.
You could check to see if the program is being run in "interactive mode" and run the appropriate command. If it's interactive, run git bisect. If it isn't, you're being run by git bisect.
An interactive shell is one started without non-option arguments (unless -s is
specified) and without the -c option whose standard input and error are both con-
nected to terminals (as determined by isatty(3)), or one started with the -i
option. PS1 is set and $- includes i if bash is interactive, allowing a shell
script or a startup file to test this state.
In bash you can check if file descriptor 1 (stdout) is outputting to a terminal.
if [ -t 1 ]; then
echo "We're run from a shell, run git-bisect"
else
echo "We're not run from a shell, do the bisecting."
fi
$ ./test.sh
We're run from a shell, run git-bisect
$ ./test.sh | cat
We're not run from a shell, do the bisecting.
But I seriously doubt that's what your professor intends you to do. The requirement to pass the same program to git bisect as runs git bisect doesn't make sense. Either the professor has an odd requirement, a distinct possibility, or perhaps you've misunderstood how to solve the problem.
When bisecter.sh is the name of the script and 102 is something we need to search in the commits.
git bisect is not for searching like that. It is for finding which commit caused a bug.
If you want to find which commit made a particular change, either in the log messages or in the changes, use git log -S or git log -G. That makes sense to do it in a single file.

Bash completion for git aliases containing multiple subcommands

I have set up the following alias in my .gitconfig file:
[alias]
ss = stash show
Unfortunately bash completion does not work correctly on this alias. When I type git ss <TAB>, I get:
❯ git ss <TAB>
apply clear drop pop show
branch create list push
Which is obviously the completion for git stash instead of git stash show.
With the original command I get the list of available stashes:
❯ git stash show <TAB>
stash#{0} stash#{1}
Is there a way to get the completion on the alias behave like on the original command?
I am on Ubuntu 20.04 and using the distro's default git completions.
I posted this question on the Git mailing list and got a reply to work around this issue:
It is possible to make completion work for your particular
alias by using our completion script's extension mechanism that allows
users to specify completion functions to their own git commands. If
you type git foo <TAB> and there is a _git_foo() function in your
shell's environment, then the completion script will invoke that
function to perform completion; this works not only for commands but
for aliases as well. So for your alias you only need to "borrow" all
the "show"-subcommand-specific case arms from _git_stash() and place
them in a _git_ss() function, e.g. like this:
_git_ss () {
case "$cur" in
--*)
__gitcomp_builtin stash_show "$__git_diff_common_options"
;;
*)
__gitcomp_nl "$(__git stash list | sed -n -e 's/:.*//p')"
;;
esac
}
Add it to your ~/.bashrc, or to a separate file that you source from
your .bashrc; If you use bash-completion, then you don't even have to
touch you .bashrc: save that function to a file git-ss (dash, not
underscore!) in one of the directories scanned by bash-completion
($BASH_COMPLETION_USER_DIR, ~/.local/share/bash-completion/completions
or its XDG_DATA_HOME-equivalent) and it will be auto-loaded as needed.

aliases are not stored in zsh history

Whenever I execute a command using an alias, this command is not stored in the shell's command history.
So if I run history these commands do not appear in the list.
Nor do they appear when I press CTRL + r for reverse searching the command history.
When I press the keyboard's arrow up for scrolling through the last commands, I will see an aliased command only if it was the last command I ran. Other aliased commands are will not be displayed.
For example:
$ cd my-repo
$ gs # an alias to git status
$ history
Outputs the following:
2374 cd my-repo
(the gs command is not displayed)
A few notes:
gs is only an example. The issue is far more annoying in more complex commands since I have to retype them all over again instead of executing them from history (e.g. k get pods | grep <pod_name>, where k=kubectl).
gs is defined so: alias gs=' git status'.
I also have a few functions in ~/.alias, e.g.:
mkcd () {
mkdir -pv $1
For some reason, mkcd (or any other function in the alias file) is included in the history.
I do not mind if it prints out gs or expands to git status, I'll take any of the two...
I am using zsh with oh-my-zsh on macOS (Monterey). My shell aliases are defined in ~/.alias which is sourced in ~/.zshrc (source ~/.alias).
This happens both in iTerm2 and in the default Mac terminal.
Thank you for taking the time to help :-)
I will assume that your example alias is exactly what you have in your ~/.alias file.
So you have aliases like this (notice the space character in front of git command):
alias gs=' git status'
There is an shell option called HIST_IGNORE_SPACE which is doing exactly what you are experiencing - in short it will not add command to the history when it starts with space character. Example:
echo 'This command will make it to the history.'
echo 'This poor command will be forgotten.'
You can check your current options using setopt or specifically:
setopt | grep 'histignorespace'
So there are two ways how you can fix this - either by fixing your aliases to not start with space or, If you really don't want this functionality at all, by unsetting it in your ~/.zshrc like this:
unsetopt HIST_IGNORE_SPACE

Git command for multiple git repositories in a single directory - GIT_DIR

I used this resource on StackOverflow to figure out how to have a single directory which is populated from two git repositories, in my case .org-git-work and org-git-personal (this is needed so I can use some software that's fussy about having all of its files together, and so I can keep personal and professional materials apart).
Now I am trying to make aliases to interact with the two git repositories in this shared work directory, but somehow I'm not managing to convey the GIT_DIR in the alias. Can anyone correct the following:
alias org-git-work='GIT_DIR=/foo/org/.work-org-git && echo $GIT_DIR && GIT_WORK_DIR=/foo/org && git'
when I try to use the above alias, git still looks in .git, instead of using the value of GIT_DIR:
org-git-work commit -m "Resource for preposterior sampling." bar.org
/foo/org/.work-org-git
fatal: not a git repository (or any of the parent directories): .git
...but the output from echo seems to indicate that it is set correctly.
Environment variables, generally
You are conflating environment variables with shell variables. (This is not surprising since the shells themselves do this to some extent.) An environment variable is passed from the shell into programs that the shell runs, while a shell variable is not.
To set a shell variable, use name=value anywhere.
To set an environment variable for the duration of a single command, use the syntax name=variable command:
GIT_DIR=/foo/org/.work-org-git git ...
Do not use:
GIT_DIR=/foo/.org/.work-org-git && git ...
as this will merely set a shell variable.
To illustrate the difference, we can use python -c:
$ python -c 'import os; print(os.getenv("FOO"))'
None
$ FOO=bar python -c 'import os; print(os.getenv("FOO"))'
bar
$ FOO=bar && python -c 'import os; print(os.getenv("FOO"))'
None
Initially, there is no environment setting for FOO. Using the syntax without the && operator, there is one, and using the syntax with the && operator, it has once again vanished.
To set an environment variable and have the setting persist, use the export keyword. There are several ways to do this:
FOO=bar export foo
or:
export FOO=bar
or:
FOO=bar; export FOO
all work. Once the variable has been explicitly exported, changes to the variable are also exported:
$ export FOO=bar && python -c 'import os; print(os.getenv("FOO"))'
bar
$ python -c 'import os; print(os.getenv("FOO"))'
bar
To remove the environment variable, use unset:
$ unset FOO
$ python -c 'import os; print(os.getenv("FOO"))'
None
Git's environment variables, specifically
The two variables you intend to fiddle with are GIT_DIR (one of the two you are using here) and GIT_WORK_TREE (not GIT_WORK_DIR). So you really want:
GIT_DIR=path1 GIT_WORK_TREE=path2 git ...
You don't have to do it this way: the git front end command has arguments --git-dir=path and --work-tree=path, so you can write:
git --git-dir=path1 --work-tree=path2 ...
Caveat
Git really only understands a single repository at a time, which contains every commit with every snapshot of every file. This singular repository containing everything has one (1) standard built-in index, which is also called its staging area or sometimes its cache. This index / staging-area indexes / caches one (1) work-tree.1 Trying to extract two separate Git repositories into a single work-tree is going to cause a lot of headaches, as you will have two repositories sharing the one work-tree, so you will have two separate index files that both think they are in complete command of what's going on in that work-tree.
It's not that this won't work (it can), it's just that you're going to experience annoyances with this setup.
1In newer Git versions (2.5+) you can add additional work-trees, each of which gets its own private index / staging-area, although there are still various bugs (I tripped over one recently). Git also understands, to some extent, shallow repositories where some commits are effectively "missing", and it supports sparse checkout, but neither of these help here.

How can I write a shell completion hook for a multi-word prefix?

Let's say I want to write a custom shell completion function that fires only on git checkout <tab>. I can write a custom hook for git completion like this:
complete -f -F _custom_completion_function git # bash
compctl -f -K _custom_completion_function git # zsh
But that will overwrite all of my existing git bash/zsh completion; I only want to change the behavior of git checkout.
What's the best strategy for achieving this?
This largely depends on your existing git completion setup. Chances are this already checks the first word on the command-line when deciding what to do. Can't it just call a separate function if that first word is "checkout".
For zsh, I'd recommend not using the ancient compctl completion system. If you use the newer system (by running autoload -U compinit; compinit), then the git completion included with zsh allows you to define your own _git-checkout function. Define that and it'll take precedence over the one zsh includes.

Resources