Display detached heads elegantly in CLI - bash

I'd like my commandline (PS1 variable) to display detached heads more elegantly, even if it's a little more ambiguous. The reason I want to do this is because while I never work on detached branches, I frequently encounter them due to our project setup, and want to know what branch they were from (or at least, a branch that they are on).
First of all, I believe I have a sound understanding of the difference between:
The head commit of master
A detached head, which happens to be the same as the head of master
That's the difference between these two commands:
git checkout master # Checkout the master branch
git checkout master~0 # Checkout the commit from the head of the master branch
Now onto the problem. At the moment, my PS1 contains $(__git_ps1) at the end, and commandline shows this as a result:
# git checkout master~1
addison:~/project ((111abcdef1...))$
# git checkout master~0
addison:~/project ((000abcdef0...))$
# git checkout master
addison:~/project (master)$
What I want to happen, is if I'm on a detached head, to be able to find a branch which has a matching commit hash (prefer master), and display the branchname, and how far behind HEAD the commit is, like this:
# git checkout master~1
addison:~/project (Detached(master~1))$
# git checkout master~0
addison:~/project (Detached(master))$
# git checkout master
addison:~/project (master)$
I understand that there may not be a utility that does this already, and I'm ready to accept that - If that is the case, I'd like to know how I can go about finding a branchname for a commit hash, and how far behind the branch's HEAD commit it is. I know that there may be multiple branches that have a commit with the same hash - I just want a 'best effort' solution. That might mean just choosing the most recent, or the closest to HEAD, etc.
Using this information, I can make my own script and embed it in my $PS1 variable, and format it exactly how I want.
UPDATE
I found out there is an option that can be set with $(__git_ps1), which can change the format of the output to be exactly how I want:
GIT_PS1_DESCRIBE_STYLE=contains # git describe --contains HEAD
GIT_PS1_DESCRIBE_STYLE=branch # git describe --contains --all HEAD
GIT_PS1_DESCRIBE_STYLE=tag # git describe --tags HEAD
GIT_PS1_DESCRIBE_STYLE=describe # git describe HEAD
GIT_PS1_DESCRIBE_STYLE=default # git describe --tags --exact-match HEAD
If I set the option to branch, then the output is much more readable.

As you've noted, there isn't a perfect solution. There are two approaches that Git itself uses. One is exemplified by git status: when you're on some branch, HEAD contains the branch name, and when you're detached, HEAD contains a hash ID, but the reflog for HEAD still has the branch name in it, and Git can scan that and pick out a recent branch and see if you're on that commit or one of its descendants and say detached at name or detached from name.1
In your case, though, you might want something more like what git describe does, only in the order that's not the default for git describe. In this case, what you want is more like what git describe --contains does. The --contains option implies the --tags option, which has git describe look at all tags, but not look at any branch names. Fortunately, you can add --all:
git describe --contains --all
which looks at tag and branch names—and other references too, including refs/stash, which may not be so great—and picks one of those to describe the current commit. So this may be the closest thing Git has built in to what you want.
1This feature was not in some very old versions of Git. I'm not sure when it first appeared, and the release notes only mention it in describing a fix to Git 2.4.0. Before 2.4.0, git branch and git status disagreed in when and how they said "detached at" vs "detached from". So it's in 2.4 and later, but earlier versions are less good at it, and at some point, the "detached at/from" stuff just isn't there at all.

Related

Do *not* show pgp signature in git log

The git log subcommand has the option --show-signature to display pgp signatures of the commits (if present).
This can also be enabled by default via git config log.showSignature true.
I have set this config option to true.
The problem is that I am now looking for an option to pass to git log to not show the signature in the log (while the config is still set to true),
something like
git config --no-show-signature.
I was unable to find anything helpful in the git documentation.
For context: Most of the time, I do want git log to be active, but in some automated scripts, I really don't need this, because it messes up some automatic parsing etc.
I'll repeat #phd's comment, which in the current state of things seems like the most straightforward way to cancel that config parameter (and only that one) :
Try git -c log.showSignature=false log
Another option is to use git log's sister command : git rev-list.
The differences are :
git rev-list isn't affected by log.* config parameters
you need to add --no-commit-header to avoid one extra line on each commit (see this answer here which links to doc)
you need to specify explicitly the refs/branches names (e.g: git rev-list will error, you need to say git rev-list HEAD)
Other than that, it understands the same options as git log :
git rev-list --no-commit-header --graph --format="%h (%an %ad) %s" HEAD

