I’m terrified to contemplate the amount of time I spend “increasing my productivity” rather than doing real work. 🙃

Dev experience is like… everything. Dev experience is IMO basically the same thing as “software engineering”. Or something. That’s a hill to die on for another time.

So I made a git cd command to jump to repos…

Anyway, benefit from my shame and misery, with this handy shortcut. You can paste this or your own custom version into your ~/.zshrc. Jump below if you hate learning and just want the goods. 😁

The rest of this post documents what I did in case you want to customize.

git cd

First, this has to run as a function in your current shell. You can’t change cd the current shell by calling a child script, as it runs in its own shell.

So the general structure of this is to front git with a function.

# Where you want to complete to
project_dirs=("$HOME/src" "$HOME/ws")

git() {
  if [[ $1 == "cd" ]]; then
     # cd to subdirectory of project_dirs
  else
    # Fwd to git if not cd
    command git "$@"
  fi
end

And then we just plugin the cd magic:

project_dirs=("$HOME/src" "$HOME/ws")

git() {
  if [[ $1 == "cd" ]]; then
    for dir in "${project_dirs[@]}"; do
      full_dir="$dir/$2/"
      for repo_dir in $dir/*/; do
        if [[ "$full_dir" == "$repo_dir" ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi
      done
    done
    echo "No repo found matching $2"
    return 1
  else
    command git "$@"
  fi
}

This is the strictest possible matching.

$> ls -d ~/ws/*   
dotfiles
hello-ltr
splainer_search
softwaredoug-blog

$> git cd dotfiles      # works
$> git cd hello-ltr     # works
$> git cd helloltr      # DOES NOT work (no fuzzy match on `hello-ltr`)
$> git cd softwaredoug  # DOES NOT work (no prefix match on `softwaredoug-blog` repo))

Fuzzier matching - ignore dashes, underscores, lowercase, etc

I like to remove dashes, underscores, and whitespace for matching. You can do this with zsh parameter substitution.

All the “squished” lines and checking are added. The param substitution syntax is pretty obscure. But you can see the - _ below and add your own characters in case you want to squish away them as well.

project_dirs=("$HOME/src" "$HOME/ws")

git() {
  if [[ $1 == "cd" ]]; then
    for dir in "${project_dirs[@]}"; do
      squished_arg="$dir/$2/"
      squished_arg="${(L)squished_arg//[- _]/}"      # REMOVE -,_ and whitespace, lowercase (from arg)
      squished_arg="${squished_arg%?}"               # Remove trailing /
      for repo_dir in $dir/*/; do
        squished="${(L)repo_dir//[- _]/}"            # REMOVE -,_ and whitespace, lowercase (from dir we're checking)
        squished="${squished%?}"                     # Remove trailing /
        if [[ "$squished" == "$squished_arg" ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi
      done
    done
    echo "No repo found matching $2"
    return 1
  else
    command git "$@"
  fi
}

Trying it out…

$> ls -d ~/ws/*   
dotfiles
hello-ltr
splainer_search
softwaredoug-blog

$> git cd dotfiles      # works
$> git cd hello-ltr     # works
$> git cd helloltr      # works! (now we fuzzy match on `hello-ltr`)
$> git cd softwaredoug  # DOES NOT work (no prefix match on `softwaredoug-blog` repo))

Adding in prefix matching

We can next add wildcard matching on the prefix as our final case. This is just an extra check after the exact match, using the * syntax for prefix checking.

project_dirs=("$HOME/src" "$HOME/ws")

git_cd_prefix() {
  if [[ $1 == "cd" ]]; then
    for dir in "${project_dirs[@]}"; do
      squished_arg="$dir/$2/"
      squished_arg="${(L)squished_arg//[- _]/}"
      squished_arg="${squished_arg%?}"
      for repo_dir in $dir/*/; do
        squished="${(L)repo_dir//[- _]/}"
        squished="${squished%?}"
        if [[ "$squished" == "$squished_arg" ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi

        # ADDED * checking
        if [[ "$squished" == "$squished_arg"* ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi
      done
    done
    echo "No repo found matching $2"
    return 1
  else
    command git "$@"
  fi
}

Now all our cases should work:

$> git cd dotfiles      # works
$> git cd hello-ltr     # works
$> git cd helloltr      # works! (now we fuzzy match on `hello-ltr`)
$> git cd softwaredoug  # works! (now matches on softwaredoug*)

Adding in tab completion

Finally, we can complete git cd with a little bit of tab completion magic. This just loads the list of directories in these repos for later completion.

project_dirs=("$HOME/src" "$HOME/ws")

repos=()
all_repos() {
  for dir in "${project_dirs[@]}"; do
    for repo_dir in $dir/*/; do
      echo `basename "$repo_dir"`
    done
  done
}
repos=($(all_repos))

_git_cd_completion() {
  if [[ "$words[2]" == "cd" ]]; then
    repos=($(all_repos)) # <- if you want to update the list on every completion
    _arguments '*:repository:($(echo ${^repos}))'
  else
    _git
  fi
}

compdef _git_cd_completion git cd

The whole shebang

Add all this to your .zshrc for nice cd experience for your repos:

#git wrapper function to cd to root of repo
# git cd <repo>   # exact repo name
# git cd <re...>  # or prefix
project_dirs=("$HOME/src" "$HOME/ws")

repos=()
all_repos() {
  for dir in "${project_dirs[@]}"; do
    for repo_dir in $dir/*/; do
      echo `basename "$repo_dir"`
    done
  done
}

repos=($(all_repos))

_git_cd_completion() {
  if [[ "$words[2]" == "cd" ]]; then
    repos=($(all_repos)) # <- if you want to update the list on every completion
    _arguments '*:repository:($(echo ${^repos}))'
  else
    _git
  fi
}

compdef _git_cd_completion git cd

git() {
  if [[ $1 == "cd" ]]; then
    for dir in "${project_dirs[@]}"; do
      squished_arg="$dir/$2/"
      squished_arg="${(L)squished_arg//[- _]/}"
      squished_arg="${squished_arg%?}"
      for repo_dir in $dir/*/; do
        squished="${(L)repo_dir//[- _]/}"
        squished="${squished%?}"
        if [[ "$squished" == "$squished_arg" ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi

        if [[ "$squished" == "$squished_arg"* ]]; then
          cd "$repo_dir" > /dev/null 2>&1
          if [[ $? == 0 ]]; then
            return 0
          fi
        fi
      done
    done
    echo "No repo found matching $2"
    return 1
  else
    command git "$@"
  fi
}

If you like this, check out my other git shortcuts that make my life oh so easier! 😁

Have Fun! -softwaredoug


Doug Turnbull

More from Doug
Twitter | LinkedIn | Mastodon
Doug's articles at OpenSource Connections | Shopify Eng Blog