loading...

Two Bash scripts I cannot live without when working with Git

erykpiast profile image Eryk Napierała Updated on ・4 min read

That's maybe an overstatement, but just a little. I use these two simple scripts for over five years now and I just cannot imagine to not have them in my shell. What's the fuzz about?

Clean untracked files safely

There are some untracked files in your repository, probably some build artifacts or other trash, but you're not sure. You know only that there is something because you can see a little exclamation mark icon in ZSH command prompt. What are you doing?

Most probably, you'll start with git status to see what exactly is there.

$ git status
?? tmp/
?? some_trash

Everything looks unnecessary, so you're deciding to remove it.

$ git clean -f
Removing some_trash

Hmm, what about tmp? It's a directory! You didn't notice the little / next to its name, so you have to repeat the command again with -d switch.

$ git clean -f -d
Removing tmp/

Now the repository is clean, but three commands were needed. If you're watchful enough, you need two, but that's still quite a lot of typing. I suppose you know many better ways of spending those five seconds!

I do, that's why I wrote the script below. To make it available in each shell, just put it in ~/.bashrc, ~/.zshrc or similar file.

function git_clean_untracked_safely {
  TO_REMOVE=`git clean -f -d -n`;
  if [[ -n "$TO_REMOVE" ]]; then
    echo "Cleaning...";
    printf "\n$TO_REMOVE\n\n";
    echo "Proceed?";

    select result in Yes No; do
      if [[ "$result" == "Yes" ]]; then
        echo "Cleaning in progress...";
        echo "";
        git clean -f -d;
        echo "";
        echo "All files and directories removed!";
      fi
      break;
    done;
  else
    echo "Everything is clean";
  fi;
}

So, how does it work?

$ git_clean_untracked_safely
Cleaning...

Would remove some_trash
Would remove tmp/

Proceed?
1) Yes  2) No
?#

You see what would be removed and if you think it's all right, you can just proceed by entering 1. Or decline with 2.

Proceed?
1) Yes  2) No
?# 1
Cleaning in progress...
Removing some_trash
Removing tmp/
Cleaning finished!

Writing the function name would be a waste of keystrokes, so I'd recommend creating an alias, for example by adding a line to ~/.bash_aliases file.

alias gcl='git_clean_untracked_safely'
# Use different name if you use GNU Common Lisp interpreter

Or, if you prefer, add a git alias.

$ git config --global alias.justclean '! bash -c "source ~/.bashrc && git_clean_untracked_safely"'

Both commands will call the script in the same way.

$ gcl
$ git justclean

Clean old local branches

How often you run git branch -vvv and see dozens of local branches that you even not remember about? I bet it happened at least once! What can you do in such a case? You could start with git remote prune origin to clean references to dead remote branches and then remove all local ones without the upstream. But picking each branch and removing it with git branch -d (or -D) doesn't sound like fun.

Can we do better than that? Guess what... A script!

function git_clean_local_branches {
  OPTION="-d";
  if [[ "$1" == "-f" ]]; then
    echo "WARNING! Removing with force";
    OPTION="-D";
  fi;

  TO_REMOVE=`git branch -r | awk "{print \\$1}" | egrep -v -f /dev/fd/0 <(git branch -vv | grep origin) | awk "{print \\$1}"`;
  if [[ -n "$TO_REMOVE" ]]; then
    echo "Removing branches...";
    echo "";
    printf "\n$TO_REMOVE\n\n";
    echo "Proceed?";

    select result in Yes No; do
      if [[ "$result" == "Yes" ]]; then
        echo "Removing in progress...";
        echo "$TO_REMOVE" | xargs git branch "$OPTION";
        if [[ "$?" -ne "0" ]]; then
          echo ""
          echo "Some branches was not removed, you have to do it manually!";
        else
          echo "All branches removed!";
        fi
      fi

      break;
    done;
  else
    echo "You have nothing to clean";
  fi
}

The usage looks like this:

$ git_clean_local_branches
Removing branches...

bugfix/foo
bugfix/bar
bugfix/baz
feature/qux
feature/tux
feature/fux

Proceed?
1) Yes  2) No
?#

As with the previous script, you're able to both proceed or abort. By default, branches are removed gracefully, so the command may fail when the branch wasn't merged to the master (or at least when git says that).

$ git_clean_local_branches
Proceed?
1) Yes  2) No
?# 1
Removing in progress...

Deleted branch bugfix/foo (was 7ff047995).
Deleted branch feature/qux (was cfad3e00c).
Deleted branch feature/tux (was 6529123af).
Deleted branch feature/fux (was b12ec9091).

error: The branch 'bugfix/bar' is not fully merged.
If you are sure you want to delete it, run 'git branch -D bugfix/bar'.
error: The branch 'bugfix/baz' is not fully merged.
If you are sure you want to delete it, run 'git branch -D bugfix/baz'.

Some branches were not removed, you have to do it manually!

If you want to prune the branches anyway, use -f switch.

$ git_clean_local_branches -f
WARNING! Removing with force
Removing branches...

bugfix/bar
bugfix/baz
1) Yes  2) No
?# 1
Removing in progress...