How I cherry pick previous branch's latest commit by one command?

For example, I have branch "feature-A".
Then I execute one commond that can help me checkout to "feature-B" and cherry pick "feature-A"'s lastest's commit.
Does git have such a magical command? Or does anyone have a ready-made script? If so, this could save me a lot of time.
Thanks!
You can concatenate 2 git commands: first to move to your desired branch (in this case feature-B) and then cherry pick the last commit on the top of your other branch (in this case feature-A)
You can do it by running git checkout feature-B && git cherry-pick feature-A
Remember you can specify -n after cherry-pick if you don't want to commit (so you can check it before committing), or on the other hand you can concatenate && git push in case you don't need to check but you want directly to push everything in the same command after the cherry pick.
Below bash/zsh script might help if you want to cherry-pick only the latest commit from feature-A
git checkout feature-A
commitId=$(git log -n 1 --pretty=format:"%h")
git checkout feature-B
git cherry-pick $commitId
git diff --stat --cached origin/feature-B
Then to push the changes, run below command
git push origin feature-B

Always prompt for a stash message in git

I tend to stash changes without remembering why I stash them.
I do make it a point to git stash push -m most of the time, but if there's a fire drill or something else that knocks me out of flow, I may forget and lose time trying to recover.
Is there a way to imitate the behavior of git commit (minus the -m) for git stash where vim pops up and abandons the operation if the message is empty?
AFAIK there's no config option for this. You'll have to write an alias in your .gitconfig and train yourself to use it.
For example, I have two stash aliases git pop and git save. (You can see I didn't get the memo about git stash save being deprecated). These are both for convenience, and to change the default behavior to something I find more useful.
save = stash save -k -u
pop = stash pop
Unfortunately git stash push -m doesn't bring up an editor, if you need to write more than a few words to describe what you were doing consider a branch instead. We can fix this by writing a little shell function and passing the argument to -m using "$#" to ensure messages with spaces are a single argument.
savem = "!f() { git save -m \"$#\"; }; f"
Now you can write git savem 'remember to remember what this was'.
$ git savem 'remember to remember what this was'
Saved working directory and index state On issue/45: remember to remember what this was
And if you forget, you'll get the normal git-stash usage message. You can snazz up the alias to provide a custom usage message if you like.
$ git savem
usage: git stash list [<options>]
or: git stash show [<stash>]
...
To me it makes sense to consider using a branch for this. It seems like you want to keep the changes. Branches can be named so it’s easier to recall what was being worked on. These can be local or pushed to remote in case it wasn’t a drill.
$ git branch topic/wip
If you want continue work on master yo can do a
$ git checkout master
Not pretty but could be achieved using bash + vipe in moreutils
msg="$(< /dev/null vipe)";
[[ -z "$msg" ]] || git stash -m "$msg"

Git get worktree for every branch in seperate folders - Bash

I need to write a bash script that copies our repository branches onto our local linux web server every hour.
We have a git remote repository (gitolite) with branches named "master" "testing" "feature-featurename" "hotfix-number"
each one of these branches is supposed to have its worktree copied to /var/www/html/branchname
First of all: How do I get different worktrees copied into different folders?
Second: How can I automate this process if the "feature-" and "hotfix-" branches constantly change names?
This has nothing to do with a git hook, this is supposed to be a script running on a different server which is triggered by a cron job.
Horrible few-liner:
mkdir -p /var/www/html
git clone --bare user#git-server:/your-repo.git && cd your-repo.git
git for-each-ref --format='%(refname)' refs/heads/ | grep 'branch-pattern' | while read branchRef; do
branchName=${branchRef#refs/heads/}
git archive --format=tar --prefix="$branchName/" "$branchRef" | tar -C/var/www/html -x
done
Let's break it down:
Make sure the target directory exists. Probably not necessary for you.
mkdir -p /var/www/html
Clone the git repository and enter the directory
git clone --bare user#git-server:/your-repo.git
List branches. This would be run from a cloned directory. Note that git branch is not used, as that can have surprising outputs when used in scripts.
git for-each-ref --format='%(refname)' refs/heads/
Filter for the branches you want. In your case the pattern would probably be something like grep -E "(master)|(testing)|(feature-.*)" etc.
grep 'branch-pattern'
The while statement reads each branch name and assigns it to the branch variable
Create a branchName variable that is the name of the branch excluding the ref prefix. Note that this is bash-specific.
git archive creates a tar archive of the selected branch, prefixing all entries with the branch name. The archive is sent to standard output
git archive --format=tar --prefix="$branch/" "$branch"
Immediately extract the archive to its target location
tar -C/var/www/html -x
How do I get different worktrees copied into different folders?
That is done with git worktree, as illustrated in Mark Adelsberger's answer.
That answer ends with:
You also could use git worktree list --porcelain instead of searching for worktree directories directly - and that may be preferable in odd cases like (again) namespaced branches.
With Git 2.36 (Q2 2022, see at the end):
git worktree list --porcelain -z
Actually, you should, especially with Git 2.31 (Q1 2021): git worktree list(man) now annotates worktrees as prunable, shows locked and prunable attributes in --porcelain mode, and gained a --verbose option.
See commit 076b444, commit 9b19a58, commit 862c723, commit 47409e7 (27 Jan 2021), and commit eb36135, commit fc0c7d5, commit a29a8b7 (19 Jan 2021) by Rafael Silva (raffs).
(Merged by Junio C Hamano -- gitster -- in commit 02fb216, 10 Feb 2021)
worktree: teach list verbose mode
Helped-by: Eric Sunshine
Signed-off-by: Rafael Silva
Reviewed-by: Eric Sunshine
"git worktree list"(man) annotates each worktree according to its state such as prunable or locked, however it is not immediately obvious why these worktrees are being annotated.
For prunable worktrees a reason is available that is returned by should_prune_worktree() and for locked worktrees a reason might be available provided by the user via lock command.
Let's teach "git worktree list" a --verbose mode that outputs the reason why the worktrees are being annotated.
The reason is a text that can take virtually any size and appending the text on the default columned format will make it difficult to extend the command with other annotations and not fit nicely on the screen.
In order to address this shortcoming the annotation is then moved to the next line indented followed by the reason If the reason is not available the annotation stays on the same line as the worktree itself.
The output of "git worktree list" with verbose becomes like so:
$ git worktree list --verbose
...
/path/to/locked-no-reason acb124 [branch-a] locked
/path/to/locked-with-reason acc125 [branch-b]
locked: worktree with a locked reason
/path/to/prunable-reason ace127 [branch-d]
prunable: gitdir file points to non-existent location
...
git worktree now includes in its man page:
For these annotations, a reason might also be available and this can be
seen using the verbose mode. The annotation is then moved to the next line
indented followed by the additional information.
$ git worktree list --verbose
/path/to/linked-worktree abcd1234 [master]
/path/to/locked-worktree-no-reason abcd5678 (detached HEAD) locked
/path/to/locked-worktree-with-reason 1234abcd (brancha)
locked: working tree path is mounted on a portable device
/path/to/prunable-worktree 5678abc1 (detached HEAD)
prunable: gitdir file points to non-existent location
Note that the annotation is moved to the next line if the additional
information is available, otherwise it stays on the same line as the
working tree itself.
And:
worktree: teach list to annotate prunable worktree
Helped-by: Eric Sunshine
Signed-off-by: Rafael Silva
Reviewed-by: Eric Sunshine
The "git worktree list"(man) command shows the absolute path to the worktree, the commit that is checked out, the name of the branch, and a "locked" annotation if the worktree is locked, however, it does not indicate whether the worktree is prunable.
The "prune" command will remove a worktree if it is prunable unless --dry-run option is specified.
This could lead to a worktree being removed without the user realizing before it is too late, in case the user forgets to pass --dry-run for instance.
If the "list" command shows which worktree is prunable, the user could verify before running "git worktree prune"(man) and hopefully prevents the working tree to be removed accidentally on the worse case scenario.
Let's teach "git worktree list" to show when a worktree is a prunable candidate for both default and porcelain format.
In the default format a "prunable" text is appended:
$ git worktree list
/path/to/main aba123 [main]
/path/to/linked 123abc [branch-a]
/path/to/prunable ace127 (detached HEAD) prunable
In the --porcelain format a prunable label is added followed by its reason:
$ git worktree list --porcelain
...
worktree /path/to/prunable
HEAD abc1234abc1234abc1234abc1234abc1234abc12
detached
prunable gitdir file points to non-existent location
...
git worktree now includes in its man page:
branch currently checked out (or "detached HEAD" if none), "locked" if
the worktree is locked, "prunable" if the worktree can be pruned by prune
command.
git worktree now includes in its man page:
The command also shows annotations for each working tree, according to its state.
These annotations are:
locked, if the working tree is locked.
prunable, if the working tree can be pruned via git worktree prune.
$ git worktree list
/path/to/linked-worktree abcd1234 [master]
/path/to/locked-worktreee acbd5678 (brancha) locked
/path/to/prunable-worktree 5678abc (detached HEAD) prunable
Before Git 2.36 (Q2 2022), "git worktree list --porcelain"(man) did not c-quote pathnames and lock reasons with unsafe bytes correctly, which is worked around by introducing NUL terminated output format with "-z".
See commit d97eb30 (31 Mar 2022) by Phillip Wood (phillipwood).
(Merged by Junio C Hamano -- gitster -- in commit 7c6d8ee, 04 Apr 2022)
worktree: add -z option for list subcommand
Signed-off-by: Phillip Wood
Add a -z option to be used in conjunction with --porcelain that gives NUL-terminated output.
As 'worktree list --porcelain' does not quote worktree paths this enables it to handle worktree paths that contain newlines.
git worktree now includes in its man page:
It is recommended to combine this with -z.
See below for details.
-z
Terminate each line with a NUL rather than a newline when
--porcelain is specified with list.
This makes it possible
to parse the output when a worktree path contains a newline
character.
git worktree now includes in its man page:
The porcelain format has a line per attribute.
If -z is given then the lines
are terminated with NUL rather than a newline.
So first you need a list of branches. For scripting purposes, the command best used for this is for-each-ref. Assuming you just want the local branch names, use something like
git for-each-ref refs/heads/* |cut -d\/ -f3
As an aside, a couple things in the above command assume that you don't use branches in "namespaces". If you use branch names like qa/feature-1 - containing / - then that changes a few things. The above command simply becomes
git for-each-ref refs/heads |cut -d\/ -f3-
but the bigger issue is you probably have to think more about how branch names should map to directory names. So for now I'll proceed with the assumption that branch names won't contain /.
You need to process each branch, so
git for-each-ref refs/heads/* |cut -d\/ -f3 |while read branch; do
# ... will process each branch here
done
Now you can use git worktree to streamline the individual checkouts. (Note that this should be much more efficient that using archive to copy the whole commit content for every branch, then invoking tar to undo the work you didn't want archive to do in the first place.)
To make sure all required work trees are defined
git for-each-ref refs/heads/* |cut -d\/ -f3 |while read branch; do
if [ ! -d .git/worktrees/$branch ]; then
git worktree add /var/www/html/$branch $branch
fi
done
Now one thing about this is that when the branches are moved (i.e. when pushes are received), it puts the work trees "out of sync" so that you appear to have staged the "undoing" of every change the push did. (The protections for the default work tree don't seem to apply.)
But that seems in line with your requirements; the alternative would be to have the directories updated as pushes come in, which you reject in your description of the problem. So your script should, in that case, sync the worktree to the new changes by "un-undoing" them
git for-each-ref refs/heads/* |cut -d\/ -f3 |while read branch; do
if [ ! -d .git/worktrees/$branch ]; then
git worktree add /var/www/html/$branch $branch
fi
git reset --hard HEAD
done
Of course sometimes branches go away; if you don't want stale worktree metadata you can add a
git worktree prune
You also could use git worktree list --porcelain instead of searching for worktree directories directly - and that may be preferable in odd cases like (again) namespaced branches.

Using git rebase --interactive on a repo cloned from subversion

I have several repos that have been converted from SVN. Each time I attempt to run git rebase -i master, the message defaults to noop. When saving, I get Successfully rebased and updated refs/heads/master which seems suggest it has done something, but all commits are still present.
I did find this question but the solution doesn't appear to apply to OsX.
I also tried git rebase -i HEAD~3 on a project with 3 commits, but I get:
fatal: Needed a single revision
Using rebase -i HEAD~2 will and rebases correctly, but I need to squash these to a single commit with the init commit date.
Interestingly, the rebase consistently misses the earliest commit on each project. In each it it the only commit not assigned to my user (it is a system commit), so I wonder if that is related.
The question cited has a possible answer: Setting the shell variable IFS (what characters are interpreted as separating "words" in shell lines, in this case separating arguments) makes two arguments interpreted as one (obviously unknown), and the (internal) command fails with the cited result.
See to what (if anything) IFS is set (echo $IFS). Try (unset IFS; git rebase -i HEAD~3) (the parentesis are required here in bash(1), OS X shell could be different, but I doubt it).

Resources