Deleted branch bugfix/bar (was 7ff047995).
Deleted branch bugfix/baz (was cfad3e00c).

All branches removed!

Both bash and git alias are available as well.

alias glpo='git_clean_local_branches'
$ git config --global alias.localprune '! bash -c "source ~/.bashrc && git_clean_local_branches"'

And that's it! Enjoy!


Thanks to @jsn1nj4 , @tgu and @euphnutz for suggestions.

Discussion

pic
Editor guide
Collapse
moopet profile image
Ben Sinclair

I do something similar to your git_clean_local_branches here: git-tidy

It's interesting to see how we both went for basically the same experience but approached it in different ways.

Collapse
erykpiast profile image
Eryk Napierała Author

Believe or not, but I didn't know that script. Thanks! Multiple discovery hipothesis confirmed! :)

Collapse
jsn1nj4 profile image
JSn1nj4‍‍👨‍💻

Interesting. I use Linux on my personal laptop. But being a Windows user at work, I've found myself using PowerShell instead. Will see if I can reimplement these in PS and share. :)

Collapse
jsn1nj4 profile image
JSn1nj4‍‍👨‍💻

I have a suggestion though. For cleaning untracked files, can you prevent having to run git clean -f -d -n twice by storing the output once and then running it through wc -l for checking in the if condition?

Similarly for cleaning branches, what about storing the output of git branch -r | awk "{print \\$1}" | egrep -v -f /dev/fd/0 <(git branch -vv | grep origin) | awk "{print \\$1}"?

I should add I'm suggesting this as someone who doesn't do that much bash scripting, so maybe there's an issue you ran into that I'm not aware of when just reading those one-liners.

Collapse
erykpiast profile image
Eryk Napierała Author

It turned out that echo -n trick isn't correct neither. I've decided to add the second condition for empty string, so the final version is:

TO_REMOVE=`git clean -f -d -n`;
if [[ "$TO_REMOVE" != "" ]] && [[ `echo $TO_REMOVE | wc -l | bc` != "0" ]]; then
Thread Thread
jsn1nj4 profile image
JSn1nj4‍‍👨‍💻

What about wc -l "$TO_REMOVE"?

Thread Thread
erykpiast profile image
Eryk Napierała Author

I don't think it gonna work. It tries to read a file under the path saved in $TO_REMOVE variable. That's not what we want to do.

Thread Thread
jsn1nj4 profile image
JSn1nj4‍‍👨‍💻

Oh interesting. Never mind then. Thanks for helping me understand all of this.

Collapse
erykpiast profile image
Eryk Napierała Author

You're perfectly right! Back then I had some issues with the "clean" solution and no idea how to do it better. It just worked, so I kept it like this :)

You made me feel a bit challenged, though, so I've decided I'll try to fix the issue.

Basically, the ideal solution would look like this:

TO_REMOVE=`git clean -f -d -n`;
if [[ `echo $TO_REMOVE | wc -l` -ne "0" ]]; then

For some reason, it doesn't work in the case when there is nothing to clean - line count is always equal or greater to one. I cannot remember it, but I suppose that was the exact issue five years ago when I was writing the script.

But now I'm smarter (or just know Bash a bit more :) ) and I know how to do it right! echo adds a new line to the end of the printed string, even when it's empty. Unless -n switch is specified! So the code below works perfectly in all cases.

TO_REMOVE=`git clean -f -d -n`;
if [[ `echo -n $TO_REMOVE | wc -l` -ne "0" ]]; then

I'll update the scripts in the article. Thank you!

Collapse
msfjarvis profile image
Harsh Shandilya

For anything augmenting Git I either build an alias for one-liners, or save it into a file titled git-<action> in PATH which lets me do git <action> like any other git subcommand. You should try that! git clean-safely is probably easier and quicker to type than git_clean_untracked_safely :D

Collapse
erykpiast profile image
Eryk Napierała Author

Totally agree! I'm really into three-letter aliases so this command is for me gcl :)

Collapse
pavelloz profile image
Paweł Kowalski

git clean is a very cool thing in general. I use it in npm scripts to clear things before build (build is usually gitignored). No need for additional packages like rimraf (and not every OS has rm)

Collapse
euphnutz profile image
Brian Cuerdon

You should change your test statement to:

if [[ -n "$TO_REMOVE" ]]; then

This will remove the dependency on bc.

Collapse
erykpiast profile image
Eryk Napierała Author

That's smart! So instead of counting lines, I'm checking if the string is not empty, right?

Collapse
euphnutz profile image
Brian Cuerdon

Yep! Using the bash string comparison operator -n: tldp.org/LDP/abs/html/comparison-o...

Thread Thread
erykpiast profile image
Eryk Napierała Author

Wonderful! Thank you for the suggestion :)

Collapse
tgu profile image
T-G-U

Hello,

I'd suggest little change for your gcl alias - in linux, this one refers to lisp interpreter.

Collapse
erykpiast profile image
Eryk Napierała Author

Huh, I wasn't aware of that. My experience with lisps didn't go beyond Clojure so far. Thank you for the suggestion, I'll add a note to the article!

Collapse
flavius profile image
Flavius Aspra

I would rewrite them in xonsh